@wrongstack/cli 0.318.0 → 0.319.1

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 (56) 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-ND7J7DTD.js} +10 -10
  5. package/dist/{chunk-QC5I4VWZ.js → chunk-635WW3II.js} +8 -14
  6. package/dist/{chunk-7CECQDNH.js → chunk-D3K4NYQL.js} +1 -1
  7. package/dist/{chunk-QYIIOMLE.js → chunk-E4I2TF5S.js} +2 -2
  8. package/dist/{chunk-4JRIWVJ4.js → chunk-ERX4BLKP.js} +1 -1
  9. package/dist/{chunk-37SPPLUD.js → chunk-F5GA2QW2.js} +15 -2
  10. package/dist/chunk-GGGUZSAU.js +235 -0
  11. package/dist/{chunk-GKHUHAIK.js → chunk-HF5VQWIY.js} +5 -5
  12. package/dist/{chunk-YX6KUIDF.js → chunk-IMT6PNV5.js} +861 -29
  13. package/dist/{chunk-G7337TTK.js → chunk-IS2PPBQV.js} +1 -1
  14. package/dist/{chunk-G3IMKQRH.js → chunk-JHPXJRRI.js} +1 -1
  15. package/dist/chunk-KYDJJLJI.js +52 -0
  16. package/dist/chunk-LIWVIEZM.js +46 -0
  17. package/dist/chunk-P2OKBDCH.js +1166 -0
  18. package/dist/chunk-PCBJSXYV.js +65 -0
  19. package/dist/{chunk-7JBKXVMK.js → chunk-TALOKMIC.js} +14 -18
  20. package/dist/{chunk-KG4X5F7Z.js → chunk-VFCJOWJB.js} +10 -12
  21. package/dist/{chunk-SYIBTG3X.js → chunk-VOTTENR5.js} +3 -5
  22. package/dist/chunk-WHOIR577.js +1 -0
  23. package/dist/{chunk-2ZNBRZ4W.js → chunk-WQZW3RDP.js} +2 -2
  24. package/dist/{chunk-BXFYBQKR.js → chunk-XPX432BZ.js} +8 -1
  25. package/dist/cli-context-K73EIND5.js +3369 -0
  26. package/dist/{cli-main-ILRTNPDP.js → cli-main-QXSKKANW.js} +84 -93
  27. package/dist/{diag-doctor-LFCCBPMD.js → diag-doctor-QZVKUAE5.js} +4 -5
  28. package/dist/{execution-BN4QIFH6.js → execution-7ZFY4FJY.js} +499 -473
  29. package/dist/{hq-3GWBFLON.js → hq-4QQJO2GN.js} +2 -2
  30. package/dist/hq-server/routes.d.ts +1 -1
  31. package/dist/{hq-server-OARPUSQJ.js → hq-server-CVAYICIS.js} +2 -2
  32. package/dist/index.js +20 -4440
  33. package/dist/{modeldiag-4NYBRSFP.js → modeldiag-ZL4ROTQ5.js} +2 -2
  34. package/dist/per-subcommand-help-6VCIKDUW.js +23 -0
  35. package/dist/{plugin-usage-AP7SJCDA.js → plugin-usage-7WCZW4NP.js} +2 -2
  36. package/dist/{plugins-HQ5FU2AM.js → plugins-7N3TF2QF.js} +3 -3
  37. package/dist/pre-launch/first-run.d.ts +51 -0
  38. package/dist/pre-launch.d.ts +1 -0
  39. package/dist/{project-manifest-YFTJ3D5N.js → project-manifest-RV5FXGQI.js} +2 -2
  40. package/dist/{project-picker-R5TYAE3Q.js → project-picker-CCQR3W4I.js} +3 -3
  41. package/dist/{providers-models-663NDPRQ.js → providers-models-OKKQZIWU.js} +2 -2
  42. package/dist/{proxy-wiring-EQB5X5YH.js → proxy-wiring-QCI7EWQC.js} +3 -3
  43. package/dist/{sdd-runtime-FLKCSTYV.js → sdd-runtime-R6FAK2G2.js} +2 -2
  44. package/dist/services/sdd-runtime.d.ts +1 -1
  45. package/dist/{sessions-config-P2LK2L32.js → sessions-config-KAMEBVOX.js} +4 -4
  46. package/dist/short-circuit-flags-7YUCERBB.js +11 -0
  47. package/dist/slash-commands/sdd.d.ts +1 -1
  48. package/dist/subcommands-KE6MEQQP.js +10 -0
  49. package/dist/{webui-server-OJLLM6PE.js → webui-server-F7MSRHOF.js} +8 -6
  50. package/dist/wiring/wrongtrace-gate.d.ts +2 -2
  51. package/dist/wiring/wrongtrace-hooks.d.ts +1 -1
  52. package/dist/{wrongtrace-gate-counters-P5BCBXQN.js → wrongtrace-gate-counters-GSVY372D.js} +2 -2
  53. package/dist/{wrongtrace-telemetry-FNLANZ5I.js → wrongtrace-telemetry-KBBM5NLQ.js} +2 -2
  54. package/package.json +28 -28
  55. package/dist/chunk-ALE5KHCA.js +0 -1
  56. package/dist/chunk-L6ZNCTRJ.js +0 -1067
package/dist/index.js CHANGED
@@ -1,4457 +1,37 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- applySimpleUiFullAutoProfile,
4
- detectProjectFacts,
5
- isSimpleUiFullAuto,
6
- makePromptDelegate,
7
- renderAgentsTemplate,
8
- renderDiff
9
- } from "./chunk-KG4X5F7Z.js";
3
+ applyNodeEnvDefault,
4
+ applySessionShellDefault
5
+ } from "./chunk-PCBJSXYV.js";
10
6
  import {
11
- theme
12
- } from "./chunk-QD544J2B.js";
13
- import {
14
- fmtTok,
15
- patchConfig
16
- } from "./chunk-TYO2OVAD.js";
17
- import {
18
- parseWebuiSessionChildOptions,
19
- writeWebuiSessionChildError
20
- } from "./chunk-GPPRNAKW.js";
21
- import {
22
- discoverAndMergeProviders
23
- } from "./chunk-52C7PD2H.js";
24
- import {
25
- runUpdateCommand
26
- } from "./chunk-DE7BZDNK.js";
27
- import "./chunk-U3SS4NRH.js";
28
- import "./chunk-YLQEGYV4.js";
29
- import {
30
- DEFAULT_PORT
31
- } from "./chunk-7JBKXVMK.js";
32
- import "./chunk-HXJYHAR4.js";
33
- import "./chunk-Q5GTM25S.js";
34
- import {
35
- helpCmd,
36
- versionCmd
37
- } from "./chunk-QAC263FZ.js";
38
- import {
39
- CLI_VERSION
40
- } from "./chunk-XJXDOF63.js";
41
- import {
42
- renderAuthLocalHelpToString
43
- } from "./chunk-2VMJ3E5Z.js";
44
- import {
45
- parseArgs,
46
- runPicker,
47
- saveToGlobalConfig
48
- } from "./chunk-L6ZNCTRJ.js";
49
- import "./chunk-B4JLSXZB.js";
50
- import {
51
- isKeylessLocalProvider,
52
- visibleModelIds
53
- } from "./chunk-VUVOMXWP.js";
54
- import {
55
- activeProfileConfigPath
56
- } from "./chunk-YMXXOOFN.js";
57
- import {
58
- mutateConfigProviders,
59
- normalizeKeys,
60
- resolveActiveApiKey,
61
- writeKeysBack
62
- } from "./chunk-BXFYBQKR.js";
63
- import "./chunk-7OCVIDC7.js";
64
-
65
- // src/cli-context.ts
66
- import { TOKENS as TOKENS3 } from "@wrongstack/core/kernel";
67
- import { writeErr as writeErr3 } from "@wrongstack/core/utils";
68
- import { setOAuthTokenPersister } from "@wrongstack/providers";
69
-
70
- // src/boot/container-wiring.ts
71
- import { DefaultPathResolver } from "@wrongstack/core/infrastructure";
72
- import { EventBus as CoreEventBus, TOKENS } from "@wrongstack/core/kernel";
73
- import { createDefaultContainer } from "@wrongstack/runtime";
74
-
75
- // src/cli-bundled-prompts.ts
76
- import { createRequire } from "node:module";
77
- import * as path from "node:path";
78
- function resolveBundledPromptsDir() {
79
- try {
80
- const req = createRequire(import.meta.url);
81
- const corePkg = req.resolve("@wrongstack/core/package.json");
82
- return path.join(path.dirname(corePkg), "data", "prompts");
83
- } catch {
84
- return void 0;
85
- }
86
- }
87
-
88
- // src/cli-bundled-skills.ts
89
- import { createRequire as createRequire2 } from "node:module";
90
- import * as path2 from "node:path";
91
- function resolveBundledSkillsDir() {
92
- try {
93
- const req = createRequire2(import.meta.url);
94
- const corePkg = req.resolve("@wrongstack/core/package.json");
95
- return path2.join(path2.dirname(corePkg), "skills");
96
- } catch {
97
- return void 0;
98
- }
99
- }
100
-
101
- // src/boot/container-wiring.ts
102
- function wireContainer(deps) {
103
- const pathResolver = new DefaultPathResolver(deps.cwd);
104
- const events = new CoreEventBus();
105
- events.setLogger(deps.logger);
106
- const container = createDefaultContainer({
107
- config: deps.config,
108
- wpaths: deps.wpaths,
109
- logger: deps.logger,
110
- modelsRegistry: deps.modelsRegistry,
111
- events,
112
- permission: {
113
- yolo: deps.config.yolo,
114
- promptDelegate: makePromptDelegate(deps.reader)
115
- },
116
- compactor: {
117
- preserveK: deps.config.context.preserveK,
118
- eliseThreshold: deps.config.context.eliseThreshold
119
- },
120
- bundledSkillsDir: deps.config.features.skills ? resolveBundledSkillsDir() : void 0,
121
- bundledPromptsDir: deps.config.features.prompts === false ? void 0 : resolveBundledPromptsDir()
122
- });
123
- container.bind(TOKENS.PathResolver, () => pathResolver);
124
- container.bind(TOKENS.Renderer, () => deps.renderer);
125
- container.bind(TOKENS.InputReader, () => deps.reader);
126
- return { pathResolver, events, container };
127
- }
128
-
129
- // src/boot/launch-menu.ts
130
- import * as fs from "node:fs/promises";
131
- import { HQ_CLI_DEFAULT_HOST } from "@wrongstack/core/hq";
132
- import { atomicWrite, color, isStdinTTY, setRawMode } from "@wrongstack/core/utils";
133
-
134
- // src/boot/launch-banner.ts
135
- var STACK_ORANGE = { r: 253, g: 159, b: 2 };
136
- var SIGNAL_PINK = { r: 254, g: 46, b: 95 };
137
- function mixRgb(a, b, t) {
138
- const clamped = Math.min(1, Math.max(0, t));
139
- return {
140
- r: Math.round(a.r + (b.r - a.r) * clamped),
141
- g: Math.round(a.g + (b.g - a.g) * clamped),
142
- b: Math.round(a.b + (b.b - a.b) * clamped)
143
- };
144
- }
145
- function fg(c) {
146
- return `\x1B[38;2;${c.r};${c.g};${c.b}m`;
147
- }
148
- function colorsEnabled() {
149
- const flag = (value) => value !== void 0 && value.trim() !== "" && !/^(0|false|no|off)$/i.test(value.trim());
150
- if (flag(process.env.NO_COLOR)) return false;
151
- if (flag(process.env.FORCE_COLOR)) return true;
152
- return process.stdout.isTTY === true;
153
- }
154
- var WORDMARK_GLYPH_BITS = Object.freeze({
155
- W: Object.freeze(["10001", "10001", "10001", "10101", "10101", "10101", "01010"]),
156
- R: Object.freeze(["11110", "10001", "10001", "11110", "10100", "10010", "10001"]),
157
- O: Object.freeze(["01110", "10001", "10001", "10001", "10001", "10001", "01110"]),
158
- N: Object.freeze(["10001", "11001", "11001", "10101", "10011", "10011", "10001"]),
159
- G: Object.freeze(["01110", "10001", "10000", "10111", "10001", "10001", "01110"]),
160
- S: Object.freeze(["01111", "10000", "10000", "01110", "00001", "00001", "11110"]),
161
- T: Object.freeze(["11111", "00100", "00100", "00100", "00100", "00100", "00100"]),
162
- A: Object.freeze(["01110", "10001", "10001", "11111", "10001", "10001", "10001"]),
163
- C: Object.freeze(["01111", "10000", "10000", "10000", "10000", "10000", "01111"]),
164
- K: Object.freeze(["10001", "10010", "10100", "11000", "10100", "10010", "10001"])
165
- });
166
- var WORDMARK = "WRONGSTACK";
167
- var WORDMARK_PIXEL_ROWS = 7;
168
- var WORDMARK_ROWS = Math.ceil(WORDMARK_PIXEL_ROWS / 2);
169
- var GLYPH_WIDTH = 5;
170
- var WORDMARK_WIDTH = WORDMARK.length * GLYPH_WIDTH + WORDMARK.length - 1;
171
- function packedCell(top, bottom) {
172
- if (top === "1" && bottom === "1") return "\u2588";
173
- if (top === "1") return "\u2580";
174
- if (bottom === "1") return "\u2584";
175
- return " ";
176
- }
177
- function packedGlyphRow(glyph, row) {
178
- const top = glyph[row * 2] ?? "";
179
- const bottom = glyph[row * 2 + 1] ?? "";
180
- return Array.from(
181
- { length: GLYPH_WIDTH },
182
- (_, column) => packedCell(top[column], bottom[column])
183
- ).join("");
184
- }
185
- var WORDMARK_LINES = Object.freeze(
186
- Array.from(
187
- { length: WORDMARK_ROWS },
188
- (_, row) => [...WORDMARK].map((letter) => {
189
- const glyph = WORDMARK_GLYPH_BITS[letter];
190
- return glyph ? packedGlyphRow(glyph, row) : " ".repeat(GLYPH_WIDTH);
191
- }).join(" ")
192
- )
193
- );
194
- function gradientLine(text, colorOn) {
195
- if (!colorOn) return text;
196
- const length = text.length;
197
- let out = "\x1B[1m";
198
- for (let i = 0; i < length; i++) {
199
- const ch = text[i];
200
- if (ch === " ") {
201
- out += ch;
202
- continue;
203
- }
204
- const t = length > 1 ? i / (length - 1) : 0.5;
205
- out += `${fg(mixRgb(STACK_ORANGE, SIGNAL_PINK, t))}${ch}`;
206
- }
207
- return `${out}\x1B[0m`;
208
- }
209
- function dimLine(text, colorOn) {
210
- return colorOn ? `\x1B[2m${text}\x1B[22m` : text;
211
- }
212
- var MARK_COLS = 5;
213
- var PINK_HOME = 1;
214
- var MARK_WIDTH = MARK_COLS * 2 + (MARK_COLS - 1);
215
- function buildMarkLines(colorOn) {
216
- const orange = colorOn ? fg(STACK_ORANGE) : "";
217
- const pink = colorOn ? fg(SIGNAL_PINK) : "";
218
- const reset = colorOn ? "\x1B[0m" : "";
219
- return Array.from({ length: 3 }, (_, row) => {
220
- const cells = Array.from({ length: MARK_COLS }, (_2, column) => {
221
- if (column === PINK_HOME && (row === 1 || row === 2)) return `${pink}\u2588\u2588${reset}`;
222
- if (row < 2 && column !== PINK_HOME) return `${orange}\u2588\u2588${reset}`;
223
- return " ";
224
- });
225
- return cells.join(" ").trimEnd();
226
- });
227
- }
228
- function launchBannerLines(columns, version) {
229
- const colorOn = colorsEnabled();
230
- const tagline = version ? `v${version} \xB7 the wrong stack for all the right reasons` : "the wrong stack for all the right reasons";
231
- if (columns < WORDMARK_WIDTH + 4) {
232
- const compact = "\u2590\u2588\u258C W R O N G S T A C K";
233
- return [gradientLine(compact, colorOn), dimLine(tagline, colorOn)];
234
- }
235
- const markPad = " ".repeat(Math.max(0, Math.floor((WORDMARK_WIDTH - MARK_WIDTH) / 2)));
236
- const tagPad = " ".repeat(Math.max(0, Math.floor((WORDMARK_WIDTH - tagline.length) / 2)));
237
- return [
238
- ...buildMarkLines(colorOn).map((line) => markPad + line),
239
- "",
240
- ...WORDMARK_LINES.map((line) => gradientLine(line, colorOn)),
241
- tagPad + dimLine(tagline, colorOn)
242
- ];
243
- }
244
-
245
- // src/boot/launch-menu.ts
246
- var DEFAULT_PORTS = {
247
- webui: 3456,
248
- simpleui: 3466,
249
- hq: DEFAULT_PORT
250
- };
251
- var DEFAULT_HOST = "127.0.0.1";
252
- var HQ_DEFAULT_HOST = HQ_CLI_DEFAULT_HOST;
253
- function bindsPort(mode) {
254
- return mode !== "tui-repl" && mode !== "desktop";
255
- }
256
- function defaultHostFor(mode) {
257
- return mode === "hq" ? HQ_DEFAULT_HOST : DEFAULT_HOST;
258
- }
259
- var MODE_OPTIONS = [
260
- {
261
- key: 1,
262
- mode: "tui-repl",
263
- label: "TUI / REPL",
264
- hint: "interactive terminal (default)",
265
- icon: "\u2328"
266
- },
267
- {
268
- key: 2,
269
- mode: "webui",
270
- label: "WebUI",
271
- hint: `browser-based project UI (port ${DEFAULT_PORTS.webui})`,
272
- icon: "\u{1F310}"
273
- },
274
- {
275
- key: 3,
276
- mode: "simpleui",
277
- label: "SimpleUI",
278
- hint: `lightweight browser UI (port ${DEFAULT_PORTS.simpleui})`,
279
- icon: "\u{1F4C4}"
280
- },
281
- {
282
- key: 4,
283
- mode: "hq",
284
- label: "HQ",
285
- hint: `project-independent dashboard (port ${DEFAULT_PORT})`,
286
- icon: "\u{1F4CA}"
287
- },
288
- {
289
- key: 5,
290
- mode: "desktop",
291
- label: "Desktop",
292
- hint: "Electron app (alias: --desktop)",
293
- icon: "\u{1F5A5}"
294
- }
295
- ];
296
- var MENU_TIMEOUT_MS = 8e3;
297
- var SUMMARY_TIMEOUT_MS = 5e3;
298
- var PORT_TIMEOUT_MS = 8e3;
299
- function shouldSkipMenu(argv, flags, positional) {
300
- if (flags["no-menu"] === true) return true;
301
- if (flags["webui"] === true) return true;
302
- if (flags["webui-session-child"] === true) return true;
303
- if (flags["simpleui"] === true) return true;
304
- if (flags["hq"] === true) return true;
305
- if (flags["desktop"] === true) return true;
306
- if (flags["no-interactive"] === true) return true;
307
- if (flags["skip"] === true) return true;
308
- if (typeof flags["prompt"] === "string") return true;
309
- if (!isStdinTTY()) return true;
310
- if (positional.length > 0) return true;
311
- void argv;
312
- return false;
313
- }
314
- async function runLaunchMenu(deps) {
315
- if (shouldSkipMenu(deps.argv, deps.flags, deps.positional)) {
316
- return null;
317
- }
318
- const { renderer, reader, lastChoice } = deps;
319
- const ports = { ...DEFAULT_PORTS, ...deps.defaultPorts ?? {} };
320
- writeBanner(renderer);
321
- if (lastChoice) {
322
- const accept = await promptSummaryGate(deps);
323
- if (accept === "use-last") {
324
- return finalize(lastChoice, ports);
325
- }
326
- if (accept === "cancel") {
327
- return { ...lastChoice, cancelled: true };
328
- }
329
- }
330
- renderer.write(`
331
- ${color.amber("?")} Choose how to run WrongStack:
332
-
333
- `);
334
- const arrowPick = await promptModeArrow(deps);
335
- let choice;
336
- if (arrowPick) {
337
- if (arrowPick.cancelled) {
338
- return { mode: "tui-repl", cancelled: true };
339
- }
340
- choice = { mode: arrowPick.mode };
341
- } else {
342
- for (const opt of MODE_OPTIONS) {
343
- renderer.write(
344
- ` ${color.bold(String(opt.key))}) ${opt.icon} ${color.bold(opt.label)} ${color.dim(opt.hint)}
345
- `
346
- );
347
- }
348
- renderer.write(`
349
- ${color.dim("\u2500".repeat(48))}
350
- `);
351
- const answer = (await reader.readLine(
352
- ` ${color.amber("?")} Mode ${color.dim("[1-5, q to quit]")} ${color.dim(`(auto 1 in ${MENU_TIMEOUT_MS / 1e3}s)`)} `,
353
- { timeoutMs: MENU_TIMEOUT_MS, defaultAnswer: "1" }
354
- )).trim().toLowerCase();
355
- if (answer === "q" || answer === "quit") {
356
- renderer.write(color.dim(" Goodbye!\n"));
357
- return { mode: "tui-repl", cancelled: true };
358
- }
359
- const picked = MODE_OPTIONS.find((o) => String(o.key) === answer || o.mode === answer);
360
- choice = picked ? { mode: picked.mode } : { mode: "tui-repl" };
361
- }
362
- if (bindsPort(choice.mode)) {
363
- const port = await promptPort(deps, ports[choice.mode]);
364
- choice.port = port;
365
- const defaultHost = defaultHostFor(choice.mode);
366
- const host = await promptHost(deps, defaultHost);
367
- if (host !== defaultHost) choice.host = host;
368
- }
369
- return finalize(choice, ports);
370
- }
371
- function writeBanner(renderer) {
372
- const columns = process.stdout.columns ?? 80;
373
- renderer.write("\n");
374
- for (const line of launchBannerLines(columns, CLI_VERSION)) {
375
- renderer.write(` ${line}
376
- `);
377
- }
378
- }
379
- function canUseArrowMenu() {
380
- if (process.env.VITEST || process.env.NODE_ENV === "test") return false;
381
- const stdin = process.stdin;
382
- return isStdinTTY() && process.stdout.isTTY === true && typeof stdin.setRawMode === "function";
383
- }
384
- async function promptModeArrow(deps) {
385
- if (!canUseArrowMenu()) return null;
386
- const { renderer } = deps;
387
- const stdin = process.stdin;
388
- const out = (s) => renderer.write(s);
389
- return await new Promise((resolve3) => {
390
- let index = 0;
391
- let drawn = 0;
392
- let remaining = Math.round(MENU_TIMEOUT_MS / 1e3);
393
- let countdown = true;
394
- let settled = false;
395
- let interval;
396
- const wasRaw = stdin.isRaw === true;
397
- const wasPaused = stdin.isPaused();
398
- const rows = () => {
399
- const lines = [];
400
- for (let i = 0; i < MODE_OPTIONS.length; i++) {
401
- const opt = MODE_OPTIONS[i];
402
- const selected = i === index;
403
- const pointer = selected ? color.pink("\u276F") : " ";
404
- const label = selected ? color.amber(color.bold(opt.label)) : color.bold(opt.label);
405
- lines.push(
406
- ` ${pointer} ${color.dim(String(opt.key))} ${opt.icon} ${label} ${color.dim(opt.hint)}`
407
- );
408
- }
409
- lines.push("");
410
- const auto = countdown ? color.dim(` \xB7 auto-launches ${MODE_OPTIONS[index].label} in ${remaining}s`) : "";
411
- lines.push(` ${color.dim("\u2191\u2193 move \xB7 Enter launch \xB7 1-5 jump \xB7 q quit")}${auto}`);
412
- return lines;
413
- };
414
- const paint = () => {
415
- const lines = rows();
416
- let frame = drawn > 0 ? `\x1B[${drawn}A` : "";
417
- frame += `${lines.map((line) => `\x1B[2K${line}`).join("\n")}
418
- `;
419
- out(frame);
420
- drawn = lines.length;
421
- };
422
- const stopInput = () => {
423
- if (interval) {
424
- clearInterval(interval);
425
- interval = void 0;
426
- }
427
- stdin.off("data", onData);
428
- stdin.off("close", onClose);
429
- try {
430
- setRawMode(stdin, wasRaw);
431
- } catch {
432
- }
433
- if (wasPaused) stdin.pause();
434
- };
435
- const settle = (result, finalLine) => {
436
- if (settled) return;
437
- settled = true;
438
- stopCountdown();
439
- stopInput();
440
- if (drawn > 0) out(`\x1B[${drawn}A`);
441
- out("\x1B[0J");
442
- out(finalLine);
443
- out("\x1B[?25h");
444
- resolve3(result);
445
- };
446
- const pick = () => {
447
- const opt = MODE_OPTIONS[index];
448
- settle(
449
- { cancelled: false, mode: opt.mode },
450
- ` ${color.green("\u2714")} ${opt.icon} ${color.bold(opt.label)} ${color.dim(opt.hint)}
451
- `
452
- );
453
- };
454
- const cancel = () => {
455
- settle({ cancelled: true, mode: "tui-repl" }, color.dim(" Goodbye!\n"));
456
- };
457
- const stopCountdown = () => {
458
- if (!countdown) return;
459
- countdown = false;
460
- if (interval) {
461
- clearInterval(interval);
462
- interval = void 0;
463
- }
464
- };
465
- const onData = (buf) => {
466
- const key = buf.toString("utf8");
467
- stopCountdown();
468
- if (key === "" || key === "q" || key === "Q" || key === "\x1B") {
469
- cancel();
470
- return;
471
- }
472
- if (key === "\r" || key === "\n") {
473
- pick();
474
- return;
475
- }
476
- if (key === "\x1B[A" || key === "\x1BOA" || key === "k") {
477
- index = (index + MODE_OPTIONS.length - 1) % MODE_OPTIONS.length;
478
- paint();
479
- return;
480
- }
481
- if (key === "\x1B[B" || key === "\x1BOB" || key === "j") {
482
- index = (index + 1) % MODE_OPTIONS.length;
483
- paint();
484
- return;
485
- }
486
- if (/^[0-9]$/.test(key)) {
487
- const digit = Number.parseInt(key, 10);
488
- if (digit >= 1 && digit <= MODE_OPTIONS.length) {
489
- index = digit - 1;
490
- pick();
491
- return;
492
- }
493
- }
494
- paint();
495
- };
496
- const onClose = () => {
497
- settle({ cancelled: true, mode: "tui-repl" }, "");
498
- };
499
- try {
500
- setRawMode(stdin, true);
501
- stdin.resume();
502
- } catch {
503
- resolve3(null);
504
- return;
505
- }
506
- out("\x1B[?25l");
507
- paint();
508
- stdin.on("data", onData);
509
- stdin.once("close", onClose);
510
- interval = setInterval(() => {
511
- remaining -= 1;
512
- if (remaining <= 0) {
513
- pick();
514
- return;
515
- }
516
- paint();
517
- }, 1e3);
518
- });
519
- }
520
- async function promptSummaryGate(deps) {
521
- const { renderer, reader, lastChoice } = deps;
522
- if (!lastChoice) return "re-prompt";
523
- const modeLabel = describeMode(lastChoice.mode);
524
- const portStr = typeof lastChoice.port === "number" ? ` \xB7 port ${color.bold(String(lastChoice.port))}` : "";
525
- const hostStr = typeof lastChoice.host === "string" ? ` \xB7 host ${color.dim(lastChoice.host)}` : "";
526
- renderer.write(`
527
- ${color.dim("\u2500".repeat(48))}
528
- `);
529
- renderer.write(
530
- ` ${color.cyan("\u23CE")} ${color.dim("Last settings:")} ${color.bold(modeLabel)}${portStr}${hostStr}
531
- `
532
- );
533
- renderer.write(` ${color.dim("\u2500".repeat(48))}
534
- `);
535
- const answer = (await reader.readLine(
536
- ` ${color.amber("?")} Continue with these? ${color.dim("[Y/n/q]")} ${color.dim("(auto Y in 5s)")} `,
537
- { timeoutMs: SUMMARY_TIMEOUT_MS, defaultAnswer: "y" }
538
- )).trim().toLowerCase();
539
- if (answer === "q" || answer === "quit") {
540
- renderer.write(color.dim(" Goodbye!\n"));
541
- return "cancel";
542
- }
543
- if (answer === "n" || answer === "no") return "re-prompt";
544
- return "use-last";
545
- }
546
- async function promptPort(deps, defaultPort) {
547
- const { renderer, reader } = deps;
548
- const maxRetries = 3;
549
- for (let attempt = 0; attempt < maxRetries; attempt++) {
550
- const answer = (await reader.readLine(
551
- ` ${color.amber("?")} Port ${color.dim(`[${defaultPort}]`)} ${color.dim("(Enter = default, q = cancel)")} `,
552
- { timeoutMs: PORT_TIMEOUT_MS, defaultAnswer: "" }
553
- )).trim();
554
- if (answer === "" || answer === "\n") return defaultPort;
555
- if (answer === "q" || answer === "quit") return defaultPort;
556
- const parsed = Number.parseInt(answer, 10);
557
- if (Number.isFinite(parsed) && parsed > 0 && parsed < 65536) {
558
- return parsed;
559
- }
560
- renderer.writeWarning(` Invalid port "${answer}". Use 1\u201365535.
561
- `);
562
- }
563
- return defaultPort;
564
- }
565
- async function promptHost(deps, defaultHost) {
566
- const { renderer, reader } = deps;
567
- renderer.write(
568
- ` ${color.dim("\u2139")} Press Enter to bind to ${color.bold(defaultHost)}${defaultHost === HQ_DEFAULT_HOST ? " (all interfaces)" : " (loopback)"}.
569
- `
570
- );
571
- const answer = (await reader.readLine(
572
- ` ${color.amber("?")} Host ${color.dim(`[${defaultHost}]`)} ${color.dim("(Enter = default, q = cancel)")} `,
573
- { timeoutMs: PORT_TIMEOUT_MS, defaultAnswer: "" }
574
- )).trim();
575
- if (answer === "" || answer === "q" || answer === "quit") return defaultHost;
576
- return answer;
577
- }
578
- function finalize(choice, ports) {
579
- if (!bindsPort(choice.mode)) return { ...choice, cancelled: false };
580
- const fallback = ports[choice.mode];
581
- return {
582
- ...choice,
583
- port: typeof choice.port === "number" ? choice.port : fallback,
584
- host: choice.host ?? defaultHostFor(choice.mode),
585
- cancelled: false
586
- };
587
- }
588
- function describeMode(mode) {
589
- switch (mode) {
590
- case "tui-repl":
591
- return "TUI / REPL";
592
- case "webui":
593
- return "WebUI";
594
- case "simpleui":
595
- return "SimpleUI";
596
- case "hq":
597
- return "HQ";
598
- case "desktop":
599
- return "Desktop";
600
- default: {
601
- const exhaustive = mode;
602
- return String(exhaustive);
603
- }
604
- }
605
- }
606
- function applyLaunchMenuToArgv(argv, result) {
607
- if (result.cancelled) return argv;
608
- const out = [...argv];
609
- switch (result.mode) {
610
- case "tui-repl":
611
- return out;
612
- case "webui":
613
- if (typeof result.port === "number") out.push(`--port=${result.port}`);
614
- if (typeof result.host === "string") out.push(`--host=${result.host}`);
615
- out.push("--webui");
616
- return out;
617
- case "simpleui":
618
- if (typeof result.port === "number") out.push(`--port=${result.port}`);
619
- if (typeof result.host === "string") out.push(`--host=${result.host}`);
620
- out.push("--simpleui");
621
- return out;
622
- case "hq":
623
- if (typeof result.port === "number") out.push(`--port=${result.port}`);
624
- if (typeof result.host === "string") out.push(`--host=${result.host}`);
625
- out.push("--hq");
626
- return out;
627
- case "desktop":
628
- out.push("--desktop");
629
- return out;
630
- default: {
631
- const exhaustive = result.mode;
632
- void exhaustive;
633
- return out;
634
- }
635
- }
636
- }
637
- function toPersistedMenuChoice(result) {
638
- if (result.cancelled) return void 0;
639
- if (result.mode === "tui-repl") return void 0;
640
- const port = typeof result.port === "number" && Number.isFinite(result.port) && result.port > 0 && result.port < 65536 ? result.port : void 0;
641
- return {
642
- mode: result.mode,
643
- ...port !== void 0 ? { port } : {},
644
- ...typeof result.host === "string" && result.host.length > 0 ? { host: result.host } : {}
645
- };
646
- }
647
- async function persistMenuChoice(globalConfigPath, choice) {
648
- let fileExists = false;
649
- try {
650
- await fs.access(globalConfigPath);
651
- fileExists = true;
652
- } catch {
653
- }
654
- let existing = {};
655
- try {
656
- const raw = await fs.readFile(globalConfigPath, "utf8");
657
- existing = JSON.parse(raw);
658
- } catch (err) {
659
- if (fileExists) {
660
- throw new Error(
661
- `Refusing to overwrite corrupt config at ${globalConfigPath} (${err.message}). Fix or move the file aside before retrying.`,
662
- { cause: err }
663
- );
664
- }
665
- existing = {};
666
- }
667
- const launch = existing.launch ?? {};
668
- existing.launch = { ...launch, menuChoice: choice };
669
- await atomicWrite(globalConfigPath, JSON.stringify(existing, null, 2), {
670
- mode: 384
671
- });
672
- }
673
-
674
- // src/boot/short-circuit-desktop.ts
675
- import { spawn } from "node:child_process";
676
- import { createRequire as createRequire3 } from "node:module";
677
- import * as path3 from "node:path";
678
- import { color as color2 } from "@wrongstack/core/utils";
679
- function stripDesktopLauncherArgs(argv) {
680
- const out = [];
681
- for (let i = 0; i < argv.length; i++) {
682
- const arg = argv[i];
683
- if (!arg) continue;
684
- if (arg === "--desktop" || arg.startsWith("--desktop=")) continue;
685
- if (i === 0 && arg === "desktop") continue;
686
- out.push(arg);
687
- }
688
- return out;
689
- }
690
- async function handleDesktopShortCircuit(flags, argv) {
691
- if (flags["desktop"] !== true) return null;
692
- return launchDesktop(stripDesktopLauncherArgs(argv));
693
- }
694
- async function launchDesktop(args) {
695
- const req = createRequire3(import.meta.url);
696
- let launcherPath;
697
- try {
698
- const desktopPkgPath = req.resolve("@wrongstack/desktop/package.json");
699
- launcherPath = path3.join(path3.dirname(desktopPkgPath), "bin", "wrongstack-desktop.js");
700
- } catch {
701
- process.stderr.write(
702
- [
703
- color2.red("\u2717 WrongStack Desktop is not installed."),
704
- "",
705
- "Install the desktop package:",
706
- " npm install -g @wrongstack/desktop",
707
- "",
708
- "The umbrella package also supports desktop when installed with optional dependencies:",
709
- " npm install -g wrongstack",
710
- ""
711
- ].join("\n")
712
- );
713
- return 1;
714
- }
715
- return await new Promise((resolve3) => {
716
- const child = spawn(process.execPath, [launcherPath, ...args], {
717
- stdio: "inherit",
718
- env: process.env,
719
- windowsHide: false
720
- });
721
- child.once("error", (err) => {
722
- process.stderr.write(
723
- `${color2.red("\u2717 Failed to start WrongStack Desktop:")} ${err.message}
724
- `
725
- );
726
- resolve3(1);
727
- });
728
- child.once("exit", (code, signal) => {
729
- if (typeof code === "number") resolve3(code);
730
- else resolve3(signal ? 1 : 0);
731
- });
732
- });
733
- }
734
-
735
- // src/subcommands/handlers/per-subcommand-help.ts
736
- import { color as color5 } from "@wrongstack/core/utils";
737
-
738
- // src/subcommands/handlers/bench-run-help.ts
739
- import { color as color3 } from "@wrongstack/core/utils";
740
- var BENCH_RUN_FLAGS = [
741
- // -- Suite selection ------------------------------------------------
742
- {
743
- name: "suite",
744
- flag: "--suite <id>",
745
- description: "Benchmark suite id (`polyglot` or `swebench`).",
746
- group: "suite",
747
- kind: "value",
748
- defaultValue: "polyglot"
749
- },
750
- {
751
- name: "polyglot-dir",
752
- flag: "--polyglot-dir <path>",
753
- description: "Path to the Aider polyglot dataset (required when --suite polyglot).",
754
- group: "suite",
755
- kind: "value",
756
- required: true
757
- },
758
- {
759
- name: "languages",
760
- flag: "--languages <csv>",
761
- description: "Comma-separated language list to filter the polyglot suite (e.g. `python,go`).",
762
- group: "suite",
763
- kind: "value"
764
- },
765
- {
766
- name: "dataset-dir",
767
- flag: "--dataset-dir <path>",
768
- description: "Path to the SWE-bench Verified dataset (optional, defaults to the official cache).",
769
- group: "suite",
770
- kind: "value"
771
- },
772
- {
773
- name: "docker",
774
- flag: "--docker",
775
- description: "Enable the SWE-bench Docker runtime (required for live grading).",
776
- group: "suite",
777
- kind: "boolean"
778
- },
779
- // -- Model matrix ---------------------------------------------------
780
- {
781
- name: "models",
782
- flag: "--models <config>",
783
- description: "Path to the model-cells config (JSON).",
784
- group: "models",
785
- kind: "value",
786
- defaultValue: "bench.config.json"
787
- },
788
- // -- Run control ----------------------------------------------------
789
- {
790
- name: "limit",
791
- flag: "--limit <N>",
792
- description: "Cap the number of tasks per cell (useful for smoke tests).",
793
- group: "control",
794
- kind: "value"
795
- },
796
- {
797
- name: "out",
798
- flag: "--out <dir>",
799
- description: "Base directory for run output (each run gets a timestamped subdir).",
800
- group: "control",
801
- kind: "value",
802
- defaultValue: "bench-results"
803
- },
804
- {
805
- name: "concurrency",
806
- flag: "--concurrency <N>",
807
- description: "Override the per-cell concurrency from the model config.",
808
- group: "control",
809
- kind: "value"
810
- }
811
- ];
812
- var BENCH_RUN_BOOLEAN_FLAG_NAMES = BENCH_RUN_FLAGS.filter(
813
- (f) => f.kind === "boolean"
814
- ).map((f) => f.name);
815
- var BENCH_RUN_VALUE_FLAG_NAMES = BENCH_RUN_FLAGS.filter(
816
- (f) => f.kind === "value"
817
- ).map((f) => f.name);
818
- var BENCH_RUN_FLAG_COLUMN_WIDTH = 28;
819
- function renderBenchRunHelpToString() {
820
- const lines = [
821
- color3.bold("wstack bench run \u2014 execute a benchmark suite across a model matrix"),
822
- color3.dim(" Runs a benchmark suite (polyglot or swebench) across every model cell"),
823
- color3.dim(" in the config. Output is a per-run directory with `report.md`, JSON"),
824
- color3.dim(" artifacts, and (for swebench) per-cell predictions for the official"),
825
- color3.dim(" harness."),
826
- "",
827
- color3.bold("Usage"),
828
- ` ${buildUsageLine()}`,
829
- "",
830
- color3.bold("Flags"),
831
- ...buildFlagBlock(),
832
- "",
833
- color3.dim(
834
- "See also: wstack bench list (show available suites + cells); wstack bench report <dir>"
835
- )
836
- ];
837
- return lines.join("\n") + "\n";
838
- }
839
- function buildUsageLine() {
840
- const parts = ["wstack bench run"];
841
- for (const f of BENCH_RUN_FLAGS) {
842
- if (f.kind === "value") {
843
- parts.push(`[${f.flag}]`);
844
- } else {
845
- parts.push(`[${f.flag}]`);
846
- }
847
- }
848
- return parts.join(" ");
849
- }
850
- function buildFlagBlock() {
851
- const rows = [];
852
- let currentGroup;
853
- for (const f of BENCH_RUN_FLAGS) {
854
- if (f.group !== currentGroup) {
855
- const header = f.group === "suite" ? "Suite selection:" : f.group === "models" ? "Model matrix:" : "Run control:";
856
- rows.push(color3.dim(` ${header}`));
857
- currentGroup = f.group;
858
- }
859
- const paddedFlag = f.flag.padEnd(BENCH_RUN_FLAG_COLUMN_WIDTH, " ");
860
- let desc = f.description;
861
- if (f.required) {
862
- desc = desc + " " + color3.bold("(required)");
863
- } else if (f.defaultValue !== void 0) {
864
- desc = desc + " " + color3.dim(`(default: ${f.defaultValue})`);
865
- }
866
- rows.push(` ${paddedFlag} ${desc}`);
867
- }
868
- return rows;
869
- }
870
-
871
- // src/subcommands/handlers/models-add-help.ts
872
- import { color as color4 } from "@wrongstack/core/utils";
873
- var MODELS_ADD_FLAGS = [
874
- // -- Identity --------------------------------------------------------
875
- {
876
- name: "provider",
877
- flag: "--provider <id>",
878
- description: "Provider id the model belongs to (defaults to the saved alias).",
879
- group: "identity",
880
- kind: "value"
881
- },
882
- {
883
- name: "name",
884
- flag: "--name <name>",
885
- description: "Human-readable display name (defaults to <mid>).",
886
- group: "identity",
887
- kind: "value"
888
- },
889
- // -- Capabilities ---------------------------------------------------
890
- {
891
- name: "max-context",
892
- flag: "--max-context <N>",
893
- description: "Context window in tokens (e.g. 200000 for 200k, or `128k`).",
894
- group: "capabilities",
895
- kind: "value"
896
- },
897
- {
898
- name: "max-output",
899
- flag: "--max-output <N>",
900
- description: "Max output tokens per request.",
901
- group: "capabilities",
902
- kind: "value"
903
- },
904
- {
905
- name: "tools",
906
- flag: "--tools / --no-tools",
907
- description: "Toggle tool/function-calling support.",
908
- group: "capabilities",
909
- kind: "boolean"
910
- },
911
- {
912
- name: "vision",
913
- flag: "--vision / --no-vision",
914
- description: "Toggle image-input support.",
915
- group: "capabilities",
916
- kind: "boolean"
917
- },
918
- {
919
- name: "reasoning",
920
- flag: "--reasoning / --no-reasoning",
921
- description: "Mark the model as a reasoning model.",
922
- group: "capabilities",
923
- kind: "boolean"
924
- },
925
- {
926
- name: "streaming",
927
- flag: "--streaming / --no-streaming",
928
- description: "Toggle streaming response support.",
929
- group: "capabilities",
930
- kind: "boolean"
931
- },
932
- {
933
- name: "json-mode",
934
- flag: "--json-mode",
935
- description: "Mark the model as supporting native JSON output.",
936
- group: "capabilities",
937
- kind: "boolean"
938
- }
939
- ];
940
- var MODELS_ADD_BOOLEAN_FLAG_NAMES = MODELS_ADD_FLAGS.filter(
941
- (f) => f.kind === "boolean"
942
- ).map((f) => f.name);
943
- var MODELS_ADD_VALUE_FLAG_NAMES = MODELS_ADD_FLAGS.filter(
944
- (f) => f.kind === "value"
945
- ).map((f) => f.name);
946
- var MODELS_ADD_FLAG_COLUMN_WIDTH = 30;
947
- function renderModelsAddHelpToString() {
948
- const lines = [
949
- color4.bold("wstack models add <mid> \u2014 register a custom model"),
950
- color4.dim(" Add or override a custom model (for self-hosted endpoints, fine-tuned"),
951
- color4.dim(" weights, or models not in the public models.dev catalog). The flags are"),
952
- color4.dim(" organized into two groups: identity (`--provider`, `--name`) and"),
953
- color4.dim(" capabilities (`--max-context`, `--tools`, `--vision`, etc.)."),
954
- "",
955
- color4.bold("Usage"),
956
- ` ${buildUsageLine2()}`,
957
- "",
958
- color4.bold("Flags"),
959
- ...buildFlagBlock2(),
960
- "",
961
- color4.dim("See also: wstack models list (verify the entry); wstack models remove")
962
- ];
963
- return lines.join("\n") + "\n";
964
- }
965
- function buildUsageLine2() {
966
- const parts = ["wstack models add <mid>"];
967
- for (const f of MODELS_ADD_FLAGS) {
968
- if (f.kind === "value") {
969
- parts.push(`[${f.flag}]`);
970
- } else {
971
- parts.push(`[${f.flag}]`);
972
- }
973
- }
974
- return parts.join(" ");
975
- }
976
- function buildFlagBlock2() {
977
- const rows = [];
978
- let currentGroup;
979
- for (const f of MODELS_ADD_FLAGS) {
980
- if (f.group !== currentGroup) {
981
- const header = f.group === "identity" ? "Identity:" : "Capabilities:";
982
- rows.push(color4.dim(` ${header}`));
983
- currentGroup = f.group;
984
- }
985
- const paddedFlag = f.flag.padEnd(MODELS_ADD_FLAG_COLUMN_WIDTH, " ");
986
- rows.push(` ${paddedFlag} ${f.description}`);
987
- }
988
- return rows;
989
- }
990
-
991
- // src/subcommands/handlers/per-subcommand-deep-help-table.ts
992
- var deepHelpTable = {
993
- // -- mcp -----------------------------------------------------------------
994
- "mcp:add": {
995
- name: "mcp:add",
996
- title: "wstack mcp add <name> \u2014 register a built-in MCP server",
997
- description: "Register a built-in MCP server by alias (e.g. `github`, `playwright`) and write the entry to the global config. The server is added in `disabled` state by default; pass `--enable` to register it active immediately.",
998
- usage: "wstack mcp add <name> [--enable]",
999
- subcommands: [
1000
- {
1001
- name: "<name>",
1002
- description: "The built-in server alias (run `wstack mcp add` for the list)."
1003
- },
1004
- {
1005
- name: "--enable / -e",
1006
- description: "Register the server enabled (default: disabled until you opt in)."
1007
- }
1008
- ],
1009
- seeAlso: "wstack mcp list (verify the entry landed); wstack mcp remove"
1010
- },
1011
- "mcp:remove": {
1012
- name: "mcp:remove",
1013
- title: "wstack mcp remove <name> \u2014 unregister an MCP server",
1014
- description: "Unregister an MCP server by alias. Removes the entry from the global config; the server process is not killed (REPL restart required to fully tear down the running process).",
1015
- usage: "wstack mcp remove <name>",
1016
- subcommands: [{ name: "<name>", description: "The server alias to unregister." }]
1017
- },
1018
- // -- plugin --------------------------------------------------------------
1019
- "plugin:add": {
1020
- name: "plugin:add",
1021
- title: "wstack plugin add <spec> \u2014 install a tool plugin",
1022
- description: "Add a tool plugin by specifier (npm package name) or official alias (`telegram`, `lsp`). Pass `--disabled` to install the plugin but leave it off until you explicitly enable it. The plugin requires a restart of the wrongstack process to take effect.",
1023
- usage: "wstack plugin add <spec|alias> [--disabled]",
1024
- subcommands: [
1025
- {
1026
- name: "<spec|alias>",
1027
- description: "NPM specifier (e.g. `@org/wrongstack-x`) or official alias."
1028
- },
1029
- {
1030
- name: "--disabled",
1031
- description: "Install the plugin but leave it disabled until you enable it."
1032
- }
1033
- ],
1034
- seeAlso: "wstack plugin official (list the official registry); wstack plugin enable"
1035
- },
1036
- "plugin:remove": {
1037
- name: "plugin:remove",
1038
- title: "wstack plugin remove <spec> \u2014 uninstall a tool plugin",
1039
- description: "Remove a tool plugin from the config. The plugin requires a restart of the wrongstack process to take effect. Aliases: `rm`, `uninstall`.",
1040
- usage: "wstack plugin remove <spec|alias>",
1041
- subcommands: [
1042
- { name: "<spec|alias>", description: "The specifier or official alias to remove." }
1043
- ]
1044
- },
1045
- "plugin:enable": {
1046
- name: "plugin:enable",
1047
- title: "wstack plugin enable <spec> \u2014 re-enable a previously-disabled plugin",
1048
- description: "Re-enable a plugin that was installed with `--disabled` or toggled off with `wstack plugin disable`. Requires a restart.",
1049
- usage: "wstack plugin enable <spec|alias>",
1050
- subcommands: [
1051
- { name: "<spec|alias>", description: "The specifier or official alias to enable." }
1052
- ]
1053
- },
1054
- "plugin:disable": {
1055
- name: "plugin:disable",
1056
- title: "wstack plugin disable <spec> \u2014 temporarily disable a plugin",
1057
- description: "Temporarily disable a plugin without removing it from the config. Use `wstack plugin enable` to re-enable. Requires a restart.",
1058
- usage: "wstack plugin disable <spec|alias>",
1059
- subcommands: [
1060
- { name: "<spec|alias>", description: "The specifier or official alias to disable." }
1061
- ]
1062
- },
1063
- "plugin:toggle": {
1064
- name: "plugin:toggle",
1065
- title: "wstack plugin toggle <name> \u2014 toggle an audit-list plugin row",
1066
- description: "Toggle one built-in audit-list plugin by its runtime name. Default-active plugins are disabled by writing `{ enabled: false }` overrides; enabling them again removes that override. All bundled audit-list rows are toggleable; future non-toggleable rows are reported as locked. Requires a restart for loaded plugin code to change in the current session.",
1067
- usage: "wstack plugin toggle <name>",
1068
- subcommands: [
1069
- { name: "<name>", description: "Audit-list runtime name, e.g. `format-on-save`." }
1070
- ],
1071
- seeAlso: "wstack plugin report (see effective state and toggle policy)"
1072
- },
1073
- "plugin:report": {
1074
- name: "plugin:report",
1075
- title: "wstack plugin report \u2014 show the plugin audit table",
1076
- description: "Print the built-in plugin audit table. Each row shows effective state, whether that state came from config or default boot behavior, risk, and the toggle policy for that row.",
1077
- usage: "wstack plugin report (alias: wstack plugin audit)",
1078
- seeAlso: "wstack plugin toggle <name> (toggle audit-list rows); wstack plugin list"
1079
- },
1080
- "plugin:menu": {
1081
- name: "plugin:menu",
1082
- title: "wstack plugin menu \u2014 open or describe the plugin picker",
1083
- description: "In the TUI, `/plugin menu` opens the interactive plugin picker. From the non-interactive `wstack plugin menu` surface, the command prints the same audit report as `wstack plugin report`.",
1084
- usage: "wstack plugin menu",
1085
- seeAlso: "/plugin menu (interactive picker in the TUI); wstack plugin report"
1086
- },
1087
- // -- models --------------------------------------------------------------
1088
- // The `models:add` deep entry delegates its body to
1089
- // `models-add-help.ts` via the `customBody` field. The flag
1090
- // list lives in exactly one place (`MODELS_ADD_FLAGS`), and
1091
- // every surface that renders the help — `wstack models add --help`,
1092
- // `/models add help`, `/help models add` — produces the same
1093
- // string. The `title` / `description` / `usage` / `seeAlso`
1094
- // fields below are required by the `PerSubcommandHelp` shape
1095
- // but never rendered (the `customBody` thunk owns the full
1096
- // layout). They're filled in with sensible defaults so a
1097
- // future refactor that drops `customBody` (e.g. to use the
1098
- // standard layout) still has a coherent fallback.
1099
- "models:add": {
1100
- name: "models:add",
1101
- title: "wstack models add <mid> \u2014 register a custom model",
1102
- description: "See renderModelsAddHelpToString() in models-add-help.ts for the full block.",
1103
- usage: "wstack models add <mid> [...flags]",
1104
- customBody: renderModelsAddHelpToString
1105
- },
1106
- "models:remove": {
1107
- name: "models:remove",
1108
- title: "wstack models remove <mid> \u2014 unregister a custom model",
1109
- description: "Remove a custom model from the config. The catalog is unaffected (catalog models are managed by `wstack models refresh`).",
1110
- usage: "wstack models remove <mid>",
1111
- subcommands: [{ name: "<mid>", description: "The model id to remove." }]
1112
- },
1113
- // -- audit / replay (--list deep-subcommand) ---------------------------
1114
- "audit:list": {
1115
- name: "audit:list",
1116
- title: "wstack audit --list \u2014 list every session with an audit log",
1117
- description: "Scan the project sessions dir for `.audit.jsonl` sidecars and print a one-line summary per session (entry count + chain status). Useful for finding a session to inspect with `wstack audit <id>`.",
1118
- usage: "wstack audit --list / wstack audit -l",
1119
- seeAlso: "wstack audit <id> (inspect a single session's chain)"
1120
- },
1121
- "replay:list": {
1122
- name: "replay:list",
1123
- title: "wstack replay --list \u2014 list every session with a replay log",
1124
- description: "Scan the project sessions dir for `.replay.jsonl` sidecars and print a one-line summary per session (entry count + log path). Useful for finding a session to inspect with `wstack replay <id>`.",
1125
- usage: "wstack replay --list / wstack replay -l",
1126
- seeAlso: "wstack replay <id> (inspect a single session's recorded responses)"
1127
- },
1128
- // -- sessions (deep subcommands) ---------------------------------------
1129
- "sessions:resume": {
1130
- name: "sessions:resume",
1131
- title: "wstack sessions resume [<id>] \u2014 resume a prior session",
1132
- description: `Resume a session by id, or the most recent one if no id is given. The REPL replays the session's history into the new run so context is preserved. Use the most recent id when the user just asked "pick up where we left off" without naming a specific session.`,
1133
- usage: "wstack sessions resume [<id>]",
1134
- subcommands: [
1135
- { name: "[<id>]", description: "Session id to resume (defaults to the most recent)." }
1136
- ],
1137
- seeAlso: "wstack sessions list (find a recent id); wstack sessions show <id> (preview before resuming)"
1138
- },
1139
- "sessions:fork": {
1140
- name: "sessions:fork",
1141
- title: "wstack sessions fork [<id>] [--to N] \u2014 branch a session journal",
1142
- description: "Create a non-destructive child JSONL journal from the latest persisted boundary or checkpoint N. The parent is unchanged and the returned checkpoint hash identifies the exact parent event prefix. New Git-backed checkpoints also report a content-addressed workspace manifest, but the fork command does not apply it automatically: files remain shared until a host materializes that manifest into a separate clean checkout.",
1143
- usage: "wstack sessions fork [<id>] [--to N]",
1144
- subcommands: [
1145
- { name: "[<id>]", description: "Parent session id (defaults to the most recent)." },
1146
- { name: "--to N", description: "Fork at checkpoint prompt index N." }
1147
- ],
1148
- seeAlso: "wstack rewind --list (find checkpoints); wstack sessions resume <child-id>"
1149
- },
1150
- "sessions:fleet": {
1151
- name: "sessions:fleet",
1152
- title: "wstack sessions fleet \u2014 list the active fleet of sessions",
1153
- description: "List the active multi-agent fleet runs (the director, the subagent set, the iteration count, the journal size). Distinct from `wstack sessions list` which only shows single-agent sessions.",
1154
- usage: "wstack sessions fleet",
1155
- seeAlso: "wstack sessions list (single-agent sessions); /fleet (the in-REPL equivalent)"
1156
- },
1157
- "sessions:show": {
1158
- name: "sessions:show",
1159
- title: "wstack sessions show <id> \u2014 preview a session in detail",
1160
- description: "Print the session metadata, the first N turns, the token/cost totals, and any errors. Use this to decide whether to resume a session before committing to it.",
1161
- usage: "wstack sessions show <id>",
1162
- subcommands: [{ name: "<id>", description: "The session id to show." }]
1163
- },
1164
- "sessions:list": {
1165
- name: "sessions:list",
1166
- title: "wstack sessions list \u2014 list recent single-agent sessions",
1167
- description: "Print a one-line summary per recorded session (id, timestamp, last prompt, model, token totals). Distinct from `wstack sessions fleet` which shows active multi-agent runs. Use this to find a session id to resume, show, or export.",
1168
- usage: "wstack sessions list",
1169
- seeAlso: "wstack sessions fleet (active runs); wstack sessions resume <id> (resume one)"
1170
- },
1171
- "sessions:config": {
1172
- name: "sessions:config",
1173
- title: "wstack sessions config \u2014 session-specific config",
1174
- description: "Show or edit the session-specific config overrides (e.g. per-session provider + model). Subcommand: see the underlying config-history commands for the full surface \u2014 this is the shortcut alias.",
1175
- usage: "wstack sessions config"
1176
- },
1177
- // -- config (subcommands of the top-level config) ---------------------
1178
- "config:show": {
1179
- name: "config:show",
1180
- title: "wstack config show \u2014 print the resolved config",
1181
- description: "Print the resolved config (active profile plus all on-disk overrides applied) to stdout. Secrets are masked. The default subcommand \u2014 `wstack config` without a sub invokes this.",
1182
- usage: "wstack config show",
1183
- seeAlso: "wstack config edit (interactive); wstack auth (most config edits are auth/key changes)"
1184
- },
1185
- "config:edit": {
1186
- name: "config:edit",
1187
- title: "wstack config edit \u2014 open the active profile config in $EDITOR",
1188
- description: "Print the active profile config path (typically `~/.wrongstack/profiles/default/config.json`) and the command to open it. Does not spawn the editor itself \u2014 the user runs the printed command (or sets `$EDITOR` and re-runs). Useful for offline edits when you want to see the full file at once.",
1189
- usage: "wstack config edit",
1190
- seeAlso: "wstack config show (verify after edit); wstack config history (audit trail)"
1191
- },
1192
- "config:history": {
1193
- name: "config:history",
1194
- title: "wstack config history \u2014 list recent config-history entries",
1195
- description: "List every recent change to the active profile config, with a one-line description and a snapshot id. Pass `--id <id>` to see the full diff + masked snapshot. The audit trail is append-only (entries are never modified post-creation).",
1196
- usage: "wstack config history [--id <id>]",
1197
- subcommands: [
1198
- { name: "<no subcommand>", description: "List every history entry (newest first)." },
1199
- { name: "--id <id>", description: "Show the full diff + masked snapshot for one entry." }
1200
- ],
1201
- seeAlso: "wstack config restore <id>|--latest (revert); wstack config show (current state)"
1202
- },
1203
- "config:restore": {
1204
- name: "config:restore",
1205
- title: "wstack config restore <id>|--latest \u2014 revert to a prior config",
1206
- description: "Restore a previous config-history entry. Pass either the history id (from `wstack config history`) or `--latest` to revert to the most recent prior version. A backup of the current config is created before the restore, so the change is itself recorded in the history (a history of histories).",
1207
- usage: "wstack config restore <id> | --latest / -l",
1208
- subcommands: [
1209
- {
1210
- name: "<id>",
1211
- description: "The history id to restore (from `wstack config history` output)."
1212
- },
1213
- {
1214
- name: "--latest / -l",
1215
- description: "Restore to the most recent prior version (without naming an id)."
1216
- }
1217
- ],
1218
- seeAlso: "wstack config history (list entries); wstack config show (verify the restore)"
1219
- },
1220
- // -- rewind (flag-shaped deep subcommands) ----------------------------
1221
- "rewind:list": {
1222
- name: "rewind:list",
1223
- title: "wstack rewind --list \u2014 list rewind checkpoints for a session",
1224
- description: "Print every checkpoint for the session (default: latest). Each checkpoint is a snapshot of the working tree + session history at a given prompt index. Use the checkpoint index as the value for `--to <idx>` when rewinding. Default `wstack rewind` (no flags) is an error \u2014 pair `--list` with a session id (positional) to discover available checkpoints first.",
1225
- usage: "wstack rewind [<sessionId>] --list",
1226
- seeAlso: "wstack rewind --to <idx> (rewind to a specific checkpoint); wstack rewind --all"
1227
- },
1228
- "rewind:all": {
1229
- name: "rewind:all",
1230
- title: "wstack rewind --all \u2014 rewind to the start of the session",
1231
- description: "Rewind the working tree + session state to the very start of the session (the first prompt). Every file modified since the start is reverted. Pair with `--resume` to also truncate the session history at the start (so a fresh `wstack` invocation begins from there).",
1232
- usage: "wstack rewind [<sessionId>] --all [--resume]",
1233
- seeAlso: "wstack rewind --last N (rewind fewer steps); wstack rewind --to <idx> (specific checkpoint)"
1234
- },
1235
- "rewind:last": {
1236
- name: "rewind:last",
1237
- title: "wstack rewind --last N \u2014 rewind the last N prompts",
1238
- description: "Rewind the last `N` prompts. For a session that was on track for prompts 1..10 and went off the rails at 11..15, `--last 5` rewinds to the state at the end of prompt 10. Pair with `--resume` to truncate the history at the rewound point.",
1239
- usage: "wstack rewind [<sessionId>] --last <N> [--resume]",
1240
- subcommands: [
1241
- { name: "<N>", description: "Number of recent prompts to rewind (must be \u2265 1)." }
1242
- ],
1243
- seeAlso: "wstack rewind --all (rewind further); wstack rewind --to <idx> (precise checkpoint)"
1244
- },
1245
- "rewind:to": {
1246
- name: "rewind:to",
1247
- title: "wstack rewind --to <idx> \u2014 rewind to a specific checkpoint",
1248
- description: "Rewind to checkpoint at the given prompt index (from `wstack rewind --list`). The most precise rewind form \u2014 lets you step back to exactly the state at a specific prompt rather than the bulk `--all` or approximate `--last N`.",
1249
- usage: "wstack rewind [<sessionId>] --to <idx> [--resume]",
1250
- subcommands: [
1251
- {
1252
- name: "<idx>",
1253
- description: "Prompt index to rewind to (must be \u2265 0). Use `wstack rewind --list` to find indices."
1254
- }
1255
- ],
1256
- seeAlso: "wstack rewind --list (find checkpoint indices); wstack rewind --resume (truncate history at the checkpoint)"
1257
- },
1258
- "rewind:resume": {
1259
- name: "rewind:resume",
1260
- title: "wstack rewind --resume \u2014 truncate session history at the checkpoint",
1261
- description: "After the rewind (any of `--all` / `--last N` / `--to N`), also truncate the session's recorded history at the rewound checkpoint so the next `wstack` invocation begins fresh from there. Without `--resume`, the rewind only reverts the working tree \u2014 the session history is preserved (you'd see the rewind point as a checkpoint in subsequent runs).",
1262
- usage: "wstack rewind [<sessionId>] {--all|--last <N>|--to <idx>} --resume",
1263
- seeAlso: "wstack rewind --list (find checkpoints); wstack sessions resume <id> (resume a rewound session)"
1264
- },
1265
- // -- mcp:restart (REPL-only) -----------------------------------------
1266
- "mcp:restart": {
1267
- name: "mcp:restart",
1268
- title: "wstack mcp restart \u2014 restart a running MCP server (REPL only)",
1269
- description: "Restart a single running MCP server by alias. This subcommand is only meaningful inside the REPL (`wstack` with no `<task>` argument) \u2014 from the top-level CLI it prints a warning and exits 0 because there's no live process to restart. Use the `/mcp restart <name>` slash command from inside the REPL.",
1270
- usage: "wstack mcp restart <name> (REPL only)",
1271
- seeAlso: "/mcp restart <name> (the in-REPL slash command); wstack mcp remove + wstack mcp add (replace the server config)"
1272
- },
1273
- // -- plugin (list / official) -----------------------------------------
1274
- "plugin:list": {
1275
- name: "plugin:list",
1276
- title: "wstack plugin list \u2014 list configured plugins",
1277
- description: "Print every plugin registered in the config, grouped by enabled vs disabled. Alias: `plugin status`. For the official registry (a curated list maintained by the WrongStack project), use `wstack plugin official` instead.",
1278
- usage: "wstack plugin list (alias: wstack plugin status)",
1279
- seeAlso: "wstack plugin official (curated registry); wstack plugin enable / disable (toggle state)"
1280
- },
1281
- "plugin:official": {
1282
- name: "plugin:official",
1283
- title: "wstack plugin official \u2014 list the curated official registry",
1284
- description: "Print every plugin in the official registry (currently `telegram` and `lsp`). Each row shows the alias (for shorthand on the command line) and the full NPM specifier (what `wstack plugin add <spec>` actually installs). Aliases: `plugin officials` (plural).",
1285
- usage: "wstack plugin official (alias: wstack plugin officials)",
1286
- seeAlso: "wstack plugin add <alias> (install one); wstack plugin list (what you have)"
1287
- },
1288
- "plugin:officials": {
1289
- // The plural form `plugin officials` is accepted as an
1290
- // alias of `plugin official` in the underlying dispatch
1291
- // (`plugin-management.ts`). The deep-help table mirrors
1292
- // the alias so `wstack plugin officials --help` and
1293
- // `wstack plugin official --help` both render the same
1294
- // focused block.
1295
- name: "plugin:officials",
1296
- title: "wstack plugin officials \u2014 list the curated official registry (plural alias)",
1297
- description: "Alias of `wstack plugin official`. Prints every plugin in the official registry. Same output as the singular form.",
1298
- usage: "wstack plugin officials (plural alias of `wstack plugin official`)",
1299
- seeAlso: "wstack plugin official (the singular form)"
1300
- },
1301
- // -- models (refresh + list) -----------------------------------------
1302
- "models:refresh": {
1303
- name: "models:refresh",
1304
- title: "wstack models refresh \u2014 force-refresh the models.dev cache",
1305
- description: "Re-fetch the models.dev catalog and replace the cached `models.json` in the global config dir. Useful when a new model is published mid-session and you want to see it in `wstack providers` / `wstack models` without restarting. The cache age is shown in the footer of every `wstack models` listing so you know when to refresh.",
1306
- usage: "wstack models refresh",
1307
- seeAlso: "wstack models <provider> (list models after refresh); wstack providers (force-refresh the provider catalog)"
1308
- },
1309
- "models:list": {
1310
- name: "models:list",
1311
- title: "wstack models list \u2014 list custom models registered locally",
1312
- description: "Print every model that's been added via `wstack models add` (i.e. the entries in `config.json`'s `models` section, not the catalog). Distinct from `wstack models <provider>` which lists the catalog for a specific provider. The list is the audit surface for self-hosted / fine-tuned / overridden models.",
1313
- usage: "wstack models list",
1314
- seeAlso: "wstack models <provider> (catalog); wstack models add <mid> (register a custom model)"
1315
- },
1316
- // -- auth (list / status / remove) -------------------------------------
1317
- // The top-level `wstack auth` entry in `helpTable` lists the
1318
- // subcommands; these deep entries give each one its own focused
1319
- // block so `wstack auth list --help`, `/help auth status`, and
1320
- // `/auth status help` all render the same string the underlying
1321
- // handler would emit. The descriptions match the actual
1322
- // handler behavior in `packages/cli/src/subcommands/handlers/auth.ts`
1323
- // — note that the `remove` subcommand is always interactive
1324
- // (prompts for confirmation); the `[--force]` token in the
1325
- // handler's error message is a documented hint that is not
1326
- // yet wired up in the parser.
1327
- "auth:list": {
1328
- name: "auth:list",
1329
- title: "wstack auth list \u2014 list saved providers and key status",
1330
- description: "Read-only listing of every provider in the active profile config. Each provider block shows the family, baseUrl, model-allowlist size, and the saved API keys (the active key is marked with a green `\u25CF`, inactive keys with a dim `\u25CB`; all values are masked). Alias: `wstack auth ls`.",
1331
- usage: "wstack auth list (alias: wstack auth ls)",
1332
- subcommands: [
1333
- { name: "list", description: "List every saved provider (this command)." },
1334
- { name: "ls", description: "Alias of list." }
1335
- ],
1336
- seeAlso: "wstack auth status <id> (detail for one provider); wstack auth remove <id> (delete one)"
1337
- },
1338
- "auth:status": {
1339
- name: "auth:status",
1340
- title: "wstack auth status <provider> \u2014 show detail for one provider",
1341
- description: "Print the full `config.json` entry for a single provider: type, family, baseUrl, the `models` allowlist, the `envVars` list, and every saved key (active key marked with a green `\u25CF`, masked value, ISO timestamp). The provider id is required as a positional \u2014 `wstack auth status` with no id prints the usage hint and exits 1.",
1342
- usage: "wstack auth status <provider>",
1343
- subcommands: [
1344
- {
1345
- name: "<provider>",
1346
- description: "The provider id to inspect (e.g. `openai`, `anthropic`)."
1347
- }
1348
- ],
1349
- seeAlso: "wstack auth list (find the id); wstack auth remove <id> (delete it); wstack auth (interactive edit)"
1350
- },
1351
- "auth:remove": {
1352
- name: "auth:remove",
1353
- title: "wstack auth remove <provider> \u2014 delete a provider and its keys",
1354
- description: "Remove a provider entry and all its saved API keys from the active profile config. The flow is always interactive: after printing a confirmation prompt the handler waits for a `y` / `yes` answer (default `N`). The active session's in-memory provider is NOT reloaded \u2014 restart the REPL to fully tear down a running provider. Alias: `wstack auth rm`.",
1355
- usage: "wstack auth remove <provider> (alias: wstack auth rm <provider>)",
1356
- subcommands: [
1357
- { name: "<provider>", description: "The provider id to remove." },
1358
- { name: "rm", description: "Alias of remove." }
1359
- ],
1360
- seeAlso: "wstack auth list (find the id); wstack auth status <id> (inspect before removing); wstack auth <provider> (re-add a different one)"
1361
- },
1362
- // The `auth:local` deep entry delegates its body to
1363
- // `auth-local-help.ts` via the `customBody` field. The flag
1364
- // list lives in exactly one place (`LOCAL_AUTH_FLAGS`), and
1365
- // every surface that renders the help — `wstack auth local --help`,
1366
- // `/auth local help`, `/help auth local` — produces the same
1367
- // string. The `title` / `description` / `usage` / `seeAlso` fields
1368
- // below are required by the `PerSubcommandHelp` shape but
1369
- // never rendered (the `customBody` thunk owns the full layout).
1370
- // They're filled in with sensible defaults so a future refactor
1371
- // that drops `customBody` (e.g. to use the standard layout)
1372
- // still has a coherent fallback.
1373
- "auth:local": {
1374
- name: "auth:local",
1375
- title: "wstack auth local \u2014 quick-add Ollama / vLLM / LM Studio",
1376
- description: "Pre-fills the base URL, runs a health probe, and persists the allowlist so you can `wstack --provider <id>` right away. Use `--no-probe` to skip the probe when the server is not running yet; `--audit <file>` captures the save lifecycle as JSONL.",
1377
- usage: "wstack auth local [--name <id>] [--base-url <url>] [--no-key] [--no-probe|--probe-only] [--model <spec>] [--audit [target]]",
1378
- seeAlso: "wstack auth (interactive menu); wstack auth <provider> (catalog add)",
1379
- customBody: renderAuthLocalHelpToString
1380
- },
1381
- // -- bench (run — delegated to bench-run-help.ts via customBody) ------
1382
- // The `bench:run` deep entry delegates its body to
1383
- // `bench-run-help.ts` via the `customBody` field. The flag
1384
- // list lives in exactly one place (`BENCH_RUN_FLAGS`), and
1385
- // every surface that renders the help — `wstack bench run --help`,
1386
- // `/bench run help`, `/help bench run` — produces the same
1387
- // string. The `title` / `description` / `usage` / `seeAlso`
1388
- // fields below are required by the `PerSubcommandHelp` shape
1389
- // but never rendered (the `customBody` thunk owns the full
1390
- // layout). They're filled in with sensible defaults so a
1391
- // future refactor that drops `customBody` (e.g. to use the
1392
- // standard layout) still has a coherent fallback.
1393
- "bench:run": {
1394
- name: "bench:run",
1395
- title: "wstack bench run \u2014 execute a benchmark suite across a model matrix",
1396
- description: "See renderBenchRunHelpToString() in bench-run-help.ts for the full block.",
1397
- usage: "wstack bench run [...flags]",
1398
- customBody: renderBenchRunHelpToString
1399
- }
1400
- };
1401
-
1402
- // src/subcommands/handlers/per-subcommand-help-table.ts
1403
- var helpTable = {
1404
- init: {
1405
- name: "init",
1406
- title: "wstack init \u2014 DEPRECATED (use wstack auth)",
1407
- description: "\u26A0 This command is deprecated. Use wstack auth for setup, key management, and model selection in one interactive workflow.",
1408
- usage: "wstack init (deprecated)",
1409
- seeAlso: "wstack auth (interactive setup + key manager)"
1410
- },
1411
- version: {
1412
- name: "version",
1413
- title: "wstack version \u2014 print the CLI version",
1414
- description: "Prints the WrongStack CLI version, the apiVersion, the Node.js version, and the host platform. Useful for bug reports and CI logs.",
1415
- usage: "wstack version"
1416
- },
1417
- mcp: {
1418
- name: "mcp",
1419
- title: "wstack mcp \u2014 manage Model Context Protocol servers",
1420
- description: "List, add, remove, restart, and serve MCP servers registered in the global config. Servers are referenced by id and use the stdio, SSE, or streamable-HTTP transports.",
1421
- usage: "wstack mcp [list|add|remove|restart|serve] [...]",
1422
- subcommands: [
1423
- { name: "list", description: "List all configured MCP servers." },
1424
- { name: "add <id> <command>", description: "Register a new stdio MCP server." },
1425
- { name: "remove <id>", description: "Unregister an MCP server." },
1426
- { name: "restart <id>", description: "Restart a running MCP server." },
1427
- {
1428
- name: "serve",
1429
- description: "Run the wstack MCP server; --resources/--prompts explicitly expose selected files."
1430
- }
1431
- ],
1432
- seeAlso: "wstack plugin (manage tool plugins similarly)"
1433
- },
1434
- plugin: {
1435
- name: "plugin",
1436
- title: "wstack plugin \u2014 manage tool plugins",
1437
- description: "List, inspect, install, add, remove, enable, disable, and toggle tool plugins. Plugins extend the agent with custom tool packs (e.g. GitHub, Playwright, project-local helpers).",
1438
- usage: "wstack plugin [list|status|report|menu|official|add|install|toggle|remove|enable|disable|manager] [...]",
1439
- subcommands: [
1440
- { name: "list", description: "List installed plugins (alias: status)." },
1441
- { name: "report", description: "Show effective state, risk, and lock/toggle policy." },
1442
- { name: "menu", description: "Print the audit report outside the TUI plugin picker." },
1443
- { name: "official", description: "List plugins from the official registry." },
1444
- { name: "add <id>", description: "Add a plugin by id (alias: install)." },
1445
- { name: "toggle <id>", description: "Toggle a safe audit-list plugin row." },
1446
- { name: "remove <id>", description: "Remove an installed plugin (aliases: rm, uninstall)." },
1447
- { name: "enable <id>", description: "Re-enable a previously-disabled plugin." },
1448
- { name: "disable <id>", description: "Temporarily disable a plugin without removing it." },
1449
- {
1450
- name: "manager [lock|unlock] <id|*>",
1451
- description: "Control whether the LLM may enable/disable individual plugins."
1452
- }
1453
- ],
1454
- seeAlso: "wstack mcp (MCP servers are registered as tool plugins)"
1455
- },
1456
- models: {
1457
- name: "models",
1458
- title: "wstack models \u2014 list and override models",
1459
- description: "List models from the models.dev catalog, or override the default model for a provider with a custom id (for self-hosted or fine-tuned models not in the public catalog).",
1460
- usage: "wstack models [<provider>] [add|remove|list|refresh] [...]",
1461
- subcommands: [
1462
- { name: "<no-subcommand>", description: "List models for the default provider." },
1463
- { name: "<provider>", description: "List models for a specific provider." },
1464
- {
1465
- name: "add <mid>",
1466
- description: "Add or override a custom model (--max-context, --tools, --vision, \u2026)."
1467
- },
1468
- { name: "remove <mid>", description: "Remove a custom model." },
1469
- { name: "list", description: "List all custom models registered locally." },
1470
- { name: "refresh", description: "Force-refresh the models.dev cache." }
1471
- ],
1472
- seeAlso: "wstack providers (list provider families and their defaults)"
1473
- },
1474
- config: {
1475
- name: "config",
1476
- title: "wstack config \u2014 show or edit effective config",
1477
- description: "Print the resolved config (with the on-disk overrides merged on top), or open the active profile config in $EDITOR for interactive edits. Also exposes a small audit log of recent config-history changes for diagnostics.",
1478
- usage: "wstack config [show|edit|history|restore] [...]",
1479
- subcommands: [
1480
- { name: "show", description: "Print the resolved config to stdout (default)." },
1481
- { name: "edit", description: "Open the active profile config in $EDITOR." },
1482
- { name: "history", description: "List recent config-history entries." },
1483
- { name: "restore <id>", description: "Restore a previous config-history entry." }
1484
- ],
1485
- seeAlso: "wstack auth (most config edits are auth/key changes)"
1486
- },
1487
- // -- API key / auth management -----------------------------------------
1488
- auth: {
1489
- name: "auth",
1490
- title: "wstack auth \u2014 manage API keys and provider credentials",
1491
- description: "Add, view, and remove provider API keys. The interactive menu supports a custom-URL path for self-hosted / local servers, a quick-shortcut path for Ollama / vLLM / LM Studio, and a catalog path for the well-known providers.",
1492
- usage: "wstack auth [list|status|remove] [...] | wstack auth <provider> | wstack auth local [...]",
1493
- subcommands: [
1494
- { name: "list", description: "List saved providers and key status." },
1495
- { name: "status <id>", description: "Show detail for one provider." },
1496
- { name: "remove <id>", description: "Remove a provider and its keys." },
1497
- { name: "<provider>", description: "Add a key for a named provider (--label, --family, \u2026)." },
1498
- {
1499
- name: "local",
1500
- description: "Pre-fill Ollama / vLLM / LM Studio (--name, --base-url, --no-probe, --model, --audit \u2026)."
1501
- },
1502
- {
1503
- name: "login <chatgpt|claude|copilot>",
1504
- description: "Subscription OAuth login: chatgpt (\u2192 openai-codex), claude (\u2192 anthropic-oauth), or copilot (\u2192 github-copilot). Opens browser; no API key. \u26A0 Using subscriptions outside official clients may violate provider ToS (account ban risk); an API key is the sanctioned path."
1505
- }
1506
- ],
1507
- seeAlso: "wstack auth local (pre-fill Ollama / vLLM / LM Studio)"
1508
- },
1509
- // -- Session list / resume / show / fork --------------------------------
1510
- sessions: {
1511
- name: "sessions",
1512
- title: "wstack sessions \u2014 list and resume recent sessions",
1513
- description: "List recent sessions, show one session in detail, resume a session, or inspect a session's audit log. The audit log is stored as JSONL next to each session's recording.",
1514
- usage: "wstack sessions [list|show|resume|fork|doctor|config|fleet] [...]",
1515
- subcommands: [
1516
- { name: "list", description: "List the most recent sessions." },
1517
- {
1518
- name: "doctor [--fix] [--json] [--limit N]",
1519
- description: "Diagnose every journal in this project: unparsable lines, truncated tails, sessions that died mid-turn, missing or stale summary sidecars, and journals large enough to be slow to open. --fix rebuilds only derived artifacts (summary sidecars + the catalog index); journals are never edited."
1520
- },
1521
- { name: "show <id>", description: "Show one session in detail." },
1522
- { name: "resume [<id>]", description: "Resume a session (latest if no id given)." },
1523
- {
1524
- name: "fork [<id>] [--to N]",
1525
- description: "Create an isolated child journal at a persisted boundary."
1526
- },
1527
- { name: "config", description: "Show or edit session-specific config." },
1528
- { name: "fleet", description: "List the active fleet of sessions." }
1529
- ],
1530
- seeAlso: "wstack audit (the session-level audit log reader)"
1531
- },
1532
- // -- Diagnostics --------------------------------------------------------
1533
- doctor: {
1534
- name: "doctor",
1535
- title: "wstack doctor \u2014 health checks",
1536
- description: "Run a series of health checks (provider + key + models cache + secret vault + sessions dir + MCP server config) and exit non-zero if any check fails. Use as a CI gate or a post-install smoke test. With --daemons, inspect this project\u2019s IPC daemons instead: each is reported live, stale, or stopped. A stale endpoint is one whose owner died without releasing it; daemons reclaim it on their next start, or --clear-stale removes it now. Listing never spawns a daemon.",
1537
- usage: "wstack doctor [--daemons [--clear-stale]]",
1538
- seeAlso: "wstack diag (read-only environment dump for bug reports)"
1539
- },
1540
- diag: {
1541
- name: "diag",
1542
- title: "wstack diag \u2014 read-only environment dump",
1543
- description: "Print a key=value environment snapshot (apiVersion, cwd, project info, paths, cache age, configured provider + model, tool/plugin counts, MCP server count). Never modifies state \u2014 safe to paste into bug reports.",
1544
- usage: "wstack diag",
1545
- seeAlso: "wstack doctor (pass/fail health checks vs. this is a dump)"
1546
- },
1547
- // -- Session audit / replay / rewind -----------------------------------
1548
- audit: {
1549
- name: "audit",
1550
- title: "wstack audit \u2014 inspect a session's tamper-evident audit log",
1551
- description: "Show the chained-hash entries for a recorded session and run a verification pass to surface any post-hoc modification. Each entry is SHA-256-chained to the previous; any tampering breaks the chain and is reported.",
1552
- usage: "wstack audit [<sessionId>] [--list]",
1553
- subcommands: [
1554
- { name: "<sessionId>", description: "Show entries + verify chain (positional)." },
1555
- { name: "--list / -l", description: "List every session that has an audit log." }
1556
- ],
1557
- seeAlso: "wstack replay (the corresponding provider-response log)"
1558
- },
1559
- replay: {
1560
- name: "replay",
1561
- title: "wstack replay \u2014 inspect a session's recorded provider responses",
1562
- description: "Show the recorded request/response pairs for a session \u2014 the frozen inputs the agent saw, in order. This is the inspection surface; to actually re-run the agent with those responses, use `wstack --replay <sessionId>`.",
1563
- usage: "wstack replay [<sessionId>] [--list]",
1564
- subcommands: [
1565
- { name: "<sessionId>", description: "Show the recorded entries (positional)." },
1566
- { name: "--list / -l", description: "List every session that has a replay log." }
1567
- ],
1568
- seeAlso: "wstack audit (the tamper-evident tool-call log)"
1569
- },
1570
- rewind: {
1571
- name: "rewind",
1572
- title: "wstack rewind \u2014 rewind a session to an earlier state",
1573
- description: "Restore a session's in-memory state to a previous point in its recording. The rewind is non-destructive: the original session is preserved, and a new resumed session picks up from the rewound point. Useful for re-running a fork of an exploration without losing the original.",
1574
- usage: "wstack rewind [<sessionId>] [--all|--last <n>|--to <id>] [--list] [--resume]",
1575
- subcommands: [
1576
- { name: "<sessionId>", description: "Session id (positional; defaults to the latest)." },
1577
- { name: "--all", description: "Rewind to the start of the session." },
1578
- { name: "--last <n>", description: "Rewind to `n` steps back from the end." },
1579
- { name: "--to <id>", description: "Rewind to a specific step id." },
1580
- { name: "--list", description: "List available rewind points for the session." },
1581
- { name: "--resume", description: "Resume the rewound session after the rewind." }
1582
- ],
1583
- seeAlso: "wstack replay (the underlying provider-response log)"
1584
- },
1585
- // -- Export & usage ----------------------------------------------------
1586
- export: {
1587
- name: "export",
1588
- title: "wstack export \u2014 render a session to a portable format",
1589
- description: "Render a recorded session to Markdown, JSON, or plain text. Use Markdown for human-readable share/audit artifacts, JSON for downstream tooling, or text for grep-friendly search. Tools and diagnostics are included by default; toggle either off with `--no-tools` or `--no-diagnostics`.",
1590
- usage: "wstack export <sessionId> [--format markdown|json|text] [--out <file>] [--no-tools] [--no-diagnostics]",
1591
- subcommands: [
1592
- { name: "<sessionId>", description: "The session id to render (positional)." },
1593
- {
1594
- name: "--format <f> / -f <f>",
1595
- description: "Output format: markdown (default), json, or text."
1596
- },
1597
- { name: "--out <file> / -o <file>", description: "Write to <file> instead of stdout." },
1598
- { name: "--no-tools", description: "Omit tool-call entries from the output." },
1599
- {
1600
- name: "--no-diagnostics",
1601
- description: "Omit diagnostic entries (errors, retries) from the output."
1602
- }
1603
- ],
1604
- seeAlso: "wstack replay (the recorded provider-response log)"
1605
- },
1606
- usage: {
1607
- name: "usage",
1608
- title: "wstack usage \u2014 token + cost summary",
1609
- description: "Print a per-session token + cost summary from the audit log. Useful for cost reviews and the post-session billing recap. Aggregates input/output tokens and the per-model cost; requires the session to have been recorded with audit enabled.",
1610
- usage: "wstack usage",
1611
- seeAlso: "wstack export (full session render for archival)"
1612
- },
1613
- // -- Listing subcommands -----------------------------------------------
1614
- providers: {
1615
- name: "providers",
1616
- title: "wstack providers \u2014 list providers from models.dev",
1617
- description: "List provider families from the live models.dev catalog. Default view shows the popular three (Anthropic, OpenAI, Google); pass `--all` to include every supported family, or `--unsupported` to surface the ones without a built-in transport (which require a plugin).",
1618
- usage: "wstack providers [--all] [--unsupported]",
1619
- subcommands: [
1620
- { name: "--all", description: "Include every supported family, not just the popular three." },
1621
- {
1622
- name: "--unsupported",
1623
- description: "Include families without a built-in transport (need a plugin)."
1624
- }
1625
- ],
1626
- seeAlso: "wstack models (list models within a provider)"
1627
- },
1628
- tools: {
1629
- name: "tools",
1630
- title: "wstack tools \u2014 list registered tools",
1631
- description: "List every tool the agent can invoke, with its owner (built-in / plugin) and permission level. Useful for auditing what a session can do, especially after installing a new plugin.",
1632
- usage: "wstack tools",
1633
- seeAlso: "wstack skills (list skills; tools + skills are the two extension surfaces)"
1634
- },
1635
- skills: {
1636
- name: "skills",
1637
- title: "wstack skills \u2014 list discovered skills",
1638
- description: "List every skill the agent can invoke, grouped by source (bundled / user-installed / project-local). Skills are on-demand context packs that load only when triggered.",
1639
- usage: "wstack skills",
1640
- seeAlso: "wstack tools (tools are always-loaded; skills are on-demand)"
1641
- },
1642
- projects: {
1643
- name: "projects",
1644
- title: "wstack projects \u2014 list tracked projects",
1645
- description: "List every project WrongStack has seen (tracked by a hashed root). Each entry shows the project root and the last-seen timestamp. Useful for cleaning up the global projects dir after a workspace migration.",
1646
- usage: "wstack projects"
1647
- },
1648
- // -- Lifecycle ---------------------------------------------------------
1649
- update: {
1650
- name: "update",
1651
- title: "wstack update \u2014 self-update the CLI",
1652
- description: "Check the latest npm version and update the globally-installed `wrongstack` package. Use `--check-only` to just print the current/latest without installing. Pass `--pm <manager>` (or its shorthand `--npm`, `--pnpm`, `--yarn`, `--bun`) to force a specific package manager; pass `--allow-scripts` (alias `--lifecycle-scripts`) to opt into package lifecycle scripts during the update (off by default). The update is global; run from any project root.",
1653
- usage: "wstack update [--check-only] [--pm <manager>] [--allow-scripts (alias: --lifecycle-scripts)]",
1654
- seeAlso: "wstack version (read-only version info)"
1655
- },
1656
- // -- ACP (Agent Client Protocol) --------------------------------------
1657
- acp: {
1658
- name: "acp",
1659
- title: "wstack acp \u2014 Agent Client Protocol (ACP) integration",
1660
- description: "Run WrongStack as an ACP server (stdio) for editor clients (Zed, JetBrains, VS Code ACP), or orchestrate external ACP agents through spawn, probe, bench, and parallel fan-out.",
1661
- usage: "wstack acp [server|list|sync|spawn|parallel|probe|bench] [...]",
1662
- subcommands: [
1663
- { name: "server / serve", description: "Start WrongStack as an ACP stdio server (default)." },
1664
- { name: "list", description: "List available ACP agents from the cache." },
1665
- { name: "sync", description: "Sync the official ACP agent registry into local cache." },
1666
- { name: "spawn <id> <task>", description: "Spawn an ACP agent and wait for results." },
1667
- {
1668
- name: "parallel <csv> <task>",
1669
- description: "Fan a task out to multiple ACP agents in parallel."
1670
- },
1671
- { name: "probe [csv]", description: "Handshake-test installed ACP agents." },
1672
- { name: "bench [csv] [--fs]", description: "End-to-end benchmark and grade ACP agents." }
1673
- ],
1674
- seeAlso: "wstack mcp serve (the MCP equivalent; pick the protocol your client speaks)"
1675
- },
1676
- // -- Model diagnostics (read-only) -----------------------------------
1677
- modeldiag: {
1678
- name: "modeldiag",
1679
- title: "wstack modeldiag \u2014 model benchmarks + heuristic diagnostics",
1680
- description: "Read-only diagnostics for the configured model: key check, capability scan (vision / tools / context window), heuristic strengths/weaknesses (bestFor / avoidFor), sequential live provider/model smoke tests, and real benchmarks against a small prompt suite. Smoke tests use tiny prompts and never print credentials.",
1681
- usage: "wstack modeldiag [test|eval] [...]",
1682
- subcommands: [
1683
- {
1684
- name: "<no subcommand>",
1685
- description: "Print configured model capabilities, context limits, and recommendations."
1686
- },
1687
- {
1688
- name: "test [--plan|--all-models|--json]",
1689
- description: "Run live sequential smoke tests across configured models."
1690
- },
1691
- {
1692
- name: "eval [--providers <csv>]",
1693
- description: "Run model competency matrix evaluation across tasks."
1694
- }
1695
- ],
1696
- seeAlso: "wstack modeldiag test --plan; wstack modeldiag test; wstack modeldiag test --all-models"
1697
- },
1698
- // -- Bench (developer / CI only) -------------------------------------
1699
- bench: {
1700
- name: "bench",
1701
- title: "wstack bench \u2014 run model-independent agentic benchmarks",
1702
- description: "Run WrongStack against the Aider polyglot or SWE-bench Verified suites with deterministic graders. Used internally to compare model quality across releases; also useful for evaluating a new model before adopting it.",
1703
- usage: "wstack bench [run|report|list] [...]",
1704
- subcommands: [
1705
- { name: "run", description: "Run a benchmark suite (--suite <id> --models <config>)." },
1706
- { name: "report <dir>", description: "Render the Markdown report for a prior run." },
1707
- { name: "list", description: "List available suites and the model configs in the catalog." }
1708
- ],
1709
- seeAlso: "wstack modeldiag (read-only diagnostics; bench actually runs the model)"
1710
- },
1711
- // -- Quick launch ------------------------------------------------------
1712
- quick: {
1713
- name: "quick",
1714
- title: "wstack quick \u2014 launch the TUI with sensible defaults",
1715
- description: "Accept every default, list installed plugins, and open the TUI with the agents-monitor panel pre-shown. Equivalent to `wstack --tui --quick`; the dedicated subcommand is for discoverability and tab-completion. The actual TUI launch is intercepted in `boot()` before this handler runs.",
1716
- usage: "wstack quick",
1717
- seeAlso: "wstack --tui (the underlying flag; quick is just a shortcut)"
1718
- },
1719
- // -- Aliases and meta commands ───────────────────────────────────────
1720
- plugins: {
1721
- name: "plugins",
1722
- title: "wstack plugins \u2014 alias for wstack plugin",
1723
- description: "Alias for `wstack plugin`. See `wstack plugin --help` for the full set of subcommands.",
1724
- usage: "wstack plugins (alias for wstack plugin)",
1725
- seeAlso: "wstack plugin (the canonical command)"
1726
- },
1727
- help: {
1728
- name: "help",
1729
- title: "wstack help \u2014 print top-level help",
1730
- description: "Print the top-level help text listing every subcommand with a one-line summary. Equivalent to `wstack --help`.",
1731
- usage: "wstack help",
1732
- seeAlso: "wstack --help"
1733
- },
1734
- // -- HQ (dashboard server) ──────────────────────────────────────────
1735
- hq: {
1736
- name: "hq",
1737
- title: "wstack hq \u2014 start HQ command center or manage tokens",
1738
- description: "Start the HQ server \u2014 a web dashboard for monitoring sessions, fleet status, and agent activity across projects \u2014 or manage browser and client authentication tokens.",
1739
- usage: "wstack hq [serve] [--port <n>] [--password <secret>] [--tunnel] [--open] | wstack hq token [create|list|revoke]",
1740
- subcommands: [
1741
- { name: "serve", description: "Start the HQ dashboard server (default)." },
1742
- {
1743
- name: "token create [label]",
1744
- description: "Mint a browser or client authentication token."
1745
- },
1746
- { name: "token list", description: "List active HQ authentication tokens." },
1747
- { name: "token revoke <id>", description: "Revoke an issued HQ authentication token." }
1748
- ],
1749
- seeAlso: "wstack sessions fleet (CLI equivalent for fleet status)"
1750
- },
1751
- // -- Mailbox (external agent bridge) ────────────────────────────────
1752
- mailbox: {
1753
- name: "mailbox",
1754
- title: "wstack mailbox \u2014 serve the external-agent mailbox HTTP bridge",
1755
- description: "Start the mailbox HTTP bridge that lets external coding agents (Claude Code, Aider, custom scripts) communicate with the shared project mailbox. The server exposes a REST + SSE API for sending, checking, and acknowledging messages. Use `wstack mailbox serve` to start the listener.",
1756
- usage: "wstack mailbox serve [--port <n>]",
1757
- subcommands: [{ name: "serve", description: "Start the mailbox HTTP bridge listener." }],
1758
- seeAlso: "wstack hq (the HQ server includes mailbox routing)"
1759
- },
1760
- // -- Permissions ────────────────────────────────────────────────────
1761
- permissions: {
1762
- name: "permissions",
1763
- title: "wstack permissions \u2014 explain tool permission decisions",
1764
- description: "Side-effect-free permission decision explainer. Evaluates the effective permission rules for a tool and input arguments without prompting, modifying trust files, or mutating state.",
1765
- usage: "wstack permissions explain <tool> [--input '<json>'] [--json]",
1766
- subcommands: [
1767
- {
1768
- name: "explain <tool>",
1769
- description: "Explain how the permission policy evaluates a tool call."
1770
- }
1771
- ],
1772
- seeAlso: "wstack tools (list available tools)"
1773
- },
1774
- // -- Project management ─────────────────────────────────────────────
1775
- project: {
1776
- name: "project",
1777
- title: "wstack project \u2014 manage committed repository identity",
1778
- description: "Manage the committed repository identity (project.json) used to scope HQ dashboards, Kanban state, and multi-agent coordination. Commit the generated ID file so every clone shares the same project.",
1779
- usage: "wstack project [id|init|rekey] [--yes]",
1780
- subcommands: [
1781
- { name: "id", description: "Show the current project ID and config path (default)." },
1782
- { name: "init", description: "Initialize a project ID if not already present." },
1783
- { name: "rekey", description: "Generate a new project ID for a fork (requires --yes)." }
1784
- ],
1785
- seeAlso: "wstack projects (list all tracked projects)"
1786
- },
1787
- governance: {
1788
- name: "governance",
1789
- title: "wstack governance \u2014 inspect deterministic project-daemon health",
1790
- description: "Read token-free attachment-broker health from the current project daemon. Warnings are advisory: they surface deterministic operator escalation signals without stopping active tasks or model execution.",
1791
- usage: "wstack governance status [--json]",
1792
- subcommands: [{ name: "status", description: "Show daemon and attachment-broker health." }],
1793
- seeAlso: "wstack doctor (broader environment diagnostics)"
1794
- },
1795
- // -- Chronicle ──────────────────────────────────────────────────────
1796
- chronicle: {
1797
- name: "chronicle",
1798
- title: "wstack chronicle \u2014 query cross-session provenance ledger",
1799
- description: "Query the cross-session event ledger, inspect daemon status, compute facet aggregations, view token/cost metrics, or purge old retention entries.",
1800
- usage: "wstack chronicle [query|status|facet|metrics|prune|compact] [...]",
1801
- subcommands: [
1802
- { name: "query [field=value ...]", description: "Query recorded provenance events." },
1803
- { name: "status", description: "Check the Chronicle daemon and pipeline status." },
1804
- { name: "facet <field>", description: "Group and count events across a facet." },
1805
- {
1806
- name: "metrics [providers|tasks|files|summary]",
1807
- description: "Query aggregated metrics from metrics.db."
1808
- },
1809
- {
1810
- name: "prune [--days N] [--dry-run]",
1811
- description: "Purge journal entries older than N days."
1812
- },
1813
- { name: "compact", description: "Defragment and compact SQLite ledger." }
1814
- ],
1815
- seeAlso: "wstack audit (per-session audit log); wstack usage (cost summary)"
1816
- }
1817
- };
1818
-
1819
- // src/subcommands/handlers/per-subcommand-help.ts
1820
- var COLUMN_WIDTH = 28;
1821
- function renderBlockToString(help) {
1822
- if (help.customBody) {
1823
- return help.customBody();
1824
- }
1825
- const lines = [
1826
- color5.bold(help.title),
1827
- color5.dim(` ${help.description}`),
1828
- "",
1829
- color5.bold("Usage"),
1830
- ` ${help.usage}`
1831
- ];
1832
- if (help.subcommands && help.subcommands.length > 0) {
1833
- lines.push("");
1834
- lines.push(color5.bold("Subcommands"));
1835
- for (const { name, description } of help.subcommands) {
1836
- const padded = name.padEnd(COLUMN_WIDTH, " ");
1837
- lines.push(` ${color5.cyan(padded)}${description}`);
1838
- }
1839
- }
1840
- if (help.seeAlso) {
1841
- lines.push("");
1842
- lines.push(color5.dim(` See also: ${help.seeAlso}`));
1843
- }
1844
- lines.push("");
1845
- lines.push(color5.dim(" Tip: `wstack --help` lists every top-level command."));
1846
- return lines.join("\n") + "\n";
1847
- }
1848
- function renderBlock(help, renderer) {
1849
- renderer.write(renderBlockToString(help));
1850
- }
1851
- function renderDeepHelp(key, renderer) {
1852
- const help = deepHelpTable[key];
1853
- if (!help) return false;
1854
- renderBlock(help, renderer);
1855
- return true;
1856
- }
1857
- var deepSubcommandsWithFocusedHelp = Object.keys(deepHelpTable);
1858
- function renderFocusedHelp(subcommand, renderer) {
1859
- const help = helpTable[subcommand];
1860
- if (!help) return false;
1861
- renderBlock(help, renderer);
1862
- return true;
1863
- }
1864
- var subcommandsWithFocusedHelp = Object.keys(helpTable);
1865
-
1866
- // src/subcommands/index.ts
1867
- var loaders = {
1868
- acp: async () => (await import("./acp-CEQINXWB.js")).acpCmd,
1869
- init: async () => (await import("./init-4JQSG66Q.js")).initCmd,
1870
- auth: async () => (await import("./auth-3OSF6BMU.js")).authCmd,
1871
- update: async () => (await import("./update-K45CS6SS.js")).updateCmd,
1872
- sessions: async () => (await import("./sessions-config-P2LK2L32.js")).sessionsCmd,
1873
- config: async () => (await import("./sessions-config-P2LK2L32.js")).configCmd,
1874
- rewind: async () => (await import("./rewind-2FFNOOKS.js")).rewindCmd,
1875
- replay: async () => (await import("./replay-RZXZKP2U.js")).replayCmd,
1876
- audit: async () => (await import("./audit-ZPCLCC3C.js")).auditCmd,
1877
- tools: async () => (await import("./tools-skills-Q4P27URS.js")).toolsCmd,
1878
- skills: async () => (await import("./tools-skills-Q4P27URS.js")).skillsCmd,
1879
- providers: async () => (await import("./providers-models-663NDPRQ.js")).providersCmd,
1880
- models: async () => (await import("./providers-models-663NDPRQ.js")).modelsCmd,
1881
- mcp: async () => (await import("./mcp-AQKSASHD.js")).mcpCmd,
1882
- plugin: async () => (await import("./plugin-usage-AP7SJCDA.js")).pluginCmd,
1883
- plugins: async () => (await import("./plugin-usage-AP7SJCDA.js")).pluginCmd,
1884
- diag: async () => (await import("./diag-doctor-LFCCBPMD.js")).diagCmd,
1885
- doctor: async () => (await import("./diag-doctor-LFCCBPMD.js")).doctorCmd,
1886
- "proxy-status": async () => (await import("./diag-doctor-LFCCBPMD.js")).proxyCmd,
1887
- export: async () => (await import("./export-ROG3DKTZ.js")).exportCmd,
1888
- usage: async () => (await import("./plugin-usage-AP7SJCDA.js")).usageCmd,
1889
- version: async () => (await import("./version-help-WMPSEL4U.js")).versionCmd,
1890
- help: async () => (await import("./version-help-WMPSEL4U.js")).helpCmd,
1891
- projects: async () => (await import("./projects-5J727S4L.js")).projectsCmd,
1892
- modeldiag: async () => (await import("./modeldiag-4NYBRSFP.js")).modeldiagCmd,
1893
- quick: async () => (await import("./quick-XLYLZX4B.js")).quickCmd,
1894
- bench: async () => (await import("./bench-XYG56AMO.js")).benchCmd,
1895
- chronicle: async () => (await import("./chronicle-6EPPMUC5.js")).chronicleCmd,
1896
- hq: async () => (await import("./hq-3GWBFLON.js")).hqCmd,
1897
- mailbox: async () => (await import("./mailbox-serve-VKQIRCCB.js")).mailboxServeCmd,
1898
- permissions: async () => (await import("./permissions-2PWBL4YU.js")).permissionsCmd,
1899
- project: async () => (await import("./project-BLQ3ALVL.js")).projectCmd,
1900
- governance: async () => (await import("./governance-JSGHHA2R.js")).governanceCmd
1901
- };
1902
- var subcommands = Object.fromEntries(
1903
- Object.entries(loaders).map(
1904
- ([name, load]) => [
1905
- name,
1906
- async (args, deps) => (await load())(args, deps)
1907
- ]
1908
- )
1909
- );
1910
- var subcommandNames = Object.keys(loaders);
1911
-
1912
- // src/boot/short-circuit-flags.ts
1913
- async function handleHelpVersionShortCircuit(argv) {
1914
- const { flags: earlyFlags, positional } = parseArgs(argv);
1915
- if (earlyFlags["help"] !== true && earlyFlags["version"] !== true) {
1916
- return null;
1917
- }
1918
- const stubRenderer = {
1919
- write: (line) => {
1920
- process.stdout.write(line);
1921
- }
1922
- };
1923
- if (earlyFlags["version"] === true) {
1924
- return await versionCmd([], { renderer: stubRenderer });
1925
- }
1926
- if (earlyFlags["help"] === true) {
1927
- if (positional.length > 0 && subcommandNames.includes(positional[0])) {
1928
- const first = positional[0];
1929
- const deep = positional[1];
1930
- if (deep && renderDeepHelp(`${first}:${deep}`, stubRenderer)) {
1931
- return 0;
1932
- }
1933
- if (renderFocusedHelp(first, stubRenderer)) {
1934
- return 0;
1935
- }
1936
- }
1937
- return await helpCmd([], { renderer: stubRenderer });
1938
- }
1939
- return null;
1940
- }
1941
-
1942
- // src/boot/short-circuit-hq.ts
1943
- import * as net from "node:net";
1944
- import {
1945
- HQ_CLI_DEFAULT_HOST as HQ_CLI_DEFAULT_HOST2,
1946
- isLoopbackHost,
1947
- readHqRuntimeFileSync,
1948
- resolveHqDataDir
1949
- } from "@wrongstack/core/hq";
1950
- import { color as color6, isPidAlive } from "@wrongstack/core/utils";
1951
- async function isHqAlreadyRunning(dataDir) {
1952
- const marker = readHqRuntimeFileSync(dataDir);
1953
- if (!marker) return null;
1954
- if (marker.pid && isPidAlive(marker.pid)) return marker;
1955
- return null;
1956
- }
1957
- async function isPortInUse(host, port) {
1958
- return new Promise((resolve3) => {
1959
- const server = net.createServer();
1960
- server.once("error", (err) => {
1961
- if (err.code === "EADDRINUSE") {
1962
- resolve3(true);
1963
- } else {
1964
- resolve3(false);
1965
- }
1966
- });
1967
- server.once("listening", () => {
1968
- server.close(() => resolve3(false));
1969
- });
1970
- server.listen(port, host);
1971
- });
1972
- }
1973
- async function handleHqShortCircuit(flags) {
1974
- if (flags["hq"] !== true) return null;
1975
- const { startHqServer } = await import("./hq-server-OARPUSQJ.js");
1976
- const tunnelRequested = flags["tunnel"] === true;
1977
- const host = typeof flags["host"] === "string" ? flags["host"] : tunnelRequested ? "127.0.0.1" : HQ_CLI_DEFAULT_HOST2;
1978
- if (tunnelRequested && !isLoopbackHost(host)) {
1979
- process.stderr.write(
1980
- `${color6.red("\u2717")} --tunnel requires a loopback origin. Remove --host or use --host 127.0.0.1.
1981
- `
1982
- );
1983
- return 1;
1984
- }
1985
- let port;
1986
- if (typeof flags["port"] === "string" && flags["port"].trim() !== "") {
1987
- const parsed = Number.parseInt(flags["port"], 10);
1988
- if (Number.isFinite(parsed) && parsed > 0 && parsed < 65536) {
1989
- port = parsed;
1990
- } else {
1991
- process.stderr.write(`${color6.red("\u2717")} Invalid --port value: ${flags["port"]}
1992
- `);
1993
- return 1;
1994
- }
1995
- } else port = DEFAULT_PORT;
1996
- const dataDir = typeof flags["data-dir"] === "string" ? flags["data-dir"] : void 0;
1997
- const password = typeof flags["password"] === "string" ? flags["password"] : process.env.WRONGSTACK_HQ_PASSWORD;
1998
- if (password !== void 0 && password.length < 8) {
1999
- process.stderr.write(`${color6.red("\u2717")} HQ password must be at least 8 characters.
2000
- `);
2001
- return 1;
2002
- }
2003
- const userProvidedPort = typeof flags["port"] === "string" && flags["port"].trim() !== "" || port !== DEFAULT_PORT;
2004
- const resolvedDataDir = dataDir ?? resolveHqDataDir();
2005
- const existing = await isHqAlreadyRunning(resolvedDataDir);
2006
- if (existing) {
2007
- process.stderr.write(
2008
- `${color6.red("\u2717")} HQ is already running at ${existing.url} (PID ${existing.pid}). Stop it first or use a different --data-dir.
2009
- `
2010
- );
2011
- return 1;
2012
- }
2013
- if (await isPortInUse(host, port)) {
2014
- process.stderr.write(
2015
- `${color6.red("\u2717")} Port ${port} is already in use. Choose a different port or stop the process using it.
2016
- `
2017
- );
2018
- return 1;
2019
- }
2020
- let tokenTtlMs;
2021
- const rawTtl = typeof flags["hq-token-ttl"] === "string" ? flags["hq-token-ttl"] : process.env.WRONGSTACK_HQ_TOKEN_TTL;
2022
- if (rawTtl !== void 0 && rawTtl.length > 0) {
2023
- const { parseTokenTtlValue } = await import("./hq-ttl-KUO3AU7X.js");
2024
- const parsed = parseTokenTtlValue(rawTtl);
2025
- if (parsed.error) {
2026
- process.stderr.write(`${color6.red("\u2717")} Invalid --hq-token-ttl: ${parsed.error}
2027
- `);
2028
- return 1;
2029
- }
2030
- tokenTtlMs = parsed.value;
2031
- }
2032
- let handle;
2033
- try {
2034
- handle = await startHqServer({
2035
- host,
2036
- port,
2037
- strictPort: flags["strict-port"] === true,
2038
- exactPort: userProvidedPort,
2039
- allowInsecureOpen: flags["insecure-open"] === true,
2040
- secureCookies: tunnelRequested,
2041
- requireBrowserAuth: tunnelRequested,
2042
- ...dataDir !== void 0 ? { dataDir } : {},
2043
- ...password !== void 0 ? { password } : {},
2044
- ...tokenTtlMs !== void 0 ? { tokenTtlMs } : {}
2045
- });
2046
- } catch (err) {
2047
- const msg = err instanceof Error ? err.message : String(err);
2048
- if (err instanceof Error && "code" in err && err.code === "EADDRINUSE") {
2049
- process.stderr.write(
2050
- `${color6.red("\u2717")} Port ${port} is already in use. Please choose a different port.
2051
- `
2052
- );
2053
- } else {
2054
- process.stderr.write(`${color6.red("\u2717")} Failed to start HQ server: ${msg}
2055
- `);
2056
- }
2057
- return 1;
2058
- }
2059
- let tunnel;
2060
- let browserUrl = handle.firstRunSetup?.browserUrl ?? `http://${handle.host}:${handle.port}`;
2061
- if (tunnelRequested) {
2062
- if (handle.firstRunSetup !== void 0 && !handle.firstRunSetup.browserTokenMode && !handle.firstRunSetup.passwordMode) {
2063
- await handle.close();
2064
- process.stderr.write(
2065
- `${color6.red("\u2717")} Refusing to publish HQ in open mode. Set --password or create a browser token first.
2066
- `
2067
- );
2068
- return 1;
2069
- }
2070
- try {
2071
- const { buildPublicHqUrl, startHqQuickTunnel } = await import("./hq-tunnel-LO55RCZE.js");
2072
- const originHost = handle.host === "::1" ? "[::1]" : handle.host;
2073
- tunnel = await startHqQuickTunnel(`http://${originHost}:${handle.port}`, {
2074
- onUnexpectedExit: (message) => process.stderr.write(`${color6.yellow("!")} ${message}
2075
- `)
2076
- });
2077
- handle.trustPublicOrigin(new URL(tunnel.url).origin);
2078
- browserUrl = buildPublicHqUrl(
2079
- tunnel.url,
2080
- handle.firstRunSetup?.browserUrl,
2081
- handle.firstRunSetup?.passwordMode !== true
2082
- );
2083
- process.stdout.write(`
2084
- ${color6.green("Cloudflare Quick Tunnel ready:")} ${browserUrl}
2085
- `);
2086
- process.stdout.write(
2087
- `${color6.dim("Temporary development URL; DNS may need a few seconds, it changes on restart and ends when HQ stops.")}
2088
- `
2089
- );
2090
- } catch (err) {
2091
- await handle.close();
2092
- process.stderr.write(
2093
- `${color6.red("\u2717")} Failed to start Cloudflare Quick Tunnel: ${err instanceof Error ? err.message : String(err)}
2094
- `
2095
- );
2096
- return 1;
2097
- }
2098
- }
2099
- if (flags["open"] === true) {
2100
- try {
2101
- const { openBrowser } = await import("@wrongstack/webui-server");
2102
- openBrowser(browserUrl);
2103
- } catch {
2104
- }
2105
- }
2106
- await new Promise((resolve3) => {
2107
- const shutdown = async () => {
2108
- const results = await Promise.allSettled([
2109
- ...tunnel ? [tunnel.close()] : [],
2110
- handle.close()
2111
- ]);
2112
- const failure = results.find(
2113
- (result) => result.status === "rejected"
2114
- );
2115
- if (failure) {
2116
- console.error(
2117
- JSON.stringify({
2118
- level: "error",
2119
- event: "hq.server_close_failed",
2120
- message: String(failure.reason),
2121
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2122
- })
2123
- );
2124
- }
2125
- resolve3();
2126
- };
2127
- process.on("SIGINT", shutdown);
2128
- process.on("SIGTERM", shutdown);
2129
- });
2130
- return 0;
2131
- }
2132
-
2133
- // src/boot.ts
2134
- import * as fs7 from "node:fs/promises";
2135
- import { createRequire as createRequire4 } from "node:module";
2136
- import * as os from "node:os";
2137
- import * as path8 from "node:path";
2138
- import { fileURLToPath } from "node:url";
2139
- import { toErrorMessage as toErrorMessage3 } from "@wrongstack/core/utils";
2140
- import { DefaultLogger } from "@wrongstack/core/infrastructure";
2141
- import { TOKENS as TOKENS2 } from "@wrongstack/core/kernel";
2142
- import { DefaultModelsRegistry } from "@wrongstack/core/models";
2143
- import { ToolRegistry } from "@wrongstack/core/registry";
2144
- import { normalizeTokenSavingTier } from "@wrongstack/core/types";
2145
- import { color as color13, isStdinTTY as isStdinTTY2, writeErr } from "@wrongstack/core/utils";
2146
- import { createDefaultContainer as createDefaultContainer2 } from "@wrongstack/runtime";
2147
- import { registerBuiltinToolTier } from "@wrongstack/tools/tool-tier";
2148
-
2149
- // src/boot/config-backup-recovery.ts
2150
- import * as fs2 from "node:fs/promises";
2151
- import * as path4 from "node:path";
2152
- import { atomicWrite as atomicWrite2, backupConfigFile, color as color7, toErrorMessage } from "@wrongstack/core/utils";
2153
- function isRecord(value) {
2154
- return value !== null && typeof value === "object" && !Array.isArray(value);
2155
- }
2156
- function providerIds(value) {
2157
- if (!isRecord(value)) return [];
2158
- const providers = value["providers"];
2159
- return isRecord(providers) ? Object.keys(providers) : [];
2160
- }
2161
- async function defaultProfileProvidersAreEmpty(profilePath) {
2162
- try {
2163
- const parsed = JSON.parse(await fs2.readFile(profilePath, "utf8"));
2164
- return providerIds(parsed).length === 0;
2165
- } catch {
2166
- return false;
2167
- }
2168
- }
2169
- function profileSnapshotTimestamp(fileName, fallback) {
2170
- const match = /^profiles-default-config-(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d{3})\.json$/i.exec(
2171
- fileName
2172
- );
2173
- if (!match) return fallback;
2174
- const [, year, month, day, hour, minute, second, millisecond] = match;
2175
- return Date.UTC(
2176
- Number(year),
2177
- Number(month) - 1,
2178
- Number(day),
2179
- Number(hour),
2180
- Number(minute),
2181
- Number(second),
2182
- Number(millisecond)
2183
- );
2184
- }
2185
- async function findLatestProviderBackup(globalRoot) {
2186
- const directory = path4.join(globalRoot, "config-history");
2187
- const candidates = [];
2188
- let entries;
2189
- try {
2190
- entries = await fs2.readdir(directory, { withFileTypes: true });
2191
- } catch {
2192
- return void 0;
2193
- }
2194
- for (const entry of entries) {
2195
- 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)) {
2196
- continue;
2197
- }
2198
- const filePath = path4.join(directory, entry.name);
2199
- try {
2200
- const stat2 = await fs2.stat(filePath);
2201
- candidates.push({
2202
- filePath,
2203
- fileName: entry.name,
2204
- modifiedAtMs: profileSnapshotTimestamp(entry.name, stat2.mtimeMs)
2205
- });
2206
- } catch {
2207
- }
2208
- }
2209
- candidates.sort((a, b) => b.modifiedAtMs - a.modifiedAtMs);
2210
- for (const candidate of candidates) {
2211
- try {
2212
- const raw = await fs2.readFile(candidate.filePath, "utf8");
2213
- const parsed = JSON.parse(raw);
2214
- const ids = providerIds(parsed);
2215
- if (ids.length === 0) continue;
2216
- return { ...candidate, raw, providerIds: ids };
2217
- } catch {
2218
- }
2219
- }
2220
- return void 0;
2221
- }
2222
- async function restoreDefaultProfileBackup(opts) {
2223
- await backupConfigFile(opts.profilePath, { globalRoot: opts.globalRoot });
2224
- await fs2.mkdir(path4.dirname(opts.profilePath), { recursive: true });
2225
- await atomicWrite2(opts.profilePath, opts.backup.raw, { mode: 384 });
2226
- }
2227
- async function maybeRestoreDefaultProfileFromBackup(opts) {
2228
- if (!await defaultProfileProvidersAreEmpty(opts.profilePath)) return false;
2229
- const backup = await findLatestProviderBackup(opts.globalRoot);
2230
- if (!backup) return false;
2231
- const shownProviders = backup.providerIds.slice(0, 5).join(", ");
2232
- const remainder = backup.providerIds.length - 5;
2233
- const providerSummary = remainder > 0 ? `${shownProviders} +${remainder}` : shownProviders;
2234
- opts.renderer.write(
2235
- `
2236
- ${color7.amber("\u25CB")} ${color7.bold("default")} profile has no saved providers.
2237
- ${color7.dim("Latest usable backup:")} ${color7.bold(backup.fileName)} ${color7.dim(`(${backup.providerIds.length} provider: ${providerSummary})`)}
2238
- `
2239
- );
2240
- const answer = (await opts.reader.readLine(
2241
- ` ${color7.amber("?")} Restore all settings from this backup? ${color7.dim("[Y/n]")} `
2242
- )).trim().toLowerCase();
2243
- if (answer === "n" || answer === "no") return false;
2244
- try {
2245
- await restoreDefaultProfileBackup({
2246
- backup,
2247
- globalRoot: opts.globalRoot,
2248
- profilePath: opts.profilePath
2249
- });
2250
- opts.renderer.write(
2251
- ` ${color7.green("\u2713")} Restored all settings from ${color7.dim(backup.fileName)}
2252
- `
2253
- );
2254
- return true;
2255
- } catch (err) {
2256
- opts.renderer.writeWarning(`Could not restore config backup: ${toErrorMessage(err)}
2257
- `);
2258
- return false;
2259
- }
2260
- }
2261
-
2262
- // src/boot/system-prompt-menu.ts
2263
- import {
2264
- countSystemPromptTokens,
2265
- persistSystemPromptVariant,
2266
- readSavedSystemPromptVariant,
2267
- SYSTEM_PROMPT_VARIANT_OPTIONS,
2268
- SYSTEM_PROMPT_VARIANTS
2269
- } from "@wrongstack/core/agent";
2270
- import { color as color10 } from "@wrongstack/core/utils";
2271
-
2272
- // src/pre-launch/launch-prompts.ts
2273
- import * as fs3 from "node:fs/promises";
2274
- import { atomicWrite as atomicWrite3, color as color8 } from "@wrongstack/core/utils";
2275
- var LaunchAbortedError = class extends Error {
2276
- exitCode = 0;
2277
- constructor() {
2278
- super("Launch cancelled by user");
2279
- this.name = "LaunchAbortedError";
2280
- }
2281
- };
2282
- async function runLaunchPrompts(opts) {
2283
- const { renderer, reader, modePinned, yoloPinned, autonomyPinned, lastChoices } = opts;
2284
- if (modePinned !== void 0 && yoloPinned !== void 0 && autonomyPinned !== void 0) {
2285
- return {
2286
- mode: modePinned,
2287
- yolo: yoloPinned,
2288
- autonomy: autonomyPinned
2289
- };
2290
- }
2291
- if (!lastChoices) {
2292
- return {
2293
- mode: modePinned ?? "tui",
2294
- yolo: yoloPinned ?? true,
2295
- autonomy: autonomyPinned ?? "auto"
2296
- };
2297
- }
2298
- const hasOverride = modePinned !== void 0 && modePinned !== lastChoices.mode || yoloPinned !== void 0 && yoloPinned !== lastChoices.yolo || autonomyPinned !== void 0 && autonomyPinned !== lastChoices.autonomy;
2299
- if (lastChoices && !hasOverride) {
2300
- const effective = {
2301
- mode: modePinned ?? lastChoices.mode,
2302
- yolo: yoloPinned ?? lastChoices.yolo,
2303
- autonomy: autonomyPinned ?? lastChoices.autonomy
2304
- };
2305
- const onOff = (v) => v ? color8.green("on") : color8.dim("off");
2306
- const modeLabel = effective.mode.toUpperCase();
2307
- renderer.write(
2308
- `
2309
- ${color8.dim("Last settings:")} ${color8.bold(modeLabel)} \xB7 YOLO ${onOff(effective.yolo)} \xB7 Autonomy ${effective.autonomy === "auto" ? color8.green("auto") : color8.dim("off")}
2310
- `
2311
- );
2312
- const answer = (await reader.readLine(
2313
- ` ${color8.amber("?")} Continue with these? ${color8.dim("[Y/n/q]")} ${color8.dim("(auto Y in 5s)")} `,
2314
- { timeoutMs: 5e3, defaultAnswer: "y" }
2315
- )).trim().toLowerCase();
2316
- if (answer === "q") {
2317
- renderer.write(color8.dim(" Goodbye!\n"));
2318
- throw new LaunchAbortedError();
2319
- }
2320
- if (answer !== "n" && answer !== "no") {
2321
- const badges2 = buildBadges(effective);
2322
- const badgeStr2 = badges2.length > 0 ? ` (${badges2.join(" \xB7 ")})` : "";
2323
- renderer.write(
2324
- `
2325
- ${color8.green("\u25B6")} Launching in ${color8.bold(modeLabel)} mode${badgeStr2}
2326
-
2327
- `
2328
- );
2329
- return effective;
2330
- }
2331
- }
2332
- let mode;
2333
- if (modePinned) {
2334
- mode = modePinned;
2335
- } else {
2336
- const answer = (await reader.readLine(
2337
- `
2338
- ${color8.amber("?")} Interactive mode: ${color8.bold("T")}UI / ${color8.bold("R")}EPL ${color8.dim("[T/r/q]")} `
2339
- )).trim().toLowerCase();
2340
- if (answer === "q") {
2341
- renderer.write(color8.dim(" Goodbye!\n"));
2342
- throw new LaunchAbortedError();
2343
- }
2344
- mode = answer === "r" || answer === "repl" ? "repl" : "tui";
2345
- }
2346
- let yolo;
2347
- if (yoloPinned !== void 0 && yoloPinned === lastChoices?.yolo) {
2348
- yolo = yoloPinned;
2349
- } else {
2350
- const answer = (await reader.readLine(
2351
- ` ${color8.amber("?")} YOLO mode ${color8.dim("(auto-approve tool calls)")} ${color8.dim("[Y/n/q]")} `
2352
- )).trim().toLowerCase();
2353
- if (answer === "q") {
2354
- renderer.write(color8.dim(" Goodbye!\n"));
2355
- throw new LaunchAbortedError();
2356
- }
2357
- yolo = answer !== "n" && answer !== "no";
2358
- }
2359
- let autonomy;
2360
- if (autonomyPinned !== void 0 && autonomyPinned === lastChoices?.autonomy) {
2361
- autonomy = autonomyPinned;
2362
- } else {
2363
- const answer = (await reader.readLine(
2364
- ` ${color8.amber("?")} Autonomy mode ${color8.dim("(auto-continue \u2014 agent picks next step)")} ${color8.dim("[Y/n/q]")} `
2365
- )).trim().toLowerCase();
2366
- if (answer === "q") {
2367
- renderer.write(color8.dim(" Goodbye!\n"));
2368
- throw new LaunchAbortedError();
2369
- }
2370
- autonomy = answer !== "n" && answer !== "no" ? "auto" : "off";
2371
- }
2372
- const badges = buildBadges({ mode, yolo, autonomy });
2373
- const badgeStr = badges.length > 0 ? ` (${badges.join(" \xB7 ")})` : "";
2374
- renderer.write(
2375
- `
2376
- ${color8.green("\u25B6")} Launching in ${color8.bold(mode.toUpperCase())} mode${badgeStr}
2377
-
2378
- `
2379
- );
2380
- return { mode, yolo, autonomy };
2381
- }
2382
- function buildBadges(chosen) {
2383
- const badges = [];
2384
- if (chosen.yolo) badges.push(color8.yellow("YOLO"));
2385
- if (chosen.autonomy !== "off")
2386
- badges.push(color8.magenta(`AUTONOMY:${chosen.autonomy.toUpperCase()}`));
2387
- return badges;
2388
- }
2389
- async function persistLaunchChoices(configPath, choices) {
2390
- let fileExists = false;
2391
- try {
2392
- await fs3.access(configPath);
2393
- fileExists = true;
2394
- } catch {
2395
- }
2396
- let existing = {};
2397
- try {
2398
- const raw = await fs3.readFile(configPath, "utf8");
2399
- existing = JSON.parse(raw);
2400
- } catch (err) {
2401
- if (fileExists) {
2402
- throw new Error(
2403
- `Refusing to overwrite corrupt config at ${configPath} (${err.message}). Fix or move the file aside before retrying.`,
2404
- { cause: err }
2405
- );
2406
- }
2407
- existing = {};
2408
- }
2409
- existing.yolo = choices.yolo;
2410
- existing.launch = {
2411
- mode: choices.mode,
2412
- autonomy: choices.autonomy
2413
- };
2414
- await atomicWrite3(configPath, JSON.stringify(existing, null, 2), { mode: 384 });
2415
- }
2416
-
2417
- // src/pre-launch/project-check.ts
2418
- import * as fs4 from "node:fs/promises";
2419
- import * as path5 from "node:path";
2420
- import { color as color9, toErrorMessage as toErrorMessage2 } from "@wrongstack/core/utils";
2421
- var MANIFESTS = [
2422
- "package.json",
2423
- "pyproject.toml",
2424
- "Cargo.toml",
2425
- "go.mod",
2426
- "Makefile",
2427
- "pom.xml",
2428
- "build.gradle",
2429
- "build.gradle.kts",
2430
- "composer.json",
2431
- "Gemfile"
2432
- ];
2433
- async function detectProjectKind(projectRoot) {
2434
- try {
2435
- await fs4.access(path5.join(projectRoot, ".wrongstack", "AGENTS.md"));
2436
- return "initialized";
2437
- } catch {
2438
- }
2439
- for (const m of MANIFESTS) {
2440
- try {
2441
- await fs4.access(path5.join(projectRoot, m));
2442
- return "project";
2443
- } catch {
2444
- }
2445
- }
2446
- return "empty";
2447
- }
2448
- async function scaffoldAgentsMd(projectRoot) {
2449
- const dir = path5.join(projectRoot, ".wrongstack");
2450
- const file = path5.join(dir, "AGENTS.md");
2451
- const facts = await detectProjectFacts(projectRoot);
2452
- const body = renderAgentsTemplate(facts);
2453
- await fs4.mkdir(dir, { recursive: true });
2454
- await fs4.writeFile(file, body, "utf8");
2455
- return file;
2456
- }
2457
- async function runProjectCheck(opts) {
2458
- const { projectRoot, cwd, renderer, reader } = opts;
2459
- const kind = await detectProjectKind(projectRoot);
2460
- if (kind === "initialized") {
2461
- renderer.write(
2462
- `
2463
- ${color9.green("\u2713")} Project initialized ${color9.dim(`(${path5.join(projectRoot, ".wrongstack", "AGENTS.md")})`)}
2464
- `
2465
- );
2466
- return true;
2467
- }
2468
- if (kind === "project") {
2469
- renderer.write(
2470
- `
2471
- ${color9.amber("\u25CF")} Project detected ${color9.dim(`(${projectRoot})`)} but ${color9.bold(".wrongstack/AGENTS.md")} is missing.
2472
- `
2473
- );
2474
- const answer2 = (await reader.readLine(
2475
- ` ${color9.amber("?")} Scaffold ${color9.bold("AGENTS.md")} now? ${color9.dim("[y/N/q]")} `
2476
- )).trim().toLowerCase();
2477
- if (answer2 === "q") {
2478
- renderer.write(color9.dim(" Cancelled.\n"));
2479
- return false;
2480
- }
2481
- if (answer2 === "y" || answer2 === "yes") {
2482
- try {
2483
- const file = await scaffoldAgentsMd(projectRoot);
2484
- renderer.write(` ${color9.green("\u2713")} Wrote ${color9.dim(file)}
2485
- `);
2486
- } catch (err) {
2487
- renderer.writeError(`Failed to scaffold AGENTS.md: ${toErrorMessage2(err)}`);
2488
- }
2489
- }
2490
- return true;
2491
- }
2492
- const gitDir = path5.join(projectRoot, ".git");
2493
- let hasGit = false;
2494
- try {
2495
- await fs4.access(gitDir);
2496
- hasGit = true;
2497
- } catch {
2498
- }
2499
- if (!hasGit) {
2500
- renderer.write(
2501
- `
2502
- ${color9.dim("\u25CB")} ${color9.dim(`No project manifest in ${projectRoot} \u2014 running in a scratch directory.`)}
2503
- `
2504
- );
2505
- const answer2 = (await reader.readLine(
2506
- ` ${color9.amber("?")} No git repo found. ${color9.bold("Initialize git?")} ${color9.dim("[y/N/q]")} `
2507
- )).trim().toLowerCase();
2508
- if (answer2 === "q") {
2509
- renderer.write(color9.dim(" Cancelled.\n"));
2510
- return false;
2511
- }
2512
- if (answer2 === "y" || answer2 === "yes") {
2513
- try {
2514
- const { spawn: spawn2 } = await import("node:child_process");
2515
- await new Promise((resolve3, reject) => {
2516
- const child = spawn2("git", ["init"], {
2517
- cwd,
2518
- signal: AbortSignal.timeout(1e4),
2519
- windowsHide: true
2520
- });
2521
- child.on("error", reject);
2522
- child.on(
2523
- "close",
2524
- (code) => code === 0 ? resolve3() : reject(new Error(`git init failed with ${code}`))
2525
- );
2526
- });
2527
- renderer.write(` ${color9.green("\u2713")} Git repository initialized
2528
- `);
2529
- } catch (err) {
2530
- renderer.writeError(`git init failed: ${toErrorMessage2(err)}
2531
- `);
2532
- }
2533
- }
2534
- } else {
2535
- renderer.write(
2536
- `
2537
- ${color9.dim("\u25CB")} ${color9.dim(`No project manifest in ${projectRoot} \u2014 running in a scratch directory.`)}
2538
- `
2539
- );
2540
- }
2541
- const answer = (await reader.readLine(` ${color9.amber("?")} Continue anyway? ${color9.dim("[Y/n/q]")} `)).trim().toLowerCase();
2542
- if (answer === "q" || answer === "n" || answer === "no") {
2543
- renderer.write(color9.dim(" Cancelled.\n"));
2544
- return false;
2545
- }
2546
- return true;
2547
- }
2548
-
2549
- // src/boot/system-prompt-menu.ts
2550
- var SYSTEM_PROMPT_OPTIONS = SYSTEM_PROMPT_VARIANT_OPTIONS;
2551
- var SUMMARY_TIMEOUT_MS2 = 5e3;
2552
- function flagOn(value) {
2553
- if (value === true) return true;
2554
- if (typeof value !== "string") return false;
2555
- const normalized = value.trim().toLowerCase();
2556
- return normalized === "true" || normalized === "1" || normalized === "yes" || normalized === "on";
2557
- }
2558
- function isSystemPromptPinned(flags) {
2559
- return flagOn(flags["system-pro"]) || flagOn(flags["system-lite"]) || typeof flags["system-prompt"] === "string";
2560
- }
2561
- function shouldSkipSystemPromptMenu(flags) {
2562
- if (isSystemPromptPinned(flags)) return true;
2563
- if (flags["no-menu"] === true) return true;
2564
- if (flags["no-interactive"] === true) return true;
2565
- if (flags["skip"] === true) return true;
2566
- if (flags["webui"] === true) return true;
2567
- return false;
2568
- }
2569
- function labelFor(variant) {
2570
- return SYSTEM_PROMPT_OPTIONS.find((o) => o.variant === variant)?.label ?? variant;
2571
- }
2572
- async function runSystemPromptMenu(deps) {
2573
- const { renderer, reader, paths, lastVariant } = deps;
2574
- const tokens = await countSystemPromptTokens(paths);
2575
- const defaultVariant = lastVariant ?? "default";
2576
- if (lastVariant) {
2577
- const answer2 = (await reader.readLine(
2578
- ` ${color10.amber("?")} Continue with ${color10.bold(labelFor(lastVariant))} system prompt ${color10.dim(`(~${fmtTok(tokens[lastVariant])} tokens)`)}? ${color10.dim("[Y/n/q]")} ${color10.dim("(auto Y in 5s)")} `,
2579
- { timeoutMs: SUMMARY_TIMEOUT_MS2, defaultAnswer: "y" }
2580
- )).trim().toLowerCase();
2581
- if (answer2 === "q" || answer2 === "quit") throw new LaunchAbortedError();
2582
- if (answer2 !== "n" && answer2 !== "no") return lastVariant;
2583
- }
2584
- renderer.write(`
2585
- ${color10.amber("?")} System prompt:
2586
- `);
2587
- for (const [index, opt] of SYSTEM_PROMPT_OPTIONS.entries()) {
2588
- const num2 = index + 1;
2589
- const isDefault = opt.variant === defaultVariant;
2590
- const marker = isDefault ? color10.cyan("\u25CF") : color10.dim("\u25CB");
2591
- const label = isDefault ? color10.bold(opt.label) : opt.label;
2592
- renderer.write(
2593
- ` ${color10.dim(`${num2}.`)} ${marker} ${label.padEnd(9)} ${color10.dim(`~${fmtTok(tokens[opt.variant])} tokens`)} ${color10.dim(opt.hint)}
2594
- `
2595
- );
2596
- }
2597
- const answer = (await reader.readLine(
2598
- `
2599
- ${color10.amber("?")} Select system prompt ${color10.dim(`[1-${SYSTEM_PROMPT_OPTIONS.length}, Enter = ${labelFor(defaultVariant)}, q to quit]`)}: `
2600
- )).trim().toLowerCase();
2601
- if (answer === "q" || answer === "quit") throw new LaunchAbortedError();
2602
- if (!answer) return defaultVariant;
2603
- const num = Number.parseInt(answer, 10);
2604
- if (!Number.isNaN(num) && num >= 1 && num <= SYSTEM_PROMPT_OPTIONS.length) {
2605
- return SYSTEM_PROMPT_OPTIONS[num - 1].variant;
2606
- }
2607
- const byName = SYSTEM_PROMPT_OPTIONS.find(
2608
- (o) => o.label.toLowerCase() === answer || o.variant === answer
2609
- );
2610
- if (byName) return byName.variant;
2611
- return defaultVariant;
2612
- }
2613
- async function maybeRunSystemPromptMenu(opts) {
2614
- if (!opts.isInteractiveTTY) return { aborted: false, changed: false };
2615
- if (shouldSkipSystemPromptMenu(opts.flags)) return { aborted: false, changed: false };
2616
- let savedVariant;
2617
- try {
2618
- savedVariant = await readSavedSystemPromptVariant(opts.profileConfigPath);
2619
- const chosen = await runSystemPromptMenu({
2620
- renderer: opts.renderer,
2621
- reader: opts.reader,
2622
- lastVariant: savedVariant,
2623
- paths: opts.paths
2624
- });
2625
- if (chosen === savedVariant) return { variant: chosen, aborted: false, changed: false };
2626
- try {
2627
- await persistSystemPromptVariant(opts.profileConfigPath, chosen);
2628
- } catch (err) {
2629
- return { variant: chosen, aborted: false, changed: true, persistError: err };
2630
- }
2631
- return { variant: chosen, aborted: false, changed: true };
2632
- } catch (err) {
2633
- if (err instanceof LaunchAbortedError) return { aborted: true, changed: false };
2634
- throw err;
2635
- }
2636
- }
2637
-
2638
- // src/boot-config.ts
2639
- import { bootConfig as coreBootConfig } from "@wrongstack/core/infrastructure";
2640
- async function bootConfig(flags) {
2641
- const isWebui = !!flags["webui"];
2642
- const opts = {
2643
- flags,
2644
- appLabel: "wstack",
2645
- ...isWebui ? { skipIdentityValidation: true } : {}
2646
- };
2647
- const { cwd, projectRoot, userHome, wpaths, pathResolver, config, vault } = await coreBootConfig(opts);
2648
- return {
2649
- paths: { cwd, projectRoot, userHome, wpaths, pathResolver },
2650
- config,
2651
- vault
2652
- };
2653
- }
2654
-
2655
- // src/input-reader.ts
2656
- import * as fs5 from "node:fs/promises";
2657
- import * as path6 from "node:path";
2658
- import * as readline from "node:readline";
7
+ CLI_VERSION
8
+ } from "./chunk-XJXDOF63.js";
2659
9
  import {
2660
- resolveWstackPaths,
2661
- setOutputLineGuard,
2662
- setRawMode as setRawMode2,
2663
- writeOut
2664
- } from "@wrongstack/core/utils";
2665
- var ReadlineInputReader = class {
2666
- rl;
2667
- historyFile;
2668
- history = [];
2669
- pending = false;
2670
- suppressNextHistory = false;
2671
- constructor(opts = {}) {
2672
- this.historyFile = opts.historyFile ?? resolveWstackPaths({ projectRoot: process.cwd() }).historyFile;
2673
- }
2674
- async loadHistory() {
2675
- try {
2676
- const raw = await fs5.readFile(this.historyFile, "utf8");
2677
- this.history = raw.split("\n").filter(Boolean).slice(-1e3);
2678
- } catch {
2679
- this.history = [];
2680
- }
2681
- }
2682
- async saveHistory() {
2683
- try {
2684
- await fs5.mkdir(path6.dirname(this.historyFile), { recursive: true });
2685
- await fs5.writeFile(this.historyFile, this.history.slice(-1e3).join("\n"));
2686
- } catch {
2687
- }
2688
- }
2689
- shouldPersistHistory(line) {
2690
- return !/^\s*\/(?:telegram-setup|tg-setup)\s+\d+:[A-Za-z0-9_-]+(?:\s|$)/i.test(line);
2691
- }
2692
- ensure() {
2693
- if (!this.rl) {
2694
- this.rl = readline.createInterface({
2695
- input: process.stdin,
2696
- output: process.stdout,
2697
- history: this.history,
2698
- terminal: process.stdin.isTTY
2699
- });
2700
- }
2701
- return this.rl;
2702
- }
2703
- async readLine(prompt, timeoutOpts) {
2704
- if (this.history.length === 0) await this.loadHistory();
2705
- while (this.pending) {
2706
- await new Promise((resolve3) => setTimeout(resolve3, 50));
2707
- }
2708
- this.pending = true;
2709
- try {
2710
- if (this.rl) {
2711
- const old = this.rl;
2712
- this.rl = void 0;
2713
- await new Promise((resolve3) => {
2714
- if (old.closed) {
2715
- resolve3();
2716
- } else {
2717
- old.once("close", resolve3);
2718
- old.close();
2719
- }
2720
- });
2721
- }
2722
- const fresh = this.ensure();
2723
- this.installPromptGuard(fresh);
2724
- const line = await new Promise((resolve3) => {
2725
- let settled = false;
2726
- let timeoutHandle;
2727
- const settle = (line2) => {
2728
- if (settled) return;
2729
- settled = true;
2730
- if (timeoutHandle) clearTimeout(timeoutHandle);
2731
- setOutputLineGuard(null);
2732
- resolve3(line2);
2733
- };
2734
- if (timeoutOpts) {
2735
- timeoutHandle = setTimeout(() => {
2736
- if (!settled) {
2737
- fresh.write(`${timeoutOpts.defaultAnswer}
2738
- `);
2739
- }
2740
- }, timeoutOpts.timeoutMs);
2741
- }
2742
- fresh.question(prompt ?? "> ", (line2) => {
2743
- const persistHistory = !this.suppressNextHistory && this.shouldPersistHistory(line2);
2744
- this.suppressNextHistory = false;
2745
- if (line2.trim() && persistHistory) {
2746
- this.history.push(line2);
2747
- this.saveHistory().then(void 0, () => {
2748
- });
2749
- }
2750
- settle(line2);
2751
- });
2752
- fresh.once("close", () => settle(""));
2753
- fresh.on?.("error", (_e) => settle(""));
2754
- }).then((result) => {
2755
- if (this.rl === fresh) this.rl = void 0;
2756
- fresh.close();
2757
- return result;
2758
- });
2759
- return line;
2760
- } finally {
2761
- this.pending = false;
2762
- }
2763
- }
2764
- /**
2765
- * Install the out-of-band write guard for the active prompt. When a log
2766
- * line or other async output lands while the user is mid-type, the guard
2767
- * clears the draft row, lets the message print, then repaints the prompt
2768
- * and the in-progress draft (cursor preserved) via readline's own
2769
- * refresh. Without it, each async write leaves the half-typed line
2770
- * stranded as a fresh scrollback row.
2771
- *
2772
- * No-op on non-TTY output (piped/redirected) — there's no draft to
2773
- * protect and the ANSI clear/repaint would be noise in a file.
2774
- */
2775
- installPromptGuard(rl) {
2776
- const out = process.stdout;
2777
- if (!out.isTTY) {
2778
- setOutputLineGuard(null);
2779
- return;
2780
- }
2781
- setOutputLineGuard({
2782
- suspend() {
2783
- readline.cursorTo(out, 0);
2784
- readline.clearLine(out, 0);
2785
- },
2786
- resume() {
2787
- try {
2788
- rl.prompt(true);
2789
- } catch {
2790
- }
2791
- }
2792
- });
2793
- }
2794
- async readKey(prompt, options) {
2795
- setOutputLineGuard(null);
2796
- writeOut(prompt);
2797
- return new Promise((resolve3) => {
2798
- const stdin = process.stdin;
2799
- const wasRaw = stdin.isRaw;
2800
- const wasPaused = stdin.isPaused();
2801
- setRawMode2(stdin, true);
2802
- stdin.resume();
2803
- const onData = (buf) => {
2804
- const key = buf.toString();
2805
- if (key === "") {
2806
- cleanup();
2807
- writeOut("\n");
2808
- resolve3("");
2809
- return;
2810
- }
2811
- const opt = options.find(
2812
- (o) => o.key.toLowerCase() === key.toLowerCase() || o.value === key
2813
- );
2814
- if (opt) {
2815
- cleanup();
2816
- writeOut(`${opt.key}
2817
- `);
2818
- resolve3(opt.value);
2819
- }
2820
- };
2821
- const onClose = () => {
2822
- cleanup();
2823
- resolve3("");
2824
- };
2825
- const cleanup = () => {
2826
- stdin.off("data", onData);
2827
- stdin.off("close", onClose);
2828
- setRawMode2(stdin, wasRaw);
2829
- if (wasPaused) stdin.pause();
2830
- };
2831
- stdin.on("data", onData);
2832
- stdin.on("close", onClose);
2833
- });
2834
- }
2835
- /**
2836
- * Read a line of input while masking each character with a bullet so the
2837
- * user gets visual confirmation that bytes are arriving (especially on
2838
- * paste, which previously felt like nothing happened). Pasted chunks
2839
- * are echoed as a run of bullets, Backspace/DEL erases one bullet, and
2840
- * Ctrl+U / Ctrl+T are honored. Non-TTY input is read normally — there's
2841
- * nothing to hide when piped, and echoing bullets to a file is noise.
2842
- *
2843
- * Returns the raw entered string (no trim — caller decides).
2844
- */
2845
- async readSecret(prompt) {
2846
- const stdin = process.stdin;
2847
- if (!stdin.isTTY) {
2848
- this.suppressNextHistory = true;
2849
- return this.readLine(prompt);
2850
- }
2851
- setOutputLineGuard(null);
2852
- this.rl?.close();
2853
- this.rl = void 0;
2854
- writeOut(prompt);
2855
- return new Promise((resolve3) => {
2856
- let buf = "";
2857
- const wasRaw = stdin.isRaw;
2858
- const cleanup = () => {
2859
- stdin.off("data", onData);
2860
- setRawMode2(stdin, wasRaw);
2861
- stdin.pause();
2862
- };
2863
- try {
2864
- setRawMode2(stdin, true);
2865
- stdin.resume();
2866
- stdin.setEncoding("utf8");
2867
- } catch (setupErr) {
2868
- setRawMode2(stdin, wasRaw);
2869
- stdin.pause();
2870
- throw setupErr;
2871
- }
2872
- const eraseChar = () => {
2873
- writeOut("\b \b");
2874
- };
2875
- const eraseAll = () => {
2876
- for (let i = 0; i < buf.length; i++) eraseChar();
2877
- };
2878
- const onData = (chunk) => {
2879
- for (const ch of chunk) {
2880
- if (ch === "\r" || ch === "\n") {
2881
- cleanup();
2882
- writeOut(` ${dim(`[${buf.length} chars]`)}
2883
- `);
2884
- resolve3(buf);
2885
- return;
2886
- }
2887
- if (ch === "") {
2888
- cleanup();
2889
- writeOut("\n");
2890
- resolve3("");
2891
- return;
2892
- }
2893
- if (ch === "") {
2894
- eraseAll();
2895
- buf = "";
2896
- continue;
2897
- }
2898
- if (ch === "") {
2899
- const m = buf.match(/(\S+\s*)$/);
2900
- const drop = m ? m[0].length : buf.length;
2901
- for (let i = 0; i < drop; i++) eraseChar();
2902
- buf = buf.slice(0, buf.length - drop);
2903
- continue;
2904
- }
2905
- if (ch === "\x7F" || ch === "\b") {
2906
- if (buf.length > 0) {
2907
- buf = buf.slice(0, -1);
2908
- eraseChar();
2909
- }
2910
- continue;
2911
- }
2912
- if (ch < " ") continue;
2913
- buf += ch;
2914
- writeOut("\u2022");
2915
- }
2916
- };
2917
- stdin.on("data", onData);
2918
- });
2919
- }
2920
- async close() {
2921
- setOutputLineGuard(null);
2922
- await this.saveHistory();
2923
- this.rl?.close();
2924
- this.rl = void 0;
2925
- }
2926
- };
2927
- function dim(s) {
2928
- if (!process.stdout.isTTY) return s;
2929
- return `\x1B[2m${s}\x1B[22m`;
2930
- }
2931
-
2932
- // src/launch-hints.ts
2933
- import * as fs6 from "node:fs/promises";
2934
- import * as path7 from "node:path";
2935
- import { color as color11 } from "@wrongstack/core/utils";
2936
- var GROUPS = [
2937
- {
2938
- title: "Autonomy",
2939
- items: [
2940
- { key: "/goal <text>", blurb: "lock in a verifiable mission \u2014 only Esc / Ctrl+C interrupt" },
2941
- {
2942
- key: "/autonomy eternal",
2943
- blurb: "sense \u2192 decide \u2192 execute \u2192 reflect loop until you stop it"
2944
- },
2945
- {
2946
- key: '--eternal "<mission>"',
2947
- blurb: "boot directly into the eternal-autonomy engine with a mission"
2948
- },
2949
- {
2950
- key: "/autonomy on|suggest",
2951
- blurb: "self-driving: auto-pick next step or just suggest it"
2952
- }
2953
- ]
2954
- },
2955
- {
2956
- title: "Agents & fleet",
2957
- items: [
2958
- {
2959
- key: "/delegate [--role=<role>] <task>",
2960
- blurb: "route work to a specialist; omit --role for automatic selection"
2961
- },
2962
- {
2963
- key: "/spawn [-p <provider>] [-m <model>] [-n <name>] <task>",
2964
- blurb: "launch one isolated subagent with optional runtime overrides"
2965
- },
2966
- {
2967
- key: "/agents [list|show <id>|stream on|off]",
2968
- blurb: "open the TUI monitor, inspect a subagent, or control its activity stream"
2969
- },
2970
- {
2971
- key: "/fleet status|usage|kill|log|manifest",
2972
- blurb: "inspect and control the active subagent fleet"
2973
- }
2974
- ]
2975
- },
2976
- {
2977
- title: "Steering",
2978
- items: [
2979
- {
2980
- key: "Esc (while busy)",
2981
- blurb: "soft interrupt \u2014 next message carries a STEERING preamble"
2982
- },
2983
- { key: "/steer <text>", blurb: "mid-flight redirect, works when Esc is eaten by tmux" },
2984
- { key: "/btw <note>", blurb: "non-aborting nudge \u2014 agent folds it in at its next step" },
2985
- {
2986
- key: "Ctrl+C \xD7 1 / \xD7 2 / \xD7 3",
2987
- blurb: "cancel iteration \xB7 force-exit Ink \xB7 hard exit(130)"
2988
- }
2989
- ]
2990
- },
2991
- {
2992
- title: "Modes & context",
2993
- items: [
2994
- {
2995
- key: "/mode",
2996
- blurb: "switch persona: code-reviewer, debugger, architect, tester, devops, \u2026"
2997
- },
2998
- { key: "/model", blurb: "two-step provider \u2192 model picker, hot-swap at runtime" },
2999
- { key: "/yolo on|off|toggle", blurb: "auto-approve tool calls without restart" },
3000
- {
3001
- key: "/context mode frugal|balanced|deep|archival",
3002
- blurb: "pick how aggressively history is trimmed"
3003
- },
3004
- { key: "/compact", blurb: "manually compact the in-flight context window" },
3005
- {
3006
- key: "/plan show|add|start|done",
3007
- blurb: "strategic roadmap, survives /resume across sessions"
3008
- }
3009
- ]
3010
- },
3011
- {
3012
- title: "Workspace tools",
3013
- items: [
3014
- {
3015
- key: "@<query>",
3016
- blurb: "search project files and insert a path into the prompt"
3017
- },
3018
- {
3019
- key: "Alt+V \xB7 /image",
3020
- blurb: "attach a clipboard image in the TUI"
3021
- },
3022
- {
3023
- key: "/mcp \xB7 wstack mcp add <name>",
3024
- blurb: "inspect or connect MCP servers"
3025
- },
3026
- {
3027
- key: "/plugin install|enable|disable <name>",
3028
- blurb: "manage optional integrations such as Telegram or LSP"
3029
- },
3030
- {
3031
- key: "/skill \xB7 /init \xB7 /commit",
3032
- blurb: "browse skills \xB7 scaffold project guidance \xB7 draft a commit"
3033
- },
3034
- {
3035
- key: "/diag \xB7 wstack usage \xB7 wstack resume <id>",
3036
- blurb: "inspect diagnostics, review costs, or continue an earlier session"
3037
- }
3038
- ]
3039
- }
3040
- ];
3041
- var HINT_COUNT = GROUPS.reduce((n, g) => n + g.items.length, 0);
3042
- var HINT_GROUP_COUNT = GROUPS.length;
3043
- var HINT_GROUP_TITLES = GROUPS.map((g) => g.title);
3044
- function shouldSuppress(flags) {
3045
- if (flags["no-hints"] === true) return true;
3046
- if (flags["hints"] === false) return true;
3047
- const env = process.env.WRONGSTACK_NO_HINTS;
3048
- if (env && env !== "0" && env.toLowerCase() !== "false") return true;
3049
- return false;
3050
- }
3051
- var DEFAULT_TERM_WIDTH = 80;
3052
- var MIN_TERM_WIDTH = 20;
3053
- var HINT_INDENT = " ";
3054
- var HINT_CONTINUATION_INDENT = " ";
3055
- var wrap = (n) => (n % GROUPS.length + GROUPS.length) % GROUPS.length;
3056
- function displayWidth(value) {
3057
- return [...value].length;
3058
- }
3059
- function wrapWords(value, firstWidth, continuationWidth = firstWidth) {
3060
- const words = value.trim().split(/\s+/).filter(Boolean);
3061
- const lines = [];
3062
- let line = "";
3063
- let width = Math.max(1, firstWidth);
3064
- for (const sourceWord of words) {
3065
- let word = sourceWord;
3066
- while (word) {
3067
- if (line) {
3068
- if (displayWidth(line) + 1 + displayWidth(word) <= width) {
3069
- line += ` ${word}`;
3070
- word = "";
3071
- } else {
3072
- lines.push(line);
3073
- line = "";
3074
- width = Math.max(1, continuationWidth);
3075
- }
3076
- continue;
3077
- }
3078
- if (displayWidth(word) <= width) {
3079
- line = word;
3080
- word = "";
3081
- } else {
3082
- const characters = [...word];
3083
- lines.push(characters.slice(0, width).join(""));
3084
- word = characters.slice(width).join("");
3085
- width = Math.max(1, continuationWidth);
3086
- }
3087
- }
3088
- }
3089
- if (line) lines.push(line);
3090
- return lines;
3091
- }
3092
- function renderHintLines(item, termWidth) {
3093
- const inline = `${HINT_INDENT}${item.key} \u2014 ${item.blurb}`;
3094
- if (displayWidth(inline) <= termWidth) {
3095
- return [`${HINT_INDENT}${color11.bold(item.key)} ${color11.dim("\u2014")} ${color11.dim(item.blurb)}`];
3096
- }
3097
- const lines = [];
3098
- const keyLines = wrapWords(
3099
- item.key,
3100
- termWidth - displayWidth(HINT_INDENT),
3101
- termWidth - displayWidth(HINT_CONTINUATION_INDENT)
3102
- );
3103
- keyLines.forEach((line, index) => {
3104
- const indent = index === 0 ? HINT_INDENT : HINT_CONTINUATION_INDENT;
3105
- lines.push(`${indent}${color11.bold(line)}`);
3106
- });
3107
- const blurbLines = wrapWords(
3108
- `\u2014 ${item.blurb}`,
3109
- termWidth - displayWidth(HINT_CONTINUATION_INDENT)
3110
- );
3111
- for (const line of blurbLines) lines.push(`${HINT_CONTINUATION_INDENT}${color11.dim(line)}`);
3112
- return lines;
3113
- }
3114
- function renderHeaderLines(group, groupIndex, termWidth) {
3115
- const progress = `${groupIndex + 1}/${GROUPS.length}`;
3116
- const plain = ` ${group.title} ${progress}`;
3117
- if (displayWidth(plain) <= termWidth) {
3118
- return [
3119
- ` ${color11.cyan("\u25C7")} ${color11.bold(group.title)} ${color11.dim(progress)}`,
3120
- ` ${color11.dim("\u2500".repeat(Math.min(termWidth - 2, 48)))}`
3121
- ];
3122
- }
3123
- return [
3124
- ` ${color11.cyan("\u25C7")} ${color11.bold(group.title)}`,
3125
- ` ${color11.dim("\u2500".repeat(Math.min(termWidth - 2, 48)))}`
3126
- ];
3127
- }
3128
- function renderFooterLines(termWidth) {
3129
- const footer = "/help lists everything \xB7 hide with --no-hints";
3130
- if (displayWidth(` ${footer}`) <= termWidth) {
3131
- return [
3132
- ` ${color11.dim("\u2500".repeat(Math.min(termWidth - 2, 48)))}`,
3133
- ` ${color11.dim(`${color11.bold("/help")} lists everything \xB7 hide with ${color11.bold("--no-hints")}`)}`
3134
- ];
3135
- }
3136
- return [...wrapWords(footer, termWidth - 2).map((line) => ` ${color11.dim(line)}`)];
3137
- }
3138
- async function pickGroupIndex(opts) {
3139
- if (typeof opts.groupIndex === "number") return wrap(opts.groupIndex);
3140
- if (opts.cursorFile) {
3141
- try {
3142
- let current = 0;
3143
- try {
3144
- const raw = await fs6.readFile(opts.cursorFile, "utf8");
3145
- const parsed = Number.parseInt(raw.trim(), 10);
3146
- if (Number.isFinite(parsed)) current = wrap(parsed);
3147
- } catch {
3148
- }
3149
- await fs6.mkdir(path7.dirname(opts.cursorFile), { recursive: true });
3150
- await fs6.writeFile(opts.cursorFile, String(wrap(current + 1)));
3151
- return current;
3152
- } catch {
3153
- }
3154
- }
3155
- return Math.floor(Math.random() * GROUPS.length);
3156
- }
3157
- async function printLaunchHints(renderer, flags, opts = {}) {
3158
- if (shouldSuppress(flags)) return;
3159
- const idx = await pickGroupIndex(opts);
3160
- const group = GROUPS[idx];
3161
- if (!group) return;
3162
- const termWidth = Math.max(
3163
- MIN_TERM_WIDTH,
3164
- Math.floor(opts.termWidth ?? process.stdout.columns ?? DEFAULT_TERM_WIDTH)
3165
- );
3166
- const lines = [];
3167
- lines.push("");
3168
- lines.push(...renderHeaderLines(group, idx, termWidth));
3169
- for (const item of group.items) lines.push(...renderHintLines(item, termWidth));
3170
- lines.push("");
3171
- lines.push(...renderFooterLines(termWidth));
3172
- lines.push("");
3173
- renderer.write(`${lines.join("\n")}
3174
- `);
3175
- }
3176
-
3177
- // src/renderer.ts
3178
- import { color as color12 } from "@wrongstack/core/utils";
3179
- var TerminalRenderer = class _TerminalRenderer {
3180
- out;
3181
- err;
3182
- lineStart = true;
3183
- /** When true, stdout-bound rendering is disabled (for non-terminal UI surfaces). */
3184
- silent = false;
3185
- /**
3186
- * When true, Ink exclusively owns the terminal. Both stdout and stderr
3187
- * rendering must be disabled: either stream can interleave with Ink's
3188
- * cursor updates and make the TUI flicker or leave stray debug notes.
3189
- */
3190
- tuiActive = false;
3191
- /**
3192
- * Per-tool on-screen result render mode. Defaults to `'extend'` so
3193
- * existing callers without `setResultRenderMode` see full content.
3194
- * `setResultRenderMode(name, mode)` overrides the entry for `name`; the
3195
- * override is one-shot for that name (the default reasserts after the
3196
- * next write) so callers can set the mode right before each tool call
3197
- * without leaking state across unrelated tool calls.
3198
- */
3199
- renderModes = /* @__PURE__ */ new Map();
3200
- /** Mode applied when no entry is set for a given tool name. */
3201
- static DEFAULT_RENDER_MODE = "extend";
3202
- /** Maximum number of one-shot hints retained when callers abandon a result. */
3203
- static MAX_RENDER_MODES = 256;
3204
- /** Lines kept per tool family in `extend` mode. */
3205
- static EXTEND_PREVIEW_LINES = 10;
3206
- constructor(opts = {}) {
3207
- this.out = opts.out ?? process.stdout;
3208
- this.err = opts.err ?? process.stderr;
3209
- }
3210
- /**
3211
- * Toggle stdout suppression for non-terminal UI surfaces such as WebUI.
3212
- * Diagnostics continue to flow to stderr. Idempotent.
3213
- */
3214
- setSilent(silent) {
3215
- this.silent = silent;
3216
- }
3217
- isSilent() {
3218
- return this.silent;
3219
- }
3220
- /**
3221
- * Toggle exclusive TUI ownership of both terminal streams. Unlike
3222
- * {@link setSilent}, TUI mode suppresses stderr as well as stdout and clears
3223
- * any CLI-only result mode staged before Ink took ownership. While active,
3224
- * new result-mode hints are ignored. The two flags compose with OR semantics
3225
- * for stdout suppression only; TUI ownership adds the other behaviors.
3226
- * Enable immediately before starting Ink and disable in the paired
3227
- * `finally`, only after the TUI has fully exited.
3228
- */
3229
- setTuiActive(active) {
3230
- if (active) this.renderModes.clear();
3231
- this.tuiActive = active;
3232
- }
3233
- isTuiActive() {
3234
- return this.tuiActive;
3235
- }
3236
- suppressStdout() {
3237
- return this.silent || this.tuiActive;
3238
- }
3239
- setResultRenderMode(name, mode) {
3240
- if (this.suppressStdout()) return;
3241
- if (this.renderModes.delete(name)) {
3242
- this.renderModes.set(name, mode);
3243
- return;
3244
- }
3245
- if (this.renderModes.size >= _TerminalRenderer.MAX_RENDER_MODES) {
3246
- const oldestName = this.renderModes.keys().next().value;
3247
- if (oldestName !== void 0) this.renderModes.delete(oldestName);
3248
- }
3249
- this.renderModes.set(name, mode);
3250
- }
3251
- getRenderMode(name) {
3252
- return this.renderModes.get(name) ?? _TerminalRenderer.DEFAULT_RENDER_MODE;
3253
- }
3254
- clearRenderMode(name) {
3255
- this.renderModes.delete(name);
3256
- }
3257
- write(input) {
3258
- if (this.suppressStdout()) return;
3259
- const text = typeof input === "string" ? input : input.text;
3260
- if (!text) return;
3261
- const rendered = renderMarkdown(text);
3262
- this.out.write(rendered);
3263
- this.lineStart = rendered.endsWith("\n");
3264
- }
3265
- writeLine(text = "") {
3266
- if (this.suppressStdout()) return;
3267
- if (!this.lineStart) this.out.write("\n");
3268
- if (text) this.out.write(`${text}
3269
- `);
3270
- else this.out.write("\n");
3271
- this.lineStart = true;
3272
- }
3273
- writeBlock(block) {
3274
- if (block.type === "tool_result") {
3275
- const text = typeof block.content === "string" ? block.content : JSON.stringify(block.content);
3276
- this.writeToolResult(block.name ?? "result", text, !!block.is_error);
3277
- return;
3278
- }
3279
- if (this.suppressStdout()) return;
3280
- if (block.type === "text") {
3281
- this.write(block);
3282
- } else if (block.type === "tool_use") {
3283
- this.writeToolCall(block.name, block.input);
3284
- }
3285
- }
3286
- writeToolCall(name, input) {
3287
- if (this.suppressStdout()) return;
3288
- if (!this.lineStart) this.out.write("\n");
3289
- const arrow = theme.primary("\u2192");
3290
- const display = formatInputSummary(input);
3291
- this.out.write(`${arrow} ${theme.bold(name)}${display ? color12.dim(` ${display}`) : ""}
3292
- `);
3293
- this.lineStart = true;
3294
- }
3295
- writeToolResult(name, content, isError) {
3296
- if (this.suppressStdout()) {
3297
- this.clearRenderMode(name);
3298
- return;
3299
- }
3300
- const txt = typeof content === "string" ? content : safeStringify(content);
3301
- const prefix = isError ? theme.error("\u2718") : theme.success("\u2713");
3302
- const renderMode = this.getRenderMode(name);
3303
- this.clearRenderMode(name);
3304
- const bodyText = extractBodyText(content, name);
3305
- if (isError) {
3306
- const firstLine = txt.split("\n")[0] ?? "";
3307
- const truncated = firstLine.length > 200 ? `${firstLine.slice(0, 197)}\u2026` : firstLine;
3308
- this.out.write(` ${prefix} ${color12.dim(truncated)}
3309
- `);
3310
- this.lineStart = true;
3311
- return;
3312
- }
3313
- const isEditLike = name === "edit" || name === "write";
3314
- const diff = extractDiff(content);
3315
- if (isEditLike && diff) {
3316
- this.out.write(` ${prefix} ${color12.dim(summarize(content, name))}
3317
- `);
3318
- const rendered = renderDiff(diff).split("\n").map((l) => ` ${l}`).join("\n");
3319
- this.out.write(`${rendered}
3320
- `);
3321
- this.lineStart = true;
3322
- return;
3323
- }
3324
- const summary = summarize(content, name);
3325
- const isReadLike = name === "read" || name === "grep" || name === "glob" || name === "bash" || name === "exec";
3326
- if (renderMode === "simple") {
3327
- this.out.write(` ${prefix} ${color12.dim(summary || compactSingleLine(bodyText))}
3328
- `);
3329
- this.lineStart = true;
3330
- return;
3331
- }
3332
- const previewLines = isEditLike ? 0 : isReadLike ? _TerminalRenderer.EXTEND_PREVIEW_LINES : 2;
3333
- const lines = bodyText.split("\n");
3334
- const head = lines.slice(0, previewLines).map((l) => l.replace(/\s+$/, ""));
3335
- const moreCount = Math.max(0, lines.length - head.length);
3336
- this.out.write(` ${prefix} ${color12.dim(summary)}
3337
- `);
3338
- for (const l of head) {
3339
- const capped = l.length > 200 ? `${l.slice(0, 197)}\u2026` : l;
3340
- this.out.write(` ${color12.dim(capped)}
3341
- `);
3342
- }
3343
- if (moreCount > 0) {
3344
- this.out.write(
3345
- ` ${color12.dim(`+${moreCount} more line${moreCount === 1 ? "" : "s"}`)}
3346
- `
3347
- );
3348
- }
3349
- this.lineStart = true;
3350
- }
3351
- writeDiff(diff) {
3352
- if (this.suppressStdout()) return;
3353
- if (!this.lineStart) this.out.write("\n");
3354
- this.out.write(`${renderDiff(diff)}
3355
- `);
3356
- this.lineStart = true;
3357
- }
3358
- writeWarning(text) {
3359
- if (this.tuiActive) return;
3360
- this.err.write(`${theme.warn("\u26A0")} ${text}
3361
- `);
3362
- }
3363
- writeError(text) {
3364
- if (this.tuiActive) return;
3365
- this.err.write(`${theme.error("\u2718")} ${text}
3366
- `);
3367
- }
3368
- writeInfo(text) {
3369
- if (this.tuiActive) return;
3370
- this.err.write(`${theme.info("\u2139")} ${text}
3371
- `);
3372
- }
3373
- /**
3374
- * Write text to stderr verbatim: no glyph prefix, no markdown processing.
3375
- *
3376
- * Deliberately stricter than writeWarning/writeError/writeInfo, which emit
3377
- * *diagnostics* and are suppressed only when a TUI owns the terminal (so
3378
- * they survive silent mode). This method is for informational mirrors of
3379
- * stdout output, where the stdout copy already carries the full content —
3380
- * in silent mode the operator asked for machine-only output, so the mirror
3381
- * is dropped rather than leaked onto a quieter stream. It therefore honors
3382
- * the full {@link suppressStdout} gate.
3383
- *
3384
- * Used by `wstack mailbox serve` to route the human-readable startup banner
3385
- * away from stdout, keeping the mailbox_serve_started JSON line cleanly
3386
- * pipeable (`mailbox serve | jq`).
3387
- */
3388
- writeStderr(text) {
3389
- if (this.suppressStdout()) return;
3390
- this.err.write(text);
3391
- }
3392
- clear() {
3393
- if (this.suppressStdout()) return;
3394
- this.out.write("\x1B[2J\x1B[H");
3395
- this.lineStart = true;
3396
- }
3397
- /**
3398
- * Write a flashy agent completion banner for delegate tool results.
3399
- * Renders a box like:
3400
- * ┌─────────────────────────────────────┐
3401
- * │ ✓ [role] done in 4m 32s │
3402
- * │ 127 iterations · 341 tools │
3403
- * │ Found 14 bugs across 6 files... │
3404
- * └─────────────────────────────────────┘
3405
- */
3406
- writeAgentSummary(summary, ok) {
3407
- if (this.suppressStdout()) return;
3408
- if (!this.lineStart) this.out.write("\n");
3409
- const lines = summary.split("\n");
3410
- const icon = ok ? theme.success("\u2713") : theme.error("\u2718");
3411
- const firstLine = `${icon} ${lines[0] ?? summary}`;
3412
- const body = lines.slice(1);
3413
- const maxWidth = Math.min(process.stdout.columns ?? 80, 120);
3414
- const contentWidth = Math.max(
3415
- firstLine.length,
3416
- body.reduce((a, l) => Math.max(a, l.length), 0)
3417
- );
3418
- const boxWidth = Math.min(Math.max(contentWidth + 4, 44), maxWidth);
3419
- const thick = "\u2501".repeat(boxWidth - 2);
3420
- const thin = "\u2500".repeat(boxWidth - 2);
3421
- this.out.write(`
3422
- ${theme.primary("\u250C")}${thick}${theme.primary("\u2510")}
3423
- `);
3424
- const centre = (s) => {
3425
- const inner = ` ${s} `;
3426
- const padLen = Math.max(0, boxWidth - 2 - s.length);
3427
- const left = Math.floor(padLen / 2);
3428
- const right = padLen - left;
3429
- return `${" ".repeat(left)}${inner}${" ".repeat(right)}`;
3430
- };
3431
- this.out.write(` ${theme.primary("\u2502")}${centre(firstLine)}${theme.primary("\u2502")}
3432
- `);
3433
- for (const l of body) {
3434
- this.out.write(
3435
- ` ${theme.primary("\u2502")} ${l}${" ".repeat(Math.max(0, boxWidth - 3 - l.length))}${theme.primary("\u2502")}
3436
- `
3437
- );
3438
- }
3439
- this.out.write(` ${theme.primary("\u2514")}${thin}${theme.primary("\u2518")}
3440
- `);
3441
- this.lineStart = true;
3442
- }
3443
- /**
3444
- * Render subagent completion banners from a RunResult.
3445
- * Uses `delegateSummaries` when available (populated by delegate tool),
3446
- * otherwise falls back to scanning message history.
3447
- */
3448
- writeDelegateSummaries(result) {
3449
- if (this.suppressStdout()) return;
3450
- if (result.delegateSummaries) {
3451
- for (const { summary, ok } of result.delegateSummaries) {
3452
- if (!summary) continue;
3453
- this.writeAgentSummary(summary, ok);
3454
- }
3455
- return;
3456
- }
3457
- if (!result.messages) return;
3458
- for (const msg of result.messages) {
3459
- const m = msg;
3460
- if (!Array.isArray(m.content)) continue;
3461
- for (const block of m.content) {
3462
- const b = block;
3463
- if (b.type !== "tool_result" || b.name !== "delegate") continue;
3464
- let obj;
3465
- try {
3466
- obj = typeof b.content === "string" ? JSON.parse(b.content) : b.content;
3467
- } catch {
3468
- continue;
3469
- }
3470
- const o = obj;
3471
- if (o.summary) {
3472
- this.writeAgentSummary(o.summary, o.ok ?? true);
3473
- }
3474
- }
3475
- }
3476
- }
3477
- };
3478
- function renderMarkdown(s) {
3479
- let out = s;
3480
- out = out.replace(/^(#{1,6}) (.+)$/gm, (_m, hashes, text) => {
3481
- return theme.primary(theme.bold(`${hashes} ${text}`));
3482
- });
3483
- out = out.replace(/```([a-zA-Z0-9_+-]*)\n([\s\S]*?)```/g, (_m, _lang, code) => {
3484
- return color12.gray(`
3485
- \u250C\u2500
3486
- ${code.replace(/^/gm, "\u2502 ")}\u2514\u2500`);
3487
- });
3488
- out = out.replace(/`([^`\n]+)`/g, (_m, code) => theme.accent(code));
3489
- out = out.replace(/\*\*([^*]+)\*\*/g, (_m, text) => theme.bold(text));
3490
- out = out.replace(
3491
- /(^|[^*])\*([^*\n]+)\*([^*]|$)/g,
3492
- (_m, l, t, r) => `${l}${color12.italic(t)}${r}`
3493
- );
3494
- return out;
3495
- }
3496
- function formatInputSummary(input) {
3497
- if (!input || typeof input !== "object") return "";
3498
- const obj = input;
3499
- if (typeof obj["path"] === "string") return obj["path"];
3500
- if (typeof obj["url"] === "string") return obj["url"];
3501
- if (typeof obj["command"] === "string") {
3502
- const cmd = obj["command"];
3503
- return cmd.length > 60 ? cmd.slice(0, 57) + "..." : cmd;
3504
- }
3505
- if (typeof obj["pattern"] === "string") return obj["pattern"];
3506
- return "";
3507
- }
3508
- function safeStringify(value) {
3509
- if (typeof value === "string") return value;
3510
- try {
3511
- return JSON.stringify(value);
3512
- } catch {
3513
- return String(value);
3514
- }
3515
- }
3516
- function extractBodyText(value, name) {
3517
- let v = value;
3518
- if (typeof value === "string") {
3519
- const trimmed = value.trimStart();
3520
- if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
3521
- try {
3522
- v = JSON.parse(value);
3523
- } catch {
3524
- return value;
3525
- }
3526
- } else {
3527
- return value;
3528
- }
3529
- }
3530
- if (typeof v !== "object" || v === null) {
3531
- return safeStringify(value);
3532
- }
3533
- const o = v;
3534
- if (name === "read" && typeof o["text"] === "string") return o["text"];
3535
- if (name === "bash") {
3536
- const output = typeof o["output"] === "string" ? o["output"] : "";
3537
- const error = typeof o["error"] === "string" ? o["error"] : "";
3538
- if (output && error) return `${output}
3539
- ${error}`;
3540
- return output || error || "";
3541
- }
3542
- if (name === "exec") {
3543
- const stdout = typeof o["stdout"] === "string" ? o["stdout"] : "";
3544
- const stderr = typeof o["stderr"] === "string" ? o["stderr"] : "";
3545
- if (stdout && stderr) return `${stdout}
3546
- ${stderr}`;
3547
- return stdout || stderr || "";
3548
- }
3549
- return safeStringify(value);
3550
- }
3551
- function extractDiff(value) {
3552
- if (typeof value === "object" && value !== null) {
3553
- const d = value.diff;
3554
- if (typeof d === "string" && d.length > 0) return d;
3555
- }
3556
- if (typeof value === "string") {
3557
- const trimmed = value.trimStart();
3558
- if (trimmed.startsWith("{")) {
3559
- try {
3560
- const parsed = JSON.parse(value);
3561
- if (typeof parsed.diff === "string" && parsed.diff.length > 0) {
3562
- return parsed.diff;
3563
- }
3564
- } catch {
3565
- }
3566
- }
3567
- if (/^---[^\n]*\n\+\+\+/m.test(value)) return value;
3568
- }
3569
- return null;
3570
- }
3571
- function summarize(value, name) {
3572
- let v = value;
3573
- if (typeof value === "string") {
3574
- const trimmed = value.trimStart();
3575
- if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
3576
- try {
3577
- v = JSON.parse(value);
3578
- } catch {
3579
- }
3580
- }
3581
- }
3582
- if (typeof v === "object" && v !== null) {
3583
- const o = v;
3584
- if (name === "edit") {
3585
- const path9 = typeof o["path"] === "string" ? o["path"] : "";
3586
- const reps = typeof o["replacements"] === "number" ? o["replacements"] : 0;
3587
- return `${path9} ${reps} replacement${reps === 1 ? "" : "s"}`.trim();
3588
- }
3589
- if (name === "write") {
3590
- const path9 = typeof o["path"] === "string" ? o["path"] : "";
3591
- const bytes = typeof o["bytes"] === "number" ? o["bytes"] : void 0;
3592
- return bytes !== void 0 ? `${path9} ${bytes}B` : path9;
3593
- }
3594
- if (name === "read") {
3595
- return summarizeRead(o);
3596
- }
3597
- if (name === "bash") {
3598
- return summarizeBash(o);
3599
- }
3600
- if (name === "exec") {
3601
- return summarizeExec(o);
3602
- }
3603
- if (typeof o["count"] === "number") {
3604
- return `${o["count"]} match${o["count"] === 1 ? "" : "es"}`;
3605
- }
3606
- }
3607
- return "";
3608
- }
3609
- function summarizeRead(o) {
3610
- const path9 = typeof o["path"] === "string" ? o["path"] : "";
3611
- const total = typeof o["total_lines"] === "number" ? o["total_lines"] : void 0;
3612
- const truncated = o["truncated"] === true;
3613
- const cached = o["cached"] === true;
3614
- const note = typeof o["note"] === "string" ? o["note"] : void 0;
3615
- const parts = [];
3616
- if (path9) parts.push(path9);
3617
- if (total !== void 0) parts.push(`${total} line${total === 1 ? "" : "s"}`);
3618
- const flags = [];
3619
- if (truncated) flags.push("truncated");
3620
- if (cached) flags.push("cached");
3621
- if (flags.length > 0) parts.push(`(${flags.join(", ")})`);
3622
- let summary = parts.join(" ").trim();
3623
- if (note) summary = summary ? `${summary} \u2014 ${note}` : note;
3624
- return summary;
3625
- }
3626
- function summarizeBash(o) {
3627
- const exit = typeof o["exit_code"] === "number" ? o["exit_code"] : void 0;
3628
- const output = typeof o["output"] === "string" ? o["output"] : "";
3629
- const lines = output ? output.split("\n").length : 0;
3630
- const parts = [];
3631
- if (exit !== void 0) parts.push(`exit=${exit}`);
3632
- else if (o["exit_code"] === null) parts.push("backgrounded");
3633
- if (o["timed_out"] === true) parts.push("timed out");
3634
- if (lines > 0) parts.push(`${lines} output line${lines === 1 ? "" : "s"}`);
3635
- if (output.length > 0) parts.push(`${output.length}B`);
3636
- return parts.join(" ");
3637
- }
3638
- function summarizeExec(o) {
3639
- const command = typeof o["command"] === "string" ? o["command"] : "";
3640
- const args = Array.isArray(o["args"]) ? o["args"].filter((a) => typeof a === "string") : [];
3641
- const exit = typeof o["exitCode"] === "number" ? o["exitCode"] : void 0;
3642
- const stdout = typeof o["stdout"] === "string" ? o["stdout"] : "";
3643
- const stderr = typeof o["stderr"] === "string" ? o["stderr"] : "";
3644
- const outLines = stdout ? stdout.split("\n").length : 0;
3645
- const errLines = stderr ? stderr.split("\n").length : 0;
3646
- const totalBytes = stdout.length + stderr.length;
3647
- const parts = [];
3648
- const invocation = [command, ...args].filter(Boolean).join(" ");
3649
- if (invocation) parts.push(`$ ${invocation}`);
3650
- if (exit !== void 0) parts.push(`exit=${exit}`);
3651
- if (outLines > 0) parts.push(`${outLines} stdout line${outLines === 1 ? "" : "s"}`);
3652
- if (errLines > 0) parts.push(`${errLines} stderr line${errLines === 1 ? "" : "s"}`);
3653
- if (totalBytes > 0) parts.push(`${totalBytes}B`);
3654
- return parts.join(" ");
3655
- }
3656
- function compactSingleLine(text) {
3657
- const first = text.split("\n")[0]?.trim() ?? "";
3658
- if (!first) return "(no output)";
3659
- return first.length > 200 ? `${first.slice(0, 197)}\u2026` : first;
3660
- }
3661
-
3662
- // src/boot.ts
3663
- var GITHUB_PROVIDERS_OVERLAY_URL = "https://raw.githubusercontent.com/WrongStack/WrongStack/main/packages/cli/data/providers.json";
3664
- function resolveBundledOverlayFile() {
3665
- try {
3666
- return fileURLToPath(new URL("../data/providers.json", import.meta.url));
3667
- } catch {
3668
- return void 0;
3669
- }
3670
- }
3671
- async function validateSavedProviderModel(config, modelsRegistry) {
3672
- const providerId = config.provider;
3673
- const modelId = config.model;
3674
- if (!providerId || !modelId) return { ok: false, reason: "missing provider/model" };
3675
- const saved = config.providers?.[providerId];
3676
- const lookupId = saved?.type && saved.type !== providerId ? saved.type : providerId;
3677
- const catalogProvider = await modelsRegistry.getProvider(lookupId).catch(() => void 0);
3678
- if (!catalogProvider && !saved?.family)
3679
- return { ok: false, reason: `provider "${providerId}" is no longer available` };
3680
- const hasCredential = (catalogProvider?.envVars ?? saved?.envVars ?? []).some(
3681
- (envVar) => Boolean(process.env[envVar])
3682
- ) || saved !== void 0 && resolveActiveApiKey(saved) !== void 0 || isKeylessLocalProvider({
3683
- apiBase: saved?.baseUrl ?? catalogProvider?.apiBase,
3684
- envVars: saved?.envVars ?? catalogProvider?.envVars
3685
- });
3686
- if (!hasCredential) return { ok: false, reason: `provider "${providerId}" has no usable key` };
3687
- const visible = visibleModelIds(
3688
- providerId,
3689
- config,
3690
- (catalogProvider?.models ?? []).map((m) => m.id),
3691
- saved
3692
- );
3693
- if (visible.length > 0 && !visible.includes(modelId)) {
3694
- return {
3695
- ok: false,
3696
- reason: `model "${modelId}" is no longer available for provider "${providerId}"`
3697
- };
3698
- }
3699
- return { ok: true };
3700
- }
3701
- async function autoSelectSavedProvider(config, modelsRegistry) {
3702
- const providers = config.providers ?? {};
3703
- const entries = config.provider && providers[config.provider] ? [[config.provider, providers[config.provider]]] : Object.entries(providers);
3704
- for (const [id, cfg] of entries) {
3705
- if (!cfg) continue;
3706
- let model = cfg.models?.[0];
3707
- if (!model) {
3708
- const catalogId = cfg.type && cfg.type !== id ? cfg.type : id;
3709
- const catalog = await modelsRegistry.getProvider(catalogId).catch(() => void 0);
3710
- model = catalog?.models?.[0]?.id;
3711
- }
3712
- if (model) return { provider: id, model };
3713
- }
3714
- return void 0;
3715
- }
3716
- function resolveBundledSkillsDir2() {
3717
- try {
3718
- const req = createRequire4(import.meta.url);
3719
- const corePkg = req.resolve("@wrongstack/core/package.json");
3720
- return path8.join(path8.dirname(corePkg), "skills");
3721
- } catch {
3722
- return void 0;
3723
- }
3724
- }
3725
- function resolveBundledPromptsDir2() {
3726
- try {
3727
- const req = createRequire4(import.meta.url);
3728
- const corePkg = req.resolve("@wrongstack/core/package.json");
3729
- return path8.join(path8.dirname(corePkg), "data", "prompts");
3730
- } catch {
3731
- return void 0;
3732
- }
3733
- }
3734
- function isHomeDirectory(cwd, userHome) {
3735
- if (!cwd || !userHome) return false;
3736
- return path8.resolve(cwd) === path8.resolve(userHome);
3737
- }
3738
- function shouldPrintYoloNotice(lastChoices, yoloPinned, yolo) {
3739
- return !lastChoices && yoloPinned === void 0 && yolo;
3740
- }
3741
- async function boot(argv) {
3742
- const { flags, positional } = parseArgs(argv);
3743
- if (positional[0] === "update") {
3744
- const renderer2 = new TerminalRenderer();
3745
- return runUpdateCommand(positional.slice(1), {
3746
- cwd: process.cwd(),
3747
- userHome: os.homedir(),
3748
- renderer: renderer2,
3749
- flags
3750
- });
3751
- }
3752
- if (positional[0] === "resume" && positional[1] && !subcommands["__noop_resume_marker"]) {
3753
- flags["resume"] = positional[1];
3754
- positional.splice(0, 2);
3755
- }
3756
- let bootResult;
3757
- try {
3758
- bootResult = await bootConfig(flags);
3759
- } catch (err) {
3760
- writeErr(`Config error: ${toErrorMessage3(err)}
3761
- `);
3762
- return 2;
3763
- }
3764
- let { paths, config, vault } = bootResult;
3765
- let first = positional[0];
3766
- const mayOfferConfigRecovery = isStdinTTY2() && (positional.length === 0 || first === "quick") && (!config.activeProfile || config.activeProfile === "default") && typeof flags["prompt"] !== "string" && !flags["webui"] && !flags["no-interactive"] && !flags["skip"];
3767
- const renderer = new TerminalRenderer();
3768
- const reader = new ReadlineInputReader({ historyFile: paths.wpaths.historyFile });
3769
- if (mayOfferConfigRecovery) {
3770
- const restored = await maybeRestoreDefaultProfileFromBackup({
3771
- globalRoot: paths.wpaths.globalRoot,
3772
- profilePath: paths.wpaths.profileConfig("default"),
3773
- renderer,
3774
- reader
3775
- });
3776
- if (restored) {
3777
- try {
3778
- bootResult = await bootConfig(flags);
3779
- ({ paths, config, vault } = bootResult);
3780
- } catch (err) {
3781
- writeErr(`Config error after backup restore: ${toErrorMessage3(err)}
3782
- `);
3783
- await reader.close();
3784
- return 2;
3785
- }
3786
- }
3787
- }
3788
- config = applySimpleUiFullAutoProfile(config, flags);
3789
- const simpleUiFullAuto = isSimpleUiFullAuto(flags);
3790
- const { cwd, projectRoot, userHome, wpaths, pathResolver } = paths;
3791
- const profileConfigPath = activeProfileConfigPath(wpaths, config);
3792
- void pathResolver;
3793
- if (first === "quick") {
3794
- flags["quick"] = true;
3795
- flags["tui"] = true;
3796
- positional.splice(0, 1);
3797
- first = void 0;
3798
- const plugins = config?.plugins ?? [];
3799
- if (plugins.length === 0) {
3800
- console.debug("[wrongstack:quick] No plugins configured");
3801
- } else {
3802
- for (const p of plugins) {
3803
- const name = typeof p === "string" ? p : p.name;
3804
- const enabled = typeof p === "object" && p.enabled === false ? " (disabled)" : "";
3805
- console.debug(`[wrongstack:quick] plugin: ${name}${enabled}`);
3806
- }
3807
- }
3808
- }
3809
- const logger = new DefaultLogger({
3810
- level: config.log.level,
3811
- file: wpaths.logFile,
3812
- // Suppress stderr output in TUI mode: plugin/library log messages
3813
- // (e.g. Telegram "getUpdates failed") write directly to stderr and
3814
- // bypass Ink, which breaks the Static/live boundary.
3815
- // Logs still go to the disk file for post-hoc debugging.
3816
- stderr: !flags.tui
3817
- });
3818
- const modelsRegistry = new DefaultModelsRegistry({
3819
- cacheFile: wpaths.modelsCache,
3820
- // Force a refresh attempt once per CLI process. Model metadata changes faster
3821
- // than releases (new model ids, corrected context windows), and stale cache
3822
- // here directly affects runtime behavior like context bars and compaction.
3823
- // If the network is unavailable, DefaultModelsRegistry still falls back to
3824
- // stale cache or the bundled overlay instead of failing startup.
3825
- ttlSeconds: 0,
3826
- // Curated overlay merged on top of models.dev: fetched from GitHub raw for
3827
- // freshness, with the bundled file as the offline floor.
3828
- overlayUrl: GITHUB_PROVIDERS_OVERLAY_URL,
3829
- overlayFile: resolveBundledOverlayFile(),
3830
- overlayCacheFile: wpaths.modelsOverlayCache
3831
- });
3832
- const subcommandHandler = first ? subcommands[first] : void 0;
3833
- if (first && subcommandHandler) {
3834
- if (flags["help"] === true || flags["h"] === true) {
3835
- const deepSub = positional[1];
3836
- if (deepSub && renderDeepHelp(`${first}:${deepSub}`, renderer)) {
3837
- await reader.close();
3838
- return 0;
3839
- }
3840
- if (renderFocusedHelp(first, renderer)) {
3841
- await reader.close();
3842
- return 0;
3843
- }
3844
- }
3845
- const container = createDefaultContainer2({
3846
- config,
3847
- wpaths,
3848
- logger,
3849
- modelsRegistry,
3850
- bundledSkillsDir: config.features.skills ? resolveBundledSkillsDir2() : void 0,
3851
- bundledPromptsDir: config.features.prompts === false ? void 0 : resolveBundledPromptsDir2()
3852
- });
3853
- const sessionStore = container.resolve(TOKENS2.SessionStore);
3854
- const skillLoader = container.resolve(TOKENS2.SkillLoader);
3855
- const toolRegistryForSubcmd = new ToolRegistry();
3856
- registerBuiltinToolTier({
3857
- registry: toolRegistryForSubcmd,
3858
- tier: normalizeTokenSavingTier(config.features.tokenSavingMode)
3859
- });
3860
- const code = await subcommandHandler(positional.slice(1), {
3861
- config,
3862
- renderer,
3863
- reader,
3864
- sessionStore,
3865
- skillLoader,
3866
- toolRegistry: toolRegistryForSubcmd,
3867
- modelsRegistry,
3868
- paths: wpaths,
3869
- vault,
3870
- cwd,
3871
- projectRoot,
3872
- userHome,
3873
- flags
3874
- });
3875
- await reader.close();
3876
- return code;
3877
- }
3878
- if (isHomeDirectory(cwd, userHome)) {
3879
- writeErr(
3880
- `
3881
- ${color13.red(color13.bold("\u26A0 This is not a working directory."))}
3882
- ${color13.red("Please open wstack in a project folder.")}
3883
-
3884
- `
3885
- );
3886
- await reader.close();
3887
- return 1;
3888
- }
3889
- if (!flags["no-models-refresh"]) {
3890
- try {
3891
- await modelsRegistry.refresh();
3892
- logger.info("models.dev catalog refreshed");
3893
- } catch (err) {
3894
- const msg = toErrorMessage3(err);
3895
- logger.warn(`models.dev refresh failed (${msg}); using cached catalog`);
3896
- }
3897
- }
3898
- try {
3899
- await discoverAndMergeProviders({
3900
- config,
3901
- registry: modelsRegistry,
3902
- cacheDir: path8.dirname(wpaths.modelsCache),
3903
- logger
3904
- });
3905
- } catch (err) {
3906
- logger.debug(`provider auto-discovery skipped: ${toErrorMessage3(err)}`);
3907
- }
3908
- const isSingleShot = positional.length > 0 || typeof flags["prompt"] === "string";
3909
- const isInteractiveTTY = isStdinTTY2() && !isSingleShot && !flags["webui"] && !flags["no-interactive"] && !flags["skip"];
3910
- if (isInteractiveTTY) {
3911
- await checkGitInCwd({ cwd, renderer, reader });
3912
- const cont = await runProjectCheck({ projectRoot, cwd, renderer, reader });
3913
- if (!cont) {
3914
- await reader.close();
3915
- return 0;
3916
- }
3917
- }
3918
- const wantsTui = flags["tui"] === true && flags["no-tui"] !== true;
3919
- if (wantsTui && (!process.stdout.isTTY || !process.stdin.isTTY)) {
3920
- writeErr(
3921
- "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"
3922
- );
3923
- await reader.close();
3924
- return 2;
3925
- }
3926
- const providerFlag = typeof flags["provider"] === "string" ? flags["provider"] : void 0;
3927
- const modelFlag = typeof flags["model"] === "string" ? flags["model"] : void 0;
3928
- const noInteractiveMode = flags["webui"] || flags["no-interactive"];
3929
- if (noInteractiveMode && (!config.provider || !config.model)) {
3930
- if (providerFlag && modelFlag) {
3931
- config = patchConfig(config, { provider: providerFlag, model: modelFlag });
3932
- } else {
3933
- const picked = await autoSelectSavedProvider(config, modelsRegistry);
3934
- if (picked) config = patchConfig(config, picked);
3935
- }
3936
- }
3937
- if (!(!!providerFlag && !!modelFlag)) {
3938
- if (isStdinTTY2() && !noInteractiveMode) {
3939
- let picked;
3940
- let skipPicker = false;
3941
- const savedProvider = config.provider;
3942
- const savedModel = config.model;
3943
- if (savedProvider && savedModel) {
3944
- const savedStatus = await validateSavedProviderModel(config, modelsRegistry);
3945
- renderer.write(
3946
- `
3947
- ${color13.dim("Last settings:")} ${color13.bold(savedProvider)} / ${color13.bold(savedModel)}
3948
- `
3949
- );
3950
- if (!savedStatus.ok) {
3951
- renderer.writeWarning(
3952
- `Saved provider/model is no longer usable (${savedStatus.reason ?? "unknown reason"}); choose a provider.
3953
- `
3954
- );
3955
- } else {
3956
- const answer = (await reader.readLine(
3957
- ` ${color13.amber("?")} Continue with these? ${color13.dim("[Y/n/q]")} ${color13.dim("(auto Y in 5s)")} `,
3958
- { timeoutMs: 5e3, defaultAnswer: "y" }
3959
- )).trim().toLowerCase();
3960
- if (answer === "q") {
3961
- renderer.write(color13.dim(" Goodbye!\n"));
3962
- await reader.close();
3963
- return 0;
3964
- }
3965
- if (answer !== "n" && answer !== "no") {
3966
- skipPicker = true;
3967
- renderer.write(
3968
- `
3969
- ${color13.green("\u25B6")} ${color13.bold(savedProvider)} / ${color13.bold(savedModel)}
3970
-
3971
- `
3972
- );
3973
- }
3974
- }
3975
- }
3976
- if (!skipPicker) {
3977
- picked = await runPicker({
3978
- modelsRegistry,
3979
- renderer,
3980
- reader,
3981
- config,
3982
- defaultProvider: providerFlag ?? config.provider,
3983
- defaultModel: modelFlag ?? config.model
3984
- });
3985
- }
3986
- if (!picked && !skipPicker) {
3987
- if (!config.provider || !config.model) {
3988
- await reader.close();
3989
- return 2;
3990
- }
3991
- }
3992
- if (picked) {
3993
- const prevProvider = config.provider;
3994
- const prevModel = config.model;
3995
- config = patchConfig(config, { provider: picked.provider, model: picked.model });
3996
- if (picked.provider !== prevProvider || picked.model !== prevModel) {
3997
- const saved = await saveToGlobalConfig(profileConfigPath, picked.provider, picked.model);
3998
- if (saved) {
3999
- renderer.writeInfo(`Saved ${picked.provider}/${picked.model} as default.
4000
- `);
4001
- } else {
4002
- renderer.writeWarning(
4003
- `Could not save ${picked.provider}/${picked.model} to config. Check permissions or disk space.
4004
- `
4005
- );
4006
- }
4007
- }
4008
- }
4009
- } else if (!config.provider || !config.model) {
4010
- writeErr(
4011
- "No provider or model configured. Run `wstack auth`, or pass --provider <id> --model <id>.\n"
4012
- );
4013
- await reader.close();
4014
- return 2;
4015
- } else {
4016
- const savedStatus = await validateSavedProviderModel(config, modelsRegistry);
4017
- if (!savedStatus.ok) {
4018
- writeErr(
4019
- `Saved provider/model is no longer usable (${savedStatus.reason ?? "unknown reason"}). Run \`wstack auth\` or pass --provider <id> --model <id>.
4020
- `
4021
- );
4022
- await reader.close();
4023
- return 2;
4024
- }
4025
- }
4026
- }
4027
- if (flags["webui"]) {
4028
- flags["tui"] = false;
4029
- flags["no-tui"] = true;
4030
- }
4031
- if (isInteractiveTTY) {
4032
- const promptMenu = await maybeRunSystemPromptMenu({
4033
- isInteractiveTTY,
4034
- flags,
4035
- renderer,
4036
- reader,
4037
- profileConfigPath,
4038
- paths: {
4039
- globalDir: wpaths.globalInstructions,
4040
- projectDir: wpaths.inProjectInstructions
4041
- }
4042
- });
4043
- if (promptMenu.aborted) {
4044
- await reader.close();
4045
- return 0;
4046
- }
4047
- if (promptMenu.changed && promptMenu.variant) {
4048
- config = patchConfig(config, { systemPrompt: { variant: promptMenu.variant } });
4049
- }
4050
- if (promptMenu.persistError) {
4051
- renderer.writeWarning(
4052
- `Could not save system prompt variant to config: ${toErrorMessage3(promptMenu.persistError)}
4053
- `
4054
- );
4055
- }
4056
- let modePinned;
4057
- if (flags["no-tui"]) modePinned = "repl";
4058
- else if (flags["tui"]) modePinned = "tui";
4059
- const yoloPinned = flags["no-yolo"] === true ? false : flags["yolo"] === true ? true : void 0;
4060
- let autonomyPinned;
4061
- if (flags["no-autonomy"] === true) autonomyPinned = "off";
4062
- else if (flags["eternal"] === true)
4063
- autonomyPinned = "off";
4064
- else if (typeof flags["autonomy"] === "string") {
4065
- const v = flags["autonomy"].toLowerCase();
4066
- autonomyPinned = v === "off" || v === "no" || v === "false" ? "off" : "auto";
4067
- } else if (flags["autonomy"] === true) {
4068
- autonomyPinned = "auto";
4069
- }
4070
- const lastChoices = config.launch ? {
4071
- mode: config.launch.mode ?? "tui",
4072
- yolo: config.yolo ?? true,
4073
- autonomy: config.launch.autonomy ?? "auto"
4074
- } : void 0;
4075
- let choices;
4076
- try {
4077
- choices = await runLaunchPrompts({
4078
- renderer,
4079
- reader,
4080
- modePinned,
4081
- yoloPinned,
4082
- autonomyPinned,
4083
- lastChoices
4084
- });
4085
- } catch (err) {
4086
- if (err instanceof LaunchAbortedError) {
4087
- await reader.close();
4088
- return 0;
4089
- }
4090
- throw err;
4091
- }
4092
- if (choices.mode === "tui") {
4093
- flags["tui"] = true;
4094
- flags["no-tui"] = false;
4095
- } else {
4096
- flags["tui"] = false;
4097
- flags["no-tui"] = true;
4098
- }
4099
- if (choices.yolo !== config.yolo) config = patchConfig(config, { yolo: choices.yolo });
4100
- flags["autonomy"] = choices.autonomy;
4101
- if (shouldPrintYoloNotice(lastChoices, yoloPinned, choices.yolo)) {
4102
- writeErr(
4103
- `
4104
- ${color13.yellow("YOLO is on")}: non-denied tool calls, including shell and file writes, run without confirmation.
4105
- ${color13.dim("Explicit deny rules still apply. Use")} --no-yolo ${color13.dim("or")} /yolo off ${color13.dim("to require prompts.")}
4106
-
4107
- `
4108
- );
4109
- }
4110
- if ((flags["skip-index"] || flags["skip"]) && config.indexing) {
4111
- config = patchConfig(config, {
4112
- indexing: { ...config.indexing, onSessionStart: false }
4113
- });
4114
- }
4115
- if (!simpleUiFullAuto) {
4116
- try {
4117
- const toPersist = flags["webui"] ? { ...choices, mode: lastChoices?.mode ?? config.launch?.mode ?? "tui" } : choices;
4118
- await persistLaunchChoices(profileConfigPath, toPersist);
4119
- } catch {
4120
- }
4121
- }
4122
- await printLaunchHints(renderer, flags, {
4123
- cursorFile: path8.join(wpaths.cacheDir, "hint-cursor")
4124
- });
4125
- } else {
4126
- const nonInteractiveAutonomy = !simpleUiFullAuto && (flags["no-autonomy"] === true || config.autonomy?.defaultMode === "off") ? "off" : "auto";
4127
- const effectiveChoices = config.launch ? {
4128
- mode: flags["no-tui"] ? "repl" : config.launch.mode ?? "tui",
4129
- yolo: config.yolo ?? true,
4130
- autonomy: nonInteractiveAutonomy
4131
- } : {
4132
- mode: "repl",
4133
- yolo: true,
4134
- autonomy: nonInteractiveAutonomy
4135
- };
4136
- if (effectiveChoices.mode === "repl") {
4137
- flags["tui"] = false;
4138
- flags["no-tui"] = true;
4139
- }
4140
- flags["autonomy"] = effectiveChoices.autonomy;
4141
- }
4142
- flags["director"] = true;
4143
- return {
4144
- config,
4145
- vault,
4146
- wpaths,
4147
- cwd,
4148
- projectRoot,
4149
- userHome,
4150
- flags,
4151
- positional,
4152
- modelsRegistry,
4153
- renderer,
4154
- reader,
4155
- logger,
4156
- needsSetup: !noInteractiveMode ? false : !config.provider || !config.model
4157
- };
4158
- }
4159
- async function checkGitInCwd(opts) {
4160
- const { cwd, renderer, reader } = opts;
4161
- const cwdGit = path8.join(cwd, ".git");
4162
- let hasCwdGit = false;
4163
- try {
4164
- await fs7.access(cwdGit);
4165
- hasCwdGit = true;
4166
- } catch {
4167
- }
4168
- if (!hasCwdGit) {
4169
- renderer.write(
4170
- `
4171
- ${color13.amber("\u25CB")} This folder has no ${color13.bold(".git")} repository.
4172
- `
4173
- );
4174
- const answer = (await reader.readLine(` ${color13.amber("?")} Initialize one here? ${color13.dim("[y/N]")} `)).trim().toLowerCase();
4175
- if (answer === "y" || answer === "yes") {
4176
- try {
4177
- const { spawn: spawn2 } = await import("node:child_process");
4178
- await new Promise((resolve3, reject) => {
4179
- const child = spawn2("git", ["init"], {
4180
- cwd,
4181
- signal: AbortSignal.timeout(1e4),
4182
- windowsHide: true
4183
- });
4184
- child.on("error", reject);
4185
- child.on(
4186
- "close",
4187
- (code) => code === 0 ? resolve3() : reject(new Error(`git init failed with ${code}`))
4188
- );
4189
- });
4190
- renderer.write(` ${color13.green("\u2713")} Git repository initialized
4191
- `);
4192
- hasCwdGit = true;
4193
- } catch (err) {
4194
- renderer.writeError(`git init failed: ${toErrorMessage3(err)}
4195
- `);
4196
- }
4197
- }
4198
- }
4199
- const parentDir = path8.dirname(cwd);
4200
- if (parentDir !== cwd) {
4201
- try {
4202
- await fs7.access(path8.join(parentDir, ".git"));
4203
- renderer.write(
4204
- ` ${color13.dim("\u2139")} A ${color13.bold(".git")} repo exists in the parent directory: ${color13.dim(parentDir)}
4205
- `
4206
- );
4207
- } catch {
4208
- }
4209
- }
4210
- }
4211
-
4212
- // src/preflight.ts
4213
- import { ensureSessionShell } from "@wrongstack/tools";
4214
-
4215
- // src/cli-update-notice.ts
4216
- import { writeErr as writeErr2 } from "@wrongstack/core/utils";
4217
- var NOTICE_FMT = `
4218
- \x1B[33m\u2191 Update available: v%s \u2192 v%s\x1B[0m Run \`wrongstack update\` to upgrade.
4219
-
4220
- `;
4221
- async function printUpdateNotice(initialUpdateInfo) {
4222
- let info = initialUpdateInfo;
4223
- if (!info?.outdated) {
4224
- const ac = new AbortController();
4225
- const timer = setTimeout(() => ac.abort(), 2e3);
4226
- try {
4227
- const { checkForUpdate } = await import("./update-check-PKSMSMD6.js");
4228
- info = await checkForUpdate(ac.signal);
4229
- } catch {
4230
- } finally {
4231
- clearTimeout(timer);
4232
- }
4233
- }
4234
- if (info?.outdated) {
4235
- writeErr2(NOTICE_FMT.replace("%s", info.current).replace("%s", info.latest));
4236
- }
4237
- return info;
4238
- }
4239
-
4240
- // src/preflight.ts
4241
- function applyNodeEnvDefault() {
4242
- if (process.env["NODE_ENV"] === void 0) {
4243
- process.env["NODE_ENV"] = "production";
4244
- process.env["WRONGSTACK_NODE_ENV_DEFAULTED"] = "1";
4245
- }
4246
- }
4247
- function applySessionShellDefault() {
4248
- ensureSessionShell();
4249
- }
4250
- async function applyPrintUpdateNotice(initialUpdateInfo) {
4251
- return await printUpdateNotice(initialUpdateInfo);
4252
- }
4253
- async function applyDebugStreamSeed(config) {
4254
- const { setDebugStreamEnabled } = await import("@wrongstack/providers");
4255
- if (config.debugStream) {
4256
- setDebugStreamEnabled(true);
4257
- }
4258
- }
4259
- async function runPreflight(config, initialUpdateInfo) {
4260
- applyNodeEnvDefault();
4261
- applySessionShellDefault();
4262
- const updateInfo = await applyPrintUpdateNotice(initialUpdateInfo);
4263
- await applyDebugStreamSeed(config);
4264
- return {
4265
- updateInfo,
4266
- debugStreamEnabled: Boolean(config.debugStream)
4267
- };
4268
- }
10
+ parseArgs
11
+ } from "./chunk-GGGUZSAU.js";
12
+ import "./chunk-7OCVIDC7.js";
4269
13
 
4270
- // src/cli-context.ts
4271
- async function initializeCli(argv) {
14
+ // src/cli-entry-main.ts
15
+ async function main(argv) {
4272
16
  applyNodeEnvDefault();
4273
17
  applySessionShellDefault();
4274
18
  const earlyFlags = parseArgs(argv).flags;
4275
- const earlyExit = await handleHelpVersionShortCircuit(argv);
4276
- if (earlyExit !== null) return earlyExit;
4277
- const desktopExit = await handleDesktopShortCircuit(earlyFlags, argv);
4278
- if (desktopExit !== null) return desktopExit;
4279
- const { flags: _earlyForMenu, positional: _positionalForMenu } = parseArgs(argv);
4280
- let webuiSessionChild;
4281
- if (_earlyForMenu["webui-session-child"] === true) {
4282
- try {
4283
- webuiSessionChild = parseWebuiSessionChildOptions(_earlyForMenu) ?? void 0;
4284
- } catch (err) {
4285
- const readyFile = typeof _earlyForMenu["ready-file"] === "string" ? _earlyForMenu["ready-file"] : void 0;
4286
- if (readyFile) {
4287
- await writeWebuiSessionChildError(readyFile, {
4288
- runtimeId: typeof _earlyForMenu["runtime-id"] === "string" ? _earlyForMenu["runtime-id"] : void 0,
4289
- parentShellId: typeof _earlyForMenu["parent-shell-id"] === "string" ? _earlyForMenu["parent-shell-id"] : void 0,
4290
- phase: "validate_args",
4291
- recoverable: false,
4292
- error: err
4293
- }).catch(() => void 0);
4294
- }
4295
- writeErr3(
4296
- `WebUI session child argument error: ${err instanceof Error ? err.message : String(err)}
4297
- `
4298
- );
4299
- return 2;
4300
- }
4301
- }
4302
- const launchMenuReader = new ReadlineInputReader();
4303
- let menuResult;
4304
- try {
4305
- menuResult = await runLaunchMenu({
4306
- argv,
4307
- flags: _earlyForMenu,
4308
- positional: _positionalForMenu,
4309
- renderer: new TerminalRenderer(),
4310
- reader: launchMenuReader,
4311
- // Last-saved menu choice wiring is intentionally minimal here:
4312
- // we only honor it when the user has *also* chosen no other
4313
- // surface flag. The persisted record is updated lazily inside
4314
- // boot() via a follow-up patch in the wiring layer, but the
4315
- // menu itself stays stateless so unit tests don't need to mock
4316
- // a ConfigStore. Users who want the saved-port shortcut without
4317
- // the menu can pass `--no-menu --webui --port=<saved>`.
4318
- lastChoice: void 0
4319
- });
4320
- } finally {
4321
- await launchMenuReader.close();
4322
- }
4323
- let effectiveArgv = argv;
4324
- if (menuResult !== null) {
4325
- if (menuResult.cancelled) {
4326
- return 0;
4327
- }
4328
- effectiveArgv = applyLaunchMenuToArgv(argv, menuResult);
4329
- const augmentedFlags = parseArgs(effectiveArgv).flags;
4330
- if (menuResult.mode === "hq") {
4331
- const hqAfterMenu = await handleHqShortCircuit(augmentedFlags);
4332
- if (hqAfterMenu !== null) return hqAfterMenu;
4333
- }
4334
- if (menuResult.mode === "desktop") {
4335
- const desktopAfterMenu = await handleDesktopShortCircuit(augmentedFlags, effectiveArgv);
4336
- if (desktopAfterMenu !== null) return desktopAfterMenu;
4337
- }
4338
- }
4339
- const effectiveFlags = parseArgs(effectiveArgv).flags;
4340
- try {
4341
- webuiSessionChild = webuiSessionChild ?? parseWebuiSessionChildOptions(effectiveFlags) ?? void 0;
4342
- } catch (err) {
4343
- const readyFile = typeof effectiveFlags["ready-file"] === "string" ? effectiveFlags["ready-file"] : void 0;
4344
- if (readyFile) {
4345
- await writeWebuiSessionChildError(readyFile, {
4346
- runtimeId: typeof effectiveFlags["runtime-id"] === "string" ? effectiveFlags["runtime-id"] : void 0,
4347
- parentShellId: typeof effectiveFlags["parent-shell-id"] === "string" ? effectiveFlags["parent-shell-id"] : void 0,
4348
- phase: "validate_args",
4349
- recoverable: false,
4350
- error: err
4351
- }).catch(() => void 0);
4352
- }
4353
- writeErr3(
4354
- `WebUI session child argument error: ${err instanceof Error ? err.message : String(err)}
4355
- `
4356
- );
4357
- return 2;
4358
- }
4359
- if (webuiSessionChild) {
4360
- try {
4361
- process.chdir(webuiSessionChild.workingDir);
4362
- if (webuiSessionChild.resume && webuiSessionChild.sessionId) {
4363
- const currentResume = effectiveFlags["resume"];
4364
- if (typeof currentResume === "string" && currentResume !== webuiSessionChild.sessionId) {
4365
- throw new Error(
4366
- `--resume ${currentResume} conflicts with --session-id ${webuiSessionChild.sessionId}`
4367
- );
4368
- }
4369
- if (currentResume !== webuiSessionChild.sessionId) {
4370
- effectiveArgv = [...effectiveArgv, "--resume", webuiSessionChild.sessionId];
4371
- }
4372
- }
4373
- } catch (err) {
4374
- await writeWebuiSessionChildError(webuiSessionChild.readyFile, {
4375
- protocolVersion: webuiSessionChild.protocolVersion,
4376
- runtimeId: webuiSessionChild.runtimeId,
4377
- parentShellId: webuiSessionChild.parentShellId,
4378
- phase: "validate_args",
4379
- recoverable: false,
4380
- error: err
4381
- }).catch(() => void 0);
4382
- writeErr3(
4383
- `WebUI session child argument error: ${err instanceof Error ? err.message : String(err)}
4384
- `
4385
- );
4386
- return 2;
4387
- }
4388
- }
4389
- const hqExit = await handleHqShortCircuit(effectiveFlags);
4390
- if (hqExit !== null) return hqExit;
4391
- const ctx = await boot(effectiveArgv);
4392
- if (typeof ctx === "number") return ctx;
4393
- if (menuResult !== null) {
4394
- const persisted = toPersistedMenuChoice(menuResult);
4395
- if (persisted) {
4396
- try {
4397
- await persistMenuChoice(activeProfileConfigPath(ctx.wpaths, ctx.config), persisted);
4398
- } catch (err) {
4399
- ctx.logger.debug(
4400
- `launch-menu: failed to persist menu choice (${err instanceof Error ? err.message : String(err)})`
4401
- );
4402
- }
4403
- }
19
+ if (earlyFlags["help"] === true || earlyFlags["version"] === true) {
20
+ const { handleHelpVersionShortCircuit } = await import("./short-circuit-flags-7YUCERBB.js");
21
+ const earlyExit = await handleHelpVersionShortCircuit(argv);
22
+ if (earlyExit !== null) return earlyExit;
4404
23
  }
4405
- const { config, vault, wpaths, cwd, modelsRegistry, renderer, reader, logger } = ctx;
4406
- const { updateInfo: refreshedUpdateInfo } = await runPreflight(config, ctx.updateInfo);
4407
- const profileConfigPath = activeProfileConfigPath(wpaths, config);
4408
- setOAuthTokenPersister((providerId, creds) => {
4409
- void mutateConfigProviders(profileConfigPath, vault, (all) => {
4410
- const p = all[providerId];
4411
- if (!p) return;
4412
- const keys = normalizeKeys(p);
4413
- const active = p.activeKey ? keys.find((k) => k.label === p.activeKey) : keys[0];
4414
- if (!active) return;
4415
- active.apiKey = creds.accessToken;
4416
- active.refreshToken = creds.refreshToken;
4417
- active.expiresAt = new Date(creds.expiresAt).toISOString();
4418
- if (creds.accountId) active.accountId = creds.accountId;
4419
- writeKeysBack(p, keys);
4420
- }).catch(() => {
4421
- });
4422
- });
4423
- const { events, container } = wireContainer({
4424
- config,
4425
- wpaths,
4426
- cwd,
4427
- logger,
4428
- reader,
4429
- renderer,
4430
- modelsRegistry
4431
- });
4432
- const configStore = container.resolve(TOKENS3.ConfigStore);
4433
- return {
4434
- ...ctx,
4435
- updateInfo: refreshedUpdateInfo,
4436
- events,
4437
- container,
4438
- configStore,
4439
- webuiSessionChild
4440
- };
4441
- }
4442
-
4443
- // src/cli-entry-main.ts
4444
- async function main(argv) {
24
+ const { initializeCli } = await import("./cli-context-K73EIND5.js");
4445
25
  const cliCtx = await initializeCli(argv);
4446
26
  if (typeof cliCtx === "number") return cliCtx;
4447
- const { runInteractive } = await import("./cli-main-ILRTNPDP.js");
27
+ const { runInteractive } = await import("./cli-main-QXSKKANW.js");
4448
28
  return runInteractive(cliCtx);
4449
29
  }
4450
30
 
4451
31
  // src/cli-entry-point.ts
4452
- import { resolve as resolve2 } from "node:path";
32
+ import { resolve } from "node:path";
4453
33
  import { pathToFileURL } from "node:url";
4454
- import { installCrashShield, runFatalSalvageSync, writeErr as writeErr4 } from "@wrongstack/core/utils";
34
+ import { installCrashShield, runFatalSalvageSync, writeErr } from "@wrongstack/core/utils";
4455
35
  var SQLITE_WARNING_RE = /sqlite is an experimental feature/i;
4456
36
  function installSqliteWarningFilter() {
4457
37
  const originalEmit = process.emitWarning.bind(process);
@@ -4468,7 +48,7 @@ installSqliteWarningFilter();
4468
48
  function isCliMain(moduleUrl, argvEntry = process.argv[1]) {
4469
49
  if (!argvEntry) return false;
4470
50
  try {
4471
- if (moduleUrl === pathToFileURL(resolve2(argvEntry)).href) return true;
51
+ if (moduleUrl === pathToFileURL(resolve(argvEntry)).href) return true;
4472
52
  } catch {
4473
53
  }
4474
54
  return argvEntry.endsWith("/cli/dist/index.js") || argvEntry.endsWith("\\cli\\dist\\index.js");
@@ -4549,7 +129,7 @@ function runAsMain(mainFn) {
4549
129
  },
4550
130
  (err) => {
4551
131
  runFatalSalvageSync();
4552
- writeErr4((err instanceof Error ? err.stack : String(err)) + "\n");
132
+ writeErr((err instanceof Error ? err.stack : String(err)) + "\n");
4553
133
  process.exitCode = 1;
4554
134
  scheduleForcedExit(1);
4555
135
  }