heyhank 0.1.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 +40 -0
- package/bin/cli.ts +168 -0
- package/bin/ctl.ts +528 -0
- package/bin/generate-token.ts +28 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/AgentsPage-BPhirnCe.js +7 -0
- package/dist/assets/AssistantPage-DJ-cMQfb.js +1 -0
- package/dist/assets/CronManager-DDbz-yiT.js +1 -0
- package/dist/assets/HelpPage-DMfkzERp.js +1 -0
- package/dist/assets/IntegrationsPage-CrOitCmJ.js +1 -0
- package/dist/assets/MediaPage-CE5rdvkC.js +1 -0
- package/dist/assets/PlatformDashboard-Do6F0O2p.js +1 -0
- package/dist/assets/Playground-Fc5cdc5p.js +109 -0
- package/dist/assets/ProcessPanel-CslEiZkI.js +2 -0
- package/dist/assets/PromptsPage-D2EhsdNO.js +4 -0
- package/dist/assets/RunsPage-C5BZF5Rx.js +1 -0
- package/dist/assets/SandboxManager-a1AVI5q2.js +8 -0
- package/dist/assets/SettingsPage-DirhjQrJ.js +51 -0
- package/dist/assets/SocialMediaPage-DBuM28vD.js +1 -0
- package/dist/assets/TailscalePage-CHiFhZXF.js +1 -0
- package/dist/assets/TelephonyPage-x0VV0fOo.js +1 -0
- package/dist/assets/TerminalPage-Drwyrnfd.js +1 -0
- package/dist/assets/gemini-audio-t-TSU-To.js +17 -0
- package/dist/assets/gemini-live-client-C7rqAW7G.js +166 -0
- package/dist/assets/index-C8M_PUmX.css +32 -0
- package/dist/assets/index-CEqZnThB.js +204 -0
- package/dist/assets/sw-register-LSSpj6RU.js +1 -0
- package/dist/assets/time-ago-B6r_l9u1.js +1 -0
- package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +2 -0
- package/dist/favicon-32-original.png +0 -0
- package/dist/favicon-32.png +0 -0
- package/dist/favicon.ico +0 -0
- package/dist/favicon.svg +8 -0
- package/dist/fonts/MesloLGSNerdFontMono-Bold.woff2 +0 -0
- package/dist/fonts/MesloLGSNerdFontMono-Regular.woff2 +0 -0
- package/dist/heyhank-mascot-poster.png +0 -0
- package/dist/heyhank-mascot.mp4 +0 -0
- package/dist/heyhank-mascot.webm +0 -0
- package/dist/icon-192-original.png +0 -0
- package/dist/icon-192.png +0 -0
- package/dist/icon-512-original.png +0 -0
- package/dist/icon-512.png +0 -0
- package/dist/index.html +21 -0
- package/dist/logo-192.png +0 -0
- package/dist/logo-512.png +0 -0
- package/dist/logo-codex.svg +14 -0
- package/dist/logo-docker.svg +4 -0
- package/dist/logo-original.png +0 -0
- package/dist/logo.png +0 -0
- package/dist/logo.svg +14 -0
- package/dist/manifest.json +24 -0
- package/dist/push-sw.js +34 -0
- package/dist/sw.js +1 -0
- package/dist/workbox-d2a0910a.js +1 -0
- package/package.json +109 -0
- package/server/agent-cron-migrator.ts +85 -0
- package/server/agent-executor.ts +357 -0
- package/server/agent-store.ts +185 -0
- package/server/agent-timeout.ts +107 -0
- package/server/agent-types.ts +122 -0
- package/server/ai-validation-settings.ts +37 -0
- package/server/ai-validator.ts +181 -0
- package/server/anthropic-provider-migration.ts +48 -0
- package/server/assistant-store.ts +272 -0
- package/server/auth-manager.ts +150 -0
- package/server/auto-approve.ts +153 -0
- package/server/auto-namer.ts +36 -0
- package/server/backend-adapter.ts +54 -0
- package/server/cache-headers.ts +61 -0
- package/server/calendar-service.ts +434 -0
- package/server/claude-adapter.ts +889 -0
- package/server/claude-container-auth.ts +30 -0
- package/server/claude-session-discovery.ts +157 -0
- package/server/claude-session-history.ts +410 -0
- package/server/cli-launcher.ts +1303 -0
- package/server/codex-adapter.ts +3027 -0
- package/server/codex-container-auth.ts +24 -0
- package/server/codex-home.ts +27 -0
- package/server/codex-ws-proxy.cjs +226 -0
- package/server/commands-discovery.ts +81 -0
- package/server/constants.ts +7 -0
- package/server/container-manager.ts +1053 -0
- package/server/cost-tracker.ts +222 -0
- package/server/cron-scheduler.ts +243 -0
- package/server/cron-store.ts +148 -0
- package/server/cron-types.ts +63 -0
- package/server/email-service.ts +354 -0
- package/server/env-manager.ts +161 -0
- package/server/event-bus-types.ts +75 -0
- package/server/event-bus.ts +124 -0
- package/server/execution-store.ts +170 -0
- package/server/federation/node-connection.ts +190 -0
- package/server/federation/node-manager.ts +366 -0
- package/server/federation/node-store.ts +86 -0
- package/server/federation/node-types.ts +121 -0
- package/server/fs-utils.ts +15 -0
- package/server/git-utils.ts +421 -0
- package/server/github-pr.ts +379 -0
- package/server/google-media.ts +342 -0
- package/server/image-pull-manager.ts +279 -0
- package/server/index.ts +491 -0
- package/server/internal-ai.ts +237 -0
- package/server/kill-switch.ts +99 -0
- package/server/llm-providers.ts +342 -0
- package/server/logger.ts +259 -0
- package/server/mcp-registry.ts +401 -0
- package/server/message-bus.ts +271 -0
- package/server/message-delivery.ts +128 -0
- package/server/metrics-collector.ts +350 -0
- package/server/metrics-types.ts +108 -0
- package/server/middleware/managed-auth.ts +195 -0
- package/server/novnc-proxy.ts +99 -0
- package/server/path-resolver.ts +186 -0
- package/server/paths.ts +13 -0
- package/server/pr-poller.ts +162 -0
- package/server/prompt-manager.ts +211 -0
- package/server/protocol/claude-upstream/README.md +19 -0
- package/server/protocol/claude-upstream/sdk.d.ts.txt +1943 -0
- package/server/protocol/codex-upstream/ClientNotification.ts.txt +5 -0
- package/server/protocol/codex-upstream/ClientRequest.ts.txt +60 -0
- package/server/protocol/codex-upstream/README.md +18 -0
- package/server/protocol/codex-upstream/ServerNotification.ts.txt +41 -0
- package/server/protocol/codex-upstream/ServerRequest.ts.txt +16 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallParams.ts.txt +6 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallResponse.ts.txt +6 -0
- package/server/protocol-monitor.ts +50 -0
- package/server/provider-manager.ts +111 -0
- package/server/provider-registry.ts +393 -0
- package/server/push-notifications.ts +221 -0
- package/server/recorder.ts +374 -0
- package/server/recording-hub/compat-validator.ts +284 -0
- package/server/recording-hub/diagnostics.ts +299 -0
- package/server/recording-hub/hub-config.ts +19 -0
- package/server/recording-hub/hub-routes.ts +236 -0
- package/server/recording-hub/hub-store.ts +265 -0
- package/server/recording-hub/replay-adapter.ts +207 -0
- package/server/relay-client.ts +320 -0
- package/server/reminder-scheduler.ts +38 -0
- package/server/replay.ts +78 -0
- package/server/routes/agent-routes.ts +264 -0
- package/server/routes/assistant-routes.ts +90 -0
- package/server/routes/cron-routes.ts +103 -0
- package/server/routes/env-routes.ts +95 -0
- package/server/routes/federation-routes.ts +76 -0
- package/server/routes/fs-routes.ts +622 -0
- package/server/routes/git-routes.ts +97 -0
- package/server/routes/llm-routes.ts +166 -0
- package/server/routes/media-routes.ts +135 -0
- package/server/routes/metrics-routes.ts +13 -0
- package/server/routes/platform-routes.ts +1379 -0
- package/server/routes/prompt-routes.ts +67 -0
- package/server/routes/provider-routes.ts +109 -0
- package/server/routes/sandbox-routes.ts +127 -0
- package/server/routes/settings-routes.ts +285 -0
- package/server/routes/skills-routes.ts +100 -0
- package/server/routes/socialmedia-routes.ts +208 -0
- package/server/routes/system-routes.ts +228 -0
- package/server/routes/tailscale-routes.ts +22 -0
- package/server/routes/telephony-routes.ts +259 -0
- package/server/routes.ts +1379 -0
- package/server/sandbox-manager.ts +168 -0
- package/server/service.ts +718 -0
- package/server/session-creation-service.ts +457 -0
- package/server/session-git-info.ts +104 -0
- package/server/session-names.ts +67 -0
- package/server/session-orchestrator.ts +824 -0
- package/server/session-state-machine.ts +207 -0
- package/server/session-store.ts +146 -0
- package/server/session-types.ts +511 -0
- package/server/settings-manager.ts +149 -0
- package/server/shared-context.ts +157 -0
- package/server/socialmedia/adapter.ts +15 -0
- package/server/socialmedia/adapters/ayrshare-adapter.ts +169 -0
- package/server/socialmedia/adapters/buffer-adapter.ts +299 -0
- package/server/socialmedia/adapters/postiz-adapter.ts +298 -0
- package/server/socialmedia/manager.ts +227 -0
- package/server/socialmedia/store.ts +98 -0
- package/server/socialmedia/types.ts +89 -0
- package/server/tailscale-manager.ts +451 -0
- package/server/telephony/audio-bridge.ts +331 -0
- package/server/telephony/call-manager.ts +457 -0
- package/server/telephony/call-types.ts +108 -0
- package/server/telephony/telephony-store.ts +119 -0
- package/server/terminal-manager.ts +240 -0
- package/server/update-checker.ts +192 -0
- package/server/usage-limits.ts +225 -0
- package/server/web-push.d.ts +51 -0
- package/server/worktree-tracker.ts +84 -0
- package/server/ws-auth.ts +41 -0
- package/server/ws-bridge-browser-ingest.ts +72 -0
- package/server/ws-bridge-browser.ts +112 -0
- package/server/ws-bridge-cli-ingest.ts +81 -0
- package/server/ws-bridge-codex.ts +266 -0
- package/server/ws-bridge-controls.ts +20 -0
- package/server/ws-bridge-persist.ts +66 -0
- package/server/ws-bridge-publish.ts +79 -0
- package/server/ws-bridge-replay.ts +61 -0
- package/server/ws-bridge-types.ts +121 -0
- package/server/ws-bridge.ts +1240 -0
|
@@ -0,0 +1,889 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code Backend Adapter
|
|
3
|
+
*
|
|
4
|
+
* Translates between the Claude Code NDJSON WebSocket protocol and
|
|
5
|
+
* HeyHank's BrowserIncomingMessage/BrowserOutgoingMessage types.
|
|
6
|
+
*
|
|
7
|
+
* This allows the bridge (and by extension the browser) to be completely
|
|
8
|
+
* unaware of which backend is running -- it sees the same message types
|
|
9
|
+
* regardless of whether Claude Code or Codex is the backend.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
import type { ServerWebSocket } from "bun";
|
|
14
|
+
import type { IBackendAdapter } from "./backend-adapter.js";
|
|
15
|
+
import type {
|
|
16
|
+
BrowserIncomingMessage,
|
|
17
|
+
BrowserOutgoingMessage,
|
|
18
|
+
CLIMessage,
|
|
19
|
+
CLISystemMessage,
|
|
20
|
+
CLISystemInitMessage,
|
|
21
|
+
CLIAssistantMessage,
|
|
22
|
+
CLIResultMessage,
|
|
23
|
+
CLIStreamEventMessage,
|
|
24
|
+
CLIToolProgressMessage,
|
|
25
|
+
CLIToolUseSummaryMessage,
|
|
26
|
+
CLIControlRequestMessage,
|
|
27
|
+
CLIControlResponseMessage,
|
|
28
|
+
CLIAuthStatusMessage,
|
|
29
|
+
CLIControlCancelRequestMessage,
|
|
30
|
+
CLIStreamlinedTextMessage,
|
|
31
|
+
CLIStreamlinedToolUseSummaryMessage,
|
|
32
|
+
CLIPromptSuggestionMessage,
|
|
33
|
+
CLICompactBoundaryMessage,
|
|
34
|
+
CLITaskNotificationMessage,
|
|
35
|
+
CLIFilesPersistedMessage,
|
|
36
|
+
CLIHookStartedMessage,
|
|
37
|
+
CLIHookProgressMessage,
|
|
38
|
+
CLIHookResponseMessage,
|
|
39
|
+
PermissionRequest,
|
|
40
|
+
McpServerDetail,
|
|
41
|
+
SessionState,
|
|
42
|
+
} from "./session-types.js";
|
|
43
|
+
import type { SocketData } from "./ws-bridge-types.js";
|
|
44
|
+
import type { PendingControlRequest } from "./ws-bridge-types.js";
|
|
45
|
+
import type { RecorderManager } from "./recorder.js";
|
|
46
|
+
import { parseNDJSON, isDuplicateCLIMessage } from "./ws-bridge-cli-ingest.js";
|
|
47
|
+
import type { CLIDedupState } from "./ws-bridge-cli-ingest.js";
|
|
48
|
+
import { reportProtocolDrift } from "./protocol-monitor.js";
|
|
49
|
+
|
|
50
|
+
// --- Constants ----------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
/** Number of recent CLI message hashes to track for deduplication on WS reconnect. */
|
|
53
|
+
const CLI_DEDUP_WINDOW = 2000;
|
|
54
|
+
|
|
55
|
+
// --- Claude Code Adapter ------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
export class ClaudeAdapter implements IBackendAdapter {
|
|
58
|
+
private sessionId: string;
|
|
59
|
+
|
|
60
|
+
// WebSocket to the Claude Code CLI process
|
|
61
|
+
private cliSocket: ServerWebSocket<SocketData> | null = null;
|
|
62
|
+
|
|
63
|
+
// Callbacks registered by the bridge via on*() methods
|
|
64
|
+
private browserMessageCb: ((msg: BrowserIncomingMessage) => void) | null = null;
|
|
65
|
+
private sessionMetaCb: ((meta: { cliSessionId?: string; model?: string; cwd?: string }) => void) | null = null;
|
|
66
|
+
private disconnectCb: (() => void) | null = null;
|
|
67
|
+
|
|
68
|
+
// Pending NDJSON messages queued before CLI WebSocket connects
|
|
69
|
+
private pendingMessages: string[] = [];
|
|
70
|
+
|
|
71
|
+
// Async control request/response pairs (e.g. MCP status queries)
|
|
72
|
+
private pendingControlRequests = new Map<string, PendingControlRequest>();
|
|
73
|
+
|
|
74
|
+
// CLI message deduplication state (rolling hash window)
|
|
75
|
+
private dedupState: CLIDedupState = {
|
|
76
|
+
recentCLIMessageHashes: [],
|
|
77
|
+
recentCLIMessageHashSet: new Set(),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// Optional recorder for raw protocol messages
|
|
81
|
+
private recorder: RecorderManager | null;
|
|
82
|
+
|
|
83
|
+
// Callback to update session.lastCliActivityTs from the bridge
|
|
84
|
+
private onActivityUpdate: (() => void) | null;
|
|
85
|
+
|
|
86
|
+
private protocolDriftSeen = new Set<string>();
|
|
87
|
+
private parseErrorSeen = new Set<string>();
|
|
88
|
+
|
|
89
|
+
constructor(
|
|
90
|
+
sessionId: string,
|
|
91
|
+
opts?: {
|
|
92
|
+
recorder?: RecorderManager | null;
|
|
93
|
+
onActivityUpdate?: () => void;
|
|
94
|
+
},
|
|
95
|
+
) {
|
|
96
|
+
this.sessionId = sessionId;
|
|
97
|
+
this.recorder = opts?.recorder ?? null;
|
|
98
|
+
this.onActivityUpdate = opts?.onActivityUpdate ?? null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// -- WebSocket lifecycle ----------------------------------------------------
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Called when the CLI WebSocket connects. Stores the socket reference and
|
|
105
|
+
* flushes any NDJSON messages that were queued before the connection.
|
|
106
|
+
*/
|
|
107
|
+
attachWebSocket(ws: ServerWebSocket<SocketData>): void {
|
|
108
|
+
this.cliSocket = ws;
|
|
109
|
+
|
|
110
|
+
// Flush pending messages
|
|
111
|
+
if (this.pendingMessages.length > 0) {
|
|
112
|
+
console.log(
|
|
113
|
+
`[claude-adapter] Flushing ${this.pendingMessages.length} queued message(s) for session ${this.sessionId}`,
|
|
114
|
+
);
|
|
115
|
+
const queued = this.pendingMessages.splice(0);
|
|
116
|
+
for (const ndjson of queued) {
|
|
117
|
+
this.sendRaw(ndjson);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Called when the CLI WebSocket closes. Guards against stale socket references
|
|
124
|
+
* (a new WS may have opened before the old one closed).
|
|
125
|
+
*/
|
|
126
|
+
detachWebSocket(ws: ServerWebSocket<SocketData>): void {
|
|
127
|
+
// Only detach if this is the current socket -- ignore stale close events
|
|
128
|
+
if (this.cliSocket !== ws) return;
|
|
129
|
+
this.cliSocket = null;
|
|
130
|
+
this.disconnectCb?.();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// -- IBackendAdapter: Event registration ------------------------------------
|
|
134
|
+
|
|
135
|
+
onBrowserMessage(cb: (msg: BrowserIncomingMessage) => void): void {
|
|
136
|
+
this.browserMessageCb = cb;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
onSessionMeta(cb: (meta: { cliSessionId?: string; model?: string; cwd?: string }) => void): void {
|
|
140
|
+
this.sessionMetaCb = cb;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
onDisconnect(cb: () => void): void {
|
|
144
|
+
this.disconnectCb = cb;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// -- IBackendAdapter: Transport state ---------------------------------------
|
|
148
|
+
|
|
149
|
+
isConnected(): boolean {
|
|
150
|
+
return this.cliSocket !== null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async disconnect(): Promise<void> {
|
|
154
|
+
// Clear pending control requests to prevent memory leaks from
|
|
155
|
+
// unresolved promises (CLI won't respond after disconnect)
|
|
156
|
+
this.pendingControlRequests.clear();
|
|
157
|
+
if (this.cliSocket) {
|
|
158
|
+
try {
|
|
159
|
+
this.cliSocket.close();
|
|
160
|
+
} catch {
|
|
161
|
+
// Socket may already be closed
|
|
162
|
+
}
|
|
163
|
+
this.cliSocket = null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Handle transport-level close (used when WS proxy drops).
|
|
169
|
+
* Clears the socket reference without triggering the disconnect callback,
|
|
170
|
+
* allowing the CLI to reconnect.
|
|
171
|
+
*/
|
|
172
|
+
handleTransportClose(): void {
|
|
173
|
+
this.cliSocket = null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// -- IBackendAdapter: Raw message ingestion from CLI ------------------------
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Called when raw NDJSON data arrives from the CLI WebSocket.
|
|
180
|
+
* Parses lines, deduplicates, and routes each message.
|
|
181
|
+
*/
|
|
182
|
+
handleRawMessage(data: string): void {
|
|
183
|
+
// Record raw incoming CLI message before any parsing
|
|
184
|
+
this.recorder?.record(
|
|
185
|
+
this.sessionId, "in", data, "cli", "claude", "",
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const lines = parseNDJSON(data);
|
|
189
|
+
for (const line of lines) {
|
|
190
|
+
let msg: CLIMessage;
|
|
191
|
+
try {
|
|
192
|
+
msg = JSON.parse(line);
|
|
193
|
+
} catch {
|
|
194
|
+
reportProtocolDrift(
|
|
195
|
+
this.parseErrorSeen,
|
|
196
|
+
{
|
|
197
|
+
backend: "claude",
|
|
198
|
+
sessionId: this.sessionId,
|
|
199
|
+
direction: "incoming",
|
|
200
|
+
messageKind: "parse_error",
|
|
201
|
+
messageName: "ndjson",
|
|
202
|
+
rawPreview: line,
|
|
203
|
+
},
|
|
204
|
+
(message) => this.browserMessageCb?.({ type: "error", message }),
|
|
205
|
+
);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (isDuplicateCLIMessage(msg, line, this.dedupState, CLI_DEDUP_WINDOW)) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
this.routeCLIMessage(msg);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// -- IBackendAdapter: send() -- browser -> CLI translation ------------------
|
|
218
|
+
|
|
219
|
+
send(msg: BrowserOutgoingMessage): boolean {
|
|
220
|
+
switch (msg.type) {
|
|
221
|
+
case "user_message":
|
|
222
|
+
return this.handleOutgoingUserMessage(msg);
|
|
223
|
+
|
|
224
|
+
case "permission_response":
|
|
225
|
+
return this.handleOutgoingPermissionResponse(msg);
|
|
226
|
+
|
|
227
|
+
case "interrupt":
|
|
228
|
+
return this.handleOutgoingInterrupt();
|
|
229
|
+
|
|
230
|
+
case "set_model":
|
|
231
|
+
return this.handleOutgoingSetModel(msg.model);
|
|
232
|
+
|
|
233
|
+
case "set_permission_mode":
|
|
234
|
+
return this.handleOutgoingSetPermissionMode(msg.mode);
|
|
235
|
+
|
|
236
|
+
case "set_ai_validation":
|
|
237
|
+
// AI validation state is managed at the bridge/session level, not
|
|
238
|
+
// forwarded to the CLI. Return true to indicate acceptance.
|
|
239
|
+
return true;
|
|
240
|
+
|
|
241
|
+
case "mcp_get_status":
|
|
242
|
+
return this.handleOutgoingMcpGetStatus();
|
|
243
|
+
|
|
244
|
+
case "mcp_toggle":
|
|
245
|
+
return this.handleOutgoingMcpToggle(msg.serverName, msg.enabled);
|
|
246
|
+
|
|
247
|
+
case "mcp_reconnect":
|
|
248
|
+
return this.handleOutgoingMcpReconnect(msg.serverName);
|
|
249
|
+
|
|
250
|
+
case "mcp_set_servers":
|
|
251
|
+
return this.handleOutgoingMcpSetServers(msg.servers);
|
|
252
|
+
|
|
253
|
+
case "end_session":
|
|
254
|
+
return this.handleOutgoingEndSession((msg as { reason?: string }).reason);
|
|
255
|
+
|
|
256
|
+
case "stop_task":
|
|
257
|
+
return this.handleOutgoingStopTask((msg as { task_id: string }).task_id);
|
|
258
|
+
|
|
259
|
+
case "update_environment_variables":
|
|
260
|
+
return this.handleOutgoingUpdateEnvVars((msg as { variables: Record<string, string> }).variables);
|
|
261
|
+
|
|
262
|
+
case "session_subscribe":
|
|
263
|
+
case "session_ack":
|
|
264
|
+
// These are handled at the bridge level -- never forwarded to the backend.
|
|
265
|
+
return false;
|
|
266
|
+
|
|
267
|
+
default:
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// -- Outgoing message handlers (browser -> NDJSON) --------------------------
|
|
273
|
+
|
|
274
|
+
private handleOutgoingUserMessage(
|
|
275
|
+
msg: { type: "user_message"; content: string; session_id?: string; images?: { media_type: string; data: string }[] },
|
|
276
|
+
): boolean {
|
|
277
|
+
// Build content: if images are present, use content block array; otherwise plain string
|
|
278
|
+
let content: string | unknown[];
|
|
279
|
+
if (msg.images?.length) {
|
|
280
|
+
const blocks: unknown[] = [];
|
|
281
|
+
for (const img of msg.images) {
|
|
282
|
+
blocks.push({
|
|
283
|
+
type: "image",
|
|
284
|
+
source: { type: "base64", media_type: img.media_type, data: img.data },
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
blocks.push({ type: "text", text: msg.content });
|
|
288
|
+
content = blocks;
|
|
289
|
+
} else {
|
|
290
|
+
content = msg.content;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const ndjson = JSON.stringify({
|
|
294
|
+
type: "user",
|
|
295
|
+
message: { role: "user", content },
|
|
296
|
+
parent_tool_use_id: null,
|
|
297
|
+
session_id: msg.session_id || "",
|
|
298
|
+
});
|
|
299
|
+
this.sendToBackend(ndjson);
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private handleOutgoingPermissionResponse(
|
|
304
|
+
msg: {
|
|
305
|
+
type: "permission_response";
|
|
306
|
+
request_id: string;
|
|
307
|
+
behavior: "allow" | "deny";
|
|
308
|
+
updated_input?: Record<string, unknown>;
|
|
309
|
+
updated_permissions?: unknown[];
|
|
310
|
+
message?: string;
|
|
311
|
+
},
|
|
312
|
+
): boolean {
|
|
313
|
+
if (msg.behavior === "allow") {
|
|
314
|
+
const response: Record<string, unknown> = {
|
|
315
|
+
behavior: "allow",
|
|
316
|
+
updatedInput: msg.updated_input ?? {},
|
|
317
|
+
};
|
|
318
|
+
if (msg.updated_permissions?.length) {
|
|
319
|
+
response.updatedPermissions = msg.updated_permissions;
|
|
320
|
+
}
|
|
321
|
+
const ndjson = JSON.stringify({
|
|
322
|
+
type: "control_response",
|
|
323
|
+
response: {
|
|
324
|
+
subtype: "success",
|
|
325
|
+
request_id: msg.request_id,
|
|
326
|
+
response,
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
this.sendToBackend(ndjson);
|
|
330
|
+
} else {
|
|
331
|
+
const ndjson = JSON.stringify({
|
|
332
|
+
type: "control_response",
|
|
333
|
+
response: {
|
|
334
|
+
subtype: "success",
|
|
335
|
+
request_id: msg.request_id,
|
|
336
|
+
response: {
|
|
337
|
+
behavior: "deny",
|
|
338
|
+
message: msg.message || "Denied by user",
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
this.sendToBackend(ndjson);
|
|
343
|
+
}
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
private handleOutgoingInterrupt(): boolean {
|
|
348
|
+
const ndjson = JSON.stringify({
|
|
349
|
+
type: "control_request",
|
|
350
|
+
request_id: randomUUID(),
|
|
351
|
+
request: { subtype: "interrupt" },
|
|
352
|
+
});
|
|
353
|
+
this.sendToBackend(ndjson);
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private handleOutgoingSetModel(model: string): boolean {
|
|
358
|
+
const ndjson = JSON.stringify({
|
|
359
|
+
type: "control_request",
|
|
360
|
+
request_id: randomUUID(),
|
|
361
|
+
request: { subtype: "set_model", model },
|
|
362
|
+
});
|
|
363
|
+
this.sendToBackend(ndjson);
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private handleOutgoingSetPermissionMode(mode: string): boolean {
|
|
368
|
+
const ndjson = JSON.stringify({
|
|
369
|
+
type: "control_request",
|
|
370
|
+
request_id: randomUUID(),
|
|
371
|
+
request: { subtype: "set_permission_mode", mode },
|
|
372
|
+
});
|
|
373
|
+
this.sendToBackend(ndjson);
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private handleOutgoingMcpGetStatus(): boolean {
|
|
378
|
+
this.sendControlRequest(
|
|
379
|
+
{ subtype: "mcp_status" },
|
|
380
|
+
{
|
|
381
|
+
subtype: "mcp_status",
|
|
382
|
+
resolve: (response) => {
|
|
383
|
+
const servers = (response as { mcpServers?: McpServerDetail[] }).mcpServers ?? [];
|
|
384
|
+
this.browserMessageCb?.({ type: "mcp_status", servers });
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
);
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private handleOutgoingMcpToggle(serverName: string, enabled: boolean): boolean {
|
|
392
|
+
this.sendControlRequest({ subtype: "mcp_toggle", serverName, enabled });
|
|
393
|
+
// Refresh MCP status after a delay to pick up the change
|
|
394
|
+
setTimeout(() => this.handleOutgoingMcpGetStatus(), 500);
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private handleOutgoingMcpReconnect(serverName: string): boolean {
|
|
399
|
+
this.sendControlRequest({ subtype: "mcp_reconnect", serverName });
|
|
400
|
+
// Refresh MCP status after a delay to pick up the reconnection
|
|
401
|
+
setTimeout(() => this.handleOutgoingMcpGetStatus(), 1000);
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private handleOutgoingMcpSetServers(servers: Record<string, unknown>): boolean {
|
|
406
|
+
this.sendControlRequest({ subtype: "mcp_set_servers", servers });
|
|
407
|
+
// Refresh MCP status after a delay to pick up the new server config
|
|
408
|
+
setTimeout(() => this.handleOutgoingMcpGetStatus(), 2000);
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private handleOutgoingEndSession(reason?: string): boolean {
|
|
413
|
+
this.sendControlRequest({ subtype: "end_session", ...(reason ? { reason } : {}) });
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private handleOutgoingStopTask(taskId: string): boolean {
|
|
418
|
+
this.sendControlRequest({ subtype: "stop_task", task_id: taskId });
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private handleOutgoingUpdateEnvVars(variables: Record<string, string>): boolean {
|
|
423
|
+
const ndjson = JSON.stringify({
|
|
424
|
+
type: "update_environment_variables",
|
|
425
|
+
variables,
|
|
426
|
+
});
|
|
427
|
+
this.sendToBackend(ndjson);
|
|
428
|
+
return true;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// -- CLI message routing (NDJSON -> BrowserIncomingMessage) -----------------
|
|
432
|
+
|
|
433
|
+
private routeCLIMessage(msg: CLIMessage): void {
|
|
434
|
+
// Track activity for idle detection (skip keepalives -- they don't indicate real work)
|
|
435
|
+
if (msg.type !== "keep_alive") {
|
|
436
|
+
this.onActivityUpdate?.();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
switch (msg.type) {
|
|
440
|
+
case "system":
|
|
441
|
+
this.handleSystemMessage(msg);
|
|
442
|
+
break;
|
|
443
|
+
|
|
444
|
+
case "assistant":
|
|
445
|
+
this.handleAssistantMessage(msg);
|
|
446
|
+
break;
|
|
447
|
+
|
|
448
|
+
case "result":
|
|
449
|
+
this.handleResultMessage(msg);
|
|
450
|
+
break;
|
|
451
|
+
|
|
452
|
+
case "stream_event":
|
|
453
|
+
this.handleStreamEvent(msg);
|
|
454
|
+
break;
|
|
455
|
+
|
|
456
|
+
case "control_request":
|
|
457
|
+
this.handleControlRequest(msg);
|
|
458
|
+
break;
|
|
459
|
+
|
|
460
|
+
case "control_response":
|
|
461
|
+
this.handleControlResponse(msg);
|
|
462
|
+
break;
|
|
463
|
+
|
|
464
|
+
case "tool_progress":
|
|
465
|
+
this.handleToolProgress(msg);
|
|
466
|
+
break;
|
|
467
|
+
|
|
468
|
+
case "tool_use_summary":
|
|
469
|
+
this.handleToolUseSummary(msg);
|
|
470
|
+
break;
|
|
471
|
+
|
|
472
|
+
case "auth_status":
|
|
473
|
+
this.handleAuthStatus(msg);
|
|
474
|
+
break;
|
|
475
|
+
|
|
476
|
+
case "keep_alive":
|
|
477
|
+
// Silently consume keepalives
|
|
478
|
+
break;
|
|
479
|
+
|
|
480
|
+
case "user":
|
|
481
|
+
// CLI echoes back user messages (including tool_result blocks from
|
|
482
|
+
// subagents). These are purely informational — the bridge already
|
|
483
|
+
// persists user messages from the browser side. Silently drop them
|
|
484
|
+
// to avoid rendering raw tool_result JSON in the chat UI.
|
|
485
|
+
break;
|
|
486
|
+
|
|
487
|
+
case "rate_limit_event":
|
|
488
|
+
// Rate-limit status from Claude API (allowed/throttled). Silently
|
|
489
|
+
// consumed — no user-facing action needed.
|
|
490
|
+
break;
|
|
491
|
+
|
|
492
|
+
case "control_cancel_request":
|
|
493
|
+
this.handleControlCancelRequest(msg as CLIControlCancelRequestMessage);
|
|
494
|
+
break;
|
|
495
|
+
|
|
496
|
+
case "streamlined_text":
|
|
497
|
+
this.handleStreamlinedText(msg as CLIStreamlinedTextMessage);
|
|
498
|
+
break;
|
|
499
|
+
|
|
500
|
+
case "streamlined_tool_use_summary":
|
|
501
|
+
this.handleStreamlinedToolUseSummary(msg as CLIStreamlinedToolUseSummaryMessage);
|
|
502
|
+
break;
|
|
503
|
+
|
|
504
|
+
case "prompt_suggestion":
|
|
505
|
+
this.handlePromptSuggestion(msg as CLIPromptSuggestionMessage);
|
|
506
|
+
break;
|
|
507
|
+
|
|
508
|
+
default:
|
|
509
|
+
reportProtocolDrift(
|
|
510
|
+
this.protocolDriftSeen,
|
|
511
|
+
{
|
|
512
|
+
backend: "claude",
|
|
513
|
+
sessionId: this.sessionId,
|
|
514
|
+
direction: "incoming",
|
|
515
|
+
messageKind: "message",
|
|
516
|
+
messageName: (msg as { type?: string }).type || "unknown",
|
|
517
|
+
rawPreview: JSON.stringify(msg),
|
|
518
|
+
},
|
|
519
|
+
(message) => this.browserMessageCb?.({ type: "error", message }),
|
|
520
|
+
);
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// -- System message handling ------------------------------------------------
|
|
526
|
+
|
|
527
|
+
private handleSystemMessage(msg: CLISystemMessage): void {
|
|
528
|
+
if (msg.subtype === "init") {
|
|
529
|
+
this.handleSystemInit(msg as CLISystemInitMessage);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (msg.subtype === "status") {
|
|
534
|
+
const statusMsg = msg as { subtype: "status"; status: "compacting" | null; permissionMode?: string; uuid: string; session_id: string };
|
|
535
|
+
// Include permissionMode in the emitted message so the bridge can update session state
|
|
536
|
+
const statusChange: Record<string, unknown> = {
|
|
537
|
+
type: "status_change",
|
|
538
|
+
status: statusMsg.status ?? null,
|
|
539
|
+
};
|
|
540
|
+
if (statusMsg.permissionMode) {
|
|
541
|
+
statusChange.permissionMode = statusMsg.permissionMode;
|
|
542
|
+
}
|
|
543
|
+
this.browserMessageCb?.(statusChange as BrowserIncomingMessage);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if (msg.subtype === "compact_boundary") {
|
|
548
|
+
const m = msg as CLICompactBoundaryMessage;
|
|
549
|
+
this.emitSystemEvent({
|
|
550
|
+
subtype: "compact_boundary",
|
|
551
|
+
compact_metadata: m.compact_metadata,
|
|
552
|
+
uuid: m.uuid,
|
|
553
|
+
session_id: m.session_id,
|
|
554
|
+
});
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (msg.subtype === "task_notification") {
|
|
559
|
+
const m = msg as CLITaskNotificationMessage;
|
|
560
|
+
this.emitSystemEvent({
|
|
561
|
+
subtype: "task_notification",
|
|
562
|
+
task_id: m.task_id,
|
|
563
|
+
status: m.status,
|
|
564
|
+
output_file: m.output_file,
|
|
565
|
+
summary: m.summary,
|
|
566
|
+
uuid: m.uuid,
|
|
567
|
+
session_id: m.session_id,
|
|
568
|
+
});
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (msg.subtype === "files_persisted") {
|
|
573
|
+
const m = msg as CLIFilesPersistedMessage;
|
|
574
|
+
this.emitSystemEvent({
|
|
575
|
+
subtype: "files_persisted",
|
|
576
|
+
files: m.files,
|
|
577
|
+
failed: m.failed,
|
|
578
|
+
processed_at: m.processed_at,
|
|
579
|
+
uuid: m.uuid,
|
|
580
|
+
session_id: m.session_id,
|
|
581
|
+
});
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (msg.subtype === "hook_started") {
|
|
586
|
+
const m = msg as CLIHookStartedMessage;
|
|
587
|
+
this.emitSystemEvent({
|
|
588
|
+
subtype: "hook_started",
|
|
589
|
+
hook_id: m.hook_id,
|
|
590
|
+
hook_name: m.hook_name,
|
|
591
|
+
hook_event: m.hook_event,
|
|
592
|
+
uuid: m.uuid,
|
|
593
|
+
session_id: m.session_id,
|
|
594
|
+
});
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (msg.subtype === "hook_progress") {
|
|
599
|
+
const m = msg as CLIHookProgressMessage;
|
|
600
|
+
// hook_progress is transient -- emitted but not persisted in message history.
|
|
601
|
+
// The bridge handler decides on persistence based on message type.
|
|
602
|
+
this.emitSystemEvent({
|
|
603
|
+
subtype: "hook_progress",
|
|
604
|
+
hook_id: m.hook_id,
|
|
605
|
+
hook_name: m.hook_name,
|
|
606
|
+
hook_event: m.hook_event,
|
|
607
|
+
stdout: m.stdout,
|
|
608
|
+
stderr: m.stderr,
|
|
609
|
+
output: m.output,
|
|
610
|
+
uuid: m.uuid,
|
|
611
|
+
session_id: m.session_id,
|
|
612
|
+
});
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (msg.subtype === "hook_response") {
|
|
617
|
+
const m = msg as CLIHookResponseMessage;
|
|
618
|
+
this.emitSystemEvent({
|
|
619
|
+
subtype: "hook_response",
|
|
620
|
+
hook_id: m.hook_id,
|
|
621
|
+
hook_name: m.hook_name,
|
|
622
|
+
hook_event: m.hook_event,
|
|
623
|
+
output: m.output,
|
|
624
|
+
stdout: m.stdout,
|
|
625
|
+
stderr: m.stderr,
|
|
626
|
+
exit_code: m.exit_code,
|
|
627
|
+
outcome: m.outcome,
|
|
628
|
+
uuid: m.uuid,
|
|
629
|
+
session_id: m.session_id,
|
|
630
|
+
});
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Unknown system subtypes are intentionally ignored until we map them.
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
private handleSystemInit(msg: CLISystemInitMessage): void {
|
|
638
|
+
// Emit session metadata so the bridge can update session state
|
|
639
|
+
this.sessionMetaCb?.({
|
|
640
|
+
cliSessionId: msg.session_id,
|
|
641
|
+
model: msg.model,
|
|
642
|
+
cwd: msg.cwd,
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// Emit session_init to browsers with CLI-provided fields only.
|
|
646
|
+
// The bridge's attachBackendAdapter handler will merge these into the
|
|
647
|
+
// canonical session state (which owns git info, cost, etc.) and broadcast.
|
|
648
|
+
this.browserMessageCb?.({
|
|
649
|
+
type: "session_init",
|
|
650
|
+
session: {
|
|
651
|
+
session_id: msg.session_id,
|
|
652
|
+
model: msg.model,
|
|
653
|
+
cwd: msg.cwd,
|
|
654
|
+
tools: msg.tools,
|
|
655
|
+
permissionMode: msg.permissionMode,
|
|
656
|
+
claude_code_version: msg.claude_code_version,
|
|
657
|
+
mcp_servers: msg.mcp_servers,
|
|
658
|
+
agents: msg.agents ?? [],
|
|
659
|
+
slash_commands: msg.slash_commands ?? [],
|
|
660
|
+
skills: msg.skills ?? [],
|
|
661
|
+
} as SessionState,
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
// Flush any NDJSON messages queued before the CLI was initialized
|
|
665
|
+
// (e.g. user sent a message while the CLI was still starting up).
|
|
666
|
+
if (this.pendingMessages.length > 0) {
|
|
667
|
+
console.log(
|
|
668
|
+
`[claude-adapter] Flushing ${this.pendingMessages.length} queued message(s) after init for session ${this.sessionId}`,
|
|
669
|
+
);
|
|
670
|
+
const queued = this.pendingMessages.splice(0);
|
|
671
|
+
for (const ndjson of queued) {
|
|
672
|
+
this.sendRaw(ndjson);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// -- Assistant, result, stream ----------------------------------------------
|
|
678
|
+
|
|
679
|
+
private handleAssistantMessage(msg: CLIAssistantMessage): void {
|
|
680
|
+
this.browserMessageCb?.({
|
|
681
|
+
type: "assistant",
|
|
682
|
+
message: msg.message,
|
|
683
|
+
parent_tool_use_id: msg.parent_tool_use_id,
|
|
684
|
+
timestamp: Date.now(),
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
private handleResultMessage(msg: CLIResultMessage): void {
|
|
689
|
+
this.browserMessageCb?.({
|
|
690
|
+
type: "result",
|
|
691
|
+
data: msg,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
private handleStreamEvent(msg: CLIStreamEventMessage): void {
|
|
696
|
+
this.browserMessageCb?.({
|
|
697
|
+
type: "stream_event",
|
|
698
|
+
event: msg.event,
|
|
699
|
+
parent_tool_use_id: msg.parent_tool_use_id,
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// -- Control request (permission) -------------------------------------------
|
|
704
|
+
|
|
705
|
+
private handleControlRequest(msg: CLIControlRequestMessage): void {
|
|
706
|
+
if (msg.request.subtype === "can_use_tool") {
|
|
707
|
+
const perm: PermissionRequest = {
|
|
708
|
+
request_id: msg.request_id,
|
|
709
|
+
tool_name: msg.request.tool_name,
|
|
710
|
+
input: msg.request.input,
|
|
711
|
+
permission_suggestions: msg.request.permission_suggestions,
|
|
712
|
+
description: msg.request.description,
|
|
713
|
+
tool_use_id: msg.request.tool_use_id,
|
|
714
|
+
agent_id: msg.request.agent_id,
|
|
715
|
+
title: msg.request.title,
|
|
716
|
+
display_name: msg.request.display_name,
|
|
717
|
+
blocked_path: msg.request.blocked_path,
|
|
718
|
+
decision_reason: msg.request.decision_reason,
|
|
719
|
+
timestamp: Date.now(),
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
this.browserMessageCb?.({
|
|
723
|
+
type: "permission_request",
|
|
724
|
+
request: perm,
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// -- Control cancel request ------------------------------------------------
|
|
730
|
+
|
|
731
|
+
private handleControlCancelRequest(msg: CLIControlCancelRequestMessage): void {
|
|
732
|
+
// Clean up any pending async control request in the adapter
|
|
733
|
+
this.pendingControlRequests.delete(msg.request_id);
|
|
734
|
+
// Emit permission_cancelled so the bridge removes from pendingPermissions
|
|
735
|
+
this.browserMessageCb?.({
|
|
736
|
+
type: "permission_cancelled",
|
|
737
|
+
request_id: msg.request_id,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// -- Streamlined messages (simplified output mode) -------------------------
|
|
742
|
+
|
|
743
|
+
private handleStreamlinedText(msg: CLIStreamlinedTextMessage): void {
|
|
744
|
+
this.browserMessageCb?.({
|
|
745
|
+
type: "streamlined_text",
|
|
746
|
+
text: msg.text,
|
|
747
|
+
} as BrowserIncomingMessage);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
private handleStreamlinedToolUseSummary(msg: CLIStreamlinedToolUseSummaryMessage): void {
|
|
751
|
+
this.browserMessageCb?.({
|
|
752
|
+
type: "streamlined_tool_use_summary",
|
|
753
|
+
tool_summary: msg.tool_summary,
|
|
754
|
+
} as BrowserIncomingMessage);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// -- Prompt suggestions ----------------------------------------------------
|
|
758
|
+
|
|
759
|
+
private handlePromptSuggestion(msg: CLIPromptSuggestionMessage): void {
|
|
760
|
+
this.browserMessageCb?.({
|
|
761
|
+
type: "prompt_suggestion",
|
|
762
|
+
suggestions: msg.suggestions,
|
|
763
|
+
} as BrowserIncomingMessage);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// -- Control response (for pending control requests like MCP status) --------
|
|
767
|
+
|
|
768
|
+
private handleControlResponse(msg: CLIControlResponseMessage): void {
|
|
769
|
+
const reqId = msg.response.request_id;
|
|
770
|
+
const pending = this.pendingControlRequests.get(reqId);
|
|
771
|
+
if (!pending) return;
|
|
772
|
+
this.pendingControlRequests.delete(reqId);
|
|
773
|
+
if (msg.response.subtype === "error") {
|
|
774
|
+
console.warn(
|
|
775
|
+
`[claude-adapter] Control request ${pending.subtype} failed: ${msg.response.error}`,
|
|
776
|
+
);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
pending.resolve(msg.response.response ?? {});
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// -- Tool progress & summary ------------------------------------------------
|
|
783
|
+
|
|
784
|
+
private handleToolProgress(msg: CLIToolProgressMessage): void {
|
|
785
|
+
this.browserMessageCb?.({
|
|
786
|
+
type: "tool_progress",
|
|
787
|
+
tool_use_id: msg.tool_use_id,
|
|
788
|
+
tool_name: msg.tool_name,
|
|
789
|
+
elapsed_time_seconds: msg.elapsed_time_seconds,
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
private handleToolUseSummary(msg: CLIToolUseSummaryMessage): void {
|
|
794
|
+
this.browserMessageCb?.({
|
|
795
|
+
type: "tool_use_summary",
|
|
796
|
+
summary: msg.summary,
|
|
797
|
+
tool_use_ids: msg.preceding_tool_use_ids,
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// -- Auth status ------------------------------------------------------------
|
|
802
|
+
|
|
803
|
+
private handleAuthStatus(msg: CLIAuthStatusMessage): void {
|
|
804
|
+
this.browserMessageCb?.({
|
|
805
|
+
type: "auth_status",
|
|
806
|
+
isAuthenticating: msg.isAuthenticating,
|
|
807
|
+
output: msg.output,
|
|
808
|
+
error: msg.error,
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// -- Helpers ----------------------------------------------------------------
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Emit a system_event BrowserIncomingMessage to browsers.
|
|
816
|
+
*/
|
|
817
|
+
private emitSystemEvent(
|
|
818
|
+
event: Extract<BrowserIncomingMessage, { type: "system_event" }>["event"],
|
|
819
|
+
): void {
|
|
820
|
+
this.browserMessageCb?.({
|
|
821
|
+
type: "system_event",
|
|
822
|
+
event,
|
|
823
|
+
timestamp: Date.now(),
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Send a control_request to the CLI and optionally track the pending response.
|
|
829
|
+
*/
|
|
830
|
+
private sendControlRequest(
|
|
831
|
+
request: Record<string, unknown>,
|
|
832
|
+
onResponse?: { subtype: string; resolve: (response: unknown) => void },
|
|
833
|
+
): void {
|
|
834
|
+
const requestId = randomUUID();
|
|
835
|
+
if (onResponse) {
|
|
836
|
+
this.pendingControlRequests.set(requestId, onResponse);
|
|
837
|
+
}
|
|
838
|
+
const ndjson = JSON.stringify({
|
|
839
|
+
type: "control_request",
|
|
840
|
+
request_id: requestId,
|
|
841
|
+
request,
|
|
842
|
+
});
|
|
843
|
+
this.sendToBackend(ndjson);
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Send a raw NDJSON string to the CLI, bypassing the BrowserOutgoingMessage
|
|
848
|
+
* translation layer. Used for Claude-specific control requests (e.g. initialize)
|
|
849
|
+
* that don't map to a BrowserOutgoingMessage type.
|
|
850
|
+
*/
|
|
851
|
+
sendRawNDJSON(ndjson: string): void {
|
|
852
|
+
this.sendToBackend(ndjson);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Send an NDJSON string to the CLI. If the CLI socket is not yet connected,
|
|
857
|
+
* queues the message for later delivery (flushed in attachWebSocket).
|
|
858
|
+
*/
|
|
859
|
+
private sendToBackend(ndjson: string): void {
|
|
860
|
+
if (!this.cliSocket) {
|
|
861
|
+
console.log(
|
|
862
|
+
`[claude-adapter] CLI not yet connected for session ${this.sessionId}, queuing message`,
|
|
863
|
+
);
|
|
864
|
+
this.pendingMessages.push(ndjson);
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
this.sendRaw(ndjson);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Low-level send: writes NDJSON to the CLI socket with newline delimiter.
|
|
872
|
+
* Records the outgoing message. Assumes cliSocket is non-null.
|
|
873
|
+
*/
|
|
874
|
+
private sendRaw(ndjson: string): void {
|
|
875
|
+
// Record raw outgoing CLI message
|
|
876
|
+
this.recorder?.record(
|
|
877
|
+
this.sessionId, "out", ndjson, "cli", "claude", "",
|
|
878
|
+
);
|
|
879
|
+
try {
|
|
880
|
+
// NDJSON requires a newline delimiter
|
|
881
|
+
this.cliSocket!.send(ndjson + "\n");
|
|
882
|
+
} catch (err) {
|
|
883
|
+
console.error(
|
|
884
|
+
`[claude-adapter] Failed to send to CLI for session ${this.sessionId}:`,
|
|
885
|
+
err,
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|