Files
SynthesisAgent/SynthesisAgent.OpenclawPlugin/index.ts
zhi 0fb46e318e chore: initial scaffolding for SynthesisAgent
Bridge between OpenClaw (multi-channel hub) and interactive Claude Code,
keeping autonomous agent usage on the subscription quota after the
2026-06-15 Agent SDK credit split.

Initial scaffolding only — two submodules with skeletons:

- SynthesisAgent.ClaudePlugin: stdio MCP server registered as a --channels
  source. Declares experimental.claude/channel capability (verified 2026-05-14
  against the official Anthropic discord plugin) and emits
  notifications/claude/channel to push OpenClaw inbound messages into the
  Claude turn loop.

- SynthesisAgent.OpenclawPlugin: process manager + session mapping +
  bridge WebSocket. Currently shaped around a custom ws protocol; will be
  rewritten as a model-provider HTTP server (contractor-agent pattern) so
  OpenClaw routes inbound channel messages through it via /v1/chat/completions.

See STATUS.md for the open punch list and docs/wire-protocol.md for the
(soon-to-change) inter-plugin frame schema.
2026-05-14 09:39:02 +00:00

69 lines
2.7 KiB
TypeScript

import { definePluginEntry } from 'openclaw/plugin-sdk/plugin-entry'
import type { OpenClawPluginApi } from 'openclaw/plugin-sdk/core'
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'
import { registerCli } from './core/cli.js'
// All long-lived state lives on globalThis so OpenClaw hot-reloads don't kill
// running Claude processes (mirrors contractor-agent's convention).
const _G = globalThis as Record<string, unknown>
const PM_KEY = '_synthesisProcessManager'
const SERVER_KEY = '_synthesisBridgeServer'
const MAPPING_KEY = '_synthesisSessionMapping'
export default definePluginEntry({
id: 'synthesis-agent',
name: 'SynthesisAgent',
description: 'Manages long-lived Claude Code processes per OpenClaw session',
register(api: OpenClawPluginApi): void {
const config: SynthesisConfig = normalizeConfig(api.pluginConfig)
// ── Reuse existing state across hot reload ─────────────────────────────
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 })
_G[PM_KEY] = pm
}
// Existing bridge server: leave it alone.
if (!_G[SERVER_KEY]) {
startBridgeServer({ config, mapping, processManager: pm })
.then(server => { _G[SERVER_KEY] = server })
.catch(err => {
api.logger?.error?.('synthesis: bridge server failed to start', err)
})
}
// ── Wire to OpenClaw inbound event bus ─────────────────────────────────
// TODO: subscribe to api.events / api.channels.onInbound (exact API TBD).
// Each inbound message arrives with an openclaw_session_id; we forward to
// the process manager, which spawns/resumes claude as needed and pushes
// the message into its plugin's bridge socket.
//
// api.channels.onInbound((evt) => {
// pm.deliverInbound(evt.openclawSessionId, evt)
// })
//
// Similarly for permission replies.
registerCli(api, { processManager: pm, mapping, config })
api.lifecycle?.onShutdown?.(async () => {
const server = _G[SERVER_KEY] as Awaited<ReturnType<typeof startBridgeServer>> | undefined
await server?.close?.()
delete _G[SERVER_KEY]
await pm!.shutdown()
delete _G[PM_KEY]
})
},
})