claude-code-rust 0.4.1 → 0.5.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
@@ -50,7 +50,7 @@ Three-layer design:
50
50
 
51
51
  **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.
52
52
 
53
- **Protocol 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.
53
+ **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.
54
54
 
55
55
  **Agent Runtime** (Anthropic Agent SDK) - The TypeScript bridge drives `@anthropic-ai/claude-agent-sdk`, which manages authentication, session/query lifecycle, and tool execution.
56
56
 
@@ -67,7 +67,7 @@ This project is pre-1.0 and under active development. See [CONTRIBUTING.md](CONT
67
67
  ## License
68
68
 
69
69
  This project is licensed under the [GNU Affero General Public License v3.0 or later](LICENSE).
70
- This license was chosen because Claude Code is not open-source and this license allows everyone to use it while stopping Antrophic from implementing it in their clsosed-source version.
70
+ This license was chosen because Claude Code is not open-source and this license allows everyone to use it while stopping Anthropic from implementing it in their closed-source version.
71
71
 
72
72
  By using this software, you agree to the terms of the AGPL-3.0. If you modify this software and make it available over a network, you must offer the source code to users of that service.
73
73
 
@@ -0,0 +1,9 @@
1
+ export const CACHE_SPLIT_POLICY = Object.freeze({
2
+ softLimitBytes: 1536,
3
+ hardLimitBytes: 4096,
4
+ previewLimitBytes: 2048,
5
+ });
6
+ export function previewKilobyteLabel(policy = CACHE_SPLIT_POLICY) {
7
+ const kb = policy.previewLimitBytes / 1024;
8
+ return Number.isInteger(kb) ? `${kb}KB` : `${kb.toFixed(1)}KB`;
9
+ }
@@ -1,4 +1,5 @@
1
1
  import { asRecordOrNull } from "./shared.js";
2
+ import { CACHE_SPLIT_POLICY, previewKilobyteLabel } from "./cache_policy.js";
2
3
  export const TOOL_RESULT_TYPES = new Set([
3
4
  "tool_result",
4
5
  "tool_search_tool_result",
@@ -138,6 +139,7 @@ export function extractText(value) {
138
139
  }
139
140
  const PERSISTED_OUTPUT_OPEN_TAG = "<persisted-output>";
140
141
  const PERSISTED_OUTPUT_CLOSE_TAG = "</persisted-output>";
142
+ const EXPECTED_PREVIEW_LINE = `preview (first ${previewKilobyteLabel(CACHE_SPLIT_POLICY).toLowerCase()}):`;
141
143
  function extractPersistedOutputInnerText(text) {
142
144
  const lower = text.toLowerCase();
143
145
  const openIdx = lower.indexOf(PERSISTED_OUTPUT_OPEN_TAG);
@@ -159,6 +161,9 @@ function persistedOutputFirstLine(text) {
159
161
  for (const line of inner.split(/\r?\n/)) {
160
162
  const cleaned = line.replace(/^[\s|│┃║]+/u, "").trim();
161
163
  if (cleaned.length > 0) {
164
+ if (cleaned.toLowerCase() === EXPECTED_PREVIEW_LINE) {
165
+ continue;
166
+ }
162
167
  return cleaned;
163
168
  }
164
169
  }
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { spawn as spawnChild } from "node:child_process";
3
3
  import fs from "node:fs";
4
+ import { createRequire } from "node:module";
4
5
  import readline from "node:readline";
5
6
  import { pathToFileURL } from "node:url";
6
7
  import { query, } from "@anthropic-ai/claude-agent-sdk";
@@ -8,12 +9,36 @@ import { parseCommandEnvelope, toPermissionMode, buildModeState } from "./bridge
8
9
  import { asRecordOrNull } from "./bridge/shared.js";
9
10
  import { looksLikeAuthRequired } from "./bridge/auth.js";
10
11
  import { TOOL_RESULT_TYPES, buildToolResultFields, createToolCall, normalizeToolKind, normalizeToolResultText, unwrapToolUseResult, } from "./bridge/tooling.js";
12
+ import { CACHE_SPLIT_POLICY, previewKilobyteLabel } from "./bridge/cache_policy.js";
11
13
  import { buildUsageUpdateFromResult, buildUsageUpdateFromResultForSession } from "./bridge/usage.js";
12
14
  import { formatPermissionUpdates, permissionOptionsFromSuggestions, permissionResultFromOutcome, } from "./bridge/permissions.js";
13
15
  import { extractSessionHistoryUpdatesFromJsonl, listRecentPersistedSessions, resolvePersistedSessionEntry, } from "./bridge/history.js";
14
- export { buildToolResultFields, buildUsageUpdateFromResult, createToolCall, extractSessionHistoryUpdatesFromJsonl, looksLikeAuthRequired, normalizeToolKind, normalizeToolResultText, parseCommandEnvelope, permissionOptionsFromSuggestions, permissionResultFromOutcome, unwrapToolUseResult, };
16
+ export { CACHE_SPLIT_POLICY, buildToolResultFields, buildUsageUpdateFromResult, createToolCall, extractSessionHistoryUpdatesFromJsonl, looksLikeAuthRequired, normalizeToolKind, normalizeToolResultText, parseCommandEnvelope, permissionOptionsFromSuggestions, permissionResultFromOutcome, previewKilobyteLabel, unwrapToolUseResult, };
15
17
  const sessions = new Map();
18
+ const EXPECTED_AGENT_SDK_VERSION = "0.2.52";
19
+ const require = createRequire(import.meta.url);
16
20
  const permissionDebugEnabled = process.env.CLAUDE_RS_SDK_PERMISSION_DEBUG === "1" || process.env.CLAUDE_RS_SDK_DEBUG === "1";
21
+ export function resolveInstalledAgentSdkVersion() {
22
+ try {
23
+ const pkg = require("@anthropic-ai/claude-agent-sdk/package.json");
24
+ return typeof pkg.version === "string" ? pkg.version : undefined;
25
+ }
26
+ catch {
27
+ return undefined;
28
+ }
29
+ }
30
+ export function agentSdkVersionCompatibilityError() {
31
+ const installed = resolveInstalledAgentSdkVersion();
32
+ if (!installed) {
33
+ return (`Agent SDK version check failed: unable to resolve installed ` +
34
+ `@anthropic-ai/claude-agent-sdk package.json (expected ${EXPECTED_AGENT_SDK_VERSION}).`);
35
+ }
36
+ if (installed === EXPECTED_AGENT_SDK_VERSION) {
37
+ return undefined;
38
+ }
39
+ return (`Unsupported @anthropic-ai/claude-agent-sdk version: expected ${EXPECTED_AGENT_SDK_VERSION}, ` +
40
+ `found ${installed}.`);
41
+ }
17
42
  function logPermissionDebug(message) {
18
43
  if (!permissionDebugEnabled) {
19
44
  return;
@@ -447,6 +472,10 @@ function handleStreamEvent(session, event) {
447
472
  }
448
473
  }
449
474
  function handleAssistantMessage(session, message) {
475
+ const assistantError = typeof message.error === "string" ? message.error : "";
476
+ if (assistantError.length > 0) {
477
+ session.lastAssistantError = assistantError;
478
+ }
450
479
  const messageObject = message.message && typeof message.message === "object"
451
480
  ? message.message
452
481
  : null;
@@ -500,6 +529,36 @@ function emitAuthRequired(session, detail) {
500
529
  : "Run `claude /login` in a terminal, then retry.",
501
530
  });
502
531
  }
532
+ function looksLikePlanLimitError(input) {
533
+ const normalized = input.toLowerCase();
534
+ return (normalized.includes("rate limit") ||
535
+ normalized.includes("rate-limit") ||
536
+ normalized.includes("max turns") ||
537
+ normalized.includes("max budget") ||
538
+ normalized.includes("quota") ||
539
+ normalized.includes("plan limit") ||
540
+ normalized.includes("too many requests") ||
541
+ normalized.includes("insufficient quota") ||
542
+ normalized.includes("429"));
543
+ }
544
+ function classifyTurnErrorKind(subtype, errors, assistantError) {
545
+ const combined = errors.join("\n");
546
+ if (subtype === "error_max_turns" ||
547
+ subtype === "error_max_budget_usd" ||
548
+ assistantError === "billing_error" ||
549
+ assistantError === "rate_limit" ||
550
+ (combined.length > 0 && looksLikePlanLimitError(combined))) {
551
+ return "plan_limit";
552
+ }
553
+ if (assistantError === "authentication_failed" ||
554
+ errors.some((entry) => looksLikeAuthRequired(entry))) {
555
+ return "auth_required";
556
+ }
557
+ if (assistantError === "server_error") {
558
+ return "internal";
559
+ }
560
+ return "other";
561
+ }
503
562
  function numberField(record, ...keys) {
504
563
  for (const key of keys) {
505
564
  const value = record[key];
@@ -516,6 +575,7 @@ function handleResultMessage(session, message) {
516
575
  }
517
576
  const subtype = typeof message.subtype === "string" ? message.subtype : "";
518
577
  if (subtype === "success") {
578
+ session.lastAssistantError = undefined;
519
579
  finalizeOpenToolCalls(session, "completed");
520
580
  writeEvent({ event: "turn_complete", session_id: session.sessionId });
521
581
  return;
@@ -523,17 +583,26 @@ function handleResultMessage(session, message) {
523
583
  const errors = Array.isArray(message.errors) && message.errors.every((entry) => typeof entry === "string")
524
584
  ? message.errors
525
585
  : [];
586
+ const assistantError = session.lastAssistantError;
526
587
  const authHint = errors.find((entry) => looksLikeAuthRequired(entry));
527
588
  if (authHint) {
528
589
  emitAuthRequired(session, authHint);
529
590
  }
591
+ if (assistantError === "authentication_failed") {
592
+ emitAuthRequired(session);
593
+ }
530
594
  finalizeOpenToolCalls(session, "failed");
595
+ const errorKind = classifyTurnErrorKind(subtype, errors, assistantError);
531
596
  const fallback = subtype ? `turn failed: ${subtype}` : "turn failed";
532
597
  writeEvent({
533
598
  event: "turn_error",
534
599
  session_id: session.sessionId,
535
600
  message: errors.length > 0 ? errors.join("\n") : fallback,
601
+ error_kind: errorKind,
602
+ ...(subtype ? { sdk_result_subtype: subtype } : {}),
603
+ ...(assistantError ? { assistant_error: assistantError } : {}),
536
604
  });
605
+ session.lastAssistantError = undefined;
537
606
  }
538
607
  function handleSdkMessage(session, message) {
539
608
  const msg = message;
@@ -1034,8 +1103,17 @@ function handlePermissionResponse(command) {
1034
1103
  resolver.resolve(permissionResult);
1035
1104
  }
1036
1105
  async function handleCommand(command, requestId) {
1106
+ const sdkVersionError = agentSdkVersionCompatibilityError();
1107
+ if (sdkVersionError && command.command !== "initialize" && command.command !== "shutdown") {
1108
+ failConnection(sdkVersionError, requestId);
1109
+ return;
1110
+ }
1037
1111
  switch (command.command) {
1038
1112
  case "initialize":
1113
+ if (sdkVersionError) {
1114
+ failConnection(sdkVersionError, requestId);
1115
+ return;
1116
+ }
1039
1117
  writeEvent({
1040
1118
  event: "initialized",
1041
1119
  result: {
@@ -3,7 +3,7 @@ import assert from "node:assert/strict";
3
3
  import fs from "node:fs";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
- import { buildToolResultFields, buildUsageUpdateFromResult, createToolCall, extractSessionHistoryUpdatesFromJsonl, looksLikeAuthRequired, normalizeToolResultText, normalizeToolKind, parseCommandEnvelope, permissionOptionsFromSuggestions, permissionResultFromOutcome, unwrapToolUseResult, } from "./bridge.js";
6
+ import { CACHE_SPLIT_POLICY, buildToolResultFields, buildUsageUpdateFromResult, createToolCall, extractSessionHistoryUpdatesFromJsonl, agentSdkVersionCompatibilityError, looksLikeAuthRequired, normalizeToolResultText, normalizeToolKind, parseCommandEnvelope, permissionOptionsFromSuggestions, permissionResultFromOutcome, previewKilobyteLabel, resolveInstalledAgentSdkVersion, unwrapToolUseResult, } from "./bridge.js";
7
7
  test("parseCommandEnvelope validates initialize command", () => {
8
8
  const parsed = parseCommandEnvelope(JSON.stringify({
9
9
  request_id: "req-1",
@@ -100,6 +100,12 @@ test("normalizeToolResultText collapses persisted-output payload to first meanin
100
100
  `);
101
101
  assert.equal(normalized, "Output too large (132.5KB). Full output saved to: C:\\tmp\\tool-results\\bbf63b9.txt");
102
102
  });
103
+ test("cache split policy defaults stay aligned with UI thresholds", () => {
104
+ assert.equal(CACHE_SPLIT_POLICY.softLimitBytes, 1536);
105
+ assert.equal(CACHE_SPLIT_POLICY.hardLimitBytes, 4096);
106
+ assert.equal(CACHE_SPLIT_POLICY.previewLimitBytes, 2048);
107
+ assert.equal(previewKilobyteLabel(CACHE_SPLIT_POLICY), "2KB");
108
+ });
103
109
  test("buildToolResultFields uses normalized persisted-output text", () => {
104
110
  const fields = buildToolResultFields(false, `<persisted-output>
105
111
  │ Output too large (14KB). Full output saved to: C:\\tmp\\tool-results\\x.txt
@@ -327,6 +333,10 @@ test("looksLikeAuthRequired detects login hints", () => {
327
333
  assert.equal(looksLikeAuthRequired("Please run /login to continue"), true);
328
334
  assert.equal(looksLikeAuthRequired("normal tool output"), false);
329
335
  });
336
+ test("agent sdk version compatibility check matches pinned version", () => {
337
+ assert.equal(resolveInstalledAgentSdkVersion(), "0.2.52");
338
+ assert.equal(agentSdkVersionCompatibilityError(), undefined);
339
+ });
330
340
  function withTempJsonl(lines, run) {
331
341
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "claude-rs-resume-test-"));
332
342
  const filePath = path.join(dir, "session.jsonl");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-rust",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "description": "Claude Code Rust - native Rust terminal interface for Claude Code",
5
5
  "keywords": [
6
6
  "cli",