claude-code-rust 0.12.4 → 0.13.2

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
@@ -4,11 +4,15 @@ A native Rust terminal interface for Claude Code. Drop-in replacement for Anthro
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/claude-code-rust)](https://www.npmjs.com/package/claude-code-rust)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/claude-code-rust)](https://www.npmjs.com/package/claude-code-rust)
7
- [![CI](https://github.com/srothgan/claude-code-rust/actions/workflows/ci.yml/badge.svg)](https://github.com/srothgan/claude-code-rust/actions/workflows/ci.yml)
7
+ [![CI](https://github.com/srothgan/claude-code-rust/actions/workflows/pr.yml/badge.svg)](https://github.com/srothgan/claude-code-rust/actions/workflows/pr.yml)
8
8
  [![Docs](https://img.shields.io/badge/docs-GitHub%20Pages-blue)](https://srothgan.github.io/claude-code-rust/)
9
9
  [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)
10
10
  [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-green.svg)](https://nodejs.org/)
11
11
 
12
+ <p align="center">
13
+ <img src="assets/banner.png" alt="Claude Code Rust running a Read tool call with syntax-highlighted output" width="900">
14
+ </p>
15
+
12
16
  ## About
13
17
 
14
18
  Claude Code Rust replaces the stock Claude Code terminal interface with a native Rust binary built on [Ratatui](https://ratatui.rs/). It connects to the same Claude API through a local Agent SDK bridge. Core Claude Code functionality - tool calls, file editing, terminal commands, and permissions - works unchanged.
@@ -26,7 +30,14 @@ Claude Code Rust replaces the stock Claude Code terminal interface with a native
26
30
  npm install -g claude-code-rust
27
31
  ```
28
32
 
29
- The published package installs a `claude-rs` command and fetches the matching prebuilt release binary for your platform during install.
33
+ The npm package installs a small launcher plus a platform-specific optional dependency containing the prebuilt Rust binary for your OS and architecture.
34
+
35
+ If `claude-rs` reports a missing platform package, check whether optional dependencies were omitted:
36
+
37
+ ```bash
38
+ npm config get omit
39
+ npm install -g claude-code-rust
40
+ ```
30
41
 
31
42
  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.
32
43
 
@@ -43,15 +54,17 @@ Full documentation is available at [srothgan.github.io/claude-code-rust](https:/
43
54
 
44
55
  ## Why
45
56
 
46
- The stock Claude Code TUI runs on Node.js with React Ink. This causes real problems:
57
+ The stock Claude Code TUI runs on Node.js with React Ink, which renders by redrawing full frames over raw ANSI escape codes. This causes real, widely-reported problems:
47
58
 
48
- - **Memory**: 200-400MB baseline vs ~20-50MB for a native binary
49
- - **Startup**: 2-5 seconds vs under 100ms
50
- - **Scrollback**: Broken virtual scrolling that loses history
51
- - **Input latency**: Event queue delays on keystroke handling
52
- - **Copy/paste**: Custom implementation instead of native terminal support
59
+ - **Flickering**: The whole view is redrawn on every status update, causing constant flicker — bad enough to crash editors' integrated terminals during long sessions
60
+ - **CPU**: Sustained high CPU even when idle, and runaway loops that spawn multiple background processes
61
+ - **Memory**: 200-400MB baseline (and climbing with conversation length) vs ~20-50MB for a native binary
62
+ - **Resize**: Window resizing leaves duplicated frames in scrollback, loses lines when shrinking, and can garble the display
63
+ - **Input latency**: Keystrokes echo with visible delay as context fills up, and noticeably worse on Windows
64
+ - **Scrollback**: Hijacks the terminal's native scrollback, erasing history you can no longer scroll back to
65
+ - **Paste**: Large pastes can flood stdout and freeze the terminal
53
66
 
54
- Claude Code Rust fixes all of these by compiling to a single native binary with direct terminal control via Crossterm.
67
+ Claude Code Rust addresses these by compiling to a single native binary with diffed, direct terminal control via Crossterm and Ratatui — no full-frame redraws, no Node runtime overhead.
55
68
 
56
69
  ## Documentation
57
70
 
@@ -48,6 +48,16 @@ function optionalTimeout(record, context) {
48
48
  }
49
49
  return value;
50
50
  }
51
+ function optionalRequestTimeoutMs(record, context) {
52
+ const value = record.request_timeout_ms;
53
+ if (value === undefined) {
54
+ return undefined;
55
+ }
56
+ if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value < 1000) {
57
+ throw new Error(`${context}.request_timeout_ms must be an integer >= 1000`);
58
+ }
59
+ return value;
60
+ }
51
61
  function optionalAlwaysLoad(record, context) {
52
62
  const value = record.always_load;
53
63
  if (value === undefined) {
@@ -98,6 +108,7 @@ export function parseMcpServerConfig(value, context) {
98
108
  throw new Error(`${context}.type must be a string`);
99
109
  }
100
110
  const timeout = optionalTimeout(record, context);
111
+ const requestTimeoutMs = optionalRequestTimeoutMs(record, context);
101
112
  const alwaysLoad = optionalAlwaysLoad(record, context);
102
113
  switch (type) {
103
114
  case "stdio": {
@@ -114,6 +125,7 @@ export function parseMcpServerConfig(value, context) {
114
125
  ...(optionalStringArray(record, "args", context) ? { args: optionalStringArray(record, "args", context) } : {}),
115
126
  ...(optionalStringMap(record, "env", context) ? { env: optionalStringMap(record, "env", context) } : {}),
116
127
  ...(timeout === undefined ? {} : { timeout }),
128
+ ...(requestTimeoutMs === undefined ? {} : { request_timeout_ms: requestTimeoutMs }),
117
129
  ...(alwaysLoad === undefined ? {} : { always_load: alwaysLoad }),
118
130
  };
119
131
  }
@@ -130,6 +142,7 @@ export function parseMcpServerConfig(value, context) {
130
142
  ...(optionalStringMap(record, "headers", context) ? { headers: optionalStringMap(record, "headers", context) } : {}),
131
143
  ...(tools === undefined ? {} : { tools }),
132
144
  ...(timeout === undefined ? {} : { timeout }),
145
+ ...(requestTimeoutMs === undefined ? {} : { request_timeout_ms: requestTimeoutMs }),
133
146
  ...(alwaysLoad === undefined ? {} : { always_load: alwaysLoad }),
134
147
  };
135
148
  }
@@ -148,6 +161,9 @@ function toSdkToolPolicies(tools) {
148
161
  ...(tool.org_max_permission === undefined ? {} : { org_max_permission: tool.org_max_permission }),
149
162
  }));
150
163
  }
164
+ function sdkRequestTimeoutConfig(config) {
165
+ return config.request_timeout_ms === undefined ? {} : { requestTimeoutMs: config.request_timeout_ms };
166
+ }
151
167
  export function bridgeMcpConfigToSdk(config) {
152
168
  switch (config.type) {
153
169
  case "stdio":
@@ -157,6 +173,7 @@ export function bridgeMcpConfigToSdk(config) {
157
173
  ...(config.args ? { args: config.args } : {}),
158
174
  ...(config.env ? { env: config.env } : {}),
159
175
  ...(config.timeout === undefined ? {} : { timeout: config.timeout }),
176
+ ...sdkRequestTimeoutConfig(config),
160
177
  ...(config.always_load === undefined ? {} : { alwaysLoad: config.always_load }),
161
178
  };
162
179
  case "sse":
@@ -166,6 +183,7 @@ export function bridgeMcpConfigToSdk(config) {
166
183
  ...(config.headers ? { headers: config.headers } : {}),
167
184
  ...(config.tools ? { tools: toSdkToolPolicies(config.tools) } : {}),
168
185
  ...(config.timeout === undefined ? {} : { timeout: config.timeout }),
186
+ ...sdkRequestTimeoutConfig(config),
169
187
  ...(config.always_load === undefined ? {} : { alwaysLoad: config.always_load }),
170
188
  };
171
189
  case "http":
@@ -175,6 +193,7 @@ export function bridgeMcpConfigToSdk(config) {
175
193
  ...(config.headers ? { headers: config.headers } : {}),
176
194
  ...(config.tools ? { tools: toSdkToolPolicies(config.tools) } : {}),
177
195
  ...(config.timeout === undefined ? {} : { timeout: config.timeout }),
196
+ ...sdkRequestTimeoutConfig(config),
178
197
  ...(config.always_load === undefined ? {} : { alwaysLoad: config.always_load }),
179
198
  };
180
199
  }
@@ -250,6 +269,16 @@ export function mapMcpServerStatus(status) {
250
269
  : [],
251
270
  };
252
271
  }
272
+ function sdkRequestTimeoutMs(config) {
273
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
274
+ return undefined;
275
+ }
276
+ const raw = config;
277
+ const value = raw.requestTimeoutMs ?? raw.request_timeout_ms;
278
+ return typeof value === "number" && Number.isFinite(value) && Number.isInteger(value)
279
+ ? value
280
+ : undefined;
281
+ }
253
282
  export function mapMcpServerStatusConfig(config) {
254
283
  switch (config.type) {
255
284
  case "stdio":
@@ -259,6 +288,7 @@ export function mapMcpServerStatusConfig(config) {
259
288
  ...(Array.isArray(config.args) && config.args.length > 0 ? { args: config.args } : {}),
260
289
  ...(config.env ? { env: config.env } : {}),
261
290
  ...(config.timeout === undefined ? {} : { timeout: config.timeout }),
291
+ ...(sdkRequestTimeoutMs(config) === undefined ? {} : { request_timeout_ms: sdkRequestTimeoutMs(config) }),
262
292
  ...(config.alwaysLoad === undefined ? {} : { always_load: config.alwaysLoad }),
263
293
  };
264
294
  case "sse": {
@@ -269,6 +299,7 @@ export function mapMcpServerStatusConfig(config) {
269
299
  ...(config.headers ? { headers: config.headers } : {}),
270
300
  ...(tools === undefined ? {} : { tools }),
271
301
  ...(config.timeout === undefined ? {} : { timeout: config.timeout }),
302
+ ...(sdkRequestTimeoutMs(config) === undefined ? {} : { request_timeout_ms: sdkRequestTimeoutMs(config) }),
272
303
  ...(config.alwaysLoad === undefined ? {} : { always_load: config.alwaysLoad }),
273
304
  };
274
305
  }
@@ -280,6 +311,7 @@ export function mapMcpServerStatusConfig(config) {
280
311
  ...(config.headers ? { headers: config.headers } : {}),
281
312
  ...(tools === undefined ? {} : { tools }),
282
313
  ...(config.timeout === undefined ? {} : { timeout: config.timeout }),
314
+ ...(sdkRequestTimeoutMs(config) === undefined ? {} : { request_timeout_ms: sdkRequestTimeoutMs(config) }),
283
315
  ...(config.alwaysLoad === undefined ? {} : { always_load: config.alwaysLoad }),
284
316
  };
285
317
  }
@@ -321,6 +353,7 @@ function mcpStatusConfigDiagnostics(config) {
321
353
  return {
322
354
  config_type: "stdio",
323
355
  ...(config.timeout === undefined ? {} : { timeout_ms: config.timeout }),
356
+ ...(config.request_timeout_ms === undefined ? {} : { request_timeout_ms: config.request_timeout_ms }),
324
357
  ...(config.always_load === undefined ? {} : { always_load: config.always_load }),
325
358
  configured_tool_policy_count: 0,
326
359
  };
@@ -329,6 +362,7 @@ function mcpStatusConfigDiagnostics(config) {
329
362
  return {
330
363
  config_type: config.type,
331
364
  ...(config.timeout === undefined ? {} : { timeout_ms: config.timeout }),
365
+ ...(config.request_timeout_ms === undefined ? {} : { request_timeout_ms: config.request_timeout_ms }),
332
366
  ...(config.always_load === undefined ? {} : { always_load: config.always_load }),
333
367
  configured_tool_policy_count: config.tools?.length ?? 0,
334
368
  };
@@ -359,6 +393,7 @@ export function summarizeMcpServersForDiagnostics(servers) {
359
393
  config_type: config.config_type,
360
394
  ...(server.scope ? { scope: server.scope } : {}),
361
395
  ...(config.timeout_ms === undefined ? {} : { timeout_ms: config.timeout_ms }),
396
+ ...(config.request_timeout_ms === undefined ? {} : { request_timeout_ms: config.request_timeout_ms }),
362
397
  ...(config.always_load === undefined ? {} : { always_load: config.always_load }),
363
398
  tool_count: server.tools.length,
364
399
  configured_tool_policy_count: config.configured_tool_policy_count,
@@ -1,11 +1,6 @@
1
- const OPUS_MODEL_ALIAS = "opus";
1
+ const DEFAULT_MODEL_ALIAS = "fable";
2
2
  const MAX_MODEL_VERSION_PARTS = 2;
3
3
  const RELEASE_BUILD_TOKEN = /^20\d{6}$/;
4
- function isUnavailableModelId(id) {
5
- const normalized = id.trim().toLowerCase();
6
- // TODO: Revisit only after a product decision if Anthropic restores Fable 5 access.
7
- return normalized === "fable" || normalized.startsWith("claude-fable-5");
8
- }
9
4
  function isEffortLevel(value) {
10
5
  return (value === "low" ||
11
6
  value === "medium" ||
@@ -19,7 +14,7 @@ function normalizeModelKey(id) {
19
14
  return { original, family: "unknown", versionParts: [], variantParts: [], buildParts: [] };
20
15
  }
21
16
  const lower = original.toLowerCase();
22
- const contextMatch = lower.match(/\[([^\]]+)\]$/);
17
+ const contextMatch = lower.match(/\[([^[\]]+)\]$/);
23
18
  const contextSuffix = contextMatch?.[1];
24
19
  const withoutContext = contextMatch ? lower.slice(0, contextMatch.index) : lower;
25
20
  const withoutPrefix = withoutContext.startsWith("claude-")
@@ -27,7 +22,10 @@ function normalizeModelKey(id) {
27
22
  : withoutContext;
28
23
  const parts = withoutPrefix.split("-").filter((part) => part.length > 0);
29
24
  const familyPart = parts[0] ?? "";
30
- const family = familyPart === "opus" || familyPart === "sonnet" || familyPart === "haiku"
25
+ const family = familyPart === "fable" ||
26
+ familyPart === "opus" ||
27
+ familyPart === "sonnet" ||
28
+ familyPart === "haiku"
31
29
  ? familyPart
32
30
  : "unknown";
33
31
  const versionParts = [];
@@ -36,6 +34,10 @@ function normalizeModelKey(id) {
36
34
  if (family !== "unknown") {
37
35
  for (const part of parts.slice(1)) {
38
36
  if (/^\d+$/.test(part)) {
37
+ if (versionParts.length > 0 && RELEASE_BUILD_TOKEN.test(part)) {
38
+ buildParts.push(part);
39
+ continue;
40
+ }
39
41
  if (versionParts.length < MAX_MODEL_VERSION_PARTS) {
40
42
  const parsed = Number.parseInt(part, 10);
41
43
  if (Number.isFinite(parsed)) {
@@ -120,11 +122,13 @@ function humanizeModelId(id) {
120
122
  if (normalized.family === "unknown") {
121
123
  return id;
122
124
  }
123
- const familyLabel = normalized.family === "opus"
124
- ? "Opus"
125
- : normalized.family === "sonnet"
126
- ? "Sonnet"
127
- : "Haiku";
125
+ const familyLabel = normalized.family === "fable"
126
+ ? "Fable"
127
+ : normalized.family === "opus"
128
+ ? "Opus"
129
+ : normalized.family === "sonnet"
130
+ ? "Sonnet"
131
+ : "Haiku";
128
132
  const versionLabel = normalized.versionParts.length > 0 ? ` ${normalized.versionParts.join(".")}` : "";
129
133
  const contextLabel = normalized.contextSuffix?.toLowerCase() === "1m"
130
134
  ? " [1M]"
@@ -144,19 +148,23 @@ function currentModelIsAuthoritative(resolvedId, requestedId) {
144
148
  return true;
145
149
  }
146
150
  function resolveCatalogModel(availableModels, resolvedId, requestedId) {
147
- const exactResolved = availableModels.find((entry) => entry.id === resolvedId);
151
+ const exactResolved = availableModels.find((entry) => entry.id === resolvedId || entry.resolved_model === resolvedId);
148
152
  if (exactResolved) {
149
153
  return exactResolved;
150
154
  }
151
155
  if (requestedId) {
152
- const exactRequested = availableModels.find((entry) => entry.id === requestedId);
156
+ const exactRequested = availableModels.find((entry) => entry.id === requestedId || entry.resolved_model === requestedId);
153
157
  if (exactRequested &&
154
- modelKeysAreCompatible(exactRequested.id, resolvedId) &&
158
+ (modelKeysAreCompatible(exactRequested.id, resolvedId) ||
159
+ (exactRequested.resolved_model !== undefined &&
160
+ modelKeysAreCompatible(exactRequested.resolved_model, resolvedId))) &&
155
161
  !hasVariantSiblingConflict(availableModels, exactRequested.id, resolvedId)) {
156
162
  return exactRequested;
157
163
  }
158
164
  }
159
- const compatible = availableModels.filter((entry) => modelKeysAreCompatible(entry.id, resolvedId) &&
165
+ const compatible = availableModels.filter((entry) => (modelKeysAreCompatible(entry.id, resolvedId) ||
166
+ (entry.resolved_model !== undefined &&
167
+ modelKeysAreCompatible(entry.resolved_model, resolvedId))) &&
160
168
  !hasVariantSiblingConflict(availableModels, entry.id, resolvedId));
161
169
  return compatible.length === 1 ? compatible[0] : undefined;
162
170
  }
@@ -168,12 +176,14 @@ export function mapAvailableModels(models) {
168
176
  .filter((entry) => {
169
177
  return (typeof entry?.value === "string" &&
170
178
  entry.value.trim().length > 0 &&
171
- !isUnavailableModelId(entry.value) &&
172
179
  typeof entry.displayName === "string" &&
173
180
  entry.displayName.trim().length > 0);
174
181
  })
175
182
  .map((entry) => ({
176
183
  id: entry.value,
184
+ ...(typeof entry.resolvedModel === "string" && entry.resolvedModel.trim().length > 0
185
+ ? { resolved_model: entry.resolvedModel.trim() }
186
+ : {}),
177
187
  display_name: entry.displayName,
178
188
  supports_effort: entry.supportsEffort === true,
179
189
  supported_effort_levels: Array.isArray(entry.supportedEffortLevels)
@@ -198,9 +208,9 @@ export function resolveCurrentModel(session) {
198
208
  const resolvedId = session.resolvedRuntimeModelId?.trim() ||
199
209
  session.model.trim() ||
200
210
  requestedId ||
201
- OPUS_MODEL_ALIAS;
211
+ DEFAULT_MODEL_ALIAS;
202
212
  const catalogModel = resolveCatalogModel(session.availableModels, resolvedId, requestedId);
203
- const runtimeDisplayId = resolvedId || requestedId || OPUS_MODEL_ALIAS;
213
+ const runtimeDisplayId = resolvedId || requestedId || DEFAULT_MODEL_ALIAS;
204
214
  const displayNameShort = shortDisplayNameForModelId(runtimeDisplayId);
205
215
  const displayNameLong = catalogModel?.display_name ?? humanizeModelId(runtimeDisplayId);
206
216
  return {
@@ -19,7 +19,7 @@ export { shouldEmitStartupAuthRequiredForAccount } from "./account_metadata.js";
19
19
  const BRIDGE_RUNTIME_PROCESS_NAME = process.platform === "win32" ? "claude-rs-bridge-node.exe" : "claude-rs-bridge-node";
20
20
  const BRIDGE_RUNTIME_GUARD_PROMPT = `Do not terminate the Claude Rust bridge runtime process \`${BRIDGE_RUNTIME_PROCESS_NAME}\`; ` +
21
21
  "when cleaning up development servers, only stop processes by explicit PIDs you started in this session.";
22
- const STARTUP_FALLBACK_MODEL_ALIAS = "opus";
22
+ const STARTUP_FALLBACK_MODEL_ALIAS = "fable";
23
23
  function permissionDisplayFromCanUseOptions(options) {
24
24
  const title = typeof options.title === "string" ? options.title.trim() : "";
25
25
  const displayName = typeof options.displayName === "string" ? options.displayName.trim() : "";
@@ -600,6 +600,7 @@ export function buildQueryOptions(params) {
600
600
  const systemPrompt = systemPromptFromLaunchSettings(params.launchSettings);
601
601
  const modelOption = startupModelOption(params.launchSettings);
602
602
  const permissionModeOptions = startupPermissionModeOptions(params.launchSettings);
603
+ const shouldPassCanUseTool = permissionModeOptions.permissionMode !== "bypassPermissions";
603
604
  const settings = normalizedSettingsFromLaunchSettings(params.launchSettings);
604
605
  return {
605
606
  cwd: params.cwd,
@@ -658,7 +659,7 @@ export function buildQueryOptions(params) {
658
659
  settingSources: DEFAULT_SETTING_SOURCES,
659
660
  resume: params.resume,
660
661
  ...(params.resumeSessionAt ? { resumeSessionAt: params.resumeSessionAt } : {}),
661
- canUseTool: params.canUseTool,
662
+ ...(shouldPassCanUseTool ? { canUseTool: params.canUseTool } : {}),
662
663
  onElicitation: async (request) => {
663
664
  const requestId = randomUUID();
664
665
  const mode = request.mode === "form" || request.mode === "url"
@@ -142,6 +142,32 @@ function buildQuestionRequest(promptToolCall, prompt, index, total) {
142
142
  function askUserQuestionTranscript(answers) {
143
143
  return answers.map((entry) => `${entry.header}: ${entry.answer}\n ${entry.question}`).join("\n");
144
144
  }
145
+ function askUserQuestionCompletedRawInput(prompts, answers, annotations, questionResults) {
146
+ return {
147
+ questions: prompts.map((prompt) => ({
148
+ question: prompt.question,
149
+ header: prompt.header,
150
+ multiSelect: prompt.multiSelect,
151
+ options: prompt.options.map((option) => ({
152
+ label: option.label,
153
+ description: option.description,
154
+ ...(option.preview ? { preview: option.preview } : {}),
155
+ })),
156
+ })),
157
+ answers,
158
+ ...(Object.keys(annotations).length > 0 ? { annotations: questionAnnotationsJson(annotations) } : {}),
159
+ question_results: questionResults,
160
+ };
161
+ }
162
+ function questionAnnotationsJson(annotations) {
163
+ return Object.fromEntries(Object.entries(annotations).map(([question, annotation]) => [
164
+ question,
165
+ {
166
+ ...(annotation.preview ? { preview: annotation.preview } : {}),
167
+ ...(annotation.notes ? { notes: annotation.notes } : {}),
168
+ },
169
+ ]));
170
+ }
145
171
  function deriveAnnotation(selectedOptions, annotation) {
146
172
  const preview = annotation?.preview?.trim().length
147
173
  ? annotation.preview
@@ -166,6 +192,7 @@ export async function requestAskUserQuestionAnswers(session, toolUseId, inputDat
166
192
  const answers = {};
167
193
  const annotations = {};
168
194
  const transcript = [];
195
+ const questionResults = [];
169
196
  for (const [index, prompt] of prompts.entries()) {
170
197
  const promptToolCall = askUserQuestionPromptToolCall(baseToolCall, prompt, index, prompts.length);
171
198
  const fields = {
@@ -214,11 +241,35 @@ export async function requestAskUserQuestionAnswers(session, toolUseId, inputDat
214
241
  annotations[prompt.question] = annotation;
215
242
  }
216
243
  transcript.push({ header: prompt.header, question: prompt.question, answer });
244
+ questionResults.push({
245
+ question: prompt.question,
246
+ header: prompt.header,
247
+ question_index: index,
248
+ total_questions: prompts.length,
249
+ selected_options: selectedOptions.map((option) => ({
250
+ option_id: option.option_id,
251
+ label: option.label,
252
+ ...(option.description ? { description: option.description } : {}),
253
+ ...(option.preview ? { preview: option.preview } : {}),
254
+ })),
255
+ ...(annotation
256
+ ? {
257
+ annotation: {
258
+ ...(annotation.preview ? { preview: annotation.preview } : {}),
259
+ ...(annotation.notes ? { notes: annotation.notes } : {}),
260
+ },
261
+ }
262
+ : {}),
263
+ });
217
264
  const summary = askUserQuestionTranscript(transcript);
265
+ const completed = index + 1 >= prompts.length;
218
266
  const progressFields = {
219
- status: index + 1 >= prompts.length ? "completed" : "in_progress",
267
+ status: completed ? "completed" : "in_progress",
220
268
  raw_output: summary,
221
269
  content: [{ type: "content", content: { type: "text", text: summary } }],
270
+ ...(completed
271
+ ? { raw_input: askUserQuestionCompletedRawInput(prompts, answers, annotations, questionResults) }
272
+ : {}),
222
273
  };
223
274
  emitToolCallUpdate(session, toolUseId, progressFields, "summary");
224
275
  }
@@ -129,7 +129,7 @@ export function emitAgentConfigOptionUpdate(sessionId, agent) {
129
129
  value: agent,
130
130
  });
131
131
  }
132
- const EXPECTED_AGENT_SDK_VERSION = "0.3.193";
132
+ const EXPECTED_AGENT_SDK_VERSION = "0.3.198";
133
133
  const require = createRequire(import.meta.url);
134
134
  export function resolveInstalledAgentSdkVersion() {
135
135
  try {
@@ -261,6 +261,19 @@ function emitRewindResult(sessionId, restoreMode, status, requestId, fileResult,
261
261
  ...(message ? { message } : {}),
262
262
  }, requestId);
263
263
  }
264
+ function requestIdFromCommandLine(line) {
265
+ try {
266
+ const parsed = JSON.parse(line);
267
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
268
+ return undefined;
269
+ }
270
+ const requestId = parsed.request_id;
271
+ return typeof requestId === "string" ? requestId : undefined;
272
+ }
273
+ catch {
274
+ return undefined;
275
+ }
276
+ }
264
277
  async function replaceConversationForRewind(command, session, targetUserMessageId, requestId, pendingRewindResult) {
265
278
  const historyMessages = await getSessionMessages(command.session_id, {
266
279
  dir: session.cwd,
@@ -1145,11 +1158,13 @@ function main() {
1145
1158
  }
1146
1159
  catch (error) {
1147
1160
  const message = error instanceof Error ? error.message : String(error);
1161
+ const requestId = requestIdFromCommandLine(line);
1148
1162
  bridgeLogger.error({
1149
1163
  target: LOG_TARGETS.BRIDGE_PROTOCOL,
1150
1164
  eventName: "bridge_command_decode_failed",
1151
1165
  message: "failed to decode bridge command envelope",
1152
1166
  outcome: "failure",
1167
+ ...(requestId ? { requestId } : {}),
1153
1168
  sizeBytes: Buffer.byteLength(line),
1154
1169
  fields: {
1155
1170
  preview: line.slice(0, 240),
@@ -1157,7 +1172,7 @@ function main() {
1157
1172
  error_message: message,
1158
1173
  },
1159
1174
  });
1160
- failConnection(`invalid command envelope: ${message}`);
1175
+ failConnection(`invalid command envelope: ${message}`, requestId);
1161
1176
  return;
1162
1177
  }
1163
1178
  try {
@@ -0,0 +1,4 @@
1
+ {
2
+ "private": true,
3
+ "type": "module"
4
+ }
package/bin/claude-rs.js CHANGED
@@ -6,10 +6,22 @@ const fs = require("node:fs");
6
6
  const path = require("node:path");
7
7
 
8
8
  const TARGETS = {
9
- "darwin:arm64": { target: "aarch64-apple-darwin", exe: "claude-rs" },
10
- "darwin:x64": { target: "x86_64-apple-darwin", exe: "claude-rs" },
11
- "linux:x64": { target: "x86_64-unknown-linux-gnu", exe: "claude-rs" },
12
- "win32:x64": { target: "x86_64-pc-windows-msvc", exe: "claude-rs.exe" }
9
+ "darwin:arm64": {
10
+ packageName: "@srothgan/claude-code-rust-darwin-arm64",
11
+ exe: "claude-rs"
12
+ },
13
+ "darwin:x64": {
14
+ packageName: "@srothgan/claude-code-rust-darwin-x64",
15
+ exe: "claude-rs"
16
+ },
17
+ "linux:x64": {
18
+ packageName: "@srothgan/claude-code-rust-linux-x64-gnu",
19
+ exe: "claude-rs"
20
+ },
21
+ "win32:x64": {
22
+ packageName: "@srothgan/claude-code-rust-win32-x64-msvc",
23
+ exe: "claude-rs.exe"
24
+ }
13
25
  };
14
26
 
15
27
  function resolveInstall() {
@@ -19,16 +31,43 @@ function resolveInstall() {
19
31
  return { error: `Unsupported platform/arch for claude-rs: ${key}` };
20
32
  }
21
33
 
22
- const binaryPath = path.join(__dirname, "..", "vendor", info.target, info.exe);
34
+ let packageJsonPath;
35
+ try {
36
+ packageJsonPath = require.resolve(`${info.packageName}/package.json`);
37
+ } catch (error) {
38
+ if (error && error.code === "MODULE_NOT_FOUND") {
39
+ return {
40
+ error:
41
+ `Missing platform package ${info.packageName} for ${key}.\n` +
42
+ "This usually means npm optional dependencies were omitted.\n" +
43
+ "Check `npm config get omit`, then reinstall with:\n" +
44
+ " npm install -g claude-code-rust"
45
+ };
46
+ }
47
+ throw error;
48
+ }
49
+
50
+ const binaryPath = path.join(path.dirname(packageJsonPath), "bin", info.exe);
23
51
  if (!fs.existsSync(binaryPath)) {
24
52
  return {
25
53
  error:
26
54
  `Missing binary at ${binaryPath}\n` +
27
- "Reinstall with `npm install -g claude-code-rust` to fetch release artifacts."
55
+ `The installed ${info.packageName} package is incomplete. Reinstall with:\n` +
56
+ " npm install -g claude-code-rust"
57
+ };
58
+ }
59
+
60
+ const bundledBridgeScript = path.join(__dirname, "..", "agent-sdk", "dist", "bridge.js");
61
+ if (!fs.existsSync(bundledBridgeScript)) {
62
+ return {
63
+ error:
64
+ `Missing bundled bridge at ${bundledBridgeScript}\n` +
65
+ "The installed claude-code-rust package is incomplete. Reinstall with:\n" +
66
+ " npm install -g claude-code-rust"
28
67
  };
29
68
  }
30
69
 
31
- return { binaryPath };
70
+ return { binaryPath, bundledBridgeScript };
32
71
  }
33
72
 
34
73
  const resolved = resolveInstall();
@@ -38,6 +77,10 @@ if (resolved.error) {
38
77
  }
39
78
 
40
79
  const child = spawn(resolved.binaryPath, process.argv.slice(2), {
80
+ env: {
81
+ ...process.env,
82
+ CLAUDE_RS_AGENT_BRIDGE: process.env.CLAUDE_RS_AGENT_BRIDGE || resolved.bundledBridgeScript
83
+ },
41
84
  stdio: "inherit",
42
85
  windowsHide: true
43
86
  });
package/package.json CHANGED
@@ -1,13 +1,18 @@
1
1
  {
2
2
  "name": "claude-code-rust",
3
- "version": "0.12.4",
3
+ "version": "0.13.2",
4
4
  "description": "Claude Code Rust - native Rust terminal interface for Claude Code",
5
5
  "keywords": [
6
6
  "cli",
7
7
  "tui",
8
8
  "claude",
9
+ "claude-code",
10
+ "anthropic",
11
+ "agent-sdk",
9
12
  "ai",
10
- "terminal"
13
+ "terminal",
14
+ "rust",
15
+ "ratatui"
11
16
  ],
12
17
  "license": "Apache-2.0",
13
18
  "repository": {
@@ -23,25 +28,21 @@
23
28
  },
24
29
  "files": [
25
30
  "bin",
26
- "agent-sdk/dist",
27
- "scripts",
28
- "LICENSE",
29
- "README.md"
31
+ "agent-sdk",
32
+ "README.md",
33
+ "LICENSE"
30
34
  ],
31
35
  "dependencies": {
32
- "@anthropic-ai/claude-agent-sdk": "0.3.193",
36
+ "@anthropic-ai/claude-agent-sdk": "0.3.198",
33
37
  "@anthropic-ai/sdk": "0.106.0",
34
38
  "@modelcontextprotocol/sdk": "1.29.0",
35
39
  "zod": "4.4.3"
36
40
  },
37
- "scripts": {
38
- "postinstall": "node ./scripts/postinstall.js",
39
- "prepack": "npm --prefix agent-sdk run build",
40
- "quality:duplicates": "jscpd --config .jscpd.json src agent-sdk/src scripts bin --no-tips --no-colors",
41
- "quality:duplicates:summary": "node scripts/jscpd-warning-summary.mjs jscpd-report/jscpd-report.json"
42
- },
43
- "devDependencies": {
44
- "jscpd": "5.0.11"
41
+ "optionalDependencies": {
42
+ "@srothgan/claude-code-rust-darwin-arm64": "0.13.2",
43
+ "@srothgan/claude-code-rust-darwin-x64": "0.13.2",
44
+ "@srothgan/claude-code-rust-linux-x64-gnu": "0.13.2",
45
+ "@srothgan/claude-code-rust-win32-x64-msvc": "0.13.2"
45
46
  },
46
47
  "engines": {
47
48
  "node": ">=18"
@@ -1,17 +0,0 @@
1
- # claude-rs agent-sdk bridge
2
-
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
-
5
- ## Local build
6
-
7
- ```bash
8
- npm install
9
- npm run build
10
- ```
11
-
12
- Build output is written to `dist/bridge.js`.
13
-
14
- ## License
15
-
16
- This bridge is part of the `claude-code-rust` project and is licensed under
17
- the Apache License 2.0. See the repository root [LICENSE](../LICENSE).