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 +7 -4
- package/agent-sdk/README.md +0 -1
- package/agent-sdk/dist/bridge/agents.js +75 -0
- package/agent-sdk/dist/bridge/commands.js +42 -11
- 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 +0 -17
- 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/user_interaction.js +175 -0
- package/agent-sdk/dist/bridge.js +21 -1323
- package/agent-sdk/dist/bridge.test.js +109 -51
- package/package.json +2 -2
- package/scripts/postinstall.js +2 -2
- package/agent-sdk/dist/bridge/usage.js +0 -95
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
|
|
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
|
|
29
|
-
prebuilt release binary for your platform during
|
|
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
|
-
-
|
|
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
|
|
package/agent-sdk/README.md
CHANGED
|
@@ -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
|
-
|
|
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;
|