@wrongstack/acp 1.0.2 → 1.0.4

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
@@ -49,6 +49,64 @@ var DEFAULT_MODES = [
49
49
  description: "Default agent mode for code-generation tasks."
50
50
  }
51
51
  ];
52
+ function parseMcpServers(raw, onSkipped) {
53
+ if (!Array.isArray(raw)) return [];
54
+ const out = [];
55
+ for (const entry of raw) {
56
+ if (typeof entry !== "object" || entry === null) {
57
+ onSkipped?.("entry is not an object");
58
+ continue;
59
+ }
60
+ const e = entry;
61
+ const name = typeof e.name === "string" ? e.name.trim() : "";
62
+ if (name === "") {
63
+ onSkipped?.("entry has no name");
64
+ continue;
65
+ }
66
+ const type = typeof e.type === "string" ? e.type : "stdio";
67
+ if (type === "http" || type === "sse") {
68
+ if (typeof e.url !== "string" || e.url === "") {
69
+ onSkipped?.(`"${name}": ${type} server has no url`);
70
+ continue;
71
+ }
72
+ const headers = parseNameValuePairs(e.headers);
73
+ const url = e.url;
74
+ out.push(
75
+ type === "http" ? { type: "http", name, url, ...headers ? { headers } : {} } : { type: "sse", name, url, ...headers ? { headers } : {} }
76
+ );
77
+ continue;
78
+ }
79
+ if (type !== "stdio") {
80
+ onSkipped?.(`"${name}": unknown transport "${type}"`);
81
+ continue;
82
+ }
83
+ if (typeof e.command !== "string" || e.command === "") {
84
+ onSkipped?.(`"${name}": stdio server has no command`);
85
+ continue;
86
+ }
87
+ const args = Array.isArray(e.args) ? e.args.filter((a) => typeof a === "string") : void 0;
88
+ const env = parseNameValuePairs(e.env);
89
+ out.push({
90
+ name,
91
+ command: e.command,
92
+ ...args && args.length > 0 ? { args } : {},
93
+ ...env ? { env } : {}
94
+ });
95
+ }
96
+ return out;
97
+ }
98
+ function parseNameValuePairs(raw) {
99
+ if (!Array.isArray(raw)) return void 0;
100
+ const out = [];
101
+ for (const pair of raw) {
102
+ if (typeof pair !== "object" || pair === null) continue;
103
+ const p = pair;
104
+ if (typeof p.name === "string" && p.name !== "" && typeof p.value === "string") {
105
+ out.push({ name: p.name, value: p.value });
106
+ }
107
+ }
108
+ return out.length > 0 ? out : void 0;
109
+ }
52
110
  async function resolveSessionCwd(requested) {
53
111
  if (!path.isAbsolute(requested)) return null;
54
112
  const resolved = path.resolve(requested);
@@ -131,9 +189,16 @@ function buildInitializeResult(agentName, modes, configOptions) {
131
189
  audio: false,
132
190
  embeddedContext: true
133
191
  },
192
+ // All three ACP transports are supported. stdio is mandatory per spec
193
+ // and cannot be declined; http and sse are declared here because the
194
+ // agent now actually connects them (see `parseMcpServers` above and the
195
+ // per-session MCP registry in `buildAcpServerAgentFactory`). Before that
196
+ // wiring existed the array was destructured and thrown away at every
197
+ // entry point, so a client got a successful `session/new` and no tools —
198
+ // flip these back to false if that connection path is ever removed.
134
199
  mcpCapabilities: {
135
- http: false,
136
- sse: false
200
+ http: true,
201
+ sse: true
137
202
  },
138
203
  sessionCapabilities: {
139
204
  close: {},
@@ -173,6 +238,8 @@ async function handleSessionNewOp(ctx, id, params) {
173
238
  }
174
239
  cwd = resolved;
175
240
  }
241
+ const skipped = [];
242
+ const mcpServers = parseMcpServers(p.mcpServers, (reason) => skipped.push(reason));
176
243
  const sessionId = `sess_${ctx.allocId()}`;
177
244
  const now = (/* @__PURE__ */ new Date()).toISOString();
178
245
  const state = {
@@ -181,7 +248,8 @@ async function handleSessionNewOp(ctx, id, params) {
181
248
  abort: new AbortController(),
182
249
  modeId: DEFAULT_MODE_ID,
183
250
  createdAt: now,
184
- updatedAt: now
251
+ updatedAt: now,
252
+ ...mcpServers.length > 0 ? { mcpServers } : {}
185
253
  };
186
254
  ctx.sessions.set(sessionId, state);
187
255
  ctx.onSessionNew(state);
@@ -202,6 +270,7 @@ async function handleSessionNewOp(ctx, id, params) {
202
270
  }
203
271
  });
204
272
  }
273
+ await reportSkippedMcpServers(ctx, sessionId, skipped);
205
274
  await ctx.sendResult(id, {
206
275
  sessionId,
207
276
  modes: ctx.modes,
@@ -213,6 +282,8 @@ async function handleSessionLoadOp(ctx, id, params) {
213
282
  const p = params ?? {};
214
283
  const sessionId = typeof p.sessionId === "string" ? p.sessionId : null;
215
284
  const loadCwd = typeof p.cwd === "string" ? p.cwd : void 0;
285
+ const loadSkipped = [];
286
+ const loadMcpServers = parseMcpServers(p.mcpServers, (reason) => loadSkipped.push(reason));
216
287
  const existing = sessionId ? ctx.sessions.get(sessionId) : void 0;
217
288
  if (!existing && sessionId && ctx.store) {
218
289
  const persisted = await ctx.store.load(sessionId);
@@ -234,7 +305,8 @@ async function handleSessionLoadOp(ctx, id, params) {
234
305
  modeId: persisted.modeId ?? DEFAULT_MODE_ID,
235
306
  createdAt: persisted.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
236
307
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
237
- ...persisted.title !== void 0 ? { title: persisted.title } : {}
308
+ ...persisted.title !== void 0 ? { title: persisted.title } : {},
309
+ ...loadMcpServers.length > 0 ? { mcpServers: loadMcpServers } : {}
238
310
  };
239
311
  ctx.sessions.set(sessionId, restored);
240
312
  ctx.seedFor?.(sessionId, persisted.history ?? []);
@@ -245,6 +317,7 @@ async function handleSessionLoadOp(ctx, id, params) {
245
317
  sessionId,
246
318
  update: { sessionUpdate: "current_mode_update", modeId: restored.modeId }
247
319
  });
320
+ await reportSkippedMcpServers(ctx, sessionId, loadSkipped);
248
321
  await ctx.sendResult(id, {
249
322
  initialMode: { currentModeId: restored.modeId, availableModes: ctx.modes }
250
323
  });
@@ -253,6 +326,9 @@ async function handleSessionLoadOp(ctx, id, params) {
253
326
  }
254
327
  if (existing) {
255
328
  existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
329
+ if (loadMcpServers.length > 0) {
330
+ existing.mcpServers = loadMcpServers;
331
+ }
256
332
  const replay = ctx.replayFor?.(sessionId);
257
333
  if (replay) {
258
334
  for (const update of replay) {
@@ -273,6 +349,7 @@ async function handleSessionLoadOp(ctx, id, params) {
273
349
  modeId: existing.modeId
274
350
  }
275
351
  });
352
+ await reportSkippedMcpServers(ctx, sessionId, loadSkipped);
276
353
  await ctx.sendResult(id, {
277
354
  initialMode: {
278
355
  currentModeId: existing.modeId,
@@ -305,6 +382,9 @@ async function handleSessionForkOp(ctx, id, params) {
305
382
  }
306
383
  forkCwd = resolved;
307
384
  }
385
+ const forkSkipped = [];
386
+ const forkRequested = parseMcpServers(p.mcpServers, (reason) => forkSkipped.push(reason));
387
+ const forkMcpServers = forkRequested.length > 0 ? forkRequested : source.mcpServers;
308
388
  const now = (/* @__PURE__ */ new Date()).toISOString();
309
389
  const sessionId = `sess_${ctx.allocId()}`;
310
390
  const forked = {
@@ -314,7 +394,8 @@ async function handleSessionForkOp(ctx, id, params) {
314
394
  modeId: source.modeId,
315
395
  createdAt: now,
316
396
  updatedAt: now,
317
- ...source.title !== void 0 ? { title: source.title } : {}
397
+ ...source.title !== void 0 ? { title: source.title } : {},
398
+ ...forkMcpServers && forkMcpServers.length > 0 ? { mcpServers: forkMcpServers } : {}
318
399
  };
319
400
  const history = (ctx.replayFor?.(sourceId) ?? []).map((update) => ({
320
401
  sessionUpdate: update.sessionUpdate,
@@ -328,6 +409,7 @@ async function handleSessionForkOp(ctx, id, params) {
328
409
  sessionId,
329
410
  update: { sessionUpdate: "current_mode_update", modeId: forked.modeId }
330
411
  });
412
+ await reportSkippedMcpServers(ctx, sessionId, forkSkipped);
331
413
  await ctx.sendResult(id, {
332
414
  sessionId,
333
415
  modes: ctx.modes,
@@ -367,7 +449,13 @@ async function handleSessionPromptOp(ctx, id, params) {
367
449
  };
368
450
  try {
369
451
  result = await ctx.runTurn(
370
- { sessionId, prompt: p.prompt, signal: turnSignal.signal },
452
+ {
453
+ sessionId,
454
+ prompt: p.prompt,
455
+ signal: turnSignal.signal,
456
+ cwd: session.cwd,
457
+ ...session.mcpServers ? { mcpServers: session.mcpServers } : {}
458
+ },
371
459
  emit,
372
460
  api
373
461
  );
@@ -425,6 +513,22 @@ async function handleSetConfigOptionOp(ctx, id, params) {
425
513
  await ctx.sendResult(id, { configOptions: [...ctx.configOptions] });
426
514
  return false;
427
515
  }
516
+ async function reportSkippedMcpServers(ctx, sessionId, skipped) {
517
+ if (skipped.length === 0) return;
518
+ try {
519
+ await ctx.sendNotification({
520
+ sessionId,
521
+ update: {
522
+ sessionUpdate: "agent_message_chunk",
523
+ content: {
524
+ type: "text",
525
+ text: `Ignored ${skipped.length} malformed mcpServers entr${skipped.length === 1 ? "y" : "ies"}: ${skipped.join("; ")}`
526
+ }
527
+ }
528
+ });
529
+ } catch {
530
+ }
531
+ }
428
532
 
429
533
  // src/agent/protocol-handler.ts
430
534
  var ACPProtocolHandler = class {
@@ -623,7 +727,7 @@ var ACPProtocolHandler = class {
623
727
  return false;
624
728
  }
625
729
  async handleAuthenticate(id, _params) {
626
- await this.sendResult(id, { outcome: "unauthenticated" });
730
+ await this.sendResult(id, {});
627
731
  return false;
628
732
  }
629
733
  async handleLogout(id, _params) {
@@ -636,6 +740,10 @@ var ACPProtocolHandler = class {
636
740
  const existing = sessionId ? this.sessions.get(sessionId) : void 0;
637
741
  if (existing) {
638
742
  existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
743
+ const resumeServers = parseMcpServers(p.mcpServers);
744
+ if (resumeServers.length > 0) {
745
+ existing.mcpServers = resumeServers;
746
+ }
639
747
  await this.sendResult(id, {
640
748
  initialMode: {
641
749
  currentModeId: existing.modeId,
@@ -1937,7 +2045,7 @@ async function authorizeAcpCallback(permissionPolicy, partial, callbackOptions)
1937
2045
  title: partial.title,
1938
2046
  kind: partial.kind,
1939
2047
  status: "pending",
1940
- ...partial.rawInput ? { rawInput: partial.rawInput } : {}
2048
+ rawInput: partial.rawInput
1941
2049
  },
1942
2050
  options: [
1943
2051
  { optionId: "allow", name: "Allow", kind: "allow_once" },
@@ -2045,6 +2153,20 @@ var ACPSessionError = class extends Error {
2045
2153
  function isJsonRpcError(v) {
2046
2154
  return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
2047
2155
  }
2156
+ function isAuthRequiredError(err) {
2157
+ const message = err instanceof Error ? err.message : String(err);
2158
+ if (/auth(_|-)?required|authentication required/i.test(message)) return true;
2159
+ const cause = err instanceof ACPSessionError ? err.cause : err && typeof err === "object" && "cause" in err ? err.cause : err;
2160
+ if (!cause || typeof cause !== "object") return false;
2161
+ const data = cause.data;
2162
+ if (data === "auth_required" || data === "AUTH_REQUIRED") return true;
2163
+ if (data && typeof data === "object") {
2164
+ const d = data;
2165
+ if (d.authRequired === true) return true;
2166
+ if (d.code === "auth_required" || d.code === "AUTH_REQUIRED") return true;
2167
+ }
2168
+ return false;
2169
+ }
2048
2170
 
2049
2171
  // src/client/acp-session-ops.ts
2050
2172
  function filterMcpServers(agentCapabilities, servers) {
@@ -2806,7 +2928,7 @@ var WebSocketClientTransport = class {
2806
2928
  if (pending === null) return;
2807
2929
  this.pendingStart = null;
2808
2930
  this.closed = true;
2809
- if (this.ws === ws) this.ws = null;
2931
+ this.ws = null;
2810
2932
  this.handlers.clear();
2811
2933
  try {
2812
2934
  ws.close();
@@ -2830,7 +2952,7 @@ var WebSocketClientTransport = class {
2830
2952
  }
2831
2953
  this.pendingStart = null;
2832
2954
  this.closed = true;
2833
- if (this.ws === ws) this.ws = null;
2955
+ this.ws = null;
2834
2956
  this.handlers.clear();
2835
2957
  clearTimeout(pending.timer);
2836
2958
  const message = ev && typeof ev === "object" && "message" in ev ? String(ev.message) : "WebSocket error";
@@ -3089,7 +3211,7 @@ var ACPSession = class _ACPSession {
3089
3211
  fs: { readTextFile: true, writeTextFile: true },
3090
3212
  terminal: true
3091
3213
  },
3092
- clientInfo: { name: "wrongstack", title: "WrongStack", version: "0.287.0" }
3214
+ clientInfo: { name: "wrongstack", title: "WrongStack", version: ACP_PACKAGE_VERSION }
3093
3215
  });
3094
3216
  if (isJsonRpcError(result)) {
3095
3217
  throw new ACPSessionError("init_failed", `initialize failed: ${result.message}`, result);
@@ -3121,12 +3243,13 @@ var ACPSession = class _ACPSession {
3121
3243
  if (this.state === "closed") {
3122
3244
  throw new ACPSessionError("closed", "session is closed");
3123
3245
  }
3124
- if (this.state !== "ready") {
3246
+ if (this.state !== "ready" && this.state !== "authenticated") {
3125
3247
  throw new ACPSessionError(
3126
3248
  "protocol_error",
3127
3249
  `authenticate called in state=${this.state} (expected 'ready')`
3128
3250
  );
3129
3251
  }
3252
+ if (this.state === "authenticated") return;
3130
3253
  if (!this.authMethods.some((m) => m.id === methodId)) {
3131
3254
  throw new ACPSessionError(
3132
3255
  "auth_failed",
@@ -3165,9 +3288,12 @@ var ACPSession = class _ACPSession {
3165
3288
  // Session management delegation
3166
3289
  // ──────────────────────────────────────────────────────────────────────
3167
3290
  opContext() {
3291
+ const self = this;
3168
3292
  return {
3169
3293
  closed: this.closed,
3170
- sessionId: this.sessionId,
3294
+ get sessionId() {
3295
+ return self.sessionId;
3296
+ },
3171
3297
  agentCapabilities: this.agentCapabilities,
3172
3298
  opts: this.opts,
3173
3299
  allocId: () => this.allocId(),
@@ -3226,7 +3352,7 @@ var ACPSession = class _ACPSession {
3226
3352
  return emptyRunResult("cancelled");
3227
3353
  }
3228
3354
  if (!this.sessionId) {
3229
- this.sessionId = await executeCreateSession(this.opContext());
3355
+ this.sessionId = await this.createSessionWithAuth();
3230
3356
  }
3231
3357
  if (signal.aborted) {
3232
3358
  return emptyRunResult("cancelled");
@@ -3362,6 +3488,54 @@ var ACPSession = class _ACPSession {
3362
3488
  });
3363
3489
  });
3364
3490
  }
3491
+ /**
3492
+ * `session/new`, then one authenticate+retry if the agent demands login.
3493
+ * Logged-in CLIs succeed on the first call even when they advertise
3494
+ * `authMethods`; we do not pop OAuth on every spawn.
3495
+ */
3496
+ async createSessionWithAuth() {
3497
+ try {
3498
+ return await executeCreateSession(this.opContext());
3499
+ } catch (err) {
3500
+ if (this.state === "authenticated" || !isAuthRequiredError(err)) {
3501
+ throw err instanceof ACPSessionError ? err : new ACPSessionError(
3502
+ "session_create_failed",
3503
+ err instanceof Error ? err.message : String(err),
3504
+ err
3505
+ );
3506
+ }
3507
+ await this.ensureAuthenticated();
3508
+ return executeCreateSession(this.opContext());
3509
+ }
3510
+ }
3511
+ /**
3512
+ * Pick a non-terminal auth method and run `authenticate`. Terminal-only
3513
+ * agents need an out-of-band login CLI (registry AUTHENTICATION.md) —
3514
+ * we refuse rather than hang a TUI inside the JSON-RPC child.
3515
+ */
3516
+ async ensureAuthenticated() {
3517
+ if (this.state === "authenticated") return;
3518
+ if (this.authMethods.length === 0) {
3519
+ throw new ACPSessionError(
3520
+ "auth_failed",
3521
+ "This agent requires authentication before a session can start, but advertised no authMethods. Log into the CLI, then retry."
3522
+ );
3523
+ }
3524
+ const inProcess = this.authMethods.find(
3525
+ (m) => m.type === void 0 || m.type === "agent" || m.type === "oauth" || m.type === "http"
3526
+ );
3527
+ if (inProcess) {
3528
+ await this.authenticate(inProcess.id);
3529
+ return;
3530
+ }
3531
+ const terminal = this.authMethods.find((m) => m.type === "terminal");
3532
+ const setupArgs = terminal?.args?.length ? terminal.args.join(" ") : void 0;
3533
+ const setup = setupArgs !== void 0 ? `${this.opts.command} ${setupArgs}` : `${this.opts.command}${this.opts.args?.length ? ` ${this.opts.args.join(" ")}` : ""}`;
3534
+ throw new ACPSessionError(
3535
+ "auth_failed",
3536
+ `This agent requires a terminal login before ACP can start. Run \`${setup}\` (or the CLI's /login), then retry.`
3537
+ );
3538
+ }
3365
3539
  sendResult(id, result) {
3366
3540
  return this.transport.send({ jsonrpc: "2.0", id, result });
3367
3541
  }
@@ -3390,7 +3564,11 @@ var ACPSession = class _ACPSession {
3390
3564
  clearTimeout(pending.timeoutHandle);
3391
3565
  this.pending.delete(msg.id);
3392
3566
  if (msg.error !== void 0) {
3393
- pending.reject(new Error(msg.error.message ?? "unknown JSON-RPC error"));
3567
+ const method = pending.method;
3568
+ const kind = method === "session/new" ? "session_create_failed" : method === "authenticate" ? "auth_failed" : "protocol_error";
3569
+ pending.reject(
3570
+ new ACPSessionError(kind, msg.error.message ?? "unknown JSON-RPC error", msg.error)
3571
+ );
3394
3572
  } else {
3395
3573
  pending.resolve(msg.result);
3396
3574
  }
@@ -3830,7 +4008,7 @@ var AGENTS_CATALOG = [
3830
4008
  id: "cline",
3831
4009
  displayName: "Cline",
3832
4010
  vendor: "community",
3833
- probe: { command: "npx", args: ["--version"] },
4011
+ probe: { command: "cline", args: ["--version"] },
3834
4012
  // Registry id `cline`: the `cline` npm package speaks ACP behind `--acp`.
3835
4013
  acp: {
3836
4014
  command: "npx",
@@ -3873,7 +4051,8 @@ var AGENTS_CATALOG = [
3873
4051
  fs: true
3874
4052
  },
3875
4053
  integration: "experimental",
3876
- // Canonical repo URL — the org renamed; All-Hands-AI/OpenHands 301-redirects here.
4054
+ // Not in the official agentclientprotocol/registry (2026-09). Keep as a
4055
+ // local-PATH fallback; probe/spawn may hang if the binary has no ACP entry.
3877
4056
  docs: "https://github.com/OpenHands/OpenHands"
3878
4057
  },
3879
4058
  // ── Vendor CLIs (native binaries) ───────────────────────────────────
@@ -3906,6 +4085,7 @@ var AGENTS_CATALOG = [
3906
4085
  fs: true
3907
4086
  },
3908
4087
  integration: "experimental",
4088
+ // Not in the official agentclientprotocol/registry (2026-09).
3909
4089
  docs: "https://kiro.dev"
3910
4090
  },
3911
4091
  {
@@ -3928,8 +4108,9 @@ var AGENTS_CATALOG = [
3928
4108
  id: "mistral-vibe",
3929
4109
  displayName: "Mistral Vibe",
3930
4110
  vendor: "community",
3931
- probe: { command: "vibe", args: ["--version"] },
3932
- acp: { command: "vibe", args: [] },
4111
+ probe: { command: "vibe-acp", args: ["--version"] },
4112
+ // Official registry ships a dedicated `vibe-acp` binary, not bare `vibe`.
4113
+ acp: { command: "vibe-acp", args: [] },
3933
4114
  supports: {
3934
4115
  loadSession: false,
3935
4116
  promptImages: false,
@@ -4051,6 +4232,10 @@ async function makeACPSubagentRunnerWithStop(options) {
4051
4232
  } catch {
4052
4233
  }
4053
4234
  options.onProgress?.(event);
4235
+ try {
4236
+ options.publishLive?.(ctx, task, event);
4237
+ } catch {
4238
+ }
4054
4239
  };
4055
4240
  try {
4056
4241
  const result = await session.prompt([textContent(task.description)], ctx.signal, onProgress);
@@ -4137,10 +4322,13 @@ var REGISTRY_ID_ALIASES = {
4137
4322
  "gemini-cli": "gemini",
4138
4323
  "codex-cli": "codex-acp",
4139
4324
  copilot: "github-copilot-cli",
4140
- // Kimi's live registry id is `kimi` — same as our catalog id, so the
4141
- // alias is identity. Listed explicitly so `resolveAcpAgentCommand`
4142
- // finds the live entry when the registry is synced.
4143
- kimi: "kimi"
4325
+ kimi: "kimi",
4326
+ cline: "cline",
4327
+ goose: "goose",
4328
+ opencode: "opencode",
4329
+ cursor: "cursor",
4330
+ "qwen-code": "qwen-code",
4331
+ "mistral-vibe": "mistral-vibe"
4144
4332
  };
4145
4333
  function resolveAcpAgentCommand(id, overrides, live) {
4146
4334
  const ov = overrides?.[id];
@@ -4563,20 +4751,20 @@ async function runEnsemble(opts) {
4563
4751
  const detectedById = new Map(detected.map((a) => [a.id, a]));
4564
4752
  const runnable = [];
4565
4753
  for (const id of requested) {
4754
+ const cmd = resolveCmd(id);
4566
4755
  const det = detectedById.get(id);
4567
- if (!det?.installed) {
4756
+ const pkgLauncher = cmd?.command === "npx" || cmd?.command === "uvx";
4757
+ if (det && !det.installed && !pkgLauncher) {
4568
4758
  setResult(results, id, {
4569
4759
  status: "skipped",
4570
- reason: det?.reason ?? "not in catalog"
4760
+ reason: det.reason ?? "binary not found"
4571
4761
  });
4572
4762
  continue;
4573
4763
  }
4574
- const cmd = resolveCmd(id);
4575
4764
  if (!cmd) {
4576
4765
  setResult(results, id, {
4577
- status: "failed",
4578
- error: { kind: "unknown_agent", message: `Unknown ACP agent: ${id}` },
4579
- durationMs: 0
4766
+ status: "skipped",
4767
+ reason: det?.reason ?? "not in catalog"
4580
4768
  });
4581
4769
  continue;
4582
4770
  }
@@ -4712,9 +4900,17 @@ function mapRegistryEntry(entry, platformKey = currentPlatformKey()) {
4712
4900
  const dist = entry.distribution;
4713
4901
  let acp = null;
4714
4902
  if (dist?.npx?.package) {
4715
- acp = { command: "npx", args: ["-y", dist.npx.package, ...dist.npx.args ?? []] };
4903
+ acp = {
4904
+ command: "npx",
4905
+ args: ["-y", dist.npx.package, ...dist.npx.args ?? []],
4906
+ ...dist.npx.env ? { env: dist.npx.env } : {}
4907
+ };
4716
4908
  } else if (dist?.uvx?.package) {
4717
- acp = { command: "uvx", args: [dist.uvx.package, ...dist.uvx.args ?? []] };
4909
+ acp = {
4910
+ command: "uvx",
4911
+ args: [dist.uvx.package, ...dist.uvx.args ?? []],
4912
+ ...dist.uvx.env ? { env: dist.uvx.env } : {}
4913
+ };
4718
4914
  } else if (dist?.binary) {
4719
4915
  const target = dist.binary[platformKey];
4720
4916
  if (target?.cmd) {
@@ -11,8 +11,8 @@
11
11
  * Connected to the Director / MultiAgentCoordinator via the
12
12
  * `SubagentRunner` interface (same shape as `AgentSubagentRunner`).
13
13
  */
14
- import type { SubagentRunner } from '@wrongstack/core/types';
15
- import { type ACPProgressHandler, ACPSession } from '../client/acp-session.js';
14
+ import type { SubagentRunContext, SubagentRunner, TaskSpec } from '@wrongstack/core/types';
15
+ import { type ACPProgressEvent, type ACPProgressHandler, ACPSession } from '../client/acp-session.js';
16
16
  import type { PermissionPolicy } from '../client/permission.js';
17
17
  import type { McpServer } from '../types/acp-v1.js';
18
18
  export interface ACPSubagentRunnerOptions {
@@ -40,6 +40,12 @@ export interface ACPSubagentRunnerOptions {
40
40
  * stream, instead of waiting for the buffered final result.
41
41
  */
42
42
  onProgress?: ACPProgressHandler | undefined;
43
+ /**
44
+ * Host live-view hook. Called with the run context so a fleet/TUI/WebUI
45
+ * publisher can attribute tool calls and text to the right subagent.
46
+ * Invoked in addition to `onProgress`.
47
+ */
48
+ publishLive?: ((ctx: SubagentRunContext, task: TaskSpec, event: ACPProgressEvent) => void) | undefined;
43
49
  /**
44
50
  * Permission policy for the external agent's `session/request_permission`
45
51
  * calls. Defaults to the session's own default. Inject the host's
@@ -39,10 +39,12 @@ export interface RegistryAgentEntry {
39
39
  npx?: {
40
40
  package: string;
41
41
  args?: string[];
42
+ env?: Record<string, string>;
42
43
  };
43
44
  uvx?: {
44
45
  package: string;
45
46
  args?: string[];
47
+ env?: Record<string, string>;
46
48
  };
47
49
  binary?: Record<string, {
48
50
  archive?: string;
@@ -95,7 +95,15 @@ export interface AuthMethod {
95
95
  id: string;
96
96
  name: string;
97
97
  description?: string | undefined;
98
- type?: 'agent' | 'oauth' | 'http' | undefined;
98
+ /**
99
+ * Registry agents advertise `agent` (OAuth in-process) or `terminal`
100
+ * (separate login CLI). `oauth`/`http` are spec aliases; `env_var` is
101
+ * used by some agents but is not a registry-supported setup path.
102
+ */
103
+ type?: 'agent' | 'oauth' | 'http' | 'terminal' | 'env_var' | undefined;
104
+ /** Extra argv for `type: 'terminal'` setup (replaces the ACP entry args). */
105
+ args?: string[] | undefined;
106
+ env?: Record<string, string> | undefined;
99
107
  }
100
108
  export interface AuthenticateRequest {
101
109
  methodId: string;