the-campfire 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. package/bin/cli.ts +182 -0
  2. package/dist/apple-touch-icon.png +0 -0
  3. package/dist/assets/MermaidDiagram-By1J3Whj.js +2 -0
  4. package/dist/assets/_basePickBy-Dq6gnD-p.js +1 -0
  5. package/dist/assets/_baseUniq-SpgN6cGc.js +1 -0
  6. package/dist/assets/arc-D9SEA8dX.js +1 -0
  7. package/dist/assets/architectureDiagram-VXUJARFQ-CoTcwLeR.js +36 -0
  8. package/dist/assets/blockDiagram-VD42YOAC-Dvc63voV.js +122 -0
  9. package/dist/assets/c4Diagram-YG6GDRKO-tuDcG4_4.js +10 -0
  10. package/dist/assets/channel-CxhcHn7S.js +1 -0
  11. package/dist/assets/chunk-4BX2VUAB-x1hChYu9.js +1 -0
  12. package/dist/assets/chunk-55IACEB6-BX6HWnWP.js +1 -0
  13. package/dist/assets/chunk-B4BG7PRW-B4IlF03G.js +165 -0
  14. package/dist/assets/chunk-DI55MBZ5-TpYb5rLg.js +220 -0
  15. package/dist/assets/chunk-FMBD7UC4-DZX4LUPx.js +15 -0
  16. package/dist/assets/chunk-QN33PNHL-CuQs0NDY.js +1 -0
  17. package/dist/assets/chunk-QZHKN3VN-CygWwVx8.js +1 -0
  18. package/dist/assets/chunk-TZMSLE5B-CtI-IdKV.js +1 -0
  19. package/dist/assets/classDiagram-2ON5EDUG-BM0LptJF.js +1 -0
  20. package/dist/assets/classDiagram-v2-WZHVMYZB-BM0LptJF.js +1 -0
  21. package/dist/assets/clone-C6kt1rE7.js +1 -0
  22. package/dist/assets/cose-bilkent-S5V4N54A-PdySs991.js +1 -0
  23. package/dist/assets/cytoscape.esm-BQaXIfA_.js +331 -0
  24. package/dist/assets/dagre-6UL2VRFP-J0SxEiEu.js +4 -0
  25. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  26. package/dist/assets/diagram-PSM6KHXK-CW0HLXaH.js +24 -0
  27. package/dist/assets/diagram-QEK2KX5R-DADHF-k4.js +43 -0
  28. package/dist/assets/diagram-S2PKOQOG-9S0-PC3S.js +24 -0
  29. package/dist/assets/erDiagram-Q2GNP2WA-BEHAiY_q.js +60 -0
  30. package/dist/assets/flowDiagram-NV44I4VS-Cu6n9Xnd.js +162 -0
  31. package/dist/assets/ganttDiagram-JELNMOA3-Cr_BHryu.js +267 -0
  32. package/dist/assets/gitGraphDiagram-V2S2FVAM-D5pT35v5.js +65 -0
  33. package/dist/assets/graph-DKRbrxcs.js +1 -0
  34. package/dist/assets/index-Byjg9zgI.css +32 -0
  35. package/dist/assets/index-Cwj3m0hT.js +284 -0
  36. package/dist/assets/infoDiagram-HS3SLOUP-BaJlrgKV.js +2 -0
  37. package/dist/assets/init-Gi6I4Gst.js +1 -0
  38. package/dist/assets/journeyDiagram-XKPGCS4Q-CXC4-DJv.js +139 -0
  39. package/dist/assets/kanban-definition-3W4ZIXB7-BtHBu9cx.js +89 -0
  40. package/dist/assets/katex-BlHpptmG.js +261 -0
  41. package/dist/assets/layout-DnxSwLDJ.js +1 -0
  42. package/dist/assets/linear-CWbbIT8l.js +1 -0
  43. package/dist/assets/mermaid.core-DXn1cFAK.js +256 -0
  44. package/dist/assets/mindmap-definition-VGOIOE7T-BR8br1hs.js +68 -0
  45. package/dist/assets/ordinal-BENe2yWM.js +1 -0
  46. package/dist/assets/pieDiagram-ADFJNKIX-B7RmXV7I.js +30 -0
  47. package/dist/assets/quadrantDiagram-AYHSOK5B-DK0FhyV_.js +7 -0
  48. package/dist/assets/requirementDiagram-UZGBJVZJ-3zAGxSpL.js +64 -0
  49. package/dist/assets/sankeyDiagram-TZEHDZUN-DZppKtjc.js +10 -0
  50. package/dist/assets/sequenceDiagram-WL72ISMW-D1x3r3wV.js +145 -0
  51. package/dist/assets/stateDiagram-FKZM4ZOC-2ByD08Vq.js +1 -0
  52. package/dist/assets/stateDiagram-v2-4FDKWEC3-Vm1cYjgV.js +1 -0
  53. package/dist/assets/timeline-definition-IT6M3QCI-l5S35biR.js +61 -0
  54. package/dist/assets/treemap-GDKQZRPO-DIPadPiF.js +162 -0
  55. package/dist/assets/xychartDiagram-PRI3JC2R-9GUvwS2b.js +7 -0
  56. package/dist/favicon.svg +3 -0
  57. package/dist/fonts/Geist-Variable.woff2 +0 -0
  58. package/dist/fonts/GeistMono-Variable.woff2 +0 -0
  59. package/dist/icon-192.png +0 -0
  60. package/dist/icon-512.png +0 -0
  61. package/dist/index.html +33 -0
  62. package/dist/logo-codex.svg +14 -0
  63. package/dist/logo.svg +3 -0
  64. package/dist/manifest.json +26 -0
  65. package/dist/og-image.png +0 -0
  66. package/dist/sw.js +130 -0
  67. package/package.json +92 -0
  68. package/server/ADAPTERS.md +121 -0
  69. package/server/adapter-registry-types.ts +28 -0
  70. package/server/adapter-registry.ts +293 -0
  71. package/server/adapter-types.ts +51 -0
  72. package/server/agent-executor.ts +249 -0
  73. package/server/agent-mcp-bridge.ts +148 -0
  74. package/server/agent-mcp-stdio.ts +171 -0
  75. package/server/agent-mcp-tools.ts +76 -0
  76. package/server/agent-store.ts +183 -0
  77. package/server/agent-types.ts +79 -0
  78. package/server/aider-adapter.ts +525 -0
  79. package/server/auth.ts +153 -0
  80. package/server/auto-namer.ts +132 -0
  81. package/server/capability-discovery.ts +431 -0
  82. package/server/claude-container-auth.ts +40 -0
  83. package/server/claude-stdio-adapter.ts +452 -0
  84. package/server/clawhub-export.ts +218 -0
  85. package/server/cli-launcher.ts +1613 -0
  86. package/server/codex-adapter.ts +1987 -0
  87. package/server/codex-container-auth.ts +40 -0
  88. package/server/codex-home.ts +26 -0
  89. package/server/collective-intelligence.ts +401 -0
  90. package/server/commands-discovery.ts +160 -0
  91. package/server/constants.ts +5 -0
  92. package/server/container-manager.ts +328 -0
  93. package/server/cron-scheduler.ts +243 -0
  94. package/server/cron-store.ts +149 -0
  95. package/server/cron-types.ts +63 -0
  96. package/server/deliberation-engine.ts +323 -0
  97. package/server/embedding.ts +89 -0
  98. package/server/env-manager.ts +146 -0
  99. package/server/environment-detector.ts +106 -0
  100. package/server/environment-rules.ts +106 -0
  101. package/server/gallery-store.ts +216 -0
  102. package/server/gallery-types.ts +52 -0
  103. package/server/gallery-votes.ts +101 -0
  104. package/server/git-utils.ts +384 -0
  105. package/server/github-pr.ts +379 -0
  106. package/server/goose-adapter.ts +929 -0
  107. package/server/image-pull-manager.ts +144 -0
  108. package/server/index.ts +363 -0
  109. package/server/linear-project-manager.ts +78 -0
  110. package/server/moltbook-client.ts +160 -0
  111. package/server/openclaw-adapter.ts +916 -0
  112. package/server/openclaw-channel/channel.ts +188 -0
  113. package/server/openclaw-channel/index.ts +68 -0
  114. package/server/openclaw-channel/package.json +19 -0
  115. package/server/openclaw-channel/types.ts +93 -0
  116. package/server/opencode-adapter.ts +951 -0
  117. package/server/openhands-adapter.ts +775 -0
  118. package/server/orchestrator-executor.ts +184 -0
  119. package/server/orchestrator-store.ts +106 -0
  120. package/server/orchestrator-types.ts +86 -0
  121. package/server/path-resolver.ts +168 -0
  122. package/server/pr-poller.ts +162 -0
  123. package/server/proactive-keepalive.ts +121 -0
  124. package/server/prompt-manager.ts +103 -0
  125. package/server/protocol-monitor.ts +197 -0
  126. package/server/race-controller.ts +295 -0
  127. package/server/race-store.ts +82 -0
  128. package/server/recorder.ts +359 -0
  129. package/server/recording-hub/compat-validator.ts +122 -0
  130. package/server/recording-hub/diagnostics.ts +289 -0
  131. package/server/recording-hub/hub-routes.ts +146 -0
  132. package/server/recording-hub/hub-store.ts +224 -0
  133. package/server/replay.ts +78 -0
  134. package/server/routes/adapter-routes.ts +31 -0
  135. package/server/routes/agent-mcp-routes.ts +35 -0
  136. package/server/routes/agent-routes.ts +185 -0
  137. package/server/routes/auth-routes.ts +101 -0
  138. package/server/routes/ci-routes.ts +165 -0
  139. package/server/routes/commands-routes.ts +94 -0
  140. package/server/routes/cron-routes.ts +100 -0
  141. package/server/routes/env-routes.ts +50 -0
  142. package/server/routes/folder-routes.ts +56 -0
  143. package/server/routes/fs-routes.ts +403 -0
  144. package/server/routes/gallery-routes.ts +410 -0
  145. package/server/routes/git-routes.ts +108 -0
  146. package/server/routes/index.ts +62 -0
  147. package/server/routes/linear-routes.ts +254 -0
  148. package/server/routes/monitor-routes.ts +11 -0
  149. package/server/routes/orchestrator-routes.ts +210 -0
  150. package/server/routes/prompt-routes.ts +47 -0
  151. package/server/routes/race-routes.ts +103 -0
  152. package/server/routes/recording-routes.ts +101 -0
  153. package/server/routes/route-deps.ts +32 -0
  154. package/server/routes/session-routes.ts +560 -0
  155. package/server/routes/settings-routes.ts +108 -0
  156. package/server/routes/skills-routes.ts +52 -0
  157. package/server/routes/system-routes.ts +243 -0
  158. package/server/routes/webhook-routes.ts +135 -0
  159. package/server/routes.ts +44 -0
  160. package/server/security-middleware.ts +100 -0
  161. package/server/semantic-memory.ts +389 -0
  162. package/server/service.ts +733 -0
  163. package/server/session-folders.ts +107 -0
  164. package/server/session-git-info.ts +89 -0
  165. package/server/session-linear-issues.ts +94 -0
  166. package/server/session-names.ts +67 -0
  167. package/server/session-store.ts +169 -0
  168. package/server/session-types.ts +415 -0
  169. package/server/settings-manager.ts +120 -0
  170. package/server/shared-context.ts +401 -0
  171. package/server/skills-manager.ts +250 -0
  172. package/server/sub-session-manager.ts +263 -0
  173. package/server/terminal-manager.ts +185 -0
  174. package/server/update-checker.ts +114 -0
  175. package/server/usage-limits.ts +192 -0
  176. package/server/webhook-manager.ts +228 -0
  177. package/server/webhook-store.ts +168 -0
  178. package/server/webhook-types.ts +66 -0
  179. package/server/worktree-tracker.ts +84 -0
  180. package/server/ws-bridge.ts +2322 -0
@@ -0,0 +1,916 @@
1
+ /**
2
+ * OpenClaw ACP (Agent Client Protocol) Adapter
3
+ *
4
+ * Translates between the OpenClaw ACP JSON-RPC protocol (stdin/stdout)
5
+ * and Campfire's BrowserIncomingMessage/BrowserOutgoingMessage types.
6
+ *
7
+ * OpenClaw uses the same ACP standard as Goose (`@agentclientprotocol/sdk`),
8
+ * so this adapter follows the identical JSON-RPC 2.0 over stdio pattern.
9
+ * The key differences from GooseAdapter:
10
+ * - Binary: `openclaw acp` instead of `goose acp`
11
+ * - Tool names: OpenClaw uses plain names (no `developer__` prefix)
12
+ * - Session keys: OpenClaw maps ACP sessions to Gateway session keys
13
+ * - Env vars: OPENCLAW_GATEWAY_TOKEN, OPENCLAW_GATEWAY_URL
14
+ *
15
+ * Protocol reference:
16
+ * - ACP uses JSON-RPC 2.0 over stdio (newline-delimited)
17
+ * - Methods: initialize, session/new, session/load, session/prompt,
18
+ * session/cancel, session/set_model
19
+ * - Notifications: session/notification with SessionUpdate payloads
20
+ * - Permission: ActionRequired → RequestPermissionOutcome
21
+ */
22
+
23
+ import { randomUUID } from "node:crypto";
24
+ import type { Subprocess } from "bun";
25
+ import type {
26
+ BrowserIncomingMessage,
27
+ BrowserOutgoingMessage,
28
+ SessionState,
29
+ PermissionRequest,
30
+ CLIResultMessage,
31
+ } from "./session-types.js";
32
+ import type { RecorderManager } from "./recorder.js";
33
+ import type { AgentAdapter } from "./adapter-types.js";
34
+
35
+ // ─── JSON-RPC Types ─────────────────────────────────────────────────────────
36
+
37
+ interface JsonRpcRequest {
38
+ method: string;
39
+ id: number;
40
+ params: Record<string, unknown>;
41
+ }
42
+
43
+ interface JsonRpcNotification {
44
+ method: string;
45
+ params: Record<string, unknown>;
46
+ }
47
+
48
+ interface JsonRpcResponse {
49
+ id: number;
50
+ result?: unknown;
51
+ error?: { code: number; message: string; data?: unknown };
52
+ }
53
+
54
+ type JsonRpcMessage = JsonRpcRequest | JsonRpcNotification | JsonRpcResponse;
55
+
56
+ // ─── Adapter Options ─────────────────────────────────────────────────────────
57
+
58
+ export interface OpenClawAdapterOptions {
59
+ model?: string;
60
+ cwd?: string;
61
+ permissionMode?: string;
62
+ /** If provided, resume an existing session instead of starting a new one. */
63
+ openclawSessionId?: string;
64
+ /** Optional recorder for raw message capture. */
65
+ recorder?: RecorderManager;
66
+ }
67
+
68
+ // ─── JSON-RPC Transport ─────────────────────────────────────────────────────
69
+ // Handles stdin/stdout NDJSON communication with the openclaw acp process.
70
+
71
+ class JsonRpcTransport {
72
+ private nextId = 1;
73
+ private pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();
74
+ private notificationHandler: ((method: string, params: Record<string, unknown>) => void) | null = null;
75
+ private requestHandler: ((method: string, id: number, params: Record<string, unknown>) => void) | null = null;
76
+ private rawInCb: ((line: string) => void) | null = null;
77
+ private rawOutCb: ((data: string) => void) | null = null;
78
+ private writer: WritableStreamDefaultWriter<Uint8Array>;
79
+ private connected = true;
80
+ private buffer = "";
81
+
82
+ constructor(
83
+ stdin: WritableStream<Uint8Array> | { write(data: Uint8Array): number },
84
+ stdout: ReadableStream<Uint8Array>,
85
+ ) {
86
+ let writable: WritableStream<Uint8Array>;
87
+ if ("write" in stdin && typeof stdin.write === "function") {
88
+ writable = new WritableStream({
89
+ write(chunk) {
90
+ (stdin as { write(data: Uint8Array): number }).write(chunk);
91
+ },
92
+ });
93
+ } else {
94
+ writable = stdin as WritableStream<Uint8Array>;
95
+ }
96
+ this.writer = writable.getWriter();
97
+ this.readStdout(stdout);
98
+ }
99
+
100
+ private async readStdout(stdout: ReadableStream<Uint8Array>): Promise<void> {
101
+ const reader = stdout.getReader();
102
+ const decoder = new TextDecoder();
103
+ try {
104
+ while (true) {
105
+ const { done, value } = await reader.read();
106
+ if (done) break;
107
+ this.buffer += decoder.decode(value, { stream: true });
108
+ this.processBuffer();
109
+ }
110
+ } catch (err) {
111
+ console.error("[openclaw-adapter] stdout reader error:", err);
112
+ } finally {
113
+ this.connected = false;
114
+ for (const [, { reject }] of this.pending) {
115
+ reject(new Error("Transport closed"));
116
+ }
117
+ this.pending.clear();
118
+ }
119
+ }
120
+
121
+ private processBuffer(): void {
122
+ const lines = this.buffer.split("\n");
123
+ this.buffer = lines.pop() || "";
124
+
125
+ for (const line of lines) {
126
+ const trimmed = line.trim();
127
+ if (!trimmed) continue;
128
+
129
+ this.rawInCb?.(trimmed);
130
+
131
+ let msg: JsonRpcMessage;
132
+ try {
133
+ msg = JSON.parse(trimmed);
134
+ } catch {
135
+ console.warn("[openclaw-adapter] Failed to parse JSON-RPC:", trimmed.substring(0, 200));
136
+ continue;
137
+ }
138
+
139
+ this.dispatch(msg);
140
+ }
141
+ }
142
+
143
+ private dispatch(msg: JsonRpcMessage): void {
144
+ if ("id" in msg && msg.id !== undefined) {
145
+ if ("method" in msg && msg.method) {
146
+ this.requestHandler?.(msg.method, msg.id as number, (msg as JsonRpcRequest).params || {});
147
+ } else {
148
+ const pending = this.pending.get(msg.id as number);
149
+ if (pending) {
150
+ this.pending.delete(msg.id as number);
151
+ const resp = msg as JsonRpcResponse;
152
+ if (resp.error) {
153
+ pending.reject(new Error(resp.error.message));
154
+ } else {
155
+ pending.resolve(resp.result);
156
+ }
157
+ }
158
+ }
159
+ } else if ("method" in msg) {
160
+ this.notificationHandler?.(msg.method, (msg as JsonRpcNotification).params || {});
161
+ }
162
+ }
163
+
164
+ async call(method: string, params: Record<string, unknown> = {}): Promise<unknown> {
165
+ const id = this.nextId++;
166
+ return new Promise(async (resolve, reject) => {
167
+ this.pending.set(id, { resolve, reject });
168
+ const request = JSON.stringify({ jsonrpc: "2.0", method, id, params });
169
+ try {
170
+ await this.writeRaw(request + "\n");
171
+ } catch (err) {
172
+ this.pending.delete(id);
173
+ reject(err instanceof Error ? err : new Error(String(err)));
174
+ }
175
+ });
176
+ }
177
+
178
+ async notify(method: string, params: Record<string, unknown> = {}): Promise<void> {
179
+ const notification = JSON.stringify({ jsonrpc: "2.0", method, params });
180
+ await this.writeRaw(notification + "\n");
181
+ }
182
+
183
+ async respond(id: number, result: unknown): Promise<void> {
184
+ const response = JSON.stringify({ jsonrpc: "2.0", id, result });
185
+ await this.writeRaw(response + "\n");
186
+ }
187
+
188
+ onNotification(handler: (method: string, params: Record<string, unknown>) => void): void {
189
+ this.notificationHandler = handler;
190
+ }
191
+
192
+ onRequest(handler: (method: string, id: number, params: Record<string, unknown>) => void): void {
193
+ this.requestHandler = handler;
194
+ }
195
+
196
+ isConnected(): boolean {
197
+ return this.connected;
198
+ }
199
+
200
+ onRawIncoming(cb: (line: string) => void): void {
201
+ this.rawInCb = cb;
202
+ }
203
+
204
+ onRawOutgoing(cb: (data: string) => void): void {
205
+ this.rawOutCb = cb;
206
+ }
207
+
208
+ private async writeRaw(data: string): Promise<void> {
209
+ if (!this.connected) {
210
+ throw new Error("Transport closed");
211
+ }
212
+ this.rawOutCb?.(data);
213
+ await this.writer.write(new TextEncoder().encode(data));
214
+ }
215
+ }
216
+
217
+ // ─── OpenClaw Adapter ────────────────────────────────────────────────────────
218
+
219
+ export class OpenClawAdapter implements AgentAdapter {
220
+ private transport: JsonRpcTransport;
221
+ private proc: Subprocess;
222
+ private sessionId: string; // Campfire session ID
223
+ private options: OpenClawAdapterOptions;
224
+
225
+ private browserMessageCb: ((msg: BrowserIncomingMessage) => void) | null = null;
226
+ private sessionMetaCb: ((meta: { cliSessionId?: string; model?: string; cwd?: string }) => void) | null = null;
227
+ private disconnectCb: (() => void) | null = null;
228
+ private initErrorCb: ((error: string) => void) | null = null;
229
+
230
+ // State
231
+ private openclawSessionId: string | null = null;
232
+ private connected = false;
233
+ private initialized = false;
234
+ private initFailed = false;
235
+
236
+ // Streaming accumulator
237
+ private streamingText = "";
238
+ private streamingActive = false;
239
+
240
+ // Tool call tracking
241
+ private activeToolCalls = new Map<string, { toolName: string; startTime: number }>();
242
+ private emittedToolUseIds = new Set<string>();
243
+
244
+ // Permission request tracking
245
+ private pendingPermissions = new Map<string, number>();
246
+
247
+ // Queue messages received before initialization completes
248
+ private pendingOutgoing: BrowserOutgoingMessage[] = [];
249
+
250
+ constructor(proc: Subprocess, sessionId: string, options: OpenClawAdapterOptions = {}) {
251
+ this.proc = proc;
252
+ this.sessionId = sessionId;
253
+ this.options = options;
254
+
255
+ const stdout = proc.stdout;
256
+ const stdin = proc.stdin;
257
+ if (!stdout || !stdin || typeof stdout === "number" || typeof stdin === "number") {
258
+ throw new Error("OpenClaw process must have stdio pipes");
259
+ }
260
+
261
+ this.transport = new JsonRpcTransport(
262
+ stdin as WritableStream<Uint8Array> | { write(data: Uint8Array): number },
263
+ stdout as ReadableStream<Uint8Array>,
264
+ );
265
+ this.transport.onNotification((method, params) => this.handleNotification(method, params));
266
+ this.transport.onRequest((method, id, params) => this.handleRequest(method, id, params));
267
+
268
+ // Wire raw message recording
269
+ if (options.recorder) {
270
+ const recorder = options.recorder;
271
+ const cwd = options.cwd || "";
272
+ this.transport.onRawIncoming((line) => {
273
+ recorder.record(sessionId, "in", line, "cli", "openclaw", cwd);
274
+ });
275
+ this.transport.onRawOutgoing((data) => {
276
+ recorder.record(sessionId, "out", data.trimEnd(), "cli", "openclaw", cwd);
277
+ });
278
+ }
279
+
280
+ // Monitor process exit
281
+ proc.exited.then(() => {
282
+ this.connected = false;
283
+ this.disconnectCb?.();
284
+ });
285
+
286
+ // Start initialization
287
+ this.initialize();
288
+ }
289
+
290
+ // ── Public API ──────────────────────────────────────────────────────────
291
+
292
+ sendBrowserMessage(msg: BrowserOutgoingMessage): boolean {
293
+ if (this.initFailed) return false;
294
+
295
+ // Queue messages if not yet initialized
296
+ if (!this.initialized || !this.openclawSessionId) {
297
+ if (
298
+ msg.type === "user_message"
299
+ || msg.type === "permission_response"
300
+ ) {
301
+ console.log(`[openclaw-adapter] Queuing ${msg.type} — adapter not yet initialized`);
302
+ this.pendingOutgoing.push(msg);
303
+ return true;
304
+ }
305
+ if (!this.connected) return false;
306
+ }
307
+
308
+ return this.dispatchOutgoing(msg);
309
+ }
310
+
311
+ private dispatchOutgoing(msg: BrowserOutgoingMessage): boolean {
312
+ switch (msg.type) {
313
+ case "user_message":
314
+ this.handleOutgoingUserMessage(msg);
315
+ return true;
316
+ case "permission_response":
317
+ this.handleOutgoingPermissionResponse(msg);
318
+ return true;
319
+ case "interrupt":
320
+ this.handleOutgoingInterrupt();
321
+ return true;
322
+ case "set_model":
323
+ this.handleOutgoingSetModel(msg.model);
324
+ return true;
325
+ case "set_permission_mode":
326
+ console.warn("[openclaw-adapter] Runtime permission mode switching not yet supported for OpenClaw");
327
+ return false;
328
+ case "mcp_get_status":
329
+ case "mcp_toggle":
330
+ case "mcp_reconnect":
331
+ case "mcp_set_servers":
332
+ console.warn(`[openclaw-adapter] MCP management (${msg.type}) not yet supported for OpenClaw`);
333
+ return false;
334
+ default:
335
+ return false;
336
+ }
337
+ }
338
+
339
+ onBrowserMessage(cb: (msg: BrowserIncomingMessage) => void): void {
340
+ this.browserMessageCb = cb;
341
+ }
342
+
343
+ onSessionMeta(cb: (meta: { cliSessionId?: string; model?: string; cwd?: string }) => void): void {
344
+ this.sessionMetaCb = cb;
345
+ }
346
+
347
+ onDisconnect(cb: () => void): void {
348
+ this.disconnectCb = cb;
349
+ }
350
+
351
+ onInitError(cb: (error: string) => void): void {
352
+ this.initErrorCb = cb;
353
+ }
354
+
355
+ isConnected(): boolean {
356
+ return this.connected;
357
+ }
358
+
359
+ async disconnect(): Promise<void> {
360
+ this.connected = false;
361
+ try {
362
+ this.proc.kill("SIGTERM");
363
+ await Promise.race([
364
+ this.proc.exited,
365
+ new Promise((r) => setTimeout(r, 5000)),
366
+ ]);
367
+ } catch {}
368
+ }
369
+
370
+ getBackendSessionId(): string | null {
371
+ return this.openclawSessionId;
372
+ }
373
+
374
+ // ── Initialization ─────────────────────────────────────────────────────
375
+
376
+ private async initialize(): Promise<void> {
377
+ try {
378
+ // Step 1: ACP initialize handshake
379
+ const initResult = await this.transport.call("initialize", {
380
+ protocolVersion: "v1",
381
+ clientCapabilities: {},
382
+ clientInfo: {
383
+ name: "campfire",
384
+ title: "Campfire",
385
+ version: "1.0.0",
386
+ },
387
+ }) as Record<string, unknown>;
388
+
389
+ this.connected = true;
390
+ this.initialized = true;
391
+
392
+ // Extract capabilities
393
+ const capabilities = initResult?.agentCapabilities as Record<string, unknown> || {};
394
+ const hasLoadSession = capabilities.loadSession === true;
395
+
396
+ // Step 2: Create or load a session
397
+ if (this.options.openclawSessionId && hasLoadSession) {
398
+ await this.transport.call("session/load", {
399
+ sessionId: this.options.openclawSessionId,
400
+ mcpServers: [],
401
+ cwd: this.options.cwd || process.cwd(),
402
+ });
403
+ this.openclawSessionId = this.options.openclawSessionId;
404
+ } else {
405
+ const newResult = await this.transport.call("session/new", {
406
+ mcpServers: [],
407
+ cwd: this.options.cwd || process.cwd(),
408
+ }) as Record<string, unknown>;
409
+ this.openclawSessionId = newResult?.sessionId as string || randomUUID();
410
+ }
411
+
412
+ // Notify session metadata
413
+ this.sessionMetaCb?.({
414
+ cliSessionId: this.openclawSessionId,
415
+ model: this.options.model,
416
+ cwd: this.options.cwd,
417
+ });
418
+
419
+ // Send session_init to browser
420
+ const state: SessionState = {
421
+ session_id: this.sessionId,
422
+ backend_type: "openclaw",
423
+ model: this.options.model || "default",
424
+ cwd: this.options.cwd || "",
425
+ tools: [],
426
+ permissionMode: this.options.permissionMode || "default",
427
+ claude_code_version: "",
428
+ mcp_servers: [],
429
+ agents: [],
430
+ slash_commands: [],
431
+ skills: [],
432
+ total_cost_usd: 0,
433
+ num_turns: 0,
434
+ context_used_percent: 0,
435
+ is_compacting: false,
436
+ git_branch: "",
437
+ is_worktree: false,
438
+ repo_root: "",
439
+ git_ahead: 0,
440
+ git_behind: 0,
441
+ total_lines_added: 0,
442
+ total_lines_removed: 0,
443
+ total_duration_api_ms: 0,
444
+ };
445
+
446
+ this.emit({ type: "session_init", session: state });
447
+
448
+ // If model was specified, set it
449
+ if (this.options.model) {
450
+ this.transport.call("session/set_model", {
451
+ model: this.options.model,
452
+ }).catch((err) => {
453
+ console.warn(`[openclaw-adapter] Failed to set model: ${err}`);
454
+ });
455
+ }
456
+
457
+ // Flush queued messages
458
+ if (this.pendingOutgoing.length > 0) {
459
+ console.log(`[openclaw-adapter] Flushing ${this.pendingOutgoing.length} queued message(s)`);
460
+ const queued = this.pendingOutgoing.splice(0);
461
+ for (const msg of queued) {
462
+ this.dispatchOutgoing(msg);
463
+ }
464
+ }
465
+ } catch (err) {
466
+ const errorMsg = `OpenClaw initialization failed: ${err}`;
467
+ console.error(`[openclaw-adapter] ${errorMsg}`);
468
+ this.initFailed = true;
469
+ this.connected = false;
470
+ this.pendingOutgoing.length = 0;
471
+ this.emit({ type: "error", message: errorMsg });
472
+ this.initErrorCb?.(errorMsg);
473
+ }
474
+ }
475
+
476
+ // ── Outgoing message handlers ──────────────────────────────────────────
477
+
478
+ private async handleOutgoingUserMessage(
479
+ msg: { type: "user_message"; content: string; images?: { media_type: string; data: string }[] },
480
+ ): Promise<void> {
481
+ if (!this.openclawSessionId) {
482
+ this.emit({ type: "error", message: "No OpenClaw session started yet" });
483
+ return;
484
+ }
485
+
486
+ const prompt: Array<Record<string, unknown>> = [];
487
+
488
+ if (msg.images?.length) {
489
+ for (const img of msg.images) {
490
+ prompt.push({
491
+ type: "image",
492
+ data: img.data,
493
+ media_type: img.media_type,
494
+ });
495
+ }
496
+ }
497
+
498
+ prompt.push({ type: "text", text: msg.content });
499
+
500
+ try {
501
+ const result = await this.transport.call("session/prompt", {
502
+ sessionId: this.openclawSessionId,
503
+ prompt,
504
+ }) as Record<string, unknown>;
505
+
506
+ this.finishTurn(result);
507
+ } catch (err) {
508
+ this.emit({ type: "error", message: `Failed to send prompt: ${err}` });
509
+ this.emitResult("error_during_execution", true);
510
+ }
511
+ }
512
+
513
+ private async handleOutgoingPermissionResponse(
514
+ msg: { type: "permission_response"; request_id: string; behavior: "allow" | "deny" },
515
+ ): Promise<void> {
516
+ const permissionMap: Record<string, string> = {
517
+ allow: "AllowOnce",
518
+ deny: "RejectOnce",
519
+ };
520
+
521
+ try {
522
+ await this.transport.notify("requestPermission", {
523
+ id: msg.request_id,
524
+ permission: permissionMap[msg.behavior] || "RejectOnce",
525
+ });
526
+ } catch (err) {
527
+ console.warn(`[openclaw-adapter] Failed to send permission response: ${err}`);
528
+ }
529
+ }
530
+
531
+ private async handleOutgoingInterrupt(): Promise<void> {
532
+ try {
533
+ await this.transport.notify("session/cancel", {});
534
+ } catch (err) {
535
+ console.warn("[openclaw-adapter] Cancel failed:", err);
536
+ }
537
+ }
538
+
539
+ private async handleOutgoingSetModel(model: string): Promise<void> {
540
+ try {
541
+ await this.transport.call("session/set_model", { model });
542
+ } catch (err) {
543
+ console.warn(`[openclaw-adapter] Set model failed: ${err}`);
544
+ }
545
+ }
546
+
547
+ // ── Incoming notification handlers ─────────────────────────────────────
548
+
549
+ private handleNotification(method: string, params: Record<string, unknown>): void {
550
+ try {
551
+ switch (method) {
552
+ case "session/notification":
553
+ this.handleSessionNotification(params);
554
+ break;
555
+ default:
556
+ console.log(`[openclaw-adapter] Unhandled notification: ${method}`);
557
+ break;
558
+ }
559
+ } catch (err) {
560
+ console.error(`[openclaw-adapter] Error handling notification ${method}:`, err);
561
+ }
562
+ }
563
+
564
+ private handleSessionNotification(params: Record<string, unknown>): void {
565
+ const update = params.update as Record<string, unknown>;
566
+ if (!update) return;
567
+
568
+ const updateType = update.sessionUpdate as string;
569
+ console.log(`[openclaw-adapter] ← session/notification: ${updateType}`);
570
+
571
+ switch (updateType) {
572
+ case "agent_message_chunk":
573
+ case "agentMessageChunk":
574
+ this.handleAgentMessageChunk(update);
575
+ break;
576
+ case "agentThoughtChunk":
577
+ this.handleAgentThoughtChunk(update);
578
+ break;
579
+ case "tool_call":
580
+ case "toolCall":
581
+ this.handleToolCall(update);
582
+ break;
583
+ case "tool_call_update":
584
+ case "toolCallUpdate":
585
+ this.handleToolCallUpdate(update);
586
+ break;
587
+ case "actionRequired":
588
+ this.handleActionRequired(update);
589
+ break;
590
+ case "available_commands_update":
591
+ // OpenClaw sends available commands after session creation — ignore
592
+ break;
593
+ case "userMessageChunk":
594
+ // Echo of user input — ignore
595
+ break;
596
+ default:
597
+ console.log(`[openclaw-adapter] Unhandled sessionUpdate: ${updateType}`);
598
+ break;
599
+ }
600
+ }
601
+
602
+ private handleRequest(method: string, id: number, params: Record<string, unknown>): void {
603
+ console.log(`[openclaw-adapter] Received request: ${method}`);
604
+ try {
605
+ switch (method) {
606
+ case "requestPermission": {
607
+ const requestId = `openclaw-perm-${randomUUID()}`;
608
+ this.pendingPermissions.set(requestId, id);
609
+ const data = params.data as Record<string, unknown> || params;
610
+ this.emitPermissionRequest(requestId, data);
611
+ break;
612
+ }
613
+ default:
614
+ console.log(`[openclaw-adapter] Unhandled request: ${method}`);
615
+ this.transport.respond(id, {});
616
+ break;
617
+ }
618
+ } catch (err) {
619
+ console.error(`[openclaw-adapter] Error handling request ${method}:`, err);
620
+ }
621
+ }
622
+
623
+ // ── SessionUpdate handlers ─────────────────────────────────────────────
624
+
625
+ private handleAgentMessageChunk(update: Record<string, unknown>): void {
626
+ const content = update.content as Record<string, unknown>;
627
+ if (!content) return;
628
+
629
+ const text = content.text as string || "";
630
+
631
+ if (!this.streamingActive) {
632
+ this.streamingActive = true;
633
+ this.streamingText = "";
634
+
635
+ this.emit({
636
+ type: "stream_event",
637
+ event: {
638
+ type: "message_start",
639
+ message: {
640
+ id: this.makeMessageId("agent"),
641
+ type: "message",
642
+ role: "assistant",
643
+ model: this.options.model || "",
644
+ content: [],
645
+ stop_reason: null,
646
+ usage: { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 },
647
+ },
648
+ },
649
+ parent_tool_use_id: null,
650
+ });
651
+
652
+ this.emit({
653
+ type: "stream_event",
654
+ event: {
655
+ type: "content_block_start",
656
+ index: 0,
657
+ content_block: { type: "text", text: "" },
658
+ },
659
+ parent_tool_use_id: null,
660
+ });
661
+ }
662
+
663
+ this.streamingText += text;
664
+
665
+ this.emit({
666
+ type: "stream_event",
667
+ event: {
668
+ type: "content_block_delta",
669
+ index: 0,
670
+ delta: { type: "text_delta", text },
671
+ },
672
+ parent_tool_use_id: null,
673
+ });
674
+ }
675
+
676
+ private handleAgentThoughtChunk(update: Record<string, unknown>): void {
677
+ const content = update.content as Record<string, unknown>;
678
+ if (!content) return;
679
+
680
+ const thinking = content.thinking as string || content.text as string || "";
681
+ if (!thinking) return;
682
+
683
+ this.emit({
684
+ type: "stream_event",
685
+ event: {
686
+ type: "content_block_start",
687
+ index: 0,
688
+ content_block: { type: "thinking", thinking },
689
+ },
690
+ parent_tool_use_id: null,
691
+ });
692
+ }
693
+
694
+ private handleToolCall(update: Record<string, unknown>): void {
695
+ this.flushStreaming();
696
+
697
+ const toolName = update.toolName as string || update.title as string || "unknown";
698
+ const args = update.arguments as Record<string, unknown>
699
+ || update.rawInput as Record<string, unknown>
700
+ || update.raw_input as Record<string, unknown>
701
+ || {};
702
+ const toolCallId = update.id as string
703
+ || update.toolCallId as string
704
+ || `openclaw-tool-${randomUUID()}`;
705
+
706
+ this.activeToolCalls.set(toolCallId, { toolName, startTime: Date.now() });
707
+
708
+ // OpenClaw uses plain tool names — pass through as-is
709
+ const mappedName = this.mapToolName(toolName);
710
+
711
+ this.emit({
712
+ type: "stream_event",
713
+ event: {
714
+ type: "content_block_start",
715
+ index: 0,
716
+ content_block: { type: "tool_use", id: toolCallId, name: mappedName, input: {} },
717
+ },
718
+ parent_tool_use_id: null,
719
+ });
720
+
721
+ this.emittedToolUseIds.add(toolCallId);
722
+ this.emit({
723
+ type: "assistant",
724
+ message: {
725
+ id: this.makeMessageId("tool_use", toolCallId),
726
+ type: "message",
727
+ role: "assistant",
728
+ model: this.options.model || "",
729
+ content: [
730
+ {
731
+ type: "tool_use",
732
+ id: toolCallId,
733
+ name: mappedName,
734
+ input: args,
735
+ },
736
+ ],
737
+ stop_reason: null,
738
+ usage: { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 },
739
+ },
740
+ parent_tool_use_id: null,
741
+ timestamp: Date.now(),
742
+ });
743
+
744
+ this.emit({
745
+ type: "tool_progress",
746
+ tool_use_id: toolCallId,
747
+ tool_name: mappedName,
748
+ elapsed_time_seconds: 0,
749
+ });
750
+ }
751
+
752
+ private handleToolCallUpdate(update: Record<string, unknown>): void {
753
+ const toolCallId = update.id as string || update.toolCallId as string || "";
754
+ const result = update.result as string || update.rawOutput as string || update.content as string || "";
755
+ const status = update.status as string || "completed";
756
+ const isError = status === "failed" || status === "error";
757
+
758
+ this.activeToolCalls.delete(toolCallId);
759
+
760
+ const safeContent = typeof result === "string" ? result : JSON.stringify(result);
761
+ this.emit({
762
+ type: "assistant",
763
+ message: {
764
+ id: this.makeMessageId("tool_result", toolCallId),
765
+ type: "message",
766
+ role: "assistant",
767
+ model: this.options.model || "",
768
+ content: [
769
+ {
770
+ type: "tool_result",
771
+ tool_use_id: toolCallId,
772
+ content: safeContent || (isError ? "Tool call failed" : "Tool call completed"),
773
+ is_error: isError,
774
+ },
775
+ ],
776
+ stop_reason: null,
777
+ usage: { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 },
778
+ },
779
+ parent_tool_use_id: null,
780
+ timestamp: Date.now(),
781
+ });
782
+ }
783
+
784
+ private handleActionRequired(update: Record<string, unknown>): void {
785
+ const data = update.data as Record<string, unknown>;
786
+ if (!data) return;
787
+
788
+ const actionType = data.actionType as string;
789
+ if (actionType === "toolConfirmation") {
790
+ const requestId = data.id as string || `openclaw-action-${randomUUID()}`;
791
+ this.emitPermissionRequest(requestId, data);
792
+ } else {
793
+ console.log(`[openclaw-adapter] Unhandled actionRequired type: ${actionType}`);
794
+ }
795
+ }
796
+
797
+ // ── Helpers ─────────────────────────────────────────────────────────────
798
+
799
+ private emit(msg: BrowserIncomingMessage): void {
800
+ this.browserMessageCb?.(msg);
801
+ }
802
+
803
+ private flushStreaming(): void {
804
+ if (!this.streamingActive) return;
805
+
806
+ this.emit({
807
+ type: "stream_event",
808
+ event: { type: "content_block_stop", index: 0 },
809
+ parent_tool_use_id: null,
810
+ });
811
+ this.emit({
812
+ type: "stream_event",
813
+ event: {
814
+ type: "message_delta",
815
+ delta: { stop_reason: null },
816
+ usage: { output_tokens: 0 },
817
+ },
818
+ parent_tool_use_id: null,
819
+ });
820
+
821
+ if (this.streamingText) {
822
+ this.emit({
823
+ type: "assistant",
824
+ message: {
825
+ id: this.makeMessageId("agent"),
826
+ type: "message",
827
+ role: "assistant",
828
+ model: this.options.model || "",
829
+ content: [{ type: "text", text: this.streamingText }],
830
+ stop_reason: "end_turn",
831
+ usage: { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 },
832
+ },
833
+ parent_tool_use_id: null,
834
+ timestamp: Date.now(),
835
+ });
836
+ }
837
+
838
+ this.streamingText = "";
839
+ this.streamingActive = false;
840
+ }
841
+
842
+ private finishTurn(result: Record<string, unknown>): void {
843
+ this.flushStreaming();
844
+
845
+ const stopReason = result?.stopReason as string || result?.stop_reason as string || "end_turn";
846
+
847
+ this.emitResult(
848
+ stopReason === "cancelled" ? "error_during_execution" : "success",
849
+ stopReason === "cancelled",
850
+ );
851
+ }
852
+
853
+ private emitResult(subtype: CLIResultMessage["subtype"], isError: boolean): void {
854
+ const result: CLIResultMessage = {
855
+ type: "result",
856
+ subtype,
857
+ is_error: isError,
858
+ duration_ms: 0,
859
+ duration_api_ms: 0,
860
+ num_turns: 1,
861
+ total_cost_usd: 0,
862
+ stop_reason: subtype === "success" ? "end_turn" : "error",
863
+ usage: { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 },
864
+ uuid: randomUUID(),
865
+ session_id: this.sessionId,
866
+ };
867
+
868
+ this.emit({ type: "result", data: result });
869
+ }
870
+
871
+ private emitPermissionRequest(requestId: string, data: Record<string, unknown>): void {
872
+ const toolName = data.toolName as string || "unknown";
873
+ const args = data.arguments as Record<string, unknown> || {};
874
+ const prompt = data.prompt as string || data.content as string;
875
+
876
+ const mappedName = this.mapToolName(toolName);
877
+
878
+ const perm: PermissionRequest = {
879
+ request_id: requestId,
880
+ tool_name: mappedName,
881
+ input: args,
882
+ description: prompt || `OpenClaw wants to use: ${mappedName}`,
883
+ tool_use_id: data.toolCallId as string || requestId,
884
+ timestamp: Date.now(),
885
+ };
886
+
887
+ this.emit({ type: "permission_request", request: perm });
888
+ }
889
+
890
+ /**
891
+ * Map OpenClaw tool names to Campfire-compatible tool names.
892
+ * Unlike Goose (which uses developer__bash, developer__text_editor, etc.),
893
+ * OpenClaw skills use plain names that mostly pass through unchanged.
894
+ * Only map known OpenClaw skill tool names to standard Campfire names.
895
+ */
896
+ private mapToolName(openclawToolName: string): string {
897
+ const toolMap: Record<string, string> = {
898
+ "bash": "Bash",
899
+ "shell": "Bash",
900
+ "text_editor": "Edit",
901
+ "read_file": "Read",
902
+ "write_file": "Write",
903
+ "list_directory": "Glob",
904
+ "search_files": "Grep",
905
+ "web_fetch": "WebFetch",
906
+ "web_search": "WebSearch",
907
+ };
908
+
909
+ return toolMap[openclawToolName] || openclawToolName;
910
+ }
911
+
912
+ private makeMessageId(kind: string, sourceId?: string): string {
913
+ if (sourceId) return `openclaw-${kind}-${sourceId}`;
914
+ return `openclaw-${kind}-${randomUUID()}`;
915
+ }
916
+ }