@wrongstack/cli 0.318.0 → 0.319.0

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 (33) hide show
  1. package/data/providers.json +1 -22
  2. package/dist/{acp-CEQINXWB.js → acp-5ANIAOZC.js} +4 -4
  3. package/dist/arg-parser.d.ts +4 -0
  4. package/dist/{auth-3OSF6BMU.js → auth-KF5TAD7V.js} +10 -10
  5. package/dist/{chunk-GKHUHAIK.js → chunk-42JD25KU.js} +5 -5
  6. package/dist/{chunk-QYIIOMLE.js → chunk-E4I2TF5S.js} +2 -2
  7. package/dist/{chunk-37SPPLUD.js → chunk-F5GA2QW2.js} +15 -2
  8. package/dist/chunk-GGGUZSAU.js +235 -0
  9. package/dist/{chunk-QC5I4VWZ.js → chunk-H7GABTGS.js} +6 -2
  10. package/dist/chunk-OZDE3OWD.js +52 -0
  11. package/dist/chunk-P2OKBDCH.js +1166 -0
  12. package/dist/chunk-PCBJSXYV.js +65 -0
  13. package/dist/{chunk-KG4X5F7Z.js → chunk-VFCJOWJB.js} +10 -12
  14. package/dist/chunk-VWCZL4OH.js +46 -0
  15. package/dist/{chunk-YX6KUIDF.js → chunk-XHIEA6LG.js} +861 -29
  16. package/dist/{chunk-BXFYBQKR.js → chunk-XPX432BZ.js} +8 -1
  17. package/dist/cli-context-LH4QP4W2.js +3369 -0
  18. package/dist/{cli-main-ILRTNPDP.js → cli-main-72XUFUCL.js} +50 -44
  19. package/dist/{execution-BN4QIFH6.js → execution-DKJDNPYF.js} +17 -17
  20. package/dist/index.js +20 -4440
  21. package/dist/{modeldiag-4NYBRSFP.js → modeldiag-5FRMT6FQ.js} +2 -2
  22. package/dist/per-subcommand-help-6VCIKDUW.js +23 -0
  23. package/dist/{plugin-usage-AP7SJCDA.js → plugin-usage-CYUORYM6.js} +2 -2
  24. package/dist/{plugins-HQ5FU2AM.js → plugins-RV3OBBND.js} +3 -3
  25. package/dist/pre-launch/first-run.d.ts +51 -0
  26. package/dist/pre-launch.d.ts +1 -0
  27. package/dist/{providers-models-663NDPRQ.js → providers-models-OKKQZIWU.js} +2 -2
  28. package/dist/{sessions-config-P2LK2L32.js → sessions-config-KAMEBVOX.js} +4 -4
  29. package/dist/short-circuit-flags-U6CJAF6J.js +11 -0
  30. package/dist/subcommands-J4JMWC5W.js +10 -0
  31. package/dist/{webui-server-OJLLM6PE.js → webui-server-SY4EQCKD.js} +5 -5
  32. package/package.json +28 -28
  33. package/dist/chunk-L6ZNCTRJ.js +0 -1067
@@ -0,0 +1,3369 @@
1
+ import {
2
+ applySimpleUiFullAutoProfile,
3
+ detectProjectFacts,
4
+ isSimpleUiFullAuto,
5
+ makePromptDelegate,
6
+ renderAgentsTemplate,
7
+ renderDiff
8
+ } from "./chunk-VFCJOWJB.js";
9
+ import {
10
+ theme as theme2
11
+ } from "./chunk-QD544J2B.js";
12
+ import {
13
+ parseWebuiSessionChildOptions,
14
+ writeWebuiSessionChildError
15
+ } from "./chunk-GPPRNAKW.js";
16
+ import {
17
+ fmtTok,
18
+ patchConfig
19
+ } from "./chunk-TYO2OVAD.js";
20
+ import {
21
+ subcommands
22
+ } from "./chunk-OZDE3OWD.js";
23
+ import {
24
+ renderDeepHelp,
25
+ renderFocusedHelp
26
+ } from "./chunk-P2OKBDCH.js";
27
+ import {
28
+ handleHelpVersionShortCircuit
29
+ } from "./chunk-VWCZL4OH.js";
30
+ import {
31
+ DEFAULT_PORT
32
+ } from "./chunk-7JBKXVMK.js";
33
+ import "./chunk-HXJYHAR4.js";
34
+ import "./chunk-Q5GTM25S.js";
35
+ import {
36
+ discoverAndMergeProviders
37
+ } from "./chunk-52C7PD2H.js";
38
+ import {
39
+ applyNodeEnvDefault,
40
+ applySessionShellDefault,
41
+ runPreflight
42
+ } from "./chunk-PCBJSXYV.js";
43
+ import "./chunk-QAC263FZ.js";
44
+ import {
45
+ CLI_VERSION
46
+ } from "./chunk-XJXDOF63.js";
47
+ import {
48
+ addFromCatalog,
49
+ boxBottom,
50
+ boxDivider,
51
+ boxRow,
52
+ boxTop,
53
+ runAuthLocal,
54
+ runOAuthLoginMenu,
55
+ runPicker,
56
+ saveToGlobalConfig,
57
+ theme
58
+ } from "./chunk-XHIEA6LG.js";
59
+ import "./chunk-2VMJ3E5Z.js";
60
+ import "./chunk-B4JLSXZB.js";
61
+ import {
62
+ isKeylessLocalProvider,
63
+ visibleModelIds
64
+ } from "./chunk-VUVOMXWP.js";
65
+ import {
66
+ activeProfileConfigPath
67
+ } from "./chunk-YMXXOOFN.js";
68
+ import {
69
+ parseArgs
70
+ } from "./chunk-GGGUZSAU.js";
71
+ import "./chunk-E4I2TF5S.js";
72
+ import {
73
+ mutateConfigProviders,
74
+ normalizeKeys,
75
+ resolveActiveApiKey,
76
+ writeKeysBack
77
+ } from "./chunk-XPX432BZ.js";
78
+ import {
79
+ runUpdateCommand
80
+ } from "./chunk-DE7BZDNK.js";
81
+ import "./chunk-U3SS4NRH.js";
82
+ import "./chunk-YLQEGYV4.js";
83
+ import "./chunk-7OCVIDC7.js";
84
+
85
+ // src/cli-context.ts
86
+ import { TOKENS as TOKENS3 } from "@wrongstack/core/kernel";
87
+ import { writeErr as writeErr2 } from "@wrongstack/core/utils";
88
+ import { setOAuthTokenPersister } from "@wrongstack/providers";
89
+
90
+ // src/boot/container-wiring.ts
91
+ import { DefaultPathResolver } from "@wrongstack/core/infrastructure";
92
+ import { EventBus as CoreEventBus, TOKENS } from "@wrongstack/core/kernel";
93
+ import { createDefaultContainer } from "@wrongstack/runtime";
94
+
95
+ // src/cli-bundled-prompts.ts
96
+ import { createRequire } from "node:module";
97
+ import * as path from "node:path";
98
+ function resolveBundledPromptsDir() {
99
+ try {
100
+ const req = createRequire(import.meta.url);
101
+ const corePkg = req.resolve("@wrongstack/core/package.json");
102
+ return path.join(path.dirname(corePkg), "data", "prompts");
103
+ } catch {
104
+ return void 0;
105
+ }
106
+ }
107
+
108
+ // src/cli-bundled-skills.ts
109
+ import { createRequire as createRequire2 } from "node:module";
110
+ import * as path2 from "node:path";
111
+ function resolveBundledSkillsDir() {
112
+ try {
113
+ const req = createRequire2(import.meta.url);
114
+ const corePkg = req.resolve("@wrongstack/core/package.json");
115
+ return path2.join(path2.dirname(corePkg), "skills");
116
+ } catch {
117
+ return void 0;
118
+ }
119
+ }
120
+
121
+ // src/boot/container-wiring.ts
122
+ function wireContainer(deps) {
123
+ const pathResolver = new DefaultPathResolver(deps.cwd);
124
+ const events = new CoreEventBus();
125
+ events.setLogger(deps.logger);
126
+ const container = createDefaultContainer({
127
+ config: deps.config,
128
+ wpaths: deps.wpaths,
129
+ logger: deps.logger,
130
+ modelsRegistry: deps.modelsRegistry,
131
+ events,
132
+ permission: {
133
+ yolo: deps.config.yolo,
134
+ promptDelegate: makePromptDelegate(deps.reader)
135
+ },
136
+ compactor: {
137
+ preserveK: deps.config.context.preserveK,
138
+ eliseThreshold: deps.config.context.eliseThreshold
139
+ },
140
+ bundledSkillsDir: deps.config.features.skills ? resolveBundledSkillsDir() : void 0,
141
+ bundledPromptsDir: deps.config.features.prompts === false ? void 0 : resolveBundledPromptsDir()
142
+ });
143
+ container.bind(TOKENS.PathResolver, () => pathResolver);
144
+ container.bind(TOKENS.Renderer, () => deps.renderer);
145
+ container.bind(TOKENS.InputReader, () => deps.reader);
146
+ return { pathResolver, events, container };
147
+ }
148
+
149
+ // src/boot/launch-menu.ts
150
+ import * as fs from "node:fs/promises";
151
+ import { HQ_CLI_DEFAULT_HOST } from "@wrongstack/core/hq";
152
+ import { atomicWrite, color, isStdinTTY, setRawMode } from "@wrongstack/core/utils";
153
+
154
+ // src/boot/launch-banner.ts
155
+ var STACK_ORANGE = { r: 253, g: 159, b: 2 };
156
+ var SIGNAL_PINK = { r: 254, g: 46, b: 95 };
157
+ function mixRgb(a, b, t) {
158
+ const clamped = Math.min(1, Math.max(0, t));
159
+ return {
160
+ r: Math.round(a.r + (b.r - a.r) * clamped),
161
+ g: Math.round(a.g + (b.g - a.g) * clamped),
162
+ b: Math.round(a.b + (b.b - a.b) * clamped)
163
+ };
164
+ }
165
+ function fg(c) {
166
+ return `\x1B[38;2;${c.r};${c.g};${c.b}m`;
167
+ }
168
+ function colorsEnabled() {
169
+ const flag = (value) => value !== void 0 && value.trim() !== "" && !/^(0|false|no|off)$/i.test(value.trim());
170
+ if (flag(process.env.NO_COLOR)) return false;
171
+ if (flag(process.env.FORCE_COLOR)) return true;
172
+ return process.stdout.isTTY === true;
173
+ }
174
+ var WORDMARK_GLYPH_BITS = Object.freeze({
175
+ W: Object.freeze(["10001", "10001", "10001", "10101", "10101", "10101", "01010"]),
176
+ R: Object.freeze(["11110", "10001", "10001", "11110", "10100", "10010", "10001"]),
177
+ O: Object.freeze(["01110", "10001", "10001", "10001", "10001", "10001", "01110"]),
178
+ N: Object.freeze(["10001", "11001", "11001", "10101", "10011", "10011", "10001"]),
179
+ G: Object.freeze(["01110", "10001", "10000", "10111", "10001", "10001", "01110"]),
180
+ S: Object.freeze(["01111", "10000", "10000", "01110", "00001", "00001", "11110"]),
181
+ T: Object.freeze(["11111", "00100", "00100", "00100", "00100", "00100", "00100"]),
182
+ A: Object.freeze(["01110", "10001", "10001", "11111", "10001", "10001", "10001"]),
183
+ C: Object.freeze(["01111", "10000", "10000", "10000", "10000", "10000", "01111"]),
184
+ K: Object.freeze(["10001", "10010", "10100", "11000", "10100", "10010", "10001"])
185
+ });
186
+ var WORDMARK = "WRONGSTACK";
187
+ var WORDMARK_PIXEL_ROWS = 7;
188
+ var WORDMARK_ROWS = Math.ceil(WORDMARK_PIXEL_ROWS / 2);
189
+ var GLYPH_WIDTH = 5;
190
+ var WORDMARK_WIDTH = WORDMARK.length * GLYPH_WIDTH + WORDMARK.length - 1;
191
+ function packedCell(top, bottom) {
192
+ if (top === "1" && bottom === "1") return "\u2588";
193
+ if (top === "1") return "\u2580";
194
+ if (bottom === "1") return "\u2584";
195
+ return " ";
196
+ }
197
+ function packedGlyphRow(glyph, row) {
198
+ const top = glyph[row * 2] ?? "";
199
+ const bottom = glyph[row * 2 + 1] ?? "";
200
+ return Array.from(
201
+ { length: GLYPH_WIDTH },
202
+ (_, column) => packedCell(top[column], bottom[column])
203
+ ).join("");
204
+ }
205
+ var WORDMARK_LINES = Object.freeze(
206
+ Array.from(
207
+ { length: WORDMARK_ROWS },
208
+ (_, row) => [...WORDMARK].map((letter) => {
209
+ const glyph = WORDMARK_GLYPH_BITS[letter];
210
+ return glyph ? packedGlyphRow(glyph, row) : " ".repeat(GLYPH_WIDTH);
211
+ }).join(" ")
212
+ )
213
+ );
214
+ function gradientLine(text, colorOn) {
215
+ if (!colorOn) return text;
216
+ const length = text.length;
217
+ let out = "\x1B[1m";
218
+ for (let i = 0; i < length; i++) {
219
+ const ch = text[i];
220
+ if (ch === " ") {
221
+ out += ch;
222
+ continue;
223
+ }
224
+ const t = length > 1 ? i / (length - 1) : 0.5;
225
+ out += `${fg(mixRgb(STACK_ORANGE, SIGNAL_PINK, t))}${ch}`;
226
+ }
227
+ return `${out}\x1B[0m`;
228
+ }
229
+ function dimLine(text, colorOn) {
230
+ return colorOn ? `\x1B[2m${text}\x1B[22m` : text;
231
+ }
232
+ var MARK_COLS = 5;
233
+ var PINK_HOME = 1;
234
+ var MARK_WIDTH = MARK_COLS * 2 + (MARK_COLS - 1);
235
+ function buildMarkLines(colorOn) {
236
+ const orange = colorOn ? fg(STACK_ORANGE) : "";
237
+ const pink = colorOn ? fg(SIGNAL_PINK) : "";
238
+ const reset = colorOn ? "\x1B[0m" : "";
239
+ return Array.from({ length: 3 }, (_, row) => {
240
+ const cells = Array.from({ length: MARK_COLS }, (_2, column) => {
241
+ if (column === PINK_HOME && (row === 1 || row === 2)) return `${pink}\u2588\u2588${reset}`;
242
+ if (row < 2 && column !== PINK_HOME) return `${orange}\u2588\u2588${reset}`;
243
+ return " ";
244
+ });
245
+ return cells.join(" ").trimEnd();
246
+ });
247
+ }
248
+ function launchBannerLines(columns, version) {
249
+ const colorOn = colorsEnabled();
250
+ const tagline = version ? `v${version} \xB7 the wrong stack for all the right reasons` : "the wrong stack for all the right reasons";
251
+ if (columns < WORDMARK_WIDTH + 4) {
252
+ const compact = "\u2590\u2588\u258C W R O N G S T A C K";
253
+ return [gradientLine(compact, colorOn), dimLine(tagline, colorOn)];
254
+ }
255
+ const markPad = " ".repeat(Math.max(0, Math.floor((WORDMARK_WIDTH - MARK_WIDTH) / 2)));
256
+ const tagPad = " ".repeat(Math.max(0, Math.floor((WORDMARK_WIDTH - tagline.length) / 2)));
257
+ return [
258
+ ...buildMarkLines(colorOn).map((line) => markPad + line),
259
+ "",
260
+ ...WORDMARK_LINES.map((line) => gradientLine(line, colorOn)),
261
+ tagPad + dimLine(tagline, colorOn)
262
+ ];
263
+ }
264
+
265
+ // src/boot/launch-menu.ts
266
+ var DEFAULT_PORTS = {
267
+ webui: 3456,
268
+ simpleui: 3466,
269
+ hq: DEFAULT_PORT
270
+ };
271
+ var DEFAULT_HOST = "127.0.0.1";
272
+ var HQ_DEFAULT_HOST = HQ_CLI_DEFAULT_HOST;
273
+ function bindsPort(mode) {
274
+ return mode !== "tui-repl" && mode !== "desktop";
275
+ }
276
+ function defaultHostFor(mode) {
277
+ return mode === "hq" ? HQ_DEFAULT_HOST : DEFAULT_HOST;
278
+ }
279
+ var MODE_OPTIONS = [
280
+ {
281
+ key: 1,
282
+ mode: "tui-repl",
283
+ label: "TUI / REPL",
284
+ hint: "interactive terminal (default)",
285
+ icon: "\u2328"
286
+ },
287
+ {
288
+ key: 2,
289
+ mode: "webui",
290
+ label: "WebUI",
291
+ hint: `browser-based project UI (port ${DEFAULT_PORTS.webui})`,
292
+ icon: "\u{1F310}"
293
+ },
294
+ {
295
+ key: 3,
296
+ mode: "simpleui",
297
+ label: "SimpleUI",
298
+ hint: `lightweight browser UI (port ${DEFAULT_PORTS.simpleui})`,
299
+ icon: "\u{1F4C4}"
300
+ },
301
+ {
302
+ key: 4,
303
+ mode: "hq",
304
+ label: "HQ",
305
+ hint: `project-independent dashboard (port ${DEFAULT_PORT})`,
306
+ icon: "\u{1F4CA}"
307
+ },
308
+ {
309
+ key: 5,
310
+ mode: "desktop",
311
+ label: "Desktop",
312
+ hint: "Electron app (alias: --desktop)",
313
+ icon: "\u{1F5A5}"
314
+ }
315
+ ];
316
+ var MENU_TIMEOUT_MS = 8e3;
317
+ var SUMMARY_TIMEOUT_MS = 5e3;
318
+ var PORT_TIMEOUT_MS = 8e3;
319
+ function shouldSkipMenu(argv, flags, positional) {
320
+ if (flags["no-menu"] === true) return true;
321
+ if (flags["webui"] === true) return true;
322
+ if (flags["webui-session-child"] === true) return true;
323
+ if (flags["simpleui"] === true) return true;
324
+ if (flags["hq"] === true) return true;
325
+ if (flags["desktop"] === true) return true;
326
+ if (flags["no-interactive"] === true) return true;
327
+ if (flags["skip"] === true) return true;
328
+ if (typeof flags["prompt"] === "string") return true;
329
+ if (!isStdinTTY()) return true;
330
+ if (positional.length > 0) return true;
331
+ void argv;
332
+ return false;
333
+ }
334
+ async function runLaunchMenu(deps) {
335
+ if (shouldSkipMenu(deps.argv, deps.flags, deps.positional)) {
336
+ return null;
337
+ }
338
+ const { renderer, reader, lastChoice } = deps;
339
+ const ports = { ...DEFAULT_PORTS, ...deps.defaultPorts ?? {} };
340
+ writeBanner(renderer);
341
+ if (lastChoice) {
342
+ const accept = await promptSummaryGate(deps);
343
+ if (accept === "use-last") {
344
+ return finalize(lastChoice, ports);
345
+ }
346
+ if (accept === "cancel") {
347
+ return { ...lastChoice, cancelled: true };
348
+ }
349
+ }
350
+ renderer.write(`
351
+ ${color.amber("?")} Choose how to run WrongStack:
352
+
353
+ `);
354
+ const arrowPick = await promptModeArrow(deps);
355
+ let choice;
356
+ if (arrowPick) {
357
+ if (arrowPick.cancelled) {
358
+ return { mode: "tui-repl", cancelled: true };
359
+ }
360
+ choice = { mode: arrowPick.mode };
361
+ } else {
362
+ for (const opt of MODE_OPTIONS) {
363
+ renderer.write(
364
+ ` ${color.bold(String(opt.key))}) ${opt.icon} ${color.bold(opt.label)} ${color.dim(opt.hint)}
365
+ `
366
+ );
367
+ }
368
+ renderer.write(`
369
+ ${color.dim("\u2500".repeat(48))}
370
+ `);
371
+ const answer = (await reader.readLine(
372
+ ` ${color.amber("?")} Mode ${color.dim("[1-5, q to quit]")} ${color.dim(`(auto 1 in ${MENU_TIMEOUT_MS / 1e3}s)`)} `,
373
+ { timeoutMs: MENU_TIMEOUT_MS, defaultAnswer: "1" }
374
+ )).trim().toLowerCase();
375
+ if (answer === "q" || answer === "quit") {
376
+ renderer.write(color.dim(" Goodbye!\n"));
377
+ return { mode: "tui-repl", cancelled: true };
378
+ }
379
+ const picked = MODE_OPTIONS.find((o) => String(o.key) === answer || o.mode === answer);
380
+ choice = picked ? { mode: picked.mode } : { mode: "tui-repl" };
381
+ }
382
+ if (bindsPort(choice.mode)) {
383
+ const port = await promptPort(deps, ports[choice.mode]);
384
+ choice.port = port;
385
+ const defaultHost = defaultHostFor(choice.mode);
386
+ const host = await promptHost(deps, defaultHost);
387
+ if (host !== defaultHost) choice.host = host;
388
+ }
389
+ return finalize(choice, ports);
390
+ }
391
+ function writeBanner(renderer) {
392
+ const columns = process.stdout.columns ?? 80;
393
+ renderer.write("\n");
394
+ for (const line of launchBannerLines(columns, CLI_VERSION)) {
395
+ renderer.write(` ${line}
396
+ `);
397
+ }
398
+ }
399
+ function canUseArrowMenu() {
400
+ if (process.env.VITEST || process.env.NODE_ENV === "test") return false;
401
+ const stdin = process.stdin;
402
+ return isStdinTTY() && process.stdout.isTTY === true && typeof stdin.setRawMode === "function";
403
+ }
404
+ async function promptModeArrow(deps) {
405
+ if (!canUseArrowMenu()) return null;
406
+ const { renderer } = deps;
407
+ const stdin = process.stdin;
408
+ const out = (s) => renderer.write(s);
409
+ return await new Promise((resolve2) => {
410
+ let index = 0;
411
+ let drawn = 0;
412
+ let remaining = Math.round(MENU_TIMEOUT_MS / 1e3);
413
+ let countdown = true;
414
+ let settled = false;
415
+ let interval;
416
+ const wasRaw = stdin.isRaw === true;
417
+ const wasPaused = stdin.isPaused();
418
+ const rows = () => {
419
+ const lines = [];
420
+ for (let i = 0; i < MODE_OPTIONS.length; i++) {
421
+ const opt = MODE_OPTIONS[i];
422
+ const selected = i === index;
423
+ const pointer = selected ? color.pink("\u276F") : " ";
424
+ const label = selected ? color.amber(color.bold(opt.label)) : color.bold(opt.label);
425
+ lines.push(
426
+ ` ${pointer} ${color.dim(String(opt.key))} ${opt.icon} ${label} ${color.dim(opt.hint)}`
427
+ );
428
+ }
429
+ lines.push("");
430
+ const auto = countdown ? color.dim(` \xB7 auto-launches ${MODE_OPTIONS[index].label} in ${remaining}s`) : "";
431
+ lines.push(` ${color.dim("\u2191\u2193 move \xB7 Enter launch \xB7 1-5 jump \xB7 q quit")}${auto}`);
432
+ return lines;
433
+ };
434
+ const paint = () => {
435
+ const lines = rows();
436
+ let frame = drawn > 0 ? `\x1B[${drawn}A` : "";
437
+ frame += `${lines.map((line) => `\x1B[2K${line}`).join("\n")}
438
+ `;
439
+ out(frame);
440
+ drawn = lines.length;
441
+ };
442
+ const stopInput = () => {
443
+ if (interval) {
444
+ clearInterval(interval);
445
+ interval = void 0;
446
+ }
447
+ stdin.off("data", onData);
448
+ stdin.off("close", onClose);
449
+ try {
450
+ setRawMode(stdin, wasRaw);
451
+ } catch {
452
+ }
453
+ if (wasPaused) stdin.pause();
454
+ };
455
+ const settle = (result, finalLine) => {
456
+ if (settled) return;
457
+ settled = true;
458
+ stopCountdown();
459
+ stopInput();
460
+ if (drawn > 0) out(`\x1B[${drawn}A`);
461
+ out("\x1B[0J");
462
+ out(finalLine);
463
+ out("\x1B[?25h");
464
+ resolve2(result);
465
+ };
466
+ const pick = () => {
467
+ const opt = MODE_OPTIONS[index];
468
+ settle(
469
+ { cancelled: false, mode: opt.mode },
470
+ ` ${color.green("\u2714")} ${opt.icon} ${color.bold(opt.label)} ${color.dim(opt.hint)}
471
+ `
472
+ );
473
+ };
474
+ const cancel = () => {
475
+ settle({ cancelled: true, mode: "tui-repl" }, color.dim(" Goodbye!\n"));
476
+ };
477
+ const stopCountdown = () => {
478
+ if (!countdown) return;
479
+ countdown = false;
480
+ if (interval) {
481
+ clearInterval(interval);
482
+ interval = void 0;
483
+ }
484
+ };
485
+ const onData = (buf) => {
486
+ const key = buf.toString("utf8");
487
+ stopCountdown();
488
+ if (key === "" || key === "q" || key === "Q" || key === "\x1B") {
489
+ cancel();
490
+ return;
491
+ }
492
+ if (key === "\r" || key === "\n") {
493
+ pick();
494
+ return;
495
+ }
496
+ if (key === "\x1B[A" || key === "\x1BOA" || key === "k") {
497
+ index = (index + MODE_OPTIONS.length - 1) % MODE_OPTIONS.length;
498
+ paint();
499
+ return;
500
+ }
501
+ if (key === "\x1B[B" || key === "\x1BOB" || key === "j") {
502
+ index = (index + 1) % MODE_OPTIONS.length;
503
+ paint();
504
+ return;
505
+ }
506
+ if (/^[0-9]$/.test(key)) {
507
+ const digit = Number.parseInt(key, 10);
508
+ if (digit >= 1 && digit <= MODE_OPTIONS.length) {
509
+ index = digit - 1;
510
+ pick();
511
+ return;
512
+ }
513
+ }
514
+ paint();
515
+ };
516
+ const onClose = () => {
517
+ settle({ cancelled: true, mode: "tui-repl" }, "");
518
+ };
519
+ try {
520
+ setRawMode(stdin, true);
521
+ stdin.resume();
522
+ } catch {
523
+ resolve2(null);
524
+ return;
525
+ }
526
+ out("\x1B[?25l");
527
+ paint();
528
+ stdin.on("data", onData);
529
+ stdin.once("close", onClose);
530
+ interval = setInterval(() => {
531
+ remaining -= 1;
532
+ if (remaining <= 0) {
533
+ pick();
534
+ return;
535
+ }
536
+ paint();
537
+ }, 1e3);
538
+ });
539
+ }
540
+ async function promptSummaryGate(deps) {
541
+ const { renderer, reader, lastChoice } = deps;
542
+ if (!lastChoice) return "re-prompt";
543
+ const modeLabel = describeMode(lastChoice.mode);
544
+ const portStr = typeof lastChoice.port === "number" ? ` \xB7 port ${color.bold(String(lastChoice.port))}` : "";
545
+ const hostStr = typeof lastChoice.host === "string" ? ` \xB7 host ${color.dim(lastChoice.host)}` : "";
546
+ renderer.write(`
547
+ ${color.dim("\u2500".repeat(48))}
548
+ `);
549
+ renderer.write(
550
+ ` ${color.cyan("\u23CE")} ${color.dim("Last settings:")} ${color.bold(modeLabel)}${portStr}${hostStr}
551
+ `
552
+ );
553
+ renderer.write(` ${color.dim("\u2500".repeat(48))}
554
+ `);
555
+ const answer = (await reader.readLine(
556
+ ` ${color.amber("?")} Continue with these? ${color.dim("[Y/n/q]")} ${color.dim("(auto Y in 5s)")} `,
557
+ { timeoutMs: SUMMARY_TIMEOUT_MS, defaultAnswer: "y" }
558
+ )).trim().toLowerCase();
559
+ if (answer === "q" || answer === "quit") {
560
+ renderer.write(color.dim(" Goodbye!\n"));
561
+ return "cancel";
562
+ }
563
+ if (answer === "n" || answer === "no") return "re-prompt";
564
+ return "use-last";
565
+ }
566
+ async function promptPort(deps, defaultPort) {
567
+ const { renderer, reader } = deps;
568
+ const maxRetries = 3;
569
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
570
+ const answer = (await reader.readLine(
571
+ ` ${color.amber("?")} Port ${color.dim(`[${defaultPort}]`)} ${color.dim("(Enter = default, q = cancel)")} `,
572
+ { timeoutMs: PORT_TIMEOUT_MS, defaultAnswer: "" }
573
+ )).trim();
574
+ if (answer === "" || answer === "\n") return defaultPort;
575
+ if (answer === "q" || answer === "quit") return defaultPort;
576
+ const parsed = Number.parseInt(answer, 10);
577
+ if (Number.isFinite(parsed) && parsed > 0 && parsed < 65536) {
578
+ return parsed;
579
+ }
580
+ renderer.writeWarning(` Invalid port "${answer}". Use 1\u201365535.
581
+ `);
582
+ }
583
+ return defaultPort;
584
+ }
585
+ async function promptHost(deps, defaultHost) {
586
+ const { renderer, reader } = deps;
587
+ renderer.write(
588
+ ` ${color.dim("\u2139")} Press Enter to bind to ${color.bold(defaultHost)}${defaultHost === HQ_DEFAULT_HOST ? " (all interfaces)" : " (loopback)"}.
589
+ `
590
+ );
591
+ const answer = (await reader.readLine(
592
+ ` ${color.amber("?")} Host ${color.dim(`[${defaultHost}]`)} ${color.dim("(Enter = default, q = cancel)")} `,
593
+ { timeoutMs: PORT_TIMEOUT_MS, defaultAnswer: "" }
594
+ )).trim();
595
+ if (answer === "" || answer === "q" || answer === "quit") return defaultHost;
596
+ return answer;
597
+ }
598
+ function finalize(choice, ports) {
599
+ if (!bindsPort(choice.mode)) return { ...choice, cancelled: false };
600
+ const fallback = ports[choice.mode];
601
+ return {
602
+ ...choice,
603
+ port: typeof choice.port === "number" ? choice.port : fallback,
604
+ host: choice.host ?? defaultHostFor(choice.mode),
605
+ cancelled: false
606
+ };
607
+ }
608
+ function describeMode(mode) {
609
+ switch (mode) {
610
+ case "tui-repl":
611
+ return "TUI / REPL";
612
+ case "webui":
613
+ return "WebUI";
614
+ case "simpleui":
615
+ return "SimpleUI";
616
+ case "hq":
617
+ return "HQ";
618
+ case "desktop":
619
+ return "Desktop";
620
+ default: {
621
+ const exhaustive = mode;
622
+ return String(exhaustive);
623
+ }
624
+ }
625
+ }
626
+ function applyLaunchMenuToArgv(argv, result) {
627
+ if (result.cancelled) return argv;
628
+ const out = [...argv];
629
+ switch (result.mode) {
630
+ case "tui-repl":
631
+ return out;
632
+ case "webui":
633
+ if (typeof result.port === "number") out.push(`--port=${result.port}`);
634
+ if (typeof result.host === "string") out.push(`--host=${result.host}`);
635
+ out.push("--webui");
636
+ return out;
637
+ case "simpleui":
638
+ if (typeof result.port === "number") out.push(`--port=${result.port}`);
639
+ if (typeof result.host === "string") out.push(`--host=${result.host}`);
640
+ out.push("--simpleui");
641
+ return out;
642
+ case "hq":
643
+ if (typeof result.port === "number") out.push(`--port=${result.port}`);
644
+ if (typeof result.host === "string") out.push(`--host=${result.host}`);
645
+ out.push("--hq");
646
+ return out;
647
+ case "desktop":
648
+ out.push("--desktop");
649
+ return out;
650
+ default: {
651
+ const exhaustive = result.mode;
652
+ void exhaustive;
653
+ return out;
654
+ }
655
+ }
656
+ }
657
+ function toPersistedMenuChoice(result) {
658
+ if (result.cancelled) return void 0;
659
+ if (result.mode === "tui-repl") return void 0;
660
+ const port = typeof result.port === "number" && Number.isFinite(result.port) && result.port > 0 && result.port < 65536 ? result.port : void 0;
661
+ return {
662
+ mode: result.mode,
663
+ ...port !== void 0 ? { port } : {},
664
+ ...typeof result.host === "string" && result.host.length > 0 ? { host: result.host } : {}
665
+ };
666
+ }
667
+ async function persistMenuChoice(globalConfigPath, choice) {
668
+ let fileExists = false;
669
+ try {
670
+ await fs.access(globalConfigPath);
671
+ fileExists = true;
672
+ } catch {
673
+ }
674
+ let existing = {};
675
+ try {
676
+ const raw = await fs.readFile(globalConfigPath, "utf8");
677
+ existing = JSON.parse(raw);
678
+ } catch (err) {
679
+ if (fileExists) {
680
+ throw new Error(
681
+ `Refusing to overwrite corrupt config at ${globalConfigPath} (${err.message}). Fix or move the file aside before retrying.`,
682
+ { cause: err }
683
+ );
684
+ }
685
+ existing = {};
686
+ }
687
+ const launch = existing.launch ?? {};
688
+ existing.launch = { ...launch, menuChoice: choice };
689
+ await atomicWrite(globalConfigPath, JSON.stringify(existing, null, 2), {
690
+ mode: 384
691
+ });
692
+ }
693
+
694
+ // src/boot/short-circuit-desktop.ts
695
+ import { spawn } from "node:child_process";
696
+ import { createRequire as createRequire3 } from "node:module";
697
+ import * as path3 from "node:path";
698
+ import { color as color2 } from "@wrongstack/core/utils";
699
+ function stripDesktopLauncherArgs(argv) {
700
+ const out = [];
701
+ for (let i = 0; i < argv.length; i++) {
702
+ const arg = argv[i];
703
+ if (!arg) continue;
704
+ if (arg === "--desktop" || arg.startsWith("--desktop=")) continue;
705
+ if (i === 0 && arg === "desktop") continue;
706
+ out.push(arg);
707
+ }
708
+ return out;
709
+ }
710
+ async function handleDesktopShortCircuit(flags, argv) {
711
+ if (flags["desktop"] !== true) return null;
712
+ return launchDesktop(stripDesktopLauncherArgs(argv));
713
+ }
714
+ async function launchDesktop(args) {
715
+ const req = createRequire3(import.meta.url);
716
+ let launcherPath;
717
+ try {
718
+ const desktopPkgPath = req.resolve("@wrongstack/desktop/package.json");
719
+ launcherPath = path3.join(path3.dirname(desktopPkgPath), "bin", "wrongstack-desktop.js");
720
+ } catch {
721
+ process.stderr.write(
722
+ [
723
+ color2.red("\u2717 WrongStack Desktop is not installed."),
724
+ "",
725
+ "Install the desktop package:",
726
+ " npm install -g @wrongstack/desktop",
727
+ "",
728
+ "The umbrella package also supports desktop when installed with optional dependencies:",
729
+ " npm install -g wrongstack",
730
+ ""
731
+ ].join("\n")
732
+ );
733
+ return 1;
734
+ }
735
+ return await new Promise((resolve2) => {
736
+ const child = spawn(process.execPath, [launcherPath, ...args], {
737
+ stdio: "inherit",
738
+ env: process.env,
739
+ windowsHide: false
740
+ });
741
+ child.once("error", (err) => {
742
+ process.stderr.write(
743
+ `${color2.red("\u2717 Failed to start WrongStack Desktop:")} ${err.message}
744
+ `
745
+ );
746
+ resolve2(1);
747
+ });
748
+ child.once("exit", (code, signal) => {
749
+ if (typeof code === "number") resolve2(code);
750
+ else resolve2(signal ? 1 : 0);
751
+ });
752
+ });
753
+ }
754
+
755
+ // src/boot/short-circuit-hq.ts
756
+ import * as net from "node:net";
757
+ import {
758
+ HQ_CLI_DEFAULT_HOST as HQ_CLI_DEFAULT_HOST2,
759
+ isLoopbackHost,
760
+ readHqRuntimeFileSync,
761
+ resolveHqDataDir
762
+ } from "@wrongstack/core/hq";
763
+ import { color as color3, isPidAlive } from "@wrongstack/core/utils";
764
+ async function isHqAlreadyRunning(dataDir) {
765
+ const marker = readHqRuntimeFileSync(dataDir);
766
+ if (!marker) return null;
767
+ if (marker.pid && isPidAlive(marker.pid)) return marker;
768
+ return null;
769
+ }
770
+ async function isPortInUse(host, port) {
771
+ return new Promise((resolve2) => {
772
+ const server = net.createServer();
773
+ server.once("error", (err) => {
774
+ if (err.code === "EADDRINUSE") {
775
+ resolve2(true);
776
+ } else {
777
+ resolve2(false);
778
+ }
779
+ });
780
+ server.once("listening", () => {
781
+ server.close(() => resolve2(false));
782
+ });
783
+ server.listen(port, host);
784
+ });
785
+ }
786
+ async function handleHqShortCircuit(flags) {
787
+ if (flags["hq"] !== true) return null;
788
+ const { startHqServer } = await import("./hq-server-OARPUSQJ.js");
789
+ const tunnelRequested = flags["tunnel"] === true;
790
+ const host = typeof flags["host"] === "string" ? flags["host"] : tunnelRequested ? "127.0.0.1" : HQ_CLI_DEFAULT_HOST2;
791
+ if (tunnelRequested && !isLoopbackHost(host)) {
792
+ process.stderr.write(
793
+ `${color3.red("\u2717")} --tunnel requires a loopback origin. Remove --host or use --host 127.0.0.1.
794
+ `
795
+ );
796
+ return 1;
797
+ }
798
+ let port;
799
+ if (typeof flags["port"] === "string" && flags["port"].trim() !== "") {
800
+ const parsed = Number.parseInt(flags["port"], 10);
801
+ if (Number.isFinite(parsed) && parsed > 0 && parsed < 65536) {
802
+ port = parsed;
803
+ } else {
804
+ process.stderr.write(`${color3.red("\u2717")} Invalid --port value: ${flags["port"]}
805
+ `);
806
+ return 1;
807
+ }
808
+ } else port = DEFAULT_PORT;
809
+ const dataDir = typeof flags["data-dir"] === "string" ? flags["data-dir"] : void 0;
810
+ const password = typeof flags["password"] === "string" ? flags["password"] : process.env.WRONGSTACK_HQ_PASSWORD;
811
+ if (password !== void 0 && password.length < 8) {
812
+ process.stderr.write(`${color3.red("\u2717")} HQ password must be at least 8 characters.
813
+ `);
814
+ return 1;
815
+ }
816
+ const userProvidedPort = typeof flags["port"] === "string" && flags["port"].trim() !== "" || port !== DEFAULT_PORT;
817
+ const resolvedDataDir = dataDir ?? resolveHqDataDir();
818
+ const existing = await isHqAlreadyRunning(resolvedDataDir);
819
+ if (existing) {
820
+ process.stderr.write(
821
+ `${color3.red("\u2717")} HQ is already running at ${existing.url} (PID ${existing.pid}). Stop it first or use a different --data-dir.
822
+ `
823
+ );
824
+ return 1;
825
+ }
826
+ if (await isPortInUse(host, port)) {
827
+ process.stderr.write(
828
+ `${color3.red("\u2717")} Port ${port} is already in use. Choose a different port or stop the process using it.
829
+ `
830
+ );
831
+ return 1;
832
+ }
833
+ let tokenTtlMs;
834
+ const rawTtl = typeof flags["hq-token-ttl"] === "string" ? flags["hq-token-ttl"] : process.env.WRONGSTACK_HQ_TOKEN_TTL;
835
+ if (rawTtl !== void 0 && rawTtl.length > 0) {
836
+ const { parseTokenTtlValue } = await import("./hq-ttl-KUO3AU7X.js");
837
+ const parsed = parseTokenTtlValue(rawTtl);
838
+ if (parsed.error) {
839
+ process.stderr.write(`${color3.red("\u2717")} Invalid --hq-token-ttl: ${parsed.error}
840
+ `);
841
+ return 1;
842
+ }
843
+ tokenTtlMs = parsed.value;
844
+ }
845
+ let handle;
846
+ try {
847
+ handle = await startHqServer({
848
+ host,
849
+ port,
850
+ strictPort: flags["strict-port"] === true,
851
+ exactPort: userProvidedPort,
852
+ allowInsecureOpen: flags["insecure-open"] === true,
853
+ secureCookies: tunnelRequested,
854
+ requireBrowserAuth: tunnelRequested,
855
+ ...dataDir !== void 0 ? { dataDir } : {},
856
+ ...password !== void 0 ? { password } : {},
857
+ ...tokenTtlMs !== void 0 ? { tokenTtlMs } : {}
858
+ });
859
+ } catch (err) {
860
+ const msg = err instanceof Error ? err.message : String(err);
861
+ if (err instanceof Error && "code" in err && err.code === "EADDRINUSE") {
862
+ process.stderr.write(
863
+ `${color3.red("\u2717")} Port ${port} is already in use. Please choose a different port.
864
+ `
865
+ );
866
+ } else {
867
+ process.stderr.write(`${color3.red("\u2717")} Failed to start HQ server: ${msg}
868
+ `);
869
+ }
870
+ return 1;
871
+ }
872
+ let tunnel;
873
+ let browserUrl = handle.firstRunSetup?.browserUrl ?? `http://${handle.host}:${handle.port}`;
874
+ if (tunnelRequested) {
875
+ if (handle.firstRunSetup !== void 0 && !handle.firstRunSetup.browserTokenMode && !handle.firstRunSetup.passwordMode) {
876
+ await handle.close();
877
+ process.stderr.write(
878
+ `${color3.red("\u2717")} Refusing to publish HQ in open mode. Set --password or create a browser token first.
879
+ `
880
+ );
881
+ return 1;
882
+ }
883
+ try {
884
+ const { buildPublicHqUrl, startHqQuickTunnel } = await import("./hq-tunnel-LO55RCZE.js");
885
+ const originHost = handle.host === "::1" ? "[::1]" : handle.host;
886
+ tunnel = await startHqQuickTunnel(`http://${originHost}:${handle.port}`, {
887
+ onUnexpectedExit: (message) => process.stderr.write(`${color3.yellow("!")} ${message}
888
+ `)
889
+ });
890
+ handle.trustPublicOrigin(new URL(tunnel.url).origin);
891
+ browserUrl = buildPublicHqUrl(
892
+ tunnel.url,
893
+ handle.firstRunSetup?.browserUrl,
894
+ handle.firstRunSetup?.passwordMode !== true
895
+ );
896
+ process.stdout.write(`
897
+ ${color3.green("Cloudflare Quick Tunnel ready:")} ${browserUrl}
898
+ `);
899
+ process.stdout.write(
900
+ `${color3.dim("Temporary development URL; DNS may need a few seconds, it changes on restart and ends when HQ stops.")}
901
+ `
902
+ );
903
+ } catch (err) {
904
+ await handle.close();
905
+ process.stderr.write(
906
+ `${color3.red("\u2717")} Failed to start Cloudflare Quick Tunnel: ${err instanceof Error ? err.message : String(err)}
907
+ `
908
+ );
909
+ return 1;
910
+ }
911
+ }
912
+ if (flags["open"] === true) {
913
+ try {
914
+ const { openBrowser } = await import("@wrongstack/webui-server");
915
+ openBrowser(browserUrl);
916
+ } catch {
917
+ }
918
+ }
919
+ await new Promise((resolve2) => {
920
+ const shutdown = async () => {
921
+ const results = await Promise.allSettled([
922
+ ...tunnel ? [tunnel.close()] : [],
923
+ handle.close()
924
+ ]);
925
+ const failure = results.find(
926
+ (result) => result.status === "rejected"
927
+ );
928
+ if (failure) {
929
+ console.error(
930
+ JSON.stringify({
931
+ level: "error",
932
+ event: "hq.server_close_failed",
933
+ message: String(failure.reason),
934
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
935
+ })
936
+ );
937
+ }
938
+ resolve2();
939
+ };
940
+ process.on("SIGINT", shutdown);
941
+ process.on("SIGTERM", shutdown);
942
+ });
943
+ return 0;
944
+ }
945
+
946
+ // src/boot.ts
947
+ import * as fs7 from "node:fs/promises";
948
+ import { createRequire as createRequire4 } from "node:module";
949
+ import * as os from "node:os";
950
+ import * as path8 from "node:path";
951
+ import { fileURLToPath } from "node:url";
952
+ import { toErrorMessage as toErrorMessage4 } from "@wrongstack/core/utils";
953
+ import { DefaultLogger } from "@wrongstack/core/infrastructure";
954
+ import { TOKENS as TOKENS2 } from "@wrongstack/core/kernel";
955
+ import { DefaultModelsRegistry } from "@wrongstack/core/models";
956
+ import { ToolRegistry } from "@wrongstack/core/registry";
957
+ import { normalizeTokenSavingTier } from "@wrongstack/core/types";
958
+ import { isSetupProvider, SETUP_MODEL_ID, SETUP_PROVIDER_ID } from "@wrongstack/providers";
959
+ import { color as color11, isStdinTTY as isStdinTTY2, writeErr } from "@wrongstack/core/utils";
960
+ import { createDefaultContainer as createDefaultContainer2 } from "@wrongstack/runtime";
961
+ import { registerBuiltinToolTier } from "@wrongstack/tools/tool-tier";
962
+
963
+ // src/boot/config-backup-recovery.ts
964
+ import * as fs2 from "node:fs/promises";
965
+ import * as path4 from "node:path";
966
+ import { atomicWrite as atomicWrite2, backupConfigFile, color as color4, toErrorMessage } from "@wrongstack/core/utils";
967
+ function isRecord(value) {
968
+ return value !== null && typeof value === "object" && !Array.isArray(value);
969
+ }
970
+ function providerIds(value) {
971
+ if (!isRecord(value)) return [];
972
+ const providers = value["providers"];
973
+ return isRecord(providers) ? Object.keys(providers) : [];
974
+ }
975
+ async function defaultProfileProvidersAreEmpty(profilePath) {
976
+ try {
977
+ const parsed = JSON.parse(await fs2.readFile(profilePath, "utf8"));
978
+ return providerIds(parsed).length === 0;
979
+ } catch {
980
+ return false;
981
+ }
982
+ }
983
+ function profileSnapshotTimestamp(fileName, fallback) {
984
+ const match = /^profiles-default-config-(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})\.json$/i.exec(
985
+ fileName
986
+ );
987
+ if (!match) return fallback;
988
+ const [, year, month, day, hour, minute, second, millisecond] = match;
989
+ return Date.UTC(
990
+ Number(year),
991
+ Number(month) - 1,
992
+ Number(day),
993
+ Number(hour),
994
+ Number(minute),
995
+ Number(second),
996
+ Number(millisecond)
997
+ );
998
+ }
999
+ async function findLatestProviderBackup(globalRoot) {
1000
+ const directory = path4.join(globalRoot, "config-history");
1001
+ const candidates = [];
1002
+ let entries;
1003
+ try {
1004
+ entries = await fs2.readdir(directory, { withFileTypes: true });
1005
+ } catch {
1006
+ return void 0;
1007
+ }
1008
+ for (const entry of entries) {
1009
+ if (!entry.isFile() || !/^profiles-default-config-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}\.json$/i.test(entry.name)) {
1010
+ continue;
1011
+ }
1012
+ const filePath = path4.join(directory, entry.name);
1013
+ try {
1014
+ const stat2 = await fs2.stat(filePath);
1015
+ candidates.push({
1016
+ filePath,
1017
+ fileName: entry.name,
1018
+ modifiedAtMs: profileSnapshotTimestamp(entry.name, stat2.mtimeMs)
1019
+ });
1020
+ } catch {
1021
+ }
1022
+ }
1023
+ candidates.sort((a, b) => b.modifiedAtMs - a.modifiedAtMs);
1024
+ for (const candidate of candidates) {
1025
+ try {
1026
+ const raw = await fs2.readFile(candidate.filePath, "utf8");
1027
+ const parsed = JSON.parse(raw);
1028
+ const ids = providerIds(parsed);
1029
+ if (ids.length === 0) continue;
1030
+ return { ...candidate, raw, providerIds: ids };
1031
+ } catch {
1032
+ }
1033
+ }
1034
+ return void 0;
1035
+ }
1036
+ async function restoreDefaultProfileBackup(opts) {
1037
+ await backupConfigFile(opts.profilePath, { globalRoot: opts.globalRoot });
1038
+ await fs2.mkdir(path4.dirname(opts.profilePath), { recursive: true });
1039
+ await atomicWrite2(opts.profilePath, opts.backup.raw, { mode: 384 });
1040
+ }
1041
+ async function maybeRestoreDefaultProfileFromBackup(opts) {
1042
+ if (!await defaultProfileProvidersAreEmpty(opts.profilePath)) return false;
1043
+ const backup = await findLatestProviderBackup(opts.globalRoot);
1044
+ if (!backup) return false;
1045
+ const shownProviders = backup.providerIds.slice(0, 5).join(", ");
1046
+ const remainder = backup.providerIds.length - 5;
1047
+ const providerSummary = remainder > 0 ? `${shownProviders} +${remainder}` : shownProviders;
1048
+ opts.renderer.write(
1049
+ `
1050
+ ${color4.amber("\u25CB")} ${color4.bold("default")} profile has no saved providers.
1051
+ ${color4.dim("Latest usable backup:")} ${color4.bold(backup.fileName)} ${color4.dim(`(${backup.providerIds.length} provider: ${providerSummary})`)}
1052
+ `
1053
+ );
1054
+ const answer = (await opts.reader.readLine(
1055
+ ` ${color4.amber("?")} Restore all settings from this backup? ${color4.dim("[Y/n]")} `
1056
+ )).trim().toLowerCase();
1057
+ if (answer === "n" || answer === "no") return false;
1058
+ try {
1059
+ await restoreDefaultProfileBackup({
1060
+ backup,
1061
+ globalRoot: opts.globalRoot,
1062
+ profilePath: opts.profilePath
1063
+ });
1064
+ opts.renderer.write(
1065
+ ` ${color4.green("\u2713")} Restored all settings from ${color4.dim(backup.fileName)}
1066
+ `
1067
+ );
1068
+ return true;
1069
+ } catch (err) {
1070
+ opts.renderer.writeWarning(`Could not restore config backup: ${toErrorMessage(err)}
1071
+ `);
1072
+ return false;
1073
+ }
1074
+ }
1075
+
1076
+ // src/boot/system-prompt-menu.ts
1077
+ import {
1078
+ countSystemPromptTokens,
1079
+ persistSystemPromptVariant,
1080
+ readSavedSystemPromptVariant,
1081
+ SYSTEM_PROMPT_VARIANT_OPTIONS,
1082
+ SYSTEM_PROMPT_VARIANTS
1083
+ } from "@wrongstack/core/agent";
1084
+ import { color as color8 } from "@wrongstack/core/utils";
1085
+
1086
+ // src/pre-launch/first-run.ts
1087
+ import { hasProviderCredential } from "@wrongstack/core/models";
1088
+ import { color as color5, toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
1089
+ import { PROVIDER_DEFINITIONS } from "@wrongstack/providers/definitions";
1090
+ async function hasAnyCredential(config, modelsRegistry, env = process.env) {
1091
+ for (const cfg of Object.values(config.providers ?? {})) {
1092
+ if (!cfg) continue;
1093
+ if (resolveActiveApiKey(cfg) !== void 0) return true;
1094
+ if (isKeylessLocalProvider({ apiBase: cfg.baseUrl, envVars: cfg.envVars })) return true;
1095
+ if ((cfg.envVars ?? []).some((name) => !!env[name])) return true;
1096
+ }
1097
+ for (const definition of Object.values(PROVIDER_DEFINITIONS)) {
1098
+ if (definition.envVars.some((name) => !!env[name])) return true;
1099
+ }
1100
+ try {
1101
+ const catalog = await modelsRegistry.listProviders();
1102
+ for (const provider of catalog) {
1103
+ if (hasProviderCredential(provider, config, env)) return true;
1104
+ }
1105
+ } catch {
1106
+ }
1107
+ return false;
1108
+ }
1109
+ function renderWelcome(renderer) {
1110
+ renderer.write("\n");
1111
+ renderer.write(`${boxTop("Welcome to WrongStack")}
1112
+ `);
1113
+ renderer.write(
1114
+ `${boxRow(color5.dim("No model is connected yet. Pick how you want to connect one:"))}
1115
+ `
1116
+ );
1117
+ renderer.write(`${boxDivider()}
1118
+ `);
1119
+ renderer.write(
1120
+ `${boxRow(
1121
+ `${theme.accent("1")} ${color5.bold("Sign in with a subscription")} ${color5.dim("ChatGPT \xB7 Claude \xB7 GitHub Copilot")}`
1122
+ )}
1123
+ `
1124
+ );
1125
+ renderer.write(
1126
+ `${boxRow(
1127
+ `${theme.accent("2")} ${color5.bold("Add an API key")} ${color5.dim("Anthropic \xB7 OpenAI \xB7 Google \xB7 OpenRouter \xB7 \u2026")}`
1128
+ )}
1129
+ `
1130
+ );
1131
+ renderer.write(
1132
+ `${boxRow(
1133
+ `${theme.accent("3")} ${color5.bold("Use a local server")} ${color5.dim("Ollama \xB7 LM Studio \xB7 vLLM \xB7 OmniRoute")}`
1134
+ )}
1135
+ `
1136
+ );
1137
+ renderer.write(
1138
+ `${boxRow(
1139
+ `${theme.accent("4")} ${color5.bold("Continue without a key")} ${color5.dim("setup mode \u2014 explore the app, no model")}`
1140
+ )}
1141
+ `
1142
+ );
1143
+ renderer.write(`${boxDivider()}
1144
+ `);
1145
+ renderer.write(
1146
+ `${boxRow(color5.dim("You can change this any time with /auth inside the app, or `wstack auth`."))}
1147
+ `
1148
+ );
1149
+ renderer.write(`${boxBottom()}
1150
+ `);
1151
+ }
1152
+ async function runFirstRunSetup(deps) {
1153
+ const { renderer, reader } = deps;
1154
+ const authDeps = {
1155
+ renderer,
1156
+ reader,
1157
+ modelsRegistry: deps.modelsRegistry,
1158
+ vault: deps.vault,
1159
+ profileConfigPath: deps.profileConfigPath
1160
+ };
1161
+ for (; ; ) {
1162
+ renderWelcome(renderer);
1163
+ const answer = (await reader.readLine(
1164
+ `
1165
+ ${color5.amber("?")} Choose ${color5.dim("(1-4)")} ${color5.dim("[q to quit]")}: `
1166
+ )).trim().toLowerCase();
1167
+ if (!answer || answer === "q" || answer === "quit" || answer === "exit") {
1168
+ renderer.write(
1169
+ `
1170
+ ${color5.dim("No provider configured. Run")} ${color5.bold("wstack auth")} ${color5.dim("when you are ready.")}
1171
+
1172
+ `
1173
+ );
1174
+ return { kind: "quit" };
1175
+ }
1176
+ if (answer === "4" || answer === "setup" || answer === "skip") {
1177
+ return { kind: "setup-mode" };
1178
+ }
1179
+ const flow = answer === "1" || answer === "signin" || answer === "login" ? () => runOAuthLoginMenu(authDeps) : answer === "2" || answer === "key" || answer === "api" ? () => addFromCatalog(authDeps) : answer === "3" || answer === "local" ? () => runAuthLocal(authDeps) : void 0;
1180
+ if (!flow) {
1181
+ renderer.writeError(`Unknown selection: "${answer}"`);
1182
+ continue;
1183
+ }
1184
+ try {
1185
+ await flow();
1186
+ } catch (err) {
1187
+ renderer.writeWarning(`Setup step did not complete: ${toErrorMessage2(err)}
1188
+ `);
1189
+ }
1190
+ let updated;
1191
+ try {
1192
+ updated = await deps.reloadConfig();
1193
+ } catch (err) {
1194
+ renderer.writeWarning(`Could not re-read config: ${toErrorMessage2(err)}
1195
+ `);
1196
+ continue;
1197
+ }
1198
+ if (await hasAnyCredential(updated, deps.modelsRegistry)) {
1199
+ return { kind: "configured" };
1200
+ }
1201
+ renderer.writeWarning("Still no usable credential \u2014 pick another option.\n");
1202
+ }
1203
+ }
1204
+
1205
+ // src/pre-launch/launch-prompts.ts
1206
+ import * as fs3 from "node:fs/promises";
1207
+ import { atomicWrite as atomicWrite3, color as color6 } from "@wrongstack/core/utils";
1208
+ var LaunchAbortedError = class extends Error {
1209
+ exitCode = 0;
1210
+ constructor() {
1211
+ super("Launch cancelled by user");
1212
+ this.name = "LaunchAbortedError";
1213
+ }
1214
+ };
1215
+ async function runLaunchPrompts(opts) {
1216
+ const { renderer, reader, modePinned, yoloPinned, autonomyPinned, lastChoices } = opts;
1217
+ if (modePinned !== void 0 && yoloPinned !== void 0 && autonomyPinned !== void 0) {
1218
+ return {
1219
+ mode: modePinned,
1220
+ yolo: yoloPinned,
1221
+ autonomy: autonomyPinned
1222
+ };
1223
+ }
1224
+ if (!lastChoices) {
1225
+ return {
1226
+ mode: modePinned ?? "tui",
1227
+ yolo: yoloPinned ?? true,
1228
+ autonomy: autonomyPinned ?? "auto"
1229
+ };
1230
+ }
1231
+ const hasOverride = modePinned !== void 0 && modePinned !== lastChoices.mode || yoloPinned !== void 0 && yoloPinned !== lastChoices.yolo || autonomyPinned !== void 0 && autonomyPinned !== lastChoices.autonomy;
1232
+ if (lastChoices && !hasOverride) {
1233
+ const effective = {
1234
+ mode: modePinned ?? lastChoices.mode,
1235
+ yolo: yoloPinned ?? lastChoices.yolo,
1236
+ autonomy: autonomyPinned ?? lastChoices.autonomy
1237
+ };
1238
+ const onOff = (v) => v ? color6.green("on") : color6.dim("off");
1239
+ const modeLabel = effective.mode.toUpperCase();
1240
+ renderer.write(
1241
+ `
1242
+ ${color6.dim("Last settings:")} ${color6.bold(modeLabel)} \xB7 YOLO ${onOff(effective.yolo)} \xB7 Autonomy ${effective.autonomy === "auto" ? color6.green("auto") : color6.dim("off")}
1243
+ `
1244
+ );
1245
+ const answer = (await reader.readLine(
1246
+ ` ${color6.amber("?")} Continue with these? ${color6.dim("[Y/n/q]")} ${color6.dim("(auto Y in 5s)")} `,
1247
+ { timeoutMs: 5e3, defaultAnswer: "y" }
1248
+ )).trim().toLowerCase();
1249
+ if (answer === "q") {
1250
+ renderer.write(color6.dim(" Goodbye!\n"));
1251
+ throw new LaunchAbortedError();
1252
+ }
1253
+ if (answer !== "n" && answer !== "no") {
1254
+ const badges2 = buildBadges(effective);
1255
+ const badgeStr2 = badges2.length > 0 ? ` (${badges2.join(" \xB7 ")})` : "";
1256
+ renderer.write(
1257
+ `
1258
+ ${color6.green("\u25B6")} Launching in ${color6.bold(modeLabel)} mode${badgeStr2}
1259
+
1260
+ `
1261
+ );
1262
+ return effective;
1263
+ }
1264
+ }
1265
+ let mode;
1266
+ if (modePinned) {
1267
+ mode = modePinned;
1268
+ } else {
1269
+ const answer = (await reader.readLine(
1270
+ `
1271
+ ${color6.amber("?")} Interactive mode: ${color6.bold("T")}UI / ${color6.bold("R")}EPL ${color6.dim("[T/r/q]")} `
1272
+ )).trim().toLowerCase();
1273
+ if (answer === "q") {
1274
+ renderer.write(color6.dim(" Goodbye!\n"));
1275
+ throw new LaunchAbortedError();
1276
+ }
1277
+ mode = answer === "r" || answer === "repl" ? "repl" : "tui";
1278
+ }
1279
+ let yolo;
1280
+ if (yoloPinned !== void 0 && yoloPinned === lastChoices?.yolo) {
1281
+ yolo = yoloPinned;
1282
+ } else {
1283
+ const answer = (await reader.readLine(
1284
+ ` ${color6.amber("?")} YOLO mode ${color6.dim("(auto-approve tool calls)")} ${color6.dim("[Y/n/q]")} `
1285
+ )).trim().toLowerCase();
1286
+ if (answer === "q") {
1287
+ renderer.write(color6.dim(" Goodbye!\n"));
1288
+ throw new LaunchAbortedError();
1289
+ }
1290
+ yolo = answer !== "n" && answer !== "no";
1291
+ }
1292
+ let autonomy;
1293
+ if (autonomyPinned !== void 0 && autonomyPinned === lastChoices?.autonomy) {
1294
+ autonomy = autonomyPinned;
1295
+ } else {
1296
+ const answer = (await reader.readLine(
1297
+ ` ${color6.amber("?")} Autonomy mode ${color6.dim("(auto-continue \u2014 agent picks next step)")} ${color6.dim("[Y/n/q]")} `
1298
+ )).trim().toLowerCase();
1299
+ if (answer === "q") {
1300
+ renderer.write(color6.dim(" Goodbye!\n"));
1301
+ throw new LaunchAbortedError();
1302
+ }
1303
+ autonomy = answer !== "n" && answer !== "no" ? "auto" : "off";
1304
+ }
1305
+ const badges = buildBadges({ mode, yolo, autonomy });
1306
+ const badgeStr = badges.length > 0 ? ` (${badges.join(" \xB7 ")})` : "";
1307
+ renderer.write(
1308
+ `
1309
+ ${color6.green("\u25B6")} Launching in ${color6.bold(mode.toUpperCase())} mode${badgeStr}
1310
+
1311
+ `
1312
+ );
1313
+ return { mode, yolo, autonomy };
1314
+ }
1315
+ function buildBadges(chosen) {
1316
+ const badges = [];
1317
+ if (chosen.yolo) badges.push(color6.yellow("YOLO"));
1318
+ if (chosen.autonomy !== "off")
1319
+ badges.push(color6.magenta(`AUTONOMY:${chosen.autonomy.toUpperCase()}`));
1320
+ return badges;
1321
+ }
1322
+ async function persistLaunchChoices(configPath, choices) {
1323
+ let fileExists = false;
1324
+ try {
1325
+ await fs3.access(configPath);
1326
+ fileExists = true;
1327
+ } catch {
1328
+ }
1329
+ let existing = {};
1330
+ try {
1331
+ const raw = await fs3.readFile(configPath, "utf8");
1332
+ existing = JSON.parse(raw);
1333
+ } catch (err) {
1334
+ if (fileExists) {
1335
+ throw new Error(
1336
+ `Refusing to overwrite corrupt config at ${configPath} (${err.message}). Fix or move the file aside before retrying.`,
1337
+ { cause: err }
1338
+ );
1339
+ }
1340
+ existing = {};
1341
+ }
1342
+ existing.yolo = choices.yolo;
1343
+ existing.launch = {
1344
+ mode: choices.mode,
1345
+ autonomy: choices.autonomy
1346
+ };
1347
+ await atomicWrite3(configPath, JSON.stringify(existing, null, 2), { mode: 384 });
1348
+ }
1349
+
1350
+ // src/pre-launch/project-check.ts
1351
+ import * as fs4 from "node:fs/promises";
1352
+ import * as path5 from "node:path";
1353
+ import { color as color7, toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils";
1354
+ var MANIFESTS = [
1355
+ "package.json",
1356
+ "pyproject.toml",
1357
+ "Cargo.toml",
1358
+ "go.mod",
1359
+ "Makefile",
1360
+ "pom.xml",
1361
+ "build.gradle",
1362
+ "build.gradle.kts",
1363
+ "composer.json",
1364
+ "Gemfile"
1365
+ ];
1366
+ async function detectProjectKind(projectRoot) {
1367
+ try {
1368
+ await fs4.access(path5.join(projectRoot, ".wrongstack", "AGENTS.md"));
1369
+ return "initialized";
1370
+ } catch {
1371
+ }
1372
+ for (const m of MANIFESTS) {
1373
+ try {
1374
+ await fs4.access(path5.join(projectRoot, m));
1375
+ return "project";
1376
+ } catch {
1377
+ }
1378
+ }
1379
+ return "empty";
1380
+ }
1381
+ async function scaffoldAgentsMd(projectRoot) {
1382
+ const dir = path5.join(projectRoot, ".wrongstack");
1383
+ const file = path5.join(dir, "AGENTS.md");
1384
+ const facts = await detectProjectFacts(projectRoot);
1385
+ const body = renderAgentsTemplate(facts);
1386
+ await fs4.mkdir(dir, { recursive: true });
1387
+ await fs4.writeFile(file, body, "utf8");
1388
+ return file;
1389
+ }
1390
+ async function runProjectCheck(opts) {
1391
+ const { projectRoot, cwd, renderer, reader } = opts;
1392
+ const kind = await detectProjectKind(projectRoot);
1393
+ if (kind === "initialized") {
1394
+ renderer.write(
1395
+ `
1396
+ ${color7.green("\u2713")} Project initialized ${color7.dim(`(${path5.join(projectRoot, ".wrongstack", "AGENTS.md")})`)}
1397
+ `
1398
+ );
1399
+ return true;
1400
+ }
1401
+ if (kind === "project") {
1402
+ renderer.write(
1403
+ `
1404
+ ${color7.amber("\u25CF")} Project detected ${color7.dim(`(${projectRoot})`)} but ${color7.bold(".wrongstack/AGENTS.md")} is missing.
1405
+ `
1406
+ );
1407
+ const answer2 = (await reader.readLine(
1408
+ ` ${color7.amber("?")} Scaffold ${color7.bold("AGENTS.md")} now? ${color7.dim("[y/N/q]")} `
1409
+ )).trim().toLowerCase();
1410
+ if (answer2 === "q") {
1411
+ renderer.write(color7.dim(" Cancelled.\n"));
1412
+ return false;
1413
+ }
1414
+ if (answer2 === "y" || answer2 === "yes") {
1415
+ try {
1416
+ const file = await scaffoldAgentsMd(projectRoot);
1417
+ renderer.write(` ${color7.green("\u2713")} Wrote ${color7.dim(file)}
1418
+ `);
1419
+ } catch (err) {
1420
+ renderer.writeError(`Failed to scaffold AGENTS.md: ${toErrorMessage3(err)}`);
1421
+ }
1422
+ }
1423
+ return true;
1424
+ }
1425
+ const gitDir = path5.join(projectRoot, ".git");
1426
+ let hasGit = false;
1427
+ try {
1428
+ await fs4.access(gitDir);
1429
+ hasGit = true;
1430
+ } catch {
1431
+ }
1432
+ if (!hasGit) {
1433
+ renderer.write(
1434
+ `
1435
+ ${color7.dim("\u25CB")} ${color7.dim(`No project manifest in ${projectRoot} \u2014 running in a scratch directory.`)}
1436
+ `
1437
+ );
1438
+ const answer2 = (await reader.readLine(
1439
+ ` ${color7.amber("?")} No git repo found. ${color7.bold("Initialize git?")} ${color7.dim("[y/N/q]")} `
1440
+ )).trim().toLowerCase();
1441
+ if (answer2 === "q") {
1442
+ renderer.write(color7.dim(" Cancelled.\n"));
1443
+ return false;
1444
+ }
1445
+ if (answer2 === "y" || answer2 === "yes") {
1446
+ try {
1447
+ const { spawn: spawn2 } = await import("node:child_process");
1448
+ await new Promise((resolve2, reject) => {
1449
+ const child = spawn2("git", ["init"], {
1450
+ cwd,
1451
+ signal: AbortSignal.timeout(1e4),
1452
+ windowsHide: true
1453
+ });
1454
+ child.on("error", reject);
1455
+ child.on(
1456
+ "close",
1457
+ (code) => code === 0 ? resolve2() : reject(new Error(`git init failed with ${code}`))
1458
+ );
1459
+ });
1460
+ renderer.write(` ${color7.green("\u2713")} Git repository initialized
1461
+ `);
1462
+ } catch (err) {
1463
+ renderer.writeError(`git init failed: ${toErrorMessage3(err)}
1464
+ `);
1465
+ }
1466
+ }
1467
+ } else {
1468
+ renderer.write(
1469
+ `
1470
+ ${color7.dim("\u25CB")} ${color7.dim(`No project manifest in ${projectRoot} \u2014 running in a scratch directory.`)}
1471
+ `
1472
+ );
1473
+ }
1474
+ const answer = (await reader.readLine(` ${color7.amber("?")} Continue anyway? ${color7.dim("[Y/n/q]")} `)).trim().toLowerCase();
1475
+ if (answer === "q" || answer === "n" || answer === "no") {
1476
+ renderer.write(color7.dim(" Cancelled.\n"));
1477
+ return false;
1478
+ }
1479
+ return true;
1480
+ }
1481
+
1482
+ // src/boot/system-prompt-menu.ts
1483
+ var SYSTEM_PROMPT_OPTIONS = SYSTEM_PROMPT_VARIANT_OPTIONS;
1484
+ var SUMMARY_TIMEOUT_MS2 = 5e3;
1485
+ function flagOn(value) {
1486
+ if (value === true) return true;
1487
+ if (typeof value !== "string") return false;
1488
+ const normalized = value.trim().toLowerCase();
1489
+ return normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on";
1490
+ }
1491
+ function isSystemPromptPinned(flags) {
1492
+ return flagOn(flags["system-pro"]) || flagOn(flags["system-lite"]) || typeof flags["system-prompt"] === "string";
1493
+ }
1494
+ function shouldSkipSystemPromptMenu(flags) {
1495
+ if (isSystemPromptPinned(flags)) return true;
1496
+ if (flags["no-menu"] === true) return true;
1497
+ if (flags["no-interactive"] === true) return true;
1498
+ if (flags["skip"] === true) return true;
1499
+ if (flags["webui"] === true) return true;
1500
+ return false;
1501
+ }
1502
+ function labelFor(variant) {
1503
+ return SYSTEM_PROMPT_OPTIONS.find((o) => o.variant === variant)?.label ?? variant;
1504
+ }
1505
+ async function runSystemPromptMenu(deps) {
1506
+ const { renderer, reader, paths, lastVariant } = deps;
1507
+ const tokens = await countSystemPromptTokens(paths);
1508
+ const defaultVariant = lastVariant ?? "default";
1509
+ if (lastVariant) {
1510
+ const answer2 = (await reader.readLine(
1511
+ ` ${color8.amber("?")} Continue with ${color8.bold(labelFor(lastVariant))} system prompt ${color8.dim(`(~${fmtTok(tokens[lastVariant])} tokens)`)}? ${color8.dim("[Y/n/q]")} ${color8.dim("(auto Y in 5s)")} `,
1512
+ { timeoutMs: SUMMARY_TIMEOUT_MS2, defaultAnswer: "y" }
1513
+ )).trim().toLowerCase();
1514
+ if (answer2 === "q" || answer2 === "quit") throw new LaunchAbortedError();
1515
+ if (answer2 !== "n" && answer2 !== "no") return lastVariant;
1516
+ }
1517
+ renderer.write(`
1518
+ ${color8.amber("?")} System prompt:
1519
+ `);
1520
+ for (const [index, opt] of SYSTEM_PROMPT_OPTIONS.entries()) {
1521
+ const num2 = index + 1;
1522
+ const isDefault = opt.variant === defaultVariant;
1523
+ const marker = isDefault ? color8.cyan("\u25CF") : color8.dim("\u25CB");
1524
+ const label = isDefault ? color8.bold(opt.label) : opt.label;
1525
+ renderer.write(
1526
+ ` ${color8.dim(`${num2}.`)} ${marker} ${label.padEnd(9)} ${color8.dim(`~${fmtTok(tokens[opt.variant])} tokens`)} ${color8.dim(opt.hint)}
1527
+ `
1528
+ );
1529
+ }
1530
+ const answer = (await reader.readLine(
1531
+ `
1532
+ ${color8.amber("?")} Select system prompt ${color8.dim(`[1-${SYSTEM_PROMPT_OPTIONS.length}, Enter = ${labelFor(defaultVariant)}, q to quit]`)}: `
1533
+ )).trim().toLowerCase();
1534
+ if (answer === "q" || answer === "quit") throw new LaunchAbortedError();
1535
+ if (!answer) return defaultVariant;
1536
+ const num = Number.parseInt(answer, 10);
1537
+ if (!Number.isNaN(num) && num >= 1 && num <= SYSTEM_PROMPT_OPTIONS.length) {
1538
+ return SYSTEM_PROMPT_OPTIONS[num - 1].variant;
1539
+ }
1540
+ const byName = SYSTEM_PROMPT_OPTIONS.find(
1541
+ (o) => o.label.toLowerCase() === answer || o.variant === answer
1542
+ );
1543
+ if (byName) return byName.variant;
1544
+ return defaultVariant;
1545
+ }
1546
+ async function maybeRunSystemPromptMenu(opts) {
1547
+ if (!opts.isInteractiveTTY) return { aborted: false, changed: false };
1548
+ if (shouldSkipSystemPromptMenu(opts.flags)) return { aborted: false, changed: false };
1549
+ let savedVariant;
1550
+ try {
1551
+ savedVariant = await readSavedSystemPromptVariant(opts.profileConfigPath);
1552
+ const chosen = await runSystemPromptMenu({
1553
+ renderer: opts.renderer,
1554
+ reader: opts.reader,
1555
+ lastVariant: savedVariant,
1556
+ paths: opts.paths
1557
+ });
1558
+ if (chosen === savedVariant) return { variant: chosen, aborted: false, changed: false };
1559
+ try {
1560
+ await persistSystemPromptVariant(opts.profileConfigPath, chosen);
1561
+ } catch (err) {
1562
+ return { variant: chosen, aborted: false, changed: true, persistError: err };
1563
+ }
1564
+ return { variant: chosen, aborted: false, changed: true };
1565
+ } catch (err) {
1566
+ if (err instanceof LaunchAbortedError) return { aborted: true, changed: false };
1567
+ throw err;
1568
+ }
1569
+ }
1570
+
1571
+ // src/boot-config.ts
1572
+ import { bootConfig as coreBootConfig } from "@wrongstack/core/infrastructure";
1573
+ async function bootConfig(flags) {
1574
+ const isWebui = !!flags["webui"];
1575
+ const opts = {
1576
+ flags,
1577
+ appLabel: "wstack",
1578
+ ...isWebui ? { skipIdentityValidation: true } : {}
1579
+ };
1580
+ const { cwd, projectRoot, userHome, wpaths, pathResolver, config, vault } = await coreBootConfig(opts);
1581
+ return {
1582
+ paths: { cwd, projectRoot, userHome, wpaths, pathResolver },
1583
+ config,
1584
+ vault
1585
+ };
1586
+ }
1587
+
1588
+ // src/input-reader.ts
1589
+ import * as fs5 from "node:fs/promises";
1590
+ import * as path6 from "node:path";
1591
+ import * as readline from "node:readline";
1592
+ import {
1593
+ resolveWstackPaths,
1594
+ setOutputLineGuard,
1595
+ setRawMode as setRawMode2,
1596
+ writeOut
1597
+ } from "@wrongstack/core/utils";
1598
+ var ReadlineInputReader = class {
1599
+ rl;
1600
+ historyFile;
1601
+ history = [];
1602
+ pending = false;
1603
+ suppressNextHistory = false;
1604
+ constructor(opts = {}) {
1605
+ this.historyFile = opts.historyFile ?? resolveWstackPaths({ projectRoot: process.cwd() }).historyFile;
1606
+ }
1607
+ async loadHistory() {
1608
+ try {
1609
+ const raw = await fs5.readFile(this.historyFile, "utf8");
1610
+ this.history = raw.split("\n").filter(Boolean).slice(-1e3);
1611
+ } catch {
1612
+ this.history = [];
1613
+ }
1614
+ }
1615
+ async saveHistory() {
1616
+ try {
1617
+ await fs5.mkdir(path6.dirname(this.historyFile), { recursive: true });
1618
+ await fs5.writeFile(this.historyFile, this.history.slice(-1e3).join("\n"));
1619
+ } catch {
1620
+ }
1621
+ }
1622
+ shouldPersistHistory(line) {
1623
+ return !/^\s*\/(?:telegram-setup|tg-setup)\s+\d+:[A-Za-z0-9_-]+(?:\s|$)/i.test(line);
1624
+ }
1625
+ ensure() {
1626
+ if (!this.rl) {
1627
+ this.rl = readline.createInterface({
1628
+ input: process.stdin,
1629
+ output: process.stdout,
1630
+ history: this.history,
1631
+ terminal: process.stdin.isTTY
1632
+ });
1633
+ }
1634
+ return this.rl;
1635
+ }
1636
+ async readLine(prompt, timeoutOpts) {
1637
+ if (this.history.length === 0) await this.loadHistory();
1638
+ while (this.pending) {
1639
+ await new Promise((resolve2) => setTimeout(resolve2, 50));
1640
+ }
1641
+ this.pending = true;
1642
+ try {
1643
+ if (this.rl) {
1644
+ const old = this.rl;
1645
+ this.rl = void 0;
1646
+ await new Promise((resolve2) => {
1647
+ if (old.closed) {
1648
+ resolve2();
1649
+ } else {
1650
+ old.once("close", resolve2);
1651
+ old.close();
1652
+ }
1653
+ });
1654
+ }
1655
+ const fresh = this.ensure();
1656
+ this.installPromptGuard(fresh);
1657
+ const line = await new Promise((resolve2) => {
1658
+ let settled = false;
1659
+ let timeoutHandle;
1660
+ const settle = (line2) => {
1661
+ if (settled) return;
1662
+ settled = true;
1663
+ if (timeoutHandle) clearTimeout(timeoutHandle);
1664
+ setOutputLineGuard(null);
1665
+ resolve2(line2);
1666
+ };
1667
+ if (timeoutOpts) {
1668
+ timeoutHandle = setTimeout(() => {
1669
+ if (!settled) {
1670
+ fresh.write(`${timeoutOpts.defaultAnswer}
1671
+ `);
1672
+ }
1673
+ }, timeoutOpts.timeoutMs);
1674
+ }
1675
+ fresh.question(prompt ?? "> ", (line2) => {
1676
+ const persistHistory = !this.suppressNextHistory && this.shouldPersistHistory(line2);
1677
+ this.suppressNextHistory = false;
1678
+ if (line2.trim() && persistHistory) {
1679
+ this.history.push(line2);
1680
+ this.saveHistory().then(void 0, () => {
1681
+ });
1682
+ }
1683
+ settle(line2);
1684
+ });
1685
+ fresh.once("close", () => settle(""));
1686
+ fresh.on?.("error", (_e) => settle(""));
1687
+ }).then((result) => {
1688
+ if (this.rl === fresh) this.rl = void 0;
1689
+ fresh.close();
1690
+ return result;
1691
+ });
1692
+ return line;
1693
+ } finally {
1694
+ this.pending = false;
1695
+ }
1696
+ }
1697
+ /**
1698
+ * Install the out-of-band write guard for the active prompt. When a log
1699
+ * line or other async output lands while the user is mid-type, the guard
1700
+ * clears the draft row, lets the message print, then repaints the prompt
1701
+ * and the in-progress draft (cursor preserved) via readline's own
1702
+ * refresh. Without it, each async write leaves the half-typed line
1703
+ * stranded as a fresh scrollback row.
1704
+ *
1705
+ * No-op on non-TTY output (piped/redirected) — there's no draft to
1706
+ * protect and the ANSI clear/repaint would be noise in a file.
1707
+ */
1708
+ installPromptGuard(rl) {
1709
+ const out = process.stdout;
1710
+ if (!out.isTTY) {
1711
+ setOutputLineGuard(null);
1712
+ return;
1713
+ }
1714
+ setOutputLineGuard({
1715
+ suspend() {
1716
+ readline.cursorTo(out, 0);
1717
+ readline.clearLine(out, 0);
1718
+ },
1719
+ resume() {
1720
+ try {
1721
+ rl.prompt(true);
1722
+ } catch {
1723
+ }
1724
+ }
1725
+ });
1726
+ }
1727
+ async readKey(prompt, options) {
1728
+ setOutputLineGuard(null);
1729
+ writeOut(prompt);
1730
+ return new Promise((resolve2) => {
1731
+ const stdin = process.stdin;
1732
+ const wasRaw = stdin.isRaw;
1733
+ const wasPaused = stdin.isPaused();
1734
+ setRawMode2(stdin, true);
1735
+ stdin.resume();
1736
+ const onData = (buf) => {
1737
+ const key = buf.toString();
1738
+ if (key === "") {
1739
+ cleanup();
1740
+ writeOut("\n");
1741
+ resolve2("");
1742
+ return;
1743
+ }
1744
+ const opt = options.find(
1745
+ (o) => o.key.toLowerCase() === key.toLowerCase() || o.value === key
1746
+ );
1747
+ if (opt) {
1748
+ cleanup();
1749
+ writeOut(`${opt.key}
1750
+ `);
1751
+ resolve2(opt.value);
1752
+ }
1753
+ };
1754
+ const onClose = () => {
1755
+ cleanup();
1756
+ resolve2("");
1757
+ };
1758
+ const cleanup = () => {
1759
+ stdin.off("data", onData);
1760
+ stdin.off("close", onClose);
1761
+ setRawMode2(stdin, wasRaw);
1762
+ if (wasPaused) stdin.pause();
1763
+ };
1764
+ stdin.on("data", onData);
1765
+ stdin.on("close", onClose);
1766
+ });
1767
+ }
1768
+ /**
1769
+ * Read a line of input while masking each character with a bullet so the
1770
+ * user gets visual confirmation that bytes are arriving (especially on
1771
+ * paste, which previously felt like nothing happened). Pasted chunks
1772
+ * are echoed as a run of bullets, Backspace/DEL erases one bullet, and
1773
+ * Ctrl+U / Ctrl+T are honored. Non-TTY input is read normally — there's
1774
+ * nothing to hide when piped, and echoing bullets to a file is noise.
1775
+ *
1776
+ * Returns the raw entered string (no trim — caller decides).
1777
+ */
1778
+ async readSecret(prompt) {
1779
+ const stdin = process.stdin;
1780
+ if (!stdin.isTTY) {
1781
+ this.suppressNextHistory = true;
1782
+ return this.readLine(prompt);
1783
+ }
1784
+ setOutputLineGuard(null);
1785
+ this.rl?.close();
1786
+ this.rl = void 0;
1787
+ writeOut(prompt);
1788
+ return new Promise((resolve2) => {
1789
+ let buf = "";
1790
+ const wasRaw = stdin.isRaw;
1791
+ const cleanup = () => {
1792
+ stdin.off("data", onData);
1793
+ setRawMode2(stdin, wasRaw);
1794
+ stdin.pause();
1795
+ };
1796
+ try {
1797
+ setRawMode2(stdin, true);
1798
+ stdin.resume();
1799
+ stdin.setEncoding("utf8");
1800
+ } catch (setupErr) {
1801
+ setRawMode2(stdin, wasRaw);
1802
+ stdin.pause();
1803
+ throw setupErr;
1804
+ }
1805
+ const eraseChar = () => {
1806
+ writeOut("\b \b");
1807
+ };
1808
+ const eraseAll = () => {
1809
+ for (let i = 0; i < buf.length; i++) eraseChar();
1810
+ };
1811
+ const onData = (chunk) => {
1812
+ for (const ch of chunk) {
1813
+ if (ch === "\r" || ch === "\n") {
1814
+ cleanup();
1815
+ writeOut(` ${dim(`[${buf.length} chars]`)}
1816
+ `);
1817
+ resolve2(buf);
1818
+ return;
1819
+ }
1820
+ if (ch === "") {
1821
+ cleanup();
1822
+ writeOut("\n");
1823
+ resolve2("");
1824
+ return;
1825
+ }
1826
+ if (ch === "") {
1827
+ eraseAll();
1828
+ buf = "";
1829
+ continue;
1830
+ }
1831
+ if (ch === "") {
1832
+ const m = buf.match(/(\S+\s*)$/);
1833
+ const drop = m ? m[0].length : buf.length;
1834
+ for (let i = 0; i < drop; i++) eraseChar();
1835
+ buf = buf.slice(0, buf.length - drop);
1836
+ continue;
1837
+ }
1838
+ if (ch === "\x7F" || ch === "\b") {
1839
+ if (buf.length > 0) {
1840
+ buf = buf.slice(0, -1);
1841
+ eraseChar();
1842
+ }
1843
+ continue;
1844
+ }
1845
+ if (ch < " ") continue;
1846
+ buf += ch;
1847
+ writeOut("\u2022");
1848
+ }
1849
+ };
1850
+ stdin.on("data", onData);
1851
+ });
1852
+ }
1853
+ async close() {
1854
+ setOutputLineGuard(null);
1855
+ await this.saveHistory();
1856
+ this.rl?.close();
1857
+ this.rl = void 0;
1858
+ }
1859
+ };
1860
+ function dim(s) {
1861
+ if (!process.stdout.isTTY) return s;
1862
+ return `\x1B[2m${s}\x1B[22m`;
1863
+ }
1864
+
1865
+ // src/launch-hints.ts
1866
+ import * as fs6 from "node:fs/promises";
1867
+ import * as path7 from "node:path";
1868
+ import { color as color9 } from "@wrongstack/core/utils";
1869
+ var GROUPS = [
1870
+ {
1871
+ title: "Autonomy",
1872
+ items: [
1873
+ { key: "/goal <text>", blurb: "lock in a verifiable mission \u2014 only Esc / Ctrl+C interrupt" },
1874
+ {
1875
+ key: "/autonomy eternal",
1876
+ blurb: "sense \u2192 decide \u2192 execute \u2192 reflect loop until you stop it"
1877
+ },
1878
+ {
1879
+ key: '--eternal "<mission>"',
1880
+ blurb: "boot directly into the eternal-autonomy engine with a mission"
1881
+ },
1882
+ {
1883
+ key: "/autonomy on|suggest",
1884
+ blurb: "self-driving: auto-pick next step or just suggest it"
1885
+ }
1886
+ ]
1887
+ },
1888
+ {
1889
+ title: "Agents & fleet",
1890
+ items: [
1891
+ {
1892
+ key: "/delegate [--role=<role>] <task>",
1893
+ blurb: "route work to a specialist; omit --role for automatic selection"
1894
+ },
1895
+ {
1896
+ key: "/spawn [-p <provider>] [-m <model>] [-n <name>] <task>",
1897
+ blurb: "launch one isolated subagent with optional runtime overrides"
1898
+ },
1899
+ {
1900
+ key: "/agents [list|show <id>|stream on|off]",
1901
+ blurb: "open the TUI monitor, inspect a subagent, or control its activity stream"
1902
+ },
1903
+ {
1904
+ key: "/fleet status|usage|kill|log|manifest",
1905
+ blurb: "inspect and control the active subagent fleet"
1906
+ }
1907
+ ]
1908
+ },
1909
+ {
1910
+ title: "Steering",
1911
+ items: [
1912
+ {
1913
+ key: "Esc (while busy)",
1914
+ blurb: "soft interrupt \u2014 next message carries a STEERING preamble"
1915
+ },
1916
+ { key: "/steer <text>", blurb: "mid-flight redirect, works when Esc is eaten by tmux" },
1917
+ { key: "/btw <note>", blurb: "non-aborting nudge \u2014 agent folds it in at its next step" },
1918
+ {
1919
+ key: "Ctrl+C \xD7 1 / \xD7 2 / \xD7 3",
1920
+ blurb: "cancel iteration \xB7 force-exit Ink \xB7 hard exit(130)"
1921
+ }
1922
+ ]
1923
+ },
1924
+ {
1925
+ title: "Modes & context",
1926
+ items: [
1927
+ {
1928
+ key: "/mode",
1929
+ blurb: "switch persona: code-reviewer, debugger, architect, tester, devops, \u2026"
1930
+ },
1931
+ { key: "/model", blurb: "two-step provider \u2192 model picker, hot-swap at runtime" },
1932
+ { key: "/yolo on|off|toggle", blurb: "auto-approve tool calls without restart" },
1933
+ {
1934
+ key: "/context mode frugal|balanced|deep|archival",
1935
+ blurb: "pick how aggressively history is trimmed"
1936
+ },
1937
+ { key: "/compact", blurb: "manually compact the in-flight context window" },
1938
+ {
1939
+ key: "/plan show|add|start|done",
1940
+ blurb: "strategic roadmap, survives /resume across sessions"
1941
+ }
1942
+ ]
1943
+ },
1944
+ {
1945
+ title: "Workspace tools",
1946
+ items: [
1947
+ {
1948
+ key: "@<query>",
1949
+ blurb: "search project files and insert a path into the prompt"
1950
+ },
1951
+ {
1952
+ key: "Alt+V \xB7 /image",
1953
+ blurb: "attach a clipboard image in the TUI"
1954
+ },
1955
+ {
1956
+ key: "/mcp \xB7 wstack mcp add <name>",
1957
+ blurb: "inspect or connect MCP servers"
1958
+ },
1959
+ {
1960
+ key: "/plugin install|enable|disable <name>",
1961
+ blurb: "manage optional integrations such as Telegram or LSP"
1962
+ },
1963
+ {
1964
+ key: "/skill \xB7 /init \xB7 /commit",
1965
+ blurb: "browse skills \xB7 scaffold project guidance \xB7 draft a commit"
1966
+ },
1967
+ {
1968
+ key: "/diag \xB7 wstack usage \xB7 wstack resume <id>",
1969
+ blurb: "inspect diagnostics, review costs, or continue an earlier session"
1970
+ }
1971
+ ]
1972
+ }
1973
+ ];
1974
+ var HINT_COUNT = GROUPS.reduce((n, g) => n + g.items.length, 0);
1975
+ var HINT_GROUP_COUNT = GROUPS.length;
1976
+ var HINT_GROUP_TITLES = GROUPS.map((g) => g.title);
1977
+ function shouldSuppress(flags) {
1978
+ if (flags["no-hints"] === true) return true;
1979
+ if (flags["hints"] === false) return true;
1980
+ const env = process.env.WRONGSTACK_NO_HINTS;
1981
+ if (env && env !== "0" && env.toLowerCase() !== "false") return true;
1982
+ return false;
1983
+ }
1984
+ var DEFAULT_TERM_WIDTH = 80;
1985
+ var MIN_TERM_WIDTH = 20;
1986
+ var HINT_INDENT = " ";
1987
+ var HINT_CONTINUATION_INDENT = " ";
1988
+ var wrap = (n) => (n % GROUPS.length + GROUPS.length) % GROUPS.length;
1989
+ function displayWidth(value) {
1990
+ return [...value].length;
1991
+ }
1992
+ function wrapWords(value, firstWidth, continuationWidth = firstWidth) {
1993
+ const words = value.trim().split(/\s+/).filter(Boolean);
1994
+ const lines = [];
1995
+ let line = "";
1996
+ let width = Math.max(1, firstWidth);
1997
+ for (const sourceWord of words) {
1998
+ let word = sourceWord;
1999
+ while (word) {
2000
+ if (line) {
2001
+ if (displayWidth(line) + 1 + displayWidth(word) <= width) {
2002
+ line += ` ${word}`;
2003
+ word = "";
2004
+ } else {
2005
+ lines.push(line);
2006
+ line = "";
2007
+ width = Math.max(1, continuationWidth);
2008
+ }
2009
+ continue;
2010
+ }
2011
+ if (displayWidth(word) <= width) {
2012
+ line = word;
2013
+ word = "";
2014
+ } else {
2015
+ const characters = [...word];
2016
+ lines.push(characters.slice(0, width).join(""));
2017
+ word = characters.slice(width).join("");
2018
+ width = Math.max(1, continuationWidth);
2019
+ }
2020
+ }
2021
+ }
2022
+ if (line) lines.push(line);
2023
+ return lines;
2024
+ }
2025
+ function renderHintLines(item, termWidth) {
2026
+ const inline = `${HINT_INDENT}${item.key} \u2014 ${item.blurb}`;
2027
+ if (displayWidth(inline) <= termWidth) {
2028
+ return [`${HINT_INDENT}${color9.bold(item.key)} ${color9.dim("\u2014")} ${color9.dim(item.blurb)}`];
2029
+ }
2030
+ const lines = [];
2031
+ const keyLines = wrapWords(
2032
+ item.key,
2033
+ termWidth - displayWidth(HINT_INDENT),
2034
+ termWidth - displayWidth(HINT_CONTINUATION_INDENT)
2035
+ );
2036
+ keyLines.forEach((line, index) => {
2037
+ const indent = index === 0 ? HINT_INDENT : HINT_CONTINUATION_INDENT;
2038
+ lines.push(`${indent}${color9.bold(line)}`);
2039
+ });
2040
+ const blurbLines = wrapWords(
2041
+ `\u2014 ${item.blurb}`,
2042
+ termWidth - displayWidth(HINT_CONTINUATION_INDENT)
2043
+ );
2044
+ for (const line of blurbLines) lines.push(`${HINT_CONTINUATION_INDENT}${color9.dim(line)}`);
2045
+ return lines;
2046
+ }
2047
+ function renderHeaderLines(group, groupIndex, termWidth) {
2048
+ const progress = `${groupIndex + 1}/${GROUPS.length}`;
2049
+ const plain = ` ${group.title} ${progress}`;
2050
+ if (displayWidth(plain) <= termWidth) {
2051
+ return [
2052
+ ` ${color9.cyan("\u25C7")} ${color9.bold(group.title)} ${color9.dim(progress)}`,
2053
+ ` ${color9.dim("\u2500".repeat(Math.min(termWidth - 2, 48)))}`
2054
+ ];
2055
+ }
2056
+ return [
2057
+ ` ${color9.cyan("\u25C7")} ${color9.bold(group.title)}`,
2058
+ ` ${color9.dim("\u2500".repeat(Math.min(termWidth - 2, 48)))}`
2059
+ ];
2060
+ }
2061
+ function renderFooterLines(termWidth) {
2062
+ const footer = "/help lists everything \xB7 hide with --no-hints";
2063
+ if (displayWidth(` ${footer}`) <= termWidth) {
2064
+ return [
2065
+ ` ${color9.dim("\u2500".repeat(Math.min(termWidth - 2, 48)))}`,
2066
+ ` ${color9.dim(`${color9.bold("/help")} lists everything \xB7 hide with ${color9.bold("--no-hints")}`)}`
2067
+ ];
2068
+ }
2069
+ return [...wrapWords(footer, termWidth - 2).map((line) => ` ${color9.dim(line)}`)];
2070
+ }
2071
+ async function pickGroupIndex(opts) {
2072
+ if (typeof opts.groupIndex === "number") return wrap(opts.groupIndex);
2073
+ if (opts.cursorFile) {
2074
+ try {
2075
+ let current = 0;
2076
+ try {
2077
+ const raw = await fs6.readFile(opts.cursorFile, "utf8");
2078
+ const parsed = Number.parseInt(raw.trim(), 10);
2079
+ if (Number.isFinite(parsed)) current = wrap(parsed);
2080
+ } catch {
2081
+ }
2082
+ await fs6.mkdir(path7.dirname(opts.cursorFile), { recursive: true });
2083
+ await fs6.writeFile(opts.cursorFile, String(wrap(current + 1)));
2084
+ return current;
2085
+ } catch {
2086
+ }
2087
+ }
2088
+ return Math.floor(Math.random() * GROUPS.length);
2089
+ }
2090
+ async function printLaunchHints(renderer, flags, opts = {}) {
2091
+ if (shouldSuppress(flags)) return;
2092
+ const idx = await pickGroupIndex(opts);
2093
+ const group = GROUPS[idx];
2094
+ if (!group) return;
2095
+ const termWidth = Math.max(
2096
+ MIN_TERM_WIDTH,
2097
+ Math.floor(opts.termWidth ?? process.stdout.columns ?? DEFAULT_TERM_WIDTH)
2098
+ );
2099
+ const lines = [];
2100
+ lines.push("");
2101
+ lines.push(...renderHeaderLines(group, idx, termWidth));
2102
+ for (const item of group.items) lines.push(...renderHintLines(item, termWidth));
2103
+ lines.push("");
2104
+ lines.push(...renderFooterLines(termWidth));
2105
+ lines.push("");
2106
+ renderer.write(`${lines.join("\n")}
2107
+ `);
2108
+ }
2109
+
2110
+ // src/renderer.ts
2111
+ import { color as color10 } from "@wrongstack/core/utils";
2112
+ var TerminalRenderer = class _TerminalRenderer {
2113
+ out;
2114
+ err;
2115
+ lineStart = true;
2116
+ /** When true, stdout-bound rendering is disabled (for non-terminal UI surfaces). */
2117
+ silent = false;
2118
+ /**
2119
+ * When true, Ink exclusively owns the terminal. Both stdout and stderr
2120
+ * rendering must be disabled: either stream can interleave with Ink's
2121
+ * cursor updates and make the TUI flicker or leave stray debug notes.
2122
+ */
2123
+ tuiActive = false;
2124
+ /**
2125
+ * Per-tool on-screen result render mode. Defaults to `'extend'` so
2126
+ * existing callers without `setResultRenderMode` see full content.
2127
+ * `setResultRenderMode(name, mode)` overrides the entry for `name`; the
2128
+ * override is one-shot for that name (the default reasserts after the
2129
+ * next write) so callers can set the mode right before each tool call
2130
+ * without leaking state across unrelated tool calls.
2131
+ */
2132
+ renderModes = /* @__PURE__ */ new Map();
2133
+ /** Mode applied when no entry is set for a given tool name. */
2134
+ static DEFAULT_RENDER_MODE = "extend";
2135
+ /** Maximum number of one-shot hints retained when callers abandon a result. */
2136
+ static MAX_RENDER_MODES = 256;
2137
+ /** Lines kept per tool family in `extend` mode. */
2138
+ static EXTEND_PREVIEW_LINES = 10;
2139
+ constructor(opts = {}) {
2140
+ this.out = opts.out ?? process.stdout;
2141
+ this.err = opts.err ?? process.stderr;
2142
+ }
2143
+ /**
2144
+ * Toggle stdout suppression for non-terminal UI surfaces such as WebUI.
2145
+ * Diagnostics continue to flow to stderr. Idempotent.
2146
+ */
2147
+ setSilent(silent) {
2148
+ this.silent = silent;
2149
+ }
2150
+ isSilent() {
2151
+ return this.silent;
2152
+ }
2153
+ /**
2154
+ * Toggle exclusive TUI ownership of both terminal streams. Unlike
2155
+ * {@link setSilent}, TUI mode suppresses stderr as well as stdout and clears
2156
+ * any CLI-only result mode staged before Ink took ownership. While active,
2157
+ * new result-mode hints are ignored. The two flags compose with OR semantics
2158
+ * for stdout suppression only; TUI ownership adds the other behaviors.
2159
+ * Enable immediately before starting Ink and disable in the paired
2160
+ * `finally`, only after the TUI has fully exited.
2161
+ */
2162
+ setTuiActive(active) {
2163
+ if (active) this.renderModes.clear();
2164
+ this.tuiActive = active;
2165
+ }
2166
+ isTuiActive() {
2167
+ return this.tuiActive;
2168
+ }
2169
+ suppressStdout() {
2170
+ return this.silent || this.tuiActive;
2171
+ }
2172
+ setResultRenderMode(name, mode) {
2173
+ if (this.suppressStdout()) return;
2174
+ if (this.renderModes.delete(name)) {
2175
+ this.renderModes.set(name, mode);
2176
+ return;
2177
+ }
2178
+ if (this.renderModes.size >= _TerminalRenderer.MAX_RENDER_MODES) {
2179
+ const oldestName = this.renderModes.keys().next().value;
2180
+ if (oldestName !== void 0) this.renderModes.delete(oldestName);
2181
+ }
2182
+ this.renderModes.set(name, mode);
2183
+ }
2184
+ getRenderMode(name) {
2185
+ return this.renderModes.get(name) ?? _TerminalRenderer.DEFAULT_RENDER_MODE;
2186
+ }
2187
+ clearRenderMode(name) {
2188
+ this.renderModes.delete(name);
2189
+ }
2190
+ write(input) {
2191
+ if (this.suppressStdout()) return;
2192
+ const text = typeof input === "string" ? input : input.text;
2193
+ if (!text) return;
2194
+ const rendered = renderMarkdown(text);
2195
+ this.out.write(rendered);
2196
+ this.lineStart = rendered.endsWith("\n");
2197
+ }
2198
+ writeLine(text = "") {
2199
+ if (this.suppressStdout()) return;
2200
+ if (!this.lineStart) this.out.write("\n");
2201
+ if (text) this.out.write(`${text}
2202
+ `);
2203
+ else this.out.write("\n");
2204
+ this.lineStart = true;
2205
+ }
2206
+ writeBlock(block) {
2207
+ if (block.type === "tool_result") {
2208
+ const text = typeof block.content === "string" ? block.content : JSON.stringify(block.content);
2209
+ this.writeToolResult(block.name ?? "result", text, !!block.is_error);
2210
+ return;
2211
+ }
2212
+ if (this.suppressStdout()) return;
2213
+ if (block.type === "text") {
2214
+ this.write(block);
2215
+ } else if (block.type === "tool_use") {
2216
+ this.writeToolCall(block.name, block.input);
2217
+ }
2218
+ }
2219
+ writeToolCall(name, input) {
2220
+ if (this.suppressStdout()) return;
2221
+ if (!this.lineStart) this.out.write("\n");
2222
+ const arrow = theme2.primary("\u2192");
2223
+ const display = formatInputSummary(input);
2224
+ this.out.write(`${arrow} ${theme2.bold(name)}${display ? color10.dim(` ${display}`) : ""}
2225
+ `);
2226
+ this.lineStart = true;
2227
+ }
2228
+ writeToolResult(name, content, isError) {
2229
+ if (this.suppressStdout()) {
2230
+ this.clearRenderMode(name);
2231
+ return;
2232
+ }
2233
+ const txt = typeof content === "string" ? content : safeStringify(content);
2234
+ const prefix = isError ? theme2.error("\u2718") : theme2.success("\u2713");
2235
+ const renderMode = this.getRenderMode(name);
2236
+ this.clearRenderMode(name);
2237
+ const bodyText = extractBodyText(content, name);
2238
+ if (isError) {
2239
+ const firstLine = txt.split("\n")[0] ?? "";
2240
+ const truncated = firstLine.length > 200 ? `${firstLine.slice(0, 197)}\u2026` : firstLine;
2241
+ this.out.write(` ${prefix} ${color10.dim(truncated)}
2242
+ `);
2243
+ this.lineStart = true;
2244
+ return;
2245
+ }
2246
+ const isEditLike = name === "edit" || name === "write";
2247
+ const diff = extractDiff(content);
2248
+ if (isEditLike && diff) {
2249
+ this.out.write(` ${prefix} ${color10.dim(summarize(content, name))}
2250
+ `);
2251
+ const rendered = renderDiff(diff).split("\n").map((l) => ` ${l}`).join("\n");
2252
+ this.out.write(`${rendered}
2253
+ `);
2254
+ this.lineStart = true;
2255
+ return;
2256
+ }
2257
+ const summary = summarize(content, name);
2258
+ const isReadLike = name === "read" || name === "grep" || name === "glob" || name === "bash" || name === "exec";
2259
+ if (renderMode === "simple") {
2260
+ this.out.write(` ${prefix} ${color10.dim(summary || compactSingleLine(bodyText))}
2261
+ `);
2262
+ this.lineStart = true;
2263
+ return;
2264
+ }
2265
+ const previewLines = isEditLike ? 0 : isReadLike ? _TerminalRenderer.EXTEND_PREVIEW_LINES : 2;
2266
+ const lines = bodyText.split("\n");
2267
+ const head = lines.slice(0, previewLines).map((l) => l.replace(/\s+$/, ""));
2268
+ const moreCount = Math.max(0, lines.length - head.length);
2269
+ this.out.write(` ${prefix} ${color10.dim(summary)}
2270
+ `);
2271
+ for (const l of head) {
2272
+ const capped = l.length > 200 ? `${l.slice(0, 197)}\u2026` : l;
2273
+ this.out.write(` ${color10.dim(capped)}
2274
+ `);
2275
+ }
2276
+ if (moreCount > 0) {
2277
+ this.out.write(
2278
+ ` ${color10.dim(`+${moreCount} more line${moreCount === 1 ? "" : "s"}`)}
2279
+ `
2280
+ );
2281
+ }
2282
+ this.lineStart = true;
2283
+ }
2284
+ writeDiff(diff) {
2285
+ if (this.suppressStdout()) return;
2286
+ if (!this.lineStart) this.out.write("\n");
2287
+ this.out.write(`${renderDiff(diff)}
2288
+ `);
2289
+ this.lineStart = true;
2290
+ }
2291
+ writeWarning(text) {
2292
+ if (this.tuiActive) return;
2293
+ this.err.write(`${theme2.warn("\u26A0")} ${text}
2294
+ `);
2295
+ }
2296
+ writeError(text) {
2297
+ if (this.tuiActive) return;
2298
+ this.err.write(`${theme2.error("\u2718")} ${text}
2299
+ `);
2300
+ }
2301
+ writeInfo(text) {
2302
+ if (this.tuiActive) return;
2303
+ this.err.write(`${theme2.info("\u2139")} ${text}
2304
+ `);
2305
+ }
2306
+ /**
2307
+ * Write text to stderr verbatim: no glyph prefix, no markdown processing.
2308
+ *
2309
+ * Deliberately stricter than writeWarning/writeError/writeInfo, which emit
2310
+ * *diagnostics* and are suppressed only when a TUI owns the terminal (so
2311
+ * they survive silent mode). This method is for informational mirrors of
2312
+ * stdout output, where the stdout copy already carries the full content —
2313
+ * in silent mode the operator asked for machine-only output, so the mirror
2314
+ * is dropped rather than leaked onto a quieter stream. It therefore honors
2315
+ * the full {@link suppressStdout} gate.
2316
+ *
2317
+ * Used by `wstack mailbox serve` to route the human-readable startup banner
2318
+ * away from stdout, keeping the mailbox_serve_started JSON line cleanly
2319
+ * pipeable (`mailbox serve | jq`).
2320
+ */
2321
+ writeStderr(text) {
2322
+ if (this.suppressStdout()) return;
2323
+ this.err.write(text);
2324
+ }
2325
+ clear() {
2326
+ if (this.suppressStdout()) return;
2327
+ this.out.write("\x1B[2J\x1B[H");
2328
+ this.lineStart = true;
2329
+ }
2330
+ /**
2331
+ * Write a flashy agent completion banner for delegate tool results.
2332
+ * Renders a box like:
2333
+ * ┌─────────────────────────────────────┐
2334
+ * │ ✓ [role] done in 4m 32s │
2335
+ * │ 127 iterations · 341 tools │
2336
+ * │ Found 14 bugs across 6 files... │
2337
+ * └─────────────────────────────────────┘
2338
+ */
2339
+ writeAgentSummary(summary, ok) {
2340
+ if (this.suppressStdout()) return;
2341
+ if (!this.lineStart) this.out.write("\n");
2342
+ const lines = summary.split("\n");
2343
+ const icon = ok ? theme2.success("\u2713") : theme2.error("\u2718");
2344
+ const firstLine = `${icon} ${lines[0] ?? summary}`;
2345
+ const body = lines.slice(1);
2346
+ const maxWidth = Math.min(process.stdout.columns ?? 80, 120);
2347
+ const contentWidth = Math.max(
2348
+ firstLine.length,
2349
+ body.reduce((a, l) => Math.max(a, l.length), 0)
2350
+ );
2351
+ const boxWidth = Math.min(Math.max(contentWidth + 4, 44), maxWidth);
2352
+ const thick = "\u2501".repeat(boxWidth - 2);
2353
+ const thin = "\u2500".repeat(boxWidth - 2);
2354
+ this.out.write(`
2355
+ ${theme2.primary("\u250C")}${thick}${theme2.primary("\u2510")}
2356
+ `);
2357
+ const centre = (s) => {
2358
+ const inner = ` ${s} `;
2359
+ const padLen = Math.max(0, boxWidth - 2 - s.length);
2360
+ const left = Math.floor(padLen / 2);
2361
+ const right = padLen - left;
2362
+ return `${" ".repeat(left)}${inner}${" ".repeat(right)}`;
2363
+ };
2364
+ this.out.write(` ${theme2.primary("\u2502")}${centre(firstLine)}${theme2.primary("\u2502")}
2365
+ `);
2366
+ for (const l of body) {
2367
+ this.out.write(
2368
+ ` ${theme2.primary("\u2502")} ${l}${" ".repeat(Math.max(0, boxWidth - 3 - l.length))}${theme2.primary("\u2502")}
2369
+ `
2370
+ );
2371
+ }
2372
+ this.out.write(` ${theme2.primary("\u2514")}${thin}${theme2.primary("\u2518")}
2373
+ `);
2374
+ this.lineStart = true;
2375
+ }
2376
+ /**
2377
+ * Render subagent completion banners from a RunResult.
2378
+ * Uses `delegateSummaries` when available (populated by delegate tool),
2379
+ * otherwise falls back to scanning message history.
2380
+ */
2381
+ writeDelegateSummaries(result) {
2382
+ if (this.suppressStdout()) return;
2383
+ if (result.delegateSummaries) {
2384
+ for (const { summary, ok } of result.delegateSummaries) {
2385
+ if (!summary) continue;
2386
+ this.writeAgentSummary(summary, ok);
2387
+ }
2388
+ return;
2389
+ }
2390
+ if (!result.messages) return;
2391
+ for (const msg of result.messages) {
2392
+ const m = msg;
2393
+ if (!Array.isArray(m.content)) continue;
2394
+ for (const block of m.content) {
2395
+ const b = block;
2396
+ if (b.type !== "tool_result" || b.name !== "delegate") continue;
2397
+ let obj;
2398
+ try {
2399
+ obj = typeof b.content === "string" ? JSON.parse(b.content) : b.content;
2400
+ } catch {
2401
+ continue;
2402
+ }
2403
+ const o = obj;
2404
+ if (o.summary) {
2405
+ this.writeAgentSummary(o.summary, o.ok ?? true);
2406
+ }
2407
+ }
2408
+ }
2409
+ }
2410
+ };
2411
+ function renderMarkdown(s) {
2412
+ let out = s;
2413
+ out = out.replace(/^(#{1,6}) (.+)$/gm, (_m, hashes, text) => {
2414
+ return theme2.primary(theme2.bold(`${hashes} ${text}`));
2415
+ });
2416
+ out = out.replace(/```([a-zA-Z0-9_+-]*)\n([\s\S]*?)```/g, (_m, _lang, code) => {
2417
+ return color10.gray(`
2418
+ \u250C\u2500
2419
+ ${code.replace(/^/gm, "\u2502 ")}\u2514\u2500`);
2420
+ });
2421
+ out = out.replace(/`([^`\n]+)`/g, (_m, code) => theme2.accent(code));
2422
+ out = out.replace(/\*\*([^*]+)\*\*/g, (_m, text) => theme2.bold(text));
2423
+ out = out.replace(
2424
+ /(^|[^*])\*([^*\n]+)\*([^*]|$)/g,
2425
+ (_m, l, t, r) => `${l}${color10.italic(t)}${r}`
2426
+ );
2427
+ return out;
2428
+ }
2429
+ function formatInputSummary(input) {
2430
+ if (!input || typeof input !== "object") return "";
2431
+ const obj = input;
2432
+ if (typeof obj["path"] === "string") return obj["path"];
2433
+ if (typeof obj["url"] === "string") return obj["url"];
2434
+ if (typeof obj["command"] === "string") {
2435
+ const cmd = obj["command"];
2436
+ return cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
2437
+ }
2438
+ if (typeof obj["pattern"] === "string") return obj["pattern"];
2439
+ return "";
2440
+ }
2441
+ function safeStringify(value) {
2442
+ if (typeof value === "string") return value;
2443
+ try {
2444
+ return JSON.stringify(value);
2445
+ } catch {
2446
+ return String(value);
2447
+ }
2448
+ }
2449
+ function extractBodyText(value, name) {
2450
+ let v = value;
2451
+ if (typeof value === "string") {
2452
+ const trimmed = value.trimStart();
2453
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
2454
+ try {
2455
+ v = JSON.parse(value);
2456
+ } catch {
2457
+ return value;
2458
+ }
2459
+ } else {
2460
+ return value;
2461
+ }
2462
+ }
2463
+ if (typeof v !== "object" || v === null) {
2464
+ return safeStringify(value);
2465
+ }
2466
+ const o = v;
2467
+ if (name === "read" && typeof o["text"] === "string") return o["text"];
2468
+ if (name === "bash") {
2469
+ const output = typeof o["output"] === "string" ? o["output"] : "";
2470
+ const error = typeof o["error"] === "string" ? o["error"] : "";
2471
+ if (output && error) return `${output}
2472
+ ${error}`;
2473
+ return output || error || "";
2474
+ }
2475
+ if (name === "exec") {
2476
+ const stdout = typeof o["stdout"] === "string" ? o["stdout"] : "";
2477
+ const stderr = typeof o["stderr"] === "string" ? o["stderr"] : "";
2478
+ if (stdout && stderr) return `${stdout}
2479
+ ${stderr}`;
2480
+ return stdout || stderr || "";
2481
+ }
2482
+ return safeStringify(value);
2483
+ }
2484
+ function extractDiff(value) {
2485
+ if (typeof value === "object" && value !== null) {
2486
+ const d = value.diff;
2487
+ if (typeof d === "string" && d.length > 0) return d;
2488
+ }
2489
+ if (typeof value === "string") {
2490
+ const trimmed = value.trimStart();
2491
+ if (trimmed.startsWith("{")) {
2492
+ try {
2493
+ const parsed = JSON.parse(value);
2494
+ if (typeof parsed.diff === "string" && parsed.diff.length > 0) {
2495
+ return parsed.diff;
2496
+ }
2497
+ } catch {
2498
+ }
2499
+ }
2500
+ if (/^---[^\n]*\n\+\+\+/m.test(value)) return value;
2501
+ }
2502
+ return null;
2503
+ }
2504
+ function summarize(value, name) {
2505
+ let v = value;
2506
+ if (typeof value === "string") {
2507
+ const trimmed = value.trimStart();
2508
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
2509
+ try {
2510
+ v = JSON.parse(value);
2511
+ } catch {
2512
+ }
2513
+ }
2514
+ }
2515
+ if (typeof v === "object" && v !== null) {
2516
+ const o = v;
2517
+ if (name === "edit") {
2518
+ const path9 = typeof o["path"] === "string" ? o["path"] : "";
2519
+ const reps = typeof o["replacements"] === "number" ? o["replacements"] : 0;
2520
+ return `${path9} ${reps} replacement${reps === 1 ? "" : "s"}`.trim();
2521
+ }
2522
+ if (name === "write") {
2523
+ const path9 = typeof o["path"] === "string" ? o["path"] : "";
2524
+ const bytes = typeof o["bytes"] === "number" ? o["bytes"] : void 0;
2525
+ return bytes !== void 0 ? `${path9} ${bytes}B` : path9;
2526
+ }
2527
+ if (name === "read") {
2528
+ return summarizeRead(o);
2529
+ }
2530
+ if (name === "bash") {
2531
+ return summarizeBash(o);
2532
+ }
2533
+ if (name === "exec") {
2534
+ return summarizeExec(o);
2535
+ }
2536
+ if (typeof o["count"] === "number") {
2537
+ return `${o["count"]} match${o["count"] === 1 ? "" : "es"}`;
2538
+ }
2539
+ }
2540
+ return "";
2541
+ }
2542
+ function summarizeRead(o) {
2543
+ const path9 = typeof o["path"] === "string" ? o["path"] : "";
2544
+ const total = typeof o["total_lines"] === "number" ? o["total_lines"] : void 0;
2545
+ const truncated = o["truncated"] === true;
2546
+ const cached = o["cached"] === true;
2547
+ const note = typeof o["note"] === "string" ? o["note"] : void 0;
2548
+ const parts = [];
2549
+ if (path9) parts.push(path9);
2550
+ if (total !== void 0) parts.push(`${total} line${total === 1 ? "" : "s"}`);
2551
+ const flags = [];
2552
+ if (truncated) flags.push("truncated");
2553
+ if (cached) flags.push("cached");
2554
+ if (flags.length > 0) parts.push(`(${flags.join(", ")})`);
2555
+ let summary = parts.join(" ").trim();
2556
+ if (note) summary = summary ? `${summary} \u2014 ${note}` : note;
2557
+ return summary;
2558
+ }
2559
+ function summarizeBash(o) {
2560
+ const exit = typeof o["exit_code"] === "number" ? o["exit_code"] : void 0;
2561
+ const output = typeof o["output"] === "string" ? o["output"] : "";
2562
+ const lines = output ? output.split("\n").length : 0;
2563
+ const parts = [];
2564
+ if (exit !== void 0) parts.push(`exit=${exit}`);
2565
+ else if (o["exit_code"] === null) parts.push("backgrounded");
2566
+ if (o["timed_out"] === true) parts.push("timed out");
2567
+ if (lines > 0) parts.push(`${lines} output line${lines === 1 ? "" : "s"}`);
2568
+ if (output.length > 0) parts.push(`${output.length}B`);
2569
+ return parts.join(" ");
2570
+ }
2571
+ function summarizeExec(o) {
2572
+ const command = typeof o["command"] === "string" ? o["command"] : "";
2573
+ const args = Array.isArray(o["args"]) ? o["args"].filter((a) => typeof a === "string") : [];
2574
+ const exit = typeof o["exitCode"] === "number" ? o["exitCode"] : void 0;
2575
+ const stdout = typeof o["stdout"] === "string" ? o["stdout"] : "";
2576
+ const stderr = typeof o["stderr"] === "string" ? o["stderr"] : "";
2577
+ const outLines = stdout ? stdout.split("\n").length : 0;
2578
+ const errLines = stderr ? stderr.split("\n").length : 0;
2579
+ const totalBytes = stdout.length + stderr.length;
2580
+ const parts = [];
2581
+ const invocation = [command, ...args].filter(Boolean).join(" ");
2582
+ if (invocation) parts.push(`$ ${invocation}`);
2583
+ if (exit !== void 0) parts.push(`exit=${exit}`);
2584
+ if (outLines > 0) parts.push(`${outLines} stdout line${outLines === 1 ? "" : "s"}`);
2585
+ if (errLines > 0) parts.push(`${errLines} stderr line${errLines === 1 ? "" : "s"}`);
2586
+ if (totalBytes > 0) parts.push(`${totalBytes}B`);
2587
+ return parts.join(" ");
2588
+ }
2589
+ function compactSingleLine(text) {
2590
+ const first = text.split("\n")[0]?.trim() ?? "";
2591
+ if (!first) return "(no output)";
2592
+ return first.length > 200 ? `${first.slice(0, 197)}\u2026` : first;
2593
+ }
2594
+
2595
+ // src/boot.ts
2596
+ var GITHUB_PROVIDERS_OVERLAY_URL = "https://raw.githubusercontent.com/WrongStack/WrongStack/main/packages/cli/data/providers.json";
2597
+ function resolveBundledOverlayFile() {
2598
+ try {
2599
+ return fileURLToPath(new URL("../data/providers.json", import.meta.url));
2600
+ } catch {
2601
+ return void 0;
2602
+ }
2603
+ }
2604
+ async function validateSavedProviderModel(config, modelsRegistry) {
2605
+ const providerId = config.provider;
2606
+ const modelId = config.model;
2607
+ if (!providerId || !modelId) return { ok: false, reason: "missing provider/model" };
2608
+ if (isSetupProvider(providerId)) return { ok: true };
2609
+ const saved = config.providers?.[providerId];
2610
+ const lookupId = saved?.type && saved.type !== providerId ? saved.type : providerId;
2611
+ const catalogProvider = await modelsRegistry.getProvider(lookupId).catch(() => void 0);
2612
+ if (!catalogProvider && !saved?.family)
2613
+ return { ok: false, reason: `provider "${providerId}" is no longer available` };
2614
+ const hasCredential = (catalogProvider?.envVars ?? saved?.envVars ?? []).some(
2615
+ (envVar) => Boolean(process.env[envVar])
2616
+ ) || saved !== void 0 && resolveActiveApiKey(saved) !== void 0 || isKeylessLocalProvider({
2617
+ apiBase: saved?.baseUrl ?? catalogProvider?.apiBase,
2618
+ envVars: saved?.envVars ?? catalogProvider?.envVars
2619
+ });
2620
+ if (!hasCredential) return { ok: false, reason: `provider "${providerId}" has no usable key` };
2621
+ const visible = visibleModelIds(
2622
+ providerId,
2623
+ config,
2624
+ (catalogProvider?.models ?? []).map((m) => m.id),
2625
+ saved
2626
+ );
2627
+ if (visible.length > 0 && !visible.includes(modelId)) {
2628
+ return {
2629
+ ok: false,
2630
+ reason: `model "${modelId}" is no longer available for provider "${providerId}"`
2631
+ };
2632
+ }
2633
+ return { ok: true };
2634
+ }
2635
+ async function autoSelectSavedProvider(config, modelsRegistry) {
2636
+ const providers = config.providers ?? {};
2637
+ const entries = config.provider && providers[config.provider] ? [[config.provider, providers[config.provider]]] : Object.entries(providers);
2638
+ for (const [id, cfg] of entries) {
2639
+ if (!cfg) continue;
2640
+ let model = cfg.models?.[0];
2641
+ if (!model) {
2642
+ const catalogId = cfg.type && cfg.type !== id ? cfg.type : id;
2643
+ const catalog = await modelsRegistry.getProvider(catalogId).catch(() => void 0);
2644
+ model = catalog?.models?.[0]?.id;
2645
+ }
2646
+ if (model) return { provider: id, model };
2647
+ }
2648
+ return void 0;
2649
+ }
2650
+ function resolveBundledSkillsDir2() {
2651
+ try {
2652
+ const req = createRequire4(import.meta.url);
2653
+ const corePkg = req.resolve("@wrongstack/core/package.json");
2654
+ return path8.join(path8.dirname(corePkg), "skills");
2655
+ } catch {
2656
+ return void 0;
2657
+ }
2658
+ }
2659
+ function resolveBundledPromptsDir2() {
2660
+ try {
2661
+ const req = createRequire4(import.meta.url);
2662
+ const corePkg = req.resolve("@wrongstack/core/package.json");
2663
+ return path8.join(path8.dirname(corePkg), "data", "prompts");
2664
+ } catch {
2665
+ return void 0;
2666
+ }
2667
+ }
2668
+ function isHomeDirectory(cwd, userHome) {
2669
+ if (!cwd || !userHome) return false;
2670
+ return path8.resolve(cwd) === path8.resolve(userHome);
2671
+ }
2672
+ function shouldPrintYoloNotice(lastChoices, yoloPinned, yolo) {
2673
+ return !lastChoices && yoloPinned === void 0 && yolo;
2674
+ }
2675
+ async function boot(argv) {
2676
+ const { flags, positional } = parseArgs(argv);
2677
+ if (positional[0] === "update") {
2678
+ const renderer2 = new TerminalRenderer();
2679
+ return runUpdateCommand(positional.slice(1), {
2680
+ cwd: process.cwd(),
2681
+ userHome: os.homedir(),
2682
+ renderer: renderer2,
2683
+ flags
2684
+ });
2685
+ }
2686
+ if (positional[0] === "resume" && positional[1] && !subcommands["__noop_resume_marker"]) {
2687
+ flags["resume"] = positional[1];
2688
+ positional.splice(0, 2);
2689
+ }
2690
+ let bootResult;
2691
+ try {
2692
+ bootResult = await bootConfig(flags);
2693
+ } catch (err) {
2694
+ writeErr(`Config error: ${toErrorMessage4(err)}
2695
+ `);
2696
+ return 2;
2697
+ }
2698
+ let { paths, config, vault } = bootResult;
2699
+ let first = positional[0];
2700
+ const mayOfferConfigRecovery = isStdinTTY2() && (positional.length === 0 || first === "quick") && (!config.activeProfile || config.activeProfile === "default") && typeof flags["prompt"] !== "string" && !flags["webui"] && !flags["no-interactive"] && !flags["skip"];
2701
+ const renderer = new TerminalRenderer();
2702
+ const reader = new ReadlineInputReader({ historyFile: paths.wpaths.historyFile });
2703
+ if (mayOfferConfigRecovery) {
2704
+ const restored = await maybeRestoreDefaultProfileFromBackup({
2705
+ globalRoot: paths.wpaths.globalRoot,
2706
+ profilePath: paths.wpaths.profileConfig("default"),
2707
+ renderer,
2708
+ reader
2709
+ });
2710
+ if (restored) {
2711
+ try {
2712
+ bootResult = await bootConfig(flags);
2713
+ ({ paths, config, vault } = bootResult);
2714
+ } catch (err) {
2715
+ writeErr(`Config error after backup restore: ${toErrorMessage4(err)}
2716
+ `);
2717
+ await reader.close();
2718
+ return 2;
2719
+ }
2720
+ }
2721
+ }
2722
+ config = applySimpleUiFullAutoProfile(config, flags);
2723
+ const simpleUiFullAuto = isSimpleUiFullAuto(flags);
2724
+ const { cwd, projectRoot, userHome, wpaths, pathResolver } = paths;
2725
+ const profileConfigPath = activeProfileConfigPath(wpaths, config);
2726
+ void pathResolver;
2727
+ if (first === "quick") {
2728
+ flags["quick"] = true;
2729
+ flags["tui"] = true;
2730
+ positional.splice(0, 1);
2731
+ first = void 0;
2732
+ const plugins = config?.plugins ?? [];
2733
+ if (plugins.length === 0) {
2734
+ console.debug("[wrongstack:quick] No plugins configured");
2735
+ } else {
2736
+ for (const p of plugins) {
2737
+ const name = typeof p === "string" ? p : p.name;
2738
+ const enabled = typeof p === "object" && p.enabled === false ? " (disabled)" : "";
2739
+ console.debug(`[wrongstack:quick] plugin: ${name}${enabled}`);
2740
+ }
2741
+ }
2742
+ }
2743
+ const logger = new DefaultLogger({
2744
+ level: config.log.level,
2745
+ file: wpaths.logFile,
2746
+ // Suppress stderr output in TUI mode: plugin/library log messages
2747
+ // (e.g. Telegram "getUpdates failed") write directly to stderr and
2748
+ // bypass Ink, which breaks the Static/live boundary.
2749
+ // Logs still go to the disk file for post-hoc debugging.
2750
+ stderr: !flags.tui
2751
+ });
2752
+ const modelsRegistry = new DefaultModelsRegistry({
2753
+ cacheFile: wpaths.modelsCache,
2754
+ // Force a refresh attempt once per CLI process. Model metadata changes faster
2755
+ // than releases (new model ids, corrected context windows), and stale cache
2756
+ // here directly affects runtime behavior like context bars and compaction.
2757
+ // If the network is unavailable, DefaultModelsRegistry still falls back to
2758
+ // stale cache or the bundled overlay instead of failing startup.
2759
+ ttlSeconds: 0,
2760
+ // Curated overlay merged on top of models.dev: fetched from GitHub raw for
2761
+ // freshness, with the bundled file as the offline floor.
2762
+ overlayUrl: GITHUB_PROVIDERS_OVERLAY_URL,
2763
+ overlayFile: resolveBundledOverlayFile(),
2764
+ overlayCacheFile: wpaths.modelsOverlayCache
2765
+ });
2766
+ const subcommandHandler = first ? subcommands[first] : void 0;
2767
+ if (first && subcommandHandler) {
2768
+ if (flags["help"] === true || flags["h"] === true) {
2769
+ const deepSub = positional[1];
2770
+ if (deepSub && renderDeepHelp(`${first}:${deepSub}`, renderer)) {
2771
+ await reader.close();
2772
+ return 0;
2773
+ }
2774
+ if (renderFocusedHelp(first, renderer)) {
2775
+ await reader.close();
2776
+ return 0;
2777
+ }
2778
+ }
2779
+ const container = createDefaultContainer2({
2780
+ config,
2781
+ wpaths,
2782
+ logger,
2783
+ modelsRegistry,
2784
+ bundledSkillsDir: config.features.skills ? resolveBundledSkillsDir2() : void 0,
2785
+ bundledPromptsDir: config.features.prompts === false ? void 0 : resolveBundledPromptsDir2()
2786
+ });
2787
+ const sessionStore = container.resolve(TOKENS2.SessionStore);
2788
+ const skillLoader = container.resolve(TOKENS2.SkillLoader);
2789
+ const toolRegistryForSubcmd = new ToolRegistry();
2790
+ registerBuiltinToolTier({
2791
+ registry: toolRegistryForSubcmd,
2792
+ tier: normalizeTokenSavingTier(config.features.tokenSavingMode)
2793
+ });
2794
+ const code = await subcommandHandler(positional.slice(1), {
2795
+ config,
2796
+ renderer,
2797
+ reader,
2798
+ sessionStore,
2799
+ skillLoader,
2800
+ toolRegistry: toolRegistryForSubcmd,
2801
+ modelsRegistry,
2802
+ paths: wpaths,
2803
+ vault,
2804
+ cwd,
2805
+ projectRoot,
2806
+ userHome,
2807
+ flags
2808
+ });
2809
+ await reader.close();
2810
+ return code;
2811
+ }
2812
+ if (isHomeDirectory(cwd, userHome)) {
2813
+ writeErr(
2814
+ `
2815
+ ${color11.red(color11.bold("\u26A0 This is not a working directory."))}
2816
+ ${color11.red("Please open wstack in a project folder.")}
2817
+
2818
+ `
2819
+ );
2820
+ await reader.close();
2821
+ return 1;
2822
+ }
2823
+ if (!flags["no-models-refresh"]) {
2824
+ try {
2825
+ await modelsRegistry.refresh();
2826
+ logger.info("models.dev catalog refreshed");
2827
+ } catch (err) {
2828
+ const msg = toErrorMessage4(err);
2829
+ logger.warn(`models.dev refresh failed (${msg}); using cached catalog`);
2830
+ }
2831
+ }
2832
+ try {
2833
+ await discoverAndMergeProviders({
2834
+ config,
2835
+ registry: modelsRegistry,
2836
+ cacheDir: path8.dirname(wpaths.modelsCache),
2837
+ logger
2838
+ });
2839
+ } catch (err) {
2840
+ logger.debug(`provider auto-discovery skipped: ${toErrorMessage4(err)}`);
2841
+ }
2842
+ const isSingleShot = positional.length > 0 || typeof flags["prompt"] === "string";
2843
+ const isInteractiveTTY = isStdinTTY2() && !isSingleShot && !flags["webui"] && !flags["no-interactive"] && !flags["skip"];
2844
+ if (isInteractiveTTY) {
2845
+ await checkGitInCwd({ cwd, renderer, reader });
2846
+ const cont = await runProjectCheck({ projectRoot, cwd, renderer, reader });
2847
+ if (!cont) {
2848
+ await reader.close();
2849
+ return 0;
2850
+ }
2851
+ }
2852
+ const wantsTui = flags["tui"] === true && flags["no-tui"] !== true;
2853
+ if (wantsTui && (!process.stdout.isTTY || !process.stdin.isTTY)) {
2854
+ writeErr(
2855
+ "wstack: --tui requires an interactive terminal on both stdin and stdout.\n Drop the flag (use the plain REPL) or run wstack directly without piping.\n"
2856
+ );
2857
+ await reader.close();
2858
+ return 2;
2859
+ }
2860
+ const providerFlag = typeof flags["provider"] === "string" ? flags["provider"] : void 0;
2861
+ const modelFlag = typeof flags["model"] === "string" ? flags["model"] : void 0;
2862
+ const noInteractiveMode = flags["webui"] || flags["no-interactive"];
2863
+ if (noInteractiveMode && (!config.provider || !config.model)) {
2864
+ if (providerFlag && modelFlag) {
2865
+ config = patchConfig(config, { provider: providerFlag, model: modelFlag });
2866
+ } else {
2867
+ const picked = await autoSelectSavedProvider(config, modelsRegistry);
2868
+ if (picked) config = patchConfig(config, picked);
2869
+ }
2870
+ }
2871
+ if (!(!!providerFlag && !!modelFlag)) {
2872
+ if (isStdinTTY2() && !noInteractiveMode) {
2873
+ let picked;
2874
+ let skipPicker = false;
2875
+ if (isSetupProvider(config.provider)) {
2876
+ skipPicker = true;
2877
+ renderer.write(
2878
+ `
2879
+ ${color11.amber("\u25B6")} ${color11.bold("Setup mode")} ${color11.dim("\u2014 no model connected. Run")} ${color11.bold("/auth")} ${color11.dim("to connect one.")}
2880
+
2881
+ `
2882
+ );
2883
+ } else if (!await hasAnyCredential(config, modelsRegistry)) {
2884
+ const outcome = await runFirstRunSetup({
2885
+ renderer,
2886
+ reader,
2887
+ modelsRegistry,
2888
+ vault,
2889
+ profileConfigPath,
2890
+ reloadConfig: async () => (await bootConfig(flags)).config
2891
+ });
2892
+ if (outcome.kind === "quit") {
2893
+ await reader.close();
2894
+ return 0;
2895
+ }
2896
+ if (outcome.kind === "setup-mode") {
2897
+ config = patchConfig(config, { provider: SETUP_PROVIDER_ID, model: SETUP_MODEL_ID });
2898
+ await saveToGlobalConfig(profileConfigPath, SETUP_PROVIDER_ID, SETUP_MODEL_ID);
2899
+ skipPicker = true;
2900
+ } else {
2901
+ try {
2902
+ const reloaded = await bootConfig(flags);
2903
+ config = reloaded.config;
2904
+ vault = reloaded.vault;
2905
+ } catch (err) {
2906
+ writeErr(`Config error after setup: ${toErrorMessage4(err)}
2907
+ `);
2908
+ await reader.close();
2909
+ return 2;
2910
+ }
2911
+ }
2912
+ }
2913
+ const savedProvider = config.provider;
2914
+ const savedModel = config.model;
2915
+ if (!skipPicker && savedProvider && savedModel) {
2916
+ const savedStatus = await validateSavedProviderModel(config, modelsRegistry);
2917
+ renderer.write(
2918
+ `
2919
+ ${color11.dim("Last settings:")} ${color11.bold(savedProvider)} / ${color11.bold(savedModel)}
2920
+ `
2921
+ );
2922
+ if (!savedStatus.ok) {
2923
+ renderer.writeWarning(
2924
+ `Saved provider/model is no longer usable (${savedStatus.reason ?? "unknown reason"}); choose a provider.
2925
+ `
2926
+ );
2927
+ } else {
2928
+ const answer = (await reader.readLine(
2929
+ ` ${color11.amber("?")} Continue with these? ${color11.dim("[Y/n/q]")} ${color11.dim("(auto Y in 5s)")} `,
2930
+ { timeoutMs: 5e3, defaultAnswer: "y" }
2931
+ )).trim().toLowerCase();
2932
+ if (answer === "q") {
2933
+ renderer.write(color11.dim(" Goodbye!\n"));
2934
+ await reader.close();
2935
+ return 0;
2936
+ }
2937
+ if (answer !== "n" && answer !== "no") {
2938
+ skipPicker = true;
2939
+ renderer.write(
2940
+ `
2941
+ ${color11.green("\u25B6")} ${color11.bold(savedProvider)} / ${color11.bold(savedModel)}
2942
+
2943
+ `
2944
+ );
2945
+ }
2946
+ }
2947
+ }
2948
+ if (!skipPicker) {
2949
+ picked = await runPicker({
2950
+ modelsRegistry,
2951
+ renderer,
2952
+ reader,
2953
+ config,
2954
+ defaultProvider: providerFlag ?? config.provider,
2955
+ defaultModel: modelFlag ?? config.model
2956
+ });
2957
+ }
2958
+ if (!picked && !skipPicker) {
2959
+ if (!config.provider || !config.model) {
2960
+ renderer.write(
2961
+ `
2962
+ ${color11.dim("No provider selected.")}
2963
+ ${color11.dim("Run")} ${color11.bold("wstack auth")} ${color11.dim("to add a key or sign in, or start with no model:")}
2964
+ ${color11.bold(`wstack --provider ${SETUP_PROVIDER_ID} --model ${SETUP_MODEL_ID}`)}
2965
+
2966
+ `
2967
+ );
2968
+ await reader.close();
2969
+ return 2;
2970
+ }
2971
+ }
2972
+ if (picked) {
2973
+ const prevProvider = config.provider;
2974
+ const prevModel = config.model;
2975
+ config = patchConfig(config, { provider: picked.provider, model: picked.model });
2976
+ if (picked.provider !== prevProvider || picked.model !== prevModel) {
2977
+ const saved = await saveToGlobalConfig(profileConfigPath, picked.provider, picked.model);
2978
+ if (saved) {
2979
+ renderer.writeInfo(`Saved ${picked.provider}/${picked.model} as default.
2980
+ `);
2981
+ } else {
2982
+ renderer.writeWarning(
2983
+ `Could not save ${picked.provider}/${picked.model} to config. Check permissions or disk space.
2984
+ `
2985
+ );
2986
+ }
2987
+ }
2988
+ }
2989
+ } else if (!config.provider || !config.model) {
2990
+ writeErr(
2991
+ `No provider or model configured. Run \`wstack auth\`, or pass --provider <id> --model <id>.
2992
+ To start the app with no model connected, pass --provider ${SETUP_PROVIDER_ID} --model ${SETUP_MODEL_ID}.
2993
+ `
2994
+ );
2995
+ await reader.close();
2996
+ return 2;
2997
+ } else {
2998
+ const savedStatus = await validateSavedProviderModel(config, modelsRegistry);
2999
+ if (!savedStatus.ok) {
3000
+ writeErr(
3001
+ `Saved provider/model is no longer usable (${savedStatus.reason ?? "unknown reason"}). Run \`wstack auth\` or pass --provider <id> --model <id>.
3002
+ `
3003
+ );
3004
+ await reader.close();
3005
+ return 2;
3006
+ }
3007
+ }
3008
+ }
3009
+ if (flags["webui"]) {
3010
+ flags["tui"] = false;
3011
+ flags["no-tui"] = true;
3012
+ }
3013
+ if (isInteractiveTTY) {
3014
+ const promptMenu = await maybeRunSystemPromptMenu({
3015
+ isInteractiveTTY,
3016
+ flags,
3017
+ renderer,
3018
+ reader,
3019
+ profileConfigPath,
3020
+ paths: {
3021
+ globalDir: wpaths.globalInstructions,
3022
+ projectDir: wpaths.inProjectInstructions
3023
+ }
3024
+ });
3025
+ if (promptMenu.aborted) {
3026
+ await reader.close();
3027
+ return 0;
3028
+ }
3029
+ if (promptMenu.changed && promptMenu.variant) {
3030
+ config = patchConfig(config, { systemPrompt: { variant: promptMenu.variant } });
3031
+ }
3032
+ if (promptMenu.persistError) {
3033
+ renderer.writeWarning(
3034
+ `Could not save system prompt variant to config: ${toErrorMessage4(promptMenu.persistError)}
3035
+ `
3036
+ );
3037
+ }
3038
+ let modePinned;
3039
+ if (flags["no-tui"]) modePinned = "repl";
3040
+ else if (flags["tui"]) modePinned = "tui";
3041
+ const yoloPinned = flags["no-yolo"] === true ? false : flags["yolo"] === true ? true : void 0;
3042
+ let autonomyPinned;
3043
+ if (flags["no-autonomy"] === true) autonomyPinned = "off";
3044
+ else if (flags["eternal"] === true)
3045
+ autonomyPinned = "off";
3046
+ else if (typeof flags["autonomy"] === "string") {
3047
+ const v = flags["autonomy"].toLowerCase();
3048
+ autonomyPinned = v === "off" || v === "no" || v === "false" ? "off" : "auto";
3049
+ } else if (flags["autonomy"] === true) {
3050
+ autonomyPinned = "auto";
3051
+ }
3052
+ const lastChoices = config.launch ? {
3053
+ mode: config.launch.mode ?? "tui",
3054
+ yolo: config.yolo ?? true,
3055
+ autonomy: config.launch.autonomy ?? "auto"
3056
+ } : void 0;
3057
+ let choices;
3058
+ try {
3059
+ choices = await runLaunchPrompts({
3060
+ renderer,
3061
+ reader,
3062
+ modePinned,
3063
+ yoloPinned,
3064
+ autonomyPinned,
3065
+ lastChoices
3066
+ });
3067
+ } catch (err) {
3068
+ if (err instanceof LaunchAbortedError) {
3069
+ await reader.close();
3070
+ return 0;
3071
+ }
3072
+ throw err;
3073
+ }
3074
+ if (choices.mode === "tui") {
3075
+ flags["tui"] = true;
3076
+ flags["no-tui"] = false;
3077
+ } else {
3078
+ flags["tui"] = false;
3079
+ flags["no-tui"] = true;
3080
+ }
3081
+ if (choices.yolo !== config.yolo) config = patchConfig(config, { yolo: choices.yolo });
3082
+ flags["autonomy"] = choices.autonomy;
3083
+ if (shouldPrintYoloNotice(lastChoices, yoloPinned, choices.yolo)) {
3084
+ writeErr(
3085
+ `
3086
+ ${color11.yellow("YOLO is on")}: non-denied tool calls, including shell and file writes, run without confirmation.
3087
+ ${color11.dim("Explicit deny rules still apply. Use")} --no-yolo ${color11.dim("or")} /yolo off ${color11.dim("to require prompts.")}
3088
+
3089
+ `
3090
+ );
3091
+ }
3092
+ if ((flags["skip-index"] || flags["skip"]) && config.indexing) {
3093
+ config = patchConfig(config, {
3094
+ indexing: { ...config.indexing, onSessionStart: false }
3095
+ });
3096
+ }
3097
+ if (!simpleUiFullAuto) {
3098
+ try {
3099
+ const toPersist = flags["webui"] ? { ...choices, mode: lastChoices?.mode ?? config.launch?.mode ?? "tui" } : choices;
3100
+ await persistLaunchChoices(profileConfigPath, toPersist);
3101
+ } catch {
3102
+ }
3103
+ }
3104
+ await printLaunchHints(renderer, flags, {
3105
+ cursorFile: path8.join(wpaths.cacheDir, "hint-cursor")
3106
+ });
3107
+ } else {
3108
+ const nonInteractiveAutonomy = !simpleUiFullAuto && (flags["no-autonomy"] === true || config.autonomy?.defaultMode === "off") ? "off" : "auto";
3109
+ const effectiveChoices = config.launch ? {
3110
+ mode: flags["no-tui"] ? "repl" : config.launch.mode ?? "tui",
3111
+ yolo: config.yolo ?? true,
3112
+ autonomy: nonInteractiveAutonomy
3113
+ } : {
3114
+ mode: "repl",
3115
+ yolo: true,
3116
+ autonomy: nonInteractiveAutonomy
3117
+ };
3118
+ if (effectiveChoices.mode === "repl") {
3119
+ flags["tui"] = false;
3120
+ flags["no-tui"] = true;
3121
+ }
3122
+ flags["autonomy"] = effectiveChoices.autonomy;
3123
+ }
3124
+ flags["director"] = true;
3125
+ return {
3126
+ config,
3127
+ vault,
3128
+ wpaths,
3129
+ cwd,
3130
+ projectRoot,
3131
+ userHome,
3132
+ flags,
3133
+ positional,
3134
+ modelsRegistry,
3135
+ renderer,
3136
+ reader,
3137
+ logger,
3138
+ needsSetup: !noInteractiveMode ? false : !config.provider || !config.model
3139
+ };
3140
+ }
3141
+ async function checkGitInCwd(opts) {
3142
+ const { cwd, renderer, reader } = opts;
3143
+ const cwdGit = path8.join(cwd, ".git");
3144
+ let hasCwdGit = false;
3145
+ try {
3146
+ await fs7.access(cwdGit);
3147
+ hasCwdGit = true;
3148
+ } catch {
3149
+ }
3150
+ if (!hasCwdGit) {
3151
+ renderer.write(
3152
+ `
3153
+ ${color11.amber("\u25CB")} This folder has no ${color11.bold(".git")} repository.
3154
+ `
3155
+ );
3156
+ const answer = (await reader.readLine(` ${color11.amber("?")} Initialize one here? ${color11.dim("[y/N]")} `)).trim().toLowerCase();
3157
+ if (answer === "y" || answer === "yes") {
3158
+ try {
3159
+ const { spawn: spawn2 } = await import("node:child_process");
3160
+ await new Promise((resolve2, reject) => {
3161
+ const child = spawn2("git", ["init"], {
3162
+ cwd,
3163
+ signal: AbortSignal.timeout(1e4),
3164
+ windowsHide: true
3165
+ });
3166
+ child.on("error", reject);
3167
+ child.on(
3168
+ "close",
3169
+ (code) => code === 0 ? resolve2() : reject(new Error(`git init failed with ${code}`))
3170
+ );
3171
+ });
3172
+ renderer.write(` ${color11.green("\u2713")} Git repository initialized
3173
+ `);
3174
+ hasCwdGit = true;
3175
+ } catch (err) {
3176
+ renderer.writeError(`git init failed: ${toErrorMessage4(err)}
3177
+ `);
3178
+ }
3179
+ }
3180
+ }
3181
+ const parentDir = path8.dirname(cwd);
3182
+ if (parentDir !== cwd) {
3183
+ try {
3184
+ await fs7.access(path8.join(parentDir, ".git"));
3185
+ renderer.write(
3186
+ ` ${color11.dim("\u2139")} A ${color11.bold(".git")} repo exists in the parent directory: ${color11.dim(parentDir)}
3187
+ `
3188
+ );
3189
+ } catch {
3190
+ }
3191
+ }
3192
+ }
3193
+
3194
+ // src/cli-context.ts
3195
+ async function initializeCli(argv) {
3196
+ applyNodeEnvDefault();
3197
+ applySessionShellDefault();
3198
+ const earlyFlags = parseArgs(argv).flags;
3199
+ const earlyExit = await handleHelpVersionShortCircuit(argv);
3200
+ if (earlyExit !== null) return earlyExit;
3201
+ const desktopExit = await handleDesktopShortCircuit(earlyFlags, argv);
3202
+ if (desktopExit !== null) return desktopExit;
3203
+ const { flags: _earlyForMenu, positional: _positionalForMenu } = parseArgs(argv);
3204
+ let webuiSessionChild;
3205
+ if (_earlyForMenu["webui-session-child"] === true) {
3206
+ try {
3207
+ webuiSessionChild = parseWebuiSessionChildOptions(_earlyForMenu) ?? void 0;
3208
+ } catch (err) {
3209
+ const readyFile = typeof _earlyForMenu["ready-file"] === "string" ? _earlyForMenu["ready-file"] : void 0;
3210
+ if (readyFile) {
3211
+ await writeWebuiSessionChildError(readyFile, {
3212
+ runtimeId: typeof _earlyForMenu["runtime-id"] === "string" ? _earlyForMenu["runtime-id"] : void 0,
3213
+ parentShellId: typeof _earlyForMenu["parent-shell-id"] === "string" ? _earlyForMenu["parent-shell-id"] : void 0,
3214
+ phase: "validate_args",
3215
+ recoverable: false,
3216
+ error: err
3217
+ }).catch(() => void 0);
3218
+ }
3219
+ writeErr2(
3220
+ `WebUI session child argument error: ${err instanceof Error ? err.message : String(err)}
3221
+ `
3222
+ );
3223
+ return 2;
3224
+ }
3225
+ }
3226
+ const launchMenuReader = new ReadlineInputReader();
3227
+ let menuResult;
3228
+ try {
3229
+ menuResult = await runLaunchMenu({
3230
+ argv,
3231
+ flags: _earlyForMenu,
3232
+ positional: _positionalForMenu,
3233
+ renderer: new TerminalRenderer(),
3234
+ reader: launchMenuReader,
3235
+ // Last-saved menu choice wiring is intentionally minimal here:
3236
+ // we only honor it when the user has *also* chosen no other
3237
+ // surface flag. The persisted record is updated lazily inside
3238
+ // boot() via a follow-up patch in the wiring layer, but the
3239
+ // menu itself stays stateless so unit tests don't need to mock
3240
+ // a ConfigStore. Users who want the saved-port shortcut without
3241
+ // the menu can pass `--no-menu --webui --port=<saved>`.
3242
+ lastChoice: void 0
3243
+ });
3244
+ } finally {
3245
+ await launchMenuReader.close();
3246
+ }
3247
+ let effectiveArgv = argv;
3248
+ if (menuResult !== null) {
3249
+ if (menuResult.cancelled) {
3250
+ return 0;
3251
+ }
3252
+ effectiveArgv = applyLaunchMenuToArgv(argv, menuResult);
3253
+ const augmentedFlags = parseArgs(effectiveArgv).flags;
3254
+ if (menuResult.mode === "hq") {
3255
+ const hqAfterMenu = await handleHqShortCircuit(augmentedFlags);
3256
+ if (hqAfterMenu !== null) return hqAfterMenu;
3257
+ }
3258
+ if (menuResult.mode === "desktop") {
3259
+ const desktopAfterMenu = await handleDesktopShortCircuit(augmentedFlags, effectiveArgv);
3260
+ if (desktopAfterMenu !== null) return desktopAfterMenu;
3261
+ }
3262
+ }
3263
+ const effectiveFlags = parseArgs(effectiveArgv).flags;
3264
+ try {
3265
+ webuiSessionChild = webuiSessionChild ?? parseWebuiSessionChildOptions(effectiveFlags) ?? void 0;
3266
+ } catch (err) {
3267
+ const readyFile = typeof effectiveFlags["ready-file"] === "string" ? effectiveFlags["ready-file"] : void 0;
3268
+ if (readyFile) {
3269
+ await writeWebuiSessionChildError(readyFile, {
3270
+ runtimeId: typeof effectiveFlags["runtime-id"] === "string" ? effectiveFlags["runtime-id"] : void 0,
3271
+ parentShellId: typeof effectiveFlags["parent-shell-id"] === "string" ? effectiveFlags["parent-shell-id"] : void 0,
3272
+ phase: "validate_args",
3273
+ recoverable: false,
3274
+ error: err
3275
+ }).catch(() => void 0);
3276
+ }
3277
+ writeErr2(
3278
+ `WebUI session child argument error: ${err instanceof Error ? err.message : String(err)}
3279
+ `
3280
+ );
3281
+ return 2;
3282
+ }
3283
+ if (webuiSessionChild) {
3284
+ try {
3285
+ process.chdir(webuiSessionChild.workingDir);
3286
+ if (webuiSessionChild.resume && webuiSessionChild.sessionId) {
3287
+ const currentResume = effectiveFlags["resume"];
3288
+ if (typeof currentResume === "string" && currentResume !== webuiSessionChild.sessionId) {
3289
+ throw new Error(
3290
+ `--resume ${currentResume} conflicts with --session-id ${webuiSessionChild.sessionId}`
3291
+ );
3292
+ }
3293
+ if (currentResume !== webuiSessionChild.sessionId) {
3294
+ effectiveArgv = [...effectiveArgv, "--resume", webuiSessionChild.sessionId];
3295
+ }
3296
+ }
3297
+ } catch (err) {
3298
+ await writeWebuiSessionChildError(webuiSessionChild.readyFile, {
3299
+ protocolVersion: webuiSessionChild.protocolVersion,
3300
+ runtimeId: webuiSessionChild.runtimeId,
3301
+ parentShellId: webuiSessionChild.parentShellId,
3302
+ phase: "validate_args",
3303
+ recoverable: false,
3304
+ error: err
3305
+ }).catch(() => void 0);
3306
+ writeErr2(
3307
+ `WebUI session child argument error: ${err instanceof Error ? err.message : String(err)}
3308
+ `
3309
+ );
3310
+ return 2;
3311
+ }
3312
+ }
3313
+ const hqExit = await handleHqShortCircuit(effectiveFlags);
3314
+ if (hqExit !== null) return hqExit;
3315
+ const ctx = await boot(effectiveArgv);
3316
+ if (typeof ctx === "number") return ctx;
3317
+ if (menuResult !== null) {
3318
+ const persisted = toPersistedMenuChoice(menuResult);
3319
+ if (persisted) {
3320
+ try {
3321
+ await persistMenuChoice(activeProfileConfigPath(ctx.wpaths, ctx.config), persisted);
3322
+ } catch (err) {
3323
+ ctx.logger.debug(
3324
+ `launch-menu: failed to persist menu choice (${err instanceof Error ? err.message : String(err)})`
3325
+ );
3326
+ }
3327
+ }
3328
+ }
3329
+ const { config, vault, wpaths, cwd, modelsRegistry, renderer, reader, logger } = ctx;
3330
+ const { updateInfo: refreshedUpdateInfo } = await runPreflight(config, ctx.updateInfo);
3331
+ const profileConfigPath = activeProfileConfigPath(wpaths, config);
3332
+ setOAuthTokenPersister((providerId, creds) => {
3333
+ void mutateConfigProviders(profileConfigPath, vault, (all) => {
3334
+ const p = all[providerId];
3335
+ if (!p) return;
3336
+ const keys = normalizeKeys(p);
3337
+ const active = p.activeKey ? keys.find((k) => k.label === p.activeKey) : keys[0];
3338
+ if (!active) return;
3339
+ active.apiKey = creds.accessToken;
3340
+ active.refreshToken = creds.refreshToken;
3341
+ active.expiresAt = new Date(creds.expiresAt).toISOString();
3342
+ if (creds.accountId) active.accountId = creds.accountId;
3343
+ writeKeysBack(p, keys);
3344
+ }).catch(() => {
3345
+ });
3346
+ });
3347
+ const { events, container } = wireContainer({
3348
+ config,
3349
+ wpaths,
3350
+ cwd,
3351
+ logger,
3352
+ reader,
3353
+ renderer,
3354
+ modelsRegistry
3355
+ });
3356
+ const configStore = container.resolve(TOKENS3.ConfigStore);
3357
+ return {
3358
+ ...ctx,
3359
+ updateInfo: refreshedUpdateInfo,
3360
+ events,
3361
+ container,
3362
+ configStore,
3363
+ webuiSessionChild
3364
+ };
3365
+ }
3366
+ export {
3367
+ initializeCli
3368
+ };
3369
+ //# sourceMappingURL=cli-context-LH4QP4W2.js.map