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