claude-code-rust 0.3.0 → 0.4.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.
@@ -0,0 +1,445 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { buildToolResultFields, buildUsageUpdateFromResult, createToolCall, extractSessionHistoryUpdatesFromJsonl, looksLikeAuthRequired, normalizeToolResultText, normalizeToolKind, parseCommandEnvelope, permissionOptionsFromSuggestions, permissionResultFromOutcome, unwrapToolUseResult, } from "./bridge.js";
7
+ test("parseCommandEnvelope validates initialize command", () => {
8
+ const parsed = parseCommandEnvelope(JSON.stringify({
9
+ request_id: "req-1",
10
+ command: "initialize",
11
+ cwd: "C:/work",
12
+ }));
13
+ assert.equal(parsed.requestId, "req-1");
14
+ assert.equal(parsed.command.command, "initialize");
15
+ if (parsed.command.command !== "initialize") {
16
+ throw new Error("unexpected command variant");
17
+ }
18
+ assert.equal(parsed.command.cwd, "C:/work");
19
+ });
20
+ test("parseCommandEnvelope validates load_session command without cwd", () => {
21
+ const parsed = parseCommandEnvelope(JSON.stringify({
22
+ request_id: "req-2",
23
+ command: "load_session",
24
+ session_id: "session-123",
25
+ }));
26
+ assert.equal(parsed.requestId, "req-2");
27
+ assert.equal(parsed.command.command, "load_session");
28
+ if (parsed.command.command !== "load_session") {
29
+ throw new Error("unexpected command variant");
30
+ }
31
+ assert.equal(parsed.command.session_id, "session-123");
32
+ });
33
+ test("parseCommandEnvelope rejects missing required fields", () => {
34
+ assert.throws(() => parseCommandEnvelope(JSON.stringify({ command: "set_model", session_id: "s1" })), /set_model\.model must be a string/);
35
+ });
36
+ test("normalizeToolKind maps known tool names", () => {
37
+ assert.equal(normalizeToolKind("Bash"), "execute");
38
+ assert.equal(normalizeToolKind("Delete"), "delete");
39
+ assert.equal(normalizeToolKind("Move"), "move");
40
+ assert.equal(normalizeToolKind("ExitPlanMode"), "switch_mode");
41
+ assert.equal(normalizeToolKind("TodoWrite"), "other");
42
+ });
43
+ test("createToolCall builds edit diff content", () => {
44
+ const toolCall = createToolCall("tc-1", "Edit", {
45
+ file_path: "src/main.rs",
46
+ old_string: "old",
47
+ new_string: "new",
48
+ });
49
+ assert.equal(toolCall.kind, "edit");
50
+ assert.equal(toolCall.content.length, 1);
51
+ assert.deepEqual(toolCall.content[0], {
52
+ type: "diff",
53
+ old_path: "src/main.rs",
54
+ new_path: "src/main.rs",
55
+ old: "old",
56
+ new: "new",
57
+ });
58
+ assert.deepEqual(toolCall.meta, { claudeCode: { toolName: "Edit" } });
59
+ });
60
+ test("createToolCall builds write preview diff content", () => {
61
+ const toolCall = createToolCall("tc-w", "Write", {
62
+ file_path: "src/new-file.ts",
63
+ content: "export const x = 1;\n",
64
+ });
65
+ assert.equal(toolCall.kind, "edit");
66
+ assert.deepEqual(toolCall.content, [
67
+ {
68
+ type: "diff",
69
+ old_path: "src/new-file.ts",
70
+ new_path: "src/new-file.ts",
71
+ old: "",
72
+ new: "export const x = 1;\n",
73
+ },
74
+ ]);
75
+ });
76
+ test("createToolCall includes glob and webfetch context in title", () => {
77
+ const glob = createToolCall("tc-g", "Glob", { pattern: "**/*.md", path: "notes" });
78
+ assert.equal(glob.title, "Glob **/*.md in notes");
79
+ const fetch = createToolCall("tc-f", "WebFetch", { url: "https://example.com" });
80
+ assert.equal(fetch.title, "WebFetch https://example.com");
81
+ });
82
+ test("buildToolResultFields extracts plain-text output", () => {
83
+ const fields = buildToolResultFields(false, [{ text: "line 1" }, { text: "line 2" }]);
84
+ assert.equal(fields.status, "completed");
85
+ assert.equal(fields.raw_output, "line 1\nline 2");
86
+ assert.deepEqual(fields.content, [
87
+ { type: "content", content: { type: "text", text: "line 1\nline 2" } },
88
+ ]);
89
+ });
90
+ test("normalizeToolResultText collapses persisted-output payload to first meaningful line", () => {
91
+ const normalized = normalizeToolResultText(`
92
+ <persisted-output>
93
+ │ Output too large (132.5KB). Full output saved to: C:\\tmp\\tool-results\\bbf63b9.txt
94
+
95
+ │ Preview (first 2KB):
96
+
97
+ │ {"huge":"payload"}
98
+ │ ...
99
+ │ </persisted-output>
100
+ `);
101
+ assert.equal(normalized, "Output too large (132.5KB). Full output saved to: C:\\tmp\\tool-results\\bbf63b9.txt");
102
+ });
103
+ test("buildToolResultFields uses normalized persisted-output text", () => {
104
+ const fields = buildToolResultFields(false, `<persisted-output>
105
+ │ Output too large (14KB). Full output saved to: C:\\tmp\\tool-results\\x.txt
106
+
107
+ │ Preview (first 2KB):
108
+ │ {"k":"v"}
109
+ │ </persisted-output>`);
110
+ assert.equal(fields.raw_output, "Output too large (14KB). Full output saved to: C:\\tmp\\tool-results\\x.txt");
111
+ assert.deepEqual(fields.content, [
112
+ {
113
+ type: "content",
114
+ content: {
115
+ type: "text",
116
+ text: "Output too large (14KB). Full output saved to: C:\\tmp\\tool-results\\x.txt",
117
+ },
118
+ },
119
+ ]);
120
+ });
121
+ test("buildToolResultFields maps structured Write output to diff content", () => {
122
+ const base = createToolCall("tc-w", "Write", {
123
+ file_path: "src/main.ts",
124
+ content: "new",
125
+ });
126
+ const fields = buildToolResultFields(false, {
127
+ type: "update",
128
+ filePath: "src/main.ts",
129
+ content: "new",
130
+ originalFile: "old",
131
+ structuredPatch: [],
132
+ }, base);
133
+ assert.equal(fields.status, "completed");
134
+ assert.deepEqual(fields.content, [
135
+ {
136
+ type: "diff",
137
+ old_path: "src/main.ts",
138
+ new_path: "src/main.ts",
139
+ old: "old",
140
+ new: "new",
141
+ },
142
+ ]);
143
+ });
144
+ test("buildToolResultFields preserves Edit diff content from input", () => {
145
+ const base = createToolCall("tc-e", "Edit", {
146
+ file_path: "src/main.ts",
147
+ old_string: "old",
148
+ new_string: "new",
149
+ });
150
+ const fields = buildToolResultFields(false, [{ text: "Updated successfully" }], base);
151
+ assert.equal(fields.status, "completed");
152
+ assert.deepEqual(fields.content, [
153
+ {
154
+ type: "diff",
155
+ old_path: "src/main.ts",
156
+ new_path: "src/main.ts",
157
+ old: "old",
158
+ new: "new",
159
+ },
160
+ ]);
161
+ });
162
+ test("unwrapToolUseResult extracts error/content payload", () => {
163
+ const parsed = unwrapToolUseResult({
164
+ is_error: true,
165
+ content: [{ text: "failure output" }],
166
+ });
167
+ assert.equal(parsed.isError, true);
168
+ assert.deepEqual(parsed.content, [{ text: "failure output" }]);
169
+ });
170
+ test("permissionResultFromOutcome maps selected and cancelled outcomes", () => {
171
+ const allow = permissionResultFromOutcome({ outcome: "selected", option_id: "allow_always" }, "tool-1", { command: "echo test" }, []);
172
+ assert.equal(allow.behavior, "allow");
173
+ if (allow.behavior === "allow") {
174
+ assert.deepEqual(allow.updatedInput, { command: "echo test" });
175
+ }
176
+ const deny = permissionResultFromOutcome({ outcome: "selected", option_id: "reject_once" }, "tool-1", { command: "echo test" });
177
+ assert.equal(deny.behavior, "deny");
178
+ assert.match(String(deny.message), /Permission denied/);
179
+ const cancelled = permissionResultFromOutcome({ outcome: "cancelled" }, "tool-1", { command: "echo test" });
180
+ assert.equal(cancelled.behavior, "deny");
181
+ assert.match(String(cancelled.message), /cancelled/i);
182
+ });
183
+ test("permissionOptionsFromSuggestions uses session label when only session scope is suggested", () => {
184
+ const options = permissionOptionsFromSuggestions([
185
+ {
186
+ type: "setMode",
187
+ mode: "acceptEdits",
188
+ destination: "session",
189
+ },
190
+ ]);
191
+ assert.deepEqual(options, [
192
+ { option_id: "allow_once", name: "Allow once", kind: "allow_once" },
193
+ { option_id: "allow_session", name: "Allow for session", kind: "allow_session" },
194
+ { option_id: "reject_once", name: "Deny", kind: "reject_once" },
195
+ ]);
196
+ });
197
+ test("permissionOptionsFromSuggestions uses persistent label when settings scope is suggested", () => {
198
+ const options = permissionOptionsFromSuggestions([
199
+ {
200
+ type: "addRules",
201
+ behavior: "allow",
202
+ destination: "localSettings",
203
+ rules: [{ toolName: "Bash", ruleContent: "npm install" }],
204
+ },
205
+ ]);
206
+ assert.deepEqual(options, [
207
+ { option_id: "allow_once", name: "Allow once", kind: "allow_once" },
208
+ { option_id: "allow_always", name: "Always allow", kind: "allow_always" },
209
+ { option_id: "reject_once", name: "Deny", kind: "reject_once" },
210
+ ]);
211
+ });
212
+ test("permissionResultFromOutcome keeps Bash allow_always suggestions unchanged", () => {
213
+ const allow = permissionResultFromOutcome({ outcome: "selected", option_id: "allow_always" }, "tool-1", { command: "npm install" }, [
214
+ {
215
+ type: "addRules",
216
+ behavior: "allow",
217
+ destination: "projectSettings",
218
+ rules: [
219
+ { toolName: "Bash", ruleContent: "npm install" },
220
+ { toolName: "WebFetch", ruleContent: "https://example.com" },
221
+ { toolName: "Bash", ruleContent: "dir /B" },
222
+ ],
223
+ },
224
+ ], "Bash");
225
+ assert.equal(allow.behavior, "allow");
226
+ if (allow.behavior !== "allow") {
227
+ throw new Error("expected allow permission result");
228
+ }
229
+ assert.deepEqual(allow.updatedPermissions, [
230
+ {
231
+ type: "addRules",
232
+ behavior: "allow",
233
+ destination: "projectSettings",
234
+ rules: [
235
+ { toolName: "Bash", ruleContent: "npm install" },
236
+ { toolName: "WebFetch", ruleContent: "https://example.com" },
237
+ { toolName: "Bash", ruleContent: "dir /B" },
238
+ ],
239
+ },
240
+ ]);
241
+ });
242
+ test("permissionResultFromOutcome keeps Write allow_session suggestions unchanged", () => {
243
+ const suggestions = [
244
+ {
245
+ type: "addRules",
246
+ behavior: "allow",
247
+ destination: "session",
248
+ rules: [{ toolName: "Write", ruleContent: "C:\\work\\foo.txt" }],
249
+ },
250
+ ];
251
+ const allow = permissionResultFromOutcome({ outcome: "selected", option_id: "allow_session" }, "tool-2", { file_path: "C:\\work\\foo.txt" }, suggestions, "Write");
252
+ assert.equal(allow.behavior, "allow");
253
+ if (allow.behavior !== "allow") {
254
+ throw new Error("expected allow permission result");
255
+ }
256
+ assert.deepEqual(allow.updatedPermissions, suggestions);
257
+ });
258
+ test("permissionResultFromOutcome falls back to session tool rule for allow_session when suggestions are missing", () => {
259
+ const allow = permissionResultFromOutcome({ outcome: "selected", option_id: "allow_session" }, "tool-3", { file_path: "C:\\work\\bar.txt" }, undefined, "Write");
260
+ assert.equal(allow.behavior, "allow");
261
+ if (allow.behavior !== "allow") {
262
+ throw new Error("expected allow permission result");
263
+ }
264
+ assert.deepEqual(allow.updatedPermissions, [
265
+ {
266
+ type: "addRules",
267
+ behavior: "allow",
268
+ destination: "session",
269
+ rules: [{ toolName: "Write" }],
270
+ },
271
+ ]);
272
+ });
273
+ test("permissionResultFromOutcome does not apply session suggestions to allow_always", () => {
274
+ const allow = permissionResultFromOutcome({ outcome: "selected", option_id: "allow_always" }, "tool-4", { file_path: "C:\\work\\baz.txt" }, [
275
+ {
276
+ type: "addRules",
277
+ behavior: "allow",
278
+ destination: "session",
279
+ rules: [{ toolName: "Write", ruleContent: "C:\\work\\baz.txt" }],
280
+ },
281
+ ], "Write");
282
+ assert.equal(allow.behavior, "allow");
283
+ if (allow.behavior !== "allow") {
284
+ throw new Error("expected allow permission result");
285
+ }
286
+ assert.equal(allow.updatedPermissions, undefined);
287
+ });
288
+ test("buildUsageUpdateFromResult maps SDK camelCase usage keys", () => {
289
+ const update = buildUsageUpdateFromResult({
290
+ usage: {
291
+ inputTokens: 12,
292
+ outputTokens: 34,
293
+ cacheReadInputTokens: 5,
294
+ cacheCreationInputTokens: 6,
295
+ },
296
+ });
297
+ assert.deepEqual(update, {
298
+ type: "usage_update",
299
+ usage: {
300
+ input_tokens: 12,
301
+ output_tokens: 34,
302
+ cache_read_tokens: 5,
303
+ cache_write_tokens: 6,
304
+ },
305
+ });
306
+ });
307
+ test("buildUsageUpdateFromResult includes cost and context window fields", () => {
308
+ const update = buildUsageUpdateFromResult({
309
+ total_cost_usd: 1.25,
310
+ modelUsage: {
311
+ "claude-sonnet-4-5": {
312
+ contextWindow: 200000,
313
+ maxOutputTokens: 64000,
314
+ },
315
+ },
316
+ });
317
+ assert.deepEqual(update, {
318
+ type: "usage_update",
319
+ usage: {
320
+ total_cost_usd: 1.25,
321
+ context_window: 200000,
322
+ max_output_tokens: 64000,
323
+ },
324
+ });
325
+ });
326
+ test("looksLikeAuthRequired detects login hints", () => {
327
+ assert.equal(looksLikeAuthRequired("Please run /login to continue"), true);
328
+ assert.equal(looksLikeAuthRequired("normal tool output"), false);
329
+ });
330
+ function withTempJsonl(lines, run) {
331
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "claude-rs-resume-test-"));
332
+ const filePath = path.join(dir, "session.jsonl");
333
+ fs.writeFileSync(filePath, `${lines.map((line) => JSON.stringify(line)).join("\n")}\n`, "utf8");
334
+ try {
335
+ run(filePath);
336
+ }
337
+ finally {
338
+ fs.rmSync(dir, { recursive: true, force: true });
339
+ }
340
+ }
341
+ test("extractSessionHistoryUpdatesFromJsonl parses nested progress message records", () => {
342
+ const lines = [
343
+ {
344
+ type: "user",
345
+ message: {
346
+ role: "user",
347
+ content: [{ type: "text", text: "Top-level user prompt" }],
348
+ },
349
+ },
350
+ {
351
+ type: "progress",
352
+ data: {
353
+ message: {
354
+ type: "assistant",
355
+ message: {
356
+ id: "msg-nested-1",
357
+ role: "assistant",
358
+ content: [
359
+ {
360
+ type: "tool_use",
361
+ id: "tool-nested-1",
362
+ name: "Bash",
363
+ input: { command: "echo hello" },
364
+ },
365
+ ],
366
+ usage: {
367
+ input_tokens: 11,
368
+ output_tokens: 7,
369
+ cache_read_input_tokens: 5,
370
+ cache_creation_input_tokens: 3,
371
+ },
372
+ },
373
+ },
374
+ },
375
+ },
376
+ {
377
+ type: "progress",
378
+ data: {
379
+ message: {
380
+ type: "user",
381
+ message: {
382
+ role: "user",
383
+ content: [
384
+ {
385
+ type: "tool_result",
386
+ tool_use_id: "tool-nested-1",
387
+ content: "ok",
388
+ is_error: false,
389
+ },
390
+ ],
391
+ },
392
+ },
393
+ },
394
+ },
395
+ {
396
+ type: "progress",
397
+ data: {
398
+ message: {
399
+ type: "assistant",
400
+ message: {
401
+ id: "msg-nested-1",
402
+ role: "assistant",
403
+ content: [{ type: "text", text: "Nested assistant final" }],
404
+ usage: {
405
+ input_tokens: 11,
406
+ output_tokens: 7,
407
+ cache_read_input_tokens: 5,
408
+ cache_creation_input_tokens: 3,
409
+ },
410
+ },
411
+ },
412
+ },
413
+ },
414
+ ];
415
+ withTempJsonl(lines, (filePath) => {
416
+ const updates = extractSessionHistoryUpdatesFromJsonl(filePath);
417
+ const variantCounts = new Map();
418
+ for (const update of updates) {
419
+ variantCounts.set(update.type, (variantCounts.get(update.type) ?? 0) + 1);
420
+ }
421
+ assert.equal(variantCounts.get("user_message_chunk"), 1);
422
+ assert.equal(variantCounts.get("agent_message_chunk"), 1);
423
+ assert.equal(variantCounts.get("tool_call"), 1);
424
+ assert.equal(variantCounts.get("tool_call_update"), 1);
425
+ assert.equal(variantCounts.get("usage_update"), 1);
426
+ const usage = updates.find((update) => update.type === "usage_update");
427
+ assert.ok(usage && usage.type === "usage_update");
428
+ assert.deepEqual(usage.usage, {
429
+ input_tokens: 11,
430
+ output_tokens: 7,
431
+ cache_read_tokens: 5,
432
+ cache_write_tokens: 3,
433
+ });
434
+ });
435
+ });
436
+ test("extractSessionHistoryUpdatesFromJsonl ignores invalid records", () => {
437
+ withTempJsonl([
438
+ { type: "queue-operation", operation: "enqueue" },
439
+ { type: "progress", data: { not_message: true } },
440
+ { type: "user", message: { role: "assistant", content: [{ type: "thinking", thinking: "h" }] } },
441
+ ], (filePath) => {
442
+ const updates = extractSessionHistoryUpdatesFromJsonl(filePath);
443
+ assert.equal(updates.length, 0);
444
+ });
445
+ });
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-rust",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Claude Code Rust - native Rust terminal interface for Claude Code",
5
5
  "keywords": [
6
6
  "cli",
@@ -23,12 +23,17 @@
23
23
  },
24
24
  "files": [
25
25
  "bin",
26
+ "agent-sdk/dist",
26
27
  "scripts",
27
28
  "LICENSE",
28
29
  "README.md"
29
30
  ],
31
+ "dependencies": {
32
+ "@anthropic-ai/claude-agent-sdk": "0.2.52"
33
+ },
30
34
  "scripts": {
31
- "postinstall": "node ./scripts/postinstall.js"
35
+ "postinstall": "node ./scripts/postinstall.js",
36
+ "prepack": "npm --prefix agent-sdk run build"
32
37
  },
33
38
  "engines": {
34
39
  "node": ">=18"