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