@wrongstack/acp 1.0.24 → 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/client.js CHANGED
@@ -335,6 +335,324 @@ function isBestEffortAckMethod(method) {
335
335
  return method === "mcp/connect" || method === "mcp/message" || method === "mcp/disconnect" || method === "elicitation/create" || method === "elicitation/complete";
336
336
  }
337
337
 
338
+ // src/client/acp-session-errors.ts
339
+ var ACPSessionError = class extends Error {
340
+ kind;
341
+ cause;
342
+ constructor(kind, message, cause) {
343
+ super(message);
344
+ this.name = "ACPSessionError";
345
+ this.kind = kind;
346
+ this.cause = cause;
347
+ }
348
+ };
349
+ function isJsonRpcError(v) {
350
+ return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
351
+ }
352
+ function isAuthRequiredError(err) {
353
+ const message = err instanceof Error ? err.message : String(err);
354
+ if (/auth(_|-)?required|authentication required/i.test(message)) return true;
355
+ const cause = err instanceof ACPSessionError ? err.cause : err && typeof err === "object" && "cause" in err ? err.cause : err;
356
+ if (!cause || typeof cause !== "object") return false;
357
+ const data = cause.data;
358
+ if (data === "auth_required" || data === "AUTH_REQUIRED") return true;
359
+ if (data && typeof data === "object") {
360
+ const d = data;
361
+ if (d.authRequired === true) return true;
362
+ if (d.code === "auth_required" || d.code === "AUTH_REQUIRED") return true;
363
+ }
364
+ return false;
365
+ }
366
+
367
+ // src/client/acp-session-ops.ts
368
+ function filterMcpServers(agentCapabilities, servers) {
369
+ if (!servers || servers.length === 0) return [];
370
+ const mcpCaps = agentCapabilities.mcpCapabilities ?? {};
371
+ return servers.filter((s) => {
372
+ if ("type" in s && s.type === "http") return mcpCaps.http === true;
373
+ if ("type" in s && s.type === "sse") return mcpCaps.sse === true;
374
+ return true;
375
+ });
376
+ }
377
+ async function executeLoadSession(ctx, sessionId, mcpServers, cwd) {
378
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
379
+ if (!ctx.agentCapabilities.loadSession) {
380
+ throw new ACPSessionError(
381
+ "unsupported_capability",
382
+ "agent does not support session/load (loadSession capability not advertised)"
383
+ );
384
+ }
385
+ if (ctx.sessionId && ctx.sessionId !== sessionId) {
386
+ await ctx.closeSession();
387
+ }
388
+ ctx.resetScratch();
389
+ const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
390
+ const id = ctx.allocId();
391
+ const result = await ctx.sendRequest(id, "session/load", {
392
+ sessionId,
393
+ cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
394
+ mcpServers: servers
395
+ });
396
+ if (isJsonRpcError(result)) {
397
+ throw new ACPSessionError("prompt_failed", `session/load failed: ${result.message}`, result);
398
+ }
399
+ ctx.setSessionId(sessionId);
400
+ }
401
+ async function executeResumeSession(ctx, sessionId, mcpServers, cwd) {
402
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
403
+ if (!ctx.agentCapabilities.sessionCapabilities?.resume) {
404
+ throw new ACPSessionError(
405
+ "unsupported_capability",
406
+ "agent does not support session/resume (sessionCapabilities.resume not advertised)"
407
+ );
408
+ }
409
+ if (ctx.sessionId && ctx.sessionId !== sessionId) {
410
+ await ctx.closeSession();
411
+ }
412
+ const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
413
+ const id = ctx.allocId();
414
+ const result = await ctx.sendRequest(id, "session/resume", {
415
+ sessionId,
416
+ cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
417
+ mcpServers: servers
418
+ });
419
+ if (isJsonRpcError(result)) {
420
+ throw new ACPSessionError("prompt_failed", `session/resume failed: ${result.message}`, result);
421
+ }
422
+ ctx.setSessionId(sessionId);
423
+ }
424
+ async function executeListSessions(ctx, cursor, cwd) {
425
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
426
+ if (!ctx.agentCapabilities.sessionCapabilities?.list) {
427
+ throw new ACPSessionError(
428
+ "unsupported_capability",
429
+ "agent does not support session/list (sessionCapabilities.list not advertised)"
430
+ );
431
+ }
432
+ const id = ctx.allocId();
433
+ const params = {};
434
+ if (cursor !== void 0) params.cursor = cursor;
435
+ if (cwd !== void 0) params.cwd = cwd;
436
+ const result = await ctx.sendRequest(id, "session/list", params);
437
+ if (isJsonRpcError(result)) {
438
+ throw new ACPSessionError("prompt_failed", `session/list failed: ${result.message}`, result);
439
+ }
440
+ const r = result;
441
+ return {
442
+ sessions: r.sessions ?? [],
443
+ nextCursor: r.nextCursor
444
+ };
445
+ }
446
+ async function executeDeleteSession(ctx, sessionId) {
447
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
448
+ if (!ctx.agentCapabilities.sessionCapabilities?.delete) {
449
+ throw new ACPSessionError(
450
+ "unsupported_capability",
451
+ "agent does not support session/delete (sessionCapabilities.delete not advertised)"
452
+ );
453
+ }
454
+ const id = ctx.allocId();
455
+ const result = await ctx.sendRequest(id, "session/delete", { sessionId });
456
+ if (isJsonRpcError(result)) {
457
+ throw new ACPSessionError("prompt_failed", `session/delete failed: ${result.message}`, result);
458
+ }
459
+ if (ctx.sessionId === sessionId) {
460
+ ctx.setSessionId(null);
461
+ }
462
+ }
463
+ async function executeForkSession(ctx, sourceSessionId, cwd, mcpServers) {
464
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
465
+ const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
466
+ const id = ctx.allocId();
467
+ const result = await ctx.sendRequest(id, "session/fork", {
468
+ sessionId: sourceSessionId,
469
+ cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
470
+ ...servers.length > 0 ? { mcpServers: servers } : {}
471
+ });
472
+ if (isJsonRpcError(result)) {
473
+ throw new ACPSessionError("prompt_failed", `session/fork failed: ${result.message}`, result);
474
+ }
475
+ const newId = result.sessionId;
476
+ if (typeof newId !== "string" || !newId) {
477
+ throw new ACPSessionError("protocol_error", "session/fork returned no sessionId", result);
478
+ }
479
+ return newId;
480
+ }
481
+ async function executeSetMode(ctx, sessionId, modeId) {
482
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
483
+ const id = ctx.allocId();
484
+ const result = await ctx.sendRequest(id, "session/set_mode", { sessionId, modeId });
485
+ if (isJsonRpcError(result)) {
486
+ throw new ACPSessionError(
487
+ "prompt_failed",
488
+ `session/set_mode failed: ${result.message}`,
489
+ result
490
+ );
491
+ }
492
+ }
493
+ async function executeSetConfigOption(ctx, sessionId, configId, value) {
494
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
495
+ const id = ctx.allocId();
496
+ const result = await ctx.sendRequest(id, "session/set_config_option", {
497
+ sessionId,
498
+ configId,
499
+ value
500
+ });
501
+ if (isJsonRpcError(result)) {
502
+ throw new ACPSessionError(
503
+ "prompt_failed",
504
+ `session/set_config_option failed: ${result.message}`,
505
+ result
506
+ );
507
+ }
508
+ }
509
+ async function executeListProviders(ctx) {
510
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
511
+ const id = ctx.allocId();
512
+ const result = await ctx.sendRequest(id, "providers/list", {});
513
+ if (isJsonRpcError(result)) {
514
+ throw new ACPSessionError("prompt_failed", `providers/list failed: ${result.message}`, result);
515
+ }
516
+ const r = result;
517
+ return { providers: r.providers ?? [], currentProviderId: r.currentProviderId ?? null };
518
+ }
519
+ async function executeSetProvider(ctx, providerId, config) {
520
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
521
+ const id = ctx.allocId();
522
+ const result = await ctx.sendRequest(id, "providers/set", { providerId, ...config ?? {} });
523
+ if (isJsonRpcError(result)) {
524
+ throw new ACPSessionError("prompt_failed", `providers/set failed: ${result.message}`, result);
525
+ }
526
+ }
527
+ async function executeDisableProvider(ctx) {
528
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
529
+ const id = ctx.allocId();
530
+ const result = await ctx.sendRequest(id, "providers/disable", {});
531
+ if (isJsonRpcError(result)) {
532
+ throw new ACPSessionError(
533
+ "prompt_failed",
534
+ `providers/disable failed: ${result.message}`,
535
+ result
536
+ );
537
+ }
538
+ }
539
+ async function executeMcpMessage(ctx, connectionId, message) {
540
+ if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
541
+ const id = ctx.allocId();
542
+ const result = await ctx.sendRequest(id, "mcp/message", { connectionId, message });
543
+ if (isJsonRpcError(result)) {
544
+ throw new ACPSessionError("prompt_failed", `mcp/message failed: ${result.message}`, result);
545
+ }
546
+ return result;
547
+ }
548
+ async function executeCreateSession(ctx) {
549
+ const servers = filterMcpServers(ctx.agentCapabilities, ctx.opts.mcpServers);
550
+ const id = ctx.allocId();
551
+ const result = await ctx.sendRequest(id, "session/new", {
552
+ cwd: ctx.opts.cwd ?? ctx.opts.projectRoot,
553
+ mcpServers: servers
554
+ });
555
+ if (isJsonRpcError(result)) {
556
+ throw new ACPSessionError(
557
+ "session_create_failed",
558
+ `session/new failed: ${result.message}`,
559
+ result
560
+ );
561
+ }
562
+ const sessionId = result.sessionId;
563
+ if (typeof sessionId !== "string" || sessionId.length === 0) {
564
+ throw new ACPSessionError("protocol_error", "session/new returned no sessionId", result);
565
+ }
566
+ return sessionId;
567
+ }
568
+
569
+ // src/client/acp-session-auth.ts
570
+ async function authenticate(host, methodId) {
571
+ if (host.state === "closed") {
572
+ throw new ACPSessionError("closed", "session is closed");
573
+ }
574
+ if (host.state !== "ready" && host.state !== "authenticated") {
575
+ throw new ACPSessionError(
576
+ "protocol_error",
577
+ `authenticate called in state=${host.state} (expected 'ready')`
578
+ );
579
+ }
580
+ if (host.state === "authenticated") return;
581
+ if (!host.authMethods.some((m) => m.id === methodId)) {
582
+ throw new ACPSessionError(
583
+ "auth_failed",
584
+ `auth method "${methodId}" not in advertised methods: ${host.authMethods.map((m) => m.id).join(", ")}`
585
+ );
586
+ }
587
+ if (host.authMethods.find((m) => m.id === methodId)?.type === "terminal") {
588
+ throw new ACPSessionError(
589
+ "auth_failed",
590
+ "Terminal authentication requires interactive login followed by reconnect; it cannot use authenticate"
591
+ );
592
+ }
593
+ const id = host.allocId();
594
+ const result = await host.sendRequest(id, "authenticate", { methodId });
595
+ if (isJsonRpcError(result)) {
596
+ throw new ACPSessionError("auth_failed", `authenticate failed: ${result.message}`, result);
597
+ }
598
+ host.state = "authenticated";
599
+ }
600
+ async function logout(host) {
601
+ if (host.state === "closed") {
602
+ throw new ACPSessionError("closed", "session is closed");
603
+ }
604
+ if (!host.agentCapabilities.auth?.logout) {
605
+ throw new ACPSessionError(
606
+ "unsupported_capability",
607
+ "agent does not support logout (auth.logout capability not advertised)"
608
+ );
609
+ }
610
+ const id = host.allocId();
611
+ const result = await host.sendRequest(id, "logout", {});
612
+ if (isJsonRpcError(result)) {
613
+ throw new ACPSessionError("logout_failed", `logout failed: ${result.message}`, result);
614
+ }
615
+ host.state = "ready";
616
+ }
617
+ async function createSessionWithAuth(host) {
618
+ try {
619
+ return await executeCreateSession(host.opContext());
620
+ } catch (err) {
621
+ if (host.state === "authenticated" || !isAuthRequiredError(err)) {
622
+ throw err instanceof ACPSessionError ? err : new ACPSessionError(
623
+ "session_create_failed",
624
+ err instanceof Error ? err.message : String(err),
625
+ err
626
+ );
627
+ }
628
+ await host.ensureAuthenticated();
629
+ return executeCreateSession(host.opContext());
630
+ }
631
+ }
632
+ async function ensureAuthenticated(host) {
633
+ if (host.state === "authenticated") return;
634
+ if (host.authMethods.length === 0) {
635
+ throw new ACPSessionError(
636
+ "auth_failed",
637
+ "This agent requires authentication before a session can start, but advertised no authMethods. Log into the CLI, then retry."
638
+ );
639
+ }
640
+ const inProcess = host.authMethods.find(
641
+ (m) => m.type === void 0 || m.type === "agent" || m.type === "oauth" || m.type === "http"
642
+ );
643
+ if (inProcess) {
644
+ await host.authenticate(inProcess.id);
645
+ return;
646
+ }
647
+ const terminal = host.authMethods.find((m) => m.type === "terminal");
648
+ const setupArgs = terminal?.args?.length ? terminal.args.join(" ") : void 0;
649
+ 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(" ")}` : ""}`;
650
+ throw new ACPSessionError(
651
+ "auth_failed",
652
+ `This agent requires a terminal login before ACP can start. Run \`${setup}\` (or the CLI's /login), then retry.`
653
+ );
654
+ }
655
+
338
656
  // src/client/file-server.ts
339
657
  import { randomBytes } from "node:crypto";
340
658
  import { realpathSync } from "node:fs";
@@ -692,351 +1010,120 @@ async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, s
692
1010
  case "terminal/release": {
693
1011
  const terminalId = String(params.terminalId ?? "");
694
1012
  terminalServer.release(terminalId);
695
- await sender.sendResult(id, {});
696
- return;
697
- }
698
- default:
699
- await sender.sendErrorResponse(id, -32601, `unknown method: ${msg.method}`);
700
- }
701
- } catch (err) {
702
- const message = err instanceof Error ? err.message : String(err);
703
- await sender.sendErrorResponse(id, -32603, message);
704
- }
705
- }
706
- async function authorizeAcpCallback(permissionPolicy, partial, callbackOptions) {
707
- try {
708
- const outcome = await runPermissionWithDeadline(
709
- permissionPolicy,
710
- {
711
- toolCall: {
712
- sessionUpdate: "tool_call_update",
713
- toolCallId: partial.toolCallId,
714
- title: partial.title,
715
- kind: partial.kind,
716
- status: "pending",
717
- rawInput: partial.rawInput
718
- },
719
- options: [
720
- { optionId: "allow", name: "Allow", kind: "allow_once" },
721
- { optionId: "reject", name: "Reject", kind: "reject_once" }
722
- ]
723
- },
724
- callbackOptions
725
- );
726
- return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always" ? "allowed" : "denied";
727
- } catch (err) {
728
- return isAbortLikeError(err) ? "cancelled" : "denied";
729
- }
730
- }
731
- async function runPermissionWithDeadline(permissionPolicy, call, callbackOptions) {
732
- const timeoutMs = resolvePermissionDeadline(callbackOptions.permissionTimeoutMs);
733
- const controller = new AbortController();
734
- const abort = () => controller.abort();
735
- const timer = timeoutMs === null ? null : setTimeout(abort, timeoutMs);
736
- let removeAbort;
737
- if (callbackOptions.signal) {
738
- if (callbackOptions.signal.aborted) {
739
- abort();
740
- } else {
741
- callbackOptions.signal.addEventListener("abort", abort, { once: true });
742
- removeAbort = () => callbackOptions.signal?.removeEventListener("abort", abort);
743
- }
744
- }
745
- try {
746
- return await Promise.race([
747
- permissionPolicy({
748
- toolCall: call.toolCall,
749
- options: call.options,
750
- signal: controller.signal
751
- }),
752
- rejectOnAbort(controller.signal)
753
- ]);
754
- } finally {
755
- if (timer !== null) clearTimeout(timer);
756
- removeAbort?.();
757
- }
758
- }
759
- function rejectOnAbort(signal) {
760
- return new Promise((_, reject) => {
761
- const rejectAbort = () => reject(new Error("permission request cancelled or timed out"));
762
- if (signal.aborted) {
763
- rejectAbort();
764
- return;
765
- }
766
- signal.addEventListener("abort", rejectAbort, { once: true });
767
- });
768
- }
769
- function isAbortLikeError(err) {
770
- return err instanceof Error && /cancelled|canceled|timed out|aborted/i.test(err.message);
771
- }
772
- function resolvePermissionDeadline(value) {
773
- if (value === Number.POSITIVE_INFINITY) return null;
774
- if (value !== void 0 && Number.isFinite(value) && value > 0) return Math.trunc(value);
775
- return DEFAULT_PERMISSION_TIMEOUT_MS;
776
- }
777
-
778
- // src/client/acp-session-content.ts
779
- function textContent(text) {
780
- return { type: "text", text };
781
- }
782
- function imageContent(mimeType, data) {
783
- return { type: "image", mimeType, data };
784
- }
785
- function audioContent(mimeType, data) {
786
- return { type: "audio", mimeType, data };
787
- }
788
- function extractText(block) {
789
- if (typeof block !== "object" || block === null) return "";
790
- const b = block;
791
- if (b.type === "text" && typeof b.text === "string") return b.text;
792
- if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
793
- return b.resource.text;
794
- }
795
- return "";
796
- }
797
- function isRecord(v) {
798
- return typeof v === "object" && v !== null && !Array.isArray(v);
799
- }
800
- function emptyRunResult(stopReason) {
801
- return {
802
- text: "",
803
- stopReason,
804
- hasText: false,
805
- toolCalls: [],
806
- diffs: [],
807
- thoughts: ""
808
- };
809
- }
810
-
811
- // src/client/acp-session-errors.ts
812
- var ACPSessionError = class extends Error {
813
- kind;
814
- cause;
815
- constructor(kind, message, cause) {
816
- super(message);
817
- this.name = "ACPSessionError";
818
- this.kind = kind;
819
- this.cause = cause;
820
- }
821
- };
822
- function isJsonRpcError(v) {
823
- return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
824
- }
825
- function isAuthRequiredError(err) {
826
- const message = err instanceof Error ? err.message : String(err);
827
- if (/auth(_|-)?required|authentication required/i.test(message)) return true;
828
- const cause = err instanceof ACPSessionError ? err.cause : err && typeof err === "object" && "cause" in err ? err.cause : err;
829
- if (!cause || typeof cause !== "object") return false;
830
- const data = cause.data;
831
- if (data === "auth_required" || data === "AUTH_REQUIRED") return true;
832
- if (data && typeof data === "object") {
833
- const d = data;
834
- if (d.authRequired === true) return true;
835
- if (d.code === "auth_required" || d.code === "AUTH_REQUIRED") return true;
836
- }
837
- return false;
838
- }
839
-
840
- // src/client/acp-session-ops.ts
841
- function filterMcpServers(agentCapabilities, servers) {
842
- if (!servers || servers.length === 0) return [];
843
- const mcpCaps = agentCapabilities.mcpCapabilities ?? {};
844
- return servers.filter((s) => {
845
- if ("type" in s && s.type === "http") return mcpCaps.http === true;
846
- if ("type" in s && s.type === "sse") return mcpCaps.sse === true;
847
- return true;
848
- });
849
- }
850
- async function executeLoadSession(ctx, sessionId, mcpServers, cwd) {
851
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
852
- if (!ctx.agentCapabilities.loadSession) {
853
- throw new ACPSessionError(
854
- "unsupported_capability",
855
- "agent does not support session/load (loadSession capability not advertised)"
856
- );
857
- }
858
- if (ctx.sessionId && ctx.sessionId !== sessionId) {
859
- await ctx.closeSession();
860
- }
861
- ctx.resetScratch();
862
- const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
863
- const id = ctx.allocId();
864
- const result = await ctx.sendRequest(id, "session/load", {
865
- sessionId,
866
- cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
867
- mcpServers: servers
868
- });
869
- if (isJsonRpcError(result)) {
870
- throw new ACPSessionError("prompt_failed", `session/load failed: ${result.message}`, result);
871
- }
872
- ctx.setSessionId(sessionId);
873
- }
874
- async function executeResumeSession(ctx, sessionId, mcpServers, cwd) {
875
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
876
- if (!ctx.agentCapabilities.sessionCapabilities?.resume) {
877
- throw new ACPSessionError(
878
- "unsupported_capability",
879
- "agent does not support session/resume (sessionCapabilities.resume not advertised)"
880
- );
881
- }
882
- if (ctx.sessionId && ctx.sessionId !== sessionId) {
883
- await ctx.closeSession();
884
- }
885
- const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
886
- const id = ctx.allocId();
887
- const result = await ctx.sendRequest(id, "session/resume", {
888
- sessionId,
889
- cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
890
- mcpServers: servers
891
- });
892
- if (isJsonRpcError(result)) {
893
- throw new ACPSessionError("prompt_failed", `session/resume failed: ${result.message}`, result);
894
- }
895
- ctx.setSessionId(sessionId);
896
- }
897
- async function executeListSessions(ctx, cursor, cwd) {
898
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
899
- if (!ctx.agentCapabilities.sessionCapabilities?.list) {
900
- throw new ACPSessionError(
901
- "unsupported_capability",
902
- "agent does not support session/list (sessionCapabilities.list not advertised)"
903
- );
904
- }
905
- const id = ctx.allocId();
906
- const params = {};
907
- if (cursor !== void 0) params.cursor = cursor;
908
- if (cwd !== void 0) params.cwd = cwd;
909
- const result = await ctx.sendRequest(id, "session/list", params);
910
- if (isJsonRpcError(result)) {
911
- throw new ACPSessionError("prompt_failed", `session/list failed: ${result.message}`, result);
1013
+ await sender.sendResult(id, {});
1014
+ return;
1015
+ }
1016
+ default:
1017
+ await sender.sendErrorResponse(id, -32601, `unknown method: ${msg.method}`);
1018
+ }
1019
+ } catch (err) {
1020
+ const message = err instanceof Error ? err.message : String(err);
1021
+ await sender.sendErrorResponse(id, -32603, message);
912
1022
  }
913
- const r = result;
914
- return {
915
- sessions: r.sessions ?? [],
916
- nextCursor: r.nextCursor
917
- };
918
1023
  }
919
- async function executeDeleteSession(ctx, sessionId) {
920
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
921
- if (!ctx.agentCapabilities.sessionCapabilities?.delete) {
922
- throw new ACPSessionError(
923
- "unsupported_capability",
924
- "agent does not support session/delete (sessionCapabilities.delete not advertised)"
1024
+ async function authorizeAcpCallback(permissionPolicy, partial, callbackOptions) {
1025
+ try {
1026
+ const outcome = await runPermissionWithDeadline(
1027
+ permissionPolicy,
1028
+ {
1029
+ toolCall: {
1030
+ sessionUpdate: "tool_call_update",
1031
+ toolCallId: partial.toolCallId,
1032
+ title: partial.title,
1033
+ kind: partial.kind,
1034
+ status: "pending",
1035
+ rawInput: partial.rawInput
1036
+ },
1037
+ options: [
1038
+ { optionId: "allow", name: "Allow", kind: "allow_once" },
1039
+ { optionId: "reject", name: "Reject", kind: "reject_once" }
1040
+ ]
1041
+ },
1042
+ callbackOptions
925
1043
  );
1044
+ return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always" ? "allowed" : "denied";
1045
+ } catch (err) {
1046
+ return isAbortLikeError(err) ? "cancelled" : "denied";
926
1047
  }
927
- const id = ctx.allocId();
928
- const result = await ctx.sendRequest(id, "session/delete", { sessionId });
929
- if (isJsonRpcError(result)) {
930
- throw new ACPSessionError("prompt_failed", `session/delete failed: ${result.message}`, result);
1048
+ }
1049
+ async function runPermissionWithDeadline(permissionPolicy, call, callbackOptions) {
1050
+ const timeoutMs = resolvePermissionDeadline(callbackOptions.permissionTimeoutMs);
1051
+ const controller = new AbortController();
1052
+ const abort = () => controller.abort();
1053
+ const timer = timeoutMs === null ? null : setTimeout(abort, timeoutMs);
1054
+ let removeAbort;
1055
+ if (callbackOptions.signal) {
1056
+ if (callbackOptions.signal.aborted) {
1057
+ abort();
1058
+ } else {
1059
+ callbackOptions.signal.addEventListener("abort", abort, { once: true });
1060
+ removeAbort = () => callbackOptions.signal?.removeEventListener("abort", abort);
1061
+ }
931
1062
  }
932
- if (ctx.sessionId === sessionId) {
933
- ctx.setSessionId(null);
1063
+ try {
1064
+ return await Promise.race([
1065
+ permissionPolicy({
1066
+ toolCall: call.toolCall,
1067
+ options: call.options,
1068
+ signal: controller.signal
1069
+ }),
1070
+ rejectOnAbort(controller.signal)
1071
+ ]);
1072
+ } finally {
1073
+ if (timer !== null) clearTimeout(timer);
1074
+ removeAbort?.();
934
1075
  }
935
1076
  }
936
- async function executeForkSession(ctx, sourceSessionId, cwd, mcpServers) {
937
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
938
- const servers = filterMcpServers(ctx.agentCapabilities, mcpServers ?? ctx.opts.mcpServers);
939
- const id = ctx.allocId();
940
- const result = await ctx.sendRequest(id, "session/fork", {
941
- sessionId: sourceSessionId,
942
- cwd: cwd ?? ctx.opts.cwd ?? ctx.opts.projectRoot,
943
- ...servers.length > 0 ? { mcpServers: servers } : {}
1077
+ function rejectOnAbort(signal) {
1078
+ return new Promise((_, reject) => {
1079
+ const rejectAbort = () => reject(new Error("permission request cancelled or timed out"));
1080
+ if (signal.aborted) {
1081
+ rejectAbort();
1082
+ return;
1083
+ }
1084
+ signal.addEventListener("abort", rejectAbort, { once: true });
944
1085
  });
945
- if (isJsonRpcError(result)) {
946
- throw new ACPSessionError("prompt_failed", `session/fork failed: ${result.message}`, result);
947
- }
948
- const newId = result.sessionId;
949
- if (typeof newId !== "string" || !newId) {
950
- throw new ACPSessionError("protocol_error", "session/fork returned no sessionId", result);
951
- }
952
- return newId;
953
1086
  }
954
- async function executeSetMode(ctx, sessionId, modeId) {
955
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
956
- const id = ctx.allocId();
957
- const result = await ctx.sendRequest(id, "session/set_mode", { sessionId, modeId });
958
- if (isJsonRpcError(result)) {
959
- throw new ACPSessionError(
960
- "prompt_failed",
961
- `session/set_mode failed: ${result.message}`,
962
- result
963
- );
964
- }
1087
+ function isAbortLikeError(err) {
1088
+ return err instanceof Error && /cancelled|canceled|timed out|aborted/i.test(err.message);
965
1089
  }
966
- async function executeSetConfigOption(ctx, sessionId, configId, value) {
967
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
968
- const id = ctx.allocId();
969
- const result = await ctx.sendRequest(id, "session/set_config_option", {
970
- sessionId,
971
- configId,
972
- value
973
- });
974
- if (isJsonRpcError(result)) {
975
- throw new ACPSessionError(
976
- "prompt_failed",
977
- `session/set_config_option failed: ${result.message}`,
978
- result
979
- );
980
- }
1090
+ function resolvePermissionDeadline(value) {
1091
+ if (value === Number.POSITIVE_INFINITY) return null;
1092
+ if (value !== void 0 && Number.isFinite(value) && value > 0) return Math.trunc(value);
1093
+ return DEFAULT_PERMISSION_TIMEOUT_MS;
981
1094
  }
982
- async function executeListProviders(ctx) {
983
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
984
- const id = ctx.allocId();
985
- const result = await ctx.sendRequest(id, "providers/list", {});
986
- if (isJsonRpcError(result)) {
987
- throw new ACPSessionError("prompt_failed", `providers/list failed: ${result.message}`, result);
988
- }
989
- const r = result;
990
- return { providers: r.providers ?? [], currentProviderId: r.currentProviderId ?? null };
1095
+
1096
+ // src/client/acp-session-content.ts
1097
+ function textContent(text) {
1098
+ return { type: "text", text };
991
1099
  }
992
- async function executeSetProvider(ctx, providerId, config) {
993
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
994
- const id = ctx.allocId();
995
- const result = await ctx.sendRequest(id, "providers/set", { providerId, ...config ?? {} });
996
- if (isJsonRpcError(result)) {
997
- throw new ACPSessionError("prompt_failed", `providers/set failed: ${result.message}`, result);
998
- }
1100
+ function imageContent(mimeType, data) {
1101
+ return { type: "image", mimeType, data };
999
1102
  }
1000
- async function executeDisableProvider(ctx) {
1001
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
1002
- const id = ctx.allocId();
1003
- const result = await ctx.sendRequest(id, "providers/disable", {});
1004
- if (isJsonRpcError(result)) {
1005
- throw new ACPSessionError(
1006
- "prompt_failed",
1007
- `providers/disable failed: ${result.message}`,
1008
- result
1009
- );
1010
- }
1103
+ function audioContent(mimeType, data) {
1104
+ return { type: "audio", mimeType, data };
1011
1105
  }
1012
- async function executeMcpMessage(ctx, connectionId, message) {
1013
- if (ctx.closed) throw new ACPSessionError("closed", "session is closed");
1014
- const id = ctx.allocId();
1015
- const result = await ctx.sendRequest(id, "mcp/message", { connectionId, message });
1016
- if (isJsonRpcError(result)) {
1017
- throw new ACPSessionError("prompt_failed", `mcp/message failed: ${result.message}`, result);
1106
+ function extractText(block) {
1107
+ if (typeof block !== "object" || block === null) return "";
1108
+ const b = block;
1109
+ if (b.type === "text" && typeof b.text === "string") return b.text;
1110
+ if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
1111
+ return b.resource.text;
1018
1112
  }
1019
- return result;
1113
+ return "";
1020
1114
  }
1021
- async function executeCreateSession(ctx) {
1022
- const servers = filterMcpServers(ctx.agentCapabilities, ctx.opts.mcpServers);
1023
- const id = ctx.allocId();
1024
- const result = await ctx.sendRequest(id, "session/new", {
1025
- cwd: ctx.opts.cwd ?? ctx.opts.projectRoot,
1026
- mcpServers: servers
1027
- });
1028
- if (isJsonRpcError(result)) {
1029
- throw new ACPSessionError(
1030
- "session_create_failed",
1031
- `session/new failed: ${result.message}`,
1032
- result
1033
- );
1034
- }
1035
- const sessionId = result.sessionId;
1036
- if (typeof sessionId !== "string" || sessionId.length === 0) {
1037
- throw new ACPSessionError("protocol_error", "session/new returned no sessionId", result);
1038
- }
1039
- return sessionId;
1115
+ function isRecord(v) {
1116
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1117
+ }
1118
+ function emptyRunResult(stopReason) {
1119
+ return {
1120
+ text: "",
1121
+ stopReason,
1122
+ hasText: false,
1123
+ toolCalls: [],
1124
+ diffs: [],
1125
+ thoughts: ""
1126
+ };
1040
1127
  }
1041
1128
 
1042
1129
  // src/client/acp-session-updates.ts
@@ -1912,55 +1999,14 @@ var ACPSession = class _ACPSession {
1912
1999
  * Call this AFTER start() and BEFORE any session/new call.
1913
2000
  */
1914
2001
  async authenticate(methodId) {
1915
- if (this.state === "closed") {
1916
- throw new ACPSessionError("closed", "session is closed");
1917
- }
1918
- if (this.state !== "ready" && this.state !== "authenticated") {
1919
- throw new ACPSessionError(
1920
- "protocol_error",
1921
- `authenticate called in state=${this.state} (expected 'ready')`
1922
- );
1923
- }
1924
- if (this.state === "authenticated") return;
1925
- if (!this.authMethods.some((m) => m.id === methodId)) {
1926
- throw new ACPSessionError(
1927
- "auth_failed",
1928
- `auth method "${methodId}" not in advertised methods: ${this.authMethods.map((m) => m.id).join(", ")}`
1929
- );
1930
- }
1931
- if (this.authMethods.find((m) => m.id === methodId)?.type === "terminal") {
1932
- throw new ACPSessionError(
1933
- "auth_failed",
1934
- "Terminal authentication requires interactive login followed by reconnect; it cannot use authenticate"
1935
- );
1936
- }
1937
- const id = this.allocId();
1938
- const result = await this.sendRequest(id, "authenticate", { methodId });
1939
- if (isJsonRpcError(result)) {
1940
- throw new ACPSessionError("auth_failed", `authenticate failed: ${result.message}`, result);
1941
- }
1942
- this.state = "authenticated";
2002
+ return authenticate(this.acpSessionAuthHost(), methodId);
1943
2003
  }
1944
2004
  /**
1945
2005
  * Log out from the current authenticated session.
1946
2006
  * Only callable if the agent advertises `auth.logout` capability.
1947
2007
  */
1948
2008
  async logout() {
1949
- if (this.state === "closed") {
1950
- throw new ACPSessionError("closed", "session is closed");
1951
- }
1952
- if (!this.agentCapabilities.auth?.logout) {
1953
- throw new ACPSessionError(
1954
- "unsupported_capability",
1955
- "agent does not support logout (auth.logout capability not advertised)"
1956
- );
1957
- }
1958
- const id = this.allocId();
1959
- const result = await this.sendRequest(id, "logout", {});
1960
- if (isJsonRpcError(result)) {
1961
- throw new ACPSessionError("logout_failed", `logout failed: ${result.message}`, result);
1962
- }
1963
- this.state = "ready";
2009
+ return logout(this.acpSessionAuthHost());
1964
2010
  }
1965
2011
  // ──────────────────────────────────────────────────────────────────────
1966
2012
  // Session management delegation
@@ -2258,19 +2304,7 @@ var ACPSession = class _ACPSession {
2258
2304
  * `authMethods`; we do not pop OAuth on every spawn.
2259
2305
  */
2260
2306
  async createSessionWithAuth() {
2261
- try {
2262
- return await executeCreateSession(this.opContext());
2263
- } catch (err) {
2264
- if (this.state === "authenticated" || !isAuthRequiredError(err)) {
2265
- throw err instanceof ACPSessionError ? err : new ACPSessionError(
2266
- "session_create_failed",
2267
- err instanceof Error ? err.message : String(err),
2268
- err
2269
- );
2270
- }
2271
- await this.ensureAuthenticated();
2272
- return executeCreateSession(this.opContext());
2273
- }
2307
+ return createSessionWithAuth(this.acpSessionAuthHost());
2274
2308
  }
2275
2309
  /**
2276
2310
  * Pick a non-terminal auth method and run `authenticate`. Terminal-only
@@ -2278,27 +2312,7 @@ var ACPSession = class _ACPSession {
2278
2312
  * we refuse rather than hang a TUI inside the JSON-RPC child.
2279
2313
  */
2280
2314
  async ensureAuthenticated() {
2281
- if (this.state === "authenticated") return;
2282
- if (this.authMethods.length === 0) {
2283
- throw new ACPSessionError(
2284
- "auth_failed",
2285
- "This agent requires authentication before a session can start, but advertised no authMethods. Log into the CLI, then retry."
2286
- );
2287
- }
2288
- const inProcess = this.authMethods.find(
2289
- (m) => m.type === void 0 || m.type === "agent" || m.type === "oauth" || m.type === "http"
2290
- );
2291
- if (inProcess) {
2292
- await this.authenticate(inProcess.id);
2293
- return;
2294
- }
2295
- const terminal = this.authMethods.find((m) => m.type === "terminal");
2296
- const setupArgs = terminal?.args?.length ? terminal.args.join(" ") : void 0;
2297
- const setup = setupArgs !== void 0 ? `${this.opts.command}${this.opts.args?.length ? ` ${this.opts.args.join(" ")}` : ""} ${setupArgs}` : `${this.opts.command}${this.opts.args?.length ? ` ${this.opts.args.join(" ")}` : ""}`;
2298
- throw new ACPSessionError(
2299
- "auth_failed",
2300
- `This agent requires a terminal login before ACP can start. Run \`${setup}\` (or the CLI's /login), then retry.`
2301
- );
2315
+ return ensureAuthenticated(this.acpSessionAuthHost());
2302
2316
  }
2303
2317
  sendResult(id, result) {
2304
2318
  return this.transport.send({ jsonrpc: "2.0", id, result });
@@ -2414,6 +2428,29 @@ var ACPSession = class _ACPSession {
2414
2428
  resetScratch() {
2415
2429
  this.scratch = createSessionScratch();
2416
2430
  }
2431
+ acpSessionAuthHost() {
2432
+ const self = this;
2433
+ return {
2434
+ get state() {
2435
+ return self.state;
2436
+ },
2437
+ set state(value) {
2438
+ self.state = value;
2439
+ },
2440
+ get authMethods() {
2441
+ return self.authMethods;
2442
+ },
2443
+ allocId: (...args) => this.allocId(...args),
2444
+ sendRequest: (...args) => this.sendRequest(...args),
2445
+ get agentCapabilities() {
2446
+ return self.agentCapabilities;
2447
+ },
2448
+ opContext: (...args) => this.opContext(...args),
2449
+ ensureAuthenticated: (...args) => this.ensureAuthenticated(...args),
2450
+ authenticate: (...args) => this.authenticate(...args),
2451
+ opts: this.opts
2452
+ };
2453
+ }
2417
2454
  };
2418
2455
 
2419
2456
  // src/integration/acp-subagent-runner.ts