claude-code-rust 0.14.0 → 0.14.1

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
@@ -18,14 +18,12 @@ Claude Code Rust replaces the stock Claude Code terminal interface with a native
18
18
 
19
19
  ## Prerequisite
20
20
 
21
- - Install the Claude Code CLI. You do not need to authenticate before installing or starting `claude-rs`; sign in later with `/login` or `claude auth login` when needed.
21
+ - The Claude Code CLI must be installed as fallback for some SDK-unsupported features.
22
22
 
23
23
  ## Install
24
24
 
25
25
  ### Install script (recommended, v0.14.0+)
26
26
 
27
- The install script downloads a complete GitHub Release archive. It does not require Rust, Node.js, npm, or Bun on the user's machine.
28
-
29
27
  **macOS/Linux:**
30
28
 
31
29
  ```bash
@@ -40,15 +38,11 @@ powershell -NoProfile -ExecutionPolicy Bypass -Command "irm 'https://raw.githubu
40
38
 
41
39
  ### npm (global)
42
40
 
43
- npm remains supported for users who prefer package-manager ownership of the global command:
44
-
45
41
  ```bash
46
42
  npm install -g claude-code-rust
47
43
  ```
48
44
 
49
- This option requires Node.js 24 and npm. The npm package installs a small launcher plus a platform-specific optional dependency containing the prebuilt Rust binary, Agent SDK bridge, and bundled private Bun runtime for your OS and architecture. A separate Rust toolchain or Bun installation is not required.
50
-
51
- See the [installation guide](https://srothgan.github.io/claude-code-rust/installation.html) for release pinning, custom locations, switching install methods, and troubleshooting.
45
+ See the [installation guide](https://srothgan.github.io/claude-code-rust/installation.html) for release pinning, custom install locations, switching install methods, uninstall, and troubleshooting.
52
46
 
53
47
  ## Usage
54
48
 
@@ -77,13 +71,15 @@ Claude Code Rust addresses these with a native terminal UI that uses diffed, dir
77
71
 
78
72
  ## Documentation
79
73
 
80
- The manual covers installation with scripts, npm, and source builds, plus help, slash commands, keyboard shortcuts, settings, diagnostics, architecture, and the changelog:
74
+ The manual covers installation with scripts and npm, plus help, slash commands, keyboard shortcuts, settings, diagnostics, troubleshooting, building from source, architecture, and the changelog:
81
75
 
82
76
  - [Installation](https://srothgan.github.io/claude-code-rust/installation.html)
83
77
  - [Usage](https://srothgan.github.io/claude-code-rust/usage.html)
84
78
  - [Help](https://srothgan.github.io/claude-code-rust/help.html)
85
79
  - [Slash commands](https://srothgan.github.io/claude-code-rust/commands.html)
86
80
  - [Settings](https://srothgan.github.io/claude-code-rust/settings.html)
81
+ - [Troubleshooting](https://srothgan.github.io/claude-code-rust/troubleshooting.html)
82
+ - [Development](https://srothgan.github.io/claude-code-rust/development.html)
87
83
 
88
84
  ## Status
89
85
 
@@ -4,6 +4,7 @@ const KNOWN_API_PROVIDERS = new Set([
4
4
  "vertex",
5
5
  "foundry",
6
6
  "anthropicAws",
7
+ "anthropicGoogleCloud",
7
8
  "mantle",
8
9
  "gateway",
9
10
  ]);
@@ -8,7 +8,7 @@ import { linkTaskToolUse, unlinkTaskToolUse } from "./task_links.js";
8
8
  import { emitAuthRequired, classifyTurnErrorKind, emitFastModeUpdateIfChanged } from "./error_classification.js";
9
9
  import { mapAvailableAgentsFromNames, emitAvailableAgentsIfChanged, refreshAvailableAgents } from "./agents.js";
10
10
  import { mapInitSlashCommands, mapSdkSlashCommands, updateAvailableCommands, } from "./available_commands.js";
11
- import { buildApiRetryUpdate, buildRateLimitUpdate, normalizeSettingsParseErrors, numberField, parseApiRetryError, parseRuntimeSessionState, } from "./state_parsing.js";
11
+ import { buildApiRetryUpdate, buildRateLimitUpdate, buildSubagentRetryUpdate, normalizeSettingsParseErrors, numberField, parseApiRetryError, parseRuntimeSessionState, } from "./state_parsing.js";
12
12
  import { looksLikeAuthRequired } from "./auth.js";
13
13
  import { emitCurrentModelUpdate, refreshCurrentModel, updateSessionId } from "./session_lifecycle.js";
14
14
  import { bridgeLogger, LOG_TARGETS } from "./logger.js";
@@ -1159,7 +1159,29 @@ export function handleSdkMessage(session, message) {
1159
1159
  },
1160
1160
  });
1161
1161
  if (resolvedToolUseId) {
1162
- emitToolProgressUpdate(session, resolvedToolUseId, toolName);
1162
+ const hasSubagentRetry = Object.hasOwn(msg, "subagent_retry");
1163
+ const subagentRetry = hasSubagentRetry ? buildSubagentRetryUpdate(msg) : null;
1164
+ if (hasSubagentRetry && !subagentRetry) {
1165
+ bridgeLogger.warn({
1166
+ target: LOG_TARGETS.APP_TOOL,
1167
+ eventName: "sdk_subagent_retry_rejected",
1168
+ message: "ignored malformed SDK subagent retry progress",
1169
+ outcome: "invalid_payload",
1170
+ sessionId: session.sessionId,
1171
+ toolCallId: resolvedToolUseId,
1172
+ });
1173
+ }
1174
+ const subagentType = typeof msg.subagent_type === "string" && msg.subagent_type.trim()
1175
+ ? msg.subagent_type.trim()
1176
+ : undefined;
1177
+ emitToolProgressUpdate(session, resolvedToolUseId, toolName, {
1178
+ ...(subagentRetry
1179
+ ? { subagentRetry }
1180
+ : hasSubagentRetry
1181
+ ? {}
1182
+ : { subagentRetry: { state: "clear" } }),
1183
+ ...(subagentType ? { subagentType } : {}),
1184
+ });
1163
1185
  }
1164
1186
  return;
1165
1187
  }
@@ -100,23 +100,28 @@ function settingsObjectFromLaunchSettings(launchSettings) {
100
100
  return launchSettings.settings;
101
101
  }
102
102
  function normalizedSettingsFromLaunchSettings(launchSettings) {
103
- const settings = settingsObjectFromLaunchSettings(launchSettings);
104
- if (!settings) {
105
- return undefined;
106
- }
103
+ const settings = settingsObjectFromLaunchSettings(launchSettings) ?? {};
104
+ // SendFeedback queues a local draft that can only be reviewed, edited, and
105
+ // discarded through the native /feedback surface. Agent SDK command
106
+ // snapshots do not expose that command to this host, so enabling drafts
107
+ // would create content that claude-rs cannot safely let the user approve.
108
+ const hostSettings = {
109
+ ...settings,
110
+ feedbackDrafts: "off",
111
+ };
107
112
  const sandbox = settings.sandbox && typeof settings.sandbox === "object" && !Array.isArray(settings.sandbox)
108
113
  ? settings.sandbox
109
114
  : undefined;
110
115
  if (sandbox?.enabled === true && sandbox.failIfUnavailable === undefined) {
111
116
  return {
112
- ...settings,
117
+ ...hostSettings,
113
118
  sandbox: {
114
119
  ...sandbox,
115
120
  failIfUnavailable: false,
116
121
  },
117
122
  };
118
123
  }
119
- return settings;
124
+ return hostSettings;
120
125
  }
121
126
  export function sessionById(sessionId) {
122
127
  return sessions.get(sessionId) ?? null;
@@ -612,9 +617,13 @@ export function buildQueryOptions(params) {
612
617
  includePartialMessages: true,
613
618
  promptSuggestions: true,
614
619
  enableFileCheckpointing: true,
620
+ // ProposeSkills reports only a proposal count and expects a native review
621
+ // surface. Keep it out of this host until claude-rs can display the actual
622
+ // proposal input and accept/reject it without losing content.
623
+ disallowedTools: ["ProposeSkills"],
615
624
  executable: "bun",
616
625
  ...(params.resume ? {} : { sessionId: params.provisionalSessionId }),
617
- ...(settings ? { settings } : {}),
626
+ settings,
618
627
  ...modelOption,
619
628
  ...permissionModeOptions,
620
629
  toolConfig: { askUserQuestion: { previewFormat: "markdown" } },
@@ -15,6 +15,34 @@ function nonNegativeNumberField(record, ...keys) {
15
15
  }
16
16
  return value;
17
17
  }
18
+ function nonNegativeIntegerField(record, ...keys) {
19
+ const value = numberField(record, ...keys);
20
+ return value !== undefined && value >= 0 && Number.isInteger(value) ? value : undefined;
21
+ }
22
+ export function buildSubagentRetryUpdate(message) {
23
+ const retry = asRecordOrNull(message.subagent_retry);
24
+ if (!retry) {
25
+ return null;
26
+ }
27
+ const attempt = nonNegativeIntegerField(retry, "attempt");
28
+ const maxRetries = nonNegativeIntegerField(retry, "max_retries", "maxRetries");
29
+ const retryDelayMs = nonNegativeIntegerField(retry, "retry_delay_ms", "retryDelayMs");
30
+ if (attempt === undefined || maxRetries === undefined || retryDelayMs === undefined) {
31
+ return null;
32
+ }
33
+ const agentId = typeof retry.agent_id === "string" ? retry.agent_id.trim() : "";
34
+ const errorCategory = typeof retry.error_category === "string" ? retry.error_category.trim() : "";
35
+ const errorStatus = nonNegativeIntegerField(retry, "error_status", "errorStatus");
36
+ return {
37
+ state: "waiting",
38
+ ...(agentId ? { agent_id: agentId } : {}),
39
+ attempt,
40
+ max_retries: maxRetries,
41
+ retry_delay_ms: retryDelayMs,
42
+ ...(errorStatus !== undefined ? { error_status: errorStatus } : {}),
43
+ ...(errorCategory ? { error_category: errorCategory } : {}),
44
+ };
45
+ }
18
46
  export function parseFastModeState(value) {
19
47
  if (value === "off" || value === "cooldown" || value === "on") {
20
48
  return value;
@@ -112,10 +112,14 @@ function mergeTaskMetadata(current, update) {
112
112
  if (update === undefined) {
113
113
  return current;
114
114
  }
115
- return {
115
+ const merged = {
116
116
  ...(current ?? {}),
117
117
  ...update,
118
118
  };
119
+ if (update.subagent_retry?.state === "clear") {
120
+ delete merged.subagent_retry;
121
+ }
122
+ return merged;
119
123
  }
120
124
  function applyFieldsToBase(base, fields) {
121
125
  if (fields.title !== undefined) {
@@ -237,6 +241,16 @@ function taskTitleContext(session, name, input) {
237
241
  }
238
242
  export function emitToolCallUpdate(session, toolUseId, fields, updateKind, sourceMessageUuid) {
239
243
  const base = session.toolCalls.get(toolUseId);
244
+ const nextStatus = fields.status ?? base?.status;
245
+ const terminal = nextStatus === "completed" || nextStatus === "failed" || nextStatus === "killed";
246
+ const hasActiveRetry = base?.task_metadata?.subagent_retry?.state === "waiting" ||
247
+ fields.task_metadata?.subagent_retry?.state === "waiting";
248
+ if (terminal && hasActiveRetry) {
249
+ fields.task_metadata = {
250
+ ...(fields.task_metadata ?? {}),
251
+ subagent_retry: { state: "clear" },
252
+ };
253
+ }
240
254
  logToolCallUpdateEmitted(session.sessionId, toolUseId, fields, base, updateKind);
241
255
  emitSessionUpdate(session.sessionId, {
242
256
  type: "tool_call_update",
@@ -324,19 +338,39 @@ export function finalizeOpenToolCalls(session, status) {
324
338
  emitToolCallUpdate(session, toolUseId, { status }, "finalize");
325
339
  }
326
340
  }
327
- export function emitToolProgressUpdate(session, toolUseId, toolName) {
328
- const existing = session.toolCalls.get(toolUseId);
341
+ export function emitToolProgressUpdate(session, toolUseId, toolName, progress = {}) {
342
+ let existing = session.toolCalls.get(toolUseId);
329
343
  if (!existing) {
330
344
  emitToolCall(session, toolUseId, toolName, {});
331
- return;
345
+ existing = session.toolCalls.get(toolUseId);
332
346
  }
333
- if (existing.status === "in_progress" ||
347
+ if (!existing ||
334
348
  existing.status === "completed" ||
335
349
  existing.status === "failed" ||
336
350
  existing.status === "killed") {
337
351
  return;
338
352
  }
339
- emitToolCallUpdate(session, toolUseId, { status: "in_progress" }, "progress");
353
+ const taskMetadata = {};
354
+ if (progress.subagentRetry?.state === "waiting") {
355
+ taskMetadata.subagent_retry = progress.subagentRetry;
356
+ }
357
+ else if (progress.subagentRetry?.state === "clear" &&
358
+ existing.task_metadata?.subagent_retry?.state === "waiting") {
359
+ taskMetadata.subagent_retry = progress.subagentRetry;
360
+ }
361
+ if (progress.subagentType && progress.subagentType !== existing.task_metadata?.subagent_type) {
362
+ taskMetadata.subagent_type = progress.subagentType;
363
+ }
364
+ const fields = {};
365
+ if (existing.status !== "in_progress") {
366
+ fields.status = "in_progress";
367
+ }
368
+ if (Object.keys(taskMetadata).length > 0) {
369
+ fields.task_metadata = taskMetadata;
370
+ }
371
+ if (Object.keys(fields).length > 0) {
372
+ emitToolCallUpdate(session, toolUseId, fields, "progress");
373
+ }
340
374
  }
341
375
  export function emitToolSummaryUpdate(session, toolUseId, summary) {
342
376
  const base = session.toolCalls.get(toolUseId);
@@ -482,9 +482,19 @@ function extractToolOutputMetadata(toolName, rawResult, rawContent) {
482
482
  if (toolName === "Bash") {
483
483
  for (const candidate of candidates) {
484
484
  const hasAssistantAutoBackgrounded = typeof candidate.assistantAutoBackgrounded === "boolean";
485
- if (hasAssistantAutoBackgrounded) {
485
+ const timedOutAfterMs = nonNegativeInteger(candidate.timedOutAfterMs);
486
+ const backgroundCwdHint = nonEmptyString(candidate.backgroundCwdHint);
487
+ if (hasAssistantAutoBackgrounded || timedOutAfterMs !== undefined || backgroundCwdHint) {
486
488
  const bashMetadata = {};
487
- bashMetadata.assistant_auto_backgrounded = candidate.assistantAutoBackgrounded;
489
+ if (hasAssistantAutoBackgrounded) {
490
+ bashMetadata.assistant_auto_backgrounded = candidate.assistantAutoBackgrounded;
491
+ }
492
+ if (timedOutAfterMs !== undefined) {
493
+ bashMetadata.timed_out_after_ms = timedOutAfterMs;
494
+ }
495
+ if (backgroundCwdHint) {
496
+ bashMetadata.background_cwd_hint = backgroundCwdHint;
497
+ }
488
498
  metadata.bash = bashMetadata;
489
499
  break;
490
500
  }
@@ -493,9 +503,11 @@ function extractToolOutputMetadata(toolName, rawResult, rawContent) {
493
503
  if (toolName === "Agent" || toolName === "Task") {
494
504
  for (const candidate of candidates) {
495
505
  const resolvedModel = nonEmptyString(candidate.resolvedModel);
496
- if (resolvedModel) {
506
+ const modelsUsed = orderedNonEmptyStrings(candidate.modelsUsed);
507
+ if (resolvedModel || modelsUsed) {
497
508
  const agentMetadata = {
498
- resolved_model: resolvedModel,
509
+ ...(resolvedModel ? { resolved_model: resolvedModel } : {}),
510
+ ...(modelsUsed ? { models_used: modelsUsed } : {}),
499
511
  };
500
512
  metadata.agent = agentMetadata;
501
513
  break;
@@ -736,6 +748,10 @@ function shellBackgroundMessage(record) {
736
748
  if (!backgroundTaskId) {
737
749
  return "";
738
750
  }
751
+ const timedOutAfterMs = nonNegativeInteger(record.timedOutAfterMs);
752
+ if (timedOutAfterMs !== undefined) {
753
+ return `Command was auto-backgrounded after ${timedOutAfterMs.toLocaleString("en-US")} ms with ID: ${backgroundTaskId}.`;
754
+ }
739
755
  if (record.assistantAutoBackgrounded === true) {
740
756
  return `Command was auto-backgrounded by assistant mode with ID: ${backgroundTaskId}.`;
741
757
  }
@@ -812,6 +828,9 @@ function recordNumber(record, key) {
812
828
  const value = record[key];
813
829
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
814
830
  }
831
+ function recordNonNegativeInteger(record, key) {
832
+ return nonNegativeInteger(record[key]);
833
+ }
815
834
  function recordString(record, key) {
816
835
  const value = record[key];
817
836
  return typeof value === "string" ? value : undefined;
@@ -857,8 +876,13 @@ function globResultText(record) {
857
876
  function grepResultText(record) {
858
877
  const filenames = searchFilenames(record);
859
878
  const content = recordString(record, "content") ?? "";
860
- const numFiles = recordNumber(record, "numFiles") ?? filenames.length;
861
- const numLines = recordNumber(record, "numLines");
879
+ const hasVisibleMatches = content.trim().length > 0 || filenames.length > 0;
880
+ const totalFiles = recordNonNegativeInteger(record, "totalFiles");
881
+ const legacyNumFiles = recordNonNegativeInteger(record, "numFiles");
882
+ const numFiles = totalFiles ??
883
+ (legacyNumFiles !== 0 || !hasVisibleMatches ? legacyNumFiles : undefined) ??
884
+ (filenames.length > 0 ? filenames.length : undefined);
885
+ const numLines = recordNonNegativeInteger(record, "totalLines") ?? recordNonNegativeInteger(record, "numLines");
862
886
  const numMatches = recordNumber(record, "numMatches");
863
887
  const appliedLimit = recordNumber(record, "appliedLimit");
864
888
  const appliedOffset = recordNumber(record, "appliedOffset");
@@ -878,7 +902,9 @@ function grepResultText(record) {
878
902
  lines.push("No matches found");
879
903
  }
880
904
  const summaryParts = [];
881
- summaryParts.push(`${numFiles} ${pluralize(numFiles, "file")}`);
905
+ if (numFiles !== undefined) {
906
+ summaryParts.push(`${numFiles} ${pluralize(numFiles, "file")}`);
907
+ }
882
908
  if (numMatches !== undefined) {
883
909
  summaryParts.push(`${numMatches} ${pluralize(numMatches, "match", "matches")}`);
884
910
  }
@@ -937,6 +963,26 @@ function pushBooleanField(lines, label, value) {
937
963
  function nonEmptyString(value) {
938
964
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
939
965
  }
966
+ function nonNegativeInteger(value) {
967
+ return typeof value === "number" && Number.isFinite(value) && value >= 0
968
+ ? Math.trunc(value)
969
+ : undefined;
970
+ }
971
+ function orderedNonEmptyStrings(value) {
972
+ if (!Array.isArray(value)) {
973
+ return undefined;
974
+ }
975
+ const seen = new Set();
976
+ const values = [];
977
+ for (const item of value) {
978
+ const normalized = nonEmptyString(item);
979
+ if (normalized && !seen.has(normalized)) {
980
+ seen.add(normalized);
981
+ values.push(normalized);
982
+ }
983
+ }
984
+ return values.length > 0 ? values : undefined;
985
+ }
940
986
  const CRON_MONTH_NAMES = [
941
987
  "January",
942
988
  "February",
@@ -108,9 +108,23 @@ export async function generatePersistedSessionTitle(query, description) {
108
108
  return title;
109
109
  }
110
110
  export async function applySessionEffort(query, effort) {
111
- const settings = { effortLevel: effort };
112
- // applyFlagSettings controls live session settings; SDK Settings typings model persisted effort levels.
113
- await query.applyFlagSettings(settings);
111
+ await query.applyFlagSettings({ effortLevel: effort });
112
+ }
113
+ export function buildPromptUserMessage(command, sessionId) {
114
+ const content = contentFromPrompt(command);
115
+ if (content.length === 0) {
116
+ return undefined;
117
+ }
118
+ return {
119
+ type: "user",
120
+ session_id: sessionId,
121
+ parent_tool_use_id: null,
122
+ origin: { kind: "human" },
123
+ message: {
124
+ role: "user",
125
+ content,
126
+ },
127
+ };
114
128
  }
115
129
  export async function applySessionAgent(query, agent) {
116
130
  const settings = { agent };
@@ -130,7 +144,7 @@ export function emitAgentConfigOptionUpdate(sessionId, agent) {
130
144
  value: agent,
131
145
  });
132
146
  }
133
- const EXPECTED_AGENT_SDK_VERSION = "0.3.207";
147
+ const EXPECTED_AGENT_SDK_VERSION = "0.3.214";
134
148
  const require = createRequire(import.meta.url);
135
149
  export function resolveInstalledAgentSdkVersion() {
136
150
  try {
@@ -662,19 +676,10 @@ async function handleCommand(command, requestId) {
662
676
  slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
663
677
  return;
664
678
  }
665
- const content = contentFromPrompt(command);
666
- if (content.length === 0) {
679
+ const message = buildPromptUserMessage(command, session.sessionId);
680
+ if (!message) {
667
681
  return;
668
682
  }
669
- const message = {
670
- type: "user",
671
- session_id: session.sessionId,
672
- parent_tool_use_id: null,
673
- message: {
674
- role: "user",
675
- content,
676
- },
677
- };
678
683
  session.input.enqueue(message);
679
684
  return;
680
685
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-rust",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "description": "Claude Code Rust - native Rust terminal interface for Claude Code",
5
5
  "keywords": [
6
6
  "cli",
@@ -33,15 +33,15 @@
33
33
  "LICENSE"
34
34
  ],
35
35
  "dependencies": {
36
- "@anthropic-ai/claude-agent-sdk": "0.3.207"
36
+ "@anthropic-ai/claude-agent-sdk": "0.3.214"
37
37
  },
38
38
  "optionalDependencies": {
39
- "@srothgan/claude-code-rust-darwin-arm64": "0.14.0",
40
- "@srothgan/claude-code-rust-darwin-x64": "0.14.0",
41
- "@srothgan/claude-code-rust-linux-x64-gnu": "0.14.0",
42
- "@srothgan/claude-code-rust-linux-arm64-gnu": "0.14.0",
43
- "@srothgan/claude-code-rust-win32-x64-msvc": "0.14.0",
44
- "@srothgan/claude-code-rust-win32-arm64-msvc": "0.14.0"
39
+ "@srothgan/claude-code-rust-darwin-arm64": "0.14.1",
40
+ "@srothgan/claude-code-rust-darwin-x64": "0.14.1",
41
+ "@srothgan/claude-code-rust-linux-x64-gnu": "0.14.1",
42
+ "@srothgan/claude-code-rust-linux-arm64-gnu": "0.14.1",
43
+ "@srothgan/claude-code-rust-win32-x64-msvc": "0.14.1",
44
+ "@srothgan/claude-code-rust-win32-arm64-msvc": "0.14.1"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=24"