@wrongstack/cli 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,13 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import { color, DefaultPathResolver, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, DefaultLogger, DefaultModelsRegistry, Container, TOKENS, DefaultSecretScrubber, DefaultRetryPolicy, DefaultErrorHandler, DefaultTokenCounter, DefaultSessionStore, DefaultMemoryStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultPermissionPolicy, HybridCompactor, ProviderRegistry, ToolRegistry, createContextManagerTool, EventBus, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, createDefaultPipelines, AutoCompactionMiddleware, Agent, SlashCommandRegistry, loadPlugins, InputBuilder, DefaultPluginAPI, rewriteConfigEncrypted, atomicWrite } from '@wrongstack/core';
3
- import * as fs2 from 'fs/promises';
2
+ import { color, DefaultLogger, DefaultModelsRegistry, Container, DefaultConfigStore, TOKENS, DefaultSecretScrubber, DefaultRetryPolicy, DefaultErrorHandler, DefaultTokenCounter, DefaultSessionStore, DefaultMemoryStore, DefaultSkillLoader, DefaultSystemPromptBuilder, DefaultPermissionPolicy, HybridCompactor, ProviderRegistry, ToolRegistry, createContextManagerTool, EventBus, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, createDefaultPipelines, AutoCompactionMiddleware, Agent, SlashCommandRegistry, loadPlugins, DefaultPathResolver, resolveWstackPaths, DefaultSecretVault, migratePlaintextSecrets, DefaultConfigLoader, InputBuilder, DefaultPluginAPI, DefaultSessionReader, atomicWrite, makeAgentSubagentRunner, DefaultMultiAgentCoordinator, decryptConfigSecrets, encryptConfigSecrets } from '@wrongstack/core';
3
+ import * as fs6 from 'fs/promises';
4
+ import { writeFileSync } from 'fs';
4
5
  import { createRequire } from 'module';
5
- import * as os2 from 'os';
6
- import * as path2 from 'path';
6
+ import * as path5 from 'path';
7
7
  import { MCPRegistry } from '@wrongstack/mcp';
8
8
  import { buildProviderFactoriesFromRegistry, makeProviderFromConfig, capabilitiesFor } from '@wrongstack/providers';
9
- import { builtinTools, rememberTool, forgetTool } from '@wrongstack/tools';
9
+ import { rememberTool, forgetTool } from '@wrongstack/tools';
10
+ import { builtinTools } from '@wrongstack/tools/builtin';
10
11
  import * as readline from 'readline';
12
+ import * as os3 from 'os';
13
+ import { randomUUID } from 'crypto';
11
14
 
12
15
  var __defProp = Object.defineProperty;
13
16
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -37,11 +40,11 @@ var ReadlineInputReader = class {
37
40
  history = [];
38
41
  pending = false;
39
42
  constructor(opts = {}) {
40
- this.historyFile = opts.historyFile ?? path2.join(os2.homedir(), ".wrongstack", "history");
43
+ this.historyFile = opts.historyFile ?? path5.join(os3.homedir(), ".wrongstack", "history");
41
44
  }
42
45
  async loadHistory() {
43
46
  try {
44
- const raw = await fs2.readFile(this.historyFile, "utf8");
47
+ const raw = await fs6.readFile(this.historyFile, "utf8");
45
48
  this.history = raw.split("\n").filter(Boolean).slice(-1e3);
46
49
  } catch {
47
50
  this.history = [];
@@ -49,8 +52,8 @@ var ReadlineInputReader = class {
49
52
  }
50
53
  async saveHistory() {
51
54
  try {
52
- await fs2.mkdir(path2.dirname(this.historyFile), { recursive: true });
53
- await fs2.writeFile(this.historyFile, this.history.slice(-1e3).join("\n"));
55
+ await fs6.mkdir(path5.dirname(this.historyFile), { recursive: true });
56
+ await fs6.writeFile(this.historyFile, this.history.slice(-1e3).join("\n"));
54
57
  } catch {
55
58
  }
56
59
  }
@@ -68,7 +71,7 @@ var ReadlineInputReader = class {
68
71
  async readLine(prompt) {
69
72
  if (this.history.length === 0) await this.loadHistory();
70
73
  while (this.pending) {
71
- await new Promise((resolve2) => setTimeout(resolve2, 50));
74
+ await new Promise((resolve3) => setTimeout(resolve3, 50));
72
75
  }
73
76
  this.pending = true;
74
77
  try {
@@ -78,13 +81,13 @@ var ReadlineInputReader = class {
78
81
  this.rl = void 0;
79
82
  }
80
83
  const fresh = this.ensure();
81
- return new Promise((resolve2, reject) => {
84
+ return new Promise((resolve3, reject) => {
82
85
  fresh.question(prompt ?? "> ", (line) => {
83
86
  if (line.trim()) {
84
87
  this.history.push(line);
85
88
  void this.saveHistory();
86
89
  }
87
- resolve2(line);
90
+ resolve3(line);
88
91
  });
89
92
  fresh.once("close", () => reject(new Error("EOF")));
90
93
  });
@@ -94,7 +97,7 @@ var ReadlineInputReader = class {
94
97
  }
95
98
  async readKey(prompt, options) {
96
99
  process.stdout.write(prompt);
97
- return new Promise((resolve2) => {
100
+ return new Promise((resolve3) => {
98
101
  const stdin = process.stdin;
99
102
  const wasRaw = stdin.isRaw;
100
103
  const wasPaused = stdin.isPaused();
@@ -109,7 +112,7 @@ var ReadlineInputReader = class {
109
112
  cleanup();
110
113
  process.stdout.write(`${opt.key}
111
114
  `);
112
- resolve2(opt.value);
115
+ resolve3(opt.value);
113
116
  }
114
117
  };
115
118
  const cleanup = () => {
@@ -121,9 +124,14 @@ var ReadlineInputReader = class {
121
124
  });
122
125
  }
123
126
  /**
124
- * Read a single line of input without echoing it to the terminal. Used
125
- * for API keys / passwords. Non-TTY input is read normally — there's
126
- * nothing to hide when piped.
127
+ * Read a line of input while masking each character with a bullet so the
128
+ * user gets visual confirmation that bytes are arriving (especially on
129
+ * paste, which previously felt like nothing happened). Pasted chunks
130
+ * are echoed as a run of bullets, Backspace/DEL erases one bullet, and
131
+ * Ctrl+U / Ctrl+W are honored. Non-TTY input is read normally — there's
132
+ * nothing to hide when piped, and echoing bullets to a file is noise.
133
+ *
134
+ * Returns the raw entered string (no trim — caller decides).
127
135
  */
128
136
  async readSecret(prompt) {
129
137
  const stdin = process.stdin;
@@ -131,18 +139,25 @@ var ReadlineInputReader = class {
131
139
  this.rl?.close();
132
140
  this.rl = void 0;
133
141
  process.stdout.write(prompt);
134
- return new Promise((resolve2) => {
142
+ return new Promise((resolve3) => {
135
143
  let buf = "";
136
144
  const wasRaw = stdin.isRaw;
137
145
  stdin.setRawMode(true);
138
146
  stdin.resume();
139
147
  stdin.setEncoding("utf8");
148
+ const eraseChar = () => {
149
+ process.stdout.write("\b \b");
150
+ };
151
+ const eraseAll = () => {
152
+ for (let i = 0; i < buf.length; i++) eraseChar();
153
+ };
140
154
  const onData = (chunk) => {
141
155
  for (const ch of chunk) {
142
156
  if (ch === "\r" || ch === "\n") {
143
157
  cleanup();
144
- process.stdout.write("\n");
145
- resolve2(buf);
158
+ process.stdout.write(` ${dim(`[${buf.length} chars]`)}
159
+ `);
160
+ resolve3(buf);
146
161
  return;
147
162
  }
148
163
  if (ch === "") {
@@ -150,11 +165,28 @@ var ReadlineInputReader = class {
150
165
  process.stdout.write("\n");
151
166
  process.exit(130);
152
167
  }
168
+ if (ch === "") {
169
+ eraseAll();
170
+ buf = "";
171
+ continue;
172
+ }
173
+ if (ch === "") {
174
+ const m = buf.match(/(\S+\s*)$/);
175
+ const drop = m ? m[0].length : buf.length;
176
+ for (let i = 0; i < drop; i++) eraseChar();
177
+ buf = buf.slice(0, buf.length - drop);
178
+ continue;
179
+ }
153
180
  if (ch === "\x7F" || ch === "\b") {
154
- buf = buf.slice(0, -1);
181
+ if (buf.length > 0) {
182
+ buf = buf.slice(0, -1);
183
+ eraseChar();
184
+ }
155
185
  continue;
156
186
  }
187
+ if (ch < " ") continue;
157
188
  buf += ch;
189
+ process.stdout.write("\u2022");
158
190
  }
159
191
  };
160
192
  const cleanup = () => {
@@ -171,6 +203,10 @@ var ReadlineInputReader = class {
171
203
  this.rl = void 0;
172
204
  }
173
205
  };
206
+ function dim(s) {
207
+ if (!process.stdout.isTTY) return s;
208
+ return `\x1B[2m${s}\x1B[22m`;
209
+ }
174
210
  function renderDiff(diff) {
175
211
  if (!diff) return "";
176
212
  const lines = diff.split("\n");
@@ -221,6 +257,13 @@ ${theme.primary("\u258D")} ${theme.bold(tool.name)}
221
257
  return answer;
222
258
  };
223
259
  }
260
+ function makeConfirmAwaiter(reader) {
261
+ const delegate = makePromptDelegate(reader);
262
+ return async (tool, input, _toolUseId, suggestedPattern) => {
263
+ const result = await delegate(tool, input, suggestedPattern);
264
+ return result;
265
+ };
266
+ }
224
267
  function stringifyInput(input) {
225
268
  if (!input || typeof input !== "object") return "";
226
269
  const obj = input;
@@ -236,8 +279,10 @@ function hasDiff(input) {
236
279
  }
237
280
  function hasApiKey(provider, config) {
238
281
  if (provider.envVars.some((v) => !!process.env[v])) return true;
239
- const stored = config?.providers?.[provider.id]?.apiKey;
240
- if (typeof stored === "string" && stored.length > 0) return true;
282
+ const entry = config?.providers?.[provider.id];
283
+ if (!entry) return false;
284
+ if (typeof entry.apiKey === "string" && entry.apiKey.length > 0) return true;
285
+ if (Array.isArray(entry.apiKeys) && entry.apiKeys.some((k) => k?.apiKey)) return true;
241
286
  return false;
242
287
  }
243
288
  async function runPicker(deps) {
@@ -254,15 +299,53 @@ ${color.bold(theme2.primary("WrongStack") + color.dim(" \u2014 Provider & Model
254
299
  return void 0;
255
300
  }
256
301
  const supported = providers.filter((p) => p.family !== "unsupported");
257
- if (supported.length === 0) {
302
+ const catalogById = new Map(supported.map((p) => [p.id, p]));
303
+ const overlay = config?.providers ?? {};
304
+ const seen = /* @__PURE__ */ new Set();
305
+ const merged = [];
306
+ for (const p of supported) {
307
+ const cfg = overlay[p.id];
308
+ seen.add(p.id);
309
+ if (cfg) {
310
+ merged.push({
311
+ ...p,
312
+ family: cfg.family ?? p.family,
313
+ apiBase: cfg.baseUrl ?? p.apiBase,
314
+ envVars: cfg.envVars && cfg.envVars.length > 0 ? cfg.envVars : p.envVars,
315
+ // When the user has saved an explicit model list, it wins — they
316
+ // know which models their endpoint actually serves (e.g. LM
317
+ // Studio, vLLM, or a proxy with custom model ids). Otherwise the
318
+ // catalog list keeps providing suggestions.
319
+ models: cfg.models && cfg.models.length > 0 ? cfg.models.map((m) => ({ id: m, name: m })) : p.models
320
+ });
321
+ } else {
322
+ merged.push(p);
323
+ }
324
+ }
325
+ for (const [id, cfg] of Object.entries(overlay)) {
326
+ if (seen.has(id)) continue;
327
+ if (!cfg?.family || cfg.family === "unsupported") continue;
328
+ const catalogType = cfg.type && cfg.type !== id ? cfg.type : void 0;
329
+ const inherited = catalogType ? catalogById.get(catalogType) : void 0;
330
+ merged.push({
331
+ id,
332
+ name: inherited ? `${inherited.name} ${color.dim("(alias)")}` : id,
333
+ family: cfg.family,
334
+ apiBase: cfg.baseUrl ?? inherited?.apiBase,
335
+ envVars: cfg.envVars ?? inherited?.envVars ?? [],
336
+ models: cfg.models && cfg.models.length > 0 ? cfg.models.map((m) => ({ id: m, name: m })) : inherited?.models ?? [],
337
+ npm: inherited?.npm
338
+ });
339
+ }
340
+ if (merged.length === 0) {
258
341
  renderer.writeError("No supported providers found in catalog.");
259
342
  return void 0;
260
343
  }
261
- const keyed = supported.filter((p) => hasApiKey(p, config));
344
+ const keyed = merged.filter((p) => hasApiKey(p, config));
262
345
  let displayList = keyed;
263
346
  let showingFallback = false;
264
347
  if (keyed.length === 0) {
265
- displayList = supported;
348
+ displayList = merged;
266
349
  showingFallback = true;
267
350
  }
268
351
  const families = /* @__PURE__ */ new Map();
@@ -283,7 +366,8 @@ ${color.bold(theme2.primary("WrongStack") + color.dim(" \u2014 Provider & Model
283
366
  `);
284
367
  for (const p of list) {
285
368
  const envFound = p.envVars.some((v) => !!process.env[v]);
286
- const configKey = typeof config?.providers?.[p.id]?.apiKey === "string" && config.providers[p.id].apiKey.length > 0;
369
+ const entry = config?.providers?.[p.id];
370
+ const configKey = typeof entry?.apiKey === "string" && entry.apiKey.length > 0 || Array.isArray(entry?.apiKeys) && entry.apiKeys.some((k) => k?.apiKey);
287
371
  const marker = envFound ? color.green("\u25CF") : configKey ? color.cyan("\u25C9") : color.dim("\u25CB");
288
372
  const isDefault = p.id === defaultProvider;
289
373
  if (isDefault) defaultIdx = idx;
@@ -321,7 +405,7 @@ ${color.amber("?")} Select provider (1-${ordered.length})${defaultHint}: `)).tri
321
405
  renderer.write(color.dim("Cancelled.\n"));
322
406
  return void 0;
323
407
  }
324
- const providerIdx = parseInt(providerAnswer, 10);
408
+ const providerIdx = Number.parseInt(providerAnswer, 10);
325
409
  if (Number.isNaN(providerIdx) || providerIdx < 1 || providerIdx > ordered.length) {
326
410
  const byId = ordered.find((o) => o.provider.id.toLowerCase() === providerAnswer.toLowerCase());
327
411
  if (!byId) {
@@ -399,7 +483,7 @@ ${color.amber("?")} Select model (1-${models.length})${defaultHint}: `)).trim();
399
483
  return resolveModelSelection(answer, models, provider, registry, renderer);
400
484
  }
401
485
  async function resolveModelSelection(answer, models, provider, _registry, renderer, _reader) {
402
- const idx = parseInt(answer, 10);
486
+ const idx = Number.parseInt(answer, 10);
403
487
  let modelId;
404
488
  if (!Number.isNaN(idx) && idx >= 1 && idx <= models.length) {
405
489
  modelId = models[idx - 1].id;
@@ -432,17 +516,17 @@ async function resolveModelSelection(answer, models, provider, _registry, render
432
516
  var theme2 = { primary: color.amber };
433
517
  async function saveToGlobalConfig(configPath, provider, model) {
434
518
  try {
435
- const { atomicWrite: atomicWrite2 } = await import('@wrongstack/core');
436
- const fs6 = await import('fs/promises');
519
+ const { atomicWrite: atomicWrite3 } = await import('@wrongstack/core');
520
+ const fs8 = await import('fs/promises');
437
521
  let existing = {};
438
522
  try {
439
- const raw = await fs6.readFile(configPath, "utf8");
523
+ const raw = await fs8.readFile(configPath, "utf8");
440
524
  existing = JSON.parse(raw);
441
525
  } catch {
442
526
  }
443
527
  existing.provider = provider;
444
528
  existing.model = model;
445
- await atomicWrite2(configPath, JSON.stringify(existing, null, 2));
529
+ await atomicWrite3(configPath, JSON.stringify(existing, null, 2));
446
530
  return true;
447
531
  } catch {
448
532
  return false;
@@ -455,58 +539,103 @@ function buildBuiltinSlashCommands(opts) {
455
539
  clearCommand(opts),
456
540
  compactCommand(opts),
457
541
  contextCommand(opts),
458
- usageCommand(opts),
459
542
  toolsCommand(opts),
460
543
  skillCommand(opts),
461
- useCommand(opts),
462
- modelCommand(opts),
463
544
  diagCommand(opts),
464
545
  statsCommand(opts),
546
+ spawnCommand(opts),
547
+ agentsCommand(opts),
548
+ metricsCommand(opts),
549
+ healthCommand(opts),
550
+ memoryCommand(opts),
465
551
  saveCommand(opts),
466
552
  loadCommand(opts),
467
553
  exitCommand(opts)
468
554
  ];
469
555
  }
556
+ function memoryCommand(opts) {
557
+ return {
558
+ name: "memory",
559
+ description: "Inspect or edit persistent memory: /memory [show|remember <text>|forget <query>|clear]",
560
+ async run(args) {
561
+ const store = opts.memoryStore;
562
+ if (!store) return { message: "No memory store configured." };
563
+ const [verb, ...rest] = args.trim().split(/\s+/);
564
+ const restJoined = rest.join(" ").trim();
565
+ switch (verb) {
566
+ case "":
567
+ case "show":
568
+ case "list": {
569
+ const text = await store.readAll();
570
+ return {
571
+ message: text.trim().length === 0 ? "Memory is empty. Add an entry with `/memory remember <text>`." : text
572
+ };
573
+ }
574
+ case "remember":
575
+ case "add": {
576
+ if (!restJoined) return { message: "Usage: /memory remember <text>" };
577
+ await store.remember(restJoined);
578
+ return { message: `Remembered: ${restJoined}` };
579
+ }
580
+ case "forget":
581
+ case "rm": {
582
+ if (!restJoined) return { message: "Usage: /memory forget <query>" };
583
+ const n = await store.forget(restJoined);
584
+ return {
585
+ message: n === 0 ? `No entries matched "${restJoined}".` : `Forgot ${n} entries.`
586
+ };
587
+ }
588
+ case "clear": {
589
+ await store.clear();
590
+ return { message: "Cleared all memory scopes." };
591
+ }
592
+ default:
593
+ return {
594
+ message: `Unknown subcommand "${verb}". Try: show | remember <text> | forget <query> | clear`
595
+ };
596
+ }
597
+ }
598
+ };
599
+ }
470
600
  function initCommand(opts) {
471
601
  return {
472
602
  name: "init",
473
603
  description: "Scaffold .wrongstack/AGENTS.md in the current project.",
474
604
  async run(args, ctx) {
475
605
  const force = args.trim() === "--force";
476
- const dir = path2.join(ctx.projectRoot, ".wrongstack");
477
- const file = path2.join(dir, "AGENTS.md");
606
+ const dir = path5.join(ctx.projectRoot, ".wrongstack");
607
+ const file = path5.join(dir, "AGENTS.md");
478
608
  try {
479
- await fs2.access(file);
609
+ await fs6.access(file);
480
610
  if (!force) {
481
- const msg = `AGENTS.md already exists at ${file}. Use "/init --force" to overwrite.`;
482
- opts.renderer.writeWarning(msg);
483
- return { message: msg };
611
+ const msg2 = `AGENTS.md already exists at ${file}. Use "/init --force" to overwrite.`;
612
+ opts.renderer.writeWarning(msg2);
613
+ return { message: msg2 };
484
614
  }
485
615
  } catch {
486
616
  }
487
617
  const detected = await detectProjectFacts(ctx.projectRoot);
488
618
  const body = renderAgentsTemplate(detected);
489
- await fs2.mkdir(dir, { recursive: true });
490
- await fs2.writeFile(file, body, "utf8");
619
+ await fs6.mkdir(dir, { recursive: true });
620
+ await fs6.writeFile(file, body, "utf8");
491
621
  if (detected.hints.length > 0) {
492
- const msg = `Wrote ${file}
622
+ const msg2 = `Wrote ${file}
493
623
  Pre-filled: ${detected.hints.join(", ")}. Edit the file to add anything else worth remembering.`;
494
624
  opts.renderer.writeInfo(`Wrote ${file}`);
495
625
  opts.renderer.writeInfo(`Pre-filled: ${detected.hints.join(", ")}. Edit the file to add anything else worth remembering.`);
496
- return { message: msg };
497
- } else {
498
- const msg = `Wrote ${file}
499
- No project type auto-detected. Edit the file to add build/test commands and conventions.`;
500
- opts.renderer.writeInfo(`Wrote ${file}`);
501
- return { message: msg };
626
+ return { message: msg2 };
502
627
  }
628
+ const msg = `Wrote ${file}
629
+ No project type auto-detected. Edit the file to add build/test commands and conventions.`;
630
+ opts.renderer.writeInfo(`Wrote ${file}`);
631
+ return { message: msg };
503
632
  }
504
633
  };
505
634
  }
506
635
  async function detectProjectFacts(root) {
507
636
  const facts = { hints: [] };
508
637
  try {
509
- const pkg = JSON.parse(await fs2.readFile(path2.join(root, "package.json"), "utf8"));
638
+ const pkg = JSON.parse(await fs6.readFile(path5.join(root, "package.json"), "utf8"));
510
639
  const scripts = pkg.scripts ?? {};
511
640
  const pm = (pkg.packageManager ?? "npm").split("@")[0] ?? "npm";
512
641
  if (scripts["build"]) facts.build = `${pm} run build`;
@@ -517,28 +646,28 @@ async function detectProjectFacts(root) {
517
646
  } catch {
518
647
  }
519
648
  try {
520
- await fs2.access(path2.join(root, "pyproject.toml"));
649
+ await fs6.access(path5.join(root, "pyproject.toml"));
521
650
  facts.test ??= "pytest";
522
651
  facts.lint ??= "ruff check .";
523
652
  facts.hints.push("pyproject.toml");
524
653
  } catch {
525
654
  }
526
655
  try {
527
- await fs2.access(path2.join(root, "go.mod"));
656
+ await fs6.access(path5.join(root, "go.mod"));
528
657
  facts.build ??= "go build ./...";
529
658
  facts.test ??= "go test ./...";
530
659
  facts.hints.push("go.mod");
531
660
  } catch {
532
661
  }
533
662
  try {
534
- await fs2.access(path2.join(root, "Cargo.toml"));
663
+ await fs6.access(path5.join(root, "Cargo.toml"));
535
664
  facts.build ??= "cargo build";
536
665
  facts.test ??= "cargo test";
537
666
  facts.hints.push("Cargo.toml");
538
667
  } catch {
539
668
  }
540
669
  try {
541
- await fs2.access(path2.join(root, "Makefile"));
670
+ await fs6.access(path5.join(root, "Makefile"));
542
671
  facts.build ??= "make";
543
672
  facts.test ??= "make test";
544
673
  facts.hints.push("Makefile");
@@ -586,12 +715,8 @@ function diagCommand(opts) {
586
715
  name: "diag",
587
716
  description: "Show runtime diagnostics (provider, tokens, tools, MCP).",
588
717
  async run() {
589
- if (opts.onDiag) {
590
- opts.onDiag();
591
- return { message: "diag" };
592
- } else {
593
- return { message: "Diag not available in this context." };
594
- }
718
+ if (!opts.onDiag) return { message: "Diag not available in this context." };
719
+ return { message: opts.onDiag() };
595
720
  }
596
721
  };
597
722
  }
@@ -600,28 +725,73 @@ function statsCommand(opts) {
600
725
  name: "stats",
601
726
  description: "Show session report: tokens, requests, tools, files, cost.",
602
727
  async run() {
603
- if (opts.onStats) {
604
- opts.onStats();
605
- return { message: "stats" };
606
- } else {
607
- return { message: "Stats not available in this context." };
608
- }
728
+ if (!opts.onStats) return { message: "Stats not available in this context." };
729
+ const text = opts.onStats();
730
+ return { message: text ?? "No session activity recorded yet." };
609
731
  }
610
732
  };
611
733
  }
612
734
  function helpCommand(opts) {
613
735
  return {
614
736
  name: "help",
615
- description: "Show available slash commands.",
616
- async run() {
737
+ description: "Show available slash commands. Pass a name for detailed help.",
738
+ help: [
739
+ "Usage:",
740
+ " /help List every command with its one-line description.",
741
+ " /help <name> Show detailed help for one command (falls back to the description).",
742
+ "",
743
+ "Examples:",
744
+ " /help",
745
+ " /help context",
746
+ " /help model"
747
+ ].join("\n"),
748
+ async run(args) {
749
+ const query = args.trim();
750
+ if (query) {
751
+ const needle = query.startsWith("/") ? query.slice(1) : query;
752
+ let match;
753
+ for (const entry of opts.registry.listWithOwner()) {
754
+ const aliases = entry.cmd.aliases ?? [];
755
+ const candidates = [
756
+ entry.cmd.name,
757
+ entry.fullName,
758
+ ...aliases,
759
+ ...aliases.map(
760
+ (a) => entry.owner === "core" ? a : `${entry.owner}:${a}`
761
+ )
762
+ ];
763
+ if (candidates.includes(needle)) {
764
+ match = entry;
765
+ break;
766
+ }
767
+ }
768
+ if (!match) {
769
+ return { message: `Unknown command: /${needle}. Run /help to list commands.` };
770
+ }
771
+ const prefix = match.owner === "core" ? "" : `${match.owner}:`;
772
+ const header = `/${prefix}${match.cmd.name}`;
773
+ const aliasLine = match.cmd.aliases && match.cmd.aliases.length > 0 ? `Aliases: ${match.cmd.aliases.map((a) => `/${prefix}${a}`).join(", ")}
774
+ ` : "";
775
+ const body = match.cmd.help ?? match.cmd.description;
776
+ return {
777
+ message: [
778
+ header,
779
+ "\u2500".repeat(header.length),
780
+ aliasLine + (match.cmd.help ? "" : `${match.cmd.description}
781
+ `),
782
+ body
783
+ ].filter(Boolean).join("\n")
784
+ };
785
+ }
617
786
  const lines = ["Available slash commands:"];
618
- for (const { cmd, owner, fullName } of opts.registry.listWithOwner()) {
787
+ for (const { cmd, owner } of opts.registry.listWithOwner()) {
619
788
  const isBuiltin = owner === "core";
620
789
  const prefix = isBuiltin ? "" : `${owner}:`;
621
790
  const aliases = cmd.aliases ? cmd.aliases.map((a) => `/${prefix}${a}`).join(", ") : "";
622
791
  const aliasStr = aliases ? ` (${aliases})` : "";
623
792
  lines.push(` /${prefix}${cmd.name}${aliasStr} \u2014 ${cmd.description}`);
624
793
  }
794
+ lines.push("", "Run `/help <name>` for detailed help on a specific command.");
625
795
  return { message: lines.join("\n") };
626
796
  }
627
797
  };
@@ -630,6 +800,14 @@ function clearCommand(opts) {
630
800
  return {
631
801
  name: "clear",
632
802
  description: "Reset the session and start a new one.",
803
+ help: [
804
+ "Usage:",
805
+ " /clear",
806
+ "",
807
+ "Wipes everything in the current REPL state: messages, todos, read-file tracking,",
808
+ "file mtimes, meta. Memory store entries (all scopes) are cleared too. The terminal",
809
+ "is wiped. Use this when you want a fresh conversation without restarting `wstack`."
810
+ ].join("\n"),
633
811
  async run(_args, ctx) {
634
812
  if (ctx) {
635
813
  ctx.messages = [];
@@ -652,6 +830,14 @@ function contextCommand(opts) {
652
830
  name: "context",
653
831
  aliases: ["ctx"],
654
832
  description: "Show context window summary.",
833
+ help: [
834
+ "Usage:",
835
+ " /context Show counts: messages, est. tokens, tool calls, todos, read files.",
836
+ " /context detail As above, plus model, cwd, projectRoot, and the file list.",
837
+ "",
838
+ "Token estimate is a `chars \xF7 4` heuristic, not a real tokenizer call \u2014",
839
+ "good enough to spot growth between turns."
840
+ ].join("\n"),
655
841
  async run(args, ctx) {
656
842
  const messages = ctx.messages;
657
843
  const detailed = args.trim() === "detail";
@@ -732,6 +918,14 @@ function compactCommand(opts) {
732
918
  return {
733
919
  name: "compact",
734
920
  description: "Compact the context window.",
921
+ help: [
922
+ "Usage:",
923
+ " /compact Run the configured compactor with default settings.",
924
+ " /compact aggressive Compact more aggressively \u2014 keeps fewer recent turns verbatim.",
925
+ "",
926
+ "The compactor summarizes older turns to reclaim tokens. The default keeps the most",
927
+ "recent K message pairs untouched; aggressive halves that window."
928
+ ].join("\n"),
735
929
  async run(args, ctx) {
736
930
  if (!opts.compactor) {
737
931
  const msg2 = "No compactor configured.";
@@ -746,26 +940,6 @@ function compactCommand(opts) {
746
940
  }
747
941
  };
748
942
  }
749
- function usageCommand(opts) {
750
- return {
751
- name: "usage",
752
- aliases: ["cost"],
753
- description: "Show token usage and estimated cost.",
754
- async run() {
755
- const total = opts.tokenCounter.total();
756
- const cost = opts.tokenCounter.estimateCost();
757
- const msg = `${color.bold("Usage")}
758
- input: ${total.input}
759
- output: ${total.output}
760
- cache read: ${total.cacheRead ?? 0}
761
- cache write: ${total.cacheWrite ?? 0}
762
- cost: $${cost.total.toFixed(4)} (input $${cost.input.toFixed(4)} / output $${cost.output.toFixed(4)})
763
- `;
764
- opts.renderer.write(msg);
765
- return { message: msg };
766
- }
767
- };
768
- }
769
943
  function toolsCommand(opts) {
770
944
  return {
771
945
  name: "tools",
@@ -803,47 +977,14 @@ function skillCommand(opts) {
803
977
  ${lines.join("\n")}
804
978
  `;
805
979
  return { message: msg };
806
- } else {
807
- const skill = await opts.skillLoader.find(args.trim());
808
- if (!skill) {
809
- const msg = `Skill "${args.trim()}" not found.`;
810
- return { message: msg };
811
- }
812
- const body = await opts.skillLoader.readBody(skill.name);
813
- return { message: body };
814
- }
815
- }
816
- };
817
- }
818
- function useCommand(opts) {
819
- return {
820
- name: "use",
821
- description: "Switch provider mid-session: /use <provider>",
822
- async run(args) {
823
- const name = args.trim();
824
- if (!name) {
825
- const msg2 = "Usage: /use <provider-name>";
826
- return { message: msg2 };
827
980
  }
828
- opts.onSwitchProvider?.(name);
829
- const msg = `Switched provider to "${name}".`;
830
- return { message: msg };
831
- }
832
- };
833
- }
834
- function modelCommand(opts) {
835
- return {
836
- name: "model",
837
- description: "Switch model mid-session: /model <model>",
838
- async run(args) {
839
- const name = args.trim();
840
- if (!name) {
841
- const msg2 = "Usage: /model <model-name>";
842
- return { message: msg2 };
981
+ const skill = await opts.skillLoader.find(args.trim());
982
+ if (!skill) {
983
+ const msg = `Skill "${args.trim()}" not found.`;
984
+ return { message: msg };
843
985
  }
844
- opts.onSwitchModel?.(name);
845
- const msg = `Switched model to "${name}".`;
846
- return { message: msg };
986
+ const body = await opts.skillLoader.readBody(skill.name);
987
+ return { message: body };
847
988
  }
848
989
  };
849
990
  }
@@ -901,6 +1042,102 @@ function exitCommand(opts) {
901
1042
  }
902
1043
  };
903
1044
  }
1045
+ function metricsCommand(opts) {
1046
+ return {
1047
+ name: "metrics",
1048
+ description: "Show metrics snapshot (requires --metrics flag).",
1049
+ async run() {
1050
+ if (!opts.metricsSink) {
1051
+ return { message: "Metrics not enabled. Restart with --metrics to collect." };
1052
+ }
1053
+ const snap = opts.metricsSink.snapshot();
1054
+ if (snap.series.length === 0) {
1055
+ return { message: "No metrics recorded yet." };
1056
+ }
1057
+ const lines = [];
1058
+ const byName = /* @__PURE__ */ new Map();
1059
+ for (const s of snap.series) {
1060
+ const bucket = byName.get(s.name) ?? [];
1061
+ bucket.push(s);
1062
+ byName.set(s.name, bucket);
1063
+ }
1064
+ for (const [name, series] of [...byName.entries()].sort()) {
1065
+ lines.push(color.dim(`# ${name}`));
1066
+ for (const s of series) {
1067
+ const labels = Object.entries(s.labels).map(([k, v]) => `${k}=${v}`).join(" ");
1068
+ const labelStr = labels ? color.dim(` {${labels}}`) : "";
1069
+ if (s.type === "histogram") {
1070
+ lines.push(
1071
+ ` count=${s.values.count} sum=${s.values.sum} min=${s.values.min} max=${s.values.max} p50=${s.values.p50} p95=${s.values.p95} p99=${s.values.p99}${labelStr}`
1072
+ );
1073
+ } else {
1074
+ lines.push(` ${s.values.value}${labelStr}`);
1075
+ }
1076
+ }
1077
+ }
1078
+ const msg = lines.join("\n");
1079
+ return { message: msg };
1080
+ }
1081
+ };
1082
+ }
1083
+ function healthCommand(opts) {
1084
+ return {
1085
+ name: "health",
1086
+ description: "Run health checks (requires --metrics flag).",
1087
+ async run() {
1088
+ if (!opts.healthRegistry) {
1089
+ return { message: "Health checks not enabled. Restart with --metrics." };
1090
+ }
1091
+ const result = await opts.healthRegistry.run();
1092
+ const lines = [
1093
+ `${statusIcon(result.status)} overall: ${result.status}`,
1094
+ ...result.checks.map((c) => {
1095
+ const detail = c.detail ? color.dim(` \u2014 ${c.detail}`) : "";
1096
+ return ` ${statusIcon(c.status)} ${c.name}: ${c.status}${detail}`;
1097
+ })
1098
+ ];
1099
+ return { message: lines.join("\n") };
1100
+ }
1101
+ };
1102
+ }
1103
+ function statusIcon(status) {
1104
+ if (status === "healthy") return color.green("\u25CF");
1105
+ if (status === "degraded") return color.yellow("\u25CF");
1106
+ return color.red("\u25CF");
1107
+ }
1108
+ function spawnCommand(opts) {
1109
+ return {
1110
+ name: "spawn",
1111
+ description: "Spawn an isolated subagent to handle a task. Usage: /spawn <task description>",
1112
+ async run(args) {
1113
+ const description = args.trim();
1114
+ if (!description) return { message: "Usage: /spawn <task description>" };
1115
+ if (!opts.onSpawn) {
1116
+ return { message: "Multi-agent is not enabled in this session." };
1117
+ }
1118
+ try {
1119
+ const summary = await opts.onSpawn(description);
1120
+ return { message: summary };
1121
+ } catch (err) {
1122
+ return {
1123
+ message: `Spawn failed: ${err instanceof Error ? err.message : String(err)}`
1124
+ };
1125
+ }
1126
+ }
1127
+ };
1128
+ }
1129
+ function agentsCommand(opts) {
1130
+ return {
1131
+ name: "agents",
1132
+ description: "Show status of spawned subagents (pending + completed tasks).",
1133
+ async run() {
1134
+ if (!opts.onAgents) {
1135
+ return { message: "Multi-agent is not enabled in this session." };
1136
+ }
1137
+ return { message: opts.onAgents() };
1138
+ }
1139
+ };
1140
+ }
904
1141
 
905
1142
  // src/pre-launch.ts
906
1143
  var MANIFESTS = [
@@ -917,13 +1154,13 @@ var MANIFESTS = [
917
1154
  ];
918
1155
  async function detectProjectKind(projectRoot) {
919
1156
  try {
920
- await fs2.access(path2.join(projectRoot, ".wrongstack", "AGENTS.md"));
1157
+ await fs6.access(path5.join(projectRoot, ".wrongstack", "AGENTS.md"));
921
1158
  return "initialized";
922
1159
  } catch {
923
1160
  }
924
1161
  for (const m of MANIFESTS) {
925
1162
  try {
926
- await fs2.access(path2.join(projectRoot, m));
1163
+ await fs6.access(path5.join(projectRoot, m));
927
1164
  return "project";
928
1165
  } catch {
929
1166
  }
@@ -931,12 +1168,12 @@ async function detectProjectKind(projectRoot) {
931
1168
  return "empty";
932
1169
  }
933
1170
  async function scaffoldAgentsMd(projectRoot) {
934
- const dir = path2.join(projectRoot, ".wrongstack");
935
- const file = path2.join(dir, "AGENTS.md");
1171
+ const dir = path5.join(projectRoot, ".wrongstack");
1172
+ const file = path5.join(dir, "AGENTS.md");
936
1173
  const facts = await detectProjectFacts(projectRoot);
937
1174
  const body = renderAgentsTemplate(facts);
938
- await fs2.mkdir(dir, { recursive: true });
939
- await fs2.writeFile(file, body, "utf8");
1175
+ await fs6.mkdir(dir, { recursive: true });
1176
+ await fs6.writeFile(file, body, "utf8");
940
1177
  return file;
941
1178
  }
942
1179
  async function runProjectCheck(opts) {
@@ -945,7 +1182,7 @@ async function runProjectCheck(opts) {
945
1182
  if (kind === "initialized") {
946
1183
  renderer.write(
947
1184
  `
948
- ${color.green("\u2713")} Project initialized ${color.dim(`(${path2.join(projectRoot, ".wrongstack", "AGENTS.md")})`)}
1185
+ ${color.green("\u2713")} Project initialized ${color.dim(`(${path5.join(projectRoot, ".wrongstack", "AGENTS.md")})`)}
949
1186
  `
950
1187
  );
951
1188
  return true;
@@ -1214,14 +1451,14 @@ function summarize(value, name) {
1214
1451
  if (typeof v === "object" && v !== null) {
1215
1452
  const o = v;
1216
1453
  if (name === "edit") {
1217
- const path6 = typeof o["path"] === "string" ? o["path"] : "";
1454
+ const path7 = typeof o["path"] === "string" ? o["path"] : "";
1218
1455
  const reps = typeof o["replacements"] === "number" ? o["replacements"] : 0;
1219
- return `${path6} ${reps} replacement${reps === 1 ? "" : "s"}`.trim();
1456
+ return `${path7} ${reps} replacement${reps === 1 ? "" : "s"}`.trim();
1220
1457
  }
1221
1458
  if (name === "write") {
1222
- const path6 = typeof o["path"] === "string" ? o["path"] : "";
1459
+ const path7 = typeof o["path"] === "string" ? o["path"] : "";
1223
1460
  const bytes = typeof o["bytes"] === "number" ? o["bytes"] : void 0;
1224
- return bytes !== void 0 ? `${path6} ${bytes}B` : path6;
1461
+ return bytes !== void 0 ? `${path7} ${bytes}B` : path7;
1225
1462
  }
1226
1463
  if (typeof o["count"] === "number") {
1227
1464
  return `${o["count"]} match${o["count"] === 1 ? "" : "es"}`;
@@ -1229,6 +1466,37 @@ function summarize(value, name) {
1229
1466
  }
1230
1467
  return "";
1231
1468
  }
1469
+ var req = createRequire(import.meta.url);
1470
+ function readOwnVersion() {
1471
+ const candidates = ["../package.json", "../../package.json"];
1472
+ for (const rel of candidates) {
1473
+ try {
1474
+ const pkg = req(rel);
1475
+ if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
1476
+ } catch {
1477
+ }
1478
+ }
1479
+ return "dev";
1480
+ }
1481
+ var CLI_VERSION = readOwnVersion();
1482
+ var API_VERSION = "0.0.0";
1483
+ try {
1484
+ const corePkg = req("@wrongstack/core/package.json");
1485
+ if (corePkg.wrongstackApiVersion) API_VERSION = corePkg.wrongstackApiVersion;
1486
+ } catch {
1487
+ }
1488
+
1489
+ // src/utils.ts
1490
+ function fmtTok(n) {
1491
+ if (n < 1e3) return String(n);
1492
+ if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
1493
+ return `${(n / 1e6).toFixed(1)}M`;
1494
+ }
1495
+ function patchConfig(base, patch) {
1496
+ return Object.freeze({ ...base, ...patch });
1497
+ }
1498
+
1499
+ // src/repl.ts
1232
1500
  async function runRepl(opts) {
1233
1501
  if (opts.banner !== false) printBanner(opts.renderer);
1234
1502
  let activeCtrl;
@@ -1289,9 +1557,13 @@ async function runRepl(opts) {
1289
1557
  if (result.status === "aborted") {
1290
1558
  opts.renderer.writeWarning("Aborted.");
1291
1559
  } else if (result.status === "failed") {
1292
- opts.renderer.writeError(
1293
- `Failed: ${result.error instanceof Error ? result.error.message : String(result.error)}`
1294
- );
1560
+ const err = result.error;
1561
+ if (err) {
1562
+ const tag = err.recoverable ? " (recoverable)" : "";
1563
+ opts.renderer.writeError(`Failed [${err.severity}]${tag}: ${err.describe()}`);
1564
+ } else {
1565
+ opts.renderer.writeError("Failed.");
1566
+ }
1295
1567
  } else if (result.status === "max_iterations") {
1296
1568
  opts.renderer.writeWarning(`Hit max iterations (${result.iterations}).`);
1297
1569
  }
@@ -1338,11 +1610,6 @@ async function readPossiblyMultiline(opts) {
1338
1610
  }
1339
1611
  return buf;
1340
1612
  }
1341
- function fmtTok(n) {
1342
- if (n < 1e3) return String(n);
1343
- if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
1344
- return `${(n / 1e6).toFixed(1)}M`;
1345
- }
1346
1613
  var FILLED = "\u2588";
1347
1614
  var EMPTY = "\u2591";
1348
1615
  function renderContextChip(used, max) {
@@ -1359,7 +1626,7 @@ function renderProgress(ratio, width) {
1359
1626
  }
1360
1627
  function printBanner(renderer) {
1361
1628
  const lines = [
1362
- theme.primary(theme.bold("WrongStack")) + color.dim(" v0.0.1"),
1629
+ theme.primary(theme.bold("WrongStack")) + color.dim(` v${CLI_VERSION}`),
1363
1630
  color.dim("Built on the wrong stack. Shipped anyway."),
1364
1631
  color.dim("Type /help for commands, /exit to quit."),
1365
1632
  ""
@@ -1401,11 +1668,11 @@ var SessionStats = class {
1401
1668
  if (e.name === "bash") this.bashCommands++;
1402
1669
  else if (e.name === "fetch") this.fetches++;
1403
1670
  if (!e.ok) return;
1404
- const path6 = typeof input?.path === "string" ? input.path : void 0;
1405
- if (e.name === "read" && path6) this.readPaths.add(path6);
1406
- else if (e.name === "edit" && path6) this.editedPaths.add(path6);
1407
- else if (e.name === "write" && path6) {
1408
- this.writtenPaths.add(path6);
1671
+ const path7 = typeof input?.path === "string" ? input.path : void 0;
1672
+ if (e.name === "read" && path7) this.readPaths.add(path7);
1673
+ else if (e.name === "edit" && path7) this.editedPaths.add(path7);
1674
+ else if (e.name === "write" && path7) {
1675
+ this.writtenPaths.add(path7);
1409
1676
  const content = typeof input?.content === "string" ? input.content : "";
1410
1677
  this.bytesWritten += Buffer.byteLength(content, "utf8");
1411
1678
  }
@@ -1414,8 +1681,15 @@ var SessionStats = class {
1414
1681
  hasActivity() {
1415
1682
  return this.apiRequests > 0 || this.iterations > 0 || this.toolStats.size > 0 || this.tokenCounter.total().input > 0;
1416
1683
  }
1417
- render(renderer) {
1418
- if (!this.hasActivity()) return;
1684
+ /**
1685
+ * Build the report string. Returns null when there's no recorded
1686
+ * activity yet — caller decides whether to emit a placeholder or stay
1687
+ * silent. Splitting `format()` out of `render()` lets the TUI's slash
1688
+ * dispatcher take the string and turn it into a history entry, while
1689
+ * REPL keeps the old direct-write path.
1690
+ */
1691
+ format() {
1692
+ if (!this.hasActivity()) return null;
1419
1693
  const u = this.tokenCounter.total();
1420
1694
  const cost = this.tokenCounter.estimateCost();
1421
1695
  const elapsedSec = ((Date.now() - this.startedAt) / 1e3).toFixed(1);
@@ -1430,12 +1704,12 @@ var SessionStats = class {
1430
1704
  lines.push(` Errors: ${color.yellow(String(this.errors))}`);
1431
1705
  }
1432
1706
  lines.push("");
1433
- lines.push(` Tokens: in ${fmtTok2(u.input)} out ${fmtTok2(u.output)}${u.cacheRead ? ` cacheR ${fmtTok2(u.cacheRead)}` : ""}${u.cacheWrite ? ` cacheW ${fmtTok2(u.cacheWrite)}` : ""}`);
1707
+ lines.push(` Tokens: in ${fmtTok(u.input)} out ${fmtTok(u.output)}${u.cacheRead ? ` cacheR ${fmtTok(u.cacheRead)}` : ""}${u.cacheWrite ? ` cacheW ${fmtTok(u.cacheWrite)}` : ""}`);
1434
1708
  const cache = this.tokenCounter.cacheStats();
1435
1709
  if (cache.readTokens > 0 || cache.writeTokens > 0) {
1436
1710
  const pct = (cache.hitRatio * 100).toFixed(1);
1437
1711
  lines.push(
1438
- ` Prompt cache: ${pct}% hit ${color.dim(`(${fmtTok2(cache.readTokens)} read / ${fmtTok2(cache.writeTokens)} write)`)}`
1712
+ ` Prompt cache: ${pct}% hit ${color.dim(`(${fmtTok(cache.readTokens)} read / ${fmtTok(cache.writeTokens)} write)`)}`
1439
1713
  );
1440
1714
  }
1441
1715
  if (cost.total > 0) {
@@ -1476,15 +1750,15 @@ var SessionStats = class {
1476
1750
  if (this.fetches > 0) lines.push(` Web fetches: ${this.fetches}`);
1477
1751
  }
1478
1752
  lines.push("");
1479
- renderer.write(`${lines.join("\n")}
1753
+ return lines.join("\n");
1754
+ }
1755
+ render(renderer) {
1756
+ const text = this.format();
1757
+ if (text === null) return;
1758
+ renderer.write(`${text}
1480
1759
  `);
1481
1760
  }
1482
1761
  };
1483
- function fmtTok2(n) {
1484
- if (n < 1e3) return String(n);
1485
- if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
1486
- return `${(n / 1e6).toFixed(1)}M`;
1487
- }
1488
1762
  function samplePaths(set) {
1489
1763
  const arr = [...set];
1490
1764
  if (arr.length <= 2) return arr.join(", ");
@@ -1555,7 +1829,7 @@ function renderContextChip2(ctx) {
1555
1829
  const pct = Math.round(ratio * 100);
1556
1830
  const chipColor = ratio >= 0.85 ? color.red : ratio >= 0.65 ? color.yellow : color.cyan;
1557
1831
  const bar = renderProgress2(ratio, 8);
1558
- return color.dim("ctx ") + chipColor(bar) + chipColor(` ${pct}%`) + color.dim(` (${fmtTok3(ctx.used)}/${fmtTok3(ctx.max)})`);
1832
+ return color.dim("ctx ") + chipColor(bar) + chipColor(` ${pct}%`) + color.dim(` (${fmtTok(ctx.used)}/${fmtTok(ctx.max)})`);
1559
1833
  }
1560
1834
  function renderProgress2(ratio, width) {
1561
1835
  const clamped = Math.max(0, Math.min(1, ratio));
@@ -1563,92 +1837,888 @@ function renderProgress2(ratio, width) {
1563
1837
  const capped = Math.min(width, filled);
1564
1838
  return FILLED2.repeat(capped) + EMPTY2.repeat(width - capped);
1565
1839
  }
1566
- function fmtTok3(n) {
1567
- if (n < 1e3) return String(n);
1568
- if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
1569
- return `${(n / 1e6).toFixed(1)}M`;
1840
+ async function bootConfig(flags) {
1841
+ const cwd = typeof flags["cwd"] === "string" ? path5.resolve(flags["cwd"]) : process.cwd();
1842
+ const pathResolver = new DefaultPathResolver(cwd);
1843
+ const projectRoot = pathResolver.projectRoot;
1844
+ const userHome = os3.homedir();
1845
+ const wpaths = resolveWstackPaths({ projectRoot, userHome });
1846
+ await ensureProjectMeta(wpaths, projectRoot);
1847
+ const vault = new DefaultSecretVault({ keyFile: wpaths.secretsKey });
1848
+ for (const file of [wpaths.globalConfig, wpaths.projectLocalConfig]) {
1849
+ try {
1850
+ const { migrated } = await migratePlaintextSecrets(file, vault);
1851
+ if (migrated > 0) {
1852
+ process.stderr.write(`[wstack] Encrypted ${migrated} plaintext secret(s) in ${file}
1853
+ `);
1854
+ }
1855
+ } catch {
1856
+ }
1857
+ }
1858
+ const configLoader = new DefaultConfigLoader({ paths: wpaths, vault });
1859
+ const config = await configLoader.load({ cliFlags: flagsToConfigPatch(flags) });
1860
+ return {
1861
+ paths: { cwd, projectRoot, userHome, wpaths, pathResolver },
1862
+ config,
1863
+ vault
1864
+ };
1570
1865
  }
1571
- var subcommands = {
1572
- init: initCmd,
1573
- auth: authCmd,
1574
- // `resume <id>` is special-cased in src/index.ts: it's lifted into
1575
- // `--resume <id>` so the normal REPL bootstrap runs with a pre-loaded
1576
- // session. There is no standalone subcommand handler.
1577
- sessions: sessionsCmd,
1578
- config: configCmd,
1579
- tools: toolsCmd,
1580
- skills: skillsCmd,
1581
- providers: providersCmd,
1582
- models: modelsCmd,
1583
- mcp: mcpCmd,
1584
- plugin: pluginCmd,
1585
- diag: diagCmd,
1586
- usage: usageCmd,
1587
- version: versionCmd,
1588
- help: helpCmd,
1589
- projects: projectsCmd
1590
- };
1591
- async function authCmd(args, deps) {
1592
- const flags = parseAuthFlags(args);
1593
- let providerId = flags.positional[0];
1594
- if (!providerId) {
1595
- providerId = (await deps.reader.readLine("Provider id: ")).trim();
1866
+ function flagsToConfigPatch(flags) {
1867
+ const patch = {};
1868
+ if (typeof flags["provider"] === "string") patch.provider = flags["provider"];
1869
+ if (typeof flags["model"] === "string") patch.model = flags["model"];
1870
+ if (typeof flags["cwd"] === "string") patch.cwd = flags["cwd"];
1871
+ if (typeof flags["log-level"] === "string") {
1872
+ patch.log = { level: flags["log-level"] };
1873
+ } else if (flags["verbose"]) {
1874
+ patch.log = { level: "debug" };
1875
+ } else if (flags["trace"]) {
1876
+ patch.log = { level: "trace" };
1596
1877
  }
1597
- if (!providerId) {
1598
- deps.renderer.writeError("Provider id is required.");
1599
- return 1;
1878
+ if (flags["yolo"]) patch.yolo = true;
1879
+ if (flags["no-features"]) {
1880
+ patch.features = {
1881
+ mcp: false,
1882
+ plugins: false,
1883
+ memory: false,
1884
+ modelsRegistry: false,
1885
+ skills: false
1886
+ };
1600
1887
  }
1601
- let family = flags.family;
1602
- let baseUrl = flags.baseUrl;
1603
- let envVars = flags.envVars;
1888
+ return patch;
1889
+ }
1890
+ async function ensureProjectMeta(paths, projectRoot) {
1604
1891
  try {
1605
- const known = await deps.modelsRegistry.getProvider(providerId);
1606
- if (known) {
1607
- if (!family) family = known.family;
1608
- if (!baseUrl) baseUrl = known.apiBase;
1609
- if (!envVars) envVars = known.envVars;
1610
- }
1892
+ await fs6.mkdir(paths.projectDir, { recursive: true });
1893
+ const meta = {
1894
+ hash: paths.projectHash,
1895
+ root: projectRoot,
1896
+ lastSeen: (/* @__PURE__ */ new Date()).toISOString()
1897
+ };
1898
+ await fs6.writeFile(paths.projectMeta, JSON.stringify(meta, null, 2));
1611
1899
  } catch {
1612
1900
  }
1613
- if (!family) {
1614
- deps.renderer.writeError(
1615
- `Provider "${providerId}" not in catalog. Pass --family <anthropic|openai|openai-compatible|google> to register it manually.`
1901
+ }
1902
+ var MultiAgentHost = class {
1903
+ constructor(deps) {
1904
+ this.deps = deps;
1905
+ }
1906
+ deps;
1907
+ coordinator;
1908
+ pending = /* @__PURE__ */ new Map();
1909
+ results = [];
1910
+ async ensureCoordinator() {
1911
+ if (this.coordinator) return this.coordinator;
1912
+ const config = this.deps.configStore.get();
1913
+ const factory = async (subCfg) => {
1914
+ const events = new EventBus();
1915
+ const provider = await this.buildSubagentProvider(config);
1916
+ const baseSystem = await this.deps.systemPromptBuilder.build({
1917
+ cwd: this.deps.cwd,
1918
+ projectRoot: this.deps.projectRoot,
1919
+ tools: this.filterTools(subCfg.tools),
1920
+ model: subCfg.model ?? config.model,
1921
+ provider: config.provider
1922
+ });
1923
+ const parentSession = this.deps.session;
1924
+ const subSession = {
1925
+ id: parentSession.id,
1926
+ append: (ev) => parentSession.append({ ...ev })
1927
+ };
1928
+ const ctx = new Context({
1929
+ systemPrompt: baseSystem,
1930
+ provider,
1931
+ session: subSession,
1932
+ signal: new AbortController().signal,
1933
+ tokenCounter: this.deps.tokenCounter,
1934
+ cwd: this.deps.cwd,
1935
+ projectRoot: this.deps.projectRoot,
1936
+ model: subCfg.model ?? config.model,
1937
+ tools: this.filterTools(subCfg.tools)
1938
+ });
1939
+ const agent = new Agent({
1940
+ container: this.deps.container,
1941
+ tools: this.subagentToolRegistry(subCfg.tools),
1942
+ providers: this.deps.providerRegistry,
1943
+ events,
1944
+ pipelines: createDefaultPipelines(),
1945
+ context: ctx
1946
+ });
1947
+ return { agent, events };
1948
+ };
1949
+ const runner = makeAgentSubagentRunner({ factory });
1950
+ this.coordinator = new DefaultMultiAgentCoordinator(
1951
+ {
1952
+ coordinatorId: randomUUID(),
1953
+ doneCondition: { type: "all_tasks_done" },
1954
+ maxConcurrent: 2,
1955
+ defaultBudget: { maxToolCalls: 20, maxIterations: 20, timeoutMs: 12e4 }
1956
+ },
1957
+ { runner }
1616
1958
  );
1617
- return 1;
1959
+ this.coordinator.on(
1960
+ "task.completed",
1961
+ ({ task, result }) => {
1962
+ this.results.push(result);
1963
+ this.pending.delete(task.id);
1964
+ }
1965
+ );
1966
+ return this.coordinator;
1618
1967
  }
1619
- const apiKey = (await deps.reader.readSecret(
1620
- `API key for ${providerId} (hidden, stored encrypted in ${deps.paths.globalConfig}): `
1621
- )).trim();
1622
- if (!apiKey) {
1623
- deps.renderer.writeError("No key entered. Nothing saved.");
1624
- return 1;
1968
+ async buildSubagentProvider(config) {
1969
+ const newCfg = config.providers?.[config.provider] ?? {
1970
+ type: config.provider,
1971
+ apiKey: config.apiKey,
1972
+ baseUrl: config.baseUrl
1973
+ };
1974
+ return makeProviderFromConfig(config.provider, {
1975
+ ...newCfg,
1976
+ type: config.provider
1977
+ });
1625
1978
  }
1626
- const patch = {
1627
- providers: {
1628
- [providerId]: {
1629
- type: providerId,
1630
- apiKey,
1631
- family,
1632
- ...baseUrl ? { baseUrl } : {},
1633
- ...envVars && envVars.length > 0 ? { envVars } : {}
1634
- }
1979
+ /** Returns a tool slice for the subagent — full set unless restricted. */
1980
+ filterTools(allow) {
1981
+ const all = this.deps.toolRegistry.list();
1982
+ if (!allow || allow.length === 0) return all;
1983
+ const allowSet = new Set(allow);
1984
+ return all.filter((t) => allowSet.has(t.name));
1985
+ }
1986
+ subagentToolRegistry(allow) {
1987
+ if (!allow || allow.length === 0) return this.deps.toolRegistry;
1988
+ const cloneCtor = this.deps.toolRegistry.constructor;
1989
+ const sub = new cloneCtor();
1990
+ for (const t of this.filterTools(allow)) sub.register(t);
1991
+ return sub;
1992
+ }
1993
+ /** Spawn a fresh subagent and assign a single task. Returns task id. */
1994
+ async spawn(description) {
1995
+ const coord = await this.ensureCoordinator();
1996
+ const spawned = await coord.spawn({
1997
+ name: "adhoc",
1998
+ role: "general",
1999
+ maxToolCalls: 20,
2000
+ maxIterations: 20
2001
+ });
2002
+ const taskId = randomUUID();
2003
+ this.pending.set(taskId, { description, subagentId: spawned.subagentId });
2004
+ await coord.assign({
2005
+ id: taskId,
2006
+ description,
2007
+ subagentId: spawned.subagentId,
2008
+ maxToolCalls: 20
2009
+ });
2010
+ return { subagentId: spawned.subagentId, taskId };
2011
+ }
2012
+ status() {
2013
+ const pending = Array.from(this.pending.entries()).map(([taskId, v]) => ({
2014
+ taskId,
2015
+ description: v.description,
2016
+ subagentId: v.subagentId
2017
+ }));
2018
+ const summary = !this.coordinator ? "No subagents have been spawned." : `${pending.length} pending, ${this.results.length} completed.`;
2019
+ return { pending, completed: this.results, summary };
2020
+ }
2021
+ async stopAll() {
2022
+ if (this.coordinator) {
2023
+ await this.coordinator.stopAll();
1635
2024
  }
1636
- };
1637
- try {
1638
- await rewriteConfigEncrypted(deps.paths.globalConfig, deps.vault, patch);
1639
- deps.renderer.writeInfo(`Stored encrypted key for ${providerId}.`);
1640
- deps.renderer.writeInfo(`Use: wstack --provider ${providerId} "<task>"`);
1641
- return 0;
1642
- } catch (err) {
1643
- deps.renderer.writeError(`auth: ${err instanceof Error ? err.message : String(err)}`);
1644
- return 1;
1645
2025
  }
1646
- }
1647
- function parseAuthFlags(args) {
2026
+ };
2027
+ async function runAuthMenu(deps) {
2028
+ for (; ; ) {
2029
+ const providers = await loadProviders(deps);
2030
+ renderTopMenu(deps.renderer, providers);
2031
+ const ids = Object.keys(providers).sort();
2032
+ const choice = (await deps.reader.readLine(`
2033
+ ${color.amber("?")} Pick: `)).trim().toLowerCase();
2034
+ if (!choice || choice === "q" || choice === "quit" || choice === "exit") {
2035
+ deps.renderer.write(color.dim("Done.\n"));
2036
+ return 0;
2037
+ }
2038
+ if (choice === "a" || choice === "add") {
2039
+ await addForNewProvider(deps);
2040
+ continue;
2041
+ }
2042
+ if (choice === "c" || choice === "custom") {
2043
+ await addCustomProvider(deps);
2044
+ continue;
2045
+ }
2046
+ const idx = Number.parseInt(choice, 10);
2047
+ if (!Number.isNaN(idx) && idx >= 1 && idx <= ids.length) {
2048
+ const pid = ids[idx - 1];
2049
+ await manageProvider(pid, deps);
2050
+ continue;
2051
+ }
2052
+ const byId = ids.find((id) => id.toLowerCase() === choice);
2053
+ if (byId) {
2054
+ await manageProvider(byId, deps);
2055
+ continue;
2056
+ }
2057
+ deps.renderer.writeError(`Unknown selection: "${choice}"`);
2058
+ }
2059
+ }
2060
+ function renderTopMenu(renderer, providers) {
2061
+ renderer.write(`
2062
+ ${color.bold("WrongStack")} ${color.dim("\u2014 API keys")}
2063
+
2064
+ `);
2065
+ const ids = Object.keys(providers).sort();
2066
+ if (ids.length === 0) {
2067
+ renderer.write(color.dim(" No providers configured yet.\n"));
2068
+ } else {
2069
+ renderer.write(` ${color.dim("Saved providers:")}
2070
+ `);
2071
+ let idx = 1;
2072
+ for (const id of ids) {
2073
+ const cfg = providers[id];
2074
+ const keys = normalizeKeys(cfg);
2075
+ const active = activeLabel(cfg, keys);
2076
+ const summary = keys.length === 0 ? color.dim("(no keys)") : keys.length === 1 ? maskedKey(keys[0].apiKey) : `${color.dim(`${keys.length} keys`)} ${color.dim("active:")} ${color.bold(active ?? "?")} ${maskedKey(keys.find((k) => k.label === active)?.apiKey ?? keys[0].apiKey)}`;
2077
+ const fam = cfg.family ? color.dim(`[${cfg.family}]`) : "";
2078
+ const aliasHint = cfg.type && cfg.type !== id ? color.dim(`\u2192 ${cfg.type}`) : "";
2079
+ renderer.write(
2080
+ ` ${color.dim(`${idx}.`.padStart(4))} ${id.padEnd(22)} ${fam} ${aliasHint} ${summary}
2081
+ `
2082
+ );
2083
+ idx++;
2084
+ }
2085
+ }
2086
+ renderer.write(`
2087
+ ${color.dim("Actions:")}
2088
+ `);
2089
+ renderer.write(` ${color.bold("a")} Add key for a new provider (from catalog)
2090
+ `);
2091
+ renderer.write(` ${color.bold("c")} Add custom provider (type + family + baseUrl)
2092
+ `);
2093
+ renderer.write(` ${color.bold("q")} Quit
2094
+ `);
2095
+ if (ids.length > 0) {
2096
+ renderer.write(color.dim(`
2097
+ Pick a number to manage that provider's keys.
2098
+ `));
2099
+ }
2100
+ }
2101
+ async function manageProvider(providerId, deps) {
2102
+ for (; ; ) {
2103
+ const providers = await loadProviders(deps);
2104
+ const cfg = providers[providerId];
2105
+ if (!cfg) {
2106
+ deps.renderer.writeError(`Provider "${providerId}" no longer in config.`);
2107
+ return;
2108
+ }
2109
+ const keys = normalizeKeys(cfg);
2110
+ const active = activeLabel(cfg, keys);
2111
+ deps.renderer.write(`
2112
+ ${color.bold(providerId)} ${cfg.family ? color.dim(`[${cfg.family}]`) : color.amber("[no family]")}
2113
+ `);
2114
+ deps.renderer.write(
2115
+ color.dim(` type: ${cfg.type ?? providerId}
2116
+ `) + color.dim(` family: ${cfg.family ?? "(unset \u2192 resolved from models.dev when type matches)"}
2117
+ `) + color.dim(` baseUrl: ${cfg.baseUrl ?? "(unset \u2192 catalog default)"}
2118
+ `)
2119
+ );
2120
+ if (cfg.envVars && cfg.envVars.length > 0) {
2121
+ deps.renderer.write(color.dim(` envVars: ${cfg.envVars.join(", ")}
2122
+ `));
2123
+ }
2124
+ if (cfg.models && cfg.models.length > 0) {
2125
+ deps.renderer.write(color.dim(` models: ${cfg.models.join(", ")}
2126
+ `));
2127
+ }
2128
+ if (keys.length === 0) {
2129
+ deps.renderer.write(color.dim(" (no keys saved)\n"));
2130
+ } else {
2131
+ for (let i = 0; i < keys.length; i++) {
2132
+ const k = keys[i];
2133
+ const marker = k.label === active ? color.green("\u25CF") : color.dim("\u25CB");
2134
+ deps.renderer.write(
2135
+ ` ${color.dim(`${i + 1}.`.padStart(4))} ${marker} ${k.label.padEnd(20)} ${maskedKey(k.apiKey)} ${color.dim(k.createdAt)}
2136
+ `
2137
+ );
2138
+ }
2139
+ }
2140
+ deps.renderer.write(`
2141
+ ${color.dim("Actions:")}
2142
+ `);
2143
+ deps.renderer.write(` ${color.bold("a")} Add another key
2144
+ `);
2145
+ if (keys.length > 0) {
2146
+ deps.renderer.write(` ${color.bold("u")} <n> Update key <n>
2147
+ `);
2148
+ deps.renderer.write(` ${color.bold("d")} <n> Delete key <n>
2149
+ `);
2150
+ deps.renderer.write(` ${color.bold("s")} <n> Set key <n> as active
2151
+ `);
2152
+ }
2153
+ deps.renderer.write(` ${color.bold("f")} Edit family
2154
+ `);
2155
+ deps.renderer.write(` ${color.bold("B")} Edit baseUrl
2156
+ `);
2157
+ deps.renderer.write(` ${color.bold("m")} Edit visible model list
2158
+ `);
2159
+ deps.renderer.write(` ${color.bold("x")} Remove this provider entirely
2160
+ `);
2161
+ deps.renderer.write(` ${color.bold("b")} Back
2162
+ `);
2163
+ const raw = (await deps.reader.readLine(`
2164
+ ${color.amber("?")} ${providerId} > `)).trim();
2165
+ if (!raw || raw === "b" || raw === "back") return;
2166
+ const [verb, argRaw] = raw.split(/\s+/, 2);
2167
+ const arg = argRaw ? Number.parseInt(argRaw, 10) : Number.NaN;
2168
+ if (verb === "a" || verb === "add") {
2169
+ await addKeyForProvider(providerId, deps, cfg);
2170
+ continue;
2171
+ }
2172
+ if (verb === "x" || verb === "remove") {
2173
+ const confirm = (await deps.reader.readLine(
2174
+ ` ${color.amber("?")} Remove provider "${providerId}" and ${keys.length} key(s)? ${color.dim("[y/N]")} `
2175
+ )).trim().toLowerCase();
2176
+ if (confirm === "y" || confirm === "yes") {
2177
+ await mutateProviders(deps, (all) => {
2178
+ delete all[providerId];
2179
+ });
2180
+ deps.renderer.write(` ${color.green("\u2713")} Removed ${providerId}.
2181
+ `);
2182
+ return;
2183
+ }
2184
+ continue;
2185
+ }
2186
+ if (verb === "u" || verb === "update") {
2187
+ if (!Number.isFinite(arg) || arg < 1 || arg > keys.length) {
2188
+ deps.renderer.writeError(`Usage: u <1-${keys.length}>`);
2189
+ continue;
2190
+ }
2191
+ const target = keys[arg - 1];
2192
+ const newKey = await readKeyInput(deps, `Updated key for ${target.label}`);
2193
+ if (!newKey) continue;
2194
+ await mutateProviders(deps, (all) => {
2195
+ const p = all[providerId];
2196
+ if (!p) return;
2197
+ const list = normalizeKeys(p).map(
2198
+ (k) => k.label === target.label ? { ...k, apiKey: newKey, createdAt: nowIso() } : k
2199
+ );
2200
+ writeKeysBack(p, list);
2201
+ });
2202
+ deps.renderer.write(` ${color.green("\u2713")} Updated ${providerId}/${target.label}.
2203
+ `);
2204
+ continue;
2205
+ }
2206
+ if (verb === "d" || verb === "delete" || verb === "rm") {
2207
+ if (!Number.isFinite(arg) || arg < 1 || arg > keys.length) {
2208
+ deps.renderer.writeError(`Usage: d <1-${keys.length}>`);
2209
+ continue;
2210
+ }
2211
+ const target = keys[arg - 1];
2212
+ const confirm = (await deps.reader.readLine(
2213
+ ` ${color.amber("?")} Delete key "${target.label}" (${maskedKey(target.apiKey)})? ${color.dim("[y/N]")} `
2214
+ )).trim().toLowerCase();
2215
+ if (confirm !== "y" && confirm !== "yes") continue;
2216
+ await mutateProviders(deps, (all) => {
2217
+ const p = all[providerId];
2218
+ if (!p) return;
2219
+ const list = normalizeKeys(p).filter((k) => k.label !== target.label);
2220
+ writeKeysBack(p, list);
2221
+ if (p.activeKey === target.label) {
2222
+ p.activeKey = list[0]?.label;
2223
+ }
2224
+ });
2225
+ deps.renderer.write(` ${color.green("\u2713")} Deleted ${providerId}/${target.label}.
2226
+ `);
2227
+ continue;
2228
+ }
2229
+ if (verb === "f" || verb === "family") {
2230
+ const current = cfg.family ?? "";
2231
+ const ans = (await deps.reader.readLine(
2232
+ ` ${color.amber("?")} Family ${color.dim(`(anthropic | openai | openai-compatible | google, empty = unset, current: ${current || "unset"})`)}: `
2233
+ )).trim();
2234
+ if (ans !== "" && !["anthropic", "openai", "openai-compatible", "google"].includes(ans)) {
2235
+ deps.renderer.writeError(`Invalid family: "${ans}"`);
2236
+ continue;
2237
+ }
2238
+ await mutateProviders(deps, (all) => {
2239
+ const p = all[providerId];
2240
+ if (!p) return;
2241
+ if (ans === "") delete p.family;
2242
+ else p.family = ans;
2243
+ });
2244
+ deps.renderer.write(` ${color.green("\u2713")} family \u2192 ${ans || "(unset)"}
2245
+ `);
2246
+ continue;
2247
+ }
2248
+ if (verb === "B" || verb === "baseurl" || verb === "base-url") {
2249
+ const current = cfg.baseUrl ?? "";
2250
+ const ans = (await deps.reader.readLine(
2251
+ ` ${color.amber("?")} Base URL ${color.dim(`(empty = unset, current: ${current || "unset"})`)}: `
2252
+ )).trim();
2253
+ await mutateProviders(deps, (all) => {
2254
+ const p = all[providerId];
2255
+ if (!p) return;
2256
+ if (ans === "") delete p.baseUrl;
2257
+ else p.baseUrl = ans;
2258
+ });
2259
+ deps.renderer.write(` ${color.green("\u2713")} baseUrl \u2192 ${ans || "(unset)"}
2260
+ `);
2261
+ continue;
2262
+ }
2263
+ if (verb === "m" || verb === "models") {
2264
+ const current = (cfg.models ?? []).join(", ");
2265
+ const ans = (await deps.reader.readLine(
2266
+ ` ${color.amber("?")} Model ids ${color.dim(`(comma-separated, empty = catalog default, current: ${current || "none"})`)}: `
2267
+ )).trim();
2268
+ const list = ans ? ans.split(",").map((s) => s.trim()).filter(Boolean) : [];
2269
+ await mutateProviders(deps, (all) => {
2270
+ const p = all[providerId];
2271
+ if (!p) return;
2272
+ if (list.length === 0) delete p.models;
2273
+ else p.models = list;
2274
+ });
2275
+ deps.renderer.write(` ${color.green("\u2713")} models \u2192 ${list.length === 0 ? "(catalog default)" : list.join(", ")}
2276
+ `);
2277
+ continue;
2278
+ }
2279
+ if (verb === "s" || verb === "set" || verb === "active") {
2280
+ if (!Number.isFinite(arg) || arg < 1 || arg > keys.length) {
2281
+ deps.renderer.writeError(`Usage: s <1-${keys.length}>`);
2282
+ continue;
2283
+ }
2284
+ const target = keys[arg - 1];
2285
+ await mutateProviders(deps, (all) => {
2286
+ const p = all[providerId];
2287
+ if (!p) return;
2288
+ const list = normalizeKeys(p);
2289
+ writeKeysBack(p, list);
2290
+ p.activeKey = target.label;
2291
+ });
2292
+ deps.renderer.write(` ${color.green("\u2713")} Active key for ${providerId} \u2192 ${color.bold(target.label)}.
2293
+ `);
2294
+ continue;
2295
+ }
2296
+ deps.renderer.writeError(`Unknown action: "${raw}"`);
2297
+ }
2298
+ }
2299
+ async function addForNewProvider(deps) {
2300
+ let catalog = [];
2301
+ try {
2302
+ catalog = (await deps.modelsRegistry.listProviders()).filter((p) => p.family !== "unsupported");
2303
+ } catch {
2304
+ deps.renderer.writeWarning("Catalog unavailable \u2014 falling back to manual entry.");
2305
+ }
2306
+ if (catalog.length === 0) {
2307
+ const pid = (await deps.reader.readLine(` ${color.amber("?")} Provider id: `)).trim();
2308
+ if (!pid) return;
2309
+ const fam = (await deps.reader.readLine(
2310
+ ` ${color.amber("?")} Family (anthropic/openai/openai-compatible/google): `
2311
+ )).trim();
2312
+ const baseUrl2 = (await deps.reader.readLine(
2313
+ ` ${color.amber("?")} Base URL ${color.dim("(optional)")}: `
2314
+ )).trim();
2315
+ await addKeyForProvider(pid, deps, {
2316
+ type: pid,
2317
+ family: fam || void 0,
2318
+ ...baseUrl2 ? { baseUrl: baseUrl2 } : {}
2319
+ });
2320
+ return;
2321
+ }
2322
+ const saved = new Set(Object.keys(await loadProviders(deps)));
2323
+ deps.renderer.write(
2324
+ color.dim(` Catalog has ${catalog.length} providers. Filter by name to narrow, or "s" for unsaved-only.
2325
+ `)
2326
+ );
2327
+ const filterRaw = (await deps.reader.readLine(
2328
+ ` ${color.amber("?")} Filter ${color.dim('(substring of id/name, "s" for unsaved-only, empty = all)')}: `
2329
+ )).trim();
2330
+ const filterLc = filterRaw.toLowerCase();
2331
+ const showUnsavedOnly = filterLc === "s" || filterLc === "unsaved";
2332
+ const matches = (p) => {
2333
+ if (showUnsavedOnly) return !saved.has(p.id);
2334
+ if (!filterLc) return true;
2335
+ return p.id.toLowerCase().includes(filterLc) || p.name.toLowerCase().includes(filterLc);
2336
+ };
2337
+ const byFamily = /* @__PURE__ */ new Map();
2338
+ let filteredCount = 0;
2339
+ for (const p of catalog) {
2340
+ if (!matches(p)) continue;
2341
+ filteredCount++;
2342
+ const list = byFamily.get(p.family) ?? [];
2343
+ list.push(p);
2344
+ byFamily.set(p.family, list);
2345
+ }
2346
+ if (filteredCount === 0) {
2347
+ deps.renderer.writeError(
2348
+ `No providers match "${filterRaw}". Try a shorter substring or check \`wstack providers\` for valid ids.`
2349
+ );
2350
+ return;
2351
+ }
2352
+ if (filterRaw && !showUnsavedOnly) {
2353
+ deps.renderer.write(
2354
+ color.dim(` ${filteredCount} match${filteredCount === 1 ? "" : "es"} for "${filterRaw}".
2355
+ `)
2356
+ );
2357
+ }
2358
+ const ordered = [];
2359
+ const familyOrder = ["anthropic", "openai", "google", "openai-compatible"];
2360
+ let idx = 1;
2361
+ deps.renderer.write("\n");
2362
+ for (const fam of familyOrder) {
2363
+ const list = byFamily.get(fam);
2364
+ if (!list || list.length === 0) continue;
2365
+ deps.renderer.write(` ${color.bold(fam)}
2366
+ `);
2367
+ for (const p of list) {
2368
+ const savedMark = saved.has(p.id) ? color.cyan("\u25C9") : color.dim("\u25CB");
2369
+ const env = p.envVars[0] ? color.dim(`[${p.envVars[0]}]`) : "";
2370
+ deps.renderer.write(
2371
+ ` ${color.dim(`${idx}.`.padStart(4))} ${savedMark} ${p.id.padEnd(22)} ${color.dim(p.name)} ${env}
2372
+ `
2373
+ );
2374
+ ordered.push(p);
2375
+ idx++;
2376
+ }
2377
+ }
2378
+ deps.renderer.write(`
2379
+ ${color.dim("\u25C9 already saved \u25CB no key yet")}
2380
+ `);
2381
+ const answer = (await deps.reader.readLine(
2382
+ `
2383
+ ${color.amber("?")} Pick (1-${ordered.length}) or type provider id: `
2384
+ )).trim();
2385
+ if (!answer) return;
2386
+ let chosen;
2387
+ const num = Number.parseInt(answer, 10);
2388
+ if (!Number.isNaN(num) && num >= 1 && num <= ordered.length) {
2389
+ chosen = ordered[num - 1];
2390
+ } else {
2391
+ chosen = ordered.find((p) => p.id.toLowerCase() === answer.toLowerCase()) ?? catalog.find((p) => p.id.toLowerCase() === answer.toLowerCase());
2392
+ }
2393
+ if (!chosen) {
2394
+ deps.renderer.writeError(`No such provider: "${answer}"`);
2395
+ return;
2396
+ }
2397
+ deps.renderer.write(
2398
+ color.dim(`
2399
+ Defaults from models.dev \u2014 press Enter to keep, or type a new value.
2400
+ `)
2401
+ );
2402
+ const famRaw = (await deps.reader.readLine(
2403
+ ` ${color.amber("?")} Family ${color.dim(`[${chosen.family}]`)}: `
2404
+ )).trim();
2405
+ let family = chosen.family;
2406
+ if (famRaw) {
2407
+ if (!["anthropic", "openai", "openai-compatible", "google"].includes(famRaw)) {
2408
+ deps.renderer.writeError(`Invalid family: "${famRaw}" (must be anthropic | openai | openai-compatible | google).`);
2409
+ return;
2410
+ }
2411
+ family = famRaw;
2412
+ }
2413
+ const baseRaw = (await deps.reader.readLine(
2414
+ ` ${color.amber("?")} Base URL ${color.dim(`[${chosen.apiBase ?? "unset"}]`)}: `
2415
+ )).trim();
2416
+ const baseUrl = baseRaw || chosen.apiBase;
2417
+ const providersNow = await loadProviders(deps);
2418
+ let suggestedAlias = chosen.id;
2419
+ if (family !== chosen.family) {
2420
+ let candidate = `${chosen.id}-${family}`;
2421
+ let n = 2;
2422
+ while (providersNow[candidate]) {
2423
+ candidate = `${chosen.id}-${family}-${n}`;
2424
+ n++;
2425
+ }
2426
+ suggestedAlias = candidate;
2427
+ }
2428
+ const aliasRaw = (await deps.reader.readLine(
2429
+ ` ${color.amber("?")} Save under alias ${color.dim(`[${suggestedAlias}]`)} ${color.dim("(used as `--provider <alias>`)")}: `
2430
+ )).trim();
2431
+ const alias = aliasRaw || suggestedAlias;
2432
+ const existing = providersNow[alias];
2433
+ if (existing) {
2434
+ const sameFamily = (existing.family ?? chosen.family) === family;
2435
+ const sameBase = (existing.baseUrl ?? chosen.apiBase) === baseUrl;
2436
+ if (!sameFamily || !sameBase) {
2437
+ deps.renderer.writeError(
2438
+ `Alias "${alias}" already exists with different family/baseUrl.
2439
+ Existing: family=${existing.family ?? "(unset)"}, baseUrl=${existing.baseUrl ?? "(unset)"}
2440
+ New: family=${family}, baseUrl=${baseUrl ?? "(unset)"}
2441
+ Pick a different alias to keep them separate.`
2442
+ );
2443
+ return;
2444
+ }
2445
+ }
2446
+ await addKeyForProvider(alias, deps, {
2447
+ type: chosen.id,
2448
+ family,
2449
+ baseUrl,
2450
+ envVars: chosen.envVars
2451
+ });
2452
+ }
2453
+ async function addCustomProvider(deps) {
2454
+ deps.renderer.write(`
2455
+ ${color.bold("Custom provider")} ${color.dim("\u2014 for local models or proxies not in the models.dev catalog.")}
2456
+ `);
2457
+ const type = (await deps.reader.readLine(
2458
+ ` ${color.amber("?")} Provider id ${color.dim('(e.g. "local-llama", "my-proxy")')}: `
2459
+ )).trim();
2460
+ if (!type) return;
2461
+ const existing = (await loadProviders(deps))[type];
2462
+ if (existing) {
2463
+ deps.renderer.writeWarning(`"${type}" already exists. Pick it from the main menu to edit.`);
2464
+ return;
2465
+ }
2466
+ const familyRaw = (await deps.reader.readLine(
2467
+ ` ${color.amber("?")} Wire family ${color.dim("(anthropic | openai | openai-compatible | google)")}: `
2468
+ )).trim();
2469
+ if (!["anthropic", "openai", "openai-compatible", "google"].includes(familyRaw)) {
2470
+ deps.renderer.writeError(`Invalid family: "${familyRaw}"`);
2471
+ return;
2472
+ }
2473
+ const family = familyRaw;
2474
+ const baseUrl = (await deps.reader.readLine(
2475
+ ` ${color.amber("?")} Base URL ${color.dim("(e.g. http://localhost:11434/v1, leave empty if not needed)")}: `
2476
+ )).trim();
2477
+ const modelsRaw = (await deps.reader.readLine(
2478
+ ` ${color.amber("?")} Model ids ${color.dim("(comma-separated, optional)")}: `
2479
+ )).trim();
2480
+ const models = modelsRaw ? modelsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
2481
+ const envVarsRaw = (await deps.reader.readLine(
2482
+ ` ${color.amber("?")} Env var names ${color.dim("(comma-separated, optional fallback for the key)")}: `
2483
+ )).trim();
2484
+ const envVars = envVarsRaw ? envVarsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
2485
+ await addKeyForProvider(type, deps, {
2486
+ type,
2487
+ family,
2488
+ ...baseUrl ? { baseUrl } : {},
2489
+ ...models ? { models } : {},
2490
+ ...envVars ? { envVars } : {}
2491
+ });
2492
+ }
2493
+ async function addKeyForProvider(providerId, deps, template) {
2494
+ const providers = await loadProviders(deps);
2495
+ const existing = providers[providerId];
2496
+ const existingKeys = existing ? normalizeKeys(existing) : [];
2497
+ const usedLabels = new Set(existingKeys.map((k) => k.label));
2498
+ let defaultLabel = "default";
2499
+ if (usedLabels.has(defaultLabel)) {
2500
+ let n = 2;
2501
+ while (usedLabels.has(`key${n}`)) n++;
2502
+ defaultLabel = `key${n}`;
2503
+ }
2504
+ const labelRaw = (await deps.reader.readLine(
2505
+ ` ${color.amber("?")} Label for this key ${color.dim(`[${defaultLabel}]`)}: `
2506
+ )).trim();
2507
+ const label = labelRaw || defaultLabel;
2508
+ if (usedLabels.has(label)) {
2509
+ deps.renderer.writeError(`Label "${label}" already used for ${providerId}. Use update (u) instead.`);
2510
+ return;
2511
+ }
2512
+ const apiKey = await readKeyInput(deps, `API key for ${providerId}/${label}`);
2513
+ if (!apiKey) {
2514
+ deps.renderer.writeError("No key entered. Nothing saved.");
2515
+ return;
2516
+ }
2517
+ await mutateProviders(deps, (all) => {
2518
+ const existingProv = all[providerId] ?? { type: providerId, ...template };
2519
+ if (!existingProv.type) existingProv.type = providerId;
2520
+ if (!existingProv.family && template.family) existingProv.family = template.family;
2521
+ if (!existingProv.baseUrl && template.baseUrl) existingProv.baseUrl = template.baseUrl;
2522
+ if (!existingProv.envVars && template.envVars) existingProv.envVars = template.envVars;
2523
+ const list = normalizeKeys(existingProv);
2524
+ list.push({ label, apiKey, createdAt: nowIso() });
2525
+ writeKeysBack(existingProv, list);
2526
+ if (!existingProv.activeKey) existingProv.activeKey = label;
2527
+ all[providerId] = existingProv;
2528
+ });
2529
+ deps.renderer.write(
2530
+ ` ${color.green("\u2713")} Saved ${color.bold(providerId)}/${color.bold(label)}. ${color.dim("Use `wstack --provider " + providerId + ' "<task>"` to launch.')}
2531
+ `
2532
+ );
2533
+ }
2534
+ async function runAuthDirect(deps, opts) {
2535
+ const { providerId } = opts;
2536
+ const providers = await loadProviders(deps);
2537
+ const existing = providers[providerId];
2538
+ if (!existing && !opts.family) {
2539
+ let knownFamily;
2540
+ let knownBase;
2541
+ let knownEnv;
2542
+ try {
2543
+ const k = await deps.modelsRegistry.getProvider(providerId);
2544
+ if (k) {
2545
+ knownFamily = k.family;
2546
+ knownBase = k.apiBase;
2547
+ knownEnv = k.envVars;
2548
+ }
2549
+ } catch {
2550
+ }
2551
+ if (!knownFamily || knownFamily === "unsupported") {
2552
+ deps.renderer.writeError(
2553
+ `Provider "${providerId}" not in catalog. Pass --family <anthropic|openai|openai-compatible|google>.`
2554
+ );
2555
+ return 1;
2556
+ }
2557
+ opts.family = knownFamily;
2558
+ opts.baseUrl ??= knownBase;
2559
+ opts.envVars ??= knownEnv;
2560
+ }
2561
+ const usedLabels = new Set(
2562
+ existing ? normalizeKeys(existing).map((k) => k.label) : []
2563
+ );
2564
+ let label = opts.label ?? "default";
2565
+ if (usedLabels.has(label)) {
2566
+ let n = 2;
2567
+ while (usedLabels.has(`${label}-${n}`)) n++;
2568
+ label = `${label}-${n}`;
2569
+ deps.renderer.writeInfo(`Label collided; saving as "${label}".`);
2570
+ }
2571
+ const apiKey = await readKeyInput(deps, `API key for ${providerId}/${label}`);
2572
+ if (!apiKey) return 1;
2573
+ await mutateProviders(deps, (all) => {
2574
+ const p = all[providerId] ?? { type: providerId };
2575
+ if (!p.type) p.type = providerId;
2576
+ if (!p.family && opts.family) p.family = opts.family;
2577
+ if (!p.baseUrl && opts.baseUrl) p.baseUrl = opts.baseUrl;
2578
+ if (!p.envVars && opts.envVars) p.envVars = opts.envVars;
2579
+ const list = normalizeKeys(p);
2580
+ list.push({ label, apiKey, createdAt: nowIso() });
2581
+ writeKeysBack(p, list);
2582
+ if (!p.activeKey) p.activeKey = label;
2583
+ all[providerId] = p;
2584
+ });
2585
+ deps.renderer.writeInfo(`Stored encrypted key for ${providerId} (label "${label}").`);
2586
+ deps.renderer.writeInfo(`Use: wstack --provider ${providerId} "<task>"`);
2587
+ return 0;
2588
+ }
2589
+ async function readKeyInput(deps, intent) {
2590
+ const key = (await deps.reader.readSecret(` ${color.amber("?")} ${intent} ${color.dim("(hidden, paste OK)")}: `)).trim();
2591
+ if (!key) {
2592
+ deps.renderer.writeError("No key entered.");
2593
+ return void 0;
2594
+ }
2595
+ return key;
2596
+ }
2597
+ async function loadProviders(deps) {
2598
+ let raw;
2599
+ try {
2600
+ raw = await fs6.readFile(deps.globalConfigPath, "utf8");
2601
+ } catch {
2602
+ return {};
2603
+ }
2604
+ let parsed = {};
2605
+ try {
2606
+ parsed = JSON.parse(raw);
2607
+ } catch {
2608
+ return {};
2609
+ }
2610
+ const decrypted = decryptConfigSecrets(parsed, deps.vault);
2611
+ return decrypted.providers ?? {};
2612
+ }
2613
+ async function mutateProviders(deps, mutator) {
2614
+ let raw;
2615
+ try {
2616
+ raw = await fs6.readFile(deps.globalConfigPath, "utf8");
2617
+ } catch {
2618
+ raw = "{}";
2619
+ }
2620
+ let parsed;
2621
+ try {
2622
+ parsed = JSON.parse(raw);
2623
+ } catch {
2624
+ parsed = {};
2625
+ }
2626
+ const decrypted = decryptConfigSecrets(parsed, deps.vault);
2627
+ const providers = decrypted.providers ?? {};
2628
+ mutator(providers);
2629
+ decrypted.providers = providers;
2630
+ const encrypted = encryptConfigSecrets(decrypted, deps.vault);
2631
+ await atomicWrite(deps.globalConfigPath, JSON.stringify(encrypted, null, 2), { mode: 384 });
2632
+ }
2633
+ function normalizeKeys(cfg) {
2634
+ if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
2635
+ return cfg.apiKeys.map((k) => ({ ...k }));
2636
+ }
2637
+ if (typeof cfg.apiKey === "string" && cfg.apiKey.length > 0) {
2638
+ return [{ label: "default", apiKey: cfg.apiKey, createdAt: "" }];
2639
+ }
2640
+ return [];
2641
+ }
2642
+ function writeKeysBack(cfg, keys) {
2643
+ if (keys.length === 0) {
2644
+ delete cfg.apiKeys;
2645
+ delete cfg.apiKey;
2646
+ delete cfg.activeKey;
2647
+ return;
2648
+ }
2649
+ cfg.apiKeys = keys;
2650
+ const active = keys.find((k) => k.label === cfg.activeKey) ?? keys[0];
2651
+ cfg.apiKey = active.apiKey;
2652
+ if (!cfg.activeKey || !keys.some((k) => k.label === cfg.activeKey)) {
2653
+ cfg.activeKey = active.label;
2654
+ }
2655
+ }
2656
+ function activeLabel(cfg, keys) {
2657
+ if (cfg.activeKey && keys.some((k) => k.label === cfg.activeKey)) return cfg.activeKey;
2658
+ return keys[0]?.label;
2659
+ }
2660
+ function maskedKey(key) {
2661
+ if (!key) return color.dim("\u2014");
2662
+ if (key.length <= 8) return color.dim("\u2022".repeat(key.length));
2663
+ const head = key.slice(0, 4);
2664
+ const tail = key.slice(-4);
2665
+ return `${color.dim(head + "\u2026")}${tail}`;
2666
+ }
2667
+ function nowIso() {
2668
+ return (/* @__PURE__ */ new Date()).toISOString();
2669
+ }
2670
+
2671
+ // src/subcommands/index.ts
2672
+ var subcommands = {
2673
+ init: initCmd,
2674
+ auth: authCmd,
2675
+ // `resume <id>` is special-cased in src/index.ts: it's lifted into
2676
+ // `--resume <id>` so the normal REPL bootstrap runs with a pre-loaded
2677
+ // session. There is no standalone subcommand handler.
2678
+ sessions: sessionsCmd,
2679
+ config: configCmd,
2680
+ tools: toolsCmd,
2681
+ skills: skillsCmd,
2682
+ providers: providersCmd,
2683
+ models: modelsCmd,
2684
+ mcp: mcpCmd,
2685
+ plugin: pluginCmd,
2686
+ diag: diagCmd,
2687
+ doctor: doctorCmd,
2688
+ export: exportCmd,
2689
+ usage: usageCmd,
2690
+ version: versionCmd,
2691
+ help: helpCmd,
2692
+ projects: projectsCmd
2693
+ };
2694
+ async function authCmd(args, deps) {
2695
+ const flags = parseAuthFlags(args);
2696
+ const menuDeps = {
2697
+ renderer: deps.renderer,
2698
+ reader: deps.reader,
2699
+ modelsRegistry: deps.modelsRegistry,
2700
+ vault: deps.vault,
2701
+ globalConfigPath: deps.paths.globalConfig
2702
+ };
2703
+ if (flags.positional.length === 0) {
2704
+ return runAuthMenu(menuDeps);
2705
+ }
2706
+ return runAuthDirect(menuDeps, {
2707
+ providerId: flags.positional[0],
2708
+ label: flags.label,
2709
+ family: flags.family,
2710
+ baseUrl: flags.baseUrl,
2711
+ envVars: flags.envVars
2712
+ });
2713
+ }
2714
+ function parseAuthFlags(args) {
1648
2715
  const out = { positional: [] };
1649
2716
  for (let i = 0; i < args.length; i++) {
1650
2717
  const a = args[i];
1651
- if (a === "--family") {
2718
+ if (a === "--label") {
2719
+ const v = args[++i];
2720
+ if (v) out.label = v;
2721
+ } else if (a === "--family") {
1652
2722
  const v = args[++i];
1653
2723
  if (v) out.family = v;
1654
2724
  } else if (a === "--base-url") {
@@ -1710,7 +2780,7 @@ async function initCmd(_args, deps) {
1710
2780
  } else {
1711
2781
  deps.renderer.writeInfo(`Found API key in env (${provider.envVars.join(" / ")}).`);
1712
2782
  }
1713
- await fs2.mkdir(deps.paths.globalRoot, { recursive: true });
2783
+ await fs6.mkdir(deps.paths.globalRoot, { recursive: true });
1714
2784
  const config = {
1715
2785
  version: 1,
1716
2786
  provider: providerId,
@@ -1718,10 +2788,10 @@ async function initCmd(_args, deps) {
1718
2788
  };
1719
2789
  if (apiKey) config.apiKey = apiKey;
1720
2790
  await atomicWrite(deps.paths.globalConfig, JSON.stringify(config, null, 2));
1721
- await fs2.mkdir(path2.join(deps.projectRoot, ".wrongstack"), { recursive: true });
1722
- const agentsFile = path2.join(deps.projectRoot, ".wrongstack", "AGENTS.md");
2791
+ await fs6.mkdir(path5.join(deps.projectRoot, ".wrongstack"), { recursive: true });
2792
+ const agentsFile = path5.join(deps.projectRoot, ".wrongstack", "AGENTS.md");
1723
2793
  try {
1724
- await fs2.access(agentsFile);
2794
+ await fs6.access(agentsFile);
1725
2795
  } catch {
1726
2796
  await atomicWrite(
1727
2797
  agentsFile,
@@ -1853,25 +2923,42 @@ async function modelsCmd(args, deps) {
1853
2923
  deps.renderer.writeError("Usage: wstack models <provider> | refresh");
1854
2924
  return 1;
1855
2925
  }
1856
- const provider = await deps.modelsRegistry.getProvider(providerId);
2926
+ let lookupId = providerId;
2927
+ const savedAlias = deps.config.providers?.[providerId];
2928
+ if (savedAlias?.type && savedAlias.type !== providerId) {
2929
+ lookupId = savedAlias.type;
2930
+ }
2931
+ const provider = await deps.modelsRegistry.getProvider(lookupId);
1857
2932
  if (!provider) {
1858
- deps.renderer.writeError(`Provider "${providerId}" not in catalog.`);
2933
+ deps.renderer.writeError(
2934
+ lookupId !== providerId ? `Alias "${providerId}" points at catalog id "${lookupId}" which is not in the cache.` : `Provider "${providerId}" not in catalog.`
2935
+ );
1859
2936
  return 1;
1860
2937
  }
2938
+ if (lookupId !== providerId) {
2939
+ deps.renderer.write(color.dim(`(showing catalog models for "${lookupId}" via alias "${providerId}")
2940
+ `));
2941
+ }
1861
2942
  deps.renderer.write(`${color.bold(provider.name)} ${color.dim(`(${provider.id})`)}
1862
2943
  `);
1863
2944
  if (provider.doc) deps.renderer.write(color.dim(`Docs: ${provider.doc}
1864
2945
  `));
1865
- const sorted = [...provider.models].sort(
2946
+ const userModels = deps.config.providers?.[providerId]?.models;
2947
+ const catalogById = new Map(provider.models.map((m) => [m.id, m]));
2948
+ const sorted = userModels && userModels.length > 0 ? userModels.map((id) => catalogById.get(id) ?? { id, name: id }) : [...provider.models].sort(
1866
2949
  (a, b) => (b.release_date ?? "").localeCompare(a.release_date ?? "")
1867
2950
  );
2951
+ if (userModels && userModels.length > 0) {
2952
+ deps.renderer.write(color.dim(`(${userModels.length} model(s) from your saved config)
2953
+ `));
2954
+ }
1868
2955
  for (const m of sorted) {
1869
2956
  const caps = [];
1870
- if (m.tool_call) caps.push("tools");
1871
- if (m.reasoning) caps.push("reasoning");
1872
- if (m.modalities?.input?.includes("image")) caps.push("vision");
1873
- const ctx = m.limit?.context ? `${(m.limit.context / 1e3).toFixed(0)}k` : "?";
1874
- const cost = m.cost?.input !== void 0 ? `$${m.cost.input}/$${m.cost.output ?? "?"}` : "";
2957
+ if ("tool_call" in m && m.tool_call) caps.push("tools");
2958
+ if ("reasoning" in m && m.reasoning) caps.push("reasoning");
2959
+ if ("modalities" in m && m.modalities?.input?.includes("image")) caps.push("vision");
2960
+ const ctx = "limit" in m && m.limit?.context ? `${(m.limit.context / 1e3).toFixed(0)}k` : "?";
2961
+ const cost = "cost" in m && m.cost?.input !== void 0 ? `$${m.cost.input}/$${m.cost.output ?? "?"}` : "";
1875
2962
  deps.renderer.write(
1876
2963
  ` ${m.id.padEnd(40)} ${color.dim(ctx.padStart(6))} ${color.dim(cost.padEnd(14))} ${color.dim(caps.join(","))}
1877
2964
  `
@@ -1893,16 +2980,46 @@ async function mcpCmd(args, deps) {
1893
2980
  const servers = deps.config.mcpServers ?? {};
1894
2981
  if (Object.keys(servers).length === 0) {
1895
2982
  deps.renderer.write("No MCP servers configured.\n");
2983
+ deps.renderer.write("Use `wstack mcp add <name>` or set mcpServers in your config.\n");
1896
2984
  return 0;
1897
2985
  }
1898
2986
  for (const [name, cfg] of Object.entries(servers)) {
2987
+ const status = cfg.enabled === false ? "disabled" : "enabled";
2988
+ const desc = cfg.description ? ` # ${cfg.description}` : "";
1899
2989
  deps.renderer.write(
1900
- ` ${name.padEnd(20)} ${cfg.transport} ${cfg.enabled === false ? "disabled" : "enabled"}
2990
+ ` ${name.padEnd(20)} ${cfg.transport.padEnd(16)} ${status}${desc}
1901
2991
  `
1902
2992
  );
1903
2993
  }
1904
2994
  return 0;
1905
2995
  }
2996
+ if (sub === "add") {
2997
+ const name = args[1];
2998
+ if (!name) {
2999
+ deps.renderer.writeError("Usage: wstack mcp add <name>\n");
3000
+ deps.renderer.write("Available servers:\n");
3001
+ for (const [sname, scfg] of Object.entries(deps.config.mcpServers ?? {})) {
3002
+ deps.renderer.write(` ${sname.padEnd(20)} ${scfg.description ?? scfg.transport}
3003
+ `);
3004
+ }
3005
+ if (Object.keys(deps.config.mcpServers ?? {}).length === 0) {
3006
+ deps.renderer.write(
3007
+ " filesystem filesystem (read/write/navigate)\n github github (issues, PRs, repos)\n context7 context7 (codebase docs & Q&A)\n brave-search brave search (web search)\n block block (Postgres via SQL)\n everart everart (AI image generation)\n slack slack (messaging & channels)\n aws aws (EC2, S3, Lambda, IAM)\n google-maps google-maps (directions, geocoding)\n sentinel sentinel (security vulnerabilities)\n"
3008
+ );
3009
+ }
3010
+ deps.renderer.write("\nRun `wstack mcp add <name> --enable` to enable immediately.\n");
3011
+ return 1;
3012
+ }
3013
+ return addMcpServer(args, deps);
3014
+ }
3015
+ if (sub === "remove") {
3016
+ const name = args[1];
3017
+ if (!name) {
3018
+ deps.renderer.writeError("Usage: wstack mcp remove <name>\n");
3019
+ return 1;
3020
+ }
3021
+ return removeMcpServer(name, deps);
3022
+ }
1906
3023
  if (sub === "restart") {
1907
3024
  deps.renderer.writeWarning("mcp restart is only available in REPL mode.");
1908
3025
  return 0;
@@ -1910,6 +3027,70 @@ async function mcpCmd(args, deps) {
1910
3027
  deps.renderer.writeError(`Unknown mcp subcommand: ${sub}`);
1911
3028
  return 1;
1912
3029
  }
3030
+ async function addMcpServer(args, deps) {
3031
+ const name = args[1];
3032
+ const enable = args.includes("--enable") || args.includes("-e");
3033
+ const builtIn = {
3034
+ filesystem: { name: "filesystem", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem", "."], permission: "confirm", description: "Read, write, and navigate the local filesystem" },
3035
+ github: { name: "github", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-github"], permission: "confirm", description: "GitHub API \u2014 issues, PRs, repos, search" },
3036
+ "context7": { name: "context7", transport: "streamable-http", url: "https://server.context7.ai/mcp", permission: "confirm", description: "Codebase-aware documentation and Q&A" },
3037
+ "brave-search": { name: "brave-search", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-brave-search"], permission: "confirm", description: "Web search (Brave)" },
3038
+ block: { name: "block", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-block"], permission: "confirm", description: "Postgres database via SQL" },
3039
+ everart: { name: "everart", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-everart"], permission: "confirm", description: "AI image generation" },
3040
+ slack: { name: "slack", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-slack"], permission: "confirm", description: "Slack messaging & channels" },
3041
+ aws: { name: "aws", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-aws"], permission: "confirm", description: "AWS \u2014 EC2, S3, Lambda, IAM" },
3042
+ "google-maps": { name: "google-maps", transport: "stdio", command: "npx", args: ["-y", "@modelcontextprotocol/server-google-maps"], permission: "confirm", description: "Google Maps \u2014 directions, geocoding, places" },
3043
+ sentinel: { name: "sentinel", transport: "streamable-http", url: "https://mcp.sentinel.ai", permission: "deny", description: "Security vulnerability scanning" }
3044
+ };
3045
+ const factory = builtIn[name];
3046
+ if (!factory) {
3047
+ deps.renderer.writeError(`Unknown server "${name}". Run \`wstack mcp add\` without args to see available servers.
3048
+ `);
3049
+ return 1;
3050
+ }
3051
+ const serverCfg = { ...factory };
3052
+ if (!enable) serverCfg.enabled = false;
3053
+ let existing = {};
3054
+ try {
3055
+ const raw = await fs6.readFile(deps.paths.globalConfig, "utf8");
3056
+ existing = JSON.parse(raw);
3057
+ } catch {
3058
+ }
3059
+ const mcpServers = existing.mcpServers ?? {};
3060
+ if (mcpServers[name]) {
3061
+ deps.renderer.writeWarning(`Server "${name}" already in config. Updating.
3062
+ `);
3063
+ }
3064
+ mcpServers[name] = serverCfg;
3065
+ existing.mcpServers = mcpServers;
3066
+ await atomicWrite(deps.paths.globalConfig, JSON.stringify(existing, null, 2));
3067
+ const verb = enable ? "Enabled" : "Added (disabled \u2014 set enabled:true to activate)";
3068
+ deps.renderer.writeInfo(`${verb} "${name}" (${serverCfg.transport}). Config written to ${deps.paths.globalConfig}.
3069
+ `);
3070
+ return 0;
3071
+ }
3072
+ async function removeMcpServer(name, deps) {
3073
+ let existing = {};
3074
+ try {
3075
+ const raw = await fs6.readFile(deps.paths.globalConfig, "utf8");
3076
+ existing = JSON.parse(raw);
3077
+ } catch {
3078
+ deps.renderer.writeError("No config file found.\n");
3079
+ return 1;
3080
+ }
3081
+ const mcpServers = existing.mcpServers ?? {};
3082
+ if (!mcpServers[name]) {
3083
+ deps.renderer.writeError(`Server "${name}" not in config.
3084
+ `);
3085
+ return 1;
3086
+ }
3087
+ delete mcpServers[name];
3088
+ existing.mcpServers = mcpServers;
3089
+ await atomicWrite(deps.paths.globalConfig, JSON.stringify(existing, null, 2));
3090
+ deps.renderer.writeInfo(`Removed "${name}" from config.
3091
+ `);
3092
+ return 0;
3093
+ }
1913
3094
  async function pluginCmd(args, deps) {
1914
3095
  const sub = args[0];
1915
3096
  if (!sub || sub === "list") {
@@ -1934,7 +3115,7 @@ async function diagCmd(_args, deps) {
1934
3115
  const age = await deps.modelsRegistry.ageSeconds();
1935
3116
  const lines = [
1936
3117
  color.bold("WrongStack diagnostics"),
1937
- ` apiVersion: 0.0.1`,
3118
+ ` apiVersion: ${API_VERSION}`,
1938
3119
  ` cwd: ${deps.cwd}`,
1939
3120
  ` projectRoot: ${deps.projectRoot}`,
1940
3121
  ` projectHash: ${deps.paths.projectHash}`,
@@ -1943,7 +3124,7 @@ async function diagCmd(_args, deps) {
1943
3124
  ` modelsCache: ${deps.paths.modelsCache}`,
1944
3125
  ` cacheAge: ${isFinite(age) ? `${Math.round(age / 60)}m` : "never"}`,
1945
3126
  ` node: ${process.version}`,
1946
- ` os: ${os2.platform()} ${os2.release()}`,
3127
+ ` os: ${os3.platform()} ${os3.release()}`,
1947
3128
  ` provider: ${cfg.provider ?? "<unset>"}`,
1948
3129
  ` model: ${cfg.model ?? "<unset>"}`,
1949
3130
  ` tools: ${deps.toolRegistry?.list().length ?? 0}`,
@@ -1953,6 +3134,182 @@ async function diagCmd(_args, deps) {
1953
3134
  deps.renderer.write(lines.join("\n") + "\n");
1954
3135
  return 0;
1955
3136
  }
3137
+ async function doctorCmd(_args, deps) {
3138
+ const checks = [];
3139
+ const cfg = deps.config;
3140
+ if (!cfg.provider) {
3141
+ checks.push({ name: "provider", status: "fail", detail: "no provider configured \u2014 run `wstack init` or `wstack auth`" });
3142
+ } else {
3143
+ checks.push({ name: "provider", status: "ok", detail: cfg.provider });
3144
+ }
3145
+ if (!cfg.model) {
3146
+ checks.push({ name: "model", status: "fail", detail: "no model configured \u2014 run `wstack init`" });
3147
+ } else {
3148
+ checks.push({ name: "model", status: "ok", detail: cfg.model });
3149
+ }
3150
+ if (cfg.provider) {
3151
+ const providerCfg = cfg.providers?.[cfg.provider];
3152
+ const hasVaultKey = typeof providerCfg?.apiKey === "string" && providerCfg.apiKey.length > 0;
3153
+ const envHit = providerCfg?.envVars?.some((v) => process.env[v]) ?? false;
3154
+ if (hasVaultKey || envHit) {
3155
+ checks.push({
3156
+ name: "api key",
3157
+ status: "ok",
3158
+ detail: hasVaultKey ? "found in vault" : "found in env"
3159
+ });
3160
+ } else {
3161
+ checks.push({
3162
+ name: "api key",
3163
+ status: "fail",
3164
+ detail: `no key for "${cfg.provider}" in vault or env \u2014 run \`wstack auth ${cfg.provider}\``
3165
+ });
3166
+ }
3167
+ }
3168
+ try {
3169
+ const age = await deps.modelsRegistry.ageSeconds();
3170
+ if (!isFinite(age)) {
3171
+ checks.push({ name: "models cache", status: "warn", detail: "never fetched \u2014 run `wstack models refresh`" });
3172
+ } else if (age > 7 * 24 * 3600) {
3173
+ checks.push({
3174
+ name: "models cache",
3175
+ status: "warn",
3176
+ detail: `${Math.round(age / 86400)} days old \u2014 run \`wstack models refresh\``
3177
+ });
3178
+ } else {
3179
+ checks.push({ name: "models cache", status: "ok", detail: `${Math.round(age / 60)}m old` });
3180
+ }
3181
+ } catch (err) {
3182
+ checks.push({
3183
+ name: "models cache",
3184
+ status: "warn",
3185
+ detail: `read failed: ${err instanceof Error ? err.message : String(err)}`
3186
+ });
3187
+ }
3188
+ try {
3189
+ await fs6.access(deps.paths.secretsKey);
3190
+ checks.push({ name: "secret vault", status: "ok", detail: deps.paths.secretsKey });
3191
+ } catch {
3192
+ checks.push({
3193
+ name: "secret vault",
3194
+ status: "warn",
3195
+ detail: "not yet initialized (created lazily on first encrypt)"
3196
+ });
3197
+ }
3198
+ try {
3199
+ await fs6.mkdir(deps.paths.projectSessions, { recursive: true });
3200
+ const probe = path5.join(deps.paths.projectSessions, `.probe-${Date.now()}`);
3201
+ await fs6.writeFile(probe, "");
3202
+ await fs6.unlink(probe);
3203
+ checks.push({ name: "sessions writable", status: "ok", detail: deps.paths.projectSessions });
3204
+ } catch (err) {
3205
+ checks.push({
3206
+ name: "sessions writable",
3207
+ status: "fail",
3208
+ detail: `cannot write to ${deps.paths.projectSessions}: ${err instanceof Error ? err.message : String(err)}`
3209
+ });
3210
+ }
3211
+ const mcpEntries = Object.entries(cfg.mcpServers ?? {});
3212
+ for (const [name, srv] of mcpEntries) {
3213
+ if (!srv.enabled) continue;
3214
+ if ((srv.transport === "sse" || srv.transport === "streamable-http") && !srv.url) {
3215
+ checks.push({ name: `mcp:${name}`, status: "fail", detail: "transport requires url" });
3216
+ } else if (srv.transport === "stdio" && !srv.command) {
3217
+ checks.push({ name: `mcp:${name}`, status: "fail", detail: "stdio transport requires command" });
3218
+ } else {
3219
+ checks.push({ name: `mcp:${name}`, status: "ok", detail: `${srv.transport} ${srv.command ?? srv.url ?? ""}`.trim() });
3220
+ }
3221
+ }
3222
+ const major = Number.parseInt(process.version.replace(/^v/, "").split(".")[0] ?? "0", 10);
3223
+ if (major < 22) {
3224
+ checks.push({ name: "node", status: "fail", detail: `${process.version} (need \u226522)` });
3225
+ } else {
3226
+ checks.push({ name: "node", status: "ok", detail: process.version });
3227
+ }
3228
+ deps.renderer.write(color.bold("WrongStack doctor\n\n"));
3229
+ let failed = 0;
3230
+ let warned = 0;
3231
+ for (const c of checks) {
3232
+ const icon = c.status === "ok" ? color.green("\u2713") : c.status === "warn" ? color.amber("\u25CF") : color.red("\u2717");
3233
+ deps.renderer.write(` ${icon} ${c.name.padEnd(20)} ${color.dim(c.detail)}
3234
+ `);
3235
+ if (c.status === "fail") failed++;
3236
+ if (c.status === "warn") warned++;
3237
+ }
3238
+ deps.renderer.write("\n");
3239
+ if (failed > 0) {
3240
+ deps.renderer.write(color.red(`${failed} failed, ${warned} warning${warned === 1 ? "" : "s"}
3241
+ `));
3242
+ return 1;
3243
+ }
3244
+ if (warned > 0) {
3245
+ deps.renderer.write(color.amber(`All checks passed (${warned} warning${warned === 1 ? "" : "s"})
3246
+ `));
3247
+ return 0;
3248
+ }
3249
+ deps.renderer.write(color.green("All checks passed.\n"));
3250
+ return 0;
3251
+ }
3252
+ async function exportCmd(args, deps) {
3253
+ if (!deps.sessionStore) {
3254
+ deps.renderer.writeError("No session store configured.");
3255
+ return 1;
3256
+ }
3257
+ let format = "markdown";
3258
+ let output;
3259
+ let includeTools = true;
3260
+ let includeDiagnostics = true;
3261
+ let sessionId;
3262
+ for (let i = 0; i < args.length; i++) {
3263
+ const a = args[i];
3264
+ if (a === "--format" || a === "-f") {
3265
+ const v = args[++i];
3266
+ if (v !== "markdown" && v !== "json" && v !== "text") {
3267
+ deps.renderer.writeError(`Unknown --format ${v}. Use markdown, json, or text.`);
3268
+ return 1;
3269
+ }
3270
+ format = v;
3271
+ } else if (a === "--out" || a === "-o") {
3272
+ output = args[++i];
3273
+ } else if (a === "--no-tools") {
3274
+ includeTools = false;
3275
+ } else if (a === "--no-diagnostics") {
3276
+ includeDiagnostics = false;
3277
+ } else if (a.startsWith("-")) {
3278
+ deps.renderer.writeError(`Unknown flag: ${a}`);
3279
+ return 1;
3280
+ } else if (!sessionId) {
3281
+ sessionId = a;
3282
+ }
3283
+ }
3284
+ if (!sessionId) {
3285
+ deps.renderer.writeError("Usage: wstack export <sessionId> [--format markdown|json|text] [--out <file>] [--no-tools] [--no-diagnostics]");
3286
+ return 1;
3287
+ }
3288
+ const reader = new DefaultSessionReader({ store: deps.sessionStore });
3289
+ let rendered;
3290
+ try {
3291
+ rendered = await reader.export(sessionId, {
3292
+ format,
3293
+ includeTools,
3294
+ includeDiagnostics
3295
+ });
3296
+ } catch (err) {
3297
+ deps.renderer.writeError(
3298
+ `Export failed: ${err instanceof Error ? err.message : String(err)}`
3299
+ );
3300
+ return 1;
3301
+ }
3302
+ if (output) {
3303
+ await fs6.mkdir(path5.dirname(path5.resolve(deps.cwd, output)), { recursive: true });
3304
+ await fs6.writeFile(path5.resolve(deps.cwd, output), rendered, "utf8");
3305
+ deps.renderer.write(`Wrote ${rendered.length} bytes to ${output}
3306
+ `);
3307
+ } else {
3308
+ deps.renderer.write(rendered);
3309
+ if (!rendered.endsWith("\n")) deps.renderer.write("\n");
3310
+ }
3311
+ return 0;
3312
+ }
1956
3313
  async function usageCmd(_args, deps) {
1957
3314
  if (!deps.sessionStore) return 0;
1958
3315
  const list = await deps.sessionStore.list(100);
@@ -1964,7 +3321,7 @@ async function usageCmd(_args, deps) {
1964
3321
  }
1965
3322
  async function versionCmd(_args, deps) {
1966
3323
  deps.renderer.write(
1967
- `WrongStack 0.0.1 (apiVersion 0.0.1, node ${process.version}, ${os2.platform()})
3324
+ `WrongStack ${CLI_VERSION} (apiVersion ${API_VERSION}, node ${process.version}, ${os3.platform()})
1968
3325
  `
1969
3326
  );
1970
3327
  return 0;
@@ -1978,7 +3335,8 @@ async function helpCmd(_args, deps) {
1978
3335
  " wstack resume [<id>] Resume a session",
1979
3336
  " wstack sessions List recent sessions",
1980
3337
  " wstack init Pick provider + model from models.dev",
1981
- " wstack auth <provider> Store API key (encrypted at rest)",
3338
+ " wstack auth Interactive key manager (list/add/update/delete)",
3339
+ " wstack auth <provider> Append one key for a provider (encrypted at rest)",
1982
3340
  " wstack resume <id> Resume a session (loads transcript + appends)",
1983
3341
  " wstack config [show|edit] Show or edit effective config",
1984
3342
  " wstack tools List registered tools",
@@ -1990,6 +3348,8 @@ async function helpCmd(_args, deps) {
1990
3348
  " wstack plugin [list] List plugins",
1991
3349
  " wstack projects List projects tracked in ~/.wrongstack/projects/",
1992
3350
  " wstack diag Full diagnostics",
3351
+ " wstack doctor Health checks (config, keys, MCP, node)",
3352
+ " wstack export <id> [opts] Render a session (--format markdown|json|text, --out <file>)",
1993
3353
  " wstack usage Token + cost summary",
1994
3354
  " wstack version Print version",
1995
3355
  "",
@@ -2000,9 +3360,9 @@ async function helpCmd(_args, deps) {
2000
3360
  return 0;
2001
3361
  }
2002
3362
  async function projectsCmd(_args, deps) {
2003
- const projectsRoot = path2.join(deps.paths.globalRoot, "projects");
3363
+ const projectsRoot = path5.join(deps.paths.globalRoot, "projects");
2004
3364
  try {
2005
- const entries = await fs2.readdir(projectsRoot);
3365
+ const entries = await fs6.readdir(projectsRoot);
2006
3366
  if (entries.length === 0) {
2007
3367
  deps.renderer.write("No projects tracked.\n");
2008
3368
  return 0;
@@ -2010,7 +3370,7 @@ async function projectsCmd(_args, deps) {
2010
3370
  for (const hash of entries) {
2011
3371
  try {
2012
3372
  const meta = JSON.parse(
2013
- await fs2.readFile(path2.join(projectsRoot, hash, "meta.json"), "utf8")
3373
+ await fs6.readFile(path5.join(projectsRoot, hash, "meta.json"), "utf8")
2014
3374
  );
2015
3375
  deps.renderer.write(
2016
3376
  ` ${color.dim(hash)} ${color.dim(meta.lastSeen ?? "")} ${meta.root ?? "?"}
@@ -2057,7 +3417,8 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
2057
3417
  "no-alt-screen",
2058
3418
  "alt-screen",
2059
3419
  "output-json",
2060
- "prompt"
3420
+ "prompt",
3421
+ "metrics"
2061
3422
  ]);
2062
3423
  function parseArgs(argv) {
2063
3424
  const flags = {};
@@ -2095,96 +3456,32 @@ function parseArgs(argv) {
2095
3456
  }
2096
3457
  return { flags, positional };
2097
3458
  }
2098
- function flagsToConfigPatch(flags) {
2099
- const patch = {};
2100
- if (typeof flags["provider"] === "string") patch.provider = flags["provider"];
2101
- if (typeof flags["model"] === "string") patch.model = flags["model"];
2102
- if (typeof flags["cwd"] === "string") patch.cwd = flags["cwd"];
2103
- if (typeof flags["log-level"] === "string") {
2104
- patch.log = { level: flags["log-level"] };
2105
- } else if (flags["verbose"]) {
2106
- patch.log = { level: "debug" };
2107
- } else if (flags["trace"]) {
2108
- patch.log = { level: "trace" };
2109
- }
2110
- if (flags["yolo"]) patch.yolo = true;
2111
- if (flags["no-features"]) {
2112
- patch.features = {
2113
- mcp: false,
2114
- plugins: false,
2115
- memory: false,
2116
- modelsRegistry: false,
2117
- skills: false
2118
- };
2119
- }
2120
- return patch;
2121
- }
2122
3459
  function resolveBundledSkillsDir() {
2123
3460
  try {
2124
- const req = createRequire(import.meta.url);
2125
- const corePkg = req.resolve("@wrongstack/core/package.json");
2126
- return path2.join(path2.dirname(corePkg), "skills");
3461
+ const req2 = createRequire(import.meta.url);
3462
+ const corePkg = req2.resolve("@wrongstack/core/package.json");
3463
+ return path5.join(path5.dirname(corePkg), "skills");
2127
3464
  } catch {
2128
3465
  return void 0;
2129
3466
  }
2130
3467
  }
2131
- function readOwnVersion() {
2132
- const req = createRequire(import.meta.url);
2133
- const candidates = ["../package.json", "../../package.json"];
2134
- for (const rel of candidates) {
2135
- try {
2136
- const pkg = req(rel);
2137
- if (typeof pkg.version === "string" && pkg.version.length > 0) return pkg.version;
2138
- } catch {
2139
- }
2140
- }
2141
- return "dev";
2142
- }
2143
- var CLI_VERSION = readOwnVersion();
2144
- async function ensureProjectMeta(paths, projectRoot) {
2145
- try {
2146
- await fs2.mkdir(paths.projectDir, { recursive: true });
2147
- const meta = {
2148
- hash: paths.projectHash,
2149
- root: projectRoot,
2150
- lastSeen: (/* @__PURE__ */ new Date()).toISOString()
2151
- };
2152
- await fs2.writeFile(paths.projectMeta, JSON.stringify(meta, null, 2));
2153
- } catch {
2154
- }
2155
- }
2156
3468
  async function main(argv) {
2157
3469
  const { flags, positional } = parseArgs(argv);
2158
- const cwd = typeof flags["cwd"] === "string" ? path2.resolve(flags["cwd"]) : process.cwd();
2159
- const pathResolver = new DefaultPathResolver(cwd);
2160
- const projectRoot = pathResolver.projectRoot;
2161
- const userHome = os2.homedir();
2162
- const wpaths = resolveWstackPaths({ projectRoot, userHome });
2163
- await ensureProjectMeta(wpaths, projectRoot);
2164
3470
  if (positional[0] === "resume" && positional[1] && !subcommands["__noop_resume_marker"]) {
2165
3471
  flags["resume"] = positional[1];
2166
3472
  positional.splice(0, 2);
2167
3473
  }
2168
- const vault = new DefaultSecretVault({ keyFile: wpaths.secretsKey });
2169
- for (const file of [wpaths.globalConfig, wpaths.projectLocalConfig]) {
2170
- try {
2171
- const { migrated } = await migratePlaintextSecrets(file, vault);
2172
- if (migrated > 0) {
2173
- process.stderr.write(`[wstack] Encrypted ${migrated} plaintext secret(s) in ${file}
2174
- `);
2175
- }
2176
- } catch {
2177
- }
2178
- }
2179
- const configLoader = new DefaultConfigLoader({ paths: wpaths, vault });
2180
- let config;
3474
+ let boot;
2181
3475
  try {
2182
- config = await configLoader.load({ cliFlags: flagsToConfigPatch(flags) });
3476
+ boot = await bootConfig(flags);
2183
3477
  } catch (err) {
2184
3478
  process.stderr.write(`Config error: ${err instanceof Error ? err.message : String(err)}
2185
3479
  `);
2186
3480
  return 2;
2187
3481
  }
3482
+ const { paths, config: _config, vault } = boot;
3483
+ let config = _config;
3484
+ const { cwd, projectRoot, userHome, wpaths, pathResolver } = paths;
2188
3485
  const logger = new DefaultLogger({
2189
3486
  level: config.log.level,
2190
3487
  file: wpaths.logFile
@@ -2251,7 +3548,7 @@ async function main(argv) {
2251
3548
  } else {
2252
3549
  const prevProvider = config.provider;
2253
3550
  const prevModel = config.model;
2254
- config = Object.freeze({ ...config, provider: picked.provider, model: picked.model });
3551
+ config = patchConfig(config, { provider: picked.provider, model: picked.model });
2255
3552
  if (picked.provider !== prevProvider || picked.model !== prevModel) {
2256
3553
  const saved = await saveToGlobalConfig(
2257
3554
  wpaths.globalConfig,
@@ -2286,15 +3583,21 @@ async function main(argv) {
2286
3583
  flags["no-tui"] = true;
2287
3584
  }
2288
3585
  if (choices.yolo !== config.yolo) {
2289
- config = Object.freeze({ ...config, yolo: choices.yolo });
3586
+ config = patchConfig(config, { yolo: choices.yolo });
2290
3587
  }
2291
3588
  }
2292
- const resolvedProvider = await modelsRegistry.getProvider(config.provider).catch(() => void 0);
3589
+ const savedProviderCfg = config.providers?.[config.provider];
3590
+ let resolvedProvider = await modelsRegistry.getProvider(config.provider).catch(() => void 0);
3591
+ if (!resolvedProvider && savedProviderCfg?.type && savedProviderCfg.type !== config.provider) {
3592
+ resolvedProvider = await modelsRegistry.getProvider(savedProviderCfg.type).catch(() => void 0);
3593
+ }
2293
3594
  if (!resolvedProvider) {
2294
- logger.warn(
2295
- `Provider "${config.provider}" not found in models.dev. Continuing with raw config.`
2296
- );
2297
- } else if (resolvedProvider.family === "unsupported") {
3595
+ if (!savedProviderCfg?.family) {
3596
+ logger.warn(
3597
+ `Provider "${config.provider}" not found in models.dev. Continuing with raw config.`
3598
+ );
3599
+ }
3600
+ } else if (resolvedProvider.family === "unsupported" && !savedProviderCfg?.family) {
2298
3601
  process.stderr.write(
2299
3602
  `Provider "${config.provider}" uses an unsupported wire family (${resolvedProvider.npm}). Install a plugin to enable it, or pick a different provider.
2300
3603
  `
@@ -2303,6 +3606,8 @@ async function main(argv) {
2303
3606
  return 2;
2304
3607
  }
2305
3608
  const container = new Container();
3609
+ const configStore = new DefaultConfigStore(config);
3610
+ container.bind(TOKENS.ConfigStore, () => configStore);
2306
3611
  container.bind(TOKENS.Logger, () => logger);
2307
3612
  container.bind(TOKENS.PathResolver, () => pathResolver);
2308
3613
  container.bind(TOKENS.SecretScrubber, () => new DefaultSecretScrubber());
@@ -2377,6 +3682,68 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
2377
3682
  }
2378
3683
  const events = new EventBus();
2379
3684
  events.setLogger(logger);
3685
+ let metricsSink;
3686
+ let healthRegistry;
3687
+ let metricsServerHandle;
3688
+ const metricsPortFlag = flags["metrics-port"];
3689
+ const metricsPort = typeof metricsPortFlag === "string" && metricsPortFlag.length > 0 ? Number.parseInt(metricsPortFlag, 10) : void 0;
3690
+ if (metricsPort !== void 0 && !flags.metrics) flags.metrics = true;
3691
+ if (flags.metrics) {
3692
+ metricsSink = new InMemoryMetricsSink();
3693
+ wireMetricsToEvents(events, metricsSink);
3694
+ healthRegistry = new DefaultHealthRegistry();
3695
+ healthRegistry.register({
3696
+ name: "session-store",
3697
+ check: async () => {
3698
+ try {
3699
+ await fs6.access(wpaths.projectSessions);
3700
+ return { status: "healthy" };
3701
+ } catch (e) {
3702
+ return { status: "unhealthy", detail: e instanceof Error ? e.message : "access denied" };
3703
+ }
3704
+ }
3705
+ });
3706
+ healthRegistry.register({
3707
+ name: "provider",
3708
+ check: async () => ({
3709
+ status: "healthy",
3710
+ data: { id: config.provider, model: config.model }
3711
+ })
3712
+ });
3713
+ const dumpMetrics = () => {
3714
+ if (!metricsSink) return;
3715
+ try {
3716
+ const out = path5.join(wpaths.projectSessions, "metrics.json");
3717
+ const snap = metricsSink.snapshot();
3718
+ writeFileSync(out, JSON.stringify(snap, null, 2));
3719
+ } catch {
3720
+ }
3721
+ };
3722
+ process.on("exit", dumpMetrics);
3723
+ process.on("SIGINT", () => {
3724
+ dumpMetrics();
3725
+ process.exit(130);
3726
+ });
3727
+ if (metricsPort !== void 0 && Number.isFinite(metricsPort)) {
3728
+ try {
3729
+ metricsServerHandle = await startMetricsServer({
3730
+ port: metricsPort,
3731
+ host: process.env.METRICS_HOST ?? "127.0.0.1",
3732
+ sink: metricsSink,
3733
+ // V2-C: mount /healthz on the same listener so k8s probes can
3734
+ // hit one endpoint per pod for both observability and liveness.
3735
+ healthRegistry
3736
+ });
3737
+ logger.info(`metrics endpoint listening on ${metricsServerHandle.url} (healthz on same port)`);
3738
+ process.on("exit", () => {
3739
+ void metricsServerHandle?.close().catch(() => {
3740
+ });
3741
+ });
3742
+ } catch (err) {
3743
+ logger.warn(`metrics endpoint failed to start: ${err instanceof Error ? err.message : String(err)}`);
3744
+ }
3745
+ }
3746
+ }
2380
3747
  const spinner = new Spinner();
2381
3748
  let lastInputTokens = 0;
2382
3749
  events.on("provider.response", (e) => {
@@ -2434,13 +3801,11 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
2434
3801
  };
2435
3802
  let provider;
2436
3803
  try {
2437
- if (config.features.modelsRegistry) {
2438
- provider = providerRegistry.create({ ...providerConfig, type: config.provider });
3804
+ const cfgWithType = { ...providerConfig, type: config.provider };
3805
+ if (config.features.modelsRegistry && providerRegistry.has(config.provider)) {
3806
+ provider = providerRegistry.create(cfgWithType);
2439
3807
  } else {
2440
- provider = makeProviderFromConfig(config.provider, {
2441
- ...providerConfig,
2442
- type: config.provider
2443
- });
3808
+ provider = makeProviderFromConfig(config.provider, cfgWithType);
2444
3809
  }
2445
3810
  } catch (err) {
2446
3811
  process.stderr.write(
@@ -2505,13 +3870,21 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
2505
3870
  }
2506
3871
  await recoveryLock.write(session.id).catch(() => void 0);
2507
3872
  const attachments = new DefaultAttachmentStore({
2508
- spoolDir: path2.join(wpaths.projectSessions, session.id, "attachments")
3873
+ spoolDir: path5.join(wpaths.projectSessions, session.id, "attachments")
2509
3874
  });
2510
3875
  const queueStore = new QueueStore({
2511
- dir: path2.join(wpaths.projectSessions, session.id)
3876
+ dir: path5.join(wpaths.projectSessions, session.id)
2512
3877
  });
2513
3878
  const tokenCounter = container.resolve(TOKENS.TokenCounter);
2514
3879
  const stats = new SessionStats(events, tokenCounter);
3880
+ const errorRing = [];
3881
+ events.on("error", (e) => {
3882
+ const err = e.err;
3883
+ const code2 = err && typeof err === "object" && "code" in err && typeof err.code === "string" ? err.code : "UNKNOWN";
3884
+ const message = e.err instanceof Error ? e.err.message : String(e.err);
3885
+ errorRing.push({ ts: (/* @__PURE__ */ new Date()).toISOString(), phase: e.phase, code: code2, message });
3886
+ if (errorRing.length > 5) errorRing.shift();
3887
+ });
2515
3888
  const ctxSignal = new AbortController().signal;
2516
3889
  const context = new Context({
2517
3890
  systemPrompt,
@@ -2527,6 +3900,26 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
2527
3900
  context.messages.push(...restoredMessages);
2528
3901
  }
2529
3902
  const pipelines = createDefaultPipelines();
3903
+ const installBoundary = (p) => {
3904
+ p.setErrorHandler((ev) => {
3905
+ const fromPlugin = !!ev.owner && ev.owner !== "core";
3906
+ logger.error(
3907
+ `Pipeline middleware "${ev.middleware}" crashed (owner=${ev.owner ?? "unknown"}); ${fromPlugin ? "swallowed" : "rethrown"}`,
3908
+ ev.err
3909
+ );
3910
+ events.emit("error", {
3911
+ err: ev.err instanceof Error ? ev.err : new Error(String(ev.err)),
3912
+ phase: `pipeline:${ev.middleware}`
3913
+ });
3914
+ return fromPlugin ? "swallow" : "rethrow";
3915
+ });
3916
+ };
3917
+ installBoundary(pipelines.request);
3918
+ installBoundary(pipelines.response);
3919
+ installBoundary(pipelines.toolCall);
3920
+ installBoundary(pipelines.userInput);
3921
+ installBoundary(pipelines.assistantOutput);
3922
+ installBoundary(pipelines.contextWindow);
2530
3923
  const compactor = container.resolve(TOKENS.Compactor);
2531
3924
  const resolvedCaps = await capabilitiesFor(modelsRegistry, config.provider, context.model).catch(
2532
3925
  () => void 0
@@ -2581,7 +3974,8 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
2581
3974
  maxIterations: config.tools.maxIterations,
2582
3975
  iterationTimeoutMs: config.tools.iterationTimeoutMs,
2583
3976
  executionStrategy: config.tools.defaultExecutionStrategy,
2584
- perIterationOutputCapBytes: config.tools.perIterationOutputCapBytes
3977
+ perIterationOutputCapBytes: config.tools.perIterationOutputCapBytes,
3978
+ confirmAwaiter: makeConfirmAwaiter(reader)
2585
3979
  });
2586
3980
  const mcpRegistry = new MCPRegistry({ toolRegistry, events, log: logger });
2587
3981
  if (config.features.mcp) {
@@ -2609,6 +4003,11 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
2609
4003
  const { default: createApi2 } = await Promise.resolve().then(() => (init_plugin_api_factory(), plugin_api_factory_exports));
2610
4004
  await loadPlugins(resolvedPlugins, {
2611
4005
  log: logger,
4006
+ // Each plugin's `configSchema` is validated against the matching
4007
+ // `Config.extensions[name]` subtree before its `setup()` runs.
4008
+ // The plugin then reads the same data through `api.config.extensions`
4009
+ // (or, once L1-B lands, via `ConfigStore.getExtension(name)`).
4010
+ pluginOptions: config.extensions ?? {},
2612
4011
  apiFactory: (plugin) => createApi2(plugin.name, {
2613
4012
  container,
2614
4013
  events,
@@ -2623,6 +4022,73 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
2623
4022
  });
2624
4023
  }
2625
4024
  }
4025
+ const buildPickableProviders = async () => {
4026
+ const overlay = config.providers ?? {};
4027
+ let catalog = [];
4028
+ try {
4029
+ catalog = await modelsRegistry.listProviders();
4030
+ } catch {
4031
+ }
4032
+ const catalogById = new Map(catalog.map((p) => [p.id, p]));
4033
+ const hasKey = (id) => {
4034
+ const entry = overlay[id];
4035
+ const envHit = catalogById.get(id)?.envVars.some((v) => !!process.env[v]);
4036
+ if (envHit) return true;
4037
+ if (!entry) return false;
4038
+ if (typeof entry.apiKey === "string" && entry.apiKey.length > 0) return true;
4039
+ if (Array.isArray(entry.apiKeys) && entry.apiKeys.some((k) => k?.apiKey)) return true;
4040
+ return false;
4041
+ };
4042
+ const seen = /* @__PURE__ */ new Set();
4043
+ const out = [];
4044
+ for (const [id, cfg] of Object.entries(overlay)) {
4045
+ if (!hasKey(id)) continue;
4046
+ seen.add(id);
4047
+ const catalogType = cfg.type && cfg.type !== id ? cfg.type : id;
4048
+ const inherited = catalogById.get(catalogType);
4049
+ const family = cfg.family ?? inherited?.family ?? "unsupported";
4050
+ if (family === "unsupported") continue;
4051
+ const models = cfg.models && cfg.models.length > 0 ? [...cfg.models] : (inherited?.models ?? []).map((m) => m.id);
4052
+ out.push({ id, family, models });
4053
+ }
4054
+ for (const p of catalog) {
4055
+ if (seen.has(p.id)) continue;
4056
+ if (p.family === "unsupported") continue;
4057
+ if (!hasKey(p.id)) continue;
4058
+ out.push({ id: p.id, family: p.family, models: p.models.map((m) => m.id) });
4059
+ }
4060
+ return out;
4061
+ };
4062
+ const switchProviderAndModel = (providerId, modelId) => {
4063
+ try {
4064
+ const newCfg = config.providers?.[providerId] ?? {
4065
+ type: providerId,
4066
+ apiKey: config.apiKey,
4067
+ baseUrl: config.baseUrl
4068
+ };
4069
+ const cfgWithType = { ...newCfg, type: providerId };
4070
+ const newProvider = config.features.modelsRegistry && providerRegistry.has(providerId) ? providerRegistry.create(cfgWithType) : makeProviderFromConfig(providerId, cfgWithType);
4071
+ context.provider = newProvider;
4072
+ context.model = modelId;
4073
+ config = patchConfig(config, { provider: providerId, model: modelId });
4074
+ configStore.update({ provider: providerId, model: modelId });
4075
+ return null;
4076
+ } catch (err) {
4077
+ return err instanceof Error ? err.message : String(err);
4078
+ }
4079
+ };
4080
+ const multiAgentHost = new MultiAgentHost({
4081
+ container,
4082
+ toolRegistry,
4083
+ providerRegistry,
4084
+ configStore,
4085
+ events,
4086
+ systemPromptBuilder: promptBuilder,
4087
+ session,
4088
+ tokenCounter,
4089
+ projectRoot,
4090
+ cwd
4091
+ });
2626
4092
  const slashCmds = buildBuiltinSlashCommands({
2627
4093
  registry: slashRegistry,
2628
4094
  toolRegistry,
@@ -2633,53 +4099,56 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
2633
4099
  renderer,
2634
4100
  memoryStore,
2635
4101
  context,
4102
+ metricsSink,
4103
+ healthRegistry,
4104
+ onSpawn: async (description) => {
4105
+ const { subagentId, taskId } = await multiAgentHost.spawn(description);
4106
+ return `Spawned subagent ${subagentId} for task ${taskId}. Use /agents to track progress.`;
4107
+ },
4108
+ onAgents: () => {
4109
+ const s = multiAgentHost.status();
4110
+ const lines = [s.summary];
4111
+ for (const p of s.pending) {
4112
+ lines.push(` pending ${p.taskId.slice(0, 8)} \u2192 ${p.description.slice(0, 60)}`);
4113
+ }
4114
+ for (const r of s.completed) {
4115
+ lines.push(
4116
+ ` ${r.status === "success" ? color.green("\u2713") : color.red("\u2717")} ${r.taskId.slice(0, 8)} ${r.iterations}it ${r.toolCalls}tc ${r.durationMs}ms`
4117
+ );
4118
+ }
4119
+ return lines.join("\n");
4120
+ },
2636
4121
  onExit: () => {
2637
4122
  void mcpRegistry.stopAll();
2638
4123
  },
2639
4124
  onClear: () => {
4125
+ if (flags.tui && !flags["no-tui"]) return;
2640
4126
  try {
2641
4127
  process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
2642
4128
  } catch {
2643
4129
  }
2644
4130
  },
2645
- onSwitchModel: (name) => {
2646
- context.model = name;
2647
- },
2648
- onSwitchProvider: (name) => {
2649
- try {
2650
- const newCfg = config.providers?.[name] ?? {
2651
- type: name,
2652
- apiKey: config.apiKey,
2653
- baseUrl: config.baseUrl
2654
- };
2655
- const newProvider = providerRegistry.create({ ...newCfg, type: name });
2656
- context.provider = newProvider;
2657
- config = Object.freeze({ ...config, provider: name });
2658
- } catch (err) {
2659
- renderer.writeError(
2660
- `Cannot switch to "${name}": ${err instanceof Error ? err.message : err}`
2661
- );
2662
- }
2663
- },
2664
4131
  onDiag: () => {
2665
4132
  const u = tokenCounter.total();
2666
4133
  const cost = tokenCounter.estimateCost();
2667
- renderer.write(
2668
- [
2669
- `${color.bold("WrongStack diag")}`,
2670
- ` provider: ${config.provider} / ${context.model}`,
2671
- ` projectRoot: ${projectRoot}`,
2672
- ` tokens: in ${u.input} out ${u.output} cacheR ${u.cacheRead ?? 0}`,
2673
- ` cost: $${cost.total.toFixed(4)}`,
2674
- ` tools: ${toolRegistry.list().length}`,
2675
- ` mcpServers: ${mcpRegistry.list().length}`,
2676
- ""
2677
- ].join("\n")
2678
- );
4134
+ const errSection = errorRing.length === 0 ? [] : [
4135
+ "",
4136
+ `${color.bold("Recent errors")} (last ${errorRing.length}):`,
4137
+ ...errorRing.map((e) => ` [${e.ts}] ${e.phase} ${e.code} \u2014 ${e.message}`)
4138
+ ];
4139
+ const liveCfg = configStore.get();
4140
+ return [
4141
+ `${color.bold("WrongStack diag")}`,
4142
+ ` provider: ${liveCfg.provider} / ${context.model}`,
4143
+ ` projectRoot: ${projectRoot}`,
4144
+ ` tokens: in ${u.input} out ${u.output} cacheR ${u.cacheRead ?? 0}`,
4145
+ ` cost: $${cost.total.toFixed(4)}`,
4146
+ ` tools: ${toolRegistry.list().length}`,
4147
+ ` mcpServers: ${mcpRegistry.list().length}`,
4148
+ ...errSection
4149
+ ].join("\n");
2679
4150
  },
2680
- onStats: () => {
2681
- stats.render(renderer);
2682
- }
4151
+ onStats: () => stats.format()
2683
4152
  });
2684
4153
  for (const cmd of slashCmds) slashRegistry.register(cmd);
2685
4154
  let code = 0;
@@ -2715,16 +4184,27 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
2715
4184
  const json = JSON.stringify({
2716
4185
  status: result.status,
2717
4186
  finalText: result.finalText ?? null,
2718
- error: result.error instanceof Error ? result.error.message : result.error ?? null,
4187
+ error: result.error ? {
4188
+ code: result.error.code,
4189
+ subsystem: result.error.subsystem,
4190
+ severity: result.error.severity,
4191
+ recoverable: result.error.recoverable,
4192
+ message: result.error.message,
4193
+ context: result.error.context ?? null
4194
+ } : null,
2719
4195
  usage
2720
4196
  });
2721
4197
  process.stdout.write(json + "\n");
2722
4198
  } else {
2723
4199
  if (result.status === "failed") {
2724
4200
  code = 1;
2725
- renderer.writeError(
2726
- "Failed: " + (result.error instanceof Error ? result.error.message : String(result.error))
2727
- );
4201
+ const err = result.error;
4202
+ if (err) {
4203
+ const tag = err.recoverable ? " (recoverable)" : "";
4204
+ renderer.writeError(`Failed [${err.severity}]${tag}: ${err.describe()}`);
4205
+ } else {
4206
+ renderer.writeError("Failed.");
4207
+ }
2728
4208
  } else if (result.status === "aborted") {
2729
4209
  code = 130;
2730
4210
  renderer.writeWarning("Aborted.");
@@ -2735,13 +4215,16 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
2735
4215
  if (result.finalText) renderer.write("\n" + result.finalText + "\n");
2736
4216
  renderer.write(
2737
4217
  "\n" + color.dim(
2738
- `[in: ${fmtTok4(usage.input)} out: ${fmtTok4(usage.output)} iters: ${usage.iterations} cost: ${usage.cost.toFixed(4)} ${(usage.elapsedMs / 1e3).toFixed(1)}s]`
4218
+ `[in: ${fmtTok(usage.input)} out: ${fmtTok(usage.output)} iters: ${usage.iterations} cost: ${usage.cost.toFixed(4)} ${(usage.elapsedMs / 1e3).toFixed(1)}s]`
2739
4219
  ) + "\n"
2740
4220
  );
2741
4221
  }
2742
4222
  } else if (flags.tui && !flags["no-tui"]) {
2743
4223
  const { runTui } = await import('@wrongstack/tui');
2744
4224
  renderer.setSilent(true);
4225
+ const banneredFamily = savedProviderCfg?.family ?? resolvedProvider?.family;
4226
+ const banneredKey = savedProviderCfg?.apiKey ?? config.apiKey ?? (resolvedProvider?.envVars ?? savedProviderCfg?.envVars ?? []).map((v) => process.env[v]).find((v) => !!v);
4227
+ const banneredKeyTail = banneredKey && banneredKey.length >= 3 ? banneredKey.slice(-3) : void 0;
2745
4228
  try {
2746
4229
  code = await runTui({
2747
4230
  agent,
@@ -2755,6 +4238,10 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
2755
4238
  yolo: !!config.yolo,
2756
4239
  appVersion: CLI_VERSION,
2757
4240
  provider: config.provider,
4241
+ family: banneredFamily,
4242
+ keyTail: banneredKeyTail,
4243
+ getPickableProviders: buildPickableProviders,
4244
+ switchProviderAndModel,
2758
4245
  effectiveMaxContext,
2759
4246
  // Opt-in: alt-screen disables the terminal's native scrollback,
2760
4247
  // so we default to false. `--no-alt-screen` is kept as a no-op
@@ -2828,11 +4315,6 @@ async function promptRecovery(reader, renderer, abandoned, autoRecover) {
2828
4315
  );
2829
4316
  return answer;
2830
4317
  }
2831
- function fmtTok4(n) {
2832
- if (n < 1e3) return String(n);
2833
- if (n < 1e6) return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
2834
- return `${(n / 1e6).toFixed(1)}M`;
2835
- }
2836
4318
  var isMain = import.meta.url === `file://${process.argv[1]?.replace(/\\/g, "/")}` || process.argv[1]?.endsWith("/cli/dist/index.js") || process.argv[1]?.endsWith("\\cli\\dist\\index.js");
2837
4319
  if (isMain) {
2838
4320
  main(process.argv.slice(2)).then(
@@ -2848,6 +4330,6 @@ if (isMain) {
2848
4330
  );
2849
4331
  }
2850
4332
 
2851
- export { main };
4333
+ export { CLI_VERSION, main };
2852
4334
  //# sourceMappingURL=index.js.map
2853
4335
  //# sourceMappingURL=index.js.map