@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.
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,
@@ -1293,11 +1401,11 @@ function toolToPriority(tool) {
1293
1401
  }
1294
1402
 
1295
1403
  // src/agent/wrongstack-acp-agent.ts
1296
- import { timingSafeEqual } from "node:crypto";
1297
1404
  import { createServer } from "node:http";
1298
1405
  import { isIP } from "node:net";
1299
1406
  import { fileURLToPath } from "node:url";
1300
1407
  import { expandIPv6, writeErr as writeErr2 } from "@wrongstack/core/utils";
1408
+ import { timingSafeTokenEqual } from "@wrongstack/primitives";
1301
1409
  var LISTEN_RETRY_LIMIT = 5;
1302
1410
  var LISTEN_RETRY_BASE_MS = 25;
1303
1411
  var WrongStackACPServer = class {
@@ -1531,13 +1639,6 @@ var WrongStackACPServer = class {
1531
1639
  var defaultEchoRunTurn = async (_input, _emit) => {
1532
1640
  return { stopReason: "end_turn" };
1533
1641
  };
1534
- function timingSafeTokenEqual(supplied, expected) {
1535
- if (!supplied || !expected) return false;
1536
- const a = Buffer.from(supplied);
1537
- const b = Buffer.from(expected);
1538
- if (a.length !== b.length) return false;
1539
- return timingSafeEqual(a, b);
1540
- }
1541
1642
  function isLoopbackPeer(req) {
1542
1643
  const address = req.socket.remoteAddress?.replace(/^::ffff:/i, "");
1543
1644
  return address !== void 0 && isLoopbackHost(address);
@@ -2045,6 +2146,20 @@ var ACPSessionError = class extends Error {
2045
2146
  function isJsonRpcError(v) {
2046
2147
  return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
2047
2148
  }
2149
+ function isAuthRequiredError(err) {
2150
+ const message = err instanceof Error ? err.message : String(err);
2151
+ if (/auth(_|-)?required|authentication required/i.test(message)) return true;
2152
+ const cause = err instanceof ACPSessionError ? err.cause : err && typeof err === "object" && "cause" in err ? err.cause : err;
2153
+ if (!cause || typeof cause !== "object") return false;
2154
+ const data = cause.data;
2155
+ if (data === "auth_required" || data === "AUTH_REQUIRED") return true;
2156
+ if (data && typeof data === "object") {
2157
+ const d = data;
2158
+ if (d.authRequired === true) return true;
2159
+ if (d.code === "auth_required" || d.code === "AUTH_REQUIRED") return true;
2160
+ }
2161
+ return false;
2162
+ }
2048
2163
 
2049
2164
  // src/client/acp-session-ops.ts
2050
2165
  function filterMcpServers(agentCapabilities, servers) {
@@ -3089,7 +3204,7 @@ var ACPSession = class _ACPSession {
3089
3204
  fs: { readTextFile: true, writeTextFile: true },
3090
3205
  terminal: true
3091
3206
  },
3092
- clientInfo: { name: "wrongstack", title: "WrongStack", version: "0.287.0" }
3207
+ clientInfo: { name: "wrongstack", title: "WrongStack", version: ACP_PACKAGE_VERSION }
3093
3208
  });
3094
3209
  if (isJsonRpcError(result)) {
3095
3210
  throw new ACPSessionError("init_failed", `initialize failed: ${result.message}`, result);
@@ -3121,12 +3236,13 @@ var ACPSession = class _ACPSession {
3121
3236
  if (this.state === "closed") {
3122
3237
  throw new ACPSessionError("closed", "session is closed");
3123
3238
  }
3124
- if (this.state !== "ready") {
3239
+ if (this.state !== "ready" && this.state !== "authenticated") {
3125
3240
  throw new ACPSessionError(
3126
3241
  "protocol_error",
3127
3242
  `authenticate called in state=${this.state} (expected 'ready')`
3128
3243
  );
3129
3244
  }
3245
+ if (this.state === "authenticated") return;
3130
3246
  if (!this.authMethods.some((m) => m.id === methodId)) {
3131
3247
  throw new ACPSessionError(
3132
3248
  "auth_failed",
@@ -3229,7 +3345,7 @@ var ACPSession = class _ACPSession {
3229
3345
  return emptyRunResult("cancelled");
3230
3346
  }
3231
3347
  if (!this.sessionId) {
3232
- this.sessionId = await executeCreateSession(this.opContext());
3348
+ this.sessionId = await this.createSessionWithAuth();
3233
3349
  }
3234
3350
  if (signal.aborted) {
3235
3351
  return emptyRunResult("cancelled");
@@ -3365,6 +3481,54 @@ var ACPSession = class _ACPSession {
3365
3481
  });
3366
3482
  });
3367
3483
  }
3484
+ /**
3485
+ * `session/new`, then one authenticate+retry if the agent demands login.
3486
+ * Logged-in CLIs succeed on the first call even when they advertise
3487
+ * `authMethods`; we do not pop OAuth on every spawn.
3488
+ */
3489
+ async createSessionWithAuth() {
3490
+ try {
3491
+ return await executeCreateSession(this.opContext());
3492
+ } catch (err) {
3493
+ if (this.state === "authenticated" || !isAuthRequiredError(err)) {
3494
+ throw err instanceof ACPSessionError ? err : new ACPSessionError(
3495
+ "session_create_failed",
3496
+ err instanceof Error ? err.message : String(err),
3497
+ err
3498
+ );
3499
+ }
3500
+ await this.ensureAuthenticated();
3501
+ return executeCreateSession(this.opContext());
3502
+ }
3503
+ }
3504
+ /**
3505
+ * Pick a non-terminal auth method and run `authenticate`. Terminal-only
3506
+ * agents need an out-of-band login CLI (registry AUTHENTICATION.md) —
3507
+ * we refuse rather than hang a TUI inside the JSON-RPC child.
3508
+ */
3509
+ async ensureAuthenticated() {
3510
+ if (this.state === "authenticated") return;
3511
+ if (this.authMethods.length === 0) {
3512
+ throw new ACPSessionError(
3513
+ "auth_failed",
3514
+ "This agent requires authentication before a session can start, but advertised no authMethods. Log into the CLI, then retry."
3515
+ );
3516
+ }
3517
+ const inProcess = this.authMethods.find(
3518
+ (m) => m.type === void 0 || m.type === "agent" || m.type === "oauth" || m.type === "http"
3519
+ );
3520
+ if (inProcess) {
3521
+ await this.authenticate(inProcess.id);
3522
+ return;
3523
+ }
3524
+ const terminal = this.authMethods.find((m) => m.type === "terminal");
3525
+ const setupArgs = terminal?.args?.length ? terminal.args.join(" ") : void 0;
3526
+ const setup = setupArgs !== void 0 ? `${this.opts.command} ${setupArgs}` : `${this.opts.command}${this.opts.args?.length ? ` ${this.opts.args.join(" ")}` : ""}`;
3527
+ throw new ACPSessionError(
3528
+ "auth_failed",
3529
+ `This agent requires a terminal login before ACP can start. Run \`${setup}\` (or the CLI's /login), then retry.`
3530
+ );
3531
+ }
3368
3532
  sendResult(id, result) {
3369
3533
  return this.transport.send({ jsonrpc: "2.0", id, result });
3370
3534
  }
@@ -3393,7 +3557,11 @@ var ACPSession = class _ACPSession {
3393
3557
  clearTimeout(pending.timeoutHandle);
3394
3558
  this.pending.delete(msg.id);
3395
3559
  if (msg.error !== void 0) {
3396
- pending.reject(new Error(msg.error.message ?? "unknown JSON-RPC error"));
3560
+ const method = pending.method;
3561
+ const kind = method === "session/new" ? "session_create_failed" : method === "authenticate" ? "auth_failed" : "protocol_error";
3562
+ pending.reject(
3563
+ new ACPSessionError(kind, msg.error.message ?? "unknown JSON-RPC error", msg.error)
3564
+ );
3397
3565
  } else {
3398
3566
  pending.resolve(msg.result);
3399
3567
  }
@@ -3833,7 +4001,7 @@ var AGENTS_CATALOG = [
3833
4001
  id: "cline",
3834
4002
  displayName: "Cline",
3835
4003
  vendor: "community",
3836
- probe: { command: "npx", args: ["--version"] },
4004
+ probe: { command: "cline", args: ["--version"] },
3837
4005
  // Registry id `cline`: the `cline` npm package speaks ACP behind `--acp`.
3838
4006
  acp: {
3839
4007
  command: "npx",
@@ -3876,7 +4044,8 @@ var AGENTS_CATALOG = [
3876
4044
  fs: true
3877
4045
  },
3878
4046
  integration: "experimental",
3879
- // Canonical repo URL — the org renamed; All-Hands-AI/OpenHands 301-redirects here.
4047
+ // Not in the official agentclientprotocol/registry (2026-09). Keep as a
4048
+ // local-PATH fallback; probe/spawn may hang if the binary has no ACP entry.
3880
4049
  docs: "https://github.com/OpenHands/OpenHands"
3881
4050
  },
3882
4051
  // ── Vendor CLIs (native binaries) ───────────────────────────────────
@@ -3909,6 +4078,7 @@ var AGENTS_CATALOG = [
3909
4078
  fs: true
3910
4079
  },
3911
4080
  integration: "experimental",
4081
+ // Not in the official agentclientprotocol/registry (2026-09).
3912
4082
  docs: "https://kiro.dev"
3913
4083
  },
3914
4084
  {
@@ -3931,8 +4101,9 @@ var AGENTS_CATALOG = [
3931
4101
  id: "mistral-vibe",
3932
4102
  displayName: "Mistral Vibe",
3933
4103
  vendor: "community",
3934
- probe: { command: "vibe", args: ["--version"] },
3935
- acp: { command: "vibe", args: [] },
4104
+ probe: { command: "vibe-acp", args: ["--version"] },
4105
+ // Official registry ships a dedicated `vibe-acp` binary, not bare `vibe`.
4106
+ acp: { command: "vibe-acp", args: [] },
3936
4107
  supports: {
3937
4108
  loadSession: false,
3938
4109
  promptImages: false,
@@ -4054,6 +4225,10 @@ async function makeACPSubagentRunnerWithStop(options) {
4054
4225
  } catch {
4055
4226
  }
4056
4227
  options.onProgress?.(event);
4228
+ try {
4229
+ options.publishLive?.(ctx, task, event);
4230
+ } catch {
4231
+ }
4057
4232
  };
4058
4233
  try {
4059
4234
  const result = await session.prompt([textContent(task.description)], ctx.signal, onProgress);
@@ -4140,10 +4315,13 @@ var REGISTRY_ID_ALIASES = {
4140
4315
  "gemini-cli": "gemini",
4141
4316
  "codex-cli": "codex-acp",
4142
4317
  copilot: "github-copilot-cli",
4143
- // Kimi's live registry id is `kimi` — same as our catalog id, so the
4144
- // alias is identity. Listed explicitly so `resolveAcpAgentCommand`
4145
- // finds the live entry when the registry is synced.
4146
- kimi: "kimi"
4318
+ kimi: "kimi",
4319
+ cline: "cline",
4320
+ goose: "goose",
4321
+ opencode: "opencode",
4322
+ cursor: "cursor",
4323
+ "qwen-code": "qwen-code",
4324
+ "mistral-vibe": "mistral-vibe"
4147
4325
  };
4148
4326
  function resolveAcpAgentCommand(id, overrides, live) {
4149
4327
  const ov = overrides?.[id];
@@ -4566,20 +4744,20 @@ async function runEnsemble(opts) {
4566
4744
  const detectedById = new Map(detected.map((a) => [a.id, a]));
4567
4745
  const runnable = [];
4568
4746
  for (const id of requested) {
4747
+ const cmd = resolveCmd(id);
4569
4748
  const det = detectedById.get(id);
4570
- if (!det?.installed) {
4749
+ const pkgLauncher = cmd?.command === "npx" || cmd?.command === "uvx";
4750
+ if (det && !det.installed && !pkgLauncher) {
4571
4751
  setResult(results, id, {
4572
4752
  status: "skipped",
4573
- reason: det?.reason ?? "not in catalog"
4753
+ reason: det.reason ?? "binary not found"
4574
4754
  });
4575
4755
  continue;
4576
4756
  }
4577
- const cmd = resolveCmd(id);
4578
4757
  if (!cmd) {
4579
4758
  setResult(results, id, {
4580
- status: "failed",
4581
- error: { kind: "unknown_agent", message: `Unknown ACP agent: ${id}` },
4582
- durationMs: 0
4759
+ status: "skipped",
4760
+ reason: det?.reason ?? "not in catalog"
4583
4761
  });
4584
4762
  continue;
4585
4763
  }
@@ -4715,9 +4893,17 @@ function mapRegistryEntry(entry, platformKey = currentPlatformKey()) {
4715
4893
  const dist = entry.distribution;
4716
4894
  let acp = null;
4717
4895
  if (dist?.npx?.package) {
4718
- acp = { command: "npx", args: ["-y", dist.npx.package, ...dist.npx.args ?? []] };
4896
+ acp = {
4897
+ command: "npx",
4898
+ args: ["-y", dist.npx.package, ...dist.npx.args ?? []],
4899
+ ...dist.npx.env ? { env: dist.npx.env } : {}
4900
+ };
4719
4901
  } else if (dist?.uvx?.package) {
4720
- acp = { command: "uvx", args: [dist.uvx.package, ...dist.uvx.args ?? []] };
4902
+ acp = {
4903
+ command: "uvx",
4904
+ args: [dist.uvx.package, ...dist.uvx.args ?? []],
4905
+ ...dist.uvx.env ? { env: dist.uvx.env } : {}
4906
+ };
4721
4907
  } else if (dist?.binary) {
4722
4908
  const target = dist.binary[platformKey];
4723
4909
  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;