@wrongstack/acp 1.0.19 → 1.0.26

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,5 +1,6 @@
1
1
  // src/agent/protocol-handler.ts
2
2
  import { randomUUID } from "node:crypto";
3
+ import { isAbsolute as isAbsolute2, resolve as resolve2 } from "node:path";
3
4
 
4
5
  // src/types/acp-v1.ts
5
6
  var ACP_PROTOCOL_VERSION = 1;
@@ -180,7 +181,7 @@ function createRunTurnApi(sessionId, clientCapabilities, request, sendSessionUpd
180
181
  ...sendSessionUpdate ? { sendSessionUpdate } : {}
181
182
  };
182
183
  }
183
- function buildInitializeResult(agentName, modes, configOptions) {
184
+ function buildInitializeResult(agentName, modes, configOptions, clientCapabilities = {}) {
184
185
  return {
185
186
  protocolVersion: ACP_PROTOCOL_VERSION,
186
187
  agentCapabilities: {
@@ -207,9 +208,6 @@ function buildInitializeResult(agentName, modes, configOptions) {
207
208
  delete: {},
208
209
  resume: {},
209
210
  fork: {}
210
- },
211
- auth: {
212
- logout: {}
213
211
  }
214
212
  },
215
213
  agentInfo: {
@@ -217,7 +215,7 @@ function buildInitializeResult(agentName, modes, configOptions) {
217
215
  title: "WrongStack",
218
216
  version: WRONGSTACK_VERSION
219
217
  },
220
- authMethods: WRONGSTACK_AUTH_METHODS,
218
+ authMethods: clientCapabilities.auth?.terminal === true ? WRONGSTACK_AUTH_METHODS : [],
221
219
  modes,
222
220
  configOptions
223
221
  };
@@ -247,7 +245,8 @@ async function handleSessionNewOp(ctx, id, params) {
247
245
  id: sessionId,
248
246
  cwd,
249
247
  abort: new AbortController(),
250
- modeId: DEFAULT_MODE_ID,
248
+ modeId: ctx.modes[0]?.id ?? DEFAULT_MODE_ID,
249
+ configOptions: structuredClone([...ctx.configOptions]),
251
250
  createdAt: now,
252
251
  updatedAt: now,
253
252
  ...mcpServers.length > 0 ? { mcpServers } : {}
@@ -259,6 +258,7 @@ async function handleSessionNewOp(ctx, id, params) {
259
258
  sessionId,
260
259
  update: {
261
260
  sessionUpdate: "current_mode_update",
261
+ currentModeId: ctx.modes[0]?.id ?? DEFAULT_MODE_ID,
262
262
  modeId: ctx.modes[0]?.id ?? DEFAULT_MODE_ID
263
263
  }
264
264
  });
@@ -274,12 +274,12 @@ async function handleSessionNewOp(ctx, id, params) {
274
274
  await reportSkippedMcpServers(ctx, sessionId, skipped);
275
275
  await ctx.sendResult(id, {
276
276
  sessionId,
277
- modes: ctx.modes,
278
- configOptions: ctx.configOptions
277
+ modes: { currentModeId: state.modeId, availableModes: ctx.modes },
278
+ configOptions: state.configOptions
279
279
  });
280
280
  return false;
281
281
  }
282
- async function handleSessionLoadOp(ctx, id, params) {
282
+ async function handleSessionLoadOp(ctx, id, params, replayHistory = true) {
283
283
  const p = params ?? {};
284
284
  const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
285
285
  const loadCwd = typeof p.cwd === "string" ? p.cwd : void 0;
@@ -304,6 +304,7 @@ async function handleSessionLoadOp(ctx, id, params) {
304
304
  cwd: restoredCwd,
305
305
  abort: new AbortController(),
306
306
  modeId: persisted.modeId ?? DEFAULT_MODE_ID,
307
+ configOptions: structuredClone(persisted.configOptions ?? [...ctx.configOptions]),
307
308
  createdAt: persisted.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
308
309
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
309
310
  ...persisted.title !== void 0 ? { title: persisted.title } : {},
@@ -311,51 +312,69 @@ async function handleSessionLoadOp(ctx, id, params) {
311
312
  };
312
313
  ctx.sessions.set(sessionId, restored);
313
314
  ctx.seedFor?.(sessionId, persisted.history ?? []);
314
- for (const update of persisted.history ?? []) {
315
+ for (const update of replayHistory ? persisted.history ?? [] : []) {
315
316
  await ctx.sendNotification({ sessionId, update });
316
317
  }
317
- await ctx.sendNotification({
318
- sessionId,
319
- update: { sessionUpdate: "current_mode_update", modeId: restored.modeId }
320
- });
318
+ if (replayHistory)
319
+ await ctx.sendNotification({
320
+ sessionId,
321
+ update: {
322
+ sessionUpdate: "current_mode_update",
323
+ currentModeId: restored.modeId,
324
+ modeId: restored.modeId
325
+ }
326
+ });
321
327
  await reportSkippedMcpServers(ctx, sessionId, loadSkipped);
322
328
  await ctx.sendResult(id, {
323
- initialMode: { currentModeId: restored.modeId, availableModes: ctx.modes }
329
+ modes: { currentModeId: restored.modeId, availableModes: ctx.modes },
330
+ configOptions: restored.configOptions
324
331
  });
325
332
  return false;
326
333
  }
327
334
  }
328
335
  if (existing) {
336
+ if (existing.prompting) {
337
+ await ctx.sendError(id, -32e3, "cannot reload a session while a prompt is running");
338
+ return false;
339
+ }
329
340
  existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
330
- if (loadMcpServers.length > 0) {
341
+ const replay = ctx.replayFor?.(sessionId);
342
+ if (Array.isArray(p.mcpServers)) {
343
+ if (JSON.stringify(existing.mcpServers ?? []) !== JSON.stringify(loadMcpServers)) {
344
+ ctx.disposeFor?.(sessionId);
345
+ ctx.seedFor?.(sessionId, replay ?? []);
346
+ }
331
347
  existing.mcpServers = loadMcpServers;
332
348
  }
333
- const replay = ctx.replayFor?.(sessionId);
334
- if (replay) {
349
+ if (replayHistory && replay) {
335
350
  for (const update of replay) {
336
351
  await ctx.sendNotification({ sessionId, update });
337
352
  }
338
353
  }
339
- await ctx.sendNotification({
340
- sessionId,
341
- update: {
342
- sessionUpdate: "session_info_update",
343
- updatedAt: existing.updatedAt
344
- }
345
- });
346
- await ctx.sendNotification({
347
- sessionId,
348
- update: {
349
- sessionUpdate: "current_mode_update",
350
- modeId: existing.modeId
351
- }
352
- });
354
+ if (replayHistory)
355
+ await ctx.sendNotification({
356
+ sessionId,
357
+ update: {
358
+ sessionUpdate: "session_info_update",
359
+ updatedAt: existing.updatedAt
360
+ }
361
+ });
362
+ if (replayHistory)
363
+ await ctx.sendNotification({
364
+ sessionId,
365
+ update: {
366
+ sessionUpdate: "current_mode_update",
367
+ currentModeId: existing.modeId,
368
+ modeId: existing.modeId
369
+ }
370
+ });
353
371
  await reportSkippedMcpServers(ctx, sessionId, loadSkipped);
354
372
  await ctx.sendResult(id, {
355
- initialMode: {
373
+ modes: {
356
374
  currentModeId: existing.modeId,
357
375
  availableModes: ctx.modes
358
- }
376
+ },
377
+ configOptions: existing.configOptions ?? [...ctx.configOptions]
359
378
  });
360
379
  return false;
361
380
  }
@@ -393,6 +412,7 @@ async function handleSessionForkOp(ctx, id, params) {
393
412
  cwd: forkCwd,
394
413
  abort: new AbortController(),
395
414
  modeId: source.modeId,
415
+ configOptions: structuredClone(source.configOptions ?? [...ctx.configOptions]),
396
416
  createdAt: now,
397
417
  updatedAt: now,
398
418
  ...source.title !== void 0 ? { title: source.title } : {},
@@ -408,13 +428,17 @@ async function handleSessionForkOp(ctx, id, params) {
408
428
  await ctx.persist(forked, history);
409
429
  await ctx.sendNotification({
410
430
  sessionId,
411
- update: { sessionUpdate: "current_mode_update", modeId: forked.modeId }
431
+ update: {
432
+ sessionUpdate: "current_mode_update",
433
+ currentModeId: forked.modeId,
434
+ modeId: forked.modeId
435
+ }
412
436
  });
413
437
  await reportSkippedMcpServers(ctx, sessionId, forkSkipped);
414
438
  await ctx.sendResult(id, {
415
439
  sessionId,
416
- modes: ctx.modes,
417
- configOptions: ctx.configOptions
440
+ modes: { currentModeId: forked.modeId, availableModes: ctx.modes },
441
+ configOptions: forked.configOptions
418
442
  });
419
443
  return false;
420
444
  }
@@ -430,6 +454,11 @@ async function handleSessionPromptOp(ctx, id, params) {
430
454
  return false;
431
455
  }
432
456
  const session = ctx.sessions.get(sessionId);
457
+ if (session.prompting) {
458
+ await ctx.sendError(id, -32e3, "a prompt is already running for this session");
459
+ return false;
460
+ }
461
+ session.prompting = true;
433
462
  if (session.abort.signal.aborted) {
434
463
  session.abort = new AbortController();
435
464
  }
@@ -458,6 +487,8 @@ async function handleSessionPromptOp(ctx, id, params) {
458
487
  {
459
488
  sessionId,
460
489
  prompt: p.prompt,
490
+ modeId: session.modeId,
491
+ configOptions: session.configOptions,
461
492
  signal: turnSignal.signal,
462
493
  cwd: session.cwd,
463
494
  ...session.mcpServers ? { mcpServers: session.mcpServers } : {}
@@ -466,16 +497,25 @@ async function handleSessionPromptOp(ctx, id, params) {
466
497
  api
467
498
  );
468
499
  } catch (err) {
500
+ session.prompting = false;
469
501
  session.abort.signal.removeEventListener("abort", onCancel);
502
+ await Promise.all(pendingNotifications);
503
+ if (turnSignal.signal.aborted) {
504
+ await ctx.sendResult(id, { stopReason: "cancelled" });
505
+ return false;
506
+ }
470
507
  const { code, message, data } = errorToJsonRpc(err);
471
508
  await ctx.sendError(id, code, message, data);
472
509
  return false;
473
510
  }
474
511
  await Promise.all(pendingNotifications);
512
+ session.prompting = false;
475
513
  session.abort.signal.removeEventListener("abort", onCancel);
476
514
  session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
477
- await ctx.persist(session);
478
- await ctx.sendResult(id, { stopReason: result.stopReason });
515
+ if (ctx.sessions.get(sessionId) === session) await ctx.persist(session);
516
+ await ctx.sendResult(id, {
517
+ stopReason: turnSignal.signal.aborted ? "cancelled" : result.stopReason
518
+ });
479
519
  return false;
480
520
  }
481
521
  async function handleSetModeOp(ctx, id, params) {
@@ -491,7 +531,11 @@ async function handleSetModeOp(ctx, id, params) {
491
531
  session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
492
532
  await ctx.sendNotification({
493
533
  sessionId,
494
- update: { sessionUpdate: "current_mode_update", modeId }
534
+ update: {
535
+ sessionUpdate: "current_mode_update",
536
+ currentModeId: modeId,
537
+ modeId
538
+ }
495
539
  });
496
540
  await ctx.sendResult(id, {});
497
541
  return false;
@@ -502,21 +546,24 @@ async function handleSetConfigOptionOp(ctx, id, params) {
502
546
  const optionId = typeof p.configId === "string" ? p.configId : null;
503
547
  const value = typeof p.value === "string" ? p.value : null;
504
548
  const session = sessionId ? ctx.sessions.get(sessionId) : void 0;
505
- const option = optionId ? ctx.configOptions.find((o) => o.id === optionId) : void 0;
549
+ const options = session?.configOptions ?? structuredClone([...ctx.configOptions]);
550
+ const option = optionId ? options.find((o) => o.id === optionId) : void 0;
506
551
  if (!session || !option || value === null || !option.options.some((o) => o.value === value)) {
507
552
  await ctx.sendError(id, -32602, "invalid sessionId, configId, or value");
508
553
  return false;
509
554
  }
510
555
  option.currentValue = value;
556
+ session.configOptions = options;
511
557
  session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
512
558
  await ctx.sendNotification({
513
559
  sessionId,
514
560
  update: {
515
561
  sessionUpdate: "config_option_update",
516
- configOptions: [...ctx.configOptions]
562
+ configOptions: options
517
563
  }
518
564
  });
519
- await ctx.sendResult(id, { configOptions: [...ctx.configOptions] });
565
+ await ctx.persist(session);
566
+ await ctx.sendResult(id, { configOptions: options });
520
567
  return false;
521
568
  }
522
569
  async function reportSkippedMcpServers(ctx, sessionId, skipped) {
@@ -558,6 +605,8 @@ var ACPProtocolHandler = class {
558
605
  // session/request_permission). Keyed by our own `srv_N` ids.
559
606
  pendingOut = /* @__PURE__ */ new Map();
560
607
  nextOutId = 1;
608
+ promptRequests = /* @__PURE__ */ new Map();
609
+ persistenceWrites = /* @__PURE__ */ new Map();
561
610
  constructor(opts) {
562
611
  this.transport = opts.transport;
563
612
  this.defaultCwd = opts.defaultCwd;
@@ -583,20 +632,46 @@ var ACPProtocolHandler = class {
583
632
  */
584
633
  request(method, params, timeoutMs = 6e4) {
585
634
  const id = `srv_${this.nextOutId++}`;
586
- return new Promise((resolve4, reject) => {
587
- const timer = setTimeout(() => {
635
+ const sessionId = params?.sessionId;
636
+ const signal = sessionId ? this.sessions.get(sessionId)?.abort.signal : void 0;
637
+ return new Promise((resolve5, reject) => {
638
+ const cleanup = () => {
639
+ clearTimeout(timer);
640
+ signal?.removeEventListener("abort", abort);
588
641
  this.pendingOut.delete(id);
589
- reject(new Error(`${method} timed out after ${timeoutMs}ms`));
642
+ };
643
+ const fail = (error) => {
644
+ cleanup();
645
+ reject(error);
646
+ };
647
+ const abort = () => {
648
+ fail(new Error("client request cancelled"));
649
+ void this.transport.send(toWire({ jsonrpc: "2.0", method: "$/cancel_request", params: { requestId: id } })).catch(() => {
650
+ });
651
+ };
652
+ const timer = setTimeout(() => {
653
+ fail(new Error(`${method} timed out after ${timeoutMs}ms`));
590
654
  }, timeoutMs);
591
- this.pendingOut.set(id, { resolve: resolve4, reject, timer });
655
+ if (signal?.aborted) {
656
+ fail(new Error("client request cancelled"));
657
+ return;
658
+ }
659
+ signal?.addEventListener("abort", abort, { once: true });
660
+ this.pendingOut.set(id, {
661
+ resolve: (value) => {
662
+ cleanup();
663
+ resolve5(value);
664
+ },
665
+ reject: fail,
666
+ timer
667
+ });
592
668
  this.transport.send(toWire({ jsonrpc: "2.0", id, method, params })).catch((e) => {
593
- clearTimeout(timer);
594
- this.pendingOut.delete(id);
595
- reject(e instanceof Error ? e : new Error(String(e)));
669
+ fail(e instanceof Error ? e : new Error(String(e)));
596
670
  });
597
671
  });
598
672
  }
599
673
  maybeResolvePending(m) {
674
+ if (m.result === void 0 && m.error === void 0) return;
600
675
  const id = m.id;
601
676
  if (typeof id !== "string") return;
602
677
  const pending = this.pendingOut.get(id);
@@ -616,6 +691,7 @@ var ACPProtocolHandler = class {
616
691
  if (typeof msg !== "object" || msg === null) return false;
617
692
  const m = msg;
618
693
  if (m.id !== void 0 && (m.result !== void 0 || m.error !== void 0)) {
694
+ this.maybeResolvePending(msg);
619
695
  return false;
620
696
  }
621
697
  if (m.id !== void 0 && typeof m.method === "string") {
@@ -655,6 +731,7 @@ var ACPProtocolHandler = class {
655
731
  store: this.store,
656
732
  replayFor: this.replayFor,
657
733
  seedFor: this.seedFor,
734
+ disposeFor: this.disposeFor,
658
735
  onSessionNew: this.onSessionNew,
659
736
  allocId: () => this.allocId(),
660
737
  persist: (state, history) => this.persist(state, history),
@@ -692,14 +769,23 @@ var ACPProtocolHandler = class {
692
769
  return await this.handleSessionClose(id, params);
693
770
  case "session/delete":
694
771
  return await this.handleSessionDelete(id, params);
695
- case "session/prompt":
696
- return await handleSessionPromptOp(this.sessionContext(), id, params);
772
+ case "session/prompt": {
773
+ const sessionId = params?.sessionId;
774
+ if (typeof sessionId === "string" && !this.sessions.get(sessionId)?.prompting) {
775
+ this.promptRequests.set(id, sessionId);
776
+ }
777
+ try {
778
+ return await handleSessionPromptOp(this.sessionContext(), id, params);
779
+ } finally {
780
+ this.promptRequests.delete(id);
781
+ }
782
+ }
697
783
  case "session/set_mode":
698
784
  return await handleSetModeOp(this.sessionContext(), id, params);
699
785
  case "session/set_config_option":
700
786
  return await handleSetConfigOptionOp(this.sessionContext(), id, params);
701
787
  case "session/list":
702
- return await this.handleSessionList(id);
788
+ return await this.handleSessionList(id, params);
703
789
  case "session/fork":
704
790
  return await handleSessionForkOp(this.sessionContext(), id, params);
705
791
  case "providers/list":
@@ -728,7 +814,12 @@ var ACPProtocolHandler = class {
728
814
  this.initialized = true;
729
815
  await this.sendResult(
730
816
  id,
731
- buildInitializeResult(this.agentName, this.modes, this.configOptions)
817
+ buildInitializeResult(
818
+ this.agentName,
819
+ this.modes,
820
+ this.configOptions,
821
+ this.clientCapabilities
822
+ )
732
823
  );
733
824
  return false;
734
825
  }
@@ -737,29 +828,15 @@ var ACPProtocolHandler = class {
737
828
  return false;
738
829
  }
739
830
  async handleLogout(id, _params) {
740
- await this.sendResult(id, {});
831
+ await this.sendError(
832
+ id,
833
+ -32601,
834
+ "logout is not supported; manage provider credentials with wstack auth"
835
+ );
741
836
  return false;
742
837
  }
743
838
  async handleSessionResume(id, params) {
744
- const p = params ?? {};
745
- const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
746
- const existing = sessionId ? this.sessions.get(sessionId) : void 0;
747
- if (existing) {
748
- existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
749
- const resumeServers = parseMcpServers(p.mcpServers);
750
- if (resumeServers.length > 0) {
751
- existing.mcpServers = resumeServers;
752
- }
753
- await this.sendResult(id, {
754
- initialMode: {
755
- currentModeId: existing.modeId,
756
- availableModes: this.modes
757
- }
758
- });
759
- return false;
760
- }
761
- await this.sendError(id, -32e3, `session not found: ${sessionId}`);
762
- return false;
839
+ return handleSessionLoadOp(this.sessionContext(), id, params, false);
763
840
  }
764
841
  async handleSessionClose(id, params) {
765
842
  const p = params ?? {};
@@ -782,14 +859,12 @@ var ACPProtocolHandler = class {
782
859
  await this.sendError(id, -32e3, `session not found: ${sessionId}`);
783
860
  return false;
784
861
  }
785
- if (!this.sessions.has(sessionId)) {
786
- await this.sendResult(id, { configOptions: [...this.configOptions] });
787
- return false;
788
- }
789
862
  const session = this.sessions.get(sessionId);
790
- session.abort.abort();
863
+ session?.abort.abort();
791
864
  this.sessions.delete(sessionId);
792
865
  this.disposeSession(sessionId);
866
+ await this.persistenceWrites.get(sessionId);
867
+ await this.store?.delete?.(sessionId);
793
868
  await this.sendResult(id, {});
794
869
  return false;
795
870
  }
@@ -816,12 +891,23 @@ var ACPProtocolHandler = class {
816
891
  await this.sendError(id, -32e3, "MCP message routing not available through ACP");
817
892
  return false;
818
893
  }
819
- async handleSessionList(id) {
820
- const sessions = Array.from(this.sessions.values()).map((s) => {
894
+ async handleSessionList(id, params) {
895
+ const p = params ?? {};
896
+ if (p.cwd != null && (typeof p.cwd !== "string" || !isAbsolute2(p.cwd)) || p.cursor != null) {
897
+ await this.sendError(id, -32602, "invalid cwd or cursor");
898
+ return false;
899
+ }
900
+ const known = new Map(this.sessions);
901
+ for (const entry of await this.store?.list?.() ?? []) {
902
+ if (known.has(entry.id)) continue;
903
+ const saved = await this.store?.load(entry.id);
904
+ if (saved?.cwd) known.set(entry.id, { ...saved, id: entry.id });
905
+ }
906
+ const sessions = Array.from(known.values()).filter((s) => typeof p.cwd !== "string" || s.cwd && resolve2(s.cwd) === resolve2(p.cwd)).map((s) => {
821
907
  const out = {
822
908
  sessionId: s.id,
823
909
  cwd: s.cwd,
824
- updatedAt: s.updatedAt
910
+ updatedAt: s.updatedAt ?? ""
825
911
  };
826
912
  if (s.title !== void 0) out.title = s.title;
827
913
  return out;
@@ -844,6 +930,11 @@ var ACPProtocolHandler = class {
844
930
  return false;
845
931
  }
846
932
  case "$/cancel_request": {
933
+ const requestId = params?.requestId;
934
+ if (typeof requestId === "string" || typeof requestId === "number") {
935
+ const sessionId = this.promptRequests.get(requestId);
936
+ if (sessionId) this.sessions.get(sessionId)?.abort.abort();
937
+ }
847
938
  return false;
848
939
  }
849
940
  case "exit":
@@ -864,10 +955,18 @@ var ACPProtocolHandler = class {
864
955
  }
865
956
  async persist(state, history = void 0) {
866
957
  if (!this.store) return;
867
- try {
868
- await this.store.save(state, history ?? this.replayFor?.(state.id));
869
- } catch {
870
- }
958
+ const store = this.store;
959
+ const replay = history ?? this.replayFor?.(state.id);
960
+ const previous = this.persistenceWrites.get(state.id) ?? Promise.resolve();
961
+ const write = previous.then(() => store.save(state, replay)).then(
962
+ () => {
963
+ },
964
+ () => {
965
+ }
966
+ );
967
+ this.persistenceWrites.set(state.id, write);
968
+ await write;
969
+ if (this.persistenceWrites.get(state.id) === write) this.persistenceWrites.delete(state.id);
871
970
  }
872
971
  async sendError(id, code, message, data) {
873
972
  const error = { code, message };
@@ -927,9 +1026,9 @@ var StdioTransport = class {
927
1026
  }
928
1027
  send(msg) {
929
1028
  if (this.closed) return Promise.resolve();
930
- return new Promise((resolve4) => {
1029
+ return new Promise((resolve5) => {
931
1030
  const line = JSON.stringify(msg) + "\n";
932
- this.stdout.write(line, "utf8", () => resolve4());
1031
+ this.stdout.write(line, "utf8", () => resolve5());
933
1032
  });
934
1033
  }
935
1034
  sendRaw(chunk) {
@@ -942,8 +1041,8 @@ var StdioTransport = class {
942
1041
  return Promise.resolve(queued.message);
943
1042
  }
944
1043
  if (this.closed) return Promise.resolve(null);
945
- return new Promise((resolve4) => {
946
- this.resolveRead = resolve4;
1044
+ return new Promise((resolve5) => {
1045
+ this.resolveRead = resolve5;
947
1046
  });
948
1047
  }
949
1048
  onMessage(handler) {
@@ -1012,9 +1111,9 @@ var StdioTransport = class {
1012
1111
  }
1013
1112
  dispatch(msg, chars = JSON.stringify(msg).length) {
1014
1113
  if (this.resolveRead) {
1015
- const resolve4 = this.resolveRead;
1114
+ const resolve5 = this.resolveRead;
1016
1115
  this.resolveRead = null;
1017
- resolve4(msg);
1116
+ resolve5(msg);
1018
1117
  } else {
1019
1118
  let claimed = false;
1020
1119
  for (const handler of this.claimHandlers) {
@@ -1086,7 +1185,7 @@ var ClientTransport = class {
1086
1185
  import("@wrongstack/core/utils"),
1087
1186
  import("node:os")
1088
1187
  ]);
1089
- return new Promise((resolve4, reject) => {
1188
+ return new Promise((resolve5, reject) => {
1090
1189
  const timeout = setTimeout(() => {
1091
1190
  const child2 = this.child;
1092
1191
  this.child = null;
@@ -1139,7 +1238,7 @@ var ClientTransport = class {
1139
1238
  if (settled) return;
1140
1239
  settled = true;
1141
1240
  clearTimeout(timeout);
1142
- resolve4();
1241
+ resolve5();
1143
1242
  });
1144
1243
  return;
1145
1244
  }
@@ -1150,7 +1249,7 @@ var ClientTransport = class {
1150
1249
  child.stderr.on("data", (c) => this.onChildError(c));
1151
1250
  child.on("close", (code) => this.onChildClose(code));
1152
1251
  clearTimeout(timeout);
1153
- resolve4();
1252
+ resolve5();
1154
1253
  };
1155
1254
  const waitForMarker = (chunk) => {
1156
1255
  this.buffer += chunk;
@@ -1169,11 +1268,11 @@ var ClientTransport = class {
1169
1268
  }
1170
1269
  send(msg) {
1171
1270
  if (!this.child) return Promise.reject(new Error("ClientTransport not started"));
1172
- return new Promise((resolve4, reject) => {
1271
+ return new Promise((resolve5, reject) => {
1173
1272
  const line = JSON.stringify(msg) + "\n";
1174
1273
  this.child?.stdin.write(line, "utf8", (err) => {
1175
1274
  if (err) reject(err);
1176
- else resolve4();
1275
+ else resolve5();
1177
1276
  });
1178
1277
  });
1179
1278
  }
@@ -1184,8 +1283,8 @@ var ClientTransport = class {
1184
1283
  return Promise.resolve(queued.message);
1185
1284
  }
1186
1285
  if (this.closed) return Promise.resolve(null);
1187
- return new Promise((resolve4) => {
1188
- this.resolveRead = resolve4;
1286
+ return new Promise((resolve5) => {
1287
+ this.resolveRead = resolve5;
1189
1288
  });
1190
1289
  }
1191
1290
  onMessage(handler) {
@@ -1256,6 +1355,7 @@ var ClientTransport = class {
1256
1355
  this.messageQueue.length = 0;
1257
1356
  this.queuedChars = 0;
1258
1357
  this.handlers.clear();
1358
+ this.claimHandlers.clear();
1259
1359
  if (code !== 0 && code !== null) {
1260
1360
  writeErr(`[acp-child exited with code ${code}]
1261
1361
  `);
@@ -1263,20 +1363,31 @@ var ClientTransport = class {
1263
1363
  }
1264
1364
  dispatch(msg, chars = JSON.stringify(msg).length) {
1265
1365
  if (this.resolveRead) {
1266
- const resolve4 = this.resolveRead;
1366
+ const resolve5 = this.resolveRead;
1267
1367
  this.resolveRead = null;
1268
- resolve4(msg);
1269
- } else if (this.handlers.size === 0) {
1270
- if (this.messageQueue.length >= this.maxQueuedMessages || this.queuedChars + chars > this.maxQueuedChars) {
1271
- writeErr(
1272
- `[acp-child message queue exceeds ${this.maxQueuedMessages} entries or ${this.maxQueuedChars} characters]
1368
+ resolve5(msg);
1369
+ } else {
1370
+ let claimed = false;
1371
+ for (const handler of this.claimHandlers) {
1372
+ try {
1373
+ if (handler(msg)) claimed = true;
1374
+ } catch (err) {
1375
+ writeErr(`[acp-child handler error] ${err}
1376
+ `);
1377
+ }
1378
+ }
1379
+ if (!claimed && this.handlers.size === 0) {
1380
+ if (this.messageQueue.length >= this.maxQueuedMessages || this.queuedChars + chars > this.maxQueuedChars) {
1381
+ writeErr(
1382
+ `[acp-child message queue exceeds ${this.maxQueuedMessages} entries or ${this.maxQueuedChars} characters]
1273
1383
  `
1274
- );
1275
- this.stop();
1276
- return;
1384
+ );
1385
+ this.stop();
1386
+ return;
1387
+ }
1388
+ this.messageQueue.push({ message: msg, chars });
1389
+ this.queuedChars += chars;
1277
1390
  }
1278
- this.messageQueue.push({ message: msg, chars });
1279
- this.queuedChars += chars;
1280
1391
  }
1281
1392
  for (const handler of this.handlers) {
1282
1393
  try {
@@ -1410,7 +1521,7 @@ function toolToPriority(tool) {
1410
1521
  import { createServer } from "node:http";
1411
1522
  import { isIP } from "node:net";
1412
1523
  import { fileURLToPath } from "node:url";
1413
- import { expandIPv6, writeErr as writeErr2 } from "@wrongstack/core/utils";
1524
+ import { expandIPv6, isStandaloneBinary, writeErr as writeErr2 } from "@wrongstack/core/utils";
1414
1525
  import { timingSafeTokenEqual } from "@wrongstack/primitives";
1415
1526
  var LISTEN_RETRY_LIMIT = 5;
1416
1527
  var LISTEN_RETRY_BASE_MS = 25;
@@ -1456,15 +1567,30 @@ var WrongStackACPServer = class {
1456
1567
  this.transport.sendStartupMarker();
1457
1568
  }
1458
1569
  this.running = true;
1570
+ const prompts = /* @__PURE__ */ new Set();
1459
1571
  try {
1460
1572
  while (this.running) {
1461
1573
  const msg = await this.transport.read();
1462
1574
  if (!msg) break;
1575
+ if (msg.method === "session/prompt") {
1576
+ const task = this.handler.handleMessage(msg).then(
1577
+ () => {
1578
+ },
1579
+ (err) => {
1580
+ writeErr2(`[wstack-acp] prompt dispatch failed: ${String(err)}
1581
+ `);
1582
+ }
1583
+ );
1584
+ prompts.add(task);
1585
+ void task.finally(() => prompts.delete(task));
1586
+ continue;
1587
+ }
1463
1588
  const terminal = await this.handler.handleMessage(msg);
1464
1589
  if (terminal) break;
1465
1590
  }
1466
1591
  } finally {
1467
1592
  this.handler.close();
1593
+ await Promise.allSettled(prompts);
1468
1594
  this.transport.close();
1469
1595
  }
1470
1596
  }
@@ -1598,10 +1724,10 @@ var WrongStackACPServer = class {
1598
1724
  listenWithRetry(port, host, attempt = 0) {
1599
1725
  const server = this.httpServer;
1600
1726
  if (!server) return Promise.resolve();
1601
- return new Promise((resolve4, reject) => {
1727
+ return new Promise((resolve5, reject) => {
1602
1728
  const onListening = () => {
1603
1729
  server.removeListener("error", onError);
1604
- resolve4();
1730
+ resolve5();
1605
1731
  };
1606
1732
  const onError = (err) => {
1607
1733
  server.removeListener("listening", onListening);
@@ -1609,7 +1735,7 @@ var WrongStackACPServer = class {
1609
1735
  if (port === 0 && transient && attempt < LISTEN_RETRY_LIMIT) {
1610
1736
  const timer = setTimeout(
1611
1737
  () => {
1612
- this.listenWithRetry(port, host, attempt + 1).then(resolve4, reject);
1738
+ this.listenWithRetry(port, host, attempt + 1).then(resolve5, reject);
1613
1739
  },
1614
1740
  LISTEN_RETRY_BASE_MS * (attempt + 1)
1615
1741
  );
@@ -1637,8 +1763,8 @@ var WrongStackACPServer = class {
1637
1763
  if (!server) return;
1638
1764
  for (const socket of this.httpSockets) socket.destroy();
1639
1765
  this.httpSockets.clear();
1640
- await new Promise((resolve4) => {
1641
- server.close(() => resolve4());
1766
+ await new Promise((resolve5) => {
1767
+ server.close(() => resolve5());
1642
1768
  });
1643
1769
  }
1644
1770
  };
@@ -1669,7 +1795,7 @@ async function main() {
1669
1795
  const server = new WrongStackACPServer();
1670
1796
  await server.start();
1671
1797
  }
1672
- var isEntrypoint = process.argv[1] !== void 0 && fileURLToPath(import.meta.url) === process.argv[1];
1798
+ var isEntrypoint = process.argv[1] !== void 0 && !isStandaloneBinary() && fileURLToPath(import.meta.url) === process.argv[1];
1673
1799
  if (isEntrypoint) {
1674
1800
  main().catch((err) => {
1675
1801
  writeErr2(`[wstack-acp fatal] ${err}
@@ -1678,93 +1804,472 @@ if (isEntrypoint) {
1678
1804
  });
1679
1805
  }
1680
1806
 
1807
+ // src/client/acp-late-session.ts
1808
+ var LATE_CANCEL_SEND_TIMEOUT_MS = 1e4;
1809
+ function cancelLateAcpSession(transport, createPromise) {
1810
+ createPromise.then(async (lateId) => {
1811
+ try {
1812
+ await new Promise((resolve5, reject) => {
1813
+ const timer = setTimeout(() => {
1814
+ reject(new Error("late session/cancel send timed out"));
1815
+ }, LATE_CANCEL_SEND_TIMEOUT_MS);
1816
+ Promise.resolve(
1817
+ transport.send({
1818
+ jsonrpc: "2.0",
1819
+ method: "session/cancel",
1820
+ params: { sessionId: lateId }
1821
+ })
1822
+ ).then(
1823
+ () => {
1824
+ clearTimeout(timer);
1825
+ resolve5();
1826
+ },
1827
+ (sendErr) => {
1828
+ clearTimeout(timer);
1829
+ reject(sendErr instanceof Error ? sendErr : new Error(String(sendErr)));
1830
+ }
1831
+ );
1832
+ });
1833
+ } catch (err) {
1834
+ const message = err instanceof Error ? err.message : String(err);
1835
+ console.warn(
1836
+ JSON.stringify({
1837
+ level: "warn",
1838
+ event: "acp_session.late_cancel_failed",
1839
+ sessionId: lateId,
1840
+ message
1841
+ })
1842
+ );
1843
+ }
1844
+ }).catch((reason) => {
1845
+ const message = reason instanceof Error ? reason.message : String(reason);
1846
+ console.warn(
1847
+ JSON.stringify({
1848
+ level: "warn",
1849
+ event: "acp_session.late_cancel_failed",
1850
+ reason: message
1851
+ })
1852
+ );
1853
+ });
1854
+ }
1855
+
1681
1856
  // src/client/acp-message-routing.ts
1682
1857
  function isBestEffortAckMethod(method) {
1683
1858
  return method === "mcp/connect" || method === "mcp/message" || method === "mcp/disconnect" || method === "elicitation/create" || method === "elicitation/complete";
1684
1859
  }
1685
1860
 
1686
- // src/client/file-server.ts
1687
- import { randomBytes } from "node:crypto";
1688
- import { realpathSync } from "node:fs";
1689
- import * as fsp2 from "node:fs/promises";
1690
- import * as path2 from "node:path";
1691
- var DEFAULT_FILE_OPERATIONS = {
1692
- stat: fsp2.stat,
1693
- readFile: fsp2.readFile,
1694
- writeFile: fsp2.writeFile,
1695
- realpath: fsp2.realpath,
1696
- rename: fsp2.rename,
1697
- unlink: fsp2.unlink
1698
- };
1699
- var DEFAULT_MAX_READ_BYTES = 5 * 1024 * 1024;
1700
- var DEFAULT_MAX_WRITE_BYTES = 5 * 1024 * 1024;
1701
- var FsError = class extends Error {
1702
- code;
1703
- path;
1704
- constructor(code, path5, message) {
1861
+ // src/client/acp-session-errors.ts
1862
+ var ACPSessionError = class extends Error {
1863
+ kind;
1864
+ cause;
1865
+ constructor(kind, message, cause) {
1705
1866
  super(message);
1706
- this.name = "FsError";
1707
- this.code = code;
1708
- this.path = path5;
1867
+ this.name = "ACPSessionError";
1868
+ this.kind = kind;
1869
+ this.cause = cause;
1709
1870
  }
1710
1871
  };
1711
- var FileServer = class {
1712
- root;
1713
- realRoot;
1714
- timeoutMs;
1715
- maxReadBytes;
1716
- maxWriteBytes;
1717
- operations;
1718
- constructor(opts) {
1719
- this.root = path2.resolve(opts.projectRoot);
1720
- this.realRoot = safeRealpathSync(this.root);
1721
- this.timeoutMs = opts.timeoutMs ?? 3e4;
1722
- this.maxReadBytes = opts.maxReadBytes ?? DEFAULT_MAX_READ_BYTES;
1723
- this.maxWriteBytes = opts.maxWriteBytes ?? DEFAULT_MAX_WRITE_BYTES;
1724
- this.operations = opts.operations ?? DEFAULT_FILE_OPERATIONS;
1872
+ function isJsonRpcError(v) {
1873
+ return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
1874
+ }
1875
+ function isAuthRequiredError(err) {
1876
+ const message = err instanceof Error ? err.message : String(err);
1877
+ if (/auth(_|-)?required|authentication required/i.test(message)) return true;
1878
+ const cause = err instanceof ACPSessionError ? err.cause : err && typeof err === "object" && "cause" in err ? err.cause : err;
1879
+ if (!cause || typeof cause !== "object") return false;
1880
+ const data = cause.data;
1881
+ if (data === "auth_required" || data === "AUTH_REQUIRED") return true;
1882
+ if (data && typeof data === "object") {
1883
+ const d = data;
1884
+ if (d.authRequired === true) return true;
1885
+ if (d.code === "auth_required" || d.code === "AUTH_REQUIRED") return true;
1725
1886
  }
1726
- /** Read a text file. Returns the content as a string. */
1727
- async readTextFile(params) {
1728
- const safe = await this.resolveInside(params.path);
1729
- const controller = new AbortController();
1730
- const timer = setTimeout(() => controller.abort(), this.timeoutMs);
1731
- try {
1732
- const stat3 = await this.operations.stat(safe).catch((err) => {
1733
- throw mapFsError(err, safe);
1734
- });
1735
- if (stat3.size > this.maxReadBytes) {
1736
- throw new FsError(
1737
- "TOO_LARGE",
1738
- safe,
1739
- `file is ${stat3.size} bytes, max read is ${this.maxReadBytes} bytes`
1740
- );
1741
- }
1742
- const content = await this.operations.readFile(safe, {
1743
- encoding: "utf8",
1744
- signal: controller.signal
1745
- });
1746
- return { content };
1747
- } catch (err) {
1748
- if (err instanceof FsError) throw err;
1749
- if (controller.signal.aborted) {
1750
- throw new FsError("TIMEOUT", safe, `readTextFile timed out after ${this.timeoutMs}ms`);
1751
- }
1752
- throw mapFsError(err, safe);
1753
- } finally {
1754
- clearTimeout(timer);
1755
- }
1887
+ return false;
1888
+ }
1889
+
1890
+ // src/client/acp-session-ops.ts
1891
+ function filterMcpServers(agentCapabilities, servers) {
1892
+ if (!servers || servers.length === 0) return [];
1893
+ const mcpCaps = agentCapabilities.mcpCapabilities ?? {};
1894
+ return servers.filter((s) => {
1895
+ if ("type" in s && s.type === "http") return mcpCaps.http === true;
1896
+ if ("type" in s && s.type === "sse") return mcpCaps.sse === true;
1897
+ return true;
1898
+ });
1899
+ }
1900
+ async function executeLoadSession(ctx, sessionId, mcpServers, cwd) {
1901
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
1902
+ if (!ctx.agentCapabilities.loadSession) {
1903
+ throw new ACPSessionError(
1904
+ "unsupported_capability",
1905
+ "agent does not support session/load (loadSession capability not advertised)"
1906
+ );
1756
1907
  }
1757
- /** Write a text file. Atomic via write-then-rename. */
1758
- async writeTextFile(params) {
1759
- if (typeof params.content !== "string") {
1760
- throw new FsError("INVALID_PATH", params.path, "content must be a string");
1761
- }
1762
- const byteLength = Buffer.byteLength(params.content, "utf8");
1763
- if (byteLength > this.maxWriteBytes) {
1764
- throw new FsError(
1765
- "TOO_LARGE",
1766
- params.path,
1767
- `content is ${byteLength} bytes, max write is ${this.maxWriteBytes} bytes`
1908
+ if (ctx.sessionId && ctx.sessionId !== sessionId) {
1909
+ await ctx.closeSession();
1910
+ }
1911
+ ctx.resetScratch();
1912
+ const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
1913
+ const id = ctx.allocId();
1914
+ const result = await ctx.sendRequest(id, "session/load", {
1915
+ sessionId,
1916
+ cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
1917
+ mcpServers: servers
1918
+ });
1919
+ if (isJsonRpcError(result)) {
1920
+ throw new ACPSessionError("prompt_failed", `session/load failed: ${result.message}`, result);
1921
+ }
1922
+ ctx.setSessionId(sessionId);
1923
+ }
1924
+ async function executeResumeSession(ctx, sessionId, mcpServers, cwd) {
1925
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
1926
+ if (!ctx.agentCapabilities.sessionCapabilities?.resume) {
1927
+ throw new ACPSessionError(
1928
+ "unsupported_capability",
1929
+ "agent does not support session/resume (sessionCapabilities.resume not advertised)"
1930
+ );
1931
+ }
1932
+ if (ctx.sessionId && ctx.sessionId !== sessionId) {
1933
+ await ctx.closeSession();
1934
+ }
1935
+ const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
1936
+ const id = ctx.allocId();
1937
+ const result = await ctx.sendRequest(id, "session/resume", {
1938
+ sessionId,
1939
+ cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
1940
+ mcpServers: servers
1941
+ });
1942
+ if (isJsonRpcError(result)) {
1943
+ throw new ACPSessionError("prompt_failed", `session/resume failed: ${result.message}`, result);
1944
+ }
1945
+ ctx.setSessionId(sessionId);
1946
+ }
1947
+ async function executeListSessions(ctx, cursor, cwd) {
1948
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
1949
+ if (!ctx.agentCapabilities.sessionCapabilities?.list) {
1950
+ throw new ACPSessionError(
1951
+ "unsupported_capability",
1952
+ "agent does not support session/list (sessionCapabilities.list not advertised)"
1953
+ );
1954
+ }
1955
+ const id = ctx.allocId();
1956
+ const params = {};
1957
+ if (cursor !== void 0) params.cursor = cursor;
1958
+ if (cwd !== void 0) params.cwd = cwd;
1959
+ const result = await ctx.sendRequest(id, "session/list", params);
1960
+ if (isJsonRpcError(result)) {
1961
+ throw new ACPSessionError("prompt_failed", `session/list failed: ${result.message}`, result);
1962
+ }
1963
+ const r = result;
1964
+ return {
1965
+ sessions: r.sessions ?? [],
1966
+ nextCursor: r.nextCursor
1967
+ };
1968
+ }
1969
+ async function executeDeleteSession(ctx, sessionId) {
1970
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
1971
+ if (!ctx.agentCapabilities.sessionCapabilities?.delete) {
1972
+ throw new ACPSessionError(
1973
+ "unsupported_capability",
1974
+ "agent does not support session/delete (sessionCapabilities.delete not advertised)"
1975
+ );
1976
+ }
1977
+ const id = ctx.allocId();
1978
+ const result = await ctx.sendRequest(id, "session/delete", { sessionId });
1979
+ if (isJsonRpcError(result)) {
1980
+ throw new ACPSessionError("prompt_failed", `session/delete failed: ${result.message}`, result);
1981
+ }
1982
+ if (ctx.sessionId === sessionId) {
1983
+ ctx.setSessionId(null);
1984
+ }
1985
+ }
1986
+ async function executeForkSession(ctx, sourceSessionId, cwd, mcpServers) {
1987
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
1988
+ const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
1989
+ const id = ctx.allocId();
1990
+ const result = await ctx.sendRequest(id, "session/fork", {
1991
+ sessionId: sourceSessionId,
1992
+ cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
1993
+ ...servers.length > 0 ? { mcpServers: servers } : {}
1994
+ });
1995
+ if (isJsonRpcError(result)) {
1996
+ throw new ACPSessionError("prompt_failed", `session/fork failed: ${result.message}`, result);
1997
+ }
1998
+ const newId = result.sessionId;
1999
+ if (typeof newId !== "string" || !newId) {
2000
+ throw new ACPSessionError("protocol_error", "session/fork returned no sessionId", result);
2001
+ }
2002
+ return newId;
2003
+ }
2004
+ async function executeSetMode(ctx, sessionId, modeId) {
2005
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2006
+ const id = ctx.allocId();
2007
+ const result = await ctx.sendRequest(id, "session/set_mode", { sessionId, modeId });
2008
+ if (isJsonRpcError(result)) {
2009
+ throw new ACPSessionError(
2010
+ "prompt_failed",
2011
+ `session/set_mode failed: ${result.message}`,
2012
+ result
2013
+ );
2014
+ }
2015
+ }
2016
+ async function executeSetConfigOption(ctx, sessionId, configId, value) {
2017
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2018
+ const id = ctx.allocId();
2019
+ const result = await ctx.sendRequest(id, "session/set_config_option", {
2020
+ sessionId,
2021
+ configId,
2022
+ value
2023
+ });
2024
+ if (isJsonRpcError(result)) {
2025
+ throw new ACPSessionError(
2026
+ "prompt_failed",
2027
+ `session/set_config_option failed: ${result.message}`,
2028
+ result
2029
+ );
2030
+ }
2031
+ }
2032
+ async function executeListProviders(ctx) {
2033
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2034
+ const id = ctx.allocId();
2035
+ const result = await ctx.sendRequest(id, "providers/list", {});
2036
+ if (isJsonRpcError(result)) {
2037
+ throw new ACPSessionError("prompt_failed", `providers/list failed: ${result.message}`, result);
2038
+ }
2039
+ const r = result;
2040
+ return { providers: r.providers ?? [], currentProviderId: r.currentProviderId ?? null };
2041
+ }
2042
+ async function executeSetProvider(ctx, providerId, config) {
2043
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2044
+ const id = ctx.allocId();
2045
+ const result = await ctx.sendRequest(id, "providers/set", { providerId, ...config ?? {} });
2046
+ if (isJsonRpcError(result)) {
2047
+ throw new ACPSessionError("prompt_failed", `providers/set failed: ${result.message}`, result);
2048
+ }
2049
+ }
2050
+ async function executeDisableProvider(ctx) {
2051
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2052
+ const id = ctx.allocId();
2053
+ const result = await ctx.sendRequest(id, "providers/disable", {});
2054
+ if (isJsonRpcError(result)) {
2055
+ throw new ACPSessionError(
2056
+ "prompt_failed",
2057
+ `providers/disable failed: ${result.message}`,
2058
+ result
2059
+ );
2060
+ }
2061
+ }
2062
+ async function executeMcpMessage(ctx, connectionId, message) {
2063
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2064
+ const id = ctx.allocId();
2065
+ const result = await ctx.sendRequest(id, "mcp/message", { connectionId, message });
2066
+ if (isJsonRpcError(result)) {
2067
+ throw new ACPSessionError("prompt_failed", `mcp/message failed: ${result.message}`, result);
2068
+ }
2069
+ return result;
2070
+ }
2071
+ async function executeCreateSession(ctx) {
2072
+ const servers = filterMcpServers(ctx.agentCapabilities, ctx.opts.mcpServers);
2073
+ const id = ctx.allocId();
2074
+ const result = await ctx.sendRequest(id, "session/new", {
2075
+ cwd: ctx.opts.cwd ?? ctx.opts.projectRoot,
2076
+ mcpServers: servers
2077
+ });
2078
+ if (isJsonRpcError(result)) {
2079
+ throw new ACPSessionError(
2080
+ "session_create_failed",
2081
+ `session/new failed: ${result.message}`,
2082
+ result
2083
+ );
2084
+ }
2085
+ const sessionId = result.sessionId;
2086
+ if (typeof sessionId !== "string" || sessionId.length === 0) {
2087
+ throw new ACPSessionError("protocol_error", "session/new returned no sessionId", result);
2088
+ }
2089
+ return sessionId;
2090
+ }
2091
+
2092
+ // src/client/acp-session-auth.ts
2093
+ async function authenticate(host, methodId) {
2094
+ if (host.state === "closed") {
2095
+ throw new ACPSessionError("closed", "session is closed");
2096
+ }
2097
+ if (host.state !== "ready" && host.state !== "authenticated") {
2098
+ throw new ACPSessionError(
2099
+ "protocol_error",
2100
+ `authenticate called in state=${host.state} (expected 'ready')`
2101
+ );
2102
+ }
2103
+ if (host.state === "authenticated") return;
2104
+ if (!host.authMethods.some((m) => m.id === methodId)) {
2105
+ throw new ACPSessionError(
2106
+ "auth_failed",
2107
+ `auth method "${methodId}" not in advertised methods: ${host.authMethods.map((m) => m.id).join(", ")}`
2108
+ );
2109
+ }
2110
+ if (host.authMethods.find((m) => m.id === methodId)?.type === "terminal") {
2111
+ throw new ACPSessionError(
2112
+ "auth_failed",
2113
+ "Terminal authentication requires interactive login followed by reconnect; it cannot use authenticate"
2114
+ );
2115
+ }
2116
+ const id = host.allocId();
2117
+ const result = await host.sendRequest(id, "authenticate", { methodId });
2118
+ if (isJsonRpcError(result)) {
2119
+ throw new ACPSessionError("auth_failed", `authenticate failed: ${result.message}`, result);
2120
+ }
2121
+ host.state = "authenticated";
2122
+ }
2123
+ async function logout(host) {
2124
+ if (host.state === "closed") {
2125
+ throw new ACPSessionError("closed", "session is closed");
2126
+ }
2127
+ if (!host.agentCapabilities.auth?.logout) {
2128
+ throw new ACPSessionError(
2129
+ "unsupported_capability",
2130
+ "agent does not support logout (auth.logout capability not advertised)"
2131
+ );
2132
+ }
2133
+ const id = host.allocId();
2134
+ const result = await host.sendRequest(id, "logout", {});
2135
+ if (isJsonRpcError(result)) {
2136
+ throw new ACPSessionError("logout_failed", `logout failed: ${result.message}`, result);
2137
+ }
2138
+ host.state = "ready";
2139
+ }
2140
+ async function createSessionWithAuth(host) {
2141
+ try {
2142
+ return await executeCreateSession(host.opContext());
2143
+ } catch (err) {
2144
+ if (host.state === "authenticated" || !isAuthRequiredError(err)) {
2145
+ throw err instanceof ACPSessionError ? err : new ACPSessionError(
2146
+ "session_create_failed",
2147
+ err instanceof Error ? err.message : String(err),
2148
+ err
2149
+ );
2150
+ }
2151
+ await host.ensureAuthenticated();
2152
+ return executeCreateSession(host.opContext());
2153
+ }
2154
+ }
2155
+ async function ensureAuthenticated(host) {
2156
+ if (host.state === "authenticated") return;
2157
+ if (host.authMethods.length === 0) {
2158
+ throw new ACPSessionError(
2159
+ "auth_failed",
2160
+ "This agent requires authentication before a session can start, but advertised no authMethods. Log into the CLI, then retry."
2161
+ );
2162
+ }
2163
+ const inProcess = host.authMethods.find(
2164
+ (m) => m.type === void 0 || m.type === "agent" || m.type === "oauth" || m.type === "http"
2165
+ );
2166
+ if (inProcess) {
2167
+ await host.authenticate(inProcess.id);
2168
+ return;
2169
+ }
2170
+ const terminal = host.authMethods.find((m) => m.type === "terminal");
2171
+ const setupArgs = terminal?.args?.length ? terminal.args.join(" ") : void 0;
2172
+ const setup = setupArgs !== void 0 ? `${host.opts.command}${host.opts.args?.length ? ` ${host.opts.args.join(" ")}` : ""} ${setupArgs}` : `${host.opts.command}${host.opts.args?.length ? ` ${host.opts.args.join(" ")}` : ""}`;
2173
+ throw new ACPSessionError(
2174
+ "auth_failed",
2175
+ `This agent requires a terminal login before ACP can start. Run \`${setup}\` (or the CLI's /login), then retry.`
2176
+ );
2177
+ }
2178
+
2179
+ // src/client/file-server.ts
2180
+ import { randomBytes } from "node:crypto";
2181
+ import { realpathSync } from "node:fs";
2182
+ import * as fsp2 from "node:fs/promises";
2183
+ import * as path2 from "node:path";
2184
+ var DEFAULT_FILE_OPERATIONS = {
2185
+ stat: fsp2.stat,
2186
+ readFile: fsp2.readFile,
2187
+ writeFile: fsp2.writeFile,
2188
+ realpath: fsp2.realpath,
2189
+ rename: fsp2.rename,
2190
+ unlink: fsp2.unlink
2191
+ };
2192
+ var DEFAULT_MAX_READ_BYTES = 5 * 1024 * 1024;
2193
+ var DEFAULT_MAX_WRITE_BYTES = 5 * 1024 * 1024;
2194
+ var FsError = class extends Error {
2195
+ code;
2196
+ path;
2197
+ constructor(code, path5, message) {
2198
+ super(message);
2199
+ this.name = "FsError";
2200
+ this.code = code;
2201
+ this.path = path5;
2202
+ }
2203
+ };
2204
+ var FileServer = class {
2205
+ root;
2206
+ realRoot;
2207
+ timeoutMs;
2208
+ maxReadBytes;
2209
+ maxWriteBytes;
2210
+ operations;
2211
+ constructor(opts) {
2212
+ this.root = path2.resolve(opts.projectRoot);
2213
+ this.realRoot = safeRealpathSync(this.root);
2214
+ this.timeoutMs = opts.timeoutMs ?? 3e4;
2215
+ this.maxReadBytes = opts.maxReadBytes ?? DEFAULT_MAX_READ_BYTES;
2216
+ this.maxWriteBytes = opts.maxWriteBytes ?? DEFAULT_MAX_WRITE_BYTES;
2217
+ this.operations = opts.operations ?? DEFAULT_FILE_OPERATIONS;
2218
+ }
2219
+ /** Read a text file. Returns the content as a string. */
2220
+ async readTextFile(params) {
2221
+ if (params.line != null && (!Number.isSafeInteger(params.line) || params.line < 1) || params.limit != null && (!Number.isSafeInteger(params.limit) || params.limit < 0)) {
2222
+ throw new FsError(
2223
+ "INVALID_PATH",
2224
+ params.path,
2225
+ "line must be a positive integer and limit a non-negative integer"
2226
+ );
2227
+ }
2228
+ const safe = await this.resolveInside(params.path);
2229
+ const controller = new AbortController();
2230
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
2231
+ try {
2232
+ const stat3 = await this.operations.stat(safe).catch((err) => {
2233
+ throw mapFsError(err, safe);
2234
+ });
2235
+ if (stat3.size > this.maxReadBytes) {
2236
+ throw new FsError(
2237
+ "TOO_LARGE",
2238
+ safe,
2239
+ `file is ${stat3.size} bytes, max read is ${this.maxReadBytes} bytes`
2240
+ );
2241
+ }
2242
+ const content = await this.operations.readFile(safe, {
2243
+ encoding: "utf8",
2244
+ signal: controller.signal
2245
+ });
2246
+ if (params.line == null && params.limit == null) return { content };
2247
+ const lines = content.match(/[^\n]*\n|[^\n]+$/g) ?? [];
2248
+ const start = (params.line ?? 1) - 1;
2249
+ return {
2250
+ content: lines.slice(start, params.limit == null ? void 0 : start + params.limit).join("")
2251
+ };
2252
+ } catch (err) {
2253
+ if (err instanceof FsError) throw err;
2254
+ if (controller.signal.aborted) {
2255
+ throw new FsError("TIMEOUT", safe, `readTextFile timed out after ${this.timeoutMs}ms`);
2256
+ }
2257
+ throw mapFsError(err, safe);
2258
+ } finally {
2259
+ clearTimeout(timer);
2260
+ }
2261
+ }
2262
+ /** Write a text file. Atomic via write-then-rename. */
2263
+ async writeTextFile(params) {
2264
+ if (typeof params.content !== "string") {
2265
+ throw new FsError("INVALID_PATH", params.path, "content must be a string");
2266
+ }
2267
+ const byteLength = Buffer.byteLength(params.content, "utf8");
2268
+ if (byteLength > this.maxWriteBytes) {
2269
+ throw new FsError(
2270
+ "TOO_LARGE",
2271
+ params.path,
2272
+ `content is ${byteLength} bytes, max write is ${this.maxWriteBytes} bytes`
1768
2273
  );
1769
2274
  }
1770
2275
  const safe = await this.resolveInside(params.path);
@@ -1848,7 +2353,8 @@ var FileServer = class {
1848
2353
  throw mapFsError(err, resolvedPath);
1849
2354
  }
1850
2355
  const normReal = isWindows ? real.toLowerCase() : real;
1851
- if (normReal === normRealRoot || normReal.startsWith(normRealRoot + path2.sep)) return;
2356
+ const realRootWithSep = normRealRoot.endsWith(path2.sep) ? normRealRoot : normRealRoot + path2.sep;
2357
+ if (normReal === normRealRoot || normReal.startsWith(realRootWithSep)) return;
1852
2358
  throw new FsError(
1853
2359
  "OUTSIDE_ROOT",
1854
2360
  resolvedPath,
@@ -1897,9 +2403,12 @@ async function handleAcpPermissionRequest(msg, permissionPolicy, sender, callbac
1897
2403
  );
1898
2404
  await sender.sendResult(id, { outcome });
1899
2405
  } catch (err) {
2406
+ if (isAbortLikeError(err)) {
2407
+ await sender.sendResult(id, { outcome: { outcome: "cancelled" } });
2408
+ return;
2409
+ }
1900
2410
  const message = err instanceof Error ? err.message : String(err);
1901
- const code = isAbortLikeError(err) ? -32800 : -32603;
1902
- await sender.sendErrorResponse(id, code, `permission policy failed: ${message}`);
2411
+ await sender.sendErrorResponse(id, -32603, `permission policy failed: ${message}`);
1903
2412
  }
1904
2413
  }
1905
2414
  async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender, callbackOptions = {}) {
@@ -1935,7 +2444,9 @@ async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender, cal
1935
2444
  if (msg.method === "fs/read_text_file") {
1936
2445
  const result = await fileServer.readTextFile({
1937
2446
  sessionId: params.sessionId ?? "",
1938
- path: params.path
2447
+ path: params.path,
2448
+ line: params.line,
2449
+ limit: params.limit
1939
2450
  });
1940
2451
  await sender.sendResult(id, result);
1941
2452
  } else {
@@ -2037,336 +2548,105 @@ async function authorizeAcpCallback(permissionPolicy, partial, callbackOptions)
2037
2548
  try {
2038
2549
  const outcome = await runPermissionWithDeadline(
2039
2550
  permissionPolicy,
2040
- {
2041
- toolCall: {
2042
- sessionUpdate: "tool_call_update",
2043
- toolCallId: partial.toolCallId,
2044
- title: partial.title,
2045
- kind: partial.kind,
2046
- status: "pending",
2047
- rawInput: partial.rawInput
2048
- },
2049
- options: [
2050
- { optionId: "allow", name: "Allow", kind: "allow_once" },
2051
- { optionId: "reject", name: "Reject", kind: "reject_once" }
2052
- ]
2053
- },
2054
- callbackOptions
2055
- );
2056
- return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always" ? "allowed" : "denied";
2057
- } catch (err) {
2058
- return isAbortLikeError(err) ? "cancelled" : "denied";
2059
- }
2060
- }
2061
- async function runPermissionWithDeadline(permissionPolicy, call, callbackOptions) {
2062
- const timeoutMs = resolvePermissionDeadline(callbackOptions.permissionTimeoutMs);
2063
- const controller = new AbortController();
2064
- const abort = () => controller.abort();
2065
- const timer = timeoutMs === null ? null : setTimeout(abort, timeoutMs);
2066
- let removeAbort;
2067
- if (callbackOptions.signal) {
2068
- if (callbackOptions.signal.aborted) {
2069
- abort();
2070
- } else {
2071
- callbackOptions.signal.addEventListener("abort", abort, { once: true });
2072
- removeAbort = () => callbackOptions.signal?.removeEventListener("abort", abort);
2073
- }
2074
- }
2075
- try {
2076
- return await Promise.race([
2077
- permissionPolicy({
2078
- toolCall: call.toolCall,
2079
- options: call.options,
2080
- signal: controller.signal
2081
- }),
2082
- rejectOnAbort(controller.signal)
2083
- ]);
2084
- } finally {
2085
- if (timer !== null) clearTimeout(timer);
2086
- removeAbort?.();
2087
- }
2088
- }
2089
- function rejectOnAbort(signal) {
2090
- return new Promise((_, reject) => {
2091
- const rejectAbort = () => reject(new Error("permission request cancelled or timed out"));
2092
- if (signal.aborted) {
2093
- rejectAbort();
2094
- return;
2095
- }
2096
- signal.addEventListener("abort", rejectAbort, { once: true });
2097
- });
2098
- }
2099
- function isAbortLikeError(err) {
2100
- return err instanceof Error && /cancelled|canceled|timed out|aborted/i.test(err.message);
2101
- }
2102
- function resolvePermissionDeadline(value) {
2103
- if (value === Number.POSITIVE_INFINITY) return null;
2104
- if (value !== void 0 && Number.isFinite(value) && value > 0) return Math.trunc(value);
2105
- return DEFAULT_PERMISSION_TIMEOUT_MS;
2106
- }
2107
-
2108
- // src/client/acp-session-content.ts
2109
- function textContent(text) {
2110
- return { type: "text", text };
2111
- }
2112
- function imageContent(mimeType, data) {
2113
- return { type: "image", mimeType, data };
2114
- }
2115
- function audioContent(mimeType, data) {
2116
- return { type: "audio", mimeType, data };
2117
- }
2118
- function extractText(block) {
2119
- if (typeof block !== "object" || block === null) return "";
2120
- const b = block;
2121
- if (b.type === "text" && typeof b.text === "string") return b.text;
2122
- if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
2123
- return b.resource.text;
2124
- }
2125
- return "";
2126
- }
2127
- function isRecord(v) {
2128
- return typeof v === "object" && v !== null && !Array.isArray(v);
2129
- }
2130
- function emptyRunResult(stopReason) {
2131
- return {
2132
- text: "",
2133
- stopReason,
2134
- hasText: false,
2135
- toolCalls: [],
2136
- diffs: [],
2137
- thoughts: ""
2138
- };
2139
- }
2140
-
2141
- // src/client/acp-session-errors.ts
2142
- var ACPSessionError = class extends Error {
2143
- kind;
2144
- cause;
2145
- constructor(kind, message, cause) {
2146
- super(message);
2147
- this.name = "ACPSessionError";
2148
- this.kind = kind;
2149
- this.cause = cause;
2150
- }
2151
- };
2152
- function isJsonRpcError(v) {
2153
- return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
2154
- }
2155
- function isAuthRequiredError(err) {
2156
- const message = err instanceof Error ? err.message : String(err);
2157
- if (/auth(_|-)?required|authentication required/i.test(message)) return true;
2158
- const cause = err instanceof ACPSessionError ? err.cause : err && typeof err === "object" && "cause" in err ? err.cause : err;
2159
- if (!cause || typeof cause !== "object") return false;
2160
- const data = cause.data;
2161
- if (data === "auth_required" || data === "AUTH_REQUIRED") return true;
2162
- if (data && typeof data === "object") {
2163
- const d = data;
2164
- if (d.authRequired === true) return true;
2165
- if (d.code === "auth_required" || d.code === "AUTH_REQUIRED") return true;
2166
- }
2167
- return false;
2168
- }
2169
-
2170
- // src/client/acp-session-ops.ts
2171
- function filterMcpServers(agentCapabilities, servers) {
2172
- if (!servers || servers.length === 0) return [];
2173
- const mcpCaps = agentCapabilities.mcpCapabilities ?? {};
2174
- return servers.filter((s) => {
2175
- if ("type" in s && s.type === "http") return mcpCaps.http === true;
2176
- if ("type" in s && s.type === "sse") return mcpCaps.sse === true;
2177
- return true;
2178
- });
2179
- }
2180
- async function executeLoadSession(ctx, sessionId, mcpServers, cwd) {
2181
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2182
- if (!ctx.agentCapabilities.loadSession) {
2183
- throw new ACPSessionError(
2184
- "unsupported_capability",
2185
- "agent does not support session/load (loadSession capability not advertised)"
2186
- );
2187
- }
2188
- if (ctx.sessionId) {
2189
- await ctx.closeSession();
2190
- }
2191
- ctx.resetScratch();
2192
- const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
2193
- const id = ctx.allocId();
2194
- const result = await ctx.sendRequest(id, "session/load", {
2195
- sessionId,
2196
- cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
2197
- mcpServers: servers
2198
- });
2199
- if (isJsonRpcError(result)) {
2200
- throw new ACPSessionError("prompt_failed", `session/load failed: ${result.message}`, result);
2201
- }
2202
- ctx.setSessionId(sessionId);
2203
- }
2204
- async function executeResumeSession(ctx, sessionId, mcpServers, cwd) {
2205
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2206
- if (!ctx.agentCapabilities.sessionCapabilities?.resume) {
2207
- throw new ACPSessionError(
2208
- "unsupported_capability",
2209
- "agent does not support session/resume (sessionCapabilities.resume not advertised)"
2210
- );
2211
- }
2212
- if (ctx.sessionId) {
2213
- await ctx.closeSession();
2214
- }
2215
- const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
2216
- const id = ctx.allocId();
2217
- const result = await ctx.sendRequest(id, "session/resume", {
2218
- sessionId,
2219
- cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
2220
- mcpServers: servers
2221
- });
2222
- if (isJsonRpcError(result)) {
2223
- throw new ACPSessionError("prompt_failed", `session/resume failed: ${result.message}`, result);
2224
- }
2225
- ctx.setSessionId(sessionId);
2226
- }
2227
- async function executeListSessions(ctx, cursor, cwd) {
2228
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2229
- if (!ctx.agentCapabilities.sessionCapabilities?.list) {
2230
- throw new ACPSessionError(
2231
- "unsupported_capability",
2232
- "agent does not support session/list (sessionCapabilities.list not advertised)"
2233
- );
2234
- }
2235
- const id = ctx.allocId();
2236
- const params = {};
2237
- if (cursor !== void 0) params.cursor = cursor;
2238
- if (cwd !== void 0) params.cwd = cwd;
2239
- const result = await ctx.sendRequest(id, "session/list", params);
2240
- if (isJsonRpcError(result)) {
2241
- throw new ACPSessionError("prompt_failed", `session/list failed: ${result.message}`, result);
2242
- }
2243
- const r = result;
2244
- return {
2245
- sessions: r.sessions ?? [],
2246
- nextCursor: r.nextCursor
2247
- };
2248
- }
2249
- async function executeDeleteSession(ctx, sessionId) {
2250
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2251
- if (!ctx.agentCapabilities.sessionCapabilities?.delete) {
2252
- throw new ACPSessionError(
2253
- "unsupported_capability",
2254
- "agent does not support session/delete (sessionCapabilities.delete not advertised)"
2551
+ {
2552
+ toolCall: {
2553
+ sessionUpdate: "tool_call_update",
2554
+ toolCallId: partial.toolCallId,
2555
+ title: partial.title,
2556
+ kind: partial.kind,
2557
+ status: "pending",
2558
+ rawInput: partial.rawInput
2559
+ },
2560
+ options: [
2561
+ { optionId: "allow", name: "Allow", kind: "allow_once" },
2562
+ { optionId: "reject", name: "Reject", kind: "reject_once" }
2563
+ ]
2564
+ },
2565
+ callbackOptions
2255
2566
  );
2567
+ return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always" ? "allowed" : "denied";
2568
+ } catch (err) {
2569
+ return isAbortLikeError(err) ? "cancelled" : "denied";
2256
2570
  }
2257
- const id = ctx.allocId();
2258
- const result = await ctx.sendRequest(id, "session/delete", { sessionId });
2259
- if (isJsonRpcError(result)) {
2260
- throw new ACPSessionError("prompt_failed", `session/delete failed: ${result.message}`, result);
2571
+ }
2572
+ async function runPermissionWithDeadline(permissionPolicy, call, callbackOptions) {
2573
+ const timeoutMs = resolvePermissionDeadline(callbackOptions.permissionTimeoutMs);
2574
+ const controller = new AbortController();
2575
+ const abort = () => controller.abort();
2576
+ const timer = timeoutMs === null ? null : setTimeout(abort, timeoutMs);
2577
+ let removeAbort;
2578
+ if (callbackOptions.signal) {
2579
+ if (callbackOptions.signal.aborted) {
2580
+ abort();
2581
+ } else {
2582
+ callbackOptions.signal.addEventListener("abort", abort, { once: true });
2583
+ removeAbort = () => callbackOptions.signal?.removeEventListener("abort", abort);
2584
+ }
2261
2585
  }
2262
- if (ctx.sessionId === sessionId) {
2263
- ctx.setSessionId(null);
2586
+ try {
2587
+ return await Promise.race([
2588
+ permissionPolicy({
2589
+ toolCall: call.toolCall,
2590
+ options: call.options,
2591
+ signal: controller.signal
2592
+ }),
2593
+ rejectOnAbort(controller.signal)
2594
+ ]);
2595
+ } finally {
2596
+ if (timer !== null) clearTimeout(timer);
2597
+ removeAbort?.();
2264
2598
  }
2265
2599
  }
2266
- async function executeForkSession(ctx, sourceSessionId, cwd, mcpServers) {
2267
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2268
- const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
2269
- const id = ctx.allocId();
2270
- const result = await ctx.sendRequest(id, "session/fork", {
2271
- sessionId: sourceSessionId,
2272
- cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
2273
- ...servers.length > 0 ? { mcpServers: servers } : {}
2600
+ function rejectOnAbort(signal) {
2601
+ return new Promise((_, reject) => {
2602
+ const rejectAbort = () => reject(new Error("permission request cancelled or timed out"));
2603
+ if (signal.aborted) {
2604
+ rejectAbort();
2605
+ return;
2606
+ }
2607
+ signal.addEventListener("abort", rejectAbort, { once: true });
2274
2608
  });
2275
- if (isJsonRpcError(result)) {
2276
- throw new ACPSessionError("prompt_failed", `session/fork failed: ${result.message}`, result);
2277
- }
2278
- const newId = result.sessionId;
2279
- if (typeof newId !== "string" || !newId) {
2280
- throw new ACPSessionError("protocol_error", "session/fork returned no sessionId", result);
2281
- }
2282
- return newId;
2283
2609
  }
2284
- async function executeSetMode(ctx, sessionId, modeId) {
2285
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2286
- const id = ctx.allocId();
2287
- const result = await ctx.sendRequest(id, "session/set_mode", { sessionId, modeId });
2288
- if (isJsonRpcError(result)) {
2289
- throw new ACPSessionError(
2290
- "prompt_failed",
2291
- `session/set_mode failed: ${result.message}`,
2292
- result
2293
- );
2294
- }
2610
+ function isAbortLikeError(err) {
2611
+ return err instanceof Error && /cancelled|canceled|timed out|aborted/i.test(err.message);
2295
2612
  }
2296
- async function executeSetConfigOption(ctx, sessionId, configId, value) {
2297
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2298
- const id = ctx.allocId();
2299
- const result = await ctx.sendRequest(id, "session/set_config_option", {
2300
- sessionId,
2301
- configId,
2302
- value
2303
- });
2304
- if (isJsonRpcError(result)) {
2305
- throw new ACPSessionError(
2306
- "prompt_failed",
2307
- `session/set_config_option failed: ${result.message}`,
2308
- result
2309
- );
2310
- }
2613
+ function resolvePermissionDeadline(value) {
2614
+ if (value === Number.POSITIVE_INFINITY) return null;
2615
+ if (value !== void 0 && Number.isFinite(value) && value > 0) return Math.trunc(value);
2616
+ return DEFAULT_PERMISSION_TIMEOUT_MS;
2311
2617
  }
2312
- async function executeListProviders(ctx) {
2313
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2314
- const id = ctx.allocId();
2315
- const result = await ctx.sendRequest(id, "providers/list", {});
2316
- if (isJsonRpcError(result)) {
2317
- throw new ACPSessionError("prompt_failed", `providers/list failed: ${result.message}`, result);
2318
- }
2319
- const r = result;
2320
- return { providers: r.providers ?? [], currentProviderId: r.currentProviderId ?? null };
2618
+
2619
+ // src/client/acp-session-content.ts
2620
+ function textContent(text) {
2621
+ return { type: "text", text };
2321
2622
  }
2322
- async function executeSetProvider(ctx, providerId, config) {
2323
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2324
- const id = ctx.allocId();
2325
- const result = await ctx.sendRequest(id, "providers/set", { providerId, ...config ?? {} });
2326
- if (isJsonRpcError(result)) {
2327
- throw new ACPSessionError("prompt_failed", `providers/set failed: ${result.message}`, result);
2328
- }
2623
+ function imageContent(mimeType, data) {
2624
+ return { type: "image", mimeType, data };
2329
2625
  }
2330
- async function executeDisableProvider(ctx) {
2331
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2332
- const id = ctx.allocId();
2333
- const result = await ctx.sendRequest(id, "providers/disable", {});
2334
- if (isJsonRpcError(result)) {
2335
- throw new ACPSessionError(
2336
- "prompt_failed",
2337
- `providers/disable failed: ${result.message}`,
2338
- result
2339
- );
2340
- }
2626
+ function audioContent(mimeType, data) {
2627
+ return { type: "audio", mimeType, data };
2341
2628
  }
2342
- async function executeMcpMessage(ctx, connectionId, message) {
2343
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
2344
- const id = ctx.allocId();
2345
- const result = await ctx.sendRequest(id, "mcp/message", { connectionId, message });
2346
- if (isJsonRpcError(result)) {
2347
- throw new ACPSessionError("prompt_failed", `mcp/message failed: ${result.message}`, result);
2629
+ function extractText(block) {
2630
+ if (typeof block !== "object" || block === null) return "";
2631
+ const b = block;
2632
+ if (b.type === "text" && typeof b.text === "string") return b.text;
2633
+ if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
2634
+ return b.resource.text;
2348
2635
  }
2349
- return result;
2636
+ return "";
2350
2637
  }
2351
- async function executeCreateSession(ctx) {
2352
- const servers = filterMcpServers(ctx.agentCapabilities, ctx.opts.mcpServers);
2353
- const id = ctx.allocId();
2354
- const result = await ctx.sendRequest(id, "session/new", {
2355
- cwd: ctx.opts.cwd ?? ctx.opts.projectRoot,
2356
- mcpServers: servers
2357
- });
2358
- if (isJsonRpcError(result)) {
2359
- throw new ACPSessionError(
2360
- "session_create_failed",
2361
- `session/new failed: ${result.message}`,
2362
- result
2363
- );
2364
- }
2365
- const sessionId = result.sessionId;
2366
- if (typeof sessionId !== "string" || sessionId.length === 0) {
2367
- throw new ACPSessionError("protocol_error", "session/new returned no sessionId", result);
2368
- }
2369
- return sessionId;
2638
+ function isRecord(v) {
2639
+ return typeof v === "object" && v !== null && !Array.isArray(v);
2640
+ }
2641
+ function emptyRunResult(stopReason) {
2642
+ return {
2643
+ text: "",
2644
+ stopReason,
2645
+ hasText: false,
2646
+ toolCalls: [],
2647
+ diffs: [],
2648
+ thoughts: ""
2649
+ };
2370
2650
  }
2371
2651
 
2372
2652
  // src/client/acp-session-updates.ts
@@ -2387,6 +2667,7 @@ function handleAcpSessionUpdate(msg, scratch, emitProgress) {
2387
2667
  }
2388
2668
  return;
2389
2669
  }
2670
+ case "agent_thought_chunk":
2390
2671
  case "thought_chunk": {
2391
2672
  const text = extractText(u.content);
2392
2673
  if (text) {
@@ -2584,7 +2865,7 @@ var TerminalServer = class {
2584
2865
  truncated: false,
2585
2866
  exitStatus: void 0,
2586
2867
  timeoutHandle: null,
2587
- exitPromise: new Promise((resolve4) => {
2868
+ exitPromise: new Promise((resolve5) => {
2588
2869
  proc.on("close", (code, signalName) => {
2589
2870
  if (state.timeoutHandle) {
2590
2871
  clearTimeout(state.timeoutHandle);
@@ -2595,7 +2876,7 @@ var TerminalServer = class {
2595
2876
  signal: typeof signalName === "string" ? signalName : null
2596
2877
  };
2597
2878
  state.exitStatus = exitStatus;
2598
- resolve4(exitStatus);
2879
+ resolve5(exitStatus);
2599
2880
  });
2600
2881
  proc.on("error", (err) => {
2601
2882
  if (state.timeoutHandle) {
@@ -2614,7 +2895,7 @@ var TerminalServer = class {
2614
2895
  }
2615
2896
  state.outputChunks.push(errorOutput);
2616
2897
  state.retainedBytes = errorOutput.length;
2617
- resolve4(exitStatus);
2898
+ resolve5(exitStatus);
2618
2899
  });
2619
2900
  })
2620
2901
  };
@@ -2921,7 +3202,7 @@ var WebSocketClientTransport = class {
2921
3202
  );
2922
3203
  }
2923
3204
  const timeoutMs = this.opts.handshakeTimeoutMs ?? 3e4;
2924
- return new Promise((resolve4, reject) => {
3205
+ return new Promise((resolve5, reject) => {
2925
3206
  const ws = new WS(this.opts.url, this.opts.protocols);
2926
3207
  this.ws = ws;
2927
3208
  const timer = setTimeout(() => {
@@ -2937,7 +3218,7 @@ var WebSocketClientTransport = class {
2937
3218
  }
2938
3219
  pending.reject(new Error(`WebSocket failed to open within ${timeoutMs}ms`));
2939
3220
  }, timeoutMs);
2940
- this.pendingStart = { resolve: resolve4, reject, timer };
3221
+ this.pendingStart = { resolve: resolve5, reject, timer };
2941
3222
  ws.addEventListener("open", () => {
2942
3223
  const pending = this.pendingStart;
2943
3224
  if (pending === null) return;
@@ -3052,7 +3333,6 @@ function finitePositiveLimit(value, fallback) {
3052
3333
  }
3053
3334
 
3054
3335
  // src/client/acp-session.ts
3055
- var LATE_CANCEL_SEND_TIMEOUT_MS = 1e4;
3056
3336
  var ACPSession = class _ACPSession {
3057
3337
  transport;
3058
3338
  fileServer;
@@ -3222,7 +3502,7 @@ var ACPSession = class _ACPSession {
3222
3502
  throw new ACPSessionError("protocol_error", "initialize returned no protocolVersion");
3223
3503
  }
3224
3504
  const r = result;
3225
- if (r.protocolVersion > ACP_PROTOCOL_VERSION) {
3505
+ if (r.protocolVersion !== ACP_PROTOCOL_VERSION) {
3226
3506
  throw new ACPSessionError(
3227
3507
  "unsupported_capability",
3228
3508
  `agent requires protocolVersion=${r.protocolVersion}, client supports up to ${ACP_PROTOCOL_VERSION}`
@@ -3242,49 +3522,14 @@ var ACPSession = class _ACPSession {
3242
3522
  * Call this AFTER start() and BEFORE any session/new call.
3243
3523
  */
3244
3524
  async authenticate(methodId) {
3245
- if (this.state === "closed") {
3246
- throw new ACPSessionError("closed", "session is closed");
3247
- }
3248
- if (this.state !== "ready" && this.state !== "authenticated") {
3249
- throw new ACPSessionError(
3250
- "protocol_error",
3251
- `authenticate called in state=${this.state} (expected 'ready')`
3252
- );
3253
- }
3254
- if (this.state === "authenticated") return;
3255
- if (!this.authMethods.some((m) => m.id === methodId)) {
3256
- throw new ACPSessionError(
3257
- "auth_failed",
3258
- `auth method "${methodId}" not in advertised methods: ${this.authMethods.map((m) => m.id).join(", ")}`
3259
- );
3260
- }
3261
- const id = this.allocId();
3262
- const result = await this.sendRequest(id, "authenticate", { methodId });
3263
- if (isJsonRpcError(result)) {
3264
- throw new ACPSessionError("auth_failed", `authenticate failed: ${result.message}`, result);
3265
- }
3266
- this.state = "authenticated";
3525
+ return authenticate(this.acpSessionAuthHost(), methodId);
3267
3526
  }
3268
3527
  /**
3269
3528
  * Log out from the current authenticated session.
3270
3529
  * Only callable if the agent advertises `auth.logout` capability.
3271
3530
  */
3272
3531
  async logout() {
3273
- if (this.state === "closed") {
3274
- throw new ACPSessionError("closed", "session is closed");
3275
- }
3276
- if (!this.agentCapabilities.auth?.logout) {
3277
- throw new ACPSessionError(
3278
- "unsupported_capability",
3279
- "agent does not support logout (auth.logout capability not advertised)"
3280
- );
3281
- }
3282
- const id = this.allocId();
3283
- const result = await this.sendRequest(id, "logout", {});
3284
- if (isJsonRpcError(result)) {
3285
- throw new ACPSessionError("logout_failed", `logout failed: ${result.message}`, result);
3286
- }
3287
- this.state = "ready";
3532
+ return logout(this.acpSessionAuthHost());
3288
3533
  }
3289
3534
  // ──────────────────────────────────────────────────────────────────────
3290
3535
  // Session management delegation
@@ -3353,6 +3598,16 @@ var ACPSession = class _ACPSession {
3353
3598
  if (signal.aborted) {
3354
3599
  return emptyRunResult("cancelled");
3355
3600
  }
3601
+ const caps = this.agentCapabilities.promptCapabilities;
3602
+ for (const block of blocks) {
3603
+ const supported = block.type === "image" ? caps?.image : block.type === "audio" ? caps?.audio : block.type === "resource" ? caps?.embeddedContext : true;
3604
+ if (supported !== true) {
3605
+ throw new ACPSessionError(
3606
+ "unsupported_capability",
3607
+ `agent does not support ${block.type} prompt content`
3608
+ );
3609
+ }
3610
+ }
3356
3611
  let cancelled = false;
3357
3612
  this.promptCallbackAbort = new AbortController();
3358
3613
  const onAbort = () => {
@@ -3487,50 +3742,7 @@ var ACPSession = class _ACPSession {
3487
3742
  * being silently swallowed.
3488
3743
  */
3489
3744
  cancelLateSession(createPromise) {
3490
- createPromise.then(async (lateId) => {
3491
- try {
3492
- await new Promise((resolve4, reject) => {
3493
- const timer = setTimeout(() => {
3494
- reject(new Error("late session/cancel send timed out"));
3495
- }, LATE_CANCEL_SEND_TIMEOUT_MS);
3496
- Promise.resolve(
3497
- this.transport.send({
3498
- jsonrpc: "2.0",
3499
- method: "session/cancel",
3500
- params: { sessionId: lateId }
3501
- })
3502
- ).then(
3503
- () => {
3504
- clearTimeout(timer);
3505
- resolve4();
3506
- },
3507
- (sendErr) => {
3508
- clearTimeout(timer);
3509
- reject(sendErr instanceof Error ? sendErr : new Error(String(sendErr)));
3510
- }
3511
- );
3512
- });
3513
- } catch (err) {
3514
- const message = err instanceof Error ? err.message : String(err);
3515
- console.warn(
3516
- JSON.stringify({
3517
- level: "warn",
3518
- event: "acp_session.late_cancel_failed",
3519
- sessionId: lateId,
3520
- message
3521
- })
3522
- );
3523
- }
3524
- }).catch((reason) => {
3525
- const message = reason instanceof Error ? reason.message : String(reason);
3526
- console.warn(
3527
- JSON.stringify({
3528
- level: "warn",
3529
- event: "acp_session.late_cancel_failed",
3530
- reason: message
3531
- })
3532
- );
3533
- });
3745
+ cancelLateAcpSession(this.transport, createPromise);
3534
3746
  }
3535
3747
  async closeSession() {
3536
3748
  if (!this.sessionId) return;
@@ -3577,10 +3789,16 @@ var ACPSession = class _ACPSession {
3577
3789
  // Wire layer
3578
3790
  // ────────────────────────────────────────────────────────────────────
3579
3791
  allocId() {
3580
- return this.nextId++;
3792
+ let candidate = Number.isSafeInteger(this.nextId) && this.nextId >= 1 ? this.nextId : 1;
3793
+ for (let probe = 0; probe <= this.pending.size; probe++) {
3794
+ this.nextId = candidate >= Number.MAX_SAFE_INTEGER ? 1 : candidate + 1;
3795
+ if (!this.pending.has(candidate)) return candidate;
3796
+ candidate = this.nextId;
3797
+ }
3798
+ throw new ACPSessionError("protocol_error", "no JSON-RPC request id is available");
3581
3799
  }
3582
3800
  async sendRequest(id, method, params, timeoutMs) {
3583
- return new Promise((resolve4, reject) => {
3801
+ return new Promise((resolve5, reject) => {
3584
3802
  const effectiveTimeout = timeoutMs ?? this.timeoutMs;
3585
3803
  const handle = setTimeout(() => {
3586
3804
  this.pending.delete(id);
@@ -3590,7 +3808,7 @@ var ACPSession = class _ACPSession {
3590
3808
  }, effectiveTimeout);
3591
3809
  this.pending.set(id, {
3592
3810
  method,
3593
- resolve: resolve4,
3811
+ resolve: resolve5,
3594
3812
  reject,
3595
3813
  timeoutMs: effectiveTimeout,
3596
3814
  timeoutHandle: handle
@@ -3609,19 +3827,7 @@ var ACPSession = class _ACPSession {
3609
3827
  * `authMethods`; we do not pop OAuth on every spawn.
3610
3828
  */
3611
3829
  async createSessionWithAuth() {
3612
- try {
3613
- return await executeCreateSession(this.opContext());
3614
- } catch (err) {
3615
- if (this.state === "authenticated" || !isAuthRequiredError(err)) {
3616
- throw err instanceof ACPSessionError ? err : new ACPSessionError(
3617
- "session_create_failed",
3618
- err instanceof Error ? err.message : String(err),
3619
- err
3620
- );
3621
- }
3622
- await this.ensureAuthenticated();
3623
- return executeCreateSession(this.opContext());
3624
- }
3830
+ return createSessionWithAuth(this.acpSessionAuthHost());
3625
3831
  }
3626
3832
  /**
3627
3833
  * Pick a non-terminal auth method and run `authenticate`. Terminal-only
@@ -3629,27 +3835,7 @@ var ACPSession = class _ACPSession {
3629
3835
  * we refuse rather than hang a TUI inside the JSON-RPC child.
3630
3836
  */
3631
3837
  async ensureAuthenticated() {
3632
- if (this.state === "authenticated") return;
3633
- if (this.authMethods.length === 0) {
3634
- throw new ACPSessionError(
3635
- "auth_failed",
3636
- "This agent requires authentication before a session can start, but advertised no authMethods. Log into the CLI, then retry."
3637
- );
3638
- }
3639
- const inProcess = this.authMethods.find(
3640
- (m) => m.type === void 0 || m.type === "agent" || m.type === "oauth" || m.type === "http"
3641
- );
3642
- if (inProcess) {
3643
- await this.authenticate(inProcess.id);
3644
- return;
3645
- }
3646
- const terminal = this.authMethods.find((m) => m.type === "terminal");
3647
- const setupArgs = terminal?.args?.length ? terminal.args.join(" ") : void 0;
3648
- const setup = setupArgs !== void 0 ? `${this.opts.command} ${setupArgs}` : `${this.opts.command}${this.opts.args?.length ? ` ${this.opts.args.join(" ")}` : ""}`;
3649
- throw new ACPSessionError(
3650
- "auth_failed",
3651
- `This agent requires a terminal login before ACP can start. Run \`${setup}\` (or the CLI's /login), then retry.`
3652
- );
3838
+ return ensureAuthenticated(this.acpSessionAuthHost());
3653
3839
  }
3654
3840
  sendResult(id, result) {
3655
3841
  return this.transport.send({ jsonrpc: "2.0", id, result });
@@ -3727,7 +3913,7 @@ var ACPSession = class _ACPSession {
3727
3913
  }
3728
3914
  if (isBestEffortAckMethod(msg.method)) {
3729
3915
  if (msg.id !== void 0) {
3730
- this.sendResult(msg.id, {}).catch(() => {
3916
+ this.sendErrorResponse(msg.id, -32601, `Unsupported method: ${msg.method}`).catch(() => {
3731
3917
  });
3732
3918
  }
3733
3919
  return;
@@ -3736,6 +3922,13 @@ var ACPSession = class _ACPSession {
3736
3922
  return;
3737
3923
  }
3738
3924
  if (msg.method) {
3925
+ if (msg.id !== void 0) {
3926
+ void this.sendErrorResponse(msg.id, -32601, `Unsupported method: ${msg.method}`).catch(
3927
+ () => {
3928
+ }
3929
+ );
3930
+ return;
3931
+ }
3739
3932
  console.warn(
3740
3933
  JSON.stringify({
3741
3934
  level: "warn",
@@ -3758,6 +3951,29 @@ var ACPSession = class _ACPSession {
3758
3951
  resetScratch() {
3759
3952
  this.scratch = createSessionScratch();
3760
3953
  }
3954
+ acpSessionAuthHost() {
3955
+ const self = this;
3956
+ return {
3957
+ get state() {
3958
+ return self.state;
3959
+ },
3960
+ set state(value) {
3961
+ self.state = value;
3962
+ },
3963
+ get authMethods() {
3964
+ return self.authMethods;
3965
+ },
3966
+ allocId: (...args) => this.allocId(...args),
3967
+ sendRequest: (...args) => this.sendRequest(...args),
3968
+ get agentCapabilities() {
3969
+ return self.agentCapabilities;
3970
+ },
3971
+ opContext: (...args) => this.opContext(...args),
3972
+ ensureAuthenticated: (...args) => this.ensureAuthenticated(...args),
3973
+ authenticate: (...args) => this.authenticate(...args),
3974
+ opts: this.opts
3975
+ };
3976
+ }
3761
3977
  };
3762
3978
 
3763
3979
  // src/client/tool-translator.ts
@@ -3809,12 +4025,12 @@ var ToolTranslator = class {
3809
4025
  id: callId,
3810
4026
  params: { name, arguments: args }
3811
4027
  });
3812
- return new Promise((resolve4, reject) => {
4028
+ return new Promise((resolve5, reject) => {
3813
4029
  const timeout = setTimeout(() => {
3814
4030
  this.pending.delete(callId);
3815
4031
  reject(new Error(`Tool call ${name} timed out after ${this.opts.totalTimeoutMs}ms`));
3816
4032
  }, this.opts.totalTimeoutMs);
3817
- this.pending.set(callId, { resolve: resolve4, reject, timeout });
4033
+ this.pending.set(callId, { resolve: resolve5, reject, timeout });
3818
4034
  });
3819
4035
  }
3820
4036
  cancelAll() {
@@ -4637,7 +4853,7 @@ async function probeWithBound(items, worker, limit) {
4637
4853
  }
4638
4854
  async function defaultProbe(desc, timeoutMs, spawnProcess = spawn2, platform = process.platform) {
4639
4855
  const start = Date.now();
4640
- return new Promise((resolve4) => {
4856
+ return new Promise((resolve5) => {
4641
4857
  let settled = false;
4642
4858
  let stdout = "";
4643
4859
  let stderr = "";
@@ -4650,7 +4866,7 @@ async function defaultProbe(desc, timeoutMs, spawnProcess = spawn2, platform = p
4650
4866
  }
4651
4867
  } catch {
4652
4868
  }
4653
- resolve4(result);
4869
+ resolve5(result);
4654
4870
  };
4655
4871
  let child;
4656
4872
  try {