claude-code-rust 0.6.0 → 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,6 +1,6 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { CACHE_SPLIT_POLICY, buildRateLimitUpdate, buildToolResultFields, buildUsageUpdateFromResult, createToolCall, mapAvailableAgents, mapSessionMessagesToUpdates, mapSdkSessions, agentSdkVersionCompatibilityError, looksLikeAuthRequired, normalizeToolResultText, parseFastModeState, parseRateLimitStatus, 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";
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/);
@@ -297,7 +395,7 @@ test("permissionOptionsFromSuggestions uses persistent label when settings scope
297
395
  type: "addRules",
298
396
  behavior: "allow",
299
397
  destination: "localSettings",
300
- rules: [{ toolName: "Bash", ruleContent: "npm install" }],
398
+ rules: [{ toolName: "Bash", ruleContent: "pnpm install" }],
301
399
  },
302
400
  ]);
303
401
  assert.deepEqual(options, [
@@ -307,13 +405,13 @@ test("permissionOptionsFromSuggestions uses persistent label when settings scope
307
405
  ]);
308
406
  });
309
407
  test("permissionResultFromOutcome keeps Bash allow_always suggestions unchanged", () => {
310
- 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" }, [
311
409
  {
312
410
  type: "addRules",
313
411
  behavior: "allow",
314
- destination: "projectSettings",
412
+ destination: "localSettings",
315
413
  rules: [
316
- { toolName: "Bash", ruleContent: "npm install" },
414
+ { toolName: "Bash", ruleContent: "pnpm install" },
317
415
  { toolName: "WebFetch", ruleContent: "https://example.com" },
318
416
  { toolName: "Bash", ruleContent: "dir /B" },
319
417
  ],
@@ -327,9 +425,9 @@ test("permissionResultFromOutcome keeps Bash allow_always suggestions unchanged"
327
425
  {
328
426
  type: "addRules",
329
427
  behavior: "allow",
330
- destination: "projectSettings",
428
+ destination: "localSettings",
331
429
  rules: [
332
- { toolName: "Bash", ruleContent: "npm install" },
430
+ { toolName: "Bash", ruleContent: "pnpm install" },
333
431
  { toolName: "WebFetch", ruleContent: "https://example.com" },
334
432
  { toolName: "Bash", ruleContent: "dir /B" },
335
433
  ],
@@ -367,7 +465,7 @@ test("permissionResultFromOutcome falls back to session tool rule for allow_sess
367
465
  },
368
466
  ]);
369
467
  });
370
- 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", () => {
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.equal(allow.updatedPermissions, undefined);
384
- });
385
- test("buildUsageUpdateFromResult maps SDK camelCase usage keys", () => {
386
- const update = buildUsageUpdateFromResult({
387
- usage: {
388
- inputTokens: 12,
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/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.6.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",
@@ -31,14 +31,13 @@
31
31
  "dependencies": {
32
32
  "@anthropic-ai/claude-agent-sdk": "0.2.63"
33
33
  },
34
- "scripts": {
35
- "postinstall": "node ./scripts/postinstall.js",
36
- "prepack": "pnpm -C agent-sdk run build"
37
- },
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
-
@@ -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
- }