import { definePluginEntry } from 'openclaw/plugin-sdk/plugin-entry' import type { OpenClawPluginApi } from 'openclaw/plugin-sdk/core' import fs from 'node:fs' import path from 'node:path' import { normalizeConfig, type SynthesisConfig } from './core/config.js' import { ProcessManager } from './core/process-manager.js' import { SessionMapping } from './core/session-mapping.js' import { startBridgeServer } from './web/bridge-server.js' // All long-lived state survives OpenClaw hot-reloads via globalThis. const _G = globalThis as Record const PM_KEY = '_synthesisProcessManager' const SERVER_KEY = '_synthesisBridgeServer' const MAPPING_KEY = '_synthesisSessionMapping' const LIFECYCLE_KEY = '_synthesisLifecycleRegistered' const OPENCLAW_API_KEY = '_synthesisOpenclawApi' function resolveAgentWorkspace(agentId: string): { workspace: string } | null { try { const configPath = path.join(process.env.HOME ?? '/root', '.openclaw', 'openclaw.json') const raw = JSON.parse(fs.readFileSync(configPath, 'utf8')) as { agents?: { list?: Array<{ id: string; workspace?: string }> } } const agent = raw.agents?.list?.find(a => a.id === agentId) if (!agent?.workspace) return null return { workspace: agent.workspace } } catch { return null } } export default definePluginEntry({ id: 'synthesis-agent', name: 'SynthesisAgent', description: 'Routes OpenClaw agent turns through long-lived interactive Claude Code processes', register(api: OpenClawPluginApi): void { const config: SynthesisConfig = normalizeConfig((api as any).pluginConfig) const logger = (api as any).logger ?? console _G[OPENCLAW_API_KEY] = api let mapping = _G[MAPPING_KEY] as SessionMapping | undefined if (!mapping) { mapping = new SessionMapping(config.mappingDbPath) _G[MAPPING_KEY] = mapping } let pm = _G[PM_KEY] as ProcessManager | undefined if (!pm) { pm = new ProcessManager({ config, mapping, logger }) _G[PM_KEY] = pm } if (!_G[LIFECYCLE_KEY]) { _G[LIFECYCLE_KEY] = true ;(api as any).on?.('gateway_start', () => { if (_G[SERVER_KEY]) return const server = startBridgeServer({ config, mapping: mapping!, processManager: pm!, resolveAgent: resolveAgentWorkspace, logger, }) _G[SERVER_KEY] = server logger.info?.(`[synthesis-agent] bridge HTTP=${config.bridgePort} WS=${config.channelWsPort}`) }) ;(api as any).on?.('gateway_stop', async () => { const s = _G[SERVER_KEY] as Awaited> | undefined if (s) await s.close?.() delete _G[SERVER_KEY] await pm!.shutdown() delete _G[PM_KEY] logger.info?.('[synthesis-agent] bridge stopped') }) } logger.info?.(`[synthesis-agent] plugin registered (bridge port=${config.bridgePort})`) }, }) // ── Standalone mode ─────────────────────────────────────────────────────────── // When this file is executed directly (e.g. `bun index.ts`), boot the bridge // without the OpenClaw plugin runtime. Useful for local development & the // laptop integration test. // // Detect direct execution via import.meta.main (Bun) or argv[1] match. declare const Bun: { main: string } | undefined const isDirectRun = (typeof Bun !== 'undefined' && Bun.main && import.meta.url === `file://${Bun.main}`) || (process.argv[1] && (process.argv[1].endsWith('/index.ts') || process.argv[1].endsWith('/index.js'))) if (isDirectRun) { const config = normalizeConfig({}) const mapping = new SessionMapping(config.mappingDbPath) const pm = new ProcessManager({ config, mapping }) const server = startBridgeServer({ config, mapping, processManager: pm, resolveAgent: resolveAgentWorkspace, }) const shutdown = async () => { process.stderr.write('shutting down…\n') await server.close() await pm.shutdown() process.exit(0) } process.on('SIGINT', shutdown) process.on('SIGTERM', shutdown) }