wave-code 0.19.2 → 0.19.3

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.
@@ -0,0 +1,756 @@
1
+ /**
2
+ * AgentBridge — wraps the SDK Agent and translates between the JSON-RPC-like
3
+ * stdio protocol and Agent method calls / callbacks.
4
+ *
5
+ * Responsibilities:
6
+ * - Route incoming requests to the appropriate Agent method
7
+ * - Translate AgentCallbacks into outgoing notifications
8
+ * - Implement the canUseTool permission flow over the stdio protocol
9
+ * - Handle config updates by destroying and recreating the Agent
10
+ */
11
+
12
+ import {
13
+ Agent,
14
+ type AgentCallbacks,
15
+ type AgentOptions,
16
+ type Message,
17
+ type PermissionDecision,
18
+ type PermissionMode,
19
+ type ToolPermissionContext,
20
+ type McpServerStatus,
21
+ type Task,
22
+ type QueuedMessage,
23
+ type SessionMetadata,
24
+ type McpServerConfig,
25
+ type Scope,
26
+ listSessions,
27
+ searchFiles,
28
+ PromptHistoryManager,
29
+ AuthService,
30
+ PluginCore,
31
+ type SlashCommand,
32
+ } from "wave-agent-sdk";
33
+ import {
34
+ type JsonRpcError,
35
+ INTERNAL_ERROR as PROTOCOL_INTERNAL_ERROR,
36
+ METHOD_NOT_FOUND as PROTOCOL_METHOD_NOT_FOUND,
37
+ } from "./protocol.js";
38
+
39
+ export type NotificationEmitter = (method: string, params: unknown) => void;
40
+
41
+ export interface AgentBridgeOptions {
42
+ emit: NotificationEmitter;
43
+ }
44
+
45
+ interface InitializeParams {
46
+ workdir?: string;
47
+ restoreSessionId?: string;
48
+ apiKey?: string;
49
+ baseURL?: string;
50
+ serverUrl?: string;
51
+ defaultHeaders?: Record<string, string>;
52
+ model?: string;
53
+ fastModel?: string;
54
+ language?: string;
55
+ permissionMode?: PermissionMode;
56
+ tools?: string[];
57
+ allowedTools?: string[];
58
+ disallowedTools?: string[];
59
+ pluginDirs?: string[];
60
+ mcpServers?: Record<string, McpServerConfig>;
61
+ }
62
+
63
+ interface UpdateConfigParams {
64
+ apiKey?: string;
65
+ baseURL?: string;
66
+ serverUrl?: string;
67
+ defaultHeaders?: Record<string, string>;
68
+ model?: string;
69
+ fastModel?: string;
70
+ language?: string;
71
+ }
72
+
73
+ interface SearchFilesParams {
74
+ query: string;
75
+ maxResults?: number;
76
+ workdir?: string;
77
+ }
78
+
79
+ export class AgentBridge {
80
+ private agent: Agent | undefined;
81
+ private pendingPermissions = new Map<
82
+ string,
83
+ (decision: PermissionDecision) => void
84
+ >();
85
+ private permissionCounter = 0;
86
+ private storedConfig: Partial<InitializeParams> = {};
87
+ private emit: NotificationEmitter;
88
+ private pluginCore: PluginCore | undefined;
89
+ private pluginCoreWorkdir: string | undefined;
90
+
91
+ constructor(options: AgentBridgeOptions) {
92
+ this.emit = options.emit;
93
+ }
94
+
95
+ // ── Public API ────────────────────────────────────────────────
96
+
97
+ async handleRequest(method: string, params: unknown): Promise<unknown> {
98
+ const p = (params ?? {}) as Record<string, unknown>;
99
+ switch (method) {
100
+ // ── Lifecycle ──
101
+ case "initialize":
102
+ return this.initialize(p as unknown as InitializeParams);
103
+ case "destroy":
104
+ return this.destroy();
105
+ case "restoreSession":
106
+ return this.restoreSession(p.sessionId as string);
107
+ case "listSessions":
108
+ return this.listSessions(p.workdir as string | undefined);
109
+ case "getSessionInfo":
110
+ return this.getSessionInfo();
111
+ case "updateConfig":
112
+ return this.updateConfig(p as unknown as UpdateConfigParams);
113
+
114
+ // ── Messages ──
115
+ case "sendMessage":
116
+ return this.sendMessage(
117
+ p as unknown as {
118
+ text: string;
119
+ images?: Array<{ path: string; mimeType: string }>;
120
+ force?: boolean;
121
+ },
122
+ );
123
+ case "bang":
124
+ return this.bang(p.command as string);
125
+ case "abortMessage":
126
+ return this.abortMessage();
127
+ case "clearMessages":
128
+ return this.clearMessages();
129
+ case "rewindToMessage":
130
+ return this.rewindToMessage(p.messageId as string);
131
+ case "deleteQueuedMessage":
132
+ return this.deleteQueuedMessage(p.index as number);
133
+ case "getMessages":
134
+ return this.getMessages();
135
+ case "getFullMessageThread":
136
+ return this.getFullMessageThread();
137
+
138
+ // ── Permissions ──
139
+ case "setPermissionMode":
140
+ return this.setPermissionMode(p.mode as PermissionMode);
141
+ case "getPermissionMode":
142
+ return this.getPermissionMode();
143
+
144
+ // ── MCP ──
145
+ case "getMcpServers":
146
+ return this.getMcpServers();
147
+ case "connectMcpServer":
148
+ return this.connectMcpServer(p.serverName as string);
149
+ case "disconnectMcpServer":
150
+ return this.disconnectMcpServer(p.serverName as string);
151
+
152
+ // ── Commands ──
153
+ case "getSlashCommands":
154
+ return this.getSlashCommands();
155
+
156
+ // ── File / History ──
157
+ case "searchFiles":
158
+ return this.searchFiles(p as unknown as SearchFilesParams);
159
+ case "getPromptHistory":
160
+ return this.getPromptHistory(p.workdir as string | undefined);
161
+ case "searchPromptHistory":
162
+ return this.searchPromptHistory(
163
+ p.query as string,
164
+ p.workdir as string | undefined,
165
+ );
166
+
167
+ // ── Auth ──
168
+ case "getAuthStatus":
169
+ return this.getAuthStatus();
170
+ case "login":
171
+ return this.login(p.serverUrl as string | undefined);
172
+ case "logout":
173
+ return this.logout();
174
+
175
+ // ── Plugins ──
176
+ case "listPlugins":
177
+ return this.listPlugins(p.workdir as string | undefined);
178
+ case "installPlugin":
179
+ return this.installPlugin(
180
+ p.pluginId as string,
181
+ p.scope as Scope | undefined,
182
+ p.workdir as string | undefined,
183
+ );
184
+ case "uninstallPlugin":
185
+ return this.uninstallPlugin(
186
+ p.pluginId as string,
187
+ p.workdir as string | undefined,
188
+ );
189
+ case "enablePlugin":
190
+ return this.enablePlugin(
191
+ p.pluginId as string,
192
+ p.scope as Scope | undefined,
193
+ p.workdir as string | undefined,
194
+ );
195
+ case "disablePlugin":
196
+ return this.disablePlugin(
197
+ p.pluginId as string,
198
+ p.scope as Scope | undefined,
199
+ p.workdir as string | undefined,
200
+ );
201
+ case "updatePlugin":
202
+ return this.updatePlugin(
203
+ p.pluginId as string,
204
+ p.workdir as string | undefined,
205
+ );
206
+ case "listMarketplaces":
207
+ return this.listMarketplaces(p.workdir as string | undefined);
208
+ case "addMarketplace":
209
+ return this.addMarketplace(
210
+ p.input as string,
211
+ p.scope as Scope | undefined,
212
+ p.workdir as string | undefined,
213
+ );
214
+ case "removeMarketplace":
215
+ return this.removeMarketplace(
216
+ p.name as string,
217
+ p.scope as Scope | undefined,
218
+ p.workdir as string | undefined,
219
+ );
220
+ case "updateMarketplace":
221
+ return this.updateMarketplace(
222
+ p.name as string | undefined,
223
+ p.workdir as string | undefined,
224
+ );
225
+
226
+ default:
227
+ throw new RpcError(
228
+ PROTOCOL_METHOD_NOT_FOUND,
229
+ `Method not found: ${method}`,
230
+ );
231
+ }
232
+ }
233
+
234
+ handleNotification(method: string, params: unknown): void {
235
+ if (method === "permissionResponse") {
236
+ const p = params as {
237
+ requestId: string;
238
+ decision: PermissionDecision;
239
+ };
240
+ const resolve = this.pendingPermissions.get(p.requestId);
241
+ if (resolve) {
242
+ this.pendingPermissions.delete(p.requestId);
243
+ resolve(p.decision);
244
+ }
245
+ }
246
+ }
247
+
248
+ // ── Lifecycle ─────────────────────────────────────────────────
249
+
250
+ private async initialize(params: InitializeParams): Promise<{
251
+ sessionId: string;
252
+ workingDirectory: string;
253
+ permissionMode: PermissionMode;
254
+ latestTotalTokens: number;
255
+ }> {
256
+ // Merge with stored config (CLI defaults can be overridden by client)
257
+ this.storedConfig = { ...this.storedConfig, ...params };
258
+
259
+ const callbacks = this.createCallbacks();
260
+ const options: AgentOptions = {
261
+ callbacks,
262
+ workdir: params.workdir,
263
+ restoreSessionId: params.restoreSessionId,
264
+ apiKey: params.apiKey,
265
+ baseURL: params.baseURL,
266
+ defaultHeaders: params.defaultHeaders,
267
+ model: params.model,
268
+ fastModel: params.fastModel,
269
+ language: params.language,
270
+ permissionMode: params.permissionMode,
271
+ tools: params.tools,
272
+ allowedTools: params.allowedTools,
273
+ disallowedTools: params.disallowedTools,
274
+ plugins: params.pluginDirs?.map((path) => ({ type: "local", path })),
275
+ mcpServers: params.mcpServers,
276
+ canUseTool: (context: ToolPermissionContext) => this.canUseTool(context),
277
+ };
278
+
279
+ this.agent = await Agent.create(options);
280
+
281
+ return {
282
+ sessionId: this.agent.sessionId,
283
+ workingDirectory: this.agent.workingDirectory,
284
+ permissionMode: this.agent.getPermissionMode(),
285
+ latestTotalTokens: this.agent.latestTotalTokens,
286
+ };
287
+ }
288
+
289
+ private async destroy(): Promise<null> {
290
+ if (this.agent) {
291
+ await this.agent.destroy();
292
+ this.agent = undefined;
293
+ }
294
+ return null;
295
+ }
296
+
297
+ private async restoreSession(sessionId: string): Promise<null> {
298
+ this.requireAgent();
299
+ await this.agent!.restoreSession(sessionId);
300
+ return null;
301
+ }
302
+
303
+ private async listSessions(
304
+ workdir?: string,
305
+ ): Promise<{ sessions: SessionMetadata[] }> {
306
+ const sessions = await listSessions(
307
+ workdir || this.agent?.workingDirectory || process.cwd(),
308
+ );
309
+ return { sessions };
310
+ }
311
+
312
+ private getSessionInfo(): {
313
+ sessionId: string;
314
+ workingDirectory: string;
315
+ latestTotalTokens: number;
316
+ permissionMode: PermissionMode;
317
+ availableTools: string[];
318
+ } {
319
+ this.requireAgent();
320
+ return {
321
+ sessionId: this.agent!.sessionId,
322
+ workingDirectory: this.agent!.workingDirectory,
323
+ latestTotalTokens: this.agent!.latestTotalTokens,
324
+ permissionMode: this.agent!.getPermissionMode(),
325
+ availableTools: this.agent!.getAvailableToolNames(),
326
+ };
327
+ }
328
+
329
+ private async updateConfig(
330
+ params: UpdateConfigParams,
331
+ ): Promise<{ sessionId: string }> {
332
+ this.requireAgent();
333
+ const currentSessionId = this.agent!.sessionId;
334
+ // Merge new config into stored config
335
+ this.storedConfig = { ...this.storedConfig, ...params };
336
+ // Destroy and recreate
337
+ await this.agent!.destroy();
338
+ this.agent = undefined;
339
+ await this.initialize({
340
+ ...this.storedConfig,
341
+ restoreSessionId: currentSessionId,
342
+ });
343
+ return { sessionId: this.agent!.sessionId };
344
+ }
345
+
346
+ // ── Messages ──────────────────────────────────────────────────
347
+
348
+ private async sendMessage(params: {
349
+ text: string;
350
+ images?: Array<{ path: string; mimeType: string }>;
351
+ force?: boolean;
352
+ }): Promise<null> {
353
+ this.requireAgent();
354
+ if (params.force) {
355
+ this.agent!.abortMessage();
356
+ }
357
+ // Save prompt to history (mirrors VSCE chatSession.ts:236-242)
358
+ try {
359
+ await PromptHistoryManager.addEntry(
360
+ params.text,
361
+ this.agent!.sessionId,
362
+ {},
363
+ this.agent!.workingDirectory,
364
+ );
365
+ } catch {
366
+ // Best-effort; don't block message sending on history save failure
367
+ }
368
+ await this.agent!.sendMessage(params.text, params.images);
369
+ return null;
370
+ }
371
+
372
+ private async bang(command: string): Promise<null> {
373
+ this.requireAgent();
374
+ await this.agent!.bang(command);
375
+ return null;
376
+ }
377
+
378
+ private async abortMessage(): Promise<null> {
379
+ this.requireAgent();
380
+ this.agent!.abortMessage();
381
+ return null;
382
+ }
383
+
384
+ private async clearMessages(): Promise<null> {
385
+ this.requireAgent();
386
+ this.agent!.clearMessages();
387
+ return null;
388
+ }
389
+
390
+ private async rewindToMessage(messageId: string): Promise<{
391
+ inputContent: string;
392
+ }> {
393
+ this.requireAgent();
394
+ const { messages } = await this.agent!.getFullMessageThread();
395
+ const index = messages.findIndex((m) => m.id === messageId);
396
+ if (index === -1) {
397
+ throw new RpcError(
398
+ PROTOCOL_INTERNAL_ERROR,
399
+ `Message not found: ${messageId}`,
400
+ );
401
+ }
402
+ const message = messages[index];
403
+ const textBlock = message.blocks.find((b) => b.type === "text") as
404
+ | { content?: string }
405
+ | undefined;
406
+ await this.agent!.truncateHistory(index);
407
+ return { inputContent: textBlock?.content || "" };
408
+ }
409
+
410
+ private deleteQueuedMessage(index: number): null {
411
+ this.requireAgent();
412
+ this.agent!.removeQueuedMessage(index);
413
+ return null;
414
+ }
415
+
416
+ private getMessages(): { messages: Message[] } {
417
+ this.requireAgent();
418
+ return { messages: this.agent!.messages };
419
+ }
420
+
421
+ private async getFullMessageThread(): Promise<{
422
+ messages: Message[];
423
+ sessionIds: string[];
424
+ }> {
425
+ this.requireAgent();
426
+ return this.agent!.getFullMessageThread();
427
+ }
428
+
429
+ // ── Permissions ───────────────────────────────────────────────
430
+
431
+ private async setPermissionMode(mode: PermissionMode): Promise<null> {
432
+ this.requireAgent();
433
+ await this.agent!.setPermissionMode(mode);
434
+ return null;
435
+ }
436
+
437
+ private getPermissionMode(): { mode: PermissionMode } {
438
+ this.requireAgent();
439
+ return { mode: this.agent!.getPermissionMode() };
440
+ }
441
+
442
+ // ── MCP ───────────────────────────────────────────────────────
443
+
444
+ private getMcpServers(): { servers: McpServerStatus[] } {
445
+ this.requireAgent();
446
+ return { servers: this.agent!.getMcpServers() };
447
+ }
448
+
449
+ private async connectMcpServer(
450
+ serverName: string,
451
+ ): Promise<{ success: boolean }> {
452
+ this.requireAgent();
453
+ const success = await this.agent!.connectMcpServer(serverName);
454
+ return { success };
455
+ }
456
+
457
+ private async disconnectMcpServer(
458
+ serverName: string,
459
+ ): Promise<{ success: boolean }> {
460
+ this.requireAgent();
461
+ const success = await this.agent!.disconnectMcpServer(serverName);
462
+ return { success };
463
+ }
464
+
465
+ // ── Commands ──────────────────────────────────────────────────
466
+
467
+ private getSlashCommands(): { commands: SlashCommand[] } {
468
+ this.requireAgent();
469
+ return { commands: this.agent!.getSlashCommands() };
470
+ }
471
+
472
+ // ── File / History ────────────────────────────────────────────
473
+
474
+ private async searchFiles(
475
+ params: SearchFilesParams,
476
+ ): Promise<{ files: Awaited<ReturnType<typeof searchFiles>> }> {
477
+ const files = await searchFiles(params.query, {
478
+ maxResults: params.maxResults,
479
+ workingDirectory:
480
+ params.workdir || this.agent?.workingDirectory || process.cwd(),
481
+ });
482
+ return { files };
483
+ }
484
+
485
+ private async getPromptHistory(workdir?: string): Promise<{
486
+ history: Awaited<ReturnType<typeof PromptHistoryManager.getHistory>>;
487
+ }> {
488
+ const history = await PromptHistoryManager.getHistory({
489
+ workdir: workdir || this.agent?.workingDirectory,
490
+ });
491
+ return { history };
492
+ }
493
+
494
+ private async searchPromptHistory(
495
+ query: string,
496
+ workdir?: string,
497
+ ): Promise<{
498
+ history: Awaited<ReturnType<typeof PromptHistoryManager.searchHistory>>;
499
+ }> {
500
+ const history = await PromptHistoryManager.searchHistory(query, {
501
+ workdir: workdir || this.agent?.workingDirectory,
502
+ });
503
+ return { history };
504
+ }
505
+
506
+ // ── canUseTool flow ───────────────────────────────────────────
507
+
508
+ private canUseTool(
509
+ context: ToolPermissionContext,
510
+ ): Promise<PermissionDecision> {
511
+ const requestId = `perm_${++this.permissionCounter}`;
512
+ return new Promise<PermissionDecision>((resolve) => {
513
+ this.pendingPermissions.set(requestId, resolve);
514
+ this.emit("permissionRequest", { requestId, context });
515
+
516
+ // 5-minute timeout → auto-deny
517
+ setTimeout(
518
+ () => {
519
+ if (this.pendingPermissions.has(requestId)) {
520
+ this.pendingPermissions.delete(requestId);
521
+ resolve({
522
+ behavior: "deny",
523
+ message: "Permission request timed out",
524
+ });
525
+ }
526
+ },
527
+ 5 * 60 * 1000,
528
+ );
529
+ });
530
+ }
531
+
532
+ // ── Auth ─────────────────────────────────────────────────────
533
+
534
+ private async getAuthStatus(): Promise<{
535
+ isAuthenticated: boolean;
536
+ user: { id: string; email?: string } | undefined;
537
+ }> {
538
+ const authService = AuthService.getInstance();
539
+ return {
540
+ isAuthenticated: authService.isSSOAuthenticated(),
541
+ user: authService.getAuthUser(),
542
+ };
543
+ }
544
+
545
+ private async login(
546
+ serverUrl?: string,
547
+ ): Promise<{ user: { id: string; email?: string } | undefined }> {
548
+ const authService = AuthService.getInstance();
549
+ await authService.login({
550
+ onAuthUrl: (url: string) => {
551
+ this.emit("authUrl", { url });
552
+ },
553
+ serverUrl,
554
+ });
555
+ return { user: authService.getAuthUser() };
556
+ }
557
+
558
+ private async logout(): Promise<null> {
559
+ const authService = AuthService.getInstance();
560
+ await authService.clearAuth();
561
+ return null;
562
+ }
563
+
564
+ // ── Plugins ──────────────────────────────────────────────────
565
+
566
+ private getPluginCore(workdir?: string): PluginCore {
567
+ const resolvedWorkdir =
568
+ workdir || this.agent?.workingDirectory || process.cwd();
569
+ if (!this.pluginCore || this.pluginCoreWorkdir !== resolvedWorkdir) {
570
+ this.pluginCore = new PluginCore(resolvedWorkdir);
571
+ this.pluginCoreWorkdir = resolvedWorkdir;
572
+ }
573
+ return this.pluginCore;
574
+ }
575
+
576
+ private async listPlugins(workdir?: string) {
577
+ const core = this.getPluginCore(workdir);
578
+ const { plugins, mergedEnabled } = await core.listPlugins();
579
+ return {
580
+ plugins: plugins.map((p) => {
581
+ const pluginId = `${p.name}@${p.marketplace}`;
582
+ return {
583
+ id: pluginId,
584
+ name: p.name,
585
+ description: p.description,
586
+ marketplace: p.marketplace,
587
+ installed: p.installed,
588
+ version: p.version,
589
+ enabled: mergedEnabled[pluginId] !== false,
590
+ scope: p.scope,
591
+ };
592
+ }),
593
+ };
594
+ }
595
+
596
+ private async installPlugin(
597
+ pluginId: string,
598
+ scope?: Scope,
599
+ workdir?: string,
600
+ ) {
601
+ return this.getPluginCore(workdir).installPlugin(pluginId, scope);
602
+ }
603
+
604
+ private async uninstallPlugin(pluginId: string, workdir?: string) {
605
+ await this.getPluginCore(workdir).uninstallPlugin(pluginId);
606
+ return null;
607
+ }
608
+
609
+ private async enablePlugin(
610
+ pluginId: string,
611
+ scope?: Scope,
612
+ workdir?: string,
613
+ ) {
614
+ return this.getPluginCore(workdir).enablePlugin(pluginId, scope);
615
+ }
616
+
617
+ private async disablePlugin(
618
+ pluginId: string,
619
+ scope?: Scope,
620
+ workdir?: string,
621
+ ) {
622
+ return this.getPluginCore(workdir).disablePlugin(pluginId, scope);
623
+ }
624
+
625
+ private async updatePlugin(pluginId: string, workdir?: string) {
626
+ return this.getPluginCore(workdir).updatePlugin(pluginId);
627
+ }
628
+
629
+ private async listMarketplaces(workdir?: string) {
630
+ return this.getPluginCore(workdir).listMarketplaces();
631
+ }
632
+
633
+ private async addMarketplace(input: string, scope?: Scope, workdir?: string) {
634
+ return this.getPluginCore(workdir).addMarketplace(input, scope);
635
+ }
636
+
637
+ private async removeMarketplace(
638
+ name: string,
639
+ scope?: Scope,
640
+ workdir?: string,
641
+ ) {
642
+ await this.getPluginCore(workdir).removeMarketplace(name, scope);
643
+ return null;
644
+ }
645
+
646
+ private async updateMarketplace(name?: string, workdir?: string) {
647
+ await this.getPluginCore(workdir).updateMarketplace(name);
648
+ return null;
649
+ }
650
+
651
+ // ── Callbacks → Notifications ─────────────────────────────────
652
+
653
+ private createCallbacks(): AgentCallbacks {
654
+ return {
655
+ onMessagesChange: (messages: Message[]) => {
656
+ this.emit("messagesChange", { messages });
657
+ },
658
+ onUserMessageAdded: () => {
659
+ const msg = this.findLastUserMessage();
660
+ if (msg) this.emit("userMessageAdded", { message: msg });
661
+ },
662
+ onAssistantMessageAdded: (messageId: string) => {
663
+ const msg = this.agent?.messages.find((m) => m.id === messageId);
664
+ if (msg) this.emit("assistantMessageAdded", { message: msg });
665
+ },
666
+ onAssistantContentUpdated: (params) => {
667
+ this.emit("assistantContentUpdated", params);
668
+ },
669
+ onAssistantReasoningUpdated: (params) => {
670
+ this.emit("assistantReasoningUpdated", params);
671
+ },
672
+ onToolBlockUpdated: (params) => {
673
+ this.emit("toolBlockUpdated", params);
674
+ },
675
+ onErrorBlockAdded: (error: string) => {
676
+ this.emit("errorBlockAdded", { error });
677
+ },
678
+ onLoadingChange: (loading: boolean) => {
679
+ this.emit("loadingChange", {
680
+ loading,
681
+ latestTotalTokens: this.agent?.latestTotalTokens,
682
+ });
683
+ },
684
+ onCommandRunningChange: (running: boolean) => {
685
+ this.emit("commandRunningChange", { running });
686
+ },
687
+ onQueuedMessagesChange: (messages: QueuedMessage[]) => {
688
+ this.emit("queuedMessagesChange", { messages });
689
+ },
690
+ onTasksChange: (tasks: Task[]) => {
691
+ this.emit("tasksChange", { tasks });
692
+ },
693
+ onSessionIdChange: (sessionId: string) => {
694
+ this.emit("sessionIdChange", { sessionId });
695
+ },
696
+ onPermissionModeChange: (mode: PermissionMode) => {
697
+ this.emit("permissionModeChange", { mode });
698
+ },
699
+ onMcpServersChange: (servers: McpServerStatus[]) => {
700
+ this.emit("mcpServersChange", { servers });
701
+ },
702
+ onAddBangMessage: () => {
703
+ this.emit("bangMessageAdded", {});
704
+ },
705
+ onUpdateBangMessage: () => {
706
+ this.emit("bangMessageUpdated", {});
707
+ },
708
+ onCompleteBangMessage: () => {
709
+ this.emit("bangMessageCompleted", {});
710
+ },
711
+ onNotificationMessageAdded: (params) => {
712
+ const msg = this.agent?.messages.find(
713
+ (m) =>
714
+ m.role === "user" &&
715
+ m.blocks.some(
716
+ (b) =>
717
+ b.type === "task_notification" &&
718
+ (b as { taskId: string }).taskId === params.taskId,
719
+ ),
720
+ );
721
+ this.emit("notificationMessageAdded", {
722
+ ...params,
723
+ message: msg,
724
+ });
725
+ },
726
+ };
727
+ }
728
+
729
+ private findLastUserMessage(): Message | undefined {
730
+ const userMessages =
731
+ this.agent?.messages.filter((m) => m.role === "user") ?? [];
732
+ return userMessages[userMessages.length - 1];
733
+ }
734
+
735
+ // ── Utils ─────────────────────────────────────────────────────
736
+
737
+ private requireAgent(): void {
738
+ if (!this.agent) {
739
+ throw new RpcError(PROTOCOL_INTERNAL_ERROR, "Agent not initialized");
740
+ }
741
+ }
742
+ }
743
+
744
+ // ── Error class for protocol errors ─────────────────────────────
745
+
746
+ export class RpcError extends Error {
747
+ code: number;
748
+ constructor(code: number, message: string) {
749
+ super(message);
750
+ this.code = code;
751
+ }
752
+
753
+ toJsonRpcError(): JsonRpcError {
754
+ return { code: this.code, message: this.message };
755
+ }
756
+ }