claude-code-acp-ts 0.16.1 → 0.24.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -46
- package/README.md +3 -3
- package/dist/acp-agent.d.ts +69 -12
- package/dist/acp-agent.d.ts.map +1 -1
- package/dist/acp-agent.js +1031 -483
- package/dist/index.js +22 -16
- package/dist/lib.d.ts +2 -3
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +1 -2
- package/dist/settings.d.ts +2 -18
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +43 -224
- package/dist/tools.d.ts +32 -20
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +191 -244
- package/dist/utils.d.ts +0 -14
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +0 -63
- package/package.json +12 -13
package/dist/acp-agent.js
CHANGED
|
@@ -1,22 +1,15 @@
|
|
|
1
1
|
import { AgentSideConnection, ndJsonStream, RequestError, } from "@agentclientprotocol/sdk";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { getSessionMessages, listSessions, query, } from "@anthropic-ai/claude-agent-sdk";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
|
-
import * as path from "node:path";
|
|
6
|
-
import * as readline from "node:readline";
|
|
7
5
|
import * as os from "node:os";
|
|
8
|
-
import
|
|
9
|
-
import { createMcpServer } from "./mcp-server.js";
|
|
10
|
-
import { EDIT_TOOL_NAMES, acpToolNames } from "./tools.js";
|
|
11
|
-
import { toolInfoFromToolUse, planEntries, toolUpdateFromToolResult, registerHookCallback, createPostToolUseHook, createPreToolUseHook, } from "./tools.js";
|
|
6
|
+
import * as path from "node:path";
|
|
12
7
|
import packageJson from "../package.json" with { type: "json" };
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
8
|
+
import { SettingsManager } from "./settings.js";
|
|
9
|
+
import { createPostToolUseHook, planEntries, registerHookCallback, toolInfoFromToolUse, toolUpdateFromEditToolResponse, toolUpdateFromToolResult, } from "./tools.js";
|
|
10
|
+
import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
|
|
15
11
|
export const CLAUDE_CONFIG_DIR = process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
|
|
16
|
-
|
|
17
|
-
return path.join(CLAUDE_CONFIG_DIR, "projects", encodeProjectPath(cwd), `${sessionId}.jsonl`);
|
|
18
|
-
}
|
|
19
|
-
const MAX_TITLE_LENGTH = 128;
|
|
12
|
+
const MAX_TITLE_LENGTH = 256;
|
|
20
13
|
function sanitizeTitle(text) {
|
|
21
14
|
// Replace newlines and collapse whitespace
|
|
22
15
|
const sanitized = text
|
|
@@ -28,9 +21,51 @@ function sanitizeTitle(text) {
|
|
|
28
21
|
}
|
|
29
22
|
return sanitized.slice(0, MAX_TITLE_LENGTH - 1) + "…";
|
|
30
23
|
}
|
|
24
|
+
function isStaticBinary() {
|
|
25
|
+
return process.env.CLAUDE_AGENT_ACP_IS_SINGLE_FILE_BUN !== undefined;
|
|
26
|
+
}
|
|
27
|
+
export async function claudeCliPath() {
|
|
28
|
+
return isStaticBinary()
|
|
29
|
+
? (await import("@anthropic-ai/claude-agent-sdk/embed")).default
|
|
30
|
+
: import.meta.resolve("@anthropic-ai/claude-agent-sdk").replace("sdk.mjs", "cli.js");
|
|
31
|
+
}
|
|
32
|
+
function shouldHideClaudeAuth() {
|
|
33
|
+
return process.argv.includes("--hide-claude-auth");
|
|
34
|
+
}
|
|
31
35
|
// Bypass Permissions doesn't work if we are a root/sudo user
|
|
32
36
|
const IS_ROOT = (process.geteuid?.() ?? process.getuid?.()) === 0;
|
|
33
37
|
const ALLOW_BYPASS = !IS_ROOT || !!process.env.IS_SANDBOX;
|
|
38
|
+
// Slash commands that the SDK handles locally without replaying the user
|
|
39
|
+
// message and without invoking the model.
|
|
40
|
+
const LOCAL_ONLY_COMMANDS = new Set(["/context", "/heapdump", "/extra-usage"]);
|
|
41
|
+
const PERMISSION_MODE_ALIASES = {
|
|
42
|
+
default: "default",
|
|
43
|
+
acceptedits: "acceptEdits",
|
|
44
|
+
dontask: "dontAsk",
|
|
45
|
+
plan: "plan",
|
|
46
|
+
bypasspermissions: "bypassPermissions",
|
|
47
|
+
bypass: "bypassPermissions",
|
|
48
|
+
};
|
|
49
|
+
export function resolvePermissionMode(defaultMode) {
|
|
50
|
+
if (defaultMode === undefined) {
|
|
51
|
+
return "default";
|
|
52
|
+
}
|
|
53
|
+
if (typeof defaultMode !== "string") {
|
|
54
|
+
throw new Error("Invalid permissions.defaultMode: expected a string.");
|
|
55
|
+
}
|
|
56
|
+
const normalized = defaultMode.trim().toLowerCase();
|
|
57
|
+
if (normalized === "") {
|
|
58
|
+
throw new Error("Invalid permissions.defaultMode: expected a non-empty string.");
|
|
59
|
+
}
|
|
60
|
+
const mapped = PERMISSION_MODE_ALIASES[normalized];
|
|
61
|
+
if (!mapped) {
|
|
62
|
+
throw new Error(`Invalid permissions.defaultMode: ${defaultMode}.`);
|
|
63
|
+
}
|
|
64
|
+
if (mapped === "bypassPermissions" && !ALLOW_BYPASS) {
|
|
65
|
+
throw new Error("Invalid permissions.defaultMode: bypassPermissions is not available when running as root.");
|
|
66
|
+
}
|
|
67
|
+
return mapped;
|
|
68
|
+
}
|
|
34
69
|
// Implement the ACP Agent interface
|
|
35
70
|
export class ClaudeAcpAgent {
|
|
36
71
|
constructor(client, logger) {
|
|
@@ -42,26 +77,46 @@ export class ClaudeAcpAgent {
|
|
|
42
77
|
}
|
|
43
78
|
async initialize(request) {
|
|
44
79
|
this.clientCapabilities = request.clientCapabilities;
|
|
45
|
-
//
|
|
46
|
-
|
|
80
|
+
// Bypasses standard auth by routing requests through a custom Anthropic-protocol gateway.
|
|
81
|
+
// Only offered when the client advertises `auth._meta.gateway` capability.
|
|
82
|
+
const supportsGatewayAuth = request.clientCapabilities?.auth?._meta?.gateway === true;
|
|
83
|
+
const gatewayAuthMethod = {
|
|
84
|
+
id: "gateway",
|
|
85
|
+
name: "Custom model gateway",
|
|
86
|
+
description: "Use a custom gateway to authenticate and access models",
|
|
87
|
+
_meta: {
|
|
88
|
+
gateway: {
|
|
89
|
+
protocol: "anthropic",
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
const terminalAuthMethod = {
|
|
47
94
|
description: "Run `claude /login` in the terminal",
|
|
48
|
-
name: "Log in with Claude
|
|
95
|
+
name: "Log in with Claude",
|
|
49
96
|
id: "claude-login",
|
|
97
|
+
type: "terminal",
|
|
98
|
+
args: ["--cli"],
|
|
50
99
|
};
|
|
100
|
+
const supportsTerminalAuth = request.clientCapabilities?.auth?.terminal === true;
|
|
51
101
|
// If client supports terminal-auth capability, use that instead.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
102
|
+
const supportsMetaTerminalAuth = request.clientCapabilities?._meta?.["terminal-auth"] === true;
|
|
103
|
+
if (supportsMetaTerminalAuth) {
|
|
104
|
+
terminalAuthMethod._meta = {
|
|
55
105
|
"terminal-auth": {
|
|
56
|
-
command:
|
|
57
|
-
args: [
|
|
58
|
-
label: "Claude
|
|
106
|
+
command: process.execPath,
|
|
107
|
+
args: [...process.argv.slice(1), "--cli"],
|
|
108
|
+
label: "Claude Login",
|
|
59
109
|
},
|
|
60
110
|
};
|
|
61
111
|
}
|
|
62
112
|
return {
|
|
63
113
|
protocolVersion: 1,
|
|
64
114
|
agentCapabilities: {
|
|
115
|
+
_meta: {
|
|
116
|
+
claudeCode: {
|
|
117
|
+
promptQueueing: true,
|
|
118
|
+
},
|
|
119
|
+
},
|
|
65
120
|
promptCapabilities: {
|
|
66
121
|
image: true,
|
|
67
122
|
embeddedContext: true,
|
|
@@ -75,28 +130,40 @@ export class ClaudeAcpAgent {
|
|
|
75
130
|
fork: {},
|
|
76
131
|
list: {},
|
|
77
132
|
resume: {},
|
|
133
|
+
close: {},
|
|
78
134
|
},
|
|
79
135
|
},
|
|
80
136
|
agentInfo: {
|
|
81
137
|
name: packageJson.name,
|
|
82
|
-
title: "Claude
|
|
138
|
+
title: "Claude Agent",
|
|
83
139
|
version: packageJson.version,
|
|
84
140
|
},
|
|
85
|
-
authMethods: [
|
|
141
|
+
authMethods: [
|
|
142
|
+
...(!shouldHideClaudeAuth() && (supportsTerminalAuth || supportsMetaTerminalAuth)
|
|
143
|
+
? [terminalAuthMethod]
|
|
144
|
+
: []),
|
|
145
|
+
...(supportsGatewayAuth ? [gatewayAuthMethod] : []),
|
|
146
|
+
],
|
|
86
147
|
};
|
|
87
148
|
}
|
|
88
149
|
async newSession(params) {
|
|
89
|
-
if (
|
|
150
|
+
if (!this.gatewayAuthMeta &&
|
|
151
|
+
fs.existsSync(path.resolve(os.homedir(), ".claude.json.backup")) &&
|
|
90
152
|
!fs.existsSync(path.resolve(os.homedir(), ".claude.json"))) {
|
|
91
153
|
throw RequestError.authRequired();
|
|
92
154
|
}
|
|
93
|
-
|
|
155
|
+
const response = await this.createSession(params, {
|
|
94
156
|
// Revisit these meta values once we support resume
|
|
95
157
|
resume: params._meta?.claudeCode?.options?.resume,
|
|
96
158
|
});
|
|
159
|
+
// Needs to happen after we return the session
|
|
160
|
+
setTimeout(() => {
|
|
161
|
+
this.sendAvailableCommandsUpdate(response.sessionId);
|
|
162
|
+
}, 0);
|
|
163
|
+
return response;
|
|
97
164
|
}
|
|
98
165
|
async unstable_forkSession(params) {
|
|
99
|
-
|
|
166
|
+
const response = await this.createSession({
|
|
100
167
|
cwd: params.cwd,
|
|
101
168
|
mcpServers: params.mcpServers ?? [],
|
|
102
169
|
_meta: params._meta,
|
|
@@ -104,401 +171,534 @@ export class ClaudeAcpAgent {
|
|
|
104
171
|
resume: params.sessionId,
|
|
105
172
|
forkSession: true,
|
|
106
173
|
});
|
|
174
|
+
// Needs to happen after we return the session
|
|
175
|
+
setTimeout(() => {
|
|
176
|
+
this.sendAvailableCommandsUpdate(response.sessionId);
|
|
177
|
+
}, 0);
|
|
178
|
+
return response;
|
|
107
179
|
}
|
|
108
180
|
async unstable_resumeSession(params) {
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
},
|
|
114
|
-
|
|
115
|
-
});
|
|
116
|
-
return response;
|
|
181
|
+
const result = await this.getOrCreateSession(params);
|
|
182
|
+
// Needs to happen after we return the session
|
|
183
|
+
setTimeout(() => {
|
|
184
|
+
this.sendAvailableCommandsUpdate(params.sessionId);
|
|
185
|
+
}, 0);
|
|
186
|
+
return result;
|
|
117
187
|
}
|
|
118
188
|
async loadSession(params) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
189
|
+
const result = await this.getOrCreateSession(params);
|
|
190
|
+
await this.replaySessionHistory(params.sessionId);
|
|
191
|
+
// Send available commands after replay so it doesn't interleave with history
|
|
192
|
+
setTimeout(() => {
|
|
193
|
+
this.sendAvailableCommandsUpdate(params.sessionId);
|
|
194
|
+
}, 0);
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
async listSessions(params) {
|
|
198
|
+
const sdk_sessions = await listSessions({ dir: params.cwd ?? undefined });
|
|
199
|
+
const sessions = [];
|
|
200
|
+
for (const session of sdk_sessions) {
|
|
201
|
+
if (!session.cwd)
|
|
202
|
+
continue;
|
|
203
|
+
sessions.push({
|
|
204
|
+
sessionId: session.sessionId,
|
|
205
|
+
cwd: session.cwd,
|
|
206
|
+
title: sanitizeTitle(session.summary),
|
|
207
|
+
updatedAt: new Date(session.lastModified).toISOString(),
|
|
208
|
+
});
|
|
124
209
|
}
|
|
125
|
-
const response = await this.createSession({
|
|
126
|
-
cwd: params.cwd,
|
|
127
|
-
mcpServers: params.mcpServers ?? [],
|
|
128
|
-
_meta: params._meta,
|
|
129
|
-
}, {
|
|
130
|
-
resume: params.sessionId,
|
|
131
|
-
});
|
|
132
|
-
await this.replaySessionHistory(params.sessionId, params.cwd);
|
|
133
210
|
return {
|
|
134
|
-
|
|
135
|
-
models: response.models,
|
|
211
|
+
sessions,
|
|
136
212
|
};
|
|
137
213
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
214
|
+
async authenticate(_params) {
|
|
215
|
+
if (_params.methodId === "gateway") {
|
|
216
|
+
this.gatewayAuthMeta = _params._meta;
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
throw new Error("Method not implemented.");
|
|
220
|
+
}
|
|
221
|
+
async prompt(params) {
|
|
222
|
+
const session = this.sessions[params.sessionId];
|
|
223
|
+
if (!session) {
|
|
224
|
+
throw new Error("Session not found");
|
|
225
|
+
}
|
|
226
|
+
session.cancelled = false;
|
|
227
|
+
session.accumulatedUsage = {
|
|
228
|
+
inputTokens: 0,
|
|
229
|
+
outputTokens: 0,
|
|
230
|
+
cachedReadTokens: 0,
|
|
231
|
+
cachedWriteTokens: 0,
|
|
232
|
+
};
|
|
233
|
+
let lastAssistantTotalUsage = null;
|
|
234
|
+
let lastAssistantModel = null;
|
|
235
|
+
let lastContextWindowSize = 200000;
|
|
236
|
+
const userMessage = promptToClaude(params);
|
|
237
|
+
const promptUuid = randomUUID();
|
|
238
|
+
userMessage.uuid = promptUuid;
|
|
239
|
+
// These local-only commands return a result without replaying the user
|
|
240
|
+
// message. Mark promptReplayed=true so their result isn't consumed as a
|
|
241
|
+
// background task result.
|
|
242
|
+
const firstText = params.prompt[0]?.type === "text" ? params.prompt[0].text : "";
|
|
243
|
+
const isLocalOnlyCommand = firstText.startsWith("/") && LOCAL_ONLY_COMMANDS.has(firstText.split(" ", 1)[0]);
|
|
244
|
+
if (session.promptRunning) {
|
|
245
|
+
session.input.push(userMessage);
|
|
246
|
+
const order = session.nextPendingOrder++;
|
|
247
|
+
const cancelled = await new Promise((resolve) => {
|
|
248
|
+
session.pendingMessages.set(promptUuid, { resolve, order });
|
|
249
|
+
});
|
|
250
|
+
if (cancelled) {
|
|
251
|
+
return { stopReason: "cancelled" };
|
|
252
|
+
}
|
|
150
253
|
}
|
|
151
|
-
|
|
152
|
-
|
|
254
|
+
else {
|
|
255
|
+
session.input.push(userMessage);
|
|
153
256
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
257
|
+
session.promptRunning = true;
|
|
258
|
+
let handedOff = false;
|
|
259
|
+
let stopReason = "end_turn";
|
|
157
260
|
try {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
// Find first user message for title
|
|
181
|
-
let title;
|
|
182
|
-
for (const line of lines) {
|
|
183
|
-
try {
|
|
184
|
-
const entry = JSON.parse(line);
|
|
185
|
-
parsedAnyEntry = true;
|
|
186
|
-
if (entry.isSidechain === true) {
|
|
187
|
-
continue;
|
|
261
|
+
while (true) {
|
|
262
|
+
const { value: message, done } = await session.query.next();
|
|
263
|
+
if (done || !message) {
|
|
264
|
+
if (session.cancelled) {
|
|
265
|
+
return { stopReason: "cancelled" };
|
|
266
|
+
}
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
switch (message.type) {
|
|
270
|
+
case "system":
|
|
271
|
+
switch (message.subtype) {
|
|
272
|
+
case "init":
|
|
273
|
+
break;
|
|
274
|
+
case "status": {
|
|
275
|
+
if (message.status === "compacting") {
|
|
276
|
+
await this.client.sessionUpdate({
|
|
277
|
+
sessionId: message.session_id,
|
|
278
|
+
update: {
|
|
279
|
+
sessionUpdate: "agent_message_chunk",
|
|
280
|
+
content: { type: "text", text: "Compacting..." },
|
|
281
|
+
},
|
|
282
|
+
});
|
|
188
283
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
case "compact_boundary": {
|
|
287
|
+
// Send used:0 immediately so the client doesn't keep showing
|
|
288
|
+
// the stale pre-compaction context size until the next turn.
|
|
289
|
+
//
|
|
290
|
+
// This is a deliberate approximation: we don't know the exact
|
|
291
|
+
// post-compaction token count (only the SDK's next API call
|
|
292
|
+
// reveals that). But used:0 is directionally correct — context
|
|
293
|
+
// just dropped dramatically — and the real value replaces it
|
|
294
|
+
// within seconds when the next result message arrives.
|
|
295
|
+
// The alternative (no update) leaves the client showing e.g.
|
|
296
|
+
// "944k/1m" right after the user sees "Compacting completed",
|
|
297
|
+
// which is confusing and wrong.
|
|
298
|
+
lastAssistantTotalUsage = 0;
|
|
299
|
+
await this.client.sessionUpdate({
|
|
300
|
+
sessionId: message.session_id,
|
|
301
|
+
update: {
|
|
302
|
+
sessionUpdate: "usage_update",
|
|
303
|
+
used: 0,
|
|
304
|
+
size: lastContextWindowSize,
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
await this.client.sessionUpdate({
|
|
308
|
+
sessionId: message.session_id,
|
|
309
|
+
update: {
|
|
310
|
+
sessionUpdate: "agent_message_chunk",
|
|
311
|
+
content: { type: "text", text: "\n\nCompacting completed." },
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
case "local_command_output": {
|
|
317
|
+
await this.client.sessionUpdate({
|
|
318
|
+
sessionId: message.session_id,
|
|
319
|
+
update: {
|
|
320
|
+
sessionUpdate: "agent_message_chunk",
|
|
321
|
+
content: { type: "text", text: message.content },
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
case "session_state_changed": {
|
|
327
|
+
if (message.state === "idle") {
|
|
328
|
+
return { stopReason, usage: sessionUsage(session) };
|
|
192
329
|
}
|
|
193
|
-
|
|
194
|
-
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
case "hook_started":
|
|
333
|
+
case "hook_progress":
|
|
334
|
+
case "hook_response":
|
|
335
|
+
case "files_persisted":
|
|
336
|
+
case "task_started":
|
|
337
|
+
case "task_notification":
|
|
338
|
+
case "task_progress":
|
|
339
|
+
case "elicitation_complete":
|
|
340
|
+
case "api_retry":
|
|
341
|
+
// Todo: process via status api: https://docs.claude.com/en/docs/claude-code/hooks#hook-output
|
|
342
|
+
break;
|
|
343
|
+
default:
|
|
344
|
+
unreachable(message, this.logger);
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
break;
|
|
348
|
+
case "result": {
|
|
349
|
+
// Accumulate usage from this result
|
|
350
|
+
session.accumulatedUsage.inputTokens += message.usage.input_tokens;
|
|
351
|
+
session.accumulatedUsage.outputTokens += message.usage.output_tokens;
|
|
352
|
+
session.accumulatedUsage.cachedReadTokens += message.usage.cache_read_input_tokens;
|
|
353
|
+
session.accumulatedUsage.cachedWriteTokens += message.usage.cache_creation_input_tokens;
|
|
354
|
+
const matchingModelUsage = lastAssistantModel
|
|
355
|
+
? getMatchingModelUsage(message.modelUsage, lastAssistantModel)
|
|
356
|
+
: null;
|
|
357
|
+
const contextWindowSize = matchingModelUsage?.contextWindow ?? 200000;
|
|
358
|
+
lastContextWindowSize = contextWindowSize;
|
|
359
|
+
// Send usage_update notification
|
|
360
|
+
if (lastAssistantTotalUsage !== null) {
|
|
361
|
+
await this.client.sessionUpdate({
|
|
362
|
+
sessionId: params.sessionId,
|
|
363
|
+
update: {
|
|
364
|
+
sessionUpdate: "usage_update",
|
|
365
|
+
used: lastAssistantTotalUsage,
|
|
366
|
+
size: contextWindowSize,
|
|
367
|
+
cost: {
|
|
368
|
+
amount: message.total_cost_usd,
|
|
369
|
+
currency: "USD",
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
if (session.cancelled) {
|
|
375
|
+
stopReason = "cancelled";
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
switch (message.subtype) {
|
|
379
|
+
case "success": {
|
|
380
|
+
if (message.result.includes("Please run /login")) {
|
|
381
|
+
throw RequestError.authRequired();
|
|
195
382
|
}
|
|
196
|
-
if (
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (text) {
|
|
209
|
-
title = sanitizeTitle(text);
|
|
210
|
-
}
|
|
383
|
+
if (message.stop_reason === "max_tokens") {
|
|
384
|
+
stopReason = "max_tokens";
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
if (message.is_error) {
|
|
388
|
+
throw RequestError.internalError(undefined, message.result);
|
|
389
|
+
}
|
|
390
|
+
// For local-only commands (no model invocation), the result
|
|
391
|
+
// text is the command output — forward it to the client.
|
|
392
|
+
if (isLocalOnlyCommand) {
|
|
393
|
+
for (const notification of toAcpNotifications(message.result, "assistant", params.sessionId, this.toolUseCache, this.client, this.logger)) {
|
|
394
|
+
await this.client.sessionUpdate(notification);
|
|
211
395
|
}
|
|
212
396
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
case "error_during_execution": {
|
|
400
|
+
if (message.stop_reason === "max_tokens") {
|
|
401
|
+
stopReason = "max_tokens";
|
|
216
402
|
break;
|
|
217
403
|
}
|
|
404
|
+
if (message.is_error) {
|
|
405
|
+
throw RequestError.internalError(undefined, message.errors.join(", ") || message.subtype);
|
|
406
|
+
}
|
|
407
|
+
stopReason = "end_turn";
|
|
408
|
+
break;
|
|
218
409
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
410
|
+
case "error_max_budget_usd":
|
|
411
|
+
case "error_max_turns":
|
|
412
|
+
case "error_max_structured_output_retries":
|
|
413
|
+
if (message.is_error) {
|
|
414
|
+
throw RequestError.internalError(undefined, message.errors.join(", ") || message.subtype);
|
|
415
|
+
}
|
|
416
|
+
stopReason = "max_turn_requests";
|
|
417
|
+
break;
|
|
418
|
+
default:
|
|
419
|
+
unreachable(message, this.logger);
|
|
420
|
+
break;
|
|
222
421
|
}
|
|
223
|
-
|
|
224
|
-
continue;
|
|
225
|
-
// SessionInfo.cwd is currently required. For entries that do not
|
|
226
|
-
// include an explicit cwd in the session JSONL (typically metadata-only files),
|
|
227
|
-
// we skip them instead of decoding folder names because path encoding is lossy.
|
|
228
|
-
if (!sessionCwd)
|
|
229
|
-
continue;
|
|
230
|
-
// Even after encoded-path pre-filtering, verify per-entry cwd to disambiguate
|
|
231
|
-
// collisions such as "/a-b" and "/a/b" that map to the same encoded folder name.
|
|
232
|
-
if (params.cwd && sessionCwd !== params.cwd)
|
|
233
|
-
continue;
|
|
234
|
-
// Get file modification time as updatedAt
|
|
235
|
-
const fileStat = await fs.promises.stat(filePath);
|
|
236
|
-
const updatedAt = fileStat.mtime.toISOString();
|
|
237
|
-
allSessions.push({
|
|
238
|
-
sessionId,
|
|
239
|
-
cwd: sessionCwd,
|
|
240
|
-
title: title ?? null,
|
|
241
|
-
updatedAt,
|
|
242
|
-
});
|
|
422
|
+
break;
|
|
243
423
|
}
|
|
244
|
-
|
|
245
|
-
|
|
424
|
+
case "stream_event": {
|
|
425
|
+
for (const notification of streamEventToAcpNotifications(message, params.sessionId, this.toolUseCache, this.client, this.logger, {
|
|
426
|
+
clientCapabilities: this.clientCapabilities,
|
|
427
|
+
cwd: session.cwd,
|
|
428
|
+
})) {
|
|
429
|
+
await this.client.sessionUpdate(notification);
|
|
430
|
+
}
|
|
431
|
+
break;
|
|
246
432
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
catch (err) {
|
|
251
|
-
this.logger.error("[unstable_listSessions] Failed to list sessions", err);
|
|
252
|
-
return { sessions: [] };
|
|
253
|
-
}
|
|
254
|
-
// Sort by updatedAt descending (most recent first)
|
|
255
|
-
allSessions.sort((a, b) => {
|
|
256
|
-
const timeA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
|
|
257
|
-
const timeB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
|
|
258
|
-
return timeB - timeA;
|
|
259
|
-
});
|
|
260
|
-
// Handle pagination with cursor
|
|
261
|
-
let startIndex = 0;
|
|
262
|
-
if (params.cursor) {
|
|
263
|
-
try {
|
|
264
|
-
const decoded = Buffer.from(params.cursor, "base64").toString("utf-8");
|
|
265
|
-
const cursorData = JSON.parse(decoded);
|
|
266
|
-
startIndex = cursorData.offset ?? 0;
|
|
267
|
-
}
|
|
268
|
-
catch {
|
|
269
|
-
// Invalid cursor, start from beginning
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
const pageOfSessions = allSessions.slice(startIndex, startIndex + PAGE_SIZE);
|
|
273
|
-
const hasMore = startIndex + PAGE_SIZE < allSessions.length;
|
|
274
|
-
const response = {
|
|
275
|
-
sessions: pageOfSessions,
|
|
276
|
-
};
|
|
277
|
-
if (hasMore) {
|
|
278
|
-
const nextCursor = Buffer.from(JSON.stringify({ offset: startIndex + PAGE_SIZE })).toString("base64");
|
|
279
|
-
response.nextCursor = nextCursor;
|
|
280
|
-
}
|
|
281
|
-
return response;
|
|
282
|
-
}
|
|
283
|
-
async authenticate(_params) {
|
|
284
|
-
throw new Error("Method not implemented.");
|
|
285
|
-
}
|
|
286
|
-
async prompt(params) {
|
|
287
|
-
if (!this.sessions[params.sessionId]) {
|
|
288
|
-
throw new Error("Session not found");
|
|
289
|
-
}
|
|
290
|
-
this.sessions[params.sessionId].cancelled = false;
|
|
291
|
-
const { query, input } = this.sessions[params.sessionId];
|
|
292
|
-
input.push(promptToClaude(params));
|
|
293
|
-
while (true) {
|
|
294
|
-
const { value: message, done } = await query.next();
|
|
295
|
-
if (done || !message) {
|
|
296
|
-
if (this.sessions[params.sessionId].cancelled) {
|
|
297
|
-
return { stopReason: "cancelled" };
|
|
298
|
-
}
|
|
299
|
-
break;
|
|
300
|
-
}
|
|
301
|
-
switch (message.type) {
|
|
302
|
-
case "system":
|
|
303
|
-
switch (message.subtype) {
|
|
304
|
-
case "init":
|
|
305
|
-
break;
|
|
306
|
-
case "compact_boundary":
|
|
307
|
-
case "hook_started":
|
|
308
|
-
case "task_notification":
|
|
309
|
-
case "hook_progress":
|
|
310
|
-
case "hook_response":
|
|
311
|
-
case "status":
|
|
312
|
-
case "files_persisted":
|
|
313
|
-
// Todo: process via status api: https://docs.claude.com/en/docs/claude-code/hooks#hook-output
|
|
314
|
-
break;
|
|
315
|
-
default:
|
|
316
|
-
unreachable(message, this.logger);
|
|
433
|
+
case "user":
|
|
434
|
+
case "assistant": {
|
|
435
|
+
if (session.cancelled) {
|
|
317
436
|
break;
|
|
318
|
-
}
|
|
319
|
-
break;
|
|
320
|
-
case "result": {
|
|
321
|
-
if (this.sessions[params.sessionId].cancelled) {
|
|
322
|
-
return { stopReason: "cancelled" };
|
|
323
|
-
}
|
|
324
|
-
switch (message.subtype) {
|
|
325
|
-
case "success": {
|
|
326
|
-
if (message.result.includes("Please run /login")) {
|
|
327
|
-
throw RequestError.authRequired();
|
|
328
|
-
}
|
|
329
|
-
if (message.is_error) {
|
|
330
|
-
throw RequestError.internalError(undefined, message.result);
|
|
331
|
-
}
|
|
332
|
-
return { stopReason: "end_turn" };
|
|
333
437
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
438
|
+
// Check for prompt replay
|
|
439
|
+
if (message.type === "user" && "uuid" in message && message.uuid) {
|
|
440
|
+
if (message.uuid === promptUuid) {
|
|
441
|
+
break;
|
|
337
442
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
443
|
+
const pending = session.pendingMessages.get(message.uuid);
|
|
444
|
+
if (pending) {
|
|
445
|
+
pending.resolve(false);
|
|
446
|
+
session.pendingMessages.delete(message.uuid);
|
|
447
|
+
handedOff = true;
|
|
448
|
+
// the current loop stops with end_turn,
|
|
449
|
+
// the loop of the next prompt continues running
|
|
450
|
+
return { stopReason: "end_turn", usage: sessionUsage(session) };
|
|
344
451
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
break;
|
|
349
|
-
}
|
|
350
|
-
break;
|
|
351
|
-
}
|
|
352
|
-
case "stream_event": {
|
|
353
|
-
for (const notification of streamEventToAcpNotifications(message, params.sessionId, this.toolUseCache, this.client, this.logger)) {
|
|
354
|
-
await this.client.sessionUpdate(notification);
|
|
355
|
-
}
|
|
356
|
-
break;
|
|
357
|
-
}
|
|
358
|
-
case "user":
|
|
359
|
-
case "assistant": {
|
|
360
|
-
if (this.sessions[params.sessionId].cancelled) {
|
|
361
|
-
break;
|
|
362
|
-
}
|
|
363
|
-
// Slash commands like /compact can generate invalid output... doesn't match
|
|
364
|
-
// their own docs: https://docs.anthropic.com/en/docs/claude-code/sdk/sdk-slash-commands#%2Fcompact-compact-conversation-history
|
|
365
|
-
if (typeof message.message.content === "string" &&
|
|
366
|
-
message.message.content.includes("<local-command-stdout>")) {
|
|
367
|
-
// Handle /context by sending its reply as regular agent message.
|
|
368
|
-
if (message.message.content.includes("Context Usage")) {
|
|
369
|
-
for (const notification of toAcpNotifications(message.message.content
|
|
370
|
-
.replace("<local-command-stdout>", "")
|
|
371
|
-
.replace("</local-command-stdout>", ""), "assistant", params.sessionId, this.toolUseCache, this.client, this.logger)) {
|
|
372
|
-
await this.client.sessionUpdate(notification);
|
|
452
|
+
if ("isReplay" in message && message.isReplay) {
|
|
453
|
+
// not pending or unrelated replay message
|
|
454
|
+
break;
|
|
373
455
|
}
|
|
374
456
|
}
|
|
375
|
-
|
|
457
|
+
// Store latest assistant usage (excluding subagents)
|
|
458
|
+
// Sum all token types as a proxy for post-turn context occupancy:
|
|
459
|
+
// current turn's output will become next turn's input.
|
|
460
|
+
// Note: per the Anthropic API, input_tokens excludes cache tokens —
|
|
461
|
+
// cache_read and cache_creation are reported separately, so summing
|
|
462
|
+
// all four fields is not double-counting.
|
|
463
|
+
if (message.message.usage && message.parent_tool_use_id === null) {
|
|
464
|
+
const messageWithUsage = message.message;
|
|
465
|
+
lastAssistantTotalUsage =
|
|
466
|
+
messageWithUsage.usage.input_tokens +
|
|
467
|
+
messageWithUsage.usage.output_tokens +
|
|
468
|
+
messageWithUsage.usage.cache_read_input_tokens +
|
|
469
|
+
messageWithUsage.usage.cache_creation_input_tokens;
|
|
470
|
+
}
|
|
471
|
+
// Track the current top-level model for context window size lookup
|
|
472
|
+
// (exclude subagent messages to stay in sync with lastAssistantTotalUsage)
|
|
473
|
+
if (message.type === "assistant" &&
|
|
474
|
+
message.parent_tool_use_id === null &&
|
|
475
|
+
message.message.model &&
|
|
476
|
+
message.message.model !== "<synthetic>") {
|
|
477
|
+
lastAssistantModel = message.message.model;
|
|
478
|
+
}
|
|
479
|
+
// Slash commands like /compact can generate invalid output... doesn't match
|
|
480
|
+
// their own docs: https://docs.anthropic.com/en/docs/claude-code/sdk/sdk-slash-commands#%2Fcompact-compact-conversation-history
|
|
481
|
+
if (typeof message.message.content === "string" &&
|
|
482
|
+
message.message.content.includes("<local-command-stdout>")) {
|
|
483
|
+
this.logger.log(message.message.content);
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
if (typeof message.message.content === "string" &&
|
|
487
|
+
message.message.content.includes("<local-command-stderr>")) {
|
|
488
|
+
this.logger.error(message.message.content);
|
|
489
|
+
break;
|
|
490
|
+
}
|
|
491
|
+
// Skip these user messages for now, since they seem to just be messages we don't want in the feed
|
|
492
|
+
if (message.type === "user" &&
|
|
493
|
+
(typeof message.message.content === "string" ||
|
|
494
|
+
(Array.isArray(message.message.content) &&
|
|
495
|
+
message.message.content.length === 1 &&
|
|
496
|
+
message.message.content[0].type === "text"))) {
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
if (message.type === "assistant" &&
|
|
500
|
+
message.message.model === "<synthetic>" &&
|
|
501
|
+
Array.isArray(message.message.content) &&
|
|
502
|
+
message.message.content.length === 1 &&
|
|
503
|
+
message.message.content[0].type === "text" &&
|
|
504
|
+
message.message.content[0].text.includes("Please run /login")) {
|
|
505
|
+
throw RequestError.authRequired();
|
|
506
|
+
}
|
|
507
|
+
const content = message.type === "assistant"
|
|
508
|
+
? // Handled by stream events above
|
|
509
|
+
message.message.content.filter((item) => !["text", "thinking"].includes(item.type))
|
|
510
|
+
: message.message.content;
|
|
511
|
+
for (const notification of toAcpNotifications(content, message.message.role, params.sessionId, this.toolUseCache, this.client, this.logger, {
|
|
512
|
+
clientCapabilities: this.clientCapabilities,
|
|
513
|
+
parentToolUseId: message.parent_tool_use_id,
|
|
514
|
+
cwd: session.cwd,
|
|
515
|
+
})) {
|
|
516
|
+
await this.client.sessionUpdate(notification);
|
|
517
|
+
}
|
|
376
518
|
break;
|
|
377
519
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
520
|
+
case "tool_progress":
|
|
521
|
+
case "tool_use_summary":
|
|
522
|
+
case "auth_status":
|
|
523
|
+
case "prompt_suggestion":
|
|
524
|
+
case "rate_limit_event":
|
|
381
525
|
break;
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
if (message.type === "user" &&
|
|
385
|
-
(typeof message.message.content === "string" ||
|
|
386
|
-
(Array.isArray(message.message.content) &&
|
|
387
|
-
message.message.content.length === 1 &&
|
|
388
|
-
message.message.content[0].type === "text"))) {
|
|
526
|
+
default:
|
|
527
|
+
unreachable(message);
|
|
389
528
|
break;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
throw new Error("Session did not end in result");
|
|
532
|
+
}
|
|
533
|
+
catch (error) {
|
|
534
|
+
if (error instanceof RequestError || !(error instanceof Error)) {
|
|
535
|
+
throw error;
|
|
536
|
+
}
|
|
537
|
+
const message = error.message;
|
|
538
|
+
if (message.includes("ProcessTransport") ||
|
|
539
|
+
message.includes("terminated process") ||
|
|
540
|
+
message.includes("process exited with") ||
|
|
541
|
+
message.includes("process terminated by signal") ||
|
|
542
|
+
message.includes("Failed to write to process stdin")) {
|
|
543
|
+
this.logger.error(`Session ${params.sessionId}: Claude Agent process died: ${message}`);
|
|
544
|
+
session.settingsManager.dispose();
|
|
545
|
+
session.input.end();
|
|
546
|
+
delete this.sessions[params.sessionId];
|
|
547
|
+
throw RequestError.internalError(undefined, "The Claude Agent process exited unexpectedly. Please start a new session.");
|
|
548
|
+
}
|
|
549
|
+
throw error;
|
|
550
|
+
}
|
|
551
|
+
finally {
|
|
552
|
+
if (!handedOff) {
|
|
553
|
+
session.promptRunning = false;
|
|
554
|
+
// This usually should not happen, but in case the loop finishes
|
|
555
|
+
// without claude sending all message replays, we resolve the
|
|
556
|
+
// next pending prompt call to ensure no prompts get stuck.
|
|
557
|
+
if (session.pendingMessages.size > 0) {
|
|
558
|
+
const next = [...session.pendingMessages.entries()].sort((a, b) => a[1].order - b[1].order)[0];
|
|
559
|
+
if (next) {
|
|
560
|
+
next[1].resolve(false);
|
|
561
|
+
session.pendingMessages.delete(next[0]);
|
|
390
562
|
}
|
|
391
|
-
if (message.type === "assistant" &&
|
|
392
|
-
message.message.model === "<synthetic>" &&
|
|
393
|
-
Array.isArray(message.message.content) &&
|
|
394
|
-
message.message.content.length === 1 &&
|
|
395
|
-
message.message.content[0].type === "text" &&
|
|
396
|
-
message.message.content[0].text.includes("Please run /login")) {
|
|
397
|
-
throw RequestError.authRequired();
|
|
398
|
-
}
|
|
399
|
-
const content = message.type === "assistant"
|
|
400
|
-
? // Handled by stream events above
|
|
401
|
-
message.message.content.filter((item) => !["text", "thinking"].includes(item.type))
|
|
402
|
-
: message.message.content;
|
|
403
|
-
for (const notification of toAcpNotifications(content, message.message.role, params.sessionId, this.toolUseCache, this.client, this.logger)) {
|
|
404
|
-
await this.client.sessionUpdate(notification);
|
|
405
|
-
}
|
|
406
|
-
break;
|
|
407
563
|
}
|
|
408
|
-
case "tool_progress":
|
|
409
|
-
case "tool_use_summary":
|
|
410
|
-
break;
|
|
411
|
-
case "auth_status":
|
|
412
|
-
break;
|
|
413
|
-
default:
|
|
414
|
-
unreachable(message);
|
|
415
|
-
break;
|
|
416
564
|
}
|
|
417
565
|
}
|
|
418
|
-
throw new Error("Session did not end in result");
|
|
419
566
|
}
|
|
420
567
|
async cancel(params) {
|
|
421
|
-
|
|
568
|
+
const session = this.sessions[params.sessionId];
|
|
569
|
+
if (!session) {
|
|
422
570
|
throw new Error("Session not found");
|
|
423
571
|
}
|
|
424
|
-
|
|
425
|
-
|
|
572
|
+
session.cancelled = true;
|
|
573
|
+
for (const [, pending] of session.pendingMessages) {
|
|
574
|
+
pending.resolve(true);
|
|
575
|
+
}
|
|
576
|
+
session.pendingMessages.clear();
|
|
577
|
+
await session.query.interrupt();
|
|
578
|
+
}
|
|
579
|
+
async unstable_closeSession(params) {
|
|
580
|
+
const session = this.sessions[params.sessionId];
|
|
581
|
+
if (!session) {
|
|
582
|
+
throw new Error("Session not found");
|
|
583
|
+
}
|
|
584
|
+
await this.cancel({ sessionId: params.sessionId });
|
|
585
|
+
session.settingsManager.dispose();
|
|
586
|
+
session.abortController.abort();
|
|
587
|
+
delete this.sessions[params.sessionId];
|
|
588
|
+
return {};
|
|
426
589
|
}
|
|
427
590
|
async unstable_setSessionModel(params) {
|
|
428
591
|
if (!this.sessions[params.sessionId]) {
|
|
429
592
|
throw new Error("Session not found");
|
|
430
593
|
}
|
|
431
594
|
await this.sessions[params.sessionId].query.setModel(params.modelId);
|
|
595
|
+
await this.updateConfigOption(params.sessionId, "model", params.modelId);
|
|
432
596
|
}
|
|
433
597
|
async setSessionMode(params) {
|
|
434
598
|
if (!this.sessions[params.sessionId]) {
|
|
435
599
|
throw new Error("Session not found");
|
|
436
600
|
}
|
|
437
|
-
|
|
601
|
+
await this.applySessionMode(params.sessionId, params.modeId);
|
|
602
|
+
await this.updateConfigOption(params.sessionId, "mode", params.modeId);
|
|
603
|
+
return {};
|
|
604
|
+
}
|
|
605
|
+
async setSessionConfigOption(params) {
|
|
606
|
+
const session = this.sessions[params.sessionId];
|
|
607
|
+
if (!session) {
|
|
608
|
+
throw new Error("Session not found");
|
|
609
|
+
}
|
|
610
|
+
if (typeof params.value !== "string") {
|
|
611
|
+
throw new Error(`Invalid value for config option ${params.configId}: ${params.value}`);
|
|
612
|
+
}
|
|
613
|
+
const option = session.configOptions.find((o) => o.id === params.configId);
|
|
614
|
+
if (!option) {
|
|
615
|
+
throw new Error(`Unknown config option: ${params.configId}`);
|
|
616
|
+
}
|
|
617
|
+
const allValues = "options" in option && Array.isArray(option.options)
|
|
618
|
+
? option.options.flatMap((o) => ("options" in o ? o.options : [o]))
|
|
619
|
+
: [];
|
|
620
|
+
let validValue = allValues.find((o) => o.value === params.value);
|
|
621
|
+
// For model options, fall back to resolveModelPreference when the exact
|
|
622
|
+
// value doesn't match. This lets callers use human-friendly aliases like
|
|
623
|
+
// "opus" or "sonnet" instead of full model IDs like "claude-opus-4-6".
|
|
624
|
+
if (!validValue && params.configId === "model") {
|
|
625
|
+
const modelInfos = allValues.map((o) => ({
|
|
626
|
+
value: o.value,
|
|
627
|
+
displayName: o.name,
|
|
628
|
+
description: o.description ?? "",
|
|
629
|
+
}));
|
|
630
|
+
const resolved = resolveModelPreference(modelInfos, params.value);
|
|
631
|
+
if (resolved) {
|
|
632
|
+
validValue = allValues.find((o) => o.value === resolved.value);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (!validValue) {
|
|
636
|
+
throw new Error(`Invalid value for config option ${params.configId}: ${params.value}`);
|
|
637
|
+
}
|
|
638
|
+
// Use the canonical option value so downstream code always receives the
|
|
639
|
+
// model ID rather than the caller-supplied alias.
|
|
640
|
+
const resolvedValue = validValue.value;
|
|
641
|
+
if (params.configId === "mode") {
|
|
642
|
+
await this.applySessionMode(params.sessionId, resolvedValue);
|
|
643
|
+
await this.client.sessionUpdate({
|
|
644
|
+
sessionId: params.sessionId,
|
|
645
|
+
update: {
|
|
646
|
+
sessionUpdate: "current_mode_update",
|
|
647
|
+
currentModeId: resolvedValue,
|
|
648
|
+
},
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
else if (params.configId === "model") {
|
|
652
|
+
await this.sessions[params.sessionId].query.setModel(resolvedValue);
|
|
653
|
+
}
|
|
654
|
+
this.syncSessionConfigState(session, params.configId, params.value);
|
|
655
|
+
session.configOptions = session.configOptions.map((o) => o.id === params.configId && typeof o.currentValue === "string"
|
|
656
|
+
? { ...o, currentValue: resolvedValue }
|
|
657
|
+
: o);
|
|
658
|
+
return { configOptions: session.configOptions };
|
|
659
|
+
}
|
|
660
|
+
async applySessionMode(sessionId, modeId) {
|
|
661
|
+
switch (modeId) {
|
|
438
662
|
case "default":
|
|
439
663
|
case "acceptEdits":
|
|
440
664
|
case "bypassPermissions":
|
|
441
665
|
case "dontAsk":
|
|
442
666
|
case "plan":
|
|
443
|
-
|
|
444
|
-
try {
|
|
445
|
-
await this.sessions[params.sessionId].query.setPermissionMode(params.modeId);
|
|
446
|
-
}
|
|
447
|
-
catch (error) {
|
|
448
|
-
const errorMessage = error instanceof Error && error.message ? error.message : "Invalid Mode";
|
|
449
|
-
throw new Error(errorMessage);
|
|
450
|
-
}
|
|
451
|
-
return {};
|
|
667
|
+
break;
|
|
452
668
|
default:
|
|
453
669
|
throw new Error("Invalid Mode");
|
|
454
670
|
}
|
|
455
|
-
}
|
|
456
|
-
async replaySessionHistory(sessionId, cwd) {
|
|
457
|
-
const filePath = sessionFilePath(cwd, sessionId);
|
|
458
|
-
const toolUseCache = {};
|
|
459
|
-
const stream = fs.createReadStream(filePath, { encoding: "utf-8" });
|
|
460
|
-
const reader = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
461
671
|
try {
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
try {
|
|
469
|
-
entry = JSON.parse(trimmed);
|
|
470
|
-
}
|
|
471
|
-
catch {
|
|
472
|
-
continue;
|
|
473
|
-
}
|
|
474
|
-
if (entry.type !== "user" && entry.type !== "assistant") {
|
|
475
|
-
continue;
|
|
476
|
-
}
|
|
477
|
-
if (entry.isSidechain) {
|
|
478
|
-
continue;
|
|
479
|
-
}
|
|
480
|
-
if (entry.sessionId && entry.sessionId !== sessionId) {
|
|
481
|
-
continue;
|
|
482
|
-
}
|
|
483
|
-
const message = entry.message;
|
|
484
|
-
if (!message) {
|
|
485
|
-
continue;
|
|
486
|
-
}
|
|
487
|
-
const role = message.role === "assistant" ? "assistant" : message.role === "user" ? "user" : null;
|
|
488
|
-
if (!role) {
|
|
489
|
-
continue;
|
|
490
|
-
}
|
|
491
|
-
const content = message.content;
|
|
492
|
-
if (typeof content !== "string" && !Array.isArray(content)) {
|
|
493
|
-
continue;
|
|
494
|
-
}
|
|
495
|
-
for (const notification of toAcpNotifications(content, role, sessionId, toolUseCache, this.client, this.logger, { registerHooks: false })) {
|
|
496
|
-
await this.client.sessionUpdate(notification);
|
|
672
|
+
await this.sessions[sessionId].query.setPermissionMode(modeId);
|
|
673
|
+
}
|
|
674
|
+
catch (error) {
|
|
675
|
+
if (error instanceof Error) {
|
|
676
|
+
if (!error.message) {
|
|
677
|
+
error.message = "Invalid Mode";
|
|
497
678
|
}
|
|
679
|
+
throw error;
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
// eslint-disable-next-line preserve-caught-error
|
|
683
|
+
throw new Error("Invalid Mode");
|
|
498
684
|
}
|
|
499
685
|
}
|
|
500
|
-
|
|
501
|
-
|
|
686
|
+
}
|
|
687
|
+
async replaySessionHistory(sessionId) {
|
|
688
|
+
const toolUseCache = {};
|
|
689
|
+
const messages = await getSessionMessages(sessionId);
|
|
690
|
+
for (const message of messages) {
|
|
691
|
+
for (const notification of toAcpNotifications(
|
|
692
|
+
// @ts-expect-error - untyped in SDK but we handle all of these
|
|
693
|
+
message.message.content,
|
|
694
|
+
// @ts-expect-error - untyped in SDK but we handle all of these
|
|
695
|
+
message.message.role, sessionId, toolUseCache, this.client, this.logger, {
|
|
696
|
+
registerHooks: false,
|
|
697
|
+
clientCapabilities: this.clientCapabilities,
|
|
698
|
+
cwd: this.sessions[sessionId]?.cwd,
|
|
699
|
+
})) {
|
|
700
|
+
await this.client.sessionUpdate(notification);
|
|
701
|
+
}
|
|
502
702
|
}
|
|
503
703
|
}
|
|
504
704
|
async readTextFile(params) {
|
|
@@ -511,38 +711,47 @@ export class ClaudeAcpAgent {
|
|
|
511
711
|
}
|
|
512
712
|
canUseTool(sessionId) {
|
|
513
713
|
return async (toolName, toolInput, { signal, suggestions, toolUseID }) => {
|
|
714
|
+
const supportsTerminalOutput = this.clientCapabilities?._meta?.["terminal_output"] === true;
|
|
514
715
|
const session = this.sessions[sessionId];
|
|
515
716
|
if (!session) {
|
|
516
717
|
return {
|
|
517
718
|
behavior: "deny",
|
|
518
719
|
message: "Session not found",
|
|
519
|
-
interrupt: true,
|
|
520
720
|
};
|
|
521
721
|
}
|
|
522
722
|
if (toolName === "ExitPlanMode") {
|
|
723
|
+
const options = [
|
|
724
|
+
{
|
|
725
|
+
kind: "allow_always",
|
|
726
|
+
name: "Yes, and auto-accept edits",
|
|
727
|
+
optionId: "acceptEdits",
|
|
728
|
+
},
|
|
729
|
+
{ kind: "allow_once", name: "Yes, and manually approve edits", optionId: "default" },
|
|
730
|
+
{ kind: "reject_once", name: "No, keep planning", optionId: "plan" },
|
|
731
|
+
];
|
|
732
|
+
if (ALLOW_BYPASS) {
|
|
733
|
+
options.unshift({
|
|
734
|
+
kind: "allow_always",
|
|
735
|
+
name: "Yes, and bypass permissions",
|
|
736
|
+
optionId: "bypassPermissions",
|
|
737
|
+
});
|
|
738
|
+
}
|
|
523
739
|
const response = await this.client.requestPermission({
|
|
524
|
-
options
|
|
525
|
-
{
|
|
526
|
-
kind: "allow_always",
|
|
527
|
-
name: "Yes, and auto-accept edits",
|
|
528
|
-
optionId: "acceptEdits",
|
|
529
|
-
},
|
|
530
|
-
{ kind: "allow_once", name: "Yes, and manually approve edits", optionId: "default" },
|
|
531
|
-
{ kind: "reject_once", name: "No, keep planning", optionId: "plan" },
|
|
532
|
-
],
|
|
740
|
+
options,
|
|
533
741
|
sessionId,
|
|
534
742
|
toolCall: {
|
|
535
743
|
toolCallId: toolUseID,
|
|
536
744
|
rawInput: toolInput,
|
|
537
|
-
|
|
745
|
+
...toolInfoFromToolUse({ name: toolName, input: toolInput, id: toolUseID }, supportsTerminalOutput, session?.cwd),
|
|
538
746
|
},
|
|
539
747
|
});
|
|
540
748
|
if (signal.aborted || response.outcome?.outcome === "cancelled") {
|
|
541
749
|
throw new Error("Tool use aborted");
|
|
542
750
|
}
|
|
543
751
|
if (response.outcome?.outcome === "selected" &&
|
|
544
|
-
(response.outcome.optionId === "default" ||
|
|
545
|
-
|
|
752
|
+
(response.outcome.optionId === "default" ||
|
|
753
|
+
response.outcome.optionId === "acceptEdits" ||
|
|
754
|
+
response.outcome.optionId === "bypassPermissions")) {
|
|
546
755
|
await this.client.sessionUpdate({
|
|
547
756
|
sessionId,
|
|
548
757
|
update: {
|
|
@@ -550,6 +759,7 @@ export class ClaudeAcpAgent {
|
|
|
550
759
|
currentModeId: response.outcome.optionId,
|
|
551
760
|
},
|
|
552
761
|
});
|
|
762
|
+
await this.updateConfigOption(sessionId, "mode", response.outcome.optionId);
|
|
553
763
|
return {
|
|
554
764
|
behavior: "allow",
|
|
555
765
|
updatedInput: toolInput,
|
|
@@ -562,12 +772,10 @@ export class ClaudeAcpAgent {
|
|
|
562
772
|
return {
|
|
563
773
|
behavior: "deny",
|
|
564
774
|
message: "User rejected request to exit plan mode.",
|
|
565
|
-
interrupt: true,
|
|
566
775
|
};
|
|
567
776
|
}
|
|
568
777
|
}
|
|
569
|
-
if (session.
|
|
570
|
-
(session.permissionMode === "acceptEdits" && EDIT_TOOL_NAMES.includes(toolName))) {
|
|
778
|
+
if (session.modes.currentModeId === "bypassPermissions") {
|
|
571
779
|
return {
|
|
572
780
|
behavior: "allow",
|
|
573
781
|
updatedInput: toolInput,
|
|
@@ -590,7 +798,7 @@ export class ClaudeAcpAgent {
|
|
|
590
798
|
toolCall: {
|
|
591
799
|
toolCallId: toolUseID,
|
|
592
800
|
rawInput: toolInput,
|
|
593
|
-
|
|
801
|
+
...toolInfoFromToolUse({ name: toolName, input: toolInput, id: toolUseID }, supportsTerminalOutput, session?.cwd),
|
|
594
802
|
},
|
|
595
803
|
});
|
|
596
804
|
if (signal.aborted || response.outcome?.outcome === "cancelled") {
|
|
@@ -622,11 +830,69 @@ export class ClaudeAcpAgent {
|
|
|
622
830
|
return {
|
|
623
831
|
behavior: "deny",
|
|
624
832
|
message: "User refused permission to run tool",
|
|
625
|
-
interrupt: true,
|
|
626
833
|
};
|
|
627
834
|
}
|
|
628
835
|
};
|
|
629
836
|
}
|
|
837
|
+
async sendAvailableCommandsUpdate(sessionId) {
|
|
838
|
+
const session = this.sessions[sessionId];
|
|
839
|
+
if (!session)
|
|
840
|
+
return;
|
|
841
|
+
const commands = await session.query.supportedCommands();
|
|
842
|
+
await this.client.sessionUpdate({
|
|
843
|
+
sessionId,
|
|
844
|
+
update: {
|
|
845
|
+
sessionUpdate: "available_commands_update",
|
|
846
|
+
availableCommands: getAvailableSlashCommands(commands),
|
|
847
|
+
},
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
async updateConfigOption(sessionId, configId, value) {
|
|
851
|
+
const session = this.sessions[sessionId];
|
|
852
|
+
if (!session)
|
|
853
|
+
return;
|
|
854
|
+
this.syncSessionConfigState(session, configId, value);
|
|
855
|
+
session.configOptions = session.configOptions.map((o) => o.id === configId && typeof o.currentValue === "string" ? { ...o, currentValue: value } : o);
|
|
856
|
+
await this.client.sessionUpdate({
|
|
857
|
+
sessionId,
|
|
858
|
+
update: {
|
|
859
|
+
sessionUpdate: "config_option_update",
|
|
860
|
+
configOptions: session.configOptions,
|
|
861
|
+
},
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
syncSessionConfigState(session, configId, value) {
|
|
865
|
+
if (configId === "mode") {
|
|
866
|
+
session.modes = { ...session.modes, currentModeId: value };
|
|
867
|
+
}
|
|
868
|
+
else if (configId === "model") {
|
|
869
|
+
session.models = { ...session.models, currentModelId: value };
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
async getOrCreateSession(params) {
|
|
873
|
+
const existingSession = this.sessions[params.sessionId];
|
|
874
|
+
if (existingSession) {
|
|
875
|
+
return {
|
|
876
|
+
sessionId: params.sessionId,
|
|
877
|
+
modes: existingSession.modes,
|
|
878
|
+
models: existingSession.models,
|
|
879
|
+
configOptions: existingSession.configOptions,
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
const response = await this.createSession({
|
|
883
|
+
cwd: params.cwd,
|
|
884
|
+
mcpServers: params.mcpServers ?? [],
|
|
885
|
+
_meta: params._meta,
|
|
886
|
+
}, {
|
|
887
|
+
resume: params.sessionId,
|
|
888
|
+
});
|
|
889
|
+
return {
|
|
890
|
+
sessionId: response.sessionId,
|
|
891
|
+
modes: response.modes,
|
|
892
|
+
models: response.models,
|
|
893
|
+
configOptions: response.configOptions,
|
|
894
|
+
};
|
|
895
|
+
}
|
|
630
896
|
async createSession(params, creationOpts = {}) {
|
|
631
897
|
// We want to create a new session id unless it is resume,
|
|
632
898
|
// but not resume + forkSession.
|
|
@@ -648,7 +914,8 @@ export class ClaudeAcpAgent {
|
|
|
648
914
|
const mcpServers = {};
|
|
649
915
|
if (Array.isArray(params.mcpServers)) {
|
|
650
916
|
for (const server of params.mcpServers) {
|
|
651
|
-
if ("type" in server) {
|
|
917
|
+
if ("type" in server && (server.type === "http" || server.type === "sse")) {
|
|
918
|
+
// HTTP or SSE type MCP server
|
|
652
919
|
mcpServers[server.name] = {
|
|
653
920
|
type: server.type,
|
|
654
921
|
url: server.url,
|
|
@@ -658,6 +925,7 @@ export class ClaudeAcpAgent {
|
|
|
658
925
|
};
|
|
659
926
|
}
|
|
660
927
|
else {
|
|
928
|
+
// Stdio type MCP server (with or without explicit type field)
|
|
661
929
|
mcpServers[server.name] = {
|
|
662
930
|
type: "stdio",
|
|
663
931
|
command: server.command,
|
|
@@ -669,15 +937,6 @@ export class ClaudeAcpAgent {
|
|
|
669
937
|
}
|
|
670
938
|
}
|
|
671
939
|
}
|
|
672
|
-
// Only add the acp MCP server if built-in tools are not disabled
|
|
673
|
-
if (!params._meta?.disableBuiltInTools) {
|
|
674
|
-
const server = createMcpServer(this, sessionId, this.clientCapabilities);
|
|
675
|
-
mcpServers["acp"] = {
|
|
676
|
-
type: "sdk",
|
|
677
|
-
name: "acp",
|
|
678
|
-
instance: server,
|
|
679
|
-
};
|
|
680
|
-
}
|
|
681
940
|
let systemPrompt = { type: "preset", preset: "claude_code" };
|
|
682
941
|
if (params._meta?.systemPrompt) {
|
|
683
942
|
const customPrompt = params._meta.systemPrompt;
|
|
@@ -690,19 +949,35 @@ export class ClaudeAcpAgent {
|
|
|
690
949
|
systemPrompt.append = customPrompt.append;
|
|
691
950
|
}
|
|
692
951
|
}
|
|
693
|
-
const permissionMode =
|
|
952
|
+
const permissionMode = resolvePermissionMode(settingsManager.getSettings().permissions?.defaultMode);
|
|
694
953
|
// Extract options from _meta if provided
|
|
695
|
-
const
|
|
954
|
+
const sessionMeta = params._meta;
|
|
955
|
+
const userProvidedOptions = sessionMeta?.claudeCode?.options;
|
|
696
956
|
// Configure thinking tokens from environment variable
|
|
697
957
|
const maxThinkingTokens = process.env.MAX_THINKING_TOKENS
|
|
698
958
|
? parseInt(process.env.MAX_THINKING_TOKENS, 10)
|
|
699
959
|
: undefined;
|
|
960
|
+
// Disable this for now, not a great way to expose this over ACP at the moment (in progress work so we can revisit)
|
|
961
|
+
const disallowedTools = ["AskUserQuestion"];
|
|
962
|
+
// Resolve which built-in tools to expose.
|
|
963
|
+
// Explicit tools array from _meta.claudeCode.options takes precedence.
|
|
964
|
+
// disableBuiltInTools is a legacy shorthand for tools: [] — kept for
|
|
965
|
+
// backward compatibility but callers should prefer the tools array.
|
|
966
|
+
const tools = userProvidedOptions?.tools ??
|
|
967
|
+
(params._meta?.disableBuiltInTools === true ? [] : { type: "preset", preset: "claude_code" });
|
|
968
|
+
const abortController = userProvidedOptions?.abortController || new AbortController();
|
|
700
969
|
const options = {
|
|
701
970
|
systemPrompt,
|
|
702
971
|
settingSources: ["user", "project", "local"],
|
|
703
|
-
stderr: (err) => this.logger.error(err),
|
|
704
972
|
...(maxThinkingTokens !== undefined && { maxThinkingTokens }),
|
|
705
973
|
...userProvidedOptions,
|
|
974
|
+
env: {
|
|
975
|
+
...process.env,
|
|
976
|
+
...userProvidedOptions?.env,
|
|
977
|
+
...createEnvForGateway(this.gatewayAuthMeta),
|
|
978
|
+
// Opt-in to session state events like when the agent is idle
|
|
979
|
+
CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: "1",
|
|
980
|
+
},
|
|
706
981
|
// Override certain fields that must be controlled by ACP
|
|
707
982
|
cwd: params.cwd,
|
|
708
983
|
includePartialMessages: true,
|
|
@@ -714,29 +989,26 @@ export class ClaudeAcpAgent {
|
|
|
714
989
|
canUseTool: this.canUseTool(sessionId),
|
|
715
990
|
// note: although not documented by the types, passing an absolute path
|
|
716
991
|
// here works to find zed's managed node version.
|
|
717
|
-
executable: process.execPath,
|
|
718
|
-
...(process.env.CLAUDE_CODE_EXECUTABLE
|
|
719
|
-
pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE
|
|
720
|
-
|
|
721
|
-
|
|
992
|
+
executable: isStaticBinary() ? undefined : process.execPath,
|
|
993
|
+
...(process.env.CLAUDE_CODE_EXECUTABLE
|
|
994
|
+
? { pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE }
|
|
995
|
+
: isStaticBinary()
|
|
996
|
+
? { pathToClaudeCodeExecutable: await claudeCliPath() }
|
|
997
|
+
: {}),
|
|
998
|
+
extraArgs: {
|
|
999
|
+
...userProvidedOptions?.extraArgs,
|
|
1000
|
+
"replay-user-messages": "",
|
|
1001
|
+
},
|
|
1002
|
+
disallowedTools: [...(userProvidedOptions?.disallowedTools || []), ...disallowedTools],
|
|
1003
|
+
tools,
|
|
722
1004
|
hooks: {
|
|
723
1005
|
...userProvidedOptions?.hooks,
|
|
724
|
-
PreToolUse: [
|
|
725
|
-
...(userProvidedOptions?.hooks?.PreToolUse || []),
|
|
726
|
-
{
|
|
727
|
-
hooks: [createPreToolUseHook(settingsManager, this.logger)],
|
|
728
|
-
},
|
|
729
|
-
],
|
|
730
1006
|
PostToolUse: [
|
|
731
1007
|
...(userProvidedOptions?.hooks?.PostToolUse || []),
|
|
732
1008
|
{
|
|
733
1009
|
hooks: [
|
|
734
1010
|
createPostToolUseHook(this.logger, {
|
|
735
1011
|
onEnterPlanMode: async () => {
|
|
736
|
-
const session = this.sessions[sessionId];
|
|
737
|
-
if (session) {
|
|
738
|
-
session.permissionMode = "plan";
|
|
739
|
-
}
|
|
740
1012
|
await this.client.sessionUpdate({
|
|
741
1013
|
sessionId,
|
|
742
1014
|
update: {
|
|
@@ -744,6 +1016,7 @@ export class ClaudeAcpAgent {
|
|
|
744
1016
|
currentModeId: "plan",
|
|
745
1017
|
},
|
|
746
1018
|
});
|
|
1019
|
+
await this.updateConfigOption(sessionId, "mode", "plan");
|
|
747
1020
|
},
|
|
748
1021
|
}),
|
|
749
1022
|
],
|
|
@@ -751,41 +1024,17 @@ export class ClaudeAcpAgent {
|
|
|
751
1024
|
],
|
|
752
1025
|
},
|
|
753
1026
|
...creationOpts,
|
|
1027
|
+
abortController,
|
|
754
1028
|
};
|
|
1029
|
+
options.additionalDirectories = [
|
|
1030
|
+
...(userProvidedOptions?.additionalDirectories ?? []),
|
|
1031
|
+
...(sessionMeta?.additionalRoots ?? []),
|
|
1032
|
+
];
|
|
755
1033
|
if (creationOpts?.resume === undefined || creationOpts?.forkSession) {
|
|
756
1034
|
// Set our own session id if not resuming an existing session.
|
|
757
1035
|
options.sessionId = sessionId;
|
|
758
1036
|
}
|
|
759
|
-
const allowedTools = [];
|
|
760
|
-
// Disable this for now, not a great way to expose this over ACP at the moment (in progress work so we can revisit)
|
|
761
|
-
const disallowedTools = ["AskUserQuestion"];
|
|
762
|
-
// Check if built-in tools should be disabled
|
|
763
|
-
const disableBuiltInTools = params._meta?.disableBuiltInTools === true;
|
|
764
|
-
if (!disableBuiltInTools) {
|
|
765
|
-
if (this.clientCapabilities?.fs?.readTextFile) {
|
|
766
|
-
allowedTools.push(acpToolNames.read);
|
|
767
|
-
disallowedTools.push("Read");
|
|
768
|
-
}
|
|
769
|
-
if (this.clientCapabilities?.fs?.writeTextFile) {
|
|
770
|
-
disallowedTools.push("Write", "Edit");
|
|
771
|
-
}
|
|
772
|
-
if (this.clientCapabilities?.terminal) {
|
|
773
|
-
allowedTools.push(acpToolNames.bashOutput, acpToolNames.killShell);
|
|
774
|
-
disallowedTools.push("Bash", "BashOutput", "KillShell");
|
|
775
|
-
}
|
|
776
|
-
}
|
|
777
|
-
else {
|
|
778
|
-
// When built-in tools are disabled, explicitly disallow all of them
|
|
779
|
-
disallowedTools.push(acpToolNames.read, acpToolNames.write, acpToolNames.edit, acpToolNames.bash, acpToolNames.bashOutput, acpToolNames.killShell, "Read", "Write", "Edit", "Bash", "BashOutput", "KillShell", "Glob", "Grep", "Task", "TodoWrite", "ExitPlanMode", "WebSearch", "WebFetch", "AskUserQuestion", "SlashCommand", "Skill", "NotebookEdit");
|
|
780
|
-
}
|
|
781
|
-
if (allowedTools.length > 0) {
|
|
782
|
-
options.allowedTools = allowedTools;
|
|
783
|
-
}
|
|
784
|
-
if (disallowedTools.length > 0) {
|
|
785
|
-
options.disallowedTools = [...(options.disallowedTools || []), ...disallowedTools];
|
|
786
|
-
}
|
|
787
1037
|
// Handle abort controller from meta options
|
|
788
|
-
const abortController = userProvidedOptions?.abortController;
|
|
789
1038
|
if (abortController?.signal.aborted) {
|
|
790
1039
|
throw new Error("Cancelled");
|
|
791
1040
|
}
|
|
@@ -802,19 +1051,24 @@ export class ClaudeAcpAgent {
|
|
|
802
1051
|
prompt: input,
|
|
803
1052
|
options,
|
|
804
1053
|
});
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1054
|
+
let initializationResult;
|
|
1055
|
+
try {
|
|
1056
|
+
initializationResult = await q.initializationResult();
|
|
1057
|
+
}
|
|
1058
|
+
catch (error) {
|
|
1059
|
+
if (creationOpts.resume &&
|
|
1060
|
+
error instanceof Error &&
|
|
1061
|
+
error.message === "Query closed before response received") {
|
|
1062
|
+
throw RequestError.resourceNotFound(sessionId);
|
|
1063
|
+
}
|
|
1064
|
+
throw error;
|
|
1065
|
+
}
|
|
1066
|
+
if (shouldHideClaudeAuth() &&
|
|
1067
|
+
initializationResult.account.subscriptionType &&
|
|
1068
|
+
!this.gatewayAuthMeta) {
|
|
1069
|
+
throw RequestError.authRequired(undefined, "This integration does not support using claude.ai subscriptions.");
|
|
1070
|
+
}
|
|
1071
|
+
const models = await getAvailableModels(q, initializationResult.models, settingsManager);
|
|
818
1072
|
const sessionCreateElapsed = Date.now() - sessionCreateStartTime;
|
|
819
1073
|
this.logger.log(`✅ [ACP] 会话创建完成, 总耗时: ${sessionCreateElapsed}ms`);
|
|
820
1074
|
const availableModes = [
|
|
@@ -847,16 +1101,217 @@ export class ClaudeAcpAgent {
|
|
|
847
1101
|
description: "Bypass all permission checks",
|
|
848
1102
|
});
|
|
849
1103
|
}
|
|
1104
|
+
const modes = {
|
|
1105
|
+
currentModeId: permissionMode,
|
|
1106
|
+
availableModes,
|
|
1107
|
+
};
|
|
1108
|
+
const configOptions = buildConfigOptions(modes, models);
|
|
1109
|
+
this.sessions[sessionId] = {
|
|
1110
|
+
query: q,
|
|
1111
|
+
input: input,
|
|
1112
|
+
cancelled: false,
|
|
1113
|
+
cwd: params.cwd,
|
|
1114
|
+
settingsManager,
|
|
1115
|
+
accumulatedUsage: {
|
|
1116
|
+
inputTokens: 0,
|
|
1117
|
+
outputTokens: 0,
|
|
1118
|
+
cachedReadTokens: 0,
|
|
1119
|
+
cachedWriteTokens: 0,
|
|
1120
|
+
},
|
|
1121
|
+
modes,
|
|
1122
|
+
models,
|
|
1123
|
+
configOptions,
|
|
1124
|
+
promptRunning: false,
|
|
1125
|
+
pendingMessages: new Map(),
|
|
1126
|
+
nextPendingOrder: 0,
|
|
1127
|
+
abortController,
|
|
1128
|
+
};
|
|
850
1129
|
return {
|
|
851
1130
|
sessionId,
|
|
852
1131
|
models,
|
|
853
|
-
modes
|
|
854
|
-
|
|
855
|
-
availableModes,
|
|
856
|
-
},
|
|
1132
|
+
modes,
|
|
1133
|
+
configOptions,
|
|
857
1134
|
};
|
|
858
1135
|
}
|
|
859
1136
|
}
|
|
1137
|
+
function sessionUsage(session) {
|
|
1138
|
+
return {
|
|
1139
|
+
inputTokens: session.accumulatedUsage.inputTokens,
|
|
1140
|
+
outputTokens: session.accumulatedUsage.outputTokens,
|
|
1141
|
+
cachedReadTokens: session.accumulatedUsage.cachedReadTokens,
|
|
1142
|
+
cachedWriteTokens: session.accumulatedUsage.cachedWriteTokens,
|
|
1143
|
+
totalTokens: session.accumulatedUsage.inputTokens +
|
|
1144
|
+
session.accumulatedUsage.outputTokens +
|
|
1145
|
+
session.accumulatedUsage.cachedReadTokens +
|
|
1146
|
+
session.accumulatedUsage.cachedWriteTokens,
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
function createEnvForGateway(gatewayMeta) {
|
|
1150
|
+
if (!gatewayMeta) {
|
|
1151
|
+
return {};
|
|
1152
|
+
}
|
|
1153
|
+
return {
|
|
1154
|
+
ANTHROPIC_BASE_URL: gatewayMeta.gateway.baseUrl,
|
|
1155
|
+
ANTHROPIC_CUSTOM_HEADERS: Object.entries(gatewayMeta.gateway.headers)
|
|
1156
|
+
.map(([key, value]) => `${key}: ${value}`)
|
|
1157
|
+
.join("\n"),
|
|
1158
|
+
ANTHROPIC_AUTH_TOKEN: "", // Must be specified to bypass claude login requirement
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
function buildConfigOptions(modes, models) {
|
|
1162
|
+
return [
|
|
1163
|
+
{
|
|
1164
|
+
id: "mode",
|
|
1165
|
+
name: "Mode",
|
|
1166
|
+
description: "Session permission mode",
|
|
1167
|
+
category: "mode",
|
|
1168
|
+
type: "select",
|
|
1169
|
+
currentValue: modes.currentModeId,
|
|
1170
|
+
options: modes.availableModes.map((m) => ({
|
|
1171
|
+
value: m.id,
|
|
1172
|
+
name: m.name,
|
|
1173
|
+
description: m.description,
|
|
1174
|
+
})),
|
|
1175
|
+
},
|
|
1176
|
+
{
|
|
1177
|
+
id: "model",
|
|
1178
|
+
name: "Model",
|
|
1179
|
+
description: "AI model to use",
|
|
1180
|
+
category: "model",
|
|
1181
|
+
type: "select",
|
|
1182
|
+
currentValue: models.currentModelId,
|
|
1183
|
+
options: models.availableModels.map((m) => ({
|
|
1184
|
+
value: m.modelId,
|
|
1185
|
+
name: m.name,
|
|
1186
|
+
description: m.description ?? undefined,
|
|
1187
|
+
})),
|
|
1188
|
+
},
|
|
1189
|
+
];
|
|
1190
|
+
}
|
|
1191
|
+
// Claude Code CLI persists display strings like "opus[1m]" in settings,
|
|
1192
|
+
// but the SDK model list uses IDs like "claude-opus-4-6-1m".
|
|
1193
|
+
const MODEL_CONTEXT_HINT_PATTERN = /\[(\d+m)\]$/i;
|
|
1194
|
+
function tokenizeModelPreference(model) {
|
|
1195
|
+
const lower = model.trim().toLowerCase();
|
|
1196
|
+
const contextHint = lower.match(MODEL_CONTEXT_HINT_PATTERN)?.[1]?.toLowerCase();
|
|
1197
|
+
const normalized = lower.replace(MODEL_CONTEXT_HINT_PATTERN, " $1 ");
|
|
1198
|
+
const rawTokens = normalized.split(/[^a-z0-9]+/).filter(Boolean);
|
|
1199
|
+
const tokens = rawTokens
|
|
1200
|
+
.map((token) => {
|
|
1201
|
+
if (token === "opusplan")
|
|
1202
|
+
return "opus";
|
|
1203
|
+
if (token === "best" || token === "default")
|
|
1204
|
+
return "";
|
|
1205
|
+
return token;
|
|
1206
|
+
})
|
|
1207
|
+
.filter((token) => token && token !== "claude")
|
|
1208
|
+
.filter((token) => /[a-z]/.test(token) || token.endsWith("m"));
|
|
1209
|
+
return { tokens, contextHint };
|
|
1210
|
+
}
|
|
1211
|
+
function scoreModelMatch(model, tokens, contextHint) {
|
|
1212
|
+
const haystack = `${model.value} ${model.displayName}`.toLowerCase();
|
|
1213
|
+
let score = 0;
|
|
1214
|
+
for (const token of tokens) {
|
|
1215
|
+
if (haystack.includes(token)) {
|
|
1216
|
+
score += token === contextHint ? 3 : 1;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
return score;
|
|
1220
|
+
}
|
|
1221
|
+
function resolveModelPreference(models, preference) {
|
|
1222
|
+
const trimmed = preference.trim();
|
|
1223
|
+
if (!trimmed)
|
|
1224
|
+
return null;
|
|
1225
|
+
const lower = trimmed.toLowerCase();
|
|
1226
|
+
// Exact match on value or display name
|
|
1227
|
+
const directMatch = models.find((model) => model.value === trimmed ||
|
|
1228
|
+
model.value.toLowerCase() === lower ||
|
|
1229
|
+
model.displayName.toLowerCase() === lower);
|
|
1230
|
+
if (directMatch)
|
|
1231
|
+
return directMatch;
|
|
1232
|
+
// Substring match
|
|
1233
|
+
const includesMatch = models.find((model) => {
|
|
1234
|
+
const value = model.value.toLowerCase();
|
|
1235
|
+
const display = model.displayName.toLowerCase();
|
|
1236
|
+
return value.includes(lower) || display.includes(lower) || lower.includes(value);
|
|
1237
|
+
});
|
|
1238
|
+
if (includesMatch)
|
|
1239
|
+
return includesMatch;
|
|
1240
|
+
// Tokenized matching for aliases like "opus[1m]"
|
|
1241
|
+
const { tokens, contextHint } = tokenizeModelPreference(trimmed);
|
|
1242
|
+
if (tokens.length === 0)
|
|
1243
|
+
return null;
|
|
1244
|
+
let bestMatch = null;
|
|
1245
|
+
let bestScore = 0;
|
|
1246
|
+
for (const model of models) {
|
|
1247
|
+
const score = scoreModelMatch(model, tokens, contextHint);
|
|
1248
|
+
if (0 < score && (!bestMatch || bestScore < score)) {
|
|
1249
|
+
bestMatch = model;
|
|
1250
|
+
bestScore = score;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
return bestMatch;
|
|
1254
|
+
}
|
|
1255
|
+
async function getAvailableModels(query, models, settingsManager) {
|
|
1256
|
+
const settings = settingsManager.getSettings();
|
|
1257
|
+
let currentModel = models[0];
|
|
1258
|
+
// 模型优先级(从高到低):
|
|
1259
|
+
// 1. ANTHROPIC_MODEL 环境变量(用户期望的最高优先级)
|
|
1260
|
+
// 2. settings.model(用户配置文件)
|
|
1261
|
+
// 3. models[0](默认使用第一个模型)
|
|
1262
|
+
if (process.env.ANTHROPIC_MODEL) {
|
|
1263
|
+
const match = resolveModelPreference(models, process.env.ANTHROPIC_MODEL);
|
|
1264
|
+
if (match) {
|
|
1265
|
+
currentModel = match;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
else if (settings.model) {
|
|
1269
|
+
const match = resolveModelPreference(models, settings.model);
|
|
1270
|
+
if (match) {
|
|
1271
|
+
currentModel = match;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
await query.setModel(currentModel.value);
|
|
1275
|
+
return {
|
|
1276
|
+
availableModels: models.map((model) => ({
|
|
1277
|
+
modelId: model.value,
|
|
1278
|
+
name: model.displayName,
|
|
1279
|
+
description: model.description,
|
|
1280
|
+
})),
|
|
1281
|
+
currentModelId: currentModel.value,
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
function getAvailableSlashCommands(commands) {
|
|
1285
|
+
const UNSUPPORTED_COMMANDS = [
|
|
1286
|
+
"cost",
|
|
1287
|
+
"keybindings-help",
|
|
1288
|
+
"login",
|
|
1289
|
+
"logout",
|
|
1290
|
+
"output-style:new",
|
|
1291
|
+
"release-notes",
|
|
1292
|
+
"todos",
|
|
1293
|
+
];
|
|
1294
|
+
return commands
|
|
1295
|
+
.map((command) => {
|
|
1296
|
+
const input = command.argumentHint
|
|
1297
|
+
? {
|
|
1298
|
+
hint: Array.isArray(command.argumentHint)
|
|
1299
|
+
? command.argumentHint.join(" ")
|
|
1300
|
+
: command.argumentHint,
|
|
1301
|
+
}
|
|
1302
|
+
: null;
|
|
1303
|
+
let name = command.name;
|
|
1304
|
+
if (command.name.endsWith(" (MCP)")) {
|
|
1305
|
+
name = `mcp:${name.replace(" (MCP)", "")}`;
|
|
1306
|
+
}
|
|
1307
|
+
return {
|
|
1308
|
+
name,
|
|
1309
|
+
description: command.description || "",
|
|
1310
|
+
input,
|
|
1311
|
+
};
|
|
1312
|
+
})
|
|
1313
|
+
.filter((command) => !UNSUPPORTED_COMMANDS.includes(command.name));
|
|
1314
|
+
}
|
|
860
1315
|
function formatUriAsLink(uri) {
|
|
861
1316
|
try {
|
|
862
1317
|
if (uri.startsWith("file://")) {
|
|
@@ -883,10 +1338,10 @@ export function promptToClaude(prompt) {
|
|
|
883
1338
|
case "text": {
|
|
884
1339
|
let text = chunk.text;
|
|
885
1340
|
// change /mcp:server:command args -> /server:command (MCP) args
|
|
886
|
-
const mcpMatch = text.match(/^\/mcp:([^:\s]+):(\S+)(
|
|
1341
|
+
const mcpMatch = text.match(/^\/mcp:([^:\s]+):(\S+)(?:\s(.*))?$/);
|
|
887
1342
|
if (mcpMatch) {
|
|
888
1343
|
const [, server, command, args] = mcpMatch;
|
|
889
|
-
text = `/${server}:${command} (MCP)${args
|
|
1344
|
+
text = `/${server}:${command} (MCP)${args ? ` ${args}` : ""}`;
|
|
890
1345
|
}
|
|
891
1346
|
content.push({ type: "text", text });
|
|
892
1347
|
break;
|
|
@@ -957,19 +1412,25 @@ export function promptToClaude(prompt) {
|
|
|
957
1412
|
*/
|
|
958
1413
|
export function toAcpNotifications(content, role, sessionId, toolUseCache, client, logger, options) {
|
|
959
1414
|
const registerHooks = options?.registerHooks !== false;
|
|
1415
|
+
const supportsTerminalOutput = options?.clientCapabilities?._meta?.["terminal_output"] === true;
|
|
960
1416
|
if (typeof content === "string") {
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
content: {
|
|
967
|
-
type: "text",
|
|
968
|
-
text: content,
|
|
969
|
-
},
|
|
970
|
-
},
|
|
1417
|
+
const update = {
|
|
1418
|
+
sessionUpdate: role === "assistant" ? "agent_message_chunk" : "user_message_chunk",
|
|
1419
|
+
content: {
|
|
1420
|
+
type: "text",
|
|
1421
|
+
text: content,
|
|
971
1422
|
},
|
|
972
|
-
|
|
1423
|
+
};
|
|
1424
|
+
if (options?.parentToolUseId) {
|
|
1425
|
+
update._meta = {
|
|
1426
|
+
...update._meta,
|
|
1427
|
+
claudeCode: {
|
|
1428
|
+
...(update._meta?.claudeCode || {}),
|
|
1429
|
+
parentToolUseId: options.parentToolUseId,
|
|
1430
|
+
},
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
return [{ sessionId, update }];
|
|
973
1434
|
}
|
|
974
1435
|
const output = [];
|
|
975
1436
|
// Only handle the first chunk for streaming; extend as needed for batching
|
|
@@ -1010,6 +1471,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
1010
1471
|
case "tool_use":
|
|
1011
1472
|
case "server_tool_use":
|
|
1012
1473
|
case "mcp_tool_use": {
|
|
1474
|
+
const alreadyCached = chunk.id in toolUseCache;
|
|
1013
1475
|
toolUseCache[chunk.id] = chunk;
|
|
1014
1476
|
if (chunk.name === "TodoWrite") {
|
|
1015
1477
|
// @ts-expect-error - sometimes input is empty object
|
|
@@ -1021,11 +1483,13 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
1021
1483
|
}
|
|
1022
1484
|
}
|
|
1023
1485
|
else {
|
|
1024
|
-
|
|
1486
|
+
// Only register hooks on first encounter to avoid double-firing
|
|
1487
|
+
if (registerHooks && !alreadyCached) {
|
|
1025
1488
|
registerHookCallback(chunk.id, {
|
|
1026
1489
|
onPostToolUseHook: async (toolUseId, toolInput, toolResponse) => {
|
|
1027
1490
|
const toolUse = toolUseCache[toolUseId];
|
|
1028
1491
|
if (toolUse) {
|
|
1492
|
+
const editDiff = toolUse.name === "Edit" ? toolUpdateFromEditToolResponse(toolResponse) : {};
|
|
1029
1493
|
const update = {
|
|
1030
1494
|
_meta: {
|
|
1031
1495
|
claudeCode: {
|
|
@@ -1035,6 +1499,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
1035
1499
|
},
|
|
1036
1500
|
toolCallId: toolUseId,
|
|
1037
1501
|
sessionUpdate: "tool_call_update",
|
|
1502
|
+
...editDiff,
|
|
1038
1503
|
};
|
|
1039
1504
|
await client.sessionUpdate({
|
|
1040
1505
|
sessionId,
|
|
@@ -1042,7 +1507,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
1042
1507
|
});
|
|
1043
1508
|
}
|
|
1044
1509
|
else {
|
|
1045
|
-
logger.error(`[claude-
|
|
1510
|
+
logger.error(`[claude-agent-acp] Got a tool response for tool use that wasn't tracked: ${toolUseId}`);
|
|
1046
1511
|
}
|
|
1047
1512
|
},
|
|
1048
1513
|
});
|
|
@@ -1054,18 +1519,41 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
1054
1519
|
catch {
|
|
1055
1520
|
// ignore if we can't turn it to JSON
|
|
1056
1521
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1522
|
+
if (alreadyCached) {
|
|
1523
|
+
// Second encounter (full assistant message after streaming) —
|
|
1524
|
+
// send as tool_call_update to refine the existing tool_call
|
|
1525
|
+
// rather than emitting a duplicate tool_call.
|
|
1526
|
+
update = {
|
|
1527
|
+
_meta: {
|
|
1528
|
+
claudeCode: {
|
|
1529
|
+
toolName: chunk.name,
|
|
1530
|
+
},
|
|
1061
1531
|
},
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1532
|
+
toolCallId: chunk.id,
|
|
1533
|
+
sessionUpdate: "tool_call_update",
|
|
1534
|
+
rawInput,
|
|
1535
|
+
...toolInfoFromToolUse(chunk, supportsTerminalOutput, options?.cwd),
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
else {
|
|
1539
|
+
// First encounter (streaming content_block_start or replay) —
|
|
1540
|
+
// send as tool_call with terminal_info for Bash tools.
|
|
1541
|
+
update = {
|
|
1542
|
+
_meta: {
|
|
1543
|
+
claudeCode: {
|
|
1544
|
+
toolName: chunk.name,
|
|
1545
|
+
},
|
|
1546
|
+
...(chunk.name === "Bash" && supportsTerminalOutput
|
|
1547
|
+
? { terminal_info: { terminal_id: chunk.id } }
|
|
1548
|
+
: {}),
|
|
1549
|
+
},
|
|
1550
|
+
toolCallId: chunk.id,
|
|
1551
|
+
sessionUpdate: "tool_call",
|
|
1552
|
+
rawInput,
|
|
1553
|
+
status: "pending",
|
|
1554
|
+
...toolInfoFromToolUse(chunk, supportsTerminalOutput, options?.cwd),
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1069
1557
|
}
|
|
1070
1558
|
break;
|
|
1071
1559
|
}
|
|
@@ -1079,21 +1567,43 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
1079
1567
|
case "mcp_tool_result": {
|
|
1080
1568
|
const toolUse = toolUseCache[chunk.tool_use_id];
|
|
1081
1569
|
if (!toolUse) {
|
|
1082
|
-
logger.error(`[claude-
|
|
1570
|
+
logger.error(`[claude-agent-acp] Got a tool result for tool use that wasn't tracked: ${chunk.tool_use_id}`);
|
|
1083
1571
|
break;
|
|
1084
1572
|
}
|
|
1085
1573
|
if (toolUse.name !== "TodoWrite") {
|
|
1574
|
+
const { _meta: toolMeta, ...toolUpdate } = toolUpdateFromToolResult(chunk, toolUseCache[chunk.tool_use_id], supportsTerminalOutput);
|
|
1575
|
+
// When terminal output is supported, send terminal_output as a
|
|
1576
|
+
// separate notification to match codex-acp's streaming lifecycle:
|
|
1577
|
+
// 1. tool_call → _meta.terminal_info (already sent above)
|
|
1578
|
+
// 2. tool_call_update → _meta.terminal_output (sent here)
|
|
1579
|
+
// 3. tool_call_update → _meta.terminal_exit (sent below with status)
|
|
1580
|
+
if (toolMeta?.terminal_output) {
|
|
1581
|
+
output.push({
|
|
1582
|
+
sessionId,
|
|
1583
|
+
update: {
|
|
1584
|
+
_meta: {
|
|
1585
|
+
terminal_output: toolMeta.terminal_output,
|
|
1586
|
+
...(options?.parentToolUseId
|
|
1587
|
+
? { claudeCode: { parentToolUseId: options.parentToolUseId } }
|
|
1588
|
+
: {}),
|
|
1589
|
+
},
|
|
1590
|
+
toolCallId: chunk.tool_use_id,
|
|
1591
|
+
sessionUpdate: "tool_call_update",
|
|
1592
|
+
},
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1086
1595
|
update = {
|
|
1087
1596
|
_meta: {
|
|
1088
1597
|
claudeCode: {
|
|
1089
1598
|
toolName: toolUse.name,
|
|
1090
1599
|
},
|
|
1600
|
+
...(toolMeta?.terminal_exit ? { terminal_exit: toolMeta.terminal_exit } : {}),
|
|
1091
1601
|
},
|
|
1092
1602
|
toolCallId: chunk.tool_use_id,
|
|
1093
1603
|
sessionUpdate: "tool_call_update",
|
|
1094
1604
|
status: "is_error" in chunk && chunk.is_error ? "failed" : "completed",
|
|
1095
1605
|
rawOutput: chunk.content,
|
|
1096
|
-
...
|
|
1606
|
+
...toolUpdate,
|
|
1097
1607
|
};
|
|
1098
1608
|
}
|
|
1099
1609
|
break;
|
|
@@ -1113,18 +1623,35 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
1113
1623
|
break;
|
|
1114
1624
|
}
|
|
1115
1625
|
if (update) {
|
|
1626
|
+
if (options?.parentToolUseId) {
|
|
1627
|
+
update._meta = {
|
|
1628
|
+
...update._meta,
|
|
1629
|
+
claudeCode: {
|
|
1630
|
+
...(update._meta?.claudeCode || {}),
|
|
1631
|
+
parentToolUseId: options.parentToolUseId,
|
|
1632
|
+
},
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1116
1635
|
output.push({ sessionId, update });
|
|
1117
1636
|
}
|
|
1118
1637
|
}
|
|
1119
1638
|
return output;
|
|
1120
1639
|
}
|
|
1121
|
-
export function streamEventToAcpNotifications(message, sessionId, toolUseCache, client, logger) {
|
|
1640
|
+
export function streamEventToAcpNotifications(message, sessionId, toolUseCache, client, logger, options) {
|
|
1122
1641
|
const event = message.event;
|
|
1123
1642
|
switch (event.type) {
|
|
1124
1643
|
case "content_block_start":
|
|
1125
|
-
return toAcpNotifications([event.content_block], "assistant", sessionId, toolUseCache, client, logger
|
|
1644
|
+
return toAcpNotifications([event.content_block], "assistant", sessionId, toolUseCache, client, logger, {
|
|
1645
|
+
clientCapabilities: options?.clientCapabilities,
|
|
1646
|
+
parentToolUseId: message.parent_tool_use_id,
|
|
1647
|
+
cwd: options?.cwd,
|
|
1648
|
+
});
|
|
1126
1649
|
case "content_block_delta":
|
|
1127
|
-
return toAcpNotifications([event.delta], "assistant", sessionId, toolUseCache, client, logger
|
|
1650
|
+
return toAcpNotifications([event.delta], "assistant", sessionId, toolUseCache, client, logger, {
|
|
1651
|
+
clientCapabilities: options?.clientCapabilities,
|
|
1652
|
+
parentToolUseId: message.parent_tool_use_id,
|
|
1653
|
+
cwd: options?.cwd,
|
|
1654
|
+
});
|
|
1128
1655
|
// No content
|
|
1129
1656
|
case "message_start":
|
|
1130
1657
|
case "message_delta":
|
|
@@ -1142,3 +1669,24 @@ export function runAcp() {
|
|
|
1142
1669
|
const stream = ndJsonStream(input, output);
|
|
1143
1670
|
new AgentSideConnection((client) => new ClaudeAcpAgent(client), stream);
|
|
1144
1671
|
}
|
|
1672
|
+
function commonPrefixLength(a, b) {
|
|
1673
|
+
let i = 0;
|
|
1674
|
+
while (i < a.length && i < b.length && a[i] === b[i]) {
|
|
1675
|
+
i++;
|
|
1676
|
+
}
|
|
1677
|
+
return i;
|
|
1678
|
+
}
|
|
1679
|
+
function getMatchingModelUsage(modelUsage, currentModel) {
|
|
1680
|
+
let bestKey = null;
|
|
1681
|
+
let bestLen = 0;
|
|
1682
|
+
for (const key of Object.keys(modelUsage)) {
|
|
1683
|
+
const len = commonPrefixLength(key, currentModel);
|
|
1684
|
+
if (len > bestLen) {
|
|
1685
|
+
bestLen = len;
|
|
1686
|
+
bestKey = key;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
if (bestKey) {
|
|
1690
|
+
return modelUsage[bestKey];
|
|
1691
|
+
}
|
|
1692
|
+
}
|