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,929 @@
1
+ /**
2
+ * Goose ACP (Agent Client Protocol) Adapter
3
+ *
4
+ * Translates between the Goose ACP JSON-RPC protocol (stdin/stdout)
5
+ * and Campfire's BrowserIncomingMessage/BrowserOutgoingMessage types.
6
+ *
7
+ * This allows the browser to be completely unaware of which backend is running —
8
+ * it sees the same message types regardless of whether Claude Code, Codex, or
9
+ * Goose is the backend.
10
+ *
11
+ * Protocol reference:
12
+ * - Goose ACP uses JSON-RPC 2.0 over stdio (newline-delimited)
13
+ * - Methods: initialize, session/new, session/load, session/prompt,
14
+ * session/cancel, session/set_model
15
+ * - Notifications: session/notification with SessionUpdate payloads
16
+ * - Permission: ActionRequired → RequestPermissionOutcome
17
+ */
18
+
19
+ import { randomUUID } from "node:crypto";
20
+ import type { Subprocess } from "bun";
21
+ import type {
22
+ BrowserIncomingMessage,
23
+ BrowserOutgoingMessage,
24
+ SessionState,
25
+ PermissionRequest,
26
+ CLIResultMessage,
27
+ } from "./session-types.js";
28
+ import type { RecorderManager } from "./recorder.js";
29
+ import type { AgentAdapter } from "./adapter-types.js";
30
+
31
+ // ─── JSON-RPC Types (shared with Codex adapter pattern) ─────────────────────
32
+
33
+ interface JsonRpcRequest {
34
+ method: string;
35
+ id: number;
36
+ params: Record<string, unknown>;
37
+ }
38
+
39
+ interface JsonRpcNotification {
40
+ method: string;
41
+ params: Record<string, unknown>;
42
+ }
43
+
44
+ interface JsonRpcResponse {
45
+ id: number;
46
+ result?: unknown;
47
+ error?: { code: number; message: string; data?: unknown };
48
+ }
49
+
50
+ type JsonRpcMessage = JsonRpcRequest | JsonRpcNotification | JsonRpcResponse;
51
+
52
+ // ─── Adapter Options ─────────────────────────────────────────────────────────
53
+
54
+ export interface GooseAdapterOptions {
55
+ model?: string;
56
+ cwd?: string;
57
+ permissionMode?: string;
58
+ /** If provided, resume an existing session instead of starting a new one. */
59
+ gooseSessionId?: string;
60
+ /** Optional recorder for raw message capture. */
61
+ recorder?: RecorderManager;
62
+ }
63
+
64
+ // ─── JSON-RPC Transport ─────────────────────────────────────────────────────
65
+ // Same transport as CodexAdapter — handles stdin/stdout NDJSON communication.
66
+
67
+ class JsonRpcTransport {
68
+ private nextId = 1;
69
+ private pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();
70
+ private notificationHandler: ((method: string, params: Record<string, unknown>) => void) | null = null;
71
+ private requestHandler: ((method: string, id: number, params: Record<string, unknown>) => void) | null = null;
72
+ private rawInCb: ((line: string) => void) | null = null;
73
+ private rawOutCb: ((data: string) => void) | null = null;
74
+ private writer: WritableStreamDefaultWriter<Uint8Array>;
75
+ private connected = true;
76
+ private buffer = "";
77
+
78
+ constructor(
79
+ stdin: WritableStream<Uint8Array> | { write(data: Uint8Array): number },
80
+ stdout: ReadableStream<Uint8Array>,
81
+ ) {
82
+ let writable: WritableStream<Uint8Array>;
83
+ if ("write" in stdin && typeof stdin.write === "function") {
84
+ writable = new WritableStream({
85
+ write(chunk) {
86
+ (stdin as { write(data: Uint8Array): number }).write(chunk);
87
+ },
88
+ });
89
+ } else {
90
+ writable = stdin as WritableStream<Uint8Array>;
91
+ }
92
+ this.writer = writable.getWriter();
93
+ this.readStdout(stdout);
94
+ }
95
+
96
+ private async readStdout(stdout: ReadableStream<Uint8Array>): Promise<void> {
97
+ const reader = stdout.getReader();
98
+ const decoder = new TextDecoder();
99
+ try {
100
+ while (true) {
101
+ const { done, value } = await reader.read();
102
+ if (done) break;
103
+ this.buffer += decoder.decode(value, { stream: true });
104
+ this.processBuffer();
105
+ }
106
+ } catch (err) {
107
+ console.error("[goose-adapter] stdout reader error:", err);
108
+ } finally {
109
+ this.connected = false;
110
+ for (const [, { reject }] of this.pending) {
111
+ reject(new Error("Transport closed"));
112
+ }
113
+ this.pending.clear();
114
+ }
115
+ }
116
+
117
+ private processBuffer(): void {
118
+ const lines = this.buffer.split("\n");
119
+ this.buffer = lines.pop() || "";
120
+
121
+ for (const line of lines) {
122
+ const trimmed = line.trim();
123
+ if (!trimmed) continue;
124
+
125
+ this.rawInCb?.(trimmed);
126
+
127
+ let msg: JsonRpcMessage;
128
+ try {
129
+ msg = JSON.parse(trimmed);
130
+ } catch {
131
+ console.warn("[goose-adapter] Failed to parse JSON-RPC:", trimmed.substring(0, 200));
132
+ continue;
133
+ }
134
+
135
+ this.dispatch(msg);
136
+ }
137
+ }
138
+
139
+ private dispatch(msg: JsonRpcMessage): void {
140
+ if ("id" in msg && msg.id !== undefined) {
141
+ if ("method" in msg && msg.method) {
142
+ // Request FROM the server
143
+ this.requestHandler?.(msg.method, msg.id as number, (msg as JsonRpcRequest).params || {});
144
+ } else {
145
+ // Response to one of our requests
146
+ const pending = this.pending.get(msg.id as number);
147
+ if (pending) {
148
+ this.pending.delete(msg.id as number);
149
+ const resp = msg as JsonRpcResponse;
150
+ if (resp.error) {
151
+ pending.reject(new Error(resp.error.message));
152
+ } else {
153
+ pending.resolve(resp.result);
154
+ }
155
+ }
156
+ }
157
+ } else if ("method" in msg) {
158
+ this.notificationHandler?.(msg.method, (msg as JsonRpcNotification).params || {});
159
+ }
160
+ }
161
+
162
+ async call(method: string, params: Record<string, unknown> = {}): Promise<unknown> {
163
+ const id = this.nextId++;
164
+ return new Promise(async (resolve, reject) => {
165
+ this.pending.set(id, { resolve, reject });
166
+ const request = JSON.stringify({ jsonrpc: "2.0", method, id, params });
167
+ try {
168
+ await this.writeRaw(request + "\n");
169
+ } catch (err) {
170
+ this.pending.delete(id);
171
+ reject(err instanceof Error ? err : new Error(String(err)));
172
+ }
173
+ });
174
+ }
175
+
176
+ async notify(method: string, params: Record<string, unknown> = {}): Promise<void> {
177
+ const notification = JSON.stringify({ jsonrpc: "2.0", method, params });
178
+ await this.writeRaw(notification + "\n");
179
+ }
180
+
181
+ async respond(id: number, result: unknown): Promise<void> {
182
+ const response = JSON.stringify({ jsonrpc: "2.0", id, result });
183
+ await this.writeRaw(response + "\n");
184
+ }
185
+
186
+ onNotification(handler: (method: string, params: Record<string, unknown>) => void): void {
187
+ this.notificationHandler = handler;
188
+ }
189
+
190
+ onRequest(handler: (method: string, id: number, params: Record<string, unknown>) => void): void {
191
+ this.requestHandler = handler;
192
+ }
193
+
194
+ isConnected(): boolean {
195
+ return this.connected;
196
+ }
197
+
198
+ onRawIncoming(cb: (line: string) => void): void {
199
+ this.rawInCb = cb;
200
+ }
201
+
202
+ onRawOutgoing(cb: (data: string) => void): void {
203
+ this.rawOutCb = cb;
204
+ }
205
+
206
+ private async writeRaw(data: string): Promise<void> {
207
+ if (!this.connected) {
208
+ throw new Error("Transport closed");
209
+ }
210
+ this.rawOutCb?.(data);
211
+ await this.writer.write(new TextEncoder().encode(data));
212
+ }
213
+ }
214
+
215
+ // ─── Goose Adapter ──────────────────────────────────────────────────────────
216
+
217
+ export class GooseAdapter implements AgentAdapter {
218
+ private transport: JsonRpcTransport;
219
+ private proc: Subprocess;
220
+ private sessionId: string; // Campfire session ID
221
+ private options: GooseAdapterOptions;
222
+
223
+ private browserMessageCb: ((msg: BrowserIncomingMessage) => void) | null = null;
224
+ private sessionMetaCb: ((meta: { cliSessionId?: string; model?: string; cwd?: string }) => void) | null = null;
225
+ private disconnectCb: (() => void) | null = null;
226
+ private initErrorCb: ((error: string) => void) | null = null;
227
+
228
+ // State
229
+ private gooseSessionId: string | null = null; // Goose's internal session ID
230
+ private connected = false;
231
+ private initialized = false;
232
+ private initFailed = false;
233
+
234
+ // Streaming accumulator
235
+ private streamingText = "";
236
+ private streamingActive = false;
237
+
238
+ // Tool call tracking
239
+ private activeToolCalls = new Map<string, { toolName: string; startTime: number }>();
240
+ private emittedToolUseIds = new Set<string>();
241
+
242
+ // Permission request tracking
243
+ private pendingPermissions = new Map<string, number>(); // request_id -> JSON-RPC id (if request-based)
244
+
245
+ // Queue messages received before initialization completes
246
+ private pendingOutgoing: BrowserOutgoingMessage[] = [];
247
+
248
+ constructor(proc: Subprocess, sessionId: string, options: GooseAdapterOptions = {}) {
249
+ this.proc = proc;
250
+ this.sessionId = sessionId;
251
+ this.options = options;
252
+
253
+ const stdout = proc.stdout;
254
+ const stdin = proc.stdin;
255
+ if (!stdout || !stdin || typeof stdout === "number" || typeof stdin === "number") {
256
+ throw new Error("Goose process must have stdio pipes");
257
+ }
258
+
259
+ this.transport = new JsonRpcTransport(
260
+ stdin as WritableStream<Uint8Array> | { write(data: Uint8Array): number },
261
+ stdout as ReadableStream<Uint8Array>,
262
+ );
263
+ this.transport.onNotification((method, params) => this.handleNotification(method, params));
264
+ this.transport.onRequest((method, id, params) => this.handleRequest(method, id, params));
265
+
266
+ // Wire raw message recording
267
+ if (options.recorder) {
268
+ const recorder = options.recorder;
269
+ const cwd = options.cwd || "";
270
+ this.transport.onRawIncoming((line) => {
271
+ recorder.record(sessionId, "in", line, "cli", "goose", cwd);
272
+ });
273
+ this.transport.onRawOutgoing((data) => {
274
+ recorder.record(sessionId, "out", data.trimEnd(), "cli", "goose", cwd);
275
+ });
276
+ }
277
+
278
+ // Monitor process exit
279
+ proc.exited.then(() => {
280
+ this.connected = false;
281
+ this.disconnectCb?.();
282
+ });
283
+
284
+ // Start initialization
285
+ this.initialize();
286
+ }
287
+
288
+ // ── Public API ──────────────────────────────────────────────────────────
289
+
290
+ sendBrowserMessage(msg: BrowserOutgoingMessage): boolean {
291
+ if (this.initFailed) return false;
292
+
293
+ // Queue messages if not yet initialized
294
+ if (!this.initialized || !this.gooseSessionId) {
295
+ if (
296
+ msg.type === "user_message"
297
+ || msg.type === "permission_response"
298
+ ) {
299
+ console.log(`[goose-adapter] Queuing ${msg.type} — adapter not yet initialized`);
300
+ this.pendingOutgoing.push(msg);
301
+ return true;
302
+ }
303
+ if (!this.connected) return false;
304
+ }
305
+
306
+ return this.dispatchOutgoing(msg);
307
+ }
308
+
309
+ private dispatchOutgoing(msg: BrowserOutgoingMessage): boolean {
310
+ switch (msg.type) {
311
+ case "user_message":
312
+ this.handleOutgoingUserMessage(msg);
313
+ return true;
314
+ case "permission_response":
315
+ this.handleOutgoingPermissionResponse(msg);
316
+ return true;
317
+ case "interrupt":
318
+ this.handleOutgoingInterrupt();
319
+ return true;
320
+ case "set_model":
321
+ this.handleOutgoingSetModel(msg.model);
322
+ return true;
323
+ case "set_permission_mode":
324
+ console.warn("[goose-adapter] Runtime permission mode switching not yet supported for Goose");
325
+ return false;
326
+ case "mcp_get_status":
327
+ case "mcp_toggle":
328
+ case "mcp_reconnect":
329
+ case "mcp_set_servers":
330
+ console.warn(`[goose-adapter] MCP management (${msg.type}) not yet supported for Goose`);
331
+ return false;
332
+ default:
333
+ return false;
334
+ }
335
+ }
336
+
337
+ onBrowserMessage(cb: (msg: BrowserIncomingMessage) => void): void {
338
+ this.browserMessageCb = cb;
339
+ }
340
+
341
+ onSessionMeta(cb: (meta: { cliSessionId?: string; model?: string; cwd?: string }) => void): void {
342
+ this.sessionMetaCb = cb;
343
+ }
344
+
345
+ onDisconnect(cb: () => void): void {
346
+ this.disconnectCb = cb;
347
+ }
348
+
349
+ onInitError(cb: (error: string) => void): void {
350
+ this.initErrorCb = cb;
351
+ }
352
+
353
+ isConnected(): boolean {
354
+ return this.connected;
355
+ }
356
+
357
+ async disconnect(): Promise<void> {
358
+ this.connected = false;
359
+ try {
360
+ this.proc.kill("SIGTERM");
361
+ await Promise.race([
362
+ this.proc.exited,
363
+ new Promise((r) => setTimeout(r, 5000)),
364
+ ]);
365
+ } catch {}
366
+ }
367
+
368
+ getGooseSessionId(): string | null {
369
+ return this.gooseSessionId;
370
+ }
371
+
372
+ getBackendSessionId(): string | null {
373
+ return this.gooseSessionId;
374
+ }
375
+
376
+ // ── Initialization ─────────────────────────────────────────────────────
377
+
378
+ private async initialize(): Promise<void> {
379
+ try {
380
+ // Step 1: ACP initialize handshake
381
+ const initResult = await this.transport.call("initialize", {
382
+ protocolVersion: "v1",
383
+ clientCapabilities: {},
384
+ clientInfo: {
385
+ name: "campfire",
386
+ title: "Campfire",
387
+ version: "1.0.0",
388
+ },
389
+ }) as Record<string, unknown>;
390
+
391
+ this.connected = true;
392
+ this.initialized = true;
393
+
394
+ // Extract capabilities
395
+ const capabilities = initResult?.agentCapabilities as Record<string, unknown> || {};
396
+ const hasLoadSession = capabilities.loadSession === true;
397
+
398
+ // Step 2: Create or load a session
399
+ if (this.options.gooseSessionId && hasLoadSession) {
400
+ // Resume existing session
401
+ const loadResult = await this.transport.call("session/load", {
402
+ sessionId: this.options.gooseSessionId,
403
+ mcpServers: [],
404
+ cwd: this.options.cwd || process.cwd(),
405
+ }) as Record<string, unknown>;
406
+ this.gooseSessionId = this.options.gooseSessionId;
407
+ } else {
408
+ // Create new session
409
+ const newResult = await this.transport.call("session/new", {
410
+ mcpServers: [],
411
+ cwd: this.options.cwd || process.cwd(),
412
+ }) as Record<string, unknown>;
413
+ this.gooseSessionId = newResult?.sessionId as string || randomUUID();
414
+ }
415
+
416
+ // Notify session metadata
417
+ this.sessionMetaCb?.({
418
+ cliSessionId: this.gooseSessionId,
419
+ model: this.options.model,
420
+ cwd: this.options.cwd,
421
+ });
422
+
423
+ // Send session_init to browser
424
+ const state: SessionState = {
425
+ session_id: this.sessionId,
426
+ backend_type: "goose",
427
+ model: this.options.model || "default",
428
+ cwd: this.options.cwd || "",
429
+ tools: ["developer__bash", "developer__text_editor"],
430
+ permissionMode: this.options.permissionMode || "default",
431
+ claude_code_version: "",
432
+ mcp_servers: [],
433
+ agents: [],
434
+ slash_commands: [],
435
+ skills: [],
436
+ total_cost_usd: 0,
437
+ num_turns: 0,
438
+ context_used_percent: 0,
439
+ is_compacting: false,
440
+ git_branch: "",
441
+ is_worktree: false,
442
+ repo_root: "",
443
+ git_ahead: 0,
444
+ git_behind: 0,
445
+ total_lines_added: 0,
446
+ total_lines_removed: 0,
447
+ total_duration_api_ms: 0,
448
+ };
449
+
450
+ this.emit({ type: "session_init", session: state });
451
+
452
+ // If model was specified, set it
453
+ if (this.options.model) {
454
+ this.transport.call("session/set_model", {
455
+ model: this.options.model,
456
+ }).catch((err) => {
457
+ console.warn(`[goose-adapter] Failed to set model: ${err}`);
458
+ });
459
+ }
460
+
461
+ // Flush queued messages
462
+ if (this.pendingOutgoing.length > 0) {
463
+ console.log(`[goose-adapter] Flushing ${this.pendingOutgoing.length} queued message(s)`);
464
+ const queued = this.pendingOutgoing.splice(0);
465
+ for (const msg of queued) {
466
+ this.dispatchOutgoing(msg);
467
+ }
468
+ }
469
+ } catch (err) {
470
+ const errorMsg = `Goose initialization failed: ${err}`;
471
+ console.error(`[goose-adapter] ${errorMsg}`);
472
+ this.initFailed = true;
473
+ this.connected = false;
474
+ this.pendingOutgoing.length = 0;
475
+ this.emit({ type: "error", message: errorMsg });
476
+ this.initErrorCb?.(errorMsg);
477
+ }
478
+ }
479
+
480
+ // ── Outgoing message handlers ──────────────────────────────────────────
481
+
482
+ private async handleOutgoingUserMessage(
483
+ msg: { type: "user_message"; content: string; images?: { media_type: string; data: string }[] },
484
+ ): Promise<void> {
485
+ if (!this.gooseSessionId) {
486
+ this.emit({ type: "error", message: "No Goose session started yet" });
487
+ return;
488
+ }
489
+
490
+ const prompt: Array<Record<string, unknown>> = [];
491
+
492
+ // Add images if present
493
+ if (msg.images?.length) {
494
+ for (const img of msg.images) {
495
+ prompt.push({
496
+ type: "image",
497
+ data: img.data,
498
+ media_type: img.media_type,
499
+ });
500
+ }
501
+ }
502
+
503
+ // Add text
504
+ prompt.push({ type: "text", text: msg.content });
505
+
506
+ try {
507
+ // session/prompt is async — Goose streams notifications and then returns the response
508
+ const result = await this.transport.call("session/prompt", {
509
+ sessionId: this.gooseSessionId,
510
+ prompt,
511
+ }) as Record<string, unknown>;
512
+
513
+ // The response comes after all streaming is done
514
+ this.finishTurn(result);
515
+ } catch (err) {
516
+ this.emit({ type: "error", message: `Failed to send prompt: ${err}` });
517
+ // Emit a result so the UI knows the turn ended
518
+ this.emitResult("error_during_execution", true);
519
+ }
520
+ }
521
+
522
+ private async handleOutgoingPermissionResponse(
523
+ msg: { type: "permission_response"; request_id: string; behavior: "allow" | "deny" },
524
+ ): Promise<void> {
525
+ // Goose uses RequestPermissionOutcome sent as a notification or response
526
+ // The permission response is sent back via the notification channel
527
+ const permissionMap: Record<string, string> = {
528
+ allow: "AllowOnce",
529
+ deny: "RejectOnce",
530
+ };
531
+
532
+ try {
533
+ await this.transport.notify("requestPermission", {
534
+ id: msg.request_id,
535
+ permission: permissionMap[msg.behavior] || "RejectOnce",
536
+ });
537
+ } catch (err) {
538
+ console.warn(`[goose-adapter] Failed to send permission response: ${err}`);
539
+ }
540
+ }
541
+
542
+ private async handleOutgoingInterrupt(): Promise<void> {
543
+ try {
544
+ await this.transport.notify("session/cancel", {});
545
+ } catch (err) {
546
+ console.warn("[goose-adapter] Cancel failed:", err);
547
+ }
548
+ }
549
+
550
+ private async handleOutgoingSetModel(model: string): Promise<void> {
551
+ try {
552
+ await this.transport.call("session/set_model", { model });
553
+ } catch (err) {
554
+ console.warn(`[goose-adapter] Set model failed: ${err}`);
555
+ }
556
+ }
557
+
558
+ // ── Incoming notification handlers ─────────────────────────────────────
559
+
560
+ private handleNotification(method: string, params: Record<string, unknown>): void {
561
+ try {
562
+ switch (method) {
563
+ case "session/notification":
564
+ this.handleSessionNotification(params);
565
+ break;
566
+ default:
567
+ console.log(`[goose-adapter] Unhandled notification: ${method}`);
568
+ break;
569
+ }
570
+ } catch (err) {
571
+ console.error(`[goose-adapter] Error handling notification ${method}:`, err);
572
+ }
573
+ }
574
+
575
+ private handleSessionNotification(params: Record<string, unknown>): void {
576
+ const update = params.update as Record<string, unknown>;
577
+ if (!update) return;
578
+
579
+ const updateType = update.sessionUpdate as string;
580
+ console.log(`[goose-adapter] ← session/notification: ${updateType}`);
581
+
582
+ switch (updateType) {
583
+ case "agentMessageChunk":
584
+ this.handleAgentMessageChunk(update);
585
+ break;
586
+ case "agentThoughtChunk":
587
+ this.handleAgentThoughtChunk(update);
588
+ break;
589
+ case "toolCall":
590
+ this.handleToolCall(update);
591
+ break;
592
+ case "toolCallUpdate":
593
+ this.handleToolCallUpdate(update);
594
+ break;
595
+ case "actionRequired":
596
+ this.handleActionRequired(update);
597
+ break;
598
+ case "userMessageChunk":
599
+ // Echo of user input — ignore
600
+ break;
601
+ default:
602
+ console.log(`[goose-adapter] Unhandled sessionUpdate: ${updateType}`);
603
+ break;
604
+ }
605
+ }
606
+
607
+ private handleRequest(method: string, id: number, params: Record<string, unknown>): void {
608
+ // Goose may send requests that need responses (e.g., permission requests)
609
+ console.log(`[goose-adapter] Received request: ${method}`);
610
+ try {
611
+ switch (method) {
612
+ case "requestPermission": {
613
+ // Server-initiated permission request as JSON-RPC request
614
+ const requestId = `goose-perm-${randomUUID()}`;
615
+ this.pendingPermissions.set(requestId, id);
616
+ const data = params.data as Record<string, unknown> || params;
617
+ this.emitPermissionRequest(requestId, data);
618
+ break;
619
+ }
620
+ default:
621
+ console.log(`[goose-adapter] Unhandled request: ${method}`);
622
+ this.transport.respond(id, {});
623
+ break;
624
+ }
625
+ } catch (err) {
626
+ console.error(`[goose-adapter] Error handling request ${method}:`, err);
627
+ }
628
+ }
629
+
630
+ // ── SessionUpdate handlers ─────────────────────────────────────────────
631
+
632
+ private handleAgentMessageChunk(update: Record<string, unknown>): void {
633
+ const content = update.content as Record<string, unknown>;
634
+ if (!content) return;
635
+
636
+ const text = content.text as string || "";
637
+
638
+ // Start streaming if not already active
639
+ if (!this.streamingActive) {
640
+ this.streamingActive = true;
641
+ this.streamingText = "";
642
+
643
+ // Emit message_start
644
+ this.emit({
645
+ type: "stream_event",
646
+ event: {
647
+ type: "message_start",
648
+ message: {
649
+ id: this.makeMessageId("agent"),
650
+ type: "message",
651
+ role: "assistant",
652
+ model: this.options.model || "",
653
+ content: [],
654
+ stop_reason: null,
655
+ usage: { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 },
656
+ },
657
+ },
658
+ parent_tool_use_id: null,
659
+ });
660
+
661
+ // Emit content_block_start
662
+ this.emit({
663
+ type: "stream_event",
664
+ event: {
665
+ type: "content_block_start",
666
+ index: 0,
667
+ content_block: { type: "text", text: "" },
668
+ },
669
+ parent_tool_use_id: null,
670
+ });
671
+ }
672
+
673
+ this.streamingText += text;
674
+
675
+ // Emit content_block_delta (matches Claude's streaming format)
676
+ this.emit({
677
+ type: "stream_event",
678
+ event: {
679
+ type: "content_block_delta",
680
+ index: 0,
681
+ delta: { type: "text_delta", text },
682
+ },
683
+ parent_tool_use_id: null,
684
+ });
685
+ }
686
+
687
+ private handleAgentThoughtChunk(update: Record<string, unknown>): void {
688
+ const content = update.content as Record<string, unknown>;
689
+ if (!content) return;
690
+
691
+ const thinking = content.thinking as string || content.text as string || "";
692
+ if (!thinking) return;
693
+
694
+ this.emit({
695
+ type: "stream_event",
696
+ event: {
697
+ type: "content_block_start",
698
+ index: 0,
699
+ content_block: { type: "thinking", thinking },
700
+ },
701
+ parent_tool_use_id: null,
702
+ });
703
+ }
704
+
705
+ private handleToolCall(update: Record<string, unknown>): void {
706
+ // Flush any active text streaming before tool call
707
+ this.flushStreaming();
708
+
709
+ const toolName = update.toolName as string || "unknown";
710
+ const args = update.arguments as Record<string, unknown> || update.raw_input as Record<string, unknown> || {};
711
+ const toolCallId = update.id as string || `goose-tool-${randomUUID()}`;
712
+
713
+ this.activeToolCalls.set(toolCallId, { toolName, startTime: Date.now() });
714
+
715
+ // Map Goose tool names to Campfire-compatible names
716
+ const mappedName = this.mapToolName(toolName);
717
+
718
+ // Emit stream event for tool_use start
719
+ this.emit({
720
+ type: "stream_event",
721
+ event: {
722
+ type: "content_block_start",
723
+ index: 0,
724
+ content_block: { type: "tool_use", id: toolCallId, name: mappedName, input: {} },
725
+ },
726
+ parent_tool_use_id: null,
727
+ });
728
+
729
+ // Emit the tool_use assistant message
730
+ this.emittedToolUseIds.add(toolCallId);
731
+ this.emit({
732
+ type: "assistant",
733
+ message: {
734
+ id: this.makeMessageId("tool_use", toolCallId),
735
+ type: "message",
736
+ role: "assistant",
737
+ model: this.options.model || "",
738
+ content: [
739
+ {
740
+ type: "tool_use",
741
+ id: toolCallId,
742
+ name: mappedName,
743
+ input: args,
744
+ },
745
+ ],
746
+ stop_reason: null,
747
+ usage: { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 },
748
+ },
749
+ parent_tool_use_id: null,
750
+ timestamp: Date.now(),
751
+ });
752
+
753
+ // Emit tool_progress
754
+ this.emit({
755
+ type: "tool_progress",
756
+ tool_use_id: toolCallId,
757
+ tool_name: mappedName,
758
+ elapsed_time_seconds: 0,
759
+ });
760
+ }
761
+
762
+ private handleToolCallUpdate(update: Record<string, unknown>): void {
763
+ const toolCallId = update.id as string || "";
764
+ const result = update.result as string || update.content as string || "";
765
+ const status = update.status as string || "completed";
766
+ const isError = status === "failed" || status === "error";
767
+
768
+ // Clean up tracking
769
+ this.activeToolCalls.delete(toolCallId);
770
+
771
+ // Emit tool result
772
+ const safeContent = typeof result === "string" ? result : JSON.stringify(result);
773
+ this.emit({
774
+ type: "assistant",
775
+ message: {
776
+ id: this.makeMessageId("tool_result", toolCallId),
777
+ type: "message",
778
+ role: "assistant",
779
+ model: this.options.model || "",
780
+ content: [
781
+ {
782
+ type: "tool_result",
783
+ tool_use_id: toolCallId,
784
+ content: safeContent || (isError ? "Tool call failed" : "Tool call completed"),
785
+ is_error: isError,
786
+ },
787
+ ],
788
+ stop_reason: null,
789
+ usage: { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 },
790
+ },
791
+ parent_tool_use_id: null,
792
+ timestamp: Date.now(),
793
+ });
794
+ }
795
+
796
+ private handleActionRequired(update: Record<string, unknown>): void {
797
+ const data = update.data as Record<string, unknown>;
798
+ if (!data) return;
799
+
800
+ const actionType = data.actionType as string;
801
+ if (actionType === "toolConfirmation") {
802
+ const requestId = data.id as string || `goose-action-${randomUUID()}`;
803
+ this.emitPermissionRequest(requestId, data);
804
+ } else {
805
+ console.log(`[goose-adapter] Unhandled actionRequired type: ${actionType}`);
806
+ }
807
+ }
808
+
809
+ // ── Helpers ─────────────────────────────────────────────────────────────
810
+
811
+ private emit(msg: BrowserIncomingMessage): void {
812
+ this.browserMessageCb?.(msg);
813
+ }
814
+
815
+ /** Flush any in-progress text streaming (emit stop events). */
816
+ private flushStreaming(): void {
817
+ if (!this.streamingActive) return;
818
+
819
+ // Emit content_block_stop + message_delta
820
+ this.emit({
821
+ type: "stream_event",
822
+ event: { type: "content_block_stop", index: 0 },
823
+ parent_tool_use_id: null,
824
+ });
825
+ this.emit({
826
+ type: "stream_event",
827
+ event: {
828
+ type: "message_delta",
829
+ delta: { stop_reason: null },
830
+ usage: { output_tokens: 0 },
831
+ },
832
+ parent_tool_use_id: null,
833
+ });
834
+
835
+ // Emit full assistant message if there's accumulated text
836
+ if (this.streamingText) {
837
+ this.emit({
838
+ type: "assistant",
839
+ message: {
840
+ id: this.makeMessageId("agent"),
841
+ type: "message",
842
+ role: "assistant",
843
+ model: this.options.model || "",
844
+ content: [{ type: "text", text: this.streamingText }],
845
+ stop_reason: "end_turn",
846
+ usage: { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 },
847
+ },
848
+ parent_tool_use_id: null,
849
+ timestamp: Date.now(),
850
+ });
851
+ }
852
+
853
+ this.streamingText = "";
854
+ this.streamingActive = false;
855
+ }
856
+
857
+ /** Called when session/prompt returns (turn completed). */
858
+ private finishTurn(result: Record<string, unknown>): void {
859
+ // Flush any remaining streaming
860
+ this.flushStreaming();
861
+
862
+ const stopReason = result?.stopReason as string || result?.stop_reason as string || "end_turn";
863
+
864
+ this.emitResult(
865
+ stopReason === "cancelled" ? "error_during_execution" : "success",
866
+ stopReason === "cancelled",
867
+ );
868
+ }
869
+
870
+ private emitResult(subtype: CLIResultMessage["subtype"], isError: boolean): void {
871
+ const result: CLIResultMessage = {
872
+ type: "result",
873
+ subtype,
874
+ is_error: isError,
875
+ duration_ms: 0,
876
+ duration_api_ms: 0,
877
+ num_turns: 1,
878
+ total_cost_usd: 0,
879
+ stop_reason: subtype === "success" ? "end_turn" : "error",
880
+ usage: { input_tokens: 0, output_tokens: 0, cache_creation_input_tokens: 0, cache_read_input_tokens: 0 },
881
+ uuid: randomUUID(),
882
+ session_id: this.sessionId,
883
+ };
884
+
885
+ this.emit({ type: "result", data: result });
886
+ }
887
+
888
+ private emitPermissionRequest(requestId: string, data: Record<string, unknown>): void {
889
+ const toolName = data.toolName as string || "unknown";
890
+ const args = data.arguments as Record<string, unknown> || {};
891
+ const prompt = data.prompt as string || data.content as string;
892
+
893
+ const mappedName = this.mapToolName(toolName);
894
+
895
+ const perm: PermissionRequest = {
896
+ request_id: requestId,
897
+ tool_name: mappedName,
898
+ input: args,
899
+ description: prompt || `Goose wants to use: ${mappedName}`,
900
+ tool_use_id: data.toolCallId as string || requestId,
901
+ timestamp: Date.now(),
902
+ };
903
+
904
+ this.emit({ type: "permission_request", request: perm });
905
+ }
906
+
907
+ /** Map Goose tool names to Campfire-compatible tool names. */
908
+ private mapToolName(gooseToolName: string): string {
909
+ // Goose uses developer__bash, developer__text_editor, etc.
910
+ const toolMap: Record<string, string> = {
911
+ "developer__bash": "Bash",
912
+ "developer__shell": "Bash",
913
+ "developer__text_editor": "Edit",
914
+ "developer__read_file": "Read",
915
+ "developer__write_file": "Write",
916
+ "developer__list_directory": "Glob",
917
+ "developer__search_files": "Grep",
918
+ "memory__set": "memory_set",
919
+ "memory__get": "memory_get",
920
+ };
921
+
922
+ return toolMap[gooseToolName] || gooseToolName;
923
+ }
924
+
925
+ private makeMessageId(kind: string, sourceId?: string): string {
926
+ if (sourceId) return `goose-${kind}-${sourceId}`;
927
+ return `goose-${kind}-${randomUUID()}`;
928
+ }
929
+ }