claude-code-rust 0.11.2 → 0.12.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
@@ -35,6 +35,13 @@ If `claude-rs` resolves to an older global shim, ensure your npm global bin dire
35
35
  claude-rs
36
36
  ```
37
37
 
38
+ > [!WARNING]
39
+ > **Agent SDK billing changes on June 15, 2026.** Anthropic says Agent SDK usage, `claude -p`, Claude Code GitHub Actions, and third-party Agent SDK apps will use a separate monthly Agent SDK credit instead of normal interactive Claude or Claude Code subscription limits. Because Claude Code Rust wraps the Agent SDK, treat usage through this project as Agent SDK usage. If that credit is exhausted, continued use may require enabling extra usage billed at standard API rates, or requests may pause until the credit refreshes.
40
+ >
41
+ > Sources:
42
+ > - [Anthropic support: Use the Claude Agent SDK with your Claude plan](https://support.claude.com/en/articles/15036540-use-the-claude-agent-sdk-with-your-claude-plan)
43
+ > - [ClaudeDevs announcement](https://x.com/ClaudeDevs/status/2054610152817619388)
44
+
38
45
  ## Why
39
46
 
40
47
  The stock Claude Code TUI runs on Node.js with React Ink. This causes real problems:
@@ -47,15 +54,14 @@ The stock Claude Code TUI runs on Node.js with React Ink. This causes real probl
47
54
 
48
55
  Claude Code Rust fixes all of these by compiling to a single native binary with direct terminal control via Crossterm.
49
56
 
50
- ## Architecture
51
-
52
- Three-layer design:
53
-
54
- **Presentation** (Rust/Ratatui) - Single binary with an async event loop (Tokio) handling keyboard input and bridge client events concurrently. Virtual-scrolled chat history with syntax-highlighted code blocks.
57
+ ## Custom Commands
55
58
 
56
- **Agent SDK Bridge** (stdio JSON envelopes) - Spawns `agent-sdk/dist/bridge.js` as a child process and communicates via line-delimited JSON envelopes over stdin/stdout. Bidirectional streaming for user messages, tool updates, and permission requests.
59
+ Claude Code Rust adds project-local slash commands that set environment variables via `.claude/settings.local.json` without leaving the TUI. Changes apply on the next session.
57
60
 
58
- **Agent Runtime** (Anthropic Agent SDK) - The TypeScript bridge drives `@anthropic-ai/claude-agent-sdk`, which manages authentication, session/query lifecycle, and tool execution.
61
+ | Command | Usage | Description |
62
+ |---------|-------|-------------|
63
+ | `/1m-context` | `/1m-context <enable\|disable\|status>` | Disable the 1 million token context window to improve model performance and prevent quality degradation on large context windows. |
64
+ | `/opus-version` | `/opus-version <4.5\|4.6\|4.7\|default\|status>` | Pin the Opus model version for the current folder. Useful for switching to 4.6 or 4.5 to avoid 4.7's tokenization issues. Use `default` to clear the pin. |
59
65
 
60
66
  ## Status
61
67
 
@@ -75,7 +81,7 @@ This project is not affiliated with, endorsed by, or supported by Anthropic.
75
81
 
76
82
  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
83
 
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.
84
+ The project authenticates through your existing Claude Code account via the Agent SDK, and the Agent SDK's terms allow building on top of it. Billing, credits, limits, and overage behavior are controlled by Anthropic, including the Agent SDK credit change noted above. 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.
79
85
 
80
86
  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).
81
87
 
@@ -1,5 +1,5 @@
1
1
  import { asRecordOrNull } from "./shared.js";
2
- import { TOOL_RESULT_TYPES, buildToolResultFields, createToolCall, isToolUseBlockType } from "./tooling.js";
2
+ import { TOOL_RESULT_TYPES, buildToolResultFields, createToolCall, isToolSearchToolName, isToolSearchToolResultType, isToolUseBlockType, } from "./tooling.js";
3
3
  function nonEmptyTrimmed(value) {
4
4
  if (typeof value !== "string") {
5
5
  return undefined;
@@ -29,23 +29,32 @@ 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, parentToolUseId) {
32
+ function pushResumeToolUse(updates, toolCalls, hiddenToolUseIds, 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
+ if (isToolSearchToolName(name)) {
40
+ hiddenToolUseIds.add(toolUseId);
41
+ return;
42
+ }
39
43
  const toolCall = createToolCall(toolUseId, name, input, parentToolUseId);
40
44
  toolCall.status = "in_progress";
41
45
  toolCalls.set(toolUseId, toolCall);
42
46
  updates.push({ type: "tool_call", tool_call: toolCall });
43
47
  }
44
- function pushResumeToolResult(updates, toolCalls, block) {
48
+ function pushResumeToolResult(updates, toolCalls, hiddenToolUseIds, block) {
45
49
  const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id : "";
46
50
  if (!toolUseId) {
47
51
  return;
48
52
  }
53
+ const blockType = typeof block.type === "string" ? block.type : "";
54
+ if (isToolSearchToolResultType(blockType) || hiddenToolUseIds.has(toolUseId)) {
55
+ hiddenToolUseIds.add(toolUseId);
56
+ return;
57
+ }
49
58
  const isError = Boolean(block.is_error);
50
59
  const base = toolCalls.get(toolUseId);
51
60
  const fields = buildToolResultFields(isError, block.content, base, block);
@@ -101,6 +110,7 @@ export function mapSdkSessions(infos, limit = 50) {
101
110
  export function mapSessionMessagesToUpdates(messages) {
102
111
  const updates = [];
103
112
  const toolCalls = new Map();
113
+ const hiddenToolUseIds = new Set();
104
114
  for (const entry of messages) {
105
115
  const fallbackRole = entry.type === "assistant" ? "assistant" : "user";
106
116
  for (const message of messageCandidates(entry.message)) {
@@ -126,11 +136,11 @@ export function mapSessionMessagesToUpdates(messages) {
126
136
  continue;
127
137
  }
128
138
  if (isToolUseBlockType(blockType) && role === "assistant") {
129
- pushResumeToolUse(updates, toolCalls, block, parentToolUseId);
139
+ pushResumeToolUse(updates, toolCalls, hiddenToolUseIds, block, parentToolUseId);
130
140
  continue;
131
141
  }
132
142
  if (TOOL_RESULT_TYPES.has(blockType)) {
133
- pushResumeToolResult(updates, toolCalls, block);
143
+ pushResumeToolResult(updates, toolCalls, hiddenToolUseIds, block);
134
144
  continue;
135
145
  }
136
146
  if (blockType === "image") {
@@ -1,8 +1,8 @@
1
1
  import { asRecordOrNull } from "./shared.js";
2
2
  import { toPermissionMode, buildModeState, refreshSupportedModesForSession } from "./commands.js";
3
3
  import { writeEvent, emitSessionUpdate, emitConnectEvent, emitSessionReplacedEvent, } from "./events.js";
4
- import { TOOL_RESULT_TYPES, unwrapToolUseResult } from "./tooling.js";
5
- import { emitToolCall, emitToolCallUpdate, emitPlanIfTodoWrite, emitToolResultUpdate, finalizeOpenToolCalls, emitToolProgressUpdate, emitToolSummaryUpdate, ensureToolCallVisible, resolveTaskToolUseId, taskProgressText, taskUpdatedFields, } from "./tool_calls.js";
4
+ import { TOOL_RESULT_TYPES, isToolSearchToolName, isToolSearchToolResultType, unwrapToolUseResult, } from "./tooling.js";
5
+ import { emitToolCall, emitToolCallUpdate, emitPlanIfTodoWrite, emitToolResultUpdate, finalizeOpenToolCalls, emitToolProgressUpdate, emitToolSummaryUpdate, ensureToolCallVisible, resolveTaskToolUseId, toolAcceptsTaskLifecycle, taskProgressText, taskUpdatedFields, } from "./tool_calls.js";
6
6
  import { emitAuthRequired, classifyTurnErrorKind, emitFastModeUpdateIfChanged } from "./error_classification.js";
7
7
  import { mapAvailableAgentsFromNames, emitAvailableAgentsIfChanged, refreshAvailableAgents } from "./agents.js";
8
8
  import { buildApiRetryUpdate, buildRateLimitUpdate, normalizeSettingsParseErrors, numberField, parseRuntimeSessionState, } from "./state_parsing.js";
@@ -155,6 +155,12 @@ export function handleTaskSystemMessage(session, subtype, msg) {
155
155
  return;
156
156
  }
157
157
  const toolCall = ensureToolCallVisible(session, toolUseId, "Agent", {});
158
+ if (!toolAcceptsTaskLifecycle(toolCall)) {
159
+ if (taskId) {
160
+ session.taskToolUseIds.delete(taskId);
161
+ }
162
+ return;
163
+ }
158
164
  if (toolCall.status === "pending") {
159
165
  emitToolCallUpdate(session, toolUseId, { status: "in_progress" }, "progress");
160
166
  }
@@ -238,6 +244,31 @@ function logContentBlockLinkage(session, blockType, toolUseId, toolName, linkage
238
244
  },
239
245
  });
240
246
  }
247
+ function hideToolUse(session, toolUseId) {
248
+ if (toolUseId) {
249
+ session.hiddenToolUseIds.add(toolUseId);
250
+ }
251
+ }
252
+ function isHiddenToolUse(session, toolUseId, toolName) {
253
+ if (!toolUseId) {
254
+ return false;
255
+ }
256
+ if (isToolSearchToolName(toolName)) {
257
+ hideToolUse(session, toolUseId);
258
+ return true;
259
+ }
260
+ return session.hiddenToolUseIds.has(toolUseId);
261
+ }
262
+ function isHiddenToolResult(session, toolUseId, blockType) {
263
+ if (!toolUseId) {
264
+ return false;
265
+ }
266
+ if (isToolSearchToolResultType(blockType)) {
267
+ hideToolUse(session, toolUseId);
268
+ return true;
269
+ }
270
+ return session.hiddenToolUseIds.has(toolUseId);
271
+ }
241
272
  export function handleContentBlock(session, block, linkage) {
242
273
  const blockType = typeof block.type === "string" ? block.type : "";
243
274
  if (blockType === "text") {
@@ -261,6 +292,9 @@ export function handleContentBlock(session, block, linkage) {
261
292
  if (!toolUseId) {
262
293
  return;
263
294
  }
295
+ if (isHiddenToolUse(session, toolUseId, name)) {
296
+ return;
297
+ }
264
298
  logContentBlockLinkage(session, blockType, toolUseId, name, linkage);
265
299
  emitPlanIfTodoWrite(session, name, input);
266
300
  emitToolCall(session, toolUseId, name, input, linkage?.parentToolUseId ?? null);
@@ -271,6 +305,9 @@ export function handleContentBlock(session, block, linkage) {
271
305
  if (!toolUseId) {
272
306
  return;
273
307
  }
308
+ if (isHiddenToolResult(session, toolUseId, blockType)) {
309
+ return;
310
+ }
274
311
  logContentBlockLinkage(session, blockType, toolUseId, undefined, linkage);
275
312
  const isError = Boolean(block.is_error);
276
313
  emitToolResultUpdate(session, toolUseId, isError, block.content, block);
@@ -594,6 +631,9 @@ export function handleSdkMessage(session, message) {
594
631
  if (type === "tool_progress") {
595
632
  const toolUseId = typeof msg.tool_use_id === "string" ? msg.tool_use_id : "";
596
633
  const toolName = typeof msg.tool_name === "string" ? msg.tool_name : "Tool";
634
+ if (isHiddenToolUse(session, toolUseId, toolName)) {
635
+ return;
636
+ }
597
637
  bridgeLogger.debug({
598
638
  target: LOG_TARGETS.APP_TOOL,
599
639
  eventName: "sdk_tool_progress_linkage_observed",
@@ -620,6 +660,9 @@ export function handleSdkMessage(session, message) {
620
660
  : [];
621
661
  if (summary && toolIds.length > 0) {
622
662
  for (const toolUseId of toolIds) {
663
+ if (session.hiddenToolUseIds.has(toolUseId)) {
664
+ continue;
665
+ }
623
666
  emitToolSummaryUpdate(session, toolUseId, summary);
624
667
  }
625
668
  }
@@ -672,6 +715,9 @@ export function handleSdkMessage(session, message) {
672
715
  handleUserToolResultBlocks(session, msg);
673
716
  const toolUseId = typeof msg.parent_tool_use_id === "string" ? msg.parent_tool_use_id : "";
674
717
  if (toolUseId && "tool_use_result" in msg) {
718
+ if (session.hiddenToolUseIds.has(toolUseId)) {
719
+ return;
720
+ }
675
721
  const parsed = unwrapToolUseResult(msg.tool_use_result);
676
722
  emitToolResultUpdate(session, toolUseId, parsed.isError, parsed.content, msg.tool_use_result);
677
723
  }
@@ -7,9 +7,13 @@ import { AsyncQueue } from "./shared.js";
7
7
  import { permissionOptionsFromSuggestions, permissionResultFromOutcome, } from "./permissions.js";
8
8
  import { failConnection, emitSessionUpdate, emitConnectEvent, emitPermissionRequestEvent, emitElicitationRequestEvent, } from "./events.js";
9
9
  import { ensureToolCallVisible, setToolCallStatus, } from "./tool_calls.js";
10
+ import { isToolSearchToolName } from "./tooling.js";
10
11
  import { requestExitPlanModeApproval, requestAskUserQuestionAnswers, EXIT_PLAN_MODE_TOOL_NAME, ASK_USER_QUESTION_TOOL_NAME, } from "./user_interaction.js";
11
12
  import { mapAvailableAgents, emitAvailableAgentsIfChanged, refreshAvailableAgents } from "./agents.js";
12
13
  import { emitAuthRequired, emitFastModeUpdateIfChanged } from "./error_classification.js";
14
+ const BRIDGE_RUNTIME_PROCESS_NAME = process.platform === "win32" ? "claude-rs-bridge-node.exe" : "claude-rs-bridge-node";
15
+ const BRIDGE_RUNTIME_GUARD_PROMPT = `Do not terminate the Claude Rust bridge runtime process \`${BRIDGE_RUNTIME_PROCESS_NAME}\`; ` +
16
+ "when cleaning up development servers, only stop processes by explicit PIDs you started in this session.";
13
17
  function permissionDisplayFromCanUseOptions(options) {
14
18
  const title = typeof options.title === "string" ? options.title.trim() : "";
15
19
  const displayName = typeof options.displayName === "string" ? options.displayName.trim() : "";
@@ -218,6 +222,10 @@ export async function createSession(params) {
218
222
  const sessionIdForLogs = () => session?.sessionId ?? provisionalSessionId;
219
223
  const canUseTool = async (toolName, inputData, options) => {
220
224
  const toolUseId = options.toolUseID;
225
+ if (isToolSearchToolName(toolName)) {
226
+ session?.hiddenToolUseIds.add(toolUseId);
227
+ return { behavior: "allow", updatedInput: inputData, toolUseID: toolUseId };
228
+ }
221
229
  if (toolName === EXIT_PLAN_MODE_TOOL_NAME) {
222
230
  const existing = ensureToolCallVisible(session, toolUseId, toolName, inputData);
223
231
  return await requestExitPlanModeApproval(session, toolUseId, inputData, existing);
@@ -340,6 +348,7 @@ export async function createSession(params) {
340
348
  pendingQuestions: new Map(),
341
349
  pendingElicitations: new Map(),
342
350
  mcpStatusRevalidatedAt: new Map(),
351
+ hiddenToolUseIds: new Set(),
343
352
  authHintSent: false,
344
353
  ...(params.resumeUpdates && params.resumeUpdates.length > 0
345
354
  ? { resumeUpdates: params.resumeUpdates }
@@ -578,14 +587,15 @@ function startupPermissionModeOptions(launchSettings) {
578
587
  }
579
588
  function systemPromptFromLaunchSettings(launchSettings) {
580
589
  const language = launchSettings.language?.trim();
581
- if (!language) {
582
- return undefined;
590
+ const appendLines = [BRIDGE_RUNTIME_GUARD_PROMPT];
591
+ if (language) {
592
+ appendLines.push(`Always respond to the user in ${language} unless the user explicitly asks for a different language. ` +
593
+ `Keep code, shell commands, file paths, API names, tool names, and raw error text unchanged unless the user explicitly asks for translation.`);
583
594
  }
584
595
  return {
585
596
  type: "preset",
586
597
  preset: "claude_code",
587
- append: `Always respond to the user in ${language} unless the user explicitly asks for a different language. ` +
588
- `Keep code, shell commands, file paths, API names, tool names, and raw error text unchanged unless the user explicitly asks for translation.`,
598
+ append: appendLines.join(" "),
589
599
  };
590
600
  }
591
601
  export function buildQueryOptions(params) {
@@ -603,7 +613,7 @@ export function buildQueryOptions(params) {
603
613
  ...modelOption,
604
614
  ...permissionModeOptions,
605
615
  toolConfig: { askUserQuestion: { previewFormat: "markdown" } },
606
- ...(systemPrompt ? { systemPrompt } : {}),
616
+ systemPrompt,
607
617
  ...(params.launchSettings.agent_progress_summaries !== undefined
608
618
  ? { agentProgressSummaries: params.launchSettings.agent_progress_summaries }
609
619
  : {}),
@@ -8,6 +8,13 @@ export function numberField(record, ...keys) {
8
8
  }
9
9
  return undefined;
10
10
  }
11
+ function nonNegativeNumberField(record, ...keys) {
12
+ const value = numberField(record, ...keys);
13
+ if (value === undefined || value < 0) {
14
+ return undefined;
15
+ }
16
+ return value;
17
+ }
11
18
  export function parseFastModeState(value) {
12
19
  if (value === "off" || value === "cooldown" || value === "on") {
13
20
  return value;
@@ -86,7 +93,7 @@ export function buildRateLimitUpdate(rateLimitInfo) {
86
93
  export function buildApiRetryUpdate(message) {
87
94
  const attempt = numberField(message, "attempt");
88
95
  const maxRetries = numberField(message, "max_retries", "maxRetries");
89
- const retryDelayMs = numberField(message, "retry_delay_ms", "retryDelayMs");
96
+ const retryDelayMs = nonNegativeNumberField(message, "retry_delay_ms", "retryDelayMs");
90
97
  if (attempt === undefined || maxRetries === undefined || retryDelayMs === undefined) {
91
98
  return null;
92
99
  }
@@ -1,6 +1,8 @@
1
1
  import { emitSessionUpdate } from "./events.js";
2
2
  import { bridgeLogger, LOG_TARGETS } from "./logger.js";
3
3
  import { buildToolResultFields, createToolCall } from "./tooling.js";
4
+ const TOOL_SUMMARY_TOOL_NAMES = new Set(["Agent", "Task", "WebSearch", "WebFetch", "ExitPlanMode"]);
5
+ const TASK_LIFECYCLE_TOOL_NAMES = new Set(["Agent", "Task"]);
4
6
  function jsonSize(value) {
5
7
  if (value === undefined) {
6
8
  return undefined;
@@ -30,6 +32,14 @@ function toolName(base, fields) {
30
32
  }
31
33
  return toolNameFromMeta(base?.meta);
32
34
  }
35
+ export function toolUsesSummaryOutput(base) {
36
+ const baseToolName = toolName(base);
37
+ return Boolean(baseToolName && TOOL_SUMMARY_TOOL_NAMES.has(baseToolName));
38
+ }
39
+ export function toolAcceptsTaskLifecycle(base) {
40
+ const baseToolName = toolName(base);
41
+ return Boolean(baseToolName && TASK_LIFECYCLE_TOOL_NAMES.has(baseToolName));
42
+ }
33
43
  function classifyFailureKind(rawOutput) {
34
44
  if (!rawOutput) {
35
45
  return "failed";
@@ -279,6 +289,9 @@ export function emitToolSummaryUpdate(session, toolUseId, summary) {
279
289
  if (!base) {
280
290
  return;
281
291
  }
292
+ if (!toolUsesSummaryOutput(base)) {
293
+ return;
294
+ }
282
295
  const fields = {
283
296
  status: base.status === "failed" || base.status === "killed" ? base.status : "completed",
284
297
  raw_output: summary,
@@ -10,6 +10,13 @@ export const TOOL_RESULT_TYPES = new Set([
10
10
  "text_editor_code_execution_tool_result",
11
11
  "mcp_tool_result",
12
12
  ]);
13
+ export function isToolSearchToolName(name) {
14
+ const normalized = name.replace(/[\s_-]+/g, "").toLowerCase();
15
+ return normalized === "toolsearch" || normalized === "toolsearchtool";
16
+ }
17
+ export function isToolSearchToolResultType(blockType) {
18
+ return blockType === "tool_search_tool_result";
19
+ }
13
20
  export function isToolUseBlockType(blockType) {
14
21
  return blockType === "tool_use" || blockType === "server_tool_use" || blockType === "mcp_tool_use";
15
22
  }
@@ -43,7 +43,7 @@ export async function generatePersistedSessionTitle(query, description) {
43
43
  }
44
44
  return title;
45
45
  }
46
- const EXPECTED_AGENT_SDK_VERSION = "0.2.112";
46
+ const EXPECTED_AGENT_SDK_VERSION = "0.3.146";
47
47
  const require = createRequire(import.meta.url);
48
48
  export function resolveInstalledAgentSdkVersion() {
49
49
  try {
@@ -6,6 +6,11 @@ import { emitCurrentModelUpdate, refreshCurrentModel, resolveCurrentModel, shoul
6
6
  import { emitToolProgressUpdate } from "./bridge/tool_calls.js";
7
7
  import { requestAskUserQuestionAnswers } from "./bridge/user_interaction.js";
8
8
  import { handleResultMessage } from "./bridge/message_handlers.js";
9
+ const BRIDGE_RUNTIME_PROCESS_NAME = process.platform === "win32" ? "claude-rs-bridge-node.exe" : "claude-rs-bridge-node";
10
+ const BRIDGE_RUNTIME_GUARD_PROMPT = `Do not terminate the Claude Rust bridge runtime process \`${BRIDGE_RUNTIME_PROCESS_NAME}\`; ` +
11
+ "when cleaning up development servers, only stop processes by explicit PIDs you started in this session.";
12
+ const GERMAN_LANGUAGE_PROMPT = "Always respond to the user in German unless the user explicitly asks for a different language. " +
13
+ "Keep code, shell commands, file paths, API names, tool names, and raw error text unchanged unless the user explicitly asks for translation.";
9
14
  function makeSessionState() {
10
15
  const input = new AsyncQueue();
11
16
  return {
@@ -28,6 +33,7 @@ function makeSessionState() {
28
33
  pendingQuestions: new Map(),
29
34
  pendingElicitations: new Map(),
30
35
  mcpStatusRevalidatedAt: new Map(),
36
+ hiddenToolUseIds: new Set(),
31
37
  authHintSent: false,
32
38
  };
33
39
  }
@@ -425,8 +431,7 @@ test("buildQueryOptions maps launch settings into sdk query options", () => {
425
431
  assert.deepEqual(options.systemPrompt, {
426
432
  type: "preset",
427
433
  preset: "claude_code",
428
- append: "Always respond to the user in German unless the user explicitly asks for a different language. " +
429
- "Keep code, shell commands, file paths, API names, tool names, and raw error text unchanged unless the user explicitly asks for translation.",
434
+ append: `${BRIDGE_RUNTIME_GUARD_PROMPT} ${GERMAN_LANGUAGE_PROMPT}`,
430
435
  });
431
436
  assert.equal(options.model, "haiku");
432
437
  assert.equal(options.permissionMode, "plan");
@@ -536,7 +541,7 @@ test("buildQueryOptions enables dangerous skip flag for bypass permissions start
536
541
  assert.equal(options.permissionMode, "bypassPermissions");
537
542
  assert.equal(options.allowDangerouslySkipPermissions, true);
538
543
  });
539
- test("buildQueryOptions omits startup overrides for default logout path", () => {
544
+ test("buildQueryOptions omits optional startup overrides but keeps bridge guard prompt", () => {
540
545
  const input = new AsyncQueue();
541
546
  const options = buildQueryOptions({
542
547
  cwd: "C:/work",
@@ -551,7 +556,11 @@ test("buildQueryOptions omits startup overrides for default logout path", () =>
551
556
  assert.equal("model" in options, false);
552
557
  assert.equal("permissionMode" in options, false);
553
558
  assert.equal("allowDangerouslySkipPermissions" in options, false);
554
- assert.equal("systemPrompt" in options, false);
559
+ assert.deepEqual(options.systemPrompt, {
560
+ type: "preset",
561
+ preset: "claude_code",
562
+ append: BRIDGE_RUNTIME_GUARD_PROMPT,
563
+ });
555
564
  assert.equal("agentProgressSummaries" in options, false);
556
565
  });
557
566
  test("buildQueryOptions makes sandbox fallback explicit when enabled", () => {
@@ -714,6 +723,161 @@ test("handleTaskSystemMessage final summary replaces prior task content and fina
714
723
  });
715
724
  assert.equal(session.taskToolUseIds.has("task-1"), false);
716
725
  });
726
+ test("handleTaskSystemMessage ignores lifecycle content for concrete output tools", () => {
727
+ const session = makeSessionState();
728
+ const protectedTools = [
729
+ createToolCall("tool-bash", "Bash", { command: "git status" }),
730
+ createToolCall("tool-read", "Read", { file_path: "src/main.rs" }),
731
+ createToolCall("tool-write", "Write", {
732
+ file_path: "src/main.rs",
733
+ content: "updated file contents",
734
+ }),
735
+ ];
736
+ for (const toolCall of protectedTools) {
737
+ toolCall.status = "in_progress";
738
+ toolCall.raw_output = `actual output for ${toolCall.tool_call_id}`;
739
+ session.toolCalls.set(toolCall.tool_call_id, toolCall);
740
+ }
741
+ const events = captureBridgeEvents(() => {
742
+ for (const toolCall of protectedTools) {
743
+ const taskId = `task-${toolCall.tool_call_id}`;
744
+ handleTaskSystemMessage(session, "task_started", {
745
+ task_id: taskId,
746
+ tool_use_id: toolCall.tool_call_id,
747
+ description: "Show working tree status",
748
+ });
749
+ handleTaskSystemMessage(session, "task_notification", {
750
+ task_id: taskId,
751
+ tool_use_id: toolCall.tool_call_id,
752
+ status: "completed",
753
+ summary: "Show diff summary for unstaged changes",
754
+ });
755
+ }
756
+ });
757
+ assert.deepEqual(events, []);
758
+ for (const toolCall of protectedTools) {
759
+ const stored = session.toolCalls.get(toolCall.tool_call_id);
760
+ assert.equal(stored?.status, "in_progress");
761
+ assert.equal(stored?.raw_output, `actual output for ${toolCall.tool_call_id}`);
762
+ }
763
+ });
764
+ test("handleSdkMessage ignores tool_use_summary for Bash Read and Write tools", () => {
765
+ const session = makeSessionState();
766
+ const protectedTools = [
767
+ createToolCall("tool-bash", "Bash", { command: "git diff" }),
768
+ createToolCall("tool-read", "Read", { file_path: "src/main.rs" }),
769
+ createToolCall("tool-write", "Write", {
770
+ file_path: "src/main.rs",
771
+ content: "updated file contents",
772
+ }),
773
+ ];
774
+ for (const toolCall of protectedTools) {
775
+ toolCall.status = "completed";
776
+ toolCall.raw_output = `actual output for ${toolCall.tool_call_id}`;
777
+ session.toolCalls.set(toolCall.tool_call_id, toolCall);
778
+ }
779
+ const events = captureBridgeEvents(() => {
780
+ handleSdkMessage(session, {
781
+ type: "tool_use_summary",
782
+ summary: "Show commits on this branch since diverging from main",
783
+ preceding_tool_use_ids: protectedTools.map((toolCall) => toolCall.tool_call_id),
784
+ uuid: "message-summary",
785
+ session_id: "session-1",
786
+ });
787
+ });
788
+ assert.deepEqual(events, []);
789
+ for (const toolCall of protectedTools) {
790
+ assert.equal(session.toolCalls.get(toolCall.tool_call_id)?.raw_output, `actual output for ${toolCall.tool_call_id}`);
791
+ }
792
+ });
793
+ test("handleSdkMessage applies tool_use_summary for summary-oriented tools", () => {
794
+ const session = makeSessionState();
795
+ const toolCall = createToolCall("tool-agent", "Agent", { prompt: "Inspect auth flow" });
796
+ session.toolCalls.set(toolCall.tool_call_id, toolCall);
797
+ const events = captureBridgeEvents(() => {
798
+ handleSdkMessage(session, {
799
+ type: "tool_use_summary",
800
+ summary: "Inspected auth flow and found the failing check",
801
+ preceding_tool_use_ids: [toolCall.tool_call_id],
802
+ uuid: "message-summary",
803
+ session_id: "session-1",
804
+ });
805
+ });
806
+ const lastEvent = events.at(-1);
807
+ assert.ok(lastEvent);
808
+ assert.equal(lastEvent.event, "session_update");
809
+ assert.deepEqual(lastEvent.update, {
810
+ type: "tool_call_update",
811
+ tool_call_update: {
812
+ tool_call_id: "tool-agent",
813
+ fields: {
814
+ status: "completed",
815
+ raw_output: "Inspected auth flow and found the failing check",
816
+ content: [
817
+ {
818
+ type: "content",
819
+ content: { type: "text", text: "Inspected auth flow and found the failing check" },
820
+ },
821
+ ],
822
+ },
823
+ },
824
+ });
825
+ assert.equal(session.toolCalls.get(toolCall.tool_call_id)?.raw_output, "Inspected auth flow and found the failing check");
826
+ });
827
+ test("handleSdkMessage suppresses ToolSearch bridge events without denying SDK use", () => {
828
+ const session = makeSessionState();
829
+ const events = captureBridgeEvents(() => {
830
+ handleSdkMessage(session, {
831
+ type: "stream_event",
832
+ event: {
833
+ type: "content_block_start",
834
+ content_block: {
835
+ type: "server_tool_use",
836
+ id: "tool-search-1",
837
+ name: "ToolSearch",
838
+ input: { query: "src/" },
839
+ },
840
+ },
841
+ uuid: "message-search-start",
842
+ session_id: "session-1",
843
+ });
844
+ handleSdkMessage(session, {
845
+ type: "tool_progress",
846
+ tool_use_id: "tool-search-1",
847
+ tool_name: "ToolSearch",
848
+ uuid: "message-search-progress",
849
+ session_id: "session-1",
850
+ });
851
+ handleSdkMessage(session, {
852
+ type: "user",
853
+ parent_tool_use_id: "tool-search-1",
854
+ tool_use_result: { content: "matched src/main.rs", is_error: false },
855
+ message: {
856
+ role: "user",
857
+ content: [
858
+ {
859
+ type: "tool_search_tool_result",
860
+ tool_use_id: "tool-search-1",
861
+ content: "matched src/main.rs",
862
+ is_error: false,
863
+ },
864
+ ],
865
+ },
866
+ uuid: "message-search-result",
867
+ session_id: "session-1",
868
+ });
869
+ handleSdkMessage(session, {
870
+ type: "tool_use_summary",
871
+ summary: "Found source files",
872
+ preceding_tool_use_ids: ["tool-search-1"],
873
+ uuid: "message-search-summary",
874
+ session_id: "session-1",
875
+ });
876
+ });
877
+ assert.deepEqual(events, []);
878
+ assert.equal(session.hiddenToolUseIds.has("tool-search-1"), true);
879
+ assert.equal(session.toolCalls.has("tool-search-1"), false);
880
+ });
717
881
  test("handleTaskSystemMessage applies task_updated description patches to the linked task", () => {
718
882
  const session = makeSessionState();
719
883
  const events = captureBridgeEvents(() => {
@@ -873,8 +1037,7 @@ test("buildQueryOptions trims language before appending system prompt", () => {
873
1037
  assert.deepEqual(options.systemPrompt, {
874
1038
  type: "preset",
875
1039
  preset: "claude_code",
876
- append: "Always respond to the user in German unless the user explicitly asks for a different language. " +
877
- "Keep code, shell commands, file paths, API names, tool names, and raw error text unchanged unless the user explicitly asks for translation.",
1040
+ append: `${BRIDGE_RUNTIME_GUARD_PROMPT} ${GERMAN_LANGUAGE_PROMPT}`,
878
1041
  });
879
1042
  });
880
1043
  test("parseCommandEnvelope rejects missing required fields", () => {
@@ -1139,7 +1302,28 @@ test("buildApiRetryUpdate maps SDK api_retry messages to wire shape", () => {
1139
1302
  error_status: null,
1140
1303
  error: "unknown",
1141
1304
  });
1305
+ assert.deepEqual(buildApiRetryUpdate({
1306
+ attempt: 1,
1307
+ max_retries: 10,
1308
+ retry_delay_ms: 549.8881698459426,
1309
+ error_status: null,
1310
+ error: "unexpected",
1311
+ }), {
1312
+ type: "api_retry_update",
1313
+ attempt: 1,
1314
+ max_retries: 10,
1315
+ retry_delay_ms: 549.8881698459426,
1316
+ error_status: null,
1317
+ error: "unknown",
1318
+ });
1142
1319
  assert.equal(buildApiRetryUpdate({ attempt: 1 }), null);
1320
+ assert.equal(buildApiRetryUpdate({
1321
+ attempt: 1,
1322
+ max_retries: 10,
1323
+ retry_delay_ms: -1,
1324
+ error_status: null,
1325
+ error: "server_error",
1326
+ }), null);
1143
1327
  });
1144
1328
  test("normalizeSettingsParseError accepts only SDK-shaped errors", () => {
1145
1329
  assert.deepEqual(normalizeSettingsParseError({
@@ -1673,7 +1857,7 @@ test("looksLikeAuthRequired detects login hints", () => {
1673
1857
  assert.equal(looksLikeAuthRequired("normal tool output"), false);
1674
1858
  });
1675
1859
  test("agent sdk version compatibility check matches pinned version", () => {
1676
- assert.equal(resolveInstalledAgentSdkVersion(), "0.2.112");
1860
+ assert.equal(resolveInstalledAgentSdkVersion(), "0.3.146");
1677
1861
  assert.equal(agentSdkVersionCompatibilityError(), undefined);
1678
1862
  });
1679
1863
  test("mapSessionMessagesToUpdates maps message content blocks", () => {
@@ -1735,6 +1919,55 @@ test("mapSessionMessagesToUpdates maps message content blocks", () => {
1735
1919
  assert.equal(variantCounts.get("tool_call"), 1);
1736
1920
  assert.equal(variantCounts.get("tool_call_update"), 1);
1737
1921
  });
1922
+ test("mapSessionMessagesToUpdates suppresses ToolSearch history blocks", () => {
1923
+ const updates = mapSessionMessagesToUpdates([
1924
+ {
1925
+ type: "assistant",
1926
+ uuid: "a1",
1927
+ session_id: "s1",
1928
+ parent_tool_use_id: null,
1929
+ message: {
1930
+ role: "assistant",
1931
+ content: [
1932
+ {
1933
+ type: "server_tool_use",
1934
+ id: "tool-search-1",
1935
+ name: "ToolSearch",
1936
+ input: { query: "src/" },
1937
+ },
1938
+ { type: "tool_use", id: "tool-bash", name: "Bash", input: { command: "echo ok" } },
1939
+ ],
1940
+ },
1941
+ },
1942
+ {
1943
+ type: "user",
1944
+ uuid: "u1",
1945
+ session_id: "s1",
1946
+ parent_tool_use_id: null,
1947
+ message: {
1948
+ role: "user",
1949
+ content: [
1950
+ {
1951
+ type: "tool_search_tool_result",
1952
+ tool_use_id: "tool-search-1",
1953
+ content: "matched src/main.rs",
1954
+ is_error: false,
1955
+ },
1956
+ {
1957
+ type: "tool_result",
1958
+ tool_use_id: "tool-bash",
1959
+ content: "ok",
1960
+ is_error: false,
1961
+ },
1962
+ ],
1963
+ },
1964
+ },
1965
+ ]);
1966
+ const toolCalls = updates.filter((update) => update.type === "tool_call");
1967
+ const toolUpdates = updates.filter((update) => update.type === "tool_call_update");
1968
+ assert.deepEqual(toolCalls.map((update) => update.tool_call.tool_call_id), ["tool-bash"]);
1969
+ assert.deepEqual(toolUpdates.map((update) => update.tool_call_update.tool_call_id), ["tool-bash"]);
1970
+ });
1738
1971
  test("mapSessionMessagesToUpdates preserves parallel tool results", () => {
1739
1972
  const updates = mapSessionMessagesToUpdates([
1740
1973
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-rust",
3
- "version": "0.11.2",
3
+ "version": "0.12.0",
4
4
  "description": "Claude Code Rust - native Rust terminal interface for Claude Code",
5
5
  "keywords": [
6
6
  "cli",
@@ -29,7 +29,10 @@
29
29
  "README.md"
30
30
  ],
31
31
  "dependencies": {
32
- "@anthropic-ai/claude-agent-sdk": "0.2.112"
32
+ "@anthropic-ai/claude-agent-sdk": "0.3.146",
33
+ "@anthropic-ai/sdk": "0.97.1",
34
+ "@modelcontextprotocol/sdk": "1.29.0",
35
+ "zod": "4.4.3"
33
36
  },
34
37
  "scripts": {
35
38
  "postinstall": "node ./scripts/postinstall.js",
@@ -4,6 +4,7 @@
4
4
  const fs = require("node:fs");
5
5
  const path = require("node:path");
6
6
  const https = require("node:https");
7
+ const { spawnSync } = require("node:child_process");
7
8
  const { pipeline } = require("node:stream/promises");
8
9
 
9
10
  const TARGETS = {
@@ -14,6 +15,8 @@ const TARGETS = {
14
15
  };
15
16
 
16
17
  const MAX_REDIRECTS = 5;
18
+ const BRIDGE_RUNTIME_EXE =
19
+ process.platform === "win32" ? "claude-rs-bridge-node.exe" : "claude-rs-bridge-node";
17
20
 
18
21
  function getTargetInfo() {
19
22
  return TARGETS[`${process.platform}:${process.arch}`];
@@ -56,6 +59,49 @@ async function downloadFile(url, outPath, redirects = 0) {
56
59
  });
57
60
  }
58
61
 
62
+ function installRenamedBridgeRuntime(installDir) {
63
+ const sourcePath = process.execPath;
64
+ const runtimePath = path.join(installDir, BRIDGE_RUNTIME_EXE);
65
+
66
+ try {
67
+ if (!sourcePath || !fs.existsSync(sourcePath)) {
68
+ throw new Error("current Node.js executable could not be resolved");
69
+ }
70
+
71
+ if (path.resolve(sourcePath) !== path.resolve(runtimePath)) {
72
+ fs.copyFileSync(sourcePath, runtimePath);
73
+ }
74
+
75
+ if (process.platform !== "win32") {
76
+ fs.chmodSync(runtimePath, 0o755);
77
+ }
78
+
79
+ const result = spawnSync(runtimePath, ["--version"], {
80
+ encoding: "utf8",
81
+ windowsHide: true
82
+ });
83
+ const version = String(result.stdout || "").trim();
84
+
85
+ if (result.status !== 0 || !/^v\d+\./.test(version)) {
86
+ throw new Error(
87
+ `copied runtime failed validation${result.stderr ? `: ${result.stderr.trim()}` : ""}`
88
+ );
89
+ }
90
+
91
+ console.log(`Installed renamed Agent SDK bridge runtime ${BRIDGE_RUNTIME_EXE} (${version})`);
92
+ } catch (error) {
93
+ try {
94
+ fs.rmSync(runtimePath, { force: true });
95
+ } catch {
96
+ // Best-effort cleanup only; the Rust binary can still fall back to `node`.
97
+ }
98
+ console.warn(
99
+ `Skipping renamed Agent SDK bridge runtime: ${error.message}. ` +
100
+ "claude-rs will fall back to the `node` executable on PATH."
101
+ );
102
+ }
103
+ }
104
+
59
105
  async function main() {
60
106
  const info = getTargetInfo();
61
107
  if (!info) {
@@ -83,6 +129,8 @@ async function main() {
83
129
  fs.chmodSync(binaryPath, 0o755);
84
130
  }
85
131
 
132
+ installRenamedBridgeRuntime(installDir);
133
+
86
134
  console.log(`Installed claude-code-rust ${version} (${info.target})`);
87
135
  }
88
136