claude-code-rust 0.3.0 → 0.4.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.
@@ -0,0 +1,1224 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { spawn as spawnChild } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import readline from "node:readline";
5
+ import { pathToFileURL } from "node:url";
6
+ import { query, } from "@anthropic-ai/claude-agent-sdk";
7
+ import { parseCommandEnvelope, toPermissionMode, buildModeState } from "./bridge/commands.js";
8
+ import { asRecordOrNull } from "./bridge/shared.js";
9
+ import { looksLikeAuthRequired } from "./bridge/auth.js";
10
+ import { TOOL_RESULT_TYPES, buildToolResultFields, createToolCall, normalizeToolKind, normalizeToolResultText, unwrapToolUseResult, } from "./bridge/tooling.js";
11
+ import { buildUsageUpdateFromResult, buildUsageUpdateFromResultForSession } from "./bridge/usage.js";
12
+ import { formatPermissionUpdates, permissionOptionsFromSuggestions, permissionResultFromOutcome, } from "./bridge/permissions.js";
13
+ import { extractSessionHistoryUpdatesFromJsonl, listRecentPersistedSessions, resolvePersistedSessionEntry, } from "./bridge/history.js";
14
+ export { buildToolResultFields, buildUsageUpdateFromResult, createToolCall, extractSessionHistoryUpdatesFromJsonl, looksLikeAuthRequired, normalizeToolKind, normalizeToolResultText, parseCommandEnvelope, permissionOptionsFromSuggestions, permissionResultFromOutcome, unwrapToolUseResult, };
15
+ const sessions = new Map();
16
+ const permissionDebugEnabled = process.env.CLAUDE_RS_SDK_PERMISSION_DEBUG === "1" || process.env.CLAUDE_RS_SDK_DEBUG === "1";
17
+ function logPermissionDebug(message) {
18
+ if (!permissionDebugEnabled) {
19
+ return;
20
+ }
21
+ console.error(`[perm debug] ${message}`);
22
+ }
23
+ class AsyncQueue {
24
+ items = [];
25
+ waiters = [];
26
+ closed = false;
27
+ enqueue(item) {
28
+ if (this.closed) {
29
+ return;
30
+ }
31
+ const waiter = this.waiters.shift();
32
+ if (waiter) {
33
+ waiter({ value: item, done: false });
34
+ return;
35
+ }
36
+ this.items.push(item);
37
+ }
38
+ close() {
39
+ if (this.closed) {
40
+ return;
41
+ }
42
+ this.closed = true;
43
+ while (this.waiters.length > 0) {
44
+ const waiter = this.waiters.shift();
45
+ waiter?.({ value: undefined, done: true });
46
+ }
47
+ }
48
+ [Symbol.asyncIterator]() {
49
+ return {
50
+ next: async () => {
51
+ if (this.items.length > 0) {
52
+ const value = this.items.shift();
53
+ return { value: value, done: false };
54
+ }
55
+ if (this.closed) {
56
+ return { value: undefined, done: true };
57
+ }
58
+ return await new Promise((resolve) => {
59
+ this.waiters.push(resolve);
60
+ });
61
+ },
62
+ };
63
+ }
64
+ }
65
+ function writeEvent(event, requestId) {
66
+ const envelope = {
67
+ ...(requestId ? { request_id: requestId } : {}),
68
+ ...event,
69
+ };
70
+ process.stdout.write(`${JSON.stringify(envelope)}\n`);
71
+ }
72
+ function failConnection(message, requestId) {
73
+ writeEvent({ event: "connection_failed", message }, requestId);
74
+ }
75
+ function slashError(sessionId, message, requestId) {
76
+ writeEvent({ event: "slash_error", session_id: sessionId, message }, requestId);
77
+ }
78
+ function emitSessionUpdate(sessionId, update) {
79
+ writeEvent({ event: "session_update", session_id: sessionId, update });
80
+ }
81
+ function emitConnectEvent(session) {
82
+ const historyUpdates = session.resumeUpdates;
83
+ const connectEvent = session.connectEvent === "session_replaced"
84
+ ? {
85
+ event: "session_replaced",
86
+ session_id: session.sessionId,
87
+ cwd: session.cwd,
88
+ model_name: session.model,
89
+ mode: buildModeState(session.mode),
90
+ ...(historyUpdates && historyUpdates.length > 0 ? { history_updates: historyUpdates } : {}),
91
+ }
92
+ : {
93
+ event: "connected",
94
+ session_id: session.sessionId,
95
+ cwd: session.cwd,
96
+ model_name: session.model,
97
+ mode: buildModeState(session.mode),
98
+ ...(historyUpdates && historyUpdates.length > 0 ? { history_updates: historyUpdates } : {}),
99
+ };
100
+ writeEvent(connectEvent, session.connectRequestId);
101
+ session.connectRequestId = undefined;
102
+ session.connected = true;
103
+ session.authHintSent = false;
104
+ session.resumeUpdates = undefined;
105
+ const staleSessions = session.sessionsToCloseAfterConnect;
106
+ session.sessionsToCloseAfterConnect = undefined;
107
+ if (!staleSessions || staleSessions.length === 0) {
108
+ return;
109
+ }
110
+ void (async () => {
111
+ for (const stale of staleSessions) {
112
+ if (stale === session) {
113
+ continue;
114
+ }
115
+ if (sessions.get(stale.sessionId) === stale) {
116
+ sessions.delete(stale.sessionId);
117
+ }
118
+ await closeSession(stale);
119
+ }
120
+ })();
121
+ }
122
+ function textFromPrompt(command) {
123
+ const chunks = command.chunks ?? [];
124
+ return chunks
125
+ .map((chunk) => {
126
+ if (chunk.kind !== "text") {
127
+ return "";
128
+ }
129
+ return typeof chunk.value === "string" ? chunk.value : "";
130
+ })
131
+ .filter((part) => part.length > 0)
132
+ .join("");
133
+ }
134
+ function sessionById(sessionId) {
135
+ return sessions.get(sessionId) ?? null;
136
+ }
137
+ function updateSessionId(session, newSessionId) {
138
+ if (session.sessionId === newSessionId) {
139
+ return;
140
+ }
141
+ sessions.delete(session.sessionId);
142
+ session.sessionId = newSessionId;
143
+ sessions.set(newSessionId, session);
144
+ }
145
+ function emitToolCall(session, toolUseId, name, input) {
146
+ const toolCall = createToolCall(toolUseId, name, input);
147
+ const status = "in_progress";
148
+ toolCall.status = status;
149
+ const existing = session.toolCalls.get(toolUseId);
150
+ if (!existing) {
151
+ session.toolCalls.set(toolUseId, toolCall);
152
+ emitSessionUpdate(session.sessionId, { type: "tool_call", tool_call: toolCall });
153
+ return;
154
+ }
155
+ const fields = {
156
+ title: toolCall.title,
157
+ kind: toolCall.kind,
158
+ status,
159
+ raw_input: toolCall.raw_input,
160
+ locations: toolCall.locations,
161
+ meta: toolCall.meta,
162
+ };
163
+ if (toolCall.content.length > 0) {
164
+ fields.content = toolCall.content;
165
+ }
166
+ emitSessionUpdate(session.sessionId, {
167
+ type: "tool_call_update",
168
+ tool_call_update: { tool_call_id: toolUseId, fields },
169
+ });
170
+ existing.title = toolCall.title;
171
+ existing.kind = toolCall.kind;
172
+ existing.status = status;
173
+ existing.raw_input = toolCall.raw_input;
174
+ existing.locations = toolCall.locations;
175
+ existing.meta = toolCall.meta;
176
+ if (toolCall.content.length > 0) {
177
+ existing.content = toolCall.content;
178
+ }
179
+ }
180
+ function ensureToolCallVisible(session, toolUseId, toolName, input) {
181
+ const existing = session.toolCalls.get(toolUseId);
182
+ if (existing) {
183
+ return existing;
184
+ }
185
+ const toolCall = createToolCall(toolUseId, toolName, input);
186
+ session.toolCalls.set(toolUseId, toolCall);
187
+ emitSessionUpdate(session.sessionId, { type: "tool_call", tool_call: toolCall });
188
+ return toolCall;
189
+ }
190
+ function emitPlanIfTodoWrite(session, name, input) {
191
+ if (name !== "TodoWrite" || !Array.isArray(input.todos)) {
192
+ return;
193
+ }
194
+ const entries = input.todos
195
+ .map((todo) => {
196
+ if (!todo || typeof todo !== "object") {
197
+ return null;
198
+ }
199
+ const todoObj = todo;
200
+ const content = typeof todoObj.content === "string" ? todoObj.content : "";
201
+ const status = typeof todoObj.status === "string" ? todoObj.status : "pending";
202
+ if (!content) {
203
+ return null;
204
+ }
205
+ return { content, status, active_form: status };
206
+ })
207
+ .filter((entry) => entry !== null);
208
+ if (entries.length > 0) {
209
+ emitSessionUpdate(session.sessionId, { type: "plan", entries });
210
+ }
211
+ }
212
+ function emitToolResultUpdate(session, toolUseId, isError, rawContent) {
213
+ const base = session.toolCalls.get(toolUseId);
214
+ const fields = buildToolResultFields(isError, rawContent, base);
215
+ const update = { tool_call_id: toolUseId, fields };
216
+ emitSessionUpdate(session.sessionId, { type: "tool_call_update", tool_call_update: update });
217
+ if (base) {
218
+ base.status = fields.status ?? base.status;
219
+ if (fields.raw_output) {
220
+ base.raw_output = fields.raw_output;
221
+ }
222
+ if (fields.content) {
223
+ base.content = fields.content;
224
+ }
225
+ }
226
+ }
227
+ function finalizeOpenToolCalls(session, status) {
228
+ for (const [toolUseId, toolCall] of session.toolCalls) {
229
+ if (toolCall.status !== "pending" && toolCall.status !== "in_progress") {
230
+ continue;
231
+ }
232
+ const fields = { status };
233
+ emitSessionUpdate(session.sessionId, {
234
+ type: "tool_call_update",
235
+ tool_call_update: { tool_call_id: toolUseId, fields },
236
+ });
237
+ toolCall.status = status;
238
+ }
239
+ }
240
+ function emitToolProgressUpdate(session, toolUseId, toolName) {
241
+ const existing = session.toolCalls.get(toolUseId);
242
+ if (!existing) {
243
+ emitToolCall(session, toolUseId, toolName, {});
244
+ return;
245
+ }
246
+ if (existing.status === "in_progress") {
247
+ return;
248
+ }
249
+ const fields = { status: "in_progress" };
250
+ emitSessionUpdate(session.sessionId, {
251
+ type: "tool_call_update",
252
+ tool_call_update: { tool_call_id: toolUseId, fields },
253
+ });
254
+ existing.status = "in_progress";
255
+ }
256
+ function emitToolSummaryUpdate(session, toolUseId, summary) {
257
+ const base = session.toolCalls.get(toolUseId);
258
+ if (!base) {
259
+ return;
260
+ }
261
+ const fields = {
262
+ status: base.status === "failed" ? "failed" : "completed",
263
+ raw_output: summary,
264
+ content: [{ type: "content", content: { type: "text", text: summary } }],
265
+ };
266
+ emitSessionUpdate(session.sessionId, {
267
+ type: "tool_call_update",
268
+ tool_call_update: { tool_call_id: toolUseId, fields },
269
+ });
270
+ base.status = fields.status ?? base.status;
271
+ base.raw_output = summary;
272
+ }
273
+ function setToolCallStatus(session, toolUseId, status, message) {
274
+ const base = session.toolCalls.get(toolUseId);
275
+ if (!base) {
276
+ return;
277
+ }
278
+ const fields = { status };
279
+ if (message && message.length > 0) {
280
+ fields.raw_output = message;
281
+ fields.content = [{ type: "content", content: { type: "text", text: message } }];
282
+ }
283
+ emitSessionUpdate(session.sessionId, {
284
+ type: "tool_call_update",
285
+ tool_call_update: { tool_call_id: toolUseId, fields },
286
+ });
287
+ base.status = status;
288
+ if (fields.raw_output) {
289
+ base.raw_output = fields.raw_output;
290
+ }
291
+ }
292
+ function resolveTaskToolUseId(session, msg) {
293
+ const direct = typeof msg.tool_use_id === "string" ? msg.tool_use_id : "";
294
+ if (direct) {
295
+ return direct;
296
+ }
297
+ const taskId = typeof msg.task_id === "string" ? msg.task_id : "";
298
+ if (!taskId) {
299
+ return "";
300
+ }
301
+ return session.taskToolUseIds.get(taskId) ?? "";
302
+ }
303
+ function taskProgressText(msg) {
304
+ const description = typeof msg.description === "string" ? msg.description : "";
305
+ const lastTool = typeof msg.last_tool_name === "string" ? msg.last_tool_name : "";
306
+ if (description && lastTool) {
307
+ return `${description} (last tool: ${lastTool})`;
308
+ }
309
+ return description || lastTool;
310
+ }
311
+ function handleTaskSystemMessage(session, subtype, msg) {
312
+ if (subtype !== "task_started" && subtype !== "task_progress" && subtype !== "task_notification") {
313
+ return;
314
+ }
315
+ const taskId = typeof msg.task_id === "string" ? msg.task_id : "";
316
+ const explicitToolUseId = typeof msg.tool_use_id === "string" ? msg.tool_use_id : "";
317
+ if (taskId && explicitToolUseId) {
318
+ session.taskToolUseIds.set(taskId, explicitToolUseId);
319
+ }
320
+ const toolUseId = resolveTaskToolUseId(session, msg);
321
+ if (!toolUseId) {
322
+ return;
323
+ }
324
+ const toolCall = ensureToolCallVisible(session, toolUseId, "Task", {});
325
+ if (toolCall.status === "pending") {
326
+ toolCall.status = "in_progress";
327
+ emitSessionUpdate(session.sessionId, {
328
+ type: "tool_call_update",
329
+ tool_call_update: { tool_call_id: toolUseId, fields: { status: "in_progress" } },
330
+ });
331
+ }
332
+ if (subtype === "task_started") {
333
+ const description = typeof msg.description === "string" ? msg.description : "";
334
+ if (!description) {
335
+ return;
336
+ }
337
+ emitSessionUpdate(session.sessionId, {
338
+ type: "tool_call_update",
339
+ tool_call_update: {
340
+ tool_call_id: toolUseId,
341
+ fields: {
342
+ status: "in_progress",
343
+ raw_output: description,
344
+ content: [{ type: "content", content: { type: "text", text: description } }],
345
+ },
346
+ },
347
+ });
348
+ return;
349
+ }
350
+ if (subtype === "task_progress") {
351
+ const progress = taskProgressText(msg);
352
+ if (!progress) {
353
+ return;
354
+ }
355
+ emitSessionUpdate(session.sessionId, {
356
+ type: "tool_call_update",
357
+ tool_call_update: {
358
+ tool_call_id: toolUseId,
359
+ fields: {
360
+ status: "in_progress",
361
+ raw_output: progress,
362
+ content: [{ type: "content", content: { type: "text", text: progress } }],
363
+ },
364
+ },
365
+ });
366
+ return;
367
+ }
368
+ const status = typeof msg.status === "string" ? msg.status : "";
369
+ const summary = typeof msg.summary === "string" ? msg.summary : "";
370
+ const finalStatus = status === "completed" ? "completed" : "failed";
371
+ const fields = { status: finalStatus };
372
+ if (summary) {
373
+ fields.raw_output = summary;
374
+ fields.content = [{ type: "content", content: { type: "text", text: summary } }];
375
+ }
376
+ emitSessionUpdate(session.sessionId, {
377
+ type: "tool_call_update",
378
+ tool_call_update: { tool_call_id: toolUseId, fields },
379
+ });
380
+ toolCall.status = finalStatus;
381
+ if (taskId) {
382
+ session.taskToolUseIds.delete(taskId);
383
+ }
384
+ }
385
+ function handleContentBlock(session, block) {
386
+ const blockType = typeof block.type === "string" ? block.type : "";
387
+ if (blockType === "text") {
388
+ const text = typeof block.text === "string" ? block.text : "";
389
+ if (text) {
390
+ emitSessionUpdate(session.sessionId, { type: "agent_message_chunk", content: { type: "text", text } });
391
+ }
392
+ return;
393
+ }
394
+ if (blockType === "thinking") {
395
+ const text = typeof block.thinking === "string" ? block.thinking : "";
396
+ if (text) {
397
+ emitSessionUpdate(session.sessionId, { type: "agent_thought_chunk", content: { type: "text", text } });
398
+ }
399
+ return;
400
+ }
401
+ if (blockType === "tool_use" || blockType === "server_tool_use" || blockType === "mcp_tool_use") {
402
+ const toolUseId = typeof block.id === "string" ? block.id : "";
403
+ const name = typeof block.name === "string" ? block.name : "Tool";
404
+ const input = block.input && typeof block.input === "object" ? block.input : {};
405
+ if (!toolUseId) {
406
+ return;
407
+ }
408
+ emitPlanIfTodoWrite(session, name, input);
409
+ emitToolCall(session, toolUseId, name, input);
410
+ return;
411
+ }
412
+ if (TOOL_RESULT_TYPES.has(blockType)) {
413
+ const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id : "";
414
+ if (!toolUseId) {
415
+ return;
416
+ }
417
+ const isError = Boolean(block.is_error);
418
+ emitToolResultUpdate(session, toolUseId, isError, block.content);
419
+ }
420
+ }
421
+ function handleStreamEvent(session, event) {
422
+ const eventType = typeof event.type === "string" ? event.type : "";
423
+ if (eventType === "content_block_start") {
424
+ if (event.content_block && typeof event.content_block === "object") {
425
+ handleContentBlock(session, event.content_block);
426
+ }
427
+ return;
428
+ }
429
+ if (eventType === "content_block_delta") {
430
+ if (!event.delta || typeof event.delta !== "object") {
431
+ return;
432
+ }
433
+ const delta = event.delta;
434
+ const deltaType = typeof delta.type === "string" ? delta.type : "";
435
+ if (deltaType === "text_delta") {
436
+ const text = typeof delta.text === "string" ? delta.text : "";
437
+ if (text) {
438
+ emitSessionUpdate(session.sessionId, { type: "agent_message_chunk", content: { type: "text", text } });
439
+ }
440
+ }
441
+ else if (deltaType === "thinking_delta") {
442
+ const text = typeof delta.thinking === "string" ? delta.thinking : "";
443
+ if (text) {
444
+ emitSessionUpdate(session.sessionId, { type: "agent_thought_chunk", content: { type: "text", text } });
445
+ }
446
+ }
447
+ }
448
+ }
449
+ function handleAssistantMessage(session, message) {
450
+ const messageObject = message.message && typeof message.message === "object"
451
+ ? message.message
452
+ : null;
453
+ if (!messageObject) {
454
+ return;
455
+ }
456
+ const content = Array.isArray(messageObject.content) ? messageObject.content : [];
457
+ for (const block of content) {
458
+ if (!block || typeof block !== "object") {
459
+ continue;
460
+ }
461
+ const blockRecord = block;
462
+ const blockType = typeof blockRecord.type === "string" ? blockRecord.type : "";
463
+ if (blockType === "tool_use" ||
464
+ blockType === "server_tool_use" ||
465
+ blockType === "mcp_tool_use" ||
466
+ TOOL_RESULT_TYPES.has(blockType)) {
467
+ handleContentBlock(session, blockRecord);
468
+ }
469
+ }
470
+ }
471
+ function handleUserToolResultBlocks(session, message) {
472
+ const messageObject = message.message && typeof message.message === "object"
473
+ ? message.message
474
+ : null;
475
+ if (!messageObject) {
476
+ return;
477
+ }
478
+ const content = Array.isArray(messageObject.content) ? messageObject.content : [];
479
+ for (const block of content) {
480
+ if (!block || typeof block !== "object") {
481
+ continue;
482
+ }
483
+ const blockRecord = block;
484
+ const blockType = typeof blockRecord.type === "string" ? blockRecord.type : "";
485
+ if (TOOL_RESULT_TYPES.has(blockType)) {
486
+ handleContentBlock(session, blockRecord);
487
+ }
488
+ }
489
+ }
490
+ function emitAuthRequired(session, detail) {
491
+ if (session.authHintSent) {
492
+ return;
493
+ }
494
+ session.authHintSent = true;
495
+ writeEvent({
496
+ event: "auth_required",
497
+ method_name: "Claude Login",
498
+ method_description: detail && detail.trim().length > 0
499
+ ? detail
500
+ : "Run `claude /login` in a terminal, then retry.",
501
+ });
502
+ }
503
+ function numberField(record, ...keys) {
504
+ for (const key of keys) {
505
+ const value = record[key];
506
+ if (typeof value === "number" && Number.isFinite(value)) {
507
+ return value;
508
+ }
509
+ }
510
+ return undefined;
511
+ }
512
+ function handleResultMessage(session, message) {
513
+ const usageUpdate = buildUsageUpdateFromResultForSession(session, message);
514
+ if (usageUpdate) {
515
+ emitSessionUpdate(session.sessionId, usageUpdate);
516
+ }
517
+ const subtype = typeof message.subtype === "string" ? message.subtype : "";
518
+ if (subtype === "success") {
519
+ finalizeOpenToolCalls(session, "completed");
520
+ writeEvent({ event: "turn_complete", session_id: session.sessionId });
521
+ return;
522
+ }
523
+ const errors = Array.isArray(message.errors) && message.errors.every((entry) => typeof entry === "string")
524
+ ? message.errors
525
+ : [];
526
+ const authHint = errors.find((entry) => looksLikeAuthRequired(entry));
527
+ if (authHint) {
528
+ emitAuthRequired(session, authHint);
529
+ }
530
+ finalizeOpenToolCalls(session, "failed");
531
+ const fallback = subtype ? `turn failed: ${subtype}` : "turn failed";
532
+ writeEvent({
533
+ event: "turn_error",
534
+ session_id: session.sessionId,
535
+ message: errors.length > 0 ? errors.join("\n") : fallback,
536
+ });
537
+ }
538
+ function handleSdkMessage(session, message) {
539
+ const msg = message;
540
+ const type = typeof msg.type === "string" ? msg.type : "";
541
+ if (type === "system") {
542
+ const subtype = typeof msg.subtype === "string" ? msg.subtype : "";
543
+ if (subtype === "init") {
544
+ const previousSessionId = session.sessionId;
545
+ const incomingSessionId = typeof msg.session_id === "string" ? msg.session_id : session.sessionId;
546
+ updateSessionId(session, incomingSessionId);
547
+ const modelName = typeof msg.model === "string" ? msg.model : session.model;
548
+ session.model = modelName;
549
+ const incomingMode = typeof msg.permissionMode === "string" ? toPermissionMode(msg.permissionMode) : null;
550
+ if (incomingMode) {
551
+ session.mode = incomingMode;
552
+ }
553
+ if (!session.connected) {
554
+ emitConnectEvent(session);
555
+ }
556
+ else if (previousSessionId !== session.sessionId) {
557
+ const historyUpdates = session.resumeUpdates;
558
+ writeEvent({
559
+ event: "session_replaced",
560
+ session_id: session.sessionId,
561
+ cwd: session.cwd,
562
+ model_name: session.model,
563
+ mode: buildModeState(session.mode),
564
+ ...(historyUpdates && historyUpdates.length > 0
565
+ ? { history_updates: historyUpdates }
566
+ : {}),
567
+ });
568
+ session.resumeUpdates = undefined;
569
+ }
570
+ if (Array.isArray(msg.slash_commands)) {
571
+ const commands = msg.slash_commands
572
+ .filter((entry) => typeof entry === "string")
573
+ .map((name) => ({ name, description: "", input_hint: undefined }));
574
+ if (commands.length > 0) {
575
+ emitSessionUpdate(session.sessionId, { type: "available_commands_update", commands });
576
+ }
577
+ }
578
+ void session.query
579
+ .supportedCommands()
580
+ .then((commands) => {
581
+ const mapped = commands.map((command) => ({
582
+ name: command.name,
583
+ description: command.description ?? "",
584
+ input_hint: command.argumentHint ?? undefined,
585
+ }));
586
+ emitSessionUpdate(session.sessionId, { type: "available_commands_update", commands: mapped });
587
+ })
588
+ .catch(() => {
589
+ // Best-effort only; slash commands from init were already emitted.
590
+ });
591
+ return;
592
+ }
593
+ if (subtype === "status") {
594
+ const mode = typeof msg.permissionMode === "string" ? toPermissionMode(msg.permissionMode) : null;
595
+ if (mode) {
596
+ session.mode = mode;
597
+ emitSessionUpdate(session.sessionId, { type: "current_mode_update", current_mode_id: mode });
598
+ }
599
+ if (msg.status === "compacting") {
600
+ emitSessionUpdate(session.sessionId, { type: "session_status_update", status: "compacting" });
601
+ }
602
+ else if (msg.status === null) {
603
+ emitSessionUpdate(session.sessionId, { type: "session_status_update", status: "idle" });
604
+ }
605
+ return;
606
+ }
607
+ if (subtype === "compact_boundary") {
608
+ const compactMetadata = asRecordOrNull(msg.compact_metadata);
609
+ if (!compactMetadata) {
610
+ return;
611
+ }
612
+ const trigger = compactMetadata.trigger;
613
+ const preTokens = numberField(compactMetadata, "pre_tokens", "preTokens");
614
+ if ((trigger === "manual" || trigger === "auto") && preTokens !== undefined) {
615
+ emitSessionUpdate(session.sessionId, {
616
+ type: "compaction_boundary",
617
+ trigger,
618
+ pre_tokens: preTokens,
619
+ });
620
+ }
621
+ return;
622
+ }
623
+ handleTaskSystemMessage(session, subtype, msg);
624
+ return;
625
+ }
626
+ if (type === "auth_status") {
627
+ const output = Array.isArray(msg.output)
628
+ ? msg.output.filter((entry) => typeof entry === "string").join("\n")
629
+ : "";
630
+ const errorText = typeof msg.error === "string" ? msg.error : "";
631
+ const combined = [errorText, output].filter((entry) => entry.length > 0).join("\n");
632
+ if (combined && looksLikeAuthRequired(combined)) {
633
+ emitAuthRequired(session, combined);
634
+ }
635
+ return;
636
+ }
637
+ if (type === "stream_event") {
638
+ if (msg.event && typeof msg.event === "object") {
639
+ handleStreamEvent(session, msg.event);
640
+ }
641
+ return;
642
+ }
643
+ if (type === "tool_progress") {
644
+ const toolUseId = typeof msg.tool_use_id === "string" ? msg.tool_use_id : "";
645
+ const toolName = typeof msg.tool_name === "string" ? msg.tool_name : "Tool";
646
+ if (toolUseId) {
647
+ emitToolProgressUpdate(session, toolUseId, toolName);
648
+ }
649
+ return;
650
+ }
651
+ if (type === "tool_use_summary") {
652
+ const summary = typeof msg.summary === "string" ? msg.summary : "";
653
+ const toolIds = Array.isArray(msg.preceding_tool_use_ids)
654
+ ? msg.preceding_tool_use_ids.filter((id) => typeof id === "string")
655
+ : [];
656
+ if (summary && toolIds.length > 0) {
657
+ for (const toolUseId of toolIds) {
658
+ emitToolSummaryUpdate(session, toolUseId, summary);
659
+ }
660
+ }
661
+ return;
662
+ }
663
+ if (type === "user") {
664
+ handleUserToolResultBlocks(session, msg);
665
+ const toolUseId = typeof msg.parent_tool_use_id === "string" ? msg.parent_tool_use_id : "";
666
+ if (toolUseId && "tool_use_result" in msg) {
667
+ const parsed = unwrapToolUseResult(msg.tool_use_result);
668
+ emitToolResultUpdate(session, toolUseId, parsed.isError, parsed.content);
669
+ }
670
+ return;
671
+ }
672
+ if (type === "assistant") {
673
+ if (msg.error === "authentication_failed") {
674
+ emitAuthRequired(session);
675
+ }
676
+ handleAssistantMessage(session, msg);
677
+ return;
678
+ }
679
+ if (type === "result") {
680
+ handleResultMessage(session, msg);
681
+ }
682
+ }
683
+ const ASK_USER_QUESTION_TOOL_NAME = "AskUserQuestion";
684
+ const QUESTION_CHOICE_KIND = "question_choice";
685
+ function parseAskUserQuestionPrompts(inputData) {
686
+ const rawQuestions = Array.isArray(inputData.questions) ? inputData.questions : [];
687
+ const prompts = [];
688
+ for (const rawQuestion of rawQuestions) {
689
+ const questionRecord = asRecordOrNull(rawQuestion);
690
+ if (!questionRecord) {
691
+ continue;
692
+ }
693
+ const question = typeof questionRecord.question === "string" ? questionRecord.question.trim() : "";
694
+ if (!question) {
695
+ continue;
696
+ }
697
+ const headerRaw = typeof questionRecord.header === "string" ? questionRecord.header.trim() : "";
698
+ const header = headerRaw || `Q${prompts.length + 1}`;
699
+ const multiSelect = Boolean(questionRecord.multiSelect);
700
+ const rawOptions = Array.isArray(questionRecord.options) ? questionRecord.options : [];
701
+ const options = [];
702
+ for (const rawOption of rawOptions) {
703
+ const optionRecord = asRecordOrNull(rawOption);
704
+ if (!optionRecord) {
705
+ continue;
706
+ }
707
+ const label = typeof optionRecord.label === "string" ? optionRecord.label.trim() : "";
708
+ const description = typeof optionRecord.description === "string" ? optionRecord.description.trim() : "";
709
+ if (!label) {
710
+ continue;
711
+ }
712
+ options.push({ label, description });
713
+ }
714
+ if (options.length < 2) {
715
+ continue;
716
+ }
717
+ prompts.push({ question, header, multiSelect, options });
718
+ }
719
+ return prompts;
720
+ }
721
+ function askUserQuestionOptions(prompt) {
722
+ return prompt.options.map((option, index) => ({
723
+ option_id: `question_${index}`,
724
+ name: option.label,
725
+ description: option.description,
726
+ kind: QUESTION_CHOICE_KIND,
727
+ }));
728
+ }
729
+ function askUserQuestionPromptToolCall(base, prompt, index, total) {
730
+ return {
731
+ ...base,
732
+ title: prompt.question,
733
+ raw_input: {
734
+ questions: [
735
+ {
736
+ question: prompt.question,
737
+ header: prompt.header,
738
+ multiSelect: prompt.multiSelect,
739
+ options: prompt.options,
740
+ },
741
+ ],
742
+ question_index: index,
743
+ total_questions: total,
744
+ },
745
+ };
746
+ }
747
+ function askUserQuestionTranscript(answers) {
748
+ return answers.map((entry) => `${entry.header}: ${entry.answer}\n ${entry.question}`).join("\n");
749
+ }
750
+ async function requestAskUserQuestionAnswers(session, toolUseId, toolName, inputData, baseToolCall) {
751
+ const prompts = parseAskUserQuestionPrompts(inputData);
752
+ if (prompts.length === 0) {
753
+ return { behavior: "allow", updatedInput: inputData, toolUseID: toolUseId };
754
+ }
755
+ const answers = {};
756
+ const transcript = [];
757
+ for (const [index, prompt] of prompts.entries()) {
758
+ const promptToolCall = askUserQuestionPromptToolCall(baseToolCall, prompt, index, prompts.length);
759
+ const fields = {
760
+ title: promptToolCall.title,
761
+ status: "in_progress",
762
+ raw_input: promptToolCall.raw_input,
763
+ };
764
+ emitSessionUpdate(session.sessionId, {
765
+ type: "tool_call_update",
766
+ tool_call_update: { tool_call_id: toolUseId, fields },
767
+ });
768
+ const tracked = session.toolCalls.get(toolUseId);
769
+ if (tracked) {
770
+ tracked.title = promptToolCall.title;
771
+ tracked.status = "in_progress";
772
+ tracked.raw_input = promptToolCall.raw_input;
773
+ }
774
+ const request = {
775
+ tool_call: promptToolCall,
776
+ options: askUserQuestionOptions(prompt),
777
+ };
778
+ const outcome = await new Promise((resolve) => {
779
+ session.pendingPermissions.set(toolUseId, {
780
+ onOutcome: resolve,
781
+ toolName,
782
+ inputData,
783
+ });
784
+ writeEvent({ event: "permission_request", session_id: session.sessionId, request });
785
+ });
786
+ if (outcome.outcome !== "selected") {
787
+ setToolCallStatus(session, toolUseId, "failed", "Question cancelled");
788
+ return { behavior: "deny", message: "Question cancelled", toolUseID: toolUseId };
789
+ }
790
+ const selected = request.options.find((option) => option.option_id === outcome.option_id);
791
+ if (!selected) {
792
+ setToolCallStatus(session, toolUseId, "failed", "Question answer was invalid");
793
+ return { behavior: "deny", message: "Question answer was invalid", toolUseID: toolUseId };
794
+ }
795
+ answers[prompt.question] = selected.name;
796
+ transcript.push({ header: prompt.header, question: prompt.question, answer: selected.name });
797
+ const summary = askUserQuestionTranscript(transcript);
798
+ const progressFields = {
799
+ status: index + 1 >= prompts.length ? "completed" : "in_progress",
800
+ raw_output: summary,
801
+ content: [{ type: "content", content: { type: "text", text: summary } }],
802
+ };
803
+ emitSessionUpdate(session.sessionId, {
804
+ type: "tool_call_update",
805
+ tool_call_update: { tool_call_id: toolUseId, fields: progressFields },
806
+ });
807
+ if (tracked) {
808
+ tracked.status = progressFields.status ?? tracked.status;
809
+ tracked.raw_output = summary;
810
+ tracked.content = progressFields.content ?? tracked.content;
811
+ }
812
+ }
813
+ return {
814
+ behavior: "allow",
815
+ updatedInput: { ...inputData, answers },
816
+ toolUseID: toolUseId,
817
+ };
818
+ }
819
+ async function closeSession(session) {
820
+ session.input.close();
821
+ session.query.close();
822
+ for (const pending of session.pendingPermissions.values()) {
823
+ pending.resolve?.({ behavior: "deny", message: "Session closed" });
824
+ pending.onOutcome?.({ outcome: "cancelled" });
825
+ }
826
+ session.pendingPermissions.clear();
827
+ }
828
+ async function closeAllSessions() {
829
+ const active = Array.from(sessions.values());
830
+ sessions.clear();
831
+ await Promise.all(active.map((session) => closeSession(session)));
832
+ }
833
+ async function createSession(params) {
834
+ const input = new AsyncQueue();
835
+ const startMode = params.yolo ? "bypassPermissions" : "default";
836
+ const provisionalSessionId = params.resume ?? randomUUID();
837
+ let session;
838
+ const canUseTool = async (toolName, inputData, options) => {
839
+ const toolUseId = options.toolUseID;
840
+ if (toolName === "ExitPlanMode") {
841
+ return { behavior: "allow", toolUseID: toolUseId };
842
+ }
843
+ logPermissionDebug(`request tool_use_id=${toolUseId} tool=${toolName} blocked_path=${options.blockedPath ?? "<none>"} ` +
844
+ `decision_reason=${options.decisionReason ?? "<none>"} suggestions=${formatPermissionUpdates(options.suggestions)}`);
845
+ const existing = ensureToolCallVisible(session, toolUseId, toolName, inputData);
846
+ if (toolName === ASK_USER_QUESTION_TOOL_NAME) {
847
+ return await requestAskUserQuestionAnswers(session, toolUseId, toolName, inputData, existing);
848
+ }
849
+ const request = {
850
+ tool_call: existing,
851
+ options: permissionOptionsFromSuggestions(options.suggestions),
852
+ };
853
+ writeEvent({ event: "permission_request", session_id: session.sessionId, request });
854
+ return await new Promise((resolve) => {
855
+ session.pendingPermissions.set(toolUseId, {
856
+ resolve,
857
+ toolName,
858
+ inputData: inputData,
859
+ suggestions: options.suggestions,
860
+ });
861
+ });
862
+ };
863
+ const claudeCodeExecutable = process.env.CLAUDE_CODE_EXECUTABLE;
864
+ const sdkDebugFile = process.env.CLAUDE_RS_SDK_DEBUG_FILE;
865
+ const enableSdkDebug = process.env.CLAUDE_RS_SDK_DEBUG === "1" || Boolean(sdkDebugFile);
866
+ const enableSpawnDebug = process.env.CLAUDE_RS_SDK_SPAWN_DEBUG === "1";
867
+ if (claudeCodeExecutable && !fs.existsSync(claudeCodeExecutable)) {
868
+ throw new Error(`CLAUDE_CODE_EXECUTABLE does not exist: ${claudeCodeExecutable}`);
869
+ }
870
+ let queryHandle;
871
+ try {
872
+ queryHandle = query({
873
+ prompt: input,
874
+ options: {
875
+ cwd: params.cwd,
876
+ includePartialMessages: true,
877
+ executable: "node",
878
+ ...(params.resume ? {} : { sessionId: provisionalSessionId }),
879
+ ...(claudeCodeExecutable
880
+ ? { pathToClaudeCodeExecutable: claudeCodeExecutable }
881
+ : {}),
882
+ ...(enableSdkDebug ? { debug: true } : {}),
883
+ ...(sdkDebugFile ? { debugFile: sdkDebugFile } : {}),
884
+ stderr: (line) => {
885
+ if (line.trim().length > 0) {
886
+ console.error(`[sdk stderr] ${line}`);
887
+ }
888
+ },
889
+ ...(enableSpawnDebug
890
+ ? {
891
+ spawnClaudeCodeProcess: (options) => {
892
+ console.error(`[sdk spawn] command=${options.command} args=${JSON.stringify(options.args)} cwd=${options.cwd ?? "<none>"}`);
893
+ const child = spawnChild(options.command, options.args, {
894
+ cwd: options.cwd,
895
+ env: options.env,
896
+ signal: options.signal,
897
+ stdio: ["pipe", "pipe", "pipe"],
898
+ windowsHide: true,
899
+ });
900
+ child.on("error", (error) => {
901
+ console.error(`[sdk spawn error] code=${error.code ?? "<none>"} message=${error.message}`);
902
+ });
903
+ return child;
904
+ },
905
+ }
906
+ : {}),
907
+ // Match claude-agent-acp defaults to avoid emitting an empty
908
+ // --setting-sources argument.
909
+ settingSources: ["user", "project", "local"],
910
+ permissionMode: startMode,
911
+ allowDangerouslySkipPermissions: params.yolo,
912
+ resume: params.resume,
913
+ model: params.model,
914
+ canUseTool,
915
+ },
916
+ });
917
+ }
918
+ catch (error) {
919
+ const message = error instanceof Error ? error.message : String(error);
920
+ throw new Error(`query() failed: node_executable=${process.execPath}; cwd=${params.cwd}; ` +
921
+ `resume=${params.resume ?? "<none>"}; model=${params.model ?? "<none>"}; ` +
922
+ `CLAUDE_CODE_EXECUTABLE=${claudeCodeExecutable ?? "<unset>"}; error=${message}`);
923
+ }
924
+ session = {
925
+ sessionId: provisionalSessionId,
926
+ cwd: params.cwd,
927
+ model: params.model ?? "default",
928
+ mode: startMode,
929
+ yolo: params.yolo,
930
+ query: queryHandle,
931
+ input,
932
+ connected: false,
933
+ connectEvent: params.connectEvent,
934
+ connectRequestId: params.requestId,
935
+ toolCalls: new Map(),
936
+ taskToolUseIds: new Map(),
937
+ pendingPermissions: new Map(),
938
+ authHintSent: false,
939
+ ...(params.resumeUpdates && params.resumeUpdates.length > 0
940
+ ? { resumeUpdates: params.resumeUpdates }
941
+ : {}),
942
+ ...(params.sessionsToCloseAfterConnect
943
+ ? { sessionsToCloseAfterConnect: params.sessionsToCloseAfterConnect }
944
+ : {}),
945
+ };
946
+ sessions.set(provisionalSessionId, session);
947
+ // In stream-input mode the SDK may defer init until input arrives.
948
+ // Trigger initialization explicitly so the Rust UI can receive `connected`
949
+ // before the first user prompt.
950
+ void session.query
951
+ .initializationResult()
952
+ .then((result) => {
953
+ if (!session.connected) {
954
+ emitConnectEvent(session);
955
+ }
956
+ const commands = Array.isArray(result.commands)
957
+ ? result.commands.map((command) => ({
958
+ name: command.name,
959
+ description: command.description ?? "",
960
+ input_hint: command.argumentHint ?? undefined,
961
+ }))
962
+ : [];
963
+ if (commands.length > 0) {
964
+ emitSessionUpdate(session.sessionId, { type: "available_commands_update", commands });
965
+ }
966
+ })
967
+ .catch((error) => {
968
+ if (session.connected) {
969
+ return;
970
+ }
971
+ const message = error instanceof Error ? error.message : String(error);
972
+ failConnection(`agent initialization failed: ${message}`, session.connectRequestId);
973
+ session.connectRequestId = undefined;
974
+ });
975
+ void (async () => {
976
+ try {
977
+ for await (const message of session.query) {
978
+ handleSdkMessage(session, message);
979
+ }
980
+ if (!session.connected) {
981
+ failConnection("agent stream ended before session initialization", params.requestId);
982
+ }
983
+ }
984
+ catch (error) {
985
+ const message = error instanceof Error ? error.message : String(error);
986
+ failConnection(`agent stream failed: ${message}`, params.requestId);
987
+ }
988
+ })();
989
+ }
990
+ function handlePermissionResponse(command) {
991
+ const session = sessionById(command.session_id);
992
+ if (!session) {
993
+ logPermissionDebug(`response dropped: unknown session session_id=${command.session_id} tool_call_id=${command.tool_call_id}`);
994
+ return;
995
+ }
996
+ const resolver = session.pendingPermissions.get(command.tool_call_id);
997
+ if (!resolver) {
998
+ logPermissionDebug(`response dropped: no pending resolver session_id=${command.session_id} tool_call_id=${command.tool_call_id}`);
999
+ return;
1000
+ }
1001
+ session.pendingPermissions.delete(command.tool_call_id);
1002
+ const outcome = command.outcome;
1003
+ if (resolver.onOutcome) {
1004
+ resolver.onOutcome(outcome);
1005
+ return;
1006
+ }
1007
+ if (!resolver.resolve) {
1008
+ logPermissionDebug(`response dropped: resolver missing callback session_id=${command.session_id} tool_call_id=${command.tool_call_id}`);
1009
+ return;
1010
+ }
1011
+ const selectedOption = outcome.outcome === "selected" ? outcome.option_id : "cancelled";
1012
+ logPermissionDebug(`response session_id=${command.session_id} tool_call_id=${command.tool_call_id} tool=${resolver.toolName} ` +
1013
+ `selected=${selectedOption} suggestions=${formatPermissionUpdates(resolver.suggestions)}`);
1014
+ if (outcome.outcome === "selected" &&
1015
+ (outcome.option_id === "allow_once" ||
1016
+ outcome.option_id === "allow_session" ||
1017
+ outcome.option_id === "allow_always")) {
1018
+ setToolCallStatus(session, command.tool_call_id, "in_progress");
1019
+ }
1020
+ else if (outcome.outcome === "selected") {
1021
+ setToolCallStatus(session, command.tool_call_id, "failed", "Permission denied");
1022
+ }
1023
+ else {
1024
+ setToolCallStatus(session, command.tool_call_id, "failed", "Permission cancelled");
1025
+ }
1026
+ const permissionResult = permissionResultFromOutcome(outcome, command.tool_call_id, resolver.inputData, resolver.suggestions, resolver.toolName);
1027
+ if (permissionResult.behavior === "allow") {
1028
+ logPermissionDebug(`result tool_call_id=${command.tool_call_id} behavior=allow updated_permissions=` +
1029
+ `${formatPermissionUpdates(permissionResult.updatedPermissions)}`);
1030
+ }
1031
+ else {
1032
+ logPermissionDebug(`result tool_call_id=${command.tool_call_id} behavior=deny message=${permissionResult.message}`);
1033
+ }
1034
+ resolver.resolve(permissionResult);
1035
+ }
1036
+ async function handleCommand(command, requestId) {
1037
+ switch (command.command) {
1038
+ case "initialize":
1039
+ writeEvent({
1040
+ event: "initialized",
1041
+ result: {
1042
+ agent_name: "claude-rs-agent-bridge",
1043
+ agent_version: "0.1.0",
1044
+ auth_methods: [
1045
+ {
1046
+ id: "claude-login",
1047
+ name: "Log in with Claude",
1048
+ description: "Run `claude /login` in a terminal",
1049
+ },
1050
+ ],
1051
+ capabilities: {
1052
+ prompt_image: false,
1053
+ prompt_embedded_context: true,
1054
+ load_session: true,
1055
+ supports_list_sessions: true,
1056
+ supports_resume: true,
1057
+ },
1058
+ },
1059
+ }, requestId);
1060
+ writeEvent({
1061
+ event: "sessions_listed",
1062
+ sessions: listRecentPersistedSessions().map((entry) => ({
1063
+ session_id: entry.session_id,
1064
+ cwd: entry.cwd,
1065
+ ...(entry.title ? { title: entry.title } : {}),
1066
+ ...(entry.updated_at ? { updated_at: entry.updated_at } : {}),
1067
+ })),
1068
+ });
1069
+ return;
1070
+ case "create_session":
1071
+ await createSession({
1072
+ cwd: command.cwd,
1073
+ yolo: command.yolo,
1074
+ model: command.model,
1075
+ resume: command.resume,
1076
+ connectEvent: "connected",
1077
+ requestId,
1078
+ });
1079
+ return;
1080
+ case "load_session": {
1081
+ const persisted = resolvePersistedSessionEntry(command.session_id);
1082
+ if (!persisted) {
1083
+ slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
1084
+ return;
1085
+ }
1086
+ const resumeUpdates = extractSessionHistoryUpdatesFromJsonl(persisted.file_path);
1087
+ const staleSessions = Array.from(sessions.values());
1088
+ const hadActiveSession = staleSessions.length > 0;
1089
+ try {
1090
+ await createSession({
1091
+ cwd: persisted.cwd,
1092
+ yolo: false,
1093
+ resume: command.session_id,
1094
+ ...(resumeUpdates.length > 0 ? { resumeUpdates } : {}),
1095
+ connectEvent: hadActiveSession ? "session_replaced" : "connected",
1096
+ requestId,
1097
+ ...(hadActiveSession ? { sessionsToCloseAfterConnect: staleSessions } : {}),
1098
+ });
1099
+ }
1100
+ catch (error) {
1101
+ const message = error instanceof Error ? error.message : String(error);
1102
+ slashError(command.session_id, `failed to resume session: ${message}`, requestId);
1103
+ }
1104
+ return;
1105
+ }
1106
+ case "new_session":
1107
+ await closeAllSessions();
1108
+ await createSession({
1109
+ cwd: command.cwd,
1110
+ yolo: command.yolo,
1111
+ model: command.model,
1112
+ connectEvent: "session_replaced",
1113
+ requestId,
1114
+ });
1115
+ return;
1116
+ case "prompt": {
1117
+ const session = sessionById(command.session_id);
1118
+ if (!session) {
1119
+ slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
1120
+ return;
1121
+ }
1122
+ const text = textFromPrompt(command);
1123
+ if (!text.trim()) {
1124
+ return;
1125
+ }
1126
+ session.input.enqueue({
1127
+ type: "user",
1128
+ session_id: session.sessionId,
1129
+ parent_tool_use_id: null,
1130
+ message: {
1131
+ role: "user",
1132
+ content: [{ type: "text", text }],
1133
+ },
1134
+ });
1135
+ return;
1136
+ }
1137
+ case "cancel_turn": {
1138
+ const session = sessionById(command.session_id);
1139
+ if (!session) {
1140
+ slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
1141
+ return;
1142
+ }
1143
+ await session.query.interrupt();
1144
+ return;
1145
+ }
1146
+ case "set_model": {
1147
+ const session = sessionById(command.session_id);
1148
+ if (!session) {
1149
+ slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
1150
+ return;
1151
+ }
1152
+ await session.query.setModel(command.model);
1153
+ session.model = command.model;
1154
+ emitSessionUpdate(session.sessionId, {
1155
+ type: "config_option_update",
1156
+ option_id: "model",
1157
+ value: command.model,
1158
+ });
1159
+ return;
1160
+ }
1161
+ case "set_mode": {
1162
+ const session = sessionById(command.session_id);
1163
+ if (!session) {
1164
+ slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
1165
+ return;
1166
+ }
1167
+ const mode = toPermissionMode(command.mode);
1168
+ if (!mode) {
1169
+ slashError(command.session_id, `unsupported mode: ${command.mode}`, requestId);
1170
+ return;
1171
+ }
1172
+ await session.query.setPermissionMode(mode);
1173
+ session.mode = mode;
1174
+ emitSessionUpdate(session.sessionId, {
1175
+ type: "current_mode_update",
1176
+ current_mode_id: mode,
1177
+ });
1178
+ return;
1179
+ }
1180
+ case "permission_response":
1181
+ handlePermissionResponse(command);
1182
+ return;
1183
+ case "shutdown":
1184
+ await closeAllSessions();
1185
+ process.exit(0);
1186
+ default:
1187
+ failConnection(`unhandled command: ${command.command ?? "unknown"}`, requestId);
1188
+ }
1189
+ }
1190
+ function main() {
1191
+ const rl = readline.createInterface({
1192
+ input: process.stdin,
1193
+ crlfDelay: Number.POSITIVE_INFINITY,
1194
+ });
1195
+ rl.on("line", (line) => {
1196
+ if (line.trim().length === 0) {
1197
+ return;
1198
+ }
1199
+ void (async () => {
1200
+ let parsed;
1201
+ try {
1202
+ parsed = parseCommandEnvelope(line);
1203
+ }
1204
+ catch (error) {
1205
+ const message = error instanceof Error ? error.message : String(error);
1206
+ failConnection(`invalid command envelope: ${message}`);
1207
+ return;
1208
+ }
1209
+ try {
1210
+ await handleCommand(parsed.command, parsed.requestId);
1211
+ }
1212
+ catch (error) {
1213
+ const message = error instanceof Error ? error.message : String(error);
1214
+ failConnection(`bridge command failed (${parsed.command.command}): ${message}`, parsed.requestId);
1215
+ }
1216
+ })();
1217
+ });
1218
+ rl.on("close", () => {
1219
+ void closeAllSessions().finally(() => process.exit(0));
1220
+ });
1221
+ }
1222
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
1223
+ main();
1224
+ }