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 CHANGED
@@ -10,7 +10,7 @@ A native Rust terminal interface for Claude Code. Drop-in replacement for Anthro
10
10
 
11
11
  ## About
12
12
 
13
- Claude Code Rust replaces the stock Claude Code terminal interface with a native Rust binary built on [Ratatui](https://ratatui.rs/). It connects to the same Claude API through a local Agent SDK bridge (`agent-sdk/dist/bridge.js`). Core Claude Code functionality - tool calls, file editing, terminal commands, and permissions - works unchanged.
13
+ Claude Code Rust replaces the stock Claude Code terminal interface with a native Rust binary built on [Ratatui](https://ratatui.rs/). It connects to the same Claude API through a local Agent SDK bridge. Core Claude Code functionality - tool calls, file editing, terminal commands, and permissions - works unchanged.
14
14
 
15
15
  ## Requisites
16
16
 
@@ -25,8 +25,11 @@ Claude Code Rust replaces the stock Claude Code terminal interface with a native
25
25
  npm install -g claude-code-rust
26
26
  ```
27
27
 
28
- The npm package installs a `claude-rs` command and downloads the matching
29
- prebuilt release binary for your platform during `postinstall`.
28
+ The published package installs a `claude-rs` command and fetches the matching
29
+ prebuilt release binary for your platform during install.
30
+
31
+ If `claude-rs` resolves to an older global shim, ensure your npm global bin
32
+ directory comes first on `PATH` or remove the stale shim before retrying.
30
33
 
31
34
  ## Usage
32
35
 
@@ -58,7 +61,7 @@ Three-layer design:
58
61
 
59
62
  ## Known Limitations
60
63
 
61
- - `/login` and `/logout` are intentionally not offered in command discovery for this release.
64
+ - The config view includes the Settings tab but the Status, Usage, and MCP tabs are not yet implemented.
62
65
 
63
66
  ## Status
64
67
 
@@ -10,4 +10,3 @@ npm run build
10
10
  ```
11
11
 
12
12
  Build output is written to `dist/bridge.mjs`.
13
-
@@ -0,0 +1,75 @@
1
+ import { emitSessionUpdate } from "./events.js";
2
+ function availableAgentsSignature(agents) {
3
+ return JSON.stringify(agents);
4
+ }
5
+ function normalizeAvailableAgentName(value) {
6
+ if (typeof value !== "string") {
7
+ return "";
8
+ }
9
+ return value.trim();
10
+ }
11
+ export function mapAvailableAgents(value) {
12
+ if (!Array.isArray(value)) {
13
+ return [];
14
+ }
15
+ const byName = new Map();
16
+ for (const entry of value) {
17
+ if (!entry || typeof entry !== "object") {
18
+ continue;
19
+ }
20
+ const record = entry;
21
+ const name = normalizeAvailableAgentName(record.name);
22
+ if (!name) {
23
+ continue;
24
+ }
25
+ const description = typeof record.description === "string" ? record.description : "";
26
+ const model = typeof record.model === "string" && record.model.trim().length > 0 ? record.model : undefined;
27
+ const existing = byName.get(name);
28
+ if (!existing) {
29
+ byName.set(name, { name, description, model });
30
+ continue;
31
+ }
32
+ if (existing.description.trim().length === 0 && description.trim().length > 0) {
33
+ existing.description = description;
34
+ }
35
+ if (!existing.model && model) {
36
+ existing.model = model;
37
+ }
38
+ }
39
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
40
+ }
41
+ export function mapAvailableAgentsFromNames(value) {
42
+ if (!Array.isArray(value)) {
43
+ return [];
44
+ }
45
+ const byName = new Map();
46
+ for (const entry of value) {
47
+ const name = normalizeAvailableAgentName(entry);
48
+ if (!name || byName.has(name)) {
49
+ continue;
50
+ }
51
+ byName.set(name, { name, description: "" });
52
+ }
53
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
54
+ }
55
+ export function emitAvailableAgentsIfChanged(session, agents) {
56
+ const signature = availableAgentsSignature(agents);
57
+ if (session.lastAvailableAgentsSignature === signature) {
58
+ return;
59
+ }
60
+ session.lastAvailableAgentsSignature = signature;
61
+ emitSessionUpdate(session.sessionId, { type: "available_agents_update", agents });
62
+ }
63
+ export function refreshAvailableAgents(session) {
64
+ if (typeof session.query.supportedAgents !== "function") {
65
+ return;
66
+ }
67
+ void session.query
68
+ .supportedAgents()
69
+ .then((agents) => {
70
+ emitAvailableAgentsIfChanged(session, mapAvailableAgents(agents));
71
+ })
72
+ .catch(() => {
73
+ // Best-effort only.
74
+ });
75
+ }
@@ -25,13 +25,6 @@ function expectString(record, key, context) {
25
25
  }
26
26
  return value;
27
27
  }
28
- function expectBoolean(record, key, context) {
29
- const value = record[key];
30
- if (typeof value !== "boolean") {
31
- throw new Error(`${context}.${key} must be a boolean`);
32
- }
33
- return value;
34
- }
35
28
  function optionalString(record, key, context) {
36
29
  const value = record[key];
37
30
  if (value === undefined || value === null) {
@@ -49,6 +42,45 @@ function optionalMetadata(record, key) {
49
42
  }
50
43
  return asRecord(value, `${key} metadata`);
51
44
  }
45
+ function optionalLaunchSettings(record, key, context) {
46
+ const value = record[key];
47
+ if (value === undefined || value === null) {
48
+ return {};
49
+ }
50
+ const parsed = asRecord(value, `${context}.${key}`);
51
+ const model = optionalString(parsed, "model", `${context}.${key}`);
52
+ const language = optionalString(parsed, "language", `${context}.${key}`);
53
+ const permissionMode = optionalString(parsed, "permission_mode", `${context}.${key}`);
54
+ const thinkingMode = optionalThinkingMode(parsed, "thinking_mode", `${context}.${key}`);
55
+ const effortLevel = optionalEffortLevel(parsed, "effort_level", `${context}.${key}`);
56
+ return {
57
+ ...(model ? { model } : {}),
58
+ ...(language ? { language } : {}),
59
+ ...(permissionMode ? { permission_mode: permissionMode } : {}),
60
+ ...(thinkingMode ? { thinking_mode: thinkingMode } : {}),
61
+ ...(effortLevel ? { effort_level: effortLevel } : {}),
62
+ };
63
+ }
64
+ function optionalThinkingMode(record, key, context) {
65
+ const value = optionalString(record, key, context);
66
+ if (value === undefined) {
67
+ return undefined;
68
+ }
69
+ if (value === "adaptive" || value === "disabled") {
70
+ return value;
71
+ }
72
+ throw new Error(`${context}.${key} must be "adaptive" or "disabled" when provided`);
73
+ }
74
+ function optionalEffortLevel(record, key, context) {
75
+ const value = optionalString(record, key, context);
76
+ if (value === undefined) {
77
+ return undefined;
78
+ }
79
+ if (value === "low" || value === "medium" || value === "high") {
80
+ return value;
81
+ }
82
+ throw new Error(`${context}.${key} must be "low", "medium", or "high" when provided`);
83
+ }
52
84
  function parsePromptChunks(record, context) {
53
85
  const rawChunks = record.chunks;
54
86
  if (!Array.isArray(rawChunks)) {
@@ -76,23 +108,22 @@ export function parseCommandEnvelope(line) {
76
108
  return {
77
109
  command: "create_session",
78
110
  cwd: expectString(raw, "cwd", "create_session"),
79
- yolo: expectBoolean(raw, "yolo", "create_session"),
80
- model: optionalString(raw, "model", "create_session"),
81
111
  resume: optionalString(raw, "resume", "create_session"),
112
+ launch_settings: optionalLaunchSettings(raw, "launch_settings", "create_session"),
82
113
  metadata: optionalMetadata(raw, "metadata"),
83
114
  };
84
115
  case "resume_session":
85
116
  return {
86
117
  command: "resume_session",
87
118
  session_id: expectString(raw, "session_id", "resume_session"),
119
+ launch_settings: optionalLaunchSettings(raw, "launch_settings", "resume_session"),
88
120
  metadata: optionalMetadata(raw, "metadata"),
89
121
  };
90
122
  case "new_session":
91
123
  return {
92
124
  command: "new_session",
93
125
  cwd: expectString(raw, "cwd", "new_session"),
94
- yolo: expectBoolean(raw, "yolo", "new_session"),
95
- model: optionalString(raw, "model", "new_session"),
126
+ launch_settings: optionalLaunchSettings(raw, "launch_settings", "new_session"),
96
127
  };
97
128
  case "prompt":
98
129
  return {
@@ -0,0 +1,55 @@
1
+ import { looksLikeAuthRequired } from "./auth.js";
2
+ import { writeEvent } from "./events.js";
3
+ import { emitSessionUpdate } from "./events.js";
4
+ import { parseFastModeState } from "./state_parsing.js";
5
+ export function emitAuthRequired(session, detail) {
6
+ if (session.authHintSent) {
7
+ return;
8
+ }
9
+ session.authHintSent = true;
10
+ writeEvent({
11
+ event: "auth_required",
12
+ method_name: "Claude Login",
13
+ method_description: detail && detail.trim().length > 0
14
+ ? detail
15
+ : "Type /login to authenticate.",
16
+ });
17
+ }
18
+ export function looksLikePlanLimitError(input) {
19
+ const normalized = input.toLowerCase();
20
+ return (normalized.includes("rate limit") ||
21
+ normalized.includes("rate-limit") ||
22
+ normalized.includes("max turns") ||
23
+ normalized.includes("max budget") ||
24
+ normalized.includes("quota") ||
25
+ normalized.includes("plan limit") ||
26
+ normalized.includes("too many requests") ||
27
+ normalized.includes("insufficient quota") ||
28
+ normalized.includes("429"));
29
+ }
30
+ export function classifyTurnErrorKind(subtype, errors, assistantError) {
31
+ const combined = errors.join("\n");
32
+ if (subtype === "error_max_turns" ||
33
+ subtype === "error_max_budget_usd" ||
34
+ assistantError === "billing_error" ||
35
+ assistantError === "rate_limit" ||
36
+ (combined.length > 0 && looksLikePlanLimitError(combined))) {
37
+ return "plan_limit";
38
+ }
39
+ if (assistantError === "authentication_failed" ||
40
+ errors.some((entry) => looksLikeAuthRequired(entry))) {
41
+ return "auth_required";
42
+ }
43
+ if (assistantError === "server_error") {
44
+ return "internal";
45
+ }
46
+ return "other";
47
+ }
48
+ export function emitFastModeUpdateIfChanged(session, value) {
49
+ const next = parseFastModeState(value);
50
+ if (!next || next === session.fastModeState) {
51
+ return;
52
+ }
53
+ session.fastModeState = next;
54
+ emitSessionUpdate(session.sessionId, { type: "fast_mode_update", fast_mode_state: next });
55
+ }
@@ -0,0 +1,83 @@
1
+ import { listSessions } from "@anthropic-ai/claude-agent-sdk";
2
+ import { buildModeState } from "./commands.js";
3
+ import { mapSdkSessions } from "./history.js";
4
+ const SESSION_LIST_LIMIT = 50;
5
+ export function writeEvent(event, requestId) {
6
+ const envelope = {
7
+ ...(requestId ? { request_id: requestId } : {}),
8
+ ...event,
9
+ };
10
+ process.stdout.write(`${JSON.stringify(envelope)}\n`);
11
+ }
12
+ export function failConnection(message, requestId) {
13
+ writeEvent({ event: "connection_failed", message }, requestId);
14
+ }
15
+ export function slashError(sessionId, message, requestId) {
16
+ writeEvent({ event: "slash_error", session_id: sessionId, message }, requestId);
17
+ }
18
+ export function emitSessionUpdate(sessionId, update) {
19
+ writeEvent({ event: "session_update", session_id: sessionId, update });
20
+ }
21
+ export function emitConnectEvent(session) {
22
+ const historyUpdates = session.resumeUpdates;
23
+ const connectEvent = session.connectEvent === "session_replaced"
24
+ ? {
25
+ event: "session_replaced",
26
+ session_id: session.sessionId,
27
+ cwd: session.cwd,
28
+ model_name: session.model,
29
+ available_models: session.availableModels,
30
+ mode: session.mode ? buildModeState(session.mode) : null,
31
+ ...(historyUpdates && historyUpdates.length > 0 ? { history_updates: historyUpdates } : {}),
32
+ }
33
+ : {
34
+ event: "connected",
35
+ session_id: session.sessionId,
36
+ cwd: session.cwd,
37
+ model_name: session.model,
38
+ available_models: session.availableModels,
39
+ mode: session.mode ? buildModeState(session.mode) : null,
40
+ ...(historyUpdates && historyUpdates.length > 0 ? { history_updates: historyUpdates } : {}),
41
+ };
42
+ writeEvent(connectEvent, session.connectRequestId);
43
+ session.connectRequestId = undefined;
44
+ session.connected = true;
45
+ session.authHintSent = false;
46
+ session.resumeUpdates = undefined;
47
+ const staleSessions = session.sessionsToCloseAfterConnect;
48
+ session.sessionsToCloseAfterConnect = undefined;
49
+ if (!staleSessions || staleSessions.length === 0) {
50
+ refreshSessionsList();
51
+ return;
52
+ }
53
+ void (async () => {
54
+ // Lazy import to break circular dependency at module-evaluation time.
55
+ const { sessions, closeSession } = await import("./session_lifecycle.js");
56
+ for (const stale of staleSessions) {
57
+ if (stale === session) {
58
+ continue;
59
+ }
60
+ if (sessions.get(stale.sessionId) === stale) {
61
+ sessions.delete(stale.sessionId);
62
+ }
63
+ await closeSession(stale);
64
+ }
65
+ refreshSessionsList();
66
+ })();
67
+ }
68
+ export async function emitSessionsList(requestId) {
69
+ try {
70
+ const sdkSessions = await listSessions({ limit: SESSION_LIST_LIMIT });
71
+ writeEvent({ event: "sessions_listed", sessions: mapSdkSessions(sdkSessions, SESSION_LIST_LIMIT) }, requestId);
72
+ }
73
+ catch (error) {
74
+ const message = error instanceof Error ? error.message : String(error);
75
+ console.error(`[sdk warn] listSessions failed: ${message}`);
76
+ writeEvent({ event: "sessions_listed", sessions: [] }, requestId);
77
+ }
78
+ }
79
+ export function refreshSessionsList() {
80
+ void emitSessionsList().catch(() => {
81
+ // Defensive no-op.
82
+ });
83
+ }
@@ -1,6 +1,5 @@
1
1
  import { asRecordOrNull } from "./shared.js";
2
2
  import { TOOL_RESULT_TYPES, buildToolResultFields, createToolCall, isToolUseBlockType } from "./tooling.js";
3
- import { buildUsageUpdateFromResult } from "./usage.js";
4
3
  function nonEmptyTrimmed(value) {
5
4
  if (typeof value !== "string") {
6
5
  return undefined;
@@ -62,20 +61,6 @@ function pushResumeToolResult(updates, toolCalls, block) {
62
61
  base.content = fields.content;
63
62
  }
64
63
  }
65
- function pushResumeUsageUpdate(updates, message, emittedUsageMessageIds) {
66
- const messageId = typeof message.id === "string" ? message.id : "";
67
- if (messageId && emittedUsageMessageIds.has(messageId)) {
68
- return;
69
- }
70
- const usageUpdate = buildUsageUpdateFromResult(message);
71
- if (!usageUpdate) {
72
- return;
73
- }
74
- updates.push(usageUpdate);
75
- if (messageId) {
76
- emittedUsageMessageIds.add(messageId);
77
- }
78
- }
79
64
  function summaryFromSession(info) {
80
65
  return (nonEmptyTrimmed(info.summary) ??
81
66
  nonEmptyTrimmed(info.customTitle) ??
@@ -113,7 +98,6 @@ export function mapSdkSessions(infos, limit = 50) {
113
98
  export function mapSessionMessagesToUpdates(messages) {
114
99
  const updates = [];
115
100
  const toolCalls = new Map();
116
- const emittedUsageMessageIds = new Set();
117
101
  for (const entry of messages) {
118
102
  const fallbackRole = entry.type === "assistant" ? "assistant" : "user";
119
103
  for (const message of messageCandidates(entry.message)) {
@@ -145,7 +129,6 @@ export function mapSessionMessagesToUpdates(messages) {
145
129
  pushResumeTextChunk(updates, role, "[image]");
146
130
  }
147
131
  }
148
- pushResumeUsageUpdate(updates, message, emittedUsageMessageIds);
149
132
  }
150
133
  }
151
134
  return updates;