claude-code-rust 0.5.1 → 0.7.0
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 +8 -8
- package/agent-sdk/dist/bridge/agents.js +75 -0
- package/agent-sdk/dist/bridge/commands.js +45 -14
- 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 +49 -258
- 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/tooling.js +27 -5
- package/agent-sdk/dist/bridge/user_interaction.js +175 -0
- package/agent-sdk/dist/bridge.js +37 -1106
- package/agent-sdk/dist/bridge.test.js +304 -150
- package/bin/claude-rs.js +1 -1
- package/package.json +6 -7
- package/scripts/postinstall.js +2 -2
- package/agent-sdk/README.md +0 -13
- package/agent-sdk/dist/bridge/usage.js +0 -95
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import test from "node:test";
|
|
2
2
|
import assert from "node:assert/strict";
|
|
3
|
-
import
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { CACHE_SPLIT_POLICY, buildToolResultFields, buildUsageUpdateFromResult, createToolCall, extractSessionHistoryUpdatesFromJsonl, agentSdkVersionCompatibilityError, looksLikeAuthRequired, normalizeToolResultText, normalizeToolKind, parseCommandEnvelope, permissionOptionsFromSuggestions, permissionResultFromOutcome, previewKilobyteLabel, resolveInstalledAgentSdkVersion, unwrapToolUseResult, } from "./bridge.js";
|
|
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";
|
|
7
4
|
test("parseCommandEnvelope validates initialize command", () => {
|
|
8
5
|
const parsed = parseCommandEnvelope(JSON.stringify({
|
|
9
6
|
request_id: "req-1",
|
|
@@ -17,18 +14,116 @@ test("parseCommandEnvelope validates initialize command", () => {
|
|
|
17
14
|
}
|
|
18
15
|
assert.equal(parsed.command.cwd, "C:/work");
|
|
19
16
|
});
|
|
20
|
-
test("parseCommandEnvelope validates
|
|
17
|
+
test("parseCommandEnvelope validates resume_session command without cwd", () => {
|
|
21
18
|
const parsed = parseCommandEnvelope(JSON.stringify({
|
|
22
19
|
request_id: "req-2",
|
|
23
|
-
command: "
|
|
20
|
+
command: "resume_session",
|
|
24
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
|
+
},
|
|
25
29
|
}));
|
|
26
30
|
assert.equal(parsed.requestId, "req-2");
|
|
27
|
-
assert.equal(parsed.command.command, "
|
|
28
|
-
if (parsed.command.command !== "
|
|
31
|
+
assert.equal(parsed.command.command, "resume_session");
|
|
32
|
+
if (parsed.command.command !== "resume_session") {
|
|
29
33
|
throw new Error("unexpected command variant");
|
|
30
34
|
}
|
|
31
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
|
+
});
|
|
32
127
|
});
|
|
33
128
|
test("parseCommandEnvelope rejects missing required fields", () => {
|
|
34
129
|
assert.throws(() => parseCommandEnvelope(JSON.stringify({ command: "set_model", session_id: "s1" })), /set_model\.model must be a string/);
|
|
@@ -37,9 +132,76 @@ test("normalizeToolKind maps known tool names", () => {
|
|
|
37
132
|
assert.equal(normalizeToolKind("Bash"), "execute");
|
|
38
133
|
assert.equal(normalizeToolKind("Delete"), "delete");
|
|
39
134
|
assert.equal(normalizeToolKind("Move"), "move");
|
|
135
|
+
assert.equal(normalizeToolKind("Task"), "think");
|
|
136
|
+
assert.equal(normalizeToolKind("Agent"), "think");
|
|
40
137
|
assert.equal(normalizeToolKind("ExitPlanMode"), "switch_mode");
|
|
41
138
|
assert.equal(normalizeToolKind("TodoWrite"), "other");
|
|
42
139
|
});
|
|
140
|
+
test("parseFastModeState accepts known values and rejects unknown values", () => {
|
|
141
|
+
assert.equal(parseFastModeState("off"), "off");
|
|
142
|
+
assert.equal(parseFastModeState("cooldown"), "cooldown");
|
|
143
|
+
assert.equal(parseFastModeState("on"), "on");
|
|
144
|
+
assert.equal(parseFastModeState("CD"), null);
|
|
145
|
+
assert.equal(parseFastModeState(undefined), null);
|
|
146
|
+
});
|
|
147
|
+
test("parseRateLimitStatus accepts known values and rejects unknown values", () => {
|
|
148
|
+
assert.equal(parseRateLimitStatus("allowed"), "allowed");
|
|
149
|
+
assert.equal(parseRateLimitStatus("allowed_warning"), "allowed_warning");
|
|
150
|
+
assert.equal(parseRateLimitStatus("rejected"), "rejected");
|
|
151
|
+
assert.equal(parseRateLimitStatus("warn"), null);
|
|
152
|
+
assert.equal(parseRateLimitStatus(undefined), null);
|
|
153
|
+
});
|
|
154
|
+
test("buildRateLimitUpdate maps SDK fields to wire shape", () => {
|
|
155
|
+
const update = buildRateLimitUpdate({
|
|
156
|
+
status: "allowed_warning",
|
|
157
|
+
resetsAt: 1_741_280_000,
|
|
158
|
+
utilization: 0.92,
|
|
159
|
+
rateLimitType: "five_hour",
|
|
160
|
+
overageStatus: "rejected",
|
|
161
|
+
overageResetsAt: 1_741_280_600,
|
|
162
|
+
overageDisabledReason: "out_of_credits",
|
|
163
|
+
isUsingOverage: false,
|
|
164
|
+
surpassedThreshold: 0.9,
|
|
165
|
+
});
|
|
166
|
+
assert.deepEqual(update, {
|
|
167
|
+
type: "rate_limit_update",
|
|
168
|
+
status: "allowed_warning",
|
|
169
|
+
resets_at: 1_741_280_000,
|
|
170
|
+
utilization: 0.92,
|
|
171
|
+
rate_limit_type: "five_hour",
|
|
172
|
+
overage_status: "rejected",
|
|
173
|
+
overage_resets_at: 1_741_280_600,
|
|
174
|
+
overage_disabled_reason: "out_of_credits",
|
|
175
|
+
is_using_overage: false,
|
|
176
|
+
surpassed_threshold: 0.9,
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
test("buildRateLimitUpdate rejects invalid payloads", () => {
|
|
180
|
+
assert.equal(buildRateLimitUpdate(null), null);
|
|
181
|
+
assert.equal(buildRateLimitUpdate({}), null);
|
|
182
|
+
assert.equal(buildRateLimitUpdate({ status: "warning" }), null);
|
|
183
|
+
assert.deepEqual(buildRateLimitUpdate({
|
|
184
|
+
status: "rejected",
|
|
185
|
+
overageStatus: "bad_status",
|
|
186
|
+
}), { type: "rate_limit_update", status: "rejected" });
|
|
187
|
+
});
|
|
188
|
+
test("mapAvailableAgents normalizes and deduplicates agents", () => {
|
|
189
|
+
const agents = mapAvailableAgents([
|
|
190
|
+
{ name: "reviewer", description: "", model: "" },
|
|
191
|
+
{ name: "reviewer", description: "Reviews code", model: "haiku" },
|
|
192
|
+
{ name: "explore", description: "Explore codebase", model: "sonnet" },
|
|
193
|
+
{ name: " ", description: "ignored" },
|
|
194
|
+
{},
|
|
195
|
+
]);
|
|
196
|
+
assert.deepEqual(agents, [
|
|
197
|
+
{ name: "explore", description: "Explore codebase", model: "sonnet" },
|
|
198
|
+
{ name: "reviewer", description: "Reviews code", model: "haiku" },
|
|
199
|
+
]);
|
|
200
|
+
});
|
|
201
|
+
test("mapAvailableAgents rejects non-array payload", () => {
|
|
202
|
+
assert.deepEqual(mapAvailableAgents(null), []);
|
|
203
|
+
assert.deepEqual(mapAvailableAgents({}), []);
|
|
204
|
+
});
|
|
43
205
|
test("createToolCall builds edit diff content", () => {
|
|
44
206
|
const toolCall = createToolCall("tc-1", "Edit", {
|
|
45
207
|
file_path: "src/main.rs",
|
|
@@ -100,6 +262,26 @@ test("normalizeToolResultText collapses persisted-output payload to first meanin
|
|
|
100
262
|
`);
|
|
101
263
|
assert.equal(normalized, "Output too large (132.5KB). Full output saved to: C:\\tmp\\tool-results\\bbf63b9.txt");
|
|
102
264
|
});
|
|
265
|
+
test("normalizeToolResultText does not sanitize non-error output", () => {
|
|
266
|
+
const text = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.";
|
|
267
|
+
assert.equal(normalizeToolResultText(text), text);
|
|
268
|
+
});
|
|
269
|
+
test("normalizeToolResultText sanitizes exact SDK rejection payloads for errors", () => {
|
|
270
|
+
const cancelledText = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.";
|
|
271
|
+
assert.equal(normalizeToolResultText(cancelledText, true), "Cancelled by user.");
|
|
272
|
+
const deniedText = "Permission for this tool use was denied. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). Try a different approach or report the limitation to complete your task.";
|
|
273
|
+
assert.equal(normalizeToolResultText(deniedText, true), "Permission denied.");
|
|
274
|
+
});
|
|
275
|
+
test("normalizeToolResultText sanitizes SDK rejection prefixes with user follow-up", () => {
|
|
276
|
+
const cancelledWithUserMessage = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). To tell you how to proceed, the user said:\nPlease skip this";
|
|
277
|
+
assert.equal(normalizeToolResultText(cancelledWithUserMessage, true), "Cancelled by user.");
|
|
278
|
+
const deniedWithUserMessage = "Permission for this tool use was denied. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). The user said:\nNot now";
|
|
279
|
+
assert.equal(normalizeToolResultText(deniedWithUserMessage, true), "Permission denied.");
|
|
280
|
+
});
|
|
281
|
+
test("normalizeToolResultText does not sanitize substring matches in error output", () => {
|
|
282
|
+
const bashOutput = "grep output: doesn't want to proceed with this tool use";
|
|
283
|
+
assert.equal(normalizeToolResultText(bashOutput, true), bashOutput);
|
|
284
|
+
});
|
|
103
285
|
test("cache split policy defaults stay aligned with UI thresholds", () => {
|
|
104
286
|
assert.equal(CACHE_SPLIT_POLICY.softLimitBytes, 1536);
|
|
105
287
|
assert.equal(CACHE_SPLIT_POLICY.hardLimitBytes, 4096);
|
|
@@ -124,6 +306,13 @@ test("buildToolResultFields uses normalized persisted-output text", () => {
|
|
|
124
306
|
},
|
|
125
307
|
]);
|
|
126
308
|
});
|
|
309
|
+
test("buildToolResultFields sanitizes SDK rejection text only for failed results", () => {
|
|
310
|
+
const sdkRejectionText = "The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.";
|
|
311
|
+
const successFields = buildToolResultFields(false, sdkRejectionText);
|
|
312
|
+
assert.equal(successFields.raw_output, sdkRejectionText);
|
|
313
|
+
const errorFields = buildToolResultFields(true, sdkRejectionText);
|
|
314
|
+
assert.equal(errorFields.raw_output, "Cancelled by user.");
|
|
315
|
+
});
|
|
127
316
|
test("buildToolResultFields maps structured Write output to diff content", () => {
|
|
128
317
|
const base = createToolCall("tc-w", "Write", {
|
|
129
318
|
file_path: "src/main.ts",
|
|
@@ -206,7 +395,7 @@ test("permissionOptionsFromSuggestions uses persistent label when settings scope
|
|
|
206
395
|
type: "addRules",
|
|
207
396
|
behavior: "allow",
|
|
208
397
|
destination: "localSettings",
|
|
209
|
-
rules: [{ toolName: "Bash", ruleContent: "
|
|
398
|
+
rules: [{ toolName: "Bash", ruleContent: "pnpm install" }],
|
|
210
399
|
},
|
|
211
400
|
]);
|
|
212
401
|
assert.deepEqual(options, [
|
|
@@ -216,13 +405,13 @@ test("permissionOptionsFromSuggestions uses persistent label when settings scope
|
|
|
216
405
|
]);
|
|
217
406
|
});
|
|
218
407
|
test("permissionResultFromOutcome keeps Bash allow_always suggestions unchanged", () => {
|
|
219
|
-
const allow = permissionResultFromOutcome({ outcome: "selected", option_id: "allow_always" }, "tool-1", { command: "
|
|
408
|
+
const allow = permissionResultFromOutcome({ outcome: "selected", option_id: "allow_always" }, "tool-1", { command: "pnpm install" }, [
|
|
220
409
|
{
|
|
221
410
|
type: "addRules",
|
|
222
411
|
behavior: "allow",
|
|
223
|
-
destination: "
|
|
412
|
+
destination: "localSettings",
|
|
224
413
|
rules: [
|
|
225
|
-
{ toolName: "Bash", ruleContent: "
|
|
414
|
+
{ toolName: "Bash", ruleContent: "pnpm install" },
|
|
226
415
|
{ toolName: "WebFetch", ruleContent: "https://example.com" },
|
|
227
416
|
{ toolName: "Bash", ruleContent: "dir /B" },
|
|
228
417
|
],
|
|
@@ -236,9 +425,9 @@ test("permissionResultFromOutcome keeps Bash allow_always suggestions unchanged"
|
|
|
236
425
|
{
|
|
237
426
|
type: "addRules",
|
|
238
427
|
behavior: "allow",
|
|
239
|
-
destination: "
|
|
428
|
+
destination: "localSettings",
|
|
240
429
|
rules: [
|
|
241
|
-
{ toolName: "Bash", ruleContent: "
|
|
430
|
+
{ toolName: "Bash", ruleContent: "pnpm install" },
|
|
242
431
|
{ toolName: "WebFetch", ruleContent: "https://example.com" },
|
|
243
432
|
{ toolName: "Bash", ruleContent: "dir /B" },
|
|
244
433
|
],
|
|
@@ -276,7 +465,7 @@ test("permissionResultFromOutcome falls back to session tool rule for allow_sess
|
|
|
276
465
|
},
|
|
277
466
|
]);
|
|
278
467
|
});
|
|
279
|
-
test("permissionResultFromOutcome
|
|
468
|
+
test("permissionResultFromOutcome falls back to localSettings rule for allow_always when only session suggestions exist", () => {
|
|
280
469
|
const allow = permissionResultFromOutcome({ outcome: "selected", option_id: "allow_always" }, "tool-4", { file_path: "C:\\work\\baz.txt" }, [
|
|
281
470
|
{
|
|
282
471
|
type: "addRules",
|
|
@@ -289,167 +478,132 @@ test("permissionResultFromOutcome does not apply session suggestions to allow_al
|
|
|
289
478
|
if (allow.behavior !== "allow") {
|
|
290
479
|
throw new Error("expected allow permission result");
|
|
291
480
|
}
|
|
292
|
-
assert.
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
outputTokens: 34,
|
|
299
|
-
cacheReadInputTokens: 5,
|
|
300
|
-
cacheCreationInputTokens: 6,
|
|
301
|
-
},
|
|
302
|
-
});
|
|
303
|
-
assert.deepEqual(update, {
|
|
304
|
-
type: "usage_update",
|
|
305
|
-
usage: {
|
|
306
|
-
input_tokens: 12,
|
|
307
|
-
output_tokens: 34,
|
|
308
|
-
cache_read_tokens: 5,
|
|
309
|
-
cache_write_tokens: 6,
|
|
310
|
-
},
|
|
311
|
-
});
|
|
312
|
-
});
|
|
313
|
-
test("buildUsageUpdateFromResult includes cost and context window fields", () => {
|
|
314
|
-
const update = buildUsageUpdateFromResult({
|
|
315
|
-
total_cost_usd: 1.25,
|
|
316
|
-
modelUsage: {
|
|
317
|
-
"claude-sonnet-4-5": {
|
|
318
|
-
contextWindow: 200000,
|
|
319
|
-
maxOutputTokens: 64000,
|
|
320
|
-
},
|
|
321
|
-
},
|
|
322
|
-
});
|
|
323
|
-
assert.deepEqual(update, {
|
|
324
|
-
type: "usage_update",
|
|
325
|
-
usage: {
|
|
326
|
-
total_cost_usd: 1.25,
|
|
327
|
-
context_window: 200000,
|
|
328
|
-
max_output_tokens: 64000,
|
|
481
|
+
assert.deepEqual(allow.updatedPermissions, [
|
|
482
|
+
{
|
|
483
|
+
type: "addRules",
|
|
484
|
+
rules: [{ toolName: "Write" }],
|
|
485
|
+
behavior: "allow",
|
|
486
|
+
destination: "localSettings",
|
|
329
487
|
},
|
|
330
|
-
|
|
488
|
+
]);
|
|
331
489
|
});
|
|
332
490
|
test("looksLikeAuthRequired detects login hints", () => {
|
|
333
491
|
assert.equal(looksLikeAuthRequired("Please run /login to continue"), true);
|
|
334
492
|
assert.equal(looksLikeAuthRequired("normal tool output"), false);
|
|
335
493
|
});
|
|
336
494
|
test("agent sdk version compatibility check matches pinned version", () => {
|
|
337
|
-
assert.equal(resolveInstalledAgentSdkVersion(), "0.2.
|
|
495
|
+
assert.equal(resolveInstalledAgentSdkVersion(), "0.2.63");
|
|
338
496
|
assert.equal(agentSdkVersionCompatibilityError(), undefined);
|
|
339
497
|
});
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
const filePath = path.join(dir, "session.jsonl");
|
|
343
|
-
fs.writeFileSync(filePath, `${lines.map((line) => JSON.stringify(line)).join("\n")}\n`, "utf8");
|
|
344
|
-
try {
|
|
345
|
-
run(filePath);
|
|
346
|
-
}
|
|
347
|
-
finally {
|
|
348
|
-
fs.rmSync(dir, { recursive: true, force: true });
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
test("extractSessionHistoryUpdatesFromJsonl parses nested progress message records", () => {
|
|
352
|
-
const lines = [
|
|
498
|
+
test("mapSessionMessagesToUpdates maps message content blocks", () => {
|
|
499
|
+
const updates = mapSessionMessagesToUpdates([
|
|
353
500
|
{
|
|
354
501
|
type: "user",
|
|
502
|
+
uuid: "u1",
|
|
503
|
+
session_id: "s1",
|
|
504
|
+
parent_tool_use_id: null,
|
|
355
505
|
message: {
|
|
356
506
|
role: "user",
|
|
357
507
|
content: [{ type: "text", text: "Top-level user prompt" }],
|
|
358
508
|
},
|
|
359
509
|
},
|
|
360
510
|
{
|
|
361
|
-
type: "
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
input_tokens: 11,
|
|
378
|
-
output_tokens: 7,
|
|
379
|
-
cache_read_input_tokens: 5,
|
|
380
|
-
cache_creation_input_tokens: 3,
|
|
381
|
-
},
|
|
382
|
-
},
|
|
511
|
+
type: "assistant",
|
|
512
|
+
uuid: "a1",
|
|
513
|
+
session_id: "s1",
|
|
514
|
+
parent_tool_use_id: null,
|
|
515
|
+
message: {
|
|
516
|
+
id: "msg-1",
|
|
517
|
+
role: "assistant",
|
|
518
|
+
content: [
|
|
519
|
+
{ type: "tool_use", id: "tool-1", name: "Bash", input: { command: "echo hello" } },
|
|
520
|
+
{ type: "text", text: "Nested assistant final" },
|
|
521
|
+
],
|
|
522
|
+
usage: {
|
|
523
|
+
input_tokens: 11,
|
|
524
|
+
output_tokens: 7,
|
|
525
|
+
cache_read_input_tokens: 5,
|
|
526
|
+
cache_creation_input_tokens: 3,
|
|
383
527
|
},
|
|
384
528
|
},
|
|
385
529
|
},
|
|
386
530
|
{
|
|
387
|
-
type: "
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
},
|
|
400
|
-
],
|
|
531
|
+
type: "user",
|
|
532
|
+
uuid: "u2",
|
|
533
|
+
session_id: "s1",
|
|
534
|
+
parent_tool_use_id: null,
|
|
535
|
+
message: {
|
|
536
|
+
role: "user",
|
|
537
|
+
content: [
|
|
538
|
+
{
|
|
539
|
+
type: "tool_result",
|
|
540
|
+
tool_use_id: "tool-1",
|
|
541
|
+
content: "ok",
|
|
542
|
+
is_error: false,
|
|
401
543
|
},
|
|
402
|
-
|
|
544
|
+
],
|
|
403
545
|
},
|
|
404
546
|
},
|
|
547
|
+
]);
|
|
548
|
+
const variantCounts = new Map();
|
|
549
|
+
for (const update of updates) {
|
|
550
|
+
variantCounts.set(update.type, (variantCounts.get(update.type) ?? 0) + 1);
|
|
551
|
+
}
|
|
552
|
+
assert.equal(variantCounts.get("user_message_chunk"), 1);
|
|
553
|
+
assert.equal(variantCounts.get("agent_message_chunk"), 1);
|
|
554
|
+
assert.equal(variantCounts.get("tool_call"), 1);
|
|
555
|
+
assert.equal(variantCounts.get("tool_call_update"), 1);
|
|
556
|
+
});
|
|
557
|
+
test("mapSessionMessagesToUpdates ignores unsupported records", () => {
|
|
558
|
+
const updates = mapSessionMessagesToUpdates([
|
|
405
559
|
{
|
|
406
|
-
type: "
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
content: [{ type: "text", text: "Nested assistant final" }],
|
|
414
|
-
usage: {
|
|
415
|
-
input_tokens: 11,
|
|
416
|
-
output_tokens: 7,
|
|
417
|
-
cache_read_input_tokens: 5,
|
|
418
|
-
cache_creation_input_tokens: 3,
|
|
419
|
-
},
|
|
420
|
-
},
|
|
421
|
-
},
|
|
560
|
+
type: "user",
|
|
561
|
+
uuid: "u1",
|
|
562
|
+
session_id: "s1",
|
|
563
|
+
parent_tool_use_id: null,
|
|
564
|
+
message: {
|
|
565
|
+
role: "assistant",
|
|
566
|
+
content: [{ type: "thinking", thinking: "h" }],
|
|
422
567
|
},
|
|
423
568
|
},
|
|
424
|
-
];
|
|
425
|
-
|
|
426
|
-
const updates = extractSessionHistoryUpdatesFromJsonl(filePath);
|
|
427
|
-
const variantCounts = new Map();
|
|
428
|
-
for (const update of updates) {
|
|
429
|
-
variantCounts.set(update.type, (variantCounts.get(update.type) ?? 0) + 1);
|
|
430
|
-
}
|
|
431
|
-
assert.equal(variantCounts.get("user_message_chunk"), 1);
|
|
432
|
-
assert.equal(variantCounts.get("agent_message_chunk"), 1);
|
|
433
|
-
assert.equal(variantCounts.get("tool_call"), 1);
|
|
434
|
-
assert.equal(variantCounts.get("tool_call_update"), 1);
|
|
435
|
-
assert.equal(variantCounts.get("usage_update"), 1);
|
|
436
|
-
const usage = updates.find((update) => update.type === "usage_update");
|
|
437
|
-
assert.ok(usage && usage.type === "usage_update");
|
|
438
|
-
assert.deepEqual(usage.usage, {
|
|
439
|
-
input_tokens: 11,
|
|
440
|
-
output_tokens: 7,
|
|
441
|
-
cache_read_tokens: 5,
|
|
442
|
-
cache_write_tokens: 3,
|
|
443
|
-
});
|
|
444
|
-
});
|
|
569
|
+
]);
|
|
570
|
+
assert.equal(updates.length, 0);
|
|
445
571
|
});
|
|
446
|
-
test("
|
|
447
|
-
|
|
448
|
-
{
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
572
|
+
test("mapSdkSessions normalizes and sorts sessions", () => {
|
|
573
|
+
const mapped = mapSdkSessions([
|
|
574
|
+
{
|
|
575
|
+
sessionId: "older",
|
|
576
|
+
summary: " Older summary ",
|
|
577
|
+
lastModified: 100,
|
|
578
|
+
fileSize: 10,
|
|
579
|
+
cwd: "C:/work",
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
sessionId: "latest",
|
|
583
|
+
summary: "",
|
|
584
|
+
lastModified: 200,
|
|
585
|
+
fileSize: 20,
|
|
586
|
+
customTitle: "Custom title",
|
|
587
|
+
gitBranch: "main",
|
|
588
|
+
firstPrompt: "hello",
|
|
589
|
+
},
|
|
590
|
+
]);
|
|
591
|
+
assert.deepEqual(mapped, [
|
|
592
|
+
{
|
|
593
|
+
session_id: "latest",
|
|
594
|
+
summary: "Custom title",
|
|
595
|
+
last_modified_ms: 200,
|
|
596
|
+
file_size_bytes: 20,
|
|
597
|
+
git_branch: "main",
|
|
598
|
+
custom_title: "Custom title",
|
|
599
|
+
first_prompt: "hello",
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
session_id: "older",
|
|
603
|
+
summary: "Older summary",
|
|
604
|
+
last_modified_ms: 100,
|
|
605
|
+
file_size_bytes: 10,
|
|
606
|
+
cwd: "C:/work",
|
|
607
|
+
},
|
|
608
|
+
]);
|
|
455
609
|
});
|
package/bin/claude-rs.js
CHANGED
|
@@ -24,7 +24,7 @@ function resolveInstall() {
|
|
|
24
24
|
return {
|
|
25
25
|
error:
|
|
26
26
|
`Missing binary at ${binaryPath}\n` +
|
|
27
|
-
"Reinstall with `
|
|
27
|
+
"Reinstall with `pnpm add -g claude-code-rust` to fetch release artifacts."
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-code-rust",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Claude Code Rust - native Rust terminal interface for Claude Code",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -29,16 +29,15 @@
|
|
|
29
29
|
"README.md"
|
|
30
30
|
],
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@anthropic-ai/claude-agent-sdk": "0.2.
|
|
33
|
-
},
|
|
34
|
-
"scripts": {
|
|
35
|
-
"postinstall": "node ./scripts/postinstall.js",
|
|
36
|
-
"prepack": "npm --prefix agent-sdk run build"
|
|
32
|
+
"@anthropic-ai/claude-agent-sdk": "0.2.63"
|
|
37
33
|
},
|
|
38
34
|
"engines": {
|
|
39
35
|
"node": ">=18"
|
|
40
36
|
},
|
|
41
37
|
"publishConfig": {
|
|
42
38
|
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"postinstall": "node ./scripts/postinstall.js"
|
|
43
42
|
}
|
|
44
|
-
}
|
|
43
|
+
}
|
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-pnpm-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");
|
package/agent-sdk/README.md
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# claude-rs agent-sdk bridge
|
|
2
|
-
|
|
3
|
-
Initial scaffold for the NDJSON stdio bridge that will connect Rust (`claude-code-rust`) with `@anthropic-ai/claude-agent-sdk`.
|
|
4
|
-
|
|
5
|
-
## Local build
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install
|
|
9
|
-
npm run build
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
Build output is written to `dist/bridge.mjs`.
|
|
13
|
-
|