claude-code-acp-ts 0.16.0 → 0.24.2
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 +970 -537
- 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 +13 -14
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,49 +21,51 @@ function sanitizeTitle(text) {
|
|
|
28
21
|
}
|
|
29
22
|
return sanitized.slice(0, MAX_TITLE_LENGTH - 1) + "…";
|
|
30
23
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
* @param promise 要执行的 Promise
|
|
42
|
-
* @param timeoutMs 超时时间(毫秒)
|
|
43
|
-
* @param operationName 操作名称(用于日志)
|
|
44
|
-
* @param logger 日志记录器
|
|
45
|
-
* @returns Promise 执行结果
|
|
46
|
-
*/
|
|
47
|
-
async function withTimeout(promise, timeoutMs, operationName, logger = console) {
|
|
48
|
-
const start = Date.now();
|
|
49
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
50
|
-
setTimeout(() => {
|
|
51
|
-
const elapsed = Date.now() - start;
|
|
52
|
-
logger.error(`⏰ [ACP] ${operationName} 超时 (${timeoutMs}ms), 实际耗时: ${elapsed}ms`);
|
|
53
|
-
reject(new Error(`${operationName} timed out after ${timeoutMs}ms`));
|
|
54
|
-
}, timeoutMs);
|
|
55
|
-
});
|
|
56
|
-
try {
|
|
57
|
-
const result = await Promise.race([promise, timeoutPromise]);
|
|
58
|
-
const elapsed = Date.now() - start;
|
|
59
|
-
if (elapsed > 5000) {
|
|
60
|
-
logger.log(`⚠️ [ACP] ${operationName} 耗时较长: ${elapsed}ms`);
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
logger.log(`✓ [ACP] ${operationName} 完成, 耗时: ${elapsed}ms`);
|
|
64
|
-
}
|
|
65
|
-
return result;
|
|
66
|
-
}
|
|
67
|
-
catch (error) {
|
|
68
|
-
throw error;
|
|
69
|
-
}
|
|
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");
|
|
70
34
|
}
|
|
71
35
|
// Bypass Permissions doesn't work if we are a root/sudo user
|
|
72
36
|
const IS_ROOT = (process.geteuid?.() ?? process.getuid?.()) === 0;
|
|
73
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
|
+
}
|
|
74
69
|
// Implement the ACP Agent interface
|
|
75
70
|
export class ClaudeAcpAgent {
|
|
76
71
|
constructor(client, logger) {
|
|
@@ -82,26 +77,46 @@ export class ClaudeAcpAgent {
|
|
|
82
77
|
}
|
|
83
78
|
async initialize(request) {
|
|
84
79
|
this.clientCapabilities = request.clientCapabilities;
|
|
85
|
-
//
|
|
86
|
-
|
|
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 = {
|
|
87
94
|
description: "Run `claude /login` in the terminal",
|
|
88
|
-
name: "Log in with Claude
|
|
95
|
+
name: "Log in with Claude",
|
|
89
96
|
id: "claude-login",
|
|
97
|
+
type: "terminal",
|
|
98
|
+
args: ["--cli"],
|
|
90
99
|
};
|
|
100
|
+
const supportsTerminalAuth = request.clientCapabilities?.auth?.terminal === true;
|
|
91
101
|
// If client supports terminal-auth capability, use that instead.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
102
|
+
const supportsMetaTerminalAuth = request.clientCapabilities?._meta?.["terminal-auth"] === true;
|
|
103
|
+
if (supportsMetaTerminalAuth) {
|
|
104
|
+
terminalAuthMethod._meta = {
|
|
95
105
|
"terminal-auth": {
|
|
96
|
-
command:
|
|
97
|
-
args: [
|
|
98
|
-
label: "Claude
|
|
106
|
+
command: process.execPath,
|
|
107
|
+
args: [...process.argv.slice(1), "--cli"],
|
|
108
|
+
label: "Claude Login",
|
|
99
109
|
},
|
|
100
110
|
};
|
|
101
111
|
}
|
|
102
112
|
return {
|
|
103
113
|
protocolVersion: 1,
|
|
104
114
|
agentCapabilities: {
|
|
115
|
+
_meta: {
|
|
116
|
+
claudeCode: {
|
|
117
|
+
promptQueueing: true,
|
|
118
|
+
},
|
|
119
|
+
},
|
|
105
120
|
promptCapabilities: {
|
|
106
121
|
image: true,
|
|
107
122
|
embeddedContext: true,
|
|
@@ -115,28 +130,40 @@ export class ClaudeAcpAgent {
|
|
|
115
130
|
fork: {},
|
|
116
131
|
list: {},
|
|
117
132
|
resume: {},
|
|
133
|
+
close: {},
|
|
118
134
|
},
|
|
119
135
|
},
|
|
120
136
|
agentInfo: {
|
|
121
137
|
name: packageJson.name,
|
|
122
|
-
title: "Claude
|
|
138
|
+
title: "Claude Agent",
|
|
123
139
|
version: packageJson.version,
|
|
124
140
|
},
|
|
125
|
-
authMethods: [
|
|
141
|
+
authMethods: [
|
|
142
|
+
...(!shouldHideClaudeAuth() && (supportsTerminalAuth || supportsMetaTerminalAuth)
|
|
143
|
+
? [terminalAuthMethod]
|
|
144
|
+
: []),
|
|
145
|
+
...(supportsGatewayAuth ? [gatewayAuthMethod] : []),
|
|
146
|
+
],
|
|
126
147
|
};
|
|
127
148
|
}
|
|
128
149
|
async newSession(params) {
|
|
129
|
-
if (
|
|
150
|
+
if (!this.gatewayAuthMeta &&
|
|
151
|
+
fs.existsSync(path.resolve(os.homedir(), ".claude.json.backup")) &&
|
|
130
152
|
!fs.existsSync(path.resolve(os.homedir(), ".claude.json"))) {
|
|
131
153
|
throw RequestError.authRequired();
|
|
132
154
|
}
|
|
133
|
-
|
|
155
|
+
const response = await this.createSession(params, {
|
|
134
156
|
// Revisit these meta values once we support resume
|
|
135
157
|
resume: params._meta?.claudeCode?.options?.resume,
|
|
136
158
|
});
|
|
159
|
+
// Needs to happen after we return the session
|
|
160
|
+
setTimeout(() => {
|
|
161
|
+
this.sendAvailableCommandsUpdate(response.sessionId);
|
|
162
|
+
}, 0);
|
|
163
|
+
return response;
|
|
137
164
|
}
|
|
138
165
|
async unstable_forkSession(params) {
|
|
139
|
-
|
|
166
|
+
const response = await this.createSession({
|
|
140
167
|
cwd: params.cwd,
|
|
141
168
|
mcpServers: params.mcpServers ?? [],
|
|
142
169
|
_meta: params._meta,
|
|
@@ -144,401 +171,534 @@ export class ClaudeAcpAgent {
|
|
|
144
171
|
resume: params.sessionId,
|
|
145
172
|
forkSession: true,
|
|
146
173
|
});
|
|
174
|
+
// Needs to happen after we return the session
|
|
175
|
+
setTimeout(() => {
|
|
176
|
+
this.sendAvailableCommandsUpdate(response.sessionId);
|
|
177
|
+
}, 0);
|
|
178
|
+
return response;
|
|
147
179
|
}
|
|
148
180
|
async unstable_resumeSession(params) {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
},
|
|
154
|
-
|
|
155
|
-
});
|
|
156
|
-
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;
|
|
157
187
|
}
|
|
158
188
|
async loadSession(params) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
+
});
|
|
164
209
|
}
|
|
165
|
-
const response = await this.createSession({
|
|
166
|
-
cwd: params.cwd,
|
|
167
|
-
mcpServers: params.mcpServers ?? [],
|
|
168
|
-
_meta: params._meta,
|
|
169
|
-
}, {
|
|
170
|
-
resume: params.sessionId,
|
|
171
|
-
});
|
|
172
|
-
await this.replaySessionHistory(params.sessionId, params.cwd);
|
|
173
210
|
return {
|
|
174
|
-
|
|
175
|
-
models: response.models,
|
|
211
|
+
sessions,
|
|
176
212
|
};
|
|
177
213
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
+
}
|
|
190
253
|
}
|
|
191
|
-
|
|
192
|
-
|
|
254
|
+
else {
|
|
255
|
+
session.input.push(userMessage);
|
|
193
256
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
257
|
+
session.promptRunning = true;
|
|
258
|
+
let handedOff = false;
|
|
259
|
+
let stopReason = "end_turn";
|
|
197
260
|
try {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
// Find first user message for title
|
|
221
|
-
let title;
|
|
222
|
-
for (const line of lines) {
|
|
223
|
-
try {
|
|
224
|
-
const entry = JSON.parse(line);
|
|
225
|
-
parsedAnyEntry = true;
|
|
226
|
-
if (entry.isSidechain === true) {
|
|
227
|
-
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
|
+
});
|
|
228
283
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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) };
|
|
329
|
+
}
|
|
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();
|
|
232
382
|
}
|
|
233
|
-
if (
|
|
234
|
-
|
|
383
|
+
if (message.stop_reason === "max_tokens") {
|
|
384
|
+
stopReason = "max_tokens";
|
|
385
|
+
break;
|
|
235
386
|
}
|
|
236
|
-
if (
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
? first
|
|
245
|
-
: first && typeof first === "object" && typeof first.text === "string"
|
|
246
|
-
? first.text
|
|
247
|
-
: undefined;
|
|
248
|
-
if (text) {
|
|
249
|
-
title = sanitizeTitle(text);
|
|
250
|
-
}
|
|
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);
|
|
251
395
|
}
|
|
252
396
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
case "error_during_execution": {
|
|
400
|
+
if (message.stop_reason === "max_tokens") {
|
|
401
|
+
stopReason = "max_tokens";
|
|
256
402
|
break;
|
|
257
403
|
}
|
|
404
|
+
if (message.is_error) {
|
|
405
|
+
throw RequestError.internalError(undefined, message.errors.join(", ") || message.subtype);
|
|
406
|
+
}
|
|
407
|
+
stopReason = "end_turn";
|
|
408
|
+
break;
|
|
258
409
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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;
|
|
262
421
|
}
|
|
263
|
-
|
|
264
|
-
continue;
|
|
265
|
-
// SessionInfo.cwd is currently required. For entries that do not
|
|
266
|
-
// include an explicit cwd in the session JSONL (typically metadata-only files),
|
|
267
|
-
// we skip them instead of decoding folder names because path encoding is lossy.
|
|
268
|
-
if (!sessionCwd)
|
|
269
|
-
continue;
|
|
270
|
-
// Even after encoded-path pre-filtering, verify per-entry cwd to disambiguate
|
|
271
|
-
// collisions such as "/a-b" and "/a/b" that map to the same encoded folder name.
|
|
272
|
-
if (params.cwd && sessionCwd !== params.cwd)
|
|
273
|
-
continue;
|
|
274
|
-
// Get file modification time as updatedAt
|
|
275
|
-
const fileStat = await fs.promises.stat(filePath);
|
|
276
|
-
const updatedAt = fileStat.mtime.toISOString();
|
|
277
|
-
allSessions.push({
|
|
278
|
-
sessionId,
|
|
279
|
-
cwd: sessionCwd,
|
|
280
|
-
title: title ?? null,
|
|
281
|
-
updatedAt,
|
|
282
|
-
});
|
|
422
|
+
break;
|
|
283
423
|
}
|
|
284
|
-
|
|
285
|
-
|
|
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;
|
|
286
432
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
catch (err) {
|
|
291
|
-
this.logger.error("[unstable_listSessions] Failed to list sessions", err);
|
|
292
|
-
return { sessions: [] };
|
|
293
|
-
}
|
|
294
|
-
// Sort by updatedAt descending (most recent first)
|
|
295
|
-
allSessions.sort((a, b) => {
|
|
296
|
-
const timeA = a.updatedAt ? new Date(a.updatedAt).getTime() : 0;
|
|
297
|
-
const timeB = b.updatedAt ? new Date(b.updatedAt).getTime() : 0;
|
|
298
|
-
return timeB - timeA;
|
|
299
|
-
});
|
|
300
|
-
// Handle pagination with cursor
|
|
301
|
-
let startIndex = 0;
|
|
302
|
-
if (params.cursor) {
|
|
303
|
-
try {
|
|
304
|
-
const decoded = Buffer.from(params.cursor, "base64").toString("utf-8");
|
|
305
|
-
const cursorData = JSON.parse(decoded);
|
|
306
|
-
startIndex = cursorData.offset ?? 0;
|
|
307
|
-
}
|
|
308
|
-
catch {
|
|
309
|
-
// Invalid cursor, start from beginning
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
const pageOfSessions = allSessions.slice(startIndex, startIndex + PAGE_SIZE);
|
|
313
|
-
const hasMore = startIndex + PAGE_SIZE < allSessions.length;
|
|
314
|
-
const response = {
|
|
315
|
-
sessions: pageOfSessions,
|
|
316
|
-
};
|
|
317
|
-
if (hasMore) {
|
|
318
|
-
const nextCursor = Buffer.from(JSON.stringify({ offset: startIndex + PAGE_SIZE })).toString("base64");
|
|
319
|
-
response.nextCursor = nextCursor;
|
|
320
|
-
}
|
|
321
|
-
return response;
|
|
322
|
-
}
|
|
323
|
-
async authenticate(_params) {
|
|
324
|
-
throw new Error("Method not implemented.");
|
|
325
|
-
}
|
|
326
|
-
async prompt(params) {
|
|
327
|
-
if (!this.sessions[params.sessionId]) {
|
|
328
|
-
throw new Error("Session not found");
|
|
329
|
-
}
|
|
330
|
-
this.sessions[params.sessionId].cancelled = false;
|
|
331
|
-
const { query, input } = this.sessions[params.sessionId];
|
|
332
|
-
input.push(promptToClaude(params));
|
|
333
|
-
while (true) {
|
|
334
|
-
const { value: message, done } = await query.next();
|
|
335
|
-
if (done || !message) {
|
|
336
|
-
if (this.sessions[params.sessionId].cancelled) {
|
|
337
|
-
return { stopReason: "cancelled" };
|
|
338
|
-
}
|
|
339
|
-
break;
|
|
340
|
-
}
|
|
341
|
-
switch (message.type) {
|
|
342
|
-
case "system":
|
|
343
|
-
switch (message.subtype) {
|
|
344
|
-
case "init":
|
|
345
|
-
break;
|
|
346
|
-
case "compact_boundary":
|
|
347
|
-
case "hook_started":
|
|
348
|
-
case "task_notification":
|
|
349
|
-
case "hook_progress":
|
|
350
|
-
case "hook_response":
|
|
351
|
-
case "status":
|
|
352
|
-
case "files_persisted":
|
|
353
|
-
// Todo: process via status api: https://docs.claude.com/en/docs/claude-code/hooks#hook-output
|
|
433
|
+
case "user":
|
|
434
|
+
case "assistant": {
|
|
435
|
+
if (session.cancelled) {
|
|
354
436
|
break;
|
|
355
|
-
default:
|
|
356
|
-
unreachable(message, this.logger);
|
|
357
|
-
break;
|
|
358
|
-
}
|
|
359
|
-
break;
|
|
360
|
-
case "result": {
|
|
361
|
-
if (this.sessions[params.sessionId].cancelled) {
|
|
362
|
-
return { stopReason: "cancelled" };
|
|
363
|
-
}
|
|
364
|
-
switch (message.subtype) {
|
|
365
|
-
case "success": {
|
|
366
|
-
if (message.result.includes("Please run /login")) {
|
|
367
|
-
throw RequestError.authRequired();
|
|
368
|
-
}
|
|
369
|
-
if (message.is_error) {
|
|
370
|
-
throw RequestError.internalError(undefined, message.result);
|
|
371
|
-
}
|
|
372
|
-
return { stopReason: "end_turn" };
|
|
373
437
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
438
|
+
// Check for prompt replay
|
|
439
|
+
if (message.type === "user" && "uuid" in message && message.uuid) {
|
|
440
|
+
if (message.uuid === promptUuid) {
|
|
441
|
+
break;
|
|
377
442
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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) };
|
|
384
451
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
break;
|
|
389
|
-
}
|
|
390
|
-
break;
|
|
391
|
-
}
|
|
392
|
-
case "stream_event": {
|
|
393
|
-
for (const notification of streamEventToAcpNotifications(message, params.sessionId, this.toolUseCache, this.client, this.logger)) {
|
|
394
|
-
await this.client.sessionUpdate(notification);
|
|
395
|
-
}
|
|
396
|
-
break;
|
|
397
|
-
}
|
|
398
|
-
case "user":
|
|
399
|
-
case "assistant": {
|
|
400
|
-
if (this.sessions[params.sessionId].cancelled) {
|
|
401
|
-
break;
|
|
402
|
-
}
|
|
403
|
-
// Slash commands like /compact can generate invalid output... doesn't match
|
|
404
|
-
// their own docs: https://docs.anthropic.com/en/docs/claude-code/sdk/sdk-slash-commands#%2Fcompact-compact-conversation-history
|
|
405
|
-
if (typeof message.message.content === "string" &&
|
|
406
|
-
message.message.content.includes("<local-command-stdout>")) {
|
|
407
|
-
// Handle /context by sending its reply as regular agent message.
|
|
408
|
-
if (message.message.content.includes("Context Usage")) {
|
|
409
|
-
for (const notification of toAcpNotifications(message.message.content
|
|
410
|
-
.replace("<local-command-stdout>", "")
|
|
411
|
-
.replace("</local-command-stdout>", ""), "assistant", params.sessionId, this.toolUseCache, this.client, this.logger)) {
|
|
412
|
-
await this.client.sessionUpdate(notification);
|
|
452
|
+
if ("isReplay" in message && message.isReplay) {
|
|
453
|
+
// not pending or unrelated replay message
|
|
454
|
+
break;
|
|
413
455
|
}
|
|
414
456
|
}
|
|
415
|
-
|
|
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
|
+
}
|
|
416
518
|
break;
|
|
417
519
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
520
|
+
case "tool_progress":
|
|
521
|
+
case "tool_use_summary":
|
|
522
|
+
case "auth_status":
|
|
523
|
+
case "prompt_suggestion":
|
|
524
|
+
case "rate_limit_event":
|
|
421
525
|
break;
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
if (message.type === "user" &&
|
|
425
|
-
(typeof message.message.content === "string" ||
|
|
426
|
-
(Array.isArray(message.message.content) &&
|
|
427
|
-
message.message.content.length === 1 &&
|
|
428
|
-
message.message.content[0].type === "text"))) {
|
|
526
|
+
default:
|
|
527
|
+
unreachable(message);
|
|
429
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]);
|
|
430
562
|
}
|
|
431
|
-
if (message.type === "assistant" &&
|
|
432
|
-
message.message.model === "<synthetic>" &&
|
|
433
|
-
Array.isArray(message.message.content) &&
|
|
434
|
-
message.message.content.length === 1 &&
|
|
435
|
-
message.message.content[0].type === "text" &&
|
|
436
|
-
message.message.content[0].text.includes("Please run /login")) {
|
|
437
|
-
throw RequestError.authRequired();
|
|
438
|
-
}
|
|
439
|
-
const content = message.type === "assistant"
|
|
440
|
-
? // Handled by stream events above
|
|
441
|
-
message.message.content.filter((item) => !["text", "thinking"].includes(item.type))
|
|
442
|
-
: message.message.content;
|
|
443
|
-
for (const notification of toAcpNotifications(content, message.message.role, params.sessionId, this.toolUseCache, this.client, this.logger)) {
|
|
444
|
-
await this.client.sessionUpdate(notification);
|
|
445
|
-
}
|
|
446
|
-
break;
|
|
447
563
|
}
|
|
448
|
-
case "tool_progress":
|
|
449
|
-
case "tool_use_summary":
|
|
450
|
-
break;
|
|
451
|
-
case "auth_status":
|
|
452
|
-
break;
|
|
453
|
-
default:
|
|
454
|
-
unreachable(message);
|
|
455
|
-
break;
|
|
456
564
|
}
|
|
457
565
|
}
|
|
458
|
-
throw new Error("Session did not end in result");
|
|
459
566
|
}
|
|
460
567
|
async cancel(params) {
|
|
461
|
-
|
|
568
|
+
const session = this.sessions[params.sessionId];
|
|
569
|
+
if (!session) {
|
|
462
570
|
throw new Error("Session not found");
|
|
463
571
|
}
|
|
464
|
-
|
|
465
|
-
|
|
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 {};
|
|
466
589
|
}
|
|
467
590
|
async unstable_setSessionModel(params) {
|
|
468
591
|
if (!this.sessions[params.sessionId]) {
|
|
469
592
|
throw new Error("Session not found");
|
|
470
593
|
}
|
|
471
594
|
await this.sessions[params.sessionId].query.setModel(params.modelId);
|
|
595
|
+
await this.updateConfigOption(params.sessionId, "model", params.modelId);
|
|
472
596
|
}
|
|
473
597
|
async setSessionMode(params) {
|
|
474
598
|
if (!this.sessions[params.sessionId]) {
|
|
475
599
|
throw new Error("Session not found");
|
|
476
600
|
}
|
|
477
|
-
|
|
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) {
|
|
478
662
|
case "default":
|
|
479
663
|
case "acceptEdits":
|
|
480
664
|
case "bypassPermissions":
|
|
481
665
|
case "dontAsk":
|
|
482
666
|
case "plan":
|
|
483
|
-
|
|
484
|
-
try {
|
|
485
|
-
await this.sessions[params.sessionId].query.setPermissionMode(params.modeId);
|
|
486
|
-
}
|
|
487
|
-
catch (error) {
|
|
488
|
-
const errorMessage = error instanceof Error && error.message ? error.message : "Invalid Mode";
|
|
489
|
-
throw new Error(errorMessage);
|
|
490
|
-
}
|
|
491
|
-
return {};
|
|
667
|
+
break;
|
|
492
668
|
default:
|
|
493
669
|
throw new Error("Invalid Mode");
|
|
494
670
|
}
|
|
495
|
-
}
|
|
496
|
-
async replaySessionHistory(sessionId, cwd) {
|
|
497
|
-
const filePath = sessionFilePath(cwd, sessionId);
|
|
498
|
-
const toolUseCache = {};
|
|
499
|
-
const stream = fs.createReadStream(filePath, { encoding: "utf-8" });
|
|
500
|
-
const reader = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
501
671
|
try {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
try {
|
|
509
|
-
entry = JSON.parse(trimmed);
|
|
510
|
-
}
|
|
511
|
-
catch {
|
|
512
|
-
continue;
|
|
513
|
-
}
|
|
514
|
-
if (entry.type !== "user" && entry.type !== "assistant") {
|
|
515
|
-
continue;
|
|
516
|
-
}
|
|
517
|
-
if (entry.isSidechain) {
|
|
518
|
-
continue;
|
|
519
|
-
}
|
|
520
|
-
if (entry.sessionId && entry.sessionId !== sessionId) {
|
|
521
|
-
continue;
|
|
522
|
-
}
|
|
523
|
-
const message = entry.message;
|
|
524
|
-
if (!message) {
|
|
525
|
-
continue;
|
|
526
|
-
}
|
|
527
|
-
const role = message.role === "assistant" ? "assistant" : message.role === "user" ? "user" : null;
|
|
528
|
-
if (!role) {
|
|
529
|
-
continue;
|
|
530
|
-
}
|
|
531
|
-
const content = message.content;
|
|
532
|
-
if (typeof content !== "string" && !Array.isArray(content)) {
|
|
533
|
-
continue;
|
|
534
|
-
}
|
|
535
|
-
for (const notification of toAcpNotifications(content, role, sessionId, toolUseCache, this.client, this.logger, { registerHooks: false })) {
|
|
536
|
-
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";
|
|
537
678
|
}
|
|
679
|
+
throw error;
|
|
680
|
+
}
|
|
681
|
+
else {
|
|
682
|
+
// eslint-disable-next-line preserve-caught-error
|
|
683
|
+
throw new Error("Invalid Mode");
|
|
538
684
|
}
|
|
539
685
|
}
|
|
540
|
-
|
|
541
|
-
|
|
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
|
+
}
|
|
542
702
|
}
|
|
543
703
|
}
|
|
544
704
|
async readTextFile(params) {
|
|
@@ -551,38 +711,47 @@ export class ClaudeAcpAgent {
|
|
|
551
711
|
}
|
|
552
712
|
canUseTool(sessionId) {
|
|
553
713
|
return async (toolName, toolInput, { signal, suggestions, toolUseID }) => {
|
|
714
|
+
const supportsTerminalOutput = this.clientCapabilities?._meta?.["terminal_output"] === true;
|
|
554
715
|
const session = this.sessions[sessionId];
|
|
555
716
|
if (!session) {
|
|
556
717
|
return {
|
|
557
718
|
behavior: "deny",
|
|
558
719
|
message: "Session not found",
|
|
559
|
-
interrupt: true,
|
|
560
720
|
};
|
|
561
721
|
}
|
|
562
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
|
+
}
|
|
563
739
|
const response = await this.client.requestPermission({
|
|
564
|
-
options
|
|
565
|
-
{
|
|
566
|
-
kind: "allow_always",
|
|
567
|
-
name: "Yes, and auto-accept edits",
|
|
568
|
-
optionId: "acceptEdits",
|
|
569
|
-
},
|
|
570
|
-
{ kind: "allow_once", name: "Yes, and manually approve edits", optionId: "default" },
|
|
571
|
-
{ kind: "reject_once", name: "No, keep planning", optionId: "plan" },
|
|
572
|
-
],
|
|
740
|
+
options,
|
|
573
741
|
sessionId,
|
|
574
742
|
toolCall: {
|
|
575
743
|
toolCallId: toolUseID,
|
|
576
744
|
rawInput: toolInput,
|
|
577
|
-
|
|
745
|
+
...toolInfoFromToolUse({ name: toolName, input: toolInput, id: toolUseID }, supportsTerminalOutput, session?.cwd),
|
|
578
746
|
},
|
|
579
747
|
});
|
|
580
748
|
if (signal.aborted || response.outcome?.outcome === "cancelled") {
|
|
581
749
|
throw new Error("Tool use aborted");
|
|
582
750
|
}
|
|
583
751
|
if (response.outcome?.outcome === "selected" &&
|
|
584
|
-
(response.outcome.optionId === "default" ||
|
|
585
|
-
|
|
752
|
+
(response.outcome.optionId === "default" ||
|
|
753
|
+
response.outcome.optionId === "acceptEdits" ||
|
|
754
|
+
response.outcome.optionId === "bypassPermissions")) {
|
|
586
755
|
await this.client.sessionUpdate({
|
|
587
756
|
sessionId,
|
|
588
757
|
update: {
|
|
@@ -590,6 +759,7 @@ export class ClaudeAcpAgent {
|
|
|
590
759
|
currentModeId: response.outcome.optionId,
|
|
591
760
|
},
|
|
592
761
|
});
|
|
762
|
+
await this.updateConfigOption(sessionId, "mode", response.outcome.optionId);
|
|
593
763
|
return {
|
|
594
764
|
behavior: "allow",
|
|
595
765
|
updatedInput: toolInput,
|
|
@@ -602,12 +772,10 @@ export class ClaudeAcpAgent {
|
|
|
602
772
|
return {
|
|
603
773
|
behavior: "deny",
|
|
604
774
|
message: "User rejected request to exit plan mode.",
|
|
605
|
-
interrupt: true,
|
|
606
775
|
};
|
|
607
776
|
}
|
|
608
777
|
}
|
|
609
|
-
if (session.
|
|
610
|
-
(session.permissionMode === "acceptEdits" && EDIT_TOOL_NAMES.includes(toolName))) {
|
|
778
|
+
if (session.modes.currentModeId === "bypassPermissions") {
|
|
611
779
|
return {
|
|
612
780
|
behavior: "allow",
|
|
613
781
|
updatedInput: toolInput,
|
|
@@ -630,7 +798,7 @@ export class ClaudeAcpAgent {
|
|
|
630
798
|
toolCall: {
|
|
631
799
|
toolCallId: toolUseID,
|
|
632
800
|
rawInput: toolInput,
|
|
633
|
-
|
|
801
|
+
...toolInfoFromToolUse({ name: toolName, input: toolInput, id: toolUseID }, supportsTerminalOutput, session?.cwd),
|
|
634
802
|
},
|
|
635
803
|
});
|
|
636
804
|
if (signal.aborted || response.outcome?.outcome === "cancelled") {
|
|
@@ -662,11 +830,69 @@ export class ClaudeAcpAgent {
|
|
|
662
830
|
return {
|
|
663
831
|
behavior: "deny",
|
|
664
832
|
message: "User refused permission to run tool",
|
|
665
|
-
interrupt: true,
|
|
666
833
|
};
|
|
667
834
|
}
|
|
668
835
|
};
|
|
669
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
|
+
}
|
|
670
896
|
async createSession(params, creationOpts = {}) {
|
|
671
897
|
// We want to create a new session id unless it is resume,
|
|
672
898
|
// but not resume + forkSession.
|
|
@@ -688,7 +914,8 @@ export class ClaudeAcpAgent {
|
|
|
688
914
|
const mcpServers = {};
|
|
689
915
|
if (Array.isArray(params.mcpServers)) {
|
|
690
916
|
for (const server of params.mcpServers) {
|
|
691
|
-
if ("type" in server) {
|
|
917
|
+
if ("type" in server && (server.type === "http" || server.type === "sse")) {
|
|
918
|
+
// HTTP or SSE type MCP server
|
|
692
919
|
mcpServers[server.name] = {
|
|
693
920
|
type: server.type,
|
|
694
921
|
url: server.url,
|
|
@@ -698,6 +925,7 @@ export class ClaudeAcpAgent {
|
|
|
698
925
|
};
|
|
699
926
|
}
|
|
700
927
|
else {
|
|
928
|
+
// Stdio type MCP server (with or without explicit type field)
|
|
701
929
|
mcpServers[server.name] = {
|
|
702
930
|
type: "stdio",
|
|
703
931
|
command: server.command,
|
|
@@ -709,15 +937,6 @@ export class ClaudeAcpAgent {
|
|
|
709
937
|
}
|
|
710
938
|
}
|
|
711
939
|
}
|
|
712
|
-
// Only add the acp MCP server if built-in tools are not disabled
|
|
713
|
-
if (!params._meta?.disableBuiltInTools) {
|
|
714
|
-
const server = createMcpServer(this, sessionId, this.clientCapabilities);
|
|
715
|
-
mcpServers["acp"] = {
|
|
716
|
-
type: "sdk",
|
|
717
|
-
name: "acp",
|
|
718
|
-
instance: server,
|
|
719
|
-
};
|
|
720
|
-
}
|
|
721
940
|
let systemPrompt = { type: "preset", preset: "claude_code" };
|
|
722
941
|
if (params._meta?.systemPrompt) {
|
|
723
942
|
const customPrompt = params._meta.systemPrompt;
|
|
@@ -730,19 +949,35 @@ export class ClaudeAcpAgent {
|
|
|
730
949
|
systemPrompt.append = customPrompt.append;
|
|
731
950
|
}
|
|
732
951
|
}
|
|
733
|
-
const permissionMode =
|
|
952
|
+
const permissionMode = resolvePermissionMode(settingsManager.getSettings().permissions?.defaultMode);
|
|
734
953
|
// Extract options from _meta if provided
|
|
735
|
-
const
|
|
954
|
+
const sessionMeta = params._meta;
|
|
955
|
+
const userProvidedOptions = sessionMeta?.claudeCode?.options;
|
|
736
956
|
// Configure thinking tokens from environment variable
|
|
737
957
|
const maxThinkingTokens = process.env.MAX_THINKING_TOKENS
|
|
738
958
|
? parseInt(process.env.MAX_THINKING_TOKENS, 10)
|
|
739
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();
|
|
740
969
|
const options = {
|
|
741
970
|
systemPrompt,
|
|
742
971
|
settingSources: ["user", "project", "local"],
|
|
743
|
-
stderr: (err) => this.logger.error(err),
|
|
744
972
|
...(maxThinkingTokens !== undefined && { maxThinkingTokens }),
|
|
745
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
|
+
},
|
|
746
981
|
// Override certain fields that must be controlled by ACP
|
|
747
982
|
cwd: params.cwd,
|
|
748
983
|
includePartialMessages: true,
|
|
@@ -754,29 +989,26 @@ export class ClaudeAcpAgent {
|
|
|
754
989
|
canUseTool: this.canUseTool(sessionId),
|
|
755
990
|
// note: although not documented by the types, passing an absolute path
|
|
756
991
|
// here works to find zed's managed node version.
|
|
757
|
-
executable: process.execPath,
|
|
758
|
-
...(process.env.CLAUDE_CODE_EXECUTABLE
|
|
759
|
-
pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE
|
|
760
|
-
|
|
761
|
-
|
|
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,
|
|
762
1004
|
hooks: {
|
|
763
1005
|
...userProvidedOptions?.hooks,
|
|
764
|
-
PreToolUse: [
|
|
765
|
-
...(userProvidedOptions?.hooks?.PreToolUse || []),
|
|
766
|
-
{
|
|
767
|
-
hooks: [createPreToolUseHook(settingsManager, this.logger)],
|
|
768
|
-
},
|
|
769
|
-
],
|
|
770
1006
|
PostToolUse: [
|
|
771
1007
|
...(userProvidedOptions?.hooks?.PostToolUse || []),
|
|
772
1008
|
{
|
|
773
1009
|
hooks: [
|
|
774
1010
|
createPostToolUseHook(this.logger, {
|
|
775
1011
|
onEnterPlanMode: async () => {
|
|
776
|
-
const session = this.sessions[sessionId];
|
|
777
|
-
if (session) {
|
|
778
|
-
session.permissionMode = "plan";
|
|
779
|
-
}
|
|
780
1012
|
await this.client.sessionUpdate({
|
|
781
1013
|
sessionId,
|
|
782
1014
|
update: {
|
|
@@ -784,6 +1016,7 @@ export class ClaudeAcpAgent {
|
|
|
784
1016
|
currentModeId: "plan",
|
|
785
1017
|
},
|
|
786
1018
|
});
|
|
1019
|
+
await this.updateConfigOption(sessionId, "mode", "plan");
|
|
787
1020
|
},
|
|
788
1021
|
}),
|
|
789
1022
|
],
|
|
@@ -791,41 +1024,17 @@ export class ClaudeAcpAgent {
|
|
|
791
1024
|
],
|
|
792
1025
|
},
|
|
793
1026
|
...creationOpts,
|
|
1027
|
+
abortController,
|
|
794
1028
|
};
|
|
1029
|
+
options.additionalDirectories = [
|
|
1030
|
+
...(userProvidedOptions?.additionalDirectories ?? []),
|
|
1031
|
+
...(sessionMeta?.additionalRoots ?? []),
|
|
1032
|
+
];
|
|
795
1033
|
if (creationOpts?.resume === undefined || creationOpts?.forkSession) {
|
|
796
1034
|
// Set our own session id if not resuming an existing session.
|
|
797
1035
|
options.sessionId = sessionId;
|
|
798
1036
|
}
|
|
799
|
-
const allowedTools = [];
|
|
800
|
-
// Disable this for now, not a great way to expose this over ACP at the moment (in progress work so we can revisit)
|
|
801
|
-
const disallowedTools = ["AskUserQuestion"];
|
|
802
|
-
// Check if built-in tools should be disabled
|
|
803
|
-
const disableBuiltInTools = params._meta?.disableBuiltInTools === true;
|
|
804
|
-
if (!disableBuiltInTools) {
|
|
805
|
-
if (this.clientCapabilities?.fs?.readTextFile) {
|
|
806
|
-
allowedTools.push(acpToolNames.read);
|
|
807
|
-
disallowedTools.push("Read");
|
|
808
|
-
}
|
|
809
|
-
if (this.clientCapabilities?.fs?.writeTextFile) {
|
|
810
|
-
disallowedTools.push("Write", "Edit");
|
|
811
|
-
}
|
|
812
|
-
if (this.clientCapabilities?.terminal) {
|
|
813
|
-
allowedTools.push(acpToolNames.bashOutput, acpToolNames.killShell);
|
|
814
|
-
disallowedTools.push("Bash", "BashOutput", "KillShell");
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
else {
|
|
818
|
-
// When built-in tools are disabled, explicitly disallow all of them
|
|
819
|
-
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");
|
|
820
|
-
}
|
|
821
|
-
if (allowedTools.length > 0) {
|
|
822
|
-
options.allowedTools = allowedTools;
|
|
823
|
-
}
|
|
824
|
-
if (disallowedTools.length > 0) {
|
|
825
|
-
options.disallowedTools = [...(options.disallowedTools || []), ...disallowedTools];
|
|
826
|
-
}
|
|
827
1037
|
// Handle abort controller from meta options
|
|
828
|
-
const abortController = userProvidedOptions?.abortController;
|
|
829
1038
|
if (abortController?.signal.aborted) {
|
|
830
1039
|
throw new Error("Cancelled");
|
|
831
1040
|
}
|
|
@@ -842,29 +1051,26 @@ export class ClaudeAcpAgent {
|
|
|
842
1051
|
prompt: input,
|
|
843
1052
|
options,
|
|
844
1053
|
});
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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
|
+
}
|
|
855
1071
|
const models = await getAvailableModels(q, initializationResult.models, settingsManager);
|
|
856
1072
|
const sessionCreateElapsed = Date.now() - sessionCreateStartTime;
|
|
857
1073
|
this.logger.log(`✅ [ACP] 会话创建完成, 总耗时: ${sessionCreateElapsed}ms`);
|
|
858
|
-
// Needs to happen after we return the session
|
|
859
|
-
setTimeout(() => {
|
|
860
|
-
this.client.sessionUpdate({
|
|
861
|
-
sessionId,
|
|
862
|
-
update: {
|
|
863
|
-
sessionUpdate: "available_commands_update",
|
|
864
|
-
availableCommands: getAvailableSlashCommands(initializationResult.commands),
|
|
865
|
-
},
|
|
866
|
-
});
|
|
867
|
-
}, 0);
|
|
868
1074
|
const availableModes = [
|
|
869
1075
|
{
|
|
870
1076
|
id: "default",
|
|
@@ -895,28 +1101,162 @@ export class ClaudeAcpAgent {
|
|
|
895
1101
|
description: "Bypass all permission checks",
|
|
896
1102
|
});
|
|
897
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
|
+
};
|
|
898
1129
|
return {
|
|
899
1130
|
sessionId,
|
|
900
1131
|
models,
|
|
901
|
-
modes
|
|
902
|
-
|
|
903
|
-
availableModes,
|
|
904
|
-
},
|
|
1132
|
+
modes,
|
|
1133
|
+
configOptions,
|
|
905
1134
|
};
|
|
906
1135
|
}
|
|
907
1136
|
}
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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
|
+
}
|
|
911
1252
|
}
|
|
1253
|
+
return bestMatch;
|
|
1254
|
+
}
|
|
1255
|
+
async function getAvailableModels(query, models, settingsManager) {
|
|
912
1256
|
const settings = settingsManager.getSettings();
|
|
913
1257
|
let currentModel = models[0];
|
|
914
1258
|
if (settings.model) {
|
|
915
|
-
const match = models
|
|
916
|
-
m.value.includes(settings.model) ||
|
|
917
|
-
settings.model.includes(m.value) ||
|
|
918
|
-
m.displayName.toLowerCase() === settings.model.toLowerCase() ||
|
|
919
|
-
m.displayName.toLowerCase().includes(settings.model.toLowerCase()));
|
|
1259
|
+
const match = resolveModelPreference(models, settings.model);
|
|
920
1260
|
if (match) {
|
|
921
1261
|
currentModel = match;
|
|
922
1262
|
}
|
|
@@ -988,10 +1328,10 @@ export function promptToClaude(prompt) {
|
|
|
988
1328
|
case "text": {
|
|
989
1329
|
let text = chunk.text;
|
|
990
1330
|
// change /mcp:server:command args -> /server:command (MCP) args
|
|
991
|
-
const mcpMatch = text.match(/^\/mcp:([^:\s]+):(\S+)(
|
|
1331
|
+
const mcpMatch = text.match(/^\/mcp:([^:\s]+):(\S+)(?:\s(.*))?$/);
|
|
992
1332
|
if (mcpMatch) {
|
|
993
1333
|
const [, server, command, args] = mcpMatch;
|
|
994
|
-
text = `/${server}:${command} (MCP)${args
|
|
1334
|
+
text = `/${server}:${command} (MCP)${args ? ` ${args}` : ""}`;
|
|
995
1335
|
}
|
|
996
1336
|
content.push({ type: "text", text });
|
|
997
1337
|
break;
|
|
@@ -1062,19 +1402,25 @@ export function promptToClaude(prompt) {
|
|
|
1062
1402
|
*/
|
|
1063
1403
|
export function toAcpNotifications(content, role, sessionId, toolUseCache, client, logger, options) {
|
|
1064
1404
|
const registerHooks = options?.registerHooks !== false;
|
|
1405
|
+
const supportsTerminalOutput = options?.clientCapabilities?._meta?.["terminal_output"] === true;
|
|
1065
1406
|
if (typeof content === "string") {
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
content: {
|
|
1072
|
-
type: "text",
|
|
1073
|
-
text: content,
|
|
1074
|
-
},
|
|
1075
|
-
},
|
|
1407
|
+
const update = {
|
|
1408
|
+
sessionUpdate: role === "assistant" ? "agent_message_chunk" : "user_message_chunk",
|
|
1409
|
+
content: {
|
|
1410
|
+
type: "text",
|
|
1411
|
+
text: content,
|
|
1076
1412
|
},
|
|
1077
|
-
|
|
1413
|
+
};
|
|
1414
|
+
if (options?.parentToolUseId) {
|
|
1415
|
+
update._meta = {
|
|
1416
|
+
...update._meta,
|
|
1417
|
+
claudeCode: {
|
|
1418
|
+
...(update._meta?.claudeCode || {}),
|
|
1419
|
+
parentToolUseId: options.parentToolUseId,
|
|
1420
|
+
},
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
return [{ sessionId, update }];
|
|
1078
1424
|
}
|
|
1079
1425
|
const output = [];
|
|
1080
1426
|
// Only handle the first chunk for streaming; extend as needed for batching
|
|
@@ -1115,6 +1461,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
1115
1461
|
case "tool_use":
|
|
1116
1462
|
case "server_tool_use":
|
|
1117
1463
|
case "mcp_tool_use": {
|
|
1464
|
+
const alreadyCached = chunk.id in toolUseCache;
|
|
1118
1465
|
toolUseCache[chunk.id] = chunk;
|
|
1119
1466
|
if (chunk.name === "TodoWrite") {
|
|
1120
1467
|
// @ts-expect-error - sometimes input is empty object
|
|
@@ -1126,11 +1473,13 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
1126
1473
|
}
|
|
1127
1474
|
}
|
|
1128
1475
|
else {
|
|
1129
|
-
|
|
1476
|
+
// Only register hooks on first encounter to avoid double-firing
|
|
1477
|
+
if (registerHooks && !alreadyCached) {
|
|
1130
1478
|
registerHookCallback(chunk.id, {
|
|
1131
1479
|
onPostToolUseHook: async (toolUseId, toolInput, toolResponse) => {
|
|
1132
1480
|
const toolUse = toolUseCache[toolUseId];
|
|
1133
1481
|
if (toolUse) {
|
|
1482
|
+
const editDiff = toolUse.name === "Edit" ? toolUpdateFromEditToolResponse(toolResponse) : {};
|
|
1134
1483
|
const update = {
|
|
1135
1484
|
_meta: {
|
|
1136
1485
|
claudeCode: {
|
|
@@ -1140,6 +1489,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
1140
1489
|
},
|
|
1141
1490
|
toolCallId: toolUseId,
|
|
1142
1491
|
sessionUpdate: "tool_call_update",
|
|
1492
|
+
...editDiff,
|
|
1143
1493
|
};
|
|
1144
1494
|
await client.sessionUpdate({
|
|
1145
1495
|
sessionId,
|
|
@@ -1147,7 +1497,7 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
1147
1497
|
});
|
|
1148
1498
|
}
|
|
1149
1499
|
else {
|
|
1150
|
-
logger.error(`[claude-
|
|
1500
|
+
logger.error(`[claude-agent-acp] Got a tool response for tool use that wasn't tracked: ${toolUseId}`);
|
|
1151
1501
|
}
|
|
1152
1502
|
},
|
|
1153
1503
|
});
|
|
@@ -1159,18 +1509,41 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
1159
1509
|
catch {
|
|
1160
1510
|
// ignore if we can't turn it to JSON
|
|
1161
1511
|
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1512
|
+
if (alreadyCached) {
|
|
1513
|
+
// Second encounter (full assistant message after streaming) —
|
|
1514
|
+
// send as tool_call_update to refine the existing tool_call
|
|
1515
|
+
// rather than emitting a duplicate tool_call.
|
|
1516
|
+
update = {
|
|
1517
|
+
_meta: {
|
|
1518
|
+
claudeCode: {
|
|
1519
|
+
toolName: chunk.name,
|
|
1520
|
+
},
|
|
1166
1521
|
},
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1522
|
+
toolCallId: chunk.id,
|
|
1523
|
+
sessionUpdate: "tool_call_update",
|
|
1524
|
+
rawInput,
|
|
1525
|
+
...toolInfoFromToolUse(chunk, supportsTerminalOutput, options?.cwd),
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
else {
|
|
1529
|
+
// First encounter (streaming content_block_start or replay) —
|
|
1530
|
+
// send as tool_call with terminal_info for Bash tools.
|
|
1531
|
+
update = {
|
|
1532
|
+
_meta: {
|
|
1533
|
+
claudeCode: {
|
|
1534
|
+
toolName: chunk.name,
|
|
1535
|
+
},
|
|
1536
|
+
...(chunk.name === "Bash" && supportsTerminalOutput
|
|
1537
|
+
? { terminal_info: { terminal_id: chunk.id } }
|
|
1538
|
+
: {}),
|
|
1539
|
+
},
|
|
1540
|
+
toolCallId: chunk.id,
|
|
1541
|
+
sessionUpdate: "tool_call",
|
|
1542
|
+
rawInput,
|
|
1543
|
+
status: "pending",
|
|
1544
|
+
...toolInfoFromToolUse(chunk, supportsTerminalOutput, options?.cwd),
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1174
1547
|
}
|
|
1175
1548
|
break;
|
|
1176
1549
|
}
|
|
@@ -1184,21 +1557,43 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
1184
1557
|
case "mcp_tool_result": {
|
|
1185
1558
|
const toolUse = toolUseCache[chunk.tool_use_id];
|
|
1186
1559
|
if (!toolUse) {
|
|
1187
|
-
logger.error(`[claude-
|
|
1560
|
+
logger.error(`[claude-agent-acp] Got a tool result for tool use that wasn't tracked: ${chunk.tool_use_id}`);
|
|
1188
1561
|
break;
|
|
1189
1562
|
}
|
|
1190
1563
|
if (toolUse.name !== "TodoWrite") {
|
|
1564
|
+
const { _meta: toolMeta, ...toolUpdate } = toolUpdateFromToolResult(chunk, toolUseCache[chunk.tool_use_id], supportsTerminalOutput);
|
|
1565
|
+
// When terminal output is supported, send terminal_output as a
|
|
1566
|
+
// separate notification to match codex-acp's streaming lifecycle:
|
|
1567
|
+
// 1. tool_call → _meta.terminal_info (already sent above)
|
|
1568
|
+
// 2. tool_call_update → _meta.terminal_output (sent here)
|
|
1569
|
+
// 3. tool_call_update → _meta.terminal_exit (sent below with status)
|
|
1570
|
+
if (toolMeta?.terminal_output) {
|
|
1571
|
+
output.push({
|
|
1572
|
+
sessionId,
|
|
1573
|
+
update: {
|
|
1574
|
+
_meta: {
|
|
1575
|
+
terminal_output: toolMeta.terminal_output,
|
|
1576
|
+
...(options?.parentToolUseId
|
|
1577
|
+
? { claudeCode: { parentToolUseId: options.parentToolUseId } }
|
|
1578
|
+
: {}),
|
|
1579
|
+
},
|
|
1580
|
+
toolCallId: chunk.tool_use_id,
|
|
1581
|
+
sessionUpdate: "tool_call_update",
|
|
1582
|
+
},
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1191
1585
|
update = {
|
|
1192
1586
|
_meta: {
|
|
1193
1587
|
claudeCode: {
|
|
1194
1588
|
toolName: toolUse.name,
|
|
1195
1589
|
},
|
|
1590
|
+
...(toolMeta?.terminal_exit ? { terminal_exit: toolMeta.terminal_exit } : {}),
|
|
1196
1591
|
},
|
|
1197
1592
|
toolCallId: chunk.tool_use_id,
|
|
1198
1593
|
sessionUpdate: "tool_call_update",
|
|
1199
1594
|
status: "is_error" in chunk && chunk.is_error ? "failed" : "completed",
|
|
1200
1595
|
rawOutput: chunk.content,
|
|
1201
|
-
...
|
|
1596
|
+
...toolUpdate,
|
|
1202
1597
|
};
|
|
1203
1598
|
}
|
|
1204
1599
|
break;
|
|
@@ -1218,18 +1613,35 @@ export function toAcpNotifications(content, role, sessionId, toolUseCache, clien
|
|
|
1218
1613
|
break;
|
|
1219
1614
|
}
|
|
1220
1615
|
if (update) {
|
|
1616
|
+
if (options?.parentToolUseId) {
|
|
1617
|
+
update._meta = {
|
|
1618
|
+
...update._meta,
|
|
1619
|
+
claudeCode: {
|
|
1620
|
+
...(update._meta?.claudeCode || {}),
|
|
1621
|
+
parentToolUseId: options.parentToolUseId,
|
|
1622
|
+
},
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1221
1625
|
output.push({ sessionId, update });
|
|
1222
1626
|
}
|
|
1223
1627
|
}
|
|
1224
1628
|
return output;
|
|
1225
1629
|
}
|
|
1226
|
-
export function streamEventToAcpNotifications(message, sessionId, toolUseCache, client, logger) {
|
|
1630
|
+
export function streamEventToAcpNotifications(message, sessionId, toolUseCache, client, logger, options) {
|
|
1227
1631
|
const event = message.event;
|
|
1228
1632
|
switch (event.type) {
|
|
1229
1633
|
case "content_block_start":
|
|
1230
|
-
return toAcpNotifications([event.content_block], "assistant", sessionId, toolUseCache, client, logger
|
|
1634
|
+
return toAcpNotifications([event.content_block], "assistant", sessionId, toolUseCache, client, logger, {
|
|
1635
|
+
clientCapabilities: options?.clientCapabilities,
|
|
1636
|
+
parentToolUseId: message.parent_tool_use_id,
|
|
1637
|
+
cwd: options?.cwd,
|
|
1638
|
+
});
|
|
1231
1639
|
case "content_block_delta":
|
|
1232
|
-
return toAcpNotifications([event.delta], "assistant", sessionId, toolUseCache, client, logger
|
|
1640
|
+
return toAcpNotifications([event.delta], "assistant", sessionId, toolUseCache, client, logger, {
|
|
1641
|
+
clientCapabilities: options?.clientCapabilities,
|
|
1642
|
+
parentToolUseId: message.parent_tool_use_id,
|
|
1643
|
+
cwd: options?.cwd,
|
|
1644
|
+
});
|
|
1233
1645
|
// No content
|
|
1234
1646
|
case "message_start":
|
|
1235
1647
|
case "message_delta":
|
|
@@ -1247,3 +1659,24 @@ export function runAcp() {
|
|
|
1247
1659
|
const stream = ndJsonStream(input, output);
|
|
1248
1660
|
new AgentSideConnection((client) => new ClaudeAcpAgent(client), stream);
|
|
1249
1661
|
}
|
|
1662
|
+
function commonPrefixLength(a, b) {
|
|
1663
|
+
let i = 0;
|
|
1664
|
+
while (i < a.length && i < b.length && a[i] === b[i]) {
|
|
1665
|
+
i++;
|
|
1666
|
+
}
|
|
1667
|
+
return i;
|
|
1668
|
+
}
|
|
1669
|
+
function getMatchingModelUsage(modelUsage, currentModel) {
|
|
1670
|
+
let bestKey = null;
|
|
1671
|
+
let bestLen = 0;
|
|
1672
|
+
for (const key of Object.keys(modelUsage)) {
|
|
1673
|
+
const len = commonPrefixLength(key, currentModel);
|
|
1674
|
+
if (len > bestLen) {
|
|
1675
|
+
bestLen = len;
|
|
1676
|
+
bestKey = key;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
if (bestKey) {
|
|
1680
|
+
return modelUsage[bestKey];
|
|
1681
|
+
}
|
|
1682
|
+
}
|