claude-code-rust 0.6.0 → 0.7.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 +7 -4
- package/agent-sdk/README.md +0 -1
- package/agent-sdk/dist/bridge/agents.js +75 -0
- package/agent-sdk/dist/bridge/commands.js +42 -11
- package/agent-sdk/dist/bridge/error_classification.js +55 -0
- package/agent-sdk/dist/bridge/events.js +83 -0
- package/agent-sdk/dist/bridge/history.js +0 -17
- package/agent-sdk/dist/bridge/message_handlers.js +428 -0
- package/agent-sdk/dist/bridge/permissions.js +15 -3
- package/agent-sdk/dist/bridge/session_lifecycle.js +368 -0
- package/agent-sdk/dist/bridge/shared.js +49 -0
- package/agent-sdk/dist/bridge/state_parsing.js +66 -0
- package/agent-sdk/dist/bridge/tool_calls.js +168 -0
- package/agent-sdk/dist/bridge/user_interaction.js +175 -0
- package/agent-sdk/dist/bridge.js +21 -1323
- package/agent-sdk/dist/bridge.test.js +109 -51
- package/package.json +2 -2
- package/scripts/postinstall.js +2 -2
- package/agent-sdk/dist/bridge/usage.js +0 -95
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import { CACHE_SPLIT_POLICY, buildRateLimitUpdate,
|
|
3
|
+
import { AsyncQueue, CACHE_SPLIT_POLICY, buildRateLimitUpdate, buildQueryOptions, buildToolResultFields, createToolCall, mapAvailableAgents, mapSessionMessagesToUpdates, mapSdkSessions, agentSdkVersionCompatibilityError, looksLikeAuthRequired, normalizeToolResultText, parseFastModeState, parseRateLimitStatus, normalizeToolKind, parseCommandEnvelope, permissionOptionsFromSuggestions, permissionResultFromOutcome, previewKilobyteLabel, resolveInstalledAgentSdkVersion, unwrapToolUseResult, } from "./bridge.js";
|
|
4
4
|
test("parseCommandEnvelope validates initialize command", () => {
|
|
5
5
|
const parsed = parseCommandEnvelope(JSON.stringify({
|
|
6
6
|
request_id: "req-1",
|
|
@@ -19,6 +19,13 @@ test("parseCommandEnvelope validates resume_session command without cwd", () =>
|
|
|
19
19
|
request_id: "req-2",
|
|
20
20
|
command: "resume_session",
|
|
21
21
|
session_id: "session-123",
|
|
22
|
+
launch_settings: {
|
|
23
|
+
model: "haiku",
|
|
24
|
+
language: "German",
|
|
25
|
+
permission_mode: "plan",
|
|
26
|
+
thinking_mode: "adaptive",
|
|
27
|
+
effort_level: "high",
|
|
28
|
+
},
|
|
22
29
|
}));
|
|
23
30
|
assert.equal(parsed.requestId, "req-2");
|
|
24
31
|
assert.equal(parsed.command.command, "resume_session");
|
|
@@ -26,6 +33,97 @@ test("parseCommandEnvelope validates resume_session command without cwd", () =>
|
|
|
26
33
|
throw new Error("unexpected command variant");
|
|
27
34
|
}
|
|
28
35
|
assert.equal(parsed.command.session_id, "session-123");
|
|
36
|
+
assert.equal(parsed.command.launch_settings.model, "haiku");
|
|
37
|
+
assert.equal(parsed.command.launch_settings.language, "German");
|
|
38
|
+
assert.equal(parsed.command.launch_settings.permission_mode, "plan");
|
|
39
|
+
assert.equal(parsed.command.launch_settings.thinking_mode, "adaptive");
|
|
40
|
+
assert.equal(parsed.command.launch_settings.effort_level, "high");
|
|
41
|
+
});
|
|
42
|
+
test("buildQueryOptions maps launch settings into sdk query options", () => {
|
|
43
|
+
const input = new AsyncQueue();
|
|
44
|
+
const options = buildQueryOptions({
|
|
45
|
+
cwd: "C:/work",
|
|
46
|
+
launchSettings: {
|
|
47
|
+
model: "haiku",
|
|
48
|
+
language: "German",
|
|
49
|
+
permission_mode: "plan",
|
|
50
|
+
thinking_mode: "adaptive",
|
|
51
|
+
effort_level: "medium",
|
|
52
|
+
},
|
|
53
|
+
provisionalSessionId: "session-1",
|
|
54
|
+
input,
|
|
55
|
+
canUseTool: async () => ({ behavior: "deny", message: "not used" }),
|
|
56
|
+
enableSdkDebug: false,
|
|
57
|
+
enableSpawnDebug: false,
|
|
58
|
+
sessionIdForLogs: () => "session-1",
|
|
59
|
+
});
|
|
60
|
+
assert.equal(options.model, "haiku");
|
|
61
|
+
assert.deepEqual(options.systemPrompt, {
|
|
62
|
+
type: "preset",
|
|
63
|
+
preset: "claude_code",
|
|
64
|
+
append: "Always respond to the user in German unless the user explicitly asks for a different language. " +
|
|
65
|
+
"Keep code, shell commands, file paths, API names, tool names, and raw error text unchanged unless the user explicitly asks for translation.",
|
|
66
|
+
});
|
|
67
|
+
assert.equal(options.permissionMode, "plan");
|
|
68
|
+
assert.deepEqual(options.thinking, { type: "adaptive" });
|
|
69
|
+
assert.equal(options.effort, "medium");
|
|
70
|
+
assert.equal(options.sessionId, "session-1");
|
|
71
|
+
assert.deepEqual(options.settingSources, ["user", "project", "local"]);
|
|
72
|
+
});
|
|
73
|
+
test("buildQueryOptions maps disabled thinking mode into sdk query options", () => {
|
|
74
|
+
const input = new AsyncQueue();
|
|
75
|
+
const options = buildQueryOptions({
|
|
76
|
+
cwd: "C:/work",
|
|
77
|
+
launchSettings: {
|
|
78
|
+
thinking_mode: "disabled",
|
|
79
|
+
effort_level: "high",
|
|
80
|
+
},
|
|
81
|
+
provisionalSessionId: "session-3",
|
|
82
|
+
input,
|
|
83
|
+
canUseTool: async () => ({ behavior: "deny", message: "not used" }),
|
|
84
|
+
enableSdkDebug: false,
|
|
85
|
+
enableSpawnDebug: false,
|
|
86
|
+
sessionIdForLogs: () => "session-3",
|
|
87
|
+
});
|
|
88
|
+
assert.deepEqual(options.thinking, { type: "disabled" });
|
|
89
|
+
assert.equal("effort" in options, false);
|
|
90
|
+
});
|
|
91
|
+
test("buildQueryOptions omits startup overrides for default logout path", () => {
|
|
92
|
+
const input = new AsyncQueue();
|
|
93
|
+
const options = buildQueryOptions({
|
|
94
|
+
cwd: "C:/work",
|
|
95
|
+
launchSettings: {},
|
|
96
|
+
provisionalSessionId: "session-2",
|
|
97
|
+
input,
|
|
98
|
+
canUseTool: async () => ({ behavior: "deny", message: "not used" }),
|
|
99
|
+
enableSdkDebug: false,
|
|
100
|
+
enableSpawnDebug: false,
|
|
101
|
+
sessionIdForLogs: () => "session-2",
|
|
102
|
+
});
|
|
103
|
+
assert.equal("model" in options, false);
|
|
104
|
+
assert.equal("permissionMode" in options, false);
|
|
105
|
+
assert.equal("systemPrompt" in options, false);
|
|
106
|
+
});
|
|
107
|
+
test("buildQueryOptions trims language before appending system prompt", () => {
|
|
108
|
+
const input = new AsyncQueue();
|
|
109
|
+
const options = buildQueryOptions({
|
|
110
|
+
cwd: "C:/work",
|
|
111
|
+
launchSettings: {
|
|
112
|
+
language: " German ",
|
|
113
|
+
},
|
|
114
|
+
provisionalSessionId: "session-4",
|
|
115
|
+
input,
|
|
116
|
+
canUseTool: async () => ({ behavior: "deny", message: "not used" }),
|
|
117
|
+
enableSdkDebug: false,
|
|
118
|
+
enableSpawnDebug: false,
|
|
119
|
+
sessionIdForLogs: () => "session-4",
|
|
120
|
+
});
|
|
121
|
+
assert.deepEqual(options.systemPrompt, {
|
|
122
|
+
type: "preset",
|
|
123
|
+
preset: "claude_code",
|
|
124
|
+
append: "Always respond to the user in German unless the user explicitly asks for a different language. " +
|
|
125
|
+
"Keep code, shell commands, file paths, API names, tool names, and raw error text unchanged unless the user explicitly asks for translation.",
|
|
126
|
+
});
|
|
29
127
|
});
|
|
30
128
|
test("parseCommandEnvelope rejects missing required fields", () => {
|
|
31
129
|
assert.throws(() => parseCommandEnvelope(JSON.stringify({ command: "set_model", session_id: "s1" })), /set_model\.model must be a string/);
|
|
@@ -311,7 +409,7 @@ test("permissionResultFromOutcome keeps Bash allow_always suggestions unchanged"
|
|
|
311
409
|
{
|
|
312
410
|
type: "addRules",
|
|
313
411
|
behavior: "allow",
|
|
314
|
-
destination: "
|
|
412
|
+
destination: "localSettings",
|
|
315
413
|
rules: [
|
|
316
414
|
{ toolName: "Bash", ruleContent: "npm install" },
|
|
317
415
|
{ toolName: "WebFetch", ruleContent: "https://example.com" },
|
|
@@ -327,7 +425,7 @@ test("permissionResultFromOutcome keeps Bash allow_always suggestions unchanged"
|
|
|
327
425
|
{
|
|
328
426
|
type: "addRules",
|
|
329
427
|
behavior: "allow",
|
|
330
|
-
destination: "
|
|
428
|
+
destination: "localSettings",
|
|
331
429
|
rules: [
|
|
332
430
|
{ toolName: "Bash", ruleContent: "npm install" },
|
|
333
431
|
{ toolName: "WebFetch", ruleContent: "https://example.com" },
|
|
@@ -367,7 +465,7 @@ test("permissionResultFromOutcome falls back to session tool rule for allow_sess
|
|
|
367
465
|
},
|
|
368
466
|
]);
|
|
369
467
|
});
|
|
370
|
-
test("permissionResultFromOutcome
|
|
468
|
+
test("permissionResultFromOutcome falls back to localSettings rule for allow_always when only session suggestions exist", () => {
|
|
371
469
|
const allow = permissionResultFromOutcome({ outcome: "selected", option_id: "allow_always" }, "tool-4", { file_path: "C:\\work\\baz.txt" }, [
|
|
372
470
|
{
|
|
373
471
|
type: "addRules",
|
|
@@ -380,45 +478,14 @@ test("permissionResultFromOutcome does not apply session suggestions to allow_al
|
|
|
380
478
|
if (allow.behavior !== "allow") {
|
|
381
479
|
throw new Error("expected allow permission result");
|
|
382
480
|
}
|
|
383
|
-
assert.
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
outputTokens: 34,
|
|
390
|
-
cacheReadInputTokens: 5,
|
|
391
|
-
cacheCreationInputTokens: 6,
|
|
392
|
-
},
|
|
393
|
-
});
|
|
394
|
-
assert.deepEqual(update, {
|
|
395
|
-
type: "usage_update",
|
|
396
|
-
usage: {
|
|
397
|
-
input_tokens: 12,
|
|
398
|
-
output_tokens: 34,
|
|
399
|
-
cache_read_tokens: 5,
|
|
400
|
-
cache_write_tokens: 6,
|
|
401
|
-
},
|
|
402
|
-
});
|
|
403
|
-
});
|
|
404
|
-
test("buildUsageUpdateFromResult includes cost and context window fields", () => {
|
|
405
|
-
const update = buildUsageUpdateFromResult({
|
|
406
|
-
total_cost_usd: 1.25,
|
|
407
|
-
modelUsage: {
|
|
408
|
-
"claude-sonnet-4-5": {
|
|
409
|
-
contextWindow: 200000,
|
|
410
|
-
maxOutputTokens: 64000,
|
|
411
|
-
},
|
|
412
|
-
},
|
|
413
|
-
});
|
|
414
|
-
assert.deepEqual(update, {
|
|
415
|
-
type: "usage_update",
|
|
416
|
-
usage: {
|
|
417
|
-
total_cost_usd: 1.25,
|
|
418
|
-
context_window: 200000,
|
|
419
|
-
max_output_tokens: 64000,
|
|
481
|
+
assert.deepEqual(allow.updatedPermissions, [
|
|
482
|
+
{
|
|
483
|
+
type: "addRules",
|
|
484
|
+
rules: [{ toolName: "Write" }],
|
|
485
|
+
behavior: "allow",
|
|
486
|
+
destination: "localSettings",
|
|
420
487
|
},
|
|
421
|
-
|
|
488
|
+
]);
|
|
422
489
|
});
|
|
423
490
|
test("looksLikeAuthRequired detects login hints", () => {
|
|
424
491
|
assert.equal(looksLikeAuthRequired("Please run /login to continue"), true);
|
|
@@ -486,15 +553,6 @@ test("mapSessionMessagesToUpdates maps message content blocks", () => {
|
|
|
486
553
|
assert.equal(variantCounts.get("agent_message_chunk"), 1);
|
|
487
554
|
assert.equal(variantCounts.get("tool_call"), 1);
|
|
488
555
|
assert.equal(variantCounts.get("tool_call_update"), 1);
|
|
489
|
-
assert.equal(variantCounts.get("usage_update"), 1);
|
|
490
|
-
const usage = updates.find((update) => update.type === "usage_update");
|
|
491
|
-
assert.ok(usage && usage.type === "usage_update");
|
|
492
|
-
assert.deepEqual(usage.usage, {
|
|
493
|
-
input_tokens: 11,
|
|
494
|
-
output_tokens: 7,
|
|
495
|
-
cache_read_tokens: 5,
|
|
496
|
-
cache_write_tokens: 3,
|
|
497
|
-
});
|
|
498
556
|
});
|
|
499
557
|
test("mapSessionMessagesToUpdates ignores unsupported records", () => {
|
|
500
558
|
const updates = mapSessionMessagesToUpdates([
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-rust",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Claude Code Rust - native Rust terminal interface for Claude Code",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"postinstall": "node ./scripts/postinstall.js",
|
|
36
|
-
"prepack": "
|
|
36
|
+
"prepack": "npm --prefix agent-sdk run build"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|
|
39
39
|
"node": ">=18"
|
package/scripts/postinstall.js
CHANGED
|
@@ -27,7 +27,7 @@ async function downloadFile(url, outPath, redirects = 0) {
|
|
|
27
27
|
await new Promise((resolve, reject) => {
|
|
28
28
|
const req = https.get(
|
|
29
29
|
url,
|
|
30
|
-
{ headers: { "User-Agent": "claude-code-rust-
|
|
30
|
+
{ headers: { "User-Agent": "claude-code-rust-installer" } },
|
|
31
31
|
(res) => {
|
|
32
32
|
const status = res.statusCode ?? 0;
|
|
33
33
|
|
|
@@ -60,7 +60,7 @@ async function main() {
|
|
|
60
60
|
const info = getTargetInfo();
|
|
61
61
|
if (!info) {
|
|
62
62
|
const key = `${process.platform}:${process.arch}`;
|
|
63
|
-
throw new Error(`Unsupported platform/arch for claude-code-rust
|
|
63
|
+
throw new Error(`Unsupported platform/arch for claude-code-rust package install: ${key}`);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
const pkgJsonPath = path.join(__dirname, "..", "package.json");
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { asRecordOrNull } from "./shared.js";
|
|
2
|
-
function numberField(record, ...keys) {
|
|
3
|
-
for (const key of keys) {
|
|
4
|
-
const value = record[key];
|
|
5
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
6
|
-
return value;
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
return undefined;
|
|
10
|
-
}
|
|
11
|
-
function selectModelUsageRecord(session, message) {
|
|
12
|
-
const modelUsageRaw = asRecordOrNull(message.modelUsage);
|
|
13
|
-
if (!modelUsageRaw) {
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
const sortedKeys = Object.keys(modelUsageRaw).sort();
|
|
17
|
-
if (sortedKeys.length === 0) {
|
|
18
|
-
return null;
|
|
19
|
-
}
|
|
20
|
-
const preferredKeys = new Set();
|
|
21
|
-
if (session?.model) {
|
|
22
|
-
preferredKeys.add(session.model);
|
|
23
|
-
}
|
|
24
|
-
if (typeof message.model === "string") {
|
|
25
|
-
preferredKeys.add(message.model);
|
|
26
|
-
}
|
|
27
|
-
for (const key of preferredKeys) {
|
|
28
|
-
const value = asRecordOrNull(modelUsageRaw[key]);
|
|
29
|
-
if (value) {
|
|
30
|
-
return value;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
for (const key of sortedKeys) {
|
|
34
|
-
const value = asRecordOrNull(modelUsageRaw[key]);
|
|
35
|
-
if (value) {
|
|
36
|
-
return value;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
export function buildUsageUpdateFromResultForSession(session, message) {
|
|
42
|
-
const usage = asRecordOrNull(message.usage);
|
|
43
|
-
const inputTokens = usage ? numberField(usage, "inputTokens", "input_tokens") : undefined;
|
|
44
|
-
const outputTokens = usage ? numberField(usage, "outputTokens", "output_tokens") : undefined;
|
|
45
|
-
const cacheReadTokens = usage
|
|
46
|
-
? numberField(usage, "cacheReadInputTokens", "cache_read_input_tokens", "cache_read_tokens")
|
|
47
|
-
: undefined;
|
|
48
|
-
const cacheWriteTokens = usage
|
|
49
|
-
? numberField(usage, "cacheCreationInputTokens", "cache_creation_input_tokens", "cache_write_tokens")
|
|
50
|
-
: undefined;
|
|
51
|
-
const totalCostUsd = numberField(message, "total_cost_usd", "totalCostUsd");
|
|
52
|
-
let turnCostUsd;
|
|
53
|
-
if (totalCostUsd !== undefined && session) {
|
|
54
|
-
if (session.lastTotalCostUsd === undefined) {
|
|
55
|
-
turnCostUsd = totalCostUsd;
|
|
56
|
-
}
|
|
57
|
-
else {
|
|
58
|
-
turnCostUsd = Math.max(0, totalCostUsd - session.lastTotalCostUsd);
|
|
59
|
-
}
|
|
60
|
-
session.lastTotalCostUsd = totalCostUsd;
|
|
61
|
-
}
|
|
62
|
-
const modelUsage = selectModelUsageRecord(session, message);
|
|
63
|
-
const contextWindow = modelUsage
|
|
64
|
-
? numberField(modelUsage, "contextWindow", "context_window")
|
|
65
|
-
: undefined;
|
|
66
|
-
const maxOutputTokens = modelUsage
|
|
67
|
-
? numberField(modelUsage, "maxOutputTokens", "max_output_tokens")
|
|
68
|
-
: undefined;
|
|
69
|
-
if (inputTokens === undefined &&
|
|
70
|
-
outputTokens === undefined &&
|
|
71
|
-
cacheReadTokens === undefined &&
|
|
72
|
-
cacheWriteTokens === undefined &&
|
|
73
|
-
totalCostUsd === undefined &&
|
|
74
|
-
turnCostUsd === undefined &&
|
|
75
|
-
contextWindow === undefined &&
|
|
76
|
-
maxOutputTokens === undefined) {
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
return {
|
|
80
|
-
type: "usage_update",
|
|
81
|
-
usage: {
|
|
82
|
-
...(inputTokens !== undefined ? { input_tokens: inputTokens } : {}),
|
|
83
|
-
...(outputTokens !== undefined ? { output_tokens: outputTokens } : {}),
|
|
84
|
-
...(cacheReadTokens !== undefined ? { cache_read_tokens: cacheReadTokens } : {}),
|
|
85
|
-
...(cacheWriteTokens !== undefined ? { cache_write_tokens: cacheWriteTokens } : {}),
|
|
86
|
-
...(totalCostUsd !== undefined ? { total_cost_usd: totalCostUsd } : {}),
|
|
87
|
-
...(turnCostUsd !== undefined ? { turn_cost_usd: turnCostUsd } : {}),
|
|
88
|
-
...(contextWindow !== undefined ? { context_window: contextWindow } : {}),
|
|
89
|
-
...(maxOutputTokens !== undefined ? { max_output_tokens: maxOutputTokens } : {}),
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
export function buildUsageUpdateFromResult(message) {
|
|
94
|
-
return buildUsageUpdateFromResultForSession(undefined, message);
|
|
95
|
-
}
|