claude-code-rust 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -25,11 +25,9 @@ Claude Code Rust replaces the stock Claude Code terminal interface with a native
25
25
  npm install -g claude-code-rust
26
26
  ```
27
27
 
28
- The published package installs a `claude-rs` command and fetches the matching
29
- prebuilt release binary for your platform during install.
28
+ The published package installs a `claude-rs` command and fetches the matching prebuilt release binary for your platform during install.
30
29
 
31
- If `claude-rs` resolves to an older global shim, ensure your npm global bin
32
- directory comes first on `PATH` or remove the stale shim before retrying.
30
+ If `claude-rs` resolves to an older global shim, ensure your npm global bin directory comes first on `PATH` or remove the stale shim before retrying.
33
31
 
34
32
  ## Usage
35
33
 
@@ -63,13 +61,22 @@ Three-layer design:
63
61
 
64
62
  This project is pre-1.0 and under active development. See [CONTRIBUTING.md](CONTRIBUTING.md) for how to get involved.
65
63
 
64
+ ## Limitations
65
+
66
+ Startup is still constrained by the upstream Claude Agent SDK runtime that this TUI wraps. The Rust interface itself is fast, but end-to-end readiness can still take noticeable time before a session is fully available. Improving that remains an active area of work.
67
+
66
68
  ## License
67
69
 
68
- This project is licensed under the [Apache License 2.0](LICENSE).
69
- Apache-2.0 was chosen to keep usage and redistribution straightforward for individual users, downstream packagers, and commercial adopters.
70
+ This project is licensed under the [Apache License 2.0](LICENSE). Apache-2.0 was chosen to keep usage and redistribution straightforward for individual users, downstream packagers, and commercial adopters.
71
+
72
+ ## Disclaimer and Legal Notice
73
+
74
+ This project is not affiliated with, endorsed by, or supported by Anthropic.
75
+
76
+ A quick note on where this project stands, since I know people worry about this kind of thing: claude-code-rust is a terminal UI that I wrote from scratch in Rust. It is not a fork, copy or port of the latest Claude Code source leak -- it talks to Anthropic's official [Agent SDK](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/agent-sdk) as a runtime dependency instead, the same way any other third-party tool would. No Anthropic source code was read or used as reference at any point during development.
77
+
78
+ The project uses your existing Claude Code subscription via the Agent SDK and the Agent SDK's terms allow building on top of it. Other community projects do the same. As far as I can tell, using this project is fine -- but I am a single maintainer, not a lawyer. If anything changes on Anthropic's end, I will update this section and adjust the project accordingly.
70
79
 
71
- ## Disclaimer
80
+ This project's source code is licensed under [Apache-2.0](LICENSE). The Agent SDK itself is proprietary and governed by [Anthropic's Commercial Terms of Service](https://www.anthropic.com/legal/commercial-terms).
72
81
 
73
- This project is an unofficial terminal UI for Claude Code and is not affiliated with, endorsed by, or supported by Anthropic.
74
- Use it at your own risk.
75
82
  For official Claude documentation, see [https://claude.ai/docs](https://claude.ai/docs).
@@ -1,6 +1,6 @@
1
1
  # claude-rs agent-sdk bridge
2
2
 
3
- Initial scaffold for the NDJSON stdio bridge that will connect Rust (`claude-code-rust`) with `@anthropic-ai/claude-agent-sdk`.
3
+ NDJSON stdio bridge that connects the Rust TUI (`claude-code-rust`) with `@anthropic-ai/claude-agent-sdk`. Spawned as a child process by the Rust binary and communicates via line-delimited JSON envelopes over stdin/stdout.
4
4
 
5
5
  ## Local build
6
6
 
@@ -1,5 +1,7 @@
1
+ import { resolveCurrentModel } from "./session_lifecycle.js";
1
2
  const MODE_NAMES = {
2
3
  default: "Default",
4
+ auto: "Auto",
3
5
  acceptEdits: "Accept Edits",
4
6
  bypassPermissions: "Bypass Permissions",
5
7
  plan: "Plan",
@@ -7,11 +9,67 @@ const MODE_NAMES = {
7
9
  };
8
10
  const MODE_OPTIONS = [
9
11
  { id: "default", name: "Default", description: "Standard permission flow" },
12
+ { id: "auto", name: "Auto", description: "Model-classified permission approvals" },
10
13
  { id: "acceptEdits", name: "Accept Edits", description: "Auto-approve edit operations" },
11
14
  { id: "plan", name: "Plan", description: "No tool execution" },
12
15
  { id: "dontAsk", name: "Don't Ask", description: "Reject non-approved tools" },
13
16
  { id: "bypassPermissions", name: "Bypass Permissions", description: "Auto-approve all tools" },
14
17
  ];
18
+ const BASE_SUPPORTED_MODE_IDS = [
19
+ "default",
20
+ "acceptEdits",
21
+ "plan",
22
+ "dontAsk",
23
+ ];
24
+ function currentModelSupportsAutoMode(session) {
25
+ const currentModel = session.currentModel ?? resolveCurrentModel(session);
26
+ return currentModel.supports_auto_mode === true;
27
+ }
28
+ function modeInfoForId(mode) {
29
+ return MODE_OPTIONS.find((entry) => entry.id === mode) ?? {
30
+ id: mode,
31
+ name: MODE_NAMES[mode],
32
+ };
33
+ }
34
+ function uniqueModeIds(modeIds) {
35
+ const unique = new Set(modeIds);
36
+ const ordered = MODE_OPTIONS.map((entry) => entry.id).filter((mode) => unique.has(mode));
37
+ const extras = Array.from(unique).filter((mode) => !ordered.includes(mode));
38
+ return [...ordered, ...extras];
39
+ }
40
+ function computedSupportedModeIds(session) {
41
+ const supported = [...BASE_SUPPORTED_MODE_IDS];
42
+ if (currentModelSupportsAutoMode(session)) {
43
+ supported.push("auto");
44
+ }
45
+ if (session.supportsBypassPermissionsMode) {
46
+ supported.push("bypassPermissions");
47
+ }
48
+ if (session.mode) {
49
+ supported.push(session.mode);
50
+ }
51
+ return uniqueModeIds(supported);
52
+ }
53
+ export function refreshSupportedModesForSession(session) {
54
+ const computed = computedSupportedModeIds(session);
55
+ session.supportedModeIds = computed.filter((mode) => mode === session.mode || !session.runtimeUnavailableModeIds.includes(mode));
56
+ }
57
+ export function markModeUnavailableForSession(session, mode) {
58
+ if (session.runtimeUnavailableModeIds.includes(mode)) {
59
+ return false;
60
+ }
61
+ session.runtimeUnavailableModeIds = [...session.runtimeUnavailableModeIds, mode];
62
+ refreshSupportedModesForSession(session);
63
+ return true;
64
+ }
65
+ export function permissionModeFailureLooksUnsupported(mode, message) {
66
+ const normalizedMessage = message.toLowerCase();
67
+ return (normalizedMessage.includes("cannot set permission mode to") &&
68
+ normalizedMessage.includes(mode.toLowerCase()));
69
+ }
70
+ export function availableModesForSession(session) {
71
+ return session.supportedModeIds.map(modeInfoForId);
72
+ }
15
73
  function asRecord(value, context) {
16
74
  if (!value || typeof value !== "object" || Array.isArray(value)) {
17
75
  throw new Error(`${context} must be an object`);
@@ -205,6 +263,16 @@ export function parseCommandEnvelope(line) {
205
263
  command: "get_status_snapshot",
206
264
  session_id: expectString(raw, "session_id", "get_status_snapshot"),
207
265
  };
266
+ case "get_context_usage":
267
+ return {
268
+ command: "get_context_usage",
269
+ session_id: expectString(raw, "session_id", "get_context_usage"),
270
+ };
271
+ case "reload_plugins":
272
+ return {
273
+ command: "reload_plugins",
274
+ session_id: expectString(raw, "session_id", "reload_plugins"),
275
+ };
208
276
  case "mcp_status":
209
277
  case "get_mcp_snapshot":
210
278
  return {
@@ -341,6 +409,7 @@ function parseQuestionAnnotation(value) {
341
409
  }
342
410
  export function toPermissionMode(mode) {
343
411
  if (mode === "default" ||
412
+ mode === "auto" ||
344
413
  mode === "acceptEdits" ||
345
414
  mode === "bypassPermissions" ||
346
415
  mode === "plan" ||
@@ -349,10 +418,10 @@ export function toPermissionMode(mode) {
349
418
  }
350
419
  return null;
351
420
  }
352
- export function buildModeState(mode) {
421
+ export function buildModeState(session, mode) {
353
422
  return {
354
423
  current_mode_id: mode,
355
424
  current_mode_name: MODE_NAMES[mode],
356
- available_modes: MODE_OPTIONS,
425
+ available_modes: availableModesForSession(session),
357
426
  };
358
427
  }
@@ -1,6 +1,8 @@
1
1
  import { listSessions } from "@anthropic-ai/claude-agent-sdk";
2
2
  import { buildModeState } from "./commands.js";
3
3
  import { mapSdkSessions } from "./history.js";
4
+ import { bridgeLogger, LOG_TARGETS, logBridgeEventSent } from "./logger.js";
5
+ import { resolveCurrentModel } from "./session_lifecycle.js";
4
6
  const SESSION_LIST_LIMIT = 50;
5
7
  let sessionListingDir;
6
8
  export function buildSessionListOptions(dir, limit = SESSION_LIST_LIMIT) {
@@ -17,7 +19,9 @@ export function writeEvent(event, requestId) {
17
19
  ...(requestId ? { request_id: requestId } : {}),
18
20
  ...event,
19
21
  };
20
- process.stdout.write(`${JSON.stringify(envelope)}\n`);
22
+ const serialized = JSON.stringify(envelope);
23
+ logBridgeEventSent(event, requestId, Buffer.byteLength(serialized) + 1);
24
+ process.stdout.write(`${serialized}\n`);
21
25
  }
22
26
  export function failConnection(message, requestId) {
23
27
  writeEvent({ event: "connection_failed", message }, requestId);
@@ -25,34 +29,110 @@ export function failConnection(message, requestId) {
25
29
  export function slashError(sessionId, message, requestId) {
26
30
  writeEvent({ event: "slash_error", session_id: sessionId, message }, requestId);
27
31
  }
32
+ export function emitRuntimeReloadCompleted(sessionId, requestId) {
33
+ writeEvent({ event: "runtime_reload_completed", session_id: sessionId }, requestId);
34
+ }
35
+ export function emitRuntimeReloadFailed(sessionId, message, requestId) {
36
+ writeEvent({ event: "runtime_reload_failed", session_id: sessionId, message }, requestId);
37
+ }
28
38
  export function emitMcpOperationError(sessionId, error, requestId) {
29
39
  writeEvent({ event: "mcp_operation_error", session_id: sessionId, error }, requestId);
30
40
  }
31
41
  export function emitSessionUpdate(sessionId, update) {
32
42
  writeEvent({ event: "session_update", session_id: sessionId, update });
33
43
  }
34
- export function emitConnectEvent(session) {
44
+ export function emitPermissionRequestEvent(sessionId, request) {
45
+ bridgeLogger.info({
46
+ target: LOG_TARGETS.BRIDGE_PERMISSION,
47
+ eventName: "permission_request_emitted",
48
+ message: "permission request emitted",
49
+ outcome: "success",
50
+ sessionId,
51
+ toolCallId: request.tool_call.tool_call_id,
52
+ count: request.options.length,
53
+ fields: {
54
+ option_count: request.options.length,
55
+ tool_title: request.tool_call.title,
56
+ },
57
+ });
58
+ writeEvent({ event: "permission_request", session_id: sessionId, request });
59
+ }
60
+ export function emitQuestionRequestEvent(sessionId, request) {
61
+ bridgeLogger.info({
62
+ target: LOG_TARGETS.BRIDGE_PERMISSION,
63
+ eventName: "question_request_emitted",
64
+ message: "question request emitted",
65
+ outcome: "success",
66
+ sessionId,
67
+ toolCallId: request.tool_call.tool_call_id,
68
+ count: request.prompt.options.length,
69
+ fields: {
70
+ question_index: request.question_index,
71
+ total_questions: request.total_questions,
72
+ option_count: request.prompt.options.length,
73
+ header: request.prompt.header,
74
+ },
75
+ });
76
+ writeEvent({ event: "question_request", session_id: sessionId, request });
77
+ }
78
+ export function emitElicitationRequestEvent(sessionId, request) {
79
+ bridgeLogger.info({
80
+ target: LOG_TARGETS.BRIDGE_PERMISSION,
81
+ eventName: "elicitation_request_emitted",
82
+ message: "elicitation request emitted",
83
+ outcome: "success",
84
+ sessionId,
85
+ requestId: request.request_id,
86
+ fields: {
87
+ server_name: request.server_name,
88
+ mode: request.mode,
89
+ has_url: request.url !== undefined,
90
+ has_requested_schema: request.requested_schema !== undefined,
91
+ },
92
+ });
93
+ writeEvent({ event: "elicitation_request", session_id: sessionId, request });
94
+ }
95
+ function buildConnectBridgeEvent(session, eventName) {
35
96
  const historyUpdates = session.resumeUpdates;
36
- const connectEvent = session.connectEvent === "session_replaced"
97
+ return eventName === "session_replaced"
37
98
  ? {
38
99
  event: "session_replaced",
39
100
  session_id: session.sessionId,
40
101
  cwd: session.cwd,
41
- model_name: session.model,
102
+ current_model: session.currentModel ?? resolveCurrentModel(session),
42
103
  available_models: session.availableModels,
43
- mode: session.mode ? buildModeState(session.mode) : null,
104
+ mode: session.mode ? buildModeState(session, session.mode) : null,
44
105
  ...(historyUpdates && historyUpdates.length > 0 ? { history_updates: historyUpdates } : {}),
45
106
  }
46
107
  : {
47
108
  event: "connected",
48
109
  session_id: session.sessionId,
49
110
  cwd: session.cwd,
50
- model_name: session.model,
111
+ current_model: session.currentModel ?? resolveCurrentModel(session),
51
112
  available_models: session.availableModels,
52
- mode: session.mode ? buildModeState(session.mode) : null,
113
+ mode: session.mode ? buildModeState(session, session.mode) : null,
53
114
  ...(historyUpdates && historyUpdates.length > 0 ? { history_updates: historyUpdates } : {}),
54
115
  };
55
- writeEvent(connectEvent, session.connectRequestId);
116
+ }
117
+ function logConnectEventEmission(session, eventName, requestId) {
118
+ bridgeLogger.info({
119
+ target: LOG_TARGETS.APP_SESSION,
120
+ eventName: eventName === "session_replaced" ? "session_replaced_emitted" : "session_connected_emitted",
121
+ message: eventName === "session_replaced" ? "session replaced event emitted" : "session connected event emitted",
122
+ outcome: "success",
123
+ ...(requestId ? { requestId } : {}),
124
+ sessionId: session.sessionId,
125
+ fields: {
126
+ history_update_count: session.resumeUpdates?.length ?? 0,
127
+ available_model_count: session.availableModels.length,
128
+ stale_session_count: session.sessionsToCloseAfterConnect?.length ?? 0,
129
+ },
130
+ });
131
+ }
132
+ export function emitConnectEvent(session) {
133
+ const bridgeEvent = buildConnectBridgeEvent(session, session.connectEvent);
134
+ logConnectEventEmission(session, session.connectEvent, session.connectRequestId);
135
+ writeEvent(bridgeEvent, session.connectRequestId);
56
136
  session.connectRequestId = undefined;
57
137
  session.connected = true;
58
138
  session.authHintSent = false;
@@ -65,7 +145,7 @@ export function emitConnectEvent(session) {
65
145
  }
66
146
  void (async () => {
67
147
  // Lazy import to break circular dependency at module-evaluation time.
68
- const { sessions, closeSession } = await import("./session_lifecycle.js");
148
+ const { sessions, closeSessionWithLogging } = await import("./session_lifecycle.js");
69
149
  for (const stale of staleSessions) {
70
150
  if (stale === session) {
71
151
  continue;
@@ -73,19 +153,54 @@ export function emitConnectEvent(session) {
73
153
  if (sessions.get(stale.sessionId) === stale) {
74
154
  sessions.delete(stale.sessionId);
75
155
  }
76
- await closeSession(stale);
156
+ await closeSessionWithLogging(stale, { reason: "stale_after_connect" });
77
157
  }
78
158
  refreshSessionsList();
79
159
  })();
80
160
  }
161
+ export function emitSessionReplacedEvent(session, requestId) {
162
+ const bridgeEvent = buildConnectBridgeEvent(session, "session_replaced");
163
+ logConnectEventEmission(session, "session_replaced", requestId);
164
+ writeEvent(bridgeEvent, requestId);
165
+ session.resumeUpdates = undefined;
166
+ refreshSessionsList();
167
+ }
81
168
  export async function emitSessionsList(requestId) {
169
+ bridgeLogger.debug({
170
+ target: LOG_TARGETS.APP_SESSION,
171
+ eventName: "sessions_list_requested",
172
+ message: "sessions list requested",
173
+ outcome: "start",
174
+ ...(requestId ? { requestId } : {}),
175
+ fields: {
176
+ has_listing_dir: sessionListingDir !== undefined,
177
+ limit: SESSION_LIST_LIMIT,
178
+ },
179
+ });
82
180
  try {
83
181
  const sdkSessions = await listSessions(currentSessionListOptions());
84
- writeEvent({ event: "sessions_listed", sessions: mapSdkSessions(sdkSessions, SESSION_LIST_LIMIT) }, requestId);
182
+ const sessions = mapSdkSessions(sdkSessions, SESSION_LIST_LIMIT);
183
+ bridgeLogger.info({
184
+ target: LOG_TARGETS.APP_SESSION,
185
+ eventName: "sessions_list_completed",
186
+ message: "sessions list completed",
187
+ outcome: "success",
188
+ ...(requestId ? { requestId } : {}),
189
+ count: sessions.length,
190
+ fields: { session_count: sessions.length },
191
+ });
192
+ writeEvent({ event: "sessions_listed", sessions }, requestId);
85
193
  }
86
194
  catch (error) {
87
195
  const message = error instanceof Error ? error.message : String(error);
88
- console.error(`[sdk warn] listSessions failed: ${message}`);
196
+ bridgeLogger.warn({
197
+ target: LOG_TARGETS.APP_SESSION,
198
+ eventName: "sessions_list_failed",
199
+ message: "failed to list SDK sessions",
200
+ outcome: "failure",
201
+ ...(requestId ? { requestId } : {}),
202
+ fields: { error_message: message },
203
+ });
89
204
  writeEvent({ event: "sessions_listed", sessions: [] }, requestId);
90
205
  }
91
206
  }
@@ -29,14 +29,14 @@ function pushResumeTextChunk(updates, role, text) {
29
29
  }
30
30
  updates.push({ type: "user_message_chunk", content: { type: "text", text } });
31
31
  }
32
- function pushResumeToolUse(updates, toolCalls, block) {
32
+ function pushResumeToolUse(updates, toolCalls, block, parentToolUseId) {
33
33
  const toolUseId = typeof block.id === "string" ? block.id : "";
34
34
  if (!toolUseId) {
35
35
  return;
36
36
  }
37
37
  const name = typeof block.name === "string" ? block.name : "Tool";
38
38
  const input = asRecordOrNull(block.input) ?? {};
39
- const toolCall = createToolCall(toolUseId, name, input);
39
+ const toolCall = createToolCall(toolUseId, name, input, parentToolUseId);
40
40
  toolCall.status = "in_progress";
41
41
  toolCalls.set(toolUseId, toolCall);
42
42
  updates.push({ type: "tool_call", tool_call: toolCall });
@@ -75,7 +75,7 @@ export function mapSdkSessionInfo(info) {
75
75
  session_id: info.sessionId,
76
76
  summary: summaryFromSession(info),
77
77
  last_modified_ms: info.lastModified,
78
- file_size_bytes: info.fileSize,
78
+ file_size_bytes: info.fileSize ?? 0,
79
79
  ...(nonEmptyTrimmed(info.cwd) ? { cwd: info.cwd?.trim() } : {}),
80
80
  ...(nonEmptyTrimmed(info.gitBranch) ? { git_branch: info.gitBranch?.trim() } : {}),
81
81
  ...(nonEmptyTrimmed(info.customTitle) ? { custom_title: info.customTitle?.trim() } : {}),
@@ -106,6 +106,11 @@ export function mapSessionMessagesToUpdates(messages) {
106
106
  for (const message of messageCandidates(entry.message)) {
107
107
  const roleCandidate = message.role;
108
108
  const role = roleCandidate === "assistant" || roleCandidate === "user" ? roleCandidate : fallbackRole;
109
+ const parentToolUseId = typeof entry.parent_tool_use_id === "string"
110
+ ? entry.parent_tool_use_id
111
+ : typeof message.parent_tool_use_id === "string"
112
+ ? message.parent_tool_use_id
113
+ : null;
109
114
  const content = Array.isArray(message.content) ? message.content : [];
110
115
  for (const item of content) {
111
116
  const block = asRecordOrNull(item);
@@ -121,7 +126,7 @@ export function mapSessionMessagesToUpdates(messages) {
121
126
  continue;
122
127
  }
123
128
  if (isToolUseBlockType(blockType) && role === "assistant") {
124
- pushResumeToolUse(updates, toolCalls, block);
129
+ pushResumeToolUse(updates, toolCalls, block, parentToolUseId);
125
130
  continue;
126
131
  }
127
132
  if (TOOL_RESULT_TYPES.has(blockType)) {
@@ -0,0 +1,253 @@
1
+ const LOG_SCHEMA = "claude-rs-log/v1";
2
+ const DIAGNOSTICS_ENABLED = process.env.CLAUDE_RS_BRIDGE_DIAGNOSTICS === "1";
3
+ export const LOG_TARGETS = {
4
+ APP_AUTH: "app.auth",
5
+ APP_CACHE: "app.cache",
6
+ APP_CONFIG: "app.config",
7
+ APP_COMMAND: "app.command",
8
+ APP_NETWORK: "app.network",
9
+ APP_INPUT: "app.input",
10
+ APP_PASTE: "app.paste",
11
+ APP_PERF: "app.perf",
12
+ APP_RENDER: "app.render",
13
+ APP_SESSION: "app.session",
14
+ APP_TOOL: "app.tool",
15
+ APP_UPDATE: "app.update",
16
+ BRIDGE_LIFECYCLE: "bridge.lifecycle",
17
+ BRIDGE_MCP: "bridge.mcp",
18
+ BRIDGE_PERMISSION: "bridge.permission",
19
+ BRIDGE_PROTOCOL: "bridge.protocol",
20
+ BRIDGE_SDK: "bridge.sdk",
21
+ };
22
+ function definedFields(fields) {
23
+ if (!fields) {
24
+ return undefined;
25
+ }
26
+ const entries = Object.entries(fields).filter(([, value]) => value !== undefined);
27
+ return entries.length > 0 ? Object.fromEntries(entries) : undefined;
28
+ }
29
+ function writeDiagnostic(level, event) {
30
+ if (!DIAGNOSTICS_ENABLED) {
31
+ return;
32
+ }
33
+ const envelope = {
34
+ schema: LOG_SCHEMA,
35
+ timestamp: new Date().toISOString(),
36
+ level,
37
+ target: event.target,
38
+ event_name: event.eventName,
39
+ message: event.message,
40
+ ...(event.outcome ? { outcome: event.outcome } : {}),
41
+ ...(event.sessionId ? { session_id: event.sessionId } : {}),
42
+ ...(event.requestId ? { request_id: event.requestId } : {}),
43
+ ...(event.toolCallId ? { tool_call_id: event.toolCallId } : {}),
44
+ ...(event.commandId ? { command_id: event.commandId } : {}),
45
+ ...(event.terminalId ? { terminal_id: event.terminalId } : {}),
46
+ ...(event.errorKind ? { error_kind: event.errorKind } : {}),
47
+ ...(event.errorCode ? { error_code: event.errorCode } : {}),
48
+ ...(event.durationMs !== undefined ? { duration_ms: event.durationMs } : {}),
49
+ ...(event.count !== undefined ? { count: event.count } : {}),
50
+ ...(event.sizeBytes !== undefined ? { size_bytes: event.sizeBytes } : {}),
51
+ };
52
+ const fields = definedFields(event.fields);
53
+ if (fields) {
54
+ envelope.fields = fields;
55
+ }
56
+ process.stderr.write(`${JSON.stringify(envelope)}\n`);
57
+ }
58
+ function previewText(value, limit) {
59
+ return value.length > limit ? `${value.slice(0, limit)}...` : value;
60
+ }
61
+ function commandSessionId(command) {
62
+ switch (command.command) {
63
+ case "resume_session":
64
+ case "prompt":
65
+ case "cancel_turn":
66
+ case "set_model":
67
+ case "set_mode":
68
+ case "generate_session_title":
69
+ case "rename_session":
70
+ case "permission_response":
71
+ case "question_response":
72
+ case "elicitation_response":
73
+ case "get_status_snapshot":
74
+ case "mcp_status":
75
+ case "mcp_reconnect":
76
+ case "mcp_toggle":
77
+ case "mcp_set_servers":
78
+ case "mcp_authenticate":
79
+ case "mcp_clear_auth":
80
+ case "mcp_oauth_callback_url":
81
+ return command.session_id;
82
+ case "create_session":
83
+ return command.resume;
84
+ case "initialize":
85
+ case "new_session":
86
+ case "shutdown":
87
+ return undefined;
88
+ }
89
+ }
90
+ function commandToolCallId(command) {
91
+ switch (command.command) {
92
+ case "permission_response":
93
+ case "question_response":
94
+ return command.tool_call_id;
95
+ case "initialize":
96
+ case "create_session":
97
+ case "resume_session":
98
+ case "prompt":
99
+ case "cancel_turn":
100
+ case "set_model":
101
+ case "set_mode":
102
+ case "generate_session_title":
103
+ case "rename_session":
104
+ case "new_session":
105
+ case "elicitation_response":
106
+ case "get_status_snapshot":
107
+ case "get_context_usage":
108
+ case "reload_plugins":
109
+ case "mcp_status":
110
+ case "mcp_reconnect":
111
+ case "mcp_toggle":
112
+ case "mcp_set_servers":
113
+ case "mcp_authenticate":
114
+ case "mcp_clear_auth":
115
+ case "mcp_oauth_callback_url":
116
+ case "shutdown":
117
+ return undefined;
118
+ }
119
+ }
120
+ function eventToolCallId(event) {
121
+ switch (event.event) {
122
+ case "permission_request":
123
+ case "question_request":
124
+ return event.request.tool_call.tool_call_id;
125
+ case "connected":
126
+ case "auth_required":
127
+ case "connection_failed":
128
+ case "session_update":
129
+ case "elicitation_request":
130
+ case "elicitation_complete":
131
+ case "mcp_auth_redirect":
132
+ case "mcp_operation_error":
133
+ case "turn_complete":
134
+ case "turn_error":
135
+ case "slash_error":
136
+ case "runtime_reload_completed":
137
+ case "runtime_reload_failed":
138
+ case "session_replaced":
139
+ case "initialized":
140
+ case "sessions_listed":
141
+ case "status_snapshot":
142
+ case "context_usage":
143
+ case "mcp_snapshot":
144
+ return undefined;
145
+ }
146
+ }
147
+ function protocolCommandLevel(command) {
148
+ switch (command.command) {
149
+ case "initialize":
150
+ case "create_session":
151
+ case "resume_session":
152
+ case "new_session":
153
+ case "shutdown":
154
+ return "info";
155
+ default:
156
+ return "debug";
157
+ }
158
+ }
159
+ function protocolEventLevel(event) {
160
+ switch (event.event) {
161
+ case "initialized":
162
+ case "connected":
163
+ case "session_replaced":
164
+ return "info";
165
+ case "connection_failed":
166
+ return "error";
167
+ case "auth_required":
168
+ case "mcp_operation_error":
169
+ case "turn_error":
170
+ case "slash_error":
171
+ case "runtime_reload_failed":
172
+ return "warn";
173
+ case "session_update":
174
+ case "permission_request":
175
+ case "question_request":
176
+ case "elicitation_request":
177
+ case "elicitation_complete":
178
+ case "mcp_auth_redirect":
179
+ case "turn_complete":
180
+ return "trace";
181
+ case "sessions_listed":
182
+ case "status_snapshot":
183
+ case "context_usage":
184
+ case "runtime_reload_completed":
185
+ case "mcp_snapshot":
186
+ return "debug";
187
+ }
188
+ }
189
+ export const bridgeLogger = {
190
+ error(event) {
191
+ writeDiagnostic("error", event);
192
+ },
193
+ warn(event) {
194
+ writeDiagnostic("warn", event);
195
+ },
196
+ info(event) {
197
+ writeDiagnostic("info", event);
198
+ },
199
+ debug(event) {
200
+ writeDiagnostic("debug", event);
201
+ },
202
+ trace(event) {
203
+ writeDiagnostic("trace", event);
204
+ },
205
+ };
206
+ export function logSdkStderrLine(line, sessionId) {
207
+ const trimmed = line.trim();
208
+ if (trimmed.length === 0) {
209
+ return;
210
+ }
211
+ const lowered = trimmed.toLowerCase();
212
+ const level = lowered.startsWith("error") || lowered.includes("panic")
213
+ ? "error"
214
+ : lowered.startsWith("warn")
215
+ ? "warn"
216
+ : "debug";
217
+ writeDiagnostic(level, {
218
+ target: LOG_TARGETS.BRIDGE_SDK,
219
+ eventName: "sdk_stderr_line",
220
+ message: "SDK stderr line received",
221
+ ...(sessionId ? { sessionId } : {}),
222
+ fields: {
223
+ preview: previewText(trimmed, 240),
224
+ preview_chars: Math.min(trimmed.length, 240),
225
+ line_chars: trimmed.length,
226
+ },
227
+ });
228
+ }
229
+ export function logBridgeCommandReceived(command, requestId) {
230
+ writeDiagnostic(protocolCommandLevel(command), {
231
+ target: LOG_TARGETS.BRIDGE_PROTOCOL,
232
+ eventName: "bridge_command_received",
233
+ message: "bridge command received",
234
+ outcome: "success",
235
+ ...(requestId ? { requestId } : {}),
236
+ ...(commandSessionId(command) ? { sessionId: commandSessionId(command) } : {}),
237
+ ...(commandToolCallId(command) ? { toolCallId: commandToolCallId(command) } : {}),
238
+ fields: { bridge_command: command.command },
239
+ });
240
+ }
241
+ export function logBridgeEventSent(event, requestId, sizeBytes) {
242
+ writeDiagnostic(protocolEventLevel(event), {
243
+ target: LOG_TARGETS.BRIDGE_PROTOCOL,
244
+ eventName: "bridge_event_sent",
245
+ message: "bridge event sent",
246
+ outcome: event.event === "connection_failed" ? "failure" : "success",
247
+ ...(requestId ? { requestId } : {}),
248
+ ...("session_id" in event ? { sessionId: event.session_id } : {}),
249
+ ...(eventToolCallId(event) ? { toolCallId: eventToolCallId(event) } : {}),
250
+ sizeBytes,
251
+ fields: { bridge_event: event.event },
252
+ });
253
+ }