claude-code-acp-ts 0.12.6
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 +222 -0
- package/README.md +53 -0
- package/dist/acp-agent.js +999 -0
- package/dist/index.js +20 -0
- package/dist/lib.js +6 -0
- package/dist/mcp-server.js +726 -0
- package/dist/settings.js +422 -0
- package/dist/tests/acp-agent.test.js +753 -0
- package/dist/tests/extract-lines.test.js +79 -0
- package/dist/tests/replace-and-calculate-location.test.js +266 -0
- package/dist/tests/settings.test.js +462 -0
- package/dist/tools.js +555 -0
- package/dist/utils.js +150 -0
- package/package.json +73 -0
|
@@ -0,0 +1,999 @@
|
|
|
1
|
+
import { AgentSideConnection, ndJsonStream, RequestError, } from "@agentclientprotocol/sdk";
|
|
2
|
+
import { SettingsManager } from "./settings.js";
|
|
3
|
+
import { query, } from "@anthropic-ai/claude-agent-sdk";
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import * as os from "node:os";
|
|
7
|
+
import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js";
|
|
8
|
+
import { createMcpServer } from "./mcp-server.js";
|
|
9
|
+
import { EDIT_TOOL_NAMES, acpToolNames } from "./tools.js";
|
|
10
|
+
import { toolInfoFromToolUse, planEntries, toolUpdateFromToolResult, registerHookCallback, createPostToolUseHook, createPreToolUseHook, } from "./tools.js";
|
|
11
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
export const CLAUDE_CONFIG_DIR = process.env.CLAUDE ?? path.join(os.homedir(), ".claude");
|
|
14
|
+
// ==================== 超时配置 ====================
|
|
15
|
+
// 可通过环境变量覆盖默认值
|
|
16
|
+
/** 获取模型列表超时时间(毫秒),默认 30 秒 */
|
|
17
|
+
const MODEL_FETCH_TIMEOUT_MS = parseInt(process.env.ACP_MODEL_FETCH_TIMEOUT_MS || "30000", 10);
|
|
18
|
+
/** 获取命令列表超时时间(毫秒),默认 30 秒 */
|
|
19
|
+
const COMMAND_FETCH_TIMEOUT_MS = parseInt(process.env.ACP_COMMAND_FETCH_TIMEOUT_MS || "30000", 10);
|
|
20
|
+
/** 超时后是否使用默认值继续(而不是抛出错误),默认 true */
|
|
21
|
+
const USE_DEFAULTS_ON_TIMEOUT = process.env.ACP_USE_DEFAULTS_ON_TIMEOUT !== "false";
|
|
22
|
+
/**
|
|
23
|
+
* 为 Promise 添加超时保护
|
|
24
|
+
* @param promise 要执行的 Promise
|
|
25
|
+
* @param timeoutMs 超时时间(毫秒)
|
|
26
|
+
* @param operationName 操作名称(用于日志)
|
|
27
|
+
* @param logger 日志记录器
|
|
28
|
+
* @returns Promise 执行结果
|
|
29
|
+
*/
|
|
30
|
+
async function withTimeout(promise, timeoutMs, operationName, logger = console) {
|
|
31
|
+
const start = Date.now();
|
|
32
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
33
|
+
setTimeout(() => {
|
|
34
|
+
const elapsed = Date.now() - start;
|
|
35
|
+
logger.error(`⏰ [ACP] ${operationName} 超时 (${timeoutMs}ms), 实际耗时: ${elapsed}ms`);
|
|
36
|
+
reject(new Error(`${operationName} timed out after ${timeoutMs}ms`));
|
|
37
|
+
}, timeoutMs);
|
|
38
|
+
});
|
|
39
|
+
try {
|
|
40
|
+
const result = await Promise.race([promise, timeoutPromise]);
|
|
41
|
+
const elapsed = Date.now() - start;
|
|
42
|
+
if (elapsed > 5000) {
|
|
43
|
+
logger.log(`⚠️ [ACP] ${operationName} 耗时较长: ${elapsed}ms`);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
logger.log(`✓ [ACP] ${operationName} 完成, 耗时: ${elapsed}ms`);
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Bypass Permissions doesn't work if we are a root/sudo user
|
|
55
|
+
const IS_ROOT = (process.geteuid?.() ?? process.getuid?.()) === 0;
|
|
56
|
+
// Implement the ACP Agent interface
|
|
57
|
+
export class ClaudeAcpAgent {
|
|
58
|
+
constructor(client, logger) {
|
|
59
|
+
this.backgroundTerminals = {};
|
|
60
|
+
this.sessions = {};
|
|
61
|
+
this.client = client;
|
|
62
|
+
this.toolUseCache = {};
|
|
63
|
+
this.logger = logger ?? console;
|
|
64
|
+
}
|
|
65
|
+
async initialize(request) {
|
|
66
|
+
this.clientCapabilities = request.clientCapabilities;
|
|
67
|
+
// Default authMethod
|
|
68
|
+
const authMethod = {
|
|
69
|
+
description: "Run `claude /login` in the terminal",
|
|
70
|
+
name: "Log in with Claude Code",
|
|
71
|
+
id: "claude-login",
|
|
72
|
+
};
|
|
73
|
+
// If client supports terminal-auth capability, use that instead.
|
|
74
|
+
// if (request.clientCapabilities?._meta?.["terminal-auth"] === true) {
|
|
75
|
+
// const cliPath = fileURLToPath(import.meta.resolve("@anthropic-ai/claude-agent-sdk/cli.js"));
|
|
76
|
+
// authMethod._meta = {
|
|
77
|
+
// "terminal-auth": {
|
|
78
|
+
// command: "node",
|
|
79
|
+
// args: [cliPath, "/login"],
|
|
80
|
+
// label: "Claude Code Login",
|
|
81
|
+
// },
|
|
82
|
+
// };
|
|
83
|
+
// }
|
|
84
|
+
return {
|
|
85
|
+
protocolVersion: 1,
|
|
86
|
+
agentCapabilities: {
|
|
87
|
+
promptCapabilities: {
|
|
88
|
+
image: true,
|
|
89
|
+
embeddedContext: true,
|
|
90
|
+
},
|
|
91
|
+
mcpCapabilities: {
|
|
92
|
+
http: true,
|
|
93
|
+
sse: true,
|
|
94
|
+
},
|
|
95
|
+
sessionCapabilities: {
|
|
96
|
+
fork: {},
|
|
97
|
+
resume: {},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
agentInfo: {
|
|
101
|
+
name: packageJson.name,
|
|
102
|
+
title: "Claude Code",
|
|
103
|
+
version: packageJson.version,
|
|
104
|
+
},
|
|
105
|
+
authMethods: [authMethod],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
async newSession(params) {
|
|
109
|
+
if (fs.existsSync(path.resolve(os.homedir(), ".claude.json.backup")) &&
|
|
110
|
+
!fs.existsSync(path.resolve(os.homedir(), ".claude.json"))) {
|
|
111
|
+
throw RequestError.authRequired();
|
|
112
|
+
}
|
|
113
|
+
return await this.createSession(params, {
|
|
114
|
+
// Revisit these meta values once we support resume
|
|
115
|
+
resume: params._meta?.claudeCode?.options?.resume,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
async unstable_forkSession(params) {
|
|
119
|
+
return await this.createSession({
|
|
120
|
+
cwd: params.cwd,
|
|
121
|
+
mcpServers: params.mcpServers ?? [],
|
|
122
|
+
_meta: params._meta,
|
|
123
|
+
}, {
|
|
124
|
+
resume: params.sessionId,
|
|
125
|
+
forkSession: true,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
async unstable_resumeSession(params) {
|
|
129
|
+
const response = await this.createSession({
|
|
130
|
+
cwd: params.cwd,
|
|
131
|
+
mcpServers: params.mcpServers ?? [],
|
|
132
|
+
_meta: params._meta,
|
|
133
|
+
}, {
|
|
134
|
+
resume: params.sessionId,
|
|
135
|
+
});
|
|
136
|
+
return response;
|
|
137
|
+
}
|
|
138
|
+
async authenticate(_params) {
|
|
139
|
+
throw new Error("Method not implemented.");
|
|
140
|
+
}
|
|
141
|
+
async prompt(params) {
|
|
142
|
+
if (!this.sessions[params.sessionId]) {
|
|
143
|
+
throw new Error("Session not found");
|
|
144
|
+
}
|
|
145
|
+
this.sessions[params.sessionId].cancelled = false;
|
|
146
|
+
const { query, input } = this.sessions[params.sessionId];
|
|
147
|
+
input.push(promptToClaude(params));
|
|
148
|
+
while (true) {
|
|
149
|
+
const { value: message, done } = await query.next();
|
|
150
|
+
if (done || !message) {
|
|
151
|
+
if (this.sessions[params.sessionId].cancelled) {
|
|
152
|
+
return { stopReason: "cancelled" };
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
switch (message.type) {
|
|
157
|
+
case "system":
|
|
158
|
+
switch (message.subtype) {
|
|
159
|
+
case "init":
|
|
160
|
+
break;
|
|
161
|
+
case "compact_boundary":
|
|
162
|
+
case "hook_response":
|
|
163
|
+
case "status":
|
|
164
|
+
// Todo: process via status api: https://docs.claude.com/en/docs/claude-code/hooks#hook-output
|
|
165
|
+
break;
|
|
166
|
+
default:
|
|
167
|
+
unreachable(message, this.logger);
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
break;
|
|
171
|
+
case "result": {
|
|
172
|
+
if (this.sessions[params.sessionId].cancelled) {
|
|
173
|
+
return { stopReason: "cancelled" };
|
|
174
|
+
}
|
|
175
|
+
switch (message.subtype) {
|
|
176
|
+
case "success": {
|
|
177
|
+
if (message.result.includes("Please run /login")) {
|
|
178
|
+
throw RequestError.authRequired();
|
|
179
|
+
}
|
|
180
|
+
if (message.is_error) {
|
|
181
|
+
throw RequestError.internalError(undefined, message.result);
|
|
182
|
+
}
|
|
183
|
+
return { stopReason: "end_turn" };
|
|
184
|
+
}
|
|
185
|
+
case "error_during_execution":
|
|
186
|
+
if (message.is_error) {
|
|
187
|
+
throw RequestError.internalError(undefined, message.errors.join(", ") || message.subtype);
|
|
188
|
+
}
|
|
189
|
+
return { stopReason: "end_turn" };
|
|
190
|
+
case "error_max_budget_usd":
|
|
191
|
+
case "error_max_turns":
|
|
192
|
+
case "error_max_structured_output_retries":
|
|
193
|
+
if (message.is_error) {
|
|
194
|
+
throw RequestError.internalError(undefined, message.errors.join(", ") || message.subtype);
|
|
195
|
+
}
|
|
196
|
+
return { stopReason: "max_turn_requests" };
|
|
197
|
+
default:
|
|
198
|
+
unreachable(message, this.logger);
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
case "stream_event": {
|
|
204
|
+
for (const notification of streamEventToAcpNotifications(message, params.sessionId, this.toolUseCache, this.client, this.logger)) {
|
|
205
|
+
await this.client.sessionUpdate(notification);
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
case "user":
|
|
210
|
+
case "assistant": {
|
|
211
|
+
if (this.sessions[params.sessionId].cancelled) {
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
// Slash commands like /compact can generate invalid output... doesn't match
|
|
215
|
+
// their own docs: https://docs.anthropic.com/en/docs/claude-code/sdk/sdk-slash-commands#%2Fcompact-compact-conversation-history
|
|
216
|
+
if (typeof message.message.content === "string" &&
|
|
217
|
+
message.message.content.includes("<local-command-stdout>")) {
|
|
218
|
+
this.logger.log(message.message.content);
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
if (typeof message.message.content === "string" &&
|
|
222
|
+
message.message.content.includes("<local-command-stderr>")) {
|
|
223
|
+
this.logger.error(message.message.content);
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
// Skip these user messages for now, since they seem to just be messages we don't want in the feed
|
|
227
|
+
if (message.type === "user" &&
|
|
228
|
+
(typeof message.message.content === "string" ||
|
|
229
|
+
(Array.isArray(message.message.content) &&
|
|
230
|
+
message.message.content.length === 1 &&
|
|
231
|
+
message.message.content[0].type === "text"))) {
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
if (message.type === "assistant" &&
|
|
235
|
+
message.message.model === "<synthetic>" &&
|
|
236
|
+
Array.isArray(message.message.content) &&
|
|
237
|
+
message.message.content.length === 1 &&
|
|
238
|
+
message.message.content[0].type === "text" &&
|
|
239
|
+
message.message.content[0].text.includes("Please run /login")) {
|
|
240
|
+
throw RequestError.authRequired();
|
|
241
|
+
}
|
|
242
|
+
const content = message.type === "assistant"
|
|
243
|
+
? // Handled by stream events above
|
|
244
|
+
message.message.content.filter((item) => !["text", "thinking"].includes(item.type))
|
|
245
|
+
: message.message.content;
|
|
246
|
+
for (const notification of toAcpNotifications(content, message.message.role, params.sessionId, this.toolUseCache, this.client, this.logger)) {
|
|
247
|
+
await this.client.sessionUpdate(notification);
|
|
248
|
+
}
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
case "tool_progress":
|
|
252
|
+
break;
|
|
253
|
+
case "auth_status":
|
|
254
|
+
break;
|
|
255
|
+
default:
|
|
256
|
+
unreachable(message);
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
throw new Error("Session did not end in result");
|
|
261
|
+
}
|
|
262
|
+
async cancel(params) {
|
|
263
|
+
if (!this.sessions[params.sessionId]) {
|
|
264
|
+
throw new Error("Session not found");
|
|
265
|
+
}
|
|
266
|
+
this.sessions[params.sessionId].cancelled = true;
|
|
267
|
+
await this.sessions[params.sessionId].query.interrupt();
|
|
268
|
+
}
|
|
269
|
+
async unstable_setSessionModel(params) {
|
|
270
|
+
if (!this.sessions[params.sessionId]) {
|
|
271
|
+
throw new Error("Session not found");
|
|
272
|
+
}
|
|
273
|
+
await this.sessions[params.sessionId].query.setModel(params.modelId);
|
|
274
|
+
}
|
|
275
|
+
async setSessionMode(params) {
|
|
276
|
+
if (!this.sessions[params.sessionId]) {
|
|
277
|
+
throw new Error("Session not found");
|
|
278
|
+
}
|
|
279
|
+
switch (params.modeId) {
|
|
280
|
+
case "default":
|
|
281
|
+
case "acceptEdits":
|
|
282
|
+
case "bypassPermissions":
|
|
283
|
+
case "dontAsk":
|
|
284
|
+
case "plan":
|
|
285
|
+
this.sessions[params.sessionId].permissionMode = params.modeId;
|
|
286
|
+
try {
|
|
287
|
+
await this.sessions[params.sessionId].query.setPermissionMode(params.modeId);
|
|
288
|
+
}
|
|
289
|
+
catch (error) {
|
|
290
|
+
const errorMessage = error instanceof Error && error.message ? error.message : "Invalid Mode";
|
|
291
|
+
throw new Error(errorMessage);
|
|
292
|
+
}
|
|
293
|
+
return {};
|
|
294
|
+
default:
|
|
295
|
+
throw new Error("Invalid Mode");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
async readTextFile(params) {
|
|
299
|
+
const response = await this.client.readTextFile(params);
|
|
300
|
+
return response;
|
|
301
|
+
}
|
|
302
|
+
async writeTextFile(params) {
|
|
303
|
+
const response = await this.client.writeTextFile(params);
|
|
304
|
+
return response;
|
|
305
|
+
}
|
|
306
|
+
canUseTool(sessionId) {
|
|
307
|
+
return async (toolName, toolInput, { signal, suggestions, toolUseID }) => {
|
|
308
|
+
const session = this.sessions[sessionId];
|
|
309
|
+
if (!session) {
|
|
310
|
+
return {
|
|
311
|
+
behavior: "deny",
|
|
312
|
+
message: "Session not found",
|
|
313
|
+
interrupt: true,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
if (toolName === "ExitPlanMode") {
|
|
317
|
+
const response = await this.client.requestPermission({
|
|
318
|
+
options: [
|
|
319
|
+
{
|
|
320
|
+
kind: "allow_always",
|
|
321
|
+
name: "Yes, and auto-accept edits",
|
|
322
|
+
optionId: "acceptEdits",
|
|
323
|
+
},
|
|
324
|
+
{ kind: "allow_once", name: "Yes, and manually approve edits", optionId: "default" },
|
|
325
|
+
{ kind: "reject_once", name: "No, keep planning", optionId: "plan" },
|
|
326
|
+
],
|
|
327
|
+
sessionId,
|
|
328
|
+
toolCall: {
|
|
329
|
+
toolCallId: toolUseID,
|
|
330
|
+
rawInput: toolInput,
|
|
331
|
+
title: toolInfoFromToolUse({ name: toolName, input: toolInput }).title,
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
if (signal.aborted || response.outcome?.outcome === "cancelled") {
|
|
335
|
+
throw new Error("Tool use aborted");
|
|
336
|
+
}
|
|
337
|
+
if (response.outcome?.outcome === "selected" &&
|
|
338
|
+
(response.outcome.optionId === "default" || response.outcome.optionId === "acceptEdits")) {
|
|
339
|
+
session.permissionMode = response.outcome.optionId;
|
|
340
|
+
await this.client.sessionUpdate({
|
|
341
|
+
sessionId,
|
|
342
|
+
update: {
|
|
343
|
+
sessionUpdate: "current_mode_update",
|
|
344
|
+
currentModeId: response.outcome.optionId,
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
return {
|
|
348
|
+
behavior: "allow",
|
|
349
|
+
updatedInput: toolInput,
|
|
350
|
+
updatedPermissions: suggestions ?? [
|
|
351
|
+
{ type: "setMode", mode: response.outcome.optionId, destination: "session" },
|
|
352
|
+
],
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
return {
|
|
357
|
+
behavior: "deny",
|
|
358
|
+
message: "User rejected request to exit plan mode.",
|
|
359
|
+
interrupt: true,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (session.permissionMode === "bypassPermissions" ||
|
|
364
|
+
(session.permissionMode === "acceptEdits" && EDIT_TOOL_NAMES.includes(toolName))) {
|
|
365
|
+
return {
|
|
366
|
+
behavior: "allow",
|
|
367
|
+
updatedInput: toolInput,
|
|
368
|
+
updatedPermissions: suggestions ?? [
|
|
369
|
+
{ type: "addRules", rules: [{ toolName }], behavior: "allow", destination: "session" },
|
|
370
|
+
],
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const response = await this.client.requestPermission({
|
|
374
|
+
options: [
|
|
375
|
+
{
|
|
376
|
+
kind: "allow_always",
|
|
377
|
+
name: "Always Allow",
|
|
378
|
+
optionId: "allow_always",
|
|
379
|
+
},
|
|
380
|
+
{ kind: "allow_once", name: "Allow", optionId: "allow" },
|
|
381
|
+
{ kind: "reject_once", name: "Reject", optionId: "reject" },
|
|
382
|
+
],
|
|
383
|
+
sessionId,
|
|
384
|
+
toolCall: {
|
|
385
|
+
toolCallId: toolUseID,
|
|
386
|
+
rawInput: toolInput,
|
|
387
|
+
title: toolInfoFromToolUse({ name: toolName, input: toolInput }).title,
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
if (signal.aborted || response.outcome?.outcome === "cancelled") {
|
|
391
|
+
throw new Error("Tool use aborted");
|
|
392
|
+
}
|
|
393
|
+
if (response.outcome?.outcome === "selected" &&
|
|
394
|
+
(response.outcome.optionId === "allow" || response.outcome.optionId === "allow_always")) {
|
|
395
|
+
// If Claude Code has suggestions, it will update their settings already
|
|
396
|
+
if (response.outcome.optionId === "allow_always") {
|
|
397
|
+
return {
|
|
398
|
+
behavior: "allow",
|
|
399
|
+
updatedInput: toolInput,
|
|
400
|
+
updatedPermissions: suggestions ?? [
|
|
401
|
+
{
|
|
402
|
+
type: "addRules",
|
|
403
|
+
rules: [{ toolName }],
|
|
404
|
+
behavior: "allow",
|
|
405
|
+
destination: "session",
|
|
406
|
+
},
|
|
407
|
+
],
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
return {
|
|
411
|
+
behavior: "allow",
|
|
412
|
+
updatedInput: toolInput,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
return {
|
|
417
|
+
behavior: "deny",
|
|
418
|
+
message: "User refused permission to run tool",
|
|
419
|
+
interrupt: true,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
async createSession(params, creationOpts = {}) {
|
|
425
|
+
// We want to create a new session id unless it is resume,
|
|
426
|
+
// but not resume + forkSession.
|
|
427
|
+
let sessionId;
|
|
428
|
+
if (creationOpts.forkSession) {
|
|
429
|
+
sessionId = randomUUID();
|
|
430
|
+
}
|
|
431
|
+
else if (creationOpts.resume) {
|
|
432
|
+
sessionId = creationOpts.resume;
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
sessionId = randomUUID();
|
|
436
|
+
}
|
|
437
|
+
const input = new Pushable();
|
|
438
|
+
const settingsManager = new SettingsManager(params.cwd, {
|
|
439
|
+
logger: this.logger,
|
|
440
|
+
});
|
|
441
|
+
await settingsManager.initialize();
|
|
442
|
+
const mcpServers = {};
|
|
443
|
+
if (Array.isArray(params.mcpServers)) {
|
|
444
|
+
for (const server of params.mcpServers) {
|
|
445
|
+
if ("type" in server) {
|
|
446
|
+
mcpServers[server.name] = {
|
|
447
|
+
type: server.type,
|
|
448
|
+
url: server.url,
|
|
449
|
+
headers: server.headers
|
|
450
|
+
? Object.fromEntries(server.headers.map((e) => [e.name, e.value]))
|
|
451
|
+
: undefined,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
mcpServers[server.name] = {
|
|
456
|
+
type: "stdio",
|
|
457
|
+
command: server.command,
|
|
458
|
+
args: server.args,
|
|
459
|
+
env: server.env
|
|
460
|
+
? Object.fromEntries(server.env.map((e) => [e.name, e.value]))
|
|
461
|
+
: undefined,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
// Only add the acp MCP server if built-in tools are not disabled
|
|
467
|
+
if (!params._meta?.disableBuiltInTools) {
|
|
468
|
+
const server = createMcpServer(this, sessionId, this.clientCapabilities);
|
|
469
|
+
mcpServers["acp"] = {
|
|
470
|
+
type: "sdk",
|
|
471
|
+
name: "acp",
|
|
472
|
+
instance: server,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
let systemPrompt = { type: "preset", preset: "claude_code" };
|
|
476
|
+
if (params._meta?.systemPrompt) {
|
|
477
|
+
const customPrompt = params._meta.systemPrompt;
|
|
478
|
+
if (typeof customPrompt === "string") {
|
|
479
|
+
systemPrompt = customPrompt;
|
|
480
|
+
}
|
|
481
|
+
else if (typeof customPrompt === "object" &&
|
|
482
|
+
"append" in customPrompt &&
|
|
483
|
+
typeof customPrompt.append === "string") {
|
|
484
|
+
systemPrompt.append = customPrompt.append;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
const permissionMode = "default";
|
|
488
|
+
// Extract options from _meta if provided
|
|
489
|
+
const userProvidedOptions = params._meta?.claudeCode?.options;
|
|
490
|
+
const extraArgs = { ...userProvidedOptions?.extraArgs };
|
|
491
|
+
if (creationOpts?.resume === undefined || creationOpts?.forkSession) {
|
|
492
|
+
// Set our own session id if not resuming an existing session.
|
|
493
|
+
extraArgs["session-id"] = sessionId;
|
|
494
|
+
}
|
|
495
|
+
const options = {
|
|
496
|
+
systemPrompt,
|
|
497
|
+
settingSources: ["user", "project", "local"],
|
|
498
|
+
stderr: (err) => this.logger.error(err),
|
|
499
|
+
...userProvidedOptions,
|
|
500
|
+
// Override certain fields that must be controlled by ACP
|
|
501
|
+
cwd: params.cwd,
|
|
502
|
+
includePartialMessages: true,
|
|
503
|
+
mcpServers: { ...(userProvidedOptions?.mcpServers || {}), ...mcpServers },
|
|
504
|
+
extraArgs,
|
|
505
|
+
// If we want bypassPermissions to be an option, we have to allow it here.
|
|
506
|
+
// But it doesn't work in root mode, so we only activate it if it will work.
|
|
507
|
+
allowDangerouslySkipPermissions: !IS_ROOT,
|
|
508
|
+
permissionMode,
|
|
509
|
+
canUseTool: this.canUseTool(sessionId),
|
|
510
|
+
// note: although not documented by the types, passing an absolute path
|
|
511
|
+
// here works to find zed's managed node version.
|
|
512
|
+
executable: process.execPath,
|
|
513
|
+
...(process.env.CLAUDE_CODE_EXECUTABLE && {
|
|
514
|
+
pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE,
|
|
515
|
+
}),
|
|
516
|
+
hooks: {
|
|
517
|
+
...userProvidedOptions?.hooks,
|
|
518
|
+
PreToolUse: [
|
|
519
|
+
...(userProvidedOptions?.hooks?.PreToolUse || []),
|
|
520
|
+
{
|
|
521
|
+
hooks: [createPreToolUseHook(settingsManager, this.logger)],
|
|
522
|
+
},
|
|
523
|
+
],
|
|
524
|
+
PostToolUse: [
|
|
525
|
+
...(userProvidedOptions?.hooks?.PostToolUse || []),
|
|
526
|
+
{
|
|
527
|
+
hooks: [createPostToolUseHook(this.logger)],
|
|
528
|
+
},
|
|
529
|
+
],
|
|
530
|
+
},
|
|
531
|
+
...creationOpts,
|
|
532
|
+
};
|
|
533
|
+
const allowedTools = [];
|
|
534
|
+
const disallowedTools = [];
|
|
535
|
+
// Check if built-in tools should be disabled
|
|
536
|
+
const disableBuiltInTools = params._meta?.disableBuiltInTools === true;
|
|
537
|
+
if (!disableBuiltInTools) {
|
|
538
|
+
if (this.clientCapabilities?.fs?.readTextFile) {
|
|
539
|
+
allowedTools.push(acpToolNames.read);
|
|
540
|
+
disallowedTools.push("Read");
|
|
541
|
+
}
|
|
542
|
+
if (this.clientCapabilities?.fs?.writeTextFile) {
|
|
543
|
+
disallowedTools.push("Write", "Edit");
|
|
544
|
+
}
|
|
545
|
+
if (this.clientCapabilities?.terminal) {
|
|
546
|
+
allowedTools.push(acpToolNames.bashOutput, acpToolNames.killShell);
|
|
547
|
+
disallowedTools.push("Bash", "BashOutput", "KillShell");
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
// When built-in tools are disabled, explicitly disallow all of them
|
|
552
|
+
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");
|
|
553
|
+
}
|
|
554
|
+
if (allowedTools.length > 0) {
|
|
555
|
+
options.allowedTools = allowedTools;
|
|
556
|
+
}
|
|
557
|
+
if (disallowedTools.length > 0) {
|
|
558
|
+
options.disallowedTools = disallowedTools;
|
|
559
|
+
}
|
|
560
|
+
// Handle abort controller from meta options
|
|
561
|
+
const abortController = userProvidedOptions?.abortController;
|
|
562
|
+
if (abortController?.signal.aborted) {
|
|
563
|
+
throw new Error("Cancelled");
|
|
564
|
+
}
|
|
565
|
+
// ==================== 会话创建开始 ====================
|
|
566
|
+
const sessionCreateStartTime = Date.now();
|
|
567
|
+
this.logger.log(`🔵 [ACP] 开始创建会话`);
|
|
568
|
+
this.logger.log(` ├─ sessionId: ${sessionId}`);
|
|
569
|
+
this.logger.log(` ├─ cwd: ${params.cwd}`);
|
|
570
|
+
this.logger.log(` └─ MCP 服务器数量: ${Object.keys(mcpServers).length}`);
|
|
571
|
+
if (Object.keys(mcpServers).length > 0) {
|
|
572
|
+
this.logger.log(` └─ MCP 服务器列表: ${Object.keys(mcpServers).join(", ")}`);
|
|
573
|
+
}
|
|
574
|
+
const q = query({
|
|
575
|
+
prompt: input,
|
|
576
|
+
options,
|
|
577
|
+
});
|
|
578
|
+
this.sessions[sessionId] = {
|
|
579
|
+
query: q,
|
|
580
|
+
input: input,
|
|
581
|
+
cancelled: false,
|
|
582
|
+
permissionMode,
|
|
583
|
+
settingsManager,
|
|
584
|
+
};
|
|
585
|
+
this.logger.log(`📋 [ACP] 开始获取可用命令和模型...`);
|
|
586
|
+
// 使用超时保护包装命令获取
|
|
587
|
+
let availableCommands = [];
|
|
588
|
+
try {
|
|
589
|
+
availableCommands = await withTimeout(getAvailableSlashCommands(q, this.logger), COMMAND_FETCH_TIMEOUT_MS, "getAvailableSlashCommands", this.logger);
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
if (USE_DEFAULTS_ON_TIMEOUT) {
|
|
593
|
+
this.logger.log(`⚠️ [ACP] 获取命令失败,使用空列表: ${error}`);
|
|
594
|
+
availableCommands = [];
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
throw error;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
// 使用超时保护包装模型获取
|
|
601
|
+
let models;
|
|
602
|
+
try {
|
|
603
|
+
models = await withTimeout(getAvailableModels(q, this.logger), MODEL_FETCH_TIMEOUT_MS, "getAvailableModels", this.logger);
|
|
604
|
+
}
|
|
605
|
+
catch (error) {
|
|
606
|
+
if (USE_DEFAULTS_ON_TIMEOUT) {
|
|
607
|
+
this.logger.log(`⚠️ [ACP] 获取模型失败,使用默认值: ${error}`);
|
|
608
|
+
models = {
|
|
609
|
+
availableModels: [],
|
|
610
|
+
currentModelId: "",
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
throw error;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
const sessionCreateElapsed = Date.now() - sessionCreateStartTime;
|
|
618
|
+
this.logger.log(`✅ [ACP] 会话创建完成, 总耗时: ${sessionCreateElapsed}ms`);
|
|
619
|
+
// Needs to happen after we return the session
|
|
620
|
+
setTimeout(() => {
|
|
621
|
+
this.client.sessionUpdate({
|
|
622
|
+
sessionId,
|
|
623
|
+
update: {
|
|
624
|
+
sessionUpdate: "available_commands_update",
|
|
625
|
+
availableCommands,
|
|
626
|
+
},
|
|
627
|
+
});
|
|
628
|
+
}, 0);
|
|
629
|
+
const availableModes = [
|
|
630
|
+
{
|
|
631
|
+
id: "default",
|
|
632
|
+
name: "Default",
|
|
633
|
+
description: "Standard behavior, prompts for dangerous operations",
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
id: "acceptEdits",
|
|
637
|
+
name: "Accept Edits",
|
|
638
|
+
description: "Auto-accept file edit operations",
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
id: "plan",
|
|
642
|
+
name: "Plan Mode",
|
|
643
|
+
description: "Planning mode, no actual tool execution",
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
id: "dontAsk",
|
|
647
|
+
name: "Don't Ask",
|
|
648
|
+
description: "Don't prompt for permissions, deny if not pre-approved",
|
|
649
|
+
},
|
|
650
|
+
];
|
|
651
|
+
// Only works in non-root mode
|
|
652
|
+
if (!IS_ROOT) {
|
|
653
|
+
availableModes.push({
|
|
654
|
+
id: "bypassPermissions",
|
|
655
|
+
name: "Bypass Permissions",
|
|
656
|
+
description: "Bypass all permission checks",
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
sessionId,
|
|
661
|
+
models,
|
|
662
|
+
modes: {
|
|
663
|
+
currentModeId: permissionMode,
|
|
664
|
+
availableModes,
|
|
665
|
+
},
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
async function getAvailableModels(query, logger = console) {
|
|
670
|
+
const startTime = Date.now();
|
|
671
|
+
logger.log(` 📋 [ACP] 开始获取可用模型列表...`);
|
|
672
|
+
const models = await query.supportedModels();
|
|
673
|
+
logger.log(` 📋 [ACP] 获取到 ${models.length} 个模型, 耗时: ${Date.now() - startTime}ms`);
|
|
674
|
+
// Query doesn't give us access to the currently selected model, so we just choose the first model in the list.
|
|
675
|
+
if (models.length === 0) {
|
|
676
|
+
logger.log(` ⚠️ [ACP] 没有可用模型`);
|
|
677
|
+
return { availableModels: [], currentModelId: "" };
|
|
678
|
+
}
|
|
679
|
+
const currentModel = models[0];
|
|
680
|
+
await query.setModel(currentModel.value);
|
|
681
|
+
const availableModels = models.map((model) => ({
|
|
682
|
+
modelId: model.value,
|
|
683
|
+
name: model.displayName,
|
|
684
|
+
description: model.description,
|
|
685
|
+
}));
|
|
686
|
+
return {
|
|
687
|
+
availableModels,
|
|
688
|
+
currentModelId: currentModel.value,
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
async function getAvailableSlashCommands(query, logger = console) {
|
|
692
|
+
const startTime = Date.now();
|
|
693
|
+
logger.log(` 📋 [ACP] 开始获取可用斜杠命令...`);
|
|
694
|
+
const UNSUPPORTED_COMMANDS = [
|
|
695
|
+
"context",
|
|
696
|
+
"cost",
|
|
697
|
+
"login",
|
|
698
|
+
"logout",
|
|
699
|
+
"output-style:new",
|
|
700
|
+
"release-notes",
|
|
701
|
+
"todos",
|
|
702
|
+
];
|
|
703
|
+
const commands = await query.supportedCommands();
|
|
704
|
+
logger.log(` 📋 [ACP] 获取到 ${commands.length} 个斜杠命令, 耗时: ${Date.now() - startTime}ms`);
|
|
705
|
+
return commands
|
|
706
|
+
.map((command) => {
|
|
707
|
+
const input = command.argumentHint ? { hint: command.argumentHint } : null;
|
|
708
|
+
let name = command.name;
|
|
709
|
+
if (command.name.endsWith(" (MCP)")) {
|
|
710
|
+
name = `mcp:${name.replace(" (MCP)", "")}`;
|
|
711
|
+
}
|
|
712
|
+
return {
|
|
713
|
+
name,
|
|
714
|
+
description: command.description || "",
|
|
715
|
+
input,
|
|
716
|
+
};
|
|
717
|
+
})
|
|
718
|
+
.filter((command) => !UNSUPPORTED_COMMANDS.includes(command.name));
|
|
719
|
+
}
|
|
720
|
+
function formatUriAsLink(uri) {
|
|
721
|
+
try {
|
|
722
|
+
if (uri.startsWith("file://")) {
|
|
723
|
+
const path = uri.slice(7); // Remove "file://"
|
|
724
|
+
const name = path.split("/").pop() || path;
|
|
725
|
+
return `[@${name}](${uri})`;
|
|
726
|
+
}
|
|
727
|
+
else if (uri.startsWith("zed://")) {
|
|
728
|
+
const parts = uri.split("/");
|
|
729
|
+
const name = parts[parts.length - 1] || uri;
|
|
730
|
+
return `[@${name}](${uri})`;
|
|
731
|
+
}
|
|
732
|
+
return uri;
|
|
733
|
+
}
|
|
734
|
+
catch {
|
|
735
|
+
return uri;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
export function promptToClaude(prompt) {
|
|
739
|
+
const content = [];
|
|
740
|
+
const context = [];
|
|
741
|
+
for (const chunk of prompt.prompt) {
|
|
742
|
+
switch (chunk.type) {
|
|
743
|
+
case "text": {
|
|
744
|
+
let text = chunk.text;
|
|
745
|
+
// change /mcp:server:command args -> /server:command (MCP) args
|
|
746
|
+
const mcpMatch = text.match(/^\/mcp:([^:\s]+):(\S+)(\s+.*)?$/);
|
|
747
|
+
if (mcpMatch) {
|
|
748
|
+
const [, server, command, args] = mcpMatch;
|
|
749
|
+
text = `/${server}:${command} (MCP)${args || ""}`;
|
|
750
|
+
}
|
|
751
|
+
content.push({ type: "text", text });
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
case "resource_link": {
|
|
755
|
+
const formattedUri = formatUriAsLink(chunk.uri);
|
|
756
|
+
content.push({
|
|
757
|
+
type: "text",
|
|
758
|
+
text: formattedUri,
|
|
759
|
+
});
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
case "resource": {
|
|
763
|
+
if ("text" in chunk.resource) {
|
|
764
|
+
const formattedUri = formatUriAsLink(chunk.resource.uri);
|
|
765
|
+
content.push({
|
|
766
|
+
type: "text",
|
|
767
|
+
text: formattedUri,
|
|
768
|
+
});
|
|
769
|
+
context.push({
|
|
770
|
+
type: "text",
|
|
771
|
+
text: `\n<context ref="${chunk.resource.uri}">\n${chunk.resource.text}\n</context>`,
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
// Ignore blob resources (unsupported)
|
|
775
|
+
break;
|
|
776
|
+
}
|
|
777
|
+
case "image":
|
|
778
|
+
if (chunk.data) {
|
|
779
|
+
content.push({
|
|
780
|
+
type: "image",
|
|
781
|
+
source: {
|
|
782
|
+
type: "base64",
|
|
783
|
+
data: chunk.data,
|
|
784
|
+
media_type: chunk.mimeType,
|
|
785
|
+
},
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
else if (chunk.uri && chunk.uri.startsWith("http")) {
|
|
789
|
+
content.push({
|
|
790
|
+
type: "image",
|
|
791
|
+
source: {
|
|
792
|
+
type: "url",
|
|
793
|
+
url: chunk.uri,
|
|
794
|
+
},
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
break;
|
|
798
|
+
// Ignore audio and other unsupported types
|
|
799
|
+
default:
|
|
800
|
+
break;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
content.push(...context);
|
|
804
|
+
return {
|
|
805
|
+
type: "user",
|
|
806
|
+
message: {
|
|
807
|
+
role: "user",
|
|
808
|
+
content: content,
|
|
809
|
+
},
|
|
810
|
+
session_id: prompt.sessionId,
|
|
811
|
+
parent_tool_use_id: null,
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Convert an SDKAssistantMessage (Claude) to a SessionNotification (ACP).
|
|
816
|
+
* Only handles text, image, and thinking chunks for now.
|
|
817
|
+
*/
|
|
818
|
+
export function toAcpNotifications(content, role, sessionId, toolUseCache, client, logger) {
|
|
819
|
+
if (typeof content === "string") {
|
|
820
|
+
return [
|
|
821
|
+
{
|
|
822
|
+
sessionId,
|
|
823
|
+
update: {
|
|
824
|
+
sessionUpdate: role === "assistant" ? "agent_message_chunk" : "user_message_chunk",
|
|
825
|
+
content: {
|
|
826
|
+
type: "text",
|
|
827
|
+
text: content,
|
|
828
|
+
},
|
|
829
|
+
},
|
|
830
|
+
},
|
|
831
|
+
];
|
|
832
|
+
}
|
|
833
|
+
const output = [];
|
|
834
|
+
// Only handle the first chunk for streaming; extend as needed for batching
|
|
835
|
+
for (const chunk of content) {
|
|
836
|
+
let update = null;
|
|
837
|
+
switch (chunk.type) {
|
|
838
|
+
case "text":
|
|
839
|
+
case "text_delta":
|
|
840
|
+
update = {
|
|
841
|
+
sessionUpdate: role === "assistant" ? "agent_message_chunk" : "user_message_chunk",
|
|
842
|
+
content: {
|
|
843
|
+
type: "text",
|
|
844
|
+
text: chunk.text,
|
|
845
|
+
},
|
|
846
|
+
};
|
|
847
|
+
break;
|
|
848
|
+
case "image":
|
|
849
|
+
update = {
|
|
850
|
+
sessionUpdate: role === "assistant" ? "agent_message_chunk" : "user_message_chunk",
|
|
851
|
+
content: {
|
|
852
|
+
type: "image",
|
|
853
|
+
data: chunk.source.type === "base64" ? chunk.source.data : "",
|
|
854
|
+
mimeType: chunk.source.type === "base64" ? chunk.source.media_type : "",
|
|
855
|
+
uri: chunk.source.type === "url" ? chunk.source.url : undefined,
|
|
856
|
+
},
|
|
857
|
+
};
|
|
858
|
+
break;
|
|
859
|
+
case "thinking":
|
|
860
|
+
case "thinking_delta":
|
|
861
|
+
update = {
|
|
862
|
+
sessionUpdate: "agent_thought_chunk",
|
|
863
|
+
content: {
|
|
864
|
+
type: "text",
|
|
865
|
+
text: chunk.thinking,
|
|
866
|
+
},
|
|
867
|
+
};
|
|
868
|
+
break;
|
|
869
|
+
case "tool_use":
|
|
870
|
+
case "server_tool_use":
|
|
871
|
+
case "mcp_tool_use": {
|
|
872
|
+
toolUseCache[chunk.id] = chunk;
|
|
873
|
+
if (chunk.name === "TodoWrite") {
|
|
874
|
+
// @ts-expect-error - sometimes input is empty object
|
|
875
|
+
if (Array.isArray(chunk.input.todos)) {
|
|
876
|
+
update = {
|
|
877
|
+
sessionUpdate: "plan",
|
|
878
|
+
entries: planEntries(chunk.input),
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
else {
|
|
883
|
+
// Register hook callback to receive the structured output from the hook
|
|
884
|
+
registerHookCallback(chunk.id, {
|
|
885
|
+
onPostToolUseHook: async (toolUseId, toolInput, toolResponse) => {
|
|
886
|
+
const toolUse = toolUseCache[toolUseId];
|
|
887
|
+
if (toolUse) {
|
|
888
|
+
const update = {
|
|
889
|
+
_meta: {
|
|
890
|
+
claudeCode: {
|
|
891
|
+
toolResponse,
|
|
892
|
+
toolName: toolUse.name,
|
|
893
|
+
},
|
|
894
|
+
},
|
|
895
|
+
toolCallId: toolUseId,
|
|
896
|
+
sessionUpdate: "tool_call_update",
|
|
897
|
+
};
|
|
898
|
+
await client.sessionUpdate({
|
|
899
|
+
sessionId,
|
|
900
|
+
update,
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
else {
|
|
904
|
+
logger.error(`[claude-code-acp] Got a tool response for tool use that wasn't tracked: ${toolUseId}`);
|
|
905
|
+
}
|
|
906
|
+
},
|
|
907
|
+
});
|
|
908
|
+
let rawInput;
|
|
909
|
+
try {
|
|
910
|
+
rawInput = JSON.parse(JSON.stringify(chunk.input));
|
|
911
|
+
}
|
|
912
|
+
catch {
|
|
913
|
+
// ignore if we can't turn it to JSON
|
|
914
|
+
}
|
|
915
|
+
update = {
|
|
916
|
+
_meta: {
|
|
917
|
+
claudeCode: {
|
|
918
|
+
toolName: chunk.name,
|
|
919
|
+
},
|
|
920
|
+
},
|
|
921
|
+
toolCallId: chunk.id,
|
|
922
|
+
sessionUpdate: "tool_call",
|
|
923
|
+
rawInput,
|
|
924
|
+
status: "pending",
|
|
925
|
+
...toolInfoFromToolUse(chunk),
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
break;
|
|
929
|
+
}
|
|
930
|
+
case "tool_result":
|
|
931
|
+
case "tool_search_tool_result":
|
|
932
|
+
case "web_fetch_tool_result":
|
|
933
|
+
case "web_search_tool_result":
|
|
934
|
+
case "code_execution_tool_result":
|
|
935
|
+
case "bash_code_execution_tool_result":
|
|
936
|
+
case "text_editor_code_execution_tool_result":
|
|
937
|
+
case "mcp_tool_result": {
|
|
938
|
+
const toolUse = toolUseCache[chunk.tool_use_id];
|
|
939
|
+
if (!toolUse) {
|
|
940
|
+
logger.error(`[claude-code-acp] Got a tool result for tool use that wasn't tracked: ${chunk.tool_use_id}`);
|
|
941
|
+
break;
|
|
942
|
+
}
|
|
943
|
+
if (toolUse.name !== "TodoWrite") {
|
|
944
|
+
update = {
|
|
945
|
+
_meta: {
|
|
946
|
+
claudeCode: {
|
|
947
|
+
toolName: toolUse.name,
|
|
948
|
+
},
|
|
949
|
+
},
|
|
950
|
+
toolCallId: chunk.tool_use_id,
|
|
951
|
+
sessionUpdate: "tool_call_update",
|
|
952
|
+
status: "is_error" in chunk && chunk.is_error ? "failed" : "completed",
|
|
953
|
+
...toolUpdateFromToolResult(chunk, toolUseCache[chunk.tool_use_id]),
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
break;
|
|
957
|
+
}
|
|
958
|
+
case "document":
|
|
959
|
+
case "search_result":
|
|
960
|
+
case "redacted_thinking":
|
|
961
|
+
case "input_json_delta":
|
|
962
|
+
case "citations_delta":
|
|
963
|
+
case "signature_delta":
|
|
964
|
+
case "container_upload":
|
|
965
|
+
break;
|
|
966
|
+
default:
|
|
967
|
+
unreachable(chunk, logger);
|
|
968
|
+
break;
|
|
969
|
+
}
|
|
970
|
+
if (update) {
|
|
971
|
+
output.push({ sessionId, update });
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return output;
|
|
975
|
+
}
|
|
976
|
+
export function streamEventToAcpNotifications(message, sessionId, toolUseCache, client, logger) {
|
|
977
|
+
const event = message.event;
|
|
978
|
+
switch (event.type) {
|
|
979
|
+
case "content_block_start":
|
|
980
|
+
return toAcpNotifications([event.content_block], "assistant", sessionId, toolUseCache, client, logger);
|
|
981
|
+
case "content_block_delta":
|
|
982
|
+
return toAcpNotifications([event.delta], "assistant", sessionId, toolUseCache, client, logger);
|
|
983
|
+
// No content
|
|
984
|
+
case "message_start":
|
|
985
|
+
case "message_delta":
|
|
986
|
+
case "message_stop":
|
|
987
|
+
case "content_block_stop":
|
|
988
|
+
return [];
|
|
989
|
+
default:
|
|
990
|
+
unreachable(event, logger);
|
|
991
|
+
return [];
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
export function runAcp() {
|
|
995
|
+
const input = nodeToWebWritable(process.stdout);
|
|
996
|
+
const output = nodeToWebReadable(process.stdin);
|
|
997
|
+
const stream = ndJsonStream(input, output);
|
|
998
|
+
new AgentSideConnection((client) => new ClaudeAcpAgent(client), stream);
|
|
999
|
+
}
|