agent-gear 1.1.5 → 1.1.7

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.
@@ -29,7 +29,7 @@ export class AcpSpawner {
29
29
 
30
30
  start(): void {
31
31
  const cmd = this.resolveCommand();
32
- console.log(`Starting ACP server ${cmd}`)
32
+ console.log(`Starting ACP server: session=${this.sessionId} cmd=${cmd} args=${JSON.stringify(this.args)} cwd=${this.cwd}`);
33
33
  mkdirSync(this.cwd, { recursive: true });
34
34
 
35
35
  const spawnOptions: SpawnOptions = {
@@ -43,17 +43,17 @@ export class AcpSpawner {
43
43
  this.process = spawn(cmd, this.args, spawnOptions);
44
44
 
45
45
  this.process.stderr?.on('data', (data: Buffer) => {
46
- console.error(`ACP stderr: ${data.toString()}`);
46
+ console.error(`ACP stderr (session=${this.sessionId}): ${data.toString()}`);
47
47
  });
48
48
 
49
49
  this.process.on('exit', (code, signal) => {
50
- console.log(`ACP process exited with code ${code} and signal ${signal}`);
50
+ console.log(`ACP process exited: session=${this.sessionId} code=${code} signal=${signal} cmd=${cmd} args=${JSON.stringify(this.args)}`);
51
51
  this.process = null;
52
52
  this.resolveClosed?.();
53
53
  });
54
54
 
55
55
  this.process.on('error', (error) => {
56
- console.error('ACP process error:', error);
56
+ console.error(`ACP process error (session=${this.sessionId}, cmd=${cmd}):`, error);
57
57
  });
58
58
  }
59
59
 
@@ -29,6 +29,7 @@ export class AcpSyncBridge implements acp.Client {
29
29
  private spawner: AcpSpawner;
30
30
  private connection: acp.ClientSideConnection | null = null;
31
31
  private updates: acp.SessionNotification[] = [];
32
+ private capabilities: acp.AgentCapabilities | null = null;
32
33
 
33
34
  constructor(options: AcpSyncBridgeOptions, spawner?: AcpSpawner) {
34
35
  this.spawner = spawner ?? new AcpSpawner({
@@ -53,15 +54,30 @@ export class AcpSyncBridge implements acp.Client {
53
54
  clientInfo: { name: 'agent-gear', version: '1.0.0' },
54
55
  });
55
56
 
56
- await withTimeout(
57
+ const result = await withTimeout(
57
58
  Promise.race([initPromise, exitPromise]),
58
59
  timeoutMs,
59
60
  `ACP connect timed out after ${timeoutMs}ms`,
60
61
  );
62
+ this.capabilities = result.agentCapabilities ?? null;
61
63
  }
62
64
 
63
- async listSessions(cwd: string): Promise<acp.ListSessionsResponse> {
64
- return this.connection!.listSessions({ cwd });
65
+ supportsDelete(): boolean {
66
+ return Boolean(this.capabilities?.sessionCapabilities?.delete);
67
+ }
68
+
69
+ async listSessions(cwd: string, cursor?: string | null): Promise<acp.ListSessionsResponse> {
70
+ if (!this.connection) {
71
+ throw new Error('ACP connection not established');
72
+ }
73
+ return this.connection.listSessions({ cwd, ...(cursor ? { cursor } : {}) });
74
+ }
75
+
76
+ async deleteSession(sessionId: string): Promise<void> {
77
+ if (!this.connection) {
78
+ throw new Error('ACP connection not established');
79
+ }
80
+ await this.connection.deleteSession({ sessionId });
65
81
  }
66
82
 
67
83
  async loadSession(
@@ -108,4 +124,12 @@ export class AcpSyncBridge implements acp.Client {
108
124
  async requestPermission(): Promise<acp.RequestPermissionResponse> {
109
125
  return { outcome: { outcome: 'cancelled' } };
110
126
  }
127
+
128
+ async unstable_createElicitation(): Promise<acp.CreateElicitationResponse> {
129
+ return { action: 'cancel' };
130
+ }
131
+
132
+ async unstable_completeElicitation(): Promise<void> {
133
+ // no-op
134
+ }
111
135
  }
@@ -7,6 +7,92 @@ interface McpProxyOptions {
7
7
  command: string;
8
8
  }
9
9
 
10
+ interface JsonRpcRequest {
11
+ jsonrpc?: string;
12
+ id?: unknown;
13
+ method?: string;
14
+ [key: string]: unknown;
15
+ }
16
+
17
+ const FORWARD_TIMEOUT_MS = 30_000;
18
+
19
+ // JSON-RPC notifications have no id and MUST NOT be waited on: per the MCP
20
+ // spec the server never responds to them. Waiting would stall every hop of
21
+ // the forward chain for the full timeout.
22
+ function isNotification(request: JsonRpcRequest): boolean {
23
+ return request.id === undefined || request.id === null;
24
+ }
25
+
26
+ // Persistent line-buffered JSON-RPC dispatcher over a child process' stdio.
27
+ // Responses are matched to requests by id, so chunked responses (a single
28
+ // 'data' event is NOT guaranteed to contain exactly one complete JSON
29
+ // message) and concurrent in-flight requests are handled correctly.
30
+ export function createStdioDispatcher(child: ChildProcess) {
31
+ let buffer = '';
32
+ const pending = new Map<unknown, { resolve: (value: unknown) => void; reject: (reason: Error) => void; timeout: NodeJS.Timeout; method: string }>();
33
+
34
+ child.stdout?.on('data', (data: Buffer) => {
35
+ buffer += data.toString();
36
+ let idx;
37
+ while ((idx = buffer.indexOf('\n')) >= 0) {
38
+ const line = buffer.slice(0, idx).trim();
39
+ buffer = buffer.slice(idx + 1);
40
+ if (!line) continue;
41
+ let msg: { id?: unknown; error?: { message?: string } };
42
+ try {
43
+ msg = JSON.parse(line);
44
+ } catch {
45
+ console.error(`[MCP proxy] ignoring non-JSON line from child: ${line.slice(0, 200)}`);
46
+ continue;
47
+ }
48
+ const entry = pending.get(msg.id);
49
+ if (!entry) {
50
+ console.error(`[MCP proxy] ignoring response with unknown id: ${JSON.stringify(msg.id)}`);
51
+ continue;
52
+ }
53
+ clearTimeout(entry.timeout);
54
+ pending.delete(msg.id);
55
+ console.log(`[MCP proxy] response: method=${entry.method} id=${JSON.stringify(msg.id)}${msg.error ? ` error=${msg.error.message}` : ''}`);
56
+ entry.resolve(msg);
57
+ }
58
+ });
59
+
60
+ const rejectAll = (reason: Error) => {
61
+ for (const [, entry] of pending) {
62
+ clearTimeout(entry.timeout);
63
+ entry.reject(reason);
64
+ }
65
+ pending.clear();
66
+ };
67
+
68
+ child.on('exit', (code) => {
69
+ console.log(`[MCP proxy] MCP command exited with code ${code}, rejecting ${pending.size} pending request(s)`);
70
+ rejectAll(new Error(`MCP command exited with code ${code}`));
71
+ });
72
+
73
+ return {
74
+ forward(request: JsonRpcRequest): Promise<unknown> {
75
+ const method = request.method ?? '(unknown)';
76
+ if (isNotification(request)) {
77
+ // Fire-and-forget: deliver the notification (e.g. notifications/initialized
78
+ // completes the MCP handshake) but do not wait for a response.
79
+ console.log(`[MCP proxy] forwarding notification: method=${method}`);
80
+ child.stdin?.write(JSON.stringify(request) + '\n');
81
+ return Promise.resolve(null);
82
+ }
83
+ console.log(`[MCP proxy] forwarding request: method=${method} id=${JSON.stringify(request.id)}`);
84
+ return new Promise((resolve, reject) => {
85
+ const timeout = setTimeout(() => {
86
+ pending.delete(request.id);
87
+ reject(new Error(`Request timeout waiting for ${method}`));
88
+ }, FORWARD_TIMEOUT_MS);
89
+ pending.set(request.id, { resolve, reject, timeout, method });
90
+ child.stdin?.write(JSON.stringify(request) + '\n');
91
+ });
92
+ },
93
+ };
94
+ }
95
+
10
96
  export async function startMcpRegistration(options: McpProxyOptions): Promise<void> {
11
97
  const { server, key, command } = options;
12
98
 
@@ -45,10 +131,6 @@ export async function startMcpRegistration(options: McpProxyOptions): Promise<vo
45
131
  console.error('Failed to start MCP command:', err.message);
46
132
  });
47
133
 
48
- child.on('exit', (code) => {
49
- console.log(`MCP command exited with code ${code}`);
50
- });
51
-
52
134
  // 3. Establish WebSocket connection
53
135
  const socket = ioClient(`${server}/mcp-proxy`, {
54
136
  transports: ['websocket'],
@@ -69,16 +151,18 @@ export async function startMcpRegistration(options: McpProxyOptions): Promise<vo
69
151
  });
70
152
 
71
153
  // 4. Bridge requests
72
- socket.on('proxy:request', async (data: { requestId: string; request: any }) => {
154
+ const dispatcher = createStdioDispatcher(child);
155
+ socket.on('proxy:request', async (data: { requestId: string; request: JsonRpcRequest }) => {
73
156
  try {
74
- const response = await forwardToChild(child, data.request);
157
+ const response = await dispatcher.forward(data.request);
75
158
  socket.emit('proxy:response', { requestId: data.requestId, response });
76
159
  } catch (err: any) {
160
+ console.error(`[MCP proxy] forward failed: method=${data.request?.method} id=${JSON.stringify(data.request?.id)} error=${err.message}`);
77
161
  socket.emit('proxy:response', {
78
162
  requestId: data.requestId,
79
163
  response: {
80
164
  jsonrpc: '2.0',
81
- id: data.request.id,
165
+ id: data.request.id ?? null,
82
166
  error: { code: -32603, message: err.message },
83
167
  },
84
168
  });
@@ -105,24 +189,3 @@ export async function startMcpRegistration(options: McpProxyOptions): Promise<vo
105
189
  process.on('SIGINT', shutdown);
106
190
  process.on('SIGTERM', shutdown);
107
191
  }
108
-
109
- function forwardToChild(child: ChildProcess, request: any): Promise<any> {
110
- return new Promise((resolve, reject) => {
111
- const timeout = setTimeout(() => {
112
- reject(new Error('Request timeout'));
113
- }, 30000);
114
-
115
- const onData = (data: Buffer) => {
116
- clearTimeout(timeout);
117
- child.stdout?.removeListener('data', onData);
118
- try {
119
- resolve(JSON.parse(data.toString()));
120
- } catch {
121
- reject(new Error('Invalid JSON response from MCP command'));
122
- }
123
- };
124
-
125
- child.stdout?.on('data', onData);
126
- child.stdin?.write(JSON.stringify(request) + '\n');
127
- });
128
- }
@@ -18,6 +18,7 @@ import { gatewayManager } from './gateway.js';
18
18
  const CONTROLLER_VERSION = '1.1.1';
19
19
  const acpBridges = new Map<string, AcpSessionBridge>();
20
20
  const permissionRequestBridge = new Map<string, AcpSessionBridge>();
21
+ const elicitationRequestBridge = new Map<string, AcpSessionBridge>();
21
22
 
22
23
  function createSpawner(serverType: string, sessionId: string, cwd: string): AcpSpawner {
23
24
  const config = acpDetector.getSpawnerConfig(serverType);
@@ -48,11 +49,26 @@ interface AcpStartPayload {
48
49
  serverType: string;
49
50
  cwd: string;
50
51
  mcpServers?: Array<{ name: string; command: string; args?: string[]; env?: Array<{ name: string; value: string }> }>;
52
+ mcpEndpoint?: { url: string; token: string; serverNames: string[] };
53
+ additionalDirectories?: string[];
51
54
  }
52
55
 
53
56
  interface AcpMessagePayload {
54
57
  sessionId: string;
55
- message: { type: string; content: string };
58
+ message: { type: string; content: string; attachments?: Array<Record<string, unknown>> };
59
+ }
60
+
61
+ // Summarize the MCP configuration of an acp:start/acp:resume payload for logs,
62
+ // without leaking the endpoint token.
63
+ function describeMcpConfig(data: { mcpServers?: unknown[]; mcpEndpoint?: { url: string; serverNames?: string[] } }): string {
64
+ const parts: string[] = [];
65
+ if (data.mcpServers && data.mcpServers.length > 0) {
66
+ parts.push(`mcpServers=${data.mcpServers.length}`);
67
+ }
68
+ if (data.mcpEndpoint) {
69
+ parts.push(`mcpEndpoint(url=${data.mcpEndpoint.url}, serverNames=${JSON.stringify(data.mcpEndpoint.serverNames ?? null)})`);
70
+ }
71
+ return parts.length > 0 ? `mcp=[${parts.join(' ')}]` : 'mcp=[none]';
56
72
  }
57
73
 
58
74
  interface AcpCancelPayload {
@@ -63,6 +79,29 @@ interface AcpStopPayload {
63
79
  sessionId: string;
64
80
  }
65
81
 
82
+ interface AcpSetConfigOptionPayload {
83
+ sessionId: string;
84
+ configId: string;
85
+ value: string | boolean;
86
+ }
87
+
88
+ interface AcpDeleteHistorySessionPayload {
89
+ requestId: string;
90
+ sessionId: string;
91
+ externalSessionId: string;
92
+ cwd: string;
93
+ serverType?: string;
94
+ }
95
+
96
+ interface AcpAuthenticatePayload {
97
+ sessionId: string;
98
+ methodId: string;
99
+ }
100
+
101
+ interface AcpLogoutPayload {
102
+ sessionId: string;
103
+ }
104
+
66
105
  interface AcpSetModePayload {
67
106
  sessionId: string;
68
107
  modeId: string;
@@ -79,12 +118,15 @@ interface AcpResumePayload {
79
118
  externalSessionId: string;
80
119
  serverType: string;
81
120
  cwd: string;
121
+ mcpEndpoint?: { url: string; token: string; serverNames: string[] };
122
+ additionalDirectories?: string[];
82
123
  }
83
124
 
84
125
  interface AcpListSessionsPayload {
85
126
  requestId: string;
86
127
  serverType: string;
87
128
  cwd: string;
129
+ cursor?: string | null;
88
130
  }
89
131
 
90
132
  interface AcpLoadSessionPayload {
@@ -218,7 +260,7 @@ export function connectToServer(
218
260
  });
219
261
 
220
262
  socket.on('acp:start', async (data: AcpStartPayload) => {
221
- console.log(`[ACP ctrl] acp:start received: requestId=${data.requestId} sessionId=${data.sessionId} serverType=${data.serverType} cwd=${data.cwd}`);
263
+ console.log(`[ACP ctrl] acp:start received: requestId=${data.requestId} sessionId=${data.sessionId} serverType=${data.serverType} cwd=${data.cwd} ${describeMcpConfig(data)}`);
222
264
 
223
265
  try {
224
266
  const spawner = createSpawner(data.serverType, data.sessionId, data.cwd);
@@ -226,6 +268,8 @@ export function connectToServer(
226
268
  sessionId: data.sessionId,
227
269
  cwd: data.cwd,
228
270
  mcpServers: data.mcpServers,
271
+ mcpEndpoint: data.mcpEndpoint,
272
+ additionalDirectories: data.additionalDirectories,
229
273
  }, spawner);
230
274
 
231
275
  acpBridges.set(data.sessionId, bridge);
@@ -234,6 +278,10 @@ export function connectToServer(
234
278
  permissionRequestBridge.set(requestId, bridge);
235
279
  });
236
280
 
281
+ bridge.on('elicitation:sent', (requestId: string) => {
282
+ elicitationRequestBridge.set(requestId, bridge);
283
+ });
284
+
237
285
  const onInitialized = (payload: Record<string, unknown>) => {
238
286
  if (payload.sessionId !== data.sessionId) return;
239
287
  bridge.off('acp:initialized', onInitialized);
@@ -250,6 +298,13 @@ export function connectToServer(
250
298
  if (payload.currentModel) result.currentModel = payload.currentModel;
251
299
  result.availableModels = payload.availableModels ?? [];
252
300
  if (payload.currentModelId) result.currentModelId = payload.currentModelId;
301
+ result.configOptions = payload.configOptions ?? [];
302
+ if (payload.promptCapabilities) result.promptCapabilities = payload.promptCapabilities;
303
+ if (payload.agentInfo) result.agentInfo = payload.agentInfo;
304
+ if (payload.authMethods) result.authMethods = payload.authMethods;
305
+ if (payload.supportsAdditionalDirectories != null) {
306
+ result.supportsAdditionalDirectories = payload.supportsAdditionalDirectories;
307
+ }
253
308
  socket.emit('acp:start:result', result);
254
309
  };
255
310
 
@@ -258,7 +313,7 @@ export function connectToServer(
258
313
  bridge.off('acp:initialized', onInitialized);
259
314
  bridge.off('acp:stopped', onStopped);
260
315
  acpBridges.delete(data.sessionId);
261
- console.log(`[ACP ctrl] emitting acp:start:result: requestId=${data.requestId} sessionId=${data.sessionId} success=false`);
316
+ console.log(`[ACP ctrl] emitting acp:start:result: requestId=${data.requestId} sessionId=${data.sessionId} success=false reason=${payload.reason} error=${payload.error ?? 'n/a'}`);
262
317
  socket.emit('acp:start:result', {
263
318
  requestId: data.requestId,
264
319
  sessionId: data.sessionId,
@@ -284,13 +339,15 @@ export function connectToServer(
284
339
  });
285
340
 
286
341
  socket.on('acp:resume', async (data: AcpResumePayload) => {
287
- console.log(`[ACP ctrl] acp:resume received: requestId=${data.requestId} sessionId=${data.sessionId} serverType=${data.serverType} externalSessionId=${data.externalSessionId}`);
342
+ console.log(`[ACP ctrl] acp:resume received: requestId=${data.requestId} sessionId=${data.sessionId} serverType=${data.serverType} externalSessionId=${data.externalSessionId} ${describeMcpConfig(data)}`);
288
343
 
289
344
  try {
290
345
  const spawner = createSpawner(data.serverType, data.sessionId, data.cwd);
291
346
  const bridge = new AcpSessionBridge(socket, {
292
347
  sessionId: data.sessionId,
293
348
  cwd: data.cwd,
349
+ mcpEndpoint: data.mcpEndpoint,
350
+ additionalDirectories: data.additionalDirectories,
294
351
  }, spawner);
295
352
 
296
353
  acpBridges.set(data.sessionId, bridge);
@@ -299,6 +356,10 @@ export function connectToServer(
299
356
  permissionRequestBridge.set(requestId, bridge);
300
357
  });
301
358
 
359
+ bridge.on('elicitation:sent', (requestId: string) => {
360
+ elicitationRequestBridge.set(requestId, bridge);
361
+ });
362
+
302
363
  const onInitialized = (payload: Record<string, unknown>) => {
303
364
  if (payload.sessionId !== data.sessionId) return;
304
365
  bridge.off('acp:initialized', onInitialized);
@@ -313,6 +374,10 @@ export function connectToServer(
313
374
  if (payload.currentModel) result.currentModel = payload.currentModel;
314
375
  result.availableModels = payload.availableModels ?? [];
315
376
  if (payload.currentModelId) result.currentModelId = payload.currentModelId;
377
+ result.configOptions = payload.configOptions ?? [];
378
+ if (payload.promptCapabilities) result.promptCapabilities = payload.promptCapabilities;
379
+ if (payload.agentInfo) result.agentInfo = payload.agentInfo;
380
+ if (payload.authMethods) result.authMethods = payload.authMethods;
316
381
  socket.emit('acp:resume:result', result);
317
382
  };
318
383
 
@@ -321,6 +386,7 @@ export function connectToServer(
321
386
  bridge.off('acp:initialized', onInitialized);
322
387
  bridge.off('acp:stopped', onStopped);
323
388
  acpBridges.delete(data.sessionId);
389
+ console.log(`[ACP ctrl] emitting acp:resume:result: requestId=${data.requestId} sessionId=${data.sessionId} success=false reason=${payload.reason} error=${payload.error ?? 'n/a'}`);
324
390
  socket.emit('acp:resume:result', {
325
391
  requestId: data.requestId,
326
392
  sessionId: data.sessionId,
@@ -351,7 +417,7 @@ export function connectToServer(
351
417
  const bridge = new AcpSyncBridge({ cwd: data.cwd }, spawner);
352
418
  try {
353
419
  await bridge.connect();
354
- const result = await bridge.listSessions(data.cwd);
420
+ const result = await bridge.listSessions(data.cwd, data.cursor);
355
421
  await bridge.disconnect();
356
422
  socket.emit('acp:list-sessions:result', {
357
423
  requestId: data.requestId,
@@ -362,6 +428,7 @@ export function connectToServer(
362
428
  cwd: s.cwd,
363
429
  updatedAt: s.updatedAt ?? null,
364
430
  })),
431
+ ...(result.nextCursor ? { nextCursor: result.nextCursor } : {}),
365
432
  });
366
433
  } catch (error) {
367
434
  await bridge.disconnect().catch(() => {});
@@ -509,6 +576,118 @@ export function connectToServer(
509
576
  }
510
577
  });
511
578
 
579
+ socket.on('acp:set-config-option', (data: AcpSetConfigOptionPayload) => {
580
+ console.log(`[ACP] acp:set-config-option received: sessionId=${data.sessionId} configId=${data.configId}`);
581
+ const bridge = acpBridges.get(data.sessionId);
582
+ if (!bridge) {
583
+ socket.emit('acp:set-config-option:result', {
584
+ sessionId: data.sessionId,
585
+ success: false,
586
+ configId: data.configId,
587
+ error: 'ACP bridge not found for session',
588
+ });
589
+ return;
590
+ }
591
+ // 成功时 bridge 内部已用响应快照更新 configOptions 并 emit acp:config-options
592
+ bridge.setConfigOption(data.configId, data.value).then(
593
+ () => {
594
+ socket.emit('acp:set-config-option:result', {
595
+ sessionId: data.sessionId,
596
+ success: true,
597
+ configId: data.configId,
598
+ });
599
+ },
600
+ (err) => {
601
+ console.error(`[ACP] setConfigOption failed: ${(err as Error).message}`);
602
+ socket.emit('acp:set-config-option:result', {
603
+ sessionId: data.sessionId,
604
+ success: false,
605
+ configId: data.configId,
606
+ error: (err as Error).message,
607
+ });
608
+ },
609
+ );
610
+ });
611
+
612
+ socket.on('acp:delete-history-session', async (data: AcpDeleteHistorySessionPayload) => {
613
+ const serverType = data.serverType ?? 'opencode';
614
+ console.log(`[ACP sync] acp:delete-history-session received: sessionId=${data.externalSessionId} cwd=${data.cwd}`);
615
+ const spawner = createSpawner(serverType, `sync_${Date.now()}`, data.cwd);
616
+ const bridge = new AcpSyncBridge({ cwd: data.cwd }, spawner);
617
+ try {
618
+ await bridge.connect();
619
+ if (!bridge.supportsDelete()) {
620
+ await bridge.disconnect().catch(() => {});
621
+ socket.emit('acp:delete-history-session:result', {
622
+ requestId: data.requestId,
623
+ sessionId: data.sessionId,
624
+ success: false,
625
+ supported: false,
626
+ error: 'Session delete not supported by agent',
627
+ });
628
+ return;
629
+ }
630
+ await bridge.deleteSession(data.externalSessionId);
631
+ await bridge.disconnect();
632
+ socket.emit('acp:delete-history-session:result', {
633
+ requestId: data.requestId,
634
+ sessionId: data.sessionId,
635
+ success: true,
636
+ supported: true,
637
+ });
638
+ } catch (error) {
639
+ await bridge.disconnect().catch(() => {});
640
+ console.error(`[ACP sync] delete-history-session failed: ${(error as Error).message}`);
641
+ socket.emit('acp:delete-history-session:result', {
642
+ requestId: data.requestId,
643
+ sessionId: data.sessionId,
644
+ success: false,
645
+ supported: true,
646
+ error: (error as Error).message,
647
+ });
648
+ }
649
+ });
650
+
651
+ socket.on('acp:authenticate', async (data: AcpAuthenticatePayload) => {
652
+ console.log(`[ACP] acp:authenticate received: sessionId=${data.sessionId} methodId=${data.methodId}`);
653
+ const bridge = acpBridges.get(data.sessionId);
654
+ try {
655
+ if (!bridge) throw new Error('ACP bridge not found for session');
656
+ await bridge.authenticate(data.methodId);
657
+ socket.emit('acp:authenticate:result', {
658
+ sessionId: data.sessionId,
659
+ success: true,
660
+ });
661
+ } catch (error) {
662
+ console.error(`[ACP] authenticate failed: ${(error as Error).message}`);
663
+ socket.emit('acp:authenticate:result', {
664
+ sessionId: data.sessionId,
665
+ success: false,
666
+ error: (error as Error).message,
667
+ });
668
+ }
669
+ });
670
+
671
+ socket.on('acp:logout', async (data: AcpLogoutPayload) => {
672
+ console.log(`[ACP] acp:logout received: sessionId=${data.sessionId}`);
673
+ const bridge = acpBridges.get(data.sessionId);
674
+ try {
675
+ if (!bridge) throw new Error('ACP bridge not found for session');
676
+ await bridge.logout();
677
+ socket.emit('acp:logout:result', {
678
+ sessionId: data.sessionId,
679
+ success: true,
680
+ });
681
+ } catch (error) {
682
+ console.error(`[ACP] logout failed: ${(error as Error).message}`);
683
+ socket.emit('acp:logout:result', {
684
+ sessionId: data.sessionId,
685
+ success: false,
686
+ error: (error as Error).message,
687
+ });
688
+ }
689
+ });
690
+
512
691
  // File system events
513
692
  socket.on('fs:browse', async (data: FsBrowsePayload) => {
514
693
  console.log(`FS browse requested: requestId=${data.requestId} path=${data.path}`);
@@ -617,11 +796,24 @@ export function connectToServer(
617
796
  }
618
797
  });
619
798
 
799
+ socket.on('acp:elicitation:response', (data: {
800
+ requestId: string;
801
+ action: 'accept' | 'decline' | 'cancel';
802
+ content?: Record<string, unknown>;
803
+ }) => {
804
+ const bridge = elicitationRequestBridge.get(data.requestId);
805
+ if (bridge) {
806
+ elicitationRequestBridge.delete(data.requestId);
807
+ bridge.handleElicitationResponse(data.requestId, data.action, data.content);
808
+ }
809
+ });
810
+
620
811
  socket.on('disconnect', (reason) => {
621
812
  console.log(`Disconnected: ${reason}`);
622
813
  heartbeat.stop();
623
814
  gatewayManager.closeAll();
624
815
  permissionRequestBridge.clear();
816
+ elicitationRequestBridge.clear();
625
817
  });
626
818
 
627
819
  socket.on('connect_error', (err) => {