@wrongstack/acp 1.0.3 → 1.0.5

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.
@@ -1,4 +1,4 @@
1
- export type { ClientCapabilities, RunTurn, RunTurnApi, RunTurnInput, RunTurnPermissionRequest, RunTurnResult, SessionPersistence, } from './protocol-handler.js';
1
+ export type { ClientCapabilities, McpServer, RunTurn, RunTurnApi, RunTurnInput, RunTurnPermissionRequest, RunTurnResult, SessionPersistence, } from './protocol-handler.js';
2
2
  export { ACPProtocolHandler } from './protocol-handler.js';
3
3
  export type { ACPServerAgentTurnOptions } from './server-agent-turn.js';
4
4
  export { makeACPServerAgentTurn } from './server-agent-turn.js';
@@ -3,9 +3,9 @@
3
3
  * Keeping these declarations separate leaves the handler focused on request dispatch.
4
4
  */
5
5
  import type { ACPMessage } from '../types/acp-messages.js';
6
- import { ACP_PROTOCOL_VERSION, type ContentBlock, type PermissionOption, type PlanEntry, type RequestPermissionOutcome, type StopReason, type ToolKind, type UsageCost } from '../types/acp-v1.js';
6
+ import { ACP_PROTOCOL_VERSION, type ContentBlock, type McpServer, type PermissionOption, type PlanEntry, type RequestPermissionOutcome, type StopReason, type ToolKind, type UsageCost } from '../types/acp-v1.js';
7
7
  import type { AgentServerTransport } from './stdio-transport.js';
8
- export type { ACPMessage, ContentBlock, RequestPermissionOutcome };
8
+ export type { ACPMessage, ContentBlock, McpServer, RequestPermissionOutcome };
9
9
  export { ACP_PROTOCOL_VERSION };
10
10
  type WireMessage = {
11
11
  jsonrpc?: '2.0';
@@ -33,6 +33,19 @@ export interface RunTurnInput {
33
33
  prompt: readonly ContentBlock[];
34
34
  /** Cancelled when the client sends `session/cancel` for this session. */
35
35
  signal: AbortSignal;
36
+ /** Session working directory from `session/new` (absolute). */
37
+ cwd?: string | undefined;
38
+ /**
39
+ * MCP servers the CLIENT asked this session to connect to, as supplied in
40
+ * `session/new` / `session/load` / `session/fork`. Validated at the wire
41
+ * boundary (see `parseMcpServers`), so entries here are well-formed.
42
+ *
43
+ * The runTurn implementation owns the connection: it is what holds the tool
44
+ * registry these servers' tools must land in. Nothing here is optional
45
+ * decoration — an editor that passes a server and gets no tools has been
46
+ * told a successful `session/new` about work that never happened.
47
+ */
48
+ mcpServers?: readonly McpServer[] | undefined;
36
49
  }
37
50
  export interface RunTurnResult {
38
51
  stopReason: StopReason;
@@ -127,6 +140,13 @@ export interface SessionState {
127
140
  updatedAt: string;
128
141
  /** Optional human title. */
129
142
  title?: string;
143
+ /**
144
+ * The client's MCP servers for this session, captured at `session/new`
145
+ * (or `load`/`fork`) and replayed into every turn. Held on the session
146
+ * because the connection is session-scoped: two ACP sessions may ask for
147
+ * different servers, and closing one must not tear down the other's.
148
+ */
149
+ mcpServers?: readonly McpServer[] | undefined;
130
150
  }
131
151
  /** MCP-style session mode advertised in current_mode_update. */
132
152
  export interface SessionMode {
@@ -1,6 +1,6 @@
1
1
  import { type ProtocolHandlerOptions, WRONGSTACK_VERSION } from './protocol-contract.js';
2
2
  import { errorToJsonRpc } from './protocol-session-ops.js';
3
- export type { AgentCapabilities, ClientCapabilities, PromptCapabilities, ProtocolHandlerOptions, RunTurn, RunTurnApi, RunTurnInput, RunTurnPermissionRequest, RunTurnResult, SessionConfigOption, SessionMode, SessionPersistence, SessionState, } from './protocol-contract.js';
3
+ export type { AgentCapabilities, ClientCapabilities, McpServer, PromptCapabilities, ProtocolHandlerOptions, RunTurn, RunTurnApi, RunTurnInput, RunTurnPermissionRequest, RunTurnResult, SessionConfigOption, SessionMode, SessionPersistence, SessionState, } from './protocol-contract.js';
4
4
  export { WRONGSTACK_VERSION };
5
5
  export declare class ACPProtocolHandler {
6
6
  private readonly transport;
@@ -1,8 +1,19 @@
1
- import { type ClientCapabilities, type RunTurnApi, type SessionConfigOption, type SessionMode } from './protocol-contract.js';
1
+ import { type ClientCapabilities, type McpServer, type RunTurnApi, type SessionConfigOption, type SessionMode } from './protocol-contract.js';
2
2
  /** Single global mode id, sufficient for v1. */
3
3
  export declare const DEFAULT_MODE_ID = "code";
4
4
  export declare const DEFAULT_MAX_SESSIONS = 64;
5
5
  export declare const DEFAULT_MODES: readonly SessionMode[];
6
+ /**
7
+ * Validate the `mcpServers` array a client sends with `session/new`,
8
+ * `session/load` or `session/fork`.
9
+ *
10
+ * Malformed entries are dropped rather than rejecting the whole request: an
11
+ * editor that ships one bad server config should still get a working session.
12
+ * A dropped entry is reported through `onSkipped` so the caller can tell the
13
+ * client instead of silently discarding what it asked for — silent discard is
14
+ * exactly how this array came to be ignored in the first place.
15
+ */
16
+ export declare function parseMcpServers(raw: unknown, onSkipped?: (reason: string) => void): McpServer[];
6
17
  export declare function resolveSessionCwd(requested: string): Promise<string | null>;
7
18
  export declare function errorToJsonRpc(err: unknown): {
8
19
  code: number;
@@ -39,7 +39,7 @@
39
39
  * `{stopReason: 'cancelled'}`.
40
40
  */
41
41
  import type { Agent, AgentInput } from '@wrongstack/core/agent';
42
- import type { ContentBlock, PlanEntry, StopReason, ToolKind, UsageCost } from '../types/acp-v1.js';
42
+ import type { ContentBlock, McpServer, PlanEntry, StopReason, ToolKind, UsageCost } from '../types/acp-v1.js';
43
43
  import type { RunTurn, RunTurnApi, RunTurnResult } from './protocol-handler.js';
44
44
  export interface ACPServerAgentTurnOptions {
45
45
  /**
@@ -53,8 +53,12 @@ export interface ACPServerAgentTurnOptions {
53
53
  * advertises those capabilities. A factory that wires it builds a
54
54
  * client-backed permission policy and ACP-backed fs/terminal tools
55
55
  * instead of silently auto-approving against the local disk.
56
+ *
57
+ * `mcpServers` is the client's per-session MCP server list from
58
+ * `session/new`. The factory owns the session's tool registry, so it is the
59
+ * only place those servers can be connected and their tools registered.
56
60
  */
57
- agentFor: (sessionId: string, cwd: string, api?: RunTurnApi) => Promise<Agent> | Agent;
61
+ agentFor: (sessionId: string, cwd: string, api?: RunTurnApi, mcpServers?: readonly McpServer[]) => Promise<Agent> | Agent;
58
62
  /**
59
63
  * Hard wall-clock cap for one turn. The agent's own provider
60
64
  * timeout is layered under this; this cap is a safety belt.
package/dist/agent.js CHANGED
@@ -46,6 +46,64 @@ var DEFAULT_MODES = [
46
46
  description: "Default agent mode for code-generation tasks."
47
47
  }
48
48
  ];
49
+ function parseMcpServers(raw, onSkipped) {
50
+ if (!Array.isArray(raw)) return [];
51
+ const out = [];
52
+ for (const entry of raw) {
53
+ if (typeof entry !== "object" || entry === null) {
54
+ onSkipped?.("entry is not an object");
55
+ continue;
56
+ }
57
+ const e = entry;
58
+ const name = typeof e.name === "string" ? e.name.trim() : "";
59
+ if (name === "") {
60
+ onSkipped?.("entry has no name");
61
+ continue;
62
+ }
63
+ const type = typeof e.type === "string" ? e.type : "stdio";
64
+ if (type === "http" || type === "sse") {
65
+ if (typeof e.url !== "string" || e.url === "") {
66
+ onSkipped?.(`"${name}": ${type} server has no url`);
67
+ continue;
68
+ }
69
+ const headers = parseNameValuePairs(e.headers);
70
+ const url = e.url;
71
+ out.push(
72
+ type === "http" ? { type: "http", name, url, ...headers ? { headers } : {} } : { type: "sse", name, url, ...headers ? { headers } : {} }
73
+ );
74
+ continue;
75
+ }
76
+ if (type !== "stdio") {
77
+ onSkipped?.(`"${name}": unknown transport "${type}"`);
78
+ continue;
79
+ }
80
+ if (typeof e.command !== "string" || e.command === "") {
81
+ onSkipped?.(`"${name}": stdio server has no command`);
82
+ continue;
83
+ }
84
+ const args = Array.isArray(e.args) ? e.args.filter((a) => typeof a === "string") : void 0;
85
+ const env = parseNameValuePairs(e.env);
86
+ out.push({
87
+ name,
88
+ command: e.command,
89
+ ...args && args.length > 0 ? { args } : {},
90
+ ...env ? { env } : {}
91
+ });
92
+ }
93
+ return out;
94
+ }
95
+ function parseNameValuePairs(raw) {
96
+ if (!Array.isArray(raw)) return void 0;
97
+ const out = [];
98
+ for (const pair of raw) {
99
+ if (typeof pair !== "object" || pair === null) continue;
100
+ const p = pair;
101
+ if (typeof p.name === "string" && p.name !== "" && typeof p.value === "string") {
102
+ out.push({ name: p.name, value: p.value });
103
+ }
104
+ }
105
+ return out.length > 0 ? out : void 0;
106
+ }
49
107
  async function resolveSessionCwd(requested) {
50
108
  if (!path.isAbsolute(requested)) return null;
51
109
  const resolved = path.resolve(requested);
@@ -128,9 +186,16 @@ function buildInitializeResult(agentName, modes, configOptions) {
128
186
  audio: false,
129
187
  embeddedContext: true
130
188
  },
189
+ // All three ACP transports are supported. stdio is mandatory per spec
190
+ // and cannot be declined; http and sse are declared here because the
191
+ // agent now actually connects them (see `parseMcpServers` above and the
192
+ // per-session MCP registry in `buildAcpServerAgentFactory`). Before that
193
+ // wiring existed the array was destructured and thrown away at every
194
+ // entry point, so a client got a successful `session/new` and no tools —
195
+ // flip these back to false if that connection path is ever removed.
131
196
  mcpCapabilities: {
132
- http: false,
133
- sse: false
197
+ http: true,
198
+ sse: true
134
199
  },
135
200
  sessionCapabilities: {
136
201
  close: {},
@@ -170,6 +235,8 @@ async function handleSessionNewOp(ctx, id, params) {
170
235
  }
171
236
  cwd = resolved;
172
237
  }
238
+ const skipped = [];
239
+ const mcpServers = parseMcpServers(p.mcpServers, (reason) => skipped.push(reason));
173
240
  const sessionId = `sess_${ctx.allocId()}`;
174
241
  const now = (/* @__PURE__ */ new Date()).toISOString();
175
242
  const state = {
@@ -178,7 +245,8 @@ async function handleSessionNewOp(ctx, id, params) {
178
245
  abort: new AbortController(),
179
246
  modeId: DEFAULT_MODE_ID,
180
247
  createdAt: now,
181
- updatedAt: now
248
+ updatedAt: now,
249
+ ...mcpServers.length > 0 ? { mcpServers } : {}
182
250
  };
183
251
  ctx.sessions.set(sessionId, state);
184
252
  ctx.onSessionNew(state);
@@ -199,6 +267,7 @@ async function handleSessionNewOp(ctx, id, params) {
199
267
  }
200
268
  });
201
269
  }
270
+ await reportSkippedMcpServers(ctx, sessionId, skipped);
202
271
  await ctx.sendResult(id, {
203
272
  sessionId,
204
273
  modes: ctx.modes,
@@ -210,6 +279,8 @@ async function handleSessionLoadOp(ctx, id, params) {
210
279
  const p = params ?? {};
211
280
  const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
212
281
  const loadCwd = typeof p.cwd === "string" ? p.cwd : void 0;
282
+ const loadSkipped = [];
283
+ const loadMcpServers = parseMcpServers(p.mcpServers, (reason) => loadSkipped.push(reason));
213
284
  const existing = sessionId ? ctx.sessions.get(sessionId) : void 0;
214
285
  if (!existing && sessionId && ctx.store) {
215
286
  const persisted = await ctx.store.load(sessionId);
@@ -231,7 +302,8 @@ async function handleSessionLoadOp(ctx, id, params) {
231
302
  modeId: persisted.modeId ?? DEFAULT_MODE_ID,
232
303
  createdAt: persisted.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
233
304
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
234
- ...persisted.title !== void 0 ? { title: persisted.title } : {}
305
+ ...persisted.title !== void 0 ? { title: persisted.title } : {},
306
+ ...loadMcpServers.length > 0 ? { mcpServers: loadMcpServers } : {}
235
307
  };
236
308
  ctx.sessions.set(sessionId, restored);
237
309
  ctx.seedFor?.(sessionId, persisted.history ?? []);
@@ -242,6 +314,7 @@ async function handleSessionLoadOp(ctx, id, params) {
242
314
  sessionId,
243
315
  update: { sessionUpdate: "current_mode_update", modeId: restored.modeId }
244
316
  });
317
+ await reportSkippedMcpServers(ctx, sessionId, loadSkipped);
245
318
  await ctx.sendResult(id, {
246
319
  initialMode: { currentModeId: restored.modeId, availableModes: ctx.modes }
247
320
  });
@@ -250,6 +323,9 @@ async function handleSessionLoadOp(ctx, id, params) {
250
323
  }
251
324
  if (existing) {
252
325
  existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
326
+ if (loadMcpServers.length > 0) {
327
+ existing.mcpServers = loadMcpServers;
328
+ }
253
329
  const replay = ctx.replayFor?.(sessionId);
254
330
  if (replay) {
255
331
  for (const update of replay) {
@@ -270,6 +346,7 @@ async function handleSessionLoadOp(ctx, id, params) {
270
346
  modeId: existing.modeId
271
347
  }
272
348
  });
349
+ await reportSkippedMcpServers(ctx, sessionId, loadSkipped);
273
350
  await ctx.sendResult(id, {
274
351
  initialMode: {
275
352
  currentModeId: existing.modeId,
@@ -302,6 +379,9 @@ async function handleSessionForkOp(ctx, id, params) {
302
379
  }
303
380
  forkCwd = resolved;
304
381
  }
382
+ const forkSkipped = [];
383
+ const forkRequested = parseMcpServers(p.mcpServers, (reason) => forkSkipped.push(reason));
384
+ const forkMcpServers = forkRequested.length > 0 ? forkRequested : source.mcpServers;
305
385
  const now = (/* @__PURE__ */ new Date()).toISOString();
306
386
  const sessionId = `sess_${ctx.allocId()}`;
307
387
  const forked = {
@@ -311,7 +391,8 @@ async function handleSessionForkOp(ctx, id, params) {
311
391
  modeId: source.modeId,
312
392
  createdAt: now,
313
393
  updatedAt: now,
314
- ...source.title !== void 0 ? { title: source.title } : {}
394
+ ...source.title !== void 0 ? { title: source.title } : {},
395
+ ...forkMcpServers && forkMcpServers.length > 0 ? { mcpServers: forkMcpServers } : {}
315
396
  };
316
397
  const history = (ctx.replayFor?.(sourceId) ?? []).map((update) => ({
317
398
  sessionUpdate: update.sessionUpdate,
@@ -325,6 +406,7 @@ async function handleSessionForkOp(ctx, id, params) {
325
406
  sessionId,
326
407
  update: { sessionUpdate: "current_mode_update", modeId: forked.modeId }
327
408
  });
409
+ await reportSkippedMcpServers(ctx, sessionId, forkSkipped);
328
410
  await ctx.sendResult(id, {
329
411
  sessionId,
330
412
  modes: ctx.modes,
@@ -364,7 +446,13 @@ async function handleSessionPromptOp(ctx, id, params) {
364
446
  };
365
447
  try {
366
448
  result = await ctx.runTurn(
367
- { sessionId, prompt: p.prompt, signal: turnSignal.signal },
449
+ {
450
+ sessionId,
451
+ prompt: p.prompt,
452
+ signal: turnSignal.signal,
453
+ cwd: session.cwd,
454
+ ...session.mcpServers ? { mcpServers: session.mcpServers } : {}
455
+ },
368
456
  emit,
369
457
  api
370
458
  );
@@ -422,6 +510,22 @@ async function handleSetConfigOptionOp(ctx, id, params) {
422
510
  await ctx.sendResult(id, { configOptions: [...ctx.configOptions] });
423
511
  return false;
424
512
  }
513
+ async function reportSkippedMcpServers(ctx, sessionId, skipped) {
514
+ if (skipped.length === 0) return;
515
+ try {
516
+ await ctx.sendNotification({
517
+ sessionId,
518
+ update: {
519
+ sessionUpdate: "agent_message_chunk",
520
+ content: {
521
+ type: "text",
522
+ text: `Ignored ${skipped.length} malformed mcpServers entr${skipped.length === 1 ? "y" : "ies"}: ${skipped.join("; ")}`
523
+ }
524
+ }
525
+ });
526
+ } catch {
527
+ }
528
+ }
425
529
 
426
530
  // src/agent/protocol-handler.ts
427
531
  var ACPProtocolHandler = class {
@@ -620,7 +724,7 @@ var ACPProtocolHandler = class {
620
724
  return false;
621
725
  }
622
726
  async handleAuthenticate(id, _params) {
623
- await this.sendResult(id, { outcome: "unauthenticated" });
727
+ await this.sendResult(id, {});
624
728
  return false;
625
729
  }
626
730
  async handleLogout(id, _params) {
@@ -633,6 +737,10 @@ var ACPProtocolHandler = class {
633
737
  const existing = sessionId ? this.sessions.get(sessionId) : void 0;
634
738
  if (existing) {
635
739
  existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
740
+ const resumeServers = parseMcpServers(p.mcpServers);
741
+ if (resumeServers.length > 0) {
742
+ existing.mcpServers = resumeServers;
743
+ }
636
744
  await this.sendResult(id, {
637
745
  initialMode: {
638
746
  currentModeId: existing.modeId,
@@ -775,7 +883,12 @@ function makeACPServerAgentTurn(opts) {
775
883
  const turn = async (input, emit, api) => {
776
884
  let agent = agents.get(input.sessionId);
777
885
  if (!agent) {
778
- agent = await opts.agentFor(input.sessionId, process.cwd(), api);
886
+ agent = await opts.agentFor(
887
+ input.sessionId,
888
+ input.cwd ?? process.cwd(),
889
+ api,
890
+ input.mcpServers
891
+ );
779
892
  agents.set(input.sessionId, agent);
780
893
  if (pendingSeed.has(input.sessionId)) {
781
894
  pendingSeed.delete(input.sessionId);
@@ -1618,11 +1731,11 @@ function toolToPriority(tool) {
1618
1731
  }
1619
1732
 
1620
1733
  // src/agent/wrongstack-acp-agent.ts
1621
- import { timingSafeEqual } from "node:crypto";
1622
1734
  import { createServer } from "node:http";
1623
1735
  import { isIP } from "node:net";
1624
1736
  import { fileURLToPath } from "node:url";
1625
1737
  import { expandIPv6, writeErr as writeErr2 } from "@wrongstack/core/utils";
1738
+ import { timingSafeTokenEqual } from "@wrongstack/primitives";
1626
1739
  var LISTEN_RETRY_LIMIT = 5;
1627
1740
  var LISTEN_RETRY_BASE_MS = 25;
1628
1741
  var WrongStackACPServer = class {
@@ -1856,13 +1969,6 @@ var WrongStackACPServer = class {
1856
1969
  var defaultEchoRunTurn = async (_input, _emit) => {
1857
1970
  return { stopReason: "end_turn" };
1858
1971
  };
1859
- function timingSafeTokenEqual(supplied, expected) {
1860
- if (!supplied || !expected) return false;
1861
- const a = Buffer.from(supplied);
1862
- const b = Buffer.from(expected);
1863
- if (a.length !== b.length) return false;
1864
- return timingSafeEqual(a, b);
1865
- }
1866
1972
  function isLoopbackPeer(req) {
1867
1973
  const address = req.socket.remoteAddress?.replace(/^::ffff:/i, "");
1868
1974
  return address !== void 0 && isLoopbackHost(address);
@@ -10,5 +10,11 @@ interface JsonRpcError {
10
10
  data?: unknown;
11
11
  }
12
12
  export declare function isJsonRpcError(v: unknown): v is JsonRpcError;
13
+ /**
14
+ * True when an agent refused `session/new` (or similar) because the user
15
+ * must authenticate first. Official registry agents often return this
16
+ * (~19/31 in the 2026-09 protocol matrix) instead of creating a session.
17
+ */
18
+ export declare function isAuthRequiredError(err: unknown): boolean;
13
19
  export {};
14
20
  //# sourceMappingURL=acp-session-errors.d.ts.map
@@ -107,6 +107,18 @@ export declare class ACPSession {
107
107
  close(): Promise<void>;
108
108
  private allocId;
109
109
  private sendRequest;
110
+ /**
111
+ * `session/new`, then one authenticate+retry if the agent demands login.
112
+ * Logged-in CLIs succeed on the first call even when they advertise
113
+ * `authMethods`; we do not pop OAuth on every spawn.
114
+ */
115
+ private createSessionWithAuth;
116
+ /**
117
+ * Pick a non-terminal auth method and run `authenticate`. Terminal-only
118
+ * agents need an out-of-band login CLI (registry AUTHENTICATION.md) —
119
+ * we refuse rather than hang a TUI inside the JSON-RPC child.
120
+ */
121
+ private ensureAuthenticated;
110
122
  private sendResult;
111
123
  private sendErrorResponse;
112
124
  private responseSender;
package/dist/client.js CHANGED
@@ -254,6 +254,21 @@ function verbatimOptions(invocation) {
254
254
  // src/types/acp-v1.ts
255
255
  var ACP_PROTOCOL_VERSION = 1;
256
256
 
257
+ // src/version.ts
258
+ import { createRequire } from "node:module";
259
+ var require2 = createRequire(import.meta.url);
260
+ function readPackageVersion(load = () => require2("../package.json")) {
261
+ try {
262
+ const packageJson = load();
263
+ if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
264
+ return packageJson.version;
265
+ }
266
+ } catch {
267
+ }
268
+ return "dev";
269
+ }
270
+ var ACP_PACKAGE_VERSION = readPackageVersion();
271
+
257
272
  // src/client/acp-message-routing.ts
258
273
  function isBestEffortAckMethod(method) {
259
274
  return method === "mcp/connect" || method === "mcp/message" || method === "mcp/disconnect" || method === "elicitation/create" || method === "elicitation/complete";
@@ -728,6 +743,20 @@ var ACPSessionError = class extends Error {
728
743
  function isJsonRpcError(v) {
729
744
  return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
730
745
  }
746
+ function isAuthRequiredError(err) {
747
+ const message = err instanceof Error ? err.message : String(err);
748
+ if (/auth(_|-)?required|authentication required/i.test(message)) return true;
749
+ const cause = err instanceof ACPSessionError ? err.cause : err && typeof err === "object" && "cause" in err ? err.cause : err;
750
+ if (!cause || typeof cause !== "object") return false;
751
+ const data = cause.data;
752
+ if (data === "auth_required" || data === "AUTH_REQUIRED") return true;
753
+ if (data && typeof data === "object") {
754
+ const d = data;
755
+ if (d.authRequired === true) return true;
756
+ if (d.code === "auth_required" || d.code === "AUTH_REQUIRED") return true;
757
+ }
758
+ return false;
759
+ }
731
760
 
732
761
  // src/client/acp-session-ops.ts
733
762
  function filterMcpServers(agentCapabilities, servers) {
@@ -1772,7 +1801,7 @@ var ACPSession = class _ACPSession {
1772
1801
  fs: { readTextFile: true, writeTextFile: true },
1773
1802
  terminal: true
1774
1803
  },
1775
- clientInfo: { name: "wrongstack", title: "WrongStack", version: "0.287.0" }
1804
+ clientInfo: { name: "wrongstack", title: "WrongStack", version: ACP_PACKAGE_VERSION }
1776
1805
  });
1777
1806
  if (isJsonRpcError(result)) {
1778
1807
  throw new ACPSessionError("init_failed", `initialize failed: ${result.message}`, result);
@@ -1804,12 +1833,13 @@ var ACPSession = class _ACPSession {
1804
1833
  if (this.state === "closed") {
1805
1834
  throw new ACPSessionError("closed", "session is closed");
1806
1835
  }
1807
- if (this.state !== "ready") {
1836
+ if (this.state !== "ready" && this.state !== "authenticated") {
1808
1837
  throw new ACPSessionError(
1809
1838
  "protocol_error",
1810
1839
  `authenticate called in state=${this.state} (expected 'ready')`
1811
1840
  );
1812
1841
  }
1842
+ if (this.state === "authenticated") return;
1813
1843
  if (!this.authMethods.some((m) => m.id === methodId)) {
1814
1844
  throw new ACPSessionError(
1815
1845
  "auth_failed",
@@ -1912,7 +1942,7 @@ var ACPSession = class _ACPSession {
1912
1942
  return emptyRunResult("cancelled");
1913
1943
  }
1914
1944
  if (!this.sessionId) {
1915
- this.sessionId = await executeCreateSession(this.opContext());
1945
+ this.sessionId = await this.createSessionWithAuth();
1916
1946
  }
1917
1947
  if (signal.aborted) {
1918
1948
  return emptyRunResult("cancelled");
@@ -2048,6 +2078,54 @@ var ACPSession = class _ACPSession {
2048
2078
  });
2049
2079
  });
2050
2080
  }
2081
+ /**
2082
+ * `session/new`, then one authenticate+retry if the agent demands login.
2083
+ * Logged-in CLIs succeed on the first call even when they advertise
2084
+ * `authMethods`; we do not pop OAuth on every spawn.
2085
+ */
2086
+ async createSessionWithAuth() {
2087
+ try {
2088
+ return await executeCreateSession(this.opContext());
2089
+ } catch (err) {
2090
+ if (this.state === "authenticated" || !isAuthRequiredError(err)) {
2091
+ throw err instanceof ACPSessionError ? err : new ACPSessionError(
2092
+ "session_create_failed",
2093
+ err instanceof Error ? err.message : String(err),
2094
+ err
2095
+ );
2096
+ }
2097
+ await this.ensureAuthenticated();
2098
+ return executeCreateSession(this.opContext());
2099
+ }
2100
+ }
2101
+ /**
2102
+ * Pick a non-terminal auth method and run `authenticate`. Terminal-only
2103
+ * agents need an out-of-band login CLI (registry AUTHENTICATION.md) —
2104
+ * we refuse rather than hang a TUI inside the JSON-RPC child.
2105
+ */
2106
+ async ensureAuthenticated() {
2107
+ if (this.state === "authenticated") return;
2108
+ if (this.authMethods.length === 0) {
2109
+ throw new ACPSessionError(
2110
+ "auth_failed",
2111
+ "This agent requires authentication before a session can start, but advertised no authMethods. Log into the CLI, then retry."
2112
+ );
2113
+ }
2114
+ const inProcess = this.authMethods.find(
2115
+ (m) => m.type === void 0 || m.type === "agent" || m.type === "oauth" || m.type === "http"
2116
+ );
2117
+ if (inProcess) {
2118
+ await this.authenticate(inProcess.id);
2119
+ return;
2120
+ }
2121
+ const terminal = this.authMethods.find((m) => m.type === "terminal");
2122
+ const setupArgs = terminal?.args?.length ? terminal.args.join(" ") : void 0;
2123
+ const setup = setupArgs !== void 0 ? `${this.opts.command} ${setupArgs}` : `${this.opts.command}${this.opts.args?.length ? ` ${this.opts.args.join(" ")}` : ""}`;
2124
+ throw new ACPSessionError(
2125
+ "auth_failed",
2126
+ `This agent requires a terminal login before ACP can start. Run \`${setup}\` (or the CLI's /login), then retry.`
2127
+ );
2128
+ }
2051
2129
  sendResult(id, result) {
2052
2130
  return this.transport.send({ jsonrpc: "2.0", id, result });
2053
2131
  }
@@ -2076,7 +2154,11 @@ var ACPSession = class _ACPSession {
2076
2154
  clearTimeout(pending.timeoutHandle);
2077
2155
  this.pending.delete(msg.id);
2078
2156
  if (msg.error !== void 0) {
2079
- pending.reject(new Error(msg.error.message ?? "unknown JSON-RPC error"));
2157
+ const method = pending.method;
2158
+ const kind = method === "session/new" ? "session_create_failed" : method === "authenticate" ? "auth_failed" : "protocol_error";
2159
+ pending.reject(
2160
+ new ACPSessionError(kind, msg.error.message ?? "unknown JSON-RPC error", msg.error)
2161
+ );
2080
2162
  } else {
2081
2163
  pending.resolve(msg.result);
2082
2164
  }
@@ -2198,6 +2280,10 @@ async function makeACPSubagentRunnerWithStop(options) {
2198
2280
  } catch {
2199
2281
  }
2200
2282
  options.onProgress?.(event);
2283
+ try {
2284
+ options.publishLive?.(ctx, task, event);
2285
+ } catch {
2286
+ }
2201
2287
  };
2202
2288
  try {
2203
2289
  const result = await session.prompt([textContent(task.description)], ctx.signal, onProgress);