@posthog/agent 2.0.0 → 2.0.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 +1 -1
- package/README.md +221 -219
- package/dist/adapters/claude/conversion/tool-use-to-acp.d.ts +21 -0
- package/dist/adapters/claude/conversion/tool-use-to-acp.js +547 -0
- package/dist/adapters/claude/conversion/tool-use-to-acp.js.map +1 -0
- package/dist/adapters/claude/permissions/permission-options.d.ts +13 -0
- package/dist/adapters/claude/permissions/permission-options.js +117 -0
- package/dist/adapters/claude/permissions/permission-options.js.map +1 -0
- package/dist/adapters/claude/questions/utils.d.ts +132 -0
- package/dist/adapters/claude/questions/utils.js +63 -0
- package/dist/adapters/claude/questions/utils.js.map +1 -0
- package/dist/adapters/claude/tools.d.ts +18 -0
- package/dist/adapters/claude/tools.js +95 -0
- package/dist/adapters/claude/tools.js.map +1 -0
- package/dist/agent-DBQY1BfC.d.ts +123 -0
- package/dist/agent.d.ts +5 -0
- package/dist/agent.js +3656 -0
- package/dist/agent.js.map +1 -0
- package/dist/claude-cli/cli.js +3695 -2746
- package/dist/claude-cli/vendor/ripgrep/COPYING +3 -0
- package/dist/claude-cli/vendor/ripgrep/arm64-darwin/rg +0 -0
- package/dist/claude-cli/vendor/ripgrep/arm64-darwin/ripgrep.node +0 -0
- package/dist/claude-cli/vendor/ripgrep/arm64-linux/rg +0 -0
- package/dist/claude-cli/vendor/ripgrep/arm64-linux/ripgrep.node +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-darwin/rg +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-darwin/ripgrep.node +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-linux/rg +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-linux/ripgrep.node +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-win32/rg.exe +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-win32/ripgrep.node +0 -0
- package/dist/gateway-models.d.ts +24 -0
- package/dist/gateway-models.js +93 -0
- package/dist/gateway-models.js.map +1 -0
- package/dist/index.d.ts +170 -1157
- package/dist/index.js +9373 -5135
- package/dist/index.js.map +1 -1
- package/dist/logger-DDBiMOOD.d.ts +24 -0
- package/dist/posthog-api.d.ts +40 -0
- package/dist/posthog-api.js +175 -0
- package/dist/posthog-api.js.map +1 -0
- package/dist/server/agent-server.d.ts +41 -0
- package/dist/server/agent-server.js +10503 -0
- package/dist/server/agent-server.js.map +1 -0
- package/dist/server/bin.d.ts +1 -0
- package/dist/server/bin.js +10558 -0
- package/dist/server/bin.js.map +1 -0
- package/dist/types.d.ts +129 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/package.json +65 -13
- package/src/acp-extensions.ts +98 -16
- package/src/adapters/acp-connection.ts +494 -0
- package/src/adapters/base-acp-agent.ts +150 -0
- package/src/adapters/claude/claude-agent.ts +596 -0
- package/src/adapters/claude/conversion/acp-to-sdk.ts +102 -0
- package/src/adapters/claude/conversion/sdk-to-acp.ts +571 -0
- package/src/adapters/claude/conversion/tool-use-to-acp.ts +618 -0
- package/src/adapters/claude/hooks.ts +64 -0
- package/src/adapters/claude/mcp/tool-metadata.ts +102 -0
- package/src/adapters/claude/permissions/permission-handlers.ts +433 -0
- package/src/adapters/claude/permissions/permission-options.ts +103 -0
- package/src/adapters/claude/plan/utils.ts +56 -0
- package/src/adapters/claude/questions/utils.ts +92 -0
- package/src/adapters/claude/session/commands.ts +38 -0
- package/src/adapters/claude/session/mcp-config.ts +37 -0
- package/src/adapters/claude/session/models.ts +12 -0
- package/src/adapters/claude/session/options.ts +236 -0
- package/src/adapters/claude/tool-meta.ts +143 -0
- package/src/adapters/claude/tools.ts +53 -688
- package/src/adapters/claude/types.ts +61 -0
- package/src/adapters/codex/spawn.ts +130 -0
- package/src/agent.ts +96 -587
- package/src/execution-mode.ts +43 -0
- package/src/gateway-models.ts +135 -0
- package/src/index.ts +79 -0
- package/src/otel-log-writer.test.ts +105 -0
- package/src/otel-log-writer.ts +94 -0
- package/src/posthog-api.ts +75 -235
- package/src/resume.ts +115 -0
- package/src/sagas/apply-snapshot-saga.test.ts +690 -0
- package/src/sagas/apply-snapshot-saga.ts +88 -0
- package/src/sagas/capture-tree-saga.test.ts +892 -0
- package/src/sagas/capture-tree-saga.ts +141 -0
- package/src/sagas/resume-saga.test.ts +558 -0
- package/src/sagas/resume-saga.ts +332 -0
- package/src/sagas/test-fixtures.ts +250 -0
- package/src/server/agent-server.test.ts +220 -0
- package/src/server/agent-server.ts +748 -0
- package/src/server/bin.ts +88 -0
- package/src/server/jwt.ts +65 -0
- package/src/server/schemas.ts +47 -0
- package/src/server/types.ts +13 -0
- package/src/server/utils/retry.test.ts +122 -0
- package/src/server/utils/retry.ts +61 -0
- package/src/server/utils/sse-parser.test.ts +93 -0
- package/src/server/utils/sse-parser.ts +46 -0
- package/src/session-log-writer.test.ts +140 -0
- package/src/session-log-writer.ts +137 -0
- package/src/test/assertions.ts +114 -0
- package/src/test/controllers/sse-controller.ts +107 -0
- package/src/test/fixtures/api.ts +111 -0
- package/src/test/fixtures/config.ts +33 -0
- package/src/test/fixtures/notifications.ts +92 -0
- package/src/test/mocks/claude-sdk.ts +251 -0
- package/src/test/mocks/msw-handlers.ts +48 -0
- package/src/test/setup.ts +114 -0
- package/src/test/wait.ts +41 -0
- package/src/tree-tracker.ts +173 -0
- package/src/types.ts +54 -137
- package/src/utils/acp-content.ts +58 -0
- package/src/utils/async-mutex.test.ts +104 -0
- package/src/utils/async-mutex.ts +31 -0
- package/src/utils/common.ts +15 -0
- package/src/utils/gateway.ts +9 -6
- package/src/utils/logger.ts +0 -30
- package/src/utils/streams.ts +220 -0
- package/CLAUDE.md +0 -331
- package/src/adapters/claude/claude.ts +0 -1947
- package/src/adapters/claude/mcp-server.ts +0 -810
- package/src/adapters/claude/utils.ts +0 -267
- package/src/adapters/connection.ts +0 -95
- package/src/file-manager.ts +0 -273
- package/src/git-manager.ts +0 -577
- package/src/schemas.ts +0 -241
- package/src/session-store.ts +0 -259
- package/src/task-manager.ts +0 -163
- package/src/todo-manager.ts +0 -180
- package/src/tools/registry.ts +0 -134
- package/src/tools/types.ts +0 -133
- package/src/utils/tapped-stream.ts +0 -60
- package/src/worktree-manager.ts +0 -974
|
@@ -1,1947 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The claude adapter has been based on the original claude-code-acp adapter,
|
|
3
|
-
* and could use some cleanup.
|
|
4
|
-
*
|
|
5
|
-
* https://github.com/zed-industries/claude-code-acp
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import * as fs from "node:fs";
|
|
9
|
-
import * as os from "node:os";
|
|
10
|
-
import * as path from "node:path";
|
|
11
|
-
import {
|
|
12
|
-
type Agent,
|
|
13
|
-
type AgentSideConnection,
|
|
14
|
-
type AuthenticateRequest,
|
|
15
|
-
type AvailableCommand,
|
|
16
|
-
type CancelNotification,
|
|
17
|
-
type ClientCapabilities,
|
|
18
|
-
type InitializeRequest,
|
|
19
|
-
type InitializeResponse,
|
|
20
|
-
type LoadSessionRequest,
|
|
21
|
-
type LoadSessionResponse,
|
|
22
|
-
type NewSessionRequest,
|
|
23
|
-
type NewSessionResponse,
|
|
24
|
-
type PromptRequest,
|
|
25
|
-
type PromptResponse,
|
|
26
|
-
type ReadTextFileRequest,
|
|
27
|
-
type ReadTextFileResponse,
|
|
28
|
-
RequestError,
|
|
29
|
-
type SessionModelState,
|
|
30
|
-
type SessionNotification,
|
|
31
|
-
type SetSessionModelRequest,
|
|
32
|
-
type SetSessionModeRequest,
|
|
33
|
-
type SetSessionModeResponse,
|
|
34
|
-
type TerminalHandle,
|
|
35
|
-
type TerminalOutputResponse,
|
|
36
|
-
type WriteTextFileRequest,
|
|
37
|
-
type WriteTextFileResponse,
|
|
38
|
-
} from "@agentclientprotocol/sdk";
|
|
39
|
-
import {
|
|
40
|
-
type CanUseTool,
|
|
41
|
-
type McpServerConfig,
|
|
42
|
-
type Options,
|
|
43
|
-
type PermissionMode,
|
|
44
|
-
type Query,
|
|
45
|
-
query,
|
|
46
|
-
type SDKPartialAssistantMessage,
|
|
47
|
-
type SDKUserMessage,
|
|
48
|
-
} from "@anthropic-ai/claude-agent-sdk";
|
|
49
|
-
import type { ContentBlockParam } from "@anthropic-ai/sdk/resources";
|
|
50
|
-
import type {
|
|
51
|
-
BetaContentBlock,
|
|
52
|
-
BetaRawContentBlockDelta,
|
|
53
|
-
} from "@anthropic-ai/sdk/resources/beta.mjs";
|
|
54
|
-
import { v7 as uuidv7 } from "uuid";
|
|
55
|
-
import type {
|
|
56
|
-
SessionPersistenceConfig,
|
|
57
|
-
SessionStore,
|
|
58
|
-
} from "@/session-store.js";
|
|
59
|
-
import { Logger } from "@/utils/logger.js";
|
|
60
|
-
import packageJson from "../../../package.json" with { type: "json" };
|
|
61
|
-
import { createMcpServer, EDIT_TOOL_NAMES, toolNames } from "./mcp-server.js";
|
|
62
|
-
import {
|
|
63
|
-
type ClaudePlanEntry,
|
|
64
|
-
createPostToolUseHook,
|
|
65
|
-
planEntries,
|
|
66
|
-
registerHookCallback,
|
|
67
|
-
toolInfoFromToolUse,
|
|
68
|
-
toolUpdateFromToolResult,
|
|
69
|
-
} from "./tools.js";
|
|
70
|
-
import { Pushable, unreachable } from "./utils.js";
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Clears the statsig cache to work around a claude-agent-sdk bug where cached
|
|
74
|
-
* tool definitions include input_examples which causes API errors.
|
|
75
|
-
* See: https://github.com/anthropics/claude-code/issues/11678
|
|
76
|
-
*/
|
|
77
|
-
function getClaudeConfigDir(): string {
|
|
78
|
-
return process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function getClaudePlansDir(): string {
|
|
82
|
-
return path.join(getClaudeConfigDir(), "plans");
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function isClaudePlanFilePath(filePath: string | undefined): boolean {
|
|
86
|
-
if (!filePath) return false;
|
|
87
|
-
const resolved = path.resolve(filePath);
|
|
88
|
-
const plansDir = path.resolve(getClaudePlansDir());
|
|
89
|
-
return resolved === plansDir || resolved.startsWith(plansDir + path.sep);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Whitelist of command prefixes that are considered read-only.
|
|
94
|
-
* These commands can be used in plan mode since they don't modify files or state.
|
|
95
|
-
*/
|
|
96
|
-
const READ_ONLY_COMMAND_PREFIXES = [
|
|
97
|
-
// File listing and info
|
|
98
|
-
"ls",
|
|
99
|
-
"find",
|
|
100
|
-
"tree",
|
|
101
|
-
"stat",
|
|
102
|
-
"file",
|
|
103
|
-
"wc",
|
|
104
|
-
"du",
|
|
105
|
-
"df",
|
|
106
|
-
// File reading (non-modifying)
|
|
107
|
-
"cat",
|
|
108
|
-
"head",
|
|
109
|
-
"tail",
|
|
110
|
-
"less",
|
|
111
|
-
"more",
|
|
112
|
-
"bat",
|
|
113
|
-
// Search
|
|
114
|
-
"grep",
|
|
115
|
-
"rg",
|
|
116
|
-
"ag",
|
|
117
|
-
"ack",
|
|
118
|
-
"fzf",
|
|
119
|
-
// Git read operations
|
|
120
|
-
"git status",
|
|
121
|
-
"git log",
|
|
122
|
-
"git diff",
|
|
123
|
-
"git show",
|
|
124
|
-
"git branch",
|
|
125
|
-
"git remote",
|
|
126
|
-
"git fetch",
|
|
127
|
-
"git rev-parse",
|
|
128
|
-
"git ls-files",
|
|
129
|
-
"git blame",
|
|
130
|
-
"git shortlog",
|
|
131
|
-
"git describe",
|
|
132
|
-
"git tag -l",
|
|
133
|
-
"git tag --list",
|
|
134
|
-
// System info
|
|
135
|
-
"pwd",
|
|
136
|
-
"whoami",
|
|
137
|
-
"which",
|
|
138
|
-
"where",
|
|
139
|
-
"type",
|
|
140
|
-
"printenv",
|
|
141
|
-
"env",
|
|
142
|
-
"echo",
|
|
143
|
-
"printf",
|
|
144
|
-
"date",
|
|
145
|
-
"uptime",
|
|
146
|
-
"uname",
|
|
147
|
-
"id",
|
|
148
|
-
"groups",
|
|
149
|
-
// Process info
|
|
150
|
-
"ps",
|
|
151
|
-
"top",
|
|
152
|
-
"htop",
|
|
153
|
-
"pgrep",
|
|
154
|
-
"lsof",
|
|
155
|
-
// Network read-only
|
|
156
|
-
"curl",
|
|
157
|
-
"wget",
|
|
158
|
-
"ping",
|
|
159
|
-
"host",
|
|
160
|
-
"dig",
|
|
161
|
-
"nslookup",
|
|
162
|
-
// Package managers (info only)
|
|
163
|
-
"npm list",
|
|
164
|
-
"npm ls",
|
|
165
|
-
"npm view",
|
|
166
|
-
"npm info",
|
|
167
|
-
"npm outdated",
|
|
168
|
-
"pnpm list",
|
|
169
|
-
"pnpm ls",
|
|
170
|
-
"pnpm why",
|
|
171
|
-
"yarn list",
|
|
172
|
-
"yarn why",
|
|
173
|
-
"yarn info",
|
|
174
|
-
// Other read-only
|
|
175
|
-
"jq",
|
|
176
|
-
"yq",
|
|
177
|
-
"xargs",
|
|
178
|
-
"sort",
|
|
179
|
-
"uniq",
|
|
180
|
-
"tr",
|
|
181
|
-
"cut",
|
|
182
|
-
"awk",
|
|
183
|
-
"sed -n",
|
|
184
|
-
];
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Checks if a bash command is read-only based on a whitelist of command prefixes.
|
|
188
|
-
* Used to allow safe bash commands in plan mode.
|
|
189
|
-
*/
|
|
190
|
-
function isReadOnlyBashCommand(command: string): boolean {
|
|
191
|
-
const trimmed = command.trim();
|
|
192
|
-
return READ_ONLY_COMMAND_PREFIXES.some(
|
|
193
|
-
(prefix) =>
|
|
194
|
-
trimmed === prefix ||
|
|
195
|
-
trimmed.startsWith(`${prefix} `) ||
|
|
196
|
-
trimmed.startsWith(`${prefix}\t`),
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function clearStatsigCache(): void {
|
|
201
|
-
const statsigPath = path.join(getClaudeConfigDir(), "statsig");
|
|
202
|
-
|
|
203
|
-
try {
|
|
204
|
-
if (fs.existsSync(statsigPath)) {
|
|
205
|
-
fs.rmSync(statsigPath, { recursive: true, force: true });
|
|
206
|
-
}
|
|
207
|
-
} catch {
|
|
208
|
-
// Ignore errors - cache clearing is best-effort
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
type Session = {
|
|
213
|
-
query: Query;
|
|
214
|
-
input: Pushable<SDKUserMessage>;
|
|
215
|
-
cancelled: boolean;
|
|
216
|
-
permissionMode: PermissionMode;
|
|
217
|
-
notificationHistory: SessionNotification[];
|
|
218
|
-
sdkSessionId?: string;
|
|
219
|
-
lastPlanFilePath?: string;
|
|
220
|
-
lastPlanContent?: string;
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
type BackgroundTerminal =
|
|
224
|
-
| {
|
|
225
|
-
handle: TerminalHandle;
|
|
226
|
-
status: "started";
|
|
227
|
-
lastOutput: TerminalOutputResponse | null;
|
|
228
|
-
}
|
|
229
|
-
| {
|
|
230
|
-
status: "aborted" | "exited" | "killed" | "timedOut";
|
|
231
|
-
pendingOutput: TerminalOutputResponse;
|
|
232
|
-
};
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* Extra metadata that can be given to Claude Code when creating a new session.
|
|
236
|
-
*/
|
|
237
|
-
export type NewSessionMeta = {
|
|
238
|
-
claudeCode?: {
|
|
239
|
-
/**
|
|
240
|
-
* Options forwarded to Claude Code when starting a new session.
|
|
241
|
-
* Those parameters will be ignored and managed by ACP:
|
|
242
|
-
* - cwd
|
|
243
|
-
* - includePartialMessages
|
|
244
|
-
* - allowDangerouslySkipPermissions
|
|
245
|
-
* - permissionMode
|
|
246
|
-
* - canUseTool
|
|
247
|
-
* - executable
|
|
248
|
-
* Those parameters will be used and updated to work with ACP:
|
|
249
|
-
* - hooks (merged with ACP's hooks)
|
|
250
|
-
* - mcpServers (merged with ACP's mcpServers)
|
|
251
|
-
*/
|
|
252
|
-
options?: Options;
|
|
253
|
-
};
|
|
254
|
-
/** Initial model to use for the session (e.g., 'claude-opus-4-5', 'gpt-5.1') */
|
|
255
|
-
model?: string;
|
|
256
|
-
};
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Extra metadata that the agent provides for each tool_call / tool_update update.
|
|
260
|
-
*/
|
|
261
|
-
export type ToolUpdateMeta = {
|
|
262
|
-
claudeCode?: {
|
|
263
|
-
/* The name of the tool that was used in Claude Code. */
|
|
264
|
-
toolName: string;
|
|
265
|
-
/* The structured output provided by Claude Code. */
|
|
266
|
-
toolResponse?: unknown;
|
|
267
|
-
};
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
type ToolUseCache = {
|
|
271
|
-
[key: string]: {
|
|
272
|
-
type: "tool_use" | "server_tool_use" | "mcp_tool_use";
|
|
273
|
-
id: string;
|
|
274
|
-
name: string;
|
|
275
|
-
input: unknown;
|
|
276
|
-
};
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
// Bypass Permissions doesn't work if we are a root/sudo user
|
|
280
|
-
const IS_ROOT = (process.geteuid?.() ?? process.getuid?.()) === 0;
|
|
281
|
-
|
|
282
|
-
// Implement the ACP Agent interface
|
|
283
|
-
export class ClaudeAcpAgent implements Agent {
|
|
284
|
-
sessions: {
|
|
285
|
-
[key: string]: Session;
|
|
286
|
-
};
|
|
287
|
-
client: AgentSideConnection;
|
|
288
|
-
toolUseCache: ToolUseCache;
|
|
289
|
-
fileContentCache: { [key: string]: string };
|
|
290
|
-
backgroundTerminals: { [key: string]: BackgroundTerminal } = {};
|
|
291
|
-
clientCapabilities?: ClientCapabilities;
|
|
292
|
-
logger: Logger = new Logger({ debug: true, prefix: "[ClaudeAcpAgent]" });
|
|
293
|
-
sessionStore?: SessionStore;
|
|
294
|
-
|
|
295
|
-
constructor(client: AgentSideConnection, sessionStore?: SessionStore) {
|
|
296
|
-
this.sessions = {};
|
|
297
|
-
this.client = client;
|
|
298
|
-
this.toolUseCache = {};
|
|
299
|
-
this.fileContentCache = {};
|
|
300
|
-
this.sessionStore = sessionStore;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
createSession(
|
|
304
|
-
sessionId: string,
|
|
305
|
-
q: Query,
|
|
306
|
-
input: Pushable<SDKUserMessage>,
|
|
307
|
-
permissionMode: PermissionMode,
|
|
308
|
-
): Session {
|
|
309
|
-
const session: Session = {
|
|
310
|
-
query: q,
|
|
311
|
-
input,
|
|
312
|
-
cancelled: false,
|
|
313
|
-
permissionMode,
|
|
314
|
-
notificationHistory: [],
|
|
315
|
-
};
|
|
316
|
-
this.sessions[sessionId] = session;
|
|
317
|
-
return session;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
private getLatestAssistantText(
|
|
321
|
-
notifications: SessionNotification[],
|
|
322
|
-
): string | null {
|
|
323
|
-
const chunks: string[] = [];
|
|
324
|
-
let started = false;
|
|
325
|
-
|
|
326
|
-
for (let i = notifications.length - 1; i >= 0; i -= 1) {
|
|
327
|
-
const update = notifications[i]?.update;
|
|
328
|
-
if (!update) continue;
|
|
329
|
-
|
|
330
|
-
if (update.sessionUpdate === "agent_message_chunk") {
|
|
331
|
-
started = true;
|
|
332
|
-
const content = update.content as {
|
|
333
|
-
type?: string;
|
|
334
|
-
text?: string;
|
|
335
|
-
} | null;
|
|
336
|
-
if (content?.type === "text" && content.text) {
|
|
337
|
-
chunks.push(content.text);
|
|
338
|
-
}
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
if (started) {
|
|
343
|
-
break;
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
if (chunks.length === 0) return null;
|
|
348
|
-
return chunks.reverse().join("");
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
private isPlanReady(plan: string | undefined): boolean {
|
|
352
|
-
if (!plan) return false;
|
|
353
|
-
const trimmed = plan.trim();
|
|
354
|
-
if (trimmed.length < 40) return false;
|
|
355
|
-
return /(^|\n)#{1,6}\s+\S/.test(trimmed);
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
appendNotification(
|
|
359
|
-
sessionId: string,
|
|
360
|
-
notification: SessionNotification,
|
|
361
|
-
): void {
|
|
362
|
-
// In-memory only - S3 persistence is now automatic via tapped stream
|
|
363
|
-
this.sessions[sessionId]?.notificationHistory.push(notification);
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
async initialize(request: InitializeRequest): Promise<InitializeResponse> {
|
|
367
|
-
this.clientCapabilities = request.clientCapabilities;
|
|
368
|
-
|
|
369
|
-
// Default authMethod
|
|
370
|
-
const authMethod: { description: string; name: string; id: string } = {
|
|
371
|
-
description: "Run `claude /login` in the terminal",
|
|
372
|
-
name: "Log in with Claude Code",
|
|
373
|
-
id: "claude-login",
|
|
374
|
-
};
|
|
375
|
-
|
|
376
|
-
// If client supports terminal-auth capability, use that instead.
|
|
377
|
-
// if (request.clientCapabilities?._meta?.["terminal-auth"] === true) {
|
|
378
|
-
// const cliPath = fileURLToPath(import.meta.resolve("@anthropic-ai/claude-agent-sdk/cli.js"));
|
|
379
|
-
|
|
380
|
-
// authMethod._meta = {
|
|
381
|
-
// "terminal-auth": {
|
|
382
|
-
// command: "node",
|
|
383
|
-
// args: [cliPath, "/login"],
|
|
384
|
-
// label: "Claude Code Login",
|
|
385
|
-
// },
|
|
386
|
-
// };
|
|
387
|
-
// }
|
|
388
|
-
|
|
389
|
-
return {
|
|
390
|
-
protocolVersion: 1,
|
|
391
|
-
agentCapabilities: {
|
|
392
|
-
promptCapabilities: {
|
|
393
|
-
image: true,
|
|
394
|
-
embeddedContext: true,
|
|
395
|
-
},
|
|
396
|
-
mcpCapabilities: {
|
|
397
|
-
http: true,
|
|
398
|
-
sse: true,
|
|
399
|
-
},
|
|
400
|
-
loadSession: true,
|
|
401
|
-
_meta: {
|
|
402
|
-
posthog: {
|
|
403
|
-
resumeSession: true,
|
|
404
|
-
},
|
|
405
|
-
},
|
|
406
|
-
},
|
|
407
|
-
agentInfo: {
|
|
408
|
-
name: packageJson.name,
|
|
409
|
-
title: "Claude Code",
|
|
410
|
-
version: packageJson.version,
|
|
411
|
-
},
|
|
412
|
-
authMethods: [authMethod],
|
|
413
|
-
};
|
|
414
|
-
}
|
|
415
|
-
async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
|
|
416
|
-
if (
|
|
417
|
-
fs.existsSync(path.resolve(os.homedir(), ".claude.json.backup")) &&
|
|
418
|
-
!fs.existsSync(path.resolve(os.homedir(), ".claude.json"))
|
|
419
|
-
) {
|
|
420
|
-
throw RequestError.authRequired();
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
// Allow caller to specify sessionId via _meta (e.g. taskRunId in our case)
|
|
424
|
-
const sessionId =
|
|
425
|
-
(params._meta as { sessionId?: string } | undefined)?.sessionId ||
|
|
426
|
-
uuidv7();
|
|
427
|
-
const input = new Pushable<SDKUserMessage>();
|
|
428
|
-
|
|
429
|
-
const mcpServers: Record<string, McpServerConfig> = {};
|
|
430
|
-
if (Array.isArray(params.mcpServers)) {
|
|
431
|
-
for (const server of params.mcpServers) {
|
|
432
|
-
if ("type" in server) {
|
|
433
|
-
mcpServers[server.name] = {
|
|
434
|
-
type: server.type,
|
|
435
|
-
url: server.url,
|
|
436
|
-
headers: server.headers
|
|
437
|
-
? Object.fromEntries(server.headers.map((e) => [e.name, e.value]))
|
|
438
|
-
: undefined,
|
|
439
|
-
};
|
|
440
|
-
} else {
|
|
441
|
-
mcpServers[server.name] = {
|
|
442
|
-
type: "stdio",
|
|
443
|
-
command: server.command,
|
|
444
|
-
args: server.args,
|
|
445
|
-
env: server.env
|
|
446
|
-
? Object.fromEntries(server.env.map((e) => [e.name, e.value]))
|
|
447
|
-
: undefined,
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// Only add the acp MCP server if built-in tools are not disabled
|
|
454
|
-
if (!params._meta?.disableBuiltInTools) {
|
|
455
|
-
const server = createMcpServer(this, sessionId, this.clientCapabilities);
|
|
456
|
-
mcpServers.acp = {
|
|
457
|
-
type: "sdk",
|
|
458
|
-
name: "acp",
|
|
459
|
-
instance: server,
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
let systemPrompt: Options["systemPrompt"] = {
|
|
464
|
-
type: "preset",
|
|
465
|
-
preset: "claude_code",
|
|
466
|
-
};
|
|
467
|
-
if (params._meta?.systemPrompt) {
|
|
468
|
-
const customPrompt = params._meta.systemPrompt;
|
|
469
|
-
if (typeof customPrompt === "string") {
|
|
470
|
-
systemPrompt = customPrompt;
|
|
471
|
-
} else if (
|
|
472
|
-
typeof customPrompt === "object" &&
|
|
473
|
-
"append" in customPrompt &&
|
|
474
|
-
typeof customPrompt.append === "string"
|
|
475
|
-
) {
|
|
476
|
-
systemPrompt.append = customPrompt.append;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// Use initialModeId from _meta if provided (e.g., "plan" for plan mode), otherwise default
|
|
481
|
-
const initialModeId = (
|
|
482
|
-
params._meta as { initialModeId?: string } | undefined
|
|
483
|
-
)?.initialModeId;
|
|
484
|
-
const ourPermissionMode = (initialModeId ?? "default") as PermissionMode;
|
|
485
|
-
const sdkPermissionMode: PermissionMode = ourPermissionMode;
|
|
486
|
-
|
|
487
|
-
// Extract options from _meta if provided
|
|
488
|
-
const userProvidedOptions = (params._meta as NewSessionMeta | undefined)
|
|
489
|
-
?.claudeCode?.options;
|
|
490
|
-
|
|
491
|
-
const options: Options = {
|
|
492
|
-
systemPrompt,
|
|
493
|
-
settingSources: ["user", "project", "local"],
|
|
494
|
-
stderr: (err) => this.logger.error(err),
|
|
495
|
-
...userProvidedOptions,
|
|
496
|
-
// Override certain fields that must be controlled by ACP
|
|
497
|
-
cwd: params.cwd,
|
|
498
|
-
includePartialMessages: true,
|
|
499
|
-
mcpServers: { ...(userProvidedOptions?.mcpServers || {}), ...mcpServers },
|
|
500
|
-
// If we want bypassPermissions to be an option, we have to allow it here.
|
|
501
|
-
// But it doesn't work in root mode, so we only activate it if it will work.
|
|
502
|
-
allowDangerouslySkipPermissions: !IS_ROOT,
|
|
503
|
-
// Use the requested permission mode (including plan mode)
|
|
504
|
-
permissionMode: sdkPermissionMode,
|
|
505
|
-
canUseTool: this.canUseTool(sessionId),
|
|
506
|
-
// Use "node" to resolve via PATH where a symlink to Electron exists.
|
|
507
|
-
// This avoids launching the Electron binary directly from the app bundle,
|
|
508
|
-
// which can cause dock icons to appear on macOS even with ELECTRON_RUN_AS_NODE.
|
|
509
|
-
executable: "node",
|
|
510
|
-
// Prevent spawned Electron processes from showing in dock/tray.
|
|
511
|
-
// Must merge with process.env since SDK replaces rather than merges.
|
|
512
|
-
// Enable AskUserQuestion tool via environment variable (required by SDK feature flag)
|
|
513
|
-
env: {
|
|
514
|
-
...process.env,
|
|
515
|
-
ELECTRON_RUN_AS_NODE: "1",
|
|
516
|
-
CLAUDE_CODE_ENABLE_ASK_USER_QUESTION_TOOL: "true",
|
|
517
|
-
},
|
|
518
|
-
...(process.env.CLAUDE_CODE_EXECUTABLE && {
|
|
519
|
-
pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE,
|
|
520
|
-
}),
|
|
521
|
-
hooks: {
|
|
522
|
-
...userProvidedOptions?.hooks,
|
|
523
|
-
PostToolUse: [
|
|
524
|
-
...(userProvidedOptions?.hooks?.PostToolUse || []),
|
|
525
|
-
{
|
|
526
|
-
hooks: [createPostToolUseHook(this.logger)],
|
|
527
|
-
},
|
|
528
|
-
],
|
|
529
|
-
},
|
|
530
|
-
};
|
|
531
|
-
|
|
532
|
-
// AskUserQuestion must be explicitly allowed for the agent to use it
|
|
533
|
-
const allowedTools: string[] = ["AskUserQuestion"];
|
|
534
|
-
const disallowedTools: string[] = [];
|
|
535
|
-
|
|
536
|
-
// Check if built-in tools should be disabled
|
|
537
|
-
const disableBuiltInTools = params._meta?.disableBuiltInTools === true;
|
|
538
|
-
|
|
539
|
-
if (!disableBuiltInTools) {
|
|
540
|
-
if (this.clientCapabilities?.fs?.readTextFile) {
|
|
541
|
-
allowedTools.push(toolNames.read);
|
|
542
|
-
disallowedTools.push("Read");
|
|
543
|
-
}
|
|
544
|
-
if (this.clientCapabilities?.fs?.writeTextFile) {
|
|
545
|
-
disallowedTools.push("Write", "Edit");
|
|
546
|
-
}
|
|
547
|
-
if (this.clientCapabilities?.terminal) {
|
|
548
|
-
allowedTools.push(toolNames.bashOutput, toolNames.killShell);
|
|
549
|
-
disallowedTools.push("Bash", "BashOutput", "KillShell");
|
|
550
|
-
}
|
|
551
|
-
} else {
|
|
552
|
-
// When built-in tools are disabled, explicitly disallow all of them
|
|
553
|
-
disallowedTools.push(
|
|
554
|
-
toolNames.read,
|
|
555
|
-
toolNames.write,
|
|
556
|
-
toolNames.edit,
|
|
557
|
-
toolNames.bash,
|
|
558
|
-
toolNames.bashOutput,
|
|
559
|
-
toolNames.killShell,
|
|
560
|
-
"Read",
|
|
561
|
-
"Write",
|
|
562
|
-
"Edit",
|
|
563
|
-
"Bash",
|
|
564
|
-
"BashOutput",
|
|
565
|
-
"KillShell",
|
|
566
|
-
"Glob",
|
|
567
|
-
"Grep",
|
|
568
|
-
"Task",
|
|
569
|
-
"TodoWrite",
|
|
570
|
-
"ExitPlanMode",
|
|
571
|
-
"WebSearch",
|
|
572
|
-
"WebFetch",
|
|
573
|
-
"AskUserQuestion",
|
|
574
|
-
"SlashCommand",
|
|
575
|
-
"Skill",
|
|
576
|
-
"NotebookEdit",
|
|
577
|
-
);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// ExitPlanMode should only be available during plan mode
|
|
581
|
-
if (ourPermissionMode !== "plan") {
|
|
582
|
-
disallowedTools.push("ExitPlanMode");
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
if (allowedTools.length > 0) {
|
|
586
|
-
options.allowedTools = allowedTools;
|
|
587
|
-
}
|
|
588
|
-
if (disallowedTools.length > 0) {
|
|
589
|
-
options.disallowedTools = disallowedTools;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// Handle abort controller from meta options
|
|
593
|
-
const abortController = userProvidedOptions?.abortController;
|
|
594
|
-
if (abortController?.signal.aborted) {
|
|
595
|
-
throw new Error("Cancelled");
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
// Clear statsig cache before creating query to avoid input_examples bug
|
|
599
|
-
clearStatsigCache();
|
|
600
|
-
|
|
601
|
-
const q = query({
|
|
602
|
-
prompt: input,
|
|
603
|
-
options,
|
|
604
|
-
});
|
|
605
|
-
|
|
606
|
-
this.createSession(sessionId, q, input, ourPermissionMode);
|
|
607
|
-
|
|
608
|
-
// Register for S3 persistence if config provided
|
|
609
|
-
const persistence = params._meta?.persistence as
|
|
610
|
-
| SessionPersistenceConfig
|
|
611
|
-
| undefined;
|
|
612
|
-
if (persistence && this.sessionStore) {
|
|
613
|
-
this.sessionStore.register(sessionId, persistence);
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
const availableCommands = await getAvailableSlashCommands(q);
|
|
617
|
-
const models = await getAvailableModels(q);
|
|
618
|
-
|
|
619
|
-
// Set initial model if provided via _meta (must be after getAvailableModels which resets to default)
|
|
620
|
-
const requestedModel = (params._meta as NewSessionMeta | undefined)?.model;
|
|
621
|
-
if (requestedModel) {
|
|
622
|
-
try {
|
|
623
|
-
await q.setModel(requestedModel);
|
|
624
|
-
this.logger.info("Set initial model", { model: requestedModel });
|
|
625
|
-
} catch (err) {
|
|
626
|
-
this.logger.warn("Failed to set initial model, using default", {
|
|
627
|
-
requestedModel,
|
|
628
|
-
error: err,
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
// Needs to happen after we return the session
|
|
634
|
-
setTimeout(() => {
|
|
635
|
-
this.client.sessionUpdate({
|
|
636
|
-
sessionId,
|
|
637
|
-
update: {
|
|
638
|
-
sessionUpdate: "available_commands_update",
|
|
639
|
-
availableCommands,
|
|
640
|
-
},
|
|
641
|
-
});
|
|
642
|
-
}, 0);
|
|
643
|
-
|
|
644
|
-
const availableModes = [
|
|
645
|
-
{
|
|
646
|
-
id: "default",
|
|
647
|
-
name: "Always Ask",
|
|
648
|
-
description: "Prompts for permission on first use of each tool",
|
|
649
|
-
},
|
|
650
|
-
{
|
|
651
|
-
id: "acceptEdits",
|
|
652
|
-
name: "Accept Edits",
|
|
653
|
-
description:
|
|
654
|
-
"Automatically accepts file edit permissions for the session",
|
|
655
|
-
},
|
|
656
|
-
{
|
|
657
|
-
id: "plan",
|
|
658
|
-
name: "Plan Mode",
|
|
659
|
-
description:
|
|
660
|
-
"Claude can analyze but not modify files or execute commands",
|
|
661
|
-
},
|
|
662
|
-
];
|
|
663
|
-
// Only works in non-root mode
|
|
664
|
-
if (!IS_ROOT) {
|
|
665
|
-
availableModes.push({
|
|
666
|
-
id: "bypassPermissions",
|
|
667
|
-
name: "Bypass Permissions",
|
|
668
|
-
description: "Skips all permission prompts",
|
|
669
|
-
});
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
return {
|
|
673
|
-
sessionId,
|
|
674
|
-
models,
|
|
675
|
-
modes: {
|
|
676
|
-
currentModeId: ourPermissionMode,
|
|
677
|
-
availableModes,
|
|
678
|
-
},
|
|
679
|
-
};
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
async authenticate(_params: AuthenticateRequest): Promise<void> {
|
|
683
|
-
throw new Error("Method not implemented.");
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
async prompt(params: PromptRequest): Promise<PromptResponse> {
|
|
687
|
-
if (!this.sessions[params.sessionId]) {
|
|
688
|
-
throw new Error("Session not found");
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
this.sessions[params.sessionId].cancelled = false;
|
|
692
|
-
|
|
693
|
-
const session = this.sessions[params.sessionId];
|
|
694
|
-
const { query, input } = session;
|
|
695
|
-
|
|
696
|
-
// Capture and store user message for replay
|
|
697
|
-
for (const chunk of params.prompt) {
|
|
698
|
-
const userNotification: SessionNotification = {
|
|
699
|
-
sessionId: params.sessionId,
|
|
700
|
-
update: {
|
|
701
|
-
sessionUpdate: "user_message_chunk",
|
|
702
|
-
content: chunk,
|
|
703
|
-
},
|
|
704
|
-
};
|
|
705
|
-
await this.client.sessionUpdate(userNotification);
|
|
706
|
-
this.appendNotification(params.sessionId, userNotification);
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
input.push(promptToClaude({ ...params, prompt: params.prompt }));
|
|
710
|
-
while (true) {
|
|
711
|
-
const { value: message, done } = await query.next();
|
|
712
|
-
if (done || !message) {
|
|
713
|
-
if (this.sessions[params.sessionId].cancelled) {
|
|
714
|
-
return { stopReason: "cancelled" };
|
|
715
|
-
}
|
|
716
|
-
break;
|
|
717
|
-
}
|
|
718
|
-
this.logger.debug("SDK message received", {
|
|
719
|
-
type: message.type,
|
|
720
|
-
subtype: (message as { subtype?: string }).subtype,
|
|
721
|
-
});
|
|
722
|
-
|
|
723
|
-
switch (message.type) {
|
|
724
|
-
case "system":
|
|
725
|
-
switch (message.subtype) {
|
|
726
|
-
case "init":
|
|
727
|
-
// Capture SDK session ID and notify client for persistence
|
|
728
|
-
if (message.session_id) {
|
|
729
|
-
const session = this.sessions[params.sessionId];
|
|
730
|
-
if (session && !session.sdkSessionId) {
|
|
731
|
-
session.sdkSessionId = message.session_id;
|
|
732
|
-
this.client.extNotification("_posthog/sdk_session", {
|
|
733
|
-
sessionId: params.sessionId,
|
|
734
|
-
sdkSessionId: message.session_id,
|
|
735
|
-
});
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
break;
|
|
739
|
-
case "compact_boundary":
|
|
740
|
-
case "hook_response":
|
|
741
|
-
case "status":
|
|
742
|
-
// Todo: process via status api: https://docs.claude.com/en/docs/claude-code/hooks#hook-output
|
|
743
|
-
break;
|
|
744
|
-
default:
|
|
745
|
-
unreachable(message, this.logger);
|
|
746
|
-
break;
|
|
747
|
-
}
|
|
748
|
-
break;
|
|
749
|
-
case "result": {
|
|
750
|
-
if (this.sessions[params.sessionId].cancelled) {
|
|
751
|
-
return { stopReason: "cancelled" };
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
switch (message.subtype) {
|
|
755
|
-
case "success": {
|
|
756
|
-
if (message.result.includes("Please run /login")) {
|
|
757
|
-
throw RequestError.authRequired();
|
|
758
|
-
}
|
|
759
|
-
if (message.is_error) {
|
|
760
|
-
throw RequestError.internalError(undefined, message.result);
|
|
761
|
-
}
|
|
762
|
-
return { stopReason: "end_turn" };
|
|
763
|
-
}
|
|
764
|
-
case "error_during_execution":
|
|
765
|
-
if (message.is_error) {
|
|
766
|
-
throw RequestError.internalError(
|
|
767
|
-
undefined,
|
|
768
|
-
message.errors.join(", ") || message.subtype,
|
|
769
|
-
);
|
|
770
|
-
}
|
|
771
|
-
return { stopReason: "end_turn" };
|
|
772
|
-
case "error_max_budget_usd":
|
|
773
|
-
case "error_max_turns":
|
|
774
|
-
case "error_max_structured_output_retries":
|
|
775
|
-
if (message.is_error) {
|
|
776
|
-
throw RequestError.internalError(
|
|
777
|
-
undefined,
|
|
778
|
-
message.errors.join(", ") || message.subtype,
|
|
779
|
-
);
|
|
780
|
-
}
|
|
781
|
-
return { stopReason: "max_turn_requests" };
|
|
782
|
-
default:
|
|
783
|
-
unreachable(message, this.logger);
|
|
784
|
-
break;
|
|
785
|
-
}
|
|
786
|
-
break;
|
|
787
|
-
}
|
|
788
|
-
case "stream_event": {
|
|
789
|
-
this.logger.debug("Stream event", { eventType: message.event?.type });
|
|
790
|
-
for (const notification of streamEventToAcpNotifications(
|
|
791
|
-
message,
|
|
792
|
-
params.sessionId,
|
|
793
|
-
this.toolUseCache,
|
|
794
|
-
this.fileContentCache,
|
|
795
|
-
this.client,
|
|
796
|
-
this.logger,
|
|
797
|
-
)) {
|
|
798
|
-
await this.client.sessionUpdate(notification);
|
|
799
|
-
this.appendNotification(params.sessionId, notification);
|
|
800
|
-
}
|
|
801
|
-
break;
|
|
802
|
-
}
|
|
803
|
-
case "user":
|
|
804
|
-
case "assistant": {
|
|
805
|
-
if (this.sessions[params.sessionId].cancelled) {
|
|
806
|
-
break;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
// Slash commands like /compact can generate invalid output... doesn't match
|
|
810
|
-
// their own docs: https://docs.anthropic.com/en/docs/claude-code/sdk/sdk-slash-commands#%2Fcompact-compact-conversation-history
|
|
811
|
-
if (
|
|
812
|
-
typeof message.message.content === "string" &&
|
|
813
|
-
message.message.content.includes("<local-command-stdout>")
|
|
814
|
-
) {
|
|
815
|
-
this.logger.info(message.message.content);
|
|
816
|
-
break;
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
if (
|
|
820
|
-
typeof message.message.content === "string" &&
|
|
821
|
-
message.message.content.includes("<local-command-stderr>")
|
|
822
|
-
) {
|
|
823
|
-
this.logger.error(message.message.content);
|
|
824
|
-
break;
|
|
825
|
-
}
|
|
826
|
-
// Skip these user messages for now, since they seem to just be messages we don't want in the feed
|
|
827
|
-
if (
|
|
828
|
-
message.type === "user" &&
|
|
829
|
-
(typeof message.message.content === "string" ||
|
|
830
|
-
(Array.isArray(message.message.content) &&
|
|
831
|
-
message.message.content.length === 1 &&
|
|
832
|
-
message.message.content[0].type === "text"))
|
|
833
|
-
) {
|
|
834
|
-
break;
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
if (
|
|
838
|
-
message.type === "assistant" &&
|
|
839
|
-
message.message.model === "<synthetic>" &&
|
|
840
|
-
Array.isArray(message.message.content) &&
|
|
841
|
-
message.message.content.length === 1 &&
|
|
842
|
-
message.message.content[0].type === "text" &&
|
|
843
|
-
message.message.content[0].text.includes("Please run /login")
|
|
844
|
-
) {
|
|
845
|
-
throw RequestError.authRequired();
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
// Text/thinking is streamed via stream_event, so skip them here to avoid duplication.
|
|
849
|
-
const content = message.message.content;
|
|
850
|
-
const contentToProcess = Array.isArray(content)
|
|
851
|
-
? content.filter(
|
|
852
|
-
(block) => block.type !== "text" && block.type !== "thinking",
|
|
853
|
-
)
|
|
854
|
-
: content;
|
|
855
|
-
|
|
856
|
-
for (const notification of toAcpNotifications(
|
|
857
|
-
contentToProcess as typeof content,
|
|
858
|
-
message.message.role,
|
|
859
|
-
params.sessionId,
|
|
860
|
-
this.toolUseCache,
|
|
861
|
-
this.fileContentCache,
|
|
862
|
-
this.client,
|
|
863
|
-
this.logger,
|
|
864
|
-
)) {
|
|
865
|
-
await this.client.sessionUpdate(notification);
|
|
866
|
-
this.appendNotification(params.sessionId, notification);
|
|
867
|
-
}
|
|
868
|
-
break;
|
|
869
|
-
}
|
|
870
|
-
case "tool_progress":
|
|
871
|
-
break;
|
|
872
|
-
case "auth_status":
|
|
873
|
-
break;
|
|
874
|
-
default:
|
|
875
|
-
unreachable(message, this.logger);
|
|
876
|
-
break;
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
throw new Error("Session did not end in result");
|
|
880
|
-
}
|
|
881
|
-
|
|
882
|
-
async cancel(params: CancelNotification): Promise<void> {
|
|
883
|
-
if (!this.sessions[params.sessionId]) {
|
|
884
|
-
throw new Error("Session not found");
|
|
885
|
-
}
|
|
886
|
-
this.sessions[params.sessionId].cancelled = true;
|
|
887
|
-
await this.sessions[params.sessionId].query.interrupt();
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
async setSessionModel(params: SetSessionModelRequest) {
|
|
891
|
-
if (!this.sessions[params.sessionId]) {
|
|
892
|
-
throw new Error("Session not found");
|
|
893
|
-
}
|
|
894
|
-
await this.sessions[params.sessionId].query.setModel(params.modelId);
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
async setSessionMode(
|
|
898
|
-
params: SetSessionModeRequest,
|
|
899
|
-
): Promise<SetSessionModeResponse> {
|
|
900
|
-
if (!this.sessions[params.sessionId]) {
|
|
901
|
-
throw new Error("Session not found");
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
switch (params.modeId) {
|
|
905
|
-
case "default":
|
|
906
|
-
case "acceptEdits":
|
|
907
|
-
case "bypassPermissions":
|
|
908
|
-
case "plan":
|
|
909
|
-
this.sessions[params.sessionId].permissionMode = params.modeId;
|
|
910
|
-
try {
|
|
911
|
-
await this.sessions[params.sessionId].query.setPermissionMode(
|
|
912
|
-
params.modeId,
|
|
913
|
-
);
|
|
914
|
-
} catch (error) {
|
|
915
|
-
const errorMessage =
|
|
916
|
-
error instanceof Error && error.message
|
|
917
|
-
? error.message
|
|
918
|
-
: "Invalid Mode";
|
|
919
|
-
|
|
920
|
-
throw new Error(errorMessage);
|
|
921
|
-
}
|
|
922
|
-
return {};
|
|
923
|
-
default:
|
|
924
|
-
throw new Error("Invalid Mode");
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
async readTextFile(
|
|
929
|
-
params: ReadTextFileRequest,
|
|
930
|
-
): Promise<ReadTextFileResponse> {
|
|
931
|
-
const response = await this.client.readTextFile(params);
|
|
932
|
-
if (!params.limit && !params.line) {
|
|
933
|
-
this.fileContentCache[params.path] = response.content;
|
|
934
|
-
}
|
|
935
|
-
return response;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
async writeTextFile(
|
|
939
|
-
params: WriteTextFileRequest,
|
|
940
|
-
): Promise<WriteTextFileResponse> {
|
|
941
|
-
const response = await this.client.writeTextFile(params);
|
|
942
|
-
this.fileContentCache[params.path] = params.content;
|
|
943
|
-
return response;
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
/**
|
|
947
|
-
* Load session delegates to resumeSession since we have no need to replay history.
|
|
948
|
-
* Client is responsible for fetching and rendering history from S3.
|
|
949
|
-
*/
|
|
950
|
-
async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
|
|
951
|
-
return this.resumeSession(params);
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
canUseTool(sessionId: string): CanUseTool {
|
|
955
|
-
return async (toolName, toolInput, { suggestions, toolUseID }) => {
|
|
956
|
-
const session = this.sessions[sessionId];
|
|
957
|
-
if (!session) {
|
|
958
|
-
return {
|
|
959
|
-
behavior: "deny",
|
|
960
|
-
message: "Session not found",
|
|
961
|
-
interrupt: true,
|
|
962
|
-
};
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
// Helper to emit a tool denial notification so the UI shows the reason
|
|
966
|
-
const emitToolDenial = async (message: string) => {
|
|
967
|
-
this.logger.info(`[canUseTool] Tool denied: ${toolName}`, { message });
|
|
968
|
-
await this.client.sessionUpdate({
|
|
969
|
-
sessionId,
|
|
970
|
-
update: {
|
|
971
|
-
sessionUpdate: "tool_call_update",
|
|
972
|
-
toolCallId: toolUseID,
|
|
973
|
-
status: "failed",
|
|
974
|
-
content: [
|
|
975
|
-
{
|
|
976
|
-
type: "content",
|
|
977
|
-
content: {
|
|
978
|
-
type: "text",
|
|
979
|
-
text: message,
|
|
980
|
-
},
|
|
981
|
-
},
|
|
982
|
-
],
|
|
983
|
-
},
|
|
984
|
-
});
|
|
985
|
-
};
|
|
986
|
-
|
|
987
|
-
if (toolName === "ExitPlanMode") {
|
|
988
|
-
// If we're already not in plan mode, just allow the tool without prompting
|
|
989
|
-
// This handles the case where mode was already changed by a previous ExitPlanMode call
|
|
990
|
-
// (Claude may call ExitPlanMode again after writing the plan file)
|
|
991
|
-
if (session.permissionMode !== "plan") {
|
|
992
|
-
return {
|
|
993
|
-
behavior: "allow",
|
|
994
|
-
updatedInput: toolInput,
|
|
995
|
-
};
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
let updatedInput = toolInput;
|
|
999
|
-
const planFromFile =
|
|
1000
|
-
session.lastPlanContent ||
|
|
1001
|
-
(session.lastPlanFilePath
|
|
1002
|
-
? this.fileContentCache[session.lastPlanFilePath]
|
|
1003
|
-
: undefined);
|
|
1004
|
-
const hasPlan =
|
|
1005
|
-
typeof (toolInput as { plan?: unknown } | undefined)?.plan ===
|
|
1006
|
-
"string";
|
|
1007
|
-
if (!hasPlan) {
|
|
1008
|
-
const fallbackPlan = planFromFile
|
|
1009
|
-
? planFromFile
|
|
1010
|
-
: this.getLatestAssistantText(session.notificationHistory);
|
|
1011
|
-
if (fallbackPlan) {
|
|
1012
|
-
updatedInput = {
|
|
1013
|
-
...(toolInput as Record<string, unknown>),
|
|
1014
|
-
plan: fallbackPlan,
|
|
1015
|
-
};
|
|
1016
|
-
}
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
const planText =
|
|
1020
|
-
typeof (updatedInput as { plan?: unknown } | undefined)?.plan ===
|
|
1021
|
-
"string"
|
|
1022
|
-
? String((updatedInput as { plan?: unknown }).plan)
|
|
1023
|
-
: undefined;
|
|
1024
|
-
if (!planText) {
|
|
1025
|
-
const message = `Plan not ready. Provide the full markdown plan in ExitPlanMode or write it to ${getClaudePlansDir()} before requesting approval.`;
|
|
1026
|
-
await emitToolDenial(message);
|
|
1027
|
-
return {
|
|
1028
|
-
behavior: "deny",
|
|
1029
|
-
message,
|
|
1030
|
-
interrupt: false,
|
|
1031
|
-
};
|
|
1032
|
-
}
|
|
1033
|
-
if (!this.isPlanReady(planText)) {
|
|
1034
|
-
const message =
|
|
1035
|
-
"Plan not ready. Provide the full markdown plan in ExitPlanMode before requesting approval.";
|
|
1036
|
-
await emitToolDenial(message);
|
|
1037
|
-
return {
|
|
1038
|
-
behavior: "deny",
|
|
1039
|
-
message,
|
|
1040
|
-
interrupt: false,
|
|
1041
|
-
};
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
// ExitPlanMode is a signal to show the permission dialog
|
|
1045
|
-
// The plan content should already be in the agent's text response
|
|
1046
|
-
// Note: The SDK's ExitPlanMode tool includes a plan parameter, so ensure it is present
|
|
1047
|
-
|
|
1048
|
-
const response = await this.client.requestPermission({
|
|
1049
|
-
options: [
|
|
1050
|
-
{
|
|
1051
|
-
kind: "allow_always",
|
|
1052
|
-
name: "Yes, and auto-accept edits",
|
|
1053
|
-
optionId: "acceptEdits",
|
|
1054
|
-
},
|
|
1055
|
-
{
|
|
1056
|
-
kind: "allow_once",
|
|
1057
|
-
name: "Yes, and manually approve edits",
|
|
1058
|
-
optionId: "default",
|
|
1059
|
-
},
|
|
1060
|
-
{
|
|
1061
|
-
kind: "reject_once",
|
|
1062
|
-
name: "No, keep planning",
|
|
1063
|
-
optionId: "plan",
|
|
1064
|
-
},
|
|
1065
|
-
],
|
|
1066
|
-
sessionId,
|
|
1067
|
-
toolCall: {
|
|
1068
|
-
toolCallId: toolUseID,
|
|
1069
|
-
rawInput: { ...updatedInput, toolName },
|
|
1070
|
-
title: toolInfoFromToolUse(
|
|
1071
|
-
{ name: toolName, input: updatedInput },
|
|
1072
|
-
this.fileContentCache,
|
|
1073
|
-
this.logger,
|
|
1074
|
-
).title,
|
|
1075
|
-
},
|
|
1076
|
-
});
|
|
1077
|
-
|
|
1078
|
-
if (
|
|
1079
|
-
response.outcome?.outcome === "selected" &&
|
|
1080
|
-
(response.outcome.optionId === "default" ||
|
|
1081
|
-
response.outcome.optionId === "acceptEdits")
|
|
1082
|
-
) {
|
|
1083
|
-
session.permissionMode = response.outcome.optionId;
|
|
1084
|
-
await this.client.sessionUpdate({
|
|
1085
|
-
sessionId,
|
|
1086
|
-
update: {
|
|
1087
|
-
sessionUpdate: "current_mode_update",
|
|
1088
|
-
currentModeId: response.outcome.optionId,
|
|
1089
|
-
},
|
|
1090
|
-
});
|
|
1091
|
-
|
|
1092
|
-
return {
|
|
1093
|
-
behavior: "allow",
|
|
1094
|
-
updatedInput,
|
|
1095
|
-
updatedPermissions: suggestions ?? [
|
|
1096
|
-
{
|
|
1097
|
-
type: "setMode",
|
|
1098
|
-
mode: response.outcome.optionId,
|
|
1099
|
-
destination: "session",
|
|
1100
|
-
},
|
|
1101
|
-
],
|
|
1102
|
-
};
|
|
1103
|
-
} else {
|
|
1104
|
-
// User chose "No, keep planning" - stay in plan mode and let agent continue
|
|
1105
|
-
const message =
|
|
1106
|
-
"User wants to continue planning. Please refine your plan based on any feedback provided, or ask clarifying questions if needed.";
|
|
1107
|
-
await emitToolDenial(message);
|
|
1108
|
-
return {
|
|
1109
|
-
behavior: "deny",
|
|
1110
|
-
message,
|
|
1111
|
-
interrupt: false,
|
|
1112
|
-
};
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
// AskUserQuestion always prompts user - never auto-approve
|
|
1117
|
-
if (toolName === "AskUserQuestion") {
|
|
1118
|
-
interface QuestionItem {
|
|
1119
|
-
question: string;
|
|
1120
|
-
header?: string;
|
|
1121
|
-
options: Array<{ label: string; description?: string }>;
|
|
1122
|
-
multiSelect?: boolean;
|
|
1123
|
-
}
|
|
1124
|
-
interface AskUserQuestionInput {
|
|
1125
|
-
// Full format: array of questions with options
|
|
1126
|
-
questions?: QuestionItem[];
|
|
1127
|
-
// Simple format: just a question string (used when Claude doesn't have proper schema)
|
|
1128
|
-
question?: string;
|
|
1129
|
-
header?: string;
|
|
1130
|
-
options?: Array<{ label: string; description?: string }>;
|
|
1131
|
-
multiSelect?: boolean;
|
|
1132
|
-
}
|
|
1133
|
-
const input = toolInput as AskUserQuestionInput;
|
|
1134
|
-
|
|
1135
|
-
// Normalize to questions array format
|
|
1136
|
-
// Support both: { questions: [...] } and { question: "..." }
|
|
1137
|
-
let questions: QuestionItem[];
|
|
1138
|
-
if (input.questions && input.questions.length > 0) {
|
|
1139
|
-
// Full format with questions array
|
|
1140
|
-
questions = input.questions;
|
|
1141
|
-
} else if (input.question) {
|
|
1142
|
-
// Simple format - convert to array
|
|
1143
|
-
// If no options provided, just use "Other" for free-form input
|
|
1144
|
-
questions = [
|
|
1145
|
-
{
|
|
1146
|
-
question: input.question,
|
|
1147
|
-
header: input.header,
|
|
1148
|
-
options: input.options || [],
|
|
1149
|
-
multiSelect: input.multiSelect,
|
|
1150
|
-
},
|
|
1151
|
-
];
|
|
1152
|
-
} else {
|
|
1153
|
-
return {
|
|
1154
|
-
behavior: "deny",
|
|
1155
|
-
message: "No questions provided",
|
|
1156
|
-
interrupt: true,
|
|
1157
|
-
};
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
// Collect all answers from all questions
|
|
1161
|
-
const allAnswers: Record<string, string | string[]> = {};
|
|
1162
|
-
|
|
1163
|
-
for (let i = 0; i < questions.length; i++) {
|
|
1164
|
-
const question = questions[i];
|
|
1165
|
-
|
|
1166
|
-
// Convert question options to permission options
|
|
1167
|
-
const options = (question.options || []).map(
|
|
1168
|
-
(opt: { label: string; description?: string }, idx: number) => ({
|
|
1169
|
-
kind: "allow_once" as const,
|
|
1170
|
-
name: opt.label,
|
|
1171
|
-
optionId: `option_${idx}`,
|
|
1172
|
-
description: opt.description,
|
|
1173
|
-
}),
|
|
1174
|
-
);
|
|
1175
|
-
|
|
1176
|
-
// Add "Other" option for free-form response
|
|
1177
|
-
options.push({
|
|
1178
|
-
kind: "allow_once" as const,
|
|
1179
|
-
name: "Other",
|
|
1180
|
-
optionId: "other",
|
|
1181
|
-
description: "Provide a custom response",
|
|
1182
|
-
});
|
|
1183
|
-
|
|
1184
|
-
const response = await this.client.requestPermission({
|
|
1185
|
-
options,
|
|
1186
|
-
sessionId,
|
|
1187
|
-
toolCall: {
|
|
1188
|
-
toolCallId: toolUseID,
|
|
1189
|
-
rawInput: {
|
|
1190
|
-
...toolInput,
|
|
1191
|
-
toolName,
|
|
1192
|
-
// Include full question data for UI rendering
|
|
1193
|
-
currentQuestion: question,
|
|
1194
|
-
questionIndex: i,
|
|
1195
|
-
totalQuestions: questions.length,
|
|
1196
|
-
},
|
|
1197
|
-
// Use the full question text as title for the selection input
|
|
1198
|
-
title: question.question,
|
|
1199
|
-
},
|
|
1200
|
-
});
|
|
1201
|
-
|
|
1202
|
-
if (response.outcome?.outcome === "selected") {
|
|
1203
|
-
const selectedOptionId = response.outcome.optionId;
|
|
1204
|
-
// Type assertion for extended outcome fields
|
|
1205
|
-
const extendedOutcome = response.outcome as {
|
|
1206
|
-
optionId: string;
|
|
1207
|
-
selectedOptionIds?: string[];
|
|
1208
|
-
customInput?: string;
|
|
1209
|
-
};
|
|
1210
|
-
|
|
1211
|
-
if (selectedOptionId === "other" && extendedOutcome.customInput) {
|
|
1212
|
-
// "Other" was selected with custom text
|
|
1213
|
-
allAnswers[question.question] = extendedOutcome.customInput;
|
|
1214
|
-
} else if (selectedOptionId === "other") {
|
|
1215
|
-
// "Other" was selected but no custom text - just record "other"
|
|
1216
|
-
allAnswers[question.question] = "other";
|
|
1217
|
-
} else if (
|
|
1218
|
-
question.multiSelect &&
|
|
1219
|
-
extendedOutcome.selectedOptionIds
|
|
1220
|
-
) {
|
|
1221
|
-
// Multi-select: collect all selected option labels
|
|
1222
|
-
const selectedLabels = extendedOutcome.selectedOptionIds
|
|
1223
|
-
.map((id: string) => {
|
|
1224
|
-
const idx = parseInt(id.replace("option_", ""), 10);
|
|
1225
|
-
return question.options?.[idx]?.label;
|
|
1226
|
-
})
|
|
1227
|
-
.filter(Boolean) as string[];
|
|
1228
|
-
allAnswers[question.question] = selectedLabels;
|
|
1229
|
-
} else {
|
|
1230
|
-
// Single select
|
|
1231
|
-
const selectedIdx = parseInt(
|
|
1232
|
-
selectedOptionId.replace("option_", ""),
|
|
1233
|
-
10,
|
|
1234
|
-
);
|
|
1235
|
-
const selectedOption = question.options?.[selectedIdx];
|
|
1236
|
-
allAnswers[question.question] =
|
|
1237
|
-
selectedOption?.label || selectedOptionId;
|
|
1238
|
-
}
|
|
1239
|
-
} else {
|
|
1240
|
-
// User cancelled or did not answer
|
|
1241
|
-
return {
|
|
1242
|
-
behavior: "deny",
|
|
1243
|
-
message: "User did not complete all questions",
|
|
1244
|
-
interrupt: true,
|
|
1245
|
-
};
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
// Return all answers in updatedInput
|
|
1250
|
-
return {
|
|
1251
|
-
behavior: "allow",
|
|
1252
|
-
updatedInput: {
|
|
1253
|
-
...toolInput,
|
|
1254
|
-
answers: allAnswers,
|
|
1255
|
-
},
|
|
1256
|
-
};
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
// In plan mode, deny write/edit tools except for Claude's plan files
|
|
1260
|
-
// This includes both MCP-wrapped tools and built-in SDK tools
|
|
1261
|
-
const WRITE_TOOL_NAMES = [
|
|
1262
|
-
...EDIT_TOOL_NAMES,
|
|
1263
|
-
"Edit",
|
|
1264
|
-
"Write",
|
|
1265
|
-
"NotebookEdit",
|
|
1266
|
-
];
|
|
1267
|
-
if (
|
|
1268
|
-
session.permissionMode === "plan" &&
|
|
1269
|
-
WRITE_TOOL_NAMES.includes(toolName)
|
|
1270
|
-
) {
|
|
1271
|
-
// Allow writes to Claude Code's plan files
|
|
1272
|
-
const filePath = (toolInput as { file_path?: string })?.file_path;
|
|
1273
|
-
const isPlanFile = isClaudePlanFilePath(filePath);
|
|
1274
|
-
|
|
1275
|
-
if (isPlanFile) {
|
|
1276
|
-
session.lastPlanFilePath = filePath;
|
|
1277
|
-
const content = (toolInput as { content?: string })?.content;
|
|
1278
|
-
if (typeof content === "string") {
|
|
1279
|
-
session.lastPlanContent = content;
|
|
1280
|
-
}
|
|
1281
|
-
return {
|
|
1282
|
-
behavior: "allow",
|
|
1283
|
-
updatedInput: toolInput,
|
|
1284
|
-
};
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
const message =
|
|
1288
|
-
"Cannot use write tools in plan mode. Use ExitPlanMode to request permission to make changes.";
|
|
1289
|
-
await emitToolDenial(message);
|
|
1290
|
-
return {
|
|
1291
|
-
behavior: "deny",
|
|
1292
|
-
message,
|
|
1293
|
-
interrupt: false,
|
|
1294
|
-
};
|
|
1295
|
-
}
|
|
1296
|
-
|
|
1297
|
-
// In plan mode, handle Bash separately - allow read-only commands
|
|
1298
|
-
if (
|
|
1299
|
-
session.permissionMode === "plan" &&
|
|
1300
|
-
(toolName === "Bash" || toolName === toolNames.bash)
|
|
1301
|
-
) {
|
|
1302
|
-
const command = (toolInput as { command?: string })?.command ?? "";
|
|
1303
|
-
if (!isReadOnlyBashCommand(command)) {
|
|
1304
|
-
const message =
|
|
1305
|
-
"Cannot run write/modify bash commands in plan mode. Use ExitPlanMode to request permission to make changes.";
|
|
1306
|
-
await emitToolDenial(message);
|
|
1307
|
-
return {
|
|
1308
|
-
behavior: "deny",
|
|
1309
|
-
message,
|
|
1310
|
-
interrupt: false,
|
|
1311
|
-
};
|
|
1312
|
-
}
|
|
1313
|
-
// Read-only bash commands are allowed - fall through to normal permission flow
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
if (
|
|
1317
|
-
session.permissionMode === "bypassPermissions" ||
|
|
1318
|
-
(session.permissionMode === "acceptEdits" &&
|
|
1319
|
-
EDIT_TOOL_NAMES.includes(toolName))
|
|
1320
|
-
) {
|
|
1321
|
-
return {
|
|
1322
|
-
behavior: "allow",
|
|
1323
|
-
updatedInput: toolInput,
|
|
1324
|
-
updatedPermissions: suggestions ?? [
|
|
1325
|
-
{
|
|
1326
|
-
type: "addRules",
|
|
1327
|
-
rules: [{ toolName }],
|
|
1328
|
-
behavior: "allow",
|
|
1329
|
-
destination: "session",
|
|
1330
|
-
},
|
|
1331
|
-
],
|
|
1332
|
-
};
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
const response = await this.client.requestPermission({
|
|
1336
|
-
options: [
|
|
1337
|
-
{
|
|
1338
|
-
kind: "allow_always",
|
|
1339
|
-
name: "Always Allow",
|
|
1340
|
-
optionId: "allow_always",
|
|
1341
|
-
},
|
|
1342
|
-
{ kind: "allow_once", name: "Allow", optionId: "allow" },
|
|
1343
|
-
{ kind: "reject_once", name: "Reject", optionId: "reject" },
|
|
1344
|
-
],
|
|
1345
|
-
sessionId,
|
|
1346
|
-
toolCall: {
|
|
1347
|
-
toolCallId: toolUseID,
|
|
1348
|
-
rawInput: toolInput,
|
|
1349
|
-
title: toolInfoFromToolUse(
|
|
1350
|
-
{ name: toolName, input: toolInput },
|
|
1351
|
-
this.fileContentCache,
|
|
1352
|
-
this.logger,
|
|
1353
|
-
).title,
|
|
1354
|
-
},
|
|
1355
|
-
});
|
|
1356
|
-
if (
|
|
1357
|
-
response.outcome?.outcome === "selected" &&
|
|
1358
|
-
(response.outcome.optionId === "allow" ||
|
|
1359
|
-
response.outcome.optionId === "allow_always")
|
|
1360
|
-
) {
|
|
1361
|
-
// If Claude Code has suggestions, it will update their settings already
|
|
1362
|
-
if (response.outcome.optionId === "allow_always") {
|
|
1363
|
-
return {
|
|
1364
|
-
behavior: "allow",
|
|
1365
|
-
updatedInput: toolInput,
|
|
1366
|
-
updatedPermissions: suggestions ?? [
|
|
1367
|
-
{
|
|
1368
|
-
type: "addRules",
|
|
1369
|
-
rules: [{ toolName }],
|
|
1370
|
-
behavior: "allow",
|
|
1371
|
-
destination: "session",
|
|
1372
|
-
},
|
|
1373
|
-
],
|
|
1374
|
-
};
|
|
1375
|
-
}
|
|
1376
|
-
return {
|
|
1377
|
-
behavior: "allow",
|
|
1378
|
-
updatedInput: toolInput,
|
|
1379
|
-
};
|
|
1380
|
-
} else {
|
|
1381
|
-
const message = "User refused permission to run tool";
|
|
1382
|
-
await emitToolDenial(message);
|
|
1383
|
-
return {
|
|
1384
|
-
behavior: "deny",
|
|
1385
|
-
message,
|
|
1386
|
-
interrupt: true,
|
|
1387
|
-
};
|
|
1388
|
-
}
|
|
1389
|
-
};
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
/**
|
|
1393
|
-
* Handle custom extension methods.
|
|
1394
|
-
* Per ACP spec, extension methods start with underscore.
|
|
1395
|
-
*/
|
|
1396
|
-
async extMethod(
|
|
1397
|
-
method: string,
|
|
1398
|
-
params: Record<string, unknown>,
|
|
1399
|
-
): Promise<Record<string, unknown>> {
|
|
1400
|
-
if (method === "_posthog/session/resume") {
|
|
1401
|
-
await this.resumeSession(params as unknown as LoadSessionRequest);
|
|
1402
|
-
return {};
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
if (method === "session/setModel") {
|
|
1406
|
-
const { sessionId, modelId } = params as {
|
|
1407
|
-
sessionId: string;
|
|
1408
|
-
modelId: string;
|
|
1409
|
-
};
|
|
1410
|
-
await this.setSessionModel({ sessionId, modelId });
|
|
1411
|
-
return {};
|
|
1412
|
-
}
|
|
1413
|
-
|
|
1414
|
-
if (method === "session/setMode") {
|
|
1415
|
-
const { sessionId, modeId } = params as {
|
|
1416
|
-
sessionId: string;
|
|
1417
|
-
modeId: string;
|
|
1418
|
-
};
|
|
1419
|
-
await this.setSessionMode({ sessionId, modeId });
|
|
1420
|
-
return {};
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
throw RequestError.methodNotFound(method);
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
/**
|
|
1427
|
-
* Resume a session without replaying history.
|
|
1428
|
-
* Client is responsible for fetching and rendering history from S3.
|
|
1429
|
-
* This basically implemetns the ACP session/resume RFD:
|
|
1430
|
-
* https://agentclientprotocol.com/rfds/session-resume
|
|
1431
|
-
*/
|
|
1432
|
-
async resumeSession(
|
|
1433
|
-
params: LoadSessionRequest,
|
|
1434
|
-
): Promise<LoadSessionResponse> {
|
|
1435
|
-
this.logger.info("[RESUME] Resuming session", { params });
|
|
1436
|
-
const { sessionId } = params;
|
|
1437
|
-
|
|
1438
|
-
// Extract persistence config and SDK session ID from _meta
|
|
1439
|
-
const persistence = params._meta?.persistence as
|
|
1440
|
-
| SessionPersistenceConfig
|
|
1441
|
-
| undefined;
|
|
1442
|
-
const sdkSessionId = params._meta?.sdkSessionId as string | undefined;
|
|
1443
|
-
|
|
1444
|
-
if (!this.sessions[sessionId]) {
|
|
1445
|
-
const input = new Pushable<SDKUserMessage>();
|
|
1446
|
-
|
|
1447
|
-
const mcpServers: Record<string, McpServerConfig> = {};
|
|
1448
|
-
if (Array.isArray(params.mcpServers)) {
|
|
1449
|
-
for (const server of params.mcpServers) {
|
|
1450
|
-
if ("type" in server) {
|
|
1451
|
-
mcpServers[server.name] = {
|
|
1452
|
-
type: server.type,
|
|
1453
|
-
url: server.url,
|
|
1454
|
-
headers: server.headers
|
|
1455
|
-
? Object.fromEntries(
|
|
1456
|
-
server.headers.map((e) => [e.name, e.value]),
|
|
1457
|
-
)
|
|
1458
|
-
: undefined,
|
|
1459
|
-
};
|
|
1460
|
-
} else {
|
|
1461
|
-
mcpServers[server.name] = {
|
|
1462
|
-
type: "stdio",
|
|
1463
|
-
command: server.command,
|
|
1464
|
-
args: server.args,
|
|
1465
|
-
env: server.env
|
|
1466
|
-
? Object.fromEntries(server.env.map((e) => [e.name, e.value]))
|
|
1467
|
-
: undefined,
|
|
1468
|
-
};
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
const server = createMcpServer(this, sessionId, this.clientCapabilities);
|
|
1474
|
-
mcpServers.acp = {
|
|
1475
|
-
type: "sdk",
|
|
1476
|
-
name: "acp",
|
|
1477
|
-
instance: server,
|
|
1478
|
-
};
|
|
1479
|
-
|
|
1480
|
-
const permissionMode = "default";
|
|
1481
|
-
|
|
1482
|
-
this.logger.info("Resuming session", {
|
|
1483
|
-
cwd: params.cwd,
|
|
1484
|
-
sdkSessionId,
|
|
1485
|
-
persistence,
|
|
1486
|
-
});
|
|
1487
|
-
const options: Options = {
|
|
1488
|
-
cwd: params.cwd,
|
|
1489
|
-
includePartialMessages: true,
|
|
1490
|
-
mcpServers,
|
|
1491
|
-
systemPrompt: { type: "preset", preset: "claude_code" },
|
|
1492
|
-
settingSources: ["user", "project", "local"],
|
|
1493
|
-
allowDangerouslySkipPermissions: !IS_ROOT,
|
|
1494
|
-
permissionMode,
|
|
1495
|
-
canUseTool: this.canUseTool(sessionId),
|
|
1496
|
-
stderr: (err) => this.logger.error(err),
|
|
1497
|
-
// Use "node" to resolve via PATH where a symlink to Electron exists.
|
|
1498
|
-
// This avoids launching the Electron binary directly from the app bundle,
|
|
1499
|
-
// which can cause dock icons to appear on macOS even with ELECTRON_RUN_AS_NODE.
|
|
1500
|
-
executable: "node",
|
|
1501
|
-
// Prevent spawned Electron processes from showing in dock/tray.
|
|
1502
|
-
// Must merge with process.env since SDK replaces rather than merges.
|
|
1503
|
-
env: { ...process.env, ELECTRON_RUN_AS_NODE: "1" },
|
|
1504
|
-
...(process.env.CLAUDE_CODE_EXECUTABLE && {
|
|
1505
|
-
pathToClaudeCodeExecutable: process.env.CLAUDE_CODE_EXECUTABLE,
|
|
1506
|
-
}),
|
|
1507
|
-
// Resume from SDK session if available
|
|
1508
|
-
...(sdkSessionId && { resume: sdkSessionId }),
|
|
1509
|
-
hooks: {
|
|
1510
|
-
PostToolUse: [
|
|
1511
|
-
{
|
|
1512
|
-
hooks: [createPostToolUseHook(this.logger)],
|
|
1513
|
-
},
|
|
1514
|
-
],
|
|
1515
|
-
},
|
|
1516
|
-
};
|
|
1517
|
-
|
|
1518
|
-
// Clear statsig cache before creating query to avoid input_examples bug
|
|
1519
|
-
clearStatsigCache();
|
|
1520
|
-
|
|
1521
|
-
const q = query({
|
|
1522
|
-
prompt: input,
|
|
1523
|
-
options,
|
|
1524
|
-
});
|
|
1525
|
-
|
|
1526
|
-
const availableCommands = await getAvailableSlashCommands(q);
|
|
1527
|
-
|
|
1528
|
-
const newSession = this.createSession(
|
|
1529
|
-
sessionId,
|
|
1530
|
-
q,
|
|
1531
|
-
input,
|
|
1532
|
-
permissionMode,
|
|
1533
|
-
);
|
|
1534
|
-
|
|
1535
|
-
// Store SDK session ID if resuming
|
|
1536
|
-
if (sdkSessionId) {
|
|
1537
|
-
newSession.sdkSessionId = sdkSessionId;
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
// Register for future persistence
|
|
1541
|
-
if (persistence && this.sessionStore) {
|
|
1542
|
-
this.sessionStore.register(sessionId, persistence);
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
setTimeout(() => {
|
|
1546
|
-
this.client.sessionUpdate({
|
|
1547
|
-
sessionId,
|
|
1548
|
-
update: {
|
|
1549
|
-
sessionUpdate: "available_commands_update",
|
|
1550
|
-
availableCommands,
|
|
1551
|
-
},
|
|
1552
|
-
});
|
|
1553
|
-
}, 0);
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
return {};
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
async function getAvailableModels(query: Query): Promise<SessionModelState> {
|
|
1561
|
-
const models = await query.supportedModels();
|
|
1562
|
-
|
|
1563
|
-
// Query doesn't give us access to the currently selected model, so we just choose the first model in the list.
|
|
1564
|
-
const currentModel = models[0];
|
|
1565
|
-
await query.setModel(currentModel.value);
|
|
1566
|
-
|
|
1567
|
-
const availableModels = models.map((model) => ({
|
|
1568
|
-
modelId: model.value,
|
|
1569
|
-
name: model.displayName,
|
|
1570
|
-
description: model.description,
|
|
1571
|
-
}));
|
|
1572
|
-
|
|
1573
|
-
return {
|
|
1574
|
-
availableModels,
|
|
1575
|
-
currentModelId: currentModel.value,
|
|
1576
|
-
};
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
async function getAvailableSlashCommands(
|
|
1580
|
-
query: Query,
|
|
1581
|
-
): Promise<AvailableCommand[]> {
|
|
1582
|
-
const UNSUPPORTED_COMMANDS = [
|
|
1583
|
-
"context",
|
|
1584
|
-
"cost",
|
|
1585
|
-
"login",
|
|
1586
|
-
"logout",
|
|
1587
|
-
"output-style:new",
|
|
1588
|
-
"release-notes",
|
|
1589
|
-
"todos",
|
|
1590
|
-
];
|
|
1591
|
-
const commands = await query.supportedCommands();
|
|
1592
|
-
|
|
1593
|
-
return commands
|
|
1594
|
-
.map((command) => {
|
|
1595
|
-
const input = command.argumentHint
|
|
1596
|
-
? { hint: command.argumentHint }
|
|
1597
|
-
: null;
|
|
1598
|
-
let name = command.name;
|
|
1599
|
-
if (command.name.endsWith(" (MCP)")) {
|
|
1600
|
-
name = `mcp:${name.replace(" (MCP)", "")}`;
|
|
1601
|
-
}
|
|
1602
|
-
return {
|
|
1603
|
-
name,
|
|
1604
|
-
description: command.description || "",
|
|
1605
|
-
input,
|
|
1606
|
-
};
|
|
1607
|
-
})
|
|
1608
|
-
.filter(
|
|
1609
|
-
(command: AvailableCommand) =>
|
|
1610
|
-
!UNSUPPORTED_COMMANDS.includes(command.name),
|
|
1611
|
-
);
|
|
1612
|
-
}
|
|
1613
|
-
|
|
1614
|
-
function formatUriAsLink(uri: string): string {
|
|
1615
|
-
try {
|
|
1616
|
-
if (uri.startsWith("file://")) {
|
|
1617
|
-
const path = uri.slice(7); // Remove "file://"
|
|
1618
|
-
const name = path.split("/").pop() || path;
|
|
1619
|
-
return `[@${name}](${uri})`;
|
|
1620
|
-
} else if (uri.startsWith("zed://")) {
|
|
1621
|
-
const parts = uri.split("/");
|
|
1622
|
-
const name = parts[parts.length - 1] || uri;
|
|
1623
|
-
return `[@${name}](${uri})`;
|
|
1624
|
-
}
|
|
1625
|
-
return uri;
|
|
1626
|
-
} catch {
|
|
1627
|
-
return uri;
|
|
1628
|
-
}
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
export function promptToClaude(prompt: PromptRequest): SDKUserMessage {
|
|
1632
|
-
const content: ContentBlockParam[] = [];
|
|
1633
|
-
const context: ContentBlockParam[] = [];
|
|
1634
|
-
|
|
1635
|
-
for (const chunk of prompt.prompt) {
|
|
1636
|
-
switch (chunk.type) {
|
|
1637
|
-
case "text": {
|
|
1638
|
-
let text = chunk.text;
|
|
1639
|
-
// change /mcp:server:command args -> /server:command (MCP) args
|
|
1640
|
-
const mcpMatch = text.match(/^\/mcp:([^:\s]+):(\S+)(\s+.*)?$/);
|
|
1641
|
-
if (mcpMatch) {
|
|
1642
|
-
const [, server, command, args] = mcpMatch;
|
|
1643
|
-
text = `/${server}:${command} (MCP)${args || ""}`;
|
|
1644
|
-
}
|
|
1645
|
-
content.push({ type: "text", text });
|
|
1646
|
-
break;
|
|
1647
|
-
}
|
|
1648
|
-
case "resource_link": {
|
|
1649
|
-
const formattedUri = formatUriAsLink(chunk.uri);
|
|
1650
|
-
content.push({
|
|
1651
|
-
type: "text",
|
|
1652
|
-
text: formattedUri,
|
|
1653
|
-
});
|
|
1654
|
-
break;
|
|
1655
|
-
}
|
|
1656
|
-
case "resource": {
|
|
1657
|
-
if ("text" in chunk.resource) {
|
|
1658
|
-
const formattedUri = formatUriAsLink(chunk.resource.uri);
|
|
1659
|
-
content.push({
|
|
1660
|
-
type: "text",
|
|
1661
|
-
text: formattedUri,
|
|
1662
|
-
});
|
|
1663
|
-
context.push({
|
|
1664
|
-
type: "text",
|
|
1665
|
-
text: `\n<context ref="${chunk.resource.uri}">\n${chunk.resource.text}\n</context>`,
|
|
1666
|
-
});
|
|
1667
|
-
}
|
|
1668
|
-
// Ignore blob resources (unsupported)
|
|
1669
|
-
break;
|
|
1670
|
-
}
|
|
1671
|
-
case "image":
|
|
1672
|
-
if (chunk.data) {
|
|
1673
|
-
content.push({
|
|
1674
|
-
type: "image",
|
|
1675
|
-
source: {
|
|
1676
|
-
type: "base64",
|
|
1677
|
-
data: chunk.data,
|
|
1678
|
-
media_type: chunk.mimeType as
|
|
1679
|
-
| "image/jpeg"
|
|
1680
|
-
| "image/png"
|
|
1681
|
-
| "image/gif"
|
|
1682
|
-
| "image/webp",
|
|
1683
|
-
},
|
|
1684
|
-
});
|
|
1685
|
-
} else if (chunk.uri?.startsWith("http")) {
|
|
1686
|
-
content.push({
|
|
1687
|
-
type: "image",
|
|
1688
|
-
source: {
|
|
1689
|
-
type: "url",
|
|
1690
|
-
url: chunk.uri,
|
|
1691
|
-
},
|
|
1692
|
-
});
|
|
1693
|
-
}
|
|
1694
|
-
break;
|
|
1695
|
-
// Ignore audio and other unsupported types
|
|
1696
|
-
default:
|
|
1697
|
-
break;
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
content.push(...context);
|
|
1702
|
-
|
|
1703
|
-
return {
|
|
1704
|
-
type: "user",
|
|
1705
|
-
message: {
|
|
1706
|
-
role: "user",
|
|
1707
|
-
content: content,
|
|
1708
|
-
},
|
|
1709
|
-
session_id: prompt.sessionId,
|
|
1710
|
-
parent_tool_use_id: null,
|
|
1711
|
-
};
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
/**
|
|
1715
|
-
* Convert an SDKAssistantMessage (Claude) to a SessionNotification (ACP).
|
|
1716
|
-
* Only handles text, image, and thinking chunks for now.
|
|
1717
|
-
*/
|
|
1718
|
-
export function toAcpNotifications(
|
|
1719
|
-
content:
|
|
1720
|
-
| string
|
|
1721
|
-
| ContentBlockParam[]
|
|
1722
|
-
| BetaContentBlock[]
|
|
1723
|
-
| BetaRawContentBlockDelta[],
|
|
1724
|
-
role: "assistant" | "user",
|
|
1725
|
-
sessionId: string,
|
|
1726
|
-
toolUseCache: ToolUseCache,
|
|
1727
|
-
fileContentCache: { [key: string]: string },
|
|
1728
|
-
client: AgentSideConnection,
|
|
1729
|
-
logger: Logger,
|
|
1730
|
-
): SessionNotification[] {
|
|
1731
|
-
if (typeof content === "string") {
|
|
1732
|
-
return [
|
|
1733
|
-
{
|
|
1734
|
-
sessionId,
|
|
1735
|
-
update: {
|
|
1736
|
-
sessionUpdate:
|
|
1737
|
-
role === "assistant" ? "agent_message_chunk" : "user_message_chunk",
|
|
1738
|
-
content: {
|
|
1739
|
-
type: "text",
|
|
1740
|
-
text: content,
|
|
1741
|
-
},
|
|
1742
|
-
},
|
|
1743
|
-
},
|
|
1744
|
-
];
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
const output = [];
|
|
1748
|
-
// Only handle the first chunk for streaming; extend as needed for batching
|
|
1749
|
-
for (const chunk of content) {
|
|
1750
|
-
let update: SessionNotification["update"] | null = null;
|
|
1751
|
-
switch (chunk.type) {
|
|
1752
|
-
case "text":
|
|
1753
|
-
case "text_delta":
|
|
1754
|
-
update = {
|
|
1755
|
-
sessionUpdate:
|
|
1756
|
-
role === "assistant" ? "agent_message_chunk" : "user_message_chunk",
|
|
1757
|
-
content: {
|
|
1758
|
-
type: "text",
|
|
1759
|
-
text: chunk.text,
|
|
1760
|
-
},
|
|
1761
|
-
};
|
|
1762
|
-
break;
|
|
1763
|
-
case "image":
|
|
1764
|
-
update = {
|
|
1765
|
-
sessionUpdate:
|
|
1766
|
-
role === "assistant" ? "agent_message_chunk" : "user_message_chunk",
|
|
1767
|
-
content: {
|
|
1768
|
-
type: "image",
|
|
1769
|
-
data: chunk.source.type === "base64" ? chunk.source.data : "",
|
|
1770
|
-
mimeType:
|
|
1771
|
-
chunk.source.type === "base64" ? chunk.source.media_type : "",
|
|
1772
|
-
uri: chunk.source.type === "url" ? chunk.source.url : undefined,
|
|
1773
|
-
},
|
|
1774
|
-
};
|
|
1775
|
-
break;
|
|
1776
|
-
case "thinking":
|
|
1777
|
-
case "thinking_delta":
|
|
1778
|
-
update = {
|
|
1779
|
-
sessionUpdate: "agent_thought_chunk",
|
|
1780
|
-
content: {
|
|
1781
|
-
type: "text",
|
|
1782
|
-
text: chunk.thinking,
|
|
1783
|
-
},
|
|
1784
|
-
};
|
|
1785
|
-
break;
|
|
1786
|
-
case "tool_use":
|
|
1787
|
-
case "server_tool_use":
|
|
1788
|
-
case "mcp_tool_use": {
|
|
1789
|
-
toolUseCache[chunk.id] = chunk;
|
|
1790
|
-
if (chunk.name === "TodoWrite") {
|
|
1791
|
-
// @ts-expect-error - sometimes input is empty object
|
|
1792
|
-
if (Array.isArray(chunk.input.todos)) {
|
|
1793
|
-
update = {
|
|
1794
|
-
sessionUpdate: "plan",
|
|
1795
|
-
entries: planEntries(chunk.input as { todos: ClaudePlanEntry[] }),
|
|
1796
|
-
};
|
|
1797
|
-
}
|
|
1798
|
-
} else {
|
|
1799
|
-
// Register hook callback to receive the structured output from the hook
|
|
1800
|
-
registerHookCallback(chunk.id, {
|
|
1801
|
-
onPostToolUseHook: async (toolUseId, _toolInput, toolResponse) => {
|
|
1802
|
-
const toolUse = toolUseCache[toolUseId];
|
|
1803
|
-
if (toolUse) {
|
|
1804
|
-
const update: SessionNotification["update"] = {
|
|
1805
|
-
_meta: {
|
|
1806
|
-
claudeCode: {
|
|
1807
|
-
toolResponse,
|
|
1808
|
-
toolName: toolUse.name,
|
|
1809
|
-
},
|
|
1810
|
-
} satisfies ToolUpdateMeta,
|
|
1811
|
-
toolCallId: toolUseId,
|
|
1812
|
-
sessionUpdate: "tool_call_update",
|
|
1813
|
-
};
|
|
1814
|
-
await client.sessionUpdate({
|
|
1815
|
-
sessionId,
|
|
1816
|
-
update,
|
|
1817
|
-
});
|
|
1818
|
-
} else {
|
|
1819
|
-
logger.error(
|
|
1820
|
-
`[claude-code-acp] Got a tool response for tool use that wasn't tracked: ${toolUseId}`,
|
|
1821
|
-
);
|
|
1822
|
-
}
|
|
1823
|
-
},
|
|
1824
|
-
});
|
|
1825
|
-
|
|
1826
|
-
let rawInput: Record<string, unknown> | undefined;
|
|
1827
|
-
try {
|
|
1828
|
-
rawInput = JSON.parse(JSON.stringify(chunk.input));
|
|
1829
|
-
} catch {
|
|
1830
|
-
// ignore if we can't turn it to JSON
|
|
1831
|
-
}
|
|
1832
|
-
update = {
|
|
1833
|
-
_meta: {
|
|
1834
|
-
claudeCode: {
|
|
1835
|
-
toolName: chunk.name,
|
|
1836
|
-
},
|
|
1837
|
-
} satisfies ToolUpdateMeta,
|
|
1838
|
-
toolCallId: chunk.id,
|
|
1839
|
-
sessionUpdate: "tool_call",
|
|
1840
|
-
rawInput,
|
|
1841
|
-
status: "pending",
|
|
1842
|
-
...toolInfoFromToolUse(chunk, fileContentCache, logger),
|
|
1843
|
-
};
|
|
1844
|
-
}
|
|
1845
|
-
break;
|
|
1846
|
-
}
|
|
1847
|
-
|
|
1848
|
-
case "tool_result":
|
|
1849
|
-
case "tool_search_tool_result":
|
|
1850
|
-
case "web_fetch_tool_result":
|
|
1851
|
-
case "web_search_tool_result":
|
|
1852
|
-
case "code_execution_tool_result":
|
|
1853
|
-
case "bash_code_execution_tool_result":
|
|
1854
|
-
case "text_editor_code_execution_tool_result":
|
|
1855
|
-
case "mcp_tool_result": {
|
|
1856
|
-
const toolUse = toolUseCache[chunk.tool_use_id];
|
|
1857
|
-
if (!toolUse) {
|
|
1858
|
-
logger.error(
|
|
1859
|
-
`[claude-code-acp] Got a tool result for tool use that wasn't tracked: ${chunk.tool_use_id}`,
|
|
1860
|
-
);
|
|
1861
|
-
break;
|
|
1862
|
-
}
|
|
1863
|
-
|
|
1864
|
-
if (toolUse.name !== "TodoWrite") {
|
|
1865
|
-
update = {
|
|
1866
|
-
_meta: {
|
|
1867
|
-
claudeCode: {
|
|
1868
|
-
toolName: toolUse.name,
|
|
1869
|
-
},
|
|
1870
|
-
} satisfies ToolUpdateMeta,
|
|
1871
|
-
toolCallId: chunk.tool_use_id,
|
|
1872
|
-
sessionUpdate: "tool_call_update",
|
|
1873
|
-
status:
|
|
1874
|
-
"is_error" in chunk && chunk.is_error ? "failed" : "completed",
|
|
1875
|
-
...toolUpdateFromToolResult(chunk, toolUseCache[chunk.tool_use_id]),
|
|
1876
|
-
};
|
|
1877
|
-
}
|
|
1878
|
-
break;
|
|
1879
|
-
}
|
|
1880
|
-
|
|
1881
|
-
case "document":
|
|
1882
|
-
case "search_result":
|
|
1883
|
-
case "redacted_thinking":
|
|
1884
|
-
case "input_json_delta":
|
|
1885
|
-
case "citations_delta":
|
|
1886
|
-
case "signature_delta":
|
|
1887
|
-
case "container_upload":
|
|
1888
|
-
break;
|
|
1889
|
-
|
|
1890
|
-
default:
|
|
1891
|
-
unreachable(chunk, logger);
|
|
1892
|
-
break;
|
|
1893
|
-
}
|
|
1894
|
-
if (update) {
|
|
1895
|
-
output.push({ sessionId, update });
|
|
1896
|
-
}
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
return output;
|
|
1900
|
-
}
|
|
1901
|
-
|
|
1902
|
-
export function streamEventToAcpNotifications(
|
|
1903
|
-
message: SDKPartialAssistantMessage,
|
|
1904
|
-
sessionId: string,
|
|
1905
|
-
toolUseCache: ToolUseCache,
|
|
1906
|
-
fileContentCache: { [key: string]: string },
|
|
1907
|
-
client: AgentSideConnection,
|
|
1908
|
-
logger: Logger,
|
|
1909
|
-
): SessionNotification[] {
|
|
1910
|
-
const event = message.event;
|
|
1911
|
-
switch (event.type) {
|
|
1912
|
-
case "content_block_start":
|
|
1913
|
-
return toAcpNotifications(
|
|
1914
|
-
[event.content_block],
|
|
1915
|
-
"assistant",
|
|
1916
|
-
sessionId,
|
|
1917
|
-
toolUseCache,
|
|
1918
|
-
fileContentCache,
|
|
1919
|
-
client,
|
|
1920
|
-
logger,
|
|
1921
|
-
);
|
|
1922
|
-
case "content_block_delta":
|
|
1923
|
-
return toAcpNotifications(
|
|
1924
|
-
[event.delta],
|
|
1925
|
-
"assistant",
|
|
1926
|
-
sessionId,
|
|
1927
|
-
toolUseCache,
|
|
1928
|
-
fileContentCache,
|
|
1929
|
-
client,
|
|
1930
|
-
logger,
|
|
1931
|
-
);
|
|
1932
|
-
// No content
|
|
1933
|
-
case "message_start":
|
|
1934
|
-
case "message_delta":
|
|
1935
|
-
case "message_stop":
|
|
1936
|
-
case "content_block_stop":
|
|
1937
|
-
return [];
|
|
1938
|
-
|
|
1939
|
-
default:
|
|
1940
|
-
unreachable(event, logger);
|
|
1941
|
-
return [];
|
|
1942
|
-
}
|
|
1943
|
-
}
|
|
1944
|
-
|
|
1945
|
-
// Note: createAcpConnection has been moved to ../connection.ts
|
|
1946
|
-
// Import from there instead:
|
|
1947
|
-
// import { createAcpConnection } from "../connection.js";
|