claude-code-rust 0.9.0 → 0.11.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.
- package/README.md +16 -9
- package/agent-sdk/README.md +1 -1
- package/agent-sdk/dist/bridge/commands.js +71 -2
- package/agent-sdk/dist/bridge/events.js +127 -12
- package/agent-sdk/dist/bridge/history.js +9 -4
- package/agent-sdk/dist/bridge/logger.js +253 -0
- package/agent-sdk/dist/bridge/mcp.js +81 -5
- package/agent-sdk/dist/bridge/message_handlers.js +318 -67
- package/agent-sdk/dist/bridge/session_lifecycle.js +774 -58
- package/agent-sdk/dist/bridge/shared.js +0 -7
- package/agent-sdk/dist/bridge/state_parsing.js +61 -0
- package/agent-sdk/dist/bridge/tool_calls.js +269 -66
- package/agent-sdk/dist/bridge/tooling.js +60 -27
- package/agent-sdk/dist/bridge/user_interaction.js +34 -23
- package/agent-sdk/dist/bridge.js +406 -42
- package/agent-sdk/dist/bridge.test.js +765 -23
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -25,11 +25,9 @@ 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 published package installs a `claude-rs` command and fetches the matching
|
|
29
|
-
prebuilt release binary for your platform during install.
|
|
28
|
+
The published package installs a `claude-rs` command and fetches the matching prebuilt release binary for your platform during install.
|
|
30
29
|
|
|
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
|
+
If `claude-rs` resolves to an older global shim, ensure your npm global bin directory comes first on `PATH` or remove the stale shim before retrying.
|
|
33
31
|
|
|
34
32
|
## Usage
|
|
35
33
|
|
|
@@ -63,13 +61,22 @@ Three-layer design:
|
|
|
63
61
|
|
|
64
62
|
This project is pre-1.0 and under active development. See [CONTRIBUTING.md](CONTRIBUTING.md) for how to get involved.
|
|
65
63
|
|
|
64
|
+
## Limitations
|
|
65
|
+
|
|
66
|
+
Startup is still constrained by the upstream Claude Agent SDK runtime that this TUI wraps. The Rust interface itself is fast, but end-to-end readiness can still take noticeable time before a session is fully available. Improving that remains an active area of work.
|
|
67
|
+
|
|
66
68
|
## License
|
|
67
69
|
|
|
68
|
-
This project is licensed under the [Apache License 2.0](LICENSE).
|
|
69
|
-
|
|
70
|
+
This project is licensed under the [Apache License 2.0](LICENSE). Apache-2.0 was chosen to keep usage and redistribution straightforward for individual users, downstream packagers, and commercial adopters.
|
|
71
|
+
|
|
72
|
+
## Disclaimer and Legal Notice
|
|
73
|
+
|
|
74
|
+
This project is not affiliated with, endorsed by, or supported by Anthropic.
|
|
75
|
+
|
|
76
|
+
A quick note on where this project stands, since I know people worry about this kind of thing: claude-code-rust is a terminal UI that I wrote from scratch in Rust. It is not a fork, copy or port of the latest Claude Code source leak -- it talks to Anthropic's official [Agent SDK](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/agent-sdk) as a runtime dependency instead, the same way any other third-party tool would. No Anthropic source code was read or used as reference at any point during development.
|
|
77
|
+
|
|
78
|
+
The project uses your existing Claude Code subscription via the Agent SDK and the Agent SDK's terms allow building on top of it. Other community projects do the same. As far as I can tell, using this project is fine -- but I am a single maintainer, not a lawyer. If anything changes on Anthropic's end, I will update this section and adjust the project accordingly.
|
|
70
79
|
|
|
71
|
-
|
|
80
|
+
This project's source code is licensed under [Apache-2.0](LICENSE). The Agent SDK itself is proprietary and governed by [Anthropic's Commercial Terms of Service](https://www.anthropic.com/legal/commercial-terms).
|
|
72
81
|
|
|
73
|
-
This project is an unofficial terminal UI for Claude Code and is not affiliated with, endorsed by, or supported by Anthropic.
|
|
74
|
-
Use it at your own risk.
|
|
75
82
|
For official Claude documentation, see [https://claude.ai/docs](https://claude.ai/docs).
|
package/agent-sdk/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# claude-rs agent-sdk bridge
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
NDJSON stdio bridge that connects the Rust TUI (`claude-code-rust`) with `@anthropic-ai/claude-agent-sdk`. Spawned as a child process by the Rust binary and communicates via line-delimited JSON envelopes over stdin/stdout.
|
|
4
4
|
|
|
5
5
|
## Local build
|
|
6
6
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { resolveCurrentModel } from "./session_lifecycle.js";
|
|
1
2
|
const MODE_NAMES = {
|
|
2
3
|
default: "Default",
|
|
4
|
+
auto: "Auto",
|
|
3
5
|
acceptEdits: "Accept Edits",
|
|
4
6
|
bypassPermissions: "Bypass Permissions",
|
|
5
7
|
plan: "Plan",
|
|
@@ -7,11 +9,67 @@ const MODE_NAMES = {
|
|
|
7
9
|
};
|
|
8
10
|
const MODE_OPTIONS = [
|
|
9
11
|
{ id: "default", name: "Default", description: "Standard permission flow" },
|
|
12
|
+
{ id: "auto", name: "Auto", description: "Model-classified permission approvals" },
|
|
10
13
|
{ id: "acceptEdits", name: "Accept Edits", description: "Auto-approve edit operations" },
|
|
11
14
|
{ id: "plan", name: "Plan", description: "No tool execution" },
|
|
12
15
|
{ id: "dontAsk", name: "Don't Ask", description: "Reject non-approved tools" },
|
|
13
16
|
{ id: "bypassPermissions", name: "Bypass Permissions", description: "Auto-approve all tools" },
|
|
14
17
|
];
|
|
18
|
+
const BASE_SUPPORTED_MODE_IDS = [
|
|
19
|
+
"default",
|
|
20
|
+
"acceptEdits",
|
|
21
|
+
"plan",
|
|
22
|
+
"dontAsk",
|
|
23
|
+
];
|
|
24
|
+
function currentModelSupportsAutoMode(session) {
|
|
25
|
+
const currentModel = session.currentModel ?? resolveCurrentModel(session);
|
|
26
|
+
return currentModel.supports_auto_mode === true;
|
|
27
|
+
}
|
|
28
|
+
function modeInfoForId(mode) {
|
|
29
|
+
return MODE_OPTIONS.find((entry) => entry.id === mode) ?? {
|
|
30
|
+
id: mode,
|
|
31
|
+
name: MODE_NAMES[mode],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function uniqueModeIds(modeIds) {
|
|
35
|
+
const unique = new Set(modeIds);
|
|
36
|
+
const ordered = MODE_OPTIONS.map((entry) => entry.id).filter((mode) => unique.has(mode));
|
|
37
|
+
const extras = Array.from(unique).filter((mode) => !ordered.includes(mode));
|
|
38
|
+
return [...ordered, ...extras];
|
|
39
|
+
}
|
|
40
|
+
function computedSupportedModeIds(session) {
|
|
41
|
+
const supported = [...BASE_SUPPORTED_MODE_IDS];
|
|
42
|
+
if (currentModelSupportsAutoMode(session)) {
|
|
43
|
+
supported.push("auto");
|
|
44
|
+
}
|
|
45
|
+
if (session.supportsBypassPermissionsMode) {
|
|
46
|
+
supported.push("bypassPermissions");
|
|
47
|
+
}
|
|
48
|
+
if (session.mode) {
|
|
49
|
+
supported.push(session.mode);
|
|
50
|
+
}
|
|
51
|
+
return uniqueModeIds(supported);
|
|
52
|
+
}
|
|
53
|
+
export function refreshSupportedModesForSession(session) {
|
|
54
|
+
const computed = computedSupportedModeIds(session);
|
|
55
|
+
session.supportedModeIds = computed.filter((mode) => mode === session.mode || !session.runtimeUnavailableModeIds.includes(mode));
|
|
56
|
+
}
|
|
57
|
+
export function markModeUnavailableForSession(session, mode) {
|
|
58
|
+
if (session.runtimeUnavailableModeIds.includes(mode)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
session.runtimeUnavailableModeIds = [...session.runtimeUnavailableModeIds, mode];
|
|
62
|
+
refreshSupportedModesForSession(session);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
export function permissionModeFailureLooksUnsupported(mode, message) {
|
|
66
|
+
const normalizedMessage = message.toLowerCase();
|
|
67
|
+
return (normalizedMessage.includes("cannot set permission mode to") &&
|
|
68
|
+
normalizedMessage.includes(mode.toLowerCase()));
|
|
69
|
+
}
|
|
70
|
+
export function availableModesForSession(session) {
|
|
71
|
+
return session.supportedModeIds.map(modeInfoForId);
|
|
72
|
+
}
|
|
15
73
|
function asRecord(value, context) {
|
|
16
74
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
17
75
|
throw new Error(`${context} must be an object`);
|
|
@@ -205,6 +263,16 @@ export function parseCommandEnvelope(line) {
|
|
|
205
263
|
command: "get_status_snapshot",
|
|
206
264
|
session_id: expectString(raw, "session_id", "get_status_snapshot"),
|
|
207
265
|
};
|
|
266
|
+
case "get_context_usage":
|
|
267
|
+
return {
|
|
268
|
+
command: "get_context_usage",
|
|
269
|
+
session_id: expectString(raw, "session_id", "get_context_usage"),
|
|
270
|
+
};
|
|
271
|
+
case "reload_plugins":
|
|
272
|
+
return {
|
|
273
|
+
command: "reload_plugins",
|
|
274
|
+
session_id: expectString(raw, "session_id", "reload_plugins"),
|
|
275
|
+
};
|
|
208
276
|
case "mcp_status":
|
|
209
277
|
case "get_mcp_snapshot":
|
|
210
278
|
return {
|
|
@@ -341,6 +409,7 @@ function parseQuestionAnnotation(value) {
|
|
|
341
409
|
}
|
|
342
410
|
export function toPermissionMode(mode) {
|
|
343
411
|
if (mode === "default" ||
|
|
412
|
+
mode === "auto" ||
|
|
344
413
|
mode === "acceptEdits" ||
|
|
345
414
|
mode === "bypassPermissions" ||
|
|
346
415
|
mode === "plan" ||
|
|
@@ -349,10 +418,10 @@ export function toPermissionMode(mode) {
|
|
|
349
418
|
}
|
|
350
419
|
return null;
|
|
351
420
|
}
|
|
352
|
-
export function buildModeState(mode) {
|
|
421
|
+
export function buildModeState(session, mode) {
|
|
353
422
|
return {
|
|
354
423
|
current_mode_id: mode,
|
|
355
424
|
current_mode_name: MODE_NAMES[mode],
|
|
356
|
-
available_modes:
|
|
425
|
+
available_modes: availableModesForSession(session),
|
|
357
426
|
};
|
|
358
427
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { listSessions } from "@anthropic-ai/claude-agent-sdk";
|
|
2
2
|
import { buildModeState } from "./commands.js";
|
|
3
3
|
import { mapSdkSessions } from "./history.js";
|
|
4
|
+
import { bridgeLogger, LOG_TARGETS, logBridgeEventSent } from "./logger.js";
|
|
5
|
+
import { resolveCurrentModel } from "./session_lifecycle.js";
|
|
4
6
|
const SESSION_LIST_LIMIT = 50;
|
|
5
7
|
let sessionListingDir;
|
|
6
8
|
export function buildSessionListOptions(dir, limit = SESSION_LIST_LIMIT) {
|
|
@@ -17,7 +19,9 @@ export function writeEvent(event, requestId) {
|
|
|
17
19
|
...(requestId ? { request_id: requestId } : {}),
|
|
18
20
|
...event,
|
|
19
21
|
};
|
|
20
|
-
|
|
22
|
+
const serialized = JSON.stringify(envelope);
|
|
23
|
+
logBridgeEventSent(event, requestId, Buffer.byteLength(serialized) + 1);
|
|
24
|
+
process.stdout.write(`${serialized}\n`);
|
|
21
25
|
}
|
|
22
26
|
export function failConnection(message, requestId) {
|
|
23
27
|
writeEvent({ event: "connection_failed", message }, requestId);
|
|
@@ -25,34 +29,110 @@ export function failConnection(message, requestId) {
|
|
|
25
29
|
export function slashError(sessionId, message, requestId) {
|
|
26
30
|
writeEvent({ event: "slash_error", session_id: sessionId, message }, requestId);
|
|
27
31
|
}
|
|
32
|
+
export function emitRuntimeReloadCompleted(sessionId, requestId) {
|
|
33
|
+
writeEvent({ event: "runtime_reload_completed", session_id: sessionId }, requestId);
|
|
34
|
+
}
|
|
35
|
+
export function emitRuntimeReloadFailed(sessionId, message, requestId) {
|
|
36
|
+
writeEvent({ event: "runtime_reload_failed", session_id: sessionId, message }, requestId);
|
|
37
|
+
}
|
|
28
38
|
export function emitMcpOperationError(sessionId, error, requestId) {
|
|
29
39
|
writeEvent({ event: "mcp_operation_error", session_id: sessionId, error }, requestId);
|
|
30
40
|
}
|
|
31
41
|
export function emitSessionUpdate(sessionId, update) {
|
|
32
42
|
writeEvent({ event: "session_update", session_id: sessionId, update });
|
|
33
43
|
}
|
|
34
|
-
export function
|
|
44
|
+
export function emitPermissionRequestEvent(sessionId, request) {
|
|
45
|
+
bridgeLogger.info({
|
|
46
|
+
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
47
|
+
eventName: "permission_request_emitted",
|
|
48
|
+
message: "permission request emitted",
|
|
49
|
+
outcome: "success",
|
|
50
|
+
sessionId,
|
|
51
|
+
toolCallId: request.tool_call.tool_call_id,
|
|
52
|
+
count: request.options.length,
|
|
53
|
+
fields: {
|
|
54
|
+
option_count: request.options.length,
|
|
55
|
+
tool_title: request.tool_call.title,
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
writeEvent({ event: "permission_request", session_id: sessionId, request });
|
|
59
|
+
}
|
|
60
|
+
export function emitQuestionRequestEvent(sessionId, request) {
|
|
61
|
+
bridgeLogger.info({
|
|
62
|
+
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
63
|
+
eventName: "question_request_emitted",
|
|
64
|
+
message: "question request emitted",
|
|
65
|
+
outcome: "success",
|
|
66
|
+
sessionId,
|
|
67
|
+
toolCallId: request.tool_call.tool_call_id,
|
|
68
|
+
count: request.prompt.options.length,
|
|
69
|
+
fields: {
|
|
70
|
+
question_index: request.question_index,
|
|
71
|
+
total_questions: request.total_questions,
|
|
72
|
+
option_count: request.prompt.options.length,
|
|
73
|
+
header: request.prompt.header,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
writeEvent({ event: "question_request", session_id: sessionId, request });
|
|
77
|
+
}
|
|
78
|
+
export function emitElicitationRequestEvent(sessionId, request) {
|
|
79
|
+
bridgeLogger.info({
|
|
80
|
+
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
81
|
+
eventName: "elicitation_request_emitted",
|
|
82
|
+
message: "elicitation request emitted",
|
|
83
|
+
outcome: "success",
|
|
84
|
+
sessionId,
|
|
85
|
+
requestId: request.request_id,
|
|
86
|
+
fields: {
|
|
87
|
+
server_name: request.server_name,
|
|
88
|
+
mode: request.mode,
|
|
89
|
+
has_url: request.url !== undefined,
|
|
90
|
+
has_requested_schema: request.requested_schema !== undefined,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
writeEvent({ event: "elicitation_request", session_id: sessionId, request });
|
|
94
|
+
}
|
|
95
|
+
function buildConnectBridgeEvent(session, eventName) {
|
|
35
96
|
const historyUpdates = session.resumeUpdates;
|
|
36
|
-
|
|
97
|
+
return eventName === "session_replaced"
|
|
37
98
|
? {
|
|
38
99
|
event: "session_replaced",
|
|
39
100
|
session_id: session.sessionId,
|
|
40
101
|
cwd: session.cwd,
|
|
41
|
-
|
|
102
|
+
current_model: session.currentModel ?? resolveCurrentModel(session),
|
|
42
103
|
available_models: session.availableModels,
|
|
43
|
-
mode: session.mode ? buildModeState(session.mode) : null,
|
|
104
|
+
mode: session.mode ? buildModeState(session, session.mode) : null,
|
|
44
105
|
...(historyUpdates && historyUpdates.length > 0 ? { history_updates: historyUpdates } : {}),
|
|
45
106
|
}
|
|
46
107
|
: {
|
|
47
108
|
event: "connected",
|
|
48
109
|
session_id: session.sessionId,
|
|
49
110
|
cwd: session.cwd,
|
|
50
|
-
|
|
111
|
+
current_model: session.currentModel ?? resolveCurrentModel(session),
|
|
51
112
|
available_models: session.availableModels,
|
|
52
|
-
mode: session.mode ? buildModeState(session.mode) : null,
|
|
113
|
+
mode: session.mode ? buildModeState(session, session.mode) : null,
|
|
53
114
|
...(historyUpdates && historyUpdates.length > 0 ? { history_updates: historyUpdates } : {}),
|
|
54
115
|
};
|
|
55
|
-
|
|
116
|
+
}
|
|
117
|
+
function logConnectEventEmission(session, eventName, requestId) {
|
|
118
|
+
bridgeLogger.info({
|
|
119
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
120
|
+
eventName: eventName === "session_replaced" ? "session_replaced_emitted" : "session_connected_emitted",
|
|
121
|
+
message: eventName === "session_replaced" ? "session replaced event emitted" : "session connected event emitted",
|
|
122
|
+
outcome: "success",
|
|
123
|
+
...(requestId ? { requestId } : {}),
|
|
124
|
+
sessionId: session.sessionId,
|
|
125
|
+
fields: {
|
|
126
|
+
history_update_count: session.resumeUpdates?.length ?? 0,
|
|
127
|
+
available_model_count: session.availableModels.length,
|
|
128
|
+
stale_session_count: session.sessionsToCloseAfterConnect?.length ?? 0,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
export function emitConnectEvent(session) {
|
|
133
|
+
const bridgeEvent = buildConnectBridgeEvent(session, session.connectEvent);
|
|
134
|
+
logConnectEventEmission(session, session.connectEvent, session.connectRequestId);
|
|
135
|
+
writeEvent(bridgeEvent, session.connectRequestId);
|
|
56
136
|
session.connectRequestId = undefined;
|
|
57
137
|
session.connected = true;
|
|
58
138
|
session.authHintSent = false;
|
|
@@ -65,7 +145,7 @@ export function emitConnectEvent(session) {
|
|
|
65
145
|
}
|
|
66
146
|
void (async () => {
|
|
67
147
|
// Lazy import to break circular dependency at module-evaluation time.
|
|
68
|
-
const { sessions,
|
|
148
|
+
const { sessions, closeSessionWithLogging } = await import("./session_lifecycle.js");
|
|
69
149
|
for (const stale of staleSessions) {
|
|
70
150
|
if (stale === session) {
|
|
71
151
|
continue;
|
|
@@ -73,19 +153,54 @@ export function emitConnectEvent(session) {
|
|
|
73
153
|
if (sessions.get(stale.sessionId) === stale) {
|
|
74
154
|
sessions.delete(stale.sessionId);
|
|
75
155
|
}
|
|
76
|
-
await
|
|
156
|
+
await closeSessionWithLogging(stale, { reason: "stale_after_connect" });
|
|
77
157
|
}
|
|
78
158
|
refreshSessionsList();
|
|
79
159
|
})();
|
|
80
160
|
}
|
|
161
|
+
export function emitSessionReplacedEvent(session, requestId) {
|
|
162
|
+
const bridgeEvent = buildConnectBridgeEvent(session, "session_replaced");
|
|
163
|
+
logConnectEventEmission(session, "session_replaced", requestId);
|
|
164
|
+
writeEvent(bridgeEvent, requestId);
|
|
165
|
+
session.resumeUpdates = undefined;
|
|
166
|
+
refreshSessionsList();
|
|
167
|
+
}
|
|
81
168
|
export async function emitSessionsList(requestId) {
|
|
169
|
+
bridgeLogger.debug({
|
|
170
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
171
|
+
eventName: "sessions_list_requested",
|
|
172
|
+
message: "sessions list requested",
|
|
173
|
+
outcome: "start",
|
|
174
|
+
...(requestId ? { requestId } : {}),
|
|
175
|
+
fields: {
|
|
176
|
+
has_listing_dir: sessionListingDir !== undefined,
|
|
177
|
+
limit: SESSION_LIST_LIMIT,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
82
180
|
try {
|
|
83
181
|
const sdkSessions = await listSessions(currentSessionListOptions());
|
|
84
|
-
|
|
182
|
+
const sessions = mapSdkSessions(sdkSessions, SESSION_LIST_LIMIT);
|
|
183
|
+
bridgeLogger.info({
|
|
184
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
185
|
+
eventName: "sessions_list_completed",
|
|
186
|
+
message: "sessions list completed",
|
|
187
|
+
outcome: "success",
|
|
188
|
+
...(requestId ? { requestId } : {}),
|
|
189
|
+
count: sessions.length,
|
|
190
|
+
fields: { session_count: sessions.length },
|
|
191
|
+
});
|
|
192
|
+
writeEvent({ event: "sessions_listed", sessions }, requestId);
|
|
85
193
|
}
|
|
86
194
|
catch (error) {
|
|
87
195
|
const message = error instanceof Error ? error.message : String(error);
|
|
88
|
-
|
|
196
|
+
bridgeLogger.warn({
|
|
197
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
198
|
+
eventName: "sessions_list_failed",
|
|
199
|
+
message: "failed to list SDK sessions",
|
|
200
|
+
outcome: "failure",
|
|
201
|
+
...(requestId ? { requestId } : {}),
|
|
202
|
+
fields: { error_message: message },
|
|
203
|
+
});
|
|
89
204
|
writeEvent({ event: "sessions_listed", sessions: [] }, requestId);
|
|
90
205
|
}
|
|
91
206
|
}
|
|
@@ -29,14 +29,14 @@ function pushResumeTextChunk(updates, role, text) {
|
|
|
29
29
|
}
|
|
30
30
|
updates.push({ type: "user_message_chunk", content: { type: "text", text } });
|
|
31
31
|
}
|
|
32
|
-
function pushResumeToolUse(updates, toolCalls, block) {
|
|
32
|
+
function pushResumeToolUse(updates, toolCalls, block, parentToolUseId) {
|
|
33
33
|
const toolUseId = typeof block.id === "string" ? block.id : "";
|
|
34
34
|
if (!toolUseId) {
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
37
|
const name = typeof block.name === "string" ? block.name : "Tool";
|
|
38
38
|
const input = asRecordOrNull(block.input) ?? {};
|
|
39
|
-
const toolCall = createToolCall(toolUseId, name, input);
|
|
39
|
+
const toolCall = createToolCall(toolUseId, name, input, parentToolUseId);
|
|
40
40
|
toolCall.status = "in_progress";
|
|
41
41
|
toolCalls.set(toolUseId, toolCall);
|
|
42
42
|
updates.push({ type: "tool_call", tool_call: toolCall });
|
|
@@ -75,7 +75,7 @@ export function mapSdkSessionInfo(info) {
|
|
|
75
75
|
session_id: info.sessionId,
|
|
76
76
|
summary: summaryFromSession(info),
|
|
77
77
|
last_modified_ms: info.lastModified,
|
|
78
|
-
file_size_bytes: info.fileSize,
|
|
78
|
+
file_size_bytes: info.fileSize ?? 0,
|
|
79
79
|
...(nonEmptyTrimmed(info.cwd) ? { cwd: info.cwd?.trim() } : {}),
|
|
80
80
|
...(nonEmptyTrimmed(info.gitBranch) ? { git_branch: info.gitBranch?.trim() } : {}),
|
|
81
81
|
...(nonEmptyTrimmed(info.customTitle) ? { custom_title: info.customTitle?.trim() } : {}),
|
|
@@ -106,6 +106,11 @@ export function mapSessionMessagesToUpdates(messages) {
|
|
|
106
106
|
for (const message of messageCandidates(entry.message)) {
|
|
107
107
|
const roleCandidate = message.role;
|
|
108
108
|
const role = roleCandidate === "assistant" || roleCandidate === "user" ? roleCandidate : fallbackRole;
|
|
109
|
+
const parentToolUseId = typeof entry.parent_tool_use_id === "string"
|
|
110
|
+
? entry.parent_tool_use_id
|
|
111
|
+
: typeof message.parent_tool_use_id === "string"
|
|
112
|
+
? message.parent_tool_use_id
|
|
113
|
+
: null;
|
|
109
114
|
const content = Array.isArray(message.content) ? message.content : [];
|
|
110
115
|
for (const item of content) {
|
|
111
116
|
const block = asRecordOrNull(item);
|
|
@@ -121,7 +126,7 @@ export function mapSessionMessagesToUpdates(messages) {
|
|
|
121
126
|
continue;
|
|
122
127
|
}
|
|
123
128
|
if (isToolUseBlockType(blockType) && role === "assistant") {
|
|
124
|
-
pushResumeToolUse(updates, toolCalls, block);
|
|
129
|
+
pushResumeToolUse(updates, toolCalls, block, parentToolUseId);
|
|
125
130
|
continue;
|
|
126
131
|
}
|
|
127
132
|
if (TOOL_RESULT_TYPES.has(blockType)) {
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
const LOG_SCHEMA = "claude-rs-log/v1";
|
|
2
|
+
const DIAGNOSTICS_ENABLED = process.env.CLAUDE_RS_BRIDGE_DIAGNOSTICS === "1";
|
|
3
|
+
export const LOG_TARGETS = {
|
|
4
|
+
APP_AUTH: "app.auth",
|
|
5
|
+
APP_CACHE: "app.cache",
|
|
6
|
+
APP_CONFIG: "app.config",
|
|
7
|
+
APP_COMMAND: "app.command",
|
|
8
|
+
APP_NETWORK: "app.network",
|
|
9
|
+
APP_INPUT: "app.input",
|
|
10
|
+
APP_PASTE: "app.paste",
|
|
11
|
+
APP_PERF: "app.perf",
|
|
12
|
+
APP_RENDER: "app.render",
|
|
13
|
+
APP_SESSION: "app.session",
|
|
14
|
+
APP_TOOL: "app.tool",
|
|
15
|
+
APP_UPDATE: "app.update",
|
|
16
|
+
BRIDGE_LIFECYCLE: "bridge.lifecycle",
|
|
17
|
+
BRIDGE_MCP: "bridge.mcp",
|
|
18
|
+
BRIDGE_PERMISSION: "bridge.permission",
|
|
19
|
+
BRIDGE_PROTOCOL: "bridge.protocol",
|
|
20
|
+
BRIDGE_SDK: "bridge.sdk",
|
|
21
|
+
};
|
|
22
|
+
function definedFields(fields) {
|
|
23
|
+
if (!fields) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
const entries = Object.entries(fields).filter(([, value]) => value !== undefined);
|
|
27
|
+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
28
|
+
}
|
|
29
|
+
function writeDiagnostic(level, event) {
|
|
30
|
+
if (!DIAGNOSTICS_ENABLED) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const envelope = {
|
|
34
|
+
schema: LOG_SCHEMA,
|
|
35
|
+
timestamp: new Date().toISOString(),
|
|
36
|
+
level,
|
|
37
|
+
target: event.target,
|
|
38
|
+
event_name: event.eventName,
|
|
39
|
+
message: event.message,
|
|
40
|
+
...(event.outcome ? { outcome: event.outcome } : {}),
|
|
41
|
+
...(event.sessionId ? { session_id: event.sessionId } : {}),
|
|
42
|
+
...(event.requestId ? { request_id: event.requestId } : {}),
|
|
43
|
+
...(event.toolCallId ? { tool_call_id: event.toolCallId } : {}),
|
|
44
|
+
...(event.commandId ? { command_id: event.commandId } : {}),
|
|
45
|
+
...(event.terminalId ? { terminal_id: event.terminalId } : {}),
|
|
46
|
+
...(event.errorKind ? { error_kind: event.errorKind } : {}),
|
|
47
|
+
...(event.errorCode ? { error_code: event.errorCode } : {}),
|
|
48
|
+
...(event.durationMs !== undefined ? { duration_ms: event.durationMs } : {}),
|
|
49
|
+
...(event.count !== undefined ? { count: event.count } : {}),
|
|
50
|
+
...(event.sizeBytes !== undefined ? { size_bytes: event.sizeBytes } : {}),
|
|
51
|
+
};
|
|
52
|
+
const fields = definedFields(event.fields);
|
|
53
|
+
if (fields) {
|
|
54
|
+
envelope.fields = fields;
|
|
55
|
+
}
|
|
56
|
+
process.stderr.write(`${JSON.stringify(envelope)}\n`);
|
|
57
|
+
}
|
|
58
|
+
function previewText(value, limit) {
|
|
59
|
+
return value.length > limit ? `${value.slice(0, limit)}...` : value;
|
|
60
|
+
}
|
|
61
|
+
function commandSessionId(command) {
|
|
62
|
+
switch (command.command) {
|
|
63
|
+
case "resume_session":
|
|
64
|
+
case "prompt":
|
|
65
|
+
case "cancel_turn":
|
|
66
|
+
case "set_model":
|
|
67
|
+
case "set_mode":
|
|
68
|
+
case "generate_session_title":
|
|
69
|
+
case "rename_session":
|
|
70
|
+
case "permission_response":
|
|
71
|
+
case "question_response":
|
|
72
|
+
case "elicitation_response":
|
|
73
|
+
case "get_status_snapshot":
|
|
74
|
+
case "mcp_status":
|
|
75
|
+
case "mcp_reconnect":
|
|
76
|
+
case "mcp_toggle":
|
|
77
|
+
case "mcp_set_servers":
|
|
78
|
+
case "mcp_authenticate":
|
|
79
|
+
case "mcp_clear_auth":
|
|
80
|
+
case "mcp_oauth_callback_url":
|
|
81
|
+
return command.session_id;
|
|
82
|
+
case "create_session":
|
|
83
|
+
return command.resume;
|
|
84
|
+
case "initialize":
|
|
85
|
+
case "new_session":
|
|
86
|
+
case "shutdown":
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function commandToolCallId(command) {
|
|
91
|
+
switch (command.command) {
|
|
92
|
+
case "permission_response":
|
|
93
|
+
case "question_response":
|
|
94
|
+
return command.tool_call_id;
|
|
95
|
+
case "initialize":
|
|
96
|
+
case "create_session":
|
|
97
|
+
case "resume_session":
|
|
98
|
+
case "prompt":
|
|
99
|
+
case "cancel_turn":
|
|
100
|
+
case "set_model":
|
|
101
|
+
case "set_mode":
|
|
102
|
+
case "generate_session_title":
|
|
103
|
+
case "rename_session":
|
|
104
|
+
case "new_session":
|
|
105
|
+
case "elicitation_response":
|
|
106
|
+
case "get_status_snapshot":
|
|
107
|
+
case "get_context_usage":
|
|
108
|
+
case "reload_plugins":
|
|
109
|
+
case "mcp_status":
|
|
110
|
+
case "mcp_reconnect":
|
|
111
|
+
case "mcp_toggle":
|
|
112
|
+
case "mcp_set_servers":
|
|
113
|
+
case "mcp_authenticate":
|
|
114
|
+
case "mcp_clear_auth":
|
|
115
|
+
case "mcp_oauth_callback_url":
|
|
116
|
+
case "shutdown":
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function eventToolCallId(event) {
|
|
121
|
+
switch (event.event) {
|
|
122
|
+
case "permission_request":
|
|
123
|
+
case "question_request":
|
|
124
|
+
return event.request.tool_call.tool_call_id;
|
|
125
|
+
case "connected":
|
|
126
|
+
case "auth_required":
|
|
127
|
+
case "connection_failed":
|
|
128
|
+
case "session_update":
|
|
129
|
+
case "elicitation_request":
|
|
130
|
+
case "elicitation_complete":
|
|
131
|
+
case "mcp_auth_redirect":
|
|
132
|
+
case "mcp_operation_error":
|
|
133
|
+
case "turn_complete":
|
|
134
|
+
case "turn_error":
|
|
135
|
+
case "slash_error":
|
|
136
|
+
case "runtime_reload_completed":
|
|
137
|
+
case "runtime_reload_failed":
|
|
138
|
+
case "session_replaced":
|
|
139
|
+
case "initialized":
|
|
140
|
+
case "sessions_listed":
|
|
141
|
+
case "status_snapshot":
|
|
142
|
+
case "context_usage":
|
|
143
|
+
case "mcp_snapshot":
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function protocolCommandLevel(command) {
|
|
148
|
+
switch (command.command) {
|
|
149
|
+
case "initialize":
|
|
150
|
+
case "create_session":
|
|
151
|
+
case "resume_session":
|
|
152
|
+
case "new_session":
|
|
153
|
+
case "shutdown":
|
|
154
|
+
return "info";
|
|
155
|
+
default:
|
|
156
|
+
return "debug";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function protocolEventLevel(event) {
|
|
160
|
+
switch (event.event) {
|
|
161
|
+
case "initialized":
|
|
162
|
+
case "connected":
|
|
163
|
+
case "session_replaced":
|
|
164
|
+
return "info";
|
|
165
|
+
case "connection_failed":
|
|
166
|
+
return "error";
|
|
167
|
+
case "auth_required":
|
|
168
|
+
case "mcp_operation_error":
|
|
169
|
+
case "turn_error":
|
|
170
|
+
case "slash_error":
|
|
171
|
+
case "runtime_reload_failed":
|
|
172
|
+
return "warn";
|
|
173
|
+
case "session_update":
|
|
174
|
+
case "permission_request":
|
|
175
|
+
case "question_request":
|
|
176
|
+
case "elicitation_request":
|
|
177
|
+
case "elicitation_complete":
|
|
178
|
+
case "mcp_auth_redirect":
|
|
179
|
+
case "turn_complete":
|
|
180
|
+
return "trace";
|
|
181
|
+
case "sessions_listed":
|
|
182
|
+
case "status_snapshot":
|
|
183
|
+
case "context_usage":
|
|
184
|
+
case "runtime_reload_completed":
|
|
185
|
+
case "mcp_snapshot":
|
|
186
|
+
return "debug";
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
export const bridgeLogger = {
|
|
190
|
+
error(event) {
|
|
191
|
+
writeDiagnostic("error", event);
|
|
192
|
+
},
|
|
193
|
+
warn(event) {
|
|
194
|
+
writeDiagnostic("warn", event);
|
|
195
|
+
},
|
|
196
|
+
info(event) {
|
|
197
|
+
writeDiagnostic("info", event);
|
|
198
|
+
},
|
|
199
|
+
debug(event) {
|
|
200
|
+
writeDiagnostic("debug", event);
|
|
201
|
+
},
|
|
202
|
+
trace(event) {
|
|
203
|
+
writeDiagnostic("trace", event);
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
export function logSdkStderrLine(line, sessionId) {
|
|
207
|
+
const trimmed = line.trim();
|
|
208
|
+
if (trimmed.length === 0) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const lowered = trimmed.toLowerCase();
|
|
212
|
+
const level = lowered.startsWith("error") || lowered.includes("panic")
|
|
213
|
+
? "error"
|
|
214
|
+
: lowered.startsWith("warn")
|
|
215
|
+
? "warn"
|
|
216
|
+
: "debug";
|
|
217
|
+
writeDiagnostic(level, {
|
|
218
|
+
target: LOG_TARGETS.BRIDGE_SDK,
|
|
219
|
+
eventName: "sdk_stderr_line",
|
|
220
|
+
message: "SDK stderr line received",
|
|
221
|
+
...(sessionId ? { sessionId } : {}),
|
|
222
|
+
fields: {
|
|
223
|
+
preview: previewText(trimmed, 240),
|
|
224
|
+
preview_chars: Math.min(trimmed.length, 240),
|
|
225
|
+
line_chars: trimmed.length,
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
export function logBridgeCommandReceived(command, requestId) {
|
|
230
|
+
writeDiagnostic(protocolCommandLevel(command), {
|
|
231
|
+
target: LOG_TARGETS.BRIDGE_PROTOCOL,
|
|
232
|
+
eventName: "bridge_command_received",
|
|
233
|
+
message: "bridge command received",
|
|
234
|
+
outcome: "success",
|
|
235
|
+
...(requestId ? { requestId } : {}),
|
|
236
|
+
...(commandSessionId(command) ? { sessionId: commandSessionId(command) } : {}),
|
|
237
|
+
...(commandToolCallId(command) ? { toolCallId: commandToolCallId(command) } : {}),
|
|
238
|
+
fields: { bridge_command: command.command },
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
export function logBridgeEventSent(event, requestId, sizeBytes) {
|
|
242
|
+
writeDiagnostic(protocolEventLevel(event), {
|
|
243
|
+
target: LOG_TARGETS.BRIDGE_PROTOCOL,
|
|
244
|
+
eventName: "bridge_event_sent",
|
|
245
|
+
message: "bridge event sent",
|
|
246
|
+
outcome: event.event === "connection_failed" ? "failure" : "success",
|
|
247
|
+
...(requestId ? { requestId } : {}),
|
|
248
|
+
...("session_id" in event ? { sessionId: event.session_id } : {}),
|
|
249
|
+
...(eventToolCallId(event) ? { toolCallId: eventToolCallId(event) } : {}),
|
|
250
|
+
sizeBytes,
|
|
251
|
+
fields: { bridge_event: event.event },
|
|
252
|
+
});
|
|
253
|
+
}
|