export { validateYonexusServerConfig, YonexusServerConfigError } from "./core/config.js"; export type { YonexusServerConfig } from "./core/config.js"; export { createClientRecord, serializeClientRecord, deserializeClientRecord, isPairable, hasPendingPairing, isPairingExpired, canAuthenticate, type PairingStatus, type ClientLivenessStatus, type PairingNotifyStatus, type NonceEntry, type HandshakeAttemptEntry, type ClientRecord, type ClientSession, type ServerRegistry, type SerializedClientRecord, type ServerPersistenceData } from "./core/persistence.js"; export { SERVER_PERSISTENCE_VERSION, YonexusServerStoreError, YonexusServerStoreCorruptionError, createYonexusServerStore, loadServerStore, saveServerStore, type ServerStoreLoadResult, type YonexusServerStore } from "./core/store.js"; import path from "node:path"; import fs from "node:fs"; import os from "node:os"; import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; import { validateYonexusServerConfig } from "./core/config.js"; import { createYonexusServerStore } from "./core/store.js"; import { createServerTransport, type ServerTransport } from "./core/transport.js"; import { createYonexusServerRuntime } from "./core/runtime.js"; import { createServerRuleRegistry, YonexusServerRuleRegistry } from "./core/rules.js"; import { encodeRuleMessage } from "../../Yonexus.Protocol/src/index.js"; import type { ServerPersistenceData } from "./core/persistence.js"; const PLUGIN_DATA_DIR = path.join(os.homedir(), ".openclaw", "yonexus-server"); const _G = globalThis as Record; const _STARTED_KEY = "_yonexusServerStarted"; const _TRANSPORT_KEY = "_yonexusServerTransport"; const _REGISTRY_KEY = "_yonexusServerRegistry"; const _CALLBACKS_KEY = "_yonexusServerOnAuthCallbacks"; export interface YonexusServerPluginManifest { readonly name: "Yonexus.Server"; readonly version: string; readonly description: string; } const manifest: YonexusServerPluginManifest = { name: "Yonexus.Server", version: "0.1.0", description: "Yonexus central hub plugin for cross-instance OpenClaw communication" }; export function createYonexusServerPlugin(api: OpenClawPluginApi): void { const stateFilePath = path.join(PLUGIN_DATA_DIR, "state.json"); // Register CLI regardless of whether the gateway is already running. // The CLI process is a separate invocation that reads from the persisted state file. api.registerCli?.(({ program }) => { const group = program .command("yonexus-server") .description("Yonexus.Server management"); group .command("pair-code ") .description("Show the pending pairing code for a device awaiting confirmation") .action((identifier: string) => { let raw: ServerPersistenceData; try { raw = JSON.parse(fs.readFileSync(stateFilePath, "utf8")) as ServerPersistenceData; } catch { console.error("Error: could not read server state. Is the gateway running?"); process.exit(1); } const client = raw.clients?.find((c) => c.identifier === identifier); if (!client) { console.error(`Error: identifier "${identifier}" not found in server registry.`); process.exit(1); } if (client.pairingStatus !== "pending" || !client.pairingCode) { const status = client.pairingStatus; console.error(`Error: no pending pairing for "${identifier}" (status: ${status}).`); process.exit(1); } if (client.pairingExpiresAt && Math.floor(Date.now() / 1000) > client.pairingExpiresAt) { console.error(`Error: pairing for "${identifier}" has expired.`); process.exit(1); } const expiresIn = client.pairingExpiresAt ? Math.max(0, client.pairingExpiresAt - Math.floor(Date.now() / 1000)) : 0; const mm = String(Math.floor(expiresIn / 60)).padStart(2, "0"); const ss = String(expiresIn % 60).padStart(2, "0"); console.log(`Identifier : ${client.identifier}`); console.log(`Pairing code : ${client.pairingCode}`); console.log(`Expires in : ${mm}m ${ss}s`); }); group .command("list-pending") .description("List all identifiers with a pending pairing code") .action(() => { let raw: ServerPersistenceData; try { raw = JSON.parse(fs.readFileSync(stateFilePath, "utf8")) as ServerPersistenceData; } catch { console.error("Error: could not read server state. Is the gateway running?"); process.exit(1); } const now = Math.floor(Date.now() / 1000); const pending = (raw.clients ?? []).filter( (c) => c.pairingStatus === "pending" && c.pairingCode && (!c.pairingExpiresAt || now <= c.pairingExpiresAt) ); if (pending.length === 0) { console.log("No pending pairings."); return; } for (const c of pending) { const expiresIn = c.pairingExpiresAt ? Math.max(0, c.pairingExpiresAt - now) : 0; const mm = String(Math.floor(expiresIn / 60)).padStart(2, "0"); const ss = String(expiresIn % 60).padStart(2, "0"); console.log(` ${c.identifier} (expires in ${mm}m ${ss}s)`); } }); }, { commands: ["yonexus-server"] }); // 1. Ensure shared state survives hot-reload — only initialise when absent if (!(_G[_REGISTRY_KEY] instanceof YonexusServerRuleRegistry)) { _G[_REGISTRY_KEY] = createServerRuleRegistry(); } if (!Array.isArray(_G[_CALLBACKS_KEY])) { _G[_CALLBACKS_KEY] = []; } const ruleRegistry = _G[_REGISTRY_KEY] as YonexusServerRuleRegistry; const onClientAuthenticatedCallbacks = _G[_CALLBACKS_KEY] as Array<(identifier: string) => void>; // 2. Refresh the cross-plugin API object every call so that sendRule closure // always reads the live transport from globalThis. _G["__yonexusServer"] = { ruleRegistry, sendRule: (identifier: string, ruleId: string, content: string): boolean => (_G[_TRANSPORT_KEY] as ServerTransport | undefined)?.send(identifier, encodeRuleMessage(ruleId, content)) ?? false, onClientAuthenticated: onClientAuthenticatedCallbacks }; // 3. Runtime startup — only fire when the gateway boots, not eagerly during // register() inside one-shot CLI subprocesses (e.g. `openclaw completion`). // Without this gate, every CLI invocation that loads plugins would try to // bind the WebSocket listener; EADDRINUSE recovery handled the gateway- // already-running case but a genuine first-bind in a subprocess would // keep that process alive forever. api.on("gateway_start", () => { if (_G[_STARTED_KEY]) return; _G[_STARTED_KEY] = true; fs.mkdirSync(PLUGIN_DATA_DIR, { recursive: true }); const config = validateYonexusServerConfig(api.pluginConfig); const store = createYonexusServerStore(stateFilePath); // runtimeRef is local; transport is stored in globalThis so sendRule closures stay valid let runtimeRef: ReturnType | null = null; const transport = createServerTransport({ config, onMessage: (conn, msg) => { runtimeRef?.handleMessage(conn, msg).catch((err: unknown) => { console.error("[yonexus-server] message handler error:", err); }); }, onDisconnect: (identifier) => { if (identifier && runtimeRef) { runtimeRef.handleDisconnect(identifier); } } }); _G[_TRANSPORT_KEY] = transport; const runtime = createYonexusServerRuntime({ config, store, transport, ruleRegistry, onClientAuthenticated: (identifier) => { for (const cb of onClientAuthenticatedCallbacks) cb(identifier); } }); runtimeRef = runtime; _G["_yonexusServerRuntime"] = runtime; runtime.start().catch((err: unknown) => { const code = (err as NodeJS.ErrnoException | undefined)?.code; if (code !== "EADDRINUSE") { console.error("[yonexus-server] failed to start:", err); } }); }); api.on("gateway_stop", () => { const runtime = _G["_yonexusServerRuntime"] as ReturnType | undefined; runtime?.stop().catch((err: unknown) => { console.error("[yonexus-server] shutdown error:", err); }); }); } export default definePluginEntry({ id: "yonexus-server", name: "Yonexus.Server", description: "Yonexus central hub plugin for cross-instance OpenClaw communication", register: createYonexusServerPlugin, }); export { createServerTransport, YonexusServerTransport, type ServerTransport, type ServerTransportOptions, type ClientConnection, type MessageHandler, type ConnectionHandler, type DisconnectionHandler } from "./core/transport.js"; export { createYonexusServerRuntime, YonexusServerRuntime, type YonexusServerRuntimeOptions, type ServerLifecycleState } from "./core/runtime.js"; export { createServerRuleRegistry, YonexusServerRuleRegistry, ServerRuleRegistryError, type ServerRuleRegistry, type ServerRuleProcessor } from "./core/rules.js"; export { createPairingService, PairingService, type PairingRequest, type PairingResult, type PairingFailureReason } from "./services/pairing.js"; export { createDiscordNotificationService, createMockNotificationService, type DiscordNotificationService, type DiscordNotificationConfig } from "./notifications/discord.js"; export { manifest };