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
|
-
**
|
|
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
|
|
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
|
}
|
package/agent-sdk/dist/bridge.js
CHANGED
|
@@ -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");
|