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.
- package/bin/cli.ts +182 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/MermaidDiagram-By1J3Whj.js +2 -0
- package/dist/assets/_basePickBy-Dq6gnD-p.js +1 -0
- package/dist/assets/_baseUniq-SpgN6cGc.js +1 -0
- package/dist/assets/arc-D9SEA8dX.js +1 -0
- package/dist/assets/architectureDiagram-VXUJARFQ-CoTcwLeR.js +36 -0
- package/dist/assets/blockDiagram-VD42YOAC-Dvc63voV.js +122 -0
- package/dist/assets/c4Diagram-YG6GDRKO-tuDcG4_4.js +10 -0
- package/dist/assets/channel-CxhcHn7S.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-x1hChYu9.js +1 -0
- package/dist/assets/chunk-55IACEB6-BX6HWnWP.js +1 -0
- package/dist/assets/chunk-B4BG7PRW-B4IlF03G.js +165 -0
- package/dist/assets/chunk-DI55MBZ5-TpYb5rLg.js +220 -0
- package/dist/assets/chunk-FMBD7UC4-DZX4LUPx.js +15 -0
- package/dist/assets/chunk-QN33PNHL-CuQs0NDY.js +1 -0
- package/dist/assets/chunk-QZHKN3VN-CygWwVx8.js +1 -0
- package/dist/assets/chunk-TZMSLE5B-CtI-IdKV.js +1 -0
- package/dist/assets/classDiagram-2ON5EDUG-BM0LptJF.js +1 -0
- package/dist/assets/classDiagram-v2-WZHVMYZB-BM0LptJF.js +1 -0
- package/dist/assets/clone-C6kt1rE7.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-PdySs991.js +1 -0
- package/dist/assets/cytoscape.esm-BQaXIfA_.js +331 -0
- package/dist/assets/dagre-6UL2VRFP-J0SxEiEu.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-PSM6KHXK-CW0HLXaH.js +24 -0
- package/dist/assets/diagram-QEK2KX5R-DADHF-k4.js +43 -0
- package/dist/assets/diagram-S2PKOQOG-9S0-PC3S.js +24 -0
- package/dist/assets/erDiagram-Q2GNP2WA-BEHAiY_q.js +60 -0
- package/dist/assets/flowDiagram-NV44I4VS-Cu6n9Xnd.js +162 -0
- package/dist/assets/ganttDiagram-JELNMOA3-Cr_BHryu.js +267 -0
- package/dist/assets/gitGraphDiagram-V2S2FVAM-D5pT35v5.js +65 -0
- package/dist/assets/graph-DKRbrxcs.js +1 -0
- package/dist/assets/index-Byjg9zgI.css +32 -0
- package/dist/assets/index-Cwj3m0hT.js +284 -0
- package/dist/assets/infoDiagram-HS3SLOUP-BaJlrgKV.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/journeyDiagram-XKPGCS4Q-CXC4-DJv.js +139 -0
- package/dist/assets/kanban-definition-3W4ZIXB7-BtHBu9cx.js +89 -0
- package/dist/assets/katex-BlHpptmG.js +261 -0
- package/dist/assets/layout-DnxSwLDJ.js +1 -0
- package/dist/assets/linear-CWbbIT8l.js +1 -0
- package/dist/assets/mermaid.core-DXn1cFAK.js +256 -0
- package/dist/assets/mindmap-definition-VGOIOE7T-BR8br1hs.js +68 -0
- package/dist/assets/ordinal-BENe2yWM.js +1 -0
- package/dist/assets/pieDiagram-ADFJNKIX-B7RmXV7I.js +30 -0
- package/dist/assets/quadrantDiagram-AYHSOK5B-DK0FhyV_.js +7 -0
- package/dist/assets/requirementDiagram-UZGBJVZJ-3zAGxSpL.js +64 -0
- package/dist/assets/sankeyDiagram-TZEHDZUN-DZppKtjc.js +10 -0
- package/dist/assets/sequenceDiagram-WL72ISMW-D1x3r3wV.js +145 -0
- package/dist/assets/stateDiagram-FKZM4ZOC-2ByD08Vq.js +1 -0
- package/dist/assets/stateDiagram-v2-4FDKWEC3-Vm1cYjgV.js +1 -0
- package/dist/assets/timeline-definition-IT6M3QCI-l5S35biR.js +61 -0
- package/dist/assets/treemap-GDKQZRPO-DIPadPiF.js +162 -0
- package/dist/assets/xychartDiagram-PRI3JC2R-9GUvwS2b.js +7 -0
- package/dist/favicon.svg +3 -0
- package/dist/fonts/Geist-Variable.woff2 +0 -0
- package/dist/fonts/GeistMono-Variable.woff2 +0 -0
- package/dist/icon-192.png +0 -0
- package/dist/icon-512.png +0 -0
- package/dist/index.html +33 -0
- package/dist/logo-codex.svg +14 -0
- package/dist/logo.svg +3 -0
- package/dist/manifest.json +26 -0
- package/dist/og-image.png +0 -0
- package/dist/sw.js +130 -0
- package/package.json +92 -0
- package/server/ADAPTERS.md +121 -0
- package/server/adapter-registry-types.ts +28 -0
- package/server/adapter-registry.ts +293 -0
- package/server/adapter-types.ts +51 -0
- package/server/agent-executor.ts +249 -0
- package/server/agent-mcp-bridge.ts +148 -0
- package/server/agent-mcp-stdio.ts +171 -0
- package/server/agent-mcp-tools.ts +76 -0
- package/server/agent-store.ts +183 -0
- package/server/agent-types.ts +79 -0
- package/server/aider-adapter.ts +525 -0
- package/server/auth.ts +153 -0
- package/server/auto-namer.ts +132 -0
- package/server/capability-discovery.ts +431 -0
- package/server/claude-container-auth.ts +40 -0
- package/server/claude-stdio-adapter.ts +452 -0
- package/server/clawhub-export.ts +218 -0
- package/server/cli-launcher.ts +1613 -0
- package/server/codex-adapter.ts +1987 -0
- package/server/codex-container-auth.ts +40 -0
- package/server/codex-home.ts +26 -0
- package/server/collective-intelligence.ts +401 -0
- package/server/commands-discovery.ts +160 -0
- package/server/constants.ts +5 -0
- package/server/container-manager.ts +328 -0
- package/server/cron-scheduler.ts +243 -0
- package/server/cron-store.ts +149 -0
- package/server/cron-types.ts +63 -0
- package/server/deliberation-engine.ts +323 -0
- package/server/embedding.ts +89 -0
- package/server/env-manager.ts +146 -0
- package/server/environment-detector.ts +106 -0
- package/server/environment-rules.ts +106 -0
- package/server/gallery-store.ts +216 -0
- package/server/gallery-types.ts +52 -0
- package/server/gallery-votes.ts +101 -0
- package/server/git-utils.ts +384 -0
- package/server/github-pr.ts +379 -0
- package/server/goose-adapter.ts +929 -0
- package/server/image-pull-manager.ts +144 -0
- package/server/index.ts +363 -0
- package/server/linear-project-manager.ts +78 -0
- package/server/moltbook-client.ts +160 -0
- package/server/openclaw-adapter.ts +916 -0
- package/server/openclaw-channel/channel.ts +188 -0
- package/server/openclaw-channel/index.ts +68 -0
- package/server/openclaw-channel/package.json +19 -0
- package/server/openclaw-channel/types.ts +93 -0
- package/server/opencode-adapter.ts +951 -0
- package/server/openhands-adapter.ts +775 -0
- package/server/orchestrator-executor.ts +184 -0
- package/server/orchestrator-store.ts +106 -0
- package/server/orchestrator-types.ts +86 -0
- package/server/path-resolver.ts +168 -0
- package/server/pr-poller.ts +162 -0
- package/server/proactive-keepalive.ts +121 -0
- package/server/prompt-manager.ts +103 -0
- package/server/protocol-monitor.ts +197 -0
- package/server/race-controller.ts +295 -0
- package/server/race-store.ts +82 -0
- package/server/recorder.ts +359 -0
- package/server/recording-hub/compat-validator.ts +122 -0
- package/server/recording-hub/diagnostics.ts +289 -0
- package/server/recording-hub/hub-routes.ts +146 -0
- package/server/recording-hub/hub-store.ts +224 -0
- package/server/replay.ts +78 -0
- package/server/routes/adapter-routes.ts +31 -0
- package/server/routes/agent-mcp-routes.ts +35 -0
- package/server/routes/agent-routes.ts +185 -0
- package/server/routes/auth-routes.ts +101 -0
- package/server/routes/ci-routes.ts +165 -0
- package/server/routes/commands-routes.ts +94 -0
- package/server/routes/cron-routes.ts +100 -0
- package/server/routes/env-routes.ts +50 -0
- package/server/routes/folder-routes.ts +56 -0
- package/server/routes/fs-routes.ts +403 -0
- package/server/routes/gallery-routes.ts +410 -0
- package/server/routes/git-routes.ts +108 -0
- package/server/routes/index.ts +62 -0
- package/server/routes/linear-routes.ts +254 -0
- package/server/routes/monitor-routes.ts +11 -0
- package/server/routes/orchestrator-routes.ts +210 -0
- package/server/routes/prompt-routes.ts +47 -0
- package/server/routes/race-routes.ts +103 -0
- package/server/routes/recording-routes.ts +101 -0
- package/server/routes/route-deps.ts +32 -0
- package/server/routes/session-routes.ts +560 -0
- package/server/routes/settings-routes.ts +108 -0
- package/server/routes/skills-routes.ts +52 -0
- package/server/routes/system-routes.ts +243 -0
- package/server/routes/webhook-routes.ts +135 -0
- package/server/routes.ts +44 -0
- package/server/security-middleware.ts +100 -0
- package/server/semantic-memory.ts +389 -0
- package/server/service.ts +733 -0
- package/server/session-folders.ts +107 -0
- package/server/session-git-info.ts +89 -0
- package/server/session-linear-issues.ts +94 -0
- package/server/session-names.ts +67 -0
- package/server/session-store.ts +169 -0
- package/server/session-types.ts +415 -0
- package/server/settings-manager.ts +120 -0
- package/server/shared-context.ts +401 -0
- package/server/skills-manager.ts +250 -0
- package/server/sub-session-manager.ts +263 -0
- package/server/terminal-manager.ts +185 -0
- package/server/update-checker.ts +114 -0
- package/server/usage-limits.ts +192 -0
- package/server/webhook-manager.ts +228 -0
- package/server/webhook-store.ts +168 -0
- package/server/webhook-types.ts +66 -0
- package/server/worktree-tracker.ts +84 -0
- 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
|
+
}
|