agent-gear 1.1.6 → 1.1.8

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.
Files changed (42) hide show
  1. package/dist/acp/bridge.d.ts +13 -0
  2. package/dist/acp/bridge.d.ts.map +1 -1
  3. package/dist/acp/bridge.js +125 -3
  4. package/dist/acp/bridge.js.map +1 -1
  5. package/dist/acp/bridge.test.js +130 -7
  6. package/dist/acp/bridge.test.js.map +1 -1
  7. package/dist/acp/prober.d.ts +24 -0
  8. package/dist/acp/prober.d.ts.map +1 -0
  9. package/dist/acp/prober.js +53 -0
  10. package/dist/acp/prober.js.map +1 -0
  11. package/dist/acp/prober.test.d.ts +2 -0
  12. package/dist/acp/prober.test.d.ts.map +1 -0
  13. package/dist/acp/prober.test.js +94 -0
  14. package/dist/acp/prober.test.js.map +1 -0
  15. package/dist/acp/spawner.js +4 -4
  16. package/dist/acp/spawner.js.map +1 -1
  17. package/dist/acp/sync-bridge.d.ts +2 -0
  18. package/dist/acp/sync-bridge.d.ts.map +1 -1
  19. package/dist/acp/sync-bridge.js +6 -0
  20. package/dist/acp/sync-bridge.js.map +1 -1
  21. package/dist/commands/mcp-proxy.d.ts +10 -0
  22. package/dist/commands/mcp-proxy.d.ts.map +1 -1
  23. package/dist/commands/mcp-proxy.js +78 -24
  24. package/dist/commands/mcp-proxy.js.map +1 -1
  25. package/dist/socketio-client.d.ts.map +1 -1
  26. package/dist/socketio-client.js +57 -3
  27. package/dist/socketio-client.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/acp/bridge.test.ts +159 -7
  30. package/src/acp/bridge.ts +152 -3
  31. package/src/acp/prober.test.ts +107 -0
  32. package/src/acp/prober.ts +73 -0
  33. package/src/acp/spawner.ts +4 -4
  34. package/src/acp/sync-bridge.ts +8 -0
  35. package/src/commands/mcp-proxy.ts +91 -28
  36. package/src/socketio-client.ts +72 -3
  37. package/tests/acp-bridge.test.ts +119 -0
  38. package/tests/acp-spawner.test.ts +28 -1
  39. package/tests/acp-wire-elicitation.test.ts +158 -0
  40. package/tests/acp-wire-mcp.test.ts +114 -0
  41. package/tests/commands/mcp-proxy-dispatcher.test.ts +117 -0
  42. package/tests/fixtures/fake-acp-agent.mjs +122 -0
@@ -0,0 +1,107 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+
3
+ const { mockNewSession, mockInitialize } = vi.hoisted(() => {
4
+ const mockNewSession = vi.fn().mockResolvedValue({ sessionId: 'acp-probe-1' });
5
+ const mockInitialize = vi.fn().mockResolvedValue({ protocolVersion: 1 });
6
+ return { mockNewSession, mockInitialize };
7
+ });
8
+
9
+ vi.mock('@agentclientprotocol/sdk', () => ({
10
+ ndJsonStream: vi.fn(() => ({})),
11
+ ClientSideConnection: vi.fn(function () {
12
+ return {
13
+ initialize: mockInitialize,
14
+ newSession: mockNewSession,
15
+ prompt: vi.fn().mockResolvedValue({ stopReason: 'end_turn' }),
16
+ cancel: vi.fn().mockResolvedValue(undefined),
17
+ closeSession: vi.fn().mockResolvedValue({}),
18
+ setSessionMode: vi.fn().mockResolvedValue({}),
19
+ setSessionConfigOption: vi.fn().mockResolvedValue({}),
20
+ };
21
+ }),
22
+ }));
23
+
24
+ const mockSpawnerStop = vi.fn();
25
+ vi.mock('./spawner.js', () => ({
26
+ AcpSpawner: vi.fn(function () {
27
+ return {
28
+ start: vi.fn(),
29
+ stop: mockSpawnerStop,
30
+ isRunning: vi.fn().mockReturnValue(true),
31
+ stdin: new WritableStream(),
32
+ stdout: new ReadableStream(),
33
+ closed: Promise.resolve(),
34
+ };
35
+ }),
36
+ }));
37
+
38
+ import { probeAcpServerOptions } from './prober.js';
39
+
40
+ describe('probeAcpServerOptions', () => {
41
+ beforeEach(() => {
42
+ vi.clearAllMocks();
43
+ });
44
+
45
+ afterEach(() => {
46
+ vi.restoreAllMocks();
47
+ });
48
+
49
+ it('session/new 返回 modes + configOptions 时提取 Mode/模型选项并关闭进程', async () => {
50
+ mockInitialize.mockResolvedValue({ protocolVersion: 1 });
51
+ mockNewSession.mockResolvedValue({
52
+ sessionId: 'acp-probe-1',
53
+ modes: {
54
+ availableModes: [
55
+ { id: 'code', name: 'Code', description: '编码模式' },
56
+ { id: 'ask', name: 'Ask' },
57
+ ],
58
+ currentModeId: 'code',
59
+ },
60
+ configOptions: [
61
+ {
62
+ id: 'model',
63
+ name: 'Model',
64
+ category: 'model',
65
+ type: 'select',
66
+ currentValue: 'gpt-5',
67
+ options: [
68
+ { value: 'gpt-5', name: 'GPT-5' },
69
+ { value: 'claude-4', name: 'Claude 4' },
70
+ ],
71
+ },
72
+ ],
73
+ });
74
+
75
+ const result = await probeAcpServerOptions('opencode');
76
+
77
+ expect(result.modes).toEqual([
78
+ { id: 'code', name: 'Code', description: '编码模式' },
79
+ { id: 'ask', name: 'Ask', description: undefined },
80
+ ]);
81
+ expect(result.currentModeId).toBe('code');
82
+ expect(result.availableModels).toEqual([
83
+ { id: 'gpt-5', name: 'GPT-5' },
84
+ { id: 'claude-4', name: 'Claude 4' },
85
+ ]);
86
+ expect(result.currentModelId).toBe('gpt-5');
87
+ expect(mockSpawnerStop).toHaveBeenCalled();
88
+ });
89
+
90
+ it('Agent 无 modes/models 时返回空数组而非失败', async () => {
91
+ mockNewSession.mockResolvedValue({ sessionId: 'acp-probe-2' });
92
+
93
+ const result = await probeAcpServerOptions('opencode');
94
+
95
+ expect(result.modes).toEqual([]);
96
+ expect(result.availableModels).toEqual([]);
97
+ expect(result.currentModeId).toBeNull();
98
+ expect(result.currentModelId).toBeNull();
99
+ });
100
+
101
+ it('initialize 失败时 reject 并关闭进程', async () => {
102
+ mockInitialize.mockRejectedValueOnce(new Error('handshake failed'));
103
+
104
+ await expect(probeAcpServerOptions('opencode')).rejects.toThrow('handshake failed');
105
+ expect(mockSpawnerStop).toHaveBeenCalled();
106
+ });
107
+ });
@@ -0,0 +1,73 @@
1
+ import { homedir } from 'os';
2
+ import type { Socket } from 'socket.io-client';
3
+ import { acpDetector } from './detector.js';
4
+ import { AcpSpawner } from './spawner.js';
5
+ import { AcpSessionBridge, type AcpInitializedPayload } from './bridge.js';
6
+
7
+ export interface AcpServerOptionsProbeResult {
8
+ modes: Array<{ id: string; name: string; description?: string }>;
9
+ availableModels: Array<{ id: string; name: string; provider?: string }>;
10
+ currentModeId: string | null;
11
+ currentModelId: string | null;
12
+ }
13
+
14
+ /**
15
+ * 空实例探测:临时拉起指定 ACP 工具进程(initialize + session/new),
16
+ * 收集 Mode/模型选项后立即关闭。用于配置阶段动态获取可选选项,
17
+ * 避免用户手填 modeId/modelId。
18
+ *
19
+ * 探测会话不与服务端交互:bridge 发出的 socket 事件全部丢弃。
20
+ * 任一环节失败(CLI 不存在 / 需要认证 / 超时)都会 reject,由调用方降级处理。
21
+ */
22
+ export async function probeAcpServerOptions(
23
+ serverType: string,
24
+ timeoutMs = 20000,
25
+ ): Promise<AcpServerOptionsProbeResult> {
26
+ const sessionId = `probe-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
27
+ const config = acpDetector.getSpawnerConfig(serverType);
28
+ const cwd = homedir();
29
+ const spawner = new AcpSpawner({ sessionId, cwd, command: config.command, args: config.args });
30
+ const noopSocket = { emit: () => false } as unknown as Socket;
31
+ const bridge = new AcpSessionBridge(noopSocket, { sessionId, cwd }, spawner);
32
+
33
+ return new Promise((resolve, reject) => {
34
+ let settled = false;
35
+ const timer = setTimeout(() => {
36
+ finish(() => reject(new Error(`probe timeout after ${timeoutMs}ms: ${serverType}`)));
37
+ }, timeoutMs);
38
+
39
+ const cleanup = () => {
40
+ clearTimeout(timer);
41
+ bridge.stop().catch(() => { /* ignore */ });
42
+ spawner.stop();
43
+ };
44
+
45
+ const finish = (fn: () => void) => {
46
+ if (settled) return;
47
+ settled = true;
48
+ cleanup();
49
+ fn();
50
+ };
51
+
52
+ bridge.on('acp:initialized', (payload: AcpInitializedPayload) => {
53
+ finish(() =>
54
+ resolve({
55
+ modes: payload.modes ?? [],
56
+ availableModels: payload.availableModels ?? [],
57
+ currentModeId: payload.currentModeId ?? null,
58
+ currentModelId: payload.currentModelId ?? null,
59
+ }),
60
+ );
61
+ });
62
+
63
+ bridge.on('acp:stopped', (payload: { reason: string; error?: string }) => {
64
+ finish(() =>
65
+ reject(new Error(payload.error ?? `ACP process stopped during probe: ${payload.reason}`)),
66
+ );
67
+ });
68
+
69
+ bridge.start().catch((error: unknown) => {
70
+ finish(() => reject(error instanceof Error ? error : new Error(String(error))));
71
+ });
72
+ });
73
+ }
@@ -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
 
@@ -124,4 +124,12 @@ export class AcpSyncBridge implements acp.Client {
124
124
  async requestPermission(): Promise<acp.RequestPermissionResponse> {
125
125
  return { outcome: { outcome: 'cancelled' } };
126
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
+ }
127
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
- }
@@ -7,6 +7,7 @@ import { homedir } from 'os';
7
7
  import { Heartbeat } from './heartbeat.js';
8
8
  import { clearConfigKey, saveKeyToConfig, saveEndpointToConfig, getResumeTokenFromConfig, saveResumeTokenToConfig, clearResumeTokenFromConfig } from './config.js';
9
9
  import { acpDetector } from './acp/detector.js';
10
+ import { probeAcpServerOptions } from './acp/prober.js';
10
11
  import { AcpSpawner } from './acp/spawner.js';
11
12
  import { AcpSessionBridge } from './acp/bridge.js';
12
13
  import { AcpSyncBridge } from './acp/sync-bridge.js';
@@ -18,6 +19,7 @@ import { gatewayManager } from './gateway.js';
18
19
  const CONTROLLER_VERSION = '1.1.1';
19
20
  const acpBridges = new Map<string, AcpSessionBridge>();
20
21
  const permissionRequestBridge = new Map<string, AcpSessionBridge>();
22
+ const elicitationRequestBridge = new Map<string, AcpSessionBridge>();
21
23
 
22
24
  function createSpawner(serverType: string, sessionId: string, cwd: string): AcpSpawner {
23
25
  const config = acpDetector.getSpawnerConfig(serverType);
@@ -42,12 +44,18 @@ interface AcpCheckPayload {
42
44
  requestId: string;
43
45
  }
44
46
 
47
+ interface AcpProbeOptionsPayload {
48
+ requestId: string;
49
+ serverType: string;
50
+ }
51
+
45
52
  interface AcpStartPayload {
46
53
  requestId: string;
47
54
  sessionId: string;
48
55
  serverType: string;
49
56
  cwd: string;
50
57
  mcpServers?: Array<{ name: string; command: string; args?: string[]; env?: Array<{ name: string; value: string }> }>;
58
+ mcpEndpoint?: { url: string; token: string; serverNames: string[] };
51
59
  additionalDirectories?: string[];
52
60
  }
53
61
 
@@ -56,6 +64,19 @@ interface AcpMessagePayload {
56
64
  message: { type: string; content: string; attachments?: Array<Record<string, unknown>> };
57
65
  }
58
66
 
67
+ // Summarize the MCP configuration of an acp:start/acp:resume payload for logs,
68
+ // without leaking the endpoint token.
69
+ function describeMcpConfig(data: { mcpServers?: unknown[]; mcpEndpoint?: { url: string; serverNames?: string[] } }): string {
70
+ const parts: string[] = [];
71
+ if (data.mcpServers && data.mcpServers.length > 0) {
72
+ parts.push(`mcpServers=${data.mcpServers.length}`);
73
+ }
74
+ if (data.mcpEndpoint) {
75
+ parts.push(`mcpEndpoint(url=${data.mcpEndpoint.url}, serverNames=${JSON.stringify(data.mcpEndpoint.serverNames ?? null)})`);
76
+ }
77
+ return parts.length > 0 ? `mcp=[${parts.join(' ')}]` : 'mcp=[none]';
78
+ }
79
+
59
80
  interface AcpCancelPayload {
60
81
  sessionId: string;
61
82
  }
@@ -103,6 +124,7 @@ interface AcpResumePayload {
103
124
  externalSessionId: string;
104
125
  serverType: string;
105
126
  cwd: string;
127
+ mcpEndpoint?: { url: string; token: string; serverNames: string[] };
106
128
  additionalDirectories?: string[];
107
129
  }
108
130
 
@@ -243,8 +265,31 @@ export function connectToServer(
243
265
  });
244
266
  });
245
267
 
268
+ // 空实例探测:拉起指定工具的临时会话,收集 Mode/模型选项后关闭
269
+ socket.on('acp:probe-options', async (data: AcpProbeOptionsPayload) => {
270
+ console.log(`[ACP ctrl] acp:probe-options received: requestId=${data.requestId} serverType=${data.serverType}`);
271
+ try {
272
+ const options = await probeAcpServerOptions(data.serverType);
273
+ console.log(`[ACP ctrl] probe-options success: serverType=${data.serverType} modes=${options.modes.length} models=${options.availableModels.length}`);
274
+ socket.emit('acp:probe-options:result', {
275
+ requestId: data.requestId,
276
+ serverType: data.serverType,
277
+ success: true,
278
+ ...options,
279
+ });
280
+ } catch (error) {
281
+ console.warn(`[ACP ctrl] probe-options failed: serverType=${data.serverType} error=${(error as Error).message}`);
282
+ socket.emit('acp:probe-options:result', {
283
+ requestId: data.requestId,
284
+ serverType: data.serverType,
285
+ success: false,
286
+ error: (error as Error).message,
287
+ });
288
+ }
289
+ });
290
+
246
291
  socket.on('acp:start', async (data: AcpStartPayload) => {
247
- console.log(`[ACP ctrl] acp:start received: requestId=${data.requestId} sessionId=${data.sessionId} serverType=${data.serverType} cwd=${data.cwd}`);
292
+ console.log(`[ACP ctrl] acp:start received: requestId=${data.requestId} sessionId=${data.sessionId} serverType=${data.serverType} cwd=${data.cwd} ${describeMcpConfig(data)}`);
248
293
 
249
294
  try {
250
295
  const spawner = createSpawner(data.serverType, data.sessionId, data.cwd);
@@ -252,6 +297,7 @@ export function connectToServer(
252
297
  sessionId: data.sessionId,
253
298
  cwd: data.cwd,
254
299
  mcpServers: data.mcpServers,
300
+ mcpEndpoint: data.mcpEndpoint,
255
301
  additionalDirectories: data.additionalDirectories,
256
302
  }, spawner);
257
303
 
@@ -261,6 +307,10 @@ export function connectToServer(
261
307
  permissionRequestBridge.set(requestId, bridge);
262
308
  });
263
309
 
310
+ bridge.on('elicitation:sent', (requestId: string) => {
311
+ elicitationRequestBridge.set(requestId, bridge);
312
+ });
313
+
264
314
  const onInitialized = (payload: Record<string, unknown>) => {
265
315
  if (payload.sessionId !== data.sessionId) return;
266
316
  bridge.off('acp:initialized', onInitialized);
@@ -292,7 +342,7 @@ export function connectToServer(
292
342
  bridge.off('acp:initialized', onInitialized);
293
343
  bridge.off('acp:stopped', onStopped);
294
344
  acpBridges.delete(data.sessionId);
295
- console.log(`[ACP ctrl] emitting acp:start:result: requestId=${data.requestId} sessionId=${data.sessionId} success=false`);
345
+ console.log(`[ACP ctrl] emitting acp:start:result: requestId=${data.requestId} sessionId=${data.sessionId} success=false reason=${payload.reason} error=${payload.error ?? 'n/a'}`);
296
346
  socket.emit('acp:start:result', {
297
347
  requestId: data.requestId,
298
348
  sessionId: data.sessionId,
@@ -318,13 +368,14 @@ export function connectToServer(
318
368
  });
319
369
 
320
370
  socket.on('acp:resume', async (data: AcpResumePayload) => {
321
- console.log(`[ACP ctrl] acp:resume received: requestId=${data.requestId} sessionId=${data.sessionId} serverType=${data.serverType} externalSessionId=${data.externalSessionId}`);
371
+ console.log(`[ACP ctrl] acp:resume received: requestId=${data.requestId} sessionId=${data.sessionId} serverType=${data.serverType} externalSessionId=${data.externalSessionId} ${describeMcpConfig(data)}`);
322
372
 
323
373
  try {
324
374
  const spawner = createSpawner(data.serverType, data.sessionId, data.cwd);
325
375
  const bridge = new AcpSessionBridge(socket, {
326
376
  sessionId: data.sessionId,
327
377
  cwd: data.cwd,
378
+ mcpEndpoint: data.mcpEndpoint,
328
379
  additionalDirectories: data.additionalDirectories,
329
380
  }, spawner);
330
381
 
@@ -334,6 +385,10 @@ export function connectToServer(
334
385
  permissionRequestBridge.set(requestId, bridge);
335
386
  });
336
387
 
388
+ bridge.on('elicitation:sent', (requestId: string) => {
389
+ elicitationRequestBridge.set(requestId, bridge);
390
+ });
391
+
337
392
  const onInitialized = (payload: Record<string, unknown>) => {
338
393
  if (payload.sessionId !== data.sessionId) return;
339
394
  bridge.off('acp:initialized', onInitialized);
@@ -360,6 +415,7 @@ export function connectToServer(
360
415
  bridge.off('acp:initialized', onInitialized);
361
416
  bridge.off('acp:stopped', onStopped);
362
417
  acpBridges.delete(data.sessionId);
418
+ console.log(`[ACP ctrl] emitting acp:resume:result: requestId=${data.requestId} sessionId=${data.sessionId} success=false reason=${payload.reason} error=${payload.error ?? 'n/a'}`);
363
419
  socket.emit('acp:resume:result', {
364
420
  requestId: data.requestId,
365
421
  sessionId: data.sessionId,
@@ -769,11 +825,24 @@ export function connectToServer(
769
825
  }
770
826
  });
771
827
 
828
+ socket.on('acp:elicitation:response', (data: {
829
+ requestId: string;
830
+ action: 'accept' | 'decline' | 'cancel';
831
+ content?: Record<string, unknown>;
832
+ }) => {
833
+ const bridge = elicitationRequestBridge.get(data.requestId);
834
+ if (bridge) {
835
+ elicitationRequestBridge.delete(data.requestId);
836
+ bridge.handleElicitationResponse(data.requestId, data.action, data.content);
837
+ }
838
+ });
839
+
772
840
  socket.on('disconnect', (reason) => {
773
841
  console.log(`Disconnected: ${reason}`);
774
842
  heartbeat.stop();
775
843
  gatewayManager.closeAll();
776
844
  permissionRequestBridge.clear();
845
+ elicitationRequestBridge.clear();
777
846
  });
778
847
 
779
848
  socket.on('connect_error', (err) => {