claude-code-rust 0.12.2 → 0.12.3

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
@@ -38,12 +38,8 @@ claude-rs
38
38
 
39
39
  Full documentation is available at [srothgan.github.io/claude-code-rust](https://srothgan.github.io/claude-code-rust/).
40
40
 
41
- > [!WARNING]
42
- > **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.
43
- >
44
- > Sources:
45
- > - [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)
46
- > - [ClaudeDevs announcement](https://x.com/ClaudeDevs/status/2054610152817619388)
41
+ > [!NOTE]
42
+ > **Agent SDK billing unchanged.** Anthropic has paused the previously announced Agent SDK credit change. For now nothing changes: Claude Agent SDK usage — including `claude -p` and third-party apps like this one still draws from your normal Claude subscription limits. See [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).
47
43
 
48
44
  ## Why
49
45
 
@@ -85,7 +81,7 @@ This project is not affiliated with, endorsed by, or supported by Anthropic.
85
81
 
86
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.
87
83
 
88
- 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.
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 any future changes Anthropic may make to how Agent SDK usage is metered. 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.
89
85
 
90
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).
91
87
 
@@ -50,6 +50,9 @@ function inputBoolean(input, key) {
50
50
  function isAgentLikeToolName(name) {
51
51
  return name === "Agent" || name === "Task";
52
52
  }
53
+ export function isShellToolName(name) {
54
+ return name === "Bash" || name === "PowerShell";
55
+ }
53
56
  function agentInputTitle(name, input) {
54
57
  if (!isAgentLikeToolName(name)) {
55
58
  return undefined;
@@ -135,9 +138,10 @@ function formatGrepTitle(input) {
135
138
  return flags.length > 0 ? `${scoped} (${flags.join(", ")})` : scoped;
136
139
  }
137
140
  export function normalizeToolKind(name) {
141
+ if (isShellToolName(name)) {
142
+ return "execute";
143
+ }
138
144
  switch (name) {
139
- case "Bash":
140
- return "execute";
141
145
  case "Read":
142
146
  case "ReadMcpResource":
143
147
  return "read";
@@ -189,7 +193,7 @@ export function toolTitle(name, input, context = {}) {
189
193
  if (agentTitle) {
190
194
  return agentTitle;
191
195
  }
192
- if (name === "Bash") {
196
+ if (isShellToolName(name)) {
193
197
  const command = typeof input.command === "string" ? input.command : "";
194
198
  return command || "Terminal";
195
199
  }
@@ -684,14 +688,14 @@ function editDiffFromResult(rawResult, rawInput) {
684
688
  }
685
689
  return editDiffFromInput(rawInput);
686
690
  }
687
- function findBashResultRecord(rawResult, rawContent) {
691
+ function findShellResultRecord(rawResult, rawContent) {
688
692
  return resultRecordCandidates(rawResult, rawContent).find((candidate) => "stdout" in candidate ||
689
693
  "stderr" in candidate ||
690
694
  "backgroundTaskId" in candidate ||
691
695
  "backgroundedByUser" in candidate ||
692
696
  "assistantAutoBackgrounded" in candidate);
693
697
  }
694
- function bashBackgroundMessage(record) {
698
+ function shellBackgroundMessage(record) {
695
699
  const backgroundTaskId = typeof record.backgroundTaskId === "string" ? record.backgroundTaskId : "";
696
700
  if (!backgroundTaskId) {
697
701
  return "";
@@ -704,7 +708,7 @@ function bashBackgroundMessage(record) {
704
708
  }
705
709
  return `Command is running in background with ID: ${backgroundTaskId}.`;
706
710
  }
707
- function buildBashDisplayOutput(record) {
711
+ function buildShellDisplayOutput(record) {
708
712
  const segments = [];
709
713
  const stdout = typeof record.stdout === "string" ? record.stdout : "";
710
714
  const stderr = typeof record.stderr === "string" ? record.stderr : "";
@@ -717,7 +721,7 @@ function buildBashDisplayOutput(record) {
717
721
  if (record.interrupted === true) {
718
722
  segments.push("Command was aborted before completion.");
719
723
  }
720
- const backgroundMessage = bashBackgroundMessage(record);
724
+ const backgroundMessage = shellBackgroundMessage(record);
721
725
  if (backgroundMessage) {
722
726
  segments.push(backgroundMessage);
723
727
  }
@@ -1777,10 +1781,12 @@ export function buildToolResultFields(isError, rawContent, base, rawResult, _con
1777
1781
  }
1778
1782
  return fields;
1779
1783
  }
1780
- const bashResultRecord = toolName === "Bash" ? findBashResultRecord(rawResult, rawContent) : undefined;
1784
+ const shellResultRecord = isShellToolName(toolName)
1785
+ ? findShellResultRecord(rawResult, rawContent)
1786
+ : undefined;
1781
1787
  const normalizedRawOutput = normalizeToolResultText(rawContent, isError);
1782
- const rawOutput = bashResultRecord
1783
- ? buildBashDisplayOutput(bashResultRecord)
1788
+ const rawOutput = shellResultRecord
1789
+ ? buildShellDisplayOutput(shellResultRecord)
1784
1790
  : normalizedRawOutput || JSON.stringify(rawContent);
1785
1791
  if (rawOutput && !(isTaskToolName(toolName) && !isError)) {
1786
1792
  fields.raw_output = rawOutput;
@@ -18,7 +18,7 @@ import { bridgeLogger, LOG_TARGETS, logBridgeCommandReceived } from "./bridge/lo
18
18
  export { AsyncQueue } from "./bridge/shared.js";
19
19
  export { asRecordOrNull } from "./bridge/shared.js";
20
20
  export { CACHE_SPLIT_POLICY, previewKilobyteLabel } from "./bridge/cache_policy.js";
21
- export { buildToolResultFields, createToolCall, normalizeToolKind, normalizeToolResultText, unwrapToolUseResult, } from "./bridge/tooling.js";
21
+ export { buildToolResultFields, createToolCall, isShellToolName, normalizeToolKind, normalizeToolResultText, unwrapToolUseResult, } from "./bridge/tooling.js";
22
22
  export { looksLikeAuthRequired } from "./bridge/auth.js";
23
23
  export { parseCommandEnvelope } from "./bridge/commands.js";
24
24
  export { buildSessionListOptions } from "./bridge/events.js";
@@ -1,6 +1,6 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { AsyncQueue, CACHE_SPLIT_POLICY, buildApiRetryUpdate, buildRateLimitUpdate, buildQueryOptions, canGenerateSessionTitle, generatePersistedSessionTitle, buildSessionMutationOptions, buildSessionListOptions, buildToolResultFields, createToolCall, applySessionAgent, applySessionEffort, emitAgentConfigOptionUpdate, emitEffortConfigOptionUpdate, handleTaskSystemMessage, handleSdkMessage, mapSdkAccountInfo, mapAvailableAgents, mapAvailableModels, mapSessionMessagesToUpdates, mapSdkSessions, agentSdkVersionCompatibilityError, looksLikeAuthRequired, normalizeToolResultText, parseFastModeState, parseRuntimeSessionState, parseRateLimitStatus, bridgeMcpConfigToSdk, mapMcpServerStatus, mapMcpServerStatusConfig, normalizeSettingsParseError, normalizeToolKind, parseCommandEnvelope, permissionOptionsFromSuggestions, permissionResultFromOutcome, previewKilobyteLabel, staleMcpAuthCandidates, resolveInstalledAgentSdkVersion, unwrapToolUseResult, updateAvailableCommands, handleReloadPluginsCommand, } from "./bridge.js";
3
+ import { AsyncQueue, CACHE_SPLIT_POLICY, buildApiRetryUpdate, buildRateLimitUpdate, buildQueryOptions, canGenerateSessionTitle, generatePersistedSessionTitle, buildSessionMutationOptions, buildSessionListOptions, buildToolResultFields, createToolCall, applySessionAgent, applySessionEffort, emitAgentConfigOptionUpdate, emitEffortConfigOptionUpdate, handleTaskSystemMessage, handleSdkMessage, isShellToolName, mapSdkAccountInfo, mapAvailableAgents, mapAvailableModels, mapSessionMessagesToUpdates, mapSdkSessions, agentSdkVersionCompatibilityError, looksLikeAuthRequired, normalizeToolResultText, parseFastModeState, parseRuntimeSessionState, parseRateLimitStatus, bridgeMcpConfigToSdk, mapMcpServerStatus, mapMcpServerStatusConfig, normalizeSettingsParseError, normalizeToolKind, parseCommandEnvelope, permissionOptionsFromSuggestions, permissionResultFromOutcome, previewKilobyteLabel, staleMcpAuthCandidates, resolveInstalledAgentSdkVersion, unwrapToolUseResult, updateAvailableCommands, handleReloadPluginsCommand, } from "./bridge.js";
4
4
  import { availableModesForSession, buildModeState, markModeUnavailableForSession, permissionModeFailureLooksUnsupported, refreshSupportedModesForSession, } from "./bridge/commands.js";
5
5
  import { handleMcpSetServersCommand } from "./bridge/mcp.js";
6
6
  import { emitCurrentModelUpdate, handleUserDialogResponse, refreshCurrentModel, resolveCurrentModel, sessions, shouldInvalidateResolvedRuntimeModel, shouldEmitStartupAuthRequiredForAccount, } from "./bridge/session_lifecycle.js";
@@ -2889,6 +2889,7 @@ test("requestAskUserQuestionAnswers preserves previews and annotations in update
2889
2889
  });
2890
2890
  test("normalizeToolKind maps known tool names", () => {
2891
2891
  assert.equal(normalizeToolKind("Bash"), "execute");
2892
+ assert.equal(normalizeToolKind("PowerShell"), "execute");
2892
2893
  assert.equal(normalizeToolKind("Delete"), "delete");
2893
2894
  assert.equal(normalizeToolKind("Move"), "move");
2894
2895
  assert.equal(normalizeToolKind("EnterWorktree"), "other");
@@ -2913,6 +2914,17 @@ test("normalizeToolKind maps known tool names", () => {
2913
2914
  assert.equal(normalizeToolKind("ExitPlanMode"), "switch_mode");
2914
2915
  assert.equal(normalizeToolKind("TodoWrite"), normalizeToolKind("FutureUnknownTool"));
2915
2916
  });
2917
+ test("isShellToolName recognizes only supported shell tools", () => {
2918
+ assert.equal(isShellToolName("Bash"), true);
2919
+ assert.equal(isShellToolName("PowerShell"), true);
2920
+ assert.equal(isShellToolName("Shell"), false);
2921
+ assert.equal(isShellToolName("bash"), false);
2922
+ });
2923
+ test("shell tool titles use input command", () => {
2924
+ assert.equal(createToolCall("tc-bash-title", "Bash", { command: "git status" }).title, "git status");
2925
+ assert.equal(createToolCall("tc-powershell-title", "PowerShell", { command: "Get-ChildItem" }).title, "Get-ChildItem");
2926
+ assert.equal(createToolCall("tc-powershell-empty", "PowerShell", {}).title, "Terminal");
2927
+ });
2916
2928
  test("parseFastModeState accepts known values and rejects unknown values", () => {
2917
2929
  assert.equal(parseFastModeState("off"), "off");
2918
2930
  assert.equal(parseFastModeState("cooldown"), "cooldown");
@@ -4048,6 +4060,22 @@ test("buildToolResultFields ignores model-facing Bash stale read hints", () => {
4048
4060
  assert.equal(fields.raw_output, "real stdout");
4049
4061
  assert.equal(fields.output_metadata, undefined);
4050
4062
  });
4063
+ test("buildToolResultFields maps PowerShell structured output like shell output", () => {
4064
+ const base = createToolCall("tc-powershell", "PowerShell", { command: "Get-ChildItem" });
4065
+ const fields = buildToolResultFields(false, {
4066
+ stdout: "stdout line",
4067
+ stderr: "stderr line",
4068
+ interrupted: true,
4069
+ }, base, {
4070
+ result: {
4071
+ stdout: "stdout line",
4072
+ stderr: "stderr line",
4073
+ interrupted: true,
4074
+ },
4075
+ });
4076
+ assert.equal(fields.raw_output, "stdout line\nstderr line\nCommand was aborted before completion.");
4077
+ assert.equal(fields.output_metadata, undefined);
4078
+ });
4051
4079
  test("buildToolResultFields adds Bash auto-backgrounded metadata and message", () => {
4052
4080
  const base = createToolCall("tc-bash-bg", "Bash", { command: "npm run watch" });
4053
4081
  const fields = buildToolResultFields(false, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-rust",
3
- "version": "0.12.2",
3
+ "version": "0.12.3",
4
4
  "description": "Claude Code Rust - native Rust terminal interface for Claude Code",
5
5
  "keywords": [
6
6
  "cli",
@@ -36,7 +36,12 @@
36
36
  },
37
37
  "scripts": {
38
38
  "postinstall": "node ./scripts/postinstall.js",
39
- "prepack": "npm --prefix agent-sdk run build"
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.9"
40
45
  },
41
46
  "engines": {
42
47
  "node": ">=18"
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+
4
+ const reportPath = process.argv[2] ?? "jscpd-report/jscpd-report.json";
5
+ const warningThreshold = Number.parseFloat(process.env.JSCPD_WARNING_THRESHOLD ?? "3.0");
6
+
7
+ function escapeWorkflowCommand(value) {
8
+ return String(value).replaceAll("%", "%25").replaceAll("\r", "%0D").replaceAll("\n", "%0A");
9
+ }
10
+
11
+ function markdownCell(value) {
12
+ return String(value).replaceAll("|", "\\|").replaceAll("\n", " ");
13
+ }
14
+
15
+ function asNumber(value, fallback = 0) {
16
+ const number = Number(value);
17
+ return Number.isFinite(number) ? number : fallback;
18
+ }
19
+
20
+ function formatPercent(value) {
21
+ return `${asNumber(value).toFixed(2)}%`;
22
+ }
23
+
24
+ function formatLocation(location) {
25
+ if (!location?.name) {
26
+ return "unknown";
27
+ }
28
+ const name = String(location.name).replaceAll("\\", "/");
29
+ const start = asNumber(location.start, asNumber(location.startLoc?.line, 1));
30
+ const end = asNumber(location.end, asNumber(location.endLoc?.line, start));
31
+ return `${name}:${start}-${end}`;
32
+ }
33
+
34
+ function appendSummary(markdown) {
35
+ const summaryPath = process.env.GITHUB_STEP_SUMMARY;
36
+ if (summaryPath) {
37
+ fs.appendFileSync(summaryPath, `${markdown}\n`);
38
+ }
39
+ }
40
+
41
+ function emitWarning(message) {
42
+ console.log(`::warning title=Duplicate code soft threshold::${escapeWorkflowCommand(message)}`);
43
+ }
44
+
45
+ if (!fs.existsSync(reportPath)) {
46
+ const message = `jscpd report not found at ${reportPath}; duplicate-code summary was skipped.`;
47
+ emitWarning(message);
48
+ appendSummary(`### Duplicate Code Scan\n\n${message}\n`);
49
+ process.exit(0);
50
+ }
51
+
52
+ const report = JSON.parse(fs.readFileSync(reportPath, "utf8"));
53
+ const total = report.statistics?.total ?? {};
54
+ const formats = report.statistics?.formats ?? {};
55
+ const duplicates = Array.isArray(report.duplicates) ? report.duplicates : [];
56
+
57
+ const percentage = asNumber(total.percentage);
58
+ const clones = asNumber(total.clones, duplicates.length);
59
+ const duplicatedLines = asNumber(total.duplicatedLines);
60
+ const totalLines = asNumber(total.lines);
61
+ const duplicatedTokens = asNumber(total.duplicatedTokens);
62
+ const totalTokens = asNumber(total.tokens);
63
+
64
+ const status =
65
+ percentage >= warningThreshold
66
+ ? `Warning: duplicated lines are ${formatPercent(percentage)}, above the ${formatPercent(warningThreshold)} soft threshold.`
67
+ : `OK: duplicated lines are ${formatPercent(percentage)}, below the ${formatPercent(warningThreshold)} soft threshold.`;
68
+
69
+ console.log(status);
70
+ console.log(
71
+ `jscpd found ${clones} clones across ${totalLines} lines and ${totalTokens} tokens ` +
72
+ `(${duplicatedLines} duplicated lines, ${duplicatedTokens} duplicated tokens).`,
73
+ );
74
+
75
+ if (percentage >= warningThreshold) {
76
+ emitWarning(
77
+ `jscpd found ${formatPercent(percentage)} duplicated lines (${clones} clones), ` +
78
+ `above the ${formatPercent(warningThreshold)} advisory threshold. ` +
79
+ "This workflow is warning-only and does not block the PR.",
80
+ );
81
+ }
82
+
83
+ const formatRows = Object.entries(formats)
84
+ .sort(([, left], [, right]) => asNumber(right.percentage) - asNumber(left.percentage))
85
+ .map(([format, stats]) =>
86
+ [
87
+ markdownCell(format),
88
+ asNumber(stats.sources),
89
+ asNumber(stats.clones),
90
+ asNumber(stats.duplicatedLines),
91
+ formatPercent(stats.percentage),
92
+ ].join(" | "),
93
+ );
94
+
95
+ const topDuplicates = [...duplicates]
96
+ .sort((left, right) => asNumber(right.lines) - asNumber(left.lines))
97
+ .slice(0, 10)
98
+ .map((duplicate) =>
99
+ [
100
+ asNumber(duplicate.lines),
101
+ asNumber(duplicate.tokens),
102
+ markdownCell(duplicate.format ?? "unknown"),
103
+ markdownCell(formatLocation(duplicate.firstFile)),
104
+ markdownCell(formatLocation(duplicate.secondFile)),
105
+ ].join(" | "),
106
+ );
107
+
108
+ const summary = [
109
+ "### Duplicate Code Scan",
110
+ "",
111
+ status,
112
+ "",
113
+ "| Metric | Value |",
114
+ "| --- | ---: |",
115
+ `| Clones | ${clones} |`,
116
+ `| Duplicated lines | ${duplicatedLines} / ${totalLines} (${formatPercent(percentage)}) |`,
117
+ `| Duplicated tokens | ${duplicatedTokens} / ${totalTokens} (${formatPercent(total.percentageTokens)}) |`,
118
+ `| Soft threshold | ${formatPercent(warningThreshold)} |`,
119
+ "",
120
+ "| Format | Files | Clones | Duplicated lines | Duplicated lines % |",
121
+ "| --- | ---: | ---: | ---: | ---: |",
122
+ ...formatRows,
123
+ "",
124
+ "| Lines | Tokens | Format | First location | Second location |",
125
+ "| ---: | ---: | --- | --- | --- |",
126
+ ...(topDuplicates.length > 0 ? topDuplicates : ["| 0 | 0 | none | n/a | n/a |"]),
127
+ "",
128
+ "The complete JSON and HTML reports are attached as the `jscpd-report` workflow artifact.",
129
+ "",
130
+ ].join("\n");
131
+
132
+ appendSummary(summary);