@posthog/agent 2.0.1 → 2.0.3
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/dist/{agent-DBQY1BfC.d.ts → agent-kRbaLUfe.d.ts} +3 -0
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +62 -5
- package/dist/agent.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6223 -94
- package/dist/index.js.map +1 -1
- package/dist/server/agent-server.js +6206 -97
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +10599 -0
- package/dist/server/bin.cjs.map +1 -0
- package/package.json +6 -6
- package/src/sagas/resume-saga.test.ts +144 -0
- package/src/sagas/resume-saga.ts +23 -0
- package/src/sagas/test-fixtures.ts +9 -0
- package/src/session-log-writer.ts +81 -1
- package/dist/server/bin.d.ts +0 -1
- package/dist/server/bin.js +0 -4507
- package/dist/server/bin.js.map +0 -1
package/dist/server/bin.js
DELETED
|
@@ -1,4507 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// src/server/bin.ts
|
|
4
|
-
import { Command } from "commander";
|
|
5
|
-
import { z as z4 } from "zod";
|
|
6
|
-
|
|
7
|
-
// src/server/agent-server.ts
|
|
8
|
-
import {
|
|
9
|
-
ClientSideConnection,
|
|
10
|
-
ndJsonStream as ndJsonStream2,
|
|
11
|
-
PROTOCOL_VERSION
|
|
12
|
-
} from "@agentclientprotocol/sdk";
|
|
13
|
-
import { serve } from "@hono/node-server";
|
|
14
|
-
import { Hono } from "hono";
|
|
15
|
-
|
|
16
|
-
// src/acp-extensions.ts
|
|
17
|
-
var POSTHOG_NOTIFICATIONS = {
|
|
18
|
-
/** Git branch was created for a task */
|
|
19
|
-
BRANCH_CREATED: "_posthog/branch_created",
|
|
20
|
-
/** Task run has started execution */
|
|
21
|
-
RUN_STARTED: "_posthog/run_started",
|
|
22
|
-
/** Task has completed (success or failure) */
|
|
23
|
-
TASK_COMPLETE: "_posthog/task_complete",
|
|
24
|
-
/** Error occurred during task execution */
|
|
25
|
-
ERROR: "_posthog/error",
|
|
26
|
-
/** Console/log output from the agent */
|
|
27
|
-
CONSOLE: "_posthog/console",
|
|
28
|
-
/** Maps taskRunId to agent's sessionId and adapter type (for resumption) */
|
|
29
|
-
SDK_SESSION: "_posthog/sdk_session",
|
|
30
|
-
/** Tree state snapshot captured (git tree hash + file archive) */
|
|
31
|
-
TREE_SNAPSHOT: "_posthog/tree_snapshot",
|
|
32
|
-
/** Agent mode changed (interactive/background) */
|
|
33
|
-
MODE_CHANGE: "_posthog/mode_change",
|
|
34
|
-
/** Request to resume a session from previous state */
|
|
35
|
-
SESSION_RESUME: "_posthog/session/resume",
|
|
36
|
-
/** User message sent from client to agent */
|
|
37
|
-
USER_MESSAGE: "_posthog/user_message",
|
|
38
|
-
/** Request to cancel current operation */
|
|
39
|
-
CANCEL: "_posthog/cancel",
|
|
40
|
-
/** Request to close the session */
|
|
41
|
-
CLOSE: "_posthog/close",
|
|
42
|
-
/** Agent status update (thinking, working, etc.) */
|
|
43
|
-
STATUS: "_posthog/status",
|
|
44
|
-
/** Task-level notification (progress, milestones) */
|
|
45
|
-
TASK_NOTIFICATION: "_posthog/task_notification",
|
|
46
|
-
/** Marks a boundary for log compaction */
|
|
47
|
-
COMPACT_BOUNDARY: "_posthog/compact_boundary"
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
// src/adapters/acp-connection.ts
|
|
51
|
-
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
|
|
52
|
-
|
|
53
|
-
// src/utils/logger.ts
|
|
54
|
-
var Logger = class _Logger {
|
|
55
|
-
debugEnabled;
|
|
56
|
-
prefix;
|
|
57
|
-
scope;
|
|
58
|
-
onLog;
|
|
59
|
-
constructor(config = {}) {
|
|
60
|
-
this.debugEnabled = config.debug ?? false;
|
|
61
|
-
this.prefix = config.prefix ?? "[PostHog Agent]";
|
|
62
|
-
this.scope = config.scope ?? "agent";
|
|
63
|
-
this.onLog = config.onLog;
|
|
64
|
-
}
|
|
65
|
-
formatMessage(level, message, data) {
|
|
66
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
67
|
-
const base = `${timestamp} ${this.prefix} [${level}] ${message}`;
|
|
68
|
-
if (data !== void 0) {
|
|
69
|
-
return `${base} ${JSON.stringify(data, null, 2)}`;
|
|
70
|
-
}
|
|
71
|
-
return base;
|
|
72
|
-
}
|
|
73
|
-
emitLog(level, message, data) {
|
|
74
|
-
if (this.onLog) {
|
|
75
|
-
this.onLog(level, this.scope, message, data);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
const shouldLog = this.debugEnabled || level === "error";
|
|
79
|
-
if (shouldLog) {
|
|
80
|
-
console[level](this.formatMessage(level.toLowerCase(), message, data));
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
error(message, error) {
|
|
84
|
-
const data = error instanceof Error ? { message: error.message, stack: error.stack } : error;
|
|
85
|
-
this.emitLog("error", message, data);
|
|
86
|
-
}
|
|
87
|
-
warn(message, data) {
|
|
88
|
-
this.emitLog("warn", message, data);
|
|
89
|
-
}
|
|
90
|
-
info(message, data) {
|
|
91
|
-
this.emitLog("info", message, data);
|
|
92
|
-
}
|
|
93
|
-
debug(message, data) {
|
|
94
|
-
this.emitLog("debug", message, data);
|
|
95
|
-
}
|
|
96
|
-
child(childPrefix) {
|
|
97
|
-
return new _Logger({
|
|
98
|
-
debug: this.debugEnabled,
|
|
99
|
-
prefix: `${this.prefix} [${childPrefix}]`,
|
|
100
|
-
scope: `${this.scope}:${childPrefix}`,
|
|
101
|
-
onLog: this.onLog
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
// src/utils/streams.ts
|
|
107
|
-
import { ReadableStream as ReadableStream2, WritableStream as WritableStream2 } from "stream/web";
|
|
108
|
-
var Pushable = class {
|
|
109
|
-
queue = [];
|
|
110
|
-
resolvers = [];
|
|
111
|
-
done = false;
|
|
112
|
-
push(item) {
|
|
113
|
-
const resolve3 = this.resolvers.shift();
|
|
114
|
-
if (resolve3) {
|
|
115
|
-
resolve3({ value: item, done: false });
|
|
116
|
-
} else {
|
|
117
|
-
this.queue.push(item);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
end() {
|
|
121
|
-
this.done = true;
|
|
122
|
-
for (const resolve3 of this.resolvers) {
|
|
123
|
-
resolve3({ value: void 0, done: true });
|
|
124
|
-
}
|
|
125
|
-
this.resolvers = [];
|
|
126
|
-
}
|
|
127
|
-
[Symbol.asyncIterator]() {
|
|
128
|
-
return {
|
|
129
|
-
next: () => {
|
|
130
|
-
if (this.queue.length > 0) {
|
|
131
|
-
const value = this.queue.shift();
|
|
132
|
-
return Promise.resolve({ value, done: false });
|
|
133
|
-
}
|
|
134
|
-
if (this.done) {
|
|
135
|
-
return Promise.resolve({
|
|
136
|
-
value: void 0,
|
|
137
|
-
done: true
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
return new Promise((resolve3) => {
|
|
141
|
-
this.resolvers.push(resolve3);
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
function pushableToReadableStream(pushable) {
|
|
148
|
-
const iterator = pushable[Symbol.asyncIterator]();
|
|
149
|
-
return new ReadableStream2({
|
|
150
|
-
async pull(controller) {
|
|
151
|
-
const { value, done } = await iterator.next();
|
|
152
|
-
if (done) {
|
|
153
|
-
controller.close();
|
|
154
|
-
} else {
|
|
155
|
-
controller.enqueue(value);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
function createBidirectionalStreams() {
|
|
161
|
-
const clientToAgentPushable = new Pushable();
|
|
162
|
-
const agentToClientPushable = new Pushable();
|
|
163
|
-
const clientToAgentReadable = pushableToReadableStream(clientToAgentPushable);
|
|
164
|
-
const agentToClientReadable = pushableToReadableStream(agentToClientPushable);
|
|
165
|
-
const clientToAgentWritable = new WritableStream2({
|
|
166
|
-
write(chunk) {
|
|
167
|
-
clientToAgentPushable.push(chunk);
|
|
168
|
-
},
|
|
169
|
-
close() {
|
|
170
|
-
clientToAgentPushable.end();
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
const agentToClientWritable = new WritableStream2({
|
|
174
|
-
write(chunk) {
|
|
175
|
-
agentToClientPushable.push(chunk);
|
|
176
|
-
},
|
|
177
|
-
close() {
|
|
178
|
-
agentToClientPushable.end();
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
return {
|
|
182
|
-
client: {
|
|
183
|
-
readable: agentToClientReadable,
|
|
184
|
-
writable: clientToAgentWritable
|
|
185
|
-
},
|
|
186
|
-
agent: {
|
|
187
|
-
readable: clientToAgentReadable,
|
|
188
|
-
writable: agentToClientWritable
|
|
189
|
-
}
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
function createTappedWritableStream(underlying, options) {
|
|
193
|
-
const { onMessage, logger } = options;
|
|
194
|
-
const decoder = new TextDecoder();
|
|
195
|
-
let buffer = "";
|
|
196
|
-
let _messageCount = 0;
|
|
197
|
-
return new WritableStream2({
|
|
198
|
-
async write(chunk) {
|
|
199
|
-
buffer += decoder.decode(chunk, { stream: true });
|
|
200
|
-
const lines = buffer.split("\n");
|
|
201
|
-
buffer = lines.pop() ?? "";
|
|
202
|
-
for (const line of lines) {
|
|
203
|
-
if (!line.trim()) continue;
|
|
204
|
-
_messageCount++;
|
|
205
|
-
onMessage(line);
|
|
206
|
-
}
|
|
207
|
-
try {
|
|
208
|
-
const writer = underlying.getWriter();
|
|
209
|
-
await writer.write(chunk);
|
|
210
|
-
writer.releaseLock();
|
|
211
|
-
} catch (err) {
|
|
212
|
-
logger?.error("ACP write error", err);
|
|
213
|
-
}
|
|
214
|
-
},
|
|
215
|
-
async close() {
|
|
216
|
-
try {
|
|
217
|
-
const writer = underlying.getWriter();
|
|
218
|
-
await writer.close();
|
|
219
|
-
writer.releaseLock();
|
|
220
|
-
} catch {
|
|
221
|
-
}
|
|
222
|
-
},
|
|
223
|
-
async abort(reason) {
|
|
224
|
-
logger?.warn("Tapped stream aborted", { reason });
|
|
225
|
-
try {
|
|
226
|
-
const writer = underlying.getWriter();
|
|
227
|
-
await writer.abort(reason);
|
|
228
|
-
writer.releaseLock();
|
|
229
|
-
} catch {
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
function nodeReadableToWebReadable(nodeStream) {
|
|
235
|
-
return new ReadableStream2({
|
|
236
|
-
start(controller) {
|
|
237
|
-
nodeStream.on("data", (chunk) => {
|
|
238
|
-
controller.enqueue(new Uint8Array(chunk));
|
|
239
|
-
});
|
|
240
|
-
nodeStream.on("end", () => {
|
|
241
|
-
controller.close();
|
|
242
|
-
});
|
|
243
|
-
nodeStream.on("error", (err) => {
|
|
244
|
-
controller.error(err);
|
|
245
|
-
});
|
|
246
|
-
},
|
|
247
|
-
cancel() {
|
|
248
|
-
nodeStream.destroy();
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
function nodeWritableToWebWritable(nodeStream) {
|
|
253
|
-
return new WritableStream2({
|
|
254
|
-
write(chunk) {
|
|
255
|
-
return new Promise((resolve3, reject) => {
|
|
256
|
-
const ok = nodeStream.write(Buffer.from(chunk), (err) => {
|
|
257
|
-
if (err) reject(err);
|
|
258
|
-
});
|
|
259
|
-
if (ok) {
|
|
260
|
-
resolve3();
|
|
261
|
-
} else {
|
|
262
|
-
nodeStream.once("drain", resolve3);
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
},
|
|
266
|
-
close() {
|
|
267
|
-
return new Promise((resolve3) => {
|
|
268
|
-
nodeStream.end(resolve3);
|
|
269
|
-
});
|
|
270
|
-
},
|
|
271
|
-
abort(reason) {
|
|
272
|
-
nodeStream.destroy(
|
|
273
|
-
reason instanceof Error ? reason : new Error(String(reason))
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// src/adapters/claude/claude-agent.ts
|
|
280
|
-
import * as fs2 from "fs";
|
|
281
|
-
import * as os3 from "os";
|
|
282
|
-
import * as path3 from "path";
|
|
283
|
-
import {
|
|
284
|
-
RequestError as RequestError2
|
|
285
|
-
} from "@agentclientprotocol/sdk";
|
|
286
|
-
import {
|
|
287
|
-
query
|
|
288
|
-
} from "@anthropic-ai/claude-agent-sdk";
|
|
289
|
-
import { v7 as uuidv7 } from "uuid";
|
|
290
|
-
|
|
291
|
-
// package.json
|
|
292
|
-
var package_default = {
|
|
293
|
-
name: "@posthog/agent",
|
|
294
|
-
version: "2.0.1",
|
|
295
|
-
repository: "https://github.com/PostHog/twig",
|
|
296
|
-
description: "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
|
|
297
|
-
exports: {
|
|
298
|
-
".": {
|
|
299
|
-
types: "./dist/index.d.ts",
|
|
300
|
-
import: "./dist/index.js"
|
|
301
|
-
},
|
|
302
|
-
"./agent": {
|
|
303
|
-
types: "./dist/agent.d.ts",
|
|
304
|
-
import: "./dist/agent.js"
|
|
305
|
-
},
|
|
306
|
-
"./gateway-models": {
|
|
307
|
-
types: "./dist/gateway-models.d.ts",
|
|
308
|
-
import: "./dist/gateway-models.js"
|
|
309
|
-
},
|
|
310
|
-
"./posthog-api": {
|
|
311
|
-
types: "./dist/posthog-api.d.ts",
|
|
312
|
-
import: "./dist/posthog-api.js"
|
|
313
|
-
},
|
|
314
|
-
"./types": {
|
|
315
|
-
types: "./dist/types.d.ts",
|
|
316
|
-
import: "./dist/types.js"
|
|
317
|
-
},
|
|
318
|
-
"./adapters/claude/questions/utils": {
|
|
319
|
-
types: "./dist/adapters/claude/questions/utils.d.ts",
|
|
320
|
-
import: "./dist/adapters/claude/questions/utils.js"
|
|
321
|
-
},
|
|
322
|
-
"./adapters/claude/permissions/permission-options": {
|
|
323
|
-
types: "./dist/adapters/claude/permissions/permission-options.d.ts",
|
|
324
|
-
import: "./dist/adapters/claude/permissions/permission-options.js"
|
|
325
|
-
},
|
|
326
|
-
"./adapters/claude/tools": {
|
|
327
|
-
types: "./dist/adapters/claude/tools.d.ts",
|
|
328
|
-
import: "./dist/adapters/claude/tools.js"
|
|
329
|
-
},
|
|
330
|
-
"./adapters/claude/conversion/tool-use-to-acp": {
|
|
331
|
-
types: "./dist/adapters/claude/conversion/tool-use-to-acp.d.ts",
|
|
332
|
-
import: "./dist/adapters/claude/conversion/tool-use-to-acp.js"
|
|
333
|
-
},
|
|
334
|
-
"./server": {
|
|
335
|
-
types: "./dist/server/agent-server.d.ts",
|
|
336
|
-
import: "./dist/server/agent-server.js"
|
|
337
|
-
}
|
|
338
|
-
},
|
|
339
|
-
bin: {
|
|
340
|
-
"agent-server": "./dist/server/bin.js"
|
|
341
|
-
},
|
|
342
|
-
type: "module",
|
|
343
|
-
keywords: [
|
|
344
|
-
"posthog",
|
|
345
|
-
"claude",
|
|
346
|
-
"agent",
|
|
347
|
-
"ai",
|
|
348
|
-
"git",
|
|
349
|
-
"typescript"
|
|
350
|
-
],
|
|
351
|
-
author: "PostHog",
|
|
352
|
-
license: "SEE LICENSE IN LICENSE",
|
|
353
|
-
scripts: {
|
|
354
|
-
build: "tsup",
|
|
355
|
-
dev: "tsup --watch",
|
|
356
|
-
test: "vitest run",
|
|
357
|
-
"test:watch": "vitest",
|
|
358
|
-
typecheck: "pnpm exec tsc --noEmit",
|
|
359
|
-
prepublishOnly: "pnpm run build"
|
|
360
|
-
},
|
|
361
|
-
engines: {
|
|
362
|
-
node: ">=20.0.0"
|
|
363
|
-
},
|
|
364
|
-
devDependencies: {
|
|
365
|
-
"@changesets/cli": "^2.27.8",
|
|
366
|
-
"@types/bun": "latest",
|
|
367
|
-
"@types/tar": "^6.1.13",
|
|
368
|
-
minimatch: "^10.0.3",
|
|
369
|
-
msw: "^2.12.7",
|
|
370
|
-
tsup: "^8.5.1",
|
|
371
|
-
tsx: "^4.20.6",
|
|
372
|
-
typescript: "^5.5.0",
|
|
373
|
-
vitest: "^2.1.8"
|
|
374
|
-
},
|
|
375
|
-
dependencies: {
|
|
376
|
-
"@opentelemetry/api-logs": "^0.208.0",
|
|
377
|
-
"@opentelemetry/exporter-logs-otlp-http": "^0.208.0",
|
|
378
|
-
"@opentelemetry/resources": "^2.0.0",
|
|
379
|
-
"@opentelemetry/sdk-logs": "^0.208.0",
|
|
380
|
-
"@opentelemetry/semantic-conventions": "^1.28.0",
|
|
381
|
-
"@agentclientprotocol/sdk": "^0.14.0",
|
|
382
|
-
"@anthropic-ai/claude-agent-sdk": "0.2.12",
|
|
383
|
-
"@anthropic-ai/sdk": "^0.71.0",
|
|
384
|
-
"@hono/node-server": "^1.19.9",
|
|
385
|
-
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
386
|
-
"@posthog/shared": "workspace:*",
|
|
387
|
-
"@twig/git": "workspace:*",
|
|
388
|
-
"@types/jsonwebtoken": "^9.0.10",
|
|
389
|
-
commander: "^14.0.2",
|
|
390
|
-
diff: "^8.0.2",
|
|
391
|
-
dotenv: "^17.2.3",
|
|
392
|
-
hono: "^4.11.7",
|
|
393
|
-
jsonwebtoken: "^9.0.2",
|
|
394
|
-
tar: "^7.5.0",
|
|
395
|
-
uuid: "13.0.0",
|
|
396
|
-
"yoga-wasm-web": "^0.3.3",
|
|
397
|
-
zod: "^3.24.1"
|
|
398
|
-
},
|
|
399
|
-
files: [
|
|
400
|
-
"dist/**/*",
|
|
401
|
-
"src/**/*",
|
|
402
|
-
"README.md",
|
|
403
|
-
"CLAUDE.md"
|
|
404
|
-
],
|
|
405
|
-
publishConfig: {
|
|
406
|
-
access: "public"
|
|
407
|
-
}
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
// src/utils/common.ts
|
|
411
|
-
var IS_ROOT = typeof process !== "undefined" && (process.geteuid?.() ?? process.getuid?.()) === 0;
|
|
412
|
-
function unreachable(value, logger) {
|
|
413
|
-
let valueAsString;
|
|
414
|
-
try {
|
|
415
|
-
valueAsString = JSON.stringify(value);
|
|
416
|
-
} catch {
|
|
417
|
-
valueAsString = value;
|
|
418
|
-
}
|
|
419
|
-
logger.error(`Unexpected case: ${valueAsString}`);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
// src/gateway-models.ts
|
|
423
|
-
var DEFAULT_GATEWAY_MODEL = "claude-opus-4-6";
|
|
424
|
-
var BLOCKED_MODELS = /* @__PURE__ */ new Set(["gpt-5-mini", "openai/gpt-5-mini"]);
|
|
425
|
-
async function fetchGatewayModels(options) {
|
|
426
|
-
const gatewayUrl = options?.gatewayUrl ?? process.env.ANTHROPIC_BASE_URL;
|
|
427
|
-
if (!gatewayUrl) {
|
|
428
|
-
return [];
|
|
429
|
-
}
|
|
430
|
-
const modelsUrl = `${gatewayUrl}/v1/models`;
|
|
431
|
-
try {
|
|
432
|
-
const response = await fetch(modelsUrl);
|
|
433
|
-
if (!response.ok) {
|
|
434
|
-
return [];
|
|
435
|
-
}
|
|
436
|
-
const data = await response.json();
|
|
437
|
-
const models = data.data ?? [];
|
|
438
|
-
return models.filter((m) => !BLOCKED_MODELS.has(m.id));
|
|
439
|
-
} catch {
|
|
440
|
-
return [];
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
function isAnthropicModel(model) {
|
|
444
|
-
if (model.owned_by) {
|
|
445
|
-
return model.owned_by === "anthropic";
|
|
446
|
-
}
|
|
447
|
-
return model.id.startsWith("claude-") || model.id.startsWith("anthropic/");
|
|
448
|
-
}
|
|
449
|
-
var PROVIDER_PREFIXES = ["anthropic/", "openai/", "google-vertex/"];
|
|
450
|
-
function formatGatewayModelName(model) {
|
|
451
|
-
let cleanId = model.id;
|
|
452
|
-
for (const prefix of PROVIDER_PREFIXES) {
|
|
453
|
-
if (cleanId.startsWith(prefix)) {
|
|
454
|
-
cleanId = cleanId.slice(prefix.length);
|
|
455
|
-
break;
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
cleanId = cleanId.replace(/(\d)-(\d)/g, "$1.$2");
|
|
459
|
-
const words = cleanId.split(/[-_]/).map((word) => {
|
|
460
|
-
if (word.match(/^[0-9.]+$/)) return word;
|
|
461
|
-
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
462
|
-
});
|
|
463
|
-
return words.join(" ");
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// src/adapters/base-acp-agent.ts
|
|
467
|
-
var BaseAcpAgent = class {
|
|
468
|
-
session;
|
|
469
|
-
sessionId;
|
|
470
|
-
client;
|
|
471
|
-
logger;
|
|
472
|
-
fileContentCache = {};
|
|
473
|
-
constructor(client) {
|
|
474
|
-
this.client = client;
|
|
475
|
-
this.logger = new Logger({ debug: true, prefix: "[BaseAcpAgent]" });
|
|
476
|
-
}
|
|
477
|
-
async cancel(params) {
|
|
478
|
-
if (this.sessionId !== params.sessionId) {
|
|
479
|
-
throw new Error("Session not found");
|
|
480
|
-
}
|
|
481
|
-
this.session.cancelled = true;
|
|
482
|
-
const meta = params._meta;
|
|
483
|
-
if (meta?.interruptReason) {
|
|
484
|
-
this.session.interruptReason = meta.interruptReason;
|
|
485
|
-
}
|
|
486
|
-
await this.interruptSession();
|
|
487
|
-
}
|
|
488
|
-
async closeSession() {
|
|
489
|
-
try {
|
|
490
|
-
this.session.abortController.abort();
|
|
491
|
-
await this.cancel({ sessionId: this.sessionId });
|
|
492
|
-
this.logger.info("Closed session", { sessionId: this.sessionId });
|
|
493
|
-
} catch (err) {
|
|
494
|
-
this.logger.warn("Failed to close session", {
|
|
495
|
-
sessionId: this.sessionId,
|
|
496
|
-
error: err
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
hasSession(sessionId) {
|
|
501
|
-
return this.sessionId === sessionId;
|
|
502
|
-
}
|
|
503
|
-
appendNotification(sessionId, notification) {
|
|
504
|
-
if (this.sessionId === sessionId) {
|
|
505
|
-
this.session.notificationHistory.push(notification);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
async readTextFile(params) {
|
|
509
|
-
const response = await this.client.readTextFile(params);
|
|
510
|
-
if (!params.limit && !params.line) {
|
|
511
|
-
this.fileContentCache[params.path] = response.content;
|
|
512
|
-
}
|
|
513
|
-
return response;
|
|
514
|
-
}
|
|
515
|
-
async writeTextFile(params) {
|
|
516
|
-
const response = await this.client.writeTextFile(params);
|
|
517
|
-
this.fileContentCache[params.path] = params.content;
|
|
518
|
-
return response;
|
|
519
|
-
}
|
|
520
|
-
async authenticate(_params) {
|
|
521
|
-
throw new Error("Method not implemented.");
|
|
522
|
-
}
|
|
523
|
-
async getModelConfigOptions(currentModelOverride) {
|
|
524
|
-
const gatewayModels = await fetchGatewayModels();
|
|
525
|
-
const options = gatewayModels.filter((model) => isAnthropicModel(model)).map((model) => ({
|
|
526
|
-
value: model.id,
|
|
527
|
-
name: formatGatewayModelName(model),
|
|
528
|
-
description: `Context: ${model.context_window.toLocaleString()} tokens`
|
|
529
|
-
}));
|
|
530
|
-
const isAnthropicModelId = (modelId) => modelId.startsWith("claude-") || modelId.startsWith("anthropic/");
|
|
531
|
-
let currentModelId = currentModelOverride ?? DEFAULT_GATEWAY_MODEL;
|
|
532
|
-
if (!options.some((opt) => opt.value === currentModelId)) {
|
|
533
|
-
if (!isAnthropicModelId(currentModelId)) {
|
|
534
|
-
currentModelId = DEFAULT_GATEWAY_MODEL;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
if (!options.some((opt) => opt.value === currentModelId)) {
|
|
538
|
-
options.unshift({
|
|
539
|
-
value: currentModelId,
|
|
540
|
-
name: currentModelId,
|
|
541
|
-
description: "Custom model"
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
return { currentModelId, options };
|
|
545
|
-
}
|
|
546
|
-
};
|
|
547
|
-
|
|
548
|
-
// src/adapters/claude/conversion/acp-to-sdk.ts
|
|
549
|
-
function sdkText(value) {
|
|
550
|
-
return { type: "text", text: value };
|
|
551
|
-
}
|
|
552
|
-
function formatUriAsLink(uri) {
|
|
553
|
-
try {
|
|
554
|
-
if (uri.startsWith("file://")) {
|
|
555
|
-
const filePath = uri.slice(7);
|
|
556
|
-
const name = filePath.split("/").pop() || filePath;
|
|
557
|
-
return `[@${name}](${uri})`;
|
|
558
|
-
}
|
|
559
|
-
if (uri.startsWith("zed://")) {
|
|
560
|
-
const parts = uri.split("/");
|
|
561
|
-
const name = parts[parts.length - 1] || uri;
|
|
562
|
-
return `[@${name}](${uri})`;
|
|
563
|
-
}
|
|
564
|
-
return uri;
|
|
565
|
-
} catch {
|
|
566
|
-
return uri;
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
function transformMcpCommand(text2) {
|
|
570
|
-
const mcpMatch = text2.match(/^\/mcp:([^:\s]+):(\S+)(\s+.*)?$/);
|
|
571
|
-
if (mcpMatch) {
|
|
572
|
-
const [, server, command, args] = mcpMatch;
|
|
573
|
-
return `/${server}:${command} (MCP)${args || ""}`;
|
|
574
|
-
}
|
|
575
|
-
return text2;
|
|
576
|
-
}
|
|
577
|
-
function processPromptChunk(chunk, content, context) {
|
|
578
|
-
switch (chunk.type) {
|
|
579
|
-
case "text":
|
|
580
|
-
content.push(sdkText(transformMcpCommand(chunk.text)));
|
|
581
|
-
break;
|
|
582
|
-
case "resource_link":
|
|
583
|
-
content.push(sdkText(formatUriAsLink(chunk.uri)));
|
|
584
|
-
break;
|
|
585
|
-
case "resource":
|
|
586
|
-
if ("text" in chunk.resource) {
|
|
587
|
-
content.push(sdkText(formatUriAsLink(chunk.resource.uri)));
|
|
588
|
-
context.push(
|
|
589
|
-
sdkText(
|
|
590
|
-
`
|
|
591
|
-
<context ref="${chunk.resource.uri}">
|
|
592
|
-
${chunk.resource.text}
|
|
593
|
-
</context>`
|
|
594
|
-
)
|
|
595
|
-
);
|
|
596
|
-
}
|
|
597
|
-
break;
|
|
598
|
-
case "image":
|
|
599
|
-
if (chunk.data) {
|
|
600
|
-
content.push({
|
|
601
|
-
type: "image",
|
|
602
|
-
source: {
|
|
603
|
-
type: "base64",
|
|
604
|
-
data: chunk.data,
|
|
605
|
-
media_type: chunk.mimeType
|
|
606
|
-
}
|
|
607
|
-
});
|
|
608
|
-
} else if (chunk.uri?.startsWith("http")) {
|
|
609
|
-
content.push({
|
|
610
|
-
type: "image",
|
|
611
|
-
source: { type: "url", url: chunk.uri }
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
break;
|
|
615
|
-
default:
|
|
616
|
-
break;
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
function promptToClaude(prompt) {
|
|
620
|
-
const content = [];
|
|
621
|
-
const context = [];
|
|
622
|
-
for (const chunk of prompt.prompt) {
|
|
623
|
-
processPromptChunk(chunk, content, context);
|
|
624
|
-
}
|
|
625
|
-
content.push(...context);
|
|
626
|
-
return {
|
|
627
|
-
type: "user",
|
|
628
|
-
message: { role: "user", content },
|
|
629
|
-
session_id: prompt.sessionId,
|
|
630
|
-
parent_tool_use_id: null
|
|
631
|
-
};
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
// src/adapters/claude/conversion/sdk-to-acp.ts
|
|
635
|
-
import { RequestError } from "@agentclientprotocol/sdk";
|
|
636
|
-
|
|
637
|
-
// src/utils/acp-content.ts
|
|
638
|
-
function text(value) {
|
|
639
|
-
return { type: "text", text: value };
|
|
640
|
-
}
|
|
641
|
-
function image(data, mimeType, uri) {
|
|
642
|
-
return { type: "image", data, mimeType, uri };
|
|
643
|
-
}
|
|
644
|
-
function resourceLink(uri, name, options) {
|
|
645
|
-
return {
|
|
646
|
-
type: "resource_link",
|
|
647
|
-
uri,
|
|
648
|
-
name,
|
|
649
|
-
...options
|
|
650
|
-
};
|
|
651
|
-
}
|
|
652
|
-
var ToolContentBuilder = class {
|
|
653
|
-
items = [];
|
|
654
|
-
text(value) {
|
|
655
|
-
this.items.push({ type: "content", content: text(value) });
|
|
656
|
-
return this;
|
|
657
|
-
}
|
|
658
|
-
image(data, mimeType, uri) {
|
|
659
|
-
this.items.push({ type: "content", content: image(data, mimeType, uri) });
|
|
660
|
-
return this;
|
|
661
|
-
}
|
|
662
|
-
diff(path4, oldText, newText) {
|
|
663
|
-
this.items.push({ type: "diff", path: path4, oldText, newText });
|
|
664
|
-
return this;
|
|
665
|
-
}
|
|
666
|
-
build() {
|
|
667
|
-
return this.items;
|
|
668
|
-
}
|
|
669
|
-
};
|
|
670
|
-
function toolContent() {
|
|
671
|
-
return new ToolContentBuilder();
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
// src/adapters/claude/hooks.ts
|
|
675
|
-
var toolUseCallbacks = {};
|
|
676
|
-
var registerHookCallback = (toolUseID, {
|
|
677
|
-
onPostToolUseHook
|
|
678
|
-
}) => {
|
|
679
|
-
toolUseCallbacks[toolUseID] = {
|
|
680
|
-
onPostToolUseHook
|
|
681
|
-
};
|
|
682
|
-
};
|
|
683
|
-
var createPostToolUseHook = ({ onModeChange }) => async (input, toolUseID) => {
|
|
684
|
-
if (input.hook_event_name === "PostToolUse") {
|
|
685
|
-
const toolName = input.tool_name;
|
|
686
|
-
if (onModeChange && toolName === "EnterPlanMode") {
|
|
687
|
-
await onModeChange("plan");
|
|
688
|
-
}
|
|
689
|
-
if (toolUseID) {
|
|
690
|
-
const onPostToolUseHook = toolUseCallbacks[toolUseID]?.onPostToolUseHook;
|
|
691
|
-
if (onPostToolUseHook) {
|
|
692
|
-
await onPostToolUseHook(
|
|
693
|
-
toolUseID,
|
|
694
|
-
input.tool_input,
|
|
695
|
-
input.tool_response
|
|
696
|
-
);
|
|
697
|
-
delete toolUseCallbacks[toolUseID];
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
return { continue: true };
|
|
702
|
-
};
|
|
703
|
-
|
|
704
|
-
// src/adapters/claude/conversion/tool-use-to-acp.ts
|
|
705
|
-
var SYSTEM_REMINDER = `
|
|
706
|
-
|
|
707
|
-
<system-reminder>
|
|
708
|
-
Whenever you read a file, you should consider whether it looks malicious. If it does, you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer high-level questions about the code behavior.
|
|
709
|
-
</system-reminder>`;
|
|
710
|
-
function replaceAndCalculateLocation(fileContent, edits) {
|
|
711
|
-
let currentContent = fileContent;
|
|
712
|
-
const randomHex = Array.from(crypto.getRandomValues(new Uint8Array(5))).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
713
|
-
const markerPrefix = `__REPLACE_MARKER_${randomHex}_`;
|
|
714
|
-
let markerCounter = 0;
|
|
715
|
-
const markers = [];
|
|
716
|
-
for (const edit of edits) {
|
|
717
|
-
if (edit.oldText === "") {
|
|
718
|
-
throw new Error(
|
|
719
|
-
`The provided \`old_string\` is empty.
|
|
720
|
-
|
|
721
|
-
No edits were applied.`
|
|
722
|
-
);
|
|
723
|
-
}
|
|
724
|
-
if (edit.replaceAll) {
|
|
725
|
-
const parts = [];
|
|
726
|
-
let lastIndex = 0;
|
|
727
|
-
let searchIndex = 0;
|
|
728
|
-
while (true) {
|
|
729
|
-
const index = currentContent.indexOf(edit.oldText, searchIndex);
|
|
730
|
-
if (index === -1) {
|
|
731
|
-
if (searchIndex === 0) {
|
|
732
|
-
throw new Error(
|
|
733
|
-
`The provided \`old_string\` does not appear in the file: "${edit.oldText}".
|
|
734
|
-
|
|
735
|
-
No edits were applied.`
|
|
736
|
-
);
|
|
737
|
-
}
|
|
738
|
-
break;
|
|
739
|
-
}
|
|
740
|
-
parts.push(currentContent.substring(lastIndex, index));
|
|
741
|
-
const marker = `${markerPrefix}${markerCounter++}__`;
|
|
742
|
-
markers.push(marker);
|
|
743
|
-
parts.push(marker + edit.newText);
|
|
744
|
-
lastIndex = index + edit.oldText.length;
|
|
745
|
-
searchIndex = lastIndex;
|
|
746
|
-
}
|
|
747
|
-
parts.push(currentContent.substring(lastIndex));
|
|
748
|
-
currentContent = parts.join("");
|
|
749
|
-
} else {
|
|
750
|
-
const index = currentContent.indexOf(edit.oldText);
|
|
751
|
-
if (index === -1) {
|
|
752
|
-
throw new Error(
|
|
753
|
-
`The provided \`old_string\` does not appear in the file: "${edit.oldText}".
|
|
754
|
-
|
|
755
|
-
No edits were applied.`
|
|
756
|
-
);
|
|
757
|
-
} else {
|
|
758
|
-
const marker = `${markerPrefix}${markerCounter++}__`;
|
|
759
|
-
markers.push(marker);
|
|
760
|
-
currentContent = currentContent.substring(0, index) + marker + edit.newText + currentContent.substring(index + edit.oldText.length);
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
const lineNumbers = [];
|
|
765
|
-
for (const marker of markers) {
|
|
766
|
-
const index = currentContent.indexOf(marker);
|
|
767
|
-
if (index !== -1) {
|
|
768
|
-
const lineNumber = Math.max(
|
|
769
|
-
0,
|
|
770
|
-
currentContent.substring(0, index).split(/\r\n|\r|\n/).length - 1
|
|
771
|
-
);
|
|
772
|
-
lineNumbers.push(lineNumber);
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
let finalContent = currentContent;
|
|
776
|
-
for (const marker of markers) {
|
|
777
|
-
finalContent = finalContent.replace(marker, "");
|
|
778
|
-
}
|
|
779
|
-
const uniqueLineNumbers = [...new Set(lineNumbers)].sort();
|
|
780
|
-
return { newContent: finalContent, lineNumbers: uniqueLineNumbers };
|
|
781
|
-
}
|
|
782
|
-
function toolInfoFromToolUse(toolUse, cachedFileContent, logger = new Logger({ debug: false, prefix: "[ClaudeTools]" })) {
|
|
783
|
-
const name = toolUse.name;
|
|
784
|
-
const input = toolUse.input;
|
|
785
|
-
switch (name) {
|
|
786
|
-
case "Task":
|
|
787
|
-
return {
|
|
788
|
-
title: input?.description ? String(input.description) : "Task",
|
|
789
|
-
kind: "think",
|
|
790
|
-
content: input?.prompt ? toolContent().text(String(input.prompt)).build() : []
|
|
791
|
-
};
|
|
792
|
-
case "NotebookRead":
|
|
793
|
-
return {
|
|
794
|
-
title: input?.notebook_path ? `Read Notebook ${String(input.notebook_path)}` : "Read Notebook",
|
|
795
|
-
kind: "read",
|
|
796
|
-
content: [],
|
|
797
|
-
locations: input?.notebook_path ? [{ path: String(input.notebook_path) }] : []
|
|
798
|
-
};
|
|
799
|
-
case "NotebookEdit":
|
|
800
|
-
return {
|
|
801
|
-
title: input?.notebook_path ? `Edit Notebook ${String(input.notebook_path)}` : "Edit Notebook",
|
|
802
|
-
kind: "edit",
|
|
803
|
-
content: input?.new_source ? toolContent().text(String(input.new_source)).build() : [],
|
|
804
|
-
locations: input?.notebook_path ? [{ path: String(input.notebook_path) }] : []
|
|
805
|
-
};
|
|
806
|
-
case "Bash":
|
|
807
|
-
return {
|
|
808
|
-
title: input?.description ? String(input.description) : "Execute command",
|
|
809
|
-
kind: "execute",
|
|
810
|
-
content: input?.command ? toolContent().text(String(input.command)).build() : []
|
|
811
|
-
};
|
|
812
|
-
case "BashOutput":
|
|
813
|
-
return {
|
|
814
|
-
title: "Tail Logs",
|
|
815
|
-
kind: "execute",
|
|
816
|
-
content: []
|
|
817
|
-
};
|
|
818
|
-
case "KillShell":
|
|
819
|
-
return {
|
|
820
|
-
title: "Kill Process",
|
|
821
|
-
kind: "execute",
|
|
822
|
-
content: []
|
|
823
|
-
};
|
|
824
|
-
case "Read": {
|
|
825
|
-
let limit = "";
|
|
826
|
-
const inputLimit = input?.limit;
|
|
827
|
-
const inputOffset = input?.offset ?? 0;
|
|
828
|
-
if (inputLimit) {
|
|
829
|
-
limit = ` (${inputOffset + 1} - ${inputOffset + inputLimit})`;
|
|
830
|
-
} else if (inputOffset) {
|
|
831
|
-
limit = ` (from line ${inputOffset + 1})`;
|
|
832
|
-
}
|
|
833
|
-
return {
|
|
834
|
-
title: `Read ${input?.file_path ? String(input.file_path) : "File"}${limit}`,
|
|
835
|
-
kind: "read",
|
|
836
|
-
locations: input?.file_path ? [
|
|
837
|
-
{
|
|
838
|
-
path: String(input.file_path),
|
|
839
|
-
line: inputOffset
|
|
840
|
-
}
|
|
841
|
-
] : [],
|
|
842
|
-
content: []
|
|
843
|
-
};
|
|
844
|
-
}
|
|
845
|
-
case "LS":
|
|
846
|
-
return {
|
|
847
|
-
title: `List the ${input?.path ? `\`${String(input.path)}\`` : "current"} directory's contents`,
|
|
848
|
-
kind: "search",
|
|
849
|
-
content: [],
|
|
850
|
-
locations: []
|
|
851
|
-
};
|
|
852
|
-
case "Edit": {
|
|
853
|
-
const path4 = input?.file_path ? String(input.file_path) : void 0;
|
|
854
|
-
let oldText = input?.old_string ? String(input.old_string) : null;
|
|
855
|
-
let newText = input?.new_string ? String(input.new_string) : "";
|
|
856
|
-
let affectedLines = [];
|
|
857
|
-
if (path4 && oldText) {
|
|
858
|
-
try {
|
|
859
|
-
const oldContent = cachedFileContent[path4] || "";
|
|
860
|
-
const newContent = replaceAndCalculateLocation(oldContent, [
|
|
861
|
-
{
|
|
862
|
-
oldText,
|
|
863
|
-
newText,
|
|
864
|
-
replaceAll: false
|
|
865
|
-
}
|
|
866
|
-
]);
|
|
867
|
-
oldText = oldContent;
|
|
868
|
-
newText = newContent.newContent;
|
|
869
|
-
affectedLines = newContent.lineNumbers;
|
|
870
|
-
} catch (e) {
|
|
871
|
-
logger.error("Failed to edit file", e);
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
return {
|
|
875
|
-
title: path4 ? `Edit \`${path4}\`` : "Edit",
|
|
876
|
-
kind: "edit",
|
|
877
|
-
content: input && path4 ? [
|
|
878
|
-
{
|
|
879
|
-
type: "diff",
|
|
880
|
-
path: path4,
|
|
881
|
-
oldText,
|
|
882
|
-
newText
|
|
883
|
-
}
|
|
884
|
-
] : [],
|
|
885
|
-
locations: path4 ? affectedLines.length > 0 ? affectedLines.map((line) => ({ line, path: path4 })) : [{ path: path4 }] : []
|
|
886
|
-
};
|
|
887
|
-
}
|
|
888
|
-
case "Write": {
|
|
889
|
-
let contentResult = [];
|
|
890
|
-
const filePath = input?.file_path ? String(input.file_path) : void 0;
|
|
891
|
-
const contentStr = input?.content ? String(input.content) : void 0;
|
|
892
|
-
if (filePath) {
|
|
893
|
-
contentResult = toolContent().diff(filePath, null, contentStr ?? "").build();
|
|
894
|
-
} else if (contentStr) {
|
|
895
|
-
contentResult = toolContent().text(contentStr).build();
|
|
896
|
-
}
|
|
897
|
-
return {
|
|
898
|
-
title: filePath ? `Write ${filePath}` : "Write",
|
|
899
|
-
kind: "edit",
|
|
900
|
-
content: contentResult,
|
|
901
|
-
locations: filePath ? [{ path: filePath }] : []
|
|
902
|
-
};
|
|
903
|
-
}
|
|
904
|
-
case "Glob": {
|
|
905
|
-
let label = "Find";
|
|
906
|
-
const pathStr = input?.path ? String(input.path) : void 0;
|
|
907
|
-
if (pathStr) {
|
|
908
|
-
label += ` "${pathStr}"`;
|
|
909
|
-
}
|
|
910
|
-
if (input?.pattern) {
|
|
911
|
-
label += ` "${String(input.pattern)}"`;
|
|
912
|
-
}
|
|
913
|
-
return {
|
|
914
|
-
title: label,
|
|
915
|
-
kind: "search",
|
|
916
|
-
content: [],
|
|
917
|
-
locations: pathStr ? [{ path: pathStr }] : []
|
|
918
|
-
};
|
|
919
|
-
}
|
|
920
|
-
case "Grep": {
|
|
921
|
-
let label = "grep";
|
|
922
|
-
if (input?.["-i"]) {
|
|
923
|
-
label += " -i";
|
|
924
|
-
}
|
|
925
|
-
if (input?.["-n"]) {
|
|
926
|
-
label += " -n";
|
|
927
|
-
}
|
|
928
|
-
if (input?.["-A"] !== void 0) {
|
|
929
|
-
label += ` -A ${input["-A"]}`;
|
|
930
|
-
}
|
|
931
|
-
if (input?.["-B"] !== void 0) {
|
|
932
|
-
label += ` -B ${input["-B"]}`;
|
|
933
|
-
}
|
|
934
|
-
if (input?.["-C"] !== void 0) {
|
|
935
|
-
label += ` -C ${input["-C"]}`;
|
|
936
|
-
}
|
|
937
|
-
if (input?.output_mode) {
|
|
938
|
-
switch (input.output_mode) {
|
|
939
|
-
case "FilesWithMatches":
|
|
940
|
-
label += " -l";
|
|
941
|
-
break;
|
|
942
|
-
case "Count":
|
|
943
|
-
label += " -c";
|
|
944
|
-
break;
|
|
945
|
-
default:
|
|
946
|
-
break;
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
if (input?.head_limit !== void 0) {
|
|
950
|
-
label += ` | head -${input.head_limit}`;
|
|
951
|
-
}
|
|
952
|
-
if (input?.glob) {
|
|
953
|
-
label += ` --include="${String(input.glob)}"`;
|
|
954
|
-
}
|
|
955
|
-
if (input?.type) {
|
|
956
|
-
label += ` --type=${String(input.type)}`;
|
|
957
|
-
}
|
|
958
|
-
if (input?.multiline) {
|
|
959
|
-
label += " -P";
|
|
960
|
-
}
|
|
961
|
-
label += ` "${input?.pattern ? String(input.pattern) : ""}"`;
|
|
962
|
-
if (input?.path) {
|
|
963
|
-
label += ` ${String(input.path)}`;
|
|
964
|
-
}
|
|
965
|
-
return {
|
|
966
|
-
title: label,
|
|
967
|
-
kind: "search",
|
|
968
|
-
content: []
|
|
969
|
-
};
|
|
970
|
-
}
|
|
971
|
-
case "WebFetch":
|
|
972
|
-
return {
|
|
973
|
-
title: "Fetch",
|
|
974
|
-
kind: "fetch",
|
|
975
|
-
content: input?.url ? [
|
|
976
|
-
{
|
|
977
|
-
type: "content",
|
|
978
|
-
content: resourceLink(String(input.url), String(input.url), {
|
|
979
|
-
description: input?.prompt ? String(input.prompt) : void 0
|
|
980
|
-
})
|
|
981
|
-
}
|
|
982
|
-
] : []
|
|
983
|
-
};
|
|
984
|
-
case "WebSearch": {
|
|
985
|
-
let label = `"${input?.query ? String(input.query) : ""}"`;
|
|
986
|
-
const allowedDomains = input?.allowed_domains;
|
|
987
|
-
const blockedDomains = input?.blocked_domains;
|
|
988
|
-
if (allowedDomains && allowedDomains.length > 0) {
|
|
989
|
-
label += ` (allowed: ${allowedDomains.join(", ")})`;
|
|
990
|
-
}
|
|
991
|
-
if (blockedDomains && blockedDomains.length > 0) {
|
|
992
|
-
label += ` (blocked: ${blockedDomains.join(", ")})`;
|
|
993
|
-
}
|
|
994
|
-
return {
|
|
995
|
-
title: label,
|
|
996
|
-
kind: "fetch",
|
|
997
|
-
content: []
|
|
998
|
-
};
|
|
999
|
-
}
|
|
1000
|
-
case "TodoWrite":
|
|
1001
|
-
return {
|
|
1002
|
-
title: Array.isArray(input?.todos) ? `Update TODOs: ${input.todos.map((todo) => todo.content).join(", ")}` : "Update TODOs",
|
|
1003
|
-
kind: "think",
|
|
1004
|
-
content: []
|
|
1005
|
-
};
|
|
1006
|
-
case "ExitPlanMode":
|
|
1007
|
-
return {
|
|
1008
|
-
title: "Ready to code?",
|
|
1009
|
-
kind: "switch_mode",
|
|
1010
|
-
content: input?.plan ? toolContent().text(String(input.plan)).build() : []
|
|
1011
|
-
};
|
|
1012
|
-
case "AskUserQuestion": {
|
|
1013
|
-
const questions = input?.questions;
|
|
1014
|
-
return {
|
|
1015
|
-
title: questions?.[0]?.question || "Question",
|
|
1016
|
-
kind: "other",
|
|
1017
|
-
content: questions ? toolContent().text(JSON.stringify(questions, null, 2)).build() : []
|
|
1018
|
-
};
|
|
1019
|
-
}
|
|
1020
|
-
case "Other": {
|
|
1021
|
-
let output;
|
|
1022
|
-
try {
|
|
1023
|
-
output = JSON.stringify(input, null, 2);
|
|
1024
|
-
} catch {
|
|
1025
|
-
output = typeof input === "string" ? input : "{}";
|
|
1026
|
-
}
|
|
1027
|
-
return {
|
|
1028
|
-
title: name || "Unknown Tool",
|
|
1029
|
-
kind: "other",
|
|
1030
|
-
content: toolContent().text(`\`\`\`json
|
|
1031
|
-
${output}\`\`\``).build()
|
|
1032
|
-
};
|
|
1033
|
-
}
|
|
1034
|
-
default:
|
|
1035
|
-
return {
|
|
1036
|
-
title: name || "Unknown Tool",
|
|
1037
|
-
kind: "other",
|
|
1038
|
-
content: []
|
|
1039
|
-
};
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
function toolUpdateFromToolResult(toolResult, toolUse) {
|
|
1043
|
-
switch (toolUse?.name) {
|
|
1044
|
-
case "Read":
|
|
1045
|
-
if (Array.isArray(toolResult.content) && toolResult.content.length > 0) {
|
|
1046
|
-
return {
|
|
1047
|
-
content: toolResult.content.map((item) => {
|
|
1048
|
-
const itemObj = item;
|
|
1049
|
-
if (itemObj.type === "text") {
|
|
1050
|
-
return {
|
|
1051
|
-
type: "content",
|
|
1052
|
-
content: text(
|
|
1053
|
-
markdownEscape(
|
|
1054
|
-
(itemObj.text ?? "").replace(SYSTEM_REMINDER, "")
|
|
1055
|
-
)
|
|
1056
|
-
)
|
|
1057
|
-
};
|
|
1058
|
-
}
|
|
1059
|
-
return {
|
|
1060
|
-
type: "content",
|
|
1061
|
-
content: item
|
|
1062
|
-
};
|
|
1063
|
-
})
|
|
1064
|
-
};
|
|
1065
|
-
} else if (typeof toolResult.content === "string" && toolResult.content.length > 0) {
|
|
1066
|
-
return {
|
|
1067
|
-
content: toolContent().text(
|
|
1068
|
-
markdownEscape(toolResult.content.replace(SYSTEM_REMINDER, ""))
|
|
1069
|
-
).build()
|
|
1070
|
-
};
|
|
1071
|
-
}
|
|
1072
|
-
return {};
|
|
1073
|
-
case "Bash": {
|
|
1074
|
-
return toAcpContentUpdate(
|
|
1075
|
-
toolResult.content,
|
|
1076
|
-
"is_error" in toolResult ? toolResult.is_error : false
|
|
1077
|
-
);
|
|
1078
|
-
}
|
|
1079
|
-
case "Edit":
|
|
1080
|
-
case "Write": {
|
|
1081
|
-
if ("is_error" in toolResult && toolResult.is_error && toolResult.content && toolResult.content.length > 0) {
|
|
1082
|
-
return toAcpContentUpdate(toolResult.content, true);
|
|
1083
|
-
}
|
|
1084
|
-
return {};
|
|
1085
|
-
}
|
|
1086
|
-
case "ExitPlanMode": {
|
|
1087
|
-
return { title: "Exited Plan Mode" };
|
|
1088
|
-
}
|
|
1089
|
-
case "AskUserQuestion": {
|
|
1090
|
-
const content = toolResult.content;
|
|
1091
|
-
if (Array.isArray(content) && content.length > 0) {
|
|
1092
|
-
const firstItem = content[0];
|
|
1093
|
-
if (typeof firstItem === "object" && firstItem !== null && "text" in firstItem) {
|
|
1094
|
-
return {
|
|
1095
|
-
title: "Answer received",
|
|
1096
|
-
content: toolContent().text(String(firstItem.text)).build()
|
|
1097
|
-
};
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
return { title: "Question answered" };
|
|
1101
|
-
}
|
|
1102
|
-
default: {
|
|
1103
|
-
return toAcpContentUpdate(
|
|
1104
|
-
toolResult.content,
|
|
1105
|
-
"is_error" in toolResult ? toolResult.is_error : false
|
|
1106
|
-
);
|
|
1107
|
-
}
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
function toAcpContentUpdate(content, isError = false) {
|
|
1111
|
-
if (Array.isArray(content) && content.length > 0) {
|
|
1112
|
-
return {
|
|
1113
|
-
content: content.map((item) => {
|
|
1114
|
-
const itemObj = item;
|
|
1115
|
-
if (isError && itemObj.type === "text") {
|
|
1116
|
-
return {
|
|
1117
|
-
type: "content",
|
|
1118
|
-
content: text(`\`\`\`
|
|
1119
|
-
${itemObj.text ?? ""}
|
|
1120
|
-
\`\`\``)
|
|
1121
|
-
};
|
|
1122
|
-
}
|
|
1123
|
-
return {
|
|
1124
|
-
type: "content",
|
|
1125
|
-
content: item
|
|
1126
|
-
};
|
|
1127
|
-
})
|
|
1128
|
-
};
|
|
1129
|
-
} else if (typeof content === "string" && content.length > 0) {
|
|
1130
|
-
return {
|
|
1131
|
-
content: toolContent().text(isError ? `\`\`\`
|
|
1132
|
-
${content}
|
|
1133
|
-
\`\`\`` : content).build()
|
|
1134
|
-
};
|
|
1135
|
-
}
|
|
1136
|
-
return {};
|
|
1137
|
-
}
|
|
1138
|
-
function planEntries(input) {
|
|
1139
|
-
return input.todos.map((input2) => ({
|
|
1140
|
-
content: input2.content,
|
|
1141
|
-
status: input2.status,
|
|
1142
|
-
priority: "medium"
|
|
1143
|
-
}));
|
|
1144
|
-
}
|
|
1145
|
-
function markdownEscape(text2) {
|
|
1146
|
-
let escapedText = "```";
|
|
1147
|
-
for (const [m] of text2.matchAll(/^```+/gm)) {
|
|
1148
|
-
while (m.length >= escapedText.length) {
|
|
1149
|
-
escapedText += "`";
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
return `${escapedText}
|
|
1153
|
-
${text2}${text2.endsWith("\n") ? "" : "\n"}${escapedText}`;
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
// src/adapters/claude/conversion/sdk-to-acp.ts
|
|
1157
|
-
function messageUpdateType(role) {
|
|
1158
|
-
return role === "assistant" ? "agent_message_chunk" : "user_message_chunk";
|
|
1159
|
-
}
|
|
1160
|
-
function toolMeta(toolName, toolResponse) {
|
|
1161
|
-
return toolResponse ? { claudeCode: { toolName, toolResponse } } : { claudeCode: { toolName } };
|
|
1162
|
-
}
|
|
1163
|
-
function handleTextChunk(chunk, role) {
|
|
1164
|
-
return {
|
|
1165
|
-
sessionUpdate: messageUpdateType(role),
|
|
1166
|
-
content: text(chunk.text)
|
|
1167
|
-
};
|
|
1168
|
-
}
|
|
1169
|
-
function handleImageChunk(chunk, role) {
|
|
1170
|
-
return {
|
|
1171
|
-
sessionUpdate: messageUpdateType(role),
|
|
1172
|
-
content: image(
|
|
1173
|
-
chunk.source.type === "base64" ? chunk.source.data ?? "" : "",
|
|
1174
|
-
chunk.source.type === "base64" ? chunk.source.media_type ?? "" : "",
|
|
1175
|
-
chunk.source.type === "url" ? chunk.source.url : void 0
|
|
1176
|
-
)
|
|
1177
|
-
};
|
|
1178
|
-
}
|
|
1179
|
-
function handleThinkingChunk(chunk) {
|
|
1180
|
-
return {
|
|
1181
|
-
sessionUpdate: "agent_thought_chunk",
|
|
1182
|
-
content: text(chunk.thinking)
|
|
1183
|
-
};
|
|
1184
|
-
}
|
|
1185
|
-
function handleToolUseChunk(chunk, ctx) {
|
|
1186
|
-
ctx.toolUseCache[chunk.id] = chunk;
|
|
1187
|
-
if (chunk.name === "TodoWrite") {
|
|
1188
|
-
const input = chunk.input;
|
|
1189
|
-
if (Array.isArray(input.todos)) {
|
|
1190
|
-
return {
|
|
1191
|
-
sessionUpdate: "plan",
|
|
1192
|
-
entries: planEntries(chunk.input)
|
|
1193
|
-
};
|
|
1194
|
-
}
|
|
1195
|
-
return null;
|
|
1196
|
-
}
|
|
1197
|
-
registerHookCallback(chunk.id, {
|
|
1198
|
-
onPostToolUseHook: async (toolUseId, _toolInput, toolResponse) => {
|
|
1199
|
-
const toolUse = ctx.toolUseCache[toolUseId];
|
|
1200
|
-
if (toolUse) {
|
|
1201
|
-
await ctx.client.sessionUpdate({
|
|
1202
|
-
sessionId: ctx.sessionId,
|
|
1203
|
-
update: {
|
|
1204
|
-
_meta: toolMeta(toolUse.name, toolResponse),
|
|
1205
|
-
toolCallId: toolUseId,
|
|
1206
|
-
sessionUpdate: "tool_call_update"
|
|
1207
|
-
}
|
|
1208
|
-
});
|
|
1209
|
-
} else {
|
|
1210
|
-
ctx.logger.error(
|
|
1211
|
-
`Got a tool response for tool use that wasn't tracked: ${toolUseId}`
|
|
1212
|
-
);
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
});
|
|
1216
|
-
let rawInput;
|
|
1217
|
-
try {
|
|
1218
|
-
rawInput = JSON.parse(JSON.stringify(chunk.input));
|
|
1219
|
-
} catch {
|
|
1220
|
-
}
|
|
1221
|
-
return {
|
|
1222
|
-
_meta: toolMeta(chunk.name),
|
|
1223
|
-
toolCallId: chunk.id,
|
|
1224
|
-
sessionUpdate: "tool_call",
|
|
1225
|
-
rawInput,
|
|
1226
|
-
status: "pending",
|
|
1227
|
-
...toolInfoFromToolUse(chunk, ctx.fileContentCache, ctx.logger)
|
|
1228
|
-
};
|
|
1229
|
-
}
|
|
1230
|
-
function handleToolResultChunk(chunk, ctx) {
|
|
1231
|
-
const toolUse = ctx.toolUseCache[chunk.tool_use_id];
|
|
1232
|
-
if (!toolUse) {
|
|
1233
|
-
ctx.logger.error(
|
|
1234
|
-
`Got a tool result for tool use that wasn't tracked: ${chunk.tool_use_id}`
|
|
1235
|
-
);
|
|
1236
|
-
return null;
|
|
1237
|
-
}
|
|
1238
|
-
if (toolUse.name === "TodoWrite") {
|
|
1239
|
-
return null;
|
|
1240
|
-
}
|
|
1241
|
-
return {
|
|
1242
|
-
_meta: toolMeta(toolUse.name),
|
|
1243
|
-
toolCallId: chunk.tool_use_id,
|
|
1244
|
-
sessionUpdate: "tool_call_update",
|
|
1245
|
-
status: chunk.is_error ? "failed" : "completed",
|
|
1246
|
-
...toolUpdateFromToolResult(
|
|
1247
|
-
chunk,
|
|
1248
|
-
toolUse
|
|
1249
|
-
)
|
|
1250
|
-
};
|
|
1251
|
-
}
|
|
1252
|
-
function processContentChunk(chunk, role, ctx) {
|
|
1253
|
-
switch (chunk.type) {
|
|
1254
|
-
case "text":
|
|
1255
|
-
case "text_delta":
|
|
1256
|
-
return handleTextChunk(chunk, role);
|
|
1257
|
-
case "image":
|
|
1258
|
-
return handleImageChunk(chunk, role);
|
|
1259
|
-
case "thinking":
|
|
1260
|
-
case "thinking_delta":
|
|
1261
|
-
return handleThinkingChunk(chunk);
|
|
1262
|
-
case "tool_use":
|
|
1263
|
-
case "server_tool_use":
|
|
1264
|
-
case "mcp_tool_use":
|
|
1265
|
-
return handleToolUseChunk(chunk, ctx);
|
|
1266
|
-
case "tool_result":
|
|
1267
|
-
case "tool_search_tool_result":
|
|
1268
|
-
case "web_fetch_tool_result":
|
|
1269
|
-
case "web_search_tool_result":
|
|
1270
|
-
case "code_execution_tool_result":
|
|
1271
|
-
case "bash_code_execution_tool_result":
|
|
1272
|
-
case "text_editor_code_execution_tool_result":
|
|
1273
|
-
case "mcp_tool_result":
|
|
1274
|
-
return handleToolResultChunk(
|
|
1275
|
-
chunk,
|
|
1276
|
-
ctx
|
|
1277
|
-
);
|
|
1278
|
-
case "document":
|
|
1279
|
-
case "search_result":
|
|
1280
|
-
case "redacted_thinking":
|
|
1281
|
-
case "input_json_delta":
|
|
1282
|
-
case "citations_delta":
|
|
1283
|
-
case "signature_delta":
|
|
1284
|
-
case "container_upload":
|
|
1285
|
-
return null;
|
|
1286
|
-
default:
|
|
1287
|
-
unreachable(chunk, ctx.logger);
|
|
1288
|
-
return null;
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
function toAcpNotifications(content, role, sessionId, toolUseCache, fileContentCache, client, logger) {
|
|
1292
|
-
if (typeof content === "string") {
|
|
1293
|
-
return [
|
|
1294
|
-
{
|
|
1295
|
-
sessionId,
|
|
1296
|
-
update: {
|
|
1297
|
-
sessionUpdate: messageUpdateType(role),
|
|
1298
|
-
content: text(content)
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
];
|
|
1302
|
-
}
|
|
1303
|
-
const ctx = {
|
|
1304
|
-
sessionId,
|
|
1305
|
-
toolUseCache,
|
|
1306
|
-
fileContentCache,
|
|
1307
|
-
client,
|
|
1308
|
-
logger
|
|
1309
|
-
};
|
|
1310
|
-
const output = [];
|
|
1311
|
-
for (const chunk of content) {
|
|
1312
|
-
const update = processContentChunk(chunk, role, ctx);
|
|
1313
|
-
if (update) {
|
|
1314
|
-
output.push({ sessionId, update });
|
|
1315
|
-
}
|
|
1316
|
-
}
|
|
1317
|
-
return output;
|
|
1318
|
-
}
|
|
1319
|
-
function streamEventToAcpNotifications(message, sessionId, toolUseCache, fileContentCache, client, logger) {
|
|
1320
|
-
const event = message.event;
|
|
1321
|
-
switch (event.type) {
|
|
1322
|
-
case "content_block_start":
|
|
1323
|
-
return toAcpNotifications(
|
|
1324
|
-
[event.content_block],
|
|
1325
|
-
"assistant",
|
|
1326
|
-
sessionId,
|
|
1327
|
-
toolUseCache,
|
|
1328
|
-
fileContentCache,
|
|
1329
|
-
client,
|
|
1330
|
-
logger
|
|
1331
|
-
);
|
|
1332
|
-
case "content_block_delta":
|
|
1333
|
-
return toAcpNotifications(
|
|
1334
|
-
[event.delta],
|
|
1335
|
-
"assistant",
|
|
1336
|
-
sessionId,
|
|
1337
|
-
toolUseCache,
|
|
1338
|
-
fileContentCache,
|
|
1339
|
-
client,
|
|
1340
|
-
logger
|
|
1341
|
-
);
|
|
1342
|
-
case "message_start":
|
|
1343
|
-
case "message_delta":
|
|
1344
|
-
case "message_stop":
|
|
1345
|
-
case "content_block_stop":
|
|
1346
|
-
return [];
|
|
1347
|
-
default:
|
|
1348
|
-
unreachable(event, logger);
|
|
1349
|
-
return [];
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
async function handleSystemMessage(message, context) {
|
|
1353
|
-
const { session, sessionId, client, logger } = context;
|
|
1354
|
-
switch (message.subtype) {
|
|
1355
|
-
case "init":
|
|
1356
|
-
if (message.session_id && session && !session.sessionId) {
|
|
1357
|
-
session.sessionId = message.session_id;
|
|
1358
|
-
if (session.taskRunId) {
|
|
1359
|
-
await client.extNotification("_posthog/sdk_session", {
|
|
1360
|
-
taskRunId: session.taskRunId,
|
|
1361
|
-
sessionId: message.session_id,
|
|
1362
|
-
adapter: "claude"
|
|
1363
|
-
});
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
break;
|
|
1367
|
-
case "compact_boundary":
|
|
1368
|
-
await client.extNotification("_posthog/compact_boundary", {
|
|
1369
|
-
sessionId,
|
|
1370
|
-
trigger: message.compact_metadata.trigger,
|
|
1371
|
-
preTokens: message.compact_metadata.pre_tokens
|
|
1372
|
-
});
|
|
1373
|
-
break;
|
|
1374
|
-
case "hook_response":
|
|
1375
|
-
logger.info("Hook response received", {
|
|
1376
|
-
hookName: message.hook_name,
|
|
1377
|
-
hookEvent: message.hook_event
|
|
1378
|
-
});
|
|
1379
|
-
break;
|
|
1380
|
-
case "status":
|
|
1381
|
-
if (message.status === "compacting") {
|
|
1382
|
-
logger.info("Session compacting started", { sessionId });
|
|
1383
|
-
await client.extNotification("_posthog/status", {
|
|
1384
|
-
sessionId,
|
|
1385
|
-
status: "compacting"
|
|
1386
|
-
});
|
|
1387
|
-
}
|
|
1388
|
-
break;
|
|
1389
|
-
case "task_notification": {
|
|
1390
|
-
logger.info("Task notification received", {
|
|
1391
|
-
sessionId,
|
|
1392
|
-
taskId: message.task_id,
|
|
1393
|
-
status: message.status,
|
|
1394
|
-
summary: message.summary
|
|
1395
|
-
});
|
|
1396
|
-
await client.extNotification("_posthog/task_notification", {
|
|
1397
|
-
sessionId,
|
|
1398
|
-
taskId: message.task_id,
|
|
1399
|
-
status: message.status,
|
|
1400
|
-
summary: message.summary,
|
|
1401
|
-
outputFile: message.output_file
|
|
1402
|
-
});
|
|
1403
|
-
break;
|
|
1404
|
-
}
|
|
1405
|
-
default:
|
|
1406
|
-
break;
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
function handleResultMessage(message, context) {
|
|
1410
|
-
const { session } = context;
|
|
1411
|
-
if (session.cancelled) {
|
|
1412
|
-
return {
|
|
1413
|
-
shouldStop: true,
|
|
1414
|
-
stopReason: "cancelled"
|
|
1415
|
-
};
|
|
1416
|
-
}
|
|
1417
|
-
switch (message.subtype) {
|
|
1418
|
-
case "success": {
|
|
1419
|
-
if (message.result.includes("Please run /login")) {
|
|
1420
|
-
return {
|
|
1421
|
-
shouldStop: true,
|
|
1422
|
-
error: RequestError.authRequired()
|
|
1423
|
-
};
|
|
1424
|
-
}
|
|
1425
|
-
if (message.is_error) {
|
|
1426
|
-
return {
|
|
1427
|
-
shouldStop: true,
|
|
1428
|
-
error: RequestError.internalError(void 0, message.result)
|
|
1429
|
-
};
|
|
1430
|
-
}
|
|
1431
|
-
return { shouldStop: true, stopReason: "end_turn" };
|
|
1432
|
-
}
|
|
1433
|
-
case "error_during_execution":
|
|
1434
|
-
if (message.is_error) {
|
|
1435
|
-
return {
|
|
1436
|
-
shouldStop: true,
|
|
1437
|
-
error: RequestError.internalError(
|
|
1438
|
-
void 0,
|
|
1439
|
-
message.errors.join(", ") || message.subtype
|
|
1440
|
-
)
|
|
1441
|
-
};
|
|
1442
|
-
}
|
|
1443
|
-
return { shouldStop: true, stopReason: "end_turn" };
|
|
1444
|
-
case "error_max_budget_usd":
|
|
1445
|
-
case "error_max_turns":
|
|
1446
|
-
case "error_max_structured_output_retries":
|
|
1447
|
-
if (message.is_error) {
|
|
1448
|
-
return {
|
|
1449
|
-
shouldStop: true,
|
|
1450
|
-
error: RequestError.internalError(
|
|
1451
|
-
void 0,
|
|
1452
|
-
message.errors.join(", ") || message.subtype
|
|
1453
|
-
)
|
|
1454
|
-
};
|
|
1455
|
-
}
|
|
1456
|
-
return { shouldStop: true, stopReason: "max_turn_requests" };
|
|
1457
|
-
default:
|
|
1458
|
-
return { shouldStop: false };
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
async function handleStreamEvent(message, context) {
|
|
1462
|
-
const { sessionId, client, toolUseCache, fileContentCache, logger } = context;
|
|
1463
|
-
for (const notification of streamEventToAcpNotifications(
|
|
1464
|
-
message,
|
|
1465
|
-
sessionId,
|
|
1466
|
-
toolUseCache,
|
|
1467
|
-
fileContentCache,
|
|
1468
|
-
client,
|
|
1469
|
-
logger
|
|
1470
|
-
)) {
|
|
1471
|
-
await client.sessionUpdate(notification);
|
|
1472
|
-
context.session.notificationHistory.push(notification);
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
function hasLocalCommandStdout(content) {
|
|
1476
|
-
return typeof content === "string" && content.includes("<local-command-stdout>");
|
|
1477
|
-
}
|
|
1478
|
-
function hasLocalCommandStderr(content) {
|
|
1479
|
-
return typeof content === "string" && content.includes("<local-command-stderr>");
|
|
1480
|
-
}
|
|
1481
|
-
function isSimpleUserMessage(message) {
|
|
1482
|
-
return message.type === "user" && (typeof message.message.content === "string" || Array.isArray(message.message.content) && message.message.content.length === 1 && message.message.content[0].type === "text");
|
|
1483
|
-
}
|
|
1484
|
-
function isLoginRequiredMessage(message) {
|
|
1485
|
-
return message.type === "assistant" && message.message.model === "<synthetic>" && Array.isArray(message.message.content) && message.message.content.length === 1 && message.message.content[0].type === "text" && message.message.content[0].text?.includes("Please run /login") === true;
|
|
1486
|
-
}
|
|
1487
|
-
function shouldSkipUserAssistantMessage(message) {
|
|
1488
|
-
return hasLocalCommandStdout(message.message.content) || hasLocalCommandStderr(message.message.content) || isSimpleUserMessage(message) || isLoginRequiredMessage(message);
|
|
1489
|
-
}
|
|
1490
|
-
function logSpecialMessages(message, logger) {
|
|
1491
|
-
const content = message.message.content;
|
|
1492
|
-
if (hasLocalCommandStdout(content) && typeof content === "string") {
|
|
1493
|
-
logger.info(content);
|
|
1494
|
-
}
|
|
1495
|
-
if (hasLocalCommandStderr(content) && typeof content === "string") {
|
|
1496
|
-
logger.error(content);
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
function filterMessageContent(content) {
|
|
1500
|
-
if (!Array.isArray(content)) {
|
|
1501
|
-
return content;
|
|
1502
|
-
}
|
|
1503
|
-
return content.filter(
|
|
1504
|
-
(block) => block.type !== "text" && block.type !== "thinking"
|
|
1505
|
-
);
|
|
1506
|
-
}
|
|
1507
|
-
async function handleUserAssistantMessage(message, context) {
|
|
1508
|
-
const { session, sessionId, client, toolUseCache, fileContentCache, logger } = context;
|
|
1509
|
-
if (session.cancelled) {
|
|
1510
|
-
return {};
|
|
1511
|
-
}
|
|
1512
|
-
if (shouldSkipUserAssistantMessage(message)) {
|
|
1513
|
-
logSpecialMessages(message, logger);
|
|
1514
|
-
if (isLoginRequiredMessage(message)) {
|
|
1515
|
-
return { shouldStop: true, error: RequestError.authRequired() };
|
|
1516
|
-
}
|
|
1517
|
-
return {};
|
|
1518
|
-
}
|
|
1519
|
-
const content = message.message.content;
|
|
1520
|
-
const contentToProcess = filterMessageContent(content);
|
|
1521
|
-
for (const notification of toAcpNotifications(
|
|
1522
|
-
contentToProcess,
|
|
1523
|
-
message.message.role,
|
|
1524
|
-
sessionId,
|
|
1525
|
-
toolUseCache,
|
|
1526
|
-
fileContentCache,
|
|
1527
|
-
client,
|
|
1528
|
-
logger
|
|
1529
|
-
)) {
|
|
1530
|
-
await client.sessionUpdate(notification);
|
|
1531
|
-
session.notificationHistory.push(notification);
|
|
1532
|
-
}
|
|
1533
|
-
return {};
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
// src/adapters/claude/mcp/tool-metadata.ts
|
|
1537
|
-
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
1538
|
-
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
1539
|
-
var mcpToolMetadataCache = /* @__PURE__ */ new Map();
|
|
1540
|
-
function buildToolKey(serverName, toolName) {
|
|
1541
|
-
return `mcp__${serverName}__${toolName}`;
|
|
1542
|
-
}
|
|
1543
|
-
function isHttpMcpServer(config) {
|
|
1544
|
-
return config.type === "http" && typeof config.url === "string";
|
|
1545
|
-
}
|
|
1546
|
-
async function fetchToolsFromHttpServer(_serverName, config) {
|
|
1547
|
-
const transport = new StreamableHTTPClientTransport(new URL(config.url), {
|
|
1548
|
-
requestInit: {
|
|
1549
|
-
headers: config.headers || {}
|
|
1550
|
-
}
|
|
1551
|
-
});
|
|
1552
|
-
const client = new Client({
|
|
1553
|
-
name: "twig-metadata-fetcher",
|
|
1554
|
-
version: "1.0.0"
|
|
1555
|
-
});
|
|
1556
|
-
try {
|
|
1557
|
-
await client.connect(transport);
|
|
1558
|
-
const result = await client.listTools();
|
|
1559
|
-
return result.tools;
|
|
1560
|
-
} finally {
|
|
1561
|
-
await client.close().catch(() => {
|
|
1562
|
-
});
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
function extractToolMetadata(tool) {
|
|
1566
|
-
return {
|
|
1567
|
-
readOnly: tool.annotations?.readOnlyHint === true
|
|
1568
|
-
};
|
|
1569
|
-
}
|
|
1570
|
-
async function fetchMcpToolMetadata(mcpServers, logger = new Logger({ debug: false, prefix: "[McpToolMetadata]" })) {
|
|
1571
|
-
const fetchPromises = [];
|
|
1572
|
-
for (const [serverName, config] of Object.entries(mcpServers)) {
|
|
1573
|
-
if (!isHttpMcpServer(config)) {
|
|
1574
|
-
continue;
|
|
1575
|
-
}
|
|
1576
|
-
const fetchPromise = fetchToolsFromHttpServer(serverName, config).then((tools) => {
|
|
1577
|
-
const toolCount = tools.length;
|
|
1578
|
-
const readOnlyCount = tools.filter(
|
|
1579
|
-
(t) => t.annotations?.readOnlyHint === true
|
|
1580
|
-
).length;
|
|
1581
|
-
for (const tool of tools) {
|
|
1582
|
-
const toolKey = buildToolKey(serverName, tool.name);
|
|
1583
|
-
mcpToolMetadataCache.set(toolKey, extractToolMetadata(tool));
|
|
1584
|
-
}
|
|
1585
|
-
logger.info("Fetched MCP tool metadata", {
|
|
1586
|
-
serverName,
|
|
1587
|
-
toolCount,
|
|
1588
|
-
readOnlyCount
|
|
1589
|
-
});
|
|
1590
|
-
}).catch((error) => {
|
|
1591
|
-
logger.error("Failed to fetch MCP tool metadata", {
|
|
1592
|
-
serverName,
|
|
1593
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1594
|
-
});
|
|
1595
|
-
});
|
|
1596
|
-
fetchPromises.push(fetchPromise);
|
|
1597
|
-
}
|
|
1598
|
-
await Promise.all(fetchPromises);
|
|
1599
|
-
}
|
|
1600
|
-
function isMcpToolReadOnly(toolName) {
|
|
1601
|
-
const metadata = mcpToolMetadataCache.get(toolName);
|
|
1602
|
-
return metadata?.readOnly === true;
|
|
1603
|
-
}
|
|
1604
|
-
|
|
1605
|
-
// src/adapters/claude/plan/utils.ts
|
|
1606
|
-
import * as os from "os";
|
|
1607
|
-
import * as path from "path";
|
|
1608
|
-
function getClaudeConfigDir() {
|
|
1609
|
-
return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
|
|
1610
|
-
}
|
|
1611
|
-
function getClaudePlansDir() {
|
|
1612
|
-
return path.join(getClaudeConfigDir(), "plans");
|
|
1613
|
-
}
|
|
1614
|
-
function isClaudePlanFilePath(filePath) {
|
|
1615
|
-
if (!filePath) return false;
|
|
1616
|
-
const resolved = path.resolve(filePath);
|
|
1617
|
-
const plansDir = path.resolve(getClaudePlansDir());
|
|
1618
|
-
return resolved === plansDir || resolved.startsWith(plansDir + path.sep);
|
|
1619
|
-
}
|
|
1620
|
-
function isPlanReady(plan) {
|
|
1621
|
-
if (!plan) return false;
|
|
1622
|
-
const trimmed = plan.trim();
|
|
1623
|
-
if (trimmed.length < 40) return false;
|
|
1624
|
-
return /(^|\n)#{1,6}\s+\S/.test(trimmed);
|
|
1625
|
-
}
|
|
1626
|
-
function getLatestAssistantText(notifications) {
|
|
1627
|
-
const chunks = [];
|
|
1628
|
-
let started = false;
|
|
1629
|
-
for (let i = notifications.length - 1; i >= 0; i -= 1) {
|
|
1630
|
-
const update = notifications[i]?.update;
|
|
1631
|
-
if (!update) continue;
|
|
1632
|
-
if (update.sessionUpdate === "agent_message_chunk") {
|
|
1633
|
-
started = true;
|
|
1634
|
-
const content = update.content;
|
|
1635
|
-
if (content?.type === "text" && content.text) {
|
|
1636
|
-
chunks.push(content.text);
|
|
1637
|
-
}
|
|
1638
|
-
continue;
|
|
1639
|
-
}
|
|
1640
|
-
if (started) {
|
|
1641
|
-
break;
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
if (chunks.length === 0) return null;
|
|
1645
|
-
return chunks.reverse().join("");
|
|
1646
|
-
}
|
|
1647
|
-
|
|
1648
|
-
// src/adapters/claude/questions/utils.ts
|
|
1649
|
-
import { z } from "zod";
|
|
1650
|
-
var OPTION_PREFIX = "option_";
|
|
1651
|
-
var QuestionOptionSchema = z.object({
|
|
1652
|
-
label: z.string(),
|
|
1653
|
-
description: z.string().optional()
|
|
1654
|
-
});
|
|
1655
|
-
var QuestionItemSchema = z.object({
|
|
1656
|
-
question: z.string(),
|
|
1657
|
-
header: z.string().optional(),
|
|
1658
|
-
options: z.array(QuestionOptionSchema),
|
|
1659
|
-
multiSelect: z.boolean().optional(),
|
|
1660
|
-
completed: z.boolean().optional()
|
|
1661
|
-
});
|
|
1662
|
-
var QuestionMetaSchema = z.object({
|
|
1663
|
-
questions: z.array(QuestionItemSchema)
|
|
1664
|
-
});
|
|
1665
|
-
function normalizeAskUserQuestionInput(input) {
|
|
1666
|
-
if (input.questions && input.questions.length > 0) {
|
|
1667
|
-
return input.questions;
|
|
1668
|
-
}
|
|
1669
|
-
if (input.question) {
|
|
1670
|
-
return [
|
|
1671
|
-
{
|
|
1672
|
-
question: input.question,
|
|
1673
|
-
header: input.header,
|
|
1674
|
-
options: input.options || [],
|
|
1675
|
-
multiSelect: input.multiSelect
|
|
1676
|
-
}
|
|
1677
|
-
];
|
|
1678
|
-
}
|
|
1679
|
-
return null;
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
// src/execution-mode.ts
|
|
1683
|
-
var MODES = [
|
|
1684
|
-
{
|
|
1685
|
-
id: "default",
|
|
1686
|
-
name: "Always Ask",
|
|
1687
|
-
description: "Prompts for permission on first use of each tool"
|
|
1688
|
-
},
|
|
1689
|
-
{
|
|
1690
|
-
id: "acceptEdits",
|
|
1691
|
-
name: "Accept Edits",
|
|
1692
|
-
description: "Automatically accepts file edit permissions for the session"
|
|
1693
|
-
},
|
|
1694
|
-
{
|
|
1695
|
-
id: "plan",
|
|
1696
|
-
name: "Plan Mode",
|
|
1697
|
-
description: "Claude can analyze but not modify files or execute commands"
|
|
1698
|
-
},
|
|
1699
|
-
{
|
|
1700
|
-
id: "bypassPermissions",
|
|
1701
|
-
name: "Bypass Permissions",
|
|
1702
|
-
description: "Skips all permission prompts"
|
|
1703
|
-
}
|
|
1704
|
-
];
|
|
1705
|
-
var TWIG_EXECUTION_MODES = [
|
|
1706
|
-
"default",
|
|
1707
|
-
"acceptEdits",
|
|
1708
|
-
"plan",
|
|
1709
|
-
"bypassPermissions"
|
|
1710
|
-
];
|
|
1711
|
-
function getAvailableModes() {
|
|
1712
|
-
return IS_ROOT ? MODES.filter((m) => m.id !== "bypassPermissions") : MODES;
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
|
-
// src/adapters/claude/tools.ts
|
|
1716
|
-
var READ_TOOLS = /* @__PURE__ */ new Set(["Read", "NotebookRead"]);
|
|
1717
|
-
var WRITE_TOOLS = /* @__PURE__ */ new Set([
|
|
1718
|
-
"Edit",
|
|
1719
|
-
"Write",
|
|
1720
|
-
"NotebookEdit"
|
|
1721
|
-
]);
|
|
1722
|
-
var BASH_TOOLS = /* @__PURE__ */ new Set([
|
|
1723
|
-
"Bash",
|
|
1724
|
-
"BashOutput",
|
|
1725
|
-
"KillShell"
|
|
1726
|
-
]);
|
|
1727
|
-
var SEARCH_TOOLS = /* @__PURE__ */ new Set(["Glob", "Grep", "LS"]);
|
|
1728
|
-
var WEB_TOOLS = /* @__PURE__ */ new Set(["WebSearch", "WebFetch"]);
|
|
1729
|
-
var AGENT_TOOLS = /* @__PURE__ */ new Set(["Task", "TodoWrite"]);
|
|
1730
|
-
var BASE_ALLOWED_TOOLS = [
|
|
1731
|
-
...READ_TOOLS,
|
|
1732
|
-
...SEARCH_TOOLS,
|
|
1733
|
-
...WEB_TOOLS,
|
|
1734
|
-
...AGENT_TOOLS
|
|
1735
|
-
];
|
|
1736
|
-
var AUTO_ALLOWED_TOOLS = {
|
|
1737
|
-
default: new Set(BASE_ALLOWED_TOOLS),
|
|
1738
|
-
acceptEdits: /* @__PURE__ */ new Set([...BASE_ALLOWED_TOOLS, ...WRITE_TOOLS]),
|
|
1739
|
-
plan: new Set(BASE_ALLOWED_TOOLS)
|
|
1740
|
-
};
|
|
1741
|
-
function isToolAllowedForMode(toolName, mode) {
|
|
1742
|
-
if (mode === "bypassPermissions") {
|
|
1743
|
-
return true;
|
|
1744
|
-
}
|
|
1745
|
-
if (AUTO_ALLOWED_TOOLS[mode]?.has(toolName) === true) {
|
|
1746
|
-
return true;
|
|
1747
|
-
}
|
|
1748
|
-
if (isMcpToolReadOnly(toolName)) {
|
|
1749
|
-
return true;
|
|
1750
|
-
}
|
|
1751
|
-
return false;
|
|
1752
|
-
}
|
|
1753
|
-
|
|
1754
|
-
// src/adapters/claude/permissions/permission-options.ts
|
|
1755
|
-
function permissionOptions(allowAlwaysLabel) {
|
|
1756
|
-
return [
|
|
1757
|
-
{ kind: "allow_once", name: "Yes", optionId: "allow" },
|
|
1758
|
-
{ kind: "allow_always", name: allowAlwaysLabel, optionId: "allow_always" },
|
|
1759
|
-
{
|
|
1760
|
-
kind: "reject_once",
|
|
1761
|
-
name: "No, and tell the agent what to do differently",
|
|
1762
|
-
optionId: "reject",
|
|
1763
|
-
_meta: { customInput: true }
|
|
1764
|
-
}
|
|
1765
|
-
];
|
|
1766
|
-
}
|
|
1767
|
-
function buildPermissionOptions(toolName, toolInput, cwd) {
|
|
1768
|
-
if (BASH_TOOLS.has(toolName)) {
|
|
1769
|
-
const command = toolInput?.command;
|
|
1770
|
-
const cmdName = command?.split(/\s+/)[0] ?? "this command";
|
|
1771
|
-
const cwdLabel = cwd ? ` in ${cwd}` : "";
|
|
1772
|
-
return permissionOptions(
|
|
1773
|
-
`Yes, and don't ask again for \`${cmdName}\` commands${cwdLabel}`
|
|
1774
|
-
);
|
|
1775
|
-
}
|
|
1776
|
-
if (toolName === "BashOutput") {
|
|
1777
|
-
return permissionOptions("Yes, allow all background process reads");
|
|
1778
|
-
}
|
|
1779
|
-
if (toolName === "KillShell") {
|
|
1780
|
-
return permissionOptions("Yes, allow killing processes");
|
|
1781
|
-
}
|
|
1782
|
-
if (WRITE_TOOLS.has(toolName)) {
|
|
1783
|
-
return permissionOptions("Yes, allow all edits during this session");
|
|
1784
|
-
}
|
|
1785
|
-
if (READ_TOOLS.has(toolName)) {
|
|
1786
|
-
return permissionOptions("Yes, allow all reads during this session");
|
|
1787
|
-
}
|
|
1788
|
-
if (SEARCH_TOOLS.has(toolName)) {
|
|
1789
|
-
return permissionOptions("Yes, allow all searches during this session");
|
|
1790
|
-
}
|
|
1791
|
-
if (toolName === "WebFetch") {
|
|
1792
|
-
const url = toolInput?.url;
|
|
1793
|
-
let domain = "";
|
|
1794
|
-
try {
|
|
1795
|
-
domain = url ? new URL(url).hostname : "";
|
|
1796
|
-
} catch {
|
|
1797
|
-
}
|
|
1798
|
-
return permissionOptions(
|
|
1799
|
-
domain ? `Yes, allow all fetches from ${domain}` : "Yes, allow all fetches"
|
|
1800
|
-
);
|
|
1801
|
-
}
|
|
1802
|
-
if (toolName === "WebSearch") {
|
|
1803
|
-
return permissionOptions("Yes, allow all web searches");
|
|
1804
|
-
}
|
|
1805
|
-
if (toolName === "Task") {
|
|
1806
|
-
return permissionOptions("Yes, allow all sub-tasks");
|
|
1807
|
-
}
|
|
1808
|
-
if (toolName === "TodoWrite") {
|
|
1809
|
-
return permissionOptions("Yes, allow all todo updates");
|
|
1810
|
-
}
|
|
1811
|
-
return permissionOptions("Yes, always allow");
|
|
1812
|
-
}
|
|
1813
|
-
function buildExitPlanModePermissionOptions() {
|
|
1814
|
-
return [
|
|
1815
|
-
{
|
|
1816
|
-
kind: "allow_always",
|
|
1817
|
-
name: "Yes, and auto-accept edits",
|
|
1818
|
-
optionId: "acceptEdits"
|
|
1819
|
-
},
|
|
1820
|
-
{
|
|
1821
|
-
kind: "allow_once",
|
|
1822
|
-
name: "Yes, and manually approve edits",
|
|
1823
|
-
optionId: "default"
|
|
1824
|
-
},
|
|
1825
|
-
{
|
|
1826
|
-
kind: "reject_once",
|
|
1827
|
-
name: "No, keep planning",
|
|
1828
|
-
optionId: "plan"
|
|
1829
|
-
}
|
|
1830
|
-
];
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
// src/adapters/claude/permissions/permission-handlers.ts
|
|
1834
|
-
async function emitToolDenial(context, message) {
|
|
1835
|
-
context.logger.info(`[canUseTool] Tool denied: ${context.toolName}`, {
|
|
1836
|
-
message
|
|
1837
|
-
});
|
|
1838
|
-
await context.client.sessionUpdate({
|
|
1839
|
-
sessionId: context.sessionId,
|
|
1840
|
-
update: {
|
|
1841
|
-
sessionUpdate: "tool_call_update",
|
|
1842
|
-
toolCallId: context.toolUseID,
|
|
1843
|
-
status: "failed",
|
|
1844
|
-
content: [{ type: "content", content: text(message) }]
|
|
1845
|
-
}
|
|
1846
|
-
});
|
|
1847
|
-
}
|
|
1848
|
-
function getPlanFromFile(session, fileContentCache) {
|
|
1849
|
-
return session.lastPlanContent || (session.lastPlanFilePath ? fileContentCache[session.lastPlanFilePath] : void 0);
|
|
1850
|
-
}
|
|
1851
|
-
function ensurePlanInInput(toolInput, fallbackPlan) {
|
|
1852
|
-
const hasPlan = typeof toolInput?.plan === "string";
|
|
1853
|
-
if (hasPlan || !fallbackPlan) {
|
|
1854
|
-
return toolInput;
|
|
1855
|
-
}
|
|
1856
|
-
return { ...toolInput, plan: fallbackPlan };
|
|
1857
|
-
}
|
|
1858
|
-
function extractPlanText(input) {
|
|
1859
|
-
const plan = input?.plan;
|
|
1860
|
-
return typeof plan === "string" ? plan : void 0;
|
|
1861
|
-
}
|
|
1862
|
-
async function createPlanValidationError(message, context) {
|
|
1863
|
-
await emitToolDenial(context, message);
|
|
1864
|
-
return { behavior: "deny", message, interrupt: false };
|
|
1865
|
-
}
|
|
1866
|
-
async function validatePlanContent(planText, context) {
|
|
1867
|
-
if (!planText) {
|
|
1868
|
-
const message = `Plan not ready. Provide the full markdown plan in ExitPlanMode or write it to ${getClaudePlansDir()} before requesting approval.`;
|
|
1869
|
-
return {
|
|
1870
|
-
valid: false,
|
|
1871
|
-
error: await createPlanValidationError(message, context)
|
|
1872
|
-
};
|
|
1873
|
-
}
|
|
1874
|
-
if (!isPlanReady(planText)) {
|
|
1875
|
-
const message = "Plan not ready. Provide the full markdown plan in ExitPlanMode before requesting approval.";
|
|
1876
|
-
return {
|
|
1877
|
-
valid: false,
|
|
1878
|
-
error: await createPlanValidationError(message, context)
|
|
1879
|
-
};
|
|
1880
|
-
}
|
|
1881
|
-
return { valid: true };
|
|
1882
|
-
}
|
|
1883
|
-
async function requestPlanApproval(context, updatedInput) {
|
|
1884
|
-
const { client, sessionId, toolUseID, fileContentCache } = context;
|
|
1885
|
-
const toolInfo = toolInfoFromToolUse(
|
|
1886
|
-
{ name: context.toolName, input: updatedInput },
|
|
1887
|
-
fileContentCache,
|
|
1888
|
-
context.logger
|
|
1889
|
-
);
|
|
1890
|
-
return await client.requestPermission({
|
|
1891
|
-
options: buildExitPlanModePermissionOptions(),
|
|
1892
|
-
sessionId,
|
|
1893
|
-
toolCall: {
|
|
1894
|
-
toolCallId: toolUseID,
|
|
1895
|
-
title: toolInfo.title,
|
|
1896
|
-
kind: toolInfo.kind,
|
|
1897
|
-
content: toolInfo.content,
|
|
1898
|
-
locations: toolInfo.locations,
|
|
1899
|
-
rawInput: { ...updatedInput, toolName: context.toolName }
|
|
1900
|
-
}
|
|
1901
|
-
});
|
|
1902
|
-
}
|
|
1903
|
-
async function applyPlanApproval(response, context, updatedInput) {
|
|
1904
|
-
const { session } = context;
|
|
1905
|
-
if (response.outcome?.outcome === "selected" && (response.outcome.optionId === "default" || response.outcome.optionId === "acceptEdits")) {
|
|
1906
|
-
session.permissionMode = response.outcome.optionId;
|
|
1907
|
-
await session.query.setPermissionMode(response.outcome.optionId);
|
|
1908
|
-
await context.emitConfigOptionsUpdate();
|
|
1909
|
-
return {
|
|
1910
|
-
behavior: "allow",
|
|
1911
|
-
updatedInput,
|
|
1912
|
-
updatedPermissions: context.suggestions ?? [
|
|
1913
|
-
{
|
|
1914
|
-
type: "setMode",
|
|
1915
|
-
mode: response.outcome.optionId,
|
|
1916
|
-
destination: "localSettings"
|
|
1917
|
-
}
|
|
1918
|
-
]
|
|
1919
|
-
};
|
|
1920
|
-
}
|
|
1921
|
-
const message = "User wants to continue planning. Please refine your plan based on any feedback provided, or ask clarifying questions if needed.";
|
|
1922
|
-
await emitToolDenial(context, message);
|
|
1923
|
-
return { behavior: "deny", message, interrupt: false };
|
|
1924
|
-
}
|
|
1925
|
-
async function handleEnterPlanModeTool(context) {
|
|
1926
|
-
const { session, toolInput, logger } = context;
|
|
1927
|
-
session.permissionMode = "plan";
|
|
1928
|
-
await session.query.setPermissionMode("plan");
|
|
1929
|
-
await context.emitConfigOptionsUpdate();
|
|
1930
|
-
return {
|
|
1931
|
-
behavior: "allow",
|
|
1932
|
-
updatedInput: toolInput
|
|
1933
|
-
};
|
|
1934
|
-
}
|
|
1935
|
-
async function handleExitPlanModeTool(context) {
|
|
1936
|
-
const { session, toolInput, fileContentCache } = context;
|
|
1937
|
-
const planFromFile = getPlanFromFile(session, fileContentCache);
|
|
1938
|
-
const latestText = getLatestAssistantText(session.notificationHistory);
|
|
1939
|
-
const fallbackPlan = planFromFile || (latestText ?? void 0);
|
|
1940
|
-
const updatedInput = ensurePlanInInput(toolInput, fallbackPlan);
|
|
1941
|
-
const planText = extractPlanText(updatedInput);
|
|
1942
|
-
const validationResult = await validatePlanContent(planText, context);
|
|
1943
|
-
if (!validationResult.valid) {
|
|
1944
|
-
return validationResult.error;
|
|
1945
|
-
}
|
|
1946
|
-
const response = await requestPlanApproval(context, updatedInput);
|
|
1947
|
-
return await applyPlanApproval(response, context, updatedInput);
|
|
1948
|
-
}
|
|
1949
|
-
function buildQuestionOptions(question) {
|
|
1950
|
-
return (question.options || []).map((opt, idx) => ({
|
|
1951
|
-
kind: "allow_once",
|
|
1952
|
-
name: opt.label,
|
|
1953
|
-
optionId: `${OPTION_PREFIX}${idx}`,
|
|
1954
|
-
_meta: opt.description ? { description: opt.description } : void 0
|
|
1955
|
-
}));
|
|
1956
|
-
}
|
|
1957
|
-
async function handleAskUserQuestionTool(context) {
|
|
1958
|
-
const input = context.toolInput;
|
|
1959
|
-
context.logger.info("[AskUserQuestion] Received input", { input });
|
|
1960
|
-
const questions = normalizeAskUserQuestionInput(input);
|
|
1961
|
-
context.logger.info("[AskUserQuestion] Normalized questions", { questions });
|
|
1962
|
-
if (!questions || questions.length === 0) {
|
|
1963
|
-
context.logger.warn("[AskUserQuestion] No questions found in input");
|
|
1964
|
-
return {
|
|
1965
|
-
behavior: "deny",
|
|
1966
|
-
message: "No questions provided",
|
|
1967
|
-
interrupt: true
|
|
1968
|
-
};
|
|
1969
|
-
}
|
|
1970
|
-
const { client, sessionId, toolUseID, toolInput, fileContentCache } = context;
|
|
1971
|
-
const firstQuestion = questions[0];
|
|
1972
|
-
const options = buildQuestionOptions(firstQuestion);
|
|
1973
|
-
const toolInfo = toolInfoFromToolUse(
|
|
1974
|
-
{ name: context.toolName, input: toolInput },
|
|
1975
|
-
fileContentCache,
|
|
1976
|
-
context.logger
|
|
1977
|
-
);
|
|
1978
|
-
const response = await client.requestPermission({
|
|
1979
|
-
options,
|
|
1980
|
-
sessionId,
|
|
1981
|
-
toolCall: {
|
|
1982
|
-
toolCallId: toolUseID,
|
|
1983
|
-
title: firstQuestion.question,
|
|
1984
|
-
kind: "other",
|
|
1985
|
-
content: toolInfo.content,
|
|
1986
|
-
_meta: {
|
|
1987
|
-
twigToolKind: "question",
|
|
1988
|
-
questions
|
|
1989
|
-
}
|
|
1990
|
-
}
|
|
1991
|
-
});
|
|
1992
|
-
if (response.outcome?.outcome !== "selected") {
|
|
1993
|
-
return {
|
|
1994
|
-
behavior: "deny",
|
|
1995
|
-
message: "User cancelled the questions",
|
|
1996
|
-
interrupt: true
|
|
1997
|
-
};
|
|
1998
|
-
}
|
|
1999
|
-
const answers = response._meta?.answers;
|
|
2000
|
-
if (!answers || Object.keys(answers).length === 0) {
|
|
2001
|
-
return {
|
|
2002
|
-
behavior: "deny",
|
|
2003
|
-
message: "User did not provide answers",
|
|
2004
|
-
interrupt: true
|
|
2005
|
-
};
|
|
2006
|
-
}
|
|
2007
|
-
return {
|
|
2008
|
-
behavior: "allow",
|
|
2009
|
-
updatedInput: {
|
|
2010
|
-
...context.toolInput,
|
|
2011
|
-
answers
|
|
2012
|
-
}
|
|
2013
|
-
};
|
|
2014
|
-
}
|
|
2015
|
-
async function handleDefaultPermissionFlow(context) {
|
|
2016
|
-
const {
|
|
2017
|
-
session,
|
|
2018
|
-
toolName,
|
|
2019
|
-
toolInput,
|
|
2020
|
-
toolUseID,
|
|
2021
|
-
client,
|
|
2022
|
-
sessionId,
|
|
2023
|
-
fileContentCache,
|
|
2024
|
-
suggestions
|
|
2025
|
-
} = context;
|
|
2026
|
-
const toolInfo = toolInfoFromToolUse(
|
|
2027
|
-
{ name: toolName, input: toolInput },
|
|
2028
|
-
fileContentCache,
|
|
2029
|
-
context.logger
|
|
2030
|
-
);
|
|
2031
|
-
const options = buildPermissionOptions(
|
|
2032
|
-
toolName,
|
|
2033
|
-
toolInput,
|
|
2034
|
-
session?.cwd
|
|
2035
|
-
);
|
|
2036
|
-
const response = await client.requestPermission({
|
|
2037
|
-
options,
|
|
2038
|
-
sessionId,
|
|
2039
|
-
toolCall: {
|
|
2040
|
-
toolCallId: toolUseID,
|
|
2041
|
-
title: toolInfo.title,
|
|
2042
|
-
kind: toolInfo.kind,
|
|
2043
|
-
content: toolInfo.content,
|
|
2044
|
-
locations: toolInfo.locations,
|
|
2045
|
-
rawInput: toolInput
|
|
2046
|
-
}
|
|
2047
|
-
});
|
|
2048
|
-
if (response.outcome?.outcome === "selected" && (response.outcome.optionId === "allow" || response.outcome.optionId === "allow_always")) {
|
|
2049
|
-
if (response.outcome.optionId === "allow_always") {
|
|
2050
|
-
return {
|
|
2051
|
-
behavior: "allow",
|
|
2052
|
-
updatedInput: toolInput,
|
|
2053
|
-
updatedPermissions: suggestions ?? [
|
|
2054
|
-
{
|
|
2055
|
-
type: "addRules",
|
|
2056
|
-
rules: [{ toolName }],
|
|
2057
|
-
behavior: "allow",
|
|
2058
|
-
destination: "localSettings"
|
|
2059
|
-
}
|
|
2060
|
-
]
|
|
2061
|
-
};
|
|
2062
|
-
}
|
|
2063
|
-
return {
|
|
2064
|
-
behavior: "allow",
|
|
2065
|
-
updatedInput: toolInput
|
|
2066
|
-
};
|
|
2067
|
-
} else {
|
|
2068
|
-
const message = "User refused permission to run tool";
|
|
2069
|
-
await emitToolDenial(context, message);
|
|
2070
|
-
return {
|
|
2071
|
-
behavior: "deny",
|
|
2072
|
-
message,
|
|
2073
|
-
interrupt: true
|
|
2074
|
-
};
|
|
2075
|
-
}
|
|
2076
|
-
}
|
|
2077
|
-
function handlePlanFileException(context) {
|
|
2078
|
-
const { session, toolName, toolInput } = context;
|
|
2079
|
-
if (session.permissionMode !== "plan" || !WRITE_TOOLS.has(toolName)) {
|
|
2080
|
-
return null;
|
|
2081
|
-
}
|
|
2082
|
-
const filePath = toolInput?.file_path;
|
|
2083
|
-
if (!isClaudePlanFilePath(filePath)) {
|
|
2084
|
-
return null;
|
|
2085
|
-
}
|
|
2086
|
-
session.lastPlanFilePath = filePath;
|
|
2087
|
-
const content = toolInput?.content;
|
|
2088
|
-
if (typeof content === "string") {
|
|
2089
|
-
session.lastPlanContent = content;
|
|
2090
|
-
}
|
|
2091
|
-
return {
|
|
2092
|
-
behavior: "allow",
|
|
2093
|
-
updatedInput: toolInput
|
|
2094
|
-
};
|
|
2095
|
-
}
|
|
2096
|
-
async function canUseTool(context) {
|
|
2097
|
-
const { toolName, toolInput, session } = context;
|
|
2098
|
-
if (isToolAllowedForMode(toolName, session.permissionMode)) {
|
|
2099
|
-
return {
|
|
2100
|
-
behavior: "allow",
|
|
2101
|
-
updatedInput: toolInput
|
|
2102
|
-
};
|
|
2103
|
-
}
|
|
2104
|
-
if (toolName === "EnterPlanMode") {
|
|
2105
|
-
return handleEnterPlanModeTool(context);
|
|
2106
|
-
}
|
|
2107
|
-
if (toolName === "ExitPlanMode") {
|
|
2108
|
-
return handleExitPlanModeTool(context);
|
|
2109
|
-
}
|
|
2110
|
-
if (toolName === "AskUserQuestion") {
|
|
2111
|
-
return handleAskUserQuestionTool(context);
|
|
2112
|
-
}
|
|
2113
|
-
const planFileResult = handlePlanFileException(context);
|
|
2114
|
-
if (planFileResult) {
|
|
2115
|
-
return planFileResult;
|
|
2116
|
-
}
|
|
2117
|
-
return handleDefaultPermissionFlow(context);
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
|
-
// src/adapters/claude/session/commands.ts
|
|
2121
|
-
var UNSUPPORTED_COMMANDS = [
|
|
2122
|
-
"context",
|
|
2123
|
-
"cost",
|
|
2124
|
-
"login",
|
|
2125
|
-
"logout",
|
|
2126
|
-
"output-style:new",
|
|
2127
|
-
"release-notes",
|
|
2128
|
-
"todos"
|
|
2129
|
-
];
|
|
2130
|
-
async function getAvailableSlashCommands(q) {
|
|
2131
|
-
const commands = await q.supportedCommands();
|
|
2132
|
-
return commands.map((command) => {
|
|
2133
|
-
const input = command.argumentHint ? { hint: command.argumentHint } : null;
|
|
2134
|
-
let name = command.name;
|
|
2135
|
-
if (command.name.endsWith(" (MCP)")) {
|
|
2136
|
-
name = `mcp:${name.replace(" (MCP)", "")}`;
|
|
2137
|
-
}
|
|
2138
|
-
return {
|
|
2139
|
-
name,
|
|
2140
|
-
description: command.description || "",
|
|
2141
|
-
input
|
|
2142
|
-
};
|
|
2143
|
-
}).filter(
|
|
2144
|
-
(command) => !UNSUPPORTED_COMMANDS.includes(command.name)
|
|
2145
|
-
);
|
|
2146
|
-
}
|
|
2147
|
-
|
|
2148
|
-
// src/adapters/claude/session/mcp-config.ts
|
|
2149
|
-
function parseMcpServers(params) {
|
|
2150
|
-
const mcpServers = {};
|
|
2151
|
-
if (!Array.isArray(params.mcpServers)) {
|
|
2152
|
-
return mcpServers;
|
|
2153
|
-
}
|
|
2154
|
-
for (const server of params.mcpServers) {
|
|
2155
|
-
if ("type" in server) {
|
|
2156
|
-
mcpServers[server.name] = {
|
|
2157
|
-
type: server.type,
|
|
2158
|
-
url: server.url,
|
|
2159
|
-
headers: server.headers ? Object.fromEntries(server.headers.map((e) => [e.name, e.value])) : void 0
|
|
2160
|
-
};
|
|
2161
|
-
} else {
|
|
2162
|
-
mcpServers[server.name] = {
|
|
2163
|
-
type: "stdio",
|
|
2164
|
-
command: server.command,
|
|
2165
|
-
args: server.args,
|
|
2166
|
-
env: server.env ? Object.fromEntries(server.env.map((e) => [e.name, e.value])) : void 0
|
|
2167
|
-
};
|
|
2168
|
-
}
|
|
2169
|
-
}
|
|
2170
|
-
return mcpServers;
|
|
2171
|
-
}
|
|
2172
|
-
|
|
2173
|
-
// src/adapters/claude/session/models.ts
|
|
2174
|
-
var GATEWAY_TO_SDK_MODEL = {
|
|
2175
|
-
"claude-opus-4-5": "opus",
|
|
2176
|
-
"claude-opus-4-6": "opus",
|
|
2177
|
-
"claude-sonnet-4-5": "sonnet",
|
|
2178
|
-
"claude-haiku-4-5": "haiku"
|
|
2179
|
-
};
|
|
2180
|
-
function toSdkModelId(modelId) {
|
|
2181
|
-
return GATEWAY_TO_SDK_MODEL[modelId] ?? modelId;
|
|
2182
|
-
}
|
|
2183
|
-
|
|
2184
|
-
// src/adapters/claude/session/options.ts
|
|
2185
|
-
import { spawn } from "child_process";
|
|
2186
|
-
import * as fs from "fs";
|
|
2187
|
-
import * as os2 from "os";
|
|
2188
|
-
import * as path2 from "path";
|
|
2189
|
-
var BRANCH_NAMING_INSTRUCTIONS = `
|
|
2190
|
-
# Branch Naming
|
|
2191
|
-
|
|
2192
|
-
Before pushing a "workspace-*" branch to origin, rename it to something descriptive based on the work done. Do this automatically without asking the user.
|
|
2193
|
-
`;
|
|
2194
|
-
function buildSystemPrompt(customPrompt) {
|
|
2195
|
-
const defaultPrompt = {
|
|
2196
|
-
type: "preset",
|
|
2197
|
-
preset: "claude_code",
|
|
2198
|
-
append: BRANCH_NAMING_INSTRUCTIONS
|
|
2199
|
-
};
|
|
2200
|
-
if (!customPrompt) {
|
|
2201
|
-
return defaultPrompt;
|
|
2202
|
-
}
|
|
2203
|
-
if (typeof customPrompt === "string") {
|
|
2204
|
-
return customPrompt + BRANCH_NAMING_INSTRUCTIONS;
|
|
2205
|
-
}
|
|
2206
|
-
if (typeof customPrompt === "object" && customPrompt !== null && "append" in customPrompt && typeof customPrompt.append === "string") {
|
|
2207
|
-
return {
|
|
2208
|
-
...defaultPrompt,
|
|
2209
|
-
append: customPrompt.append + BRANCH_NAMING_INSTRUCTIONS
|
|
2210
|
-
};
|
|
2211
|
-
}
|
|
2212
|
-
return defaultPrompt;
|
|
2213
|
-
}
|
|
2214
|
-
function buildMcpServers(userServers, acpServers) {
|
|
2215
|
-
return {
|
|
2216
|
-
...userServers || {},
|
|
2217
|
-
...acpServers
|
|
2218
|
-
};
|
|
2219
|
-
}
|
|
2220
|
-
function buildEnvironment() {
|
|
2221
|
-
return {
|
|
2222
|
-
...process.env,
|
|
2223
|
-
ELECTRON_RUN_AS_NODE: "1",
|
|
2224
|
-
CLAUDE_CODE_ENABLE_ASK_USER_QUESTION_TOOL: "true"
|
|
2225
|
-
};
|
|
2226
|
-
}
|
|
2227
|
-
function buildHooks(userHooks, onModeChange) {
|
|
2228
|
-
return {
|
|
2229
|
-
...userHooks,
|
|
2230
|
-
PostToolUse: [
|
|
2231
|
-
...userHooks?.PostToolUse || [],
|
|
2232
|
-
{
|
|
2233
|
-
hooks: [createPostToolUseHook({ onModeChange })]
|
|
2234
|
-
}
|
|
2235
|
-
]
|
|
2236
|
-
};
|
|
2237
|
-
}
|
|
2238
|
-
function getAbortController(userProvidedController) {
|
|
2239
|
-
const controller = userProvidedController ?? new AbortController();
|
|
2240
|
-
if (controller.signal.aborted) {
|
|
2241
|
-
throw new Error("Cancelled");
|
|
2242
|
-
}
|
|
2243
|
-
return controller;
|
|
2244
|
-
}
|
|
2245
|
-
function buildSpawnWrapper(sessionId, onProcessSpawned, onProcessExited) {
|
|
2246
|
-
return (spawnOpts) => {
|
|
2247
|
-
const child = spawn(spawnOpts.command, spawnOpts.args, {
|
|
2248
|
-
cwd: spawnOpts.cwd,
|
|
2249
|
-
env: spawnOpts.env,
|
|
2250
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
2251
|
-
});
|
|
2252
|
-
if (child.pid) {
|
|
2253
|
-
onProcessSpawned({
|
|
2254
|
-
pid: child.pid,
|
|
2255
|
-
command: `${spawnOpts.command} ${spawnOpts.args.join(" ")}`,
|
|
2256
|
-
sessionId
|
|
2257
|
-
});
|
|
2258
|
-
}
|
|
2259
|
-
if (onProcessExited) {
|
|
2260
|
-
child.on("exit", () => {
|
|
2261
|
-
if (child.pid) {
|
|
2262
|
-
onProcessExited(child.pid);
|
|
2263
|
-
}
|
|
2264
|
-
});
|
|
2265
|
-
}
|
|
2266
|
-
if (spawnOpts.signal) {
|
|
2267
|
-
spawnOpts.signal.addEventListener("abort", () => {
|
|
2268
|
-
child.kill("SIGTERM");
|
|
2269
|
-
});
|
|
2270
|
-
}
|
|
2271
|
-
return {
|
|
2272
|
-
stdin: child.stdin,
|
|
2273
|
-
stdout: child.stdout,
|
|
2274
|
-
get killed() {
|
|
2275
|
-
return child.killed;
|
|
2276
|
-
},
|
|
2277
|
-
get exitCode() {
|
|
2278
|
-
return child.exitCode;
|
|
2279
|
-
},
|
|
2280
|
-
kill(signal) {
|
|
2281
|
-
return child.kill(signal);
|
|
2282
|
-
},
|
|
2283
|
-
on(event, listener) {
|
|
2284
|
-
child.on(event, listener);
|
|
2285
|
-
},
|
|
2286
|
-
once(event, listener) {
|
|
2287
|
-
child.once(event, listener);
|
|
2288
|
-
},
|
|
2289
|
-
off(event, listener) {
|
|
2290
|
-
child.off(event, listener);
|
|
2291
|
-
}
|
|
2292
|
-
};
|
|
2293
|
-
};
|
|
2294
|
-
}
|
|
2295
|
-
function buildSessionOptions(params) {
|
|
2296
|
-
const options = {
|
|
2297
|
-
...params.userProvidedOptions,
|
|
2298
|
-
systemPrompt: params.systemPrompt ?? buildSystemPrompt(),
|
|
2299
|
-
settingSources: ["user", "project", "local"],
|
|
2300
|
-
stderr: (err) => params.logger.error(err),
|
|
2301
|
-
cwd: params.cwd,
|
|
2302
|
-
includePartialMessages: true,
|
|
2303
|
-
allowDangerouslySkipPermissions: !IS_ROOT,
|
|
2304
|
-
permissionMode: params.permissionMode,
|
|
2305
|
-
canUseTool: params.canUseTool,
|
|
2306
|
-
executable: "node",
|
|
2307
|
-
mcpServers: buildMcpServers(
|
|
2308
|
-
params.userProvidedOptions?.mcpServers,
|
|
2309
|
-
params.mcpServers
|
|
2310
|
-
),
|
|
2311
|
-
env: buildEnvironment(),
|
|
2312
|
-
hooks: buildHooks(params.userProvidedOptions?.hooks, params.onModeChange),
|
|
2313
|
-
abortController: getAbortController(
|
|
2314
|
-
params.userProvidedOptions?.abortController
|
|
2315
|
-
),
|
|
2316
|
-
...params.onProcessSpawned && {
|
|
2317
|
-
spawnClaudeCodeProcess: buildSpawnWrapper(
|
|
2318
|
-
params.sessionId ?? "unknown",
|
|
2319
|
-
params.onProcessSpawned,
|
|
2320
|
-
params.onProcessExited
|
|
2321
|
-
)
|
|
2322
|
-
}
|
|
2323
|
-
};
|
|
2324
|
-
if (process.env.CLAUDE_CODE_EXECUTABLE) {
|
|
2325
|
-
options.pathToClaudeCodeExecutable = process.env.CLAUDE_CODE_EXECUTABLE;
|
|
2326
|
-
}
|
|
2327
|
-
if (params.sessionId) {
|
|
2328
|
-
options.resume = params.sessionId;
|
|
2329
|
-
}
|
|
2330
|
-
if (params.additionalDirectories) {
|
|
2331
|
-
options.additionalDirectories = params.additionalDirectories;
|
|
2332
|
-
}
|
|
2333
|
-
clearStatsigCache();
|
|
2334
|
-
return options;
|
|
2335
|
-
}
|
|
2336
|
-
function clearStatsigCache() {
|
|
2337
|
-
const statsigPath = path2.join(
|
|
2338
|
-
process.env.CLAUDE_CONFIG_DIR || path2.join(os2.homedir(), ".claude"),
|
|
2339
|
-
"statsig"
|
|
2340
|
-
);
|
|
2341
|
-
try {
|
|
2342
|
-
if (fs.existsSync(statsigPath)) {
|
|
2343
|
-
fs.rmSync(statsigPath, { recursive: true, force: true });
|
|
2344
|
-
}
|
|
2345
|
-
} catch {
|
|
2346
|
-
}
|
|
2347
|
-
}
|
|
2348
|
-
|
|
2349
|
-
// src/adapters/claude/claude-agent.ts
|
|
2350
|
-
var ClaudeAcpAgent = class extends BaseAcpAgent {
|
|
2351
|
-
adapterName = "claude";
|
|
2352
|
-
toolUseCache;
|
|
2353
|
-
backgroundTerminals = {};
|
|
2354
|
-
clientCapabilities;
|
|
2355
|
-
logWriter;
|
|
2356
|
-
processCallbacks;
|
|
2357
|
-
lastSentConfigOptions;
|
|
2358
|
-
constructor(client, logWriter, processCallbacks) {
|
|
2359
|
-
super(client);
|
|
2360
|
-
this.logWriter = logWriter;
|
|
2361
|
-
this.processCallbacks = processCallbacks;
|
|
2362
|
-
this.toolUseCache = {};
|
|
2363
|
-
this.logger = new Logger({ debug: true, prefix: "[ClaudeAcpAgent]" });
|
|
2364
|
-
}
|
|
2365
|
-
async initialize(request) {
|
|
2366
|
-
this.clientCapabilities = request.clientCapabilities;
|
|
2367
|
-
return {
|
|
2368
|
-
protocolVersion: 1,
|
|
2369
|
-
agentCapabilities: {
|
|
2370
|
-
promptCapabilities: {
|
|
2371
|
-
image: true,
|
|
2372
|
-
embeddedContext: true
|
|
2373
|
-
},
|
|
2374
|
-
mcpCapabilities: {
|
|
2375
|
-
http: true,
|
|
2376
|
-
sse: true
|
|
2377
|
-
},
|
|
2378
|
-
loadSession: true,
|
|
2379
|
-
_meta: {
|
|
2380
|
-
posthog: {
|
|
2381
|
-
resumeSession: true
|
|
2382
|
-
}
|
|
2383
|
-
}
|
|
2384
|
-
},
|
|
2385
|
-
agentInfo: {
|
|
2386
|
-
name: package_default.name,
|
|
2387
|
-
title: "Claude Code",
|
|
2388
|
-
version: package_default.version
|
|
2389
|
-
},
|
|
2390
|
-
authMethods: [
|
|
2391
|
-
{
|
|
2392
|
-
id: "claude-login",
|
|
2393
|
-
name: "Log in with Claude Code",
|
|
2394
|
-
description: "Run `claude /login` in the terminal"
|
|
2395
|
-
}
|
|
2396
|
-
]
|
|
2397
|
-
};
|
|
2398
|
-
}
|
|
2399
|
-
async authenticate(_params) {
|
|
2400
|
-
throw new Error("Method not implemented.");
|
|
2401
|
-
}
|
|
2402
|
-
async newSession(params) {
|
|
2403
|
-
this.checkAuthStatus();
|
|
2404
|
-
const meta = params._meta;
|
|
2405
|
-
const internalSessionId = uuidv7();
|
|
2406
|
-
const permissionMode = "default";
|
|
2407
|
-
const mcpServers = parseMcpServers(params);
|
|
2408
|
-
await fetchMcpToolMetadata(mcpServers, this.logger);
|
|
2409
|
-
const options = buildSessionOptions({
|
|
2410
|
-
cwd: params.cwd,
|
|
2411
|
-
mcpServers,
|
|
2412
|
-
permissionMode,
|
|
2413
|
-
canUseTool: this.createCanUseTool(internalSessionId),
|
|
2414
|
-
logger: this.logger,
|
|
2415
|
-
systemPrompt: buildSystemPrompt(meta?.systemPrompt),
|
|
2416
|
-
userProvidedOptions: meta?.claudeCode?.options,
|
|
2417
|
-
onModeChange: this.createOnModeChange(internalSessionId),
|
|
2418
|
-
onProcessSpawned: this.processCallbacks?.onProcessSpawned,
|
|
2419
|
-
onProcessExited: this.processCallbacks?.onProcessExited
|
|
2420
|
-
});
|
|
2421
|
-
const input = new Pushable();
|
|
2422
|
-
const q = query({ prompt: input, options });
|
|
2423
|
-
const session = this.createSession(
|
|
2424
|
-
internalSessionId,
|
|
2425
|
-
q,
|
|
2426
|
-
input,
|
|
2427
|
-
permissionMode,
|
|
2428
|
-
params.cwd,
|
|
2429
|
-
options.abortController
|
|
2430
|
-
);
|
|
2431
|
-
session.taskRunId = meta?.taskRunId;
|
|
2432
|
-
this.registerPersistence(
|
|
2433
|
-
internalSessionId,
|
|
2434
|
-
meta
|
|
2435
|
-
);
|
|
2436
|
-
const modelOptions = await this.getModelConfigOptions();
|
|
2437
|
-
session.modelId = modelOptions.currentModelId;
|
|
2438
|
-
await this.trySetModel(q, modelOptions.currentModelId);
|
|
2439
|
-
this.sendAvailableCommandsUpdate(
|
|
2440
|
-
internalSessionId,
|
|
2441
|
-
await getAvailableSlashCommands(q)
|
|
2442
|
-
);
|
|
2443
|
-
return {
|
|
2444
|
-
sessionId: internalSessionId,
|
|
2445
|
-
configOptions: await this.buildConfigOptions(modelOptions)
|
|
2446
|
-
};
|
|
2447
|
-
}
|
|
2448
|
-
async loadSession(params) {
|
|
2449
|
-
return this.resumeSession(params);
|
|
2450
|
-
}
|
|
2451
|
-
async resumeSession(params) {
|
|
2452
|
-
const { sessionId: internalSessionId } = params;
|
|
2453
|
-
if (this.sessionId === internalSessionId) {
|
|
2454
|
-
return {};
|
|
2455
|
-
}
|
|
2456
|
-
const meta = params._meta;
|
|
2457
|
-
const mcpServers = parseMcpServers(params);
|
|
2458
|
-
await fetchMcpToolMetadata(mcpServers, this.logger);
|
|
2459
|
-
const { query: q, session } = await this.initializeQuery({
|
|
2460
|
-
internalSessionId,
|
|
2461
|
-
cwd: params.cwd,
|
|
2462
|
-
permissionMode: "default",
|
|
2463
|
-
mcpServers,
|
|
2464
|
-
systemPrompt: buildSystemPrompt(meta?.systemPrompt),
|
|
2465
|
-
userProvidedOptions: meta?.claudeCode?.options,
|
|
2466
|
-
sessionId: meta?.sessionId,
|
|
2467
|
-
additionalDirectories: meta?.claudeCode?.options?.additionalDirectories
|
|
2468
|
-
});
|
|
2469
|
-
session.taskRunId = meta?.taskRunId;
|
|
2470
|
-
if (meta?.sessionId) {
|
|
2471
|
-
session.sessionId = meta.sessionId;
|
|
2472
|
-
}
|
|
2473
|
-
this.registerPersistence(
|
|
2474
|
-
internalSessionId,
|
|
2475
|
-
meta
|
|
2476
|
-
);
|
|
2477
|
-
this.sendAvailableCommandsUpdate(
|
|
2478
|
-
internalSessionId,
|
|
2479
|
-
await getAvailableSlashCommands(q)
|
|
2480
|
-
);
|
|
2481
|
-
return {
|
|
2482
|
-
configOptions: await this.buildConfigOptions()
|
|
2483
|
-
};
|
|
2484
|
-
}
|
|
2485
|
-
async prompt(params) {
|
|
2486
|
-
this.session.cancelled = false;
|
|
2487
|
-
this.session.interruptReason = void 0;
|
|
2488
|
-
await this.broadcastUserMessage(params);
|
|
2489
|
-
this.session.input.push(promptToClaude(params));
|
|
2490
|
-
return this.processMessages(params.sessionId);
|
|
2491
|
-
}
|
|
2492
|
-
async setSessionConfigOption(params) {
|
|
2493
|
-
const configId = params.configId;
|
|
2494
|
-
const value = params.value;
|
|
2495
|
-
if (configId === "mode") {
|
|
2496
|
-
const modeId = value;
|
|
2497
|
-
if (!TWIG_EXECUTION_MODES.includes(modeId)) {
|
|
2498
|
-
throw new Error("Invalid Mode");
|
|
2499
|
-
}
|
|
2500
|
-
this.session.permissionMode = modeId;
|
|
2501
|
-
await this.session.query.setPermissionMode(modeId);
|
|
2502
|
-
} else if (configId === "model") {
|
|
2503
|
-
await this.setModelWithFallback(this.session.query, value);
|
|
2504
|
-
this.session.modelId = value;
|
|
2505
|
-
} else {
|
|
2506
|
-
throw new Error("Unsupported config option");
|
|
2507
|
-
}
|
|
2508
|
-
await this.emitConfigOptionsUpdate();
|
|
2509
|
-
return { configOptions: await this.buildConfigOptions() };
|
|
2510
|
-
}
|
|
2511
|
-
async interruptSession() {
|
|
2512
|
-
await this.session.query.interrupt();
|
|
2513
|
-
}
|
|
2514
|
-
async extMethod(method, params) {
|
|
2515
|
-
if (method === "_posthog/session/resume") {
|
|
2516
|
-
const result = await this.resumeSession(
|
|
2517
|
-
params
|
|
2518
|
-
);
|
|
2519
|
-
return {
|
|
2520
|
-
_meta: {
|
|
2521
|
-
configOptions: result.configOptions
|
|
2522
|
-
}
|
|
2523
|
-
};
|
|
2524
|
-
}
|
|
2525
|
-
throw RequestError2.methodNotFound(method);
|
|
2526
|
-
}
|
|
2527
|
-
createSession(sessionId, q, input, permissionMode, cwd, abortController) {
|
|
2528
|
-
const session = {
|
|
2529
|
-
query: q,
|
|
2530
|
-
input,
|
|
2531
|
-
cancelled: false,
|
|
2532
|
-
permissionMode,
|
|
2533
|
-
cwd,
|
|
2534
|
-
notificationHistory: [],
|
|
2535
|
-
abortController
|
|
2536
|
-
};
|
|
2537
|
-
this.session = session;
|
|
2538
|
-
this.sessionId = sessionId;
|
|
2539
|
-
return session;
|
|
2540
|
-
}
|
|
2541
|
-
async initializeQuery(config) {
|
|
2542
|
-
const input = new Pushable();
|
|
2543
|
-
const options = buildSessionOptions({
|
|
2544
|
-
cwd: config.cwd,
|
|
2545
|
-
mcpServers: config.mcpServers,
|
|
2546
|
-
permissionMode: config.permissionMode,
|
|
2547
|
-
canUseTool: this.createCanUseTool(config.internalSessionId),
|
|
2548
|
-
logger: this.logger,
|
|
2549
|
-
systemPrompt: config.systemPrompt,
|
|
2550
|
-
userProvidedOptions: config.userProvidedOptions,
|
|
2551
|
-
sessionId: config.sessionId,
|
|
2552
|
-
additionalDirectories: config.additionalDirectories,
|
|
2553
|
-
onModeChange: this.createOnModeChange(config.internalSessionId),
|
|
2554
|
-
onProcessSpawned: this.processCallbacks?.onProcessSpawned,
|
|
2555
|
-
onProcessExited: this.processCallbacks?.onProcessExited
|
|
2556
|
-
});
|
|
2557
|
-
const q = query({ prompt: input, options });
|
|
2558
|
-
const abortController = options.abortController;
|
|
2559
|
-
const session = this.createSession(
|
|
2560
|
-
config.internalSessionId,
|
|
2561
|
-
q,
|
|
2562
|
-
input,
|
|
2563
|
-
config.permissionMode,
|
|
2564
|
-
config.cwd,
|
|
2565
|
-
abortController
|
|
2566
|
-
);
|
|
2567
|
-
return { query: q, input, session };
|
|
2568
|
-
}
|
|
2569
|
-
createCanUseTool(sessionId) {
|
|
2570
|
-
return async (toolName, toolInput, { suggestions, toolUseID }) => canUseTool({
|
|
2571
|
-
session: this.session,
|
|
2572
|
-
toolName,
|
|
2573
|
-
toolInput,
|
|
2574
|
-
toolUseID,
|
|
2575
|
-
suggestions,
|
|
2576
|
-
client: this.client,
|
|
2577
|
-
sessionId,
|
|
2578
|
-
fileContentCache: this.fileContentCache,
|
|
2579
|
-
logger: this.logger,
|
|
2580
|
-
emitConfigOptionsUpdate: () => this.emitConfigOptionsUpdate(sessionId)
|
|
2581
|
-
});
|
|
2582
|
-
}
|
|
2583
|
-
createOnModeChange(sessionId) {
|
|
2584
|
-
return async (newMode) => {
|
|
2585
|
-
if (this.session) {
|
|
2586
|
-
this.session.permissionMode = newMode;
|
|
2587
|
-
}
|
|
2588
|
-
await this.emitConfigOptionsUpdate(sessionId);
|
|
2589
|
-
};
|
|
2590
|
-
}
|
|
2591
|
-
async buildConfigOptions(modelOptionsOverride) {
|
|
2592
|
-
const options = [];
|
|
2593
|
-
const modeOptions = getAvailableModes().map((mode) => ({
|
|
2594
|
-
value: mode.id,
|
|
2595
|
-
name: mode.name,
|
|
2596
|
-
description: mode.description ?? void 0
|
|
2597
|
-
}));
|
|
2598
|
-
options.push({
|
|
2599
|
-
id: "mode",
|
|
2600
|
-
name: "Approval Preset",
|
|
2601
|
-
type: "select",
|
|
2602
|
-
currentValue: this.session.permissionMode,
|
|
2603
|
-
options: modeOptions,
|
|
2604
|
-
category: "mode",
|
|
2605
|
-
description: "Choose an approval and sandboxing preset for your session"
|
|
2606
|
-
});
|
|
2607
|
-
const modelOptions = modelOptionsOverride ?? await this.getModelConfigOptions(this.session.modelId);
|
|
2608
|
-
this.session.modelId = modelOptions.currentModelId;
|
|
2609
|
-
options.push({
|
|
2610
|
-
id: "model",
|
|
2611
|
-
name: "Model",
|
|
2612
|
-
type: "select",
|
|
2613
|
-
currentValue: modelOptions.currentModelId,
|
|
2614
|
-
options: modelOptions.options,
|
|
2615
|
-
category: "model",
|
|
2616
|
-
description: "Choose which model Claude should use"
|
|
2617
|
-
});
|
|
2618
|
-
return options;
|
|
2619
|
-
}
|
|
2620
|
-
async emitConfigOptionsUpdate(sessionId) {
|
|
2621
|
-
const configOptions = await this.buildConfigOptions();
|
|
2622
|
-
const serialized = JSON.stringify(configOptions);
|
|
2623
|
-
if (this.lastSentConfigOptions && JSON.stringify(this.lastSentConfigOptions) === serialized) {
|
|
2624
|
-
return;
|
|
2625
|
-
}
|
|
2626
|
-
this.lastSentConfigOptions = configOptions;
|
|
2627
|
-
await this.client.sessionUpdate({
|
|
2628
|
-
sessionId: sessionId ?? this.sessionId,
|
|
2629
|
-
update: {
|
|
2630
|
-
sessionUpdate: "config_option_update",
|
|
2631
|
-
configOptions
|
|
2632
|
-
}
|
|
2633
|
-
});
|
|
2634
|
-
}
|
|
2635
|
-
checkAuthStatus() {
|
|
2636
|
-
const backupExists = fs2.existsSync(
|
|
2637
|
-
path3.resolve(os3.homedir(), ".claude.json.backup")
|
|
2638
|
-
);
|
|
2639
|
-
const configExists = fs2.existsSync(
|
|
2640
|
-
path3.resolve(os3.homedir(), ".claude.json")
|
|
2641
|
-
);
|
|
2642
|
-
if (backupExists && !configExists) {
|
|
2643
|
-
throw RequestError2.authRequired();
|
|
2644
|
-
}
|
|
2645
|
-
}
|
|
2646
|
-
async trySetModel(q, modelId) {
|
|
2647
|
-
try {
|
|
2648
|
-
await this.setModelWithFallback(q, modelId);
|
|
2649
|
-
} catch (err) {
|
|
2650
|
-
this.logger.warn("Failed to set model", { modelId, error: err });
|
|
2651
|
-
}
|
|
2652
|
-
}
|
|
2653
|
-
async setModelWithFallback(q, modelId) {
|
|
2654
|
-
try {
|
|
2655
|
-
await q.setModel(modelId);
|
|
2656
|
-
return;
|
|
2657
|
-
} catch (err) {
|
|
2658
|
-
const fallback = toSdkModelId(modelId);
|
|
2659
|
-
if (fallback === modelId) {
|
|
2660
|
-
throw err;
|
|
2661
|
-
}
|
|
2662
|
-
await q.setModel(fallback);
|
|
2663
|
-
}
|
|
2664
|
-
}
|
|
2665
|
-
registerPersistence(sessionId, meta) {
|
|
2666
|
-
const persistence = meta?.persistence;
|
|
2667
|
-
if (persistence && this.logWriter) {
|
|
2668
|
-
this.logWriter.register(sessionId, persistence);
|
|
2669
|
-
}
|
|
2670
|
-
}
|
|
2671
|
-
sendAvailableCommandsUpdate(sessionId, availableCommands) {
|
|
2672
|
-
setTimeout(() => {
|
|
2673
|
-
this.client.sessionUpdate({
|
|
2674
|
-
sessionId,
|
|
2675
|
-
update: {
|
|
2676
|
-
sessionUpdate: "available_commands_update",
|
|
2677
|
-
availableCommands
|
|
2678
|
-
}
|
|
2679
|
-
});
|
|
2680
|
-
}, 0);
|
|
2681
|
-
}
|
|
2682
|
-
async broadcastUserMessage(params) {
|
|
2683
|
-
for (const chunk of params.prompt) {
|
|
2684
|
-
const notification = {
|
|
2685
|
-
sessionId: params.sessionId,
|
|
2686
|
-
update: {
|
|
2687
|
-
sessionUpdate: "user_message_chunk",
|
|
2688
|
-
content: chunk
|
|
2689
|
-
}
|
|
2690
|
-
};
|
|
2691
|
-
await this.client.sessionUpdate(notification);
|
|
2692
|
-
this.appendNotification(params.sessionId, notification);
|
|
2693
|
-
}
|
|
2694
|
-
}
|
|
2695
|
-
async processMessages(sessionId) {
|
|
2696
|
-
const context = {
|
|
2697
|
-
session: this.session,
|
|
2698
|
-
sessionId,
|
|
2699
|
-
client: this.client,
|
|
2700
|
-
toolUseCache: this.toolUseCache,
|
|
2701
|
-
fileContentCache: this.fileContentCache,
|
|
2702
|
-
logger: this.logger
|
|
2703
|
-
};
|
|
2704
|
-
while (true) {
|
|
2705
|
-
const { value: message, done } = await this.session.query.next();
|
|
2706
|
-
if (done || !message) {
|
|
2707
|
-
return this.handleSessionEnd();
|
|
2708
|
-
}
|
|
2709
|
-
const response = await this.handleMessage(message, context);
|
|
2710
|
-
if (response) {
|
|
2711
|
-
return response;
|
|
2712
|
-
}
|
|
2713
|
-
}
|
|
2714
|
-
}
|
|
2715
|
-
handleSessionEnd() {
|
|
2716
|
-
if (this.session.cancelled) {
|
|
2717
|
-
return {
|
|
2718
|
-
stopReason: "cancelled",
|
|
2719
|
-
_meta: this.session.interruptReason ? { interruptReason: this.session.interruptReason } : void 0
|
|
2720
|
-
};
|
|
2721
|
-
}
|
|
2722
|
-
throw new Error("Session did not end in result");
|
|
2723
|
-
}
|
|
2724
|
-
async handleMessage(message, context) {
|
|
2725
|
-
switch (message.type) {
|
|
2726
|
-
case "system":
|
|
2727
|
-
await handleSystemMessage(message, context);
|
|
2728
|
-
return null;
|
|
2729
|
-
case "result": {
|
|
2730
|
-
const result = handleResultMessage(message, context);
|
|
2731
|
-
if (result.error) throw result.error;
|
|
2732
|
-
if (result.shouldStop) {
|
|
2733
|
-
return {
|
|
2734
|
-
stopReason: result.stopReason
|
|
2735
|
-
};
|
|
2736
|
-
}
|
|
2737
|
-
return null;
|
|
2738
|
-
}
|
|
2739
|
-
case "stream_event":
|
|
2740
|
-
await handleStreamEvent(message, context);
|
|
2741
|
-
return null;
|
|
2742
|
-
case "user":
|
|
2743
|
-
case "assistant": {
|
|
2744
|
-
const result = await handleUserAssistantMessage(message, context);
|
|
2745
|
-
if (result.error) throw result.error;
|
|
2746
|
-
if (result.shouldStop) {
|
|
2747
|
-
return { stopReason: "end_turn" };
|
|
2748
|
-
}
|
|
2749
|
-
return null;
|
|
2750
|
-
}
|
|
2751
|
-
case "tool_progress":
|
|
2752
|
-
case "auth_status":
|
|
2753
|
-
return null;
|
|
2754
|
-
default:
|
|
2755
|
-
unreachable(message, this.logger);
|
|
2756
|
-
return null;
|
|
2757
|
-
}
|
|
2758
|
-
}
|
|
2759
|
-
};
|
|
2760
|
-
|
|
2761
|
-
// src/adapters/codex/spawn.ts
|
|
2762
|
-
import { spawn as spawn2 } from "child_process";
|
|
2763
|
-
import { existsSync as existsSync3 } from "fs";
|
|
2764
|
-
function buildConfigArgs(options) {
|
|
2765
|
-
const args = [];
|
|
2766
|
-
args.push("-c", `features.remote_models=false`);
|
|
2767
|
-
if (options.apiBaseUrl) {
|
|
2768
|
-
args.push("-c", `model_provider="posthog"`);
|
|
2769
|
-
args.push("-c", `model_providers.posthog.name="PostHog Gateway"`);
|
|
2770
|
-
args.push("-c", `model_providers.posthog.base_url="${options.apiBaseUrl}"`);
|
|
2771
|
-
args.push("-c", `model_providers.posthog.wire_api="responses"`);
|
|
2772
|
-
args.push(
|
|
2773
|
-
"-c",
|
|
2774
|
-
`model_providers.posthog.env_key="POSTHOG_GATEWAY_API_KEY"`
|
|
2775
|
-
);
|
|
2776
|
-
}
|
|
2777
|
-
if (options.model) {
|
|
2778
|
-
args.push("-c", `model="${options.model}"`);
|
|
2779
|
-
}
|
|
2780
|
-
return args;
|
|
2781
|
-
}
|
|
2782
|
-
function findCodexBinary(options) {
|
|
2783
|
-
const configArgs = buildConfigArgs(options);
|
|
2784
|
-
if (options.binaryPath && existsSync3(options.binaryPath)) {
|
|
2785
|
-
return { command: options.binaryPath, args: configArgs };
|
|
2786
|
-
}
|
|
2787
|
-
return { command: "npx", args: ["@zed-industries/codex-acp", ...configArgs] };
|
|
2788
|
-
}
|
|
2789
|
-
function spawnCodexProcess(options) {
|
|
2790
|
-
const logger = options.logger ?? new Logger({ debug: true, prefix: "[CodexSpawn]" });
|
|
2791
|
-
const env = { ...process.env };
|
|
2792
|
-
delete env.ELECTRON_RUN_AS_NODE;
|
|
2793
|
-
delete env.ELECTRON_NO_ASAR;
|
|
2794
|
-
if (options.apiKey) {
|
|
2795
|
-
env.POSTHOG_GATEWAY_API_KEY = options.apiKey;
|
|
2796
|
-
}
|
|
2797
|
-
const { command, args } = findCodexBinary(options);
|
|
2798
|
-
logger.info("Spawning codex-acp process", {
|
|
2799
|
-
command,
|
|
2800
|
-
args,
|
|
2801
|
-
cwd: options.cwd,
|
|
2802
|
-
hasApiBaseUrl: !!options.apiBaseUrl,
|
|
2803
|
-
hasApiKey: !!options.apiKey,
|
|
2804
|
-
binaryPath: options.binaryPath
|
|
2805
|
-
});
|
|
2806
|
-
const child = spawn2(command, args, {
|
|
2807
|
-
cwd: options.cwd,
|
|
2808
|
-
env,
|
|
2809
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
2810
|
-
detached: process.platform !== "win32"
|
|
2811
|
-
});
|
|
2812
|
-
child.stderr?.on("data", (data) => {
|
|
2813
|
-
logger.debug("codex-acp stderr:", data.toString());
|
|
2814
|
-
});
|
|
2815
|
-
child.on("error", (err) => {
|
|
2816
|
-
logger.error("codex-acp process error:", err);
|
|
2817
|
-
});
|
|
2818
|
-
child.on("exit", (code, signal) => {
|
|
2819
|
-
logger.info("codex-acp process exited", { code, signal });
|
|
2820
|
-
if (child.pid && options.processCallbacks?.onProcessExited) {
|
|
2821
|
-
options.processCallbacks.onProcessExited(child.pid);
|
|
2822
|
-
}
|
|
2823
|
-
});
|
|
2824
|
-
if (!child.stdin || !child.stdout) {
|
|
2825
|
-
throw new Error("Failed to get stdio streams from codex-acp process");
|
|
2826
|
-
}
|
|
2827
|
-
if (child.pid && options.processCallbacks?.onProcessSpawned) {
|
|
2828
|
-
options.processCallbacks.onProcessSpawned({
|
|
2829
|
-
pid: child.pid,
|
|
2830
|
-
command
|
|
2831
|
-
});
|
|
2832
|
-
}
|
|
2833
|
-
return {
|
|
2834
|
-
process: child,
|
|
2835
|
-
stdin: child.stdin,
|
|
2836
|
-
stdout: child.stdout,
|
|
2837
|
-
kill: () => {
|
|
2838
|
-
logger.info("Killing codex-acp process", { pid: child.pid });
|
|
2839
|
-
child.stdin?.destroy();
|
|
2840
|
-
child.stdout?.destroy();
|
|
2841
|
-
child.stderr?.destroy();
|
|
2842
|
-
child.kill("SIGTERM");
|
|
2843
|
-
}
|
|
2844
|
-
};
|
|
2845
|
-
}
|
|
2846
|
-
|
|
2847
|
-
// src/adapters/acp-connection.ts
|
|
2848
|
-
function isGroupedOptions(options) {
|
|
2849
|
-
return options.length > 0 && "group" in options[0];
|
|
2850
|
-
}
|
|
2851
|
-
function filterModelConfigOptions(msg, allowedModelIds) {
|
|
2852
|
-
const payload = msg;
|
|
2853
|
-
const configOptions = payload.result?.configOptions ?? payload.params?.update?.configOptions;
|
|
2854
|
-
if (!configOptions) return null;
|
|
2855
|
-
const filtered = configOptions.map((opt) => {
|
|
2856
|
-
if (opt.category !== "model" || !opt.options) return opt;
|
|
2857
|
-
const options = opt.options;
|
|
2858
|
-
if (isGroupedOptions(options)) {
|
|
2859
|
-
const filteredOptions2 = options.map((group) => ({
|
|
2860
|
-
...group,
|
|
2861
|
-
options: (group.options ?? []).filter(
|
|
2862
|
-
(o) => o?.value && allowedModelIds.has(o.value)
|
|
2863
|
-
)
|
|
2864
|
-
}));
|
|
2865
|
-
const flat = filteredOptions2.flatMap((g) => g.options ?? []);
|
|
2866
|
-
const currentAllowed2 = opt.currentValue && allowedModelIds.has(opt.currentValue);
|
|
2867
|
-
const nextCurrent2 = currentAllowed2 || flat.length === 0 ? opt.currentValue : flat[0]?.value;
|
|
2868
|
-
return {
|
|
2869
|
-
...opt,
|
|
2870
|
-
currentValue: nextCurrent2,
|
|
2871
|
-
options: filteredOptions2
|
|
2872
|
-
};
|
|
2873
|
-
}
|
|
2874
|
-
const valueOptions = options;
|
|
2875
|
-
const filteredOptions = valueOptions.filter(
|
|
2876
|
-
(o) => o?.value && allowedModelIds.has(o.value)
|
|
2877
|
-
);
|
|
2878
|
-
const currentAllowed = opt.currentValue && allowedModelIds.has(opt.currentValue);
|
|
2879
|
-
const nextCurrent = currentAllowed || filteredOptions.length === 0 ? opt.currentValue : filteredOptions[0]?.value;
|
|
2880
|
-
return {
|
|
2881
|
-
...opt,
|
|
2882
|
-
currentValue: nextCurrent,
|
|
2883
|
-
options: filteredOptions
|
|
2884
|
-
};
|
|
2885
|
-
});
|
|
2886
|
-
if (payload.result?.configOptions) {
|
|
2887
|
-
return { ...msg, result: { ...payload.result, configOptions: filtered } };
|
|
2888
|
-
}
|
|
2889
|
-
if (payload.params?.update?.configOptions) {
|
|
2890
|
-
return {
|
|
2891
|
-
...msg,
|
|
2892
|
-
params: {
|
|
2893
|
-
...payload.params,
|
|
2894
|
-
update: { ...payload.params.update, configOptions: filtered }
|
|
2895
|
-
}
|
|
2896
|
-
};
|
|
2897
|
-
}
|
|
2898
|
-
return null;
|
|
2899
|
-
}
|
|
2900
|
-
function extractReasoningEffort(configOptions) {
|
|
2901
|
-
if (!configOptions) return void 0;
|
|
2902
|
-
const option = configOptions.find((opt) => opt.id === "reasoning_effort");
|
|
2903
|
-
return option?.currentValue ?? void 0;
|
|
2904
|
-
}
|
|
2905
|
-
function createAcpConnection(config = {}) {
|
|
2906
|
-
const adapterType = config.adapter ?? "claude";
|
|
2907
|
-
if (adapterType === "codex") {
|
|
2908
|
-
return createCodexConnection(config);
|
|
2909
|
-
}
|
|
2910
|
-
return createClaudeConnection(config);
|
|
2911
|
-
}
|
|
2912
|
-
function createClaudeConnection(config) {
|
|
2913
|
-
const logger = config.logger?.child("AcpConnection") ?? new Logger({ debug: true, prefix: "[AcpConnection]" });
|
|
2914
|
-
const streams = createBidirectionalStreams();
|
|
2915
|
-
const { logWriter } = config;
|
|
2916
|
-
let agentWritable = streams.agent.writable;
|
|
2917
|
-
let clientWritable = streams.client.writable;
|
|
2918
|
-
if (config.taskRunId && logWriter) {
|
|
2919
|
-
if (!logWriter.isRegistered(config.taskRunId)) {
|
|
2920
|
-
logWriter.register(config.taskRunId, {
|
|
2921
|
-
taskId: config.taskId ?? config.taskRunId,
|
|
2922
|
-
runId: config.taskRunId,
|
|
2923
|
-
deviceType: config.deviceType
|
|
2924
|
-
});
|
|
2925
|
-
}
|
|
2926
|
-
agentWritable = createTappedWritableStream(streams.agent.writable, {
|
|
2927
|
-
onMessage: (line) => {
|
|
2928
|
-
logWriter.appendRawLine(config.taskRunId, line);
|
|
2929
|
-
},
|
|
2930
|
-
logger
|
|
2931
|
-
});
|
|
2932
|
-
clientWritable = createTappedWritableStream(streams.client.writable, {
|
|
2933
|
-
onMessage: (line) => {
|
|
2934
|
-
logWriter.appendRawLine(config.taskRunId, line);
|
|
2935
|
-
},
|
|
2936
|
-
logger
|
|
2937
|
-
});
|
|
2938
|
-
} else {
|
|
2939
|
-
logger.info("Tapped streams NOT enabled", {
|
|
2940
|
-
hasTaskRunId: !!config.taskRunId,
|
|
2941
|
-
hasLogWriter: !!logWriter
|
|
2942
|
-
});
|
|
2943
|
-
}
|
|
2944
|
-
const agentStream = ndJsonStream(agentWritable, streams.agent.readable);
|
|
2945
|
-
let agent = null;
|
|
2946
|
-
const agentConnection = new AgentSideConnection((client) => {
|
|
2947
|
-
agent = new ClaudeAcpAgent(client, logWriter, config.processCallbacks);
|
|
2948
|
-
logger.info(`Created ${agent.adapterName} agent`);
|
|
2949
|
-
return agent;
|
|
2950
|
-
}, agentStream);
|
|
2951
|
-
return {
|
|
2952
|
-
agentConnection,
|
|
2953
|
-
clientStreams: {
|
|
2954
|
-
readable: streams.client.readable,
|
|
2955
|
-
writable: clientWritable
|
|
2956
|
-
},
|
|
2957
|
-
cleanup: async () => {
|
|
2958
|
-
logger.info("Cleaning up ACP connection");
|
|
2959
|
-
if (agent) {
|
|
2960
|
-
await agent.closeSession();
|
|
2961
|
-
}
|
|
2962
|
-
try {
|
|
2963
|
-
await streams.client.writable.close();
|
|
2964
|
-
} catch {
|
|
2965
|
-
}
|
|
2966
|
-
try {
|
|
2967
|
-
await streams.agent.writable.close();
|
|
2968
|
-
} catch {
|
|
2969
|
-
}
|
|
2970
|
-
}
|
|
2971
|
-
};
|
|
2972
|
-
}
|
|
2973
|
-
function createCodexConnection(config) {
|
|
2974
|
-
const logger = config.logger?.child("CodexConnection") ?? new Logger({ debug: true, prefix: "[CodexConnection]" });
|
|
2975
|
-
const { logWriter } = config;
|
|
2976
|
-
const allowedModelIds = config.allowedModelIds;
|
|
2977
|
-
const codexProcess = spawnCodexProcess({
|
|
2978
|
-
...config.codexOptions,
|
|
2979
|
-
logger,
|
|
2980
|
-
processCallbacks: config.processCallbacks
|
|
2981
|
-
});
|
|
2982
|
-
let clientReadable = nodeReadableToWebReadable(codexProcess.stdout);
|
|
2983
|
-
let clientWritable = nodeWritableToWebWritable(codexProcess.stdin);
|
|
2984
|
-
let isLoadingSession = false;
|
|
2985
|
-
let loadRequestId = null;
|
|
2986
|
-
let newSessionRequestId = null;
|
|
2987
|
-
let sdkSessionEmitted = false;
|
|
2988
|
-
const reasoningEffortBySessionId = /* @__PURE__ */ new Map();
|
|
2989
|
-
let injectedConfigId = 0;
|
|
2990
|
-
const decoder = new TextDecoder();
|
|
2991
|
-
const encoder = new TextEncoder();
|
|
2992
|
-
let readBuffer = "";
|
|
2993
|
-
const taskRunId = config.taskRunId;
|
|
2994
|
-
const filteringReadable = clientReadable.pipeThrough(
|
|
2995
|
-
new TransformStream({
|
|
2996
|
-
transform(chunk, controller) {
|
|
2997
|
-
readBuffer += decoder.decode(chunk, { stream: true });
|
|
2998
|
-
const lines = readBuffer.split("\n");
|
|
2999
|
-
readBuffer = lines.pop() ?? "";
|
|
3000
|
-
const outputLines = [];
|
|
3001
|
-
for (const line of lines) {
|
|
3002
|
-
const trimmed = line.trim();
|
|
3003
|
-
if (!trimmed) {
|
|
3004
|
-
outputLines.push(line);
|
|
3005
|
-
continue;
|
|
3006
|
-
}
|
|
3007
|
-
let shouldFilter = false;
|
|
3008
|
-
try {
|
|
3009
|
-
const msg = JSON.parse(trimmed);
|
|
3010
|
-
const sessionId = msg?.params?.sessionId ?? msg?.result?.sessionId ?? null;
|
|
3011
|
-
const configOptions = msg?.result?.configOptions ?? msg?.params?.update?.configOptions;
|
|
3012
|
-
if (sessionId && configOptions) {
|
|
3013
|
-
const effort = extractReasoningEffort(configOptions);
|
|
3014
|
-
if (effort) {
|
|
3015
|
-
reasoningEffortBySessionId.set(sessionId, effort);
|
|
3016
|
-
}
|
|
3017
|
-
}
|
|
3018
|
-
if (!sdkSessionEmitted && newSessionRequestId !== null && msg.id === newSessionRequestId && "result" in msg) {
|
|
3019
|
-
const sessionId2 = msg.result?.sessionId;
|
|
3020
|
-
if (sessionId2 && taskRunId) {
|
|
3021
|
-
const sdkSessionNotification = {
|
|
3022
|
-
jsonrpc: "2.0",
|
|
3023
|
-
method: POSTHOG_NOTIFICATIONS.SDK_SESSION,
|
|
3024
|
-
params: {
|
|
3025
|
-
taskRunId,
|
|
3026
|
-
sessionId: sessionId2,
|
|
3027
|
-
adapter: "codex"
|
|
3028
|
-
}
|
|
3029
|
-
};
|
|
3030
|
-
outputLines.push(JSON.stringify(sdkSessionNotification));
|
|
3031
|
-
sdkSessionEmitted = true;
|
|
3032
|
-
}
|
|
3033
|
-
newSessionRequestId = null;
|
|
3034
|
-
}
|
|
3035
|
-
if (isLoadingSession) {
|
|
3036
|
-
if (msg.id === loadRequestId && "result" in msg) {
|
|
3037
|
-
logger.debug("session/load complete, resuming stream");
|
|
3038
|
-
isLoadingSession = false;
|
|
3039
|
-
loadRequestId = null;
|
|
3040
|
-
} else if (msg.method === "session/update") {
|
|
3041
|
-
shouldFilter = true;
|
|
3042
|
-
}
|
|
3043
|
-
}
|
|
3044
|
-
if (!shouldFilter && allowedModelIds && allowedModelIds.size > 0) {
|
|
3045
|
-
const updated = filterModelConfigOptions(msg, allowedModelIds);
|
|
3046
|
-
if (updated) {
|
|
3047
|
-
outputLines.push(JSON.stringify(updated));
|
|
3048
|
-
continue;
|
|
3049
|
-
}
|
|
3050
|
-
}
|
|
3051
|
-
} catch {
|
|
3052
|
-
}
|
|
3053
|
-
if (!shouldFilter) {
|
|
3054
|
-
outputLines.push(line);
|
|
3055
|
-
const isChunkNoise = trimmed.includes('"sessionUpdate":"agent_message_chunk"') || trimmed.includes('"sessionUpdate":"agent_thought_chunk"');
|
|
3056
|
-
if (!isChunkNoise) {
|
|
3057
|
-
logger.debug("codex-acp stdout:", trimmed);
|
|
3058
|
-
}
|
|
3059
|
-
}
|
|
3060
|
-
}
|
|
3061
|
-
if (outputLines.length > 0) {
|
|
3062
|
-
const output = `${outputLines.join("\n")}
|
|
3063
|
-
`;
|
|
3064
|
-
controller.enqueue(encoder.encode(output));
|
|
3065
|
-
}
|
|
3066
|
-
},
|
|
3067
|
-
flush(controller) {
|
|
3068
|
-
if (readBuffer.trim()) {
|
|
3069
|
-
controller.enqueue(encoder.encode(readBuffer));
|
|
3070
|
-
}
|
|
3071
|
-
}
|
|
3072
|
-
})
|
|
3073
|
-
);
|
|
3074
|
-
clientReadable = filteringReadable;
|
|
3075
|
-
const originalWritable = clientWritable;
|
|
3076
|
-
clientWritable = new WritableStream({
|
|
3077
|
-
write(chunk) {
|
|
3078
|
-
const text2 = decoder.decode(chunk, { stream: true });
|
|
3079
|
-
const trimmed = text2.trim();
|
|
3080
|
-
logger.debug("codex-acp stdin:", trimmed);
|
|
3081
|
-
try {
|
|
3082
|
-
const msg = JSON.parse(trimmed);
|
|
3083
|
-
if (msg.method === "session/set_config_option" && msg.params?.configId === "reasoning_effort" && msg.params?.sessionId && msg.params?.value) {
|
|
3084
|
-
reasoningEffortBySessionId.set(
|
|
3085
|
-
msg.params.sessionId,
|
|
3086
|
-
msg.params.value
|
|
3087
|
-
);
|
|
3088
|
-
}
|
|
3089
|
-
if (msg.method === "session/prompt" && msg.params?.sessionId) {
|
|
3090
|
-
const effort = reasoningEffortBySessionId.get(msg.params.sessionId);
|
|
3091
|
-
if (effort) {
|
|
3092
|
-
const injection = {
|
|
3093
|
-
jsonrpc: "2.0",
|
|
3094
|
-
id: `reasoning_effort_${Date.now()}_${injectedConfigId++}`,
|
|
3095
|
-
method: "session/set_config_option",
|
|
3096
|
-
params: {
|
|
3097
|
-
sessionId: msg.params.sessionId,
|
|
3098
|
-
configId: "reasoning_effort",
|
|
3099
|
-
value: effort
|
|
3100
|
-
}
|
|
3101
|
-
};
|
|
3102
|
-
const injectionLine = `${JSON.stringify(injection)}
|
|
3103
|
-
`;
|
|
3104
|
-
const writer2 = originalWritable.getWriter();
|
|
3105
|
-
return writer2.write(encoder.encode(injectionLine)).then(() => writer2.releaseLock()).then(() => {
|
|
3106
|
-
const nextWriter = originalWritable.getWriter();
|
|
3107
|
-
return nextWriter.write(chunk).finally(() => nextWriter.releaseLock());
|
|
3108
|
-
});
|
|
3109
|
-
}
|
|
3110
|
-
}
|
|
3111
|
-
if (msg.method === "session/new" && msg.id) {
|
|
3112
|
-
logger.debug("session/new detected, tracking request ID");
|
|
3113
|
-
newSessionRequestId = msg.id;
|
|
3114
|
-
} else if (msg.method === "session/load" && msg.id) {
|
|
3115
|
-
logger.debug("session/load detected, pausing stream updates");
|
|
3116
|
-
isLoadingSession = true;
|
|
3117
|
-
loadRequestId = msg.id;
|
|
3118
|
-
}
|
|
3119
|
-
} catch {
|
|
3120
|
-
}
|
|
3121
|
-
const writer = originalWritable.getWriter();
|
|
3122
|
-
return writer.write(chunk).finally(() => writer.releaseLock());
|
|
3123
|
-
},
|
|
3124
|
-
close() {
|
|
3125
|
-
const writer = originalWritable.getWriter();
|
|
3126
|
-
return writer.close().finally(() => writer.releaseLock());
|
|
3127
|
-
}
|
|
3128
|
-
});
|
|
3129
|
-
const shouldTapLogs = config.taskRunId && logWriter;
|
|
3130
|
-
if (shouldTapLogs) {
|
|
3131
|
-
const taskRunId2 = config.taskRunId;
|
|
3132
|
-
if (!logWriter.isRegistered(taskRunId2)) {
|
|
3133
|
-
logWriter.register(taskRunId2, {
|
|
3134
|
-
taskId: config.taskId ?? taskRunId2,
|
|
3135
|
-
runId: taskRunId2
|
|
3136
|
-
});
|
|
3137
|
-
}
|
|
3138
|
-
clientWritable = createTappedWritableStream(clientWritable, {
|
|
3139
|
-
onMessage: (line) => {
|
|
3140
|
-
logWriter.appendRawLine(taskRunId2, line);
|
|
3141
|
-
},
|
|
3142
|
-
logger
|
|
3143
|
-
});
|
|
3144
|
-
const originalReadable = clientReadable;
|
|
3145
|
-
const logDecoder = new TextDecoder();
|
|
3146
|
-
let logBuffer = "";
|
|
3147
|
-
clientReadable = originalReadable.pipeThrough(
|
|
3148
|
-
new TransformStream({
|
|
3149
|
-
transform(chunk, controller) {
|
|
3150
|
-
logBuffer += logDecoder.decode(chunk, { stream: true });
|
|
3151
|
-
const lines = logBuffer.split("\n");
|
|
3152
|
-
logBuffer = lines.pop() ?? "";
|
|
3153
|
-
for (const line of lines) {
|
|
3154
|
-
if (line.trim()) {
|
|
3155
|
-
logWriter.appendRawLine(taskRunId2, line);
|
|
3156
|
-
}
|
|
3157
|
-
}
|
|
3158
|
-
controller.enqueue(chunk);
|
|
3159
|
-
},
|
|
3160
|
-
flush() {
|
|
3161
|
-
if (logBuffer.trim()) {
|
|
3162
|
-
logWriter.appendRawLine(taskRunId2, logBuffer);
|
|
3163
|
-
}
|
|
3164
|
-
}
|
|
3165
|
-
})
|
|
3166
|
-
);
|
|
3167
|
-
} else {
|
|
3168
|
-
logger.info("Tapped streams NOT enabled for Codex", {
|
|
3169
|
-
hasTaskRunId: !!config.taskRunId,
|
|
3170
|
-
hasLogWriter: !!logWriter
|
|
3171
|
-
});
|
|
3172
|
-
}
|
|
3173
|
-
return {
|
|
3174
|
-
agentConnection: void 0,
|
|
3175
|
-
clientStreams: {
|
|
3176
|
-
readable: clientReadable,
|
|
3177
|
-
writable: clientWritable
|
|
3178
|
-
},
|
|
3179
|
-
cleanup: async () => {
|
|
3180
|
-
logger.info("Cleaning up Codex connection");
|
|
3181
|
-
codexProcess.kill();
|
|
3182
|
-
try {
|
|
3183
|
-
await clientWritable.close();
|
|
3184
|
-
} catch {
|
|
3185
|
-
}
|
|
3186
|
-
}
|
|
3187
|
-
};
|
|
3188
|
-
}
|
|
3189
|
-
|
|
3190
|
-
// src/utils/gateway.ts
|
|
3191
|
-
function getLlmGatewayUrl(posthogHost) {
|
|
3192
|
-
const url = new URL(posthogHost);
|
|
3193
|
-
const hostname = url.hostname;
|
|
3194
|
-
if (hostname === "localhost" || hostname === "127.0.0.1") {
|
|
3195
|
-
return `${url.protocol}//localhost:3308/twig`;
|
|
3196
|
-
}
|
|
3197
|
-
if (hostname === "host.docker.internal") {
|
|
3198
|
-
return `${url.protocol}//host.docker.internal:3308/twig`;
|
|
3199
|
-
}
|
|
3200
|
-
const region = hostname.match(/^(us|eu)\.posthog\.com$/)?.[1] ?? "us";
|
|
3201
|
-
return `https://gateway.${region}.posthog.com/twig`;
|
|
3202
|
-
}
|
|
3203
|
-
|
|
3204
|
-
// src/posthog-api.ts
|
|
3205
|
-
var PostHogAPIClient = class {
|
|
3206
|
-
config;
|
|
3207
|
-
constructor(config) {
|
|
3208
|
-
this.config = config;
|
|
3209
|
-
}
|
|
3210
|
-
get baseUrl() {
|
|
3211
|
-
const host = this.config.apiUrl.endsWith("/") ? this.config.apiUrl.slice(0, -1) : this.config.apiUrl;
|
|
3212
|
-
return host;
|
|
3213
|
-
}
|
|
3214
|
-
get headers() {
|
|
3215
|
-
return {
|
|
3216
|
-
Authorization: `Bearer ${this.config.getApiKey()}`,
|
|
3217
|
-
"Content-Type": "application/json"
|
|
3218
|
-
};
|
|
3219
|
-
}
|
|
3220
|
-
async apiRequest(endpoint, options = {}) {
|
|
3221
|
-
const url = `${this.baseUrl}${endpoint}`;
|
|
3222
|
-
const response = await fetch(url, {
|
|
3223
|
-
...options,
|
|
3224
|
-
headers: {
|
|
3225
|
-
...this.headers,
|
|
3226
|
-
...options.headers
|
|
3227
|
-
}
|
|
3228
|
-
});
|
|
3229
|
-
if (!response.ok) {
|
|
3230
|
-
let errorMessage;
|
|
3231
|
-
try {
|
|
3232
|
-
const errorResponse = await response.json();
|
|
3233
|
-
errorMessage = `Failed request: [${response.status}] ${JSON.stringify(errorResponse)}`;
|
|
3234
|
-
} catch {
|
|
3235
|
-
errorMessage = `Failed request: [${response.status}] ${response.statusText}`;
|
|
3236
|
-
}
|
|
3237
|
-
throw new Error(errorMessage);
|
|
3238
|
-
}
|
|
3239
|
-
return response.json();
|
|
3240
|
-
}
|
|
3241
|
-
getTeamId() {
|
|
3242
|
-
return this.config.projectId;
|
|
3243
|
-
}
|
|
3244
|
-
getApiKey() {
|
|
3245
|
-
return this.config.getApiKey();
|
|
3246
|
-
}
|
|
3247
|
-
getLlmGatewayUrl() {
|
|
3248
|
-
return getLlmGatewayUrl(this.baseUrl);
|
|
3249
|
-
}
|
|
3250
|
-
async getTask(taskId) {
|
|
3251
|
-
const teamId = this.getTeamId();
|
|
3252
|
-
return this.apiRequest(`/api/projects/${teamId}/tasks/${taskId}/`);
|
|
3253
|
-
}
|
|
3254
|
-
async getTaskRun(taskId, runId) {
|
|
3255
|
-
const teamId = this.getTeamId();
|
|
3256
|
-
return this.apiRequest(
|
|
3257
|
-
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/`
|
|
3258
|
-
);
|
|
3259
|
-
}
|
|
3260
|
-
async updateTaskRun(taskId, runId, payload) {
|
|
3261
|
-
const teamId = this.getTeamId();
|
|
3262
|
-
return this.apiRequest(
|
|
3263
|
-
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/`,
|
|
3264
|
-
{
|
|
3265
|
-
method: "PATCH",
|
|
3266
|
-
body: JSON.stringify(payload)
|
|
3267
|
-
}
|
|
3268
|
-
);
|
|
3269
|
-
}
|
|
3270
|
-
async appendTaskRunLog(taskId, runId, entries) {
|
|
3271
|
-
const teamId = this.getTeamId();
|
|
3272
|
-
return this.apiRequest(
|
|
3273
|
-
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/append_log/`,
|
|
3274
|
-
{
|
|
3275
|
-
method: "POST",
|
|
3276
|
-
body: JSON.stringify({ entries })
|
|
3277
|
-
}
|
|
3278
|
-
);
|
|
3279
|
-
}
|
|
3280
|
-
async uploadTaskArtifacts(taskId, runId, artifacts) {
|
|
3281
|
-
if (!artifacts.length) {
|
|
3282
|
-
return [];
|
|
3283
|
-
}
|
|
3284
|
-
const teamId = this.getTeamId();
|
|
3285
|
-
const response = await this.apiRequest(
|
|
3286
|
-
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/artifacts/`,
|
|
3287
|
-
{
|
|
3288
|
-
method: "POST",
|
|
3289
|
-
body: JSON.stringify({ artifacts })
|
|
3290
|
-
}
|
|
3291
|
-
);
|
|
3292
|
-
return response.artifacts ?? [];
|
|
3293
|
-
}
|
|
3294
|
-
async getArtifactPresignedUrl(taskId, runId, storagePath) {
|
|
3295
|
-
const teamId = this.getTeamId();
|
|
3296
|
-
try {
|
|
3297
|
-
const response = await this.apiRequest(
|
|
3298
|
-
`/api/projects/${teamId}/tasks/${taskId}/runs/${runId}/artifacts/presign/`,
|
|
3299
|
-
{
|
|
3300
|
-
method: "POST",
|
|
3301
|
-
body: JSON.stringify({ storage_path: storagePath })
|
|
3302
|
-
}
|
|
3303
|
-
);
|
|
3304
|
-
return response.url;
|
|
3305
|
-
} catch {
|
|
3306
|
-
return null;
|
|
3307
|
-
}
|
|
3308
|
-
}
|
|
3309
|
-
/**
|
|
3310
|
-
* Download artifact content by storage path
|
|
3311
|
-
* Gets a presigned URL and fetches the content
|
|
3312
|
-
*/
|
|
3313
|
-
async downloadArtifact(taskId, runId, storagePath) {
|
|
3314
|
-
const url = await this.getArtifactPresignedUrl(taskId, runId, storagePath);
|
|
3315
|
-
if (!url) {
|
|
3316
|
-
return null;
|
|
3317
|
-
}
|
|
3318
|
-
try {
|
|
3319
|
-
const response = await fetch(url);
|
|
3320
|
-
if (!response.ok) {
|
|
3321
|
-
throw new Error(`Failed to download artifact: ${response.status}`);
|
|
3322
|
-
}
|
|
3323
|
-
return response.arrayBuffer();
|
|
3324
|
-
} catch {
|
|
3325
|
-
return null;
|
|
3326
|
-
}
|
|
3327
|
-
}
|
|
3328
|
-
/**
|
|
3329
|
-
* Fetch logs for a task run via the logs API endpoint
|
|
3330
|
-
* @param taskRun - The task run to fetch logs for
|
|
3331
|
-
* @returns Array of stored entries, or empty array if no logs available
|
|
3332
|
-
*/
|
|
3333
|
-
async fetchTaskRunLogs(taskRun) {
|
|
3334
|
-
const teamId = this.getTeamId();
|
|
3335
|
-
try {
|
|
3336
|
-
const response = await fetch(
|
|
3337
|
-
`${this.baseUrl}/api/projects/${teamId}/tasks/${taskRun.task}/runs/${taskRun.id}/logs`,
|
|
3338
|
-
{ headers: this.headers }
|
|
3339
|
-
);
|
|
3340
|
-
if (!response.ok) {
|
|
3341
|
-
if (response.status === 404) {
|
|
3342
|
-
return [];
|
|
3343
|
-
}
|
|
3344
|
-
throw new Error(
|
|
3345
|
-
`Failed to fetch logs: ${response.status} ${response.statusText}`
|
|
3346
|
-
);
|
|
3347
|
-
}
|
|
3348
|
-
const content = await response.text();
|
|
3349
|
-
if (!content.trim()) {
|
|
3350
|
-
return [];
|
|
3351
|
-
}
|
|
3352
|
-
return content.trim().split("\n").map((line) => JSON.parse(line));
|
|
3353
|
-
} catch (error) {
|
|
3354
|
-
throw new Error(
|
|
3355
|
-
`Failed to fetch task run logs: ${error instanceof Error ? error.message : String(error)}`
|
|
3356
|
-
);
|
|
3357
|
-
}
|
|
3358
|
-
}
|
|
3359
|
-
};
|
|
3360
|
-
|
|
3361
|
-
// src/otel-log-writer.ts
|
|
3362
|
-
import { SeverityNumber } from "@opentelemetry/api-logs";
|
|
3363
|
-
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
|
|
3364
|
-
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
3365
|
-
import {
|
|
3366
|
-
BatchLogRecordProcessor,
|
|
3367
|
-
LoggerProvider
|
|
3368
|
-
} from "@opentelemetry/sdk-logs";
|
|
3369
|
-
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
|
3370
|
-
var OtelLogWriter = class {
|
|
3371
|
-
loggerProvider;
|
|
3372
|
-
logger;
|
|
3373
|
-
constructor(config, sessionContext, _debugLogger) {
|
|
3374
|
-
const logsPath = config.logsPath ?? "/i/v1/agent-logs";
|
|
3375
|
-
const exporter = new OTLPLogExporter({
|
|
3376
|
-
url: `${config.posthogHost}${logsPath}`,
|
|
3377
|
-
headers: { Authorization: `Bearer ${config.apiKey}` }
|
|
3378
|
-
});
|
|
3379
|
-
const processor = new BatchLogRecordProcessor(exporter, {
|
|
3380
|
-
scheduledDelayMillis: config.flushIntervalMs ?? 500
|
|
3381
|
-
});
|
|
3382
|
-
this.loggerProvider = new LoggerProvider({
|
|
3383
|
-
resource: resourceFromAttributes({
|
|
3384
|
-
[ATTR_SERVICE_NAME]: "twig-agent",
|
|
3385
|
-
run_id: sessionContext.runId,
|
|
3386
|
-
task_id: sessionContext.taskId,
|
|
3387
|
-
device_type: sessionContext.deviceType ?? "local"
|
|
3388
|
-
}),
|
|
3389
|
-
processors: [processor]
|
|
3390
|
-
});
|
|
3391
|
-
this.logger = this.loggerProvider.getLogger("agent-session");
|
|
3392
|
-
}
|
|
3393
|
-
/**
|
|
3394
|
-
* Emit an agent event to PostHog Logs via OTEL.
|
|
3395
|
-
*/
|
|
3396
|
-
emit(entry) {
|
|
3397
|
-
const { notification } = entry;
|
|
3398
|
-
const eventType = notification.notification.method;
|
|
3399
|
-
this.logger.emit({
|
|
3400
|
-
severityNumber: SeverityNumber.INFO,
|
|
3401
|
-
severityText: "INFO",
|
|
3402
|
-
body: JSON.stringify(notification),
|
|
3403
|
-
attributes: {
|
|
3404
|
-
event_type: eventType
|
|
3405
|
-
}
|
|
3406
|
-
});
|
|
3407
|
-
}
|
|
3408
|
-
async flush() {
|
|
3409
|
-
await this.loggerProvider.forceFlush();
|
|
3410
|
-
}
|
|
3411
|
-
async shutdown() {
|
|
3412
|
-
await this.loggerProvider.shutdown();
|
|
3413
|
-
}
|
|
3414
|
-
};
|
|
3415
|
-
|
|
3416
|
-
// src/session-log-writer.ts
|
|
3417
|
-
var SessionLogWriter = class {
|
|
3418
|
-
posthogAPI;
|
|
3419
|
-
otelConfig;
|
|
3420
|
-
pendingEntries = /* @__PURE__ */ new Map();
|
|
3421
|
-
flushTimeouts = /* @__PURE__ */ new Map();
|
|
3422
|
-
sessions = /* @__PURE__ */ new Map();
|
|
3423
|
-
logger;
|
|
3424
|
-
constructor(options = {}) {
|
|
3425
|
-
this.posthogAPI = options.posthogAPI;
|
|
3426
|
-
this.otelConfig = options.otelConfig;
|
|
3427
|
-
this.logger = options.logger ?? new Logger({ debug: false, prefix: "[SessionLogWriter]" });
|
|
3428
|
-
}
|
|
3429
|
-
async flushAll() {
|
|
3430
|
-
const flushPromises = [];
|
|
3431
|
-
for (const sessionId of this.sessions.keys()) {
|
|
3432
|
-
flushPromises.push(this.flush(sessionId));
|
|
3433
|
-
}
|
|
3434
|
-
await Promise.all(flushPromises);
|
|
3435
|
-
}
|
|
3436
|
-
register(sessionId, context) {
|
|
3437
|
-
if (this.sessions.has(sessionId)) {
|
|
3438
|
-
return;
|
|
3439
|
-
}
|
|
3440
|
-
let otelWriter;
|
|
3441
|
-
if (this.otelConfig) {
|
|
3442
|
-
otelWriter = new OtelLogWriter(
|
|
3443
|
-
this.otelConfig,
|
|
3444
|
-
context,
|
|
3445
|
-
this.logger.child(`OtelWriter:${sessionId}`)
|
|
3446
|
-
);
|
|
3447
|
-
}
|
|
3448
|
-
this.sessions.set(sessionId, { context, otelWriter });
|
|
3449
|
-
}
|
|
3450
|
-
isRegistered(sessionId) {
|
|
3451
|
-
return this.sessions.has(sessionId);
|
|
3452
|
-
}
|
|
3453
|
-
appendRawLine(sessionId, line) {
|
|
3454
|
-
const session = this.sessions.get(sessionId);
|
|
3455
|
-
if (!session) {
|
|
3456
|
-
return;
|
|
3457
|
-
}
|
|
3458
|
-
try {
|
|
3459
|
-
const message = JSON.parse(line);
|
|
3460
|
-
const entry = {
|
|
3461
|
-
type: "notification",
|
|
3462
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3463
|
-
notification: message
|
|
3464
|
-
};
|
|
3465
|
-
if (session.otelWriter) {
|
|
3466
|
-
session.otelWriter.emit({ notification: entry });
|
|
3467
|
-
}
|
|
3468
|
-
if (this.posthogAPI) {
|
|
3469
|
-
const pending = this.pendingEntries.get(sessionId) ?? [];
|
|
3470
|
-
pending.push(entry);
|
|
3471
|
-
this.pendingEntries.set(sessionId, pending);
|
|
3472
|
-
this.scheduleFlush(sessionId);
|
|
3473
|
-
}
|
|
3474
|
-
} catch {
|
|
3475
|
-
this.logger.warn("Failed to parse raw line for persistence", {
|
|
3476
|
-
sessionId,
|
|
3477
|
-
lineLength: line.length
|
|
3478
|
-
});
|
|
3479
|
-
}
|
|
3480
|
-
}
|
|
3481
|
-
async flush(sessionId) {
|
|
3482
|
-
const session = this.sessions.get(sessionId);
|
|
3483
|
-
if (!session) return;
|
|
3484
|
-
if (session.otelWriter) {
|
|
3485
|
-
await session.otelWriter.flush();
|
|
3486
|
-
}
|
|
3487
|
-
const pending = this.pendingEntries.get(sessionId);
|
|
3488
|
-
if (!this.posthogAPI || !pending?.length) return;
|
|
3489
|
-
this.pendingEntries.delete(sessionId);
|
|
3490
|
-
const timeout = this.flushTimeouts.get(sessionId);
|
|
3491
|
-
if (timeout) {
|
|
3492
|
-
clearTimeout(timeout);
|
|
3493
|
-
this.flushTimeouts.delete(sessionId);
|
|
3494
|
-
}
|
|
3495
|
-
try {
|
|
3496
|
-
await this.posthogAPI.appendTaskRunLog(
|
|
3497
|
-
session.context.taskId,
|
|
3498
|
-
session.context.runId,
|
|
3499
|
-
pending
|
|
3500
|
-
);
|
|
3501
|
-
} catch (error) {
|
|
3502
|
-
this.logger.error("Failed to persist session logs:", error);
|
|
3503
|
-
}
|
|
3504
|
-
}
|
|
3505
|
-
scheduleFlush(sessionId) {
|
|
3506
|
-
const existing = this.flushTimeouts.get(sessionId);
|
|
3507
|
-
if (existing) clearTimeout(existing);
|
|
3508
|
-
const timeout = setTimeout(() => this.flush(sessionId), 500);
|
|
3509
|
-
this.flushTimeouts.set(sessionId, timeout);
|
|
3510
|
-
}
|
|
3511
|
-
};
|
|
3512
|
-
|
|
3513
|
-
// src/tree-tracker.ts
|
|
3514
|
-
import { isCommitOnRemote as gitIsCommitOnRemote } from "@twig/git/queries";
|
|
3515
|
-
|
|
3516
|
-
// src/sagas/apply-snapshot-saga.ts
|
|
3517
|
-
import { mkdir, rm, writeFile } from "fs/promises";
|
|
3518
|
-
import { join as join3 } from "path";
|
|
3519
|
-
import { Saga } from "@posthog/shared";
|
|
3520
|
-
import { ApplyTreeSaga as GitApplyTreeSaga } from "@twig/git/sagas/tree";
|
|
3521
|
-
var ApplySnapshotSaga = class extends Saga {
|
|
3522
|
-
archivePath = null;
|
|
3523
|
-
async execute(input) {
|
|
3524
|
-
const { snapshot, repositoryPath, apiClient, taskId, runId } = input;
|
|
3525
|
-
const tmpDir = join3(repositoryPath, ".posthog", "tmp");
|
|
3526
|
-
if (!snapshot.archiveUrl) {
|
|
3527
|
-
throw new Error("Cannot apply snapshot: no archive URL");
|
|
3528
|
-
}
|
|
3529
|
-
await this.step({
|
|
3530
|
-
name: "create_tmp_dir",
|
|
3531
|
-
execute: () => mkdir(tmpDir, { recursive: true }),
|
|
3532
|
-
rollback: async () => {
|
|
3533
|
-
}
|
|
3534
|
-
});
|
|
3535
|
-
this.archivePath = join3(tmpDir, `${snapshot.treeHash}.tar.gz`);
|
|
3536
|
-
await this.step({
|
|
3537
|
-
name: "download_archive",
|
|
3538
|
-
execute: async () => {
|
|
3539
|
-
const arrayBuffer = await apiClient.downloadArtifact(
|
|
3540
|
-
taskId,
|
|
3541
|
-
runId,
|
|
3542
|
-
snapshot.archiveUrl
|
|
3543
|
-
);
|
|
3544
|
-
if (!arrayBuffer) {
|
|
3545
|
-
throw new Error("Failed to download archive");
|
|
3546
|
-
}
|
|
3547
|
-
const base64Content = Buffer.from(arrayBuffer).toString("utf-8");
|
|
3548
|
-
const binaryContent = Buffer.from(base64Content, "base64");
|
|
3549
|
-
await writeFile(this.archivePath, binaryContent);
|
|
3550
|
-
},
|
|
3551
|
-
rollback: async () => {
|
|
3552
|
-
if (this.archivePath) {
|
|
3553
|
-
await rm(this.archivePath, { force: true }).catch(() => {
|
|
3554
|
-
});
|
|
3555
|
-
}
|
|
3556
|
-
}
|
|
3557
|
-
});
|
|
3558
|
-
const gitApplySaga = new GitApplyTreeSaga(this.log);
|
|
3559
|
-
const applyResult = await gitApplySaga.run({
|
|
3560
|
-
baseDir: repositoryPath,
|
|
3561
|
-
treeHash: snapshot.treeHash,
|
|
3562
|
-
baseCommit: snapshot.baseCommit,
|
|
3563
|
-
changes: snapshot.changes,
|
|
3564
|
-
archivePath: this.archivePath
|
|
3565
|
-
});
|
|
3566
|
-
if (!applyResult.success) {
|
|
3567
|
-
throw new Error(`Failed to apply tree: ${applyResult.error}`);
|
|
3568
|
-
}
|
|
3569
|
-
await rm(this.archivePath, { force: true }).catch(() => {
|
|
3570
|
-
});
|
|
3571
|
-
this.log.info("Tree snapshot applied", {
|
|
3572
|
-
treeHash: snapshot.treeHash,
|
|
3573
|
-
totalChanges: snapshot.changes.length,
|
|
3574
|
-
deletedFiles: snapshot.changes.filter((c) => c.status === "D").length
|
|
3575
|
-
});
|
|
3576
|
-
return { treeHash: snapshot.treeHash };
|
|
3577
|
-
}
|
|
3578
|
-
};
|
|
3579
|
-
|
|
3580
|
-
// src/sagas/capture-tree-saga.ts
|
|
3581
|
-
import { existsSync as existsSync4 } from "fs";
|
|
3582
|
-
import { readFile, rm as rm2 } from "fs/promises";
|
|
3583
|
-
import { join as join4 } from "path";
|
|
3584
|
-
import { Saga as Saga2 } from "@posthog/shared";
|
|
3585
|
-
import { CaptureTreeSaga as GitCaptureTreeSaga } from "@twig/git/sagas/tree";
|
|
3586
|
-
var CaptureTreeSaga = class extends Saga2 {
|
|
3587
|
-
async execute(input) {
|
|
3588
|
-
const {
|
|
3589
|
-
repositoryPath,
|
|
3590
|
-
lastTreeHash,
|
|
3591
|
-
interrupted,
|
|
3592
|
-
apiClient,
|
|
3593
|
-
taskId,
|
|
3594
|
-
runId
|
|
3595
|
-
} = input;
|
|
3596
|
-
const tmpDir = join4(repositoryPath, ".posthog", "tmp");
|
|
3597
|
-
if (existsSync4(join4(repositoryPath, ".gitmodules"))) {
|
|
3598
|
-
this.log.warn(
|
|
3599
|
-
"Repository has submodules - snapshot may not capture submodule state"
|
|
3600
|
-
);
|
|
3601
|
-
}
|
|
3602
|
-
const shouldArchive = !!apiClient;
|
|
3603
|
-
const archivePath = shouldArchive ? join4(tmpDir, `tree-${Date.now()}.tar.gz`) : void 0;
|
|
3604
|
-
const gitCaptureSaga = new GitCaptureTreeSaga(this.log);
|
|
3605
|
-
const captureResult = await gitCaptureSaga.run({
|
|
3606
|
-
baseDir: repositoryPath,
|
|
3607
|
-
lastTreeHash,
|
|
3608
|
-
archivePath
|
|
3609
|
-
});
|
|
3610
|
-
if (!captureResult.success) {
|
|
3611
|
-
throw new Error(`Failed to capture tree: ${captureResult.error}`);
|
|
3612
|
-
}
|
|
3613
|
-
const {
|
|
3614
|
-
snapshot: gitSnapshot,
|
|
3615
|
-
archivePath: createdArchivePath,
|
|
3616
|
-
changed
|
|
3617
|
-
} = captureResult.data;
|
|
3618
|
-
if (!changed || !gitSnapshot) {
|
|
3619
|
-
this.log.debug("No changes since last capture", { lastTreeHash });
|
|
3620
|
-
return { snapshot: null, newTreeHash: lastTreeHash };
|
|
3621
|
-
}
|
|
3622
|
-
let archiveUrl;
|
|
3623
|
-
if (apiClient && createdArchivePath) {
|
|
3624
|
-
try {
|
|
3625
|
-
archiveUrl = await this.uploadArchive(
|
|
3626
|
-
createdArchivePath,
|
|
3627
|
-
gitSnapshot.treeHash,
|
|
3628
|
-
apiClient,
|
|
3629
|
-
taskId,
|
|
3630
|
-
runId
|
|
3631
|
-
);
|
|
3632
|
-
} finally {
|
|
3633
|
-
await rm2(createdArchivePath, { force: true }).catch(() => {
|
|
3634
|
-
});
|
|
3635
|
-
}
|
|
3636
|
-
}
|
|
3637
|
-
const snapshot = {
|
|
3638
|
-
treeHash: gitSnapshot.treeHash,
|
|
3639
|
-
baseCommit: gitSnapshot.baseCommit,
|
|
3640
|
-
changes: gitSnapshot.changes,
|
|
3641
|
-
timestamp: gitSnapshot.timestamp,
|
|
3642
|
-
interrupted,
|
|
3643
|
-
archiveUrl
|
|
3644
|
-
};
|
|
3645
|
-
this.log.info("Tree captured", {
|
|
3646
|
-
treeHash: snapshot.treeHash,
|
|
3647
|
-
changes: snapshot.changes.length,
|
|
3648
|
-
interrupted,
|
|
3649
|
-
archiveUrl
|
|
3650
|
-
});
|
|
3651
|
-
return { snapshot, newTreeHash: snapshot.treeHash };
|
|
3652
|
-
}
|
|
3653
|
-
async uploadArchive(archivePath, treeHash, apiClient, taskId, runId) {
|
|
3654
|
-
const archiveUrl = await this.step({
|
|
3655
|
-
name: "upload_archive",
|
|
3656
|
-
execute: async () => {
|
|
3657
|
-
const archiveContent = await readFile(archivePath);
|
|
3658
|
-
const base64Content = archiveContent.toString("base64");
|
|
3659
|
-
const artifacts = await apiClient.uploadTaskArtifacts(taskId, runId, [
|
|
3660
|
-
{
|
|
3661
|
-
name: `trees/${treeHash}.tar.gz`,
|
|
3662
|
-
type: "tree_snapshot",
|
|
3663
|
-
content: base64Content,
|
|
3664
|
-
content_type: "application/gzip"
|
|
3665
|
-
}
|
|
3666
|
-
]);
|
|
3667
|
-
if (artifacts.length > 0 && artifacts[0].storage_path) {
|
|
3668
|
-
this.log.info("Tree archive uploaded", {
|
|
3669
|
-
storagePath: artifacts[0].storage_path,
|
|
3670
|
-
treeHash
|
|
3671
|
-
});
|
|
3672
|
-
return artifacts[0].storage_path;
|
|
3673
|
-
}
|
|
3674
|
-
return void 0;
|
|
3675
|
-
},
|
|
3676
|
-
rollback: async () => {
|
|
3677
|
-
await rm2(archivePath, { force: true }).catch(() => {
|
|
3678
|
-
});
|
|
3679
|
-
}
|
|
3680
|
-
});
|
|
3681
|
-
return archiveUrl;
|
|
3682
|
-
}
|
|
3683
|
-
};
|
|
3684
|
-
|
|
3685
|
-
// src/tree-tracker.ts
|
|
3686
|
-
var TreeTracker = class {
|
|
3687
|
-
repositoryPath;
|
|
3688
|
-
taskId;
|
|
3689
|
-
runId;
|
|
3690
|
-
apiClient;
|
|
3691
|
-
logger;
|
|
3692
|
-
lastTreeHash = null;
|
|
3693
|
-
constructor(config) {
|
|
3694
|
-
this.repositoryPath = config.repositoryPath;
|
|
3695
|
-
this.taskId = config.taskId;
|
|
3696
|
-
this.runId = config.runId;
|
|
3697
|
-
this.apiClient = config.apiClient;
|
|
3698
|
-
this.logger = config.logger || new Logger({ debug: false, prefix: "[TreeTracker]" });
|
|
3699
|
-
}
|
|
3700
|
-
/**
|
|
3701
|
-
* Capture current working tree state as a snapshot.
|
|
3702
|
-
* Uses a temporary index to avoid modifying user's staging area.
|
|
3703
|
-
* Uses Saga pattern for atomic operation with automatic cleanup on failure.
|
|
3704
|
-
*/
|
|
3705
|
-
async captureTree(options) {
|
|
3706
|
-
const saga = new CaptureTreeSaga(this.logger);
|
|
3707
|
-
const result = await saga.run({
|
|
3708
|
-
repositoryPath: this.repositoryPath,
|
|
3709
|
-
taskId: this.taskId,
|
|
3710
|
-
runId: this.runId,
|
|
3711
|
-
apiClient: this.apiClient,
|
|
3712
|
-
lastTreeHash: this.lastTreeHash,
|
|
3713
|
-
interrupted: options?.interrupted
|
|
3714
|
-
});
|
|
3715
|
-
if (!result.success) {
|
|
3716
|
-
this.logger.error("Failed to capture tree", {
|
|
3717
|
-
error: result.error,
|
|
3718
|
-
failedStep: result.failedStep
|
|
3719
|
-
});
|
|
3720
|
-
throw new Error(
|
|
3721
|
-
`Failed to capture tree at step '${result.failedStep}': ${result.error}`
|
|
3722
|
-
);
|
|
3723
|
-
}
|
|
3724
|
-
if (result.data.newTreeHash !== null) {
|
|
3725
|
-
this.lastTreeHash = result.data.newTreeHash;
|
|
3726
|
-
}
|
|
3727
|
-
return result.data.snapshot;
|
|
3728
|
-
}
|
|
3729
|
-
/**
|
|
3730
|
-
* Download and apply a tree snapshot.
|
|
3731
|
-
* Uses Saga pattern for atomic operation with rollback on failure.
|
|
3732
|
-
*/
|
|
3733
|
-
async applyTreeSnapshot(snapshot) {
|
|
3734
|
-
if (!this.apiClient) {
|
|
3735
|
-
throw new Error("Cannot apply snapshot: API client not configured");
|
|
3736
|
-
}
|
|
3737
|
-
if (!snapshot.archiveUrl) {
|
|
3738
|
-
this.logger.warn("Cannot apply snapshot: no archive URL", {
|
|
3739
|
-
treeHash: snapshot.treeHash,
|
|
3740
|
-
changes: snapshot.changes.length
|
|
3741
|
-
});
|
|
3742
|
-
throw new Error("Cannot apply snapshot: no archive URL");
|
|
3743
|
-
}
|
|
3744
|
-
const saga = new ApplySnapshotSaga(this.logger);
|
|
3745
|
-
const result = await saga.run({
|
|
3746
|
-
snapshot,
|
|
3747
|
-
repositoryPath: this.repositoryPath,
|
|
3748
|
-
apiClient: this.apiClient,
|
|
3749
|
-
taskId: this.taskId,
|
|
3750
|
-
runId: this.runId
|
|
3751
|
-
});
|
|
3752
|
-
if (!result.success) {
|
|
3753
|
-
this.logger.error("Failed to apply tree snapshot", {
|
|
3754
|
-
error: result.error,
|
|
3755
|
-
failedStep: result.failedStep,
|
|
3756
|
-
treeHash: snapshot.treeHash
|
|
3757
|
-
});
|
|
3758
|
-
throw new Error(
|
|
3759
|
-
`Failed to apply snapshot at step '${result.failedStep}': ${result.error}`
|
|
3760
|
-
);
|
|
3761
|
-
}
|
|
3762
|
-
this.lastTreeHash = result.data.treeHash;
|
|
3763
|
-
}
|
|
3764
|
-
/**
|
|
3765
|
-
* Get the last captured tree hash.
|
|
3766
|
-
*/
|
|
3767
|
-
getLastTreeHash() {
|
|
3768
|
-
return this.lastTreeHash;
|
|
3769
|
-
}
|
|
3770
|
-
/**
|
|
3771
|
-
* Set the last tree hash (used when resuming).
|
|
3772
|
-
*/
|
|
3773
|
-
setLastTreeHash(hash) {
|
|
3774
|
-
this.lastTreeHash = hash;
|
|
3775
|
-
}
|
|
3776
|
-
};
|
|
3777
|
-
|
|
3778
|
-
// src/utils/async-mutex.ts
|
|
3779
|
-
var AsyncMutex = class {
|
|
3780
|
-
locked = false;
|
|
3781
|
-
queue = [];
|
|
3782
|
-
async acquire() {
|
|
3783
|
-
if (!this.locked) {
|
|
3784
|
-
this.locked = true;
|
|
3785
|
-
return;
|
|
3786
|
-
}
|
|
3787
|
-
return new Promise((resolve3) => {
|
|
3788
|
-
this.queue.push(resolve3);
|
|
3789
|
-
});
|
|
3790
|
-
}
|
|
3791
|
-
release() {
|
|
3792
|
-
const next = this.queue.shift();
|
|
3793
|
-
if (next) {
|
|
3794
|
-
next();
|
|
3795
|
-
} else {
|
|
3796
|
-
this.locked = false;
|
|
3797
|
-
}
|
|
3798
|
-
}
|
|
3799
|
-
isLocked() {
|
|
3800
|
-
return this.locked;
|
|
3801
|
-
}
|
|
3802
|
-
get queueLength() {
|
|
3803
|
-
return this.queue.length;
|
|
3804
|
-
}
|
|
3805
|
-
};
|
|
3806
|
-
|
|
3807
|
-
// src/server/jwt.ts
|
|
3808
|
-
import jwt from "jsonwebtoken";
|
|
3809
|
-
import { z as z2 } from "zod";
|
|
3810
|
-
var SANDBOX_CONNECTION_AUDIENCE = "posthog:sandbox_connection";
|
|
3811
|
-
var userDataSchema = z2.object({
|
|
3812
|
-
run_id: z2.string(),
|
|
3813
|
-
task_id: z2.string(),
|
|
3814
|
-
team_id: z2.number(),
|
|
3815
|
-
user_id: z2.number(),
|
|
3816
|
-
distinct_id: z2.string(),
|
|
3817
|
-
mode: z2.enum(["interactive", "background"]).optional().default("interactive")
|
|
3818
|
-
});
|
|
3819
|
-
var jwtPayloadSchema = userDataSchema.extend({
|
|
3820
|
-
exp: z2.number(),
|
|
3821
|
-
iat: z2.number().optional(),
|
|
3822
|
-
aud: z2.string().optional()
|
|
3823
|
-
});
|
|
3824
|
-
var JwtValidationError = class extends Error {
|
|
3825
|
-
constructor(message, code) {
|
|
3826
|
-
super(message);
|
|
3827
|
-
this.code = code;
|
|
3828
|
-
this.name = "JwtValidationError";
|
|
3829
|
-
}
|
|
3830
|
-
};
|
|
3831
|
-
function validateJwt(token, publicKey) {
|
|
3832
|
-
try {
|
|
3833
|
-
const decoded = jwt.verify(token, publicKey, {
|
|
3834
|
-
algorithms: ["RS256"],
|
|
3835
|
-
audience: SANDBOX_CONNECTION_AUDIENCE
|
|
3836
|
-
});
|
|
3837
|
-
const result = jwtPayloadSchema.safeParse(decoded);
|
|
3838
|
-
if (!result.success) {
|
|
3839
|
-
throw new JwtValidationError(
|
|
3840
|
-
`Missing required fields: ${result.error.message}`,
|
|
3841
|
-
"invalid_token"
|
|
3842
|
-
);
|
|
3843
|
-
}
|
|
3844
|
-
return result.data;
|
|
3845
|
-
} catch (error) {
|
|
3846
|
-
if (error instanceof JwtValidationError) {
|
|
3847
|
-
throw error;
|
|
3848
|
-
}
|
|
3849
|
-
if (error instanceof jwt.TokenExpiredError) {
|
|
3850
|
-
throw new JwtValidationError("Token expired", "expired");
|
|
3851
|
-
}
|
|
3852
|
-
if (error instanceof jwt.JsonWebTokenError) {
|
|
3853
|
-
throw new JwtValidationError("Invalid signature", "invalid_signature");
|
|
3854
|
-
}
|
|
3855
|
-
throw new JwtValidationError("Invalid token", "invalid_token");
|
|
3856
|
-
}
|
|
3857
|
-
}
|
|
3858
|
-
|
|
3859
|
-
// src/server/schemas.ts
|
|
3860
|
-
import { z as z3 } from "zod";
|
|
3861
|
-
var jsonRpcRequestSchema = z3.object({
|
|
3862
|
-
jsonrpc: z3.literal("2.0"),
|
|
3863
|
-
method: z3.string(),
|
|
3864
|
-
params: z3.record(z3.unknown()).optional(),
|
|
3865
|
-
id: z3.union([z3.string(), z3.number()]).optional()
|
|
3866
|
-
});
|
|
3867
|
-
var userMessageParamsSchema = z3.object({
|
|
3868
|
-
content: z3.string().min(1, "Content is required")
|
|
3869
|
-
});
|
|
3870
|
-
var commandParamsSchemas = {
|
|
3871
|
-
user_message: userMessageParamsSchema,
|
|
3872
|
-
"posthog/user_message": userMessageParamsSchema,
|
|
3873
|
-
cancel: z3.object({}).optional(),
|
|
3874
|
-
"posthog/cancel": z3.object({}).optional(),
|
|
3875
|
-
close: z3.object({}).optional(),
|
|
3876
|
-
"posthog/close": z3.object({}).optional()
|
|
3877
|
-
};
|
|
3878
|
-
function validateCommandParams(method, params) {
|
|
3879
|
-
const schema = commandParamsSchemas[method] ?? commandParamsSchemas[method.replace("posthog/", "")];
|
|
3880
|
-
if (!schema) {
|
|
3881
|
-
return { success: false, error: `Unknown method: ${method}` };
|
|
3882
|
-
}
|
|
3883
|
-
const result = schema.safeParse(params);
|
|
3884
|
-
if (!result.success) {
|
|
3885
|
-
return { success: false, error: result.error.message };
|
|
3886
|
-
}
|
|
3887
|
-
return { success: true, data: result.data };
|
|
3888
|
-
}
|
|
3889
|
-
|
|
3890
|
-
// src/server/agent-server.ts
|
|
3891
|
-
var NdJsonTap = class {
|
|
3892
|
-
constructor(onMessage) {
|
|
3893
|
-
this.onMessage = onMessage;
|
|
3894
|
-
}
|
|
3895
|
-
decoder = new TextDecoder();
|
|
3896
|
-
buffer = "";
|
|
3897
|
-
process(chunk) {
|
|
3898
|
-
this.buffer += this.decoder.decode(chunk, { stream: true });
|
|
3899
|
-
const lines = this.buffer.split("\n");
|
|
3900
|
-
this.buffer = lines.pop() ?? "";
|
|
3901
|
-
for (const line of lines) {
|
|
3902
|
-
if (!line.trim()) continue;
|
|
3903
|
-
try {
|
|
3904
|
-
this.onMessage(JSON.parse(line));
|
|
3905
|
-
} catch {
|
|
3906
|
-
}
|
|
3907
|
-
}
|
|
3908
|
-
}
|
|
3909
|
-
};
|
|
3910
|
-
function createTappedReadableStream(underlying, onMessage, logger) {
|
|
3911
|
-
const reader = underlying.getReader();
|
|
3912
|
-
const tap = new NdJsonTap(onMessage);
|
|
3913
|
-
return new ReadableStream({
|
|
3914
|
-
async pull(controller) {
|
|
3915
|
-
try {
|
|
3916
|
-
const { value, done } = await reader.read();
|
|
3917
|
-
if (done) {
|
|
3918
|
-
controller.close();
|
|
3919
|
-
return;
|
|
3920
|
-
}
|
|
3921
|
-
tap.process(value);
|
|
3922
|
-
controller.enqueue(value);
|
|
3923
|
-
} catch (error) {
|
|
3924
|
-
logger?.debug("Read failed, closing stream", error);
|
|
3925
|
-
controller.close();
|
|
3926
|
-
}
|
|
3927
|
-
},
|
|
3928
|
-
cancel() {
|
|
3929
|
-
reader.releaseLock();
|
|
3930
|
-
}
|
|
3931
|
-
});
|
|
3932
|
-
}
|
|
3933
|
-
function createTappedWritableStream2(underlying, onMessage, logger) {
|
|
3934
|
-
const tap = new NdJsonTap(onMessage);
|
|
3935
|
-
const mutex = new AsyncMutex();
|
|
3936
|
-
return new WritableStream({
|
|
3937
|
-
async write(chunk) {
|
|
3938
|
-
tap.process(chunk);
|
|
3939
|
-
await mutex.acquire();
|
|
3940
|
-
try {
|
|
3941
|
-
const writer = underlying.getWriter();
|
|
3942
|
-
await writer.write(chunk);
|
|
3943
|
-
writer.releaseLock();
|
|
3944
|
-
} catch (error) {
|
|
3945
|
-
logger?.debug("Write failed (stream may be closed)", error);
|
|
3946
|
-
} finally {
|
|
3947
|
-
mutex.release();
|
|
3948
|
-
}
|
|
3949
|
-
},
|
|
3950
|
-
async close() {
|
|
3951
|
-
await mutex.acquire();
|
|
3952
|
-
try {
|
|
3953
|
-
const writer = underlying.getWriter();
|
|
3954
|
-
await writer.close();
|
|
3955
|
-
writer.releaseLock();
|
|
3956
|
-
} catch (error) {
|
|
3957
|
-
logger?.debug("Close failed (stream may be closed)", error);
|
|
3958
|
-
} finally {
|
|
3959
|
-
mutex.release();
|
|
3960
|
-
}
|
|
3961
|
-
},
|
|
3962
|
-
async abort(reason) {
|
|
3963
|
-
await mutex.acquire();
|
|
3964
|
-
try {
|
|
3965
|
-
const writer = underlying.getWriter();
|
|
3966
|
-
await writer.abort(reason);
|
|
3967
|
-
writer.releaseLock();
|
|
3968
|
-
} catch (error) {
|
|
3969
|
-
logger?.debug("Abort failed (stream may be closed)", error);
|
|
3970
|
-
} finally {
|
|
3971
|
-
mutex.release();
|
|
3972
|
-
}
|
|
3973
|
-
}
|
|
3974
|
-
});
|
|
3975
|
-
}
|
|
3976
|
-
var AgentServer = class {
|
|
3977
|
-
config;
|
|
3978
|
-
logger;
|
|
3979
|
-
server = null;
|
|
3980
|
-
session = null;
|
|
3981
|
-
app;
|
|
3982
|
-
posthogAPI;
|
|
3983
|
-
constructor(config) {
|
|
3984
|
-
this.config = config;
|
|
3985
|
-
this.logger = new Logger({ debug: true, prefix: "[AgentServer]" });
|
|
3986
|
-
this.posthogAPI = new PostHogAPIClient({
|
|
3987
|
-
apiUrl: config.apiUrl,
|
|
3988
|
-
projectId: config.projectId,
|
|
3989
|
-
getApiKey: () => config.apiKey
|
|
3990
|
-
});
|
|
3991
|
-
this.app = this.createApp();
|
|
3992
|
-
}
|
|
3993
|
-
getEffectiveMode(payload) {
|
|
3994
|
-
return payload.mode ?? this.config.mode;
|
|
3995
|
-
}
|
|
3996
|
-
createApp() {
|
|
3997
|
-
const app = new Hono();
|
|
3998
|
-
app.get("/health", (c) => {
|
|
3999
|
-
return c.json({ status: "ok", hasSession: !!this.session });
|
|
4000
|
-
});
|
|
4001
|
-
app.get("/events", async (c) => {
|
|
4002
|
-
let payload;
|
|
4003
|
-
try {
|
|
4004
|
-
payload = this.authenticateRequest(c.req.header.bind(c.req));
|
|
4005
|
-
} catch (error) {
|
|
4006
|
-
return c.json(
|
|
4007
|
-
{
|
|
4008
|
-
error: error instanceof JwtValidationError ? error.message : "Invalid token",
|
|
4009
|
-
code: error instanceof JwtValidationError ? error.code : "invalid_token"
|
|
4010
|
-
},
|
|
4011
|
-
401
|
|
4012
|
-
);
|
|
4013
|
-
}
|
|
4014
|
-
const stream = new ReadableStream({
|
|
4015
|
-
start: async (controller) => {
|
|
4016
|
-
const sseController = {
|
|
4017
|
-
send: (data) => {
|
|
4018
|
-
try {
|
|
4019
|
-
controller.enqueue(
|
|
4020
|
-
new TextEncoder().encode(`data: ${JSON.stringify(data)}
|
|
4021
|
-
|
|
4022
|
-
`)
|
|
4023
|
-
);
|
|
4024
|
-
} catch (error) {
|
|
4025
|
-
this.logger.debug(
|
|
4026
|
-
"SSE send failed (stream may be closed)",
|
|
4027
|
-
error
|
|
4028
|
-
);
|
|
4029
|
-
}
|
|
4030
|
-
},
|
|
4031
|
-
close: () => {
|
|
4032
|
-
try {
|
|
4033
|
-
controller.close();
|
|
4034
|
-
} catch (error) {
|
|
4035
|
-
this.logger.debug("SSE close failed (already closed)", error);
|
|
4036
|
-
}
|
|
4037
|
-
}
|
|
4038
|
-
};
|
|
4039
|
-
if (!this.session || this.session.payload.run_id !== payload.run_id) {
|
|
4040
|
-
await this.initializeSession(payload, sseController);
|
|
4041
|
-
} else {
|
|
4042
|
-
this.session.sseController = sseController;
|
|
4043
|
-
}
|
|
4044
|
-
this.sendSseEvent(sseController, {
|
|
4045
|
-
type: "connected",
|
|
4046
|
-
run_id: payload.run_id
|
|
4047
|
-
});
|
|
4048
|
-
},
|
|
4049
|
-
cancel: () => {
|
|
4050
|
-
this.logger.info("SSE connection closed");
|
|
4051
|
-
if (this.session?.sseController) {
|
|
4052
|
-
this.session.sseController = null;
|
|
4053
|
-
}
|
|
4054
|
-
}
|
|
4055
|
-
});
|
|
4056
|
-
return new Response(stream, {
|
|
4057
|
-
headers: {
|
|
4058
|
-
"Content-Type": "text/event-stream",
|
|
4059
|
-
"Cache-Control": "no-cache",
|
|
4060
|
-
Connection: "keep-alive"
|
|
4061
|
-
}
|
|
4062
|
-
});
|
|
4063
|
-
});
|
|
4064
|
-
app.post("/command", async (c) => {
|
|
4065
|
-
let payload;
|
|
4066
|
-
try {
|
|
4067
|
-
payload = this.authenticateRequest(c.req.header.bind(c.req));
|
|
4068
|
-
} catch (error) {
|
|
4069
|
-
return c.json(
|
|
4070
|
-
{
|
|
4071
|
-
error: error instanceof JwtValidationError ? error.message : "Invalid token"
|
|
4072
|
-
},
|
|
4073
|
-
401
|
|
4074
|
-
);
|
|
4075
|
-
}
|
|
4076
|
-
if (!this.session || this.session.payload.run_id !== payload.run_id) {
|
|
4077
|
-
return c.json({ error: "No active session for this run" }, 400);
|
|
4078
|
-
}
|
|
4079
|
-
const rawBody = await c.req.json().catch(() => null);
|
|
4080
|
-
const parseResult = jsonRpcRequestSchema.safeParse(rawBody);
|
|
4081
|
-
if (!parseResult.success) {
|
|
4082
|
-
return c.json({ error: "Invalid JSON-RPC request" }, 400);
|
|
4083
|
-
}
|
|
4084
|
-
const command = parseResult.data;
|
|
4085
|
-
const paramsValidation = validateCommandParams(
|
|
4086
|
-
command.method,
|
|
4087
|
-
command.params ?? {}
|
|
4088
|
-
);
|
|
4089
|
-
if (!paramsValidation.success) {
|
|
4090
|
-
return c.json(
|
|
4091
|
-
{
|
|
4092
|
-
jsonrpc: "2.0",
|
|
4093
|
-
id: command.id,
|
|
4094
|
-
error: {
|
|
4095
|
-
code: -32602,
|
|
4096
|
-
message: paramsValidation.error
|
|
4097
|
-
}
|
|
4098
|
-
},
|
|
4099
|
-
200
|
|
4100
|
-
);
|
|
4101
|
-
}
|
|
4102
|
-
try {
|
|
4103
|
-
const result = await this.executeCommand(
|
|
4104
|
-
command.method,
|
|
4105
|
-
command.params || {}
|
|
4106
|
-
);
|
|
4107
|
-
return c.json({
|
|
4108
|
-
jsonrpc: "2.0",
|
|
4109
|
-
id: command.id,
|
|
4110
|
-
result
|
|
4111
|
-
});
|
|
4112
|
-
} catch (error) {
|
|
4113
|
-
return c.json({
|
|
4114
|
-
jsonrpc: "2.0",
|
|
4115
|
-
id: command.id,
|
|
4116
|
-
error: {
|
|
4117
|
-
code: -32e3,
|
|
4118
|
-
message: error instanceof Error ? error.message : "Unknown error"
|
|
4119
|
-
}
|
|
4120
|
-
});
|
|
4121
|
-
}
|
|
4122
|
-
});
|
|
4123
|
-
app.notFound((c) => {
|
|
4124
|
-
return c.json({ error: "Not found" }, 404);
|
|
4125
|
-
});
|
|
4126
|
-
return app;
|
|
4127
|
-
}
|
|
4128
|
-
async start() {
|
|
4129
|
-
await new Promise((resolve3) => {
|
|
4130
|
-
this.server = serve(
|
|
4131
|
-
{
|
|
4132
|
-
fetch: this.app.fetch,
|
|
4133
|
-
port: this.config.port
|
|
4134
|
-
},
|
|
4135
|
-
() => {
|
|
4136
|
-
this.logger.info(`HTTP server listening on port ${this.config.port}`);
|
|
4137
|
-
resolve3();
|
|
4138
|
-
}
|
|
4139
|
-
);
|
|
4140
|
-
});
|
|
4141
|
-
await this.autoInitializeSession();
|
|
4142
|
-
}
|
|
4143
|
-
async autoInitializeSession() {
|
|
4144
|
-
const { taskId, runId, mode, projectId } = this.config;
|
|
4145
|
-
this.logger.info("Auto-initializing session", { taskId, runId, mode });
|
|
4146
|
-
const payload = {
|
|
4147
|
-
task_id: taskId,
|
|
4148
|
-
run_id: runId,
|
|
4149
|
-
team_id: projectId,
|
|
4150
|
-
user_id: 0,
|
|
4151
|
-
// System-initiated
|
|
4152
|
-
distinct_id: "agent-server",
|
|
4153
|
-
mode
|
|
4154
|
-
};
|
|
4155
|
-
await this.initializeSession(payload, null);
|
|
4156
|
-
}
|
|
4157
|
-
async stop() {
|
|
4158
|
-
this.logger.info("Stopping agent server...");
|
|
4159
|
-
if (this.session) {
|
|
4160
|
-
await this.cleanupSession();
|
|
4161
|
-
}
|
|
4162
|
-
if (this.server) {
|
|
4163
|
-
this.server.close();
|
|
4164
|
-
this.server = null;
|
|
4165
|
-
}
|
|
4166
|
-
this.logger.info("Agent server stopped");
|
|
4167
|
-
}
|
|
4168
|
-
authenticateRequest(getHeader) {
|
|
4169
|
-
if (!this.config.jwtPublicKey) {
|
|
4170
|
-
throw new JwtValidationError(
|
|
4171
|
-
"Server not configured with JWT public key",
|
|
4172
|
-
"server_error"
|
|
4173
|
-
);
|
|
4174
|
-
}
|
|
4175
|
-
const authHeader = getHeader("authorization");
|
|
4176
|
-
if (!authHeader?.startsWith("Bearer ")) {
|
|
4177
|
-
throw new JwtValidationError(
|
|
4178
|
-
"Missing authorization header",
|
|
4179
|
-
"invalid_token"
|
|
4180
|
-
);
|
|
4181
|
-
}
|
|
4182
|
-
const token = authHeader.slice(7);
|
|
4183
|
-
return validateJwt(token, this.config.jwtPublicKey);
|
|
4184
|
-
}
|
|
4185
|
-
async executeCommand(method, params) {
|
|
4186
|
-
if (!this.session) {
|
|
4187
|
-
throw new Error("No active session");
|
|
4188
|
-
}
|
|
4189
|
-
switch (method) {
|
|
4190
|
-
case POSTHOG_NOTIFICATIONS.USER_MESSAGE:
|
|
4191
|
-
case "user_message": {
|
|
4192
|
-
const content = params.content;
|
|
4193
|
-
this.logger.info(
|
|
4194
|
-
`Processing user message: ${content.substring(0, 100)}...`
|
|
4195
|
-
);
|
|
4196
|
-
const result = await this.session.clientConnection.prompt({
|
|
4197
|
-
sessionId: this.session.payload.run_id,
|
|
4198
|
-
prompt: [{ type: "text", text: content }]
|
|
4199
|
-
});
|
|
4200
|
-
return { stopReason: result.stopReason };
|
|
4201
|
-
}
|
|
4202
|
-
case POSTHOG_NOTIFICATIONS.CANCEL:
|
|
4203
|
-
case "cancel": {
|
|
4204
|
-
this.logger.info("Cancel requested");
|
|
4205
|
-
await this.session.clientConnection.cancel({
|
|
4206
|
-
sessionId: this.session.payload.run_id
|
|
4207
|
-
});
|
|
4208
|
-
return { cancelled: true };
|
|
4209
|
-
}
|
|
4210
|
-
case POSTHOG_NOTIFICATIONS.CLOSE:
|
|
4211
|
-
case "close": {
|
|
4212
|
-
this.logger.info("Close requested");
|
|
4213
|
-
await this.cleanupSession();
|
|
4214
|
-
return { closed: true };
|
|
4215
|
-
}
|
|
4216
|
-
default:
|
|
4217
|
-
throw new Error(`Unknown method: ${method}`);
|
|
4218
|
-
}
|
|
4219
|
-
}
|
|
4220
|
-
async initializeSession(payload, sseController) {
|
|
4221
|
-
if (this.session) {
|
|
4222
|
-
await this.cleanupSession();
|
|
4223
|
-
}
|
|
4224
|
-
this.logger.info("Initializing session", {
|
|
4225
|
-
runId: payload.run_id,
|
|
4226
|
-
taskId: payload.task_id
|
|
4227
|
-
});
|
|
4228
|
-
const deviceInfo = {
|
|
4229
|
-
type: "cloud",
|
|
4230
|
-
name: process.env.HOSTNAME || "cloud-sandbox"
|
|
4231
|
-
};
|
|
4232
|
-
this.configureEnvironment();
|
|
4233
|
-
const treeTracker = new TreeTracker({
|
|
4234
|
-
repositoryPath: this.config.repositoryPath,
|
|
4235
|
-
taskId: payload.task_id,
|
|
4236
|
-
runId: payload.run_id,
|
|
4237
|
-
logger: new Logger({ debug: true, prefix: "[TreeTracker]" })
|
|
4238
|
-
});
|
|
4239
|
-
const _posthogAPI = new PostHogAPIClient({
|
|
4240
|
-
apiUrl: this.config.apiUrl,
|
|
4241
|
-
projectId: this.config.projectId,
|
|
4242
|
-
getApiKey: () => this.config.apiKey
|
|
4243
|
-
});
|
|
4244
|
-
const logWriter = new SessionLogWriter({
|
|
4245
|
-
otelConfig: {
|
|
4246
|
-
posthogHost: this.config.apiUrl,
|
|
4247
|
-
apiKey: this.config.apiKey,
|
|
4248
|
-
logsPath: "/i/v1/agent-logs"
|
|
4249
|
-
},
|
|
4250
|
-
logger: new Logger({ debug: true, prefix: "[SessionLogWriter]" })
|
|
4251
|
-
});
|
|
4252
|
-
const acpConnection = createAcpConnection({
|
|
4253
|
-
taskRunId: payload.run_id,
|
|
4254
|
-
taskId: payload.task_id,
|
|
4255
|
-
deviceType: deviceInfo.type,
|
|
4256
|
-
logWriter
|
|
4257
|
-
});
|
|
4258
|
-
const onAcpMessage = (message) => {
|
|
4259
|
-
this.broadcastEvent({
|
|
4260
|
-
type: "notification",
|
|
4261
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4262
|
-
notification: message
|
|
4263
|
-
});
|
|
4264
|
-
};
|
|
4265
|
-
const tappedReadable = createTappedReadableStream(
|
|
4266
|
-
acpConnection.clientStreams.readable,
|
|
4267
|
-
onAcpMessage,
|
|
4268
|
-
this.logger
|
|
4269
|
-
);
|
|
4270
|
-
const tappedWritable = createTappedWritableStream2(
|
|
4271
|
-
acpConnection.clientStreams.writable,
|
|
4272
|
-
onAcpMessage,
|
|
4273
|
-
this.logger
|
|
4274
|
-
);
|
|
4275
|
-
const clientStream = ndJsonStream2(tappedWritable, tappedReadable);
|
|
4276
|
-
const clientConnection = new ClientSideConnection(
|
|
4277
|
-
() => this.createCloudClient(payload),
|
|
4278
|
-
clientStream
|
|
4279
|
-
);
|
|
4280
|
-
await clientConnection.initialize({
|
|
4281
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
4282
|
-
clientCapabilities: {}
|
|
4283
|
-
});
|
|
4284
|
-
await clientConnection.newSession({
|
|
4285
|
-
cwd: this.config.repositoryPath,
|
|
4286
|
-
mcpServers: [],
|
|
4287
|
-
_meta: { sessionId: payload.run_id }
|
|
4288
|
-
});
|
|
4289
|
-
this.session = {
|
|
4290
|
-
payload,
|
|
4291
|
-
acpConnection,
|
|
4292
|
-
clientConnection,
|
|
4293
|
-
treeTracker,
|
|
4294
|
-
sseController,
|
|
4295
|
-
deviceInfo,
|
|
4296
|
-
logWriter
|
|
4297
|
-
};
|
|
4298
|
-
this.logger.info("Session initialized successfully");
|
|
4299
|
-
await this.sendInitialTaskMessage(payload);
|
|
4300
|
-
}
|
|
4301
|
-
async sendInitialTaskMessage(payload) {
|
|
4302
|
-
if (!this.session) return;
|
|
4303
|
-
try {
|
|
4304
|
-
this.logger.info("Fetching task details", { taskId: payload.task_id });
|
|
4305
|
-
const task = await this.posthogAPI.getTask(payload.task_id);
|
|
4306
|
-
if (!task.description) {
|
|
4307
|
-
this.logger.warn("Task has no description, skipping initial message");
|
|
4308
|
-
return;
|
|
4309
|
-
}
|
|
4310
|
-
this.logger.info("Sending initial task message", {
|
|
4311
|
-
taskId: payload.task_id,
|
|
4312
|
-
descriptionLength: task.description.length
|
|
4313
|
-
});
|
|
4314
|
-
const result = await this.session.clientConnection.prompt({
|
|
4315
|
-
sessionId: payload.run_id,
|
|
4316
|
-
prompt: [{ type: "text", text: task.description }]
|
|
4317
|
-
});
|
|
4318
|
-
this.logger.info("Initial task message completed", {
|
|
4319
|
-
stopReason: result.stopReason
|
|
4320
|
-
});
|
|
4321
|
-
const mode = this.getEffectiveMode(payload);
|
|
4322
|
-
if (mode === "background") {
|
|
4323
|
-
await this.signalTaskComplete(payload, result.stopReason);
|
|
4324
|
-
} else {
|
|
4325
|
-
this.logger.info("Interactive mode - staying open for conversation");
|
|
4326
|
-
}
|
|
4327
|
-
} catch (error) {
|
|
4328
|
-
this.logger.error("Failed to send initial task message", error);
|
|
4329
|
-
const mode = this.getEffectiveMode(payload);
|
|
4330
|
-
if (mode === "background") {
|
|
4331
|
-
await this.signalTaskComplete(payload, "error");
|
|
4332
|
-
}
|
|
4333
|
-
}
|
|
4334
|
-
}
|
|
4335
|
-
async signalTaskComplete(payload, stopReason) {
|
|
4336
|
-
const status = stopReason === "cancelled" ? "cancelled" : stopReason === "error" ? "failed" : "completed";
|
|
4337
|
-
try {
|
|
4338
|
-
await this.posthogAPI.updateTaskRun(payload.task_id, payload.run_id, {
|
|
4339
|
-
status,
|
|
4340
|
-
error_message: stopReason === "error" ? "Agent error" : void 0
|
|
4341
|
-
});
|
|
4342
|
-
this.logger.info("Task completion signaled", { status, stopReason });
|
|
4343
|
-
} catch (error) {
|
|
4344
|
-
this.logger.error("Failed to signal task completion", error);
|
|
4345
|
-
}
|
|
4346
|
-
}
|
|
4347
|
-
configureEnvironment() {
|
|
4348
|
-
const { apiKey, apiUrl, projectId } = this.config;
|
|
4349
|
-
const gatewayUrl = process.env.LLM_GATEWAY_URL || getLlmGatewayUrl(apiUrl);
|
|
4350
|
-
const openaiBaseUrl = gatewayUrl.endsWith("/v1") ? gatewayUrl : `${gatewayUrl}/v1`;
|
|
4351
|
-
Object.assign(process.env, {
|
|
4352
|
-
// PostHog
|
|
4353
|
-
POSTHOG_API_KEY: apiKey,
|
|
4354
|
-
POSTHOG_API_URL: apiUrl,
|
|
4355
|
-
POSTHOG_API_HOST: apiUrl,
|
|
4356
|
-
POSTHOG_AUTH_HEADER: `Bearer ${apiKey}`,
|
|
4357
|
-
POSTHOG_PROJECT_ID: String(projectId),
|
|
4358
|
-
// Anthropic
|
|
4359
|
-
ANTHROPIC_API_KEY: apiKey,
|
|
4360
|
-
ANTHROPIC_AUTH_TOKEN: apiKey,
|
|
4361
|
-
ANTHROPIC_BASE_URL: gatewayUrl,
|
|
4362
|
-
// OpenAI (for models like GPT-4, o1, etc.)
|
|
4363
|
-
OPENAI_API_KEY: apiKey,
|
|
4364
|
-
OPENAI_BASE_URL: openaiBaseUrl,
|
|
4365
|
-
// Generic gateway
|
|
4366
|
-
LLM_GATEWAY_URL: gatewayUrl
|
|
4367
|
-
});
|
|
4368
|
-
}
|
|
4369
|
-
createCloudClient(payload) {
|
|
4370
|
-
const mode = this.getEffectiveMode(payload);
|
|
4371
|
-
return {
|
|
4372
|
-
requestPermission: async (params) => {
|
|
4373
|
-
this.logger.debug("Permission request", {
|
|
4374
|
-
mode,
|
|
4375
|
-
options: params.options
|
|
4376
|
-
});
|
|
4377
|
-
const allowOption = params.options.find(
|
|
4378
|
-
(o) => o.kind === "allow_once" || o.kind === "allow_always"
|
|
4379
|
-
);
|
|
4380
|
-
return {
|
|
4381
|
-
outcome: {
|
|
4382
|
-
outcome: "selected",
|
|
4383
|
-
optionId: allowOption?.optionId ?? params.options[0].optionId
|
|
4384
|
-
}
|
|
4385
|
-
};
|
|
4386
|
-
},
|
|
4387
|
-
sessionUpdate: async (params) => {
|
|
4388
|
-
if (params.update?.sessionUpdate === "tool_call_update") {
|
|
4389
|
-
const meta = params.update?._meta?.claudeCode;
|
|
4390
|
-
const toolName = meta?.toolName;
|
|
4391
|
-
const toolResponse = meta?.toolResponse;
|
|
4392
|
-
if ((toolName === "Write" || toolName === "Edit") && toolResponse?.filePath) {
|
|
4393
|
-
await this.captureTreeState();
|
|
4394
|
-
}
|
|
4395
|
-
}
|
|
4396
|
-
}
|
|
4397
|
-
};
|
|
4398
|
-
}
|
|
4399
|
-
async cleanupSession() {
|
|
4400
|
-
if (!this.session) return;
|
|
4401
|
-
this.logger.info("Cleaning up session");
|
|
4402
|
-
try {
|
|
4403
|
-
await this.captureTreeState();
|
|
4404
|
-
} catch (error) {
|
|
4405
|
-
this.logger.error("Failed to capture final tree state", error);
|
|
4406
|
-
}
|
|
4407
|
-
try {
|
|
4408
|
-
await this.session.logWriter.flush(this.session.payload.run_id);
|
|
4409
|
-
} catch (error) {
|
|
4410
|
-
this.logger.error("Failed to flush session logs", error);
|
|
4411
|
-
}
|
|
4412
|
-
try {
|
|
4413
|
-
await this.session.acpConnection.cleanup();
|
|
4414
|
-
} catch (error) {
|
|
4415
|
-
this.logger.error("Failed to cleanup ACP connection", error);
|
|
4416
|
-
}
|
|
4417
|
-
if (this.session.sseController) {
|
|
4418
|
-
this.session.sseController.close();
|
|
4419
|
-
}
|
|
4420
|
-
this.session = null;
|
|
4421
|
-
}
|
|
4422
|
-
async captureTreeState() {
|
|
4423
|
-
if (!this.session?.treeTracker) return;
|
|
4424
|
-
try {
|
|
4425
|
-
const snapshot = await this.session.treeTracker.captureTree({});
|
|
4426
|
-
if (snapshot) {
|
|
4427
|
-
const snapshotWithDevice = {
|
|
4428
|
-
...snapshot,
|
|
4429
|
-
device: this.session.deviceInfo
|
|
4430
|
-
};
|
|
4431
|
-
this.broadcastEvent({
|
|
4432
|
-
type: "notification",
|
|
4433
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4434
|
-
notification: {
|
|
4435
|
-
jsonrpc: "2.0",
|
|
4436
|
-
method: POSTHOG_NOTIFICATIONS.TREE_SNAPSHOT,
|
|
4437
|
-
params: snapshotWithDevice
|
|
4438
|
-
}
|
|
4439
|
-
});
|
|
4440
|
-
}
|
|
4441
|
-
} catch (error) {
|
|
4442
|
-
this.logger.error("Failed to capture tree state", error);
|
|
4443
|
-
}
|
|
4444
|
-
}
|
|
4445
|
-
broadcastEvent(event) {
|
|
4446
|
-
if (this.session?.sseController) {
|
|
4447
|
-
this.sendSseEvent(this.session.sseController, event);
|
|
4448
|
-
}
|
|
4449
|
-
}
|
|
4450
|
-
sendSseEvent(controller, data) {
|
|
4451
|
-
controller.send(data);
|
|
4452
|
-
}
|
|
4453
|
-
};
|
|
4454
|
-
|
|
4455
|
-
// src/server/bin.ts
|
|
4456
|
-
var envSchema = z4.object({
|
|
4457
|
-
JWT_PUBLIC_KEY: z4.string({
|
|
4458
|
-
required_error: "JWT_PUBLIC_KEY is required for authenticating client connections"
|
|
4459
|
-
}).min(1, "JWT_PUBLIC_KEY cannot be empty"),
|
|
4460
|
-
POSTHOG_API_URL: z4.string({
|
|
4461
|
-
required_error: "POSTHOG_API_URL is required for LLM gateway communication"
|
|
4462
|
-
}).url("POSTHOG_API_URL must be a valid URL"),
|
|
4463
|
-
POSTHOG_PERSONAL_API_KEY: z4.string({
|
|
4464
|
-
required_error: "POSTHOG_PERSONAL_API_KEY is required for authenticating with PostHog services"
|
|
4465
|
-
}).min(1, "POSTHOG_PERSONAL_API_KEY cannot be empty"),
|
|
4466
|
-
POSTHOG_PROJECT_ID: z4.string({
|
|
4467
|
-
required_error: "POSTHOG_PROJECT_ID is required for routing requests to the correct project"
|
|
4468
|
-
}).regex(/^\d+$/, "POSTHOG_PROJECT_ID must be a numeric string").transform((val) => parseInt(val, 10))
|
|
4469
|
-
});
|
|
4470
|
-
var program = new Command();
|
|
4471
|
-
program.name("agent-server").description("PostHog cloud agent server - runs in sandbox environments").option("--port <port>", "HTTP server port", "3001").option(
|
|
4472
|
-
"--mode <mode>",
|
|
4473
|
-
"Execution mode: interactive or background",
|
|
4474
|
-
"interactive"
|
|
4475
|
-
).requiredOption("--repositoryPath <path>", "Path to the repository").requiredOption("--taskId <id>", "Task ID").requiredOption("--runId <id>", "Task run ID").action(async (options) => {
|
|
4476
|
-
const envResult = envSchema.safeParse(process.env);
|
|
4477
|
-
if (!envResult.success) {
|
|
4478
|
-
const errors = envResult.error.issues.map((issue) => ` - ${issue.message}`).join("\n");
|
|
4479
|
-
program.error(`Environment validation failed:
|
|
4480
|
-
${errors}`);
|
|
4481
|
-
return;
|
|
4482
|
-
}
|
|
4483
|
-
const env = envResult.data;
|
|
4484
|
-
const mode = options.mode === "background" ? "background" : "interactive";
|
|
4485
|
-
const server = new AgentServer({
|
|
4486
|
-
port: parseInt(options.port, 10),
|
|
4487
|
-
jwtPublicKey: env.JWT_PUBLIC_KEY,
|
|
4488
|
-
repositoryPath: options.repositoryPath,
|
|
4489
|
-
apiUrl: env.POSTHOG_API_URL,
|
|
4490
|
-
apiKey: env.POSTHOG_PERSONAL_API_KEY,
|
|
4491
|
-
projectId: env.POSTHOG_PROJECT_ID,
|
|
4492
|
-
mode,
|
|
4493
|
-
taskId: options.taskId,
|
|
4494
|
-
runId: options.runId
|
|
4495
|
-
});
|
|
4496
|
-
process.on("SIGINT", async () => {
|
|
4497
|
-
await server.stop();
|
|
4498
|
-
process.exit(0);
|
|
4499
|
-
});
|
|
4500
|
-
process.on("SIGTERM", async () => {
|
|
4501
|
-
await server.stop();
|
|
4502
|
-
process.exit(0);
|
|
4503
|
-
});
|
|
4504
|
-
await server.start();
|
|
4505
|
-
});
|
|
4506
|
-
program.parse();
|
|
4507
|
-
//# sourceMappingURL=bin.js.map
|