@wrongstack/cli 1.1.1 → 1.1.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.
Files changed (42) hide show
  1. package/dist/{acp-HCGUTFNE.js → acp-MZ5MQBFN.js} +13 -5
  2. package/dist/acp-leader/acp-leader-confirm.d.ts +18 -0
  3. package/dist/acp-leader/acp-leader-context.d.ts +29 -0
  4. package/dist/acp-leader/acp-leader-fleet.d.ts +26 -0
  5. package/dist/acp-leader/acp-leader-host-binding.d.ts +16 -0
  6. package/dist/acp-leader/acp-leader-host.d.ts +80 -0
  7. package/dist/acp-leader/acp-leader-picker.d.ts +21 -0
  8. package/dist/acp-leader/acp-leader-provider.d.ts +85 -0
  9. package/dist/acp-leader/acp-leader-resume.d.ts +10 -0
  10. package/dist/acp-leader/acp-leader-stream.d.ts +82 -0
  11. package/dist/{auth-ANOVZVHE.js → auth-IHQS6AFW.js} +60 -64
  12. package/dist/auth-menu/index.d.ts +1 -2
  13. package/dist/boot/dispatch-webui.d.ts +16 -0
  14. package/dist/{chunk-GRAGMA4B.js → chunk-2LTHDKZX.js} +4 -4
  15. package/dist/chunk-3LEIFO5D.js +65 -0
  16. package/dist/chunk-COGI7EQU.js +1458 -0
  17. package/dist/chunk-GKGWPFW2.js +50 -0
  18. package/dist/{chunk-RAWHRRUJ.js → chunk-I5LJLKA3.js} +198 -197
  19. package/dist/{chunk-5TRZZHMN.js → chunk-JCZ2D3VV.js} +4 -65
  20. package/dist/{chunk-XT3NAHRX.js → chunk-JKTJSLUT.js} +192 -1
  21. package/dist/{chunk-NMXQFQ26.js → chunk-TAE3EGBM.js} +3 -3
  22. package/dist/{chunk-3R3OADC5.js → chunk-ZF4HRR5F.js} +7 -3
  23. package/dist/{cli-context-A6XDEXN5.js → cli-context-E44HQ4ZO.js} +17 -8
  24. package/dist/{cli-main-5UGSW4TO.js → cli-main-OT63I2FX.js} +490 -228
  25. package/dist/{execution-BAP566QC.js → execution-OK34CJKJ.js} +7 -2
  26. package/dist/fleet/acp-live-steer.d.ts +28 -0
  27. package/dist/fleet/acp-permission-bridge.d.ts +49 -0
  28. package/dist/fleet/acp-quota-hold.d.ts +3 -0
  29. package/dist/fleet/acp-quota-reading.d.ts +3 -0
  30. package/dist/fleet/acp-turn-reminders.d.ts +15 -0
  31. package/dist/fleet/acp-wrongstack-bridge.d.ts +42 -0
  32. package/dist/fleet/host-acp.d.ts +20 -2
  33. package/dist/index.js +7 -4
  34. package/dist/{mcp-2XWBJEQN.js → mcp-W7A5HUXV.js} +97 -1
  35. package/dist/mcp-serve-project.d.ts +18 -0
  36. package/dist/{per-subcommand-help-D6J4ZXW2.js → per-subcommand-help-4GYSCMLU.js} +2 -2
  37. package/dist/{short-circuit-flags-CPCT5MWB.js → short-circuit-flags-QEXUS3BY.js} +2 -2
  38. package/dist/{subcommands-VTMIZQBP.js → subcommands-YADUV7IJ.js} +2 -2
  39. package/dist/{webui-server-HIUY3RHD.js → webui-server-CB64CFDG.js} +41 -3
  40. package/package.json +34 -30
  41. package/dist/auth-menu/anthropic-oauth.d.ts +0 -43
  42. package/dist/chunk-ZNNFEKDZ.js +0 -319
@@ -0,0 +1,1458 @@
1
+ import {
2
+ wrongstackBridgeSetup
3
+ } from "./chunk-JKTJSLUT.js";
4
+ import {
5
+ isRestrictedMode
6
+ } from "./chunk-3LEIFO5D.js";
7
+ import {
8
+ buildProviderForId,
9
+ resolveProviderCfgWithProxy
10
+ } from "./chunk-UFRVTBV6.js";
11
+ import {
12
+ ACP_LEADER_PROVIDER_ID,
13
+ acpLeaderHost,
14
+ trackAcpLeader
15
+ } from "./chunk-GKGWPFW2.js";
16
+
17
+ // src/acp-agent-list.ts
18
+ import { REGISTRY_ID_ALIASES } from "@wrongstack/acp";
19
+ function reverseAliases(aliases = REGISTRY_ID_ALIASES) {
20
+ const out = {};
21
+ for (const [ours, theirs] of Object.entries(aliases)) {
22
+ if (ours !== theirs) out[theirs] = ours;
23
+ }
24
+ return out;
25
+ }
26
+ function distKind(command) {
27
+ if (command === "npx") return "npx";
28
+ if (command === "uvx") return "uvx";
29
+ return "local";
30
+ }
31
+ function localProbe(registryId, alias, detectedById) {
32
+ return detectedById.get(registryId) ?? (alias ? detectedById.get(alias) : void 0);
33
+ }
34
+ function formatRegistryRow(agent, alias, local, idWidth) {
35
+ const kind = distKind(agent.acp.command);
36
+ const aliasBit = alias ? ` (also ${alias})` : "";
37
+ if (kind === "local") {
38
+ if (local?.installed) {
39
+ const ver = local.version?.split("\n")[0]?.trim();
40
+ const note2 = ver ? ` ${ver}` : " on PATH";
41
+ return ` \u2713 ${agent.id.padEnd(idWidth)} ${agent.displayName}${aliasBit}${note2}`;
42
+ }
43
+ const reason = local?.reason ?? "not on PATH";
44
+ return ` \u2717 ${agent.id.padEnd(idWidth)} ${agent.displayName}${aliasBit} (${reason})`;
45
+ }
46
+ const pkg = (agent.acp.args ?? []).find((a) => a !== "-y" && !a.startsWith("--"));
47
+ const note = pkg ? `${kind} ${pkg}` : kind;
48
+ return ` \u25B8 ${agent.id.padEnd(idWidth)} ${agent.displayName}${aliasBit} ${note}`;
49
+ }
50
+ function formatAcpAgentList(opts) {
51
+ const { live, detected } = opts;
52
+ const detectedById = new Map(detected.map((d) => [d.id, d]));
53
+ if (live && live.agents.length > 0) {
54
+ const aliasOf = reverseAliases();
55
+ const when = live.fetchedAt ? live.fetchedAt.slice(0, 19).replace("T", " ") : "locally";
56
+ const rows = [...live.agents].sort((a, b) => {
57
+ const rank = (command) => {
58
+ const k = distKind(command);
59
+ return k === "local" ? 0 : k === "uvx" ? 1 : 2;
60
+ };
61
+ const r = rank(a.acp.command) - rank(b.acp.command);
62
+ return r !== 0 ? r : a.id.localeCompare(b.id);
63
+ });
64
+ const idWidth = Math.max(16, ...rows.map((a) => a.id.length));
65
+ const lines2 = [
66
+ `Official ACP registry \u2014 ${live.agents.length} agents (synced ${when}).`,
67
+ "Spawn ids below are what `/acp <id> <task>` and `/acp <id> --bg` accept.",
68
+ ""
69
+ ];
70
+ for (const agent of rows) {
71
+ const alias = aliasOf[agent.id];
72
+ lines2.push(
73
+ formatRegistryRow(agent, alias, localProbe(agent.id, alias, detectedById), idWidth)
74
+ );
75
+ }
76
+ const leftover = detected.filter((d) => {
77
+ if (live.byId[d.id]) return false;
78
+ const mapped = REGISTRY_ID_ALIASES[d.id];
79
+ return !(mapped && live.byId[mapped]);
80
+ });
81
+ if (leftover.length > 0) {
82
+ lines2.push("");
83
+ lines2.push("Bundled extras (not in the official registry \u2014 experimental):");
84
+ for (const d of leftover) {
85
+ const mark = d.installed ? "\u2713" : "\u2717";
86
+ const extra = d.installed ? d.version?.split("\n")[0] ?? "" : d.reason ?? "not installed";
87
+ lines2.push(` ${mark} ${d.id.padEnd(16)} ${d.displayName} (${extra})`);
88
+ }
89
+ }
90
+ lines2.push("");
91
+ lines2.push(
92
+ "Spawn: /acp <id> <task> Background: /acp <id> --bg <task> Probe: /acp probe <id>"
93
+ );
94
+ return lines2.join("\n");
95
+ }
96
+ const installed = detected.filter((a) => a.installed);
97
+ const missing = detected.filter((a) => !a.installed);
98
+ const lines = [
99
+ "Bundled offline catalog (not the official registry).",
100
+ "Run `/acp sync` to replace this list with agentclientprotocol/registry.",
101
+ ""
102
+ ];
103
+ for (const a of installed) {
104
+ const ver = a.version ? ` (${a.version.split("\n")[0]})` : "";
105
+ lines.push(` \u2713 ${a.id.padEnd(16)} ${a.displayName}${ver}`);
106
+ }
107
+ for (const a of missing) {
108
+ lines.push(` \u2717 ${a.id.padEnd(16)} ${a.displayName} (${a.reason ?? "not installed"})`);
109
+ }
110
+ lines.push("");
111
+ lines.push(`${installed.length} of ${detected.length} bundled agents installed locally.`);
112
+ return lines.join("\n");
113
+ }
114
+
115
+ // src/acp-registry-cache.ts
116
+ import * as fs from "node:fs/promises";
117
+ import * as path from "node:path";
118
+ import { fetchAcpRegistry } from "@wrongstack/acp";
119
+ function acpRegistryCachePath(paths) {
120
+ return path.join(paths.cacheDir, "acp-registry.json");
121
+ }
122
+ async function loadCachedAcpRegistry(paths) {
123
+ try {
124
+ const raw = await fs.readFile(acpRegistryCachePath(paths), "utf8");
125
+ const env = JSON.parse(raw);
126
+ if (!Array.isArray(env.agents)) return null;
127
+ const byId = /* @__PURE__ */ Object.create(null);
128
+ for (const a of env.agents) {
129
+ if (!a?.id || !a.acp?.command) continue;
130
+ byId[a.id] = {
131
+ command: a.acp.command,
132
+ args: a.acp.args ?? [],
133
+ ...a.acp.env ? { env: a.acp.env } : {}
134
+ };
135
+ }
136
+ return { fetchedAt: env.fetchedAt ?? "", byId, agents: env.agents };
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+ async function refreshAcpRegistry(paths, opts) {
142
+ const result = await fetchAcpRegistry({
143
+ ...opts?.signal ? { signal: opts.signal } : {},
144
+ ...opts?.timeoutMs ? { timeoutMs: opts.timeoutMs } : {}
145
+ });
146
+ const location = acpRegistryCachePath(paths);
147
+ await fs.mkdir(path.dirname(location), { recursive: true });
148
+ const env = { fetchedAt: result.fetchedAt, agents: result.agents };
149
+ await fs.writeFile(location, JSON.stringify(env), "utf8");
150
+ return { count: result.agents.length, location, fetchedAt: result.fetchedAt };
151
+ }
152
+
153
+ // src/shutdown-cleanup.ts
154
+ function createGracefulShutdown(cleanup) {
155
+ const exitCode = cleanup.exitCode ?? 0;
156
+ let started = false;
157
+ let installed = false;
158
+ const runOnce = (force) => {
159
+ if (started) {
160
+ if (force) process.exit(exitCode);
161
+ return;
162
+ }
163
+ started = true;
164
+ void cleanup.run().catch(() => {
165
+ }).finally(() => {
166
+ if (force) {
167
+ process.exitCode = exitCode;
168
+ const t = setTimeout(() => process.exit(exitCode), 500);
169
+ t.unref();
170
+ }
171
+ });
172
+ };
173
+ const signalHandler = () => runOnce(true);
174
+ const beforeExitHandler = () => runOnce(false);
175
+ return {
176
+ install() {
177
+ if (installed) return;
178
+ installed = true;
179
+ process.once("SIGINT", signalHandler);
180
+ process.once("SIGTERM", signalHandler);
181
+ process.once("beforeExit", beforeExitHandler);
182
+ },
183
+ uninstall() {
184
+ if (!installed) return;
185
+ installed = false;
186
+ process.removeListener("SIGINT", signalHandler);
187
+ process.removeListener("SIGTERM", signalHandler);
188
+ process.removeListener("beforeExit", beforeExitHandler);
189
+ },
190
+ get cleanupStarted() {
191
+ return started;
192
+ }
193
+ };
194
+ }
195
+
196
+ // src/acp-leader/acp-leader-provider.ts
197
+ import { findAgentDescriptor } from "@wrongstack/acp";
198
+ import { consumeBtwNotes as consumeBtwNotes2 } from "@wrongstack/core/agent";
199
+ import { ProviderError as ProviderError2 } from "@wrongstack/core/types";
200
+ import { subjectForToolInput } from "@wrongstack/core/utils";
201
+
202
+ // src/fleet/acp-permission-bridge.ts
203
+ import { ToolCapabilities } from "@wrongstack/core/security";
204
+ import { toErrorMessage } from "@wrongstack/core/utils";
205
+ var str = (v) => typeof v === "string" && v ? v : void 0;
206
+ var MCP_TOOL = /^mcp__[^_].*__.+/;
207
+ function mcpToolName(call) {
208
+ const meta = call._meta;
209
+ const name = str(meta?.claudeCode?.toolName) ?? str(call.title);
210
+ return name && MCP_TOOL.test(name) ? name : void 0;
211
+ }
212
+ function translate(req) {
213
+ const call = req.toolCall;
214
+ const raw = call.rawInput ?? {};
215
+ const label = str(call.title) ?? call.kind ?? "unknown";
216
+ const refuse = (why) => ({
217
+ refuse: why,
218
+ call: { tool: label, input: raw }
219
+ });
220
+ const path3 = str(raw.file_path) ?? str(raw.path) ?? str(raw.notebook_path) ?? call.locations?.[0]?.path;
221
+ switch (call.kind) {
222
+ case "execute": {
223
+ const command = str(raw.command);
224
+ return command ? { call: { tool: "bash", input: { command } } } : refuse("a command request that names no command");
225
+ }
226
+ case "edit":
227
+ if (!path3) return refuse("an edit that names no file");
228
+ return typeof raw.old_string === "string" ? {
229
+ call: {
230
+ tool: "edit",
231
+ input: { path: path3, old_string: raw.old_string, new_string: str(raw.new_string) ?? "" }
232
+ }
233
+ } : {
234
+ call: {
235
+ tool: "write",
236
+ input: { path: path3, content: typeof raw.content === "string" ? raw.content : "" }
237
+ }
238
+ };
239
+ case "delete":
240
+ return path3 ? { call: { tool: "bash", input: { command: `rm ${JSON.stringify(path3)}` } } } : refuse("a delete that names no file");
241
+ case "move": {
242
+ const to = str(raw.destination) ?? str(raw.new_path) ?? call.locations?.[1]?.path;
243
+ return path3 && to ? {
244
+ call: {
245
+ tool: "bash",
246
+ input: { command: `mv ${JSON.stringify(path3)} ${JSON.stringify(to)}` }
247
+ }
248
+ } : refuse("a move that names no source or destination");
249
+ }
250
+ case "read":
251
+ case "search":
252
+ return path3 ? { call: { tool: "read", input: { path: path3 } } } : { safe: true };
253
+ case "fetch": {
254
+ const host = str(raw.host);
255
+ const url = str(raw.url) ?? (host ? `https://${host}` : void 0);
256
+ if (url) return { call: { tool: "fetch", input: { url } } };
257
+ const query = str(raw.query);
258
+ return query ? { call: { tool: "fetch", input: { query } } } : refuse("a network request that names no URL or query");
259
+ }
260
+ case "think":
261
+ case "switch_mode":
262
+ return { safe: true };
263
+ default: {
264
+ const mcp = mcpToolName(call);
265
+ return mcp ? { call: { tool: mcp, input: raw }, mcp: true } : refuse("no WrongStack counterpart to check it against");
266
+ }
267
+ }
268
+ }
269
+ function pick(req, allow) {
270
+ const kinds = allow ? ["allow_once"] : ["reject_once", "reject_always"];
271
+ const option = req.options.find((o) => kinds.includes(o.kind));
272
+ return option ? { outcome: "selected", optionId: option.optionId } : { outcome: "cancelled" };
273
+ }
274
+ function mcpToolFor(name) {
275
+ return { name, capabilities: [ToolCapabilities.MCP_PROXY] };
276
+ }
277
+ function wrongstackAcpPermissionPolicy(opts) {
278
+ const refuse = (req, call, reason) => {
279
+ opts.onRefused?.(call, reason);
280
+ return pick(req, false);
281
+ };
282
+ return async (req) => {
283
+ if (req.signal.aborted) return { outcome: "cancelled" };
284
+ const translated = translate(req);
285
+ if ("safe" in translated) return pick(req, true);
286
+ if ("refuse" in translated) return refuse(req, translated.call, translated.refuse);
287
+ const { call } = translated;
288
+ let tool = opts.getTool(call.tool);
289
+ if (!tool && translated.mcp) {
290
+ if (opts.allowMcp === false) return refuse(req, call, "MCP tools are off in this run");
291
+ tool = mcpToolFor(call.tool);
292
+ }
293
+ if (!tool) return refuse(req, call, `${call.tool} is not available in this run`);
294
+ let decision;
295
+ try {
296
+ decision = await opts.evaluate(tool, call.input, {
297
+ projectRoot: opts.projectRoot,
298
+ cwd: opts.projectRoot
299
+ });
300
+ } catch (err) {
301
+ decision = {
302
+ permission: "deny",
303
+ source: "default",
304
+ reason: `permission check failed: ${toErrorMessage(err)}`
305
+ };
306
+ }
307
+ if (decision.permission === "auto") return pick(req, true);
308
+ if (decision.permission === "confirm" && opts.confirm) {
309
+ let approved = false;
310
+ try {
311
+ approved = await opts.confirm(tool, call, {
312
+ toolCallId: req.toolCall.toolCallId,
313
+ signal: req.signal
314
+ });
315
+ } catch {
316
+ approved = false;
317
+ }
318
+ if (req.signal.aborted) return { outcome: "cancelled" };
319
+ return approved ? pick(req, true) : refuse(req, call, "declined by the user");
320
+ }
321
+ return refuse(req, call, decision.reason ?? decision.source);
322
+ };
323
+ }
324
+
325
+ // src/fleet/acp-quota-reading.ts
326
+ import { recordProviderQuota } from "@wrongstack/core/quota";
327
+ var WINDOWS = {
328
+ five_hour: { id: "5h", minutes: 300 },
329
+ seven_day: { id: "7d", minutes: 10080 },
330
+ seven_day_opus: { id: "7d opus", minutes: 10080 },
331
+ seven_day_sonnet: { id: "7d sonnet", minutes: 10080 }
332
+ };
333
+ var lastWindows = /* @__PURE__ */ new Map();
334
+ var num = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : void 0;
335
+ var toSeconds = (v) => Math.trunc(v > 1e12 ? v / 1e3 : v);
336
+ function windowFrom(name, raw) {
337
+ const spec = WINDOWS[name];
338
+ if (!spec || !raw || typeof raw !== "object") return void 0;
339
+ const utilization = num(raw.utilization);
340
+ if (utilization === void 0) return void 0;
341
+ const resetsAt = num(raw.resetsAt);
342
+ return {
343
+ id: spec.id,
344
+ label: spec.id,
345
+ // Sent as a 0..1 fraction; anything above 1 can only be a percent already.
346
+ usedPercent: Math.min(
347
+ 100,
348
+ Math.round((utilization <= 1 ? utilization * 100 : utilization) * 100) / 100
349
+ ),
350
+ windowMinutes: spec.minutes,
351
+ ...resetsAt !== void 0 && resetsAt > 0 ? { resetsAt: toSeconds(resetsAt) } : {}
352
+ };
353
+ }
354
+ function recordAcpRateLimit(agentId, info, now = Date.now()) {
355
+ const known = lastWindows.get(agentId) ?? /* @__PURE__ */ new Map();
356
+ const unified = info.unifiedWindows;
357
+ if (unified && typeof unified === "object") {
358
+ for (const [name, raw] of Object.entries(unified)) {
359
+ const window = windowFrom(name, raw);
360
+ if (window) known.set(window.id, window);
361
+ }
362
+ }
363
+ const type = typeof info.rateLimitType === "string" ? info.rateLimitType : void 0;
364
+ const reached = info.status === "rejected" && type ? WINDOWS[type]?.id : void 0;
365
+ if (known.size === 0 && reached === void 0) return;
366
+ lastWindows.set(agentId, known);
367
+ recordProviderQuota(
368
+ agentId,
369
+ [
370
+ {
371
+ providerId: agentId,
372
+ meterId: "claude",
373
+ meterLabel: "Claude subscription (delegated worker)",
374
+ windows: [...known.values()],
375
+ ...reached !== void 0 ? { reachedWindowId: reached } : {},
376
+ capturedAt: now
377
+ }
378
+ ],
379
+ now
380
+ );
381
+ }
382
+
383
+ // src/fleet/acp-turn-reminders.ts
384
+ import {
385
+ buildBtwBlock,
386
+ consumeBtwNotes,
387
+ pendingBtwCount,
388
+ setBtwNote
389
+ } from "@wrongstack/core/agent";
390
+ var INBOX_NUDGE = '[WrongStack mailbox] New mail is waiting. Call mcp__wrongstack-mailbox__mailbox_read with action "inbox"; acknowledge messages you handle, then continue your current task.';
391
+ function acpBtwBlock(notes) {
392
+ const ordinary = notes.filter((note) => !note.startsWith("[MAILBOX BTW]"));
393
+ const mailWaiting = ordinary.length !== notes.length;
394
+ return [ordinary.length > 0 ? buildBtwBlock(ordinary) : "", mailWaiting ? INBOX_NUDGE : ""].filter(Boolean).join("\n\n");
395
+ }
396
+ function startAcpTurnReminders(options) {
397
+ let stopped = false;
398
+ let btwInFlight = false;
399
+ let mailInFlight = false;
400
+ let lastMailNoticeAt = Number.NEGATIVE_INFINITY;
401
+ let lastMailNoticeCount = 0;
402
+ const checkBtw = async () => {
403
+ const context = options.context;
404
+ if (stopped || btwInFlight || !context || pendingBtwCount(context) === 0) return;
405
+ btwInFlight = true;
406
+ const notes = consumeBtwNotes(context);
407
+ try {
408
+ const outcome = await options.steer(acpBtwBlock(notes));
409
+ if (outcome !== "injected") for (const note of notes) setBtwNote(context, note);
410
+ } catch {
411
+ for (const note of notes) setBtwNote(context, note);
412
+ } finally {
413
+ btwInFlight = false;
414
+ }
415
+ };
416
+ const checkMail = async () => {
417
+ if (stopped || mailInFlight || !options.unreadCount) return;
418
+ mailInFlight = true;
419
+ try {
420
+ const count = await options.unreadCount();
421
+ if (stopped || !Number.isFinite(count) || count <= 0) {
422
+ lastMailNoticeCount = 0;
423
+ return;
424
+ }
425
+ const now = Date.now();
426
+ if (count === lastMailNoticeCount && now - lastMailNoticeAt < 6e4) return;
427
+ const outcome = await options.steer(
428
+ `[WrongStack mailbox] ${count} unread message(s). ${INBOX_NUDGE}`
429
+ );
430
+ if (outcome === "injected") {
431
+ lastMailNoticeCount = count;
432
+ lastMailNoticeAt = now;
433
+ }
434
+ } catch {
435
+ } finally {
436
+ mailInFlight = false;
437
+ }
438
+ };
439
+ const btwTimer = options.context ? setInterval(() => void checkBtw(), options.btwPollMs ?? 1e3) : void 0;
440
+ const mailTimer = options.unreadCount ? setInterval(() => void checkMail(), options.mailPollMs ?? 15e3) : void 0;
441
+ btwTimer?.unref?.();
442
+ mailTimer?.unref?.();
443
+ return () => {
444
+ stopped = true;
445
+ if (btwTimer) clearInterval(btwTimer);
446
+ if (mailTimer) clearInterval(mailTimer);
447
+ };
448
+ }
449
+
450
+ // src/acp-leader/acp-leader-confirm.ts
451
+ import { confirmObserverCount } from "@wrongstack/core/agent";
452
+ var ACP_CONFIRM_TIMEOUT_MS = 12e4;
453
+ async function confirmThroughEvents(req) {
454
+ if (req.events.listenerCount("tool.confirm_needed") - confirmObserverCount() <= 0) return false;
455
+ const timeoutMs = req.timeoutMs ?? ACP_CONFIRM_TIMEOUT_MS;
456
+ const answer = await new Promise((resolve) => {
457
+ let settled = false;
458
+ const settle = (choice, source) => {
459
+ if (settled) return;
460
+ settled = true;
461
+ clearTimeout(timer);
462
+ req.signal.removeEventListener("abort", onAbort);
463
+ req.events.emit("tool.confirm_resolved", {
464
+ sessionId: req.sessionId,
465
+ toolUseId: req.toolUseId,
466
+ toolName: req.tool.name,
467
+ decision: choice,
468
+ source,
469
+ ...source === "brain_timeout" ? { rationale: "Nobody answered; an outside agent is not approved by default." } : {}
470
+ });
471
+ resolve(choice);
472
+ };
473
+ const onAbort = () => settle("abort", "abort");
474
+ const timer = setTimeout(() => settle("no", "brain_timeout"), timeoutMs);
475
+ timer.unref?.();
476
+ if (req.signal.aborted) {
477
+ settle("abort", "abort");
478
+ return;
479
+ }
480
+ req.signal.addEventListener("abort", onAbort, { once: true });
481
+ req.events.emit("tool.confirm_needed", {
482
+ sessionId: req.sessionId,
483
+ tool: req.tool,
484
+ input: req.input,
485
+ toolUseId: req.toolUseId,
486
+ suggestedPattern: req.suggestedPattern,
487
+ deadlineAt: Date.now() + timeoutMs,
488
+ resolve: (decision) => settle(decision, "user")
489
+ });
490
+ });
491
+ if (answer === "abort" || answer === "no") return false;
492
+ const rule = { tool: req.tool.name, pattern: req.suggestedPattern };
493
+ try {
494
+ if (answer === "deny") await req.permission?.deny(rule);
495
+ else if (answer !== "yes") await req.permission?.trust(rule);
496
+ } catch {
497
+ }
498
+ return answer !== "deny";
499
+ }
500
+
501
+ // src/acp-leader/acp-leader-context.ts
502
+ function compactedTokens(call) {
503
+ if (call.status !== "completed") return void 0;
504
+ const raw = call.rawOutput;
505
+ const post = raw?.postTokens;
506
+ return typeof raw?.preTokens === "number" && typeof post === "number" && Number.isFinite(post) && post >= 0 ? post : void 0;
507
+ }
508
+ var LeaderContext = class {
509
+ reading;
510
+ /** The agent's last reading. */
511
+ get current() {
512
+ return this.reading;
513
+ }
514
+ /** A `usage_update`: the context after the agent's latest model call. */
515
+ usage(update) {
516
+ this.reading = { used: update.used, size: update.size || (this.reading?.size ?? 0) };
517
+ return this.reading;
518
+ }
519
+ /** A settled tool call; a compaction resets the context to what it kept. */
520
+ toolCall(call) {
521
+ const post = compactedTokens(call);
522
+ if (post === void 0) return void 0;
523
+ this.reading = { used: post, size: this.reading?.size ?? 0 };
524
+ return this.reading;
525
+ }
526
+ };
527
+ function publishLeaderContext(events, sessionId, reading, fallbackMaxContext) {
528
+ const maxContext = reading.size || fallbackMaxContext;
529
+ const rawLoad = maxContext > 0 ? reading.used / maxContext : 0;
530
+ events?.emit("ctx.pct", {
531
+ sessionId,
532
+ load: Math.max(0, Math.min(1, rawLoad)),
533
+ rawLoad,
534
+ tokens: reading.used,
535
+ maxContext
536
+ });
537
+ }
538
+
539
+ // src/acp-leader/acp-leader-fleet.ts
540
+ import { randomBytes } from "node:crypto";
541
+ import { MCPServer, serveHttp } from "@wrongstack/mcp";
542
+ var FLEET_SERVER_NAME = "wrongstack-fleet";
543
+ var FLEET_TOOLS = [
544
+ "delegate",
545
+ "spawn_subagent",
546
+ "assign_task",
547
+ "await_tasks",
548
+ "roll_up",
549
+ "fleet_status",
550
+ "session_note",
551
+ "terminate_subagent"
552
+ ];
553
+ var MCP_PREFIX = `mcp__${FLEET_SERVER_NAME}__`;
554
+ function fleetToolName(mcpName) {
555
+ if (!mcpName.startsWith(MCP_PREFIX)) return void 0;
556
+ const name = mcpName.slice(MCP_PREFIX.length);
557
+ return FLEET_TOOLS.includes(name) ? name : void 0;
558
+ }
559
+ async function startFleetEndpoint(host, run) {
560
+ const tools = FLEET_TOOLS.flatMap((name) => {
561
+ const tool = host.getTool(name);
562
+ return tool ? [tool] : [];
563
+ });
564
+ if (tools.length === 0) return void 0;
565
+ const offered = new Set(tools.map((t) => t.name));
566
+ const server = new MCPServer({
567
+ host: {
568
+ listTools: () => tools.map((t) => ({
569
+ name: t.name,
570
+ description: t.description,
571
+ inputSchema: t.inputSchema ?? { type: "object" }
572
+ })),
573
+ callTool: async (name, args) => {
574
+ if (!offered.has(name)) {
575
+ return { content: `Tool "${name}" is not offered here`, isError: true };
576
+ }
577
+ return run ? host.runTool(name, args, run) : host.runTool(name, args);
578
+ }
579
+ }
580
+ });
581
+ const token = randomBytes(24).toString("base64url");
582
+ const handle = await serveHttp(server, { host: "127.0.0.1", port: 0, token });
583
+ return { url: handle.url, token, tools: [...offered], close: () => handle.close() };
584
+ }
585
+ function fleetNote(tools) {
586
+ const has = (name) => tools.includes(name);
587
+ return [
588
+ "",
589
+ `${FLEET_SERVER_NAME} gives you WrongStack's workers. Hand work you would otherwise do`,
590
+ "serially, or that a cheaper model can do, to them; review what they return.",
591
+ ...has("delegate") ? [
592
+ "- delegate: hand a task to a subagent (scope and outOfScope are required). It returns at",
593
+ " once; the result arrives later in a message starting with [DELEGATION RESULT], so end",
594
+ " your turn or keep working. Pass wait: true only when your next step needs the result."
595
+ ] : [],
596
+ ...has("spawn_subagent") ? [
597
+ "- spawn_subagent + assign_task + await_tasks: start a worker, give it tasks, and wait for",
598
+ " their results inside this turn."
599
+ ] : [],
600
+ ...has("fleet_status") ? ["- fleet_status: what is running and its progress."] : [],
601
+ ...has("roll_up") ? ["- roll_up: the full result of finished tasks."] : [],
602
+ ...has("session_note") ? ["- session_note: steer a running worker."] : [],
603
+ ...has("terminate_subagent") ? ["- terminate_subagent: stop a worker."] : []
604
+ ].join("\n");
605
+ }
606
+
607
+ // src/acp-leader/acp-leader-resume.ts
608
+ import { mkdir as mkdir2, readFile as readFile2 } from "node:fs/promises";
609
+ import * as path2 from "node:path";
610
+ import { atomicWrite } from "@wrongstack/core/utils";
611
+ var FILE = "acp-leader-sessions.json";
612
+ var MAX_RECORDS = 200;
613
+ async function readRecords(stateDir) {
614
+ try {
615
+ const parsed = JSON.parse(await readFile2(path2.join(stateDir, FILE), "utf8"));
616
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
617
+ return Object.fromEntries(
618
+ Object.entries(parsed).filter(
619
+ ([, record]) => record && typeof record === "object" && typeof record.agentId === "string" && typeof record.agentSessionId === "string" && typeof record.updatedAt === "string"
620
+ )
621
+ );
622
+ } catch {
623
+ return {};
624
+ }
625
+ }
626
+ async function readLeaderSession(stateDir, sessionId, agentId) {
627
+ const record = (await readRecords(stateDir))[sessionId];
628
+ return record?.agentId === agentId && typeof record.agentSessionId === "string" ? record.agentSessionId : void 0;
629
+ }
630
+ var writing = Promise.resolve();
631
+ function writeLeaderSession(stateDir, sessionId, record) {
632
+ writing = writing.catch(() => void 0).then(async () => {
633
+ const records = await readRecords(stateDir);
634
+ records[sessionId] = { ...record, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
635
+ const kept = Object.entries(records).sort(([, a], [, b]) => b.updatedAt.localeCompare(a.updatedAt)).slice(0, MAX_RECORDS);
636
+ await mkdir2(stateDir, { recursive: true });
637
+ await atomicWrite(
638
+ path2.join(stateDir, FILE),
639
+ `${JSON.stringify(Object.fromEntries(kept), null, 2)}
640
+ `
641
+ );
642
+ });
643
+ return writing.catch(() => void 0);
644
+ }
645
+
646
+ // src/acp-leader/acp-leader-stream.ts
647
+ import { ProviderError } from "@wrongstack/core/types";
648
+ import { toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
649
+ var StreamQueue = class {
650
+ items = [];
651
+ wake;
652
+ done = false;
653
+ failure;
654
+ push(event) {
655
+ this.items.push(event);
656
+ this.wake?.();
657
+ }
658
+ end() {
659
+ this.done = true;
660
+ this.wake?.();
661
+ }
662
+ fail(err) {
663
+ this.failure = err;
664
+ this.end();
665
+ }
666
+ async *drain() {
667
+ for (; ; ) {
668
+ const next = this.items.shift();
669
+ if (next) {
670
+ yield next;
671
+ continue;
672
+ }
673
+ if (this.done) {
674
+ if (this.failure !== void 0) throw this.failure;
675
+ return;
676
+ }
677
+ await new Promise((resolve) => {
678
+ this.wake = resolve;
679
+ });
680
+ this.wake = void 0;
681
+ }
682
+ }
683
+ };
684
+ var TextBlocks = class {
685
+ constructor(queue) {
686
+ this.queue = queue;
687
+ }
688
+ queue;
689
+ open = false;
690
+ index = 0;
691
+ text(chunk) {
692
+ if (!chunk) return;
693
+ if (!this.open) {
694
+ this.queue.push({ type: "content_block_start", kind: "text" });
695
+ this.open = true;
696
+ }
697
+ this.queue.push({ type: "text_delta", text: chunk });
698
+ }
699
+ close() {
700
+ if (!this.open) return;
701
+ this.queue.push({ type: "content_block_stop", index: this.index++ });
702
+ this.open = false;
703
+ }
704
+ };
705
+ var OUTPUT_PREVIEW_CHARS = 4e3;
706
+ var FULL_OUTPUT_CHARS = 256 * 1024;
707
+ function clip(text, max) {
708
+ if (text.length <= max) return text;
709
+ let end = max;
710
+ const last = text.charCodeAt(end - 1);
711
+ if (last >= 55296 && last <= 56319) end -= 1;
712
+ return `${text.slice(0, end)}\u2026`;
713
+ }
714
+ function cardOutput(call) {
715
+ const raw = call.rawOutput;
716
+ const text = call.output ?? (raw ? typeof raw.output === "string" ? raw.output : JSON.stringify(raw) : void 0);
717
+ if (text === void 0) return void 0;
718
+ const output = clip(text, OUTPUT_PREVIEW_CHARS);
719
+ return {
720
+ output,
721
+ ...output !== text ? { fullOutput: clip(text, FULL_OUTPUT_CHARS) } : {},
722
+ // The size of what the call returned, not of the preview.
723
+ outputBytes: Buffer.byteLength(text)
724
+ };
725
+ }
726
+ var ToolCardRelay = class {
727
+ constructor(events, meta) {
728
+ this.events = events;
729
+ this.meta = meta;
730
+ }
731
+ events;
732
+ meta;
733
+ started = /* @__PURE__ */ new Map();
734
+ settled = /* @__PURE__ */ new Set();
735
+ update(call) {
736
+ if (!this.events) return;
737
+ const id = call.toolCallId;
738
+ const name = call.title || call.kind || "tool";
739
+ if (!this.started.has(id)) {
740
+ this.started.set(id, Date.now());
741
+ this.events.emit("tool.started", {
742
+ sessionId: this.meta.sessionId,
743
+ agentId: "leader",
744
+ name,
745
+ id,
746
+ input: call.rawInput,
747
+ provider: "acp",
748
+ model: this.meta.model
749
+ });
750
+ }
751
+ if ((call.status === "completed" || call.status === "failed") && !this.settled.has(id)) {
752
+ this.settled.add(id);
753
+ const output = cardOutput(call);
754
+ this.events.emit("tool.executed", {
755
+ sessionId: this.meta.sessionId,
756
+ agentId: "leader",
757
+ id,
758
+ name,
759
+ durationMs: Date.now() - (this.started.get(id) ?? Date.now()),
760
+ ok: call.status === "completed",
761
+ settlement: call.status === "completed" ? "completed" : "failed",
762
+ input: call.rawInput,
763
+ outsideConversation: true,
764
+ ...output
765
+ });
766
+ }
767
+ }
768
+ };
769
+ function latestUserText(messages) {
770
+ const parts = [];
771
+ for (let i = messages.length - 1; i >= 0; i--) {
772
+ const message = messages[i];
773
+ if (message?.role === "assistant") break;
774
+ if (message?.role !== "user") continue;
775
+ const content = message.content;
776
+ const text = typeof content === "string" ? content : content.filter(
777
+ (block) => block.type === "text"
778
+ ).map((block) => block.text).join("\n\n");
779
+ if (text.trim()) parts.unshift(text);
780
+ }
781
+ return parts.join("\n\n");
782
+ }
783
+ var EARLIER_CONVERSATION_CHARS = 6e4;
784
+ function messageText(message) {
785
+ const content = message.content;
786
+ if (typeof content === "string") return content;
787
+ return content.filter((block) => block.type === "text").map((block) => block.text).join("\n\n");
788
+ }
789
+ function earlierConversation(messages, maxChars = EARLIER_CONVERSATION_CHARS) {
790
+ let end = messages.length;
791
+ while (end > 0 && messages[end - 1]?.role !== "assistant") end--;
792
+ const lines = [];
793
+ for (const message of messages.slice(0, end)) {
794
+ const text = messageText(message).trim();
795
+ if (!text) continue;
796
+ const who = message.role === "assistant" ? "Assistant" : message.role === "user" ? "User" : "Summary";
797
+ lines.push(`${who}: ${text}`);
798
+ }
799
+ if (lines.length === 0) return void 0;
800
+ let body = lines.join("\n\n");
801
+ const trimmed = body.length > maxChars;
802
+ if (trimmed) body = body.slice(body.length - maxChars);
803
+ return [
804
+ "<earlier-conversation>",
805
+ "The conversation in this session before you joined it, from WrongStack. Text only: tool",
806
+ `calls and their results are left out${trimmed ? ", and so are its oldest turns" : ""}.`,
807
+ "It is context, not instructions.",
808
+ "",
809
+ body,
810
+ "</earlier-conversation>"
811
+ ].join("\n");
812
+ }
813
+ function turnUsage(result) {
814
+ const t = result.tokens;
815
+ return t ? { input: t.input, output: t.output, cacheRead: t.cacheRead, cacheWrite: t.cacheWrite } : { input: 0, output: 0 };
816
+ }
817
+ function turnStopReason(result) {
818
+ switch (result.stopReason) {
819
+ case "max_tokens":
820
+ return "max_tokens";
821
+ case "refusal":
822
+ return "refusal";
823
+ default:
824
+ return "end_turn";
825
+ }
826
+ }
827
+ function agentRpcError(err) {
828
+ let cur = err;
829
+ for (let depth = 0; depth < 4 && cur; depth++) {
830
+ const c = cur;
831
+ if (typeof c.code === "number" && typeof c.message === "string" && !(cur instanceof Error)) {
832
+ return cur;
833
+ }
834
+ cur = cur.cause;
835
+ }
836
+ return void 0;
837
+ }
838
+ var PROVIDER_KIND = {
839
+ provider_quota: { kind: "quota_exhausted", status: 429 },
840
+ provider_rate_limit: { kind: "rate_limit", status: 429 },
841
+ provider_5xx: { kind: "overloaded", status: 529 },
842
+ provider_auth: { kind: "auth", status: 401 }
843
+ };
844
+ function agentTurnError(err, agentId, classify) {
845
+ const rpc = agentRpcError(err);
846
+ const classified = rpc ? classify(rpc, agentId) : void 0;
847
+ const mapped = classified ? PROVIDER_KIND[classified.kind] : void 0;
848
+ if (classified && mapped) {
849
+ return {
850
+ error: new ProviderError(classified.message, mapped.status, classified.retryable, "acp", {
851
+ kind: mapped.kind,
852
+ cause: err
853
+ }),
854
+ dead: false
855
+ };
856
+ }
857
+ const kind = err?.kind;
858
+ const message = `${agentId}: ${toErrorMessage2(err)}`;
859
+ if (kind === "stalled") {
860
+ return {
861
+ error: new ProviderError(message, 408, false, "acp", { kind: "timeout", cause: err }),
862
+ dead: true
863
+ };
864
+ }
865
+ const dead = kind === "agent_died" || kind === "closed" || kind === "spawn_failed" || kind === "init_failed" || kind === "session_create_failed";
866
+ return {
867
+ error: new ProviderError(message, dead ? 502 : 500, false, "acp", {
868
+ kind: dead ? "server" : "unknown",
869
+ cause: err
870
+ }),
871
+ dead
872
+ };
873
+ }
874
+
875
+ // src/acp-leader/acp-leader-provider.ts
876
+ var CAPABILITIES = {
877
+ tools: true,
878
+ parallelTools: false,
879
+ vision: false,
880
+ streaming: true,
881
+ promptCache: false,
882
+ systemPrompt: true,
883
+ jsonMode: false,
884
+ reasoning: false,
885
+ // Per agent: the catalog's declared window, 0 (unknown) otherwise.
886
+ maxContext: 0,
887
+ maxOutput: 32e3,
888
+ cacheControl: "none",
889
+ topK: false,
890
+ frequencyPenalty: false,
891
+ presencePenalty: false,
892
+ seed: false,
893
+ structuredOutput: false,
894
+ logprobs: false,
895
+ audio: false,
896
+ multipleCompletions: false,
897
+ // The agent runs its own conversation and compacts it itself.
898
+ keepsOwnContext: true
899
+ };
900
+ function fleetGrantKey(sessionId, tool, input) {
901
+ const stable = (value) => Array.isArray(value) ? value.map(stable) : value && typeof value === "object" ? Object.fromEntries(
902
+ Object.keys(value).sort().map((k) => [k, stable(value[k])])
903
+ ) : value;
904
+ return JSON.stringify([sessionId, tool, stable(input)]);
905
+ }
906
+ function parseSideModel(ref) {
907
+ const slash = ref?.indexOf("/") ?? -1;
908
+ if (!ref || slash <= 0 || slash === ref.length - 1) return void 0;
909
+ const provider = ref.slice(0, slash);
910
+ if (provider === ACP_LEADER_PROVIDER_ID) return void 0;
911
+ return { provider, model: ref.slice(slash + 1) };
912
+ }
913
+ function leaderSideModel(config) {
914
+ return parseSideModel(config.acp?.leader?.sideModel) ?? parseSideModel(config.fallbackBridge) ?? parseSideModel(config.fallbackModels?.[0]);
915
+ }
916
+ function isLeaderTurn(req) {
917
+ const { sessionId, threadId } = req.cache ?? {};
918
+ return !sessionId || !threadId || sessionId === threadId;
919
+ }
920
+ var AcpLeaderProvider = class {
921
+ constructor(opts) {
922
+ this.opts = opts;
923
+ this.id = opts.id ?? ACP_LEADER_PROVIDER_ID;
924
+ trackAcpLeader(this);
925
+ }
926
+ opts;
927
+ id;
928
+ caps;
929
+ acp;
930
+ sessions = /* @__PURE__ */ new Map();
931
+ sideProviders = /* @__PURE__ */ new Map();
932
+ /**
933
+ * Fleet calls the user already approved through the agent's permission
934
+ * request, by session, tool and input: the call that follows runs on that
935
+ * approval instead of asking again.
936
+ */
937
+ fleetGrants = /* @__PURE__ */ new Map();
938
+ config() {
939
+ return acpLeaderHost()?.getConfig() ?? this.opts.bootConfig;
940
+ }
941
+ /**
942
+ * The leading agent's capabilities. One object per agent, so a host that
943
+ * refines a field in place keeps it until the user picks another agent.
944
+ */
945
+ get capabilities() {
946
+ const agentId = this.config().model;
947
+ if (this.caps?.agentId !== agentId) {
948
+ const maxContext = findAgentDescriptor(agentId)?.contextWindow ?? CAPABILITIES.maxContext;
949
+ this.caps = { agentId, value: { ...CAPABILITIES, maxContext } };
950
+ }
951
+ return this.caps.value;
952
+ }
953
+ /** The WrongStack provider and request for anything that is not a leader turn. */
954
+ side(req) {
955
+ const ref = leaderSideModel(this.config());
956
+ let provider = ref ? this.sideProviders.get(ref.provider) : void 0;
957
+ if (ref && !provider) {
958
+ provider = this.opts.buildProvider(ref.provider, this.config());
959
+ if (provider) this.sideProviders.set(ref.provider, provider);
960
+ }
961
+ if (!ref || !provider) {
962
+ throw new ProviderError2(
963
+ "The ACP leader answers only its own turns. Set acp.leader.sideModel (provider/model) for side requests such as titles, summaries and WrongStack subagents.",
964
+ 400,
965
+ false,
966
+ this.id,
967
+ { kind: "invalid_request" }
968
+ );
969
+ }
970
+ return { provider, req: { ...req, model: ref.model } };
971
+ }
972
+ async complete(req, opts) {
973
+ const side = this.side(req);
974
+ return side.provider.complete(side.req, opts);
975
+ }
976
+ async *stream(req, opts) {
977
+ if (!isLeaderTurn(req)) {
978
+ const side = this.side(req);
979
+ yield* side.provider.stream(side.req, opts);
980
+ return;
981
+ }
982
+ const agentId = req.model;
983
+ const key = req.cache?.sessionId ?? "default";
984
+ const acp = await this.loadAcp();
985
+ let leader;
986
+ try {
987
+ leader = await this.sessionFor(key, agentId);
988
+ } catch (err) {
989
+ throw agentTurnError(err, agentId, acp.classifyAgentFailure).error;
990
+ }
991
+ const queue = new StreamQueue();
992
+ const blocks = new TextBlocks(queue);
993
+ const onProgress = this.progress(leader, req.cache?.sessionId, {
994
+ text: (text) => blocks.text(text),
995
+ toolCall: () => blocks.close()
996
+ });
997
+ yield { type: "message_start", model: agentId };
998
+ const hostContext = acpLeaderHost()?.context(key);
999
+ const stopReminders = leader.session.canSteer() && hostContext?.meta ? startAcpTurnReminders({
1000
+ context: hostContext,
1001
+ steer: (text) => leader.session.steer(text)
1002
+ }) : void 0;
1003
+ const turn = leader.session.prompt([acp.textContent(this.promptText(leader, req))], opts.signal, onProgress).then(
1004
+ (result) => {
1005
+ blocks.close();
1006
+ if (result.usage) this.observeContext(leader, req.cache?.sessionId, result.usage);
1007
+ const agentContext = leader.context.current;
1008
+ queue.push({
1009
+ type: "message_stop",
1010
+ stopReason: turnStopReason(result),
1011
+ usage: turnUsage(result),
1012
+ ...agentContext ? { agentContext } : {}
1013
+ });
1014
+ queue.end();
1015
+ },
1016
+ (err) => {
1017
+ blocks.close();
1018
+ const { error, dead } = agentTurnError(err, agentId, acp.classifyAgentFailure);
1019
+ if (dead) this.drop(key, leader);
1020
+ queue.fail(error);
1021
+ }
1022
+ );
1023
+ try {
1024
+ yield* queue.drain();
1025
+ } finally {
1026
+ stopReminders?.();
1027
+ await turn;
1028
+ }
1029
+ }
1030
+ /** The turn's prompt; an agent joining a conversation under way gets the turns before it. */
1031
+ promptText(leader, req) {
1032
+ const latest = latestUserText(req.messages);
1033
+ const earlier = leader.joined ? void 0 : earlierConversation(req.messages);
1034
+ leader.joined = true;
1035
+ const prompt = earlier ? `${earlier}
1036
+
1037
+ ${latest}` : latest;
1038
+ const context = acpLeaderHost()?.context(req.cache?.sessionId);
1039
+ const notes = context?.meta ? consumeBtwNotes2(context) : [];
1040
+ const withNotes = notes.length > 0 ? `${prompt}
1041
+
1042
+ ${acpBtwBlock(notes)}` : prompt;
1043
+ return leader.promptPrefix ? `${leader.promptPrefix}
1044
+
1045
+ ${withNotes}` : withNotes;
1046
+ }
1047
+ /**
1048
+ * `/compact` on this route: the agent compacts its own conversation. No
1049
+ * agent started yet means there is nothing in its context to compact.
1050
+ */
1051
+ async compactOwnContext(opts) {
1052
+ const pending = this.sessions.get(opts.sessionId);
1053
+ if (!pending) return void 0;
1054
+ const leader = await pending;
1055
+ const acp = await this.loadAcp();
1056
+ await leader.session.prompt(
1057
+ [acp.textContent("/compact")],
1058
+ opts.signal,
1059
+ this.progress(leader, opts.sessionId)
1060
+ );
1061
+ return leader.context.current;
1062
+ }
1063
+ /** The progress handler of one agent turn: text, tool cards, rate limits and context. */
1064
+ progress(leader, sessionId, on = {}) {
1065
+ const tools = new ToolCardRelay(acpLeaderHost()?.events, {
1066
+ sessionId,
1067
+ model: leader.agentId
1068
+ });
1069
+ return (event) => {
1070
+ switch (event.type) {
1071
+ case "message":
1072
+ on.text?.(event.text);
1073
+ return;
1074
+ case "tool_call":
1075
+ case "tool_call_update": {
1076
+ if (event.type === "tool_call") on.toolCall?.();
1077
+ tools.update(event.toolCall);
1078
+ const compacted = leader.context.toolCall(event.toolCall);
1079
+ if (compacted) this.publishContext(sessionId, compacted);
1080
+ return;
1081
+ }
1082
+ case "usage":
1083
+ this.observeContext(leader, sessionId, event.usage);
1084
+ return;
1085
+ case "rate_limit":
1086
+ recordAcpRateLimit(leader.agentId, event.info);
1087
+ return;
1088
+ default:
1089
+ return;
1090
+ }
1091
+ };
1092
+ }
1093
+ /** A context reading from the agent: its window is the route's, and the fill shows at once. */
1094
+ observeContext(leader, sessionId, usage) {
1095
+ const reading = leader.context.usage(usage);
1096
+ if (reading.size > 0 && leader.agentId === this.config().model) {
1097
+ this.capabilities.maxContext = reading.size;
1098
+ }
1099
+ this.publishContext(sessionId, reading);
1100
+ }
1101
+ publishContext(sessionId, reading) {
1102
+ publishLeaderContext(acpLeaderHost()?.events, sessionId, reading, this.capabilities.maxContext);
1103
+ }
1104
+ async loadAcp() {
1105
+ this.acp ??= await import("@wrongstack/acp");
1106
+ return this.acp;
1107
+ }
1108
+ drop(key, leader) {
1109
+ this.sessions.delete(key);
1110
+ void leader.session.close().catch(() => {
1111
+ });
1112
+ void leader.fleet?.close().catch(() => {
1113
+ });
1114
+ }
1115
+ /** The persistent agent session for this conversation, started on first use. */
1116
+ sessionFor(key, agentId) {
1117
+ const existing = this.sessions.get(key);
1118
+ if (existing) {
1119
+ return existing.then((leader) => {
1120
+ if (leader.agentId === agentId) return leader;
1121
+ this.drop(key, leader);
1122
+ return this.sessionFor(key, agentId);
1123
+ });
1124
+ }
1125
+ const starting = this.startSession(key, agentId);
1126
+ this.sessions.set(key, starting);
1127
+ starting.catch(() => {
1128
+ if (this.sessions.get(key) === starting) this.sessions.delete(key);
1129
+ });
1130
+ return starting;
1131
+ }
1132
+ /**
1133
+ * Ask the user about one of the agent's calls on the prompt the surface
1134
+ * has: the REPL's own, or the TUI/WebUI approval prompt in the session's tab.
1135
+ */
1136
+ async confirm(sessionId, tool, input, request) {
1137
+ const approved = await this.ask(sessionId, tool, input, request);
1138
+ if (approved && FLEET_TOOLS.includes(tool.name)) {
1139
+ const key = fleetGrantKey(sessionId, tool.name, input);
1140
+ this.fleetGrants.set(key, (this.fleetGrants.get(key) ?? 0) + 1);
1141
+ }
1142
+ return approved;
1143
+ }
1144
+ /**
1145
+ * A fleet call the policy wants confirmed: the approval the user gave the
1146
+ * agent's permission request for it, once, or a question now (an agent
1147
+ * whose own settings let it call the tool without asking).
1148
+ */
1149
+ async approveFleetCall(sessionId, tool, input) {
1150
+ const key = fleetGrantKey(sessionId, tool.name, input);
1151
+ const granted = this.fleetGrants.get(key) ?? 0;
1152
+ if (granted > 0) {
1153
+ if (granted === 1) this.fleetGrants.delete(key);
1154
+ else this.fleetGrants.set(key, granted - 1);
1155
+ return true;
1156
+ }
1157
+ return this.ask(sessionId, tool, input, {
1158
+ toolCallId: void 0,
1159
+ signal: new AbortController().signal
1160
+ });
1161
+ }
1162
+ async ask(sessionId, tool, input, request) {
1163
+ const host = acpLeaderHost();
1164
+ const suggestedPattern = subjectForToolInput(tool.name, input, tool.subjectKey, tool.subjectFields) ?? tool.name;
1165
+ const toolUseId = request.toolCallId ?? `acp-${Date.now()}`;
1166
+ if (host?.confirmAwaiter) {
1167
+ const answer = await host.confirmAwaiter(tool, input, toolUseId, suggestedPattern);
1168
+ return answer !== "no" && answer !== "deny";
1169
+ }
1170
+ if (!host?.confirmThroughEvents) return false;
1171
+ return confirmThroughEvents({
1172
+ events: host.events,
1173
+ sessionId,
1174
+ tool,
1175
+ input,
1176
+ toolUseId,
1177
+ suggestedPattern,
1178
+ signal: request.signal,
1179
+ permission: host.permission(sessionId)
1180
+ });
1181
+ }
1182
+ async startSession(key, agentId) {
1183
+ const acp = await this.loadAcp();
1184
+ const config = this.config();
1185
+ const host = acpLeaderHost();
1186
+ const projectRoot = host?.projectRoot ?? process.cwd();
1187
+ const cmd = acp.resolveAcpAgentCommand(agentId, config.acp?.agents);
1188
+ if (!cmd) {
1189
+ throw new ProviderError2(
1190
+ `Unknown ACP agent "${agentId}". Run /acp to see the agents installed here.`,
1191
+ 400,
1192
+ false,
1193
+ this.id,
1194
+ { kind: "invalid_request" }
1195
+ );
1196
+ }
1197
+ const leaderCtx = {
1198
+ subagentId: "leader",
1199
+ config: { id: "leader", name: agentId, role: "leader" },
1200
+ sessionId: key
1201
+ };
1202
+ const setup = wrongstackBridgeSetup(
1203
+ agentId,
1204
+ projectRoot,
1205
+ config.acp?.bridge,
1206
+ "leader"
1207
+ )?.({ id: "leader", description: "" }, leaderCtx);
1208
+ const fleet = host ? await startFleetEndpoint(host, {
1209
+ sessionId: key,
1210
+ approve: (tool, input) => this.approveFleetCall(key, tool, input)
1211
+ }).catch(() => void 0) : void 0;
1212
+ const mcpServers = [
1213
+ ...setup?.mcpServers ?? [],
1214
+ ...fleet ? [
1215
+ {
1216
+ type: "http",
1217
+ name: FLEET_SERVER_NAME,
1218
+ url: fleet.url,
1219
+ headers: [{ name: "Authorization", value: `Bearer ${fleet.token}` }]
1220
+ }
1221
+ ] : []
1222
+ ];
1223
+ const meta = setup?.meta;
1224
+ const sessionMeta = meta?.systemPrompt?.append !== void 0 ? {
1225
+ ...meta,
1226
+ systemPrompt: {
1227
+ ...meta.systemPrompt,
1228
+ append: meta.systemPrompt.append + (fleet ? `
1229
+ ${fleetNote(fleet.tools)}` : "")
1230
+ }
1231
+ } : meta;
1232
+ const permissionPolicy = host ? wrongstackAcpPermissionPolicy({
1233
+ evaluate: (tool, input) => host.permission(key).evaluate(tool, input, host.context(key)),
1234
+ // A fleet tool is decided as the leader tool it runs, not as an MCP proxy.
1235
+ getTool: (name) => host.getTool(fleetToolName(name) ?? name),
1236
+ projectRoot,
1237
+ allowMcp: !isRestrictedMode({}),
1238
+ ...host.confirmAwaiter || host.confirmThroughEvents ? {
1239
+ confirm: (tool, call, request) => this.confirm(key, tool, call.input, request)
1240
+ } : {}
1241
+ }) : void 0;
1242
+ const stateDir = host?.stateDir;
1243
+ const resumeSessionId = stateDir ? await readLeaderSession(stateDir, key, agentId).catch(() => void 0) : void 0;
1244
+ let session;
1245
+ let opened;
1246
+ try {
1247
+ session = await acp.ACPSession.start({
1248
+ command: cmd.command,
1249
+ ...cmd.args ? { args: cmd.args } : {},
1250
+ ...cmd.env ? { env: cmd.env } : {},
1251
+ cwd: projectRoot,
1252
+ projectRoot,
1253
+ role: agentId,
1254
+ ...permissionPolicy ? { permissionPolicy } : {},
1255
+ ...mcpServers.length > 0 ? { mcpServers } : {},
1256
+ ...sessionMeta ? { sessionMeta } : {},
1257
+ // Claude asks before edits and commands only in `default`.
1258
+ ...permissionPolicy ? { initialModeId: "default", requireInitialMode: agentId === "claude-code" } : {},
1259
+ ...resumeSessionId ? { resumeSessionId } : {}
1260
+ });
1261
+ opened = await session.openSession();
1262
+ } catch (err) {
1263
+ void session?.close().catch(() => void 0);
1264
+ void fleet?.close().catch(() => void 0);
1265
+ throw err;
1266
+ }
1267
+ if (stateDir) {
1268
+ void writeLeaderSession(stateDir, key, { agentId, agentSessionId: opened.sessionId });
1269
+ }
1270
+ return {
1271
+ agentId,
1272
+ session,
1273
+ context: new LeaderContext(),
1274
+ fleet,
1275
+ joined: opened.resumed,
1276
+ ...setup?.promptPrefix ? { promptPrefix: setup.promptPrefix } : {}
1277
+ };
1278
+ }
1279
+ /** Close every agent session (process shutdown). */
1280
+ async closeAll() {
1281
+ const all = [...this.sessions.values()];
1282
+ this.sessions.clear();
1283
+ await Promise.all(
1284
+ all.map(
1285
+ (p) => p.then((leader) => {
1286
+ void leader.fleet?.close().catch(() => void 0);
1287
+ return leader.session.close({ graceMs: 500 });
1288
+ }).catch(() => void 0)
1289
+ )
1290
+ );
1291
+ }
1292
+ };
1293
+ function createAcpLeaderProviderFactory(opts) {
1294
+ return {
1295
+ type: ACP_LEADER_PROVIDER_ID,
1296
+ // Routing metadata only: the provider does no wire conversion.
1297
+ family: "openai-compatible",
1298
+ create: (cfg) => new AcpLeaderProvider({
1299
+ ...opts,
1300
+ id: cfg.type || ACP_LEADER_PROVIDER_ID
1301
+ })
1302
+ };
1303
+ }
1304
+
1305
+ // src/wiring/provider.ts
1306
+ import { ProviderAuthRegistry, ProviderRegistry } from "@wrongstack/core/registry";
1307
+ import { ConfigError } from "@wrongstack/core/types";
1308
+ import {
1309
+ buildProviderFactoriesFromRegistry,
1310
+ createAiGatewayProviderFactory,
1311
+ createSetupProviderFactory,
1312
+ installCatalogModelOutputLimits,
1313
+ isSetupProvider,
1314
+ makeProviderFromConfig,
1315
+ setStreamTimeoutDefaults,
1316
+ setupProviderResolved,
1317
+ withCatalogCapabilities
1318
+ } from "@wrongstack/providers";
1319
+ import { registerBuiltinProviderAuthStrategies } from "@wrongstack/providers/oauth";
1320
+ async function setupProvider(params) {
1321
+ const { config, modelsRegistry, logger } = params;
1322
+ const providerAuthRegistry = new ProviderAuthRegistry();
1323
+ registerBuiltinProviderAuthStrategies(providerAuthRegistry);
1324
+ if (isSetupProvider(config.provider)) {
1325
+ const providerRegistry2 = new ProviderRegistry();
1326
+ providerRegistry2.register(createSetupProviderFactory());
1327
+ return {
1328
+ resolvedProvider: setupProviderResolved(),
1329
+ provider: providerRegistry2.create({ type: config.provider }),
1330
+ providerRegistry: providerRegistry2,
1331
+ providerAuthRegistry
1332
+ };
1333
+ }
1334
+ const savedProviderCfg = config.providers?.[config.provider];
1335
+ const acpLeader = config.provider === ACP_LEADER_PROVIDER_ID;
1336
+ let resolvedProvider = acpLeader ? {
1337
+ id: ACP_LEADER_PROVIDER_ID,
1338
+ name: "ACP agent",
1339
+ family: "openai-compatible",
1340
+ apiBase: void 0,
1341
+ envVars: [],
1342
+ models: [{ id: config.model, name: config.model }],
1343
+ npm: void 0
1344
+ } : await modelsRegistry.getProvider(config.provider).catch(() => void 0);
1345
+ if (!resolvedProvider && savedProviderCfg?.type && savedProviderCfg.type !== config.provider) {
1346
+ resolvedProvider = await modelsRegistry.getProvider(savedProviderCfg.type).catch(() => void 0);
1347
+ }
1348
+ if (!resolvedProvider) {
1349
+ if (savedProviderCfg?.family) {
1350
+ const family = savedProviderCfg.family;
1351
+ const savedModels = savedProviderCfg.models;
1352
+ let models;
1353
+ if (savedModels && savedModels.length > 0) {
1354
+ models = savedModels.map((m) => ({ id: m, name: m }));
1355
+ } else {
1356
+ models = [];
1357
+ }
1358
+ resolvedProvider = {
1359
+ id: config.provider,
1360
+ name: config.provider,
1361
+ family,
1362
+ apiBase: savedProviderCfg.baseUrl,
1363
+ envVars: savedProviderCfg.envVars ?? [],
1364
+ models,
1365
+ npm: void 0
1366
+ };
1367
+ } else {
1368
+ logger.warn(
1369
+ `Provider "${config.provider}" not found in models.dev. Continuing with raw config.`
1370
+ );
1371
+ }
1372
+ } else if (resolvedProvider.family === "unsupported" && !savedProviderCfg?.family) {
1373
+ throw new ConfigError({
1374
+ message: `Provider "${config.provider}" uses an unsupported wire family (${resolvedProvider.npm}). Install a plugin to enable it, or pick a different provider.`,
1375
+ code: "CONFIG_INVALID",
1376
+ context: { provider: config.provider, family: resolvedProvider.npm, kind: "unsupported" }
1377
+ });
1378
+ }
1379
+ setStreamTimeoutDefaults(config.modelRuntime?.streaming);
1380
+ if (config.features.modelsRegistry) {
1381
+ await installCatalogModelOutputLimits({
1382
+ registry: modelsRegistry,
1383
+ getConfig: () => config,
1384
+ log: (message) => logger.debug(message)
1385
+ });
1386
+ }
1387
+ const providerRegistry = new ProviderRegistry();
1388
+ providerRegistry.register(createAiGatewayProviderFactory());
1389
+ providerRegistry.register(createSetupProviderFactory());
1390
+ providerRegistry.register(
1391
+ createAcpLeaderProviderFactory({
1392
+ bootConfig: config,
1393
+ buildProvider: (providerId, live) => buildProviderForId({ config: live, providerRegistry, logger }, providerId)
1394
+ })
1395
+ );
1396
+ if (config.features.modelsRegistry) {
1397
+ try {
1398
+ const factories = await buildProviderFactoriesFromRegistry({
1399
+ registry: modelsRegistry,
1400
+ log: logger
1401
+ });
1402
+ for (const f of factories) providerRegistry.register(f);
1403
+ } catch (err) {
1404
+ throw new ConfigError({
1405
+ message: `Failed to load models.dev registry: ${err instanceof Error ? err.message : err}
1406
+ Try \`wstack models refresh\` once you have network access.`,
1407
+ code: "CONFIG_INVALID",
1408
+ context: { phase: "registry-build", provider: config.provider },
1409
+ cause: err
1410
+ });
1411
+ }
1412
+ }
1413
+ const { cfg: providerConfig, factoryType } = resolveProviderCfgWithProxy(
1414
+ config,
1415
+ config.provider,
1416
+ // Boot-time rewrite decision — the first "is traffic proxied?" line in
1417
+ // wrongstack.log. info-level only; skips log too (they answer WHY).
1418
+ { logger }
1419
+ );
1420
+ let provider;
1421
+ try {
1422
+ const cfgWithType = { ...providerConfig, type: config.provider };
1423
+ if (providerRegistry.has(factoryType)) {
1424
+ provider = providerRegistry.create(cfgWithType, factoryType);
1425
+ } else {
1426
+ provider = makeProviderFromConfig(config.provider, { ...cfgWithType, type: factoryType });
1427
+ }
1428
+ } catch (err) {
1429
+ throw new ConfigError({
1430
+ message: `Failed to create provider: ${err instanceof Error ? err.message : err}`,
1431
+ code: "CONFIG_INVALID",
1432
+ context: { phase: "provider-create", provider: config.provider },
1433
+ cause: err
1434
+ });
1435
+ }
1436
+ if (config.features.modelsRegistry && !acpLeader) {
1437
+ provider = await withCatalogCapabilities(
1438
+ modelsRegistry,
1439
+ config.provider,
1440
+ provider,
1441
+ { ...providerConfig, type: config.provider, model: config.model },
1442
+ logger
1443
+ );
1444
+ }
1445
+ return { resolvedProvider, provider, providerRegistry, providerAuthRegistry };
1446
+ }
1447
+
1448
+ export {
1449
+ formatAcpAgentList,
1450
+ loadCachedAcpRegistry,
1451
+ refreshAcpRegistry,
1452
+ wrongstackAcpPermissionPolicy,
1453
+ recordAcpRateLimit,
1454
+ startAcpTurnReminders,
1455
+ setupProvider,
1456
+ createGracefulShutdown
1457
+ };
1458
+ //# sourceMappingURL=chunk-COGI7EQU.js.map