@ccpocket-base-auth/bridge 1.26.0
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 +67 -0
- package/dist/archive-store.d.ts +28 -0
- package/dist/archive-store.js +68 -0
- package/dist/archive-store.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +82 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex-process.d.ts +171 -0
- package/dist/codex-process.js +1928 -0
- package/dist/codex-process.js.map +1 -0
- package/dist/debug-trace-store.d.ts +15 -0
- package/dist/debug-trace-store.js +78 -0
- package/dist/debug-trace-store.js.map +1 -0
- package/dist/doctor.d.ts +58 -0
- package/dist/doctor.js +663 -0
- package/dist/doctor.js.map +1 -0
- package/dist/firebase-auth.d.ts +35 -0
- package/dist/firebase-auth.js +132 -0
- package/dist/firebase-auth.js.map +1 -0
- package/dist/gallery-store.d.ts +67 -0
- package/dist/gallery-store.js +333 -0
- package/dist/gallery-store.js.map +1 -0
- package/dist/image-store.d.ts +23 -0
- package/dist/image-store.js +142 -0
- package/dist/image-store.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +191 -0
- package/dist/index.js.map +1 -0
- package/dist/mdns.d.ts +7 -0
- package/dist/mdns.js +49 -0
- package/dist/mdns.js.map +1 -0
- package/dist/parser.d.ts +465 -0
- package/dist/parser.js +251 -0
- package/dist/parser.js.map +1 -0
- package/dist/project-history.d.ts +10 -0
- package/dist/project-history.js +73 -0
- package/dist/project-history.js.map +1 -0
- package/dist/prompt-history-backup.d.ts +15 -0
- package/dist/prompt-history-backup.js +46 -0
- package/dist/prompt-history-backup.js.map +1 -0
- package/dist/proxy.d.ts +15 -0
- package/dist/proxy.js +95 -0
- package/dist/proxy.js.map +1 -0
- package/dist/push-i18n.d.ts +7 -0
- package/dist/push-i18n.js +75 -0
- package/dist/push-i18n.js.map +1 -0
- package/dist/push-relay.d.ts +29 -0
- package/dist/push-relay.js +70 -0
- package/dist/push-relay.js.map +1 -0
- package/dist/recording-store.d.ts +51 -0
- package/dist/recording-store.js +158 -0
- package/dist/recording-store.js.map +1 -0
- package/dist/screenshot.d.ts +28 -0
- package/dist/screenshot.js +98 -0
- package/dist/screenshot.js.map +1 -0
- package/dist/sdk-process.d.ts +180 -0
- package/dist/sdk-process.js +937 -0
- package/dist/sdk-process.js.map +1 -0
- package/dist/session.d.ts +142 -0
- package/dist/session.js +615 -0
- package/dist/session.js.map +1 -0
- package/dist/sessions-index.d.ts +128 -0
- package/dist/sessions-index.js +1767 -0
- package/dist/sessions-index.js.map +1 -0
- package/dist/setup-launchd.d.ts +8 -0
- package/dist/setup-launchd.js +109 -0
- package/dist/setup-launchd.js.map +1 -0
- package/dist/setup-systemd.d.ts +8 -0
- package/dist/setup-systemd.js +118 -0
- package/dist/setup-systemd.js.map +1 -0
- package/dist/startup-info.d.ts +8 -0
- package/dist/startup-info.js +92 -0
- package/dist/startup-info.js.map +1 -0
- package/dist/usage.d.ts +69 -0
- package/dist/usage.js +545 -0
- package/dist/usage.js.map +1 -0
- package/dist/version.d.ts +13 -0
- package/dist/version.js +43 -0
- package/dist/version.js.map +1 -0
- package/dist/websocket.d.ts +127 -0
- package/dist/websocket.js +2482 -0
- package/dist/websocket.js.map +1 -0
- package/dist/worktree-store.d.ts +25 -0
- package/dist/worktree-store.js +59 -0
- package/dist/worktree-store.js.map +1 -0
- package/dist/worktree.d.ts +47 -0
- package/dist/worktree.js +313 -0
- package/dist/worktree.js.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,937 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
4
|
+
import { normalizeToolResultContent, } from "./parser.js";
|
|
5
|
+
import { getClaudeAuthStatus, getValidClaudeAccessToken, validateClaudeAccessToken, } from "./usage.js";
|
|
6
|
+
// Tools that are auto-approved in acceptEdits mode
|
|
7
|
+
export const ACCEPT_EDITS_AUTO_APPROVE = new Set([
|
|
8
|
+
"Read", "Glob", "Grep",
|
|
9
|
+
"Edit", "Write", "NotebookEdit",
|
|
10
|
+
"TaskCreate", "TaskUpdate", "TaskList", "TaskGet",
|
|
11
|
+
"EnterPlanMode", "AskUserQuestion",
|
|
12
|
+
"WebSearch", "WebFetch",
|
|
13
|
+
"Task", "Skill",
|
|
14
|
+
]);
|
|
15
|
+
const FILE_EDIT_TOOLS = new Set([
|
|
16
|
+
"Edit",
|
|
17
|
+
"Write",
|
|
18
|
+
"MultiEdit",
|
|
19
|
+
"NotebookEdit",
|
|
20
|
+
]);
|
|
21
|
+
function toFiniteNumber(value) {
|
|
22
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
23
|
+
return undefined;
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
export function isFileEditToolName(toolName) {
|
|
27
|
+
return FILE_EDIT_TOOLS.has(toolName);
|
|
28
|
+
}
|
|
29
|
+
export function extractTokenUsage(usage) {
|
|
30
|
+
if (!usage || typeof usage !== "object" || Array.isArray(usage)) {
|
|
31
|
+
return {};
|
|
32
|
+
}
|
|
33
|
+
const obj = usage;
|
|
34
|
+
const inputTokens = toFiniteNumber(obj.input_tokens)
|
|
35
|
+
?? toFiniteNumber(obj.inputTokens);
|
|
36
|
+
const outputTokens = toFiniteNumber(obj.output_tokens)
|
|
37
|
+
?? toFiniteNumber(obj.outputTokens);
|
|
38
|
+
const cachedReadTokens = toFiniteNumber(obj.cached_input_tokens)
|
|
39
|
+
?? toFiniteNumber(obj.cache_read_input_tokens)
|
|
40
|
+
?? toFiniteNumber(obj.cachedInputTokens)
|
|
41
|
+
?? toFiniteNumber(obj.cacheReadInputTokens);
|
|
42
|
+
return {
|
|
43
|
+
...(inputTokens != null ? { inputTokens } : {}),
|
|
44
|
+
...(cachedReadTokens != null ? { cachedInputTokens: cachedReadTokens } : {}),
|
|
45
|
+
...(outputTokens != null ? { outputTokens } : {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Parse a permission rule in ToolName(ruleContent) format.
|
|
50
|
+
* Matches the CLI's internal pzT() function: /^([^(]+)\(([^)]+)\)$/
|
|
51
|
+
*/
|
|
52
|
+
export function parseRule(rule) {
|
|
53
|
+
const match = rule.match(/^([^(]+)\(([^)]+)\)$/);
|
|
54
|
+
if (!match || !match[1] || !match[2])
|
|
55
|
+
return { toolName: rule };
|
|
56
|
+
return { toolName: match[1], ruleContent: match[2] };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Check if a tool invocation matches any session allow rule.
|
|
60
|
+
*/
|
|
61
|
+
export function matchesSessionRule(toolName, input, rules) {
|
|
62
|
+
for (const rule of rules) {
|
|
63
|
+
const parsed = parseRule(rule);
|
|
64
|
+
if (parsed.toolName !== toolName)
|
|
65
|
+
continue;
|
|
66
|
+
// No ruleContent -> matches any invocation of this tool
|
|
67
|
+
if (!parsed.ruleContent)
|
|
68
|
+
return true;
|
|
69
|
+
// Bash: prefix matching with ":*" suffix
|
|
70
|
+
if (toolName === "Bash" && typeof input.command === "string") {
|
|
71
|
+
if (parsed.ruleContent.endsWith(":*")) {
|
|
72
|
+
const prefix = parsed.ruleContent.slice(0, -2);
|
|
73
|
+
const firstWord = input.command.trim().split(/\s+/)[0] ?? "";
|
|
74
|
+
if (firstWord === prefix)
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
if (input.command === parsed.ruleContent)
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Build a session allow rule string from a tool name and input.
|
|
87
|
+
* Bash: uses first word as prefix (e.g., "Bash(npm:*)")
|
|
88
|
+
* Others: tool name only (e.g., "Edit")
|
|
89
|
+
*/
|
|
90
|
+
export function buildSessionRule(toolName, input) {
|
|
91
|
+
if (toolName === "Bash" && typeof input.command === "string") {
|
|
92
|
+
const firstWord = input.command.trim().split(/\s+/)[0] ?? "";
|
|
93
|
+
if (firstWord)
|
|
94
|
+
return `${toolName}(${firstWord}:*)`;
|
|
95
|
+
}
|
|
96
|
+
return toolName;
|
|
97
|
+
}
|
|
98
|
+
const AUTH_REMEDY = "Fix: Run this command in the terminal on the machine running Bridge:\n claude auth login";
|
|
99
|
+
/**
|
|
100
|
+
* Build a user-friendly auth error result.
|
|
101
|
+
* The `message` field is designed to be helpful even without errorCode parsing
|
|
102
|
+
* (i.e. for older app versions that only display the raw message text).
|
|
103
|
+
*/
|
|
104
|
+
export function buildAuthError(reason, detail) {
|
|
105
|
+
switch (reason) {
|
|
106
|
+
case "no_credentials":
|
|
107
|
+
return {
|
|
108
|
+
authenticated: false,
|
|
109
|
+
errorCode: "auth_login_required",
|
|
110
|
+
message: `⚠ Claude Code authentication required\n\nClaude is not logged in on this machine.\nCredentials file not found (~/.claude/.credentials.json).\n\n${AUTH_REMEDY}`,
|
|
111
|
+
};
|
|
112
|
+
case "no_access_token":
|
|
113
|
+
return {
|
|
114
|
+
authenticated: false,
|
|
115
|
+
errorCode: "auth_login_required",
|
|
116
|
+
message: `⚠ Claude Code authentication required\n\nCredentials file exists but contains no access token.\n\n${AUTH_REMEDY}`,
|
|
117
|
+
};
|
|
118
|
+
case "token_expired":
|
|
119
|
+
return {
|
|
120
|
+
authenticated: false,
|
|
121
|
+
errorCode: "auth_token_expired",
|
|
122
|
+
message: `⚠ Claude Code session expired\n\nYour login session has expired and could not be refreshed automatically.\n\n${AUTH_REMEDY}`,
|
|
123
|
+
};
|
|
124
|
+
case "general":
|
|
125
|
+
return {
|
|
126
|
+
authenticated: false,
|
|
127
|
+
errorCode: "auth_api_error",
|
|
128
|
+
message: `⚠ Claude Code authentication failed\n\n${detail ?? "Unknown error"}\n\n${AUTH_REMEDY}`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Check if Claude CLI is authenticated and ensure the access token is valid.
|
|
134
|
+
* If the token is expired, automatically refreshes it using the refresh token.
|
|
135
|
+
* Returns authenticated=false with a message when login is required.
|
|
136
|
+
*/
|
|
137
|
+
async function checkClaudeAuth() {
|
|
138
|
+
// Skip auth check when using API key directly
|
|
139
|
+
if (process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN) {
|
|
140
|
+
return { authenticated: true };
|
|
141
|
+
}
|
|
142
|
+
// When usage is not explicitly enabled, use local-only credential check
|
|
143
|
+
// (no upstream API calls to Anthropic). Token refresh uses standard OAuth
|
|
144
|
+
// (platform.claude.com), NOT the Anthropic API (api.anthropic.com).
|
|
145
|
+
if (!process.env.BRIDGE_ENABLE_USAGE) {
|
|
146
|
+
// First try to ensure the token is fresh (refresh if expired).
|
|
147
|
+
// This only contacts platform.claude.com for OAuth token refresh,
|
|
148
|
+
// without probing api.anthropic.com.
|
|
149
|
+
try {
|
|
150
|
+
await getValidClaudeAccessToken();
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Refresh failed — fall through to local status check which will
|
|
154
|
+
// surface the appropriate error (missing credentials, expired, etc.).
|
|
155
|
+
}
|
|
156
|
+
const status = await getClaudeAuthStatus();
|
|
157
|
+
if (status.authenticated) {
|
|
158
|
+
return { authenticated: true };
|
|
159
|
+
}
|
|
160
|
+
if (status.errorCode === "auth_login_required") {
|
|
161
|
+
return buildAuthError("no_credentials");
|
|
162
|
+
}
|
|
163
|
+
return buildAuthError("general", status.message);
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
// getValidClaudeAccessToken() handles expiry detection + refresh + save
|
|
167
|
+
// in a single serialised flow (with mutex to prevent concurrent refreshes).
|
|
168
|
+
await getValidClaudeAccessToken();
|
|
169
|
+
// Probe the upstream API to catch revoked tokens that haven't expired yet.
|
|
170
|
+
// validateClaudeAccessToken() re-reads the (now-fresh) credentials from
|
|
171
|
+
// disk and will attempt one more refresh if the probe returns 401/403.
|
|
172
|
+
const validation = await validateClaudeAccessToken();
|
|
173
|
+
if (!validation.ok) {
|
|
174
|
+
return buildAuthError("general", validation.detail);
|
|
175
|
+
}
|
|
176
|
+
return { authenticated: true };
|
|
177
|
+
}
|
|
178
|
+
catch (err) {
|
|
179
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
180
|
+
// Distinguish between missing credentials and refresh failure
|
|
181
|
+
if (detail.includes("not found")) {
|
|
182
|
+
return buildAuthError("no_credentials");
|
|
183
|
+
}
|
|
184
|
+
if (detail.includes("refresh token") || detail.includes("No OAuth")) {
|
|
185
|
+
return buildAuthError("token_expired");
|
|
186
|
+
}
|
|
187
|
+
return buildAuthError("general", detail);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Convert SDK messages to the ServerMessage format used by the WebSocket protocol.
|
|
192
|
+
* Exported for testing.
|
|
193
|
+
*/
|
|
194
|
+
export function sdkMessageToServerMessage(msg) {
|
|
195
|
+
switch (msg.type) {
|
|
196
|
+
case "system": {
|
|
197
|
+
const sys = msg;
|
|
198
|
+
if (sys.subtype === "init") {
|
|
199
|
+
return {
|
|
200
|
+
type: "system",
|
|
201
|
+
subtype: "init",
|
|
202
|
+
sessionId: msg.session_id,
|
|
203
|
+
model: sys.model,
|
|
204
|
+
...(sys.slash_commands ? { slashCommands: sys.slash_commands } : {}),
|
|
205
|
+
...(sys.skills ? { skills: sys.skills } : {}),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
if (sys.subtype === "compact_boundary") {
|
|
209
|
+
return { type: "status", status: "compacting" };
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
case "assistant": {
|
|
214
|
+
const ast = msg;
|
|
215
|
+
return {
|
|
216
|
+
type: "assistant",
|
|
217
|
+
message: ast.message,
|
|
218
|
+
...(ast.uuid ? { messageUuid: ast.uuid } : {}),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
case "user": {
|
|
222
|
+
const usr = msg;
|
|
223
|
+
// Filter out meta messages early (e.g., skill loading prompts).
|
|
224
|
+
// Following Happy Coder's approach: isMeta messages are not user-facing.
|
|
225
|
+
if (usr.isMeta)
|
|
226
|
+
return null;
|
|
227
|
+
const content = usr.message?.content;
|
|
228
|
+
if (!Array.isArray(content))
|
|
229
|
+
return null;
|
|
230
|
+
const results = content.filter((c) => c.type === "tool_result");
|
|
231
|
+
if (results.length > 0) {
|
|
232
|
+
const first = results[0];
|
|
233
|
+
const rawContent = first.content;
|
|
234
|
+
return {
|
|
235
|
+
type: "tool_result",
|
|
236
|
+
toolUseId: first.tool_use_id,
|
|
237
|
+
content: normalizeToolResultContent(rawContent),
|
|
238
|
+
...(Array.isArray(rawContent) ? { rawContentBlocks: rawContent } : {}),
|
|
239
|
+
...(usr.uuid ? { userMessageUuid: usr.uuid } : {}),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
// User text input (first prompt of each turn)
|
|
243
|
+
const texts = content
|
|
244
|
+
.filter((c) => c.type === "text")
|
|
245
|
+
.map((c) => c.text);
|
|
246
|
+
if (texts.length > 0) {
|
|
247
|
+
return {
|
|
248
|
+
type: "user_input",
|
|
249
|
+
text: texts.join("\n"),
|
|
250
|
+
...(usr.uuid ? { userMessageUuid: usr.uuid } : {}),
|
|
251
|
+
...(usr.isSynthetic ? { isSynthetic: true } : {}),
|
|
252
|
+
...(usr.isMeta ? { isMeta: true } : {}),
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
case "result": {
|
|
258
|
+
const res = msg;
|
|
259
|
+
const tokenUsage = extractTokenUsage(res.usage);
|
|
260
|
+
if (res.subtype === "success") {
|
|
261
|
+
return {
|
|
262
|
+
type: "result",
|
|
263
|
+
subtype: "success",
|
|
264
|
+
result: res.result,
|
|
265
|
+
cost: res.total_cost_usd,
|
|
266
|
+
duration: res.duration_ms,
|
|
267
|
+
sessionId: msg.session_id,
|
|
268
|
+
stopReason: res.stop_reason,
|
|
269
|
+
...tokenUsage,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
// All other result subtypes are errors
|
|
273
|
+
const errorText = Array.isArray(res.errors) ? res.errors.join("\n") : "Unknown error";
|
|
274
|
+
// Suppress spurious CLI runtime errors (SDK bug: Bun API referenced on Node.js)
|
|
275
|
+
if (errorText.includes("Bun is not defined")) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
type: "result",
|
|
280
|
+
subtype: "error",
|
|
281
|
+
error: errorText,
|
|
282
|
+
sessionId: msg.session_id,
|
|
283
|
+
stopReason: res.stop_reason,
|
|
284
|
+
...tokenUsage,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
case "stream_event": {
|
|
288
|
+
const stream = msg;
|
|
289
|
+
const event = stream.event;
|
|
290
|
+
if (event.type === "content_block_delta") {
|
|
291
|
+
const delta = event.delta;
|
|
292
|
+
if (delta.type === "text_delta" && delta.text) {
|
|
293
|
+
return { type: "stream_delta", text: delta.text };
|
|
294
|
+
}
|
|
295
|
+
if (delta.type === "thinking_delta" && delta.thinking) {
|
|
296
|
+
return { type: "thinking_delta", text: delta.thinking };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
case "tool_use_summary": {
|
|
302
|
+
const summary = msg;
|
|
303
|
+
return {
|
|
304
|
+
type: "tool_use_summary",
|
|
305
|
+
summary: summary.summary,
|
|
306
|
+
precedingToolUseIds: summary.preceding_tool_use_ids,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
default:
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
export class SdkProcess extends EventEmitter {
|
|
314
|
+
queryInstance = null;
|
|
315
|
+
_status = "idle";
|
|
316
|
+
_sessionId = null;
|
|
317
|
+
pendingPermissions = new Map();
|
|
318
|
+
_permissionMode;
|
|
319
|
+
get permissionMode() { return this._permissionMode; }
|
|
320
|
+
_model;
|
|
321
|
+
get model() { return this._model; }
|
|
322
|
+
sessionAllowRules = new Set();
|
|
323
|
+
initTimeoutId = null;
|
|
324
|
+
sessionEndEmitted = false;
|
|
325
|
+
// User message channel
|
|
326
|
+
userMessageResolve = null;
|
|
327
|
+
stopped = false;
|
|
328
|
+
pendingInputQueue = [];
|
|
329
|
+
_projectPath = null;
|
|
330
|
+
toolCallsSinceLastResult = 0;
|
|
331
|
+
fileEditsSinceLastResult = 0;
|
|
332
|
+
get status() {
|
|
333
|
+
return this._status;
|
|
334
|
+
}
|
|
335
|
+
get isWaitingForInput() {
|
|
336
|
+
return this.userMessageResolve !== null;
|
|
337
|
+
}
|
|
338
|
+
get sessionId() {
|
|
339
|
+
return this._sessionId;
|
|
340
|
+
}
|
|
341
|
+
get isRunning() {
|
|
342
|
+
return this.queryInstance !== null;
|
|
343
|
+
}
|
|
344
|
+
start(projectPath, options) {
|
|
345
|
+
if (this.queryInstance) {
|
|
346
|
+
this.stop();
|
|
347
|
+
}
|
|
348
|
+
this._projectPath = projectPath;
|
|
349
|
+
if (!existsSync(projectPath)) {
|
|
350
|
+
try {
|
|
351
|
+
mkdirSync(projectPath, { recursive: true });
|
|
352
|
+
}
|
|
353
|
+
catch (err) {
|
|
354
|
+
throw new Error(`Cannot create project directory: ${projectPath} (${err.code ?? err})`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
this.stopped = false;
|
|
358
|
+
this._sessionId = null;
|
|
359
|
+
this.sessionEndEmitted = false;
|
|
360
|
+
this.pendingPermissions.clear();
|
|
361
|
+
this._permissionMode = options?.permissionMode;
|
|
362
|
+
this.sessionAllowRules.clear();
|
|
363
|
+
this.toolCallsSinceLastResult = 0;
|
|
364
|
+
this.fileEditsSinceLastResult = 0;
|
|
365
|
+
if (options?.initialInput) {
|
|
366
|
+
this.pendingInputQueue.push({ text: options.initialInput });
|
|
367
|
+
}
|
|
368
|
+
this.setStatus("starting");
|
|
369
|
+
// Pre-check Claude auth (async: refreshes expired tokens) then start SDK.
|
|
370
|
+
this.startAfterAuthCheck(projectPath, options);
|
|
371
|
+
}
|
|
372
|
+
startAfterAuthCheck(projectPath, options) {
|
|
373
|
+
checkClaudeAuth()
|
|
374
|
+
.then((authCheck) => {
|
|
375
|
+
if (this.stopped)
|
|
376
|
+
return; // Cancelled while awaiting auth
|
|
377
|
+
if (!authCheck.authenticated) {
|
|
378
|
+
console.log(`[sdk-process] Auth pre-check failed: ${authCheck.message}`);
|
|
379
|
+
this.emitMessage({
|
|
380
|
+
type: "error",
|
|
381
|
+
message: authCheck.message ?? "Claude is not authenticated. Please run: claude auth login",
|
|
382
|
+
...(authCheck.errorCode ? { errorCode: authCheck.errorCode } : {}),
|
|
383
|
+
});
|
|
384
|
+
this.setStatus("idle");
|
|
385
|
+
this.emit("exit", 1);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
this.startSdkQuery(projectPath, options);
|
|
389
|
+
})
|
|
390
|
+
.catch((err) => {
|
|
391
|
+
if (this.stopped)
|
|
392
|
+
return;
|
|
393
|
+
console.error("[sdk-process] Auth check error:", err);
|
|
394
|
+
this.emitMessage({
|
|
395
|
+
type: "error",
|
|
396
|
+
message: `Auth check failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
397
|
+
});
|
|
398
|
+
this.setStatus("idle");
|
|
399
|
+
this.emit("exit", 1);
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
startSdkQuery(projectPath, options) {
|
|
403
|
+
console.log(`[sdk-process] Starting SDK query (cwd: ${projectPath}, mode: ${options?.permissionMode ?? "default"}${options?.sessionId ? `, resume: ${options.sessionId}` : ""}${options?.continueMode ? ", continue: true" : ""})`);
|
|
404
|
+
// In -p mode with --input-format stream-json, Claude CLI won't emit
|
|
405
|
+
// system/init until the first user input. Set a fallback timeout to
|
|
406
|
+
// transition to "idle" if init hasn't arrived, since the process IS
|
|
407
|
+
// ready to accept input at that point.
|
|
408
|
+
if (this.initTimeoutId)
|
|
409
|
+
clearTimeout(this.initTimeoutId);
|
|
410
|
+
this.initTimeoutId = setTimeout(() => {
|
|
411
|
+
if (this._status === "starting") {
|
|
412
|
+
console.log("[sdk-process] Init timeout: setting status to idle (process ready for input)");
|
|
413
|
+
this.setStatus("idle");
|
|
414
|
+
}
|
|
415
|
+
this.initTimeoutId = null;
|
|
416
|
+
}, 3000);
|
|
417
|
+
this.queryInstance = query({
|
|
418
|
+
prompt: this.createUserMessageStream(),
|
|
419
|
+
options: {
|
|
420
|
+
cwd: projectPath,
|
|
421
|
+
resume: options?.sessionId,
|
|
422
|
+
continue: options?.continueMode,
|
|
423
|
+
permissionMode: options?.permissionMode ?? "default",
|
|
424
|
+
...(options?.model ? { model: options.model } : {}),
|
|
425
|
+
...(options?.effort ? { effort: options.effort } : {}),
|
|
426
|
+
...(options?.maxTurns != null ? { maxTurns: options.maxTurns } : {}),
|
|
427
|
+
...(options?.maxBudgetUsd != null ? { maxBudgetUsd: options.maxBudgetUsd } : {}),
|
|
428
|
+
...(options?.fallbackModel ? { fallbackModel: options.fallbackModel } : {}),
|
|
429
|
+
...(options?.forkSession != null ? { forkSession: options.forkSession } : {}),
|
|
430
|
+
...(options?.persistSession != null ? { persistSession: options.persistSession } : {}),
|
|
431
|
+
hooks: {
|
|
432
|
+
PostToolUse: [{
|
|
433
|
+
hooks: [async (input) => {
|
|
434
|
+
this.handlePostToolUseHook(input);
|
|
435
|
+
return { continue: true };
|
|
436
|
+
}],
|
|
437
|
+
}],
|
|
438
|
+
},
|
|
439
|
+
includePartialMessages: true,
|
|
440
|
+
canUseTool: this.handleCanUseTool.bind(this),
|
|
441
|
+
settingSources: ["user", "project", "local"],
|
|
442
|
+
enableFileCheckpointing: true,
|
|
443
|
+
...(options?.resumeSessionAt ? { resumeSessionAt: options.resumeSessionAt } : {}),
|
|
444
|
+
...(options?.sandboxEnabled === true
|
|
445
|
+
? { sandbox: { enabled: true } }
|
|
446
|
+
: options?.sandboxEnabled === false
|
|
447
|
+
? { sandbox: { enabled: false } }
|
|
448
|
+
: {}),
|
|
449
|
+
stderr: (data) => {
|
|
450
|
+
// Capture CLI stderr for resume failure diagnostics
|
|
451
|
+
const trimmed = data.trim();
|
|
452
|
+
if (trimmed) {
|
|
453
|
+
console.error(`[sdk-process:stderr] ${trimmed}`);
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
// Background message processing
|
|
459
|
+
this.processMessages().catch((err) => {
|
|
460
|
+
if (this.stopped) {
|
|
461
|
+
// Suppress errors from intentional stop (SDK bug: Bun API referenced on Node.js)
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
console.error("[sdk-process] Message processing error:", err);
|
|
465
|
+
this.emitMessage({ type: "error", message: `SDK error: ${err instanceof Error ? err.message : String(err)}` });
|
|
466
|
+
this.setStatus("idle");
|
|
467
|
+
this.emit("exit", 1);
|
|
468
|
+
});
|
|
469
|
+
// Proactively fetch supported commands via SDK API (non-blocking)
|
|
470
|
+
this.fetchSupportedCommands();
|
|
471
|
+
}
|
|
472
|
+
stop() {
|
|
473
|
+
if (this.initTimeoutId) {
|
|
474
|
+
clearTimeout(this.initTimeoutId);
|
|
475
|
+
this.initTimeoutId = null;
|
|
476
|
+
}
|
|
477
|
+
this.stopped = true;
|
|
478
|
+
this.pendingInputQueue = [];
|
|
479
|
+
if (this.queryInstance) {
|
|
480
|
+
console.log("[sdk-process] Stopping query");
|
|
481
|
+
this.queryInstance.close();
|
|
482
|
+
this.queryInstance = null;
|
|
483
|
+
}
|
|
484
|
+
this.pendingPermissions.clear();
|
|
485
|
+
this.userMessageResolve = null;
|
|
486
|
+
this.toolCallsSinceLastResult = 0;
|
|
487
|
+
this.fileEditsSinceLastResult = 0;
|
|
488
|
+
// Emit session_end so listeners can re-persist metadata before cleanup.
|
|
489
|
+
// processMessages() won't reach its session_end emit because close()
|
|
490
|
+
// causes the iterator to throw and the error is suppressed.
|
|
491
|
+
this.emitSessionEnd();
|
|
492
|
+
this.setStatus("idle");
|
|
493
|
+
}
|
|
494
|
+
interrupt() {
|
|
495
|
+
if (this.queryInstance) {
|
|
496
|
+
console.log("[sdk-process] Interrupting query");
|
|
497
|
+
// NOTE: Do NOT clear pendingInputQueue here — queued messages should
|
|
498
|
+
// survive an interrupt so they are delivered on the next turn.
|
|
499
|
+
this.queryInstance.interrupt().catch((err) => {
|
|
500
|
+
console.error("[sdk-process] Interrupt error:", err);
|
|
501
|
+
});
|
|
502
|
+
this.pendingPermissions.clear();
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Returns true when the SDK async generator is blocked waiting for the
|
|
507
|
+
* next user message (i.e. the agent is idle between turns).
|
|
508
|
+
* When false, the agent is mid-turn and input will be queued.
|
|
509
|
+
*/
|
|
510
|
+
get hasInputQueue() {
|
|
511
|
+
return this.pendingInputQueue.length > 0;
|
|
512
|
+
}
|
|
513
|
+
sendInput(text) {
|
|
514
|
+
if (!this.userMessageResolve) {
|
|
515
|
+
// Queue the message. The async generator (createUserMessageStream)
|
|
516
|
+
// drains pendingInputQueue on each iteration, so it will be
|
|
517
|
+
// delivered once the SDK is ready for the next turn.
|
|
518
|
+
this.pendingInputQueue.push({ text });
|
|
519
|
+
console.log(`[sdk-process] Queued input (queue depth: ${this.pendingInputQueue.length})`);
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
const resolve = this.userMessageResolve;
|
|
523
|
+
this.userMessageResolve = null;
|
|
524
|
+
resolve({
|
|
525
|
+
type: "user",
|
|
526
|
+
session_id: this._sessionId ?? "",
|
|
527
|
+
message: {
|
|
528
|
+
role: "user",
|
|
529
|
+
content: [{ type: "text", text }],
|
|
530
|
+
},
|
|
531
|
+
parent_tool_use_id: null,
|
|
532
|
+
});
|
|
533
|
+
return false;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Send a message with one or more image attachments.
|
|
537
|
+
* @param text - The text message
|
|
538
|
+
* @param images - Array of base64-encoded image data with mime types
|
|
539
|
+
*/
|
|
540
|
+
sendInputWithImages(text, images) {
|
|
541
|
+
if (!this.userMessageResolve) {
|
|
542
|
+
this.pendingInputQueue.push({ text, images });
|
|
543
|
+
console.log(`[sdk-process] Queued input with ${images.length} image(s) (queue depth: ${this.pendingInputQueue.length})`);
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
const resolve = this.userMessageResolve;
|
|
547
|
+
this.userMessageResolve = null;
|
|
548
|
+
const content = [];
|
|
549
|
+
// Add image blocks first (Claude processes images before text)
|
|
550
|
+
for (const image of images) {
|
|
551
|
+
content.push({
|
|
552
|
+
type: "image",
|
|
553
|
+
source: {
|
|
554
|
+
type: "base64",
|
|
555
|
+
media_type: image.mimeType,
|
|
556
|
+
data: image.base64,
|
|
557
|
+
},
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
// Add text block
|
|
561
|
+
content.push({ type: "text", text });
|
|
562
|
+
const totalKB = images.reduce((sum, img) => sum + Math.round(img.base64.length / 1024), 0);
|
|
563
|
+
console.log(`[sdk-process] Sending message with ${images.length} image(s) (${totalKB}KB base64 total)`);
|
|
564
|
+
resolve({
|
|
565
|
+
type: "user",
|
|
566
|
+
session_id: this._sessionId ?? "",
|
|
567
|
+
message: {
|
|
568
|
+
role: "user",
|
|
569
|
+
content,
|
|
570
|
+
},
|
|
571
|
+
parent_tool_use_id: null,
|
|
572
|
+
});
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Approve a pending permission request.
|
|
577
|
+
* With the SDK, this actually blocks tool execution until approved.
|
|
578
|
+
*/
|
|
579
|
+
approve(toolUseId, updatedInput) {
|
|
580
|
+
const id = toolUseId ?? this.firstPendingId();
|
|
581
|
+
const pending = id ? this.pendingPermissions.get(id) : undefined;
|
|
582
|
+
if (!pending) {
|
|
583
|
+
console.log("[sdk-process] approve() called but no pending permission requests");
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
const mergedInput = updatedInput
|
|
587
|
+
? { ...pending.input, ...updatedInput }
|
|
588
|
+
: pending.input;
|
|
589
|
+
this.pendingPermissions.delete(id);
|
|
590
|
+
pending.resolve({
|
|
591
|
+
behavior: "allow",
|
|
592
|
+
updatedInput: mergedInput,
|
|
593
|
+
});
|
|
594
|
+
if (this.pendingPermissions.size === 0) {
|
|
595
|
+
this.setStatus("running");
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Approve a pending permission request and add a session-scoped allow rule.
|
|
600
|
+
*/
|
|
601
|
+
approveAlways(toolUseId) {
|
|
602
|
+
const id = toolUseId ?? this.firstPendingId();
|
|
603
|
+
const pending = id ? this.pendingPermissions.get(id) : undefined;
|
|
604
|
+
if (pending) {
|
|
605
|
+
const rule = buildSessionRule(pending.toolName, pending.input);
|
|
606
|
+
this.sessionAllowRules.add(rule);
|
|
607
|
+
console.log(`[sdk-process] Added session allow rule: ${rule}`);
|
|
608
|
+
}
|
|
609
|
+
this.approve(id);
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Reject a pending permission request.
|
|
613
|
+
* The SDK's canUseTool will return deny, which tells Claude the tool was rejected.
|
|
614
|
+
*/
|
|
615
|
+
reject(toolUseId, message) {
|
|
616
|
+
const id = toolUseId ?? this.firstPendingId();
|
|
617
|
+
const pending = id ? this.pendingPermissions.get(id) : undefined;
|
|
618
|
+
if (!pending) {
|
|
619
|
+
console.log("[sdk-process] reject() called but no pending permission requests");
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
this.pendingPermissions.delete(id);
|
|
623
|
+
pending.resolve({
|
|
624
|
+
behavior: "deny",
|
|
625
|
+
message: message ?? "User rejected this action",
|
|
626
|
+
});
|
|
627
|
+
if (this.pendingPermissions.size === 0) {
|
|
628
|
+
this.setStatus("running");
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Answer an AskUserQuestion tool call.
|
|
633
|
+
* The SDK handles this through canUseTool with updatedInput.
|
|
634
|
+
*/
|
|
635
|
+
answer(toolUseId, result) {
|
|
636
|
+
const pending = this.pendingPermissions.get(toolUseId);
|
|
637
|
+
if (!pending || pending.toolName !== "AskUserQuestion") {
|
|
638
|
+
console.log("[sdk-process] answer() called but no pending AskUserQuestion");
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
this.pendingPermissions.delete(toolUseId);
|
|
642
|
+
pending.resolve({
|
|
643
|
+
behavior: "allow",
|
|
644
|
+
updatedInput: {
|
|
645
|
+
...pending.input,
|
|
646
|
+
answers: { ...(pending.input.answers ?? {}), result },
|
|
647
|
+
},
|
|
648
|
+
});
|
|
649
|
+
if (this.pendingPermissions.size === 0) {
|
|
650
|
+
this.setStatus("running");
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Update permission mode for the current session.
|
|
655
|
+
* Only available while the query instance is active.
|
|
656
|
+
*/
|
|
657
|
+
async setPermissionMode(mode) {
|
|
658
|
+
if (!this.queryInstance) {
|
|
659
|
+
throw new Error("No active query instance");
|
|
660
|
+
}
|
|
661
|
+
await this.queryInstance.setPermissionMode(mode);
|
|
662
|
+
this._permissionMode = mode;
|
|
663
|
+
this.emitMessage({
|
|
664
|
+
type: "system",
|
|
665
|
+
subtype: "set_permission_mode",
|
|
666
|
+
permissionMode: mode,
|
|
667
|
+
sessionId: this._sessionId ?? undefined,
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Rewind files to their state at the specified user message.
|
|
672
|
+
* Requires enableFileCheckpointing to be enabled (done in start()).
|
|
673
|
+
*/
|
|
674
|
+
async rewindFiles(userMessageId, dryRun) {
|
|
675
|
+
if (!this.queryInstance) {
|
|
676
|
+
return { canRewind: false, error: "No active query instance" };
|
|
677
|
+
}
|
|
678
|
+
try {
|
|
679
|
+
const result = await this.queryInstance.rewindFiles(userMessageId, { dryRun });
|
|
680
|
+
return result;
|
|
681
|
+
}
|
|
682
|
+
catch (err) {
|
|
683
|
+
return { canRewind: false, error: err instanceof Error ? err.message : String(err) };
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
// ---- Private ----
|
|
687
|
+
/**
|
|
688
|
+
* Proactively fetch supported commands from the SDK.
|
|
689
|
+
* This may resolve before the first user input, providing slash commands
|
|
690
|
+
* without waiting for system/init.
|
|
691
|
+
*/
|
|
692
|
+
fetchSupportedCommands() {
|
|
693
|
+
if (!this.queryInstance)
|
|
694
|
+
return;
|
|
695
|
+
const TIMEOUT_MS = 10_000;
|
|
696
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
697
|
+
setTimeout(() => resolve(null), TIMEOUT_MS);
|
|
698
|
+
});
|
|
699
|
+
Promise.race([
|
|
700
|
+
this.queryInstance.supportedCommands(),
|
|
701
|
+
timeoutPromise,
|
|
702
|
+
])
|
|
703
|
+
.then((result) => {
|
|
704
|
+
if (this.stopped || !result)
|
|
705
|
+
return;
|
|
706
|
+
const slashCommands = result.map((cmd) => cmd.name);
|
|
707
|
+
// Build skill metadata from description field returned by the SDK.
|
|
708
|
+
// This provides human-readable descriptions for custom skills
|
|
709
|
+
// that are not in the client's hardcoded knownCommands map.
|
|
710
|
+
const skillMetadata = result
|
|
711
|
+
.filter((cmd) => cmd.description && cmd.description !== cmd.name)
|
|
712
|
+
.map((cmd) => ({
|
|
713
|
+
name: cmd.name,
|
|
714
|
+
path: "",
|
|
715
|
+
description: cmd.description,
|
|
716
|
+
shortDescription: cmd.description,
|
|
717
|
+
enabled: true,
|
|
718
|
+
scope: "project",
|
|
719
|
+
}));
|
|
720
|
+
const skills = skillMetadata.map((m) => m.name);
|
|
721
|
+
console.log(`[sdk-process] supportedCommands() returned ${slashCommands.length} commands (${skills.length} with descriptions)`);
|
|
722
|
+
this.emitMessage({
|
|
723
|
+
type: "system",
|
|
724
|
+
subtype: "supported_commands",
|
|
725
|
+
slashCommands,
|
|
726
|
+
...(skills.length > 0 ? { skills, skillMetadata } : {}),
|
|
727
|
+
});
|
|
728
|
+
})
|
|
729
|
+
.catch((err) => {
|
|
730
|
+
console.log(`[sdk-process] supportedCommands() failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
firstPendingId() {
|
|
734
|
+
const first = this.pendingPermissions.keys().next();
|
|
735
|
+
return first.done ? undefined : first.value;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Returns a snapshot of a pending permission request.
|
|
739
|
+
* Used by the bridge to support Clear & Accept flows.
|
|
740
|
+
*/
|
|
741
|
+
getPendingPermission(toolUseId) {
|
|
742
|
+
const id = toolUseId ?? this.firstPendingId();
|
|
743
|
+
const pending = id ? this.pendingPermissions.get(id) : undefined;
|
|
744
|
+
if (!pending || !id)
|
|
745
|
+
return undefined;
|
|
746
|
+
return {
|
|
747
|
+
toolUseId: id,
|
|
748
|
+
toolName: pending.toolName,
|
|
749
|
+
input: { ...pending.input },
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
async *createUserMessageStream() {
|
|
753
|
+
while (!this.stopped) {
|
|
754
|
+
// Drain queued messages first (FIFO order)
|
|
755
|
+
if (this.pendingInputQueue.length > 0) {
|
|
756
|
+
const { text, images } = this.pendingInputQueue.shift();
|
|
757
|
+
console.log(`[sdk-process] Sending queued input${images ? ` with ${images.length} image(s)` : ""} (remaining: ${this.pendingInputQueue.length})`);
|
|
758
|
+
const content = [];
|
|
759
|
+
if (images) {
|
|
760
|
+
for (const image of images) {
|
|
761
|
+
content.push({
|
|
762
|
+
type: "image",
|
|
763
|
+
source: {
|
|
764
|
+
type: "base64",
|
|
765
|
+
media_type: image.mimeType,
|
|
766
|
+
data: image.base64,
|
|
767
|
+
},
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
content.push({ type: "text", text });
|
|
772
|
+
yield {
|
|
773
|
+
type: "user",
|
|
774
|
+
session_id: this._sessionId ?? "",
|
|
775
|
+
message: {
|
|
776
|
+
role: "user",
|
|
777
|
+
content,
|
|
778
|
+
},
|
|
779
|
+
parent_tool_use_id: null,
|
|
780
|
+
};
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
const msg = await new Promise((resolve) => {
|
|
784
|
+
this.userMessageResolve = resolve;
|
|
785
|
+
});
|
|
786
|
+
if (this.stopped)
|
|
787
|
+
break;
|
|
788
|
+
yield msg;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
async processMessages() {
|
|
792
|
+
if (!this.queryInstance)
|
|
793
|
+
return;
|
|
794
|
+
for await (const message of this.queryInstance) {
|
|
795
|
+
if (this.stopped)
|
|
796
|
+
break;
|
|
797
|
+
// Convert SDK message to ServerMessage
|
|
798
|
+
let serverMsg = sdkMessageToServerMessage(message);
|
|
799
|
+
if (serverMsg?.type === "result") {
|
|
800
|
+
if (this.toolCallsSinceLastResult > 0 || this.fileEditsSinceLastResult > 0) {
|
|
801
|
+
serverMsg = {
|
|
802
|
+
...serverMsg,
|
|
803
|
+
...(this.toolCallsSinceLastResult > 0
|
|
804
|
+
? { toolCalls: this.toolCallsSinceLastResult }
|
|
805
|
+
: {}),
|
|
806
|
+
...(this.fileEditsSinceLastResult > 0
|
|
807
|
+
? { fileEdits: this.fileEditsSinceLastResult }
|
|
808
|
+
: {}),
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
this.toolCallsSinceLastResult = 0;
|
|
812
|
+
this.fileEditsSinceLastResult = 0;
|
|
813
|
+
}
|
|
814
|
+
if (serverMsg) {
|
|
815
|
+
this.emitMessage(serverMsg);
|
|
816
|
+
}
|
|
817
|
+
// Extract session ID and model from system/init
|
|
818
|
+
if (message.type === "system" && "subtype" in message && message.subtype === "init") {
|
|
819
|
+
if (this.initTimeoutId) {
|
|
820
|
+
clearTimeout(this.initTimeoutId);
|
|
821
|
+
this.initTimeoutId = null;
|
|
822
|
+
}
|
|
823
|
+
this._sessionId = message.session_id;
|
|
824
|
+
const initModel = message.model;
|
|
825
|
+
if (typeof initModel === "string" && initModel) {
|
|
826
|
+
this._model = initModel;
|
|
827
|
+
}
|
|
828
|
+
this.setStatus("idle");
|
|
829
|
+
}
|
|
830
|
+
// Update status from message type
|
|
831
|
+
this.updateStatusFromMessage(message);
|
|
832
|
+
}
|
|
833
|
+
// Query finished — CLI has completed shutdown including file writes.
|
|
834
|
+
this.queryInstance = null;
|
|
835
|
+
// Emit session_end before exit so listeners can re-persist metadata
|
|
836
|
+
// (e.g. customTitle) that the CLI may have overwritten during shutdown.
|
|
837
|
+
this.emitSessionEnd();
|
|
838
|
+
this.setStatus("idle");
|
|
839
|
+
this.emit("exit", 0);
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Core permission handler: called by SDK before each tool execution.
|
|
843
|
+
* Returns a Promise that resolves when the user approves/rejects.
|
|
844
|
+
*/
|
|
845
|
+
async handleCanUseTool(toolName, input, options) {
|
|
846
|
+
// AskUserQuestion: always forward to client for response
|
|
847
|
+
if (toolName === "AskUserQuestion") {
|
|
848
|
+
return this.waitForPermission(options.toolUseID, toolName, input, options.signal);
|
|
849
|
+
}
|
|
850
|
+
// Auto-approve check: session allow rules
|
|
851
|
+
if (matchesSessionRule(toolName, input, this.sessionAllowRules)) {
|
|
852
|
+
return { behavior: "allow", updatedInput: input };
|
|
853
|
+
}
|
|
854
|
+
// SDK handles permissionMode internally, but canUseTool is only called
|
|
855
|
+
// for tools that the SDK thinks need permission. We emit the request
|
|
856
|
+
// to the mobile client and wait.
|
|
857
|
+
return this.waitForPermission(options.toolUseID, toolName, input, options.signal);
|
|
858
|
+
}
|
|
859
|
+
waitForPermission(toolUseId, toolName, input, signal) {
|
|
860
|
+
// Emit permission request to client
|
|
861
|
+
this.emitMessage({
|
|
862
|
+
type: "permission_request",
|
|
863
|
+
toolUseId,
|
|
864
|
+
toolName,
|
|
865
|
+
input,
|
|
866
|
+
});
|
|
867
|
+
this.setStatus("waiting_approval");
|
|
868
|
+
return new Promise((resolve) => {
|
|
869
|
+
this.pendingPermissions.set(toolUseId, { resolve, toolName, input });
|
|
870
|
+
// Handle abort (timeout)
|
|
871
|
+
if (signal.aborted) {
|
|
872
|
+
this.pendingPermissions.delete(toolUseId);
|
|
873
|
+
resolve({ behavior: "deny", message: "Permission request aborted" });
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
signal.addEventListener("abort", () => {
|
|
877
|
+
if (this.pendingPermissions.has(toolUseId)) {
|
|
878
|
+
this.pendingPermissions.delete(toolUseId);
|
|
879
|
+
resolve({ behavior: "deny", message: "Permission request timed out" });
|
|
880
|
+
}
|
|
881
|
+
}, { once: true });
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
updateStatusFromMessage(msg) {
|
|
885
|
+
switch (msg.type) {
|
|
886
|
+
case "system":
|
|
887
|
+
// Already handled in processMessages for init
|
|
888
|
+
break;
|
|
889
|
+
case "assistant":
|
|
890
|
+
if (this.pendingPermissions.size === 0) {
|
|
891
|
+
this.setStatus("running");
|
|
892
|
+
}
|
|
893
|
+
break;
|
|
894
|
+
case "user":
|
|
895
|
+
if (this.pendingPermissions.size === 0) {
|
|
896
|
+
this.setStatus("running");
|
|
897
|
+
}
|
|
898
|
+
break;
|
|
899
|
+
case "result":
|
|
900
|
+
this.pendingPermissions.clear();
|
|
901
|
+
this.setStatus("idle");
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
handlePostToolUseHook(input) {
|
|
906
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
const hookInput = input;
|
|
910
|
+
const toolName = hookInput.tool_name;
|
|
911
|
+
if (typeof toolName !== "string" || toolName.length === 0) {
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
this.toolCallsSinceLastResult += 1;
|
|
915
|
+
if (isFileEditToolName(toolName)) {
|
|
916
|
+
this.fileEditsSinceLastResult += 1;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
setStatus(status) {
|
|
920
|
+
if (this._status !== status) {
|
|
921
|
+
this._status = status;
|
|
922
|
+
this.emit("status", status);
|
|
923
|
+
this.emitMessage({ type: "status", status });
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
/** Emit session_end at most once per session lifecycle. */
|
|
927
|
+
emitSessionEnd() {
|
|
928
|
+
if (this.sessionEndEmitted)
|
|
929
|
+
return;
|
|
930
|
+
this.sessionEndEmitted = true;
|
|
931
|
+
this.emit("session_end");
|
|
932
|
+
}
|
|
933
|
+
emitMessage(msg) {
|
|
934
|
+
this.emit("message", msg);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
//# sourceMappingURL=sdk-process.js.map
|