claude-code-rust 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -4
- package/agent-sdk/README.md +0 -1
- package/agent-sdk/dist/bridge/agents.js +75 -0
- package/agent-sdk/dist/bridge/commands.js +42 -11
- package/agent-sdk/dist/bridge/error_classification.js +55 -0
- package/agent-sdk/dist/bridge/events.js +83 -0
- package/agent-sdk/dist/bridge/history.js +0 -17
- package/agent-sdk/dist/bridge/message_handlers.js +428 -0
- package/agent-sdk/dist/bridge/permissions.js +15 -3
- package/agent-sdk/dist/bridge/session_lifecycle.js +368 -0
- package/agent-sdk/dist/bridge/shared.js +49 -0
- package/agent-sdk/dist/bridge/state_parsing.js +66 -0
- package/agent-sdk/dist/bridge/tool_calls.js +168 -0
- package/agent-sdk/dist/bridge/user_interaction.js +175 -0
- package/agent-sdk/dist/bridge.js +21 -1323
- package/agent-sdk/dist/bridge.test.js +109 -51
- package/package.json +2 -2
- package/scripts/postinstall.js +2 -2
- package/agent-sdk/dist/bridge/usage.js +0 -95
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { spawn as spawnChild } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import { query, } from "@anthropic-ai/claude-agent-sdk";
|
|
5
|
+
import { AsyncQueue, logPermissionDebug } from "./shared.js";
|
|
6
|
+
import { formatPermissionUpdates, permissionOptionsFromSuggestions, permissionResultFromOutcome, } from "./permissions.js";
|
|
7
|
+
import { writeEvent, failConnection, emitSessionUpdate, emitConnectEvent, } from "./events.js";
|
|
8
|
+
import { ensureToolCallVisible, setToolCallStatus, } from "./tool_calls.js";
|
|
9
|
+
import { requestExitPlanModeApproval, requestAskUserQuestionAnswers, EXIT_PLAN_MODE_TOOL_NAME, ASK_USER_QUESTION_TOOL_NAME, } from "./user_interaction.js";
|
|
10
|
+
import { mapAvailableAgents, emitAvailableAgentsIfChanged, refreshAvailableAgents } from "./agents.js";
|
|
11
|
+
import { emitAuthRequired, emitFastModeUpdateIfChanged } from "./error_classification.js";
|
|
12
|
+
export const sessions = new Map();
|
|
13
|
+
const DEFAULT_SETTING_SOURCES = ["user", "project", "local"];
|
|
14
|
+
const DEFAULT_MODEL_NAME = "default";
|
|
15
|
+
const DEFAULT_PERMISSION_MODE = "default";
|
|
16
|
+
export function sessionById(sessionId) {
|
|
17
|
+
return sessions.get(sessionId) ?? null;
|
|
18
|
+
}
|
|
19
|
+
export function updateSessionId(session, newSessionId) {
|
|
20
|
+
if (session.sessionId === newSessionId) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
sessions.delete(session.sessionId);
|
|
24
|
+
session.sessionId = newSessionId;
|
|
25
|
+
sessions.set(newSessionId, session);
|
|
26
|
+
}
|
|
27
|
+
export async function closeSession(session) {
|
|
28
|
+
session.input.close();
|
|
29
|
+
session.query.close();
|
|
30
|
+
for (const pending of session.pendingPermissions.values()) {
|
|
31
|
+
pending.resolve?.({ behavior: "deny", message: "Session closed" });
|
|
32
|
+
pending.onOutcome?.({ outcome: "cancelled" });
|
|
33
|
+
}
|
|
34
|
+
session.pendingPermissions.clear();
|
|
35
|
+
}
|
|
36
|
+
export async function closeAllSessions() {
|
|
37
|
+
const active = Array.from(sessions.values());
|
|
38
|
+
sessions.clear();
|
|
39
|
+
await Promise.all(active.map((session) => closeSession(session)));
|
|
40
|
+
}
|
|
41
|
+
export async function createSession(params) {
|
|
42
|
+
const input = new AsyncQueue();
|
|
43
|
+
const provisionalSessionId = params.resume ?? randomUUID();
|
|
44
|
+
const initialModel = initialSessionModel(params.launchSettings);
|
|
45
|
+
const initialMode = initialSessionMode(params.launchSettings);
|
|
46
|
+
let session;
|
|
47
|
+
const canUseTool = async (toolName, inputData, options) => {
|
|
48
|
+
const toolUseId = options.toolUseID;
|
|
49
|
+
if (toolName === EXIT_PLAN_MODE_TOOL_NAME) {
|
|
50
|
+
const existing = ensureToolCallVisible(session, toolUseId, toolName, inputData);
|
|
51
|
+
return await requestExitPlanModeApproval(session, toolUseId, inputData, existing);
|
|
52
|
+
}
|
|
53
|
+
logPermissionDebug(`request tool_use_id=${toolUseId} tool=${toolName} blocked_path=${options.blockedPath ?? "<none>"} ` +
|
|
54
|
+
`decision_reason=${options.decisionReason ?? "<none>"} suggestions=${formatPermissionUpdates(options.suggestions)}`);
|
|
55
|
+
const existing = ensureToolCallVisible(session, toolUseId, toolName, inputData);
|
|
56
|
+
if (toolName === ASK_USER_QUESTION_TOOL_NAME) {
|
|
57
|
+
return await requestAskUserQuestionAnswers(session, toolUseId, toolName, inputData, existing);
|
|
58
|
+
}
|
|
59
|
+
const request = {
|
|
60
|
+
tool_call: existing,
|
|
61
|
+
options: permissionOptionsFromSuggestions(options.suggestions),
|
|
62
|
+
};
|
|
63
|
+
writeEvent({ event: "permission_request", session_id: session.sessionId, request });
|
|
64
|
+
return await new Promise((resolve) => {
|
|
65
|
+
session.pendingPermissions.set(toolUseId, {
|
|
66
|
+
resolve,
|
|
67
|
+
toolName,
|
|
68
|
+
inputData: inputData,
|
|
69
|
+
suggestions: options.suggestions,
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
const claudeCodeExecutable = process.env.CLAUDE_CODE_EXECUTABLE;
|
|
74
|
+
const sdkDebugFile = process.env.CLAUDE_RS_SDK_DEBUG_FILE;
|
|
75
|
+
const enableSdkDebug = process.env.CLAUDE_RS_SDK_DEBUG === "1" || Boolean(sdkDebugFile);
|
|
76
|
+
const enableSpawnDebug = process.env.CLAUDE_RS_SDK_SPAWN_DEBUG === "1";
|
|
77
|
+
if (claudeCodeExecutable && !fs.existsSync(claudeCodeExecutable)) {
|
|
78
|
+
throw new Error(`CLAUDE_CODE_EXECUTABLE does not exist: ${claudeCodeExecutable}`);
|
|
79
|
+
}
|
|
80
|
+
let queryHandle;
|
|
81
|
+
try {
|
|
82
|
+
queryHandle = query({
|
|
83
|
+
prompt: input,
|
|
84
|
+
options: buildQueryOptions({
|
|
85
|
+
cwd: params.cwd,
|
|
86
|
+
resume: params.resume,
|
|
87
|
+
launchSettings: params.launchSettings,
|
|
88
|
+
provisionalSessionId,
|
|
89
|
+
input,
|
|
90
|
+
canUseTool,
|
|
91
|
+
claudeCodeExecutable,
|
|
92
|
+
sdkDebugFile,
|
|
93
|
+
enableSdkDebug,
|
|
94
|
+
enableSpawnDebug,
|
|
95
|
+
sessionIdForLogs: () => session.sessionId,
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
101
|
+
throw new Error(`query() failed: node_executable=${process.execPath}; cwd=${params.cwd}; ` +
|
|
102
|
+
`resume=${params.resume ?? "<none>"}; ` +
|
|
103
|
+
`CLAUDE_CODE_EXECUTABLE=${claudeCodeExecutable ?? "<unset>"}; error=${message}`);
|
|
104
|
+
}
|
|
105
|
+
session = {
|
|
106
|
+
sessionId: provisionalSessionId,
|
|
107
|
+
cwd: params.cwd,
|
|
108
|
+
model: initialModel,
|
|
109
|
+
availableModels: [],
|
|
110
|
+
mode: initialMode,
|
|
111
|
+
fastModeState: "off",
|
|
112
|
+
query: queryHandle,
|
|
113
|
+
input,
|
|
114
|
+
connected: false,
|
|
115
|
+
connectEvent: params.connectEvent,
|
|
116
|
+
connectRequestId: params.requestId,
|
|
117
|
+
toolCalls: new Map(),
|
|
118
|
+
taskToolUseIds: new Map(),
|
|
119
|
+
pendingPermissions: new Map(),
|
|
120
|
+
authHintSent: false,
|
|
121
|
+
...(params.resumeUpdates && params.resumeUpdates.length > 0
|
|
122
|
+
? { resumeUpdates: params.resumeUpdates }
|
|
123
|
+
: {}),
|
|
124
|
+
...(params.sessionsToCloseAfterConnect
|
|
125
|
+
? { sessionsToCloseAfterConnect: params.sessionsToCloseAfterConnect }
|
|
126
|
+
: {}),
|
|
127
|
+
};
|
|
128
|
+
sessions.set(provisionalSessionId, session);
|
|
129
|
+
// In stream-input mode the SDK may defer init until input arrives.
|
|
130
|
+
// Trigger initialization explicitly so the Rust UI can receive `connected`
|
|
131
|
+
// before the first user prompt.
|
|
132
|
+
void session.query
|
|
133
|
+
.initializationResult()
|
|
134
|
+
.then((result) => {
|
|
135
|
+
session.availableModels = mapAvailableModels(result.models);
|
|
136
|
+
if (!session.connected) {
|
|
137
|
+
emitConnectEvent(session);
|
|
138
|
+
}
|
|
139
|
+
// Proactively detect missing auth from account info so the UI can
|
|
140
|
+
// show the login hint immediately, without waiting for the first prompt.
|
|
141
|
+
const acct = result.account;
|
|
142
|
+
const hasCredentials = (typeof acct.email === "string" && acct.email.trim().length > 0) ||
|
|
143
|
+
(typeof acct.apiKeySource === "string" && acct.apiKeySource.trim().length > 0);
|
|
144
|
+
if (!hasCredentials) {
|
|
145
|
+
emitAuthRequired(session);
|
|
146
|
+
}
|
|
147
|
+
emitFastModeUpdateIfChanged(session, result.fast_mode_state);
|
|
148
|
+
const commands = Array.isArray(result.commands)
|
|
149
|
+
? result.commands.map((command) => ({
|
|
150
|
+
name: command.name,
|
|
151
|
+
description: command.description ?? "",
|
|
152
|
+
input_hint: command.argumentHint ?? undefined,
|
|
153
|
+
}))
|
|
154
|
+
: [];
|
|
155
|
+
if (commands.length > 0) {
|
|
156
|
+
emitSessionUpdate(session.sessionId, { type: "available_commands_update", commands });
|
|
157
|
+
}
|
|
158
|
+
emitAvailableAgentsIfChanged(session, mapAvailableAgents(result.agents));
|
|
159
|
+
refreshAvailableAgents(session);
|
|
160
|
+
})
|
|
161
|
+
.catch((error) => {
|
|
162
|
+
if (session.connected) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
166
|
+
failConnection(`agent initialization failed: ${message}`, session.connectRequestId);
|
|
167
|
+
session.connectRequestId = undefined;
|
|
168
|
+
});
|
|
169
|
+
void (async () => {
|
|
170
|
+
try {
|
|
171
|
+
for await (const message of session.query) {
|
|
172
|
+
// Lazy import to break circular dependency at module-evaluation time.
|
|
173
|
+
const { handleSdkMessage } = await import("./message_handlers.js");
|
|
174
|
+
handleSdkMessage(session, message);
|
|
175
|
+
}
|
|
176
|
+
if (!session.connected) {
|
|
177
|
+
failConnection("agent stream ended before session initialization", params.requestId);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
182
|
+
failConnection(`agent stream failed: ${message}`, params.requestId);
|
|
183
|
+
}
|
|
184
|
+
})();
|
|
185
|
+
}
|
|
186
|
+
function permissionModeFromLaunchSettings(rawMode) {
|
|
187
|
+
if (rawMode === undefined) {
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
switch (rawMode) {
|
|
191
|
+
case "default":
|
|
192
|
+
case "acceptEdits":
|
|
193
|
+
case "bypassPermissions":
|
|
194
|
+
case "plan":
|
|
195
|
+
case "dontAsk":
|
|
196
|
+
return rawMode;
|
|
197
|
+
default:
|
|
198
|
+
throw new Error(`unsupported launch_settings.permission_mode: ${rawMode}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
function initialSessionModel(launchSettings) {
|
|
202
|
+
return launchSettings.model?.trim() || DEFAULT_MODEL_NAME;
|
|
203
|
+
}
|
|
204
|
+
function initialSessionMode(launchSettings) {
|
|
205
|
+
return permissionModeFromLaunchSettings(launchSettings.permission_mode) ?? DEFAULT_PERMISSION_MODE;
|
|
206
|
+
}
|
|
207
|
+
function thinkingConfigFromLaunchSettings(launchSettings) {
|
|
208
|
+
switch (launchSettings.thinking_mode) {
|
|
209
|
+
case undefined:
|
|
210
|
+
return undefined;
|
|
211
|
+
case "adaptive":
|
|
212
|
+
return { type: "adaptive" };
|
|
213
|
+
case "disabled":
|
|
214
|
+
return { type: "disabled" };
|
|
215
|
+
default:
|
|
216
|
+
throw new Error(`unsupported launch_settings.thinking_mode: ${String(launchSettings.thinking_mode)}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function effortFromLaunchSettings(launchSettings) {
|
|
220
|
+
if (launchSettings.thinking_mode !== "adaptive") {
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
return launchSettings.effort_level;
|
|
224
|
+
}
|
|
225
|
+
function systemPromptFromLaunchSettings(launchSettings) {
|
|
226
|
+
const language = launchSettings.language?.trim();
|
|
227
|
+
if (!language) {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
type: "preset",
|
|
232
|
+
preset: "claude_code",
|
|
233
|
+
append: `Always respond to the user in ${language} unless the user explicitly asks for a different language. ` +
|
|
234
|
+
`Keep code, shell commands, file paths, API names, tool names, and raw error text unchanged unless the user explicitly asks for translation.`,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
export function buildQueryOptions(params) {
|
|
238
|
+
const permissionMode = permissionModeFromLaunchSettings(params.launchSettings.permission_mode);
|
|
239
|
+
const thinking = thinkingConfigFromLaunchSettings(params.launchSettings);
|
|
240
|
+
const effort = effortFromLaunchSettings(params.launchSettings);
|
|
241
|
+
const systemPrompt = systemPromptFromLaunchSettings(params.launchSettings);
|
|
242
|
+
return {
|
|
243
|
+
cwd: params.cwd,
|
|
244
|
+
includePartialMessages: true,
|
|
245
|
+
executable: "node",
|
|
246
|
+
...(params.resume ? {} : { sessionId: params.provisionalSessionId }),
|
|
247
|
+
...(params.launchSettings.model ? { model: params.launchSettings.model } : {}),
|
|
248
|
+
...(systemPrompt ? { systemPrompt } : {}),
|
|
249
|
+
...(permissionMode ? { permissionMode } : {}),
|
|
250
|
+
...(thinking ? { thinking } : {}),
|
|
251
|
+
...(effort ? { effort } : {}),
|
|
252
|
+
...(params.claudeCodeExecutable
|
|
253
|
+
? { pathToClaudeCodeExecutable: params.claudeCodeExecutable }
|
|
254
|
+
: {}),
|
|
255
|
+
...(params.enableSdkDebug ? { debug: true } : {}),
|
|
256
|
+
...(params.sdkDebugFile ? { debugFile: params.sdkDebugFile } : {}),
|
|
257
|
+
stderr: (line) => {
|
|
258
|
+
if (line.trim().length > 0) {
|
|
259
|
+
console.error(`[sdk stderr] ${line}`);
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
...(params.enableSpawnDebug
|
|
263
|
+
? {
|
|
264
|
+
spawnClaudeCodeProcess: (options) => {
|
|
265
|
+
console.error(`[sdk spawn] command=${options.command} args=${JSON.stringify(options.args)} cwd=${options.cwd ?? "<none>"}`);
|
|
266
|
+
const child = spawnChild(options.command, options.args, {
|
|
267
|
+
cwd: options.cwd,
|
|
268
|
+
env: options.env,
|
|
269
|
+
signal: options.signal,
|
|
270
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
271
|
+
windowsHide: true,
|
|
272
|
+
});
|
|
273
|
+
child.on("error", (error) => {
|
|
274
|
+
console.error(`[sdk spawn error] code=${error.code ?? "<none>"} message=${error.message}`);
|
|
275
|
+
});
|
|
276
|
+
return child;
|
|
277
|
+
},
|
|
278
|
+
}
|
|
279
|
+
: {}),
|
|
280
|
+
// Match claude-agent-acp defaults to avoid emitting an empty
|
|
281
|
+
// --setting-sources argument.
|
|
282
|
+
settingSources: DEFAULT_SETTING_SOURCES,
|
|
283
|
+
resume: params.resume,
|
|
284
|
+
canUseTool: params.canUseTool,
|
|
285
|
+
onElicitation: async (request) => {
|
|
286
|
+
const requestMode = typeof request.mode === "string" ? request.mode : "unknown";
|
|
287
|
+
const requestServer = typeof request.serverName === "string" && request.serverName.trim().length > 0
|
|
288
|
+
? request.serverName
|
|
289
|
+
: "unknown";
|
|
290
|
+
const requestMessage = typeof request.message === "string" && request.message.trim().length > 0
|
|
291
|
+
? request.message
|
|
292
|
+
: "<no message>";
|
|
293
|
+
console.error(`[sdk warn] elicitation unsupported without MCP settings UI; ` +
|
|
294
|
+
`auto-canceling session_id=${params.sessionIdForLogs()} server=${requestServer} ` +
|
|
295
|
+
`mode=${requestMode} message=${JSON.stringify(requestMessage)}`);
|
|
296
|
+
return { action: "cancel" };
|
|
297
|
+
},
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function mapAvailableModels(models) {
|
|
301
|
+
if (!Array.isArray(models)) {
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
return models
|
|
305
|
+
.filter((entry) => {
|
|
306
|
+
return (typeof entry?.value === "string" &&
|
|
307
|
+
entry.value.trim().length > 0 &&
|
|
308
|
+
typeof entry.displayName === "string" &&
|
|
309
|
+
entry.displayName.trim().length > 0);
|
|
310
|
+
})
|
|
311
|
+
.map((entry) => ({
|
|
312
|
+
id: entry.value,
|
|
313
|
+
display_name: entry.displayName,
|
|
314
|
+
supports_effort: entry.supportsEffort === true,
|
|
315
|
+
supported_effort_levels: Array.isArray(entry.supportedEffortLevels)
|
|
316
|
+
? entry.supportedEffortLevels.filter((level) => level === "low" || level === "medium" || level === "high")
|
|
317
|
+
: [],
|
|
318
|
+
...(typeof entry.description === "string" && entry.description.trim().length > 0
|
|
319
|
+
? { description: entry.description }
|
|
320
|
+
: {}),
|
|
321
|
+
}));
|
|
322
|
+
}
|
|
323
|
+
export function handlePermissionResponse(command) {
|
|
324
|
+
const session = sessionById(command.session_id);
|
|
325
|
+
if (!session) {
|
|
326
|
+
logPermissionDebug(`response dropped: unknown session session_id=${command.session_id} tool_call_id=${command.tool_call_id}`);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const resolver = session.pendingPermissions.get(command.tool_call_id);
|
|
330
|
+
if (!resolver) {
|
|
331
|
+
logPermissionDebug(`response dropped: no pending resolver session_id=${command.session_id} tool_call_id=${command.tool_call_id}`);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
session.pendingPermissions.delete(command.tool_call_id);
|
|
335
|
+
const outcome = command.outcome;
|
|
336
|
+
if (resolver.onOutcome) {
|
|
337
|
+
resolver.onOutcome(outcome);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (!resolver.resolve) {
|
|
341
|
+
logPermissionDebug(`response dropped: resolver missing callback session_id=${command.session_id} tool_call_id=${command.tool_call_id}`);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const selectedOption = outcome.outcome === "selected" ? outcome.option_id : "cancelled";
|
|
345
|
+
logPermissionDebug(`response session_id=${command.session_id} tool_call_id=${command.tool_call_id} tool=${resolver.toolName} ` +
|
|
346
|
+
`selected=${selectedOption} suggestions=${formatPermissionUpdates(resolver.suggestions)}`);
|
|
347
|
+
if (outcome.outcome === "selected" &&
|
|
348
|
+
(outcome.option_id === "allow_once" ||
|
|
349
|
+
outcome.option_id === "allow_session" ||
|
|
350
|
+
outcome.option_id === "allow_always")) {
|
|
351
|
+
setToolCallStatus(session, command.tool_call_id, "in_progress");
|
|
352
|
+
}
|
|
353
|
+
else if (outcome.outcome === "selected") {
|
|
354
|
+
setToolCallStatus(session, command.tool_call_id, "failed", "Permission denied");
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
setToolCallStatus(session, command.tool_call_id, "failed", "Permission cancelled");
|
|
358
|
+
}
|
|
359
|
+
const permissionResult = permissionResultFromOutcome(outcome, command.tool_call_id, resolver.inputData, resolver.suggestions, resolver.toolName);
|
|
360
|
+
if (permissionResult.behavior === "allow") {
|
|
361
|
+
logPermissionDebug(`result tool_call_id=${command.tool_call_id} behavior=allow updated_permissions=` +
|
|
362
|
+
`${formatPermissionUpdates(permissionResult.updatedPermissions)}`);
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
logPermissionDebug(`result tool_call_id=${command.tool_call_id} behavior=deny message=${permissionResult.message}`);
|
|
366
|
+
}
|
|
367
|
+
resolver.resolve(permissionResult);
|
|
368
|
+
}
|
|
@@ -4,3 +4,52 @@ export function asRecordOrNull(value) {
|
|
|
4
4
|
}
|
|
5
5
|
return value;
|
|
6
6
|
}
|
|
7
|
+
export class AsyncQueue {
|
|
8
|
+
items = [];
|
|
9
|
+
waiters = [];
|
|
10
|
+
closed = false;
|
|
11
|
+
enqueue(item) {
|
|
12
|
+
if (this.closed) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const waiter = this.waiters.shift();
|
|
16
|
+
if (waiter) {
|
|
17
|
+
waiter({ value: item, done: false });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
this.items.push(item);
|
|
21
|
+
}
|
|
22
|
+
close() {
|
|
23
|
+
if (this.closed) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
this.closed = true;
|
|
27
|
+
while (this.waiters.length > 0) {
|
|
28
|
+
const waiter = this.waiters.shift();
|
|
29
|
+
waiter?.({ value: undefined, done: true });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
[Symbol.asyncIterator]() {
|
|
33
|
+
return {
|
|
34
|
+
next: async () => {
|
|
35
|
+
if (this.items.length > 0) {
|
|
36
|
+
const value = this.items.shift();
|
|
37
|
+
return { value: value, done: false };
|
|
38
|
+
}
|
|
39
|
+
if (this.closed) {
|
|
40
|
+
return { value: undefined, done: true };
|
|
41
|
+
}
|
|
42
|
+
return await new Promise((resolve) => {
|
|
43
|
+
this.waiters.push(resolve);
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const permissionDebugEnabled = process.env.CLAUDE_RS_SDK_PERMISSION_DEBUG === "1" || process.env.CLAUDE_RS_SDK_DEBUG === "1";
|
|
50
|
+
export function logPermissionDebug(message) {
|
|
51
|
+
if (!permissionDebugEnabled) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
console.error(`[perm debug] ${message}`);
|
|
55
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { asRecordOrNull } from "./shared.js";
|
|
2
|
+
export function numberField(record, ...keys) {
|
|
3
|
+
for (const key of keys) {
|
|
4
|
+
const value = record[key];
|
|
5
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
export function parseFastModeState(value) {
|
|
12
|
+
if (value === "off" || value === "cooldown" || value === "on") {
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
export function parseRateLimitStatus(value) {
|
|
18
|
+
if (value === "allowed" || value === "allowed_warning" || value === "rejected") {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
export function buildRateLimitUpdate(rateLimitInfo) {
|
|
24
|
+
const info = asRecordOrNull(rateLimitInfo);
|
|
25
|
+
if (!info) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const status = parseRateLimitStatus(info.status);
|
|
29
|
+
if (!status) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const update = {
|
|
33
|
+
type: "rate_limit_update",
|
|
34
|
+
status,
|
|
35
|
+
};
|
|
36
|
+
const resetsAt = numberField(info, "resetsAt");
|
|
37
|
+
if (resetsAt !== undefined) {
|
|
38
|
+
update.resets_at = resetsAt;
|
|
39
|
+
}
|
|
40
|
+
const utilization = numberField(info, "utilization");
|
|
41
|
+
if (utilization !== undefined) {
|
|
42
|
+
update.utilization = utilization;
|
|
43
|
+
}
|
|
44
|
+
if (typeof info.rateLimitType === "string" && info.rateLimitType.length > 0) {
|
|
45
|
+
update.rate_limit_type = info.rateLimitType;
|
|
46
|
+
}
|
|
47
|
+
const overageStatus = parseRateLimitStatus(info.overageStatus);
|
|
48
|
+
if (overageStatus) {
|
|
49
|
+
update.overage_status = overageStatus;
|
|
50
|
+
}
|
|
51
|
+
const overageResetsAt = numberField(info, "overageResetsAt");
|
|
52
|
+
if (overageResetsAt !== undefined) {
|
|
53
|
+
update.overage_resets_at = overageResetsAt;
|
|
54
|
+
}
|
|
55
|
+
if (typeof info.overageDisabledReason === "string" && info.overageDisabledReason.length > 0) {
|
|
56
|
+
update.overage_disabled_reason = info.overageDisabledReason;
|
|
57
|
+
}
|
|
58
|
+
if (typeof info.isUsingOverage === "boolean") {
|
|
59
|
+
update.is_using_overage = info.isUsingOverage;
|
|
60
|
+
}
|
|
61
|
+
const surpassedThreshold = numberField(info, "surpassedThreshold");
|
|
62
|
+
if (surpassedThreshold !== undefined) {
|
|
63
|
+
update.surpassed_threshold = surpassedThreshold;
|
|
64
|
+
}
|
|
65
|
+
return update;
|
|
66
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { emitSessionUpdate } from "./events.js";
|
|
2
|
+
import { buildToolResultFields, createToolCall } from "./tooling.js";
|
|
3
|
+
export function emitToolCall(session, toolUseId, name, input) {
|
|
4
|
+
const toolCall = createToolCall(toolUseId, name, input);
|
|
5
|
+
const status = "in_progress";
|
|
6
|
+
toolCall.status = status;
|
|
7
|
+
const existing = session.toolCalls.get(toolUseId);
|
|
8
|
+
if (!existing) {
|
|
9
|
+
session.toolCalls.set(toolUseId, toolCall);
|
|
10
|
+
emitSessionUpdate(session.sessionId, { type: "tool_call", tool_call: toolCall });
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const fields = {
|
|
14
|
+
title: toolCall.title,
|
|
15
|
+
kind: toolCall.kind,
|
|
16
|
+
status,
|
|
17
|
+
raw_input: toolCall.raw_input,
|
|
18
|
+
locations: toolCall.locations,
|
|
19
|
+
meta: toolCall.meta,
|
|
20
|
+
};
|
|
21
|
+
if (toolCall.content.length > 0) {
|
|
22
|
+
fields.content = toolCall.content;
|
|
23
|
+
}
|
|
24
|
+
emitSessionUpdate(session.sessionId, {
|
|
25
|
+
type: "tool_call_update",
|
|
26
|
+
tool_call_update: { tool_call_id: toolUseId, fields },
|
|
27
|
+
});
|
|
28
|
+
existing.title = toolCall.title;
|
|
29
|
+
existing.kind = toolCall.kind;
|
|
30
|
+
existing.status = status;
|
|
31
|
+
existing.raw_input = toolCall.raw_input;
|
|
32
|
+
existing.locations = toolCall.locations;
|
|
33
|
+
existing.meta = toolCall.meta;
|
|
34
|
+
if (toolCall.content.length > 0) {
|
|
35
|
+
existing.content = toolCall.content;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function ensureToolCallVisible(session, toolUseId, toolName, input) {
|
|
39
|
+
const existing = session.toolCalls.get(toolUseId);
|
|
40
|
+
if (existing) {
|
|
41
|
+
return existing;
|
|
42
|
+
}
|
|
43
|
+
const toolCall = createToolCall(toolUseId, toolName, input);
|
|
44
|
+
session.toolCalls.set(toolUseId, toolCall);
|
|
45
|
+
emitSessionUpdate(session.sessionId, { type: "tool_call", tool_call: toolCall });
|
|
46
|
+
return toolCall;
|
|
47
|
+
}
|
|
48
|
+
export function emitPlanIfTodoWrite(session, name, input) {
|
|
49
|
+
if (name !== "TodoWrite" || !Array.isArray(input.todos)) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const entries = input.todos
|
|
53
|
+
.map((todo) => {
|
|
54
|
+
if (!todo || typeof todo !== "object") {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const todoObj = todo;
|
|
58
|
+
const content = typeof todoObj.content === "string" ? todoObj.content : "";
|
|
59
|
+
const status = typeof todoObj.status === "string" ? todoObj.status : "pending";
|
|
60
|
+
if (!content) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return { content, status, active_form: status };
|
|
64
|
+
})
|
|
65
|
+
.filter((entry) => entry !== null);
|
|
66
|
+
if (entries.length > 0) {
|
|
67
|
+
emitSessionUpdate(session.sessionId, { type: "plan", entries });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function emitToolResultUpdate(session, toolUseId, isError, rawContent) {
|
|
71
|
+
const base = session.toolCalls.get(toolUseId);
|
|
72
|
+
const fields = buildToolResultFields(isError, rawContent, base);
|
|
73
|
+
const update = { tool_call_id: toolUseId, fields };
|
|
74
|
+
emitSessionUpdate(session.sessionId, { type: "tool_call_update", tool_call_update: update });
|
|
75
|
+
if (base) {
|
|
76
|
+
base.status = fields.status ?? base.status;
|
|
77
|
+
if (fields.raw_output) {
|
|
78
|
+
base.raw_output = fields.raw_output;
|
|
79
|
+
}
|
|
80
|
+
if (fields.content) {
|
|
81
|
+
base.content = fields.content;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function finalizeOpenToolCalls(session, status) {
|
|
86
|
+
for (const [toolUseId, toolCall] of session.toolCalls) {
|
|
87
|
+
if (toolCall.status !== "pending" && toolCall.status !== "in_progress") {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const fields = { status };
|
|
91
|
+
emitSessionUpdate(session.sessionId, {
|
|
92
|
+
type: "tool_call_update",
|
|
93
|
+
tool_call_update: { tool_call_id: toolUseId, fields },
|
|
94
|
+
});
|
|
95
|
+
toolCall.status = status;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export function emitToolProgressUpdate(session, toolUseId, toolName) {
|
|
99
|
+
const existing = session.toolCalls.get(toolUseId);
|
|
100
|
+
if (!existing) {
|
|
101
|
+
emitToolCall(session, toolUseId, toolName, {});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (existing.status === "in_progress") {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const fields = { status: "in_progress" };
|
|
108
|
+
emitSessionUpdate(session.sessionId, {
|
|
109
|
+
type: "tool_call_update",
|
|
110
|
+
tool_call_update: { tool_call_id: toolUseId, fields },
|
|
111
|
+
});
|
|
112
|
+
existing.status = "in_progress";
|
|
113
|
+
}
|
|
114
|
+
export function emitToolSummaryUpdate(session, toolUseId, summary) {
|
|
115
|
+
const base = session.toolCalls.get(toolUseId);
|
|
116
|
+
if (!base) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const fields = {
|
|
120
|
+
status: base.status === "failed" ? "failed" : "completed",
|
|
121
|
+
raw_output: summary,
|
|
122
|
+
content: [{ type: "content", content: { type: "text", text: summary } }],
|
|
123
|
+
};
|
|
124
|
+
emitSessionUpdate(session.sessionId, {
|
|
125
|
+
type: "tool_call_update",
|
|
126
|
+
tool_call_update: { tool_call_id: toolUseId, fields },
|
|
127
|
+
});
|
|
128
|
+
base.status = fields.status ?? base.status;
|
|
129
|
+
base.raw_output = summary;
|
|
130
|
+
}
|
|
131
|
+
export function setToolCallStatus(session, toolUseId, status, message) {
|
|
132
|
+
const base = session.toolCalls.get(toolUseId);
|
|
133
|
+
if (!base) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const fields = { status };
|
|
137
|
+
if (message && message.length > 0) {
|
|
138
|
+
fields.raw_output = message;
|
|
139
|
+
fields.content = [{ type: "content", content: { type: "text", text: message } }];
|
|
140
|
+
}
|
|
141
|
+
emitSessionUpdate(session.sessionId, {
|
|
142
|
+
type: "tool_call_update",
|
|
143
|
+
tool_call_update: { tool_call_id: toolUseId, fields },
|
|
144
|
+
});
|
|
145
|
+
base.status = status;
|
|
146
|
+
if (fields.raw_output) {
|
|
147
|
+
base.raw_output = fields.raw_output;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export function resolveTaskToolUseId(session, msg) {
|
|
151
|
+
const direct = typeof msg.tool_use_id === "string" ? msg.tool_use_id : "";
|
|
152
|
+
if (direct) {
|
|
153
|
+
return direct;
|
|
154
|
+
}
|
|
155
|
+
const taskId = typeof msg.task_id === "string" ? msg.task_id : "";
|
|
156
|
+
if (!taskId) {
|
|
157
|
+
return "";
|
|
158
|
+
}
|
|
159
|
+
return session.taskToolUseIds.get(taskId) ?? "";
|
|
160
|
+
}
|
|
161
|
+
export function taskProgressText(msg) {
|
|
162
|
+
const description = typeof msg.description === "string" ? msg.description : "";
|
|
163
|
+
const lastTool = typeof msg.last_tool_name === "string" ? msg.last_tool_name : "";
|
|
164
|
+
if (description && lastTool) {
|
|
165
|
+
return `${description} (last tool: ${lastTool})`;
|
|
166
|
+
}
|
|
167
|
+
return description || lastTool;
|
|
168
|
+
}
|