@wrongstack/acp 0.289.0 → 0.291.1

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/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