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.
- package/dist/acp/bridge.d.ts +83 -3
- package/dist/acp/bridge.d.ts.map +1 -1
- package/dist/acp/bridge.js +386 -62
- package/dist/acp/bridge.js.map +1 -1
- package/dist/acp/bridge.test.js +131 -8
- package/dist/acp/bridge.test.js.map +1 -1
- package/dist/acp/spawner.js +4 -4
- package/dist/acp/spawner.js.map +1 -1
- package/dist/acp/sync-bridge.d.ts +6 -1
- package/dist/acp/sync-bridge.d.ts.map +1 -1
- package/dist/acp/sync-bridge.js +23 -3
- package/dist/acp/sync-bridge.js.map +1 -1
- package/dist/commands/mcp-proxy.d.ts +10 -0
- package/dist/commands/mcp-proxy.d.ts.map +1 -1
- package/dist/commands/mcp-proxy.js +78 -24
- package/dist/commands/mcp-proxy.js.map +1 -1
- package/dist/socketio-client.d.ts.map +1 -1
- package/dist/socketio-client.js +164 -4
- package/dist/socketio-client.js.map +1 -1
- package/package.json +2 -2
- package/src/acp/bridge.test.ts +160 -8
- package/src/acp/bridge.ts +479 -63
- package/src/acp/spawner.ts +4 -4
- package/src/acp/sync-bridge.ts +27 -3
- package/src/commands/mcp-proxy.ts +91 -28
- package/src/socketio-client.ts +197 -5
- package/tests/acp-bridge.test.ts +369 -31
- package/tests/acp-spawner.test.ts +28 -1
- package/tests/acp-sync-bridge.test.ts +39 -1
- package/tests/acp-wire-elicitation.test.ts +158 -0
- package/tests/acp-wire-mcp.test.ts +114 -0
- package/tests/commands/mcp-proxy-dispatcher.test.ts +117 -0
- package/tests/fixtures/fake-acp-agent.mjs +122 -0
- package/tests/socketio-client.test.ts +211 -0
package/src/acp/spawner.ts
CHANGED
|
@@ -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
|
|
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
|
|
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(
|
|
56
|
+
console.error(`ACP process error (session=${this.sessionId}, cmd=${cmd}):`, error);
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
59
|
|
package/src/acp/sync-bridge.ts
CHANGED
|
@@ -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
|
-
|
|
64
|
-
return this.
|
|
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
|
-
|
|
154
|
+
const dispatcher = createStdioDispatcher(child);
|
|
155
|
+
socket.on('proxy:request', async (data: { requestId: string; request: JsonRpcRequest }) => {
|
|
73
156
|
try {
|
|
74
|
-
const response = await
|
|
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
|
-
}
|
package/src/socketio-client.ts
CHANGED
|
@@ -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) => {
|