@wrongstack/acp 0.289.0 → 0.291.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/server-agent-turn.d.ts +2 -1
- package/dist/agent/server-agent-turn.d.ts.map +1 -1
- package/dist/agent/session-store.d.ts +2 -1
- package/dist/agent/session-store.d.ts.map +1 -1
- package/dist/agent.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/sdk.d.ts +5 -37
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +0 -3719
- package/dist/sdk.js.map +4 -4
- package/dist/types/acp-messages.d.ts +1 -39
- package/dist/types/acp-messages.d.ts.map +1 -1
- package/package.json +5 -5
package/dist/sdk.js
CHANGED
|
@@ -1,3720 +1 @@
|
|
|
1
|
-
// src/sdk.ts
|
|
2
|
-
import {
|
|
3
|
-
AGENT_METHODS,
|
|
4
|
-
CLIENT_METHODS,
|
|
5
|
-
PROTOCOL_METHODS,
|
|
6
|
-
PROTOCOL_VERSION
|
|
7
|
-
} from "@agentclientprotocol/sdk";
|
|
8
|
-
import {
|
|
9
|
-
AgentApp,
|
|
10
|
-
ClientApp,
|
|
11
|
-
ActiveSession,
|
|
12
|
-
SessionBuilder,
|
|
13
|
-
methods
|
|
14
|
-
} from "@agentclientprotocol/sdk";
|
|
15
|
-
import {
|
|
16
|
-
AcpServer
|
|
17
|
-
} from "@agentclientprotocol/sdk/experimental/server";
|
|
18
|
-
import {
|
|
19
|
-
createWebSocketStream
|
|
20
|
-
} from "@agentclientprotocol/sdk/experimental/ws-client";
|
|
21
|
-
import {
|
|
22
|
-
createNodeHttpHandler,
|
|
23
|
-
createNodeWebSocketUpgradeHandler
|
|
24
|
-
} from "@agentclientprotocol/sdk/experimental/node";
|
|
25
|
-
|
|
26
|
-
// src/agent/stdio-transport.ts
|
|
27
|
-
import { expectDefined, writeErr } from "@wrongstack/core";
|
|
28
|
-
|
|
29
|
-
// src/win32-cmd.ts
|
|
30
|
-
var WIN32_CMD_META = /[&|<>"\r\n\0]/;
|
|
31
|
-
function buildWin32CmdShimInvocation(command, args = []) {
|
|
32
|
-
assertSafeWin32CmdArgs([command, ...args]);
|
|
33
|
-
const line = ["call", quoteWin32CmdArg(command), ...args.map(quoteWin32CmdArg)].join(" ");
|
|
34
|
-
return {
|
|
35
|
-
command: process.env["COMSPEC"] ?? "cmd.exe",
|
|
36
|
-
args: ["/d", "/c", line],
|
|
37
|
-
windowsVerbatimArguments: true
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
function assertSafeWin32CmdArgs(args) {
|
|
41
|
-
for (const arg of args) {
|
|
42
|
-
if (typeof arg === "string" && WIN32_CMD_META.test(arg)) {
|
|
43
|
-
throw new Error(
|
|
44
|
-
'win32 cmd shim spawn: argument contains a shell metacharacter (one of & | < > ", or a newline) that could enable command injection through the .cmd/.bat wrapper - refusing to run. Offending argument: ' + JSON.stringify(arg)
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
function quoteWin32CmdArg(arg) {
|
|
50
|
-
return `"${arg}"`;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// src/agent/stdio-transport.ts
|
|
54
|
-
var StdioTransport = class {
|
|
55
|
-
stdin = process.stdin;
|
|
56
|
-
stdout = process.stdout;
|
|
57
|
-
stderr = process.stderr;
|
|
58
|
-
buffer = "";
|
|
59
|
-
handlers = /* @__PURE__ */ new Set();
|
|
60
|
-
closed = false;
|
|
61
|
-
resolveRead = null;
|
|
62
|
-
messageQueue = [];
|
|
63
|
-
constructor() {
|
|
64
|
-
this.stdin.resume();
|
|
65
|
-
this.stdin.setEncoding("utf8");
|
|
66
|
-
this.stdin.on("data", (chunk) => this.onData(chunk));
|
|
67
|
-
this.stdin.on("end", () => this.handleClose());
|
|
68
|
-
this.stdin.on("error", (err) => this.failAll(err));
|
|
69
|
-
}
|
|
70
|
-
sendStartupMarker() {
|
|
71
|
-
this.stdout.write("[wstack-acp]\n", "utf8");
|
|
72
|
-
}
|
|
73
|
-
send(msg) {
|
|
74
|
-
if (this.closed) return Promise.resolve();
|
|
75
|
-
return new Promise((resolve3) => {
|
|
76
|
-
const line = JSON.stringify(msg) + "\n";
|
|
77
|
-
this.stdout.write(line, "utf8", () => resolve3());
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
sendRaw(chunk) {
|
|
81
|
-
this.stdout.write(chunk, "utf8");
|
|
82
|
-
}
|
|
83
|
-
read() {
|
|
84
|
-
if (this.messageQueue.length > 0) return Promise.resolve(expectDefined(this.messageQueue.shift()));
|
|
85
|
-
if (this.closed) return Promise.resolve(null);
|
|
86
|
-
return new Promise((resolve3) => {
|
|
87
|
-
this.resolveRead = resolve3;
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
onMessage(handler) {
|
|
91
|
-
this.handlers.add(handler);
|
|
92
|
-
return () => this.handlers.delete(handler);
|
|
93
|
-
}
|
|
94
|
-
close() {
|
|
95
|
-
this.closed = true;
|
|
96
|
-
this.stdin.pause();
|
|
97
|
-
this.resolveRead?.(null);
|
|
98
|
-
this.resolveRead = null;
|
|
99
|
-
}
|
|
100
|
-
onData(chunk) {
|
|
101
|
-
this.buffer += chunk;
|
|
102
|
-
const lines = this.buffer.split("\n");
|
|
103
|
-
this.buffer = lines.pop() ?? "";
|
|
104
|
-
for (const raw of lines) {
|
|
105
|
-
if (!raw.trim()) continue;
|
|
106
|
-
try {
|
|
107
|
-
this.dispatch(JSON.parse(raw));
|
|
108
|
-
} catch (err) {
|
|
109
|
-
this.stderr.write(`[wstack-acp parse error] ${err}
|
|
110
|
-
`, "utf8");
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
dispatch(msg) {
|
|
115
|
-
if (this.resolveRead) {
|
|
116
|
-
const resolve3 = this.resolveRead;
|
|
117
|
-
this.resolveRead = null;
|
|
118
|
-
resolve3(msg);
|
|
119
|
-
} else {
|
|
120
|
-
this.messageQueue.push(msg);
|
|
121
|
-
}
|
|
122
|
-
for (const handler of this.handlers) {
|
|
123
|
-
try {
|
|
124
|
-
handler(msg);
|
|
125
|
-
} catch (err) {
|
|
126
|
-
this.stderr.write(`[wstack-acp handler error] ${err}
|
|
127
|
-
`, "utf8");
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
handleClose() {
|
|
132
|
-
this.closed = true;
|
|
133
|
-
this.resolveRead?.(null);
|
|
134
|
-
this.resolveRead = null;
|
|
135
|
-
}
|
|
136
|
-
failAll(err) {
|
|
137
|
-
this.stderr.write(`[wstack-acp stdin error] ${err.message}
|
|
138
|
-
`, "utf8");
|
|
139
|
-
this.close();
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
var ClientTransport = class {
|
|
143
|
-
child = null;
|
|
144
|
-
buffer = "";
|
|
145
|
-
handlers = /* @__PURE__ */ new Set();
|
|
146
|
-
closed = false;
|
|
147
|
-
resolveRead = null;
|
|
148
|
-
messageQueue = [];
|
|
149
|
-
opts;
|
|
150
|
-
constructor(options) {
|
|
151
|
-
this.opts = {
|
|
152
|
-
handshakeTimeoutMs: 3e4,
|
|
153
|
-
...options
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
async start() {
|
|
157
|
-
if (this.child) return;
|
|
158
|
-
const [{ spawn: spawn2 }, { buildChildEnv: buildChildEnv2 }, os] = await Promise.all([
|
|
159
|
-
import("node:child_process"),
|
|
160
|
-
import("@wrongstack/core"),
|
|
161
|
-
import("node:os")
|
|
162
|
-
]);
|
|
163
|
-
return new Promise((resolve3, reject) => {
|
|
164
|
-
const timeout = setTimeout(() => {
|
|
165
|
-
reject(
|
|
166
|
-
new Error(`ACP child process failed to start within ${this.opts.handshakeTimeoutMs}ms`)
|
|
167
|
-
);
|
|
168
|
-
}, this.opts.handshakeTimeoutMs);
|
|
169
|
-
const isPkgLauncher = this.opts.command === "npx" || this.opts.command === "uvx";
|
|
170
|
-
const spawnCwd = isPkgLauncher ? os.homedir() : this.opts.cwd;
|
|
171
|
-
try {
|
|
172
|
-
const childArgs = this.opts.args ?? [];
|
|
173
|
-
const shim = process.platform === "win32" ? buildWin32CmdShimInvocation(this.opts.command, childArgs) : null;
|
|
174
|
-
this.child = spawn2(shim?.command ?? this.opts.command, shim?.args ?? childArgs, {
|
|
175
|
-
env: { ...buildChildEnv2(), ...this.opts.env },
|
|
176
|
-
cwd: spawnCwd,
|
|
177
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
178
|
-
windowsHide: true,
|
|
179
|
-
...shim ? { windowsVerbatimArguments: shim.windowsVerbatimArguments } : {}
|
|
180
|
-
});
|
|
181
|
-
} catch (err) {
|
|
182
|
-
clearTimeout(timeout);
|
|
183
|
-
reject(err);
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
const child = this.child;
|
|
187
|
-
child.stdout.setEncoding("utf8");
|
|
188
|
-
let settled = false;
|
|
189
|
-
const onSpawnFailure = (err) => {
|
|
190
|
-
if (settled) {
|
|
191
|
-
this.closed = true;
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
settled = true;
|
|
195
|
-
clearTimeout(timeout);
|
|
196
|
-
reject(err);
|
|
197
|
-
};
|
|
198
|
-
child.on("error", onSpawnFailure);
|
|
199
|
-
child.stdout.on("error", onSpawnFailure);
|
|
200
|
-
if (this.opts.skipHandshakeMarker) {
|
|
201
|
-
child.stdout.on("data", (c) => this.onChildData(c));
|
|
202
|
-
child.stderr.on("data", (c) => this.onChildError(c));
|
|
203
|
-
child.on("close", (code) => this.onChildClose(code));
|
|
204
|
-
child.once("spawn", () => {
|
|
205
|
-
if (settled) return;
|
|
206
|
-
settled = true;
|
|
207
|
-
clearTimeout(timeout);
|
|
208
|
-
resolve3();
|
|
209
|
-
});
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
const onReady = () => {
|
|
213
|
-
if (settled) return;
|
|
214
|
-
settled = true;
|
|
215
|
-
child.stdout.on("data", (c) => this.onChildData(c));
|
|
216
|
-
child.stderr.on("data", (c) => this.onChildError(c));
|
|
217
|
-
child.on("close", (code) => this.onChildClose(code));
|
|
218
|
-
clearTimeout(timeout);
|
|
219
|
-
resolve3();
|
|
220
|
-
};
|
|
221
|
-
const waitForMarker = (chunk) => {
|
|
222
|
-
this.buffer += chunk;
|
|
223
|
-
const idx = this.buffer.indexOf("[wstack-acp]\n");
|
|
224
|
-
if (idx !== -1) {
|
|
225
|
-
this.buffer = this.buffer.slice(idx + "[wstack-acp]\n".length);
|
|
226
|
-
child.stdout.removeListener("data", waitForMarker);
|
|
227
|
-
onReady();
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
child.stdout.on("data", waitForMarker);
|
|
231
|
-
});
|
|
232
|
-
}
|
|
233
|
-
send(msg) {
|
|
234
|
-
if (!this.child) return Promise.reject(new Error("ClientTransport not started"));
|
|
235
|
-
return new Promise((resolve3, reject) => {
|
|
236
|
-
const line = JSON.stringify(msg) + "\n";
|
|
237
|
-
this.child?.stdin.write(line, "utf8", (err) => {
|
|
238
|
-
if (err) reject(err);
|
|
239
|
-
else resolve3();
|
|
240
|
-
});
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
read() {
|
|
244
|
-
if (this.messageQueue.length > 0) return Promise.resolve(expectDefined(this.messageQueue.shift()));
|
|
245
|
-
if (this.closed) return Promise.resolve(null);
|
|
246
|
-
return new Promise((resolve3) => {
|
|
247
|
-
this.resolveRead = resolve3;
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
onMessage(handler) {
|
|
251
|
-
this.handlers.add(handler);
|
|
252
|
-
return () => this.handlers.delete(handler);
|
|
253
|
-
}
|
|
254
|
-
stop() {
|
|
255
|
-
if (!this.child) return;
|
|
256
|
-
this.closed = true;
|
|
257
|
-
try {
|
|
258
|
-
this.child.kill();
|
|
259
|
-
} catch {
|
|
260
|
-
}
|
|
261
|
-
this.child = null;
|
|
262
|
-
}
|
|
263
|
-
onChildData(chunk) {
|
|
264
|
-
this.buffer += chunk;
|
|
265
|
-
const lines = this.buffer.split("\n");
|
|
266
|
-
this.buffer = lines.pop() ?? "";
|
|
267
|
-
for (const raw of lines) {
|
|
268
|
-
if (!raw.trim()) continue;
|
|
269
|
-
try {
|
|
270
|
-
this.dispatch(JSON.parse(raw));
|
|
271
|
-
} catch {
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
onChildError(chunk) {
|
|
276
|
-
writeErr(`[acp-child stderr] ${chunk}`);
|
|
277
|
-
}
|
|
278
|
-
onChildClose(code) {
|
|
279
|
-
this.closed = true;
|
|
280
|
-
this.resolveRead?.(null);
|
|
281
|
-
this.resolveRead = null;
|
|
282
|
-
if (code !== 0 && code !== null) {
|
|
283
|
-
writeErr(`[acp-child exited with code ${code}]
|
|
284
|
-
`);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
dispatch(msg) {
|
|
288
|
-
if (this.resolveRead) {
|
|
289
|
-
const resolve3 = this.resolveRead;
|
|
290
|
-
this.resolveRead = null;
|
|
291
|
-
resolve3(msg);
|
|
292
|
-
} else {
|
|
293
|
-
this.messageQueue.push(msg);
|
|
294
|
-
}
|
|
295
|
-
for (const handler of this.handlers) {
|
|
296
|
-
try {
|
|
297
|
-
handler(msg);
|
|
298
|
-
} catch {
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
};
|
|
303
|
-
|
|
304
|
-
// src/client/websocket-transport.ts
|
|
305
|
-
var WebSocketClientTransport = class {
|
|
306
|
-
ws = null;
|
|
307
|
-
handlers = /* @__PURE__ */ new Set();
|
|
308
|
-
closed = false;
|
|
309
|
-
opts;
|
|
310
|
-
constructor(opts) {
|
|
311
|
-
this.opts = opts;
|
|
312
|
-
}
|
|
313
|
-
start() {
|
|
314
|
-
const WS = globalThis.WebSocket;
|
|
315
|
-
if (!WS) {
|
|
316
|
-
return Promise.reject(
|
|
317
|
-
new Error(
|
|
318
|
-
"global WebSocket is not available \u2014 Node \u2265 22 is required for the remote ACP transport"
|
|
319
|
-
)
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
const timeoutMs = this.opts.handshakeTimeoutMs ?? 3e4;
|
|
323
|
-
return new Promise((resolve3, reject) => {
|
|
324
|
-
let settled = false;
|
|
325
|
-
const ws = new WS(this.opts.url, this.opts.protocols);
|
|
326
|
-
this.ws = ws;
|
|
327
|
-
const timer = setTimeout(() => {
|
|
328
|
-
if (settled) return;
|
|
329
|
-
settled = true;
|
|
330
|
-
try {
|
|
331
|
-
ws.close();
|
|
332
|
-
} catch {
|
|
333
|
-
}
|
|
334
|
-
reject(new Error(`WebSocket failed to open within ${timeoutMs}ms`));
|
|
335
|
-
}, timeoutMs);
|
|
336
|
-
ws.addEventListener("open", () => {
|
|
337
|
-
if (settled) return;
|
|
338
|
-
settled = true;
|
|
339
|
-
clearTimeout(timer);
|
|
340
|
-
resolve3();
|
|
341
|
-
});
|
|
342
|
-
ws.addEventListener("error", (ev) => {
|
|
343
|
-
if (settled) {
|
|
344
|
-
this.closed = true;
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
settled = true;
|
|
348
|
-
clearTimeout(timer);
|
|
349
|
-
const message = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "WebSocket error";
|
|
350
|
-
reject(new Error(message));
|
|
351
|
-
});
|
|
352
|
-
ws.addEventListener("close", () => {
|
|
353
|
-
this.closed = true;
|
|
354
|
-
});
|
|
355
|
-
ws.addEventListener("message", (ev) => {
|
|
356
|
-
this.onData(ev.data);
|
|
357
|
-
});
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
send(msg) {
|
|
361
|
-
if (this.closed || !this.ws) {
|
|
362
|
-
return Promise.reject(new Error("WebSocket transport is not open"));
|
|
363
|
-
}
|
|
364
|
-
try {
|
|
365
|
-
this.ws.send(JSON.stringify(msg));
|
|
366
|
-
return Promise.resolve();
|
|
367
|
-
} catch (err) {
|
|
368
|
-
return Promise.reject(err instanceof Error ? err : new Error(String(err)));
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
onMessage(handler) {
|
|
372
|
-
this.handlers.add(handler);
|
|
373
|
-
return () => this.handlers.delete(handler);
|
|
374
|
-
}
|
|
375
|
-
stop() {
|
|
376
|
-
this.closed = true;
|
|
377
|
-
if (this.ws) {
|
|
378
|
-
try {
|
|
379
|
-
this.ws.close();
|
|
380
|
-
} catch {
|
|
381
|
-
}
|
|
382
|
-
this.ws = null;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
onData(data) {
|
|
386
|
-
const text = typeof data === "string" ? data : data instanceof ArrayBuffer ? Buffer.from(data).toString("utf8") : Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
|
|
387
|
-
if (!text.trim()) return;
|
|
388
|
-
let msg;
|
|
389
|
-
try {
|
|
390
|
-
msg = JSON.parse(text);
|
|
391
|
-
} catch {
|
|
392
|
-
for (const line of text.split("\n")) {
|
|
393
|
-
if (!line.trim()) continue;
|
|
394
|
-
try {
|
|
395
|
-
this.dispatch(JSON.parse(line));
|
|
396
|
-
} catch {
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
this.dispatch(msg);
|
|
402
|
-
}
|
|
403
|
-
dispatch(msg) {
|
|
404
|
-
for (const handler of [...this.handlers]) {
|
|
405
|
-
try {
|
|
406
|
-
handler(msg);
|
|
407
|
-
} catch {
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
};
|
|
412
|
-
|
|
413
|
-
// src/types/acp-v1.ts
|
|
414
|
-
var ACP_PROTOCOL_VERSION = 1;
|
|
415
|
-
|
|
416
|
-
// src/client/file-server.ts
|
|
417
|
-
import { randomBytes } from "node:crypto";
|
|
418
|
-
import { realpathSync } from "node:fs";
|
|
419
|
-
import * as fsp from "node:fs/promises";
|
|
420
|
-
import * as path from "node:path";
|
|
421
|
-
var DEFAULT_MAX_READ_BYTES = 5 * 1024 * 1024;
|
|
422
|
-
var DEFAULT_MAX_WRITE_BYTES = 5 * 1024 * 1024;
|
|
423
|
-
var FsError = class extends Error {
|
|
424
|
-
code;
|
|
425
|
-
path;
|
|
426
|
-
constructor(code, path4, message) {
|
|
427
|
-
super(message);
|
|
428
|
-
this.name = "FsError";
|
|
429
|
-
this.code = code;
|
|
430
|
-
this.path = path4;
|
|
431
|
-
}
|
|
432
|
-
};
|
|
433
|
-
var FileServer = class {
|
|
434
|
-
root;
|
|
435
|
-
realRoot;
|
|
436
|
-
timeoutMs;
|
|
437
|
-
maxReadBytes;
|
|
438
|
-
maxWriteBytes;
|
|
439
|
-
constructor(opts) {
|
|
440
|
-
this.root = path.resolve(opts.projectRoot);
|
|
441
|
-
this.realRoot = safeRealpathSync(this.root);
|
|
442
|
-
this.timeoutMs = opts.timeoutMs ?? 3e4;
|
|
443
|
-
this.maxReadBytes = opts.maxReadBytes ?? DEFAULT_MAX_READ_BYTES;
|
|
444
|
-
this.maxWriteBytes = opts.maxWriteBytes ?? DEFAULT_MAX_WRITE_BYTES;
|
|
445
|
-
}
|
|
446
|
-
/** Read a text file. Returns the content as a string. */
|
|
447
|
-
async readTextFile(params) {
|
|
448
|
-
const safe = await this.resolveInside(params.path);
|
|
449
|
-
const controller = new AbortController();
|
|
450
|
-
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
451
|
-
try {
|
|
452
|
-
const stat2 = await fsp.stat(safe).catch((err) => {
|
|
453
|
-
throw mapFsError(err, safe);
|
|
454
|
-
});
|
|
455
|
-
if (stat2.size > this.maxReadBytes) {
|
|
456
|
-
throw new FsError(
|
|
457
|
-
"TOO_LARGE",
|
|
458
|
-
safe,
|
|
459
|
-
`file is ${stat2.size} bytes, max read is ${this.maxReadBytes} bytes`
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
|
-
const content = await fsp.readFile(safe, {
|
|
463
|
-
encoding: "utf8",
|
|
464
|
-
signal: controller.signal
|
|
465
|
-
});
|
|
466
|
-
return { content };
|
|
467
|
-
} catch (err) {
|
|
468
|
-
if (err instanceof FsError) throw err;
|
|
469
|
-
if (controller.signal.aborted) {
|
|
470
|
-
throw new FsError("TIMEOUT", safe, `readTextFile timed out after ${this.timeoutMs}ms`);
|
|
471
|
-
}
|
|
472
|
-
throw mapFsError(err, safe);
|
|
473
|
-
} finally {
|
|
474
|
-
clearTimeout(timer);
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
/** Write a text file. Atomic via write-then-rename. */
|
|
478
|
-
async writeTextFile(params) {
|
|
479
|
-
const byteLength = Buffer.byteLength(params.content, "utf8");
|
|
480
|
-
if (byteLength > this.maxWriteBytes) {
|
|
481
|
-
throw new FsError(
|
|
482
|
-
"TOO_LARGE",
|
|
483
|
-
params.path,
|
|
484
|
-
`content is ${byteLength} bytes, max write is ${this.maxWriteBytes} bytes`
|
|
485
|
-
);
|
|
486
|
-
}
|
|
487
|
-
const safe = await this.resolveInside(params.path);
|
|
488
|
-
const controller = new AbortController();
|
|
489
|
-
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
490
|
-
const tmp = `${safe}.${randomBytes(6).toString("hex")}.tmp`;
|
|
491
|
-
try {
|
|
492
|
-
await fsp.writeFile(tmp, params.content, {
|
|
493
|
-
encoding: "utf8",
|
|
494
|
-
signal: controller.signal
|
|
495
|
-
});
|
|
496
|
-
await this.assertRealInside(tmp);
|
|
497
|
-
await this.assertRealInside(path.dirname(safe));
|
|
498
|
-
await fsp.rename(tmp, safe);
|
|
499
|
-
} catch (err) {
|
|
500
|
-
if (err instanceof FsError) {
|
|
501
|
-
await fsp.unlink(tmp).catch(() => void 0);
|
|
502
|
-
throw err;
|
|
503
|
-
}
|
|
504
|
-
try {
|
|
505
|
-
await fsp.unlink(tmp);
|
|
506
|
-
} catch {
|
|
507
|
-
}
|
|
508
|
-
if (controller.signal.aborted) {
|
|
509
|
-
throw new FsError("TIMEOUT", safe, `writeTextFile timed out after ${this.timeoutMs}ms`);
|
|
510
|
-
}
|
|
511
|
-
throw mapFsError(err, safe);
|
|
512
|
-
} finally {
|
|
513
|
-
clearTimeout(timer);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
/**
|
|
517
|
-
* Resolve a path and verify it is inside the project root by realpath.
|
|
518
|
-
* Rejects with `FsError` if the textual path, the resolved path, or the
|
|
519
|
-
* real (symlink-resolved) path escapes the project root.
|
|
520
|
-
*
|
|
521
|
-
* For files that don't exist yet (e.g. a write to a new file), the
|
|
522
|
-
* nearest existing ancestor directory is realpath-checked instead.
|
|
523
|
-
*/
|
|
524
|
-
async resolveInside(p) {
|
|
525
|
-
if (typeof p !== "string" || p.length === 0) {
|
|
526
|
-
throw new FsError("INVALID_PATH", p, "path is empty or not a string");
|
|
527
|
-
}
|
|
528
|
-
if (!path.isAbsolute(p)) {
|
|
529
|
-
throw new FsError("INVALID_PATH", p, "path must be absolute (ACP requirement)");
|
|
530
|
-
}
|
|
531
|
-
const resolved = path.resolve(p);
|
|
532
|
-
const rootWithSep = this.root.endsWith(path.sep) ? this.root : this.root + path.sep;
|
|
533
|
-
if (resolved !== this.root && !resolved.startsWith(rootWithSep)) {
|
|
534
|
-
throw new FsError("OUTSIDE_ROOT", resolved, "path is outside the project root");
|
|
535
|
-
}
|
|
536
|
-
await this.assertRealInside(resolved);
|
|
537
|
-
return resolved;
|
|
538
|
-
}
|
|
539
|
-
/**
|
|
540
|
-
* Resolve `resolvedPath` through `fs.realpath` and verify the result is
|
|
541
|
-
* inside `realRoot`. For non-existent paths (new files), walk up to the
|
|
542
|
-
* nearest existing ancestor and check that instead.
|
|
543
|
-
*/
|
|
544
|
-
async assertRealInside(resolvedPath) {
|
|
545
|
-
let probe = resolvedPath;
|
|
546
|
-
for (; ; ) {
|
|
547
|
-
let real;
|
|
548
|
-
try {
|
|
549
|
-
real = await fsp.realpath(probe);
|
|
550
|
-
} catch (err) {
|
|
551
|
-
const code = err.code;
|
|
552
|
-
if (code === "ENOENT") {
|
|
553
|
-
const parent = path.dirname(probe);
|
|
554
|
-
if (parent === probe) return;
|
|
555
|
-
probe = parent;
|
|
556
|
-
continue;
|
|
557
|
-
}
|
|
558
|
-
throw mapFsError(err, resolvedPath);
|
|
559
|
-
}
|
|
560
|
-
if (real === this.realRoot || real.startsWith(this.realRoot + path.sep)) return;
|
|
561
|
-
throw new FsError(
|
|
562
|
-
"OUTSIDE_ROOT",
|
|
563
|
-
resolvedPath,
|
|
564
|
-
"path resolves through a symlink outside the project root"
|
|
565
|
-
);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
};
|
|
569
|
-
function mapFsError(err, p) {
|
|
570
|
-
const code = err?.code;
|
|
571
|
-
if (code === "ENOENT") return new FsError("ENOENT", p, `no such file: ${p}`);
|
|
572
|
-
if (code === "EACCES" || code === "EPERM") {
|
|
573
|
-
return new FsError("EACCES", p, `permission denied: ${p}`);
|
|
574
|
-
}
|
|
575
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
576
|
-
return new FsError("INVALID_PATH", p, msg);
|
|
577
|
-
}
|
|
578
|
-
function safeRealpathSync(p) {
|
|
579
|
-
try {
|
|
580
|
-
return realpathSync(p);
|
|
581
|
-
} catch {
|
|
582
|
-
return p;
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
// src/client/permission.ts
|
|
587
|
-
function pickAllow(options) {
|
|
588
|
-
const ranked = [...options].sort((a, b) => {
|
|
589
|
-
const score = (k) => {
|
|
590
|
-
if (k === "allow_once") return 0;
|
|
591
|
-
if (k === "allow_always") return 1;
|
|
592
|
-
if (k === "reject_once") return 2;
|
|
593
|
-
return 3;
|
|
594
|
-
};
|
|
595
|
-
return score(a.kind) - score(b.kind);
|
|
596
|
-
});
|
|
597
|
-
const chosen = ranked[0];
|
|
598
|
-
if (!chosen || chosen.kind === "reject_once" || chosen.kind === "reject_always") {
|
|
599
|
-
return { outcome: "cancelled" };
|
|
600
|
-
}
|
|
601
|
-
return { outcome: "selected", optionId: chosen.optionId };
|
|
602
|
-
}
|
|
603
|
-
function pickReject(options) {
|
|
604
|
-
const reject = options.find(
|
|
605
|
-
(o) => o.kind === "reject_once" || o.kind === "reject_always"
|
|
606
|
-
);
|
|
607
|
-
return reject ? { outcome: "selected", optionId: reject.optionId } : { outcome: "cancelled" };
|
|
608
|
-
}
|
|
609
|
-
var READ_ONLY_KINDS = /* @__PURE__ */ new Set(["read", "search", "fetch", "think"]);
|
|
610
|
-
var defaultPermissionPolicy = async (req) => {
|
|
611
|
-
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
612
|
-
return pickAllow(req.options);
|
|
613
|
-
};
|
|
614
|
-
var readOnlyPermissionPolicy = async (req) => {
|
|
615
|
-
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
616
|
-
const kind = req.toolCall.kind;
|
|
617
|
-
if (kind && READ_ONLY_KINDS.has(kind)) {
|
|
618
|
-
return pickAllow(req.options);
|
|
619
|
-
}
|
|
620
|
-
return pickReject(req.options);
|
|
621
|
-
};
|
|
622
|
-
function makePermissionPolicy(decide) {
|
|
623
|
-
return async (req) => {
|
|
624
|
-
if (req.signal.aborted) return { outcome: "cancelled" };
|
|
625
|
-
const allow = await decide(req);
|
|
626
|
-
return allow ? pickAllow(req.options) : pickReject(req.options);
|
|
627
|
-
};
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// src/client/terminal-server.ts
|
|
631
|
-
import { spawn } from "node:child_process";
|
|
632
|
-
import { realpathSync as realpathSync2 } from "node:fs";
|
|
633
|
-
import * as path2 from "node:path";
|
|
634
|
-
import { buildChildEnv } from "@wrongstack/core/utils";
|
|
635
|
-
var TerminalServer = class {
|
|
636
|
-
terminals = /* @__PURE__ */ new Map();
|
|
637
|
-
projectRoot;
|
|
638
|
-
commandTimeoutMs;
|
|
639
|
-
outputByteLimit;
|
|
640
|
-
maxOutputByteLimit;
|
|
641
|
-
nextId = 1;
|
|
642
|
-
constructor(opts) {
|
|
643
|
-
this.projectRoot = path2.resolve(opts.projectRoot);
|
|
644
|
-
this.commandTimeoutMs = opts.commandTimeoutMs ?? 5 * 6e4;
|
|
645
|
-
this.outputByteLimit = opts.outputByteLimit ?? 1024 * 1024;
|
|
646
|
-
this.maxOutputByteLimit = opts.maxOutputByteLimit ?? 16 * 1024 * 1024;
|
|
647
|
-
if (opts.signal) {
|
|
648
|
-
opts.signal.addEventListener("abort", () => this.releaseAll());
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
/** Spawn a new terminal. Returns the agent-facing id. */
|
|
652
|
-
create(params) {
|
|
653
|
-
const id = `term_${this.nextId++}`;
|
|
654
|
-
const cwd = this.resolveCwd(params.cwd);
|
|
655
|
-
const proc = spawn(params.command, params.args ?? [], {
|
|
656
|
-
cwd,
|
|
657
|
-
env: this.buildEnv(params.env),
|
|
658
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
659
|
-
windowsHide: true
|
|
660
|
-
// shell: false on purpose. The terminal server is invoked with
|
|
661
|
-
// the agent's explicit argv; turning on shell-mode would make
|
|
662
|
-
// the command a single shell-parsed string, which breaks
|
|
663
|
-
// Windows cmd quoting for the common case of running node with
|
|
664
|
-
// `-e "<script>"`. If a future feature needs shell features
|
|
665
|
-
// (pipes, redirects), it should be opt-in per-call, not the
|
|
666
|
-
// default.
|
|
667
|
-
});
|
|
668
|
-
const state = {
|
|
669
|
-
proc,
|
|
670
|
-
cwd,
|
|
671
|
-
command: params.command,
|
|
672
|
-
args: params.args ?? [],
|
|
673
|
-
output: "",
|
|
674
|
-
retainedBytes: 0,
|
|
675
|
-
truncated: false,
|
|
676
|
-
exitStatus: void 0,
|
|
677
|
-
timeoutHandle: null,
|
|
678
|
-
exitPromise: new Promise((resolve3) => {
|
|
679
|
-
proc.on("close", (code, signalName) => {
|
|
680
|
-
if (state.timeoutHandle) {
|
|
681
|
-
clearTimeout(state.timeoutHandle);
|
|
682
|
-
state.timeoutHandle = null;
|
|
683
|
-
}
|
|
684
|
-
const exitStatus = {
|
|
685
|
-
exitCode: typeof code === "number" ? code : null,
|
|
686
|
-
signal: typeof signalName === "string" ? signalName : null
|
|
687
|
-
};
|
|
688
|
-
state.exitStatus = exitStatus;
|
|
689
|
-
resolve3(exitStatus);
|
|
690
|
-
});
|
|
691
|
-
proc.on("error", (err) => {
|
|
692
|
-
if (state.timeoutHandle) {
|
|
693
|
-
clearTimeout(state.timeoutHandle);
|
|
694
|
-
state.timeoutHandle = null;
|
|
695
|
-
}
|
|
696
|
-
const exitStatus = { exitCode: 127, signal: null };
|
|
697
|
-
state.exitStatus = exitStatus;
|
|
698
|
-
state.output += `[spawn error] ${err.message}
|
|
699
|
-
`;
|
|
700
|
-
state.retainedBytes = Buffer.byteLength(state.output, "utf8");
|
|
701
|
-
resolve3(exitStatus);
|
|
702
|
-
});
|
|
703
|
-
})
|
|
704
|
-
};
|
|
705
|
-
const perCallByteLimit = Math.min(
|
|
706
|
-
Math.max(1, this.clampFiniteInt(params.outputByteLimit, this.outputByteLimit)),
|
|
707
|
-
this.maxOutputByteLimit
|
|
708
|
-
);
|
|
709
|
-
proc.stdout?.setEncoding("utf8");
|
|
710
|
-
proc.stderr?.setEncoding("utf8");
|
|
711
|
-
const onData = (chunk) => {
|
|
712
|
-
state.output += chunk;
|
|
713
|
-
state.retainedBytes = Buffer.byteLength(state.output, "utf8");
|
|
714
|
-
while (state.retainedBytes > perCallByteLimit) {
|
|
715
|
-
const trimmed = state.output.slice(1);
|
|
716
|
-
state.output = trimmed;
|
|
717
|
-
const newBytes = Buffer.byteLength(state.output, "utf8");
|
|
718
|
-
if (newBytes >= state.retainedBytes) {
|
|
719
|
-
break;
|
|
720
|
-
}
|
|
721
|
-
state.retainedBytes = newBytes;
|
|
722
|
-
state.truncated = true;
|
|
723
|
-
}
|
|
724
|
-
};
|
|
725
|
-
proc.stdout?.on("data", onData);
|
|
726
|
-
proc.stderr?.on("data", onData);
|
|
727
|
-
state.timeoutHandle = setTimeout(() => {
|
|
728
|
-
try {
|
|
729
|
-
proc.kill("SIGTERM");
|
|
730
|
-
} catch {
|
|
731
|
-
}
|
|
732
|
-
}, this.commandTimeoutMs);
|
|
733
|
-
this.terminals.set(id, state);
|
|
734
|
-
return { terminalId: id };
|
|
735
|
-
}
|
|
736
|
-
/** Return captured output and (if available) the exit status. */
|
|
737
|
-
output(terminalId) {
|
|
738
|
-
const state = this.terminals.get(terminalId);
|
|
739
|
-
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
740
|
-
return {
|
|
741
|
-
output: state.output,
|
|
742
|
-
truncated: state.truncated,
|
|
743
|
-
...state.exitStatus ? { exitStatus: state.exitStatus } : {}
|
|
744
|
-
};
|
|
745
|
-
}
|
|
746
|
-
/** Block until the process exits. Resolves with the exit status. */
|
|
747
|
-
async waitForExit(terminalId) {
|
|
748
|
-
const state = this.terminals.get(terminalId);
|
|
749
|
-
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
750
|
-
return state.exitPromise;
|
|
751
|
-
}
|
|
752
|
-
/** Kill the process but keep the terminal record (agent can still read output). */
|
|
753
|
-
kill(terminalId) {
|
|
754
|
-
const state = this.terminals.get(terminalId);
|
|
755
|
-
if (!state) throw new Error(`unknown terminal: ${terminalId}`);
|
|
756
|
-
try {
|
|
757
|
-
state.proc.kill("SIGTERM");
|
|
758
|
-
} catch {
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
/** Kill the process if alive and remove the record. */
|
|
762
|
-
release(terminalId) {
|
|
763
|
-
const state = this.terminals.get(terminalId);
|
|
764
|
-
if (!state) return;
|
|
765
|
-
if (state.timeoutHandle) {
|
|
766
|
-
clearTimeout(state.timeoutHandle);
|
|
767
|
-
state.timeoutHandle = null;
|
|
768
|
-
}
|
|
769
|
-
try {
|
|
770
|
-
state.proc.kill("SIGKILL");
|
|
771
|
-
} catch {
|
|
772
|
-
}
|
|
773
|
-
this.terminals.delete(terminalId);
|
|
774
|
-
}
|
|
775
|
-
/** Kill all active terminals. Used on session close. */
|
|
776
|
-
releaseAll() {
|
|
777
|
-
for (const id of [...this.terminals.keys()]) {
|
|
778
|
-
this.release(id);
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
resolveCwd(cwd) {
|
|
782
|
-
if (!cwd) return this.projectRoot;
|
|
783
|
-
const resolved = path2.resolve(cwd);
|
|
784
|
-
const rootWithSep = this.projectRoot.endsWith(path2.sep) ? this.projectRoot : this.projectRoot + path2.sep;
|
|
785
|
-
if (resolved !== this.projectRoot && !resolved.startsWith(rootWithSep)) {
|
|
786
|
-
return this.projectRoot;
|
|
787
|
-
}
|
|
788
|
-
try {
|
|
789
|
-
const realRoot = realpathSync2(this.projectRoot);
|
|
790
|
-
const realCwd = realpathSync2(resolved);
|
|
791
|
-
const realRootWithSep = realRoot.endsWith(path2.sep) ? realRoot : realRoot + path2.sep;
|
|
792
|
-
if (realCwd !== realRoot && !realCwd.startsWith(realRootWithSep)) {
|
|
793
|
-
return realRoot;
|
|
794
|
-
}
|
|
795
|
-
return realCwd;
|
|
796
|
-
} catch {
|
|
797
|
-
return this.projectRoot;
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
buildEnv(agentEnv) {
|
|
801
|
-
const env = buildChildEnv();
|
|
802
|
-
if (agentEnv) {
|
|
803
|
-
for (const { name, value } of agentEnv) {
|
|
804
|
-
const upper = name.toUpperCase();
|
|
805
|
-
if (DENIED_AGENT_ENV_KEYS.has(upper)) continue;
|
|
806
|
-
env[name] = value;
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
return env;
|
|
810
|
-
}
|
|
811
|
-
/**
|
|
812
|
-
* Clamp an agent-supplied numeric to a finite positive safe integer, falling
|
|
813
|
-
* back to `defaultValue` for undefined/NaN/non-finite values. Prevents
|
|
814
|
-
* negative, NaN, or Infinity values from disabling output caps or causing
|
|
815
|
-
* unbounded memory growth.
|
|
816
|
-
*/
|
|
817
|
-
clampFiniteInt(value, defaultValue) {
|
|
818
|
-
if (value === void 0 || !Number.isFinite(value) || value < 1) {
|
|
819
|
-
return defaultValue;
|
|
820
|
-
}
|
|
821
|
-
return Math.trunc(value);
|
|
822
|
-
}
|
|
823
|
-
};
|
|
824
|
-
var DENIED_AGENT_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
825
|
-
"NODE_OPTIONS",
|
|
826
|
-
"LD_PRELOAD",
|
|
827
|
-
"LD_LIBRARY_PATH",
|
|
828
|
-
"DYLD_INSERT_LIBRARIES",
|
|
829
|
-
"DYLD_LIBRARY_PATH",
|
|
830
|
-
"DYLD_FALLBACK_LIBRARY_PATH",
|
|
831
|
-
"PATH",
|
|
832
|
-
"PYTHONPATH",
|
|
833
|
-
"PYTHONSTARTUP",
|
|
834
|
-
"PERL5OPT",
|
|
835
|
-
"PERLLIB",
|
|
836
|
-
"RUBYOPT",
|
|
837
|
-
"RUBYLIB"
|
|
838
|
-
]);
|
|
839
|
-
|
|
840
|
-
// src/client/acp-session.ts
|
|
841
|
-
var ACPSessionError = class extends Error {
|
|
842
|
-
kind;
|
|
843
|
-
cause;
|
|
844
|
-
constructor(kind, message, cause) {
|
|
845
|
-
super(message);
|
|
846
|
-
this.name = "ACPSessionError";
|
|
847
|
-
this.kind = kind;
|
|
848
|
-
this.cause = cause;
|
|
849
|
-
}
|
|
850
|
-
};
|
|
851
|
-
function isJsonRpcError(v) {
|
|
852
|
-
return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
|
|
853
|
-
}
|
|
854
|
-
var ACPSession = class _ACPSession {
|
|
855
|
-
transport;
|
|
856
|
-
fileServer;
|
|
857
|
-
terminalServer;
|
|
858
|
-
permissionPolicy;
|
|
859
|
-
timeoutMs;
|
|
860
|
-
opts;
|
|
861
|
-
state = "init";
|
|
862
|
-
sessionId = null;
|
|
863
|
-
/** Pending outbound requests (initialize, session/new, session/prompt, etc). */
|
|
864
|
-
pending = /* @__PURE__ */ new Map();
|
|
865
|
-
nextId = 1;
|
|
866
|
-
/** True after close() has been called. */
|
|
867
|
-
closed = false;
|
|
868
|
-
// Agent-provided info from the initialize handshake
|
|
869
|
-
agentCapabilities = {};
|
|
870
|
-
agentInfo = null;
|
|
871
|
-
authMethods = [];
|
|
872
|
-
/** Protocol version negotiated with the agent during initialize. */
|
|
873
|
-
negotiatedVersion = ACP_PROTOCOL_VERSION;
|
|
874
|
-
constructor(opts, transport) {
|
|
875
|
-
this.opts = opts;
|
|
876
|
-
this.transport = transport;
|
|
877
|
-
this.timeoutMs = opts.timeoutMs ?? 5 * 6e4;
|
|
878
|
-
const fsOpts = {
|
|
879
|
-
projectRoot: opts.projectRoot
|
|
880
|
-
};
|
|
881
|
-
if (opts.fsTimeoutMs !== void 0) fsOpts.timeoutMs = opts.fsTimeoutMs;
|
|
882
|
-
this.fileServer = new FileServer(fsOpts);
|
|
883
|
-
const termOpts = {
|
|
884
|
-
projectRoot: opts.projectRoot
|
|
885
|
-
};
|
|
886
|
-
if (opts.terminalTimeoutMs !== void 0) {
|
|
887
|
-
termOpts.commandTimeoutMs = opts.terminalTimeoutMs;
|
|
888
|
-
}
|
|
889
|
-
if (opts.terminalOutputByteLimit !== void 0) {
|
|
890
|
-
termOpts.outputByteLimit = opts.terminalOutputByteLimit;
|
|
891
|
-
}
|
|
892
|
-
this.terminalServer = new TerminalServer(termOpts);
|
|
893
|
-
this.permissionPolicy = opts.permissionPolicy ?? defaultPermissionPolicy;
|
|
894
|
-
}
|
|
895
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
896
|
-
// Public accessors
|
|
897
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
898
|
-
/** Agent capabilities advertised during initialize. */
|
|
899
|
-
getCapabilities() {
|
|
900
|
-
return { ...this.agentCapabilities };
|
|
901
|
-
}
|
|
902
|
-
/** Authentication methods advertised by the agent. */
|
|
903
|
-
getAuthMethods() {
|
|
904
|
-
return [...this.authMethods];
|
|
905
|
-
}
|
|
906
|
-
/** Agent info (name, title, version) from initialize. */
|
|
907
|
-
getAgentInfo() {
|
|
908
|
-
return this.agentInfo;
|
|
909
|
-
}
|
|
910
|
-
/** Whether the agent requires authentication (has auth methods). */
|
|
911
|
-
requiresAuth() {
|
|
912
|
-
return this.authMethods.length > 0;
|
|
913
|
-
}
|
|
914
|
-
/** Current session id, if one exists. */
|
|
915
|
-
getSessionId() {
|
|
916
|
-
return this.sessionId;
|
|
917
|
-
}
|
|
918
|
-
/** Protocol version negotiated during initialize. */
|
|
919
|
-
getNegotiatedVersion() {
|
|
920
|
-
return this.negotiatedVersion;
|
|
921
|
-
}
|
|
922
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
923
|
-
// Lifecycle — start
|
|
924
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
925
|
-
/**
|
|
926
|
-
* Spawn the child, run the initialize handshake, install the
|
|
927
|
-
* message dispatch, and return a ready session.
|
|
928
|
-
*/
|
|
929
|
-
static async start(opts) {
|
|
930
|
-
const transportOpts = {
|
|
931
|
-
command: opts.command,
|
|
932
|
-
args: opts.args ? [...opts.args] : [],
|
|
933
|
-
handshakeTimeoutMs: 3e4,
|
|
934
|
-
skipHandshakeMarker: true
|
|
935
|
-
};
|
|
936
|
-
if (opts.env !== void 0) transportOpts.env = opts.env;
|
|
937
|
-
if (opts.cwd !== void 0) transportOpts.cwd = opts.cwd;
|
|
938
|
-
const transport = new ClientTransport(transportOpts);
|
|
939
|
-
return _ACPSession.attach(opts, transport, `failed to spawn ${opts.command}`);
|
|
940
|
-
}
|
|
941
|
-
/**
|
|
942
|
-
* Connect to a REMOTE ACP agent over a WebSocket instead of spawning a
|
|
943
|
-
* local subprocess. `opts.command` is ignored for the wire (a label is
|
|
944
|
-
* still useful for `role`); everything else (projectRoot sandbox for
|
|
945
|
-
* fs/terminal, timeouts, permission policy, MCP servers) applies the same.
|
|
946
|
-
*/
|
|
947
|
-
static async connectWebSocket(wsOpts, opts) {
|
|
948
|
-
const transport = new WebSocketClientTransport(wsOpts);
|
|
949
|
-
return _ACPSession.attach(opts, transport, `failed to connect to ${wsOpts.url}`);
|
|
950
|
-
}
|
|
951
|
-
/**
|
|
952
|
-
* Connect using a caller-supplied transport. Lets advanced callers plug
|
|
953
|
-
* in their own wire (SDK streams, in-process pipes, test doubles).
|
|
954
|
-
*/
|
|
955
|
-
static async connect(transport, opts) {
|
|
956
|
-
return _ACPSession.attach(opts, transport, "failed to connect transport");
|
|
957
|
-
}
|
|
958
|
-
/** Shared connect path: start the transport, install dispatch, handshake. */
|
|
959
|
-
static async attach(opts, transport, spawnErrLabel) {
|
|
960
|
-
try {
|
|
961
|
-
await transport.start();
|
|
962
|
-
} catch (err) {
|
|
963
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
964
|
-
throw new ACPSessionError("spawn_failed", `${spawnErrLabel}: ${msg}`, err);
|
|
965
|
-
}
|
|
966
|
-
const session = new _ACPSession(opts, transport);
|
|
967
|
-
transport.onMessage((msg) => session.handleMessage(msg));
|
|
968
|
-
try {
|
|
969
|
-
await session.initialize();
|
|
970
|
-
} catch (err) {
|
|
971
|
-
try {
|
|
972
|
-
transport.stop();
|
|
973
|
-
} catch {
|
|
974
|
-
}
|
|
975
|
-
throw err;
|
|
976
|
-
}
|
|
977
|
-
return session;
|
|
978
|
-
}
|
|
979
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
980
|
-
// Initialization
|
|
981
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
982
|
-
async initialize() {
|
|
983
|
-
const id = this.allocId();
|
|
984
|
-
const result = await this.sendRequest(id, "initialize", {
|
|
985
|
-
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
986
|
-
clientCapabilities: {
|
|
987
|
-
fs: { readTextFile: true, writeTextFile: true },
|
|
988
|
-
terminal: true
|
|
989
|
-
},
|
|
990
|
-
clientInfo: { name: "wrongstack", title: "WrongStack", version: "0.287.0" }
|
|
991
|
-
});
|
|
992
|
-
if (isJsonRpcError(result)) {
|
|
993
|
-
throw new ACPSessionError("init_failed", `initialize failed: ${result.message}`, result);
|
|
994
|
-
}
|
|
995
|
-
if (typeof result !== "object" || result === null || typeof result.protocolVersion !== "number") {
|
|
996
|
-
throw new ACPSessionError("protocol_error", "initialize returned no protocolVersion");
|
|
997
|
-
}
|
|
998
|
-
const r = result;
|
|
999
|
-
if (r.protocolVersion > ACP_PROTOCOL_VERSION) {
|
|
1000
|
-
throw new ACPSessionError(
|
|
1001
|
-
"unsupported_capability",
|
|
1002
|
-
`agent requires protocolVersion=${r.protocolVersion}, client supports up to ${ACP_PROTOCOL_VERSION}`
|
|
1003
|
-
);
|
|
1004
|
-
}
|
|
1005
|
-
this.negotiatedVersion = r.protocolVersion;
|
|
1006
|
-
this.agentCapabilities = r.agentCapabilities ?? {};
|
|
1007
|
-
this.agentInfo = r.agentInfo ?? null;
|
|
1008
|
-
this.authMethods = r.authMethods ?? [];
|
|
1009
|
-
this.state = "ready";
|
|
1010
|
-
}
|
|
1011
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
1012
|
-
// Authentication
|
|
1013
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
1014
|
-
/**
|
|
1015
|
-
* Authenticate with the agent using one of the advertised auth methods.
|
|
1016
|
-
* Call this AFTER start() and BEFORE any session/new call.
|
|
1017
|
-
*
|
|
1018
|
-
* Throws ACPSessionError('auth_failed') if the agent rejects the
|
|
1019
|
-
* authentication or if the methodId is not in the advertised list.
|
|
1020
|
-
*/
|
|
1021
|
-
async authenticate(methodId) {
|
|
1022
|
-
if (this.state === "closed") {
|
|
1023
|
-
throw new ACPSessionError("closed", "session is closed");
|
|
1024
|
-
}
|
|
1025
|
-
if (this.state !== "ready") {
|
|
1026
|
-
throw new ACPSessionError(
|
|
1027
|
-
"protocol_error",
|
|
1028
|
-
`authenticate called in state=${this.state} (expected 'ready')`
|
|
1029
|
-
);
|
|
1030
|
-
}
|
|
1031
|
-
if (!this.authMethods.some((m) => m.id === methodId)) {
|
|
1032
|
-
throw new ACPSessionError(
|
|
1033
|
-
"auth_failed",
|
|
1034
|
-
`auth method "${methodId}" not in advertised methods: ${this.authMethods.map((m) => m.id).join(", ")}`
|
|
1035
|
-
);
|
|
1036
|
-
}
|
|
1037
|
-
const id = this.allocId();
|
|
1038
|
-
const result = await this.sendRequest(id, "authenticate", { methodId });
|
|
1039
|
-
if (isJsonRpcError(result)) {
|
|
1040
|
-
throw new ACPSessionError("auth_failed", `authenticate failed: ${result.message}`, result);
|
|
1041
|
-
}
|
|
1042
|
-
this.state = "authenticated";
|
|
1043
|
-
}
|
|
1044
|
-
/**
|
|
1045
|
-
* Log out from the current authenticated session.
|
|
1046
|
-
* Only callable if the agent advertises `auth.logout` capability.
|
|
1047
|
-
*/
|
|
1048
|
-
async logout() {
|
|
1049
|
-
if (this.state === "closed") {
|
|
1050
|
-
throw new ACPSessionError("closed", "session is closed");
|
|
1051
|
-
}
|
|
1052
|
-
if (!this.agentCapabilities.auth?.logout) {
|
|
1053
|
-
throw new ACPSessionError(
|
|
1054
|
-
"unsupported_capability",
|
|
1055
|
-
"agent does not support logout (auth.logout capability not advertised)"
|
|
1056
|
-
);
|
|
1057
|
-
}
|
|
1058
|
-
const id = this.allocId();
|
|
1059
|
-
const result = await this.sendRequest(id, "logout", {});
|
|
1060
|
-
if (isJsonRpcError(result)) {
|
|
1061
|
-
throw new ACPSessionError("logout_failed", `logout failed: ${result.message}`, result);
|
|
1062
|
-
}
|
|
1063
|
-
this.state = "ready";
|
|
1064
|
-
}
|
|
1065
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
1066
|
-
// Session management
|
|
1067
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
1068
|
-
/**
|
|
1069
|
-
* Load an existing session. The agent replays the conversation history
|
|
1070
|
-
* via session/update notifications before responding.
|
|
1071
|
-
*
|
|
1072
|
-
* Only works if the agent advertises `loadSession` capability.
|
|
1073
|
-
*
|
|
1074
|
-
* @param sessionId - The session to load
|
|
1075
|
-
* @param mcpServers - Optional MCP servers (defaults to options.mcpServers)
|
|
1076
|
-
* @param cwd - Optional working directory (defaults to options.cwd or projectRoot)
|
|
1077
|
-
*/
|
|
1078
|
-
async loadSession(sessionId, mcpServers, cwd) {
|
|
1079
|
-
if (this.closed) {
|
|
1080
|
-
throw new ACPSessionError("closed", "session is closed");
|
|
1081
|
-
}
|
|
1082
|
-
if (!this.agentCapabilities.loadSession) {
|
|
1083
|
-
throw new ACPSessionError(
|
|
1084
|
-
"unsupported_capability",
|
|
1085
|
-
"agent does not support session/load (loadSession capability not advertised)"
|
|
1086
|
-
);
|
|
1087
|
-
}
|
|
1088
|
-
if (this.sessionId) {
|
|
1089
|
-
await this.closeSession();
|
|
1090
|
-
}
|
|
1091
|
-
this.resetScratch();
|
|
1092
|
-
const servers = this.filterMcpServers(mcpServers ?? this.opts.mcpServers);
|
|
1093
|
-
const id = this.allocId();
|
|
1094
|
-
const result = await this.sendRequest(id, "session/load", {
|
|
1095
|
-
sessionId,
|
|
1096
|
-
cwd: cwd ?? this.opts.cwd ?? this.opts.projectRoot,
|
|
1097
|
-
mcpServers: servers
|
|
1098
|
-
});
|
|
1099
|
-
if (isJsonRpcError(result)) {
|
|
1100
|
-
throw new ACPSessionError("prompt_failed", `session/load failed: ${result.message}`, result);
|
|
1101
|
-
}
|
|
1102
|
-
this.sessionId = sessionId;
|
|
1103
|
-
}
|
|
1104
|
-
/**
|
|
1105
|
-
* Resume an existing session without replaying history.
|
|
1106
|
-
*
|
|
1107
|
-
* Only works if the agent advertises `sessionCapabilities.resume`.
|
|
1108
|
-
*
|
|
1109
|
-
* @param sessionId - The session to resume
|
|
1110
|
-
* @param mcpServers - Optional MCP servers (defaults to options.mcpServers)
|
|
1111
|
-
* @param cwd - Optional working directory (defaults to options.cwd or projectRoot)
|
|
1112
|
-
*/
|
|
1113
|
-
async resumeSession(sessionId, mcpServers, cwd) {
|
|
1114
|
-
if (this.closed) {
|
|
1115
|
-
throw new ACPSessionError("closed", "session is closed");
|
|
1116
|
-
}
|
|
1117
|
-
if (!this.agentCapabilities.sessionCapabilities?.resume) {
|
|
1118
|
-
throw new ACPSessionError(
|
|
1119
|
-
"unsupported_capability",
|
|
1120
|
-
"agent does not support session/resume (sessionCapabilities.resume not advertised)"
|
|
1121
|
-
);
|
|
1122
|
-
}
|
|
1123
|
-
if (this.sessionId) {
|
|
1124
|
-
await this.closeSession();
|
|
1125
|
-
}
|
|
1126
|
-
const servers = this.filterMcpServers(mcpServers ?? this.opts.mcpServers);
|
|
1127
|
-
const id = this.allocId();
|
|
1128
|
-
const result = await this.sendRequest(id, "session/resume", {
|
|
1129
|
-
sessionId,
|
|
1130
|
-
cwd: cwd ?? this.opts.cwd ?? this.opts.projectRoot,
|
|
1131
|
-
mcpServers: servers
|
|
1132
|
-
});
|
|
1133
|
-
if (isJsonRpcError(result)) {
|
|
1134
|
-
throw new ACPSessionError("prompt_failed", `session/resume failed: ${result.message}`, result);
|
|
1135
|
-
}
|
|
1136
|
-
this.sessionId = sessionId;
|
|
1137
|
-
}
|
|
1138
|
-
/**
|
|
1139
|
-
* List existing sessions known to the agent.
|
|
1140
|
-
*
|
|
1141
|
-
* Only works if the agent advertises `sessionCapabilities.list`.
|
|
1142
|
-
*/
|
|
1143
|
-
async listSessions(cursor, cwd) {
|
|
1144
|
-
if (this.closed) {
|
|
1145
|
-
throw new ACPSessionError("closed", "session is closed");
|
|
1146
|
-
}
|
|
1147
|
-
if (!this.agentCapabilities.sessionCapabilities?.list) {
|
|
1148
|
-
throw new ACPSessionError(
|
|
1149
|
-
"unsupported_capability",
|
|
1150
|
-
"agent does not support session/list (sessionCapabilities.list not advertised)"
|
|
1151
|
-
);
|
|
1152
|
-
}
|
|
1153
|
-
const id = this.allocId();
|
|
1154
|
-
const params = {};
|
|
1155
|
-
if (cursor !== void 0) params.cursor = cursor;
|
|
1156
|
-
if (cwd !== void 0) params.cwd = cwd;
|
|
1157
|
-
const result = await this.sendRequest(id, "session/list", params);
|
|
1158
|
-
if (isJsonRpcError(result)) {
|
|
1159
|
-
throw new ACPSessionError("prompt_failed", `session/list failed: ${result.message}`, result);
|
|
1160
|
-
}
|
|
1161
|
-
const r = result;
|
|
1162
|
-
return {
|
|
1163
|
-
sessions: r.sessions ?? [],
|
|
1164
|
-
nextCursor: r.nextCursor
|
|
1165
|
-
};
|
|
1166
|
-
}
|
|
1167
|
-
/**
|
|
1168
|
-
* Delete a session from the agent's session list.
|
|
1169
|
-
*
|
|
1170
|
-
* Only works if the agent advertises `sessionCapabilities.delete`.
|
|
1171
|
-
*/
|
|
1172
|
-
async deleteSession(sessionId) {
|
|
1173
|
-
if (this.closed) {
|
|
1174
|
-
throw new ACPSessionError("closed", "session is closed");
|
|
1175
|
-
}
|
|
1176
|
-
if (!this.agentCapabilities.sessionCapabilities?.delete) {
|
|
1177
|
-
throw new ACPSessionError(
|
|
1178
|
-
"unsupported_capability",
|
|
1179
|
-
"agent does not support session/delete (sessionCapabilities.delete not advertised)"
|
|
1180
|
-
);
|
|
1181
|
-
}
|
|
1182
|
-
const id = this.allocId();
|
|
1183
|
-
const result = await this.sendRequest(id, "session/delete", { sessionId });
|
|
1184
|
-
if (isJsonRpcError(result)) {
|
|
1185
|
-
throw new ACPSessionError("prompt_failed", `session/delete failed: ${result.message}`, result);
|
|
1186
|
-
}
|
|
1187
|
-
if (this.sessionId === sessionId) {
|
|
1188
|
-
this.sessionId = null;
|
|
1189
|
-
}
|
|
1190
|
-
}
|
|
1191
|
-
/**
|
|
1192
|
-
* Fork a session — create a new session from an existing one.
|
|
1193
|
-
*/
|
|
1194
|
-
async forkSession(sourceSessionId, cwd, mcpServers) {
|
|
1195
|
-
if (this.closed) throw new ACPSessionError("closed", "session is closed");
|
|
1196
|
-
const servers = this.filterMcpServers(mcpServers ?? this.opts.mcpServers);
|
|
1197
|
-
const id = this.allocId();
|
|
1198
|
-
const result = await this.sendRequest(id, "session/fork", {
|
|
1199
|
-
sessionId: sourceSessionId,
|
|
1200
|
-
cwd: cwd ?? this.opts.cwd ?? this.opts.projectRoot,
|
|
1201
|
-
...servers.length > 0 ? { mcpServers: servers } : {}
|
|
1202
|
-
});
|
|
1203
|
-
if (isJsonRpcError(result)) {
|
|
1204
|
-
throw new ACPSessionError("prompt_failed", `session/fork failed: ${result.message}`, result);
|
|
1205
|
-
}
|
|
1206
|
-
const newId = result.sessionId;
|
|
1207
|
-
if (typeof newId !== "string" || !newId) {
|
|
1208
|
-
throw new ACPSessionError("protocol_error", "session/fork returned no sessionId", result);
|
|
1209
|
-
}
|
|
1210
|
-
return newId;
|
|
1211
|
-
}
|
|
1212
|
-
/**
|
|
1213
|
-
* Set the active mode for a session.
|
|
1214
|
-
*/
|
|
1215
|
-
async setMode(sessionId, modeId) {
|
|
1216
|
-
if (this.closed) throw new ACPSessionError("closed", "session is closed");
|
|
1217
|
-
const id = this.allocId();
|
|
1218
|
-
const result = await this.sendRequest(id, "session/set_mode", { sessionId, modeId });
|
|
1219
|
-
if (isJsonRpcError(result)) {
|
|
1220
|
-
throw new ACPSessionError("prompt_failed", `session/set_mode failed: ${result.message}`, result);
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
/**
|
|
1224
|
-
* Set a configuration option for a session.
|
|
1225
|
-
*/
|
|
1226
|
-
async setConfigOption(sessionId, configId, value) {
|
|
1227
|
-
if (this.closed) throw new ACPSessionError("closed", "session is closed");
|
|
1228
|
-
const id = this.allocId();
|
|
1229
|
-
const result = await this.sendRequest(id, "session/set_config_option", {
|
|
1230
|
-
sessionId,
|
|
1231
|
-
configId,
|
|
1232
|
-
value
|
|
1233
|
-
});
|
|
1234
|
-
if (isJsonRpcError(result)) {
|
|
1235
|
-
throw new ACPSessionError("prompt_failed", `session/set_config_option failed: ${result.message}`, result);
|
|
1236
|
-
}
|
|
1237
|
-
}
|
|
1238
|
-
/**
|
|
1239
|
-
* List available providers and the current provider.
|
|
1240
|
-
*/
|
|
1241
|
-
async listProviders() {
|
|
1242
|
-
if (this.closed) throw new ACPSessionError("closed", "session is closed");
|
|
1243
|
-
const id = this.allocId();
|
|
1244
|
-
const result = await this.sendRequest(id, "providers/list", {});
|
|
1245
|
-
if (isJsonRpcError(result)) {
|
|
1246
|
-
throw new ACPSessionError("prompt_failed", `providers/list failed: ${result.message}`, result);
|
|
1247
|
-
}
|
|
1248
|
-
const r = result;
|
|
1249
|
-
return { providers: r.providers ?? [], currentProviderId: r.currentProviderId ?? null };
|
|
1250
|
-
}
|
|
1251
|
-
/**
|
|
1252
|
-
* Send an MCP message to the agent for routing.
|
|
1253
|
-
*/
|
|
1254
|
-
async mcpMessage(connectionId, message) {
|
|
1255
|
-
if (this.closed) throw new ACPSessionError("closed", "session is closed");
|
|
1256
|
-
const id = this.allocId();
|
|
1257
|
-
const result = await this.sendRequest(id, "mcp/message", { connectionId, message });
|
|
1258
|
-
if (isJsonRpcError(result)) {
|
|
1259
|
-
throw new ACPSessionError("prompt_failed", `mcp/message failed: ${result.message}`, result);
|
|
1260
|
-
}
|
|
1261
|
-
return result;
|
|
1262
|
-
}
|
|
1263
|
-
/**
|
|
1264
|
-
* Set the active provider for the agent.
|
|
1265
|
-
*/
|
|
1266
|
-
async setProvider(providerId, config) {
|
|
1267
|
-
if (this.closed) throw new ACPSessionError("closed", "session is closed");
|
|
1268
|
-
const id = this.allocId();
|
|
1269
|
-
const result = await this.sendRequest(id, "providers/set", { providerId, ...config ?? {} });
|
|
1270
|
-
if (isJsonRpcError(result)) {
|
|
1271
|
-
throw new ACPSessionError("prompt_failed", `providers/set failed: ${result.message}`, result);
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
/**
|
|
1275
|
-
* Disable the current provider.
|
|
1276
|
-
*/
|
|
1277
|
-
async disableProvider() {
|
|
1278
|
-
if (this.closed) throw new ACPSessionError("closed", "session is closed");
|
|
1279
|
-
const id = this.allocId();
|
|
1280
|
-
const result = await this.sendRequest(id, "providers/disable", {});
|
|
1281
|
-
if (isJsonRpcError(result)) {
|
|
1282
|
-
throw new ACPSessionError("prompt_failed", `providers/disable failed: ${result.message}`, result);
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
1286
|
-
// Prompt
|
|
1287
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
1288
|
-
/**
|
|
1289
|
-
* Run one prompt turn. Creates a session if needed, sends the
|
|
1290
|
-
* prompt, streams session/update notifications, and resolves with
|
|
1291
|
-
* the agent's response.
|
|
1292
|
-
*
|
|
1293
|
-
* @param blocks - Content blocks to send. Use `textContent()` for plain
|
|
1294
|
-
* text, or include ImageContent/AudioContent if the agent's
|
|
1295
|
-
* `promptCapabilities` allow it.
|
|
1296
|
-
* @param signal - AbortSignal for cancellation.
|
|
1297
|
-
*
|
|
1298
|
-
* Cancellation: if `signal` aborts mid-prompt, we send
|
|
1299
|
-
* `session/cancel` (a notification per spec) and keep accepting
|
|
1300
|
-
* updates until the agent returns with `stopReason: 'cancelled'`.
|
|
1301
|
-
* The result is the same shape as a normal turn, with
|
|
1302
|
-
* `stopReason === 'cancelled'`.
|
|
1303
|
-
*/
|
|
1304
|
-
async prompt(blocks, signal, onProgress) {
|
|
1305
|
-
if (this.closed) {
|
|
1306
|
-
throw new ACPSessionError("closed", "session is closed");
|
|
1307
|
-
}
|
|
1308
|
-
if (this.state !== "ready" && this.state !== "authenticated" && this.state !== "done") {
|
|
1309
|
-
throw new ACPSessionError("protocol_error", `prompt called in state=${this.state}`);
|
|
1310
|
-
}
|
|
1311
|
-
if (signal.aborted) {
|
|
1312
|
-
return emptyRunResult("cancelled");
|
|
1313
|
-
}
|
|
1314
|
-
if (!this.sessionId) {
|
|
1315
|
-
await this.createSession();
|
|
1316
|
-
}
|
|
1317
|
-
this.resetScratch();
|
|
1318
|
-
this.progressHandler = onProgress ?? null;
|
|
1319
|
-
const promptId = this.allocId();
|
|
1320
|
-
const turnPromise = this.sendRequest(
|
|
1321
|
-
promptId,
|
|
1322
|
-
"session/prompt",
|
|
1323
|
-
{
|
|
1324
|
-
sessionId: this.sessionId,
|
|
1325
|
-
prompt: blocks
|
|
1326
|
-
},
|
|
1327
|
-
this.timeoutMs
|
|
1328
|
-
);
|
|
1329
|
-
let cancelled = false;
|
|
1330
|
-
const onAbort = () => {
|
|
1331
|
-
cancelled = true;
|
|
1332
|
-
this.transport.send({
|
|
1333
|
-
jsonrpc: "2.0",
|
|
1334
|
-
method: "session/cancel",
|
|
1335
|
-
params: { sessionId: this.sessionId }
|
|
1336
|
-
}).catch(() => {
|
|
1337
|
-
});
|
|
1338
|
-
};
|
|
1339
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
1340
|
-
this.state = "prompting";
|
|
1341
|
-
let response;
|
|
1342
|
-
try {
|
|
1343
|
-
response = await turnPromise;
|
|
1344
|
-
} catch (err) {
|
|
1345
|
-
this.state = "done";
|
|
1346
|
-
signal.removeEventListener("abort", onAbort);
|
|
1347
|
-
if (cancelled || signal.aborted) {
|
|
1348
|
-
throw new ACPSessionError("aborted", "prompt was aborted by the parent");
|
|
1349
|
-
}
|
|
1350
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1351
|
-
throw new ACPSessionError("prompt_failed", `session/prompt failed: ${msg}`, err);
|
|
1352
|
-
} finally {
|
|
1353
|
-
signal.removeEventListener("abort", onAbort);
|
|
1354
|
-
this.progressHandler = null;
|
|
1355
|
-
}
|
|
1356
|
-
this.state = "done";
|
|
1357
|
-
if (isJsonRpcError(response)) {
|
|
1358
|
-
throw new ACPSessionError("prompt_failed", `agent error: ${response.message}`, response);
|
|
1359
|
-
}
|
|
1360
|
-
const stopReason = response.stopReason ?? "end_turn";
|
|
1361
|
-
const finalText = this.scratch.text;
|
|
1362
|
-
return {
|
|
1363
|
-
text: finalText,
|
|
1364
|
-
stopReason,
|
|
1365
|
-
hasText: finalText.length > 0,
|
|
1366
|
-
usage: this.scratch.usage,
|
|
1367
|
-
plan: this.scratch.plan,
|
|
1368
|
-
toolCalls: [...this.scratch.toolCalls.values()],
|
|
1369
|
-
diffs: this.scratch.diffs,
|
|
1370
|
-
thoughts: this.scratch.thoughts
|
|
1371
|
-
};
|
|
1372
|
-
}
|
|
1373
|
-
async createSession() {
|
|
1374
|
-
const servers = this.filterMcpServers(this.opts.mcpServers);
|
|
1375
|
-
const id = this.allocId();
|
|
1376
|
-
const result = await this.sendRequest(id, "session/new", {
|
|
1377
|
-
cwd: this.opts.cwd ?? this.opts.projectRoot,
|
|
1378
|
-
mcpServers: servers
|
|
1379
|
-
});
|
|
1380
|
-
if (isJsonRpcError(result)) {
|
|
1381
|
-
throw new ACPSessionError(
|
|
1382
|
-
"session_create_failed",
|
|
1383
|
-
`session/new failed: ${result.message}`,
|
|
1384
|
-
result
|
|
1385
|
-
);
|
|
1386
|
-
}
|
|
1387
|
-
const sessionId = result.sessionId;
|
|
1388
|
-
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
1389
|
-
throw new ACPSessionError(
|
|
1390
|
-
"protocol_error",
|
|
1391
|
-
"session/new returned no sessionId",
|
|
1392
|
-
result
|
|
1393
|
-
);
|
|
1394
|
-
}
|
|
1395
|
-
this.sessionId = sessionId;
|
|
1396
|
-
}
|
|
1397
|
-
/**
|
|
1398
|
-
* Close the current session gracefully (if the agent supports it).
|
|
1399
|
-
*
|
|
1400
|
-
* Sends `session/close` JSON-RPC request, then clears the local
|
|
1401
|
-
* session id. Best-effort — errors are swallowed so the caller can
|
|
1402
|
-
* always proceed to transport teardown.
|
|
1403
|
-
*/
|
|
1404
|
-
async closeSession() {
|
|
1405
|
-
if (!this.sessionId) return;
|
|
1406
|
-
const sid = this.sessionId;
|
|
1407
|
-
this.sessionId = null;
|
|
1408
|
-
if (this.agentCapabilities.sessionCapabilities?.close) {
|
|
1409
|
-
const id = this.allocId();
|
|
1410
|
-
try {
|
|
1411
|
-
await this.sendRequest(id, "session/close", { sessionId: sid }, 1e4);
|
|
1412
|
-
} catch {
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
1417
|
-
// Lifecycle — close
|
|
1418
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
1419
|
-
/** Tear down the session and kill the child process. */
|
|
1420
|
-
async close() {
|
|
1421
|
-
if (this.closed) return;
|
|
1422
|
-
this.closed = true;
|
|
1423
|
-
this.state = "closed";
|
|
1424
|
-
this.terminalServer.releaseAll();
|
|
1425
|
-
if (this.sessionId && this.agentCapabilities.sessionCapabilities?.close) {
|
|
1426
|
-
try {
|
|
1427
|
-
await this.closeSession();
|
|
1428
|
-
} catch {
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
for (const [, p] of this.pending) {
|
|
1432
|
-
clearTimeout(p.timeoutHandle);
|
|
1433
|
-
p.reject(new ACPSessionError("closed", "session was closed"));
|
|
1434
|
-
}
|
|
1435
|
-
this.pending.clear();
|
|
1436
|
-
try {
|
|
1437
|
-
this.transport.stop();
|
|
1438
|
-
} catch {
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
1442
|
-
// Helpers
|
|
1443
|
-
// ──────────────────────────────────────────────────────────────────────
|
|
1444
|
-
/**
|
|
1445
|
-
* Filter MCP servers according to agent capabilities.
|
|
1446
|
-
* - Stdio servers are always included.
|
|
1447
|
-
* - HTTP servers are only included if agent supports mcpCapabilities.http.
|
|
1448
|
-
* - SSE servers are only included if agent supports mcpCapabilities.sse.
|
|
1449
|
-
*/
|
|
1450
|
-
filterMcpServers(servers) {
|
|
1451
|
-
if (!servers || servers.length === 0) return [];
|
|
1452
|
-
const mcpCaps = this.agentCapabilities.mcpCapabilities ?? {};
|
|
1453
|
-
return servers.filter((s) => {
|
|
1454
|
-
if ("type" in s && s.type === "http") return mcpCaps.http === true;
|
|
1455
|
-
if ("type" in s && s.type === "sse") return mcpCaps.sse === true;
|
|
1456
|
-
return true;
|
|
1457
|
-
});
|
|
1458
|
-
}
|
|
1459
|
-
// ────────────────────────────────────────────────────────────────────
|
|
1460
|
-
// Wire layer
|
|
1461
|
-
// ────────────────────────────────────────────────────────────────────
|
|
1462
|
-
allocId() {
|
|
1463
|
-
return this.nextId++;
|
|
1464
|
-
}
|
|
1465
|
-
async sendRequest(id, method, params, timeoutMs) {
|
|
1466
|
-
return new Promise((resolve3, reject) => {
|
|
1467
|
-
const effectiveTimeout = timeoutMs ?? this.timeoutMs;
|
|
1468
|
-
const handle = setTimeout(() => {
|
|
1469
|
-
this.pending.delete(id);
|
|
1470
|
-
reject(
|
|
1471
|
-
new ACPSessionError(
|
|
1472
|
-
"protocol_error",
|
|
1473
|
-
`${method} timed out after ${effectiveTimeout}ms`
|
|
1474
|
-
)
|
|
1475
|
-
);
|
|
1476
|
-
}, effectiveTimeout);
|
|
1477
|
-
this.pending.set(id, {
|
|
1478
|
-
method,
|
|
1479
|
-
resolve: resolve3,
|
|
1480
|
-
reject,
|
|
1481
|
-
timeoutMs: effectiveTimeout,
|
|
1482
|
-
timeoutHandle: handle
|
|
1483
|
-
});
|
|
1484
|
-
this.transport.send({ jsonrpc: "2.0", id, method, params }).catch((err) => {
|
|
1485
|
-
clearTimeout(handle);
|
|
1486
|
-
this.pending.delete(id);
|
|
1487
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1488
|
-
reject(new ACPSessionError("protocol_error", `send ${method} failed: ${msg}`, err));
|
|
1489
|
-
});
|
|
1490
|
-
});
|
|
1491
|
-
}
|
|
1492
|
-
/**
|
|
1493
|
-
* Send a JSON-RPC 2.0 success response to an agent-initiated request.
|
|
1494
|
-
*
|
|
1495
|
-
* Per JSON-RPC 2.0 (and the official ACP SDK's message router) a Response
|
|
1496
|
-
* object MUST carry `jsonrpc: "2.0"` and MUST NOT carry a `method` field —
|
|
1497
|
-
* the SDK classifies any object with a `method` key as a Request and drops
|
|
1498
|
-
* it as a response, so an agent's `fs/*`, `terminal/*`, or
|
|
1499
|
-
* `session/request_permission` callback would hang forever. The legacy
|
|
1500
|
-
* `ACPMessage` type predates v1 (requires `method`, lacks `jsonrpc`), so we
|
|
1501
|
-
* build the correct wire object and cast at the boundary.
|
|
1502
|
-
*/
|
|
1503
|
-
sendResult(id, result) {
|
|
1504
|
-
return this.transport.send({ jsonrpc: "2.0", id, result });
|
|
1505
|
-
}
|
|
1506
|
-
/** Send a JSON-RPC 2.0 error response (no `method` field, per spec). */
|
|
1507
|
-
sendErrorResponse(id, code, message) {
|
|
1508
|
-
return this.transport.send({
|
|
1509
|
-
jsonrpc: "2.0",
|
|
1510
|
-
id,
|
|
1511
|
-
error: { code, message }
|
|
1512
|
-
});
|
|
1513
|
-
}
|
|
1514
|
-
handleMessage(msg) {
|
|
1515
|
-
if (msg.id !== void 0 && (msg.result !== void 0 || msg.error !== void 0)) {
|
|
1516
|
-
const pending = this.pending.get(msg.id);
|
|
1517
|
-
if (!pending) return;
|
|
1518
|
-
clearTimeout(pending.timeoutHandle);
|
|
1519
|
-
this.pending.delete(msg.id);
|
|
1520
|
-
if (msg.error !== void 0) {
|
|
1521
|
-
pending.reject(new Error(msg.error.message ?? "unknown JSON-RPC error"));
|
|
1522
|
-
} else {
|
|
1523
|
-
pending.resolve(msg.result);
|
|
1524
|
-
}
|
|
1525
|
-
return;
|
|
1526
|
-
}
|
|
1527
|
-
if (msg.method === "session/update") {
|
|
1528
|
-
this.handleUpdate(msg);
|
|
1529
|
-
return;
|
|
1530
|
-
}
|
|
1531
|
-
if (msg.method === "session/request_permission") {
|
|
1532
|
-
void this.handlePermissionRequest(msg);
|
|
1533
|
-
return;
|
|
1534
|
-
}
|
|
1535
|
-
if (msg.method === "fs/read_text_file" || msg.method === "fs/write_text_file") {
|
|
1536
|
-
void this.handleFsRequest(msg);
|
|
1537
|
-
return;
|
|
1538
|
-
}
|
|
1539
|
-
if (msg.method?.startsWith("terminal/")) {
|
|
1540
|
-
void this.handleTerminalRequest(msg);
|
|
1541
|
-
return;
|
|
1542
|
-
}
|
|
1543
|
-
if (msg.method === "mcp/connect" || msg.method === "mcp/message" || msg.method === "mcp/disconnect") {
|
|
1544
|
-
if (msg.id !== void 0) {
|
|
1545
|
-
this.sendResult(msg.id, {}).catch(() => {
|
|
1546
|
-
});
|
|
1547
|
-
}
|
|
1548
|
-
return;
|
|
1549
|
-
}
|
|
1550
|
-
if (msg.method === "elicitation/create" || msg.method === "elicitation/complete") {
|
|
1551
|
-
if (msg.id !== void 0) {
|
|
1552
|
-
this.sendResult(msg.id, {}).catch(() => {
|
|
1553
|
-
});
|
|
1554
|
-
}
|
|
1555
|
-
return;
|
|
1556
|
-
}
|
|
1557
|
-
if (msg.method === "$/cancel_request") {
|
|
1558
|
-
return;
|
|
1559
|
-
}
|
|
1560
|
-
if (msg.method) {
|
|
1561
|
-
console.warn(
|
|
1562
|
-
JSON.stringify({
|
|
1563
|
-
level: "warn",
|
|
1564
|
-
event: "acp_session.unhandled_method",
|
|
1565
|
-
method: msg.method,
|
|
1566
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1567
|
-
})
|
|
1568
|
-
);
|
|
1569
|
-
}
|
|
1570
|
-
}
|
|
1571
|
-
handleUpdate(msg) {
|
|
1572
|
-
const update = msg.params?.update;
|
|
1573
|
-
if (typeof update !== "object" || update === null) return;
|
|
1574
|
-
const u = update;
|
|
1575
|
-
this.emitProgress({ type: "raw", update: u });
|
|
1576
|
-
switch (u.sessionUpdate) {
|
|
1577
|
-
case "agent_message_chunk": {
|
|
1578
|
-
const text = extractText(u.content);
|
|
1579
|
-
if (text) {
|
|
1580
|
-
this.scratch.text += text;
|
|
1581
|
-
this.emitProgress({ type: "message", text });
|
|
1582
|
-
}
|
|
1583
|
-
return;
|
|
1584
|
-
}
|
|
1585
|
-
case "thought_chunk": {
|
|
1586
|
-
const text = extractText(u.content);
|
|
1587
|
-
if (text) {
|
|
1588
|
-
this.scratch.thoughts += text;
|
|
1589
|
-
this.emitProgress({ type: "thought", text });
|
|
1590
|
-
}
|
|
1591
|
-
return;
|
|
1592
|
-
}
|
|
1593
|
-
case "tool_call":
|
|
1594
|
-
case "tool_call_update": {
|
|
1595
|
-
this.captureToolCall(u, u.sessionUpdate === "tool_call");
|
|
1596
|
-
return;
|
|
1597
|
-
}
|
|
1598
|
-
case "plan":
|
|
1599
|
-
if (Array.isArray(u.entries)) {
|
|
1600
|
-
this.scratch.plan = u.entries;
|
|
1601
|
-
this.emitProgress({ type: "plan", entries: u.entries });
|
|
1602
|
-
}
|
|
1603
|
-
return;
|
|
1604
|
-
case "usage_update":
|
|
1605
|
-
if (typeof u.used === "number" && typeof u.size === "number") {
|
|
1606
|
-
const usage = {
|
|
1607
|
-
used: u.used,
|
|
1608
|
-
size: u.size,
|
|
1609
|
-
...typeof u.cost === "object" && u.cost !== null ? { cost: u.cost } : {}
|
|
1610
|
-
};
|
|
1611
|
-
this.scratch.usage = usage;
|
|
1612
|
-
this.emitProgress({ type: "usage", usage });
|
|
1613
|
-
}
|
|
1614
|
-
return;
|
|
1615
|
-
case "available_commands_update":
|
|
1616
|
-
case "current_mode_update":
|
|
1617
|
-
case "config_option_update":
|
|
1618
|
-
case "session_info_update":
|
|
1619
|
-
case "user_message_chunk":
|
|
1620
|
-
case "next_edit_suggestions":
|
|
1621
|
-
case "elicitation":
|
|
1622
|
-
return;
|
|
1623
|
-
default:
|
|
1624
|
-
return;
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
/**
|
|
1628
|
-
* Fold a `tool_call` / `tool_call_update` notification into the scratch
|
|
1629
|
-
* tool-call map (deduped by toolCallId), extract any `diff` content into
|
|
1630
|
-
* the diffs list, and emit live progress.
|
|
1631
|
-
*/
|
|
1632
|
-
captureToolCall(u, isNew) {
|
|
1633
|
-
const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : "";
|
|
1634
|
-
if (!toolCallId) return;
|
|
1635
|
-
const prev = this.scratch.toolCalls.get(toolCallId);
|
|
1636
|
-
const record = {
|
|
1637
|
-
toolCallId,
|
|
1638
|
-
title: typeof u.title === "string" ? u.title : prev?.title ?? toolCallId,
|
|
1639
|
-
kind: typeof u.kind === "string" ? u.kind : prev?.kind,
|
|
1640
|
-
status: typeof u.status === "string" ? u.status : prev?.status ?? (isNew ? "pending" : "in_progress"),
|
|
1641
|
-
rawInput: isRecord(u.rawInput) ? u.rawInput : prev?.rawInput,
|
|
1642
|
-
rawOutput: isRecord(u.rawOutput) ? u.rawOutput : prev?.rawOutput
|
|
1643
|
-
};
|
|
1644
|
-
this.scratch.toolCalls.set(toolCallId, record);
|
|
1645
|
-
if (Array.isArray(u.content)) {
|
|
1646
|
-
for (const c of u.content) {
|
|
1647
|
-
if (c && typeof c === "object" && c.type === "diff") {
|
|
1648
|
-
const diff = {
|
|
1649
|
-
path: c.path,
|
|
1650
|
-
oldText: c.oldText,
|
|
1651
|
-
newText: c.newText
|
|
1652
|
-
};
|
|
1653
|
-
this.scratch.diffs.push(diff);
|
|
1654
|
-
this.emitProgress({ type: "diff", diff });
|
|
1655
|
-
}
|
|
1656
|
-
}
|
|
1657
|
-
}
|
|
1658
|
-
this.emitProgress({
|
|
1659
|
-
type: isNew ? "tool_call" : "tool_call_update",
|
|
1660
|
-
toolCall: record
|
|
1661
|
-
});
|
|
1662
|
-
}
|
|
1663
|
-
emitProgress(event) {
|
|
1664
|
-
if (!this.progressHandler) return;
|
|
1665
|
-
try {
|
|
1666
|
-
this.progressHandler(event);
|
|
1667
|
-
} catch {
|
|
1668
|
-
}
|
|
1669
|
-
}
|
|
1670
|
-
/** Live progress handler installed for the duration of a `prompt()` turn. */
|
|
1671
|
-
progressHandler = null;
|
|
1672
|
-
// Per-prompt scratch state
|
|
1673
|
-
scratch = { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
|
|
1674
|
-
resetScratch() {
|
|
1675
|
-
this.scratch = { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
|
|
1676
|
-
}
|
|
1677
|
-
async handlePermissionRequest(msg) {
|
|
1678
|
-
const id = msg.id;
|
|
1679
|
-
if (id === void 0) return;
|
|
1680
|
-
const params = msg.params;
|
|
1681
|
-
const toolCall = params?.toolCall;
|
|
1682
|
-
const options = Array.isArray(params?.options) ? params.options : [];
|
|
1683
|
-
if (!toolCall) {
|
|
1684
|
-
await this.sendErrorResponse(id, -32602, "toolCall is required");
|
|
1685
|
-
return;
|
|
1686
|
-
}
|
|
1687
|
-
const policyAbort = new AbortController();
|
|
1688
|
-
try {
|
|
1689
|
-
const outcome = await this.permissionPolicy({
|
|
1690
|
-
toolCall,
|
|
1691
|
-
options,
|
|
1692
|
-
signal: policyAbort.signal
|
|
1693
|
-
});
|
|
1694
|
-
await this.sendResult(id, { outcome });
|
|
1695
|
-
} catch (err) {
|
|
1696
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1697
|
-
await this.sendErrorResponse(id, -32603, `permission policy failed: ${message}`);
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
/**
|
|
1701
|
-
* Enforce authorization at privileged callback sinks (fs/write,
|
|
1702
|
-
* terminal/create). Unlike `handlePermissionRequest` which responds to
|
|
1703
|
-
* agent-initiated `session/request_permission` messages, this method is
|
|
1704
|
-
* called by the handler BEFORE dispatching to FileServer/TerminalServer,
|
|
1705
|
-
* closing the gap where the agent simply skips the voluntary permission
|
|
1706
|
-
* request and sends the privileged callback directly.
|
|
1707
|
-
*
|
|
1708
|
-
* Uses the session's permission policy. The default policy
|
|
1709
|
-
* (`defaultPermissionPolicy`) auto-approves everything — this is correct
|
|
1710
|
-
* for trusted local agents (CLI `acp spawn`, Director fan-out). For
|
|
1711
|
-
* untrusted/remote agents, the host should inject
|
|
1712
|
-
* `readOnlyPermissionPolicy` or an interactive policy.
|
|
1713
|
-
*
|
|
1714
|
-
* Returns true if the callback is authorized, false if denied.
|
|
1715
|
-
*/
|
|
1716
|
-
async authorizeCallback(partial) {
|
|
1717
|
-
try {
|
|
1718
|
-
const outcome = await this.permissionPolicy({
|
|
1719
|
-
toolCall: {
|
|
1720
|
-
sessionUpdate: "tool_call_update",
|
|
1721
|
-
toolCallId: partial.toolCallId,
|
|
1722
|
-
title: partial.title,
|
|
1723
|
-
kind: partial.kind,
|
|
1724
|
-
status: "pending"
|
|
1725
|
-
},
|
|
1726
|
-
options: [
|
|
1727
|
-
{ optionId: "allow", name: "Allow", kind: "allow_once" },
|
|
1728
|
-
{ optionId: "reject", name: "Reject", kind: "reject_once" }
|
|
1729
|
-
],
|
|
1730
|
-
signal: new AbortController().signal
|
|
1731
|
-
});
|
|
1732
|
-
return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always";
|
|
1733
|
-
} catch {
|
|
1734
|
-
return false;
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1737
|
-
async handleFsRequest(msg) {
|
|
1738
|
-
const id = msg.id;
|
|
1739
|
-
if (id === void 0) return;
|
|
1740
|
-
const params = msg.params;
|
|
1741
|
-
if (!params?.path) {
|
|
1742
|
-
await this.sendErrorResponse(id, -32602, "path is required");
|
|
1743
|
-
return;
|
|
1744
|
-
}
|
|
1745
|
-
if (msg.method === "fs/write_text_file") {
|
|
1746
|
-
const allowed = await this.authorizeCallback({
|
|
1747
|
-
toolCallId: `acp-fs-write-${id}`,
|
|
1748
|
-
title: `Write file: ${params.path}`,
|
|
1749
|
-
kind: "edit"
|
|
1750
|
-
});
|
|
1751
|
-
if (!allowed) {
|
|
1752
|
-
await this.sendErrorResponse(id, -32602, "filesystem write denied by permission policy");
|
|
1753
|
-
return;
|
|
1754
|
-
}
|
|
1755
|
-
}
|
|
1756
|
-
try {
|
|
1757
|
-
if (msg.method === "fs/read_text_file") {
|
|
1758
|
-
const result = await this.fileServer.readTextFile({
|
|
1759
|
-
sessionId: params.sessionId ?? "",
|
|
1760
|
-
path: params.path
|
|
1761
|
-
});
|
|
1762
|
-
await this.sendResult(id, result);
|
|
1763
|
-
} else {
|
|
1764
|
-
await this.fileServer.writeTextFile({
|
|
1765
|
-
sessionId: params.sessionId ?? "",
|
|
1766
|
-
path: params.path,
|
|
1767
|
-
content: params.content ?? ""
|
|
1768
|
-
});
|
|
1769
|
-
await this.sendResult(id, {});
|
|
1770
|
-
}
|
|
1771
|
-
} catch (err) {
|
|
1772
|
-
const code = err instanceof FsError ? -32602 : -32603;
|
|
1773
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1774
|
-
await this.sendErrorResponse(id, code, message);
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
async handleTerminalRequest(msg) {
|
|
1778
|
-
const id = msg.id;
|
|
1779
|
-
if (id === void 0) return;
|
|
1780
|
-
const params = msg.params ?? {};
|
|
1781
|
-
try {
|
|
1782
|
-
switch (msg.method) {
|
|
1783
|
-
case "terminal/create": {
|
|
1784
|
-
const allowed = await this.authorizeCallback({
|
|
1785
|
-
toolCallId: `acp-terminal-create-${id}`,
|
|
1786
|
-
title: `Run command: ${String(params.command ?? "")} ${(Array.isArray(params.args) ? params.args : []).join(" ")}`.trim(),
|
|
1787
|
-
kind: "execute"
|
|
1788
|
-
});
|
|
1789
|
-
if (!allowed) {
|
|
1790
|
-
await this.sendErrorResponse(id, -32602, "terminal create denied by permission policy");
|
|
1791
|
-
return;
|
|
1792
|
-
}
|
|
1793
|
-
const createOpts = {
|
|
1794
|
-
sessionId: String(params.sessionId ?? ""),
|
|
1795
|
-
command: String(params.command ?? ""),
|
|
1796
|
-
args: Array.isArray(params.args) ? params.args : []
|
|
1797
|
-
};
|
|
1798
|
-
if (Array.isArray(params.env)) {
|
|
1799
|
-
createOpts.env = params.env;
|
|
1800
|
-
}
|
|
1801
|
-
if (typeof params.cwd === "string") {
|
|
1802
|
-
createOpts.cwd = params.cwd;
|
|
1803
|
-
}
|
|
1804
|
-
if (typeof params.outputByteLimit === "number") {
|
|
1805
|
-
createOpts.outputByteLimit = params.outputByteLimit;
|
|
1806
|
-
}
|
|
1807
|
-
const result = this.terminalServer.create(createOpts);
|
|
1808
|
-
await this.sendResult(id, result);
|
|
1809
|
-
return;
|
|
1810
|
-
}
|
|
1811
|
-
case "terminal/output": {
|
|
1812
|
-
const terminalId = String(params.terminalId ?? "");
|
|
1813
|
-
const out = this.terminalServer.output(terminalId);
|
|
1814
|
-
await this.sendResult(id, out);
|
|
1815
|
-
return;
|
|
1816
|
-
}
|
|
1817
|
-
case "terminal/wait_for_exit": {
|
|
1818
|
-
const terminalId = String(params.terminalId ?? "");
|
|
1819
|
-
const exit = await this.terminalServer.waitForExit(terminalId);
|
|
1820
|
-
await this.sendResult(id, exit);
|
|
1821
|
-
return;
|
|
1822
|
-
}
|
|
1823
|
-
case "terminal/kill": {
|
|
1824
|
-
const terminalId = String(params.terminalId ?? "");
|
|
1825
|
-
this.terminalServer.kill(terminalId);
|
|
1826
|
-
await this.sendResult(id, {});
|
|
1827
|
-
return;
|
|
1828
|
-
}
|
|
1829
|
-
case "terminal/release": {
|
|
1830
|
-
const terminalId = String(params.terminalId ?? "");
|
|
1831
|
-
this.terminalServer.release(terminalId);
|
|
1832
|
-
await this.sendResult(id, {});
|
|
1833
|
-
return;
|
|
1834
|
-
}
|
|
1835
|
-
default:
|
|
1836
|
-
await this.sendErrorResponse(id, -32601, `unknown method: ${msg.method}`);
|
|
1837
|
-
}
|
|
1838
|
-
} catch (err) {
|
|
1839
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1840
|
-
await this.sendErrorResponse(id, -32603, message);
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
};
|
|
1844
|
-
function textContent(text) {
|
|
1845
|
-
return { type: "text", text };
|
|
1846
|
-
}
|
|
1847
|
-
function imageContent(mimeType, data) {
|
|
1848
|
-
return { type: "image", mimeType, data };
|
|
1849
|
-
}
|
|
1850
|
-
function audioContent(mimeType, data) {
|
|
1851
|
-
return { type: "audio", mimeType, data };
|
|
1852
|
-
}
|
|
1853
|
-
function extractText(block) {
|
|
1854
|
-
if (typeof block !== "object" || block === null) return "";
|
|
1855
|
-
const b = block;
|
|
1856
|
-
if (b.type === "text" && typeof b.text === "string") return b.text;
|
|
1857
|
-
if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
|
|
1858
|
-
return b.resource.text;
|
|
1859
|
-
}
|
|
1860
|
-
return "";
|
|
1861
|
-
}
|
|
1862
|
-
function isRecord(v) {
|
|
1863
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1864
|
-
}
|
|
1865
|
-
function emptyRunResult(stopReason) {
|
|
1866
|
-
return {
|
|
1867
|
-
text: "",
|
|
1868
|
-
stopReason,
|
|
1869
|
-
hasText: false,
|
|
1870
|
-
toolCalls: [],
|
|
1871
|
-
diffs: [],
|
|
1872
|
-
thoughts: ""
|
|
1873
|
-
};
|
|
1874
|
-
}
|
|
1875
|
-
|
|
1876
|
-
// src/agent/protocol-handler.ts
|
|
1877
|
-
function toWire(msg) {
|
|
1878
|
-
return msg;
|
|
1879
|
-
}
|
|
1880
|
-
var WRONGSTACK_VERSION = "0.274.1";
|
|
1881
|
-
var WRONGSTACK_AUTH_METHODS = [
|
|
1882
|
-
{
|
|
1883
|
-
id: "wrongstack-auth",
|
|
1884
|
-
name: "Run wstack auth",
|
|
1885
|
-
description: "Configure a WrongStack model provider in an interactive terminal.",
|
|
1886
|
-
type: "terminal",
|
|
1887
|
-
args: ["auth"]
|
|
1888
|
-
}
|
|
1889
|
-
];
|
|
1890
|
-
var DEFAULT_MODE_ID = "code";
|
|
1891
|
-
var DEFAULT_MODES = [
|
|
1892
|
-
{
|
|
1893
|
-
id: DEFAULT_MODE_ID,
|
|
1894
|
-
name: "Code",
|
|
1895
|
-
description: "Default agent mode for code-generation tasks."
|
|
1896
|
-
}
|
|
1897
|
-
];
|
|
1898
|
-
var ACPProtocolHandler = class {
|
|
1899
|
-
transport;
|
|
1900
|
-
defaultCwd;
|
|
1901
|
-
runTurn;
|
|
1902
|
-
onSessionNew;
|
|
1903
|
-
modes;
|
|
1904
|
-
configOptions;
|
|
1905
|
-
agentName;
|
|
1906
|
-
replayFor;
|
|
1907
|
-
seedFor;
|
|
1908
|
-
store;
|
|
1909
|
-
initialized = false;
|
|
1910
|
-
clientCapabilities = {};
|
|
1911
|
-
sessions = /* @__PURE__ */ new Map();
|
|
1912
|
-
nextId = 1;
|
|
1913
|
-
// Outbound request correlation (server → client requests, e.g.
|
|
1914
|
-
// session/request_permission). Keyed by our own `srv_N` ids.
|
|
1915
|
-
pendingOut = /* @__PURE__ */ new Map();
|
|
1916
|
-
nextOutId = 1;
|
|
1917
|
-
constructor(opts) {
|
|
1918
|
-
this.transport = opts.transport;
|
|
1919
|
-
this.defaultCwd = opts.defaultCwd;
|
|
1920
|
-
this.runTurn = opts.runTurn;
|
|
1921
|
-
this.onSessionNew = opts.onSessionNew ?? (() => {
|
|
1922
|
-
});
|
|
1923
|
-
this.modes = opts.modes ?? DEFAULT_MODES;
|
|
1924
|
-
this.configOptions = opts.configOptions ?? [];
|
|
1925
|
-
this.agentName = opts.agentName ?? "wrongstack";
|
|
1926
|
-
this.replayFor = opts.replayFor;
|
|
1927
|
-
this.seedFor = opts.seedFor;
|
|
1928
|
-
this.store = opts.store;
|
|
1929
|
-
if (typeof this.transport.onMessage === "function") {
|
|
1930
|
-
this.transport.onMessage((m) => this.maybeResolvePending(m));
|
|
1931
|
-
}
|
|
1932
|
-
}
|
|
1933
|
-
/**
|
|
1934
|
-
* Send a request to the client and await its response. Used for
|
|
1935
|
-
* server-initiated calls like `session/request_permission`. Rejects on
|
|
1936
|
-
* timeout or transport error so the caller can pick a safe fallback.
|
|
1937
|
-
*/
|
|
1938
|
-
request(method, params, timeoutMs = 6e4) {
|
|
1939
|
-
const id = `srv_${this.nextOutId++}`;
|
|
1940
|
-
return new Promise((resolve3, reject) => {
|
|
1941
|
-
const timer = setTimeout(() => {
|
|
1942
|
-
this.pendingOut.delete(id);
|
|
1943
|
-
reject(new Error(`${method} timed out after ${timeoutMs}ms`));
|
|
1944
|
-
}, timeoutMs);
|
|
1945
|
-
this.pendingOut.set(id, { resolve: resolve3, reject, timer });
|
|
1946
|
-
this.transport.send(toWire({ jsonrpc: "2.0", id, method, params })).catch((e) => {
|
|
1947
|
-
clearTimeout(timer);
|
|
1948
|
-
this.pendingOut.delete(id);
|
|
1949
|
-
reject(e instanceof Error ? e : new Error(String(e)));
|
|
1950
|
-
});
|
|
1951
|
-
});
|
|
1952
|
-
}
|
|
1953
|
-
maybeResolvePending(m) {
|
|
1954
|
-
const id = m.id;
|
|
1955
|
-
if (typeof id !== "string") return;
|
|
1956
|
-
const pending = this.pendingOut.get(id);
|
|
1957
|
-
if (!pending) return;
|
|
1958
|
-
this.pendingOut.delete(id);
|
|
1959
|
-
clearTimeout(pending.timer);
|
|
1960
|
-
const err = m.error;
|
|
1961
|
-
if (err) pending.reject(new Error(err.message ?? "client request failed"));
|
|
1962
|
-
else pending.resolve(m.result);
|
|
1963
|
-
}
|
|
1964
|
-
/**
|
|
1965
|
-
* Process one inbound message. Returns true if this was a terminal
|
|
1966
|
-
* message (rare; reserved for future use by the server's own
|
|
1967
|
-
* shutdown signal).
|
|
1968
|
-
*/
|
|
1969
|
-
async handleMessage(msg) {
|
|
1970
|
-
if (typeof msg !== "object" || msg === null) return false;
|
|
1971
|
-
const m = msg;
|
|
1972
|
-
if (m.id !== void 0 && (m.result !== void 0 || m.error !== void 0)) {
|
|
1973
|
-
return false;
|
|
1974
|
-
}
|
|
1975
|
-
if (m.id !== void 0 && typeof m.method === "string") {
|
|
1976
|
-
return this.handleRequest(m.id, m.method, m.params);
|
|
1977
|
-
}
|
|
1978
|
-
if (typeof m.method === "string") {
|
|
1979
|
-
return this.handleNotification(m.method, m.params);
|
|
1980
|
-
}
|
|
1981
|
-
return false;
|
|
1982
|
-
}
|
|
1983
|
-
/** Abort all active turns and drop session state. */
|
|
1984
|
-
close() {
|
|
1985
|
-
for (const [, session] of this.sessions) {
|
|
1986
|
-
session.abort.abort();
|
|
1987
|
-
}
|
|
1988
|
-
this.sessions.clear();
|
|
1989
|
-
for (const [, p] of this.pendingOut) {
|
|
1990
|
-
clearTimeout(p.timer);
|
|
1991
|
-
p.reject(new Error("protocol handler closed"));
|
|
1992
|
-
}
|
|
1993
|
-
this.pendingOut.clear();
|
|
1994
|
-
}
|
|
1995
|
-
// ────────────────────────────────────────────────────────────────────
|
|
1996
|
-
// Requests
|
|
1997
|
-
// ────────────────────────────────────────────────────────────────────
|
|
1998
|
-
async handleRequest(id, method, params) {
|
|
1999
|
-
if (method !== "initialize" && !this.initialized) {
|
|
2000
|
-
await this.sendError(id, -32e3, "Not initialized");
|
|
2001
|
-
return false;
|
|
2002
|
-
}
|
|
2003
|
-
try {
|
|
2004
|
-
switch (method) {
|
|
2005
|
-
case "initialize":
|
|
2006
|
-
return await this.handleInitialize(id, params);
|
|
2007
|
-
case "authenticate":
|
|
2008
|
-
return await this.handleAuthenticate(id, params);
|
|
2009
|
-
case "logout":
|
|
2010
|
-
return await this.handleLogout(id, params);
|
|
2011
|
-
case "session/new":
|
|
2012
|
-
return await this.handleSessionNew(id, params);
|
|
2013
|
-
case "session/load":
|
|
2014
|
-
return await this.handleSessionLoad(id, params);
|
|
2015
|
-
case "session/resume":
|
|
2016
|
-
return await this.handleSessionResume(id, params);
|
|
2017
|
-
case "session/close":
|
|
2018
|
-
return await this.handleSessionClose(id, params);
|
|
2019
|
-
case "session/delete":
|
|
2020
|
-
return await this.handleSessionDelete(id, params);
|
|
2021
|
-
case "session/prompt":
|
|
2022
|
-
return await this.handleSessionPrompt(id, params);
|
|
2023
|
-
case "session/set_mode":
|
|
2024
|
-
return await this.handleSetMode(id, params);
|
|
2025
|
-
case "session/set_config_option":
|
|
2026
|
-
return await this.handleSetConfigOption(id, params);
|
|
2027
|
-
case "session/list":
|
|
2028
|
-
return await this.handleSessionList(id);
|
|
2029
|
-
case "session/fork":
|
|
2030
|
-
return await this.handleSessionFork(id, params);
|
|
2031
|
-
case "providers/list":
|
|
2032
|
-
return await this.handleProvidersList(id, params);
|
|
2033
|
-
case "providers/set":
|
|
2034
|
-
return await this.handleProvidersSet(id, params);
|
|
2035
|
-
case "providers/disable":
|
|
2036
|
-
return await this.handleProvidersDisable(id, params);
|
|
2037
|
-
case "mcp/message":
|
|
2038
|
-
return await this.handleMcpMessage(id, params);
|
|
2039
|
-
default:
|
|
2040
|
-
await this.sendError(id, -32601, `Unknown method: ${method}`);
|
|
2041
|
-
return false;
|
|
2042
|
-
}
|
|
2043
|
-
} catch (err) {
|
|
2044
|
-
const { code, message, data } = errorToJsonRpc(err);
|
|
2045
|
-
await this.sendError(id, code, message, data);
|
|
2046
|
-
return false;
|
|
2047
|
-
}
|
|
2048
|
-
}
|
|
2049
|
-
async handleInitialize(id, params) {
|
|
2050
|
-
const p = params ?? {};
|
|
2051
|
-
if (p.clientCapabilities && typeof p.clientCapabilities === "object") {
|
|
2052
|
-
this.clientCapabilities = p.clientCapabilities;
|
|
2053
|
-
}
|
|
2054
|
-
this.initialized = true;
|
|
2055
|
-
await this.transport.send(toWire({
|
|
2056
|
-
jsonrpc: "2.0",
|
|
2057
|
-
id,
|
|
2058
|
-
result: {
|
|
2059
|
-
protocolVersion: ACP_PROTOCOL_VERSION,
|
|
2060
|
-
agentCapabilities: {
|
|
2061
|
-
loadSession: true,
|
|
2062
|
-
promptCapabilities: {
|
|
2063
|
-
// We route ACP image blocks into the core agent's multimodal
|
|
2064
|
-
// input (server-agent-turn.promptToAgentInput); whether the
|
|
2065
|
-
// model can see them is the configured provider's concern.
|
|
2066
|
-
image: true,
|
|
2067
|
-
audio: false,
|
|
2068
|
-
embeddedContext: true
|
|
2069
|
-
},
|
|
2070
|
-
mcpCapabilities: {
|
|
2071
|
-
http: false,
|
|
2072
|
-
sse: false
|
|
2073
|
-
},
|
|
2074
|
-
sessionCapabilities: {
|
|
2075
|
-
close: {},
|
|
2076
|
-
list: {},
|
|
2077
|
-
delete: {},
|
|
2078
|
-
resume: {},
|
|
2079
|
-
fork: {}
|
|
2080
|
-
},
|
|
2081
|
-
auth: {
|
|
2082
|
-
logout: {}
|
|
2083
|
-
}
|
|
2084
|
-
},
|
|
2085
|
-
agentInfo: {
|
|
2086
|
-
name: this.agentName,
|
|
2087
|
-
title: "WrongStack",
|
|
2088
|
-
version: WRONGSTACK_VERSION
|
|
2089
|
-
},
|
|
2090
|
-
authMethods: WRONGSTACK_AUTH_METHODS,
|
|
2091
|
-
modes: this.modes,
|
|
2092
|
-
configOptions: this.configOptions
|
|
2093
|
-
}
|
|
2094
|
-
}));
|
|
2095
|
-
return false;
|
|
2096
|
-
}
|
|
2097
|
-
async handleAuthenticate(id, _params) {
|
|
2098
|
-
await this.transport.send(toWire({
|
|
2099
|
-
jsonrpc: "2.0",
|
|
2100
|
-
id,
|
|
2101
|
-
result: { outcome: "unauthenticated" }
|
|
2102
|
-
}));
|
|
2103
|
-
return false;
|
|
2104
|
-
}
|
|
2105
|
-
async handleLogout(id, _params) {
|
|
2106
|
-
await this.transport.send(toWire({
|
|
2107
|
-
jsonrpc: "2.0",
|
|
2108
|
-
id,
|
|
2109
|
-
result: {}
|
|
2110
|
-
}));
|
|
2111
|
-
return false;
|
|
2112
|
-
}
|
|
2113
|
-
async handleSessionNew(id, params) {
|
|
2114
|
-
const p = params ?? {};
|
|
2115
|
-
const cwd = typeof p.cwd === "string" ? p.cwd : this.defaultCwd;
|
|
2116
|
-
const sessionId = `sess_${this.allocId()}`;
|
|
2117
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2118
|
-
const state = {
|
|
2119
|
-
id: sessionId,
|
|
2120
|
-
cwd,
|
|
2121
|
-
abort: new AbortController(),
|
|
2122
|
-
modeId: DEFAULT_MODE_ID,
|
|
2123
|
-
createdAt: now,
|
|
2124
|
-
updatedAt: now
|
|
2125
|
-
};
|
|
2126
|
-
this.sessions.set(sessionId, state);
|
|
2127
|
-
this.onSessionNew(state);
|
|
2128
|
-
await this.persist(state);
|
|
2129
|
-
await this.sendNotification({
|
|
2130
|
-
sessionId,
|
|
2131
|
-
update: {
|
|
2132
|
-
sessionUpdate: "current_mode_update",
|
|
2133
|
-
modeId: this.modes[0]?.id ?? DEFAULT_MODE_ID
|
|
2134
|
-
}
|
|
2135
|
-
});
|
|
2136
|
-
if (this.configOptions.length > 0) {
|
|
2137
|
-
await this.sendNotification({
|
|
2138
|
-
sessionId,
|
|
2139
|
-
update: {
|
|
2140
|
-
sessionUpdate: "config_option_update",
|
|
2141
|
-
configOptions: [...this.configOptions]
|
|
2142
|
-
}
|
|
2143
|
-
});
|
|
2144
|
-
}
|
|
2145
|
-
await this.transport.send(toWire({
|
|
2146
|
-
jsonrpc: "2.0",
|
|
2147
|
-
id,
|
|
2148
|
-
result: {
|
|
2149
|
-
sessionId,
|
|
2150
|
-
modes: this.modes,
|
|
2151
|
-
configOptions: this.configOptions
|
|
2152
|
-
}
|
|
2153
|
-
}));
|
|
2154
|
-
return false;
|
|
2155
|
-
}
|
|
2156
|
-
async handleSessionLoad(id, params) {
|
|
2157
|
-
const p = params ?? {};
|
|
2158
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
2159
|
-
const loadCwd = typeof p.cwd === "string" ? p.cwd : void 0;
|
|
2160
|
-
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
2161
|
-
if (!existing && sessionId && this.store) {
|
|
2162
|
-
const persisted = await this.store.load(sessionId);
|
|
2163
|
-
if (persisted) {
|
|
2164
|
-
const restored = {
|
|
2165
|
-
id: sessionId,
|
|
2166
|
-
cwd: persisted.cwd ?? loadCwd ?? this.defaultCwd,
|
|
2167
|
-
abort: new AbortController(),
|
|
2168
|
-
modeId: persisted.modeId ?? DEFAULT_MODE_ID,
|
|
2169
|
-
createdAt: persisted.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2170
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2171
|
-
...persisted.title !== void 0 ? { title: persisted.title } : {}
|
|
2172
|
-
};
|
|
2173
|
-
this.sessions.set(sessionId, restored);
|
|
2174
|
-
this.seedFor?.(sessionId, persisted.history ?? []);
|
|
2175
|
-
for (const update of persisted.history ?? []) {
|
|
2176
|
-
await this.sendNotification({ sessionId, update });
|
|
2177
|
-
}
|
|
2178
|
-
await this.sendNotification({
|
|
2179
|
-
sessionId,
|
|
2180
|
-
update: { sessionUpdate: "current_mode_update", modeId: restored.modeId }
|
|
2181
|
-
});
|
|
2182
|
-
await this.transport.send(toWire({
|
|
2183
|
-
jsonrpc: "2.0",
|
|
2184
|
-
id,
|
|
2185
|
-
result: {
|
|
2186
|
-
initialMode: { currentModeId: restored.modeId, availableModes: this.modes }
|
|
2187
|
-
}
|
|
2188
|
-
}));
|
|
2189
|
-
return false;
|
|
2190
|
-
}
|
|
2191
|
-
}
|
|
2192
|
-
if (existing) {
|
|
2193
|
-
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2194
|
-
const replay = sessionId ? this.replayFor?.(sessionId) : void 0;
|
|
2195
|
-
if (replay) {
|
|
2196
|
-
for (const update of replay) {
|
|
2197
|
-
await this.sendNotification({ sessionId, update });
|
|
2198
|
-
}
|
|
2199
|
-
}
|
|
2200
|
-
await this.sendNotification({
|
|
2201
|
-
sessionId,
|
|
2202
|
-
update: {
|
|
2203
|
-
sessionUpdate: "session_info_update",
|
|
2204
|
-
updatedAt: existing.updatedAt
|
|
2205
|
-
}
|
|
2206
|
-
});
|
|
2207
|
-
await this.sendNotification({
|
|
2208
|
-
sessionId,
|
|
2209
|
-
update: {
|
|
2210
|
-
sessionUpdate: "current_mode_update",
|
|
2211
|
-
modeId: existing.modeId
|
|
2212
|
-
}
|
|
2213
|
-
});
|
|
2214
|
-
await this.transport.send(toWire({
|
|
2215
|
-
jsonrpc: "2.0",
|
|
2216
|
-
id,
|
|
2217
|
-
result: {
|
|
2218
|
-
initialMode: {
|
|
2219
|
-
currentModeId: existing.modeId,
|
|
2220
|
-
availableModes: this.modes
|
|
2221
|
-
}
|
|
2222
|
-
}
|
|
2223
|
-
}));
|
|
2224
|
-
return false;
|
|
2225
|
-
}
|
|
2226
|
-
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
2227
|
-
return false;
|
|
2228
|
-
}
|
|
2229
|
-
async handleSessionResume(id, params) {
|
|
2230
|
-
const p = params ?? {};
|
|
2231
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
2232
|
-
const existing = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
2233
|
-
if (existing) {
|
|
2234
|
-
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2235
|
-
await this.transport.send(toWire({
|
|
2236
|
-
jsonrpc: "2.0",
|
|
2237
|
-
id,
|
|
2238
|
-
result: {
|
|
2239
|
-
initialMode: {
|
|
2240
|
-
currentModeId: existing.modeId,
|
|
2241
|
-
availableModes: this.modes
|
|
2242
|
-
}
|
|
2243
|
-
}
|
|
2244
|
-
}));
|
|
2245
|
-
return false;
|
|
2246
|
-
}
|
|
2247
|
-
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
2248
|
-
return false;
|
|
2249
|
-
}
|
|
2250
|
-
async handleSessionClose(id, params) {
|
|
2251
|
-
const p = params ?? {};
|
|
2252
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
2253
|
-
const session = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
2254
|
-
if (!session) {
|
|
2255
|
-
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
2256
|
-
return false;
|
|
2257
|
-
}
|
|
2258
|
-
session.abort.abort();
|
|
2259
|
-
if (sessionId) this.sessions.delete(sessionId);
|
|
2260
|
-
await this.transport.send(toWire({
|
|
2261
|
-
jsonrpc: "2.0",
|
|
2262
|
-
id,
|
|
2263
|
-
result: {}
|
|
2264
|
-
}));
|
|
2265
|
-
return false;
|
|
2266
|
-
}
|
|
2267
|
-
async handleSessionDelete(id, params) {
|
|
2268
|
-
const p = params ?? {};
|
|
2269
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
2270
|
-
if (!sessionId) {
|
|
2271
|
-
await this.sendError(id, -32e3, `session not found: ${sessionId}`);
|
|
2272
|
-
return false;
|
|
2273
|
-
}
|
|
2274
|
-
if (!this.sessions.has(sessionId)) {
|
|
2275
|
-
await this.transport.send(toWire({ jsonrpc: "2.0", id, result: { configOptions: [...this.configOptions] } }));
|
|
2276
|
-
return false;
|
|
2277
|
-
}
|
|
2278
|
-
const session = this.sessions.get(sessionId);
|
|
2279
|
-
session.abort.abort();
|
|
2280
|
-
this.sessions.delete(sessionId);
|
|
2281
|
-
await this.transport.send(toWire({
|
|
2282
|
-
jsonrpc: "2.0",
|
|
2283
|
-
id,
|
|
2284
|
-
result: {}
|
|
2285
|
-
}));
|
|
2286
|
-
return false;
|
|
2287
|
-
}
|
|
2288
|
-
async handleSessionFork(id, params) {
|
|
2289
|
-
const p = params ?? {};
|
|
2290
|
-
const sourceId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
2291
|
-
const source = sourceId ? this.sessions.get(sourceId) : void 0;
|
|
2292
|
-
if (!sourceId || !source) {
|
|
2293
|
-
await this.sendError(id, -32e3, `session not found: ${sourceId}`);
|
|
2294
|
-
return false;
|
|
2295
|
-
}
|
|
2296
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2297
|
-
const sessionId = `sess_${this.allocId()}`;
|
|
2298
|
-
const forked = {
|
|
2299
|
-
id: sessionId,
|
|
2300
|
-
cwd: typeof p.cwd === "string" ? p.cwd : source.cwd,
|
|
2301
|
-
abort: new AbortController(),
|
|
2302
|
-
modeId: source.modeId,
|
|
2303
|
-
createdAt: now,
|
|
2304
|
-
updatedAt: now,
|
|
2305
|
-
...source.title !== void 0 ? { title: source.title } : {}
|
|
2306
|
-
};
|
|
2307
|
-
const history = (this.replayFor?.(sourceId) ?? []).map((update) => ({
|
|
2308
|
-
sessionUpdate: update.sessionUpdate,
|
|
2309
|
-
content: structuredClone(update.content)
|
|
2310
|
-
}));
|
|
2311
|
-
this.sessions.set(sessionId, forked);
|
|
2312
|
-
this.seedFor?.(sessionId, history);
|
|
2313
|
-
this.onSessionNew(forked);
|
|
2314
|
-
await this.persist(forked, history);
|
|
2315
|
-
await this.sendNotification({
|
|
2316
|
-
sessionId,
|
|
2317
|
-
update: { sessionUpdate: "current_mode_update", modeId: forked.modeId }
|
|
2318
|
-
});
|
|
2319
|
-
await this.transport.send(toWire({
|
|
2320
|
-
jsonrpc: "2.0",
|
|
2321
|
-
id,
|
|
2322
|
-
result: {
|
|
2323
|
-
sessionId,
|
|
2324
|
-
modes: this.modes,
|
|
2325
|
-
configOptions: this.configOptions
|
|
2326
|
-
}
|
|
2327
|
-
}));
|
|
2328
|
-
return false;
|
|
2329
|
-
}
|
|
2330
|
-
async handleProvidersList(id, _params) {
|
|
2331
|
-
await this.transport.send(toWire({
|
|
2332
|
-
jsonrpc: "2.0",
|
|
2333
|
-
id,
|
|
2334
|
-
result: {
|
|
2335
|
-
providers: [],
|
|
2336
|
-
currentProviderId: null
|
|
2337
|
-
}
|
|
2338
|
-
}));
|
|
2339
|
-
return false;
|
|
2340
|
-
}
|
|
2341
|
-
async handleProvidersSet(id, _params) {
|
|
2342
|
-
await this.sendError(id, -32e3, "provider configuration not available through ACP; use wstack auth");
|
|
2343
|
-
return false;
|
|
2344
|
-
}
|
|
2345
|
-
async handleProvidersDisable(id, _params) {
|
|
2346
|
-
await this.transport.send(toWire({
|
|
2347
|
-
jsonrpc: "2.0",
|
|
2348
|
-
id,
|
|
2349
|
-
result: {}
|
|
2350
|
-
}));
|
|
2351
|
-
return false;
|
|
2352
|
-
}
|
|
2353
|
-
async handleMcpMessage(id, _params) {
|
|
2354
|
-
await this.sendError(id, -32e3, "MCP message routing not available through ACP");
|
|
2355
|
-
return false;
|
|
2356
|
-
}
|
|
2357
|
-
async handleSessionPrompt(id, params) {
|
|
2358
|
-
const p = params ?? {};
|
|
2359
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
2360
|
-
if (!sessionId || !this.sessions.has(sessionId)) {
|
|
2361
|
-
await this.sendError(id, -32e3, "unknown or missing sessionId");
|
|
2362
|
-
return false;
|
|
2363
|
-
}
|
|
2364
|
-
if (!Array.isArray(p.prompt)) {
|
|
2365
|
-
await this.sendError(id, -32602, "prompt must be an array of content blocks");
|
|
2366
|
-
return false;
|
|
2367
|
-
}
|
|
2368
|
-
const session = this.sessions.get(sessionId);
|
|
2369
|
-
if (session.abort.signal.aborted) {
|
|
2370
|
-
session.abort = new AbortController();
|
|
2371
|
-
}
|
|
2372
|
-
const turnSignal = new AbortController();
|
|
2373
|
-
const onCancel = () => turnSignal.abort();
|
|
2374
|
-
session.abort.signal.addEventListener("abort", onCancel, { once: true });
|
|
2375
|
-
const api = {
|
|
2376
|
-
clientCapabilities: this.clientCapabilities,
|
|
2377
|
-
requestPermission: async (req) => {
|
|
2378
|
-
const res = await this.request("session/request_permission", {
|
|
2379
|
-
sessionId,
|
|
2380
|
-
toolCall: req.toolCall,
|
|
2381
|
-
options: req.options
|
|
2382
|
-
});
|
|
2383
|
-
const outcome = res?.outcome;
|
|
2384
|
-
return outcome ?? { outcome: "cancelled" };
|
|
2385
|
-
},
|
|
2386
|
-
readTextFile: async (params2) => {
|
|
2387
|
-
const res = await this.request("fs/read_text_file", { sessionId, ...params2 });
|
|
2388
|
-
return String(res?.content ?? "");
|
|
2389
|
-
},
|
|
2390
|
-
writeTextFile: async (params2) => {
|
|
2391
|
-
await this.request("fs/write_text_file", { sessionId, ...params2 });
|
|
2392
|
-
},
|
|
2393
|
-
runTerminal: async ({ command, args, cwd }) => {
|
|
2394
|
-
const created = await this.request("terminal/create", {
|
|
2395
|
-
sessionId,
|
|
2396
|
-
command,
|
|
2397
|
-
...args ? { args } : {},
|
|
2398
|
-
...cwd ? { cwd } : {}
|
|
2399
|
-
});
|
|
2400
|
-
const terminalId = created?.terminalId;
|
|
2401
|
-
if (!terminalId) return { output: "", exitCode: null };
|
|
2402
|
-
try {
|
|
2403
|
-
const exit = await this.request("terminal/wait_for_exit", { sessionId, terminalId });
|
|
2404
|
-
const out = await this.request("terminal/output", { sessionId, terminalId });
|
|
2405
|
-
return {
|
|
2406
|
-
output: String(out?.output ?? ""),
|
|
2407
|
-
exitCode: typeof exit?.exitCode === "number" ? exit.exitCode : null
|
|
2408
|
-
};
|
|
2409
|
-
} finally {
|
|
2410
|
-
try {
|
|
2411
|
-
await this.request("terminal/release", { sessionId, terminalId });
|
|
2412
|
-
} catch {
|
|
2413
|
-
}
|
|
2414
|
-
}
|
|
2415
|
-
}
|
|
2416
|
-
};
|
|
2417
|
-
let result;
|
|
2418
|
-
try {
|
|
2419
|
-
result = await this.runTurn(
|
|
2420
|
-
{ sessionId, prompt: p.prompt, signal: turnSignal.signal },
|
|
2421
|
-
(update) => this.sendNotification({ sessionId, update }),
|
|
2422
|
-
api
|
|
2423
|
-
);
|
|
2424
|
-
} catch (err) {
|
|
2425
|
-
session.abort.signal.removeEventListener("abort", onCancel);
|
|
2426
|
-
const { code, message, data } = errorToJsonRpc(err);
|
|
2427
|
-
await this.sendError(id, code, message, data);
|
|
2428
|
-
return false;
|
|
2429
|
-
}
|
|
2430
|
-
session.abort.signal.removeEventListener("abort", onCancel);
|
|
2431
|
-
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2432
|
-
await this.persist(session);
|
|
2433
|
-
await this.transport.send(toWire({
|
|
2434
|
-
jsonrpc: "2.0",
|
|
2435
|
-
id,
|
|
2436
|
-
result: { stopReason: result.stopReason }
|
|
2437
|
-
}));
|
|
2438
|
-
return false;
|
|
2439
|
-
}
|
|
2440
|
-
async handleSetMode(id, params) {
|
|
2441
|
-
const p = params ?? {};
|
|
2442
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
2443
|
-
const modeId = typeof p.modeId === "string" ? p.modeId : null;
|
|
2444
|
-
const session = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
2445
|
-
if (!session || !modeId || !this.modes.some((m) => m.id === modeId)) {
|
|
2446
|
-
await this.sendError(id, -32602, "invalid sessionId or modeId");
|
|
2447
|
-
return false;
|
|
2448
|
-
}
|
|
2449
|
-
session.modeId = modeId;
|
|
2450
|
-
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2451
|
-
await this.sendNotification({
|
|
2452
|
-
sessionId,
|
|
2453
|
-
update: { sessionUpdate: "current_mode_update", modeId }
|
|
2454
|
-
});
|
|
2455
|
-
await this.transport.send(toWire({ jsonrpc: "2.0", id, result: {} }));
|
|
2456
|
-
return false;
|
|
2457
|
-
}
|
|
2458
|
-
async handleSetConfigOption(id, params) {
|
|
2459
|
-
const p = params ?? {};
|
|
2460
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
2461
|
-
const optionId = typeof p.configId === "string" ? p.configId : null;
|
|
2462
|
-
const value = typeof p.value === "string" ? p.value : null;
|
|
2463
|
-
const session = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
2464
|
-
const option = optionId ? this.configOptions.find((o) => o.id === optionId) : void 0;
|
|
2465
|
-
if (!session || !option || value === null || !option.options.some((o) => o.value === value)) {
|
|
2466
|
-
await this.sendError(id, -32602, "invalid sessionId, configId, or value");
|
|
2467
|
-
return false;
|
|
2468
|
-
}
|
|
2469
|
-
option.currentValue = value;
|
|
2470
|
-
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2471
|
-
await this.sendNotification({
|
|
2472
|
-
sessionId,
|
|
2473
|
-
update: {
|
|
2474
|
-
sessionUpdate: "config_option_update",
|
|
2475
|
-
configOptions: [...this.configOptions]
|
|
2476
|
-
}
|
|
2477
|
-
});
|
|
2478
|
-
await this.transport.send(toWire({ jsonrpc: "2.0", id, result: { configOptions: [...this.configOptions] } }));
|
|
2479
|
-
return false;
|
|
2480
|
-
}
|
|
2481
|
-
async handleSessionList(id) {
|
|
2482
|
-
const sessions = Array.from(this.sessions.values()).map((s) => {
|
|
2483
|
-
const out = {
|
|
2484
|
-
sessionId: s.id,
|
|
2485
|
-
cwd: s.cwd,
|
|
2486
|
-
updatedAt: s.updatedAt
|
|
2487
|
-
};
|
|
2488
|
-
if (s.title !== void 0) out.title = s.title;
|
|
2489
|
-
return out;
|
|
2490
|
-
});
|
|
2491
|
-
await this.transport.send(toWire({
|
|
2492
|
-
jsonrpc: "2.0",
|
|
2493
|
-
id,
|
|
2494
|
-
result: { sessions }
|
|
2495
|
-
}));
|
|
2496
|
-
return false;
|
|
2497
|
-
}
|
|
2498
|
-
// ────────────────────────────────────────────────────────────────────
|
|
2499
|
-
// Notifications
|
|
2500
|
-
// ────────────────────────────────────────────────────────────────────
|
|
2501
|
-
async handleNotification(method, params) {
|
|
2502
|
-
switch (method) {
|
|
2503
|
-
case "session/cancel": {
|
|
2504
|
-
const p = params ?? {};
|
|
2505
|
-
const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
|
|
2506
|
-
const session = sessionId ? this.sessions.get(sessionId) : void 0;
|
|
2507
|
-
if (session) {
|
|
2508
|
-
session.abort.abort();
|
|
2509
|
-
}
|
|
2510
|
-
return false;
|
|
2511
|
-
}
|
|
2512
|
-
case "$/cancel_request": {
|
|
2513
|
-
return false;
|
|
2514
|
-
}
|
|
2515
|
-
case "exit":
|
|
2516
|
-
this.close();
|
|
2517
|
-
return true;
|
|
2518
|
-
default:
|
|
2519
|
-
return false;
|
|
2520
|
-
}
|
|
2521
|
-
}
|
|
2522
|
-
// ────────────────────────────────────────────────────────────────────
|
|
2523
|
-
// Wire helpers
|
|
2524
|
-
// ────────────────────────────────────────────────────────────────────
|
|
2525
|
-
async sendNotification(params) {
|
|
2526
|
-
await this.transport.send(toWire({ jsonrpc: "2.0", method: "session/update", params }));
|
|
2527
|
-
}
|
|
2528
|
-
/** Best-effort durable persistence of a session + its recorded history. */
|
|
2529
|
-
async persist(state, history = void 0) {
|
|
2530
|
-
if (!this.store) return;
|
|
2531
|
-
try {
|
|
2532
|
-
await this.store.save(state, history ?? this.replayFor?.(state.id));
|
|
2533
|
-
} catch {
|
|
2534
|
-
}
|
|
2535
|
-
}
|
|
2536
|
-
async sendError(id, code, message, data) {
|
|
2537
|
-
const error = { code, message };
|
|
2538
|
-
if (data !== void 0) error.data = data;
|
|
2539
|
-
await this.transport.send(toWire({ jsonrpc: "2.0", id, error }));
|
|
2540
|
-
}
|
|
2541
|
-
allocId() {
|
|
2542
|
-
return this.nextId++;
|
|
2543
|
-
}
|
|
2544
|
-
};
|
|
2545
|
-
function errorToJsonRpc(err) {
|
|
2546
|
-
if (err && typeof err === "object") {
|
|
2547
|
-
const e = err;
|
|
2548
|
-
if (typeof e.code === "number" && typeof e.message === "string") {
|
|
2549
|
-
const result = {
|
|
2550
|
-
code: e.code,
|
|
2551
|
-
message: e.message
|
|
2552
|
-
};
|
|
2553
|
-
if (e.data !== void 0) result.data = e.data;
|
|
2554
|
-
return result;
|
|
2555
|
-
}
|
|
2556
|
-
}
|
|
2557
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
2558
|
-
return { code: -32603, message };
|
|
2559
|
-
}
|
|
2560
|
-
|
|
2561
|
-
// src/agent/wrongstack-acp-agent.ts
|
|
2562
|
-
import { fileURLToPath } from "node:url";
|
|
2563
|
-
import { createServer } from "node:http";
|
|
2564
|
-
import { writeErr as writeErr2 } from "@wrongstack/core";
|
|
2565
|
-
var WrongStackACPServer = class {
|
|
2566
|
-
transport;
|
|
2567
|
-
handler;
|
|
2568
|
-
options;
|
|
2569
|
-
/** HTTP server when transport mode is HTTP. */
|
|
2570
|
-
httpServer = null;
|
|
2571
|
-
running = false;
|
|
2572
|
-
constructor(opts = {}) {
|
|
2573
|
-
this.options = opts;
|
|
2574
|
-
this.transport = new StdioTransport();
|
|
2575
|
-
const runTurn = opts.runTurn ?? defaultEchoRunTurn;
|
|
2576
|
-
this.handler = new ACPProtocolHandler({
|
|
2577
|
-
transport: this.transport,
|
|
2578
|
-
defaultCwd: opts.defaultCwd ?? process.cwd(),
|
|
2579
|
-
runTurn,
|
|
2580
|
-
agentName: opts.agentName,
|
|
2581
|
-
...opts.replayFor ? { replayFor: opts.replayFor } : {},
|
|
2582
|
-
...opts.seedFor ? { seedFor: opts.seedFor } : {},
|
|
2583
|
-
...opts.store ? { store: opts.store } : {}
|
|
2584
|
-
});
|
|
2585
|
-
}
|
|
2586
|
-
/**
|
|
2587
|
-
* Start the server. Mode depends on `options.transport`:
|
|
2588
|
-
* - 'stdio' (default): reads JSON-RPC from stdin, writes to stdout.
|
|
2589
|
-
* - number: listens as HTTP on the given port.
|
|
2590
|
-
*/
|
|
2591
|
-
async start() {
|
|
2592
|
-
const transportMode = this.options.transport;
|
|
2593
|
-
if (typeof transportMode === "number") {
|
|
2594
|
-
await this.startHttp(transportMode);
|
|
2595
|
-
} else {
|
|
2596
|
-
await this.startStdio();
|
|
2597
|
-
}
|
|
2598
|
-
}
|
|
2599
|
-
async startStdio() {
|
|
2600
|
-
if (this.options.legacyStartupMarker) {
|
|
2601
|
-
this.transport.sendStartupMarker();
|
|
2602
|
-
}
|
|
2603
|
-
this.running = true;
|
|
2604
|
-
while (this.running) {
|
|
2605
|
-
const msg = await this.transport.read();
|
|
2606
|
-
if (!msg) break;
|
|
2607
|
-
const terminal = await this.handler.handleMessage(msg);
|
|
2608
|
-
if (terminal) break;
|
|
2609
|
-
}
|
|
2610
|
-
this.transport.close();
|
|
2611
|
-
}
|
|
2612
|
-
async startHttp(port) {
|
|
2613
|
-
const host = this.options.host ?? "127.0.0.1";
|
|
2614
|
-
const handler = this.handler;
|
|
2615
|
-
const authToken = this.options.authToken;
|
|
2616
|
-
let httpChain = Promise.resolve();
|
|
2617
|
-
this.httpServer = createServer(async (req, res) => {
|
|
2618
|
-
if (authToken) {
|
|
2619
|
-
const url = new URL(req.url ?? "/", `http://${host}:${port}`);
|
|
2620
|
-
const queryToken = url.searchParams.get("token");
|
|
2621
|
-
const authHeader = req.headers["authorization"];
|
|
2622
|
-
const bearerToken = Array.isArray(authHeader) ? authHeader[0]?.replace(/^Bearer\s+/i, "") : authHeader?.replace(/^Bearer\s+/i, "");
|
|
2623
|
-
const supplied = queryToken ?? bearerToken ?? "";
|
|
2624
|
-
if (supplied !== authToken) {
|
|
2625
|
-
res.writeHead(401, { "Content-Type": "application/json" });
|
|
2626
|
-
res.end(JSON.stringify({ error: { code: -32001, message: "Unauthorized" } }));
|
|
2627
|
-
return;
|
|
2628
|
-
}
|
|
2629
|
-
}
|
|
2630
|
-
const selfOrigin = `http://${host}:${port}`;
|
|
2631
|
-
const reqOrigin = Array.isArray(req.headers.origin) ? req.headers.origin[0] : req.headers.origin;
|
|
2632
|
-
if (reqOrigin && reqOrigin !== selfOrigin) {
|
|
2633
|
-
res.writeHead(403);
|
|
2634
|
-
res.end(JSON.stringify({ error: "cross-origin request forbidden" }));
|
|
2635
|
-
return;
|
|
2636
|
-
}
|
|
2637
|
-
if (reqOrigin) res.setHeader("Access-Control-Allow-Origin", reqOrigin);
|
|
2638
|
-
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
2639
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id, Authorization");
|
|
2640
|
-
if (req.method === "OPTIONS") {
|
|
2641
|
-
res.writeHead(204);
|
|
2642
|
-
res.end();
|
|
2643
|
-
return;
|
|
2644
|
-
}
|
|
2645
|
-
if (req.method !== "POST") {
|
|
2646
|
-
res.writeHead(405);
|
|
2647
|
-
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
2648
|
-
return;
|
|
2649
|
-
}
|
|
2650
|
-
const MAX_HTTP_BODY = 10 * 1024 * 1024;
|
|
2651
|
-
let body = "";
|
|
2652
|
-
let bodyBytes = 0;
|
|
2653
|
-
let tooLarge = false;
|
|
2654
|
-
for await (const chunk of req) {
|
|
2655
|
-
bodyBytes += chunk.length;
|
|
2656
|
-
if (bodyBytes > MAX_HTTP_BODY) {
|
|
2657
|
-
tooLarge = true;
|
|
2658
|
-
break;
|
|
2659
|
-
}
|
|
2660
|
-
body += chunk;
|
|
2661
|
-
}
|
|
2662
|
-
if (tooLarge) {
|
|
2663
|
-
res.writeHead(413, { "Content-Type": "application/json" });
|
|
2664
|
-
res.end(JSON.stringify({ error: { code: -32700, message: "Request body too large" } }));
|
|
2665
|
-
return;
|
|
2666
|
-
}
|
|
2667
|
-
let msg;
|
|
2668
|
-
try {
|
|
2669
|
-
msg = JSON.parse(body);
|
|
2670
|
-
} catch {
|
|
2671
|
-
res.writeHead(400);
|
|
2672
|
-
res.end(JSON.stringify({ error: { code: -32700, message: "Parse error" } }));
|
|
2673
|
-
return;
|
|
2674
|
-
}
|
|
2675
|
-
const isNotification = typeof msg === "object" && msg !== null && msg.id === void 0 && typeof msg.method === "string";
|
|
2676
|
-
if (isNotification) {
|
|
2677
|
-
try {
|
|
2678
|
-
await handler.handleMessage(msg);
|
|
2679
|
-
} catch {
|
|
2680
|
-
}
|
|
2681
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2682
|
-
res.end(JSON.stringify({ notifications: [] }));
|
|
2683
|
-
return;
|
|
2684
|
-
}
|
|
2685
|
-
const requestPromise = httpChain.then(async () => {
|
|
2686
|
-
const notifications = [];
|
|
2687
|
-
let response = null;
|
|
2688
|
-
const originalSend = this.transport.send.bind(this.transport);
|
|
2689
|
-
this.transport.send = async (m) => {
|
|
2690
|
-
if (m.id !== void 0 && (m.result !== void 0 || m.error !== void 0)) {
|
|
2691
|
-
response = m;
|
|
2692
|
-
} else if (m.method === "session/update") {
|
|
2693
|
-
notifications.push(m.params);
|
|
2694
|
-
} else {
|
|
2695
|
-
notifications.push(m);
|
|
2696
|
-
}
|
|
2697
|
-
};
|
|
2698
|
-
try {
|
|
2699
|
-
await handler.handleMessage(msg);
|
|
2700
|
-
} finally {
|
|
2701
|
-
this.transport.send = originalSend;
|
|
2702
|
-
}
|
|
2703
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2704
|
-
const responseBody = response !== null ? { ...response, notifications } : { notifications };
|
|
2705
|
-
res.end(JSON.stringify(responseBody));
|
|
2706
|
-
});
|
|
2707
|
-
httpChain = requestPromise.catch(() => void 0);
|
|
2708
|
-
try {
|
|
2709
|
-
await requestPromise;
|
|
2710
|
-
} catch {
|
|
2711
|
-
}
|
|
2712
|
-
});
|
|
2713
|
-
return new Promise((resolve3) => {
|
|
2714
|
-
this.httpServer.listen(port, host, () => {
|
|
2715
|
-
writeErr2(`[wstack-acp] HTTP server listening on http://${host}:${port}
|
|
2716
|
-
`);
|
|
2717
|
-
this.running = true;
|
|
2718
|
-
resolve3();
|
|
2719
|
-
});
|
|
2720
|
-
});
|
|
2721
|
-
}
|
|
2722
|
-
/** Stop the server. */
|
|
2723
|
-
stop() {
|
|
2724
|
-
this.running = false;
|
|
2725
|
-
this.transport.close();
|
|
2726
|
-
if (this.httpServer) {
|
|
2727
|
-
this.httpServer.close();
|
|
2728
|
-
this.httpServer = null;
|
|
2729
|
-
}
|
|
2730
|
-
}
|
|
2731
|
-
};
|
|
2732
|
-
var defaultEchoRunTurn = async (_input, _emit) => {
|
|
2733
|
-
return { stopReason: "end_turn" };
|
|
2734
|
-
};
|
|
2735
|
-
async function main() {
|
|
2736
|
-
const server = new WrongStackACPServer();
|
|
2737
|
-
await server.start();
|
|
2738
|
-
}
|
|
2739
|
-
var isEntrypoint = process.argv[1] !== void 0 && fileURLToPath(import.meta.url) === process.argv[1];
|
|
2740
|
-
if (isEntrypoint) {
|
|
2741
|
-
main().catch((err) => {
|
|
2742
|
-
writeErr2(`[wstack-acp fatal] ${err}
|
|
2743
|
-
`);
|
|
2744
|
-
process.exit(1);
|
|
2745
|
-
});
|
|
2746
|
-
}
|
|
2747
|
-
|
|
2748
|
-
// src/agent/server-agent-turn.ts
|
|
2749
|
-
function makeACPServerAgentTurn(opts) {
|
|
2750
|
-
const agents = /* @__PURE__ */ new Map();
|
|
2751
|
-
const timeouts = /* @__PURE__ */ new Map();
|
|
2752
|
-
const history = /* @__PURE__ */ new Map();
|
|
2753
|
-
const pendingSeed = /* @__PURE__ */ new Set();
|
|
2754
|
-
const timeoutMs = opts.timeoutMs ?? 5 * 6e4;
|
|
2755
|
-
const turn = async (input, emit, api) => {
|
|
2756
|
-
let agent = agents.get(input.sessionId);
|
|
2757
|
-
if (!agent) {
|
|
2758
|
-
agent = await opts.agentFor(input.sessionId, process.cwd(), api);
|
|
2759
|
-
agents.set(input.sessionId, agent);
|
|
2760
|
-
if (pendingSeed.has(input.sessionId)) {
|
|
2761
|
-
pendingSeed.delete(input.sessionId);
|
|
2762
|
-
seedAgentContext(agent, history.get(input.sessionId) ?? []);
|
|
2763
|
-
}
|
|
2764
|
-
}
|
|
2765
|
-
const turnAbort = new AbortController();
|
|
2766
|
-
const abortForTimeout = () => turnAbort.abort();
|
|
2767
|
-
const onParentAbort = () => turnAbort.abort();
|
|
2768
|
-
if (input.signal.aborted) {
|
|
2769
|
-
turnAbort.abort();
|
|
2770
|
-
} else {
|
|
2771
|
-
input.signal.addEventListener("abort", onParentAbort, { once: true });
|
|
2772
|
-
}
|
|
2773
|
-
const timer = setTimeout(() => {
|
|
2774
|
-
timeouts.delete(input.sessionId);
|
|
2775
|
-
abortForTimeout();
|
|
2776
|
-
}, timeoutMs);
|
|
2777
|
-
timeouts.set(input.sessionId, timer);
|
|
2778
|
-
const unsub = [];
|
|
2779
|
-
const bus = agent.events;
|
|
2780
|
-
if (bus?.on) {
|
|
2781
|
-
unsub.push(
|
|
2782
|
-
bus.on("tool.started", (e) => {
|
|
2783
|
-
emit({
|
|
2784
|
-
sessionUpdate: "tool_call",
|
|
2785
|
-
toolCallId: e.id,
|
|
2786
|
-
title: toolTitle(e.name, e.input),
|
|
2787
|
-
kind: toolNameToKind(e.name),
|
|
2788
|
-
status: "in_progress",
|
|
2789
|
-
...isRecord2(e.input) ? { rawInput: e.input } : {}
|
|
2790
|
-
});
|
|
2791
|
-
}),
|
|
2792
|
-
bus.on("tool.executed", (e) => {
|
|
2793
|
-
emit({
|
|
2794
|
-
sessionUpdate: "tool_call_update",
|
|
2795
|
-
toolCallId: e.id ?? e.name,
|
|
2796
|
-
status: e.ok ? "completed" : "failed",
|
|
2797
|
-
...e.output !== void 0 ? {
|
|
2798
|
-
content: [
|
|
2799
|
-
{ type: "content", content: { type: "text", text: e.output } }
|
|
2800
|
-
]
|
|
2801
|
-
} : {}
|
|
2802
|
-
});
|
|
2803
|
-
})
|
|
2804
|
-
);
|
|
2805
|
-
}
|
|
2806
|
-
try {
|
|
2807
|
-
const userInput = promptToAgentInput(input.prompt);
|
|
2808
|
-
const result = await agent.run(userInput, { signal: turnAbort.signal });
|
|
2809
|
-
const text = extractText2(result);
|
|
2810
|
-
if (text) {
|
|
2811
|
-
emit({
|
|
2812
|
-
sessionUpdate: "agent_message_chunk",
|
|
2813
|
-
content: { type: "text", text }
|
|
2814
|
-
});
|
|
2815
|
-
}
|
|
2816
|
-
const userText = promptToText(input.prompt);
|
|
2817
|
-
const hist = history.get(input.sessionId) ?? [];
|
|
2818
|
-
if (userText) {
|
|
2819
|
-
hist.push({ sessionUpdate: "user_message_chunk", content: { type: "text", text: userText } });
|
|
2820
|
-
}
|
|
2821
|
-
if (text) {
|
|
2822
|
-
hist.push({ sessionUpdate: "agent_message_chunk", content: { type: "text", text } });
|
|
2823
|
-
}
|
|
2824
|
-
if (hist.length > 0) history.set(input.sessionId, hist);
|
|
2825
|
-
const plan = extractPlan(result);
|
|
2826
|
-
if (plan.length > 0) {
|
|
2827
|
-
emit({
|
|
2828
|
-
sessionUpdate: "plan",
|
|
2829
|
-
entries: plan
|
|
2830
|
-
});
|
|
2831
|
-
}
|
|
2832
|
-
const usage = extractUsage(result);
|
|
2833
|
-
if (usage) {
|
|
2834
|
-
emit({
|
|
2835
|
-
sessionUpdate: "usage_update",
|
|
2836
|
-
used: usage.used,
|
|
2837
|
-
size: usage.size,
|
|
2838
|
-
...usage.cost ? { cost: usage.cost } : {}
|
|
2839
|
-
});
|
|
2840
|
-
}
|
|
2841
|
-
const result_out = {
|
|
2842
|
-
// `turnAbort.signal` covers both client cancellation and the
|
|
2843
|
-
// wall-clock timeout, so either maps to stopReason 'cancelled'.
|
|
2844
|
-
stopReason: pickStopReason(result, turnAbort.signal)
|
|
2845
|
-
};
|
|
2846
|
-
if (text) result_out.text = text;
|
|
2847
|
-
const runTurnPlan = extractPlan(result);
|
|
2848
|
-
if (runTurnPlan.length > 0) result_out.plan = runTurnPlan;
|
|
2849
|
-
if (usage) result_out.usage = usage;
|
|
2850
|
-
return result_out;
|
|
2851
|
-
} finally {
|
|
2852
|
-
clearTimeout(timer);
|
|
2853
|
-
timeouts.delete(input.sessionId);
|
|
2854
|
-
input.signal.removeEventListener("abort", onParentAbort);
|
|
2855
|
-
for (const u of unsub) u();
|
|
2856
|
-
}
|
|
2857
|
-
};
|
|
2858
|
-
const replay = (sessionId) => history.get(sessionId) ?? [];
|
|
2859
|
-
const seed = (sessionId, incoming) => {
|
|
2860
|
-
if (incoming.length === 0) return;
|
|
2861
|
-
history.set(sessionId, [...incoming]);
|
|
2862
|
-
pendingSeed.add(sessionId);
|
|
2863
|
-
};
|
|
2864
|
-
return Object.assign(turn, { replay, seed });
|
|
2865
|
-
}
|
|
2866
|
-
function seedAgentContext(agent, history) {
|
|
2867
|
-
const state = agent.ctx?.state;
|
|
2868
|
-
if (!state?.appendMessage) return;
|
|
2869
|
-
for (const u of history) {
|
|
2870
|
-
const text = u.content?.text;
|
|
2871
|
-
if (typeof text !== "string" || text.length === 0) continue;
|
|
2872
|
-
const role = u.sessionUpdate === "user_message_chunk" ? "user" : "assistant";
|
|
2873
|
-
state.appendMessage({ role, content: text });
|
|
2874
|
-
}
|
|
2875
|
-
}
|
|
2876
|
-
function toolNameToKind(name) {
|
|
2877
|
-
const n = name.toLowerCase();
|
|
2878
|
-
if (n.includes("read") || n.includes("cat")) return "read";
|
|
2879
|
-
if (n.includes("write") || n.includes("edit") || n.includes("apply") || n.includes("patch")) return "edit";
|
|
2880
|
-
if (n.includes("delete") || n.includes("rm")) return "delete";
|
|
2881
|
-
if (n.includes("move") || n.includes("rename") || n.includes("mv")) return "move";
|
|
2882
|
-
if (n.includes("grep") || n.includes("glob") || n.includes("search") || n.includes("find")) return "search";
|
|
2883
|
-
if (n.includes("bash") || n.includes("shell") || n.includes("exec") || n.includes("run") || n.includes("terminal")) return "execute";
|
|
2884
|
-
if (n.includes("fetch") || n.includes("http") || n.includes("web") || n.includes("url")) return "fetch";
|
|
2885
|
-
if (n.includes("think") || n.includes("plan")) return "think";
|
|
2886
|
-
return "other";
|
|
2887
|
-
}
|
|
2888
|
-
function toolTitle(name, input) {
|
|
2889
|
-
if (isRecord2(input)) {
|
|
2890
|
-
const path4 = input.path ?? input.file ?? input.filePath ?? input.pattern ?? input.command;
|
|
2891
|
-
if (typeof path4 === "string" && path4.length > 0) {
|
|
2892
|
-
return `${name}: ${path4.length > 80 ? `${path4.slice(0, 77)}\u2026` : path4}`;
|
|
2893
|
-
}
|
|
2894
|
-
}
|
|
2895
|
-
return name;
|
|
2896
|
-
}
|
|
2897
|
-
function isRecord2(v) {
|
|
2898
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2899
|
-
}
|
|
2900
|
-
function promptToAgentInput(blocks) {
|
|
2901
|
-
const hasImage = blocks.some((b) => b.type === "image");
|
|
2902
|
-
if (!hasImage) {
|
|
2903
|
-
return promptToText(blocks);
|
|
2904
|
-
}
|
|
2905
|
-
const out = [];
|
|
2906
|
-
for (const b of blocks) {
|
|
2907
|
-
if (b.type === "text") {
|
|
2908
|
-
out.push({ type: "text", text: b.text });
|
|
2909
|
-
} else if (b.type === "image") {
|
|
2910
|
-
out.push({
|
|
2911
|
-
type: "image",
|
|
2912
|
-
source: { type: "base64", media_type: b.mimeType, data: b.data }
|
|
2913
|
-
});
|
|
2914
|
-
} else if (b.type === "audio") {
|
|
2915
|
-
out.push({ type: "text", text: `[audio: ${b.mimeType}]` });
|
|
2916
|
-
} else if (b.type === "resource") {
|
|
2917
|
-
const text = "text" in b.resource && typeof b.resource.text === "string" ? b.resource.text : `[embedded resource: ${b.resource.uri}]`;
|
|
2918
|
-
out.push({ type: "text", text });
|
|
2919
|
-
} else if (b.type === "resource_link") {
|
|
2920
|
-
out.push({ type: "text", text: `[resource link: ${b.uri}]` });
|
|
2921
|
-
}
|
|
2922
|
-
}
|
|
2923
|
-
return out;
|
|
2924
|
-
}
|
|
2925
|
-
function disposeACPServerAgentTurn(opts) {
|
|
2926
|
-
return Promise.allSettled(
|
|
2927
|
-
Array.from(opts.agents.values()).map((agent) => agent.teardown())
|
|
2928
|
-
).then(() => void 0);
|
|
2929
|
-
}
|
|
2930
|
-
function promptToText(blocks) {
|
|
2931
|
-
const parts = [];
|
|
2932
|
-
for (const b of blocks) {
|
|
2933
|
-
if (b.type === "text") {
|
|
2934
|
-
parts.push(b.text);
|
|
2935
|
-
} else if (b.type === "image") {
|
|
2936
|
-
parts.push(`[image: ${b.mimeType}]`);
|
|
2937
|
-
} else if (b.type === "audio") {
|
|
2938
|
-
parts.push(`[audio: ${b.mimeType}]`);
|
|
2939
|
-
} else if (b.type === "resource") {
|
|
2940
|
-
parts.push(`[embedded resource: ${b.resource.uri}]`);
|
|
2941
|
-
} else if (b.type === "resource_link") {
|
|
2942
|
-
parts.push(`[resource link: ${b.uri}]`);
|
|
2943
|
-
}
|
|
2944
|
-
}
|
|
2945
|
-
return parts.join("\n").trim();
|
|
2946
|
-
}
|
|
2947
|
-
function extractText2(result) {
|
|
2948
|
-
if (typeof result !== "object" || result === null) return "";
|
|
2949
|
-
const r = result;
|
|
2950
|
-
if (typeof r.text === "string") return r.text;
|
|
2951
|
-
if (Array.isArray(r.content)) {
|
|
2952
|
-
const parts = [];
|
|
2953
|
-
for (const c of r.content) {
|
|
2954
|
-
if (typeof c === "object" && c !== null) {
|
|
2955
|
-
const cb = c;
|
|
2956
|
-
if (cb.type === "text" && typeof cb.text === "string") parts.push(cb.text);
|
|
2957
|
-
}
|
|
2958
|
-
}
|
|
2959
|
-
return parts.join("");
|
|
2960
|
-
}
|
|
2961
|
-
return "";
|
|
2962
|
-
}
|
|
2963
|
-
function pickStopReason(result, signal) {
|
|
2964
|
-
if (signal.aborted) return "cancelled";
|
|
2965
|
-
if (typeof result !== "object" || result === null) return "end_turn";
|
|
2966
|
-
const r = result;
|
|
2967
|
-
if (r.error) {
|
|
2968
|
-
return "end_turn";
|
|
2969
|
-
}
|
|
2970
|
-
if (typeof r.stopReason === "string" && r.stopReason) {
|
|
2971
|
-
return r.stopReason;
|
|
2972
|
-
}
|
|
2973
|
-
return "end_turn";
|
|
2974
|
-
}
|
|
2975
|
-
function extractPlan(result) {
|
|
2976
|
-
if (typeof result !== "object" || result === null) return [];
|
|
2977
|
-
const r = result;
|
|
2978
|
-
if (Array.isArray(r.plan)) {
|
|
2979
|
-
return r.plan.filter(
|
|
2980
|
-
(e) => typeof e === "object" && e !== null && typeof e.content === "string"
|
|
2981
|
-
);
|
|
2982
|
-
}
|
|
2983
|
-
return [];
|
|
2984
|
-
}
|
|
2985
|
-
function extractUsage(result) {
|
|
2986
|
-
if (typeof result !== "object" || result === null) return null;
|
|
2987
|
-
const r = result;
|
|
2988
|
-
if (typeof r.usage === "object" && r.usage !== null) {
|
|
2989
|
-
const u = r.usage;
|
|
2990
|
-
if (typeof u.used === "number" && typeof u.size === "number") {
|
|
2991
|
-
return {
|
|
2992
|
-
used: u.used,
|
|
2993
|
-
size: u.size,
|
|
2994
|
-
...typeof u.cost === "object" && u.cost !== null ? { cost: u.cost } : {}
|
|
2995
|
-
};
|
|
2996
|
-
}
|
|
2997
|
-
}
|
|
2998
|
-
return null;
|
|
2999
|
-
}
|
|
3000
|
-
|
|
3001
|
-
// src/agent/session-store.ts
|
|
3002
|
-
import * as fsp2 from "node:fs/promises";
|
|
3003
|
-
import * as path3 from "node:path";
|
|
3004
|
-
var ACPSessionStore = class {
|
|
3005
|
-
dir;
|
|
3006
|
-
/**
|
|
3007
|
-
* Memoized result of the first successful `init()`. Saved sessions
|
|
3008
|
-
* are the hot path — calling `mkdir(..., {recursive:true})` on every
|
|
3009
|
-
* turn adds an avoidable syscall to the per-prompt persistence flow.
|
|
3010
|
-
* Cleared automatically if the directory disappears between calls.
|
|
3011
|
-
*/
|
|
3012
|
-
initialized = false;
|
|
3013
|
-
constructor(opts = {}) {
|
|
3014
|
-
this.dir = opts.dir ?? path3.join(process.cwd(), ".acp-sessions");
|
|
3015
|
-
}
|
|
3016
|
-
/** Ensure the store directory exists. Memoized — only mkdirs once. */
|
|
3017
|
-
async init() {
|
|
3018
|
-
if (this.initialized) return;
|
|
3019
|
-
await fsp2.mkdir(this.dir, { recursive: true });
|
|
3020
|
-
this.initialized = true;
|
|
3021
|
-
}
|
|
3022
|
-
/**
|
|
3023
|
-
* Persist a session state (and optionally its conversation history) to
|
|
3024
|
-
* disk. Returns the session id. `history` enables cross-restart
|
|
3025
|
-
* `session/load` replay.
|
|
3026
|
-
*/
|
|
3027
|
-
async save(state, history) {
|
|
3028
|
-
await this.init();
|
|
3029
|
-
await fsp2.writeFile(
|
|
3030
|
-
path3.join(this.dir, `${state.id}.json`),
|
|
3031
|
-
JSON.stringify({
|
|
3032
|
-
id: state.id,
|
|
3033
|
-
cwd: state.cwd,
|
|
3034
|
-
modeId: state.modeId,
|
|
3035
|
-
createdAt: state.createdAt,
|
|
3036
|
-
updatedAt: state.updatedAt,
|
|
3037
|
-
title: state.title,
|
|
3038
|
-
...history && history.length > 0 ? { history } : {}
|
|
3039
|
-
}),
|
|
3040
|
-
"utf8"
|
|
3041
|
-
);
|
|
3042
|
-
await this.updateIndex(state.id, state.updatedAt);
|
|
3043
|
-
return state.id;
|
|
3044
|
-
}
|
|
3045
|
-
/** Load a persisted session (metadata + history) from disk, or null. */
|
|
3046
|
-
async load(sessionId) {
|
|
3047
|
-
try {
|
|
3048
|
-
const data = await fsp2.readFile(path3.join(this.dir, `${sessionId}.json`), "utf8");
|
|
3049
|
-
return JSON.parse(data);
|
|
3050
|
-
} catch {
|
|
3051
|
-
return null;
|
|
3052
|
-
}
|
|
3053
|
-
}
|
|
3054
|
-
/** List all persisted sessions. */
|
|
3055
|
-
async list() {
|
|
3056
|
-
const indexEntries = await this.readIndex();
|
|
3057
|
-
if (indexEntries !== null) {
|
|
3058
|
-
return indexEntries.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
3059
|
-
}
|
|
3060
|
-
const files = [];
|
|
3061
|
-
try {
|
|
3062
|
-
const entries = await fsp2.readdir(this.dir);
|
|
3063
|
-
for (const entry of entries) {
|
|
3064
|
-
if (entry.endsWith(".json") && entry !== "index.json") {
|
|
3065
|
-
files.push(entry);
|
|
3066
|
-
}
|
|
3067
|
-
}
|
|
3068
|
-
} catch {
|
|
3069
|
-
return [];
|
|
3070
|
-
}
|
|
3071
|
-
const sessions = [];
|
|
3072
|
-
for (const file of files) {
|
|
3073
|
-
try {
|
|
3074
|
-
const data = await fsp2.readFile(path3.join(this.dir, file), "utf8");
|
|
3075
|
-
const parsed = JSON.parse(data);
|
|
3076
|
-
if (parsed.id) {
|
|
3077
|
-
sessions.push({ id: parsed.id, updatedAt: parsed.updatedAt ?? "" });
|
|
3078
|
-
}
|
|
3079
|
-
} catch {
|
|
3080
|
-
}
|
|
3081
|
-
}
|
|
3082
|
-
sessions.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
3083
|
-
void this.writeIndex(sessions).catch(() => void 0);
|
|
3084
|
-
return sessions;
|
|
3085
|
-
}
|
|
3086
|
-
/** Sidecar path that stores `{id, updatedAt}` for every saved session. */
|
|
3087
|
-
indexPath() {
|
|
3088
|
-
return path3.join(this.dir, "index.json");
|
|
3089
|
-
}
|
|
3090
|
-
/** Read the sidecar index. Returns `null` when missing or unreadable. */
|
|
3091
|
-
async readIndex() {
|
|
3092
|
-
try {
|
|
3093
|
-
const data = await fsp2.readFile(this.indexPath(), "utf8");
|
|
3094
|
-
const parsed = JSON.parse(data);
|
|
3095
|
-
if (!Array.isArray(parsed)) return null;
|
|
3096
|
-
const out = [];
|
|
3097
|
-
for (const e of parsed) {
|
|
3098
|
-
if (e && typeof e.id === "string" && typeof e.updatedAt === "string") {
|
|
3099
|
-
out.push({
|
|
3100
|
-
id: e.id,
|
|
3101
|
-
updatedAt: e.updatedAt
|
|
3102
|
-
});
|
|
3103
|
-
}
|
|
3104
|
-
}
|
|
3105
|
-
return out;
|
|
3106
|
-
} catch {
|
|
3107
|
-
return null;
|
|
3108
|
-
}
|
|
3109
|
-
}
|
|
3110
|
-
/** Atomically replace the sidecar index with the supplied entries. */
|
|
3111
|
-
async writeIndex(entries) {
|
|
3112
|
-
const target = this.indexPath();
|
|
3113
|
-
const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
3114
|
-
await fsp2.writeFile(tmp, JSON.stringify(entries), "utf8");
|
|
3115
|
-
await fsp2.rename(tmp, target);
|
|
3116
|
-
}
|
|
3117
|
-
/** Update one entry in the index, adding it if missing. Best-effort. */
|
|
3118
|
-
async updateIndex(id, updatedAt) {
|
|
3119
|
-
const entries = await this.readIndex();
|
|
3120
|
-
if (entries === null) {
|
|
3121
|
-
await this.list();
|
|
3122
|
-
return;
|
|
3123
|
-
}
|
|
3124
|
-
const i = entries.findIndex((e) => e.id === id);
|
|
3125
|
-
if (i >= 0) entries[i] = { id, updatedAt };
|
|
3126
|
-
else entries.push({ id, updatedAt });
|
|
3127
|
-
try {
|
|
3128
|
-
await this.writeIndex(entries);
|
|
3129
|
-
} catch {
|
|
3130
|
-
}
|
|
3131
|
-
}
|
|
3132
|
-
/** Delete a session file. */
|
|
3133
|
-
async delete(sessionId) {
|
|
3134
|
-
try {
|
|
3135
|
-
await fsp2.unlink(path3.join(this.dir, `${sessionId}.json`));
|
|
3136
|
-
} catch {
|
|
3137
|
-
}
|
|
3138
|
-
const entries = await this.readIndex();
|
|
3139
|
-
if (entries === null) return;
|
|
3140
|
-
const next = entries.filter((e) => e.id !== sessionId);
|
|
3141
|
-
if (next.length !== entries.length) {
|
|
3142
|
-
try {
|
|
3143
|
-
await this.writeIndex(next);
|
|
3144
|
-
} catch {
|
|
3145
|
-
}
|
|
3146
|
-
}
|
|
3147
|
-
}
|
|
3148
|
-
/** Get the store directory path. */
|
|
3149
|
-
getDirectory() {
|
|
3150
|
-
return this.dir;
|
|
3151
|
-
}
|
|
3152
|
-
};
|
|
3153
|
-
|
|
3154
|
-
// src/registry/agents.catalog.ts
|
|
3155
|
-
var AGENTS_CATALOG = [
|
|
3156
|
-
// ── Anthropic ────────────────────────────────────────────────────────
|
|
3157
|
-
{
|
|
3158
|
-
id: "claude-code",
|
|
3159
|
-
displayName: "Claude Code",
|
|
3160
|
-
vendor: "anthropic",
|
|
3161
|
-
probe: { command: "claude", args: ["--version"] },
|
|
3162
|
-
// Claude Code does not speak stdio ACP from the bare `claude` binary —
|
|
3163
|
-
// it drops into its interactive TUI. The official ACP adapter
|
|
3164
|
-
// (`@agentclientprotocol/claude-agent-acp`, registry id `claude-acp`)
|
|
3165
|
-
// wraps the logged-in Claude Code CLI and translates ACP ↔ Claude Code.
|
|
3166
|
-
// Verify with `/acp probe claude-code`; override via `config.acp.agents`.
|
|
3167
|
-
acp: { command: "npx", args: ["-y", "@agentclientprotocol/claude-agent-acp"] },
|
|
3168
|
-
supports: {
|
|
3169
|
-
loadSession: true,
|
|
3170
|
-
promptImages: true,
|
|
3171
|
-
terminal: true,
|
|
3172
|
-
fs: true
|
|
3173
|
-
},
|
|
3174
|
-
integration: "adapter",
|
|
3175
|
-
docs: "https://docs.anthropic.com/en/docs/claude-code"
|
|
3176
|
-
},
|
|
3177
|
-
// ── Google ───────────────────────────────────────────────────────────
|
|
3178
|
-
{
|
|
3179
|
-
id: "gemini-cli",
|
|
3180
|
-
displayName: "Gemini CLI",
|
|
3181
|
-
vendor: "google",
|
|
3182
|
-
probe: { command: "gemini", args: ["--version"] },
|
|
3183
|
-
// Gemini CLI (the @google/gemini-cli package, registry id `gemini`)
|
|
3184
|
-
// speaks ACP behind `--acp`. We invoke the locally-installed binary so it
|
|
3185
|
-
// uses the user's existing login. Confirm with `/acp probe gemini-cli`.
|
|
3186
|
-
acp: { command: "gemini", args: ["--acp"] },
|
|
3187
|
-
supports: {
|
|
3188
|
-
loadSession: true,
|
|
3189
|
-
promptImages: true,
|
|
3190
|
-
terminal: true,
|
|
3191
|
-
fs: true
|
|
3192
|
-
},
|
|
3193
|
-
integration: "native",
|
|
3194
|
-
docs: "https://github.com/google-gemini/gemini-cli"
|
|
3195
|
-
},
|
|
3196
|
-
// ── OpenAI ───────────────────────────────────────────────────────────
|
|
3197
|
-
{
|
|
3198
|
-
id: "codex-cli",
|
|
3199
|
-
displayName: "Codex CLI",
|
|
3200
|
-
vendor: "openai",
|
|
3201
|
-
probe: { command: "codex", args: ["--version"] },
|
|
3202
|
-
// Bare `codex` has no stdio-ACP entry; the official adapter
|
|
3203
|
-
// (`@agentclientprotocol/codex-acp`, registry id `codex-acp`) wraps the
|
|
3204
|
-
// logged-in Codex CLI. Confirm with `/acp probe codex-cli`.
|
|
3205
|
-
acp: { command: "npx", args: ["-y", "@agentclientprotocol/codex-acp"] },
|
|
3206
|
-
supports: {
|
|
3207
|
-
loadSession: false,
|
|
3208
|
-
promptImages: false,
|
|
3209
|
-
terminal: true,
|
|
3210
|
-
fs: true
|
|
3211
|
-
},
|
|
3212
|
-
integration: "adapter",
|
|
3213
|
-
docs: "https://github.com/openai/codex"
|
|
3214
|
-
},
|
|
3215
|
-
// ── GitHub ───────────────────────────────────────────────────────────
|
|
3216
|
-
{
|
|
3217
|
-
id: "copilot",
|
|
3218
|
-
displayName: "GitHub Copilot CLI",
|
|
3219
|
-
vendor: "github",
|
|
3220
|
-
probe: { command: "gh", args: ["copilot", "--help"] },
|
|
3221
|
-
// ACP is in the standalone @github/copilot CLI (registry id
|
|
3222
|
-
// `github-copilot-cli`), not the `gh copilot` extension. Use the package.
|
|
3223
|
-
acp: { command: "npx", args: ["-y", "@github/copilot", "--acp"] },
|
|
3224
|
-
supports: {
|
|
3225
|
-
loadSession: false,
|
|
3226
|
-
promptImages: false,
|
|
3227
|
-
terminal: true,
|
|
3228
|
-
fs: false
|
|
3229
|
-
},
|
|
3230
|
-
integration: "experimental",
|
|
3231
|
-
docs: "https://github.com/features/copilot/cli"
|
|
3232
|
-
},
|
|
3233
|
-
// ── Community / wrappers ─────────────────────────────────────────────
|
|
3234
|
-
{
|
|
3235
|
-
id: "cline",
|
|
3236
|
-
displayName: "Cline",
|
|
3237
|
-
vendor: "community",
|
|
3238
|
-
probe: { command: "npx", args: ["--version"] },
|
|
3239
|
-
// Registry id `cline`: the `cline` npm package speaks ACP behind `--acp`.
|
|
3240
|
-
acp: {
|
|
3241
|
-
command: "npx",
|
|
3242
|
-
args: ["-y", "cline", "--acp"]
|
|
3243
|
-
},
|
|
3244
|
-
supports: {
|
|
3245
|
-
loadSession: true,
|
|
3246
|
-
promptImages: true,
|
|
3247
|
-
terminal: true,
|
|
3248
|
-
fs: true
|
|
3249
|
-
},
|
|
3250
|
-
integration: "community",
|
|
3251
|
-
docs: "https://github.com/cline/cline"
|
|
3252
|
-
},
|
|
3253
|
-
{
|
|
3254
|
-
id: "goose",
|
|
3255
|
-
displayName: "Goose",
|
|
3256
|
-
vendor: "community",
|
|
3257
|
-
probe: { command: "goose", args: ["--version"] },
|
|
3258
|
-
acp: { command: "goose", args: ["acp"] },
|
|
3259
|
-
supports: {
|
|
3260
|
-
loadSession: true,
|
|
3261
|
-
promptImages: true,
|
|
3262
|
-
terminal: true,
|
|
3263
|
-
fs: true
|
|
3264
|
-
},
|
|
3265
|
-
integration: "experimental",
|
|
3266
|
-
docs: "https://github.com/block/goose"
|
|
3267
|
-
},
|
|
3268
|
-
{
|
|
3269
|
-
id: "openhands",
|
|
3270
|
-
displayName: "OpenHands",
|
|
3271
|
-
vendor: "community",
|
|
3272
|
-
probe: { command: "openhands", args: ["--version"] },
|
|
3273
|
-
acp: { command: "openhands", args: [] },
|
|
3274
|
-
supports: {
|
|
3275
|
-
loadSession: false,
|
|
3276
|
-
promptImages: true,
|
|
3277
|
-
terminal: true,
|
|
3278
|
-
fs: true
|
|
3279
|
-
},
|
|
3280
|
-
integration: "experimental",
|
|
3281
|
-
// Canonical repo URL — the org renamed; All-Hands-AI/OpenHands 301-redirects here.
|
|
3282
|
-
docs: "https://github.com/OpenHands/OpenHands"
|
|
3283
|
-
},
|
|
3284
|
-
// ── Vendor CLIs (native binaries) ───────────────────────────────────
|
|
3285
|
-
{
|
|
3286
|
-
id: "qwen-code",
|
|
3287
|
-
displayName: "Qwen Code",
|
|
3288
|
-
vendor: "community",
|
|
3289
|
-
probe: { command: "qwen", args: ["--version"] },
|
|
3290
|
-
// Qwen Code (the @qwen-code/qwen-code package) speaks ACP behind `--acp`.
|
|
3291
|
-
acp: { command: "qwen", args: ["--acp"] },
|
|
3292
|
-
supports: {
|
|
3293
|
-
loadSession: false,
|
|
3294
|
-
promptImages: false,
|
|
3295
|
-
terminal: true,
|
|
3296
|
-
fs: false
|
|
3297
|
-
},
|
|
3298
|
-
integration: "experimental",
|
|
3299
|
-
docs: "https://github.com/QwenLM/Qwen3-Coder"
|
|
3300
|
-
},
|
|
3301
|
-
{
|
|
3302
|
-
id: "kiro-cli",
|
|
3303
|
-
displayName: "Kiro CLI",
|
|
3304
|
-
vendor: "community",
|
|
3305
|
-
probe: { command: "kiro", args: ["--version"] },
|
|
3306
|
-
acp: { command: "kiro", args: [] },
|
|
3307
|
-
supports: {
|
|
3308
|
-
loadSession: false,
|
|
3309
|
-
promptImages: false,
|
|
3310
|
-
terminal: true,
|
|
3311
|
-
fs: true
|
|
3312
|
-
},
|
|
3313
|
-
integration: "experimental",
|
|
3314
|
-
docs: "https://kiro.dev"
|
|
3315
|
-
},
|
|
3316
|
-
{
|
|
3317
|
-
id: "opencode",
|
|
3318
|
-
displayName: "OpenCode",
|
|
3319
|
-
vendor: "community",
|
|
3320
|
-
probe: { command: "opencode", args: ["--version"] },
|
|
3321
|
-
// OpenCode speaks ACP via its `acp` subcommand (registry id `opencode`).
|
|
3322
|
-
acp: { command: "opencode", args: ["acp"] },
|
|
3323
|
-
supports: {
|
|
3324
|
-
loadSession: true,
|
|
3325
|
-
promptImages: true,
|
|
3326
|
-
terminal: true,
|
|
3327
|
-
fs: true
|
|
3328
|
-
},
|
|
3329
|
-
integration: "native",
|
|
3330
|
-
docs: "https://github.com/sst/opencode"
|
|
3331
|
-
},
|
|
3332
|
-
{
|
|
3333
|
-
id: "mistral-vibe",
|
|
3334
|
-
displayName: "Mistral Vibe",
|
|
3335
|
-
vendor: "community",
|
|
3336
|
-
probe: { command: "vibe", args: ["--version"] },
|
|
3337
|
-
acp: { command: "vibe", args: [] },
|
|
3338
|
-
supports: {
|
|
3339
|
-
loadSession: false,
|
|
3340
|
-
promptImages: false,
|
|
3341
|
-
terminal: true,
|
|
3342
|
-
fs: false
|
|
3343
|
-
},
|
|
3344
|
-
integration: "experimental",
|
|
3345
|
-
docs: "https://github.com/mistralai/mistral-vibe"
|
|
3346
|
-
},
|
|
3347
|
-
{
|
|
3348
|
-
id: "cursor",
|
|
3349
|
-
displayName: "Cursor",
|
|
3350
|
-
vendor: "community",
|
|
3351
|
-
probe: { command: "cursor", args: ["--version"] },
|
|
3352
|
-
// Cursor's ACP entry is the `cursor-agent acp` binary (registry id `cursor`).
|
|
3353
|
-
acp: { command: "cursor-agent", args: ["acp"] },
|
|
3354
|
-
supports: {
|
|
3355
|
-
loadSession: true,
|
|
3356
|
-
promptImages: true,
|
|
3357
|
-
terminal: true,
|
|
3358
|
-
fs: true
|
|
3359
|
-
},
|
|
3360
|
-
integration: "experimental",
|
|
3361
|
-
docs: "https://cursor.com"
|
|
3362
|
-
},
|
|
3363
|
-
// ── Moonshot AI (Kimi) ─────────────────────────────────────────────
|
|
3364
|
-
{
|
|
3365
|
-
id: "kimi",
|
|
3366
|
-
displayName: "Kimi Code CLI",
|
|
3367
|
-
vendor: "moonshot",
|
|
3368
|
-
probe: { command: "kimi", args: ["--version"] },
|
|
3369
|
-
// Kimi Code CLI speaks ACP behind `kimi acp`. The user must complete
|
|
3370
|
-
// terminal login (`kimi` → `/login`) before launching `kimi acp`;
|
|
3371
|
-
// otherwise session creation fails with `Authentication required`.
|
|
3372
|
-
// The adapter reuses the CLI's existing auth state — WrongStack does
|
|
3373
|
-
// NOT capture or replay the Kimi OAuth tokens.
|
|
3374
|
-
// Docs: https://www.kimi.com/code/docs/en/kimi-code-cli/reference/kimi-acp.html
|
|
3375
|
-
acp: { command: "kimi", args: ["acp"] },
|
|
3376
|
-
supports: {
|
|
3377
|
-
loadSession: true,
|
|
3378
|
-
promptImages: true,
|
|
3379
|
-
terminal: true,
|
|
3380
|
-
fs: true
|
|
3381
|
-
},
|
|
3382
|
-
integration: "native",
|
|
3383
|
-
docs: "https://www.kimi.com/code/docs/en/kimi-code-cli/guides/ides.html"
|
|
3384
|
-
}
|
|
3385
|
-
];
|
|
3386
|
-
function findAgentDescriptor(id) {
|
|
3387
|
-
return AGENTS_CATALOG.find((a) => a.id === id);
|
|
3388
|
-
}
|
|
3389
|
-
|
|
3390
|
-
// src/integration/run-one-acp-task.ts
|
|
3391
|
-
import { SubagentBudget } from "@wrongstack/core/coordination";
|
|
3392
|
-
async function runOneAcpTask(opts) {
|
|
3393
|
-
const role = opts.role ?? "acp";
|
|
3394
|
-
const timeoutMs = opts.timeoutMs ?? 5 * 6e4;
|
|
3395
|
-
const { runner, stop } = await makeACPSubagentRunnerWithStop({
|
|
3396
|
-
command: opts.command,
|
|
3397
|
-
...opts.args !== void 0 ? { args: opts.args } : {},
|
|
3398
|
-
...opts.env !== void 0 ? { env: opts.env } : {},
|
|
3399
|
-
...opts.cwd !== void 0 ? { cwd: opts.cwd } : {},
|
|
3400
|
-
...opts.projectRoot !== void 0 ? { projectRoot: opts.projectRoot } : {},
|
|
3401
|
-
role,
|
|
3402
|
-
timeoutMs,
|
|
3403
|
-
...opts.onProgress !== void 0 ? { onProgress: opts.onProgress } : {},
|
|
3404
|
-
...opts.permissionPolicy !== void 0 ? { permissionPolicy: opts.permissionPolicy } : {}
|
|
3405
|
-
});
|
|
3406
|
-
try {
|
|
3407
|
-
const budget = new SubagentBudget({
|
|
3408
|
-
timeoutMs,
|
|
3409
|
-
maxIterations: 2e3,
|
|
3410
|
-
maxToolCalls: 5e3
|
|
3411
|
-
});
|
|
3412
|
-
budget.start();
|
|
3413
|
-
const ctx = {
|
|
3414
|
-
subagentId: role,
|
|
3415
|
-
config: { id: role, name: role, role, provider: "acp", prompt: "" },
|
|
3416
|
-
budget,
|
|
3417
|
-
signal: opts.signal ?? new AbortController().signal,
|
|
3418
|
-
bridge: null
|
|
3419
|
-
};
|
|
3420
|
-
const result = await runner({ id: `acp-${role}`, description: opts.task }, ctx);
|
|
3421
|
-
return {
|
|
3422
|
-
result: result.result == null ? "" : String(result.result),
|
|
3423
|
-
iterations: result.iterations,
|
|
3424
|
-
toolCalls: result.toolCalls
|
|
3425
|
-
};
|
|
3426
|
-
} finally {
|
|
3427
|
-
try {
|
|
3428
|
-
await stop();
|
|
3429
|
-
} catch {
|
|
3430
|
-
}
|
|
3431
|
-
}
|
|
3432
|
-
}
|
|
3433
|
-
|
|
3434
|
-
// src/integration/acp-subagent-runner.ts
|
|
3435
|
-
var ACP_AGENT_COMMANDS = {
|
|
3436
|
-
cline: {
|
|
3437
|
-
command: "npx",
|
|
3438
|
-
args: ["-y", "@agentify/cline"],
|
|
3439
|
-
role: "cline"
|
|
3440
|
-
},
|
|
3441
|
-
"gemini-cli": {
|
|
3442
|
-
command: "gemini",
|
|
3443
|
-
role: "gemini-cli"
|
|
3444
|
-
},
|
|
3445
|
-
copilot: {
|
|
3446
|
-
command: "gh",
|
|
3447
|
-
args: ["copilot", "agent"],
|
|
3448
|
-
role: "copilot"
|
|
3449
|
-
},
|
|
3450
|
-
openhands: {
|
|
3451
|
-
command: "openhands",
|
|
3452
|
-
role: "openhands"
|
|
3453
|
-
},
|
|
3454
|
-
goose: {
|
|
3455
|
-
command: "goose",
|
|
3456
|
-
role: "goose"
|
|
3457
|
-
}
|
|
3458
|
-
};
|
|
3459
|
-
async function makeACPSubagentRunner(options) {
|
|
3460
|
-
const { runner, stop } = await makeACPSubagentRunnerWithStop(options);
|
|
3461
|
-
const wrappedRunner = async (task, ctx) => {
|
|
3462
|
-
try {
|
|
3463
|
-
return await runner(task, ctx);
|
|
3464
|
-
} finally {
|
|
3465
|
-
stop();
|
|
3466
|
-
}
|
|
3467
|
-
};
|
|
3468
|
-
return wrappedRunner;
|
|
3469
|
-
}
|
|
3470
|
-
async function makeACPSubagentRunnerWithStop(options) {
|
|
3471
|
-
const projectRoot = options.projectRoot ?? options.cwd ?? process.cwd();
|
|
3472
|
-
const timeoutMs = options.timeoutMs ?? 5 * 6e4;
|
|
3473
|
-
const persistent = options.persistent === true;
|
|
3474
|
-
let shared = null;
|
|
3475
|
-
const startSession = async () => {
|
|
3476
|
-
return ACPSession.start({
|
|
3477
|
-
command: options.command,
|
|
3478
|
-
...options.args !== void 0 ? { args: options.args } : {},
|
|
3479
|
-
...options.env !== void 0 ? { env: options.env } : {},
|
|
3480
|
-
...options.cwd !== void 0 ? { cwd: options.cwd } : {},
|
|
3481
|
-
projectRoot,
|
|
3482
|
-
timeoutMs,
|
|
3483
|
-
role: options.role,
|
|
3484
|
-
...options.permissionPolicy !== void 0 ? { permissionPolicy: options.permissionPolicy } : {},
|
|
3485
|
-
...options.mcpServers !== void 0 ? { mcpServers: options.mcpServers } : {}
|
|
3486
|
-
});
|
|
3487
|
-
};
|
|
3488
|
-
const runner = async (task, ctx) => {
|
|
3489
|
-
let session;
|
|
3490
|
-
const reuse = persistent && shared !== null;
|
|
3491
|
-
try {
|
|
3492
|
-
session = reuse ? shared : await startSession();
|
|
3493
|
-
if (persistent) shared = session;
|
|
3494
|
-
} catch (err) {
|
|
3495
|
-
throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
|
|
3496
|
-
}
|
|
3497
|
-
const onProgress = (event) => {
|
|
3498
|
-
try {
|
|
3499
|
-
ctx.budget.markActivity();
|
|
3500
|
-
} catch {
|
|
3501
|
-
}
|
|
3502
|
-
options.onProgress?.(event);
|
|
3503
|
-
};
|
|
3504
|
-
try {
|
|
3505
|
-
const result = await session.prompt(
|
|
3506
|
-
[textContent(task.description)],
|
|
3507
|
-
ctx.signal,
|
|
3508
|
-
onProgress
|
|
3509
|
-
);
|
|
3510
|
-
return {
|
|
3511
|
-
result: result.text,
|
|
3512
|
-
iterations: 1,
|
|
3513
|
-
toolCalls: result.toolCalls.length
|
|
3514
|
-
};
|
|
3515
|
-
} catch (err) {
|
|
3516
|
-
throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
|
|
3517
|
-
} finally {
|
|
3518
|
-
if (!persistent) {
|
|
3519
|
-
try {
|
|
3520
|
-
await session.close();
|
|
3521
|
-
} catch {
|
|
3522
|
-
}
|
|
3523
|
-
}
|
|
3524
|
-
}
|
|
3525
|
-
};
|
|
3526
|
-
const stop = async () => {
|
|
3527
|
-
if (shared) {
|
|
3528
|
-
const s = shared;
|
|
3529
|
-
shared = null;
|
|
3530
|
-
try {
|
|
3531
|
-
await s.close();
|
|
3532
|
-
} catch {
|
|
3533
|
-
}
|
|
3534
|
-
}
|
|
3535
|
-
};
|
|
3536
|
-
return { runner, stop };
|
|
3537
|
-
}
|
|
3538
|
-
function acpErrorToSubagentError(err, subagentId) {
|
|
3539
|
-
if (err instanceof ACPSessionError) {
|
|
3540
|
-
const kind = mapACPKind(err.kind);
|
|
3541
|
-
return {
|
|
3542
|
-
kind,
|
|
3543
|
-
message: `${subagentId}: ${err.message}`,
|
|
3544
|
-
retryable: isRetryable(kind),
|
|
3545
|
-
cause: {
|
|
3546
|
-
name: err.name,
|
|
3547
|
-
message: err.message,
|
|
3548
|
-
...err.stack !== void 0 ? { stack: err.stack } : {}
|
|
3549
|
-
}
|
|
3550
|
-
};
|
|
3551
|
-
}
|
|
3552
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
3553
|
-
return {
|
|
3554
|
-
kind: "bridge_failed",
|
|
3555
|
-
message: `${subagentId}: ${message}`,
|
|
3556
|
-
retryable: false,
|
|
3557
|
-
cause: {
|
|
3558
|
-
name: err instanceof Error ? err.name : "Error",
|
|
3559
|
-
message,
|
|
3560
|
-
...err instanceof Error && err.stack !== void 0 ? { stack: err.stack } : {}
|
|
3561
|
-
}
|
|
3562
|
-
};
|
|
3563
|
-
}
|
|
3564
|
-
function mapACPKind(acpKind) {
|
|
3565
|
-
switch (acpKind) {
|
|
3566
|
-
case "spawn_failed":
|
|
3567
|
-
case "init_failed":
|
|
3568
|
-
case "session_create_failed":
|
|
3569
|
-
case "agent_died":
|
|
3570
|
-
case "protocol_error":
|
|
3571
|
-
return "bridge_failed";
|
|
3572
|
-
case "prompt_failed":
|
|
3573
|
-
return "tool_failed";
|
|
3574
|
-
case "auth_failed":
|
|
3575
|
-
case "logout_failed":
|
|
3576
|
-
return "bridge_failed";
|
|
3577
|
-
case "aborted":
|
|
3578
|
-
return "aborted_by_parent";
|
|
3579
|
-
case "closed":
|
|
3580
|
-
case "unsupported_capability":
|
|
3581
|
-
return "unknown";
|
|
3582
|
-
}
|
|
3583
|
-
}
|
|
3584
|
-
function isRetryable(kind) {
|
|
3585
|
-
switch (kind) {
|
|
3586
|
-
case "provider_5xx":
|
|
3587
|
-
case "provider_rate_limit":
|
|
3588
|
-
case "provider_timeout":
|
|
3589
|
-
case "tool_threw":
|
|
3590
|
-
case "budget_timeout":
|
|
3591
|
-
return true;
|
|
3592
|
-
default:
|
|
3593
|
-
return false;
|
|
3594
|
-
}
|
|
3595
|
-
}
|
|
3596
|
-
var REGISTRY_ID_ALIASES = {
|
|
3597
|
-
"claude-code": "claude-acp",
|
|
3598
|
-
"gemini-cli": "gemini",
|
|
3599
|
-
"codex-cli": "codex-acp",
|
|
3600
|
-
copilot: "github-copilot-cli",
|
|
3601
|
-
// Kimi's live registry id is `kimi` — same as our catalog id, so the
|
|
3602
|
-
// alias is identity. Listed explicitly so `resolveAcpAgentCommand`
|
|
3603
|
-
// finds the live entry when the registry is synced.
|
|
3604
|
-
kimi: "kimi"
|
|
3605
|
-
};
|
|
3606
|
-
function resolveAcpAgentCommand(id, overrides, live) {
|
|
3607
|
-
const ov = overrides?.[id];
|
|
3608
|
-
if (ov && typeof ov.command === "string" && ov.command.length > 0) {
|
|
3609
|
-
const out = {
|
|
3610
|
-
command: ov.command,
|
|
3611
|
-
args: [...ov.args ?? []],
|
|
3612
|
-
role: id
|
|
3613
|
-
};
|
|
3614
|
-
if (ov.env) out.env = ov.env;
|
|
3615
|
-
return out;
|
|
3616
|
-
}
|
|
3617
|
-
const desc = findAgentDescriptor(id);
|
|
3618
|
-
if (desc) {
|
|
3619
|
-
const out = {
|
|
3620
|
-
command: desc.acp.command,
|
|
3621
|
-
args: [...desc.acp.args ?? []],
|
|
3622
|
-
role: id
|
|
3623
|
-
};
|
|
3624
|
-
if (desc.acp.env) out.env = desc.acp.env;
|
|
3625
|
-
return out;
|
|
3626
|
-
}
|
|
3627
|
-
const liveEntry = live?.[id] ?? live?.[REGISTRY_ID_ALIASES[id] ?? ""];
|
|
3628
|
-
if (liveEntry && typeof liveEntry.command === "string" && liveEntry.command.length > 0) {
|
|
3629
|
-
const out = {
|
|
3630
|
-
command: liveEntry.command,
|
|
3631
|
-
args: [...liveEntry.args ?? []],
|
|
3632
|
-
role: id
|
|
3633
|
-
};
|
|
3634
|
-
if (liveEntry.env) out.env = liveEntry.env;
|
|
3635
|
-
return out;
|
|
3636
|
-
}
|
|
3637
|
-
const fromMap = ACP_AGENT_COMMANDS[id];
|
|
3638
|
-
if (fromMap) return fromMap;
|
|
3639
|
-
return null;
|
|
3640
|
-
}
|
|
3641
|
-
async function probeAcpAgent(idOrCmd, opts) {
|
|
3642
|
-
const id = typeof idOrCmd === "string" ? idOrCmd : idOrCmd.role ?? idOrCmd.command;
|
|
3643
|
-
const cmd = typeof idOrCmd === "string" ? resolveAcpAgentCommand(idOrCmd, opts?.overrides, opts?.live) : idOrCmd;
|
|
3644
|
-
if (!cmd) return { id, ok: false, ms: 0, error: "unknown agent" };
|
|
3645
|
-
const timeoutMs = opts?.timeoutMs ?? 8e3;
|
|
3646
|
-
const startedAt = Date.now();
|
|
3647
|
-
let session = null;
|
|
3648
|
-
try {
|
|
3649
|
-
session = await ACPSession.start({
|
|
3650
|
-
command: cmd.command,
|
|
3651
|
-
...cmd.args !== void 0 ? { args: cmd.args } : {},
|
|
3652
|
-
...cmd.env !== void 0 ? { env: cmd.env } : {},
|
|
3653
|
-
projectRoot: opts?.projectRoot ?? process.cwd(),
|
|
3654
|
-
// Bounds the `initialize` request: a CLI that spawns but never answers
|
|
3655
|
-
// the handshake fails after this instead of blocking.
|
|
3656
|
-
timeoutMs
|
|
3657
|
-
});
|
|
3658
|
-
const info = session.getAgentInfo();
|
|
3659
|
-
return {
|
|
3660
|
-
id,
|
|
3661
|
-
ok: true,
|
|
3662
|
-
ms: Date.now() - startedAt,
|
|
3663
|
-
...info ? { agentInfo: info } : {}
|
|
3664
|
-
};
|
|
3665
|
-
} catch (err) {
|
|
3666
|
-
return {
|
|
3667
|
-
id,
|
|
3668
|
-
ok: false,
|
|
3669
|
-
ms: Date.now() - startedAt,
|
|
3670
|
-
error: err instanceof Error ? err.message : String(err)
|
|
3671
|
-
};
|
|
3672
|
-
} finally {
|
|
3673
|
-
if (session) {
|
|
3674
|
-
try {
|
|
3675
|
-
await session.close();
|
|
3676
|
-
} catch {
|
|
3677
|
-
}
|
|
3678
|
-
}
|
|
3679
|
-
}
|
|
3680
|
-
}
|
|
3681
|
-
export {
|
|
3682
|
-
ACPProtocolHandler,
|
|
3683
|
-
ACPSession,
|
|
3684
|
-
ACPSessionError,
|
|
3685
|
-
ACPSessionStore,
|
|
3686
|
-
ACP_AGENT_COMMANDS,
|
|
3687
|
-
AGENT_METHODS,
|
|
3688
|
-
AcpServer,
|
|
3689
|
-
ActiveSession,
|
|
3690
|
-
AgentApp,
|
|
3691
|
-
CLIENT_METHODS,
|
|
3692
|
-
ClientApp,
|
|
3693
|
-
FileServer,
|
|
3694
|
-
FsError,
|
|
3695
|
-
PROTOCOL_METHODS,
|
|
3696
|
-
PROTOCOL_VERSION,
|
|
3697
|
-
SessionBuilder,
|
|
3698
|
-
TerminalServer,
|
|
3699
|
-
WRONGSTACK_VERSION,
|
|
3700
|
-
WebSocketClientTransport,
|
|
3701
|
-
WrongStackACPServer,
|
|
3702
|
-
audioContent,
|
|
3703
|
-
createNodeHttpHandler,
|
|
3704
|
-
createNodeWebSocketUpgradeHandler,
|
|
3705
|
-
createWebSocketStream,
|
|
3706
|
-
defaultPermissionPolicy,
|
|
3707
|
-
disposeACPServerAgentTurn,
|
|
3708
|
-
imageContent,
|
|
3709
|
-
makeACPServerAgentTurn,
|
|
3710
|
-
makeACPSubagentRunner,
|
|
3711
|
-
makeACPSubagentRunnerWithStop,
|
|
3712
|
-
makePermissionPolicy,
|
|
3713
|
-
methods,
|
|
3714
|
-
probeAcpAgent,
|
|
3715
|
-
readOnlyPermissionPolicy,
|
|
3716
|
-
resolveAcpAgentCommand,
|
|
3717
|
-
runOneAcpTask,
|
|
3718
|
-
textContent
|
|
3719
|
-
};
|
|
3720
1
|
//# sourceMappingURL=sdk.js.map
|