claude-code-rust 0.9.0 → 0.10.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 +9 -3
- package/agent-sdk/dist/bridge/events.js +116 -8
- package/agent-sdk/dist/bridge/logger.js +245 -0
- package/agent-sdk/dist/bridge/mcp.js +81 -5
- package/agent-sdk/dist/bridge/message_handlers.js +87 -48
- package/agent-sdk/dist/bridge/session_lifecycle.js +385 -51
- package/agent-sdk/dist/bridge/shared.js +0 -7
- package/agent-sdk/dist/bridge/tool_calls.js +174 -59
- package/agent-sdk/dist/bridge/user_interaction.js +34 -23
- package/agent-sdk/dist/bridge.js +203 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,8 +68,14 @@ This project is pre-1.0 and under active development. See [CONTRIBUTING.md](CONT
|
|
|
68
68
|
This project is licensed under the [Apache License 2.0](LICENSE).
|
|
69
69
|
Apache-2.0 was chosen to keep usage and redistribution straightforward for individual users, downstream packagers, and commercial adopters.
|
|
70
70
|
|
|
71
|
-
## Disclaimer
|
|
71
|
+
## Disclaimer and Legal Notice
|
|
72
|
+
|
|
73
|
+
This project is not affiliated with, endorsed by, or supported by Anthropic.
|
|
74
|
+
|
|
75
|
+
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.
|
|
76
|
+
|
|
77
|
+
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.
|
|
78
|
+
|
|
79
|
+
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
80
|
|
|
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
81
|
For official Claude documentation, see [https://claude.ai/docs](https://claude.ai/docs).
|
|
@@ -1,6 +1,7 @@
|
|
|
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";
|
|
4
5
|
const SESSION_LIST_LIMIT = 50;
|
|
5
6
|
let sessionListingDir;
|
|
6
7
|
export function buildSessionListOptions(dir, limit = SESSION_LIST_LIMIT) {
|
|
@@ -17,7 +18,9 @@ export function writeEvent(event, requestId) {
|
|
|
17
18
|
...(requestId ? { request_id: requestId } : {}),
|
|
18
19
|
...event,
|
|
19
20
|
};
|
|
20
|
-
|
|
21
|
+
const serialized = JSON.stringify(envelope);
|
|
22
|
+
logBridgeEventSent(event, requestId, Buffer.byteLength(serialized) + 1);
|
|
23
|
+
process.stdout.write(`${serialized}\n`);
|
|
21
24
|
}
|
|
22
25
|
export function failConnection(message, requestId) {
|
|
23
26
|
writeEvent({ event: "connection_failed", message }, requestId);
|
|
@@ -31,9 +34,60 @@ export function emitMcpOperationError(sessionId, error, requestId) {
|
|
|
31
34
|
export function emitSessionUpdate(sessionId, update) {
|
|
32
35
|
writeEvent({ event: "session_update", session_id: sessionId, update });
|
|
33
36
|
}
|
|
34
|
-
export function
|
|
37
|
+
export function emitPermissionRequestEvent(sessionId, request) {
|
|
38
|
+
bridgeLogger.info({
|
|
39
|
+
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
40
|
+
eventName: "permission_request_emitted",
|
|
41
|
+
message: "permission request emitted",
|
|
42
|
+
outcome: "success",
|
|
43
|
+
sessionId,
|
|
44
|
+
toolCallId: request.tool_call.tool_call_id,
|
|
45
|
+
count: request.options.length,
|
|
46
|
+
fields: {
|
|
47
|
+
option_count: request.options.length,
|
|
48
|
+
tool_title: request.tool_call.title,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
writeEvent({ event: "permission_request", session_id: sessionId, request });
|
|
52
|
+
}
|
|
53
|
+
export function emitQuestionRequestEvent(sessionId, request) {
|
|
54
|
+
bridgeLogger.info({
|
|
55
|
+
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
56
|
+
eventName: "question_request_emitted",
|
|
57
|
+
message: "question request emitted",
|
|
58
|
+
outcome: "success",
|
|
59
|
+
sessionId,
|
|
60
|
+
toolCallId: request.tool_call.tool_call_id,
|
|
61
|
+
count: request.prompt.options.length,
|
|
62
|
+
fields: {
|
|
63
|
+
question_index: request.question_index,
|
|
64
|
+
total_questions: request.total_questions,
|
|
65
|
+
option_count: request.prompt.options.length,
|
|
66
|
+
header: request.prompt.header,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
writeEvent({ event: "question_request", session_id: sessionId, request });
|
|
70
|
+
}
|
|
71
|
+
export function emitElicitationRequestEvent(sessionId, request) {
|
|
72
|
+
bridgeLogger.info({
|
|
73
|
+
target: LOG_TARGETS.BRIDGE_PERMISSION,
|
|
74
|
+
eventName: "elicitation_request_emitted",
|
|
75
|
+
message: "elicitation request emitted",
|
|
76
|
+
outcome: "success",
|
|
77
|
+
sessionId,
|
|
78
|
+
requestId: request.request_id,
|
|
79
|
+
fields: {
|
|
80
|
+
server_name: request.server_name,
|
|
81
|
+
mode: request.mode,
|
|
82
|
+
has_url: request.url !== undefined,
|
|
83
|
+
has_requested_schema: request.requested_schema !== undefined,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
writeEvent({ event: "elicitation_request", session_id: sessionId, request });
|
|
87
|
+
}
|
|
88
|
+
function buildConnectBridgeEvent(session, eventName) {
|
|
35
89
|
const historyUpdates = session.resumeUpdates;
|
|
36
|
-
|
|
90
|
+
return eventName === "session_replaced"
|
|
37
91
|
? {
|
|
38
92
|
event: "session_replaced",
|
|
39
93
|
session_id: session.sessionId,
|
|
@@ -52,7 +106,26 @@ export function emitConnectEvent(session) {
|
|
|
52
106
|
mode: session.mode ? buildModeState(session.mode) : null,
|
|
53
107
|
...(historyUpdates && historyUpdates.length > 0 ? { history_updates: historyUpdates } : {}),
|
|
54
108
|
};
|
|
55
|
-
|
|
109
|
+
}
|
|
110
|
+
function logConnectEventEmission(session, eventName, requestId) {
|
|
111
|
+
bridgeLogger.info({
|
|
112
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
113
|
+
eventName: eventName === "session_replaced" ? "session_replaced_emitted" : "session_connected_emitted",
|
|
114
|
+
message: eventName === "session_replaced" ? "session replaced event emitted" : "session connected event emitted",
|
|
115
|
+
outcome: "success",
|
|
116
|
+
...(requestId ? { requestId } : {}),
|
|
117
|
+
sessionId: session.sessionId,
|
|
118
|
+
fields: {
|
|
119
|
+
history_update_count: session.resumeUpdates?.length ?? 0,
|
|
120
|
+
available_model_count: session.availableModels.length,
|
|
121
|
+
stale_session_count: session.sessionsToCloseAfterConnect?.length ?? 0,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
export function emitConnectEvent(session) {
|
|
126
|
+
const bridgeEvent = buildConnectBridgeEvent(session, session.connectEvent);
|
|
127
|
+
logConnectEventEmission(session, session.connectEvent, session.connectRequestId);
|
|
128
|
+
writeEvent(bridgeEvent, session.connectRequestId);
|
|
56
129
|
session.connectRequestId = undefined;
|
|
57
130
|
session.connected = true;
|
|
58
131
|
session.authHintSent = false;
|
|
@@ -65,7 +138,7 @@ export function emitConnectEvent(session) {
|
|
|
65
138
|
}
|
|
66
139
|
void (async () => {
|
|
67
140
|
// Lazy import to break circular dependency at module-evaluation time.
|
|
68
|
-
const { sessions,
|
|
141
|
+
const { sessions, closeSessionWithLogging } = await import("./session_lifecycle.js");
|
|
69
142
|
for (const stale of staleSessions) {
|
|
70
143
|
if (stale === session) {
|
|
71
144
|
continue;
|
|
@@ -73,19 +146,54 @@ export function emitConnectEvent(session) {
|
|
|
73
146
|
if (sessions.get(stale.sessionId) === stale) {
|
|
74
147
|
sessions.delete(stale.sessionId);
|
|
75
148
|
}
|
|
76
|
-
await
|
|
149
|
+
await closeSessionWithLogging(stale, { reason: "stale_after_connect" });
|
|
77
150
|
}
|
|
78
151
|
refreshSessionsList();
|
|
79
152
|
})();
|
|
80
153
|
}
|
|
154
|
+
export function emitSessionReplacedEvent(session, requestId) {
|
|
155
|
+
const bridgeEvent = buildConnectBridgeEvent(session, "session_replaced");
|
|
156
|
+
logConnectEventEmission(session, "session_replaced", requestId);
|
|
157
|
+
writeEvent(bridgeEvent, requestId);
|
|
158
|
+
session.resumeUpdates = undefined;
|
|
159
|
+
refreshSessionsList();
|
|
160
|
+
}
|
|
81
161
|
export async function emitSessionsList(requestId) {
|
|
162
|
+
bridgeLogger.debug({
|
|
163
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
164
|
+
eventName: "sessions_list_requested",
|
|
165
|
+
message: "sessions list requested",
|
|
166
|
+
outcome: "start",
|
|
167
|
+
...(requestId ? { requestId } : {}),
|
|
168
|
+
fields: {
|
|
169
|
+
has_listing_dir: sessionListingDir !== undefined,
|
|
170
|
+
limit: SESSION_LIST_LIMIT,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
82
173
|
try {
|
|
83
174
|
const sdkSessions = await listSessions(currentSessionListOptions());
|
|
84
|
-
|
|
175
|
+
const sessions = mapSdkSessions(sdkSessions, SESSION_LIST_LIMIT);
|
|
176
|
+
bridgeLogger.info({
|
|
177
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
178
|
+
eventName: "sessions_list_completed",
|
|
179
|
+
message: "sessions list completed",
|
|
180
|
+
outcome: "success",
|
|
181
|
+
...(requestId ? { requestId } : {}),
|
|
182
|
+
count: sessions.length,
|
|
183
|
+
fields: { session_count: sessions.length },
|
|
184
|
+
});
|
|
185
|
+
writeEvent({ event: "sessions_listed", sessions }, requestId);
|
|
85
186
|
}
|
|
86
187
|
catch (error) {
|
|
87
188
|
const message = error instanceof Error ? error.message : String(error);
|
|
88
|
-
|
|
189
|
+
bridgeLogger.warn({
|
|
190
|
+
target: LOG_TARGETS.APP_SESSION,
|
|
191
|
+
eventName: "sessions_list_failed",
|
|
192
|
+
message: "failed to list SDK sessions",
|
|
193
|
+
outcome: "failure",
|
|
194
|
+
...(requestId ? { requestId } : {}),
|
|
195
|
+
fields: { error_message: message },
|
|
196
|
+
});
|
|
89
197
|
writeEvent({ event: "sessions_listed", sessions: [] }, requestId);
|
|
90
198
|
}
|
|
91
199
|
}
|
|
@@ -0,0 +1,245 @@
|
|
|
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 "mcp_status":
|
|
108
|
+
case "mcp_reconnect":
|
|
109
|
+
case "mcp_toggle":
|
|
110
|
+
case "mcp_set_servers":
|
|
111
|
+
case "mcp_authenticate":
|
|
112
|
+
case "mcp_clear_auth":
|
|
113
|
+
case "mcp_oauth_callback_url":
|
|
114
|
+
case "shutdown":
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function eventToolCallId(event) {
|
|
119
|
+
switch (event.event) {
|
|
120
|
+
case "permission_request":
|
|
121
|
+
case "question_request":
|
|
122
|
+
return event.request.tool_call.tool_call_id;
|
|
123
|
+
case "connected":
|
|
124
|
+
case "auth_required":
|
|
125
|
+
case "connection_failed":
|
|
126
|
+
case "session_update":
|
|
127
|
+
case "elicitation_request":
|
|
128
|
+
case "elicitation_complete":
|
|
129
|
+
case "mcp_auth_redirect":
|
|
130
|
+
case "mcp_operation_error":
|
|
131
|
+
case "turn_complete":
|
|
132
|
+
case "turn_error":
|
|
133
|
+
case "slash_error":
|
|
134
|
+
case "session_replaced":
|
|
135
|
+
case "initialized":
|
|
136
|
+
case "sessions_listed":
|
|
137
|
+
case "status_snapshot":
|
|
138
|
+
case "mcp_snapshot":
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function protocolCommandLevel(command) {
|
|
143
|
+
switch (command.command) {
|
|
144
|
+
case "initialize":
|
|
145
|
+
case "create_session":
|
|
146
|
+
case "resume_session":
|
|
147
|
+
case "new_session":
|
|
148
|
+
case "shutdown":
|
|
149
|
+
return "info";
|
|
150
|
+
default:
|
|
151
|
+
return "debug";
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function protocolEventLevel(event) {
|
|
155
|
+
switch (event.event) {
|
|
156
|
+
case "initialized":
|
|
157
|
+
case "connected":
|
|
158
|
+
case "session_replaced":
|
|
159
|
+
return "info";
|
|
160
|
+
case "connection_failed":
|
|
161
|
+
return "error";
|
|
162
|
+
case "auth_required":
|
|
163
|
+
case "mcp_operation_error":
|
|
164
|
+
case "turn_error":
|
|
165
|
+
case "slash_error":
|
|
166
|
+
return "warn";
|
|
167
|
+
case "session_update":
|
|
168
|
+
case "permission_request":
|
|
169
|
+
case "question_request":
|
|
170
|
+
case "elicitation_request":
|
|
171
|
+
case "elicitation_complete":
|
|
172
|
+
case "mcp_auth_redirect":
|
|
173
|
+
case "turn_complete":
|
|
174
|
+
return "trace";
|
|
175
|
+
case "sessions_listed":
|
|
176
|
+
case "status_snapshot":
|
|
177
|
+
case "mcp_snapshot":
|
|
178
|
+
return "debug";
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
export const bridgeLogger = {
|
|
182
|
+
error(event) {
|
|
183
|
+
writeDiagnostic("error", event);
|
|
184
|
+
},
|
|
185
|
+
warn(event) {
|
|
186
|
+
writeDiagnostic("warn", event);
|
|
187
|
+
},
|
|
188
|
+
info(event) {
|
|
189
|
+
writeDiagnostic("info", event);
|
|
190
|
+
},
|
|
191
|
+
debug(event) {
|
|
192
|
+
writeDiagnostic("debug", event);
|
|
193
|
+
},
|
|
194
|
+
trace(event) {
|
|
195
|
+
writeDiagnostic("trace", event);
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
export function logSdkStderrLine(line, sessionId) {
|
|
199
|
+
const trimmed = line.trim();
|
|
200
|
+
if (trimmed.length === 0) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const lowered = trimmed.toLowerCase();
|
|
204
|
+
const level = lowered.startsWith("error") || lowered.includes("panic")
|
|
205
|
+
? "error"
|
|
206
|
+
: lowered.startsWith("warn")
|
|
207
|
+
? "warn"
|
|
208
|
+
: "debug";
|
|
209
|
+
writeDiagnostic(level, {
|
|
210
|
+
target: LOG_TARGETS.BRIDGE_SDK,
|
|
211
|
+
eventName: "sdk_stderr_line",
|
|
212
|
+
message: "SDK stderr line received",
|
|
213
|
+
...(sessionId ? { sessionId } : {}),
|
|
214
|
+
fields: {
|
|
215
|
+
preview: previewText(trimmed, 240),
|
|
216
|
+
preview_chars: Math.min(trimmed.length, 240),
|
|
217
|
+
line_chars: trimmed.length,
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
export function logBridgeCommandReceived(command, requestId) {
|
|
222
|
+
writeDiagnostic(protocolCommandLevel(command), {
|
|
223
|
+
target: LOG_TARGETS.BRIDGE_PROTOCOL,
|
|
224
|
+
eventName: "bridge_command_received",
|
|
225
|
+
message: "bridge command received",
|
|
226
|
+
outcome: "success",
|
|
227
|
+
...(requestId ? { requestId } : {}),
|
|
228
|
+
...(commandSessionId(command) ? { sessionId: commandSessionId(command) } : {}),
|
|
229
|
+
...(commandToolCallId(command) ? { toolCallId: commandToolCallId(command) } : {}),
|
|
230
|
+
fields: { bridge_command: command.command },
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
export function logBridgeEventSent(event, requestId, sizeBytes) {
|
|
234
|
+
writeDiagnostic(protocolEventLevel(event), {
|
|
235
|
+
target: LOG_TARGETS.BRIDGE_PROTOCOL,
|
|
236
|
+
eventName: "bridge_event_sent",
|
|
237
|
+
message: "bridge event sent",
|
|
238
|
+
outcome: event.event === "connection_failed" ? "failure" : "success",
|
|
239
|
+
...(requestId ? { requestId } : {}),
|
|
240
|
+
...("session_id" in event ? { sessionId: event.session_id } : {}),
|
|
241
|
+
...(eventToolCallId(event) ? { toolCallId: eventToolCallId(event) } : {}),
|
|
242
|
+
sizeBytes,
|
|
243
|
+
fields: { bridge_event: event.event },
|
|
244
|
+
});
|
|
245
|
+
}
|
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
import { emitMcpOperationError, slashError, writeEvent } from "./events.js";
|
|
2
|
+
import { bridgeLogger, LOG_TARGETS } from "./logger.js";
|
|
2
3
|
export const MCP_STALE_STATUS_REVALIDATION_COOLDOWN_MS = 30_000;
|
|
3
4
|
const knownConnectedMcpServers = new Set();
|
|
5
|
+
function logMcpSuccess(eventName, message, sessionId, requestId, fields) {
|
|
6
|
+
bridgeLogger.info({
|
|
7
|
+
target: LOG_TARGETS.BRIDGE_MCP,
|
|
8
|
+
eventName,
|
|
9
|
+
message,
|
|
10
|
+
outcome: "success",
|
|
11
|
+
sessionId,
|
|
12
|
+
...(requestId ? { requestId } : {}),
|
|
13
|
+
...(fields ? { fields } : {}),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function logMcpFailure(eventName, message, sessionId, errorMessage, requestId, fields) {
|
|
17
|
+
bridgeLogger.warn({
|
|
18
|
+
target: LOG_TARGETS.BRIDGE_MCP,
|
|
19
|
+
eventName,
|
|
20
|
+
message,
|
|
21
|
+
outcome: "failure",
|
|
22
|
+
sessionId,
|
|
23
|
+
...(requestId ? { requestId } : {}),
|
|
24
|
+
fields: {
|
|
25
|
+
...(fields ?? {}),
|
|
26
|
+
error_message: errorMessage,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
4
30
|
function queryWithMcpAuth(session) {
|
|
5
31
|
return session.query;
|
|
6
32
|
}
|
|
@@ -51,6 +77,9 @@ export async function emitMcpSnapshotEvent(session, requestId) {
|
|
|
51
77
|
let mapped = servers.map(mapMcpServerStatus);
|
|
52
78
|
mapped = await reconcileSuspiciousMcpStatuses(session, mapped);
|
|
53
79
|
rememberKnownConnectedMcpServers(mapped);
|
|
80
|
+
logMcpSuccess("mcp_snapshot_emitted", "MCP snapshot emitted", session.sessionId, requestId, {
|
|
81
|
+
server_count: mapped.length,
|
|
82
|
+
});
|
|
54
83
|
writeEvent({
|
|
55
84
|
event: "mcp_snapshot",
|
|
56
85
|
session_id: session.sessionId,
|
|
@@ -90,15 +119,36 @@ async function reconcileSuspiciousMcpStatuses(session, servers) {
|
|
|
90
119
|
const now = Date.now();
|
|
91
120
|
for (const serverName of candidates) {
|
|
92
121
|
session.mcpStatusRevalidatedAt.set(serverName, now);
|
|
93
|
-
|
|
94
|
-
|
|
122
|
+
bridgeLogger.info({
|
|
123
|
+
target: LOG_TARGETS.BRIDGE_MCP,
|
|
124
|
+
eventName: "mcp_auth_revalidation_started",
|
|
125
|
+
message: "revalidating stale MCP auth status",
|
|
126
|
+
outcome: "start",
|
|
127
|
+
sessionId: session.sessionId,
|
|
128
|
+
fields: {
|
|
129
|
+
server_name: serverName,
|
|
130
|
+
status: "needs-auth",
|
|
131
|
+
reason: "previously_connected",
|
|
132
|
+
action: "reconnect",
|
|
133
|
+
},
|
|
134
|
+
});
|
|
95
135
|
try {
|
|
96
136
|
await session.query.reconnectMcpServer(serverName);
|
|
97
137
|
}
|
|
98
138
|
catch (error) {
|
|
99
139
|
const message = error instanceof Error ? error.message : String(error);
|
|
100
|
-
|
|
101
|
-
|
|
140
|
+
bridgeLogger.warn({
|
|
141
|
+
target: LOG_TARGETS.BRIDGE_MCP,
|
|
142
|
+
eventName: "mcp_auth_revalidation_failed",
|
|
143
|
+
message: "failed to revalidate MCP auth status",
|
|
144
|
+
outcome: "failure",
|
|
145
|
+
sessionId: session.sessionId,
|
|
146
|
+
fields: {
|
|
147
|
+
server_name: serverName,
|
|
148
|
+
action: "reconnect",
|
|
149
|
+
error_message: message,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
102
152
|
}
|
|
103
153
|
}
|
|
104
154
|
return (await session.query.mcpServerStatus()).map(mapMcpServerStatus);
|
|
@@ -137,6 +187,7 @@ export async function handleMcpStatusCommand(session, requestId) {
|
|
|
137
187
|
}
|
|
138
188
|
catch (error) {
|
|
139
189
|
const message = error instanceof Error ? error.message : String(error);
|
|
190
|
+
logMcpFailure("mcp_snapshot_failed", "failed to emit MCP snapshot", session.sessionId, message, requestId);
|
|
140
191
|
writeEvent({
|
|
141
192
|
event: "mcp_snapshot",
|
|
142
193
|
session_id: session.sessionId,
|
|
@@ -148,27 +199,35 @@ export async function handleMcpStatusCommand(session, requestId) {
|
|
|
148
199
|
export async function handleMcpReconnectCommand(session, command, requestId) {
|
|
149
200
|
try {
|
|
150
201
|
await session.query.reconnectMcpServer(command.server_name);
|
|
202
|
+
logMcpSuccess("mcp_reconnect_completed", "MCP reconnect completed", command.session_id, requestId, {
|
|
203
|
+
server_name: command.server_name,
|
|
204
|
+
});
|
|
151
205
|
}
|
|
152
206
|
catch (error) {
|
|
153
207
|
const message = error instanceof Error ? error.message : String(error);
|
|
208
|
+
logMcpFailure("mcp_reconnect_failed", "MCP reconnect failed", command.session_id, message, requestId, { server_name: command.server_name });
|
|
154
209
|
emitMcpCommandError(command.session_id, "reconnect", message, requestId, command.server_name);
|
|
155
210
|
}
|
|
156
211
|
}
|
|
157
212
|
export async function handleMcpToggleCommand(session, command, requestId) {
|
|
158
213
|
try {
|
|
159
214
|
await session.query.toggleMcpServer(command.server_name, command.enabled);
|
|
215
|
+
logMcpSuccess("mcp_toggle_completed", "MCP server toggle completed", command.session_id, requestId, { server_name: command.server_name, enabled: command.enabled });
|
|
160
216
|
}
|
|
161
217
|
catch (error) {
|
|
162
218
|
const message = error instanceof Error ? error.message : String(error);
|
|
219
|
+
logMcpFailure("mcp_toggle_failed", "MCP server toggle failed", command.session_id, message, requestId, { server_name: command.server_name, enabled: command.enabled });
|
|
163
220
|
emitMcpCommandError(command.session_id, "toggle", message, requestId, command.server_name);
|
|
164
221
|
}
|
|
165
222
|
}
|
|
166
223
|
export async function handleMcpSetServersCommand(session, command, requestId) {
|
|
167
224
|
try {
|
|
168
225
|
await session.query.setMcpServers(command.servers);
|
|
226
|
+
logMcpSuccess("mcp_servers_set_completed", "MCP server configuration updated", command.session_id, requestId, { server_count: Object.keys(command.servers).length });
|
|
169
227
|
}
|
|
170
228
|
catch (error) {
|
|
171
229
|
const message = error instanceof Error ? error.message : String(error);
|
|
230
|
+
logMcpFailure("mcp_servers_set_failed", "failed to update MCP server configuration", command.session_id, message, requestId, { server_count: Object.keys(command.servers).length });
|
|
172
231
|
slashError(command.session_id, `failed to set MCP servers: ${message}`, requestId);
|
|
173
232
|
}
|
|
174
233
|
}
|
|
@@ -177,16 +236,21 @@ export async function handleMcpAuthenticateCommand(session, command, requestId)
|
|
|
177
236
|
const result = await callMcpAuthMethod(session, "mcpAuthenticate", [command.server_name]);
|
|
178
237
|
const redirect = extractMcpAuthRedirect(command.server_name, result);
|
|
179
238
|
if (redirect) {
|
|
239
|
+
logMcpSuccess("mcp_auth_redirect_emitted", "MCP auth redirect emitted", command.session_id, requestId, { server_name: command.server_name, requires_user_action: redirect.requires_user_action });
|
|
180
240
|
writeEvent({
|
|
181
241
|
event: "mcp_auth_redirect",
|
|
182
242
|
session_id: command.session_id,
|
|
183
243
|
redirect,
|
|
184
|
-
});
|
|
244
|
+
}, requestId);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
logMcpSuccess("mcp_authenticate_completed", "MCP authentication command completed", command.session_id, requestId, { server_name: command.server_name });
|
|
185
248
|
}
|
|
186
249
|
scheduleMcpAuthSnapshotMonitor(session, command.server_name);
|
|
187
250
|
}
|
|
188
251
|
catch (error) {
|
|
189
252
|
const message = error instanceof Error ? error.message : String(error);
|
|
253
|
+
logMcpFailure("mcp_authenticate_failed", "MCP authentication command failed", command.session_id, message, requestId, { server_name: command.server_name });
|
|
190
254
|
emitMcpCommandError(command.session_id, "authenticate", message, requestId, command.server_name);
|
|
191
255
|
}
|
|
192
256
|
}
|
|
@@ -195,9 +259,13 @@ export async function handleMcpClearAuthCommand(session, command, requestId) {
|
|
|
195
259
|
await callMcpAuthMethod(session, "mcpClearAuth", [command.server_name]);
|
|
196
260
|
forgetKnownConnectedMcpServer(command.server_name);
|
|
197
261
|
session.mcpStatusRevalidatedAt.delete(command.server_name);
|
|
262
|
+
logMcpSuccess("mcp_clear_auth_completed", "MCP auth cleared", command.session_id, requestId, {
|
|
263
|
+
server_name: command.server_name,
|
|
264
|
+
});
|
|
198
265
|
}
|
|
199
266
|
catch (error) {
|
|
200
267
|
const message = error instanceof Error ? error.message : String(error);
|
|
268
|
+
logMcpFailure("mcp_clear_auth_failed", "failed to clear MCP auth", command.session_id, message, requestId, { server_name: command.server_name });
|
|
201
269
|
emitMcpCommandError(command.session_id, "clear-auth", message, requestId, command.server_name);
|
|
202
270
|
}
|
|
203
271
|
}
|
|
@@ -207,9 +275,17 @@ export async function handleMcpOauthCallbackUrlCommand(session, command, request
|
|
|
207
275
|
command.server_name,
|
|
208
276
|
command.callback_url,
|
|
209
277
|
]);
|
|
278
|
+
logMcpSuccess("mcp_oauth_callback_completed", "MCP OAuth callback URL submitted", command.session_id, requestId, {
|
|
279
|
+
server_name: command.server_name,
|
|
280
|
+
callback_url_chars: command.callback_url.length,
|
|
281
|
+
});
|
|
210
282
|
}
|
|
211
283
|
catch (error) {
|
|
212
284
|
const message = error instanceof Error ? error.message : String(error);
|
|
285
|
+
logMcpFailure("mcp_oauth_callback_failed", "failed to submit MCP OAuth callback URL", command.session_id, message, requestId, {
|
|
286
|
+
server_name: command.server_name,
|
|
287
|
+
callback_url_chars: command.callback_url.length,
|
|
288
|
+
});
|
|
213
289
|
emitMcpCommandError(command.session_id, "submit-callback-url", message, requestId, command.server_name);
|
|
214
290
|
}
|
|
215
291
|
}
|