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.
@@ -1,9 +1,6 @@
1
1
  import test from "node:test";
2
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 { 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 load_session command without cwd", () => {
17
+ test("parseCommandEnvelope validates resume_session command without cwd", () => {
21
18
  const parsed = parseCommandEnvelope(JSON.stringify({
22
19
  request_id: "req-2",
23
- command: "load_session",
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, "load_session");
28
- if (parsed.command.command !== "load_session") {
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: "npm install" }],
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: "npm install" }, [
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: "projectSettings",
412
+ destination: "localSettings",
224
413
  rules: [
225
- { toolName: "Bash", ruleContent: "npm install" },
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: "projectSettings",
428
+ destination: "localSettings",
240
429
  rules: [
241
- { toolName: "Bash", ruleContent: "npm install" },
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 does not apply session suggestions to allow_always", () => {
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.equal(allow.updatedPermissions, undefined);
293
- });
294
- test("buildUsageUpdateFromResult maps SDK camelCase usage keys", () => {
295
- const update = buildUsageUpdateFromResult({
296
- usage: {
297
- inputTokens: 12,
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.52");
495
+ assert.equal(resolveInstalledAgentSdkVersion(), "0.2.63");
338
496
  assert.equal(agentSdkVersionCompatibilityError(), undefined);
339
497
  });
340
- function withTempJsonl(lines, run) {
341
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "claude-rs-resume-test-"));
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: "progress",
362
- data: {
363
- message: {
364
- type: "assistant",
365
- message: {
366
- id: "msg-nested-1",
367
- role: "assistant",
368
- content: [
369
- {
370
- type: "tool_use",
371
- id: "tool-nested-1",
372
- name: "Bash",
373
- input: { command: "echo hello" },
374
- },
375
- ],
376
- usage: {
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: "progress",
388
- data: {
389
- message: {
390
- type: "user",
391
- message: {
392
- role: "user",
393
- content: [
394
- {
395
- type: "tool_result",
396
- tool_use_id: "tool-nested-1",
397
- content: "ok",
398
- is_error: false,
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: "progress",
407
- data: {
408
- message: {
409
- type: "assistant",
410
- message: {
411
- id: "msg-nested-1",
412
- role: "assistant",
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
- withTempJsonl(lines, (filePath) => {
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("extractSessionHistoryUpdatesFromJsonl ignores invalid records", () => {
447
- withTempJsonl([
448
- { type: "queue-operation", operation: "enqueue" },
449
- { type: "progress", data: { not_message: true } },
450
- { type: "user", message: { role: "assistant", content: [{ type: "thinking", thinking: "h" }] } },
451
- ], (filePath) => {
452
- const updates = extractSessionHistoryUpdatesFromJsonl(filePath);
453
- assert.equal(updates.length, 0);
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 `npm install -g claude-code-rust` to fetch release artifacts."
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.5.1",
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.52"
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
+ }
@@ -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-npm-installer" } },
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 npm install: ${key}`);
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,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
-