claude-code-rust 0.14.0 → 0.14.2

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
@@ -18,14 +18,12 @@ Claude Code Rust replaces the stock Claude Code terminal interface with a native
18
18
 
19
19
  ## Prerequisite
20
20
 
21
- - Install the Claude Code CLI. You do not need to authenticate before installing or starting `claude-rs`; sign in later with `/login` or `claude auth login` when needed.
21
+ - The Claude Code CLI must be installed as fallback for some SDK-unsupported features.
22
22
 
23
23
  ## Install
24
24
 
25
25
  ### Install script (recommended, v0.14.0+)
26
26
 
27
- The install script downloads a complete GitHub Release archive. It does not require Rust, Node.js, npm, or Bun on the user's machine.
28
-
29
27
  **macOS/Linux:**
30
28
 
31
29
  ```bash
@@ -40,15 +38,11 @@ powershell -NoProfile -ExecutionPolicy Bypass -Command "irm 'https://raw.githubu
40
38
 
41
39
  ### npm (global)
42
40
 
43
- npm remains supported for users who prefer package-manager ownership of the global command:
44
-
45
41
  ```bash
46
42
  npm install -g claude-code-rust
47
43
  ```
48
44
 
49
- This option requires Node.js 24 and npm. The npm package installs a small launcher plus a platform-specific optional dependency containing the prebuilt Rust binary, Agent SDK bridge, and bundled private Bun runtime for your OS and architecture. A separate Rust toolchain or Bun installation is not required.
50
-
51
- See the [installation guide](https://srothgan.github.io/claude-code-rust/installation.html) for release pinning, custom locations, switching install methods, and troubleshooting.
45
+ See the [installation guide](https://srothgan.github.io/claude-code-rust/installation.html) for release pinning, custom install locations, switching install methods, uninstall, and troubleshooting.
52
46
 
53
47
  ## Usage
54
48
 
@@ -77,13 +71,15 @@ Claude Code Rust addresses these with a native terminal UI that uses diffed, dir
77
71
 
78
72
  ## Documentation
79
73
 
80
- The manual covers installation with scripts, npm, and source builds, plus help, slash commands, keyboard shortcuts, settings, diagnostics, architecture, and the changelog:
74
+ The manual covers installation with scripts and npm, plus help, slash commands, keyboard shortcuts, settings, diagnostics, troubleshooting, building from source, architecture, and the changelog:
81
75
 
82
76
  - [Installation](https://srothgan.github.io/claude-code-rust/installation.html)
83
77
  - [Usage](https://srothgan.github.io/claude-code-rust/usage.html)
84
78
  - [Help](https://srothgan.github.io/claude-code-rust/help.html)
85
79
  - [Slash commands](https://srothgan.github.io/claude-code-rust/commands.html)
86
80
  - [Settings](https://srothgan.github.io/claude-code-rust/settings.html)
81
+ - [Troubleshooting](https://srothgan.github.io/claude-code-rust/troubleshooting.html)
82
+ - [Development](https://srothgan.github.io/claude-code-rust/development.html)
87
83
 
88
84
  ## Status
89
85
 
@@ -4,6 +4,7 @@ const KNOWN_API_PROVIDERS = new Set([
4
4
  "vertex",
5
5
  "foundry",
6
6
  "anthropicAws",
7
+ "anthropicGoogleCloud",
7
8
  "mantle",
8
9
  "gateway",
9
10
  ]);
@@ -48,7 +48,7 @@ function shouldAcceptAvailableCommandsSnapshot(session, source, commands) {
48
48
  if (source === "supportedCommands" && current.commands.length > 0) {
49
49
  return {
50
50
  accept: false,
51
- reason: "supportedCommands is an initialize-time fallback and current snapshot already exists",
51
+ reason: "supportedCommands bootstrap cannot replace an existing snapshot",
52
52
  };
53
53
  }
54
54
  if (commands.length === 0) {
@@ -0,0 +1,16 @@
1
+ import { handleElicitationResponse, handlePermissionResponse, handleQuestionResponse, handleUserDialogResponse, } from "./session_lifecycle.js";
2
+ export function handleInteractionCommand(command) {
3
+ switch (command.command) {
4
+ case "permission_response":
5
+ handlePermissionResponse(command);
6
+ return;
7
+ case "question_response":
8
+ handleQuestionResponse(command);
9
+ return;
10
+ case "user_dialog_response":
11
+ handleUserDialogResponse(command);
12
+ return;
13
+ case "elicitation_response":
14
+ handleElicitationResponse(command);
15
+ }
16
+ }
@@ -0,0 +1,184 @@
1
+ import { getSessionMessages, listSessions, } from "@anthropic-ai/claude-agent-sdk";
2
+ import { currentSessionListOptions, emitSessionsList, failConnection, setSessionListingDir, slashError, writeEvent, } from "./events.js";
3
+ import { mapSessionMessagesToUpdates } from "./history.js";
4
+ import { bridgeLogger, LOG_TARGETS } from "./logger.js";
5
+ import { closeAllSessions, createSession, sessions, } from "./session_lifecycle.js";
6
+ export async function handleLifecycleCommand(command, requestId, sdkVersionError) {
7
+ switch (command.command) {
8
+ case "initialize":
9
+ await initialize(command, requestId, sdkVersionError);
10
+ return;
11
+ case "create_session":
12
+ await create(command, requestId);
13
+ return;
14
+ case "resume_session":
15
+ await resume(command, requestId);
16
+ return;
17
+ case "new_session":
18
+ await replace(command, requestId);
19
+ return;
20
+ case "shutdown":
21
+ await shutdown(requestId);
22
+ }
23
+ }
24
+ async function initialize(command, requestId, sdkVersionError) {
25
+ if (sdkVersionError) {
26
+ bridgeLogger.error({
27
+ target: LOG_TARGETS.BRIDGE_LIFECYCLE,
28
+ eventName: "bridge_initialize_failed",
29
+ message: "bridge initialization failed due to unsupported SDK version",
30
+ outcome: "failure",
31
+ ...(requestId ? { requestId } : {}),
32
+ fields: { error_message: sdkVersionError },
33
+ });
34
+ failConnection(sdkVersionError, requestId);
35
+ return;
36
+ }
37
+ setSessionListingDir(command.cwd);
38
+ writeEvent({
39
+ event: "initialized",
40
+ result: {
41
+ agent_name: "claude-rs-agent-bridge",
42
+ agent_version: "0.1.0",
43
+ auth_methods: [
44
+ {
45
+ id: "claude-login",
46
+ name: "Log in with Claude",
47
+ description: "Run `claude /login` in a terminal",
48
+ },
49
+ ],
50
+ capabilities: {
51
+ prompt_image: true,
52
+ prompt_embedded_context: true,
53
+ supports_session_listing: true,
54
+ supports_resume_session: true,
55
+ },
56
+ },
57
+ }, requestId);
58
+ await emitSessionsList(requestId);
59
+ }
60
+ async function create(command, requestId) {
61
+ bridgeLogger.info({
62
+ target: LOG_TARGETS.APP_SESSION,
63
+ eventName: "session_create_requested",
64
+ message: "session creation requested",
65
+ outcome: "start",
66
+ ...(requestId ? { requestId } : {}),
67
+ fields: {
68
+ cwd: command.cwd,
69
+ resume_requested: command.resume !== undefined,
70
+ },
71
+ });
72
+ setSessionListingDir(command.cwd);
73
+ await createSession({
74
+ cwd: command.cwd,
75
+ resume: command.resume,
76
+ launchSettings: command.launch_settings,
77
+ connectEvent: "connected",
78
+ requestId,
79
+ });
80
+ }
81
+ async function resume(command, requestId) {
82
+ bridgeLogger.info({
83
+ target: LOG_TARGETS.APP_SESSION,
84
+ eventName: "session_resume_requested",
85
+ message: "session resume requested",
86
+ outcome: "start",
87
+ ...(requestId ? { requestId } : {}),
88
+ sessionId: command.session_id,
89
+ });
90
+ try {
91
+ const sdkSessions = await listSessions(currentSessionListOptions());
92
+ const matched = sdkSessions.find((entry) => entry.sessionId === command.session_id);
93
+ if (!matched) {
94
+ bridgeLogger.warn({
95
+ target: LOG_TARGETS.APP_SESSION,
96
+ eventName: "session_resume_lookup_failed",
97
+ message: "session resume requested for an unknown session",
98
+ outcome: "failure",
99
+ ...(requestId ? { requestId } : {}),
100
+ sessionId: command.session_id,
101
+ fields: { reason: "unknown_session" },
102
+ });
103
+ slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
104
+ return;
105
+ }
106
+ setSessionListingDir(matched.cwd ?? process.cwd());
107
+ const historyMessages = await getSessionMessages(command.session_id, matched.cwd
108
+ ? { dir: matched.cwd, includeSystemMessages: true }
109
+ : { includeSystemMessages: true });
110
+ const resumeUpdates = mapSessionMessagesToUpdates(historyMessages);
111
+ const staleSessions = Array.from(sessions.values());
112
+ const hadActiveSession = staleSessions.length > 0;
113
+ bridgeLogger.info({
114
+ target: LOG_TARGETS.APP_SESSION,
115
+ eventName: "session_resume_history_loaded",
116
+ message: "session resume history loaded",
117
+ outcome: "success",
118
+ ...(requestId ? { requestId } : {}),
119
+ sessionId: command.session_id,
120
+ fields: {
121
+ history_update_count: resumeUpdates.length,
122
+ stale_session_count: staleSessions.length,
123
+ },
124
+ });
125
+ await createSession({
126
+ cwd: matched.cwd ?? process.cwd(),
127
+ resume: command.session_id,
128
+ launchSettings: command.launch_settings,
129
+ ...(resumeUpdates.length > 0 ? { resumeUpdates } : {}),
130
+ connectEvent: hadActiveSession ? "session_replaced" : "connected",
131
+ requestId,
132
+ ...(hadActiveSession ? { sessionsToCloseAfterConnect: staleSessions } : {}),
133
+ });
134
+ }
135
+ catch (error) {
136
+ const message = error instanceof Error ? error.message : String(error);
137
+ bridgeLogger.error({
138
+ target: LOG_TARGETS.APP_SESSION,
139
+ eventName: "session_resume_failed",
140
+ message: "session resume failed",
141
+ outcome: "failure",
142
+ ...(requestId ? { requestId } : {}),
143
+ sessionId: command.session_id,
144
+ fields: { error_message: message },
145
+ });
146
+ slashError(command.session_id, `failed to resume session: ${message}`, requestId);
147
+ }
148
+ }
149
+ async function replace(command, requestId) {
150
+ bridgeLogger.info({
151
+ target: LOG_TARGETS.APP_SESSION,
152
+ eventName: "session_new_requested",
153
+ message: "replacement session requested",
154
+ outcome: "start",
155
+ ...(requestId ? { requestId } : {}),
156
+ fields: { cwd: command.cwd },
157
+ });
158
+ await closeAllSessions({ reason: "new_session_requested", requestId });
159
+ setSessionListingDir(command.cwd);
160
+ await createSession({
161
+ cwd: command.cwd,
162
+ launchSettings: command.launch_settings,
163
+ connectEvent: "session_replaced",
164
+ requestId,
165
+ });
166
+ }
167
+ async function shutdown(requestId) {
168
+ bridgeLogger.info({
169
+ target: LOG_TARGETS.BRIDGE_LIFECYCLE,
170
+ eventName: "bridge_shutdown_requested",
171
+ message: "bridge shutdown requested",
172
+ outcome: "start",
173
+ ...(requestId ? { requestId } : {}),
174
+ });
175
+ await closeAllSessions({ reason: "bridge_shutdown_requested", requestId });
176
+ bridgeLogger.info({
177
+ target: LOG_TARGETS.BRIDGE_LIFECYCLE,
178
+ eventName: "bridge_shutdown_completed",
179
+ message: "bridge shutdown completed",
180
+ outcome: "success",
181
+ ...(requestId ? { requestId } : {}),
182
+ });
183
+ process.exit(0);
184
+ }
@@ -0,0 +1,32 @@
1
+ import { slashError } from "./events.js";
2
+ import { handleMcpAuthenticateCommand, handleMcpClearAuthCommand, handleMcpOauthCallbackUrlCommand, handleMcpReconnectCommand, handleMcpSetServersCommand, handleMcpStatusCommand, handleMcpToggleCommand, } from "./mcp.js";
3
+ import { sessionById } from "./session_lifecycle.js";
4
+ export async function handleMcpCommand(command, requestId) {
5
+ const session = sessionById(command.session_id);
6
+ if (!session) {
7
+ slashError(command.session_id, `unknown session: ${command.session_id}`, requestId);
8
+ return;
9
+ }
10
+ switch (command.command) {
11
+ case "mcp_status":
12
+ await handleMcpStatusCommand(session, requestId);
13
+ return;
14
+ case "mcp_reconnect":
15
+ await handleMcpReconnectCommand(session, command, requestId);
16
+ return;
17
+ case "mcp_toggle":
18
+ await handleMcpToggleCommand(session, command, requestId);
19
+ return;
20
+ case "mcp_set_servers":
21
+ await handleMcpSetServersCommand(session, command, requestId);
22
+ return;
23
+ case "mcp_authenticate":
24
+ await handleMcpAuthenticateCommand(session, command, requestId);
25
+ return;
26
+ case "mcp_clear_auth":
27
+ await handleMcpClearAuthCommand(session, command, requestId);
28
+ return;
29
+ case "mcp_oauth_callback_url":
30
+ await handleMcpOauthCallbackUrlCommand(session, command, requestId);
31
+ }
32
+ }
@@ -0,0 +1,136 @@
1
+ const LIFECYCLE_COMMANDS = new Set([
2
+ "initialize",
3
+ "create_session",
4
+ "resume_session",
5
+ "new_session",
6
+ "rewind",
7
+ "shutdown",
8
+ ]);
9
+ const UNBLOCKING_COMMANDS = new Set([
10
+ "cancel_turn",
11
+ "permission_response",
12
+ "question_response",
13
+ "user_dialog_response",
14
+ "elicitation_response",
15
+ "mcp_oauth_callback_url",
16
+ ]);
17
+ function commandLane(command) {
18
+ if (UNBLOCKING_COMMANDS.has(command.command)) {
19
+ return { kind: "unblocking" };
20
+ }
21
+ if (LIFECYCLE_COMMANDS.has(command.command)) {
22
+ return { kind: "lifecycle" };
23
+ }
24
+ if ("session_id" in command) {
25
+ return { kind: "session", sessionId: command.session_id };
26
+ }
27
+ return { kind: "lifecycle" };
28
+ }
29
+ function settle(task) {
30
+ return task.then(() => undefined, () => undefined);
31
+ }
32
+ export class BridgeCommandScheduler {
33
+ lifecycleTail;
34
+ sessionTails = new Map();
35
+ activeTasks = new Set();
36
+ shutdownState = "accepting";
37
+ schedule(command, task) {
38
+ const lane = commandLane(command);
39
+ if (!this.accepts(command, lane)) {
40
+ return undefined;
41
+ }
42
+ let scheduledTask = task;
43
+ if (command.command === "shutdown") {
44
+ this.shutdownState = "queued";
45
+ scheduledTask = async () => {
46
+ this.shutdownState = "running";
47
+ try {
48
+ await task();
49
+ }
50
+ finally {
51
+ this.shutdownState = "complete";
52
+ }
53
+ };
54
+ }
55
+ const operation = this.scheduleInLane(lane, scheduledTask);
56
+ this.track(operation);
57
+ return operation;
58
+ }
59
+ stopAccepting() {
60
+ if (this.shutdownState === "accepting") {
61
+ this.shutdownState = "complete";
62
+ }
63
+ }
64
+ async whenIdle() {
65
+ while (this.activeTasks.size > 0) {
66
+ await Promise.all(this.activeTasks);
67
+ }
68
+ }
69
+ accepts(command, lane) {
70
+ if (this.shutdownState === "accepting") {
71
+ return true;
72
+ }
73
+ return (this.shutdownState === "queued" &&
74
+ command.command !== "shutdown" &&
75
+ lane.kind === "unblocking");
76
+ }
77
+ scheduleInLane(lane, task) {
78
+ switch (lane.kind) {
79
+ case "unblocking":
80
+ return this.start(task);
81
+ case "lifecycle":
82
+ return this.scheduleLifecycle(task);
83
+ case "session":
84
+ return this.scheduleSession(lane.sessionId, task);
85
+ }
86
+ }
87
+ scheduleLifecycle(task) {
88
+ const dependencies = new Set(this.sessionTails.values());
89
+ if (this.lifecycleTail) {
90
+ dependencies.add(this.lifecycleTail);
91
+ }
92
+ const operation = dependencies.size === 0 ? this.start(task) : Promise.all(dependencies).then(task);
93
+ const tail = settle(operation);
94
+ this.lifecycleTail = tail;
95
+ void tail.then(() => {
96
+ if (this.lifecycleTail === tail) {
97
+ this.lifecycleTail = undefined;
98
+ }
99
+ });
100
+ return operation;
101
+ }
102
+ scheduleSession(sessionId, task) {
103
+ const dependencies = [];
104
+ if (this.lifecycleTail) {
105
+ dependencies.push(this.lifecycleTail);
106
+ }
107
+ const previousSessionTask = this.sessionTails.get(sessionId);
108
+ if (previousSessionTask) {
109
+ dependencies.push(previousSessionTask);
110
+ }
111
+ const operation = dependencies.length === 0 ? this.start(task) : Promise.all(dependencies).then(task);
112
+ const tail = settle(operation);
113
+ this.sessionTails.set(sessionId, tail);
114
+ void tail.then(() => {
115
+ if (this.sessionTails.get(sessionId) === tail) {
116
+ this.sessionTails.delete(sessionId);
117
+ }
118
+ });
119
+ return operation;
120
+ }
121
+ track(operation) {
122
+ const tracked = settle(operation);
123
+ this.activeTasks.add(tracked);
124
+ void tracked.then(() => {
125
+ this.activeTasks.delete(tracked);
126
+ });
127
+ }
128
+ start(task) {
129
+ try {
130
+ return task();
131
+ }
132
+ catch (error) {
133
+ return Promise.reject(error);
134
+ }
135
+ }
136
+ }