@wrongstack/acp 0.274.0 → 0.275.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/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { writeErr, expectDefined } from '@wrongstack/core';
2
2
  import { fileURLToPath } from 'url';
3
+ import { createServer } from 'http';
4
+ import { SubagentBudget } from '@wrongstack/core/coordination';
3
5
  import * as fsp from 'fs/promises';
4
6
  import * as path from 'path';
5
7
  import { spawn } from 'child_process';
6
- import { SubagentBudget } from '@wrongstack/core/coordination';
7
8
 
8
9
  // src/agent/stdio-transport.ts
9
10
  var StdioTransport = class {
@@ -110,9 +111,10 @@ var ClientTransport = class {
110
111
  }
111
112
  async start() {
112
113
  if (this.child) return;
113
- const [{ spawn: spawn3 }, { buildChildEnv }] = await Promise.all([
114
+ const [{ spawn: spawn3 }, { buildChildEnv }, os] = await Promise.all([
114
115
  import('child_process'),
115
- import('@wrongstack/core')
116
+ import('@wrongstack/core'),
117
+ import('os')
116
118
  ]);
117
119
  return new Promise((resolve3, reject) => {
118
120
  const timeout = setTimeout(() => {
@@ -120,10 +122,12 @@ var ClientTransport = class {
120
122
  new Error(`ACP child process failed to start within ${this.opts.handshakeTimeoutMs}ms`)
121
123
  );
122
124
  }, this.opts.handshakeTimeoutMs);
125
+ const isPkgLauncher = this.opts.command === "npx" || this.opts.command === "uvx";
126
+ const spawnCwd = isPkgLauncher ? os.homedir() : this.opts.cwd;
123
127
  try {
124
128
  this.child = spawn3(this.opts.command, this.opts.args ?? [], {
125
129
  env: { ...buildChildEnv(), ...this.opts.env },
126
- cwd: this.opts.cwd,
130
+ cwd: spawnCwd,
127
131
  stdio: ["pipe", "pipe", "pipe"],
128
132
  windowsHide: true,
129
133
  // On Windows, most ACP-supporting tools (claude, gemini, codex,
@@ -142,17 +146,39 @@ var ClientTransport = class {
142
146
  }
143
147
  const child = this.child;
144
148
  child.stdout.setEncoding("utf8");
149
+ let settled = false;
150
+ const onSpawnFailure = (err) => {
151
+ if (settled) {
152
+ this.closed = true;
153
+ return;
154
+ }
155
+ settled = true;
156
+ clearTimeout(timeout);
157
+ reject(err);
158
+ };
159
+ child.on("error", onSpawnFailure);
160
+ child.stdout.on("error", onSpawnFailure);
161
+ if (this.opts.skipHandshakeMarker) {
162
+ child.stdout.on("data", (c) => this.onChildData(c));
163
+ child.stderr.on("data", (c) => this.onChildError(c));
164
+ child.on("close", (code) => this.onChildClose(code));
165
+ child.once("spawn", () => {
166
+ if (settled) return;
167
+ settled = true;
168
+ clearTimeout(timeout);
169
+ resolve3();
170
+ });
171
+ return;
172
+ }
145
173
  const onReady = () => {
174
+ if (settled) return;
175
+ settled = true;
146
176
  child.stdout.on("data", (c) => this.onChildData(c));
147
177
  child.stderr.on("data", (c) => this.onChildError(c));
148
178
  child.on("close", (code) => this.onChildClose(code));
149
179
  clearTimeout(timeout);
150
180
  resolve3();
151
181
  };
152
- if (this.opts.skipHandshakeMarker) {
153
- onReady();
154
- return;
155
- }
156
182
  const waitForMarker = (chunk) => {
157
183
  this.buffer += chunk;
158
184
  const idx = this.buffer.indexOf("[wstack-acp]\n");
@@ -163,14 +189,6 @@ var ClientTransport = class {
163
189
  }
164
190
  };
165
191
  child.stdout.on("data", waitForMarker);
166
- child.stdout.on("error", (err) => {
167
- clearTimeout(timeout);
168
- reject(err);
169
- });
170
- child.on("error", (err) => {
171
- clearTimeout(timeout);
172
- reject(err);
173
- });
174
192
  });
175
193
  }
176
194
  send(msg) {
@@ -365,7 +383,16 @@ var ACP_PROTOCOL_VERSION = 1;
365
383
  function toWire(msg) {
366
384
  return msg;
367
385
  }
368
- var WRONGSTACK_VERSION = "0.263.0";
386
+ var WRONGSTACK_VERSION = "0.274.1";
387
+ var WRONGSTACK_AUTH_METHODS = [
388
+ {
389
+ id: "wrongstack-auth",
390
+ name: "Run wstack auth",
391
+ description: "Configure a WrongStack model provider in an interactive terminal.",
392
+ type: "terminal",
393
+ args: ["auth"]
394
+ }
395
+ ];
369
396
  var DEFAULT_MODE_ID = "code";
370
397
  var DEFAULT_MODES = [
371
398
  {
@@ -382,9 +409,17 @@ var ACPProtocolHandler = class {
382
409
  modes;
383
410
  configOptions;
384
411
  agentName;
412
+ replayFor;
413
+ seedFor;
414
+ store;
385
415
  initialized = false;
416
+ clientCapabilities = {};
386
417
  sessions = /* @__PURE__ */ new Map();
387
418
  nextId = 1;
419
+ // Outbound request correlation (server → client requests, e.g.
420
+ // session/request_permission). Keyed by our own `srv_N` ids.
421
+ pendingOut = /* @__PURE__ */ new Map();
422
+ nextOutId = 1;
388
423
  constructor(opts) {
389
424
  this.transport = opts.transport;
390
425
  this.defaultCwd = opts.defaultCwd;
@@ -394,6 +429,43 @@ var ACPProtocolHandler = class {
394
429
  this.modes = opts.modes ?? DEFAULT_MODES;
395
430
  this.configOptions = opts.configOptions ?? [];
396
431
  this.agentName = opts.agentName ?? "wrongstack";
432
+ this.replayFor = opts.replayFor;
433
+ this.seedFor = opts.seedFor;
434
+ this.store = opts.store;
435
+ if (typeof this.transport.onMessage === "function") {
436
+ this.transport.onMessage((m) => this.maybeResolvePending(m));
437
+ }
438
+ }
439
+ /**
440
+ * Send a request to the client and await its response. Used for
441
+ * server-initiated calls like `session/request_permission`. Rejects on
442
+ * timeout or transport error so the caller can pick a safe fallback.
443
+ */
444
+ request(method, params, timeoutMs = 6e4) {
445
+ const id = `srv_${this.nextOutId++}`;
446
+ return new Promise((resolve3, reject) => {
447
+ const timer = setTimeout(() => {
448
+ this.pendingOut.delete(id);
449
+ reject(new Error(`${method} timed out after ${timeoutMs}ms`));
450
+ }, timeoutMs);
451
+ this.pendingOut.set(id, { resolve: resolve3, reject, timer });
452
+ this.transport.send(toWire({ jsonrpc: "2.0", id, method, params })).catch((e) => {
453
+ clearTimeout(timer);
454
+ this.pendingOut.delete(id);
455
+ reject(e instanceof Error ? e : new Error(String(e)));
456
+ });
457
+ });
458
+ }
459
+ maybeResolvePending(m) {
460
+ const id = m.id;
461
+ if (typeof id !== "string") return;
462
+ const pending = this.pendingOut.get(id);
463
+ if (!pending) return;
464
+ this.pendingOut.delete(id);
465
+ clearTimeout(pending.timer);
466
+ const err = m.error;
467
+ if (err) pending.reject(new Error(err.message ?? "client request failed"));
468
+ else pending.resolve(m.result);
397
469
  }
398
470
  /**
399
471
  * Process one inbound message. Returns true if this was a terminal
@@ -420,6 +492,11 @@ var ACPProtocolHandler = class {
420
492
  session.abort.abort();
421
493
  }
422
494
  this.sessions.clear();
495
+ for (const [, p] of this.pendingOut) {
496
+ clearTimeout(p.timer);
497
+ p.reject(new Error("protocol handler closed"));
498
+ }
499
+ this.pendingOut.clear();
423
500
  }
424
501
  // ────────────────────────────────────────────────────────────────────
425
502
  // Requests
@@ -435,10 +512,18 @@ var ACPProtocolHandler = class {
435
512
  return await this.handleInitialize(id, params);
436
513
  case "authenticate":
437
514
  return await this.handleAuthenticate(id, params);
515
+ case "logout":
516
+ return await this.handleLogout(id, params);
438
517
  case "session/new":
439
518
  return await this.handleSessionNew(id, params);
440
519
  case "session/load":
441
520
  return await this.handleSessionLoad(id, params);
521
+ case "session/resume":
522
+ return await this.handleSessionResume(id, params);
523
+ case "session/close":
524
+ return await this.handleSessionClose(id, params);
525
+ case "session/delete":
526
+ return await this.handleSessionDelete(id, params);
442
527
  case "session/prompt":
443
528
  return await this.handleSessionPrompt(id, params);
444
529
  case "session/set_mode":
@@ -447,6 +532,16 @@ var ACPProtocolHandler = class {
447
532
  return await this.handleSetConfigOption(id, params);
448
533
  case "session/list":
449
534
  return await this.handleSessionList(id);
535
+ case "session/fork":
536
+ return await this.handleSessionFork(id, params);
537
+ case "providers/list":
538
+ return await this.handleProvidersList(id, params);
539
+ case "providers/set":
540
+ return await this.handleProvidersSet(id, params);
541
+ case "providers/disable":
542
+ return await this.handleProvidersDisable(id, params);
543
+ case "mcp/message":
544
+ return await this.handleMcpMessage(id, params);
450
545
  default:
451
546
  await this.sendError(id, -32601, `Unknown method: ${method}`);
452
547
  return false;
@@ -459,14 +554,8 @@ var ACPProtocolHandler = class {
459
554
  }
460
555
  async handleInitialize(id, params) {
461
556
  const p = params ?? {};
462
- const requested = typeof p.protocolVersion === "number" ? p.protocolVersion : 1;
463
- if (requested !== ACP_PROTOCOL_VERSION) {
464
- await this.sendError(
465
- id,
466
- -32e3,
467
- `server speaks protocolVersion=${ACP_PROTOCOL_VERSION}, client requested ${requested}`
468
- );
469
- return false;
557
+ if (p.clientCapabilities && typeof p.clientCapabilities === "object") {
558
+ this.clientCapabilities = p.clientCapabilities;
470
559
  }
471
560
  this.initialized = true;
472
561
  await this.transport.send(toWire({
@@ -477,9 +566,25 @@ var ACPProtocolHandler = class {
477
566
  agentCapabilities: {
478
567
  loadSession: true,
479
568
  promptCapabilities: {
480
- image: false,
569
+ // We route ACP image blocks into the core agent's multimodal
570
+ // input (server-agent-turn.promptToAgentInput); whether the
571
+ // model can see them is the configured provider's concern.
572
+ image: true,
481
573
  audio: false,
482
574
  embeddedContext: true
575
+ },
576
+ mcpCapabilities: {
577
+ http: false,
578
+ sse: false
579
+ },
580
+ sessionCapabilities: {
581
+ close: {},
582
+ list: {},
583
+ delete: {},
584
+ resume: {}
585
+ },
586
+ auth: {
587
+ logout: {}
483
588
  }
484
589
  },
485
590
  agentInfo: {
@@ -487,10 +592,7 @@ var ACPProtocolHandler = class {
487
592
  title: "WrongStack",
488
593
  version: WRONGSTACK_VERSION
489
594
  },
490
- // Static options advertised at handshake. They are also
491
- // re-sent on every `current_mode_update` / `config_option_update`
492
- // notification so late-joining clients see them.
493
- authMethods: [],
595
+ authMethods: WRONGSTACK_AUTH_METHODS,
494
596
  modes: this.modes,
495
597
  configOptions: this.configOptions
496
598
  }
@@ -505,6 +607,14 @@ var ACPProtocolHandler = class {
505
607
  }));
506
608
  return false;
507
609
  }
610
+ async handleLogout(id, _params) {
611
+ await this.transport.send(toWire({
612
+ jsonrpc: "2.0",
613
+ id,
614
+ result: {}
615
+ }));
616
+ return false;
617
+ }
508
618
  async handleSessionNew(id, params) {
509
619
  const p = params ?? {};
510
620
  const cwd = typeof p.cwd === "string" ? p.cwd : this.defaultCwd;
@@ -520,6 +630,7 @@ var ACPProtocolHandler = class {
520
630
  };
521
631
  this.sessions.set(sessionId, state);
522
632
  this.onSessionNew(state);
633
+ await this.persist(state);
523
634
  await this.sendNotification({
524
635
  sessionId,
525
636
  update: {
@@ -548,7 +659,173 @@ var ACPProtocolHandler = class {
548
659
  return false;
549
660
  }
550
661
  async handleSessionLoad(id, params) {
551
- return this.handleSessionNew(id, params);
662
+ const p = params ?? {};
663
+ const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
664
+ const loadCwd = typeof p.cwd === "string" ? p.cwd : void 0;
665
+ let existing = sessionId ? this.sessions.get(sessionId) : void 0;
666
+ if (!existing && sessionId && this.store) {
667
+ const persisted = await this.store.load(sessionId);
668
+ if (persisted) {
669
+ const restored = {
670
+ id: sessionId,
671
+ cwd: persisted.cwd ?? loadCwd ?? this.defaultCwd,
672
+ abort: new AbortController(),
673
+ modeId: persisted.modeId ?? DEFAULT_MODE_ID,
674
+ createdAt: persisted.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
675
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
676
+ ...persisted.title !== void 0 ? { title: persisted.title } : {}
677
+ };
678
+ this.sessions.set(sessionId, restored);
679
+ this.seedFor?.(sessionId, persisted.history ?? []);
680
+ for (const update of persisted.history ?? []) {
681
+ await this.sendNotification({ sessionId, update });
682
+ }
683
+ await this.sendNotification({
684
+ sessionId,
685
+ update: { sessionUpdate: "current_mode_update", modeId: restored.modeId }
686
+ });
687
+ await this.transport.send(toWire({
688
+ jsonrpc: "2.0",
689
+ id,
690
+ result: {
691
+ initialMode: { currentModeId: restored.modeId, availableModes: this.modes }
692
+ }
693
+ }));
694
+ return false;
695
+ }
696
+ }
697
+ if (existing) {
698
+ existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
699
+ const replay = sessionId ? this.replayFor?.(sessionId) : void 0;
700
+ if (replay) {
701
+ for (const update of replay) {
702
+ await this.sendNotification({ sessionId, update });
703
+ }
704
+ }
705
+ await this.sendNotification({
706
+ sessionId,
707
+ update: {
708
+ sessionUpdate: "session_info_update",
709
+ updatedAt: existing.updatedAt
710
+ }
711
+ });
712
+ await this.sendNotification({
713
+ sessionId,
714
+ update: {
715
+ sessionUpdate: "current_mode_update",
716
+ modeId: existing.modeId
717
+ }
718
+ });
719
+ await this.transport.send(toWire({
720
+ jsonrpc: "2.0",
721
+ id,
722
+ result: {
723
+ initialMode: {
724
+ currentModeId: existing.modeId,
725
+ availableModes: this.modes
726
+ }
727
+ }
728
+ }));
729
+ return false;
730
+ }
731
+ await this.sendError(id, -32e3, `session not found: ${sessionId}`);
732
+ return false;
733
+ }
734
+ async handleSessionResume(id, params) {
735
+ const p = params ?? {};
736
+ const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
737
+ const existing = sessionId ? this.sessions.get(sessionId) : void 0;
738
+ if (existing) {
739
+ existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
740
+ await this.transport.send(toWire({
741
+ jsonrpc: "2.0",
742
+ id,
743
+ result: {
744
+ initialMode: {
745
+ currentModeId: existing.modeId,
746
+ availableModes: this.modes
747
+ }
748
+ }
749
+ }));
750
+ return false;
751
+ }
752
+ await this.sendError(id, -32e3, `session not found: ${sessionId}`);
753
+ return false;
754
+ }
755
+ async handleSessionClose(id, params) {
756
+ const p = params ?? {};
757
+ const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
758
+ const session = sessionId ? this.sessions.get(sessionId) : void 0;
759
+ if (!session) {
760
+ await this.sendError(id, -32e3, `session not found: ${sessionId}`);
761
+ return false;
762
+ }
763
+ session.abort.abort();
764
+ if (sessionId) this.sessions.delete(sessionId);
765
+ await this.transport.send(toWire({
766
+ jsonrpc: "2.0",
767
+ id,
768
+ result: {}
769
+ }));
770
+ return false;
771
+ }
772
+ async handleSessionDelete(id, params) {
773
+ const p = params ?? {};
774
+ const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
775
+ if (!sessionId) {
776
+ await this.sendError(id, -32e3, `session not found: ${sessionId}`);
777
+ return false;
778
+ }
779
+ if (!this.sessions.has(sessionId)) {
780
+ await this.transport.send(toWire({ jsonrpc: "2.0", id, result: { configOptions: [...this.configOptions] } }));
781
+ return false;
782
+ }
783
+ const session = this.sessions.get(sessionId);
784
+ session.abort.abort();
785
+ this.sessions.delete(sessionId);
786
+ await this.transport.send(toWire({
787
+ jsonrpc: "2.0",
788
+ id,
789
+ result: {}
790
+ }));
791
+ return false;
792
+ }
793
+ async handleSessionFork(id, params) {
794
+ const p = params ?? {};
795
+ const sourceId = typeof p.sessionId === "string" ? p.sessionId : null;
796
+ if (!sourceId || !this.sessions.has(sourceId)) {
797
+ await this.sendError(id, -32e3, `session not found: ${sourceId}`);
798
+ return false;
799
+ }
800
+ const forkParams = params;
801
+ return this.handleSessionNew(id, { ...forkParams, cwd: p.cwd ?? this.defaultCwd });
802
+ }
803
+ async handleProvidersList(id, _params) {
804
+ await this.transport.send(toWire({
805
+ jsonrpc: "2.0",
806
+ id,
807
+ result: {
808
+ providers: [],
809
+ currentProviderId: null
810
+ }
811
+ }));
812
+ return false;
813
+ }
814
+ async handleProvidersSet(id, _params) {
815
+ await this.sendError(id, -32e3, "provider configuration not available through ACP; use wstack auth");
816
+ return false;
817
+ }
818
+ async handleProvidersDisable(id, _params) {
819
+ await this.transport.send(toWire({
820
+ jsonrpc: "2.0",
821
+ id,
822
+ result: {}
823
+ }));
824
+ return false;
825
+ }
826
+ async handleMcpMessage(id, _params) {
827
+ await this.sendError(id, -32e3, "MCP message routing not available through ACP");
828
+ return false;
552
829
  }
553
830
  async handleSessionPrompt(id, params) {
554
831
  const p = params ?? {};
@@ -568,11 +845,54 @@ var ACPProtocolHandler = class {
568
845
  const turnSignal = new AbortController();
569
846
  const onCancel = () => turnSignal.abort();
570
847
  session.abort.signal.addEventListener("abort", onCancel, { once: true });
848
+ const api = {
849
+ clientCapabilities: this.clientCapabilities,
850
+ requestPermission: async (req) => {
851
+ const res = await this.request("session/request_permission", {
852
+ sessionId,
853
+ toolCall: req.toolCall,
854
+ options: req.options
855
+ });
856
+ const outcome = res?.outcome;
857
+ return outcome ?? { outcome: "cancelled" };
858
+ },
859
+ readTextFile: async (params2) => {
860
+ const res = await this.request("fs/read_text_file", { sessionId, ...params2 });
861
+ return String(res?.content ?? "");
862
+ },
863
+ writeTextFile: async (params2) => {
864
+ await this.request("fs/write_text_file", { sessionId, ...params2 });
865
+ },
866
+ runTerminal: async ({ command, args, cwd }) => {
867
+ const created = await this.request("terminal/create", {
868
+ sessionId,
869
+ command,
870
+ ...args ? { args } : {},
871
+ ...cwd ? { cwd } : {}
872
+ });
873
+ const terminalId = created?.terminalId;
874
+ if (!terminalId) return { output: "", exitCode: null };
875
+ try {
876
+ const exit = await this.request("terminal/wait_for_exit", { sessionId, terminalId });
877
+ const out = await this.request("terminal/output", { sessionId, terminalId });
878
+ return {
879
+ output: String(out?.output ?? ""),
880
+ exitCode: typeof exit?.exitCode === "number" ? exit.exitCode : null
881
+ };
882
+ } finally {
883
+ try {
884
+ await this.request("terminal/release", { sessionId, terminalId });
885
+ } catch {
886
+ }
887
+ }
888
+ }
889
+ };
571
890
  let result;
572
891
  try {
573
892
  result = await this.runTurn(
574
893
  { sessionId, prompt: p.prompt, signal: turnSignal.signal },
575
- (update) => this.sendNotification({ sessionId, update })
894
+ (update) => this.sendNotification({ sessionId, update }),
895
+ api
576
896
  );
577
897
  } catch (err) {
578
898
  session.abort.signal.removeEventListener("abort", onCancel);
@@ -582,6 +902,7 @@ var ACPProtocolHandler = class {
582
902
  }
583
903
  session.abort.signal.removeEventListener("abort", onCancel);
584
904
  session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
905
+ await this.persist(session);
585
906
  await this.transport.send(toWire({
586
907
  jsonrpc: "2.0",
587
908
  id,
@@ -610,12 +931,12 @@ var ACPProtocolHandler = class {
610
931
  async handleSetConfigOption(id, params) {
611
932
  const p = params ?? {};
612
933
  const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
613
- const optionId = typeof p.configOptionId === "string" ? p.configOptionId : null;
934
+ const optionId = typeof p.configId === "string" ? p.configId : null;
614
935
  const value = typeof p.value === "string" ? p.value : null;
615
936
  const session = sessionId ? this.sessions.get(sessionId) : void 0;
616
937
  const option = optionId ? this.configOptions.find((o) => o.id === optionId) : void 0;
617
938
  if (!session || !option || value === null || !option.options.some((o) => o.value === value)) {
618
- await this.sendError(id, -32602, "invalid sessionId, configOptionId, or value");
939
+ await this.sendError(id, -32602, "invalid sessionId, configId, or value");
619
940
  return false;
620
941
  }
621
942
  option.currentValue = value;
@@ -627,7 +948,7 @@ var ACPProtocolHandler = class {
627
948
  configOptions: [...this.configOptions]
628
949
  }
629
950
  });
630
- await this.transport.send(toWire({ jsonrpc: "2.0", id, result: {} }));
951
+ await this.transport.send(toWire({ jsonrpc: "2.0", id, result: { configOptions: [...this.configOptions] } }));
631
952
  return false;
632
953
  }
633
954
  async handleSessionList(id) {
@@ -661,6 +982,9 @@ var ACPProtocolHandler = class {
661
982
  }
662
983
  return false;
663
984
  }
985
+ case "$/cancel_request": {
986
+ return false;
987
+ }
664
988
  case "exit":
665
989
  this.close();
666
990
  return true;
@@ -674,6 +998,14 @@ var ACPProtocolHandler = class {
674
998
  async sendNotification(params) {
675
999
  await this.transport.send(toWire({ jsonrpc: "2.0", method: "session/update", params }));
676
1000
  }
1001
+ /** Best-effort durable persistence of a session + its recorded history. */
1002
+ async persist(state) {
1003
+ if (!this.store) return;
1004
+ try {
1005
+ await this.store.save(state, this.replayFor?.(state.id));
1006
+ } catch {
1007
+ }
1008
+ }
677
1009
  async sendError(id, code, message, data) {
678
1010
  const error = { code, message };
679
1011
  if (data !== void 0) error.data = data;
@@ -701,26 +1033,41 @@ function errorToJsonRpc(err) {
701
1033
  var WrongStackACPServer = class {
702
1034
  transport;
703
1035
  handler;
1036
+ options;
1037
+ /** HTTP server when transport mode is HTTP. */
1038
+ httpServer = null;
704
1039
  running = false;
705
1040
  constructor(opts = {}) {
1041
+ this.options = opts;
706
1042
  this.transport = new StdioTransport();
707
1043
  const runTurn = opts.runTurn ?? defaultEchoRunTurn;
708
1044
  this.handler = new ACPProtocolHandler({
709
1045
  transport: this.transport,
710
1046
  defaultCwd: opts.defaultCwd ?? process.cwd(),
711
1047
  runTurn,
712
- agentName: opts.agentName
1048
+ agentName: opts.agentName,
1049
+ ...opts.replayFor ? { replayFor: opts.replayFor } : {},
1050
+ ...opts.seedFor ? { seedFor: opts.seedFor } : {},
1051
+ ...opts.store ? { store: opts.store } : {}
713
1052
  });
714
1053
  }
715
1054
  /**
716
- * Start the server. Blocks until the client disconnects.
717
- *
718
- * 1. Print the legacy `[wstack-acp]\n` marker so the client knows the
719
- * process is the ACP server (the old `StdioTransport` handshake).
720
- * 2. Loop: read messages, dispatch to the handler, until EOF / error.
1055
+ * Start the server. Mode depends on `options.transport`:
1056
+ * - 'stdio' (default): reads JSON-RPC from stdin, writes to stdout.
1057
+ * - number: listens as HTTP on the given port.
721
1058
  */
722
1059
  async start() {
723
- this.transport.sendStartupMarker();
1060
+ const transportMode = this.options.transport;
1061
+ if (typeof transportMode === "number") {
1062
+ await this.startHttp(transportMode);
1063
+ } else {
1064
+ await this.startStdio();
1065
+ }
1066
+ }
1067
+ async startStdio() {
1068
+ if (this.options.legacyStartupMarker) {
1069
+ this.transport.sendStartupMarker();
1070
+ }
724
1071
  this.running = true;
725
1072
  while (this.running) {
726
1073
  const msg = await this.transport.read();
@@ -730,10 +1077,80 @@ var WrongStackACPServer = class {
730
1077
  }
731
1078
  this.transport.close();
732
1079
  }
1080
+ async startHttp(port) {
1081
+ const host = this.options.host ?? "127.0.0.1";
1082
+ const handler = this.handler;
1083
+ this.httpServer = createServer(async (req, res) => {
1084
+ const selfOrigin = `http://${host}:${port}`;
1085
+ const reqOrigin = Array.isArray(req.headers.origin) ? req.headers.origin[0] : req.headers.origin;
1086
+ if (reqOrigin && reqOrigin !== selfOrigin) {
1087
+ res.writeHead(403);
1088
+ res.end(JSON.stringify({ error: "cross-origin request forbidden" }));
1089
+ return;
1090
+ }
1091
+ if (reqOrigin) res.setHeader("Access-Control-Allow-Origin", reqOrigin);
1092
+ res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
1093
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id");
1094
+ if (req.method === "OPTIONS") {
1095
+ res.writeHead(204);
1096
+ res.end();
1097
+ return;
1098
+ }
1099
+ if (req.method !== "POST") {
1100
+ res.writeHead(405);
1101
+ res.end(JSON.stringify({ error: "method not allowed" }));
1102
+ return;
1103
+ }
1104
+ let body = "";
1105
+ for await (const chunk of req) {
1106
+ body += chunk;
1107
+ }
1108
+ let msg;
1109
+ try {
1110
+ msg = JSON.parse(body);
1111
+ } catch {
1112
+ res.writeHead(400);
1113
+ res.end(JSON.stringify({ error: { code: -32700, message: "Parse error" } }));
1114
+ return;
1115
+ }
1116
+ const notifications = [];
1117
+ let response = null;
1118
+ const originalSend = this.transport.send.bind(this.transport);
1119
+ this.transport.send = async (m) => {
1120
+ if (m.id !== void 0 && (m.result !== void 0 || m.error !== void 0)) {
1121
+ response = m;
1122
+ } else if (m.method === "session/update") {
1123
+ notifications.push(m.params);
1124
+ } else {
1125
+ notifications.push(m);
1126
+ }
1127
+ };
1128
+ try {
1129
+ await handler.handleMessage(msg);
1130
+ } finally {
1131
+ this.transport.send = originalSend;
1132
+ }
1133
+ res.writeHead(200, { "Content-Type": "application/json" });
1134
+ const responseBody = response !== null ? { ...response, notifications } : { notifications };
1135
+ res.end(JSON.stringify(responseBody));
1136
+ });
1137
+ return new Promise((resolve3) => {
1138
+ this.httpServer.listen(port, host, () => {
1139
+ writeErr(`[wstack-acp] HTTP server listening on http://${host}:${port}
1140
+ `);
1141
+ this.running = true;
1142
+ resolve3();
1143
+ });
1144
+ });
1145
+ }
733
1146
  /** Stop the server. */
734
1147
  stop() {
735
1148
  this.running = false;
736
1149
  this.transport.close();
1150
+ if (this.httpServer) {
1151
+ this.httpServer.close();
1152
+ this.httpServer = null;
1153
+ }
737
1154
  }
738
1155
  };
739
1156
  var defaultEchoRunTurn = async (_input, _emit) => {
@@ -751,6 +1168,115 @@ if (isEntrypoint) {
751
1168
  process.exit(1);
752
1169
  });
753
1170
  }
1171
+
1172
+ // src/client/websocket-transport.ts
1173
+ var WebSocketClientTransport = class {
1174
+ ws = null;
1175
+ handlers = /* @__PURE__ */ new Set();
1176
+ closed = false;
1177
+ opts;
1178
+ constructor(opts) {
1179
+ this.opts = opts;
1180
+ }
1181
+ start() {
1182
+ const WS = globalThis.WebSocket;
1183
+ if (!WS) {
1184
+ return Promise.reject(
1185
+ new Error(
1186
+ "global WebSocket is not available \u2014 Node \u2265 22 is required for the remote ACP transport"
1187
+ )
1188
+ );
1189
+ }
1190
+ const timeoutMs = this.opts.handshakeTimeoutMs ?? 3e4;
1191
+ return new Promise((resolve3, reject) => {
1192
+ let settled = false;
1193
+ const ws = new WS(this.opts.url, this.opts.protocols);
1194
+ this.ws = ws;
1195
+ const timer = setTimeout(() => {
1196
+ if (settled) return;
1197
+ settled = true;
1198
+ try {
1199
+ ws.close();
1200
+ } catch {
1201
+ }
1202
+ reject(new Error(`WebSocket failed to open within ${timeoutMs}ms`));
1203
+ }, timeoutMs);
1204
+ ws.addEventListener("open", () => {
1205
+ if (settled) return;
1206
+ settled = true;
1207
+ clearTimeout(timer);
1208
+ resolve3();
1209
+ });
1210
+ ws.addEventListener("error", (ev) => {
1211
+ if (settled) {
1212
+ this.closed = true;
1213
+ return;
1214
+ }
1215
+ settled = true;
1216
+ clearTimeout(timer);
1217
+ const message = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "WebSocket error";
1218
+ reject(new Error(message));
1219
+ });
1220
+ ws.addEventListener("close", () => {
1221
+ this.closed = true;
1222
+ });
1223
+ ws.addEventListener("message", (ev) => {
1224
+ this.onData(ev.data);
1225
+ });
1226
+ });
1227
+ }
1228
+ send(msg) {
1229
+ if (this.closed || !this.ws) {
1230
+ return Promise.reject(new Error("WebSocket transport is not open"));
1231
+ }
1232
+ try {
1233
+ this.ws.send(JSON.stringify(msg));
1234
+ return Promise.resolve();
1235
+ } catch (err) {
1236
+ return Promise.reject(err instanceof Error ? err : new Error(String(err)));
1237
+ }
1238
+ }
1239
+ onMessage(handler) {
1240
+ this.handlers.add(handler);
1241
+ return () => this.handlers.delete(handler);
1242
+ }
1243
+ stop() {
1244
+ this.closed = true;
1245
+ if (this.ws) {
1246
+ try {
1247
+ this.ws.close();
1248
+ } catch {
1249
+ }
1250
+ this.ws = null;
1251
+ }
1252
+ }
1253
+ onData(data) {
1254
+ const text = typeof data === "string" ? data : data instanceof ArrayBuffer ? Buffer.from(data).toString("utf8") : Buffer.isBuffer(data) ? data.toString("utf8") : String(data);
1255
+ if (!text.trim()) return;
1256
+ let msg;
1257
+ try {
1258
+ msg = JSON.parse(text);
1259
+ } catch {
1260
+ for (const line of text.split("\n")) {
1261
+ if (!line.trim()) continue;
1262
+ try {
1263
+ this.dispatch(JSON.parse(line));
1264
+ } catch {
1265
+ }
1266
+ }
1267
+ return;
1268
+ }
1269
+ this.dispatch(msg);
1270
+ }
1271
+ dispatch(msg) {
1272
+ for (const handler of [...this.handlers]) {
1273
+ try {
1274
+ handler(msg);
1275
+ } catch {
1276
+ }
1277
+ }
1278
+ }
1279
+ };
754
1280
  var DEFAULT_OPTIONS = {
755
1281
  asyncTools: true,
756
1282
  pollIntervalMs: 500,
@@ -793,6 +1319,7 @@ var ToolTranslator = class {
793
1319
  */
794
1320
  async callTool(transport, name, args, callId = crypto.randomUUID()) {
795
1321
  await transport.send({
1322
+ jsonrpc: "2.0",
796
1323
  method: "tools/call",
797
1324
  id: callId,
798
1325
  params: { name, arguments: args }
@@ -815,11 +1342,11 @@ var ToolTranslator = class {
815
1342
  var FsError = class extends Error {
816
1343
  code;
817
1344
  path;
818
- constructor(code, path3, message) {
1345
+ constructor(code, path4, message) {
819
1346
  super(message);
820
1347
  this.name = "FsError";
821
1348
  this.code = code;
822
- this.path = path3;
1349
+ this.path = path4;
823
1350
  }
824
1351
  };
825
1352
  var FileServer = class {
@@ -913,12 +1440,11 @@ function randomHex(bytes) {
913
1440
  }
914
1441
 
915
1442
  // src/client/permission.ts
916
- var defaultPermissionPolicy = async (req) => {
917
- if (req.signal.aborted) return { outcome: "cancelled" };
918
- const ranked = [...req.options].sort((a, b) => {
1443
+ function pickAllow(options) {
1444
+ const ranked = [...options].sort((a, b) => {
919
1445
  const score = (k) => {
920
- if (k === "allow_always") return 0;
921
- if (k === "allow_once") return 1;
1446
+ if (k === "allow_once") return 0;
1447
+ if (k === "allow_always") return 1;
922
1448
  if (k === "reject_once") return 2;
923
1449
  return 3;
924
1450
  };
@@ -929,7 +1455,33 @@ var defaultPermissionPolicy = async (req) => {
929
1455
  return { outcome: "cancelled" };
930
1456
  }
931
1457
  return { outcome: "selected", optionId: chosen.optionId };
1458
+ }
1459
+ function pickReject(options) {
1460
+ const reject = options.find(
1461
+ (o) => o.kind === "reject_once" || o.kind === "reject_always"
1462
+ );
1463
+ return reject ? { outcome: "selected", optionId: reject.optionId } : { outcome: "cancelled" };
1464
+ }
1465
+ var READ_ONLY_KINDS = /* @__PURE__ */ new Set(["read", "search", "fetch", "think"]);
1466
+ var defaultPermissionPolicy = async (req) => {
1467
+ if (req.signal.aborted) return { outcome: "cancelled" };
1468
+ return pickAllow(req.options);
1469
+ };
1470
+ var readOnlyPermissionPolicy = async (req) => {
1471
+ if (req.signal.aborted) return { outcome: "cancelled" };
1472
+ const kind = req.toolCall.kind;
1473
+ if (kind && READ_ONLY_KINDS.has(kind)) {
1474
+ return pickAllow(req.options);
1475
+ }
1476
+ return pickReject(req.options);
932
1477
  };
1478
+ function makePermissionPolicy(decide) {
1479
+ return async (req) => {
1480
+ if (req.signal.aborted) return { outcome: "cancelled" };
1481
+ const allow = await decide(req);
1482
+ return allow ? pickAllow(req.options) : pickReject(req.options);
1483
+ };
1484
+ }
933
1485
  var TerminalServer = class {
934
1486
  terminals = /* @__PURE__ */ new Map();
935
1487
  projectRoot;
@@ -1125,6 +1677,12 @@ var ACPSession = class _ACPSession {
1125
1677
  nextId = 1;
1126
1678
  /** True after close() has been called. */
1127
1679
  closed = false;
1680
+ // Agent-provided info from the initialize handshake
1681
+ agentCapabilities = {};
1682
+ agentInfo = null;
1683
+ authMethods = [];
1684
+ /** Protocol version negotiated with the agent during initialize. */
1685
+ negotiatedVersion = ACP_PROTOCOL_VERSION;
1128
1686
  constructor(opts, transport) {
1129
1687
  this.opts = opts;
1130
1688
  this.transport = transport;
@@ -1146,6 +1704,36 @@ var ACPSession = class _ACPSession {
1146
1704
  this.terminalServer = new TerminalServer(termOpts);
1147
1705
  this.permissionPolicy = opts.permissionPolicy ?? defaultPermissionPolicy;
1148
1706
  }
1707
+ // ──────────────────────────────────────────────────────────────────────
1708
+ // Public accessors
1709
+ // ──────────────────────────────────────────────────────────────────────
1710
+ /** Agent capabilities advertised during initialize. */
1711
+ getCapabilities() {
1712
+ return { ...this.agentCapabilities };
1713
+ }
1714
+ /** Authentication methods advertised by the agent. */
1715
+ getAuthMethods() {
1716
+ return [...this.authMethods];
1717
+ }
1718
+ /** Agent info (name, title, version) from initialize. */
1719
+ getAgentInfo() {
1720
+ return this.agentInfo;
1721
+ }
1722
+ /** Whether the agent requires authentication (has auth methods). */
1723
+ requiresAuth() {
1724
+ return this.authMethods.length > 0;
1725
+ }
1726
+ /** Current session id, if one exists. */
1727
+ getSessionId() {
1728
+ return this.sessionId;
1729
+ }
1730
+ /** Protocol version negotiated during initialize. */
1731
+ getNegotiatedVersion() {
1732
+ return this.negotiatedVersion;
1733
+ }
1734
+ // ──────────────────────────────────────────────────────────────────────
1735
+ // Lifecycle — start
1736
+ // ──────────────────────────────────────────────────────────────────────
1149
1737
  /**
1150
1738
  * Spawn the child, run the initialize handshake, install the
1151
1739
  * message dispatch, and return a ready session.
@@ -1155,20 +1743,37 @@ var ACPSession = class _ACPSession {
1155
1743
  command: opts.command,
1156
1744
  args: opts.args ? [...opts.args] : [],
1157
1745
  handshakeTimeoutMs: 3e4,
1158
- // ACPSession is the v1 CLIENT side: it speaks to external agents
1159
- // (Claude Code, Gemini CLI, …) that do NOT emit a `[wstack-acp]\n`
1160
- // startup marker. The transport should treat the child as ready
1161
- // as soon as the process is spawned and stdout is flowing.
1162
1746
  skipHandshakeMarker: true
1163
1747
  };
1164
1748
  if (opts.env !== void 0) transportOpts.env = opts.env;
1165
1749
  if (opts.cwd !== void 0) transportOpts.cwd = opts.cwd;
1166
1750
  const transport = new ClientTransport(transportOpts);
1751
+ return _ACPSession.attach(opts, transport, `failed to spawn ${opts.command}`);
1752
+ }
1753
+ /**
1754
+ * Connect to a REMOTE ACP agent over a WebSocket instead of spawning a
1755
+ * local subprocess. `opts.command` is ignored for the wire (a label is
1756
+ * still useful for `role`); everything else (projectRoot sandbox for
1757
+ * fs/terminal, timeouts, permission policy, MCP servers) applies the same.
1758
+ */
1759
+ static async connectWebSocket(wsOpts, opts) {
1760
+ const transport = new WebSocketClientTransport(wsOpts);
1761
+ return _ACPSession.attach(opts, transport, `failed to connect to ${wsOpts.url}`);
1762
+ }
1763
+ /**
1764
+ * Connect using a caller-supplied transport. Lets advanced callers plug
1765
+ * in their own wire (SDK streams, in-process pipes, test doubles).
1766
+ */
1767
+ static async connect(transport, opts) {
1768
+ return _ACPSession.attach(opts, transport, "failed to connect transport");
1769
+ }
1770
+ /** Shared connect path: start the transport, install dispatch, handshake. */
1771
+ static async attach(opts, transport, spawnErrLabel) {
1167
1772
  try {
1168
1773
  await transport.start();
1169
1774
  } catch (err) {
1170
1775
  const msg = err instanceof Error ? err.message : String(err);
1171
- throw new ACPSessionError("spawn_failed", `failed to spawn ${opts.command}: ${msg}`, err);
1776
+ throw new ACPSessionError("spawn_failed", `${spawnErrLabel}: ${msg}`, err);
1172
1777
  }
1173
1778
  const session = new _ACPSession(opts, transport);
1174
1779
  transport.onMessage((msg) => session.handleMessage(msg));
@@ -1183,14 +1788,16 @@ var ACPSession = class _ACPSession {
1183
1788
  }
1184
1789
  return session;
1185
1790
  }
1791
+ // ──────────────────────────────────────────────────────────────────────
1792
+ // Initialization
1793
+ // ──────────────────────────────────────────────────────────────────────
1186
1794
  async initialize() {
1187
1795
  const id = this.allocId();
1188
1796
  const result = await this.sendRequest(id, "initialize", {
1189
1797
  protocolVersion: ACP_PROTOCOL_VERSION,
1190
1798
  clientCapabilities: {
1191
1799
  fs: { readTextFile: true, writeTextFile: true },
1192
- terminal: true,
1193
- promptCapabilities: { image: false, audio: false, embeddedContext: true }
1800
+ terminal: true
1194
1801
  },
1195
1802
  clientInfo: { name: "wrongstack", title: "WrongStack", version: "0.263.0" }
1196
1803
  });
@@ -1201,53 +1808,344 @@ var ACPSession = class _ACPSession {
1201
1808
  throw new ACPSessionError("protocol_error", "initialize returned no protocolVersion");
1202
1809
  }
1203
1810
  const r = result;
1204
- if (r.protocolVersion !== ACP_PROTOCOL_VERSION) {
1811
+ if (r.protocolVersion > ACP_PROTOCOL_VERSION) {
1205
1812
  throw new ACPSessionError(
1206
1813
  "unsupported_capability",
1207
- `agent speaks protocolVersion=${r.protocolVersion}, client speaks ${ACP_PROTOCOL_VERSION}`
1814
+ `agent requires protocolVersion=${r.protocolVersion}, client supports up to ${ACP_PROTOCOL_VERSION}`
1208
1815
  );
1209
1816
  }
1817
+ this.negotiatedVersion = r.protocolVersion;
1818
+ this.agentCapabilities = r.agentCapabilities ?? {};
1819
+ this.agentInfo = r.agentInfo ?? null;
1820
+ this.authMethods = r.authMethods ?? [];
1210
1821
  this.state = "ready";
1211
1822
  }
1823
+ // ──────────────────────────────────────────────────────────────────────
1824
+ // Authentication
1825
+ // ──────────────────────────────────────────────────────────────────────
1212
1826
  /**
1213
- * Run one prompt turn. Creates a session if needed, sends the
1214
- * prompt, streams session/update notifications, and resolves with
1215
- * the agent's response.
1827
+ * Authenticate with the agent using one of the advertised auth methods.
1828
+ * Call this AFTER start() and BEFORE any session/new call.
1216
1829
  *
1217
- * Cancellation: if `signal` aborts mid-prompt, we send
1218
- * `session/cancel` (a notification per spec) and keep accepting
1830
+ * Throws ACPSessionError('auth_failed') if the agent rejects the
1831
+ * authentication or if the methodId is not in the advertised list.
1832
+ */
1833
+ async authenticate(methodId) {
1834
+ if (this.state === "closed") {
1835
+ throw new ACPSessionError("closed", "session is closed");
1836
+ }
1837
+ if (this.state !== "ready") {
1838
+ throw new ACPSessionError(
1839
+ "protocol_error",
1840
+ `authenticate called in state=${this.state} (expected 'ready')`
1841
+ );
1842
+ }
1843
+ if (!this.authMethods.some((m) => m.id === methodId)) {
1844
+ throw new ACPSessionError(
1845
+ "auth_failed",
1846
+ `auth method "${methodId}" not in advertised methods: ${this.authMethods.map((m) => m.id).join(", ")}`
1847
+ );
1848
+ }
1849
+ const id = this.allocId();
1850
+ const result = await this.sendRequest(id, "authenticate", { methodId });
1851
+ if (isJsonRpcError(result)) {
1852
+ throw new ACPSessionError("auth_failed", `authenticate failed: ${result.message}`, result);
1853
+ }
1854
+ this.state = "authenticated";
1855
+ }
1856
+ /**
1857
+ * Log out from the current authenticated session.
1858
+ * Only callable if the agent advertises `auth.logout` capability.
1859
+ */
1860
+ async logout() {
1861
+ if (this.state === "closed") {
1862
+ throw new ACPSessionError("closed", "session is closed");
1863
+ }
1864
+ if (!this.agentCapabilities.auth?.logout) {
1865
+ throw new ACPSessionError(
1866
+ "unsupported_capability",
1867
+ "agent does not support logout (auth.logout capability not advertised)"
1868
+ );
1869
+ }
1870
+ const id = this.allocId();
1871
+ const result = await this.sendRequest(id, "logout", {});
1872
+ if (isJsonRpcError(result)) {
1873
+ throw new ACPSessionError("logout_failed", `logout failed: ${result.message}`, result);
1874
+ }
1875
+ this.state = "ready";
1876
+ }
1877
+ // ──────────────────────────────────────────────────────────────────────
1878
+ // Session management
1879
+ // ──────────────────────────────────────────────────────────────────────
1880
+ /**
1881
+ * Load an existing session. The agent replays the conversation history
1882
+ * via session/update notifications before responding.
1883
+ *
1884
+ * Only works if the agent advertises `loadSession` capability.
1885
+ *
1886
+ * @param sessionId - The session to load
1887
+ * @param mcpServers - Optional MCP servers (defaults to options.mcpServers)
1888
+ * @param cwd - Optional working directory (defaults to options.cwd or projectRoot)
1889
+ */
1890
+ async loadSession(sessionId, mcpServers, cwd) {
1891
+ if (this.closed) {
1892
+ throw new ACPSessionError("closed", "session is closed");
1893
+ }
1894
+ if (!this.agentCapabilities.loadSession) {
1895
+ throw new ACPSessionError(
1896
+ "unsupported_capability",
1897
+ "agent does not support session/load (loadSession capability not advertised)"
1898
+ );
1899
+ }
1900
+ if (this.sessionId) {
1901
+ await this.closeSession();
1902
+ }
1903
+ this.resetScratch();
1904
+ const servers = this.filterMcpServers(mcpServers ?? this.opts.mcpServers);
1905
+ const id = this.allocId();
1906
+ const result = await this.sendRequest(id, "session/load", {
1907
+ sessionId,
1908
+ cwd: cwd ?? this.opts.cwd ?? this.opts.projectRoot,
1909
+ mcpServers: servers
1910
+ });
1911
+ if (isJsonRpcError(result)) {
1912
+ throw new ACPSessionError("prompt_failed", `session/load failed: ${result.message}`, result);
1913
+ }
1914
+ this.sessionId = sessionId;
1915
+ }
1916
+ /**
1917
+ * Resume an existing session without replaying history.
1918
+ *
1919
+ * Only works if the agent advertises `sessionCapabilities.resume`.
1920
+ *
1921
+ * @param sessionId - The session to resume
1922
+ * @param mcpServers - Optional MCP servers (defaults to options.mcpServers)
1923
+ * @param cwd - Optional working directory (defaults to options.cwd or projectRoot)
1924
+ */
1925
+ async resumeSession(sessionId, mcpServers, cwd) {
1926
+ if (this.closed) {
1927
+ throw new ACPSessionError("closed", "session is closed");
1928
+ }
1929
+ if (!this.agentCapabilities.sessionCapabilities?.resume) {
1930
+ throw new ACPSessionError(
1931
+ "unsupported_capability",
1932
+ "agent does not support session/resume (sessionCapabilities.resume not advertised)"
1933
+ );
1934
+ }
1935
+ if (this.sessionId) {
1936
+ await this.closeSession();
1937
+ }
1938
+ const servers = this.filterMcpServers(mcpServers ?? this.opts.mcpServers);
1939
+ const id = this.allocId();
1940
+ const result = await this.sendRequest(id, "session/resume", {
1941
+ sessionId,
1942
+ cwd: cwd ?? this.opts.cwd ?? this.opts.projectRoot,
1943
+ mcpServers: servers
1944
+ });
1945
+ if (isJsonRpcError(result)) {
1946
+ throw new ACPSessionError("prompt_failed", `session/resume failed: ${result.message}`, result);
1947
+ }
1948
+ this.sessionId = sessionId;
1949
+ }
1950
+ /**
1951
+ * List existing sessions known to the agent.
1952
+ *
1953
+ * Only works if the agent advertises `sessionCapabilities.list`.
1954
+ */
1955
+ async listSessions(cursor, cwd) {
1956
+ if (this.closed) {
1957
+ throw new ACPSessionError("closed", "session is closed");
1958
+ }
1959
+ if (!this.agentCapabilities.sessionCapabilities?.list) {
1960
+ throw new ACPSessionError(
1961
+ "unsupported_capability",
1962
+ "agent does not support session/list (sessionCapabilities.list not advertised)"
1963
+ );
1964
+ }
1965
+ const id = this.allocId();
1966
+ const params = {};
1967
+ if (cursor !== void 0) params.cursor = cursor;
1968
+ if (cwd !== void 0) params.cwd = cwd;
1969
+ const result = await this.sendRequest(id, "session/list", params);
1970
+ if (isJsonRpcError(result)) {
1971
+ throw new ACPSessionError("prompt_failed", `session/list failed: ${result.message}`, result);
1972
+ }
1973
+ const r = result;
1974
+ return {
1975
+ sessions: r.sessions ?? [],
1976
+ nextCursor: r.nextCursor
1977
+ };
1978
+ }
1979
+ /**
1980
+ * Delete a session from the agent's session list.
1981
+ *
1982
+ * Only works if the agent advertises `sessionCapabilities.delete`.
1983
+ */
1984
+ async deleteSession(sessionId) {
1985
+ if (this.closed) {
1986
+ throw new ACPSessionError("closed", "session is closed");
1987
+ }
1988
+ if (!this.agentCapabilities.sessionCapabilities?.delete) {
1989
+ throw new ACPSessionError(
1990
+ "unsupported_capability",
1991
+ "agent does not support session/delete (sessionCapabilities.delete not advertised)"
1992
+ );
1993
+ }
1994
+ const id = this.allocId();
1995
+ const result = await this.sendRequest(id, "session/delete", { sessionId });
1996
+ if (isJsonRpcError(result)) {
1997
+ throw new ACPSessionError("prompt_failed", `session/delete failed: ${result.message}`, result);
1998
+ }
1999
+ if (this.sessionId === sessionId) {
2000
+ this.sessionId = null;
2001
+ }
2002
+ }
2003
+ /**
2004
+ * Fork a session — create a new session from an existing one.
2005
+ */
2006
+ async forkSession(sourceSessionId, cwd, mcpServers) {
2007
+ if (this.closed) throw new ACPSessionError("closed", "session is closed");
2008
+ const servers = this.filterMcpServers(mcpServers ?? this.opts.mcpServers);
2009
+ const id = this.allocId();
2010
+ const result = await this.sendRequest(id, "session/fork", {
2011
+ sessionId: sourceSessionId,
2012
+ cwd: cwd ?? this.opts.cwd ?? this.opts.projectRoot,
2013
+ ...servers.length > 0 ? { mcpServers: servers } : {}
2014
+ });
2015
+ if (isJsonRpcError(result)) {
2016
+ throw new ACPSessionError("prompt_failed", `session/fork failed: ${result.message}`, result);
2017
+ }
2018
+ const newId = result.sessionId;
2019
+ if (typeof newId !== "string" || !newId) {
2020
+ throw new ACPSessionError("protocol_error", "session/fork returned no sessionId", result);
2021
+ }
2022
+ return newId;
2023
+ }
2024
+ /**
2025
+ * Set the active mode for a session.
2026
+ */
2027
+ async setMode(sessionId, modeId) {
2028
+ if (this.closed) throw new ACPSessionError("closed", "session is closed");
2029
+ const id = this.allocId();
2030
+ const result = await this.sendRequest(id, "session/set_mode", { sessionId, modeId });
2031
+ if (isJsonRpcError(result)) {
2032
+ throw new ACPSessionError("prompt_failed", `session/set_mode failed: ${result.message}`, result);
2033
+ }
2034
+ }
2035
+ /**
2036
+ * Set a configuration option for a session.
2037
+ */
2038
+ async setConfigOption(sessionId, configId, value) {
2039
+ if (this.closed) throw new ACPSessionError("closed", "session is closed");
2040
+ const id = this.allocId();
2041
+ const result = await this.sendRequest(id, "session/set_config_option", {
2042
+ sessionId,
2043
+ configId,
2044
+ value
2045
+ });
2046
+ if (isJsonRpcError(result)) {
2047
+ throw new ACPSessionError("prompt_failed", `session/set_config_option failed: ${result.message}`, result);
2048
+ }
2049
+ }
2050
+ /**
2051
+ * List available providers and the current provider.
2052
+ */
2053
+ async listProviders() {
2054
+ if (this.closed) throw new ACPSessionError("closed", "session is closed");
2055
+ const id = this.allocId();
2056
+ const result = await this.sendRequest(id, "providers/list", {});
2057
+ if (isJsonRpcError(result)) {
2058
+ throw new ACPSessionError("prompt_failed", `providers/list failed: ${result.message}`, result);
2059
+ }
2060
+ const r = result;
2061
+ return { providers: r.providers ?? [], currentProviderId: r.currentProviderId ?? null };
2062
+ }
2063
+ /**
2064
+ * Send an MCP message to the agent for routing.
2065
+ */
2066
+ async mcpMessage(connectionId, message) {
2067
+ if (this.closed) throw new ACPSessionError("closed", "session is closed");
2068
+ const id = this.allocId();
2069
+ const result = await this.sendRequest(id, "mcp/message", { connectionId, message });
2070
+ if (isJsonRpcError(result)) {
2071
+ throw new ACPSessionError("prompt_failed", `mcp/message failed: ${result.message}`, result);
2072
+ }
2073
+ return result;
2074
+ }
2075
+ /**
2076
+ * Set the active provider for the agent.
2077
+ */
2078
+ async setProvider(providerId, config) {
2079
+ if (this.closed) throw new ACPSessionError("closed", "session is closed");
2080
+ const id = this.allocId();
2081
+ const result = await this.sendRequest(id, "providers/set", { providerId, ...config ?? {} });
2082
+ if (isJsonRpcError(result)) {
2083
+ throw new ACPSessionError("prompt_failed", `providers/set failed: ${result.message}`, result);
2084
+ }
2085
+ }
2086
+ /**
2087
+ * Disable the current provider.
2088
+ */
2089
+ async disableProvider() {
2090
+ if (this.closed) throw new ACPSessionError("closed", "session is closed");
2091
+ const id = this.allocId();
2092
+ const result = await this.sendRequest(id, "providers/disable", {});
2093
+ if (isJsonRpcError(result)) {
2094
+ throw new ACPSessionError("prompt_failed", `providers/disable failed: ${result.message}`, result);
2095
+ }
2096
+ }
2097
+ // ──────────────────────────────────────────────────────────────────────
2098
+ // Prompt
2099
+ // ──────────────────────────────────────────────────────────────────────
2100
+ /**
2101
+ * Run one prompt turn. Creates a session if needed, sends the
2102
+ * prompt, streams session/update notifications, and resolves with
2103
+ * the agent's response.
2104
+ *
2105
+ * @param blocks - Content blocks to send. Use `textContent()` for plain
2106
+ * text, or include ImageContent/AudioContent if the agent's
2107
+ * `promptCapabilities` allow it.
2108
+ * @param signal - AbortSignal for cancellation.
2109
+ *
2110
+ * Cancellation: if `signal` aborts mid-prompt, we send
2111
+ * `session/cancel` (a notification per spec) and keep accepting
1219
2112
  * updates until the agent returns with `stopReason: 'cancelled'`.
1220
2113
  * The result is the same shape as a normal turn, with
1221
2114
  * `stopReason === 'cancelled'`.
1222
2115
  */
1223
- async prompt(text, signal) {
2116
+ async prompt(blocks, signal, onProgress) {
1224
2117
  if (this.closed) {
1225
2118
  throw new ACPSessionError("closed", "session is closed");
1226
2119
  }
1227
- if (this.state !== "ready" && this.state !== "done") {
2120
+ if (this.state !== "ready" && this.state !== "authenticated" && this.state !== "done") {
1228
2121
  throw new ACPSessionError("protocol_error", `prompt called in state=${this.state}`);
1229
2122
  }
1230
2123
  if (signal.aborted) {
1231
- return { text: "", stopReason: "cancelled", hasText: false };
2124
+ return emptyRunResult("cancelled");
1232
2125
  }
1233
2126
  if (!this.sessionId) {
1234
2127
  await this.createSession();
1235
2128
  }
1236
2129
  this.resetScratch();
2130
+ this.progressHandler = onProgress ?? null;
1237
2131
  const promptId = this.allocId();
1238
2132
  const turnPromise = this.sendRequest(
1239
2133
  promptId,
1240
2134
  "session/prompt",
1241
2135
  {
1242
2136
  sessionId: this.sessionId,
1243
- prompt: [textContent(text)]
2137
+ prompt: blocks
1244
2138
  },
1245
2139
  this.timeoutMs
1246
2140
  );
1247
2141
  let cancelled = false;
1248
2142
  const onAbort = () => {
1249
2143
  cancelled = true;
1250
- this.transport.send({ method: "session/cancel", params: { sessionId: this.sessionId } }).catch(() => {
2144
+ this.transport.send({
2145
+ jsonrpc: "2.0",
2146
+ method: "session/cancel",
2147
+ params: { sessionId: this.sessionId }
2148
+ }).catch(() => {
1251
2149
  });
1252
2150
  };
1253
2151
  signal.addEventListener("abort", onAbort, { once: true });
@@ -1265,6 +2163,7 @@ var ACPSession = class _ACPSession {
1265
2163
  throw new ACPSessionError("prompt_failed", `session/prompt failed: ${msg}`, err);
1266
2164
  } finally {
1267
2165
  signal.removeEventListener("abort", onAbort);
2166
+ this.progressHandler = null;
1268
2167
  }
1269
2168
  this.state = "done";
1270
2169
  if (isJsonRpcError(response)) {
@@ -1277,14 +2176,18 @@ var ACPSession = class _ACPSession {
1277
2176
  stopReason,
1278
2177
  hasText: finalText.length > 0,
1279
2178
  usage: this.scratch.usage,
1280
- plan: this.scratch.plan
2179
+ plan: this.scratch.plan,
2180
+ toolCalls: [...this.scratch.toolCalls.values()],
2181
+ diffs: this.scratch.diffs,
2182
+ thoughts: this.scratch.thoughts
1281
2183
  };
1282
2184
  }
1283
2185
  async createSession() {
2186
+ const servers = this.filterMcpServers(this.opts.mcpServers);
1284
2187
  const id = this.allocId();
1285
2188
  const result = await this.sendRequest(id, "session/new", {
1286
2189
  cwd: this.opts.cwd ?? this.opts.projectRoot,
1287
- mcpServers: []
2190
+ mcpServers: servers
1288
2191
  });
1289
2192
  if (isJsonRpcError(result)) {
1290
2193
  throw new ACPSessionError(
@@ -1303,12 +2206,40 @@ var ACPSession = class _ACPSession {
1303
2206
  }
1304
2207
  this.sessionId = sessionId;
1305
2208
  }
2209
+ /**
2210
+ * Close the current session gracefully (if the agent supports it).
2211
+ *
2212
+ * Sends `session/close` JSON-RPC request, then clears the local
2213
+ * session id. Best-effort — errors are swallowed so the caller can
2214
+ * always proceed to transport teardown.
2215
+ */
2216
+ async closeSession() {
2217
+ if (!this.sessionId) return;
2218
+ const sid = this.sessionId;
2219
+ this.sessionId = null;
2220
+ if (this.agentCapabilities.sessionCapabilities?.close) {
2221
+ const id = this.allocId();
2222
+ try {
2223
+ await this.sendRequest(id, "session/close", { sessionId: sid }, 1e4);
2224
+ } catch {
2225
+ }
2226
+ }
2227
+ }
2228
+ // ──────────────────────────────────────────────────────────────────────
2229
+ // Lifecycle — close
2230
+ // ──────────────────────────────────────────────────────────────────────
1306
2231
  /** Tear down the session and kill the child process. */
1307
2232
  async close() {
1308
2233
  if (this.closed) return;
1309
2234
  this.closed = true;
1310
2235
  this.state = "closed";
1311
2236
  this.terminalServer.releaseAll();
2237
+ if (this.sessionId && this.agentCapabilities.sessionCapabilities?.close) {
2238
+ try {
2239
+ await this.closeSession();
2240
+ } catch {
2241
+ }
2242
+ }
1312
2243
  for (const [, p] of this.pending) {
1313
2244
  clearTimeout(p.timeoutHandle);
1314
2245
  p.reject(new ACPSessionError("closed", "session was closed"));
@@ -1319,6 +2250,24 @@ var ACPSession = class _ACPSession {
1319
2250
  } catch {
1320
2251
  }
1321
2252
  }
2253
+ // ──────────────────────────────────────────────────────────────────────
2254
+ // Helpers
2255
+ // ──────────────────────────────────────────────────────────────────────
2256
+ /**
2257
+ * Filter MCP servers according to agent capabilities.
2258
+ * - Stdio servers are always included.
2259
+ * - HTTP servers are only included if agent supports mcpCapabilities.http.
2260
+ * - SSE servers are only included if agent supports mcpCapabilities.sse.
2261
+ */
2262
+ filterMcpServers(servers) {
2263
+ if (!servers || servers.length === 0) return [];
2264
+ const mcpCaps = this.agentCapabilities.mcpCapabilities ?? {};
2265
+ return servers.filter((s) => {
2266
+ if ("type" in s && s.type === "http") return mcpCaps.http === true;
2267
+ if ("type" in s && s.type === "sse") return mcpCaps.sse === true;
2268
+ return true;
2269
+ });
2270
+ }
1322
2271
  // ────────────────────────────────────────────────────────────────────
1323
2272
  // Wire layer
1324
2273
  // ────────────────────────────────────────────────────────────────────
@@ -1352,6 +2301,28 @@ var ACPSession = class _ACPSession {
1352
2301
  });
1353
2302
  });
1354
2303
  }
2304
+ /**
2305
+ * Send a JSON-RPC 2.0 success response to an agent-initiated request.
2306
+ *
2307
+ * Per JSON-RPC 2.0 (and the official ACP SDK's message router) a Response
2308
+ * object MUST carry `jsonrpc: "2.0"` and MUST NOT carry a `method` field —
2309
+ * the SDK classifies any object with a `method` key as a Request and drops
2310
+ * it as a response, so an agent's `fs/*`, `terminal/*`, or
2311
+ * `session/request_permission` callback would hang forever. The legacy
2312
+ * `ACPMessage` type predates v1 (requires `method`, lacks `jsonrpc`), so we
2313
+ * build the correct wire object and cast at the boundary.
2314
+ */
2315
+ sendResult(id, result) {
2316
+ return this.transport.send({ jsonrpc: "2.0", id, result });
2317
+ }
2318
+ /** Send a JSON-RPC 2.0 error response (no `method` field, per spec). */
2319
+ sendErrorResponse(id, code, message) {
2320
+ return this.transport.send({
2321
+ jsonrpc: "2.0",
2322
+ id,
2323
+ error: { code, message }
2324
+ });
2325
+ }
1355
2326
  handleMessage(msg) {
1356
2327
  if (msg.id !== void 0 && (msg.result !== void 0 || msg.error !== void 0)) {
1357
2328
  const pending = this.pending.get(msg.id);
@@ -1381,6 +2352,23 @@ var ACPSession = class _ACPSession {
1381
2352
  void this.handleTerminalRequest(msg);
1382
2353
  return;
1383
2354
  }
2355
+ if (msg.method === "mcp/connect" || msg.method === "mcp/message" || msg.method === "mcp/disconnect") {
2356
+ if (msg.id !== void 0) {
2357
+ this.sendResult(msg.id, {}).catch(() => {
2358
+ });
2359
+ }
2360
+ return;
2361
+ }
2362
+ if (msg.method === "elicitation/create" || msg.method === "elicitation/complete") {
2363
+ if (msg.id !== void 0) {
2364
+ this.sendResult(msg.id, {}).catch(() => {
2365
+ });
2366
+ }
2367
+ return;
2368
+ }
2369
+ if (msg.method === "$/cancel_request") {
2370
+ return;
2371
+ }
1384
2372
  if (msg.method) {
1385
2373
  console.warn(`[acp-session] unhandled method: ${msg.method}`);
1386
2374
  }
@@ -1389,31 +2377,44 @@ var ACPSession = class _ACPSession {
1389
2377
  const update = msg.params?.update;
1390
2378
  if (typeof update !== "object" || update === null) return;
1391
2379
  const u = update;
2380
+ this.emitProgress({ type: "raw", update: u });
1392
2381
  switch (u.sessionUpdate) {
1393
2382
  case "agent_message_chunk": {
1394
2383
  const text = extractText(u.content);
1395
- if (text) this.accumulatedText(text);
2384
+ if (text) {
2385
+ this.scratch.text += text;
2386
+ this.emitProgress({ type: "message", text });
2387
+ }
1396
2388
  return;
1397
2389
  }
1398
- case "thought_chunk":
2390
+ case "thought_chunk": {
2391
+ const text = extractText(u.content);
2392
+ if (text) {
2393
+ this.scratch.thoughts += text;
2394
+ this.emitProgress({ type: "thought", text });
2395
+ }
1399
2396
  return;
2397
+ }
1400
2398
  case "tool_call":
1401
- case "tool_call_update":
2399
+ case "tool_call_update": {
2400
+ this.captureToolCall(u, u.sessionUpdate === "tool_call");
1402
2401
  return;
2402
+ }
1403
2403
  case "plan":
1404
2404
  if (Array.isArray(u.entries)) {
1405
- this.accumulatedPlan(u.entries);
2405
+ this.scratch.plan = u.entries;
2406
+ this.emitProgress({ type: "plan", entries: u.entries });
1406
2407
  }
1407
2408
  return;
1408
2409
  case "usage_update":
1409
2410
  if (typeof u.used === "number" && typeof u.size === "number") {
1410
- this.accumulatedUsage({
2411
+ const usage = {
1411
2412
  used: u.used,
1412
2413
  size: u.size,
1413
- ...typeof u.cost === "object" && u.cost !== null ? {
1414
- cost: u.cost
1415
- } : {}
1416
- });
2414
+ ...typeof u.cost === "object" && u.cost !== null ? { cost: u.cost } : {}
2415
+ };
2416
+ this.scratch.usage = usage;
2417
+ this.emitProgress({ type: "usage", usage });
1417
2418
  }
1418
2419
  return;
1419
2420
  case "available_commands_update":
@@ -1421,27 +2422,62 @@ var ACPSession = class _ACPSession {
1421
2422
  case "config_option_update":
1422
2423
  case "session_info_update":
1423
2424
  case "user_message_chunk":
2425
+ case "next_edit_suggestions":
2426
+ case "elicitation":
1424
2427
  return;
1425
2428
  default:
1426
- console.warn(`[acp-session] unhandled sessionUpdate: ${u.sessionUpdate}`);
1427
2429
  return;
1428
2430
  }
1429
2431
  }
1430
- // Per-prompt scratch state. Reset at the start of each prompt() and
1431
- // read at the end to assemble the ACPSessionRunResult. The stream
1432
- // pump writes to it via the three `accumulated*` helpers below.
1433
- scratch = { text: "" };
1434
- accumulatedText(chunk) {
1435
- this.scratch.text += chunk;
1436
- }
1437
- accumulatedPlan(entries) {
1438
- this.scratch.plan = entries;
2432
+ /**
2433
+ * Fold a `tool_call` / `tool_call_update` notification into the scratch
2434
+ * tool-call map (deduped by toolCallId), extract any `diff` content into
2435
+ * the diffs list, and emit live progress.
2436
+ */
2437
+ captureToolCall(u, isNew) {
2438
+ const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : "";
2439
+ if (!toolCallId) return;
2440
+ const prev = this.scratch.toolCalls.get(toolCallId);
2441
+ const record = {
2442
+ toolCallId,
2443
+ title: typeof u.title === "string" ? u.title : prev?.title ?? toolCallId,
2444
+ kind: typeof u.kind === "string" ? u.kind : prev?.kind,
2445
+ status: typeof u.status === "string" ? u.status : prev?.status ?? (isNew ? "pending" : "in_progress"),
2446
+ rawInput: isRecord(u.rawInput) ? u.rawInput : prev?.rawInput,
2447
+ rawOutput: isRecord(u.rawOutput) ? u.rawOutput : prev?.rawOutput
2448
+ };
2449
+ this.scratch.toolCalls.set(toolCallId, record);
2450
+ if (Array.isArray(u.content)) {
2451
+ for (const c of u.content) {
2452
+ if (c && typeof c === "object" && c.type === "diff") {
2453
+ const diff = {
2454
+ path: c.path,
2455
+ oldText: c.oldText,
2456
+ newText: c.newText
2457
+ };
2458
+ this.scratch.diffs.push(diff);
2459
+ this.emitProgress({ type: "diff", diff });
2460
+ }
2461
+ }
2462
+ }
2463
+ this.emitProgress({
2464
+ type: isNew ? "tool_call" : "tool_call_update",
2465
+ toolCall: record
2466
+ });
1439
2467
  }
1440
- accumulatedUsage(u) {
1441
- this.scratch.usage = u;
2468
+ emitProgress(event) {
2469
+ if (!this.progressHandler) return;
2470
+ try {
2471
+ this.progressHandler(event);
2472
+ } catch {
2473
+ }
1442
2474
  }
2475
+ /** Live progress handler installed for the duration of a `prompt()` turn. */
2476
+ progressHandler = null;
2477
+ // Per-prompt scratch state
2478
+ scratch = { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
1443
2479
  resetScratch() {
1444
- this.scratch = { text: "" };
2480
+ this.scratch = { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
1445
2481
  }
1446
2482
  async handlePermissionRequest(msg) {
1447
2483
  const id = msg.id;
@@ -1450,11 +2486,7 @@ var ACPSession = class _ACPSession {
1450
2486
  const toolCall = params?.toolCall;
1451
2487
  const options = Array.isArray(params?.options) ? params.options : [];
1452
2488
  if (!toolCall) {
1453
- await this.transport.send({
1454
- id,
1455
- method: "session/request_permission",
1456
- error: { code: -32602, message: "toolCall is required" }
1457
- });
2489
+ await this.sendErrorResponse(id, -32602, "toolCall is required");
1458
2490
  return;
1459
2491
  }
1460
2492
  const policyAbort = new AbortController();
@@ -1463,22 +2495,14 @@ var ACPSession = class _ACPSession {
1463
2495
  options,
1464
2496
  signal: policyAbort.signal
1465
2497
  });
1466
- await this.transport.send({
1467
- id,
1468
- method: "session/request_permission",
1469
- result: { outcome }
1470
- });
2498
+ await this.sendResult(id, { outcome });
1471
2499
  }
1472
2500
  async handleFsRequest(msg) {
1473
2501
  const id = msg.id;
1474
2502
  if (id === void 0) return;
1475
2503
  const params = msg.params;
1476
2504
  if (!params?.path) {
1477
- await this.transport.send({
1478
- id,
1479
- method: msg.method,
1480
- error: { code: -32602, message: "path is required" }
1481
- });
2505
+ await this.sendErrorResponse(id, -32602, "path is required");
1482
2506
  return;
1483
2507
  }
1484
2508
  try {
@@ -1487,19 +2511,19 @@ var ACPSession = class _ACPSession {
1487
2511
  sessionId: params.sessionId ?? "",
1488
2512
  path: params.path
1489
2513
  });
1490
- await this.transport.send({ id, method: msg.method, result });
2514
+ await this.sendResult(id, result);
1491
2515
  } else {
1492
2516
  await this.fileServer.writeTextFile({
1493
2517
  sessionId: params.sessionId ?? "",
1494
2518
  path: params.path,
1495
2519
  content: params.content ?? ""
1496
2520
  });
1497
- await this.transport.send({ id, method: msg.method, result: {} });
2521
+ await this.sendResult(id, {});
1498
2522
  }
1499
2523
  } catch (err) {
1500
2524
  const code = err instanceof FsError ? -32602 : -32603;
1501
2525
  const message = err instanceof Error ? err.message : String(err);
1502
- await this.transport.send({ id, method: msg.method, error: { code, message } });
2526
+ await this.sendErrorResponse(id, code, message);
1503
2527
  }
1504
2528
  }
1505
2529
  async handleTerminalRequest(msg) {
@@ -1524,192 +2548,73 @@ var ACPSession = class _ACPSession {
1524
2548
  createOpts.outputByteLimit = params.outputByteLimit;
1525
2549
  }
1526
2550
  const result = this.terminalServer.create(createOpts);
1527
- await this.transport.send({ id, method: msg.method, result });
2551
+ await this.sendResult(id, result);
1528
2552
  return;
1529
2553
  }
1530
2554
  case "terminal/output": {
1531
2555
  const terminalId = String(params.terminalId ?? "");
1532
2556
  const out = this.terminalServer.output(terminalId);
1533
- await this.transport.send({ id, method: msg.method, result: out });
2557
+ await this.sendResult(id, out);
1534
2558
  return;
1535
2559
  }
1536
2560
  case "terminal/wait_for_exit": {
1537
2561
  const terminalId = String(params.terminalId ?? "");
1538
2562
  const exit = await this.terminalServer.waitForExit(terminalId);
1539
- await this.transport.send({ id, method: msg.method, result: exit });
2563
+ await this.sendResult(id, exit);
1540
2564
  return;
1541
2565
  }
1542
2566
  case "terminal/kill": {
1543
2567
  const terminalId = String(params.terminalId ?? "");
1544
2568
  this.terminalServer.kill(terminalId);
1545
- await this.transport.send({ id, method: msg.method, result: {} });
2569
+ await this.sendResult(id, {});
1546
2570
  return;
1547
2571
  }
1548
2572
  case "terminal/release": {
1549
2573
  const terminalId = String(params.terminalId ?? "");
1550
2574
  this.terminalServer.release(terminalId);
1551
- await this.transport.send({ id, method: msg.method, result: {} });
2575
+ await this.sendResult(id, {});
1552
2576
  return;
1553
2577
  }
1554
2578
  default:
1555
- await this.transport.send({
1556
- id,
1557
- method: msg.method,
1558
- error: { code: -32601, message: `unknown method: ${msg.method}` }
1559
- });
2579
+ await this.sendErrorResponse(id, -32601, `unknown method: ${msg.method}`);
1560
2580
  }
1561
2581
  } catch (err) {
1562
2582
  const message = err instanceof Error ? err.message : String(err);
1563
- await this.transport.send({
1564
- id,
1565
- method: msg.method,
1566
- error: { code: -32603, message }
1567
- });
2583
+ await this.sendErrorResponse(id, -32603, message);
1568
2584
  }
1569
2585
  }
1570
2586
  };
1571
2587
  function textContent(text) {
1572
2588
  return { type: "text", text };
1573
2589
  }
2590
+ function imageContent(mimeType, data) {
2591
+ return { type: "image", mimeType, data };
2592
+ }
2593
+ function audioContent(mimeType, data) {
2594
+ return { type: "audio", mimeType, data };
2595
+ }
1574
2596
  function extractText(block) {
1575
2597
  if (typeof block !== "object" || block === null) return "";
1576
2598
  const b = block;
1577
2599
  if (b.type === "text" && typeof b.text === "string") return b.text;
1578
- return "";
1579
- }
1580
-
1581
- // src/integration/acp-subagent-runner.ts
1582
- var ACP_AGENT_COMMANDS = {
1583
- cline: {
1584
- command: "npx",
1585
- args: ["-y", "@agentify/cline"],
1586
- role: "cline"
1587
- },
1588
- "gemini-cli": {
1589
- command: "gemini",
1590
- role: "gemini-cli"
1591
- },
1592
- copilot: {
1593
- command: "gh",
1594
- args: ["copilot", "agent"],
1595
- role: "copilot"
1596
- },
1597
- openhands: {
1598
- command: "openhands",
1599
- role: "openhands"
1600
- },
1601
- goose: {
1602
- command: "goose",
1603
- role: "goose"
2600
+ if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
2601
+ return b.resource.text;
1604
2602
  }
1605
- };
1606
- async function makeACPSubagentRunner(options) {
1607
- const { runner, stop } = await makeACPSubagentRunnerWithStop(options);
1608
- const wrappedRunner = async (task, ctx) => {
1609
- try {
1610
- return await runner(task, ctx);
1611
- } finally {
1612
- stop();
1613
- }
1614
- };
1615
- return wrappedRunner;
2603
+ return "";
1616
2604
  }
1617
- async function makeACPSubagentRunnerWithStop(options) {
1618
- const projectRoot = options.projectRoot ?? options.cwd ?? process.cwd();
1619
- const timeoutMs = options.timeoutMs ?? 5 * 6e4;
1620
- const runner = async (task, ctx) => {
1621
- let session = null;
1622
- try {
1623
- session = await ACPSession.start({
1624
- command: options.command,
1625
- ...options.args !== void 0 ? { args: options.args } : {},
1626
- ...options.env !== void 0 ? { env: options.env } : {},
1627
- ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
1628
- projectRoot,
1629
- timeoutMs,
1630
- role: options.role
1631
- });
1632
- } catch (err) {
1633
- throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
1634
- }
1635
- try {
1636
- const result = await session.prompt(task.description, ctx.signal);
1637
- return {
1638
- result: result.text,
1639
- iterations: 1,
1640
- toolCalls: 0
1641
- };
1642
- } catch (err) {
1643
- if (err instanceof ACPSessionError && err.kind === "aborted") {
1644
- throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
1645
- }
1646
- throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
1647
- } finally {
1648
- try {
1649
- await session.close();
1650
- } catch {
1651
- }
1652
- }
1653
- };
1654
- const stop = () => {
1655
- };
1656
- return { runner, stop };
2605
+ function isRecord(v) {
2606
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1657
2607
  }
1658
- function acpErrorToSubagentError(err, subagentId) {
1659
- if (err instanceof ACPSessionError) {
1660
- const kind = mapACPKind(err.kind);
1661
- return {
1662
- kind,
1663
- message: `${subagentId}: ${err.message}`,
1664
- retryable: isRetryable(kind),
1665
- cause: {
1666
- name: err.name,
1667
- message: err.message,
1668
- ...err.stack !== void 0 ? { stack: err.stack } : {}
1669
- }
1670
- };
1671
- }
1672
- const message = err instanceof Error ? err.message : String(err);
2608
+ function emptyRunResult(stopReason) {
1673
2609
  return {
1674
- kind: "bridge_failed",
1675
- message: `${subagentId}: ${message}`,
1676
- retryable: false,
1677
- cause: {
1678
- name: err instanceof Error ? err.name : "Error",
1679
- message,
1680
- ...err instanceof Error && err.stack !== void 0 ? { stack: err.stack } : {}
1681
- }
2610
+ text: "",
2611
+ stopReason,
2612
+ hasText: false,
2613
+ toolCalls: [],
2614
+ diffs: [],
2615
+ thoughts: ""
1682
2616
  };
1683
2617
  }
1684
- function mapACPKind(acpKind) {
1685
- switch (acpKind) {
1686
- case "spawn_failed":
1687
- case "init_failed":
1688
- case "session_create_failed":
1689
- case "agent_died":
1690
- case "protocol_error":
1691
- return "bridge_failed";
1692
- case "prompt_failed":
1693
- return "tool_failed";
1694
- case "aborted":
1695
- return "aborted_by_parent";
1696
- case "closed":
1697
- case "unsupported_capability":
1698
- return "unknown";
1699
- }
1700
- }
1701
- function isRetryable(kind) {
1702
- switch (kind) {
1703
- case "provider_5xx":
1704
- case "provider_rate_limit":
1705
- case "provider_timeout":
1706
- case "tool_threw":
1707
- case "budget_timeout":
1708
- return true;
1709
- default:
1710
- return false;
1711
- }
1712
- }
1713
2618
 
1714
2619
  // src/registry/agents.catalog.ts
1715
2620
  var AGENTS_CATALOG = [
@@ -1719,9 +2624,12 @@ var AGENTS_CATALOG = [
1719
2624
  displayName: "Claude Code",
1720
2625
  vendor: "anthropic",
1721
2626
  probe: { command: "claude", args: ["--version"] },
1722
- // Native ACP entry is gated behind the SDK adapter in early releases;
1723
- // see https://agentclientprotocol.com/get-started/agents
1724
- acp: { command: "claude", args: [] },
2627
+ // Claude Code does not speak stdio ACP from the bare `claude` binary
2628
+ // it drops into its interactive TUI. The official ACP adapter
2629
+ // (`@agentclientprotocol/claude-agent-acp`, registry id `claude-acp`)
2630
+ // wraps the logged-in Claude Code CLI and translates ACP ↔ Claude Code.
2631
+ // Verify with `/acp probe claude-code`; override via `config.acp.agents`.
2632
+ acp: { command: "npx", args: ["-y", "@agentclientprotocol/claude-agent-acp"] },
1725
2633
  supports: {
1726
2634
  loadSession: true,
1727
2635
  promptImages: true,
@@ -1737,7 +2645,10 @@ var AGENTS_CATALOG = [
1737
2645
  displayName: "Gemini CLI",
1738
2646
  vendor: "google",
1739
2647
  probe: { command: "gemini", args: ["--version"] },
1740
- acp: { command: "gemini", args: [] },
2648
+ // Gemini CLI (the @google/gemini-cli package, registry id `gemini`)
2649
+ // speaks ACP behind `--acp`. We invoke the locally-installed binary so it
2650
+ // uses the user's existing login. Confirm with `/acp probe gemini-cli`.
2651
+ acp: { command: "gemini", args: ["--acp"] },
1741
2652
  supports: {
1742
2653
  loadSession: true,
1743
2654
  promptImages: true,
@@ -1753,7 +2664,10 @@ var AGENTS_CATALOG = [
1753
2664
  displayName: "Codex CLI",
1754
2665
  vendor: "openai",
1755
2666
  probe: { command: "codex", args: ["--version"] },
1756
- acp: { command: "codex", args: [] },
2667
+ // Bare `codex` has no stdio-ACP entry; the official adapter
2668
+ // (`@agentclientprotocol/codex-acp`, registry id `codex-acp`) wraps the
2669
+ // logged-in Codex CLI. Confirm with `/acp probe codex-cli`.
2670
+ acp: { command: "npx", args: ["-y", "@agentclientprotocol/codex-acp"] },
1757
2671
  supports: {
1758
2672
  loadSession: false,
1759
2673
  promptImages: false,
@@ -1769,7 +2683,9 @@ var AGENTS_CATALOG = [
1769
2683
  displayName: "GitHub Copilot CLI",
1770
2684
  vendor: "github",
1771
2685
  probe: { command: "gh", args: ["copilot", "--help"] },
1772
- acp: { command: "gh", args: ["copilot"] },
2686
+ // ACP is in the standalone @github/copilot CLI (registry id
2687
+ // `github-copilot-cli`), not the `gh copilot` extension. Use the package.
2688
+ acp: { command: "npx", args: ["-y", "@github/copilot", "--acp"] },
1773
2689
  supports: {
1774
2690
  loadSession: false,
1775
2691
  promptImages: false,
@@ -1785,9 +2701,10 @@ var AGENTS_CATALOG = [
1785
2701
  displayName: "Cline",
1786
2702
  vendor: "community",
1787
2703
  probe: { command: "npx", args: ["--version"] },
2704
+ // Registry id `cline`: the `cline` npm package speaks ACP behind `--acp`.
1788
2705
  acp: {
1789
2706
  command: "npx",
1790
- args: ["-y", "@agentify/cline"]
2707
+ args: ["-y", "cline", "--acp"]
1791
2708
  },
1792
2709
  supports: {
1793
2710
  loadSession: true,
@@ -1803,7 +2720,7 @@ var AGENTS_CATALOG = [
1803
2720
  displayName: "Goose",
1804
2721
  vendor: "community",
1805
2722
  probe: { command: "goose", args: ["--version"] },
1806
- acp: { command: "goose", args: [] },
2723
+ acp: { command: "goose", args: ["acp"] },
1807
2724
  supports: {
1808
2725
  loadSession: true,
1809
2726
  promptImages: true,
@@ -1835,7 +2752,8 @@ var AGENTS_CATALOG = [
1835
2752
  displayName: "Qwen Code",
1836
2753
  vendor: "community",
1837
2754
  probe: { command: "qwen", args: ["--version"] },
1838
- acp: { command: "qwen", args: [] },
2755
+ // Qwen Code (the @qwen-code/qwen-code package) speaks ACP behind `--acp`.
2756
+ acp: { command: "qwen", args: ["--acp"] },
1839
2757
  supports: {
1840
2758
  loadSession: false,
1841
2759
  promptImages: false,
@@ -1865,7 +2783,8 @@ var AGENTS_CATALOG = [
1865
2783
  displayName: "OpenCode",
1866
2784
  vendor: "community",
1867
2785
  probe: { command: "opencode", args: ["--version"] },
1868
- acp: { command: "opencode", args: [] },
2786
+ // OpenCode speaks ACP via its `acp` subcommand (registry id `opencode`).
2787
+ acp: { command: "opencode", args: ["acp"] },
1869
2788
  supports: {
1870
2789
  loadSession: true,
1871
2790
  promptImages: true,
@@ -1895,7 +2814,8 @@ var AGENTS_CATALOG = [
1895
2814
  displayName: "Cursor",
1896
2815
  vendor: "community",
1897
2816
  probe: { command: "cursor", args: ["--version"] },
1898
- acp: { command: "cursor", args: [] },
2817
+ // Cursor's ACP entry is the `cursor-agent acp` binary (registry id `cursor`).
2818
+ acp: { command: "cursor-agent", args: ["acp"] },
1899
2819
  supports: {
1900
2820
  loadSession: true,
1901
2821
  promptImages: true,
@@ -1909,8 +2829,369 @@ var AGENTS_CATALOG = [
1909
2829
  function findAgentDescriptor(id) {
1910
2830
  return AGENTS_CATALOG.find((a) => a.id === id);
1911
2831
  }
2832
+
2833
+ // src/integration/acp-subagent-runner.ts
2834
+ var ACP_AGENT_COMMANDS = {
2835
+ cline: {
2836
+ command: "npx",
2837
+ args: ["-y", "@agentify/cline"],
2838
+ role: "cline"
2839
+ },
2840
+ "gemini-cli": {
2841
+ command: "gemini",
2842
+ role: "gemini-cli"
2843
+ },
2844
+ copilot: {
2845
+ command: "gh",
2846
+ args: ["copilot", "agent"],
2847
+ role: "copilot"
2848
+ },
2849
+ openhands: {
2850
+ command: "openhands",
2851
+ role: "openhands"
2852
+ },
2853
+ goose: {
2854
+ command: "goose",
2855
+ role: "goose"
2856
+ }
2857
+ };
2858
+ async function makeACPSubagentRunner(options) {
2859
+ const { runner, stop } = await makeACPSubagentRunnerWithStop(options);
2860
+ const wrappedRunner = async (task, ctx) => {
2861
+ try {
2862
+ return await runner(task, ctx);
2863
+ } finally {
2864
+ stop();
2865
+ }
2866
+ };
2867
+ return wrappedRunner;
2868
+ }
2869
+ async function makeACPSubagentRunnerWithStop(options) {
2870
+ const projectRoot = options.projectRoot ?? options.cwd ?? process.cwd();
2871
+ const timeoutMs = options.timeoutMs ?? 5 * 6e4;
2872
+ const persistent = options.persistent === true;
2873
+ let shared = null;
2874
+ const startSession = async () => {
2875
+ return ACPSession.start({
2876
+ command: options.command,
2877
+ ...options.args !== void 0 ? { args: options.args } : {},
2878
+ ...options.env !== void 0 ? { env: options.env } : {},
2879
+ ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
2880
+ projectRoot,
2881
+ timeoutMs,
2882
+ role: options.role,
2883
+ ...options.permissionPolicy !== void 0 ? { permissionPolicy: options.permissionPolicy } : {},
2884
+ ...options.mcpServers !== void 0 ? { mcpServers: options.mcpServers } : {}
2885
+ });
2886
+ };
2887
+ const runner = async (task, ctx) => {
2888
+ let session;
2889
+ const reuse = persistent && shared !== null;
2890
+ try {
2891
+ session = reuse ? shared : await startSession();
2892
+ if (persistent) shared = session;
2893
+ } catch (err) {
2894
+ throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
2895
+ }
2896
+ const onProgress = (event) => {
2897
+ try {
2898
+ ctx.budget.markActivity();
2899
+ } catch {
2900
+ }
2901
+ options.onProgress?.(event);
2902
+ };
2903
+ try {
2904
+ const result = await session.prompt(
2905
+ [textContent(task.description)],
2906
+ ctx.signal,
2907
+ onProgress
2908
+ );
2909
+ return {
2910
+ result: result.text,
2911
+ iterations: 1,
2912
+ toolCalls: result.toolCalls.length
2913
+ };
2914
+ } catch (err) {
2915
+ throw acpErrorToSubagentError(err, options.role ?? "acp-subagent");
2916
+ } finally {
2917
+ if (!persistent) {
2918
+ try {
2919
+ await session.close();
2920
+ } catch {
2921
+ }
2922
+ }
2923
+ }
2924
+ };
2925
+ const stop = async () => {
2926
+ if (shared) {
2927
+ const s = shared;
2928
+ shared = null;
2929
+ try {
2930
+ await s.close();
2931
+ } catch {
2932
+ }
2933
+ }
2934
+ };
2935
+ return { runner, stop };
2936
+ }
2937
+ function acpErrorToSubagentError(err, subagentId) {
2938
+ if (err instanceof ACPSessionError) {
2939
+ const kind = mapACPKind(err.kind);
2940
+ return {
2941
+ kind,
2942
+ message: `${subagentId}: ${err.message}`,
2943
+ retryable: isRetryable(kind),
2944
+ cause: {
2945
+ name: err.name,
2946
+ message: err.message,
2947
+ ...err.stack !== void 0 ? { stack: err.stack } : {}
2948
+ }
2949
+ };
2950
+ }
2951
+ const message = err instanceof Error ? err.message : String(err);
2952
+ return {
2953
+ kind: "bridge_failed",
2954
+ message: `${subagentId}: ${message}`,
2955
+ retryable: false,
2956
+ cause: {
2957
+ name: err instanceof Error ? err.name : "Error",
2958
+ message,
2959
+ ...err instanceof Error && err.stack !== void 0 ? { stack: err.stack } : {}
2960
+ }
2961
+ };
2962
+ }
2963
+ function mapACPKind(acpKind) {
2964
+ switch (acpKind) {
2965
+ case "spawn_failed":
2966
+ case "init_failed":
2967
+ case "session_create_failed":
2968
+ case "agent_died":
2969
+ case "protocol_error":
2970
+ return "bridge_failed";
2971
+ case "prompt_failed":
2972
+ return "tool_failed";
2973
+ case "auth_failed":
2974
+ case "logout_failed":
2975
+ return "bridge_failed";
2976
+ case "aborted":
2977
+ return "aborted_by_parent";
2978
+ case "closed":
2979
+ case "unsupported_capability":
2980
+ return "unknown";
2981
+ }
2982
+ }
2983
+ function isRetryable(kind) {
2984
+ switch (kind) {
2985
+ case "provider_5xx":
2986
+ case "provider_rate_limit":
2987
+ case "provider_timeout":
2988
+ case "tool_threw":
2989
+ case "budget_timeout":
2990
+ return true;
2991
+ default:
2992
+ return false;
2993
+ }
2994
+ }
2995
+ var REGISTRY_ID_ALIASES = {
2996
+ "claude-code": "claude-acp",
2997
+ "gemini-cli": "gemini",
2998
+ "codex-cli": "codex-acp",
2999
+ copilot: "github-copilot-cli"
3000
+ };
3001
+ function resolveAcpAgentCommand(id, overrides, live) {
3002
+ const ov = overrides?.[id];
3003
+ if (ov && typeof ov.command === "string" && ov.command.length > 0) {
3004
+ const out = {
3005
+ command: ov.command,
3006
+ args: [...ov.args ?? []],
3007
+ role: id
3008
+ };
3009
+ if (ov.env) out.env = ov.env;
3010
+ return out;
3011
+ }
3012
+ const desc = findAgentDescriptor(id);
3013
+ if (desc) {
3014
+ const out = {
3015
+ command: desc.acp.command,
3016
+ args: [...desc.acp.args ?? []],
3017
+ role: id
3018
+ };
3019
+ if (desc.acp.env) out.env = desc.acp.env;
3020
+ return out;
3021
+ }
3022
+ const liveEntry = live?.[id] ?? live?.[REGISTRY_ID_ALIASES[id] ?? ""];
3023
+ if (liveEntry && typeof liveEntry.command === "string" && liveEntry.command.length > 0) {
3024
+ const out = {
3025
+ command: liveEntry.command,
3026
+ args: [...liveEntry.args ?? []],
3027
+ role: id
3028
+ };
3029
+ if (liveEntry.env) out.env = liveEntry.env;
3030
+ return out;
3031
+ }
3032
+ const fromMap = ACP_AGENT_COMMANDS[id];
3033
+ if (fromMap) return fromMap;
3034
+ return null;
3035
+ }
3036
+ async function runOneAcpTask(opts) {
3037
+ const role = opts.role ?? "acp";
3038
+ const timeoutMs = opts.timeoutMs ?? 5 * 6e4;
3039
+ const { runner, stop } = await makeACPSubagentRunnerWithStop({
3040
+ command: opts.command,
3041
+ ...opts.args !== void 0 ? { args: opts.args } : {},
3042
+ ...opts.env !== void 0 ? { env: opts.env } : {},
3043
+ ...opts.cwd !== void 0 ? { cwd: opts.cwd } : {},
3044
+ ...opts.projectRoot !== void 0 ? { projectRoot: opts.projectRoot } : {},
3045
+ role,
3046
+ timeoutMs,
3047
+ ...opts.onProgress !== void 0 ? { onProgress: opts.onProgress } : {},
3048
+ ...opts.permissionPolicy !== void 0 ? { permissionPolicy: opts.permissionPolicy } : {}
3049
+ });
3050
+ try {
3051
+ const budget = new SubagentBudget({
3052
+ timeoutMs,
3053
+ maxIterations: 2e3,
3054
+ maxToolCalls: 5e3
3055
+ });
3056
+ budget.start();
3057
+ const ctx = {
3058
+ subagentId: role,
3059
+ config: { id: role, name: role, role, provider: "acp", prompt: "" },
3060
+ budget,
3061
+ signal: opts.signal ?? new AbortController().signal,
3062
+ bridge: null
3063
+ };
3064
+ const result = await runner({ id: `acp-${role}`, description: opts.task }, ctx);
3065
+ return {
3066
+ result: result.result == null ? "" : String(result.result),
3067
+ iterations: result.iterations,
3068
+ toolCalls: result.toolCalls
3069
+ };
3070
+ } finally {
3071
+ try {
3072
+ await stop();
3073
+ } catch {
3074
+ }
3075
+ }
3076
+ }
3077
+ async function probeAcpAgent(idOrCmd, opts) {
3078
+ const id = typeof idOrCmd === "string" ? idOrCmd : idOrCmd.role ?? idOrCmd.command;
3079
+ const cmd = typeof idOrCmd === "string" ? resolveAcpAgentCommand(idOrCmd, opts?.overrides, opts?.live) : idOrCmd;
3080
+ if (!cmd) return { id, ok: false, ms: 0, error: "unknown agent" };
3081
+ const timeoutMs = opts?.timeoutMs ?? 8e3;
3082
+ const startedAt = Date.now();
3083
+ let session = null;
3084
+ try {
3085
+ session = await ACPSession.start({
3086
+ command: cmd.command,
3087
+ ...cmd.args !== void 0 ? { args: cmd.args } : {},
3088
+ ...cmd.env !== void 0 ? { env: cmd.env } : {},
3089
+ projectRoot: opts?.projectRoot ?? process.cwd(),
3090
+ // Bounds the `initialize` request: a CLI that spawns but never answers
3091
+ // the handshake fails after this instead of blocking.
3092
+ timeoutMs
3093
+ });
3094
+ const info = session.getAgentInfo();
3095
+ return {
3096
+ id,
3097
+ ok: true,
3098
+ ms: Date.now() - startedAt,
3099
+ ...info ? { agentInfo: info } : {}
3100
+ };
3101
+ } catch (err) {
3102
+ return {
3103
+ id,
3104
+ ok: false,
3105
+ ms: Date.now() - startedAt,
3106
+ error: err instanceof Error ? err.message : String(err)
3107
+ };
3108
+ } finally {
3109
+ if (session) {
3110
+ try {
3111
+ await session.close();
3112
+ } catch {
3113
+ }
3114
+ }
3115
+ }
3116
+ }
3117
+ async function probeAcpAgents(opts) {
3118
+ const localTimeout = opts.timeoutMs ?? 2e4;
3119
+ const pkgTimeout = opts.packageTimeoutMs ?? 9e4;
3120
+ const ids = opts.agentIds;
3121
+ const byId = /* @__PURE__ */ new Map();
3122
+ const local = [];
3123
+ const pkg = [];
3124
+ const cmds = /* @__PURE__ */ new Map();
3125
+ for (const id of ids) {
3126
+ const cmd = opts.resolveCmd(id);
3127
+ cmds.set(id, cmd);
3128
+ if (!cmd) continue;
3129
+ if (cmd.command === "npx" || cmd.command === "uvx") pkg.push(id);
3130
+ else local.push(id);
3131
+ }
3132
+ const runPhase = async (phaseIds, concurrency, timeoutMs) => {
3133
+ let next = 0;
3134
+ const workerCount = Math.min(Math.max(1, concurrency), Math.max(1, phaseIds.length));
3135
+ const workers = [];
3136
+ for (let w = 0; w < workerCount; w++) {
3137
+ workers.push(
3138
+ (async () => {
3139
+ while (true) {
3140
+ const current = next++;
3141
+ if (current >= phaseIds.length) return;
3142
+ const id = phaseIds[current];
3143
+ if (opts.signal?.aborted) {
3144
+ byId.set(id, { id, ok: false, ms: 0, error: "aborted" });
3145
+ continue;
3146
+ }
3147
+ const cmd = cmds.get(id);
3148
+ const r = await probeAcpAgent(cmd, {
3149
+ timeoutMs,
3150
+ ...opts.projectRoot !== void 0 ? { projectRoot: opts.projectRoot } : {}
3151
+ });
3152
+ r.id = id;
3153
+ byId.set(id, r);
3154
+ opts.onProgress?.(id, r);
3155
+ }
3156
+ })()
3157
+ );
3158
+ }
3159
+ await Promise.all(workers);
3160
+ };
3161
+ for (const id of ids) {
3162
+ if (cmds.get(id) === null) {
3163
+ const r = { id, ok: false, ms: 0, error: "unknown agent" };
3164
+ byId.set(id, r);
3165
+ opts.onProgress?.(id, r);
3166
+ }
3167
+ }
3168
+ await runPhase(local, opts.concurrency ?? 4, localTimeout);
3169
+ await runPhase(pkg, 2, pkgTimeout);
3170
+ return ids.map((id) => byId.get(id) ?? { id, ok: false, ms: 0, error: "not probed" });
3171
+ }
1912
3172
  var PROBE_TIMEOUT_MS = 5e3;
1913
3173
  var PROBE_CACHE_MS = 5e3;
3174
+ var MAX_PARALLEL_PROBES = 4;
3175
+ async function probeWithBound(items, worker, limit) {
3176
+ const safeLimit = Math.max(1, Math.min(limit, items.length));
3177
+ const results = new Array(items.length);
3178
+ let nextIndex = 0;
3179
+ const workerCount = Math.min(safeLimit, items.length);
3180
+ const runners = [];
3181
+ for (let i = 0; i < workerCount; i++) {
3182
+ runners.push(
3183
+ (async () => {
3184
+ while (true) {
3185
+ const current = nextIndex++;
3186
+ if (current >= items.length) return;
3187
+ results[current] = await worker(items[current]);
3188
+ }
3189
+ })()
3190
+ );
3191
+ }
3192
+ await Promise.all(runners);
3193
+ return results;
3194
+ }
1914
3195
  async function defaultProbe(desc, timeoutMs) {
1915
3196
  const start = Date.now();
1916
3197
  return new Promise((resolve3) => {
@@ -2010,13 +3291,21 @@ var EnsembleRegistry = class {
2010
3291
  /**
2011
3292
  * Probe every catalog entry in parallel and return the detection
2012
3293
  * results. Results are cached for `PROBE_CACHE_MS`.
3294
+ *
3295
+ * Probes are dispatched with bounded concurrency (`MAX_PARALLEL_PROBES`)
3296
+ * so cold-start lists do not spawn `catalog.length` subprocesses at
3297
+ * once — which is especially wasteful on Windows shells and on hosts
3298
+ * with large agent catalogs. The output order still matches
3299
+ * `this.catalog` so callers can index by position.
2013
3300
  */
2014
3301
  async list() {
2015
3302
  if (this.cache && Date.now() - this.cache.at < PROBE_CACHE_MS) {
2016
3303
  return this.cache.result;
2017
3304
  }
2018
- const result = await Promise.all(
2019
- this.catalog.map((d) => this.detect(d))
3305
+ const result = await probeWithBound(
3306
+ this.catalog,
3307
+ (d) => this.detect(d),
3308
+ MAX_PARALLEL_PROBES
2020
3309
  );
2021
3310
  this.cache = { at: Date.now(), result };
2022
3311
  return result;
@@ -2045,31 +3334,126 @@ var EnsembleRegistry = class {
2045
3334
  return all.filter((a) => a.installed);
2046
3335
  }
2047
3336
  };
2048
- var defaultEnsembleCmdResolver = (id) => {
2049
- const fromMap = ACP_AGENT_COMMANDS[id];
2050
- if (fromMap) return fromMap;
2051
- const desc = findAgentDescriptor(id);
2052
- if (!desc) return null;
2053
- const out = {
2054
- command: desc.acp.command,
2055
- args: [...desc.acp.args ?? []],
2056
- role: id
3337
+
3338
+ // src/registry/acp-registry-fetch.ts
3339
+ var ACP_REGISTRY_URL = "https://cdn.agentclientprotocol.com/registry/v1/latest/registry.json";
3340
+ function currentPlatformKey() {
3341
+ const os = process.platform === "win32" ? "windows" : process.platform === "darwin" ? "darwin" : "linux";
3342
+ const arch = process.arch === "arm64" ? "aarch64" : process.arch === "x64" ? "x86_64" : process.arch;
3343
+ return `${os}-${arch}`;
3344
+ }
3345
+ function basename(cmd) {
3346
+ const cleaned = cmd.replace(/^\.\//, "").replace(/\\/g, "/");
3347
+ const parts = cleaned.split("/");
3348
+ return parts[parts.length - 1] || cleaned;
3349
+ }
3350
+ function mapRegistryEntry(entry, platformKey = currentPlatformKey()) {
3351
+ if (!entry || typeof entry.id !== "string" || entry.id.length === 0) return null;
3352
+ const dist = entry.distribution;
3353
+ let acp = null;
3354
+ if (dist?.npx?.package) {
3355
+ acp = { command: "npx", args: ["-y", dist.npx.package, ...dist.npx.args ?? []] };
3356
+ } else if (dist?.uvx?.package) {
3357
+ acp = { command: "uvx", args: [dist.uvx.package, ...dist.uvx.args ?? []] };
3358
+ } else if (dist?.binary) {
3359
+ const target = dist.binary[platformKey];
3360
+ if (target?.cmd) {
3361
+ acp = {
3362
+ command: basename(target.cmd),
3363
+ args: [...target.args ?? []],
3364
+ ...target.env ? { env: target.env } : {}
3365
+ };
3366
+ }
3367
+ }
3368
+ if (!acp) return null;
3369
+ const probeCmd = acp.command === "npx" ? "npx" : acp.command === "uvx" ? "uvx" : acp.command;
3370
+ return {
3371
+ id: entry.id,
3372
+ displayName: entry.name ?? entry.id,
3373
+ vendor: inferVendor(entry),
3374
+ probe: { command: probeCmd, args: ["--version"] },
3375
+ acp,
3376
+ supports: { loadSession: true, promptImages: true, terminal: true, fs: true },
3377
+ integration: "native",
3378
+ docs: entry.repository ?? entry.website ?? ""
2057
3379
  };
2058
- if (desc.acp.env) out.env = desc.acp.env;
2059
- return out;
2060
- };
3380
+ }
3381
+ function inferVendor(entry) {
3382
+ const hay = `${entry.id} ${entry.name ?? ""} ${(entry.authors ?? []).join(" ")}`.toLowerCase();
3383
+ if (hay.includes("anthropic") || hay.includes("claude")) return "anthropic";
3384
+ if (hay.includes("google") || hay.includes("gemini")) return "google";
3385
+ if (hay.includes("openai") || hay.includes("codex")) return "openai";
3386
+ if (hay.includes("github") || hay.includes("copilot")) return "github";
3387
+ return "community";
3388
+ }
3389
+ async function fetchAcpRegistry(opts = {}) {
3390
+ const url = opts.url ?? ACP_REGISTRY_URL;
3391
+ const timeoutMs = opts.timeoutMs ?? 15e3;
3392
+ const controller = new AbortController();
3393
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
3394
+ const onParentAbort = () => controller.abort();
3395
+ if (opts.signal) {
3396
+ if (opts.signal.aborted) controller.abort();
3397
+ else opts.signal.addEventListener("abort", onParentAbort, { once: true });
3398
+ }
3399
+ try {
3400
+ const res = await fetch(url, { signal: controller.signal });
3401
+ if (!res.ok) {
3402
+ throw new Error(`ACP registry fetch failed: HTTP ${res.status}`);
3403
+ }
3404
+ const body = await res.json();
3405
+ const rawAgents = Array.isArray(body) ? body : Array.isArray(body?.agents) ? body.agents : null;
3406
+ if (!rawAgents) {
3407
+ throw new Error("ACP registry response had no agents array");
3408
+ }
3409
+ const platformKey = opts.platformKey ?? currentPlatformKey();
3410
+ const agents = [];
3411
+ for (const raw of rawAgents) {
3412
+ const mapped = mapRegistryEntry(raw, platformKey);
3413
+ if (mapped) agents.push(mapped);
3414
+ }
3415
+ return { fetchedAt: opts.now ?? (/* @__PURE__ */ new Date()).toISOString(), agents };
3416
+ } finally {
3417
+ clearTimeout(timer);
3418
+ opts.signal?.removeEventListener("abort", onParentAbort);
3419
+ }
3420
+ }
3421
+ var DEFAULT_MAX_CONCURRENCY = 4;
3422
+ async function mapBound(items, worker, limit) {
3423
+ const results = new Array(items.length);
3424
+ if (items.length === 0) return results;
3425
+ const safeLimit = Math.max(1, Math.min(limit, items.length));
3426
+ let nextIndex = 0;
3427
+ const workerCount = Math.min(safeLimit, items.length);
3428
+ const runners = [];
3429
+ for (let w = 0; w < workerCount; w++) {
3430
+ runners.push(
3431
+ (async () => {
3432
+ while (true) {
3433
+ const current = nextIndex++;
3434
+ if (current >= items.length) return;
3435
+ results[current] = await worker(items[current], current);
3436
+ }
3437
+ })()
3438
+ );
3439
+ }
3440
+ await Promise.all(runners);
3441
+ return results;
3442
+ }
3443
+ var defaultEnsembleCmdResolver = (id) => resolveAcpAgentCommand(id);
2061
3444
  function setResult(results, agentId, patch) {
2062
3445
  const i = results.findIndex((r) => r.agentId === agentId);
2063
3446
  if (i < 0) return;
2064
3447
  const current = results[i];
2065
3448
  results[i] = { ...current, ...patch };
2066
3449
  }
2067
- async function runOne(agentId, cmd, task, timeoutMs, signal) {
3450
+ async function runOne(agentId, cmd, task, timeoutMs, signal, onProgress) {
2068
3451
  const startedAt = Date.now();
2069
3452
  try {
2070
3453
  const { runner, stop } = await makeACPSubagentRunnerWithStop({
2071
3454
  ...cmd,
2072
- timeoutMs
3455
+ timeoutMs,
3456
+ ...onProgress ? { onProgress: (event) => onProgress(agentId, event) } : {}
2073
3457
  });
2074
3458
  try {
2075
3459
  const budget = new SubagentBudget({
@@ -2173,8 +3557,13 @@ async function runEnsemble(opts) {
2173
3557
  }
2174
3558
  runnable.push({ id, cmd });
2175
3559
  }
2176
- await Promise.allSettled(
2177
- runnable.map(async ({ id, cmd }) => {
3560
+ const concurrency = Math.max(
3561
+ 1,
3562
+ opts.maxConcurrency ?? DEFAULT_MAX_CONCURRENCY
3563
+ );
3564
+ await mapBound(
3565
+ runnable,
3566
+ async ({ id, cmd }) => {
2178
3567
  if (opts.signal?.aborted) {
2179
3568
  setResult(results, id, {
2180
3569
  status: "cancelled",
@@ -2183,9 +3572,10 @@ async function runEnsemble(opts) {
2183
3572
  });
2184
3573
  return;
2185
3574
  }
2186
- const outcome = await runOne(id, cmd, opts.task, timeoutMs, opts.signal);
3575
+ const outcome = await runOne(id, cmd, opts.task, timeoutMs, opts.signal, opts.onProgress);
2187
3576
  setResult(results, id, outcome);
2188
- })
3577
+ },
3578
+ concurrency
2189
3579
  );
2190
3580
  const summary = { succeeded: 0, failed: 0, skipped: 0, cancelled: 0 };
2191
3581
  for (const r of results) {
@@ -2242,7 +3632,212 @@ Ensemble summary: ${succeeded} succeeded, ${failed} failed, ${cancelled} cancell
2242
3632
  );
2243
3633
  return lines.join("\n");
2244
3634
  }
3635
+ function firstLine(s) {
3636
+ const line = s.split("\n").map((l) => l.trim()).find((l) => l.length > 0) ?? "";
3637
+ return line.length > 120 ? `${line.slice(0, 117)}\u2026` : line;
3638
+ }
3639
+ function randomMarker() {
3640
+ return `ACP_OK_${Math.random().toString(36).slice(2, 8).toUpperCase()}`;
3641
+ }
3642
+ async function benchOne(agentId, cmd, opts) {
3643
+ const checks = [];
3644
+ const startedAt = opts.now();
3645
+ let session = null;
3646
+ const signal = opts.signal ?? new AbortController().signal;
3647
+ const hsStart = opts.now();
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,
3654
+ timeoutMs: opts.timeoutMs
3655
+ });
3656
+ } catch (err) {
3657
+ const reason2 = err instanceof Error ? err.message : String(err);
3658
+ checks.push({ name: "handshake", ok: false, detail: reason2 });
3659
+ return {
3660
+ agentId,
3661
+ status: "fail",
3662
+ checks,
3663
+ reason: reason2,
3664
+ handshakeMs: opts.now() - hsStart,
3665
+ durationMs: opts.now() - startedAt
3666
+ };
3667
+ }
3668
+ const handshakeMs = opts.now() - hsStart;
3669
+ const agentInfo = session.getAgentInfo() ?? void 0;
3670
+ checks.push({
3671
+ name: "handshake",
3672
+ ok: true,
3673
+ detail: agentInfo ? `${agentInfo.name} ${agentInfo.version}` : void 0
3674
+ });
3675
+ let promptMs;
3676
+ let sample;
3677
+ let reason;
3678
+ try {
3679
+ const pStart = opts.now();
3680
+ const res = await session.prompt(
3681
+ [textContent(`Reply with exactly this token and nothing else: ${opts.marker}`)],
3682
+ signal
3683
+ );
3684
+ promptMs = opts.now() - pStart;
3685
+ sample = res.text ? firstLine(res.text) : void 0;
3686
+ const promptOk = res.hasText && res.stopReason !== "refusal";
3687
+ checks.push({
3688
+ name: "prompt",
3689
+ ok: promptOk,
3690
+ detail: `stopReason=${res.stopReason}${res.hasText ? "" : ", no text"}`
3691
+ });
3692
+ const markerOk = res.text.includes(opts.marker);
3693
+ checks.push({
3694
+ name: "marker",
3695
+ ok: markerOk,
3696
+ detail: markerOk ? void 0 : "reply did not contain the token"
3697
+ });
3698
+ if (opts.checkFs) {
3699
+ const fileToken = `FILE_${opts.marker}`;
3700
+ const fileName = `acp-bench-${opts.marker}.txt`;
3701
+ const filePath = path.join(opts.projectRoot, fileName);
3702
+ let fsOk = false;
3703
+ let fsDetail;
3704
+ try {
3705
+ await fsp.writeFile(filePath, fileToken, "utf8");
3706
+ const fsRes = await session.prompt(
3707
+ [
3708
+ textContent(
3709
+ `Read the file "${fileName}" in the current directory and reply with its exact contents.`
3710
+ )
3711
+ ],
3712
+ signal
3713
+ );
3714
+ fsOk = fsRes.text.includes(fileToken);
3715
+ if (!fsOk) fsDetail = "agent did not return the file contents (may not have used a read tool)";
3716
+ } catch (err) {
3717
+ fsDetail = err instanceof Error ? err.message : String(err);
3718
+ } finally {
3719
+ await fsp.rm(filePath, { force: true }).catch(() => {
3720
+ });
3721
+ }
3722
+ checks.push({ name: "fs", ok: fsOk, detail: fsDetail });
3723
+ }
3724
+ } catch (err) {
3725
+ reason = err instanceof Error ? err.message : String(err);
3726
+ checks.push({ name: "prompt", ok: false, detail: reason });
3727
+ } finally {
3728
+ try {
3729
+ await session.close();
3730
+ } catch {
3731
+ }
3732
+ }
3733
+ const required = checks.filter((c) => c.name !== "fs" || opts.checkFs);
3734
+ const allReq = required.every((c) => c.ok);
3735
+ const handshakeOk = checks.find((c) => c.name === "handshake")?.ok === true;
3736
+ const status = allReq ? "pass" : handshakeOk ? "partial" : "fail";
3737
+ return {
3738
+ agentId,
3739
+ status,
3740
+ checks,
3741
+ ...agentInfo ? { agentInfo } : {},
3742
+ handshakeMs,
3743
+ ...promptMs !== void 0 ? { promptMs } : {},
3744
+ ...sample ? { sample } : {},
3745
+ ...reason ? { reason } : {},
3746
+ durationMs: opts.now() - startedAt
3747
+ };
3748
+ }
3749
+ async function runAcpBench(opts) {
3750
+ const now = opts.now ?? Date.now;
3751
+ const projectRoot = opts.projectRoot ?? process.cwd();
3752
+ const timeoutMs = opts.timeoutMs ?? 6e4;
3753
+ const checkFs = opts.checkFs ?? false;
3754
+ const marker = opts.marker ?? randomMarker();
3755
+ const concurrency = Math.max(1, opts.concurrency ?? 2);
3756
+ const seen = /* @__PURE__ */ new Set();
3757
+ const ids = [];
3758
+ for (const raw of opts.agentIds) {
3759
+ const id = raw.trim();
3760
+ if (id && !seen.has(id)) {
3761
+ seen.add(id);
3762
+ ids.push(id);
3763
+ }
3764
+ }
3765
+ const results = ids.map((agentId) => ({
3766
+ agentId,
3767
+ status: "skipped",
3768
+ checks: [],
3769
+ durationMs: 0,
3770
+ reason: "unknown agent"
3771
+ }));
3772
+ const startMs = now();
3773
+ const runnable = [];
3774
+ ids.forEach((id, index) => {
3775
+ const cmd = opts.resolveCmd(id);
3776
+ if (cmd) runnable.push({ id, cmd, index });
3777
+ });
3778
+ let next = 0;
3779
+ const workers = [];
3780
+ const workerCount = Math.min(concurrency, runnable.length);
3781
+ for (let w = 0; w < workerCount; w++) {
3782
+ workers.push(
3783
+ (async () => {
3784
+ while (true) {
3785
+ const current = next++;
3786
+ if (current >= runnable.length) return;
3787
+ const { id, cmd, index } = runnable[current];
3788
+ if (opts.signal?.aborted) {
3789
+ results[index] = {
3790
+ agentId: id,
3791
+ status: "skipped",
3792
+ checks: [],
3793
+ durationMs: 0,
3794
+ reason: "aborted"
3795
+ };
3796
+ continue;
3797
+ }
3798
+ opts.onProgress?.(id, "start");
3799
+ const r = await benchOne(id, cmd, {
3800
+ projectRoot,
3801
+ timeoutMs,
3802
+ checkFs,
3803
+ marker,
3804
+ now,
3805
+ ...opts.signal ? { signal: opts.signal } : {}
3806
+ });
3807
+ results[index] = r;
3808
+ opts.onProgress?.(id, "done", r);
3809
+ }
3810
+ })()
3811
+ );
3812
+ }
3813
+ await Promise.all(workers);
3814
+ const summary = { pass: 0, partial: 0, fail: 0, skipped: 0 };
3815
+ for (const r of results) summary[r.status]++;
3816
+ return { results, summary, totalDurationMs: now() - startMs };
3817
+ }
3818
+ function renderAcpBenchText(result) {
3819
+ const icon = (s) => s === "pass" ? "\u2713" : s === "partial" ? "\u25D0" : s === "skipped" ? "\u2013" : "\u2717";
3820
+ const lines = ["ACP client bench:", ""];
3821
+ if (result.results.length === 0) {
3822
+ lines.push("No agents to bench.");
3823
+ return lines.join("\n");
3824
+ }
3825
+ for (const r of result.results) {
3826
+ const checks = r.checks.map((c) => `${c.ok ? "\u2713" : "\u2717"}${c.name}`).join(" ");
3827
+ const timing = r.handshakeMs !== void 0 ? ` hs=${r.handshakeMs}ms${r.promptMs !== void 0 ? ` prompt=${r.promptMs}ms` : ""}` : "";
3828
+ lines.push(` ${icon(r.status)} ${r.agentId.padEnd(16)} ${r.status.toUpperCase().padEnd(7)} ${checks}${timing}`);
3829
+ if (r.agentInfo) lines.push(` agent: ${r.agentInfo.name} ${r.agentInfo.version}`);
3830
+ if (r.sample) lines.push(` reply: ${r.sample}`);
3831
+ if (r.reason) lines.push(` reason: ${r.reason}`);
3832
+ }
3833
+ const { pass, partial, fail, skipped } = result.summary;
3834
+ lines.push("");
3835
+ lines.push(
3836
+ `Bench summary: ${pass} pass, ${partial} partial, ${fail} fail, ${skipped} skipped. (${result.totalDurationMs}ms total)`
3837
+ );
3838
+ return lines.join("\n");
3839
+ }
2245
3840
 
2246
- export { ACPProtocolHandler, ACPSession, ACPSessionError, ACPToolsRegistry, ACP_AGENT_COMMANDS, AGENTS_CATALOG, ClientTransport, EnsembleRegistry, FileServer, FsError, StdioTransport, TerminalServer, ToolTranslator, WrongStackACPServer, defaultEnsembleCmdResolver, defaultPermissionPolicy, findAgentDescriptor, makeACPSubagentRunner, makeACPSubagentRunnerWithStop, renderEnsembleText, runEnsemble };
3841
+ export { ACPProtocolHandler, ACPSession, ACPSessionError, ACPToolsRegistry, ACP_AGENT_COMMANDS, ACP_REGISTRY_URL, AGENTS_CATALOG, ClientTransport, EnsembleRegistry, FileServer, FsError, REGISTRY_ID_ALIASES, StdioTransport, TerminalServer, ToolTranslator, WebSocketClientTransport, WrongStackACPServer, audioContent, currentPlatformKey, defaultEnsembleCmdResolver, defaultPermissionPolicy, fetchAcpRegistry, findAgentDescriptor, imageContent, makeACPSubagentRunner, makeACPSubagentRunnerWithStop, makePermissionPolicy, mapRegistryEntry, probeAcpAgent, probeAcpAgents, readOnlyPermissionPolicy, renderAcpBenchText, renderEnsembleText, resolveAcpAgentCommand, runAcpBench, runEnsemble, runOneAcpTask, textContent };
2247
3842
  //# sourceMappingURL=index.js.map
2248
3843
  //# sourceMappingURL=index.js.map