diff --git a/plugin/index.ts b/plugin/index.ts index b006bd1..7054776 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -43,8 +43,14 @@ import path from "node:path"; import { validateYonexusClientConfig } from "./core/config.js"; import { createYonexusClientStateStore } from "./core/state.js"; import { createClientTransport } from "./core/transport.js"; -import { createYonexusClientRuntime } from "./core/runtime.js"; -import { createClientRuleRegistry } from "./core/rules.js"; +import { createYonexusClientRuntime, type YonexusClientRuntime } from "./core/runtime.js"; +import { createClientRuleRegistry, YonexusClientRuleRegistry } from "./core/rules.js"; + +const _G = globalThis as Record; +const _STARTED_KEY = "_yonexusClientStarted"; +const _RUNTIME_KEY = "_yonexusClientRuntime"; +const _REGISTRY_KEY = "_yonexusClientRegistry"; +const _CALLBACKS_KEY = "_yonexusClientOnAuthCallbacks"; export interface YonexusClientPluginManifest { readonly name: "Yonexus.Client"; @@ -58,41 +64,48 @@ const manifest: YonexusClientPluginManifest = { description: "Yonexus client plugin for cross-instance OpenClaw communication" }; -let _clientStarted = false; - export function createYonexusClientPlugin(api: { rootDir: string; pluginConfig: unknown }): void { - if (_clientStarted) return; - _clientStarted = true; + // 1. Ensure shared state survives hot-reload — only initialise when absent + if (!(_G[_REGISTRY_KEY] instanceof YonexusClientRuleRegistry)) { + _G[_REGISTRY_KEY] = createClientRuleRegistry(); + } + if (!Array.isArray(_G[_CALLBACKS_KEY])) { + _G[_CALLBACKS_KEY] = []; + } + + const ruleRegistry = _G[_REGISTRY_KEY] as YonexusClientRuleRegistry; + const onAuthenticatedCallbacks = _G[_CALLBACKS_KEY] as Array<() => void>; + + // 2. Refresh the cross-plugin API object every call so that sendRule / submitPairingCode + // closures always read the live runtime from globalThis. + _G["__yonexusClient"] = { + ruleRegistry, + sendRule: (ruleId: string, content: string): boolean => + (_G[_RUNTIME_KEY] as YonexusClientRuntime | undefined)?.sendRuleMessage(ruleId, content) ?? false, + submitPairingCode: (code: string): boolean => + (_G[_RUNTIME_KEY] as YonexusClientRuntime | undefined)?.submitPairingCode(code) ?? false, + onAuthenticated: onAuthenticatedCallbacks + }; + + // 3. Start the runtime only once — the globalThis flag survives hot-reload + if (_G[_STARTED_KEY]) return; + _G[_STARTED_KEY] = true; const config = validateYonexusClientConfig(api.pluginConfig); const stateStore = createYonexusClientStateStore(path.join(api.rootDir, "state.json")); - const ruleRegistry = createClientRuleRegistry(); - const onAuthenticatedCallbacks: Array<() => void> = []; - - let runtimeRef: ReturnType | null = null; const transport = createClientTransport({ config, onMessage: (msg) => { - runtimeRef?.handleMessage(msg).catch((err: unknown) => { + (_G[_RUNTIME_KEY] as YonexusClientRuntime | undefined)?.handleMessage(msg).catch((err: unknown) => { console.error("[yonexus-client] message handler error:", err); }); }, onStateChange: (state) => { - runtimeRef?.handleTransportStateChange(state); + (_G[_RUNTIME_KEY] as YonexusClientRuntime | undefined)?.handleTransportStateChange(state); } }); - // Expose registry and helpers for other plugins loaded in the same process - (globalThis as Record)["__yonexusClient"] = { - ruleRegistry, - sendRule: (ruleId: string, content: string): boolean => - runtimeRef?.sendRuleMessage(ruleId, content) ?? false, - submitPairingCode: (code: string): boolean => - runtimeRef?.submitPairingCode(code) ?? false, - onAuthenticated: onAuthenticatedCallbacks - }; - const runtime = createYonexusClientRuntime({ config, transport, @@ -102,7 +115,7 @@ export function createYonexusClientPlugin(api: { rootDir: string; pluginConfig: for (const cb of onAuthenticatedCallbacks) cb(); } }); - runtimeRef = runtime; + _G[_RUNTIME_KEY] = runtime; const shutdown = (): void => { runtime.stop().catch((err: unknown) => {