@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.
Files changed (89) hide show
  1. package/README.md +67 -0
  2. package/dist/archive-store.d.ts +28 -0
  3. package/dist/archive-store.js +68 -0
  4. package/dist/archive-store.js.map +1 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +82 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/codex-process.d.ts +171 -0
  9. package/dist/codex-process.js +1928 -0
  10. package/dist/codex-process.js.map +1 -0
  11. package/dist/debug-trace-store.d.ts +15 -0
  12. package/dist/debug-trace-store.js +78 -0
  13. package/dist/debug-trace-store.js.map +1 -0
  14. package/dist/doctor.d.ts +58 -0
  15. package/dist/doctor.js +663 -0
  16. package/dist/doctor.js.map +1 -0
  17. package/dist/firebase-auth.d.ts +35 -0
  18. package/dist/firebase-auth.js +132 -0
  19. package/dist/firebase-auth.js.map +1 -0
  20. package/dist/gallery-store.d.ts +67 -0
  21. package/dist/gallery-store.js +333 -0
  22. package/dist/gallery-store.js.map +1 -0
  23. package/dist/image-store.d.ts +23 -0
  24. package/dist/image-store.js +142 -0
  25. package/dist/image-store.js.map +1 -0
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +191 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/mdns.d.ts +7 -0
  30. package/dist/mdns.js +49 -0
  31. package/dist/mdns.js.map +1 -0
  32. package/dist/parser.d.ts +465 -0
  33. package/dist/parser.js +251 -0
  34. package/dist/parser.js.map +1 -0
  35. package/dist/project-history.d.ts +10 -0
  36. package/dist/project-history.js +73 -0
  37. package/dist/project-history.js.map +1 -0
  38. package/dist/prompt-history-backup.d.ts +15 -0
  39. package/dist/prompt-history-backup.js +46 -0
  40. package/dist/prompt-history-backup.js.map +1 -0
  41. package/dist/proxy.d.ts +15 -0
  42. package/dist/proxy.js +95 -0
  43. package/dist/proxy.js.map +1 -0
  44. package/dist/push-i18n.d.ts +7 -0
  45. package/dist/push-i18n.js +75 -0
  46. package/dist/push-i18n.js.map +1 -0
  47. package/dist/push-relay.d.ts +29 -0
  48. package/dist/push-relay.js +70 -0
  49. package/dist/push-relay.js.map +1 -0
  50. package/dist/recording-store.d.ts +51 -0
  51. package/dist/recording-store.js +158 -0
  52. package/dist/recording-store.js.map +1 -0
  53. package/dist/screenshot.d.ts +28 -0
  54. package/dist/screenshot.js +98 -0
  55. package/dist/screenshot.js.map +1 -0
  56. package/dist/sdk-process.d.ts +180 -0
  57. package/dist/sdk-process.js +937 -0
  58. package/dist/sdk-process.js.map +1 -0
  59. package/dist/session.d.ts +142 -0
  60. package/dist/session.js +615 -0
  61. package/dist/session.js.map +1 -0
  62. package/dist/sessions-index.d.ts +128 -0
  63. package/dist/sessions-index.js +1767 -0
  64. package/dist/sessions-index.js.map +1 -0
  65. package/dist/setup-launchd.d.ts +8 -0
  66. package/dist/setup-launchd.js +109 -0
  67. package/dist/setup-launchd.js.map +1 -0
  68. package/dist/setup-systemd.d.ts +8 -0
  69. package/dist/setup-systemd.js +118 -0
  70. package/dist/setup-systemd.js.map +1 -0
  71. package/dist/startup-info.d.ts +8 -0
  72. package/dist/startup-info.js +92 -0
  73. package/dist/startup-info.js.map +1 -0
  74. package/dist/usage.d.ts +69 -0
  75. package/dist/usage.js +545 -0
  76. package/dist/usage.js.map +1 -0
  77. package/dist/version.d.ts +13 -0
  78. package/dist/version.js +43 -0
  79. package/dist/version.js.map +1 -0
  80. package/dist/websocket.d.ts +127 -0
  81. package/dist/websocket.js +2482 -0
  82. package/dist/websocket.js.map +1 -0
  83. package/dist/worktree-store.d.ts +25 -0
  84. package/dist/worktree-store.js +59 -0
  85. package/dist/worktree-store.js.map +1 -0
  86. package/dist/worktree.d.ts +47 -0
  87. package/dist/worktree.js +313 -0
  88. package/dist/worktree.js.map +1 -0
  89. 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