@wrongstack/cli 0.89.1 → 0.104.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { color, writeErr, renderProgress, SpecStore, TaskGraphStore, analyzeCriticalPath, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, DefaultTaskStore, TaskTracker, renderTaskGraph, DefaultSecretScrubber, DefaultPathResolver, TOKENS, mergeCustomModelDefs, DefaultSystemPromptBuilder, makeAutonomyPromptContributor, ToolRegistry, createContextManagerTool, EventBus, resolveSessionLoggingConfig, createSessionEventBridge, HookRegistry, HookRunner, SlashCommandRegistry, BrainDecisionQueue, ObservableBrainArbiter, HumanEscalatingBrainArbiter, DefaultBrainArbiter, createDelegateTool, FLEET_ROSTER, createMcpControlTool, SpecVersioning, atomicWrite, DefaultLogger, DefaultModelsRegistry, isStdinTTY, writeOut, runProviderWithRetry, ReplayLogStore, ReplayProviderRunner, ProviderRegistry, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, DEFAULT_SESSION_PRUNE_DAYS, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, resolveContextWindowPolicy, resolveAuditLevel, AutoCompactionMiddleware, estimateRequestTokensCalibrated, Agent, loadPlugins, FleetManager, makeDirectorSessionFactory, Director, makeFleetEmitTool, makeFleetStatusTool, resolveModelMatrix, DEFAULT_SUBAGENT_BASELINE, AutoApprovePermissionPolicy, PhaseStore, AutoPhasePlanner, PhaseGraphBuilder, WorktreeManager, PhaseOrchestrator, makeLLMClassifier, ParallelEternalEngine, EternalAutonomyEngine, allServers as allServers$1, decryptConfigSecrets as decryptConfigSecrets$1, encryptConfigSecrets as encryptConfigSecrets$1, bootConfig as bootConfig$1, setOutputLineGuard, setRawMode, DefaultSessionReader, resolveWstackPaths, ToolAuditLog, DefaultSessionRewinder, DefaultSessionStore, DefaultPluginAPI, ProviderError, makeAgentSubagentRunner, NULL_FLEET_BUS, buildChildEnv, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, AGENTS_BY_PHASE, dispatchAgent, formatTodosList, SessionRecovery, loadGoal, goalFilePath, summarizeUsage, saveGoal, emptyGoal, buildGoalPreamble, formatGoal, pendingBtwCount, setBtwNote, MATRIX_PHASE_KEYS, AGENT_CATALOG, matrixKeyKind, onResize, ERROR_CODES, InputBuilder, FsError } from '@wrongstack/core';
3
- import * as path8 from 'path';
4
- import { join } from 'path';
2
+ import { color, writeErr, renderProgress, SpecStore, TaskGraphStore, analyzeCriticalPath, getTemplate, listTemplates, templateToMarkdown, SpecParser, renderSpecAnalysis, AISpecBuilder, expectDefined, DefaultTaskStore, TaskTracker, renderTaskGraph, DefaultSecretScrubber, DefaultPathResolver, EventBus, TOKENS, mergeCustomModelDefs, DefaultSystemPromptBuilder, makeAutonomyPromptContributor, ToolRegistry, createContextManagerTool, resolveSessionLoggingConfig, createSessionEventBridge, HookRegistry, HookRunner, SlashCommandRegistry, SessionMemoryConsolidator, BrainDecisionQueue, ObservableBrainArbiter, HumanEscalatingBrainArbiter, DefaultBrainArbiter, createDelegateTool, FLEET_ROSTER, createMcpControlTool, SpecVersioning, atomicWrite, DefaultLogger, DefaultModelsRegistry, isStdinTTY, writeOut, runProviderWithRetry, ReplayLogStore, ReplayProviderRunner, ProviderRegistry, InMemoryMetricsSink, wireMetricsToEvents, DefaultHealthRegistry, startMetricsServer, DEFAULT_SESSION_PRUNE_DAYS, RecoveryLock, DefaultAttachmentStore, QueueStore, Context, loadTodosCheckpoint, attachTodosCheckpoint, loadDirectorState, loadPlan, createDefaultPipelines, resolveContextWindowPolicy, resolveAuditLevel, AutoCompactionMiddleware, estimateRequestTokensCalibrated, Agent, loadPlugins, FleetManager, makeDirectorSessionFactory, Director, makeFleetEmitTool, makeFleetStatusTool, resolveModelMatrix, DEFAULT_SUBAGENT_BASELINE, AutoApprovePermissionPolicy, PhaseStore, AutoPhasePlanner, PhaseGraphBuilder, WorktreeManager, PhaseOrchestrator, makeLLMClassifier, ParallelEternalEngine, EternalAutonomyEngine, allServers as allServers$1, decryptConfigSecrets as decryptConfigSecrets$1, encryptConfigSecrets as encryptConfigSecrets$1, bootConfig as bootConfig$1, setOutputLineGuard, setRawMode, DefaultSessionReader, resolveWstackPaths, ToolAuditLog, DefaultSessionRewinder, DefaultSessionStore, DefaultPluginAPI, ProviderError, makeAgentSubagentRunner, NULL_FLEET_BUS, buildChildEnv, formatContextWindowModeList, repairToolUseAdjacency, getContextWindowMode, AGENTS_BY_PHASE, dispatchAgent, formatTodosList, loadTasks, emptyTaskFile, saveTasks, formatTaskProgress, formatTaskList, SessionRecovery, loadGoal, goalFilePath, summarizeUsage, saveGoal, formatGoal, emptyGoal, buildGoalPreamble, pendingBtwCount, setBtwNote, MATRIX_PHASE_KEYS, AGENT_CATALOG, matrixKeyKind, phaseForRole, onResize, ERROR_CODES, InputBuilder, FsError } from '@wrongstack/core';
5
3
  import * as fsp4 from 'fs/promises';
6
4
  import { DefaultSecretVault, decryptConfigSecrets, encryptConfigSecrets, isSecretField } from '@wrongstack/core/security';
5
+ import * as path8 from 'path';
6
+ import { join } from 'path';
7
7
  import { createRequire } from 'module';
8
8
  import * as os2 from 'os';
9
9
  import os2__default from 'os';
@@ -15,10 +15,10 @@ import { spawn } from 'child_process';
15
15
  import { MCPRegistry, MCPServer, serveHttp, serveStdio } from '@wrongstack/mcp';
16
16
  import { capabilitiesFor, buildProviderFactoriesFromRegistry, makeProviderFromConfig } from '@wrongstack/providers';
17
17
  import { createDefaultContainer, routeImagesForModel, readClipboardImage } from '@wrongstack/runtime';
18
- import { builtinToolsPack, rememberTool, forgetTool, runStartupIndex, isIndexableFile, enqueueReindex, cancelPendingReindexes } from '@wrongstack/tools';
18
+ import { builtinToolsPack, rememberTool, forgetTool, searchMemoryTool, relatedMemoryTool, runStartupIndex, isIndexableFile, enqueueReindex, cancelPendingReindexes } from '@wrongstack/tools';
19
19
  import { fileURLToPath } from 'url';
20
20
  import * as readline from 'readline';
21
- import * as fs12 from 'fs';
21
+ import * as fs13 from 'fs';
22
22
  import { writeFileSync, existsSync, readFileSync } from 'fs';
23
23
  import { WrongStackACPServer } from '@wrongstack/acp/agent';
24
24
  import { ACP_AGENT_COMMANDS, makeACPSubagentRunner, makeACPSubagentRunnerWithStop } from '@wrongstack/acp';
@@ -28,9 +28,7 @@ import { ToolExecutor } from '@wrongstack/core/execution';
28
28
  import { createToolVisionAdapters } from '@wrongstack/runtime/vision';
29
29
 
30
30
  var __defProp = Object.defineProperty;
31
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
32
31
  var __getOwnPropNames = Object.getOwnPropertyNames;
33
- var __hasOwnProp = Object.prototype.hasOwnProperty;
34
32
  var __esm = (fn, res) => function __init() {
35
33
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
36
34
  };
@@ -38,15 +36,102 @@ var __export = (target, all) => {
38
36
  for (var name in all)
39
37
  __defProp(target, name, { get: all[name], enumerable: true });
40
38
  };
41
- var __copyProps = (to, from, except, desc) => {
42
- if (from && typeof from === "object" || typeof from === "function") {
43
- for (let key of __getOwnPropNames(from))
44
- if (!__hasOwnProp.call(to, key) && key !== except)
45
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
39
+ function normalizeKeys(cfg) {
40
+ if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
41
+ return cfg.apiKeys.map((k) => ({ ...k }));
46
42
  }
47
- return to;
48
- };
49
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
43
+ if (typeof cfg.apiKey === "string" && cfg.apiKey.length > 0) {
44
+ return [{ label: "default", apiKey: cfg.apiKey, createdAt: "" }];
45
+ }
46
+ return [];
47
+ }
48
+ function writeKeysBack(cfg, keys) {
49
+ if (keys.length === 0) {
50
+ delete cfg.apiKeys;
51
+ delete cfg.apiKey;
52
+ delete cfg.activeKey;
53
+ return;
54
+ }
55
+ cfg.apiKeys = keys;
56
+ const active = keys.find((k) => k.label === cfg.activeKey) ?? expectDefined(keys[0]);
57
+ cfg.apiKey = active.apiKey;
58
+ if (!cfg.activeKey || !keys.some((k) => k.label === cfg.activeKey)) {
59
+ cfg.activeKey = active.label;
60
+ }
61
+ }
62
+ function activeLabel(cfg, keys) {
63
+ if (cfg.activeKey && keys.some((k) => k.label === cfg.activeKey)) return cfg.activeKey;
64
+ return keys[0]?.label;
65
+ }
66
+ function maskedKey(key) {
67
+ if (!key) return color.dim("\u2014");
68
+ if (key.length <= 8) return color.dim("\u2022".repeat(key.length));
69
+ const head = key.slice(0, 4);
70
+ const tail = key.slice(-4);
71
+ return `${color.dim(head + "\u2026")}${tail}`;
72
+ }
73
+ function nowIso() {
74
+ return (/* @__PURE__ */ new Date()).toISOString();
75
+ }
76
+ async function loadConfigProviders(configPath2, vault, opts) {
77
+ const warn = opts?.warn;
78
+ let raw;
79
+ try {
80
+ raw = await fsp4.readFile(configPath2, "utf8");
81
+ } catch (err) {
82
+ if (err.code !== "ENOENT") {
83
+ warn?.(`Could not read ${configPath2}: ${err.message}. Treating as empty.`);
84
+ }
85
+ return {};
86
+ }
87
+ let parsed;
88
+ try {
89
+ parsed = JSON.parse(raw);
90
+ } catch (err) {
91
+ warn?.(`Config at ${configPath2} is not valid JSON: ${err.message}`);
92
+ return {};
93
+ }
94
+ const decrypted = decryptConfigSecrets(parsed, vault);
95
+ return decrypted.providers ?? {};
96
+ }
97
+ async function mutateConfigProviders(configPath2, vault, mutator) {
98
+ let raw;
99
+ let fileExists = true;
100
+ try {
101
+ raw = await fsp4.readFile(configPath2, "utf8");
102
+ } catch (err) {
103
+ if (err.code !== "ENOENT") {
104
+ throw new Error(
105
+ `Refusing to mutate ${configPath2}: ${err.message}`,
106
+ { cause: err }
107
+ );
108
+ }
109
+ fileExists = false;
110
+ raw = "{}";
111
+ }
112
+ let parsed;
113
+ try {
114
+ parsed = JSON.parse(raw);
115
+ } catch (err) {
116
+ if (fileExists) {
117
+ throw new Error(
118
+ `Refusing to overwrite corrupt config at ${configPath2} (${err.message}). Fix or move the file aside before retrying.`,
119
+ { cause: err }
120
+ );
121
+ }
122
+ parsed = {};
123
+ }
124
+ const decrypted = decryptConfigSecrets(parsed, vault);
125
+ const providers = decrypted.providers ?? {};
126
+ mutator(providers);
127
+ decrypted.providers = providers;
128
+ const encrypted = encryptConfigSecrets(decrypted, vault);
129
+ await atomicWrite(configPath2, JSON.stringify(encrypted, null, 2), { mode: 384 });
130
+ }
131
+ var init_provider_config_utils = __esm({
132
+ "src/provider-config-utils.ts"() {
133
+ }
134
+ });
50
135
  function getSessionState(ctx) {
51
136
  if (!ctx) return sddState;
52
137
  let state = ctx.meta[SDD_META_KEY];
@@ -132,12 +217,6 @@ var init_state = __esm({
132
217
  sddState = new SDDState();
133
218
  }
134
219
  });
135
- function expectDefined3(value) {
136
- if (value === null || value === void 0) {
137
- throw new Error("Expected value to be defined");
138
- }
139
- return value;
140
- }
141
220
  function formatElapsed(ms) {
142
221
  if (ms < 1e3) return `${ms}ms`;
143
222
  const s = Math.floor(ms / 1e3);
@@ -201,7 +280,7 @@ function getCurrentTask() {
201
280
  if (!tracker) return null;
202
281
  const nodes = tracker.getAllNodes({ status: ["in_progress"] });
203
282
  if (nodes.length === 0) return null;
204
- const n = expectDefined3(nodes[0]);
283
+ const n = expectDefined(nodes[0]);
205
284
  return { id: n.id, title: n.title, description: n.description, priority: n.priority, estimateHours: n.estimateHours ?? 0, tags: n.tags ?? [], startedAt: n.startedAt };
206
285
  }
207
286
  function advanceToNextTask() {
@@ -240,7 +319,7 @@ function renderTaskListWithProgress() {
240
319
  return (order[a.status] ?? 6) - (order[b.status] ?? 6);
241
320
  });
242
321
  for (let i = 0; i < sorted.length; i++) {
243
- const n = expectDefined3(sorted[i]);
322
+ const n = expectDefined(sorted[i]);
244
323
  const status = n.status === "completed" ? "\u2705" : n.status === "in_progress" ? "\u{1F504}" : n.status === "failed" ? "\u274C" : n.status === "blocked" ? "\u{1F6AB}" : n.status === "review" ? "\u{1F441}" : "\u23F3";
245
324
  const title = n.title.length > 50 ? n.title.slice(0, 49) + "\u2026" : n.title;
246
325
  let elapsed = "";
@@ -254,7 +333,7 @@ function getCurrentExecutingContext() {
254
333
  if (!tracker) return null;
255
334
  const nodes = tracker.getAllNodes({ status: ["in_progress"] });
256
335
  if (nodes.length === 0) return null;
257
- const n = expectDefined3(nodes[0]);
336
+ const n = expectDefined(nodes[0]);
258
337
  const elapsed = n.startedAt ? ` \xB7 elapsed: ${formatElapsed(Date.now() - n.startedAt)}` : "";
259
338
  const progress = tracker.getProgress();
260
339
  return [
@@ -562,12 +641,6 @@ __export(sdd_exports, {
562
641
  trySaveSpecFromAIOutput: () => trySaveSpecFromAIOutput,
563
642
  trySaveTasksFromAIOutput: () => trySaveTasksFromAIOutput
564
643
  });
565
- function expectDefined4(value) {
566
- if (value === null || value === void 0) {
567
- throw new Error("Expected value to be defined");
568
- }
569
- return value;
570
- }
571
644
  function getTaskTracker() {
572
645
  return getTaskTrackerExport();
573
646
  }
@@ -633,7 +706,7 @@ function buildSddCommand(opts) {
633
706
  }));
634
707
  sddState.setSessionStartTime(Date.now());
635
708
  sddState.setPhaseStartTime(Date.now());
636
- const builder = expectDefined4(sddState.getBuilder());
709
+ const builder = expectDefined(sddState.getBuilder());
637
710
  builder.startSession(title);
638
711
  const aiPrompt = builder.getAIPrompt();
639
712
  return {
@@ -875,7 +948,7 @@ Start executing the tasks one by one.`
875
948
  return (order[a.status] ?? 6) - (order[b.status] ?? 6);
876
949
  });
877
950
  for (let i = 0; i < sorted.length; i++) {
878
- const n = expectDefined4(sorted[i]);
951
+ const n = expectDefined(sorted[i]);
879
952
  const status = n.status === "completed" ? "\u2705" : n.status === "in_progress" ? "\u{1F504}" : n.status === "failed" ? "\u274C" : n.status === "blocked" ? "\u{1F6AB}" : n.status === "review" ? "\u{1F441}" : "\u23F3";
880
953
  const num = `${i + 1}`.padStart(3);
881
954
  const prio = n.priority.slice(0, 4).padEnd(5);
@@ -883,7 +956,7 @@ Start executing the tasks one by one.`
883
956
  const elapsed = n.status === "in_progress" && n.startedAt ? ` (${formatElapsed(Date.now() - n.startedAt)})` : "";
884
957
  lines.push(` ${num} ${status} ${prio} ${title}${elapsed}`);
885
958
  if (n.description && n.status !== "completed") {
886
- const first = expectDefined4(n.description.split("\n")[0]);
959
+ const first = expectDefined(n.description.split("\n")[0]);
887
960
  const truncated = first.length > 42 ? first.slice(0, 41) + "\u2026" : first;
888
961
  lines.push(` \u21B3 ${truncated}`);
889
962
  }
@@ -1050,7 +1123,7 @@ Start executing the tasks one by one.`
1050
1123
  if (completed.length === 0) {
1051
1124
  return { message: "No completed tasks to undo." };
1052
1125
  }
1053
- const last = expectDefined4(completed[completed.length - 1]);
1126
+ const last = expectDefined(completed[completed.length - 1]);
1054
1127
  undoTracker.updateNodeStatus(last.id, "pending");
1055
1128
  const progress = undoTracker.getProgress();
1056
1129
  return {
@@ -1100,7 +1173,7 @@ Start executing the tasks one by one.`
1100
1173
  ` \u{1F504} ${next.title}`
1101
1174
  ];
1102
1175
  if (next.description) {
1103
- const first = expectDefined4(next.description.split("\n")[0]);
1176
+ const first = expectDefined(next.description.split("\n")[0]);
1104
1177
  lines.push(` \u21B3 ${first}`);
1105
1178
  }
1106
1179
  const taskElapsed = next.startedAt ? ` \u23F1 ${formatElapsed(Date.now() - next.startedAt)}` : "";
@@ -1212,7 +1285,7 @@ Start executing the tasks one by one.`
1212
1285
  return (order[a.status] ?? 6) - (order[b.status] ?? 6);
1213
1286
  });
1214
1287
  for (let i = 0; i < sorted2.length; i++) {
1215
- const n = expectDefined4(sorted2[i]);
1288
+ const n = expectDefined(sorted2[i]);
1216
1289
  const status = n.status === "completed" ? "\u2705" : n.status === "in_progress" ? "\u{1F504}" : n.status === "failed" ? "\u274C" : n.status === "blocked" ? "\u{1F6AB}" : n.status === "review" ? "\u{1F441}" : "\u23F3";
1217
1290
  lines2.push(`${i + 1}. ${status} [${n.priority}] ${n.title}`);
1218
1291
  }
@@ -1237,7 +1310,7 @@ Start executing the tasks one by one.`
1237
1310
  return (order[a.status] ?? 6) - (order[b.status] ?? 6);
1238
1311
  });
1239
1312
  for (let i = 0; i < sorted.length; i++) {
1240
- const n = expectDefined4(sorted[i]);
1313
+ const n = expectDefined(sorted[i]);
1241
1314
  const status = n.status === "completed" ? "\u2705" : n.status === "in_progress" ? "\u{1F504}" : n.status === "failed" ? "\u274C" : n.status === "blocked" ? "\u{1F6AB}" : n.status === "review" ? "\u{1F441}" : "\u23F3";
1242
1315
  lines.push(`${i + 1}. ${status} [${n.priority}] ${n.title}`);
1243
1316
  }
@@ -1285,7 +1358,7 @@ Start executing the tasks one by one.`
1285
1358
  maxQuestions: 10,
1286
1359
  sessionPath
1287
1360
  }));
1288
- const resumeBuilder = expectDefined4(sddState.getBuilder());
1361
+ const resumeBuilder = expectDefined(sddState.getBuilder());
1289
1362
  const loaded = await resumeBuilder.loadSession();
1290
1363
  if (!loaded) {
1291
1364
  sddState.setBuilder(null);
@@ -1519,108 +1592,6 @@ var init_sdd = __esm({
1519
1592
  init_rendering();
1520
1593
  }
1521
1594
  });
1522
- function expectDefined7(value) {
1523
- if (value === null || value === void 0) {
1524
- throw new Error("Expected value to be defined");
1525
- }
1526
- return value;
1527
- }
1528
- function normalizeKeys(cfg) {
1529
- if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
1530
- return cfg.apiKeys.map((k) => ({ ...k }));
1531
- }
1532
- if (typeof cfg.apiKey === "string" && cfg.apiKey.length > 0) {
1533
- return [{ label: "default", apiKey: cfg.apiKey, createdAt: "" }];
1534
- }
1535
- return [];
1536
- }
1537
- function writeKeysBack(cfg, keys) {
1538
- if (keys.length === 0) {
1539
- delete cfg.apiKeys;
1540
- delete cfg.apiKey;
1541
- delete cfg.activeKey;
1542
- return;
1543
- }
1544
- cfg.apiKeys = keys;
1545
- const active = keys.find((k) => k.label === cfg.activeKey) ?? expectDefined7(keys[0]);
1546
- cfg.apiKey = active.apiKey;
1547
- if (!cfg.activeKey || !keys.some((k) => k.label === cfg.activeKey)) {
1548
- cfg.activeKey = active.label;
1549
- }
1550
- }
1551
- function activeLabel(cfg, keys) {
1552
- if (cfg.activeKey && keys.some((k) => k.label === cfg.activeKey)) return cfg.activeKey;
1553
- return keys[0]?.label;
1554
- }
1555
- function maskedKey(key) {
1556
- if (!key) return color.dim("\u2014");
1557
- if (key.length <= 8) return color.dim("\u2022".repeat(key.length));
1558
- const head = key.slice(0, 4);
1559
- const tail = key.slice(-4);
1560
- return `${color.dim(head + "\u2026")}${tail}`;
1561
- }
1562
- function nowIso() {
1563
- return (/* @__PURE__ */ new Date()).toISOString();
1564
- }
1565
- async function loadConfigProviders(configPath2, vault, opts) {
1566
- const warn = opts?.warn;
1567
- let raw;
1568
- try {
1569
- raw = await fsp4.readFile(configPath2, "utf8");
1570
- } catch (err) {
1571
- if (err.code !== "ENOENT") {
1572
- warn?.(`Could not read ${configPath2}: ${err.message}. Treating as empty.`);
1573
- }
1574
- return {};
1575
- }
1576
- let parsed;
1577
- try {
1578
- parsed = JSON.parse(raw);
1579
- } catch (err) {
1580
- warn?.(`Config at ${configPath2} is not valid JSON: ${err.message}`);
1581
- return {};
1582
- }
1583
- const decrypted = decryptConfigSecrets(parsed, vault);
1584
- return decrypted.providers ?? {};
1585
- }
1586
- async function mutateConfigProviders(configPath2, vault, mutator) {
1587
- let raw;
1588
- let fileExists = true;
1589
- try {
1590
- raw = await fsp4.readFile(configPath2, "utf8");
1591
- } catch (err) {
1592
- if (err.code !== "ENOENT") {
1593
- throw new Error(
1594
- `Refusing to mutate ${configPath2}: ${err.message}`,
1595
- { cause: err }
1596
- );
1597
- }
1598
- fileExists = false;
1599
- raw = "{}";
1600
- }
1601
- let parsed;
1602
- try {
1603
- parsed = JSON.parse(raw);
1604
- } catch (err) {
1605
- if (fileExists) {
1606
- throw new Error(
1607
- `Refusing to overwrite corrupt config at ${configPath2} (${err.message}). Fix or move the file aside before retrying.`,
1608
- { cause: err }
1609
- );
1610
- }
1611
- parsed = {};
1612
- }
1613
- const decrypted = decryptConfigSecrets(parsed, vault);
1614
- const providers = decrypted.providers ?? {};
1615
- mutator(providers);
1616
- decrypted.providers = providers;
1617
- const encrypted = encryptConfigSecrets(decrypted, vault);
1618
- await atomicWrite(configPath2, JSON.stringify(encrypted, null, 2), { mode: 384 });
1619
- }
1620
- var init_provider_config_utils = __esm({
1621
- "src/provider-config-utils.ts"() {
1622
- }
1623
- });
1624
1595
 
1625
1596
  // src/update-check.ts
1626
1597
  var update_check_exports = {};
@@ -2337,7 +2308,7 @@ async function runWebUI(opts) {
2337
2308
  const keys = normalizeKeys(existing);
2338
2309
  const existingIdx = keys.findIndex((k) => k.label === label);
2339
2310
  if (existingIdx >= 0) {
2340
- keys[existingIdx] = { ...expectDefined7(keys[existingIdx]), apiKey, createdAt: nowIso() };
2311
+ keys[existingIdx] = { ...expectDefined(keys[existingIdx]), apiKey, createdAt: nowIso() };
2341
2312
  } else {
2342
2313
  keys.push({ label, apiKey, createdAt: nowIso() });
2343
2314
  }
@@ -2471,14 +2442,6 @@ try {
2471
2442
  if (corePkg.wrongstackApiVersion) API_VERSION = corePkg.wrongstackApiVersion;
2472
2443
  } catch {
2473
2444
  }
2474
-
2475
- // src/slash-commands/commit-llm.ts
2476
- function expectDefined(value) {
2477
- if (value === null || value === void 0) {
2478
- throw new Error("Expected value to be defined");
2479
- }
2480
- return value;
2481
- }
2482
2445
  async function generateCommitMessageWithLLM(diff, opts) {
2483
2446
  const systemPrompt = "You are a helpful assistant that generates concise, conventional-commit-formatted git commit messages. Analyze the provided diff and output ONLY the commit message (no explanation, no quotes). Format: <type>(<scope>): <short description> \u2014 <type> is one of: feat, fix, docs, style, refactor, test, chore, perf, ci, build, temp. If the diff contains multiple unrelated changes, pick the most important one. Keep the description under 72 characters. Example: feat(cli): add /commit LLM integration";
2484
2447
  const userPrompt = `Here is the git diff:
@@ -2925,212 +2888,147 @@ function estimateTokens(messages) {
2925
2888
  }
2926
2889
  return total;
2927
2890
  }
2928
- function buildAutonomyCommand(opts) {
2891
+
2892
+ // src/slash-commands/auth.ts
2893
+ init_provider_config_utils();
2894
+ function buildAuthCommand(opts) {
2895
+ const help = [
2896
+ "Usage:",
2897
+ " /auth Show saved providers and key status",
2898
+ " /auth status <provider> Show detail for one provider",
2899
+ " /auth open Show how to launch the interactive menu",
2900
+ "",
2901
+ "Run `wstack auth` for the full interactive key manager (add, edit, delete)."
2902
+ ].join("\n");
2929
2903
  return {
2930
- name: "autonomy",
2931
- category: "Agent",
2932
- description: "Toggle or query autonomy mode (self-driving agent).",
2933
- help: [
2934
- "Usage:",
2935
- " /autonomy Show current autonomy status",
2936
- " /autonomy off Disabled \u2014 agent stops after each turn (default)",
2937
- " /autonomy suggest Show next-step suggestions after each turn",
2938
- " /autonomy on Auto-continue \u2014 agent picks next step and proceeds",
2939
- " /autonomy eternal Goal-driven loop \u2014 runs forever against /goal",
2940
- " (prompts to confirm an existing goal; `--keep` to skip prompt)",
2941
- " /autonomy parallel Parallel mode \u2014 4-8 agents per tick, fan-out parallelism",
2942
- " (prompts to confirm an existing goal; `--keep` to skip prompt)",
2943
- " /autonomy stop Stop eternal mode (no-op for other modes)",
2944
- " /autonomy toggle Cycle: off \u2192 suggest \u2192 auto \u2192 eternal \u2192 parallel \u2192 off",
2945
- "",
2946
- "Modes:",
2947
- " off \u2014 Normal interactive mode. Agent stops and waits.",
2948
- " suggest \u2014 After each turn, agent suggests next steps. You pick.",
2949
- " auto \u2014 After each turn, agent picks the best next step and continues.",
2950
- " Runs indefinitely until you press Esc or Ctrl+C.",
2951
- " eternal \u2014 Goal-driven sense/decide/execute/reflect loop. Requires /goal.",
2952
- " Force-enables regular YOLO; destructive-gated calls still use",
2953
- " the permission flow. Runs until /autonomy stop or Ctrl+C twice.",
2954
- " parallel \u2014 Fan-out 4\u20138 subagents per tick. Each tick decomposes the goal,",
2955
- " spawns N agents, awaits results, aggregates. Requires /goal.",
2956
- " Force-enables regular YOLO; destructive-gated calls still use",
2957
- " the permission flow. Runs until /autonomy stop or Ctrl+C twice.",
2958
- "",
2959
- "Eternal stage flow: decide \u2192 execute \u2192 reflect \u2192 sleep | paused | stopped",
2960
- "Stage shown in real-time. Use /goal pause to pause, /goal resume to continue.",
2961
- "",
2962
- "In auto/eternal/parallel modes the agent works autonomously. Press Esc to redirect,",
2963
- "Ctrl+C to stop the active iteration. /autonomy stop ends the eternal loop."
2964
- ].join("\n"),
2904
+ name: "auth",
2905
+ category: "Config",
2906
+ description: "View API key status. Run wstack auth for the full interactive key manager.",
2907
+ help,
2965
2908
  async run(args) {
2966
- const parts = args.trim().toLowerCase().split(/\s+/).filter(Boolean);
2967
- const arg = parts[0] ?? "";
2968
- const modifiers = parts.slice(1);
2969
- if (!opts.onAutonomy) {
2970
- const msg2 = "Autonomy mode is not available in this session.";
2971
- opts.renderer.writeWarning(msg2);
2972
- return { message: msg2 };
2973
- }
2974
- if (!arg || arg === "status") {
2975
- const current = opts.onAutonomy();
2976
- const labels2 = {
2977
- off: `${color.green("OFF")} ${color.dim("(agent stops after each turn)")}`,
2978
- suggest: `${color.cyan("SUGGEST")} ${color.dim("(shows next-step suggestions)")}`,
2979
- auto: `${color.yellow("AUTO")} ${color.dim("(self-driving \u2014 Esc to redirect, Ctrl+C to stop)")}`,
2980
- eternal: `${color.red("ETERNAL")} ${color.dim("(goal-driven loop \u2014 YOLO, until /autonomy stop)")}`,
2981
- "eternal-parallel": `${color.magenta("PARALLEL")} ${color.dim("(4-8 subagents per tick \u2014 fan-out, until /autonomy stop)")}`
2909
+ const parts = args.trim().split(/\s+/).filter(Boolean);
2910
+ const sub = (parts[0] ?? "").toLowerCase();
2911
+ if (sub === "help" || sub === "--help") {
2912
+ return { message: this.help ?? "" };
2913
+ }
2914
+ if (!opts.paths?.globalConfig) {
2915
+ return { message: `${color.red("Error")} auth not available \u2014 config path missing.` };
2916
+ }
2917
+ if (sub === "open") {
2918
+ return {
2919
+ message: [
2920
+ `${color.bold("API Key Manager")}`,
2921
+ "",
2922
+ ` Run ${color.bold("wstack auth")} in a separate terminal to manage API keys interactively:`,
2923
+ "",
2924
+ ` ${color.cyan("wstack auth")} Interactive menu`,
2925
+ ` ${color.cyan("wstack auth <provider>")} Add a key for <provider>`,
2926
+ ` ${color.cyan("wstack auth <p> --label <l>")} Add with custom label`,
2927
+ "",
2928
+ color.dim(" The interactive menu requires standard input (readline) which is not"),
2929
+ color.dim(" available inside the WrongStack session REPL.")
2930
+ ].join("\n")
2982
2931
  };
2983
- const lines = [`Autonomy mode: ${labels2[current] ?? current}`];
2984
- try {
2985
- const goal = await loadGoal(goalFilePath(opts.projectRoot));
2986
- if (goal) {
2987
- const u = summarizeUsage(goal);
2988
- lines.push(
2989
- color.dim(
2990
- ` Goal: ${goal.goal.length > 80 ? `${goal.goal.slice(0, 77)}\u2026` : goal.goal}`
2991
- )
2992
- );
2993
- lines.push(
2994
- color.dim(
2995
- ` Engine state: ${goal.engineState} \xB7 iterations: ${goal.iterations} \xB7 journal: ${goal.journal.length}`
2996
- )
2997
- );
2998
- if (u.iterationsWithUsage > 0) {
2999
- lines.push(
3000
- color.dim(
3001
- ` Spent: $${u.totalCostUsd.toFixed(4)} \xB7 ${u.totalInputTokens} in / ${u.totalOutputTokens} out tokens`
3002
- )
3003
- );
3004
- }
3005
- const recent = goal.journal.slice(-10);
3006
- const failed = recent.filter((e) => e.status === "failure").length;
3007
- if (failed > 0) {
3008
- lines.push(
3009
- color.amber(` Recent failures: ${failed} of last ${recent.length} iterations`)
3010
- );
3011
- }
2932
+ }
2933
+ let providers;
2934
+ try {
2935
+ providers = await loadConfigProviders(
2936
+ opts.paths.globalConfig,
2937
+ // We don't have a full vault reference in slash commands;
2938
+ // use a simple passthrough vault since keys won't decrypt anyway
2939
+ // in this read-only view and the config may not have encrypted fields.
2940
+ {
2941
+ encrypt: (v) => v,
2942
+ decrypt: (v) => v,
2943
+ isEncrypted: () => false
3012
2944
  }
3013
- } catch {
3014
- }
3015
- const msg2 = lines.join("\n");
3016
- opts.renderer.write(msg2);
3017
- return { message: msg2 };
2945
+ );
2946
+ } catch {
2947
+ return { message: `${color.red("Error")} could not read config file.` };
3018
2948
  }
3019
- if (arg === "stop" || arg === "halt" || arg === "kill") {
3020
- if (!opts.onEternalStop) {
3021
- const msg3 = "No eternal-mode controller wired in this session.";
3022
- opts.renderer.writeWarning(msg3);
3023
- return { message: msg3 };
2949
+ if (sub === "status") {
2950
+ const pid = parts[1];
2951
+ if (!pid) {
2952
+ return { message: `${color.amber("Usage:")} /auth status <provider>` };
3024
2953
  }
3025
- opts.getEternalEngine?.()?.stop();
3026
- opts.getParallelEngine?.()?.stop();
3027
- opts.onEternalStop();
3028
- opts.onAutonomy("off");
3029
- let summaryLine = "";
3030
- try {
3031
- const goal = await loadGoal(goalFilePath(opts.projectRoot));
3032
- if (goal) {
3033
- const u = summarizeUsage(goal);
3034
- if (u.iterationsWithUsage > 0) {
3035
- summaryLine = "\n" + color.dim(
3036
- ` Spent so far: $${u.totalCostUsd.toFixed(4)} \xB7 ${u.totalInputTokens} in / ${u.totalOutputTokens} out tokens \xB7 ${goal.iterations} total iterations.`
3037
- );
3038
- } else if (goal.iterations > 0) {
3039
- summaryLine = "\n" + color.dim(` Total iterations: ${goal.iterations}.`);
3040
- }
3041
- }
3042
- } catch {
2954
+ const cfg = providers[pid];
2955
+ if (!cfg) {
2956
+ return { message: `${color.yellow("Provider")} "${pid}" not found in saved config.` };
3043
2957
  }
3044
- const msg2 = `${color.amber("Eternal/parallel mode stop requested.")} In-flight eternal work is cancelled; parallel fan-out stops after the current tick cleans up.${summaryLine}`;
3045
- opts.renderer.write(msg2);
3046
- return { message: msg2 };
3047
- }
3048
- let newMode;
3049
- if (arg === "on" || arg === "enable" || arg === "true" || arg === "auto") {
3050
- newMode = "auto";
3051
- } else if (arg === "off" || arg === "disable" || arg === "false") {
3052
- newMode = "off";
3053
- } else if (arg === "suggest" || arg === "suggestions") {
3054
- newMode = "suggest";
3055
- } else if (arg === "eternal" || arg === "forever" || arg === "infinite" || arg === "sittinsene") {
3056
- newMode = "eternal";
3057
- } else if (arg === "parallel" || arg === "eternal-parallel" || arg === "fanout") {
3058
- newMode = "eternal-parallel";
3059
- } else if (arg === "toggle" || arg === "cycle") {
3060
- const current = opts.onAutonomy() ?? "off";
3061
- const cycle = ["off", "suggest", "auto", "eternal", "eternal-parallel"];
3062
- newMode = cycle[(cycle.indexOf(current) + 1) % cycle.length] ?? "off";
3063
- } else {
3064
- const msg2 = `Unknown argument: ${arg}. Use /autonomy on, off, suggest, eternal, parallel, stop, or toggle.`;
3065
- opts.renderer.writeWarning(msg2);
3066
- return { message: msg2 };
3067
- }
3068
- if (newMode === "eternal" || newMode === "eternal-parallel") {
3069
- const wantKeep = modifiers.includes("--keep") || modifiers.includes("keep");
3070
- const wantNew = modifiers.includes("--new") || modifiers.includes("new");
3071
- const goal = await loadGoal(goalFilePath(opts.projectRoot));
3072
- if (!goal) {
3073
- const msg3 = `${color.red("Eternal/parallel mode requires a goal.")} Run \`/goal set <mission>\` first.`;
3074
- opts.renderer.writeWarning(msg3);
3075
- return { message: msg3 };
2958
+ const keys = cfg.apiKeys ?? [];
2959
+ const active = keys.find(
2960
+ (k) => cfg && cfg.activeKey === k.label
2961
+ ) ?? keys[0];
2962
+ const lines2 = [
2963
+ `${color.bold(pid)} ${cfg.family ? color.dim(`[${cfg.family}]`) : color.amber("[no family]")}`,
2964
+ "",
2965
+ ` type: ${color.cyan(cfg.type ?? pid)}`,
2966
+ ` family: ${cfg.family ? color.cyan(cfg.family) : color.dim("unset")}`,
2967
+ ` baseUrl: ${cfg.baseUrl ? color.cyan(cfg.baseUrl) : color.dim("unset")}`
2968
+ ];
2969
+ if (cfg.models?.length) {
2970
+ lines2.push(` models: ${color.cyan(cfg.models.join(", "))}`);
3076
2971
  }
3077
- if (wantNew) {
3078
- const msg3 = `${color.amber("New mission requested.")} Clear the current goal first: ${color.bold("/goal clear")}, then ${color.bold("/goal set <mission>")}, then re-run ${color.bold(`/autonomy ${newMode}`)}.`;
3079
- opts.renderer.writeWarning(msg3);
3080
- return { message: msg3 };
2972
+ if (cfg.envVars?.length) {
2973
+ lines2.push(` envVars: ${color.cyan(cfg.envVars.join(", "))}`);
3081
2974
  }
3082
- const isStale = goal.iterations > 0 || goal.engineState === "running";
3083
- if (!wantKeep) {
3084
- if (opts.confirm) {
3085
- const goalPreview = goal.goal.length > 80 ? `${goal.goal.slice(0, 77)}\u2026` : goal.goal;
3086
- const detail = isStale ? `${color.amber("Stale goal")} (${goal.iterations} iterations, engineState: ${goal.engineState}): "${goalPreview}". Continue with this mission?` : `Existing goal: "${goalPreview}". Use this mission?`;
3087
- const defaultYes = !isStale;
3088
- const answer = await opts.confirm(detail, defaultYes);
3089
- if (answer === null) {
3090
- const msg3 = `${color.dim("Cancelled.")} Autonomy mode unchanged.`;
3091
- opts.renderer.write(msg3);
3092
- return { message: msg3 };
3093
- }
3094
- if (!answer) {
3095
- const msg3 = `${color.amber("Skipped.")} To start a new mission: ${color.bold("/goal clear")} \u2192 ${color.bold("/goal set <mission>")} \u2192 ${color.bold(`/autonomy ${newMode}`)}. To force the existing one: ${color.bold(`/autonomy ${newMode} --keep`)}.`;
3096
- opts.renderer.write(msg3);
3097
- return { message: msg3 };
3098
- }
3099
- } else if (isStale) {
3100
- const msg3 = `${color.amber("Stale goal detected.")} Previous mission has ${goal.iterations} iterations (engineState: ${goal.engineState}). Clear it first: ${color.bold("/goal clear")}, then set a new one: ${color.bold("/goal set <mission>")}.`;
3101
- opts.renderer.writeWarning(msg3);
3102
- return { message: msg3 };
2975
+ lines2.push("");
2976
+ if (keys.length === 0) {
2977
+ lines2.push(color.dim(" (no keys saved)"));
2978
+ } else {
2979
+ lines2.push(` ${color.dim("Keys:")}`);
2980
+ for (const k of keys) {
2981
+ const marker = k.label === active?.label ? color.green("\u25CF") : color.dim("\u25CB");
2982
+ const masked = k.label === active?.label ? color.dim("(active \u2014 masked)") : color.dim("(masked)");
2983
+ lines2.push(
2984
+ ` ${marker} ${color.bold(k.label.padEnd(18))} ${masked} ${color.dim(k.createdAt)}`
2985
+ );
3103
2986
  }
3104
2987
  }
3105
- if (!opts.onEternalStart) {
3106
- const msg3 = "Eternal mode controller is not wired in this session.";
3107
- opts.renderer.writeWarning(msg3);
3108
- return { message: msg3 };
3109
- }
3110
- if (opts.onYolo) opts.onYolo(true);
3111
- opts.onAutonomy(newMode);
3112
- opts.onEternalStart(newMode);
3113
- const modeLabel = newMode === "eternal-parallel" ? `${color.magenta("PARALLEL")} mode` : `${color.red("ETERNAL")} mode`;
3114
- const msg2 = `Autonomy mode: ${modeLabel} \u2014 engine launching against goal: ${color.bold(goal.goal)}
3115
- ${color.dim("Regular YOLO enabled; destructive-gated calls still use the permission flow. Use /autonomy stop to end. Journal at /goal journal.")}`;
3116
- opts.renderer.write(msg2);
3117
- return { message: msg2 };
2988
+ lines2.push("", color.dim(` Manage: wstack auth \u2192 pick ${pid}`));
2989
+ return { message: lines2.join("\n") };
3118
2990
  }
3119
- const previous = opts.onAutonomy();
3120
- if ((previous === "eternal" || previous === "eternal-parallel") && opts.onEternalStop) {
3121
- opts.onEternalStop();
2991
+ const ids = Object.keys(providers).sort();
2992
+ if (ids.length === 0) {
2993
+ return {
2994
+ message: [
2995
+ `${color.bold("API Keys")} ${color.dim("\u2014 No providers configured")}`,
2996
+ "",
2997
+ color.dim(" Run `wstack auth` to add a provider with an API key."),
2998
+ "",
2999
+ color.dim(" Quick start:"),
3000
+ ` ${color.cyan("wstack auth")} Interactive menu`,
3001
+ ` ${color.cyan("wstack auth anthropic")} Direct add`,
3002
+ "",
3003
+ color.dim(" Or /auth help for more commands.")
3004
+ ].join("\n")
3005
+ };
3122
3006
  }
3123
- opts.onAutonomy(newMode);
3124
- const labels = {
3125
- off: `${color.green("OFF")} \u2014 agent stops after each turn`,
3126
- suggest: `${color.cyan("SUGGEST")} \u2014 shows next-step suggestions after each turn`,
3127
- auto: `${color.yellow("AUTO")} \u2014 self-driving, agent continues automatically`,
3128
- eternal: `${color.red("ETERNAL")} \u2014 goal-driven sittin-sene loop`,
3129
- "eternal-parallel": `${color.magenta("PARALLEL")} \u2014 fan-out 4-8 subagents per tick`
3130
- };
3131
- const msg = `Autonomy mode: ${labels[newMode]}`;
3132
- opts.renderer.write(msg);
3133
- return { message: msg };
3007
+ const lines = [
3008
+ `${color.bold("API Keys")} ${color.dim(`\u2014 ${ids.length} provider${ids.length === 1 ? "" : "s"}`)}`,
3009
+ ""
3010
+ ];
3011
+ for (const id of ids) {
3012
+ const cfg = providers[id];
3013
+ if (!cfg) continue;
3014
+ const keys = cfg.apiKeys ?? [];
3015
+ const famTag = cfg.family ? color.dim(`[${cfg.family}]`) : "";
3016
+ const aliasTag = cfg.type && cfg.type !== id ? color.dim(`\u2192 ${cfg.type}`) : "";
3017
+ let status;
3018
+ if (keys.length === 0) {
3019
+ status = color.amber("no keys");
3020
+ } else if (keys.length === 1) {
3021
+ status = color.green(`1 key`);
3022
+ } else {
3023
+ status = color.green(`${keys.length} keys`);
3024
+ }
3025
+ lines.push(
3026
+ ` ${color.bold(id.padEnd(22))} ${famTag} ${aliasTag} ${status}`
3027
+ );
3028
+ }
3029
+ lines.push("");
3030
+ lines.push(color.dim(" /auth status <id> Detail /auth open Full menu"));
3031
+ return { message: lines.join("\n") };
3134
3032
  }
3135
3033
  };
3136
3034
  }
@@ -3292,6 +3190,215 @@ function buildAutoPhaseCommand(opts) {
3292
3190
  }
3293
3191
  };
3294
3192
  }
3193
+ function buildAutonomyCommand(opts) {
3194
+ return {
3195
+ name: "autonomy",
3196
+ category: "Agent",
3197
+ description: "Toggle or query autonomy mode (self-driving agent).",
3198
+ help: [
3199
+ "Usage:",
3200
+ " /autonomy Show current autonomy status",
3201
+ " /autonomy off Disabled \u2014 agent stops after each turn (default)",
3202
+ " /autonomy suggest Show next-step suggestions after each turn",
3203
+ " /autonomy on Auto-continue \u2014 agent picks next step and proceeds",
3204
+ " /autonomy eternal Goal-driven loop \u2014 runs forever against /goal",
3205
+ " (prompts to confirm an existing goal; `--keep` to skip prompt)",
3206
+ " /autonomy parallel Parallel mode \u2014 4-8 agents per tick, fan-out parallelism",
3207
+ " (prompts to confirm an existing goal; `--keep` to skip prompt)",
3208
+ " /autonomy stop Stop eternal mode (no-op for other modes)",
3209
+ " /autonomy toggle Cycle: off \u2192 suggest \u2192 auto \u2192 eternal \u2192 parallel \u2192 off",
3210
+ "",
3211
+ "Modes:",
3212
+ " off \u2014 Normal interactive mode. Agent stops and waits.",
3213
+ " suggest \u2014 After each turn, agent suggests next steps. You pick.",
3214
+ " auto \u2014 After each turn, agent picks the best next step and continues.",
3215
+ " Runs indefinitely until you press Esc or Ctrl+C.",
3216
+ " eternal \u2014 Goal-driven sense/decide/execute/reflect loop. Requires /goal.",
3217
+ " Force-enables regular YOLO; destructive-gated calls still use",
3218
+ " the permission flow. Runs until /autonomy stop or Ctrl+C twice.",
3219
+ " parallel \u2014 Fan-out 4\u20138 subagents per tick. Each tick decomposes the goal,",
3220
+ " spawns N agents, awaits results, aggregates. Requires /goal.",
3221
+ " Force-enables regular YOLO; destructive-gated calls still use",
3222
+ " the permission flow. Runs until /autonomy stop or Ctrl+C twice.",
3223
+ "",
3224
+ "Eternal stage flow: decide \u2192 execute \u2192 reflect \u2192 sleep | paused | stopped",
3225
+ "Stage shown in real-time. Use /goal pause to pause, /goal resume to continue.",
3226
+ "",
3227
+ "In auto/eternal/parallel modes the agent works autonomously. Press Esc to redirect,",
3228
+ "Ctrl+C to stop the active iteration. /autonomy stop ends the eternal loop."
3229
+ ].join("\n"),
3230
+ async run(args) {
3231
+ const parts = args.trim().toLowerCase().split(/\s+/).filter(Boolean);
3232
+ const arg = parts[0] ?? "";
3233
+ const modifiers = parts.slice(1);
3234
+ if (!opts.onAutonomy) {
3235
+ const msg2 = "Autonomy mode is not available in this session.";
3236
+ opts.renderer.writeWarning(msg2);
3237
+ return { message: msg2 };
3238
+ }
3239
+ if (!arg || arg === "status") {
3240
+ const current = opts.onAutonomy();
3241
+ const labels2 = {
3242
+ off: `${color.green("OFF")} ${color.dim("(agent stops after each turn)")}`,
3243
+ suggest: `${color.cyan("SUGGEST")} ${color.dim("(shows next-step suggestions)")}`,
3244
+ auto: `${color.yellow("AUTO")} ${color.dim("(self-driving \u2014 Esc to redirect, Ctrl+C to stop)")}`,
3245
+ eternal: `${color.red("ETERNAL")} ${color.dim("(goal-driven loop \u2014 YOLO, until /autonomy stop)")}`,
3246
+ "eternal-parallel": `${color.magenta("PARALLEL")} ${color.dim("(4-8 subagents per tick \u2014 fan-out, until /autonomy stop)")}`
3247
+ };
3248
+ const lines = [`Autonomy mode: ${labels2[current] ?? current}`];
3249
+ try {
3250
+ const goal = await loadGoal(goalFilePath(opts.projectRoot));
3251
+ if (goal) {
3252
+ const u = summarizeUsage(goal);
3253
+ lines.push(
3254
+ color.dim(
3255
+ ` Goal: ${goal.goal.length > 80 ? `${goal.goal.slice(0, 77)}\u2026` : goal.goal}`
3256
+ )
3257
+ );
3258
+ lines.push(
3259
+ color.dim(
3260
+ ` Engine state: ${goal.engineState} \xB7 iterations: ${goal.iterations} \xB7 journal: ${goal.journal.length}`
3261
+ )
3262
+ );
3263
+ if (u.iterationsWithUsage > 0) {
3264
+ lines.push(
3265
+ color.dim(
3266
+ ` Spent: $${u.totalCostUsd.toFixed(4)} \xB7 ${u.totalInputTokens} in / ${u.totalOutputTokens} out tokens`
3267
+ )
3268
+ );
3269
+ }
3270
+ const recent = goal.journal.slice(-10);
3271
+ const failed = recent.filter((e) => e.status === "failure").length;
3272
+ if (failed > 0) {
3273
+ lines.push(
3274
+ color.amber(` Recent failures: ${failed} of last ${recent.length} iterations`)
3275
+ );
3276
+ }
3277
+ }
3278
+ } catch {
3279
+ }
3280
+ const msg2 = lines.join("\n");
3281
+ opts.renderer.write(msg2);
3282
+ return { message: msg2 };
3283
+ }
3284
+ if (arg === "stop" || arg === "halt" || arg === "kill") {
3285
+ if (!opts.onEternalStop) {
3286
+ const msg3 = "No eternal-mode controller wired in this session.";
3287
+ opts.renderer.writeWarning(msg3);
3288
+ return { message: msg3 };
3289
+ }
3290
+ opts.getEternalEngine?.()?.stop();
3291
+ opts.getParallelEngine?.()?.stop();
3292
+ opts.onEternalStop();
3293
+ opts.onAutonomy("off");
3294
+ let summaryLine = "";
3295
+ try {
3296
+ const goal = await loadGoal(goalFilePath(opts.projectRoot));
3297
+ if (goal) {
3298
+ const u = summarizeUsage(goal);
3299
+ if (u.iterationsWithUsage > 0) {
3300
+ summaryLine = "\n" + color.dim(
3301
+ ` Spent so far: $${u.totalCostUsd.toFixed(4)} \xB7 ${u.totalInputTokens} in / ${u.totalOutputTokens} out tokens \xB7 ${goal.iterations} total iterations.`
3302
+ );
3303
+ } else if (goal.iterations > 0) {
3304
+ summaryLine = "\n" + color.dim(` Total iterations: ${goal.iterations}.`);
3305
+ }
3306
+ }
3307
+ } catch {
3308
+ }
3309
+ const msg2 = `${color.amber("Eternal/parallel mode stop requested.")} In-flight eternal work is cancelled; parallel fan-out stops after the current tick cleans up.${summaryLine}`;
3310
+ opts.renderer.write(msg2);
3311
+ return { message: msg2 };
3312
+ }
3313
+ let newMode;
3314
+ if (arg === "on" || arg === "enable" || arg === "true" || arg === "auto") {
3315
+ newMode = "auto";
3316
+ } else if (arg === "off" || arg === "disable" || arg === "false") {
3317
+ newMode = "off";
3318
+ } else if (arg === "suggest" || arg === "suggestions") {
3319
+ newMode = "suggest";
3320
+ } else if (arg === "eternal" || arg === "forever" || arg === "infinite" || arg === "sittinsene") {
3321
+ newMode = "eternal";
3322
+ } else if (arg === "parallel" || arg === "eternal-parallel" || arg === "fanout") {
3323
+ newMode = "eternal-parallel";
3324
+ } else if (arg === "toggle" || arg === "cycle") {
3325
+ const current = opts.onAutonomy() ?? "off";
3326
+ const cycle = ["off", "suggest", "auto", "eternal", "eternal-parallel"];
3327
+ newMode = cycle[(cycle.indexOf(current) + 1) % cycle.length] ?? "off";
3328
+ } else {
3329
+ const msg2 = `Unknown argument: ${arg}. Use /autonomy on, off, suggest, eternal, parallel, stop, or toggle.`;
3330
+ opts.renderer.writeWarning(msg2);
3331
+ return { message: msg2 };
3332
+ }
3333
+ if (newMode === "eternal" || newMode === "eternal-parallel") {
3334
+ const wantKeep = modifiers.includes("--keep") || modifiers.includes("keep");
3335
+ const wantNew = modifiers.includes("--new") || modifiers.includes("new");
3336
+ const goal = await loadGoal(goalFilePath(opts.projectRoot));
3337
+ if (!goal) {
3338
+ const msg3 = `${color.red("Eternal/parallel mode requires a goal.")} Run \`/goal set <mission>\` first.`;
3339
+ opts.renderer.writeWarning(msg3);
3340
+ return { message: msg3 };
3341
+ }
3342
+ if (wantNew) {
3343
+ const msg3 = `${color.amber("New mission requested.")} Clear the current goal first: ${color.bold("/goal clear")}, then ${color.bold("/goal set <mission>")}, then re-run ${color.bold(`/autonomy ${newMode}`)}.`;
3344
+ opts.renderer.writeWarning(msg3);
3345
+ return { message: msg3 };
3346
+ }
3347
+ const isStale = goal.iterations > 0 || goal.engineState === "running";
3348
+ if (!wantKeep) {
3349
+ if (opts.confirm) {
3350
+ const goalPreview = goal.goal.length > 80 ? `${goal.goal.slice(0, 77)}\u2026` : goal.goal;
3351
+ const detail = isStale ? `${color.amber("Stale goal")} (${goal.iterations} iterations, engineState: ${goal.engineState}): "${goalPreview}". Continue with this mission?` : `Existing goal: "${goalPreview}". Use this mission?`;
3352
+ const defaultYes = !isStale;
3353
+ const answer = await opts.confirm(detail, defaultYes);
3354
+ if (answer === null) {
3355
+ const msg3 = `${color.dim("Cancelled.")} Autonomy mode unchanged.`;
3356
+ opts.renderer.write(msg3);
3357
+ return { message: msg3 };
3358
+ }
3359
+ if (!answer) {
3360
+ const msg3 = `${color.amber("Skipped.")} To start a new mission: ${color.bold("/goal clear")} \u2192 ${color.bold("/goal set <mission>")} \u2192 ${color.bold(`/autonomy ${newMode}`)}. To force the existing one: ${color.bold(`/autonomy ${newMode} --keep`)}.`;
3361
+ opts.renderer.write(msg3);
3362
+ return { message: msg3 };
3363
+ }
3364
+ } else if (isStale) {
3365
+ const msg3 = `${color.amber("Stale goal detected.")} Previous mission has ${goal.iterations} iterations (engineState: ${goal.engineState}). Clear it first: ${color.bold("/goal clear")}, then set a new one: ${color.bold("/goal set <mission>")}.`;
3366
+ opts.renderer.writeWarning(msg3);
3367
+ return { message: msg3 };
3368
+ }
3369
+ }
3370
+ if (!opts.onEternalStart) {
3371
+ const msg3 = "Eternal mode controller is not wired in this session.";
3372
+ opts.renderer.writeWarning(msg3);
3373
+ return { message: msg3 };
3374
+ }
3375
+ if (opts.onYolo) opts.onYolo(true);
3376
+ opts.onAutonomy(newMode);
3377
+ opts.onEternalStart(newMode);
3378
+ const modeLabel = newMode === "eternal-parallel" ? `${color.magenta("PARALLEL")} mode` : `${color.red("ETERNAL")} mode`;
3379
+ const msg2 = `Autonomy mode: ${modeLabel} \u2014 engine launching against goal: ${color.bold(goal.goal)}
3380
+ ${color.dim("Regular YOLO enabled; destructive-gated calls still use the permission flow. Use /autonomy stop to end. Journal at /goal journal.")}`;
3381
+ opts.renderer.write(msg2);
3382
+ return { message: msg2 };
3383
+ }
3384
+ const previous = opts.onAutonomy();
3385
+ if ((previous === "eternal" || previous === "eternal-parallel") && opts.onEternalStop) {
3386
+ opts.onEternalStop();
3387
+ }
3388
+ opts.onAutonomy(newMode);
3389
+ const labels = {
3390
+ off: `${color.green("OFF")} \u2014 agent stops after each turn`,
3391
+ suggest: `${color.cyan("SUGGEST")} \u2014 shows next-step suggestions after each turn`,
3392
+ auto: `${color.yellow("AUTO")} \u2014 self-driving, agent continues automatically`,
3393
+ eternal: `${color.red("ETERNAL")} \u2014 goal-driven sittin-sene loop`,
3394
+ "eternal-parallel": `${color.magenta("PARALLEL")} \u2014 fan-out 4-8 subagents per tick`
3395
+ };
3396
+ const msg = `Autonomy mode: ${labels[newMode]}`;
3397
+ opts.renderer.write(msg);
3398
+ return { message: msg };
3399
+ }
3400
+ };
3401
+ }
3295
3402
  function buildBtwCommand(opts) {
3296
3403
  return {
3297
3404
  name: "btw",
@@ -5168,6 +5275,112 @@ function buildFleetCommand(opts) {
5168
5275
  }
5169
5276
  };
5170
5277
  }
5278
+
5279
+ // src/slash-commands/goal-refiner.ts
5280
+ async function refineGoal(rawGoal, provider, model) {
5281
+ const prompt = buildRefinementPrompt(rawGoal);
5282
+ try {
5283
+ const signal = AbortSignal.timeout(3e4);
5284
+ const response = await provider.complete({
5285
+ model,
5286
+ system: [{ type: "text", text: prompt }],
5287
+ messages: [{ role: "user", content: "Produce the refined goal." }],
5288
+ maxTokens: 1e3
5289
+ }, { signal });
5290
+ const text = extractText(response);
5291
+ if (!text) return null;
5292
+ return parseRefinement(text, rawGoal);
5293
+ } catch {
5294
+ return null;
5295
+ }
5296
+ }
5297
+ function buildRefinementPrompt(rawGoal) {
5298
+ return [
5299
+ "You are a goal refinement assistant. Your job is to take a user's raw",
5300
+ "goal description and turn it into a clear, unambiguous, actionable mission",
5301
+ "with concrete, verifiable deliverables.",
5302
+ "",
5303
+ "Rules:",
5304
+ "- The refined goal must be self-contained \u2014 someone reading only the",
5305
+ " refined goal should understand exactly what to do without seeing the",
5306
+ " original.",
5307
+ "- Each deliverable must be a single, checkable item. Prefer concrete",
5308
+ ' artifacts: "file X exists at path Y", "test Z passes", "function A',
5309
+ ' is refactored into module B". Avoid vague items like "improve code".',
5310
+ "- Include acceptance criteria where helpful.",
5311
+ "- If the goal is already clear and concrete, refine it minimally \u2014 do",
5312
+ " not add fluff.",
5313
+ "",
5314
+ "Output format (exact \u2014 use these markers):",
5315
+ "",
5316
+ "REFINED_GOAL:",
5317
+ "<the refined goal text, 1-3 sentences>",
5318
+ "",
5319
+ "DELIVERABLES:",
5320
+ "- <deliverable 1>",
5321
+ "- <deliverable 2>",
5322
+ "- ...",
5323
+ "",
5324
+ "---",
5325
+ "",
5326
+ `RAW GOAL: ${rawGoal}`,
5327
+ "",
5328
+ "---",
5329
+ "",
5330
+ "Now produce the refined version:"
5331
+ ].join("\n");
5332
+ }
5333
+ function extractText(result) {
5334
+ if (!result || typeof result !== "object") return null;
5335
+ const r = result;
5336
+ if (Array.isArray(r.content)) {
5337
+ const texts = r.content.filter((b) => b.type === "text").map((b) => b.text ?? "");
5338
+ return texts.join("") || null;
5339
+ }
5340
+ if (Array.isArray(r.choices)) {
5341
+ const choice = r.choices[0];
5342
+ return choice?.message?.content ?? null;
5343
+ }
5344
+ if (typeof r.text === "string") return r.text;
5345
+ return null;
5346
+ }
5347
+ function parseRefinement(text, fallbackGoal) {
5348
+ const refinedMatch = text.match(/REFINED_GOAL:\s*\n?([\s\S]*?)(?=\nDELIVERABLES:|$)/i);
5349
+ const refinedGoal = refinedMatch?.[1]?.trim() || fallbackGoal;
5350
+ const deliverablesMatch = text.match(/DELIVERABLES:\s*\n([\s\S]*?)$/i);
5351
+ const deliverablesRaw = deliverablesMatch?.[1] ?? "";
5352
+ const deliverables = deliverablesRaw.split("\n").map((line) => line.replace(/^[\s-]*[-*]\s*/, "").trim()).filter((line) => line.length > 0 && !line.startsWith("REFINED_GOAL"));
5353
+ return {
5354
+ refinedGoal,
5355
+ deliverables: deliverables.length > 0 ? deliverables : []
5356
+ };
5357
+ }
5358
+ function refineGoalHeuristic(rawGoal) {
5359
+ const trimmed = rawGoal.trim();
5360
+ return {
5361
+ refinedGoal: trimmed,
5362
+ deliverables: extractHeuristicDeliverables(trimmed)
5363
+ };
5364
+ }
5365
+ function extractHeuristicDeliverables(goal) {
5366
+ const deliverables = [];
5367
+ const lines = goal.split(/[.;]\s*/);
5368
+ for (const line of lines) {
5369
+ const cleaned = line.trim();
5370
+ if (!cleaned) continue;
5371
+ if (/\b(add|build|create|fix|implement|refactor|write|remove|update|migrate|set up|configure|deploy|test|document)\b/i.test(
5372
+ cleaned
5373
+ )) {
5374
+ deliverables.push(cleaned);
5375
+ }
5376
+ }
5377
+ if (deliverables.length === 0) {
5378
+ deliverables.push(goal.trim());
5379
+ }
5380
+ return deliverables;
5381
+ }
5382
+
5383
+ // src/slash-commands/goal.ts
5171
5384
  var KNOWN_VERBS = /* @__PURE__ */ new Set([
5172
5385
  "",
5173
5386
  "show",
@@ -5179,25 +5392,31 @@ var KNOWN_VERBS = /* @__PURE__ */ new Set([
5179
5392
  "journal",
5180
5393
  "log",
5181
5394
  "pause",
5182
- "resume"
5395
+ "resume",
5396
+ "refine"
5183
5397
  ]);
5184
5398
  function buildGoalCommand(opts) {
5185
5399
  return {
5186
5400
  name: "goal",
5187
5401
  category: "Agent",
5188
- description: "Set, inspect, or clear the long-running autonomous mission used by /autonomy eternal.",
5402
+ description: "Set, inspect, or clear the long-running autonomous mission. Auto-refines goals for clarity.",
5189
5403
  help: [
5190
5404
  "Usage:",
5191
- " /goal Show current goal + recent journal",
5192
- " /goal set <text> Set a new goal (overwrites previous)",
5405
+ " /goal Show current goal + progress + recent journal",
5406
+ " /goal set <text> Set a new goal (auto-refined for clarity)",
5407
+ " /goal refine Re-refine the current goal",
5193
5408
  " /goal clear Clear the goal (stops eternal mode if running)",
5194
- " /goal pause Pause at end of current iteration (no-op if already paused)",
5195
- " /goal resume Resume a paused goal (no-op if not paused)",
5196
- " /goal status Same as /goal (alias)",
5409
+ " /goal pause Pause at end of current iteration",
5410
+ " /goal resume Resume a paused goal",
5197
5411
  " /goal journal [N] Show last N journal entries (default 25)",
5198
5412
  "",
5413
+ "When a goal is set, WrongStack auto-refines it using the LLM to:",
5414
+ " \u2022 Make it unambiguous and concrete",
5415
+ " \u2022 Extract verifiable deliverables with acceptance criteria",
5416
+ " \u2022 Estimate completion progress (shown as a progress bar)",
5417
+ "",
5199
5418
  "Stage flow: decide \u2192 execute \u2192 reflect \u2192 sleep | paused | stopped",
5200
- "Pausing stops after current iteration completes. Resume continues from next iteration.",
5419
+ "The engine updates progress after each iteration toward the deliverable list.",
5201
5420
  "",
5202
5421
  "Goals live in ~/.wrongstack/projects/<hash>/goal.json and persist across sessions.",
5203
5422
  "A goal is the prerequisite for /autonomy eternal \u2014 the engine consults it on",
@@ -5233,24 +5452,93 @@ function buildGoalCommand(opts) {
5233
5452
  opts.renderer.writeWarning(msg2);
5234
5453
  return { message: msg2 };
5235
5454
  }
5455
+ let refined = null;
5456
+ if (opts.llmProvider && opts.llmModel) {
5457
+ opts.renderer.write(color.dim("Refining goal with LLM\u2026"));
5458
+ refined = await refineGoal(setText, opts.llmProvider, opts.llmModel);
5459
+ }
5460
+ if (!refined) {
5461
+ refined = refineGoalHeuristic(setText);
5462
+ }
5236
5463
  const existing = await loadGoal(goalPath);
5237
- const next = existing ? { ...existing, goal: setText, setAt: (/* @__PURE__ */ new Date()).toISOString(), lastActivityAt: (/* @__PURE__ */ new Date()).toISOString() } : emptyGoal(setText);
5464
+ const now = (/* @__PURE__ */ new Date()).toISOString();
5465
+ const next = existing ? {
5466
+ ...existing,
5467
+ goal: setText,
5468
+ refinedGoal: refined.refinedGoal,
5469
+ deliverables: refined.deliverables,
5470
+ setAt: now,
5471
+ lastActivityAt: now,
5472
+ progress: void 0,
5473
+ // reset progress
5474
+ progressNote: void 0
5475
+ } : {
5476
+ ...emptyGoal(setText),
5477
+ refinedGoal: refined.refinedGoal,
5478
+ deliverables: refined.deliverables
5479
+ };
5238
5480
  await saveGoal(goalPath, next);
5239
- const shortGoal = setText.length > 80 ? `${setText.slice(0, 80)}\u2026` : setText;
5240
- const msg = `\u{1F3AF} ${color.green("Goal locked:")} ${shortGoal}
5241
- ${color.dim(`Stored in ${goalPath} \u2014 Esc / /steer to redirect, Ctrl+C to stop.`)}`;
5481
+ const lines = [];
5482
+ lines.push(
5483
+ `\u{1F3AF} ${color.green("Goal locked:")} ${color.bold(refined.refinedGoal)}`
5484
+ );
5485
+ if (refined.refinedGoal !== setText) {
5486
+ lines.push(color.dim(` (original: "${setText.length > 60 ? setText.slice(0, 60) + "\u2026" : setText}")`));
5487
+ }
5488
+ if (refined.deliverables.length > 0) {
5489
+ lines.push("");
5490
+ lines.push(`${color.bold("Deliverables")} (${refined.deliverables.length}):`);
5491
+ for (const d of refined.deliverables) {
5492
+ lines.push(` ${color.dim("\u25CB")} ${d}`);
5493
+ }
5494
+ }
5495
+ lines.push("");
5496
+ lines.push(
5497
+ color.dim(`Stored in ${goalPath} \u2014 progress tracked automatically.`)
5498
+ );
5499
+ const msg = lines.join("\n");
5500
+ opts.renderer.write(msg);
5501
+ return {
5502
+ message: msg,
5503
+ runText: buildGoalPreamble(refined.refinedGoal, refined.deliverables)
5504
+ };
5505
+ }
5506
+ case "refine": {
5507
+ const current = await loadGoal(goalPath);
5508
+ if (!current) {
5509
+ const msg2 = "No goal set to refine. Use /goal set <text> first.";
5510
+ opts.renderer.writeWarning(msg2);
5511
+ return { message: msg2 };
5512
+ }
5513
+ let refined = null;
5514
+ if (opts.llmProvider && opts.llmModel) {
5515
+ opts.renderer.write(color.dim("Re-refining goal with LLM\u2026"));
5516
+ refined = await refineGoal(current.goal, opts.llmProvider, opts.llmModel);
5517
+ }
5518
+ if (!refined) {
5519
+ refined = refineGoalHeuristic(current.goal);
5520
+ }
5521
+ const updated = {
5522
+ ...current,
5523
+ refinedGoal: refined.refinedGoal,
5524
+ deliverables: refined.deliverables
5525
+ };
5526
+ await saveGoal(goalPath, updated);
5527
+ const msg = `${color.green("\u2713")} Goal re-refined with ${refined.deliverables.length} deliverables.`;
5242
5528
  opts.renderer.write(msg);
5243
- return { message: msg, runText: buildGoalPreamble(setText) };
5529
+ return { message: `${msg}
5530
+
5531
+ ${formatGoal(updated)}` };
5244
5532
  }
5245
5533
  case "clear":
5246
5534
  case "reset": {
5247
- const existing = await loadGoal(goalPath);
5248
- if (!existing) {
5535
+ const current = await loadGoal(goalPath);
5536
+ if (!current) {
5249
5537
  const msg2 = "No goal to clear.";
5250
5538
  opts.renderer.write(msg2);
5251
5539
  return { message: msg2 };
5252
5540
  }
5253
- const abandoned = { ...existing, goalState: "abandoned" };
5541
+ const abandoned = { ...current, goalState: "abandoned" };
5254
5542
  await saveGoal(goalPath, abandoned);
5255
5543
  const { unlink: unlink4 } = await import('fs/promises');
5256
5544
  try {
@@ -5325,7 +5613,7 @@ ${lines.join("\n")}`;
5325
5613
  return { message: msg };
5326
5614
  }
5327
5615
  default: {
5328
- const msg = `Unknown subcommand "${verb}". Try: show | set <text> | clear | journal [N]`;
5616
+ const msg = `Unknown subcommand "${verb}". Try: show | set <text> | refine | clear | journal [N]`;
5329
5617
  opts.renderer.writeWarning(msg);
5330
5618
  return { message: msg };
5331
5619
  }
@@ -5424,17 +5712,11 @@ No project type auto-detected. Edit the file with project context and instructio
5424
5712
  }
5425
5713
  };
5426
5714
  }
5427
- function expectDefined2(value) {
5428
- if (value === null || value === void 0) {
5429
- throw new Error("Expected value to be defined");
5430
- }
5431
- return value;
5432
- }
5433
5715
  function parseMcpArgs(args) {
5434
5716
  const trimmed = args.trim();
5435
5717
  if (!trimmed || trimmed === "list") return { action: "list", name: "" };
5436
5718
  const parts = trimmed.split(/\s+/);
5437
- const action = expectDefined2(parts[0]);
5719
+ const action = expectDefined(parts[0]);
5438
5720
  const name = parts[1] ?? "";
5439
5721
  const enable = parts.includes("--enable") || parts.includes("-e");
5440
5722
  switch (action) {
@@ -5681,7 +5963,7 @@ function buildMemoryCommand(opts) {
5681
5963
  return {
5682
5964
  name: "memory",
5683
5965
  category: "Inspect",
5684
- description: "Inspect or edit persistent memory: /memory [show|remember <text>|forget <query>|clear]",
5966
+ description: "Inspect or edit persistent memory: /memory [show|remember <text>|forget <query>|clear|compact|stats]",
5685
5967
  async run(args) {
5686
5968
  const store = opts.memoryStore;
5687
5969
  if (!store) return { message: "No memory store configured." };
@@ -5714,14 +5996,350 @@ function buildMemoryCommand(opts) {
5714
5996
  await store.clear();
5715
5997
  return { message: "Cleared all memory scopes." };
5716
5998
  }
5999
+ case "compact": {
6000
+ return runCompact(opts);
6001
+ }
6002
+ case "stats": {
6003
+ return runStats(opts);
6004
+ }
5717
6005
  default:
5718
6006
  return {
5719
- message: `Unknown subcommand "${verb}". Try: show | remember <text> | forget <query> | clear`
6007
+ message: `Unknown subcommand "${verb}". Try: show | remember <text> | forget <query> | clear | compact | stats`
5720
6008
  };
5721
6009
  }
5722
6010
  }
5723
6011
  };
5724
6012
  }
6013
+ function buildCompactPrompt(entries) {
6014
+ const entriesBlock = entries.map(
6015
+ (e, i) => `${i + 1}. [${e.ts.slice(0, 10)}] ${e.id}
6016
+ ${e.text}${e.tags ? `
6017
+ tags: ${e.tags.join(", ")}` : ""}${e.type ? `
6018
+ type: ${e.type}` : ""}${e.priority ? `
6019
+ priority: ${e.priority}` : ""}`
6020
+ ).join("\n\n");
6021
+ return `You are a memory curator. Your task is to review, deduplicate, and improve a set of long-term memory entries.
6022
+
6023
+ These entries are injected into the context of an AI coding agent. Every token counts. The memory must be concise, accurate, and free of noise.
6024
+
6025
+ ## Current Memory Entries
6026
+
6027
+ ${entriesBlock}
6028
+
6029
+ ## Your Task
6030
+
6031
+ Review each entry and return a JSON object with an "operations" array. Each operation targets one or more entries:
6032
+
6033
+ ### Actions
6034
+
6035
+ - **"keep"** \u2014 The entry is valuable as-is. Include it in the operations so I know you reviewed it.
6036
+ - **"rewrite"** \u2014 The entry has value but needs better wording. Provide improved "newText". Target a single entry.
6037
+ - **"merge"** \u2014 Two or more entries say essentially the same thing. Combine them into one concise entry. The "targets" should list all entries being merged. Provide the combined "newText".
6038
+ - **"delete"** \u2014 The entry is obsolete, redundant, too vague, or not useful for future sessions. Target one or more entries.
6039
+
6040
+ ### Rules
6041
+
6042
+ 1. **Be ruthless about noise.** If an entry won't help a future AI agent do its job better, delete it.
6043
+ 2. **Deduplicate aggressively.** Similar entries should be merged. Identical entries MUST be merged.
6044
+ 3. **Keep entries concise.** Each entry should be one clear sentence. Remove filler words.
6045
+ 4. **Preserve factual accuracy.** Don't change the meaning of entries unless they're wrong.
6046
+ 5. **Handle every entry.** Every entry must appear in at least one operation (keep, rewrite, merge, or delete).
6047
+ 6. **Prefer quality over quantity.** 10 excellent entries > 30 mediocre ones.
6048
+ 7. **Tag entries appropriately.** If an entry mentions a technology or concept that could be tagged, suggest tags in the newText using #hashtag syntax.
6049
+
6050
+ ### Response Format
6051
+
6052
+ Return ONLY valid JSON with this structure:
6053
+
6054
+ {
6055
+ "operations": [
6056
+ { "action": "keep", "targets": ["mem_1234_abcd"], "reason": "Clear and useful" },
6057
+ { "action": "rewrite", "targets": ["mem_5678_ef01"], "newText": "Project uses pnpm v9 with ESM-only modules #pnpm #esm", "reason": "Added version and ESM detail" },
6058
+ { "action": "merge", "targets": ["mem_aaaa_1111", "mem_bbbb_2222"], "newText": "All packages use TypeScript strict mode with noUncheckedIndexedAccess #typescript", "reason": "Two entries about TS config, merged" },
6059
+ { "action": "delete", "targets": ["mem_cccc_3333"], "reason": "Obsolete \u2014 was a temporary debug note" }
6060
+ ],
6061
+ "summary": "Merged 2 TS entries, rewrote 1 for clarity, deleted 1 obsolete note. 12 entries \u2192 10 entries."
6062
+ }
6063
+
6064
+ Use the EXACT entry IDs from the list above for "targets". No markdown, no explanation outside the JSON.`;
6065
+ }
6066
+ async function runCompact(opts) {
6067
+ const store = opts.memoryStore;
6068
+ if (!store) return { message: "No memory store configured." };
6069
+ const entries = await store.list("project-memory");
6070
+ if (entries.length === 0) {
6071
+ return { message: "Memory is empty \u2014 nothing to compact." };
6072
+ }
6073
+ const raw = await store.read("project-memory");
6074
+ const compactEntries = parseCompactEntries(raw);
6075
+ if (compactEntries.length === 0) {
6076
+ return { message: "No parseable entries found." };
6077
+ }
6078
+ const provider = opts.llmProvider;
6079
+ if (!provider || !provider.complete) {
6080
+ return { message: "No LLM provider available. /memory compact requires an active session with a configured provider." };
6081
+ }
6082
+ const prompt = buildCompactPrompt(compactEntries);
6083
+ let responseText;
6084
+ try {
6085
+ const signal = AbortSignal.timeout(3e4);
6086
+ const response = await provider.complete(
6087
+ {
6088
+ model: opts.llmModel ?? "",
6089
+ system: [{ type: "text", text: prompt }],
6090
+ messages: [
6091
+ {
6092
+ role: "user",
6093
+ content: `Review the ${compactEntries.length} memory entries above and return operations as JSON.`
6094
+ }
6095
+ ],
6096
+ maxTokens: 2e3,
6097
+ temperature: 0.1
6098
+ // low temperature for deterministic curation
6099
+ },
6100
+ { signal }
6101
+ );
6102
+ responseText = response.content.filter((b) => b.type === "text").map((b) => b.text).join("").trim();
6103
+ } catch (err) {
6104
+ return {
6105
+ message: `LLM call failed: ${err instanceof Error ? err.message : String(err)}`
6106
+ };
6107
+ }
6108
+ if (!responseText) {
6109
+ return { message: "LLM returned empty response." };
6110
+ }
6111
+ let parsed;
6112
+ try {
6113
+ const jsonMatch = responseText.match(/\{[\s\S]*\}/);
6114
+ if (!jsonMatch) {
6115
+ return { message: `LLM response is not valid JSON:
6116
+ ${responseText.slice(0, 500)}` };
6117
+ }
6118
+ parsed = JSON.parse(jsonMatch[0]);
6119
+ } catch (err) {
6120
+ return {
6121
+ message: `Failed to parse LLM response: ${err instanceof Error ? err.message : String(err)}
6122
+
6123
+ Raw response:
6124
+ ${responseText.slice(0, 500)}`
6125
+ };
6126
+ }
6127
+ if (!Array.isArray(parsed.operations) || parsed.operations.length === 0) {
6128
+ return { message: "LLM returned no operations." };
6129
+ }
6130
+ let kept = 0;
6131
+ let rewritten = 0;
6132
+ let merged = 0;
6133
+ let deleted = 0;
6134
+ const errors = [];
6135
+ for (const op of parsed.operations) {
6136
+ try {
6137
+ switch (op.action) {
6138
+ case "keep": {
6139
+ kept += op.targets.length;
6140
+ break;
6141
+ }
6142
+ case "rewrite": {
6143
+ if (!op.newText) {
6144
+ errors.push(`rewrite missing newText for targets: ${op.targets.join(", ")}`);
6145
+ continue;
6146
+ }
6147
+ for (const target of op.targets) {
6148
+ await store.forget(target);
6149
+ }
6150
+ await store.remember(op.newText);
6151
+ rewritten++;
6152
+ break;
6153
+ }
6154
+ case "merge": {
6155
+ if (!op.newText) {
6156
+ errors.push(`merge missing newText for targets: ${op.targets.join(", ")}`);
6157
+ continue;
6158
+ }
6159
+ for (const target of op.targets) {
6160
+ await store.forget(target);
6161
+ }
6162
+ await store.remember(op.newText);
6163
+ merged++;
6164
+ break;
6165
+ }
6166
+ case "delete": {
6167
+ for (const target of op.targets) {
6168
+ await store.forget(target);
6169
+ }
6170
+ deleted += op.targets.length;
6171
+ break;
6172
+ }
6173
+ default: {
6174
+ errors.push(`unknown action "${op.action}"`);
6175
+ }
6176
+ }
6177
+ } catch (err) {
6178
+ errors.push(
6179
+ `${op.action} failed for ${op.targets.join(", ")}: ${err instanceof Error ? err.message : String(err)}`
6180
+ );
6181
+ }
6182
+ }
6183
+ const lines = ["## Memory Compact \u2014 Complete"];
6184
+ const stats = [];
6185
+ if (kept > 0) stats.push(`${kept} kept`);
6186
+ if (rewritten > 0) stats.push(`${rewritten} rewritten`);
6187
+ if (merged > 0) stats.push(`${merged} merged`);
6188
+ if (deleted > 0) stats.push(`${deleted} deleted`);
6189
+ lines.push(`**Result:** ${stats.join(", ")}`);
6190
+ lines.push(`**Before:** ${compactEntries.length} entries \u2192 **After:** ${kept + rewritten + merged} entries`);
6191
+ if (parsed.summary) {
6192
+ lines.push("");
6193
+ lines.push(parsed.summary);
6194
+ }
6195
+ lines.push("");
6196
+ lines.push("### Operations");
6197
+ for (const op of parsed.operations) {
6198
+ const icon = op.action === "keep" ? "\u2713" : op.action === "rewrite" ? "\u270F\uFE0F" : op.action === "merge" ? "\u{1F500}" : op.action === "delete" ? "\u2717" : "?";
6199
+ const detail = op.newText ? ` \u2192 "${op.newText}"` : "";
6200
+ lines.push(`- ${icon} **${op.action}** ${op.targets.join(", ")}${detail}`);
6201
+ if (op.reason) lines.push(` _${op.reason}_`);
6202
+ }
6203
+ if (errors.length > 0) {
6204
+ lines.push("");
6205
+ lines.push("### Errors");
6206
+ for (const err of errors) {
6207
+ lines.push(`- \u26A0\uFE0F ${err}`);
6208
+ }
6209
+ }
6210
+ return { message: lines.join("\n") };
6211
+ }
6212
+ function parseCompactEntries(raw) {
6213
+ const entries = [];
6214
+ for (const line of raw.split("\n")) {
6215
+ const trimmed = line.trim();
6216
+ if (!trimmed.startsWith("- [")) continue;
6217
+ const idMatch = trimmed.match(/mem_(\d+_\w+)/);
6218
+ if (!idMatch) continue;
6219
+ const id = idMatch[0] ?? "";
6220
+ const afterId = trimmed.slice((idMatch.index ?? 0) + id.length).trim();
6221
+ const tsMatch = trimmed.match(/^-\s*\[([^\]]+)\]/);
6222
+ const ts = tsMatch?.[1] ?? "";
6223
+ const tags = [];
6224
+ const tagRe = /#([\w-]+)/g;
6225
+ let tm;
6226
+ while ((tm = tagRe.exec(afterId)) !== null) {
6227
+ tags.push(tm[1] ?? "");
6228
+ }
6229
+ const text = afterId.replace(tagRe, "").replace(/\s{2,}/g, " ").trim();
6230
+ if (!text) continue;
6231
+ entries.push({ id, text, ts, tags: tags.length > 0 ? tags : void 0 });
6232
+ }
6233
+ return entries;
6234
+ }
6235
+ async function runStats(opts) {
6236
+ const store = opts.memoryStore;
6237
+ if (!store) return { message: "No memory store configured." };
6238
+ const entries = await store.list("project-memory");
6239
+ if (entries.length === 0) {
6240
+ return { message: "\u{1F4CA} Memory is empty. Start adding entries with `/memory remember <text>`." };
6241
+ }
6242
+ const now = Date.now();
6243
+ const lines = ["## \u{1F4CA} Memory Stats"];
6244
+ const raw = await store.read("project-memory");
6245
+ const byteSize = Buffer.byteLength(raw, "utf8");
6246
+ const kbSize = (byteSize / 1024).toFixed(1);
6247
+ const maxKb = (32e3 / 1024).toFixed(1);
6248
+ const pctFull = (byteSize / 32e3 * 100).toFixed(0);
6249
+ lines.push(`**Total:** ${entries.length} entries \xB7 ${kbSize} KB / ${maxKb} KB (${pctFull}%)`);
6250
+ const byType = /* @__PURE__ */ new Map();
6251
+ for (const e of entries) {
6252
+ const t = e.type ?? "untyped";
6253
+ byType.set(t, (byType.get(t) ?? 0) + 1);
6254
+ }
6255
+ if (byType.size > 0) {
6256
+ lines.push("");
6257
+ lines.push("### By Type");
6258
+ const typeOrder = ["convention", "decision", "fact", "preference", "reference", "anti_pattern", "untyped"];
6259
+ for (const t of typeOrder) {
6260
+ const count = byType.get(t);
6261
+ if (count) {
6262
+ const bar = "\u2588".repeat(Math.min(count, 20));
6263
+ lines.push(`- \`${t}\` ${bar} ${count}`);
6264
+ }
6265
+ }
6266
+ }
6267
+ const byPriority = /* @__PURE__ */ new Map();
6268
+ for (const e of entries) {
6269
+ const p = e.priority ?? "unset";
6270
+ byPriority.set(p, (byPriority.get(p) ?? 0) + 1);
6271
+ }
6272
+ if (byPriority.size > 0) {
6273
+ lines.push("");
6274
+ lines.push("### By Priority");
6275
+ const icon = { critical: "\u26A1", high: "\u25B2", medium: "\u25CF", low: "\u25CB", unset: "\xB7" };
6276
+ for (const [p, count] of [...byPriority.entries()].sort((a, b) => b[1] - a[1])) {
6277
+ lines.push(`- ${icon[p] ?? "\xB7"} \`${p}\`: ${count}`);
6278
+ }
6279
+ }
6280
+ const ages = entries.map((e) => {
6281
+ const ageDays = (now - new Date(e.ts).getTime()) / (1e3 * 60 * 60 * 24);
6282
+ if (ageDays < 1) return "<1d";
6283
+ if (ageDays < 7) return "<7d";
6284
+ if (ageDays < 30) return "<30d";
6285
+ return ">30d";
6286
+ });
6287
+ const byAge = /* @__PURE__ */ new Map();
6288
+ for (const a of ages) byAge.set(a, (byAge.get(a) ?? 0) + 1);
6289
+ lines.push("");
6290
+ lines.push("### By Age");
6291
+ for (const age of ["<1d", "<7d", "<30d", ">30d"]) {
6292
+ const actual = byAge.get(age) ?? 0;
6293
+ if (actual > 0 || age === "<7d") {
6294
+ lines.push(`- ${age}: ${actual}`);
6295
+ }
6296
+ }
6297
+ const tagCounts = /* @__PURE__ */ new Map();
6298
+ for (const e of entries) {
6299
+ for (const t of e.tags ?? []) {
6300
+ tagCounts.set(t, (tagCounts.get(t) ?? 0) + 1);
6301
+ }
6302
+ }
6303
+ if (tagCounts.size > 0) {
6304
+ lines.push("");
6305
+ lines.push("### Top Tags");
6306
+ const sorted = [...tagCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10);
6307
+ for (const [tag, count] of sorted) {
6308
+ lines.push(`- \`#${tag}\`: ${count}`);
6309
+ }
6310
+ }
6311
+ lines.push("");
6312
+ lines.push("### Health");
6313
+ const untyped = byType.get("untyped") ?? 0;
6314
+ const unsetPriority = byPriority.get("unset") ?? 0;
6315
+ const old = byAge.get(">30d") ?? 0;
6316
+ if (untyped > entries.length * 0.5) {
6317
+ lines.push(`- \u26A0\uFE0F ${untyped}/${entries.length} entries have no type \u2014 run \`/memory compact\` to categorize`);
6318
+ } else if (untyped > 0) {
6319
+ lines.push(`- \u2139\uFE0F ${untyped} entries untyped \u2014 consider categorizing`);
6320
+ } else {
6321
+ lines.push("- \u2705 All entries have types");
6322
+ }
6323
+ if (unsetPriority > entries.length * 0.5) {
6324
+ lines.push(`- \u26A0\uFE0F ${unsetPriority}/${entries.length} entries have no priority`);
6325
+ } else if (unsetPriority > 0) {
6326
+ lines.push(`- \u2139\uFE0F ${unsetPriority} entries have no priority set`);
6327
+ } else {
6328
+ lines.push("- \u2705 All entries have priorities");
6329
+ }
6330
+ if (old > 5) {
6331
+ lines.push(`- \u26A0\uFE0F ${old} entries older than 30 days \u2014 run \`/memory compact\` to review`);
6332
+ }
6333
+ const pct2 = Number.parseInt(pctFull);
6334
+ if (pct2 > 80) {
6335
+ lines.push(`- \u26A0\uFE0F Storage ${pct2}% full \u2014 run \`/memory compact\` to free space`);
6336
+ } else {
6337
+ lines.push(`- \u2705 Storage ${pct2}% full \u2014 healthy`);
6338
+ }
6339
+ lines.push("");
6340
+ lines.push("**Commands:** `/memory show` \xB7 `/memory compact` \xB7 `/memory remember <text>`");
6341
+ return { message: lines.join("\n") };
6342
+ }
5725
6343
  async function runModePicker(modeStore, reader) {
5726
6344
  const modes = await modeStore.listModes();
5727
6345
  const active = await modeStore.getActiveMode();
@@ -6397,12 +7015,6 @@ function summariseEvent(ev) {
6397
7015
  return color.dim("\u2026");
6398
7016
  }
6399
7017
  }
6400
- function expectDefined5(value) {
6401
- if (value === null || value === void 0) {
6402
- throw new Error("Expected value to be defined");
6403
- }
6404
- return value;
6405
- }
6406
7018
  var noOpVault3 = {
6407
7019
  encrypt: (v) => v,
6408
7020
  decrypt: (v) => v,
@@ -6464,12 +7076,14 @@ async function patchGlobalConfig2(globalConfigPath, mutate) {
6464
7076
  function buildSetModelCommand(opts) {
6465
7077
  const help = [
6466
7078
  "Usage:",
6467
- " /setmodel Show leader model + the task\u2192model matrix",
7079
+ " /setmodel Show leader model + matrix + resolution summary",
6468
7080
  " /setmodel list List keyed providers, their models, and valid keys",
6469
- " /setmodel leader <provider> <model> Set the main (leader) model",
7081
+ " /setmodel leader <provider> <model> Set the main (leader / brain) model",
6470
7082
  " /setmodel set <key> <provider>/<model> Pin a role/phase/* to a model",
6471
7083
  " /setmodel set <key> <model> Pin to a model on the leader provider",
6472
7084
  " /setmodel clear <key> Remove a matrix entry",
7085
+ " /setmodel resolve <role> Walk the resolution chain for one role",
7086
+ " /setmodel doctor Validate matrix entries (orphans, typos, missing keys)",
6473
7087
  "",
6474
7088
  "Keys: a catalog role (e.g. security-scanner), a phase (" + MATRIX_PHASE_KEYS.join(", ") + "),",
6475
7089
  "or * for the fleet-wide default. Precedence at spawn: role \u2192 phase \u2192 * \u2192 leader.",
@@ -6483,24 +7097,57 @@ function buildSetModelCommand(opts) {
6483
7097
  const lines = [
6484
7098
  `${color.bold("WrongStack")} ${color.dim("\u2014 Models")}`,
6485
7099
  "",
6486
- ` leader: ${color.cyan(`${config.provider}/${config.model}`)} ${color.dim("change: /setmodel leader <provider> <model>")}`,
6487
- "",
6488
- ` ${color.bold("task \u2192 model matrix")} ${color.dim("(role \u2192 phase \u2192 * \u2192 leader)")}`
7100
+ ` ${color.bold("leader")} ${color.cyan(`${config.provider}/${config.model}`)} ${color.dim("/setmodel leader <provider> <model>")}`,
7101
+ ""
6489
7102
  ];
6490
7103
  if (keys.length === 0) {
6491
7104
  lines.push(
6492
- ` ${color.dim("(empty) set one: /setmodel set <role|phase|*> <provider>/<model>")}`
7105
+ ` ${color.bold("matrix")} ${color.dim("(empty)")}`,
7106
+ ` ${color.dim("pin a role: /setmodel set <role> <provider>/<model>")}`,
7107
+ ` ${color.dim("set default: /setmodel set * <provider>/<model>")}`
6493
7108
  );
6494
7109
  } else {
7110
+ lines.push(` ${color.bold("matrix")} ${color.dim("(role \u2192 phase \u2192 * \u2192 leader)")}`);
6495
7111
  for (const k of keys.sort()) {
6496
7112
  const kind = matrixKeyKind(k);
6497
7113
  const tag = kind === "unknown" ? color.red("?") : color.dim(kind);
6498
- lines.push(` ${color.amber(k.padEnd(22))} \u2192 ${fmtEntry(expectDefined5(matrix[k]))} ${tag}`);
7114
+ lines.push(` ${color.amber(k.padEnd(22))} \u2192 ${fmtEntry(expectDefined(matrix[k]))} ${tag}`);
7115
+ }
7116
+ }
7117
+ const summaryRoles = getSummaryRoles();
7118
+ if (summaryRoles.length > 0) {
7119
+ lines.push("");
7120
+ lines.push(` ${color.bold("resolution")} ${color.dim("(selected roles)")}`);
7121
+ for (const role of summaryRoles) {
7122
+ const entry = resolveModelMatrix(matrix, role);
7123
+ const provider = entry?.provider ?? config.provider;
7124
+ const model = entry?.model ?? config.model;
7125
+ const source = resolutionSource(matrix, role);
7126
+ lines.push(` ${color.dim(role.padEnd(22))} \u2192 ${color.cyan(`${provider}/${model}`)} ${color.dim(source)}`);
6499
7127
  }
6500
7128
  }
6501
- lines.push("", color.dim(" /setmodel list for valid keys \xB7 /setmodel help for usage"));
7129
+ lines.push("", color.dim(" /setmodel list \xB7 resolve <role> \xB7 doctor \xB7 help"));
6502
7130
  return lines.join("\n");
6503
7131
  }
7132
+ function getSummaryRoles() {
7133
+ const picks = [];
7134
+ for (const phase of MATRIX_PHASE_KEYS) {
7135
+ const agents = AGENTS_BY_PHASE[phase];
7136
+ if (agents && agents.length > 0) {
7137
+ picks.push(agents[0].config.role);
7138
+ }
7139
+ }
7140
+ picks.push("security-scanner", "bug-hunter");
7141
+ return [...new Set(picks)].sort();
7142
+ }
7143
+ function resolutionSource(matrix, role) {
7144
+ if (!matrix) return "leader";
7145
+ if (matrix[role]) return "role";
7146
+ const phase = phaseForRole(role);
7147
+ if (phase && matrix[phase]) return `phase (${phase})`;
7148
+ if (matrix["*"]) return "default (*)";
7149
+ return "leader";
7150
+ }
6504
7151
  return {
6505
7152
  name: "setmodel",
6506
7153
  category: "Config",
@@ -6517,6 +7164,7 @@ function buildSetModelCommand(opts) {
6517
7164
  const config = opts.configStore.get();
6518
7165
  const keyed = keyedProviderIds(config);
6519
7166
  const globalConfigPath = opts.paths.globalConfig;
7167
+ const matrix = config.modelMatrix ?? {};
6520
7168
  if (sub === "list") {
6521
7169
  const provLines = keyed.map((id) => {
6522
7170
  const models = config.providers?.[id]?.models ?? [];
@@ -6537,6 +7185,119 @@ function buildSetModelCommand(opts) {
6537
7185
  ].join("\n")
6538
7186
  };
6539
7187
  }
7188
+ if (sub === "resolve") {
7189
+ const role = parts[1];
7190
+ if (!role) {
7191
+ return { message: `${color.amber("Usage:")} /setmodel resolve <role>` };
7192
+ }
7193
+ const kind = matrixKeyKind(role);
7194
+ if (kind === "unknown" && role !== "*") {
7195
+ return {
7196
+ message: `${color.red("Unknown role")}: "${role}". Use ${color.dim("/setmodel list")} to see valid roles.`
7197
+ };
7198
+ }
7199
+ const lines = [
7200
+ `${color.bold("Resolution chain")} for ${color.amber(role)}`,
7201
+ ""
7202
+ ];
7203
+ const phase = phaseForRole(role);
7204
+ const resolved = resolveModelMatrix(matrix, role);
7205
+ if (matrix[role]) {
7206
+ lines.push(` 1. matrix["${role}"] \u2192 ${fmtEntry(expectDefined(matrix[role]))} ${color.green("\u2713 exact role")}`);
7207
+ } else {
7208
+ lines.push(` 1. matrix["${role}"] \u2192 ${color.dim("not set")}`);
7209
+ }
7210
+ if (phase) {
7211
+ if (matrix[phase]) {
7212
+ lines.push(` 2. matrix["${phase}"] \u2192 ${fmtEntry(expectDefined(matrix[phase]))} ${matrix[role] ? color.dim("(skipped \u2014 role matched)") : color.green("\u2713 phase match")}`);
7213
+ } else {
7214
+ lines.push(` 2. matrix["${phase}"] \u2192 ${color.dim("not set")}`);
7215
+ }
7216
+ }
7217
+ if (matrix["*"]) {
7218
+ const skipped = matrix[role] || phase && matrix[phase];
7219
+ lines.push(` 3. matrix["*"] \u2192 ${fmtEntry(expectDefined(matrix["*"]))} ${skipped ? color.dim("(skipped)") : color.green("\u2713 default")}`);
7220
+ } else {
7221
+ lines.push(` 3. matrix["*"] \u2192 ${color.dim("not set")}`);
7222
+ }
7223
+ const leaderSkipped = matrix[role] || phase && matrix[phase] || matrix["*"];
7224
+ lines.push(` 4. ${color.dim("leader fallback")} \u2192 ${color.cyan(`${config.provider}/${config.model}`)} ${leaderSkipped ? color.dim("(skipped)") : color.green("\u2713 used")}`);
7225
+ lines.push("");
7226
+ if (resolved) {
7227
+ const rp = resolved.provider ?? config.provider;
7228
+ lines.push(`${color.green("\u2713 Resolved")}: ${color.cyan(`${rp}/${resolved.model}`)}`);
7229
+ } else {
7230
+ lines.push(`${color.green("\u2713 Resolved")}: ${color.cyan(`${config.provider}/${config.model}`)} ${color.dim("(leader)")}`);
7231
+ }
7232
+ return { message: lines.join("\n") };
7233
+ }
7234
+ if (sub === "doctor") {
7235
+ const issues = [];
7236
+ const warnings = [];
7237
+ for (const [key, entry] of Object.entries(matrix)) {
7238
+ const kind = matrixKeyKind(key);
7239
+ if (kind === "unknown") {
7240
+ issues.push(
7241
+ `${color.red("\u2717")} ${color.amber(key)}: not a valid role, phase, or * \u2014 ${color.dim("typo or stale entry?")}`
7242
+ );
7243
+ }
7244
+ if (entry.provider) {
7245
+ const provCfg2 = config.providers?.[entry.provider];
7246
+ if (!provCfg2) {
7247
+ issues.push(
7248
+ `${color.red("\u2717")} ${color.amber(key)}: provider "${entry.provider}" is not configured`
7249
+ );
7250
+ } else if (!providerHasKey(provCfg2)) {
7251
+ warnings.push(
7252
+ `${color.amber("\u26A0")} ${color.amber(key)}: provider "${entry.provider}" has no API key`
7253
+ );
7254
+ }
7255
+ }
7256
+ const effectiveProvider = entry.provider ?? config.provider;
7257
+ const provCfg = config.providers?.[effectiveProvider];
7258
+ if (provCfg?.models && provCfg.models.length > 0) {
7259
+ if (!provCfg.models.includes(entry.model)) {
7260
+ warnings.push(
7261
+ `${color.amber("\u26A0")} ${color.amber(key)}: model "${entry.model}" not in ${effectiveProvider}'s model list (${provCfg.models.join(", ")})`
7262
+ );
7263
+ }
7264
+ }
7265
+ }
7266
+ if (Object.keys(matrix).length > 0 && !matrix["*"]) {
7267
+ const covered = /* @__PURE__ */ new Set();
7268
+ for (const [key] of Object.entries(matrix)) {
7269
+ covered.add(key);
7270
+ }
7271
+ const phasesCovered = new Set(Object.keys(matrix).filter((k) => matrixKeyKind(k) === "phase"));
7272
+ const unprotected = [];
7273
+ for (const role of Object.keys(AGENT_CATALOG)) {
7274
+ if (covered.has(role)) continue;
7275
+ const ph = phaseForRole(role);
7276
+ if (ph && phasesCovered.has(ph)) continue;
7277
+ unprotected.push(role);
7278
+ }
7279
+ if (unprotected.length > 0) {
7280
+ const sample = unprotected.slice(0, 10);
7281
+ const suffix = unprotected.length > 10 ? ` +${unprotected.length - 10} more` : "";
7282
+ warnings.push(
7283
+ `${color.amber("\u26A0")} ${unprotected.length} role(s) have no matrix coverage and no * default: ${sample.join(", ")}${suffix}`
7284
+ );
7285
+ }
7286
+ }
7287
+ const header = [
7288
+ `${color.bold("Matrix Doctor")} ${color.dim("\u2014 " + Object.keys(matrix).length + " entries")}`,
7289
+ ""
7290
+ ];
7291
+ if (issues.length === 0 && warnings.length === 0) {
7292
+ header.push(`${color.green("\u2713")} All matrix entries are valid. No issues found.`);
7293
+ }
7294
+ const allLines = [
7295
+ ...header,
7296
+ ...issues.length ? ["", `${color.bold("Issues")}:`, ...issues] : [],
7297
+ ...warnings.length ? ["", `${color.bold("Warnings")}:`, ...warnings] : []
7298
+ ];
7299
+ return { message: allLines.join("\n") };
7300
+ }
6540
7301
  try {
6541
7302
  if (sub === "leader") {
6542
7303
  const provider = parts[1];
@@ -6581,9 +7342,9 @@ function buildSetModelCommand(opts) {
6581
7342
  };
6582
7343
  }
6583
7344
  const decrypted = await patchGlobalConfig2(globalConfigPath, (cfg) => {
6584
- const matrix = { ...cfg.modelMatrix ?? {} };
6585
- matrix[key] = parsed.provider ? { provider: parsed.provider, model: parsed.model } : { model: parsed.model };
6586
- cfg.modelMatrix = matrix;
7345
+ const matrix2 = { ...cfg.modelMatrix ?? {} };
7346
+ matrix2[key] = parsed.provider ? { provider: parsed.provider, model: parsed.model } : { model: parsed.model };
7347
+ cfg.modelMatrix = matrix2;
6587
7348
  });
6588
7349
  opts.configStore.update({
6589
7350
  modelMatrix: decrypted.modelMatrix
@@ -6598,9 +7359,9 @@ function buildSetModelCommand(opts) {
6598
7359
  return { message: `${color.amber("No matrix entry")} for "${key}".` };
6599
7360
  }
6600
7361
  const decrypted = await patchGlobalConfig2(globalConfigPath, (cfg) => {
6601
- const matrix = { ...cfg.modelMatrix ?? {} };
6602
- delete matrix[key];
6603
- cfg.modelMatrix = matrix;
7362
+ const matrix2 = { ...cfg.modelMatrix ?? {} };
7363
+ delete matrix2[key];
7364
+ cfg.modelMatrix = matrix2;
6604
7365
  });
6605
7366
  opts.configStore.update({
6606
7367
  modelMatrix: decrypted.modelMatrix
@@ -7169,6 +7930,198 @@ function buildTodosCommand(opts) {
7169
7930
  }
7170
7931
  };
7171
7932
  }
7933
+ function findTask(tasks, query) {
7934
+ const asIndex = Number.parseInt(query, 10);
7935
+ if (!Number.isNaN(asIndex)) {
7936
+ const idx = asIndex - 1;
7937
+ const item = tasks[idx];
7938
+ if (item) return { idx, item };
7939
+ }
7940
+ const byId = tasks.findIndex((t) => t.id === query);
7941
+ if (byId >= 0) {
7942
+ const item = tasks[byId];
7943
+ if (item) return { idx: byId, item };
7944
+ }
7945
+ const q = query.toLowerCase();
7946
+ const byTitle = tasks.findIndex((t) => t.title.toLowerCase().includes(q));
7947
+ if (byTitle >= 0) {
7948
+ const item = tasks[byTitle];
7949
+ if (item) return { idx: byTitle, item };
7950
+ }
7951
+ return null;
7952
+ }
7953
+ function validateType(s) {
7954
+ const valid = ["feature", "bugfix", "refactor", "docs", "test", "chore"];
7955
+ return valid.includes(s) ? s : null;
7956
+ }
7957
+ function validatePriority(s) {
7958
+ const valid = ["critical", "high", "medium", "low"];
7959
+ return valid.includes(s) ? s : null;
7960
+ }
7961
+ function validateStatus(s) {
7962
+ const valid = ["pending", "in_progress", "blocked", "failed", "review", "completed"];
7963
+ return valid.includes(s) ? s : null;
7964
+ }
7965
+ function buildTasksCommand(_opts) {
7966
+ return {
7967
+ name: "tasks",
7968
+ category: "Inspect",
7969
+ description: "Manage structured tasks with dependencies, types, and priorities: /tasks [show|add <title>|start|done|fail|status <id> <status>|promote <id>|clear]",
7970
+ help: [
7971
+ "Usage:",
7972
+ " /tasks Show task progress + list",
7973
+ " /tasks show Same as no args",
7974
+ " /tasks add <title> [type] [prio] Add a task",
7975
+ " /tasks start <id|index> Mark task in-progress",
7976
+ " /tasks done <id|index> Mark task completed",
7977
+ " /tasks fail <id|index> Mark task failed",
7978
+ " /tasks status <id> <status> Set exact status (pending|in_progress|blocked|review|completed|failed)",
7979
+ " /tasks depends <id> <depId...> Set dependencies for a task",
7980
+ " /tasks assign <id> <agent> Assign task to an agent/subagent",
7981
+ " /tasks promote <id> Promote task to todo items",
7982
+ " /tasks clear Remove all tasks",
7983
+ "",
7984
+ "Types: feature, bugfix, refactor, docs, test, chore",
7985
+ "Priorities: critical, high, medium, low"
7986
+ ].join("\n"),
7987
+ async run(args, ctx) {
7988
+ const taskPath = ctx.meta?.["task.path"];
7989
+ if (typeof taskPath !== "string" || !taskPath) {
7990
+ return { message: "Task storage is not configured for this session." };
7991
+ }
7992
+ const sessionId = ctx.session?.id ?? "unknown";
7993
+ const file = await loadTasks(taskPath) ?? emptyTaskFile(sessionId);
7994
+ const [verb, ...rest] = args.trim().split(/\s+/);
7995
+ const restJoined = rest.join(" ").trim();
7996
+ switch (verb) {
7997
+ case "":
7998
+ case "show":
7999
+ case "list":
8000
+ return { message: formatTaskList(file.tasks) };
8001
+ case "progress":
8002
+ case "statusline":
8003
+ return { message: formatTaskProgress(file.tasks) };
8004
+ case "add": {
8005
+ if (!restJoined) return { message: "Usage: /tasks add <title> [type] [priority]" };
8006
+ const parts = restJoined.split(/\s+/);
8007
+ const title = parts[0] ?? "";
8008
+ const type = validateType(parts[1] ?? "") ?? "feature";
8009
+ const priority = validatePriority(parts[2] ?? "") ?? "medium";
8010
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8011
+ const task = {
8012
+ id: `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
8013
+ title,
8014
+ type,
8015
+ priority,
8016
+ status: "pending",
8017
+ createdAt: now,
8018
+ updatedAt: now
8019
+ };
8020
+ file.tasks.push(task);
8021
+ await saveTasks(taskPath, file);
8022
+ return { message: `Added: ${task.title}
8023
+
8024
+ ${formatTaskProgress(file.tasks)}` };
8025
+ }
8026
+ case "start":
8027
+ case "done":
8028
+ case "fail": {
8029
+ if (!restJoined) return { message: `Usage: /tasks ${verb} <id|index>` };
8030
+ const found = findTask(file.tasks, restJoined);
8031
+ if (!found) return { message: `No task matched "${restJoined}".` };
8032
+ const statusMap = {
8033
+ start: "in_progress",
8034
+ done: "completed",
8035
+ fail: "failed"
8036
+ };
8037
+ found.item.status = statusMap[verb] ?? "pending";
8038
+ found.item.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
8039
+ await saveTasks(taskPath, file);
8040
+ return { message: `Marked ${verb}: ${found.item.title}
8041
+
8042
+ ${formatTaskProgress(file.tasks)}` };
8043
+ }
8044
+ case "status": {
8045
+ if (rest.length < 2) return { message: "Usage: /tasks status <id> <pending|in_progress|blocked|review|completed|failed>" };
8046
+ const targetId = rest[0] ?? "";
8047
+ const newStatus = validateStatus(rest[1] ?? "");
8048
+ if (!newStatus) return { message: `Invalid status "${rest[1]}". Use: pending, in_progress, blocked, review, completed, failed.` };
8049
+ const found = findTask(file.tasks, targetId);
8050
+ if (!found) return { message: `No task matched "${targetId}".` };
8051
+ found.item.status = newStatus;
8052
+ found.item.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
8053
+ await saveTasks(taskPath, file);
8054
+ return { message: `Status \u2192 ${newStatus}: ${found.item.title}
8055
+
8056
+ ${formatTaskProgress(file.tasks)}` };
8057
+ }
8058
+ case "depends":
8059
+ case "deps": {
8060
+ if (rest.length < 2) return { message: "Usage: /tasks depends <id> <depId1> [depId2 ...]" };
8061
+ const targetId = rest[0] ?? "";
8062
+ const depIds = rest.slice(1);
8063
+ const found = findTask(file.tasks, targetId);
8064
+ if (!found) return { message: `No task matched "${targetId}".` };
8065
+ found.item.dependsOn = depIds;
8066
+ found.item.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
8067
+ await saveTasks(taskPath, file);
8068
+ return { message: `Dependencies set for "${found.item.title}": ${depIds.join(", ")}` };
8069
+ }
8070
+ case "assign": {
8071
+ if (rest.length < 2) return { message: "Usage: /tasks assign <id> <agent>" };
8072
+ const targetId = rest[0] ?? "";
8073
+ const agent = rest.slice(1).join(" ");
8074
+ const found = findTask(file.tasks, targetId);
8075
+ if (!found) return { message: `No task matched "${targetId}".` };
8076
+ found.item.assignee = agent;
8077
+ found.item.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
8078
+ await saveTasks(taskPath, file);
8079
+ return { message: `Assigned to ${agent}: "${found.item.title}"` };
8080
+ }
8081
+ case "promote": {
8082
+ if (!restJoined) return { message: "Usage: /tasks promote <id|index>" };
8083
+ const found = findTask(file.tasks, restJoined);
8084
+ if (!found) return { message: `No task matched "${restJoined}".` };
8085
+ found.item.status = "in_progress";
8086
+ found.item.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
8087
+ const todos = [
8088
+ {
8089
+ id: `todo_${Date.now()}_task`,
8090
+ content: found.item.title,
8091
+ status: "in_progress",
8092
+ activeForm: found.item.title
8093
+ }
8094
+ ];
8095
+ if (found.item.description) {
8096
+ todos.push({
8097
+ id: `todo_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
8098
+ content: found.item.description.slice(0, 200),
8099
+ status: "pending"
8100
+ });
8101
+ }
8102
+ ctx.state.replaceTodos(todos);
8103
+ await saveTasks(taskPath, file);
8104
+ return {
8105
+ message: `Promoted to ${todos.length} todo(s): "${found.item.title}"
8106
+
8107
+ ${formatTaskProgress(file.tasks)}`
8108
+ };
8109
+ }
8110
+ case "clear": {
8111
+ const n = file.tasks.length;
8112
+ if (n === 0) return { message: "Tasks were already empty." };
8113
+ file.tasks = [];
8114
+ await saveTasks(taskPath, file);
8115
+ return { message: `Cleared ${n} task${n === 1 ? "" : "s"}.` };
8116
+ }
8117
+ default:
8118
+ return {
8119
+ message: `Unknown subcommand "${verb}". Try: show | add <title> | start <id> | done <id> | fail <id> | status <id> <s> | depends <id> <deps> | assign <id> <agent> | promote <id> | clear`
8120
+ };
8121
+ }
8122
+ }
8123
+ };
8124
+ }
7172
8125
  function buildToolsCommand(opts) {
7173
8126
  return {
7174
8127
  name: "tools",
@@ -7296,6 +8249,7 @@ function buildBuiltinSlashCommands(opts) {
7296
8249
  buildPluginCommand(opts),
7297
8250
  buildPruneCommand(opts),
7298
8251
  buildMcpSlashCommand(opts),
8252
+ buildAuthCommand(opts),
7299
8253
  buildDiagCommand(opts),
7300
8254
  buildStatsCommand(opts),
7301
8255
  buildSpawnCommand(opts),
@@ -7305,6 +8259,7 @@ function buildBuiltinSlashCommands(opts) {
7305
8259
  buildEnhanceCommand(opts),
7306
8260
  buildMemoryCommand(opts),
7307
8261
  buildTodosCommand(opts),
8262
+ buildTasksCommand(),
7308
8263
  buildSddCommand(opts),
7309
8264
  buildSaveCommand(opts),
7310
8265
  buildLoadCommand(opts),
@@ -7433,7 +8388,7 @@ async function runProjectCheck(opts) {
7433
8388
  try {
7434
8389
  const { spawn: spawn4 } = await import('child_process');
7435
8390
  await new Promise((resolve5, reject) => {
7436
- const child = spawn4("git", ["init"], { cwd });
8391
+ const child = spawn4("git", ["init"], { cwd, signal: AbortSignal.timeout(1e4) });
7437
8392
  child.on("error", reject);
7438
8393
  child.on("close", (code) => code === 0 ? resolve5() : reject(new Error(`git init failed with ${code}`)));
7439
8394
  });
@@ -8139,20 +9094,14 @@ async function restoreLast(homeFn = defaultHomeDir) {
8139
9094
  }
8140
9095
 
8141
9096
  // src/picker.ts
8142
- function expectDefined6(value) {
8143
- if (value === null || value === void 0) {
8144
- throw new Error("Expected value to be defined");
8145
- }
8146
- return value;
8147
- }
8148
9097
  var theme = { primary: color.amber };
8149
9098
  async function saveToGlobalConfig(configPath2, provider, model, homeFn = () => process.env.HOME ?? os2__default.homedir()) {
8150
9099
  try {
8151
9100
  const { atomicWrite: atomicWrite14 } = await import('@wrongstack/core');
8152
- const fs26 = await import('fs/promises');
9101
+ const fs27 = await import('fs/promises');
8153
9102
  let existing = {};
8154
9103
  try {
8155
- const raw = await fs26.readFile(configPath2, "utf8");
9104
+ const raw = await fs27.readFile(configPath2, "utf8");
8156
9105
  existing = JSON.parse(raw);
8157
9106
  } catch {
8158
9107
  }
@@ -8340,7 +9289,7 @@ async function pickModel(provider, registry, renderer, reader, defaultModel) {
8340
9289
  while (offset < models.length) {
8341
9290
  const page = models.slice(offset, offset + pageSize);
8342
9291
  for (let i = 0; i < page.length; i++) {
8343
- const m = expectDefined6(page[i]);
9292
+ const m = expectDefined(page[i]);
8344
9293
  const num = offset + i + 1;
8345
9294
  const ctx = m.limit?.context ? `${(m.limit.context / 1e3).toFixed(0)}k`.padStart(6) : " ?";
8346
9295
  const cost = m.cost?.input !== void 0 ? `$${m.cost.input}/$${m.cost.output ?? "?"}` : "";
@@ -8488,12 +9437,12 @@ function pickGroupIndex(opts) {
8488
9437
  try {
8489
9438
  let current = 0;
8490
9439
  try {
8491
- const parsed = Number.parseInt(fs12.readFileSync(opts.cursorFile, "utf8").trim(), 10);
9440
+ const parsed = Number.parseInt(fs13.readFileSync(opts.cursorFile, "utf8").trim(), 10);
8492
9441
  if (Number.isFinite(parsed)) current = wrap(parsed);
8493
9442
  } catch {
8494
9443
  }
8495
- fs12.mkdirSync(path8.dirname(opts.cursorFile), { recursive: true });
8496
- fs12.writeFileSync(opts.cursorFile, String(wrap(current + 1)));
9444
+ fs13.mkdirSync(path8.dirname(opts.cursorFile), { recursive: true });
9445
+ fs13.writeFileSync(opts.cursorFile, String(wrap(current + 1)));
8497
9446
  return current;
8498
9447
  } catch {
8499
9448
  }
@@ -9001,333 +9950,201 @@ async function spawnACPAgent(args, deps) {
9001
9950
  }
9002
9951
  }
9003
9952
 
9004
- // src/auth-menu.ts
9953
+ // src/auth-menu/add-provider.ts
9005
9954
  init_provider_config_utils();
9006
- async function runAuthMenu(deps) {
9007
- for (; ; ) {
9008
- const providers = await loadProviders(deps);
9009
- renderTopMenu(deps.renderer, providers);
9010
- const ids = Object.keys(providers).sort();
9011
- const choice = (await deps.reader.readLine(`
9012
- ${color.amber("?")} Pick: `)).trim().toLowerCase();
9013
- if (!choice || choice === "q" || choice === "quit" || choice === "exit") {
9014
- deps.renderer.write(color.dim("Done.\n"));
9015
- return 0;
9016
- }
9017
- if (choice === "a" || choice === "add") {
9018
- await addForNewProvider(deps);
9019
- continue;
9020
- }
9021
- if (choice === "c" || choice === "custom") {
9022
- await addCustomProvider(deps);
9023
- continue;
9024
- }
9025
- const idx = Number.parseInt(choice, 10);
9026
- if (!Number.isNaN(idx) && idx >= 1 && idx <= ids.length) {
9027
- const pid = expectDefined7(ids[idx - 1]);
9028
- await manageProvider(pid, deps);
9029
- continue;
9030
- }
9031
- const byId = ids.find((id) => id.toLowerCase() === choice);
9032
- if (byId) {
9033
- await manageProvider(byId, deps);
9034
- continue;
9035
- }
9036
- deps.renderer.writeError(`Unknown selection: "${choice}"`);
9037
- }
9955
+
9956
+ // src/auth-menu/helpers.ts
9957
+ init_provider_config_utils();
9958
+ async function loadProviders(deps) {
9959
+ return loadConfigProviders(deps.globalConfigPath, deps.vault, {
9960
+ warn: (msg) => deps.renderer.writeWarning(msg)
9961
+ });
9038
9962
  }
9039
- function renderTopMenu(renderer, providers) {
9040
- renderer.write(`
9041
- ${color.bold("WrongStack")} ${color.dim("\u2014 API keys")}
9042
9963
 
9043
- `);
9044
- const ids = Object.keys(providers).sort();
9045
- if (ids.length === 0) {
9046
- renderer.write(color.dim(" No providers configured yet.\n"));
9964
+ // src/auth-menu/shared.ts
9965
+ init_provider_config_utils();
9966
+ function renderProviderLine(renderer, id, cfg, idx) {
9967
+ const keys = normalizeKeys(cfg);
9968
+ const active = activeLabel(cfg, keys);
9969
+ const firstKey = keys[0];
9970
+ let summary;
9971
+ if (keys.length === 0) {
9972
+ summary = color.dim("(no keys)");
9973
+ } else if (keys.length === 1) {
9974
+ summary = maskedKey(firstKey?.apiKey ?? "");
9047
9975
  } else {
9048
- renderer.write(` ${color.dim("Saved providers:")}
9049
- `);
9050
- let idx = 1;
9051
- for (const id of ids) {
9052
- const cfg = providers[id];
9053
- if (!cfg) continue;
9054
- const keys = normalizeKeys(cfg);
9055
- const active = activeLabel(cfg, keys);
9056
- const firstKey = keys[0];
9057
- const summary = keys.length === 0 ? color.dim("(no keys)") : keys.length === 1 ? maskedKey(firstKey?.apiKey ?? "") : `${color.dim(`${keys.length} keys`)} ${color.dim("active:")} ${color.bold(active ?? "?")} ${maskedKey(keys.find((k) => k.label === active)?.apiKey ?? firstKey?.apiKey ?? "")}`;
9058
- const fam = cfg.family ? color.dim(`[${cfg.family}]`) : "";
9059
- const aliasHint = cfg.type && cfg.type !== id ? color.dim(`\u2192 ${cfg.type}`) : "";
9060
- renderer.write(
9061
- ` ${color.dim(`${idx}.`.padStart(4))} ${id.padEnd(22)} ${fam} ${aliasHint} ${summary}
9062
- `
9063
- );
9064
- idx++;
9065
- }
9066
- }
9067
- renderer.write(`
9068
- ${color.dim("Actions:")}
9069
- `);
9070
- renderer.write(` ${color.bold("a")} Add key for a new provider (from catalog)
9071
- `);
9072
- renderer.write(` ${color.bold("c")} Add custom provider (type + family + baseUrl)
9073
- `);
9074
- renderer.write(` ${color.bold("q")} Quit
9075
- `);
9076
- if (ids.length > 0) {
9077
- renderer.write(color.dim(`
9078
- Pick a number to manage that provider's keys.
9079
- `));
9976
+ const activeKeyObj = active != null ? keys.find((k) => k.label === active) : void 0;
9977
+ summary = `${color.dim(`${keys.length} keys`)} ${color.dim("active:")} ${color.bold(active ?? "?")} ` + maskedKey(activeKeyObj?.apiKey ?? firstKey?.apiKey ?? "");
9080
9978
  }
9979
+ const fam = cfg.family ? color.dim(`[${cfg.family}]`) : "";
9980
+ const aliasHint = cfg.type && cfg.type !== id ? color.dim(`\u2192 ${cfg.type}`) : "";
9981
+ renderer.write(
9982
+ ` ${color.dim(`${idx}.`.padStart(4))} ${id.padEnd(22)} ${fam} ${aliasHint} ${summary}
9983
+ `
9984
+ );
9081
9985
  }
9082
- async function manageProvider(providerId, deps) {
9083
- for (; ; ) {
9084
- const providers = await loadProviders(deps);
9085
- const cfg = providers[providerId];
9086
- if (!cfg) {
9087
- deps.renderer.writeError(`Provider "${providerId}" no longer in config.`);
9088
- return;
9089
- }
9090
- const keys = normalizeKeys(cfg);
9091
- const active = activeLabel(cfg, keys);
9092
- deps.renderer.write(
9093
- `
9986
+ function renderProviderHeader(renderer, providerId, cfg) {
9987
+ const keys = normalizeKeys(cfg);
9988
+ const active = activeLabel(cfg, keys);
9989
+ renderer.write(
9990
+ `
9094
9991
  ${color.bold(providerId)} ${cfg.family ? color.dim(`[${cfg.family}]`) : color.amber("[no family]")}
9095
9992
  `
9096
- );
9097
- deps.renderer.write(
9098
- color.dim(` type: ${cfg.type ?? providerId}
9099
- `) + color.dim(
9100
- ` family: ${cfg.family ?? "(unset \u2192 resolved from models.dev when type matches)"}
9101
- `
9102
- ) + color.dim(` baseUrl: ${cfg.baseUrl ?? "(unset \u2192 catalog default)"}
9103
- `)
9104
- );
9105
- if (cfg.envVars && cfg.envVars.length > 0) {
9106
- deps.renderer.write(color.dim(` envVars: ${cfg.envVars.join(", ")}
9107
- `));
9108
- }
9109
- if (cfg.models && cfg.models.length > 0) {
9110
- deps.renderer.write(color.dim(` models: ${cfg.models.join(", ")}
9111
- `));
9993
+ );
9994
+ const details = [
9995
+ color.dim(` type: ${cfg.type ?? providerId}`),
9996
+ color.dim(
9997
+ ` family: ${cfg.family ?? "(unset \u2192 resolved from models.dev when type matches)"}`
9998
+ ),
9999
+ color.dim(` baseUrl: ${cfg.baseUrl ?? "(unset \u2192 catalog default)"}`)
10000
+ ];
10001
+ if (cfg.envVars && cfg.envVars.length > 0) {
10002
+ details.push(color.dim(` envVars: ${cfg.envVars.join(", ")}`));
10003
+ }
10004
+ if (cfg.models && cfg.models.length > 0) {
10005
+ details.push(color.dim(` models: ${cfg.models.join(", ")}`));
10006
+ }
10007
+ renderer.write(details.join("\n") + "\n");
10008
+ if (keys.length === 0) {
10009
+ renderer.write(color.dim(" (no keys saved)\n"));
10010
+ } else {
10011
+ for (let i = 0; i < keys.length; i++) {
10012
+ renderKeyLine(renderer, keys[i], i + 1, active);
9112
10013
  }
9113
- if (keys.length === 0) {
9114
- deps.renderer.write(color.dim(" (no keys saved)\n"));
9115
- } else {
9116
- for (let i = 0; i < keys.length; i++) {
9117
- const k = expectDefined7(keys[i]);
9118
- const marker = k.label === active ? color.green("\u25CF") : color.dim("\u25CB");
9119
- deps.renderer.write(
9120
- ` ${color.dim(`${i + 1}.`.padStart(4))} ${marker} ${k.label.padEnd(20)} ${maskedKey(k.apiKey)} ${color.dim(k.createdAt)}
10014
+ }
10015
+ }
10016
+ function renderKeyLine(renderer, key, idx, active) {
10017
+ const marker = key.label === active ? color.green("\u25CF") : color.dim("\u25CB");
10018
+ renderer.write(
10019
+ ` ${color.dim(`${idx}.`.padStart(4))} ${marker} ${key.label.padEnd(20)} ${maskedKey(key.apiKey)} ${color.dim(key.createdAt)}
9121
10020
  `
9122
- );
9123
- }
9124
- }
9125
- deps.renderer.write(`
10021
+ );
10022
+ }
10023
+ function renderActions(renderer, keysLength) {
10024
+ renderer.write(`
9126
10025
  ${color.dim("Actions:")}
9127
10026
  `);
9128
- deps.renderer.write(` ${color.bold("a")} Add another key
10027
+ renderer.write(` ${color.bold("a")} Add another key
9129
10028
  `);
9130
- if (keys.length > 0) {
9131
- deps.renderer.write(` ${color.bold("u")} <n> Update key <n>
10029
+ if (keysLength > 0) {
10030
+ renderer.write(` ${color.bold("u")} <n> Update key <n>
9132
10031
  `);
9133
- deps.renderer.write(` ${color.bold("d")} <n> Delete key <n>
10032
+ renderer.write(` ${color.bold("d")} <n> Delete key <n>
9134
10033
  `);
9135
- deps.renderer.write(` ${color.bold("s")} <n> Set key <n> as active
10034
+ renderer.write(` ${color.bold("s")} <n> Set key <n> as active
9136
10035
  `);
9137
- }
9138
- deps.renderer.write(` ${color.bold("f")} Edit family
10036
+ }
10037
+ renderer.write(` ${color.bold("f")} Edit family
9139
10038
  `);
9140
- deps.renderer.write(` ${color.bold("B")} Edit baseUrl
10039
+ renderer.write(` ${color.bold("B")} Edit baseUrl
9141
10040
  `);
9142
- deps.renderer.write(` ${color.bold("m")} Edit visible model list
10041
+ renderer.write(` ${color.bold("m")} Edit visible model list
9143
10042
  `);
9144
- deps.renderer.write(` ${color.bold("x")} Remove this provider entirely
10043
+ renderer.write(` ${color.bold("x")} Remove this provider entirely
9145
10044
  `);
9146
- deps.renderer.write(` ${color.bold("b")} Back
10045
+ renderer.write(` ${color.bold("b")} Back
9147
10046
  `);
9148
- deps.renderer.write(` ${color.bold("q")} Quit
10047
+ renderer.write(` ${color.bold("q")} Quit
9149
10048
  `);
9150
- const raw = (await deps.reader.readLine(`
9151
- ${color.amber("?")} ${providerId} > `)).trim();
9152
- if (!raw || raw === "b" || raw === "back" || raw === "q" || raw === "quit") return;
9153
- const [verb, argRaw] = raw.split(/\s+/, 2);
9154
- const arg = argRaw ? Number.parseInt(argRaw, 10) : Number.NaN;
9155
- if (verb === "a" || verb === "add") {
9156
- await addKeyForProvider(providerId, deps, cfg);
9157
- continue;
9158
- }
9159
- if (verb === "x" || verb === "remove") {
9160
- const confirm = (await deps.reader.readLine(
9161
- ` ${color.amber("?")} Remove provider "${providerId}" and ${keys.length} key(s)? ${color.dim("[y/N/q]")} `
9162
- )).trim().toLowerCase();
9163
- if (confirm === "q") continue;
9164
- if (confirm === "y" || confirm === "yes") {
9165
- await mutateProviders(deps, (all) => {
9166
- delete all[providerId];
9167
- });
9168
- deps.renderer.write(` ${color.green("\u2713")} Removed ${providerId}.
10049
+ }
10050
+ function renderTopMenu(renderer, providers) {
10051
+ renderer.write(
10052
+ `
10053
+ ${color.bold("WrongStack")} ${color.dim("\u2014 API key manager")}
10054
+
10055
+ `
10056
+ );
10057
+ const ids = Object.keys(providers).sort();
10058
+ if (ids.length === 0) {
10059
+ renderer.write(color.dim(" No providers configured yet.\n"));
10060
+ renderer.write(color.dim(" Use (a) to add one from the models.dev catalog, or (c) for a custom provider.\n"));
10061
+ } else {
10062
+ renderer.write(` ${color.dim("Saved providers:")}
9169
10063
  `);
9170
- return;
9171
- }
9172
- continue;
10064
+ let idx = 1;
10065
+ for (const id of ids) {
10066
+ const cfg = providers[id];
10067
+ if (!cfg) continue;
10068
+ renderProviderLine(renderer, id, cfg, idx);
10069
+ idx++;
9173
10070
  }
9174
- if (verb === "u" || verb === "update") {
9175
- if (!Number.isFinite(arg) || arg < 1 || arg > keys.length) {
9176
- deps.renderer.writeError(`Usage: u <1-${keys.length}>`);
9177
- continue;
9178
- }
9179
- const target = expectDefined7(keys[arg - 1]);
9180
- const newKey = await readKeyInput(deps, `Updated key for ${target.label}`);
9181
- if (!newKey) continue;
9182
- await mutateProviders(deps, (all) => {
9183
- const p = all[providerId];
9184
- if (!p) return;
9185
- const list = normalizeKeys(p).map(
9186
- (k) => k.label === target.label ? { ...k, apiKey: newKey, createdAt: nowIso() } : k
9187
- );
9188
- writeKeysBack(p, list);
9189
- });
9190
- deps.renderer.write(` ${color.green("\u2713")} Updated ${providerId}/${target.label}.
10071
+ }
10072
+ renderer.write(`
10073
+ ${color.dim("Actions:")}
9191
10074
  `);
9192
- continue;
9193
- }
9194
- if (verb === "d" || verb === "delete" || verb === "rm") {
9195
- if (!Number.isFinite(arg) || arg < 1 || arg > keys.length) {
9196
- deps.renderer.writeError(`Usage: d <1-${keys.length}>`);
9197
- continue;
9198
- }
9199
- const target = expectDefined7(keys[arg - 1]);
9200
- const confirm = (await deps.reader.readLine(
9201
- ` ${color.amber("?")} Delete key "${target.label}" (${maskedKey(target.apiKey)})? ${color.dim("[y/N/q]")} `
9202
- )).trim().toLowerCase();
9203
- if (confirm === "q") continue;
9204
- if (confirm !== "y" && confirm !== "yes") continue;
9205
- await mutateProviders(deps, (all) => {
9206
- const p = all[providerId];
9207
- if (!p) return;
9208
- const list = normalizeKeys(p).filter((k) => k.label !== target.label);
9209
- writeKeysBack(p, list);
9210
- if (p.activeKey === target.label) {
9211
- p.activeKey = list[0]?.label;
9212
- }
9213
- });
9214
- deps.renderer.write(` ${color.green("\u2713")} Deleted ${providerId}/${target.label}.
10075
+ renderer.write(` ${color.bold("a")} Add a provider (from catalog)
9215
10076
  `);
9216
- continue;
9217
- }
9218
- if (verb === "f" || verb === "family") {
9219
- const current = cfg.family ?? "";
9220
- const ans = (await deps.reader.readLine(
9221
- ` ${color.amber("?")} Family ${color.dim(`(anthropic | openai | openai-compatible | google, empty = unset, current: ${current || "unset"})`)}: `
9222
- )).trim();
9223
- if (ans !== "" && !["anthropic", "openai", "openai-compatible", "google"].includes(ans)) {
9224
- deps.renderer.writeError(`Invalid family: "${ans}"`);
9225
- continue;
9226
- }
9227
- await mutateProviders(deps, (all) => {
9228
- const p = all[providerId];
9229
- if (!p) return;
9230
- if (ans === "") delete p.family;
9231
- else p.family = ans;
9232
- });
9233
- deps.renderer.write(` ${color.green("\u2713")} family \u2192 ${ans || "(unset)"}
10077
+ renderer.write(` ${color.bold("c")} Add a custom provider
9234
10078
  `);
9235
- continue;
9236
- }
9237
- if (verb === "B" || verb === "baseurl" || verb === "base-url") {
9238
- const current = cfg.baseUrl ?? "";
9239
- const ans = (await deps.reader.readLine(
9240
- ` ${color.amber("?")} Base URL ${color.dim(`(empty = unset, current: ${current || "unset"})`)}: `
9241
- )).trim();
9242
- await mutateProviders(deps, (all) => {
9243
- const p = all[providerId];
9244
- if (!p) return;
9245
- if (ans === "") delete p.baseUrl;
9246
- else p.baseUrl = ans;
9247
- });
9248
- deps.renderer.write(` ${color.green("\u2713")} baseUrl \u2192 ${ans || "(unset)"}
10079
+ if (ids.length > 0) {
10080
+ renderer.write(` ${color.dim("1-")}${color.dim(String(ids.length))} ${color.bold("Manage a provider")}
9249
10081
  `);
9250
- continue;
9251
- }
9252
- if (verb === "m" || verb === "models") {
9253
- const current = (cfg.models ?? []).join(", ");
9254
- const ans = (await deps.reader.readLine(
9255
- ` ${color.amber("?")} Model ids ${color.dim(`(comma-separated, empty = catalog default, current: ${current || "none"})`)}: `
9256
- )).trim();
9257
- const list = ans ? ans.split(",").map((s) => s.trim()).filter(Boolean) : [];
9258
- await mutateProviders(deps, (all) => {
9259
- const p = all[providerId];
9260
- if (!p) return;
9261
- if (list.length === 0) delete p.models;
9262
- else p.models = list;
9263
- });
9264
- deps.renderer.write(
9265
- ` ${color.green("\u2713")} models \u2192 ${list.length === 0 ? "(catalog default)" : list.join(", ")}
9266
- `
9267
- );
9268
- continue;
9269
- }
9270
- if (verb === "s" || verb === "set" || verb === "active") {
9271
- if (!Number.isFinite(arg) || arg < 1 || arg > keys.length) {
9272
- deps.renderer.writeError(`Usage: s <1-${keys.length}>`);
9273
- continue;
9274
- }
9275
- const target = expectDefined7(keys[arg - 1]);
9276
- await mutateProviders(deps, (all) => {
9277
- const p = all[providerId];
9278
- if (!p) return;
9279
- const list = normalizeKeys(p);
9280
- writeKeysBack(p, list);
9281
- p.activeKey = target.label;
9282
- });
9283
- deps.renderer.write(
9284
- ` ${color.green("\u2713")} Active key for ${providerId} \u2192 ${color.bold(target.label)}.
9285
- `
9286
- );
9287
- continue;
9288
- }
9289
- deps.renderer.writeError(`Unknown action: "${raw}"`);
9290
10082
  }
10083
+ renderer.write(` ${color.bold("q")} Quit
10084
+ `);
10085
+ }
10086
+ async function readKeyInput(deps, intent) {
10087
+ const key = (await deps.reader.readSecret(
10088
+ ` ${color.amber("?")} ${intent} ${color.dim("(hidden, paste OK)")}: `
10089
+ )).trim();
10090
+ if (!key) {
10091
+ deps.renderer.writeError("No key entered.");
10092
+ return void 0;
10093
+ }
10094
+ return key;
10095
+ }
10096
+ async function confirm(deps, question) {
10097
+ const answer = (await deps.reader.readLine(
10098
+ ` ${color.amber("?")} ${question} ${color.dim("[y/N/q]")} `
10099
+ )).trim().toLowerCase();
10100
+ if (answer === "q" || answer === "quit") return null;
10101
+ return answer === "y" || answer === "yes";
10102
+ }
10103
+ function suggestLabel(usedLabels) {
10104
+ let candidate = "default";
10105
+ if (!usedLabels.has(candidate)) return candidate;
10106
+ let n = 2;
10107
+ while (usedLabels.has(`key${n}`)) n++;
10108
+ return `key${n}`;
10109
+ }
10110
+ function validateFamily(raw) {
10111
+ const valid = ["anthropic", "openai", "openai-compatible", "google"];
10112
+ return valid.includes(raw) ? raw : null;
9291
10113
  }
9292
- async function addForNewProvider(deps) {
10114
+
10115
+ // src/auth-menu/add-provider.ts
10116
+ async function addFromCatalog(deps) {
9293
10117
  let catalog = [];
9294
10118
  try {
9295
- catalog = (await deps.modelsRegistry.listProviders()).filter((p) => p.family !== "unsupported");
10119
+ catalog = (await deps.modelsRegistry.listProviders()).filter(
10120
+ (p) => p.family !== "unsupported"
10121
+ );
9296
10122
  } catch {
9297
- deps.renderer.writeWarning("Catalog unavailable \u2014 falling back to manual entry.");
10123
+ deps.renderer.writeWarning(
10124
+ "Catalog unavailable \u2014 falling back to manual entry.\n"
10125
+ );
9298
10126
  }
9299
10127
  if (catalog.length === 0) {
9300
- const pid = (await deps.reader.readLine(` ${color.amber("?")} Provider id ${color.dim("[q to quit]")}: `)).trim();
9301
- if (!pid || pid === "q") return;
9302
- const fam = (await deps.reader.readLine(
9303
- ` ${color.amber("?")} Family (anthropic/openai/openai-compatible/google): `
9304
- )).trim();
9305
- const baseUrl2 = (await deps.reader.readLine(` ${color.amber("?")} Base URL ${color.dim("(optional)")}: `)).trim();
9306
- await addKeyForProvider(pid, deps, {
9307
- type: pid,
9308
- family: fam || void 0,
9309
- ...baseUrl2 ? { baseUrl: baseUrl2 } : {}
9310
- });
9311
- return;
10128
+ return addManualEntry(deps);
9312
10129
  }
9313
10130
  const saved = new Set(Object.keys(await loadProviders(deps)));
9314
10131
  deps.renderer.write(
9315
10132
  color.dim(
9316
- ` Catalog has ${catalog.length} providers. Filter by name to narrow, or "s" for unsaved-only.
10133
+ ` Catalog: ${catalog.length} providers. Filter to narrow, "s" for unsaved-only, or enter to show all.
9317
10134
  `
9318
10135
  )
9319
10136
  );
9320
10137
  const filterRaw = (await deps.reader.readLine(
9321
- ` ${color.amber("?")} Filter ${color.dim('(substring, "s" for unsaved-only, q to quit)')}: `
10138
+ ` ${color.amber("?")} Filter ${color.dim('(substring / "s" / q to quit)')}: `
9322
10139
  )).trim();
9323
- if (filterRaw === "q") return;
10140
+ if (filterRaw === "q") return false;
9324
10141
  const filterLc = filterRaw.toLowerCase();
9325
10142
  const showUnsavedOnly = filterLc === "s" || filterLc === "unsaved";
9326
- const matches = (p) => {
10143
+ function matches(p) {
9327
10144
  if (showUnsavedOnly) return !saved.has(p.id);
9328
10145
  if (!filterLc) return true;
9329
10146
  return p.id.toLowerCase().includes(filterLc) || p.name.toLowerCase().includes(filterLc);
9330
- };
10147
+ }
9331
10148
  const byFamily = /* @__PURE__ */ new Map();
9332
10149
  let filteredCount = 0;
9333
10150
  for (const p of catalog) {
@@ -9339,18 +10156,25 @@ async function addForNewProvider(deps) {
9339
10156
  }
9340
10157
  if (filteredCount === 0) {
9341
10158
  deps.renderer.writeError(
9342
- `No providers match "${filterRaw}". Try a shorter substring or check \`wstack providers\` for valid ids.`
10159
+ `No providers match "${filterRaw}". Try a shorter substring or check \`wstack providers\`.`
9343
10160
  );
9344
- return;
10161
+ return false;
9345
10162
  }
9346
10163
  if (filterRaw && !showUnsavedOnly) {
9347
10164
  deps.renderer.write(
9348
- color.dim(` ${filteredCount} match${filteredCount === 1 ? "" : "es"} for "${filterRaw}".
9349
- `)
10165
+ color.dim(
10166
+ ` ${filteredCount} match${filteredCount === 1 ? "" : "es"} for "${filterRaw}".
10167
+ `
10168
+ )
9350
10169
  );
9351
10170
  }
9352
10171
  const ordered = [];
9353
- const familyOrder = ["anthropic", "openai", "google", "openai-compatible"];
10172
+ const familyOrder = [
10173
+ "anthropic",
10174
+ "openai",
10175
+ "google",
10176
+ "openai-compatible"
10177
+ ];
9354
10178
  let idx = 1;
9355
10179
  deps.renderer.write("\n");
9356
10180
  for (const fam of familyOrder) {
@@ -9376,7 +10200,7 @@ async function addForNewProvider(deps) {
9376
10200
  `
9377
10201
  ${color.amber("?")} Pick (1-${ordered.length}) or type provider id ${color.dim("[q to quit]")}: `
9378
10202
  )).trim();
9379
- if (!answer || answer === "q") return;
10203
+ if (!answer || answer === "q") return false;
9380
10204
  let chosen;
9381
10205
  const num = Number.parseInt(answer, 10);
9382
10206
  if (!Number.isNaN(num) && num >= 1 && num <= ordered.length) {
@@ -9386,29 +10210,35 @@ ${color.amber("?")} Pick (1-${ordered.length}) or type provider id ${color.dim("
9386
10210
  }
9387
10211
  if (!chosen) {
9388
10212
  deps.renderer.writeError(`No such provider: "${answer}"`);
9389
- return;
10213
+ return false;
9390
10214
  }
10215
+ return addKeyForCatalogProvider(deps, chosen);
10216
+ }
10217
+ async function addKeyForCatalogProvider(deps, chosen) {
9391
10218
  deps.renderer.write(
9392
10219
  color.dim(`
9393
- Defaults from models.dev \u2014 press Enter to keep, or type a new value.
10220
+ Defaults from models.dev \u2014 press Enter to keep, or type overrides.
9394
10221
  `)
9395
10222
  );
9396
- const famRaw = (await deps.reader.readLine(` ${color.amber("?")} Family ${color.dim(`[${chosen.family}]`)} ${color.dim("(q to quit)")}: `)).trim();
9397
- if (famRaw === "q") return;
10223
+ const famRaw = (await deps.reader.readLine(
10224
+ ` ${color.amber("?")} Family ${color.dim(`[${chosen.family}]`)} ${color.dim("(q to quit)")}: `
10225
+ )).trim();
10226
+ if (famRaw === "q") return false;
9398
10227
  let family = chosen.family;
9399
10228
  if (famRaw) {
9400
- if (!["anthropic", "openai", "openai-compatible", "google"].includes(famRaw)) {
10229
+ const validated = validateFamily(famRaw);
10230
+ if (!validated) {
9401
10231
  deps.renderer.writeError(
9402
- `Invalid family: "${famRaw}" (must be anthropic | openai | openai-compatible | google).`
10232
+ `Invalid family: "${famRaw}" (must be: anthropic, openai, openai-compatible, google).`
9403
10233
  );
9404
- return;
10234
+ return false;
9405
10235
  }
9406
- family = famRaw;
10236
+ family = validated;
9407
10237
  }
9408
10238
  const baseRaw = (await deps.reader.readLine(
9409
10239
  ` ${color.amber("?")} Base URL ${color.dim(`[${chosen.apiBase ?? "unset"}]`)} ${color.dim("(q to quit)")}: `
9410
10240
  )).trim();
9411
- if (baseRaw === "q") return;
10241
+ if (baseRaw === "q") return false;
9412
10242
  const baseUrl = baseRaw || chosen.apiBase;
9413
10243
  const providersNow = await loadProviders(deps);
9414
10244
  let suggestedAlias = chosen.id;
@@ -9422,7 +10252,7 @@ ${color.amber("?")} Pick (1-${ordered.length}) or type provider id ${color.dim("
9422
10252
  suggestedAlias = candidate;
9423
10253
  }
9424
10254
  const aliasRaw = (await deps.reader.readLine(
9425
- ` ${color.amber("?")} Save under alias ${color.dim(`[${suggestedAlias}]`)} ${color.dim("(used as `--provider <alias>`)")}: `
10255
+ ` ${color.amber("?")} Save as alias ${color.dim(`[${suggestedAlias}]`)} ${color.dim("(used with --provider <alias>)")}: `
9426
10256
  )).trim();
9427
10257
  const alias = aliasRaw || suggestedAlias;
9428
10258
  const existing = providersNow[alias];
@@ -9436,10 +10266,10 @@ ${color.amber("?")} Pick (1-${ordered.length}) or type provider id ${color.dim("
9436
10266
  New: family=${family}, baseUrl=${baseUrl ?? "(unset)"}
9437
10267
  Pick a different alias to keep them separate.`
9438
10268
  );
9439
- return;
10269
+ return false;
9440
10270
  }
9441
10271
  }
9442
- await addKeyForProvider(alias, deps, {
10272
+ return addKeyForProvider(alias, deps, {
9443
10273
  type: chosen.id,
9444
10274
  family,
9445
10275
  baseUrl,
@@ -9449,39 +10279,41 @@ ${color.amber("?")} Pick (1-${ordered.length}) or type provider id ${color.dim("
9449
10279
  async function addCustomProvider(deps) {
9450
10280
  deps.renderer.write(
9451
10281
  `
9452
- ${color.bold("Custom provider")} ${color.dim("\u2014 for local models or proxies not in the models.dev catalog.")}
10282
+ ${color.bold("Custom provider")} ${color.dim("\u2014 for local models or proxies not in the catalog.")}
9453
10283
  `
9454
10284
  );
9455
10285
  const type = (await deps.reader.readLine(
9456
10286
  ` ${color.amber("?")} Provider id ${color.dim('(e.g. "local-llama", "my-proxy", q to quit)')}: `
9457
10287
  )).trim();
9458
- if (!type || type === "q") return;
10288
+ if (!type || type === "q") return false;
9459
10289
  const existing = (await loadProviders(deps))[type];
9460
10290
  if (existing) {
9461
- deps.renderer.writeWarning(`"${type}" already exists. Pick it from the main menu to edit.`);
9462
- return;
10291
+ deps.renderer.writeWarning(
10292
+ `"${type}" already exists. Pick it from the main menu to edit.`
10293
+ );
10294
+ return false;
9463
10295
  }
9464
10296
  const familyRaw = (await deps.reader.readLine(
9465
10297
  ` ${color.amber("?")} Wire family ${color.dim("(anthropic | openai | openai-compatible | google)")} ${color.dim("(q to quit)")}: `
9466
10298
  )).trim();
9467
- if (familyRaw === "q") return;
9468
- if (!["anthropic", "openai", "openai-compatible", "google"].includes(familyRaw)) {
10299
+ if (familyRaw === "q") return false;
10300
+ const family = validateFamily(familyRaw);
10301
+ if (!family) {
9469
10302
  deps.renderer.writeError(`Invalid family: "${familyRaw}"`);
9470
- return;
10303
+ return false;
9471
10304
  }
9472
- const family = familyRaw;
9473
10305
  const baseUrl = (await deps.reader.readLine(
9474
- ` ${color.amber("?")} Base URL ${color.dim("(e.g. http://localhost:11434/v1, leave empty if not needed)")}: `
10306
+ ` ${color.amber("?")} Base URL ${color.dim("(e.g. http://localhost:11434/v1, optional)")}: `
9475
10307
  )).trim();
9476
10308
  const modelsRaw = (await deps.reader.readLine(
9477
10309
  ` ${color.amber("?")} Model ids ${color.dim("(comma-separated, optional)")}: `
9478
10310
  )).trim();
9479
10311
  const models = modelsRaw ? modelsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
9480
10312
  const envVarsRaw = (await deps.reader.readLine(
9481
- ` ${color.amber("?")} Env var names ${color.dim("(comma-separated, optional fallback for the key)")}: `
10313
+ ` ${color.amber("?")} Env var names ${color.dim("(comma-separated, optional)")}: `
9482
10314
  )).trim();
9483
10315
  const envVars = envVarsRaw ? envVarsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
9484
- await addKeyForProvider(type, deps, {
10316
+ return addKeyForProvider(type, deps, {
9485
10317
  type,
9486
10318
  family,
9487
10319
  ...baseUrl ? { baseUrl } : {},
@@ -9489,49 +10321,295 @@ ${color.bold("Custom provider")} ${color.dim("\u2014 for local models or proxies
9489
10321
  ...envVars ? { envVars } : {}
9490
10322
  });
9491
10323
  }
10324
+ async function addManualEntry(deps) {
10325
+ const pid = (await deps.reader.readLine(
10326
+ ` ${color.amber("?")} Provider id ${color.dim("[q to quit]")}: `
10327
+ )).trim();
10328
+ if (!pid || pid === "q") return false;
10329
+ const famRaw = (await deps.reader.readLine(
10330
+ ` ${color.amber("?")} Family ${color.dim("(anthropic/openai/openai-compatible/google)")}: `
10331
+ )).trim();
10332
+ const family = validateFamily(famRaw);
10333
+ if (!family) {
10334
+ deps.renderer.writeError(`Invalid family: "${famRaw}"`);
10335
+ return false;
10336
+ }
10337
+ const baseUrl = (await deps.reader.readLine(
10338
+ ` ${color.amber("?")} Base URL ${color.dim("(optional)")}: `
10339
+ )).trim();
10340
+ return addKeyForProvider(pid, deps, {
10341
+ type: pid,
10342
+ family,
10343
+ ...baseUrl ? { baseUrl } : {}
10344
+ });
10345
+ }
9492
10346
  async function addKeyForProvider(providerId, deps, template) {
9493
10347
  const providers = await loadProviders(deps);
9494
10348
  const existing = providers[providerId];
9495
10349
  const existingKeys = existing ? normalizeKeys(existing) : [];
9496
10350
  const usedLabels = new Set(existingKeys.map((k) => k.label));
9497
- let defaultLabel = "default";
9498
- if (usedLabels.has(defaultLabel)) {
9499
- let n = 2;
9500
- while (usedLabels.has(`key${n}`)) n++;
9501
- defaultLabel = `key${n}`;
9502
- }
10351
+ const label = await promptForLabel(deps, usedLabels);
10352
+ if (!label) return false;
10353
+ const apiKey = await readKeyInput(deps, `API key for ${providerId}/${label}`);
10354
+ if (!apiKey) return false;
10355
+ await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
10356
+ const existingProv = all[providerId] ?? {
10357
+ type: providerId,
10358
+ ...template
10359
+ };
10360
+ if (!existingProv.type) existingProv.type = providerId;
10361
+ if (!existingProv.family && template.family) {
10362
+ existingProv.family = template.family;
10363
+ }
10364
+ if (!existingProv.baseUrl && template.baseUrl) {
10365
+ existingProv.baseUrl = template.baseUrl;
10366
+ }
10367
+ if (!existingProv.envVars && template.envVars) {
10368
+ existingProv.envVars = template.envVars;
10369
+ }
10370
+ const list = normalizeKeys(existingProv);
10371
+ list.push({ label, apiKey, createdAt: nowIso() });
10372
+ writeKeysBack(existingProv, list);
10373
+ if (!existingProv.activeKey) existingProv.activeKey = label;
10374
+ all[providerId] = existingProv;
10375
+ });
10376
+ deps.renderer.write(
10377
+ ` ${color.green("\u2713")} Saved ${color.bold(providerId)}/${color.bold(label)}.
10378
+ `
10379
+ );
10380
+ deps.renderer.write(
10381
+ color.dim(
10382
+ ` Launch: wstack --provider ${providerId} "<task>"
10383
+ `
10384
+ )
10385
+ );
10386
+ return true;
10387
+ }
10388
+ async function promptForLabel(deps, usedLabels) {
10389
+ const defaultLabel = suggestLabel(usedLabels);
9503
10390
  const labelRaw = (await deps.reader.readLine(
9504
10391
  ` ${color.amber("?")} Label for this key ${color.dim(`[${defaultLabel}]`)}: `
9505
10392
  )).trim();
9506
10393
  const label = labelRaw || defaultLabel;
9507
10394
  if (usedLabels.has(label)) {
9508
10395
  deps.renderer.writeError(
9509
- `Label "${label}" already used for ${providerId}. Use update (u) instead.`
10396
+ `Label "${label}" is already used. Use update (u) instead.`
10397
+ );
10398
+ return null;
10399
+ }
10400
+ return label;
10401
+ }
10402
+
10403
+ // src/auth-menu/provider-menu.ts
10404
+ init_provider_config_utils();
10405
+ async function manageProvider(providerId, deps) {
10406
+ for (; ; ) {
10407
+ const providers = await loadProviders(deps);
10408
+ const cfg = providers[providerId];
10409
+ if (!cfg) {
10410
+ deps.renderer.writeError(`Provider "${providerId}" no longer in config.`);
10411
+ return;
10412
+ }
10413
+ const keys = normalizeKeys(cfg);
10414
+ renderProviderHeader(deps.renderer, providerId, cfg);
10415
+ renderActions(deps.renderer, keys.length);
10416
+ const raw = (await deps.reader.readLine(
10417
+ `
10418
+ ${color.amber("?")} ${providerId} > `
10419
+ )).trim();
10420
+ if (!raw || raw === "b" || raw === "back" || raw === "q" || raw === "quit") {
10421
+ return;
10422
+ }
10423
+ const [verb, argRaw] = raw.split(/\s+/, 2);
10424
+ const arg = argRaw ? Number.parseInt(argRaw, 10) : Number.NaN;
10425
+ const handled = await dispatchAction(verb, arg, providerId, keys, cfg, deps);
10426
+ if (handled === "exit") return;
10427
+ if (handled === "continue") continue;
10428
+ }
10429
+ }
10430
+ async function dispatchAction(verb, arg, providerId, keys, cfg, deps) {
10431
+ if (verb === "a" || verb === "add") {
10432
+ await addKeyForProvider(providerId, deps, cfg);
10433
+ return "continue";
10434
+ }
10435
+ if (verb === "x" || verb === "remove") {
10436
+ const answer = await confirm(deps, `Remove provider "${providerId}" and ${keys.length} key(s)?`);
10437
+ if (answer === null) return "continue";
10438
+ if (answer) {
10439
+ await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
10440
+ delete all[providerId];
10441
+ });
10442
+ deps.renderer.write(` ${color.green("\u2713")} Removed ${providerId}.
10443
+ `);
10444
+ return "exit";
10445
+ }
10446
+ return "continue";
10447
+ }
10448
+ if (verb === "u" || verb === "update") {
10449
+ if (!validKeyIndex(arg, keys.length, deps, "u")) return "continue";
10450
+ const target = expectDefined(keys[arg - 1]);
10451
+ const newKey = await readKeyInput(deps, `New key for ${target.label}`);
10452
+ if (!newKey) return "continue";
10453
+ await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
10454
+ const p = all[providerId];
10455
+ if (!p) return;
10456
+ const list = normalizeKeys(p).map(
10457
+ (k) => k.label === target.label ? { ...k, apiKey: newKey, createdAt: nowIso() } : k
10458
+ );
10459
+ writeKeysBack(p, list);
10460
+ });
10461
+ deps.renderer.write(` ${color.green("\u2713")} Updated ${providerId}/${target.label}.
10462
+ `);
10463
+ return "continue";
10464
+ }
10465
+ if (verb === "d" || verb === "delete" || verb === "rm") {
10466
+ if (!validKeyIndex(arg, keys.length, deps, "d")) return "continue";
10467
+ const target = expectDefined(keys[arg - 1]);
10468
+ const answer = await confirm(
10469
+ deps,
10470
+ `Delete key "${target.label}" (${maskedKey(target.apiKey)})?`
9510
10471
  );
9511
- return;
10472
+ if (answer === null) return "continue";
10473
+ if (!answer) return "continue";
10474
+ await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
10475
+ const p = all[providerId];
10476
+ if (!p) return;
10477
+ const list = normalizeKeys(p).filter((k) => k.label !== target.label);
10478
+ writeKeysBack(p, list);
10479
+ if (p.activeKey === target.label) {
10480
+ p.activeKey = list[0]?.label;
10481
+ }
10482
+ });
10483
+ deps.renderer.write(` ${color.green("\u2713")} Deleted ${providerId}/${target.label}.
10484
+ `);
10485
+ return "continue";
10486
+ }
10487
+ if (verb === "s" || verb === "set" || verb === "active") {
10488
+ if (!validKeyIndex(arg, keys.length, deps, "s")) return "continue";
10489
+ const target = expectDefined(keys[arg - 1]);
10490
+ await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
10491
+ const p = all[providerId];
10492
+ if (!p) return;
10493
+ const list = normalizeKeys(p);
10494
+ writeKeysBack(p, list);
10495
+ p.activeKey = target.label;
10496
+ });
10497
+ deps.renderer.write(
10498
+ ` ${color.green("\u2713")} Active key \u2192 ${color.bold(target.label)}.
10499
+ `
10500
+ );
10501
+ return "continue";
9512
10502
  }
9513
- const apiKey = await readKeyInput(deps, `API key for ${providerId}/${label}`);
9514
- if (!apiKey) {
9515
- deps.renderer.writeError("No key entered. Nothing saved.");
9516
- return;
10503
+ if (verb === "f" || verb === "family") {
10504
+ const current = cfg.family ?? "";
10505
+ const ans = (await deps.reader.readLine(
10506
+ ` ${color.amber("?")} Family ${color.dim(`(anthropic | openai | openai-compatible | google, empty = unset, current: ${current || "unset"})`)}: `
10507
+ )).trim();
10508
+ if (ans !== "") {
10509
+ const validated = validateFamily(ans);
10510
+ if (!validated) {
10511
+ deps.renderer.writeError(`Invalid family: "${ans}". Must be one of: anthropic, openai, openai-compatible, google.`);
10512
+ return "continue";
10513
+ }
10514
+ await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
10515
+ const p = all[providerId];
10516
+ if (!p) return;
10517
+ p.family = validated;
10518
+ });
10519
+ deps.renderer.write(` ${color.green("\u2713")} family \u2192 ${validated}
10520
+ `);
10521
+ } else {
10522
+ await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
10523
+ const p = all[providerId];
10524
+ if (!p) return;
10525
+ delete p.family;
10526
+ });
10527
+ deps.renderer.write(` ${color.green("\u2713")} family \u2192 (unset)
10528
+ `);
10529
+ }
10530
+ return "continue";
9517
10531
  }
9518
- await mutateProviders(deps, (all) => {
9519
- const existingProv = all[providerId] ?? { type: providerId, ...template };
9520
- if (!existingProv.type) existingProv.type = providerId;
9521
- if (!existingProv.family && template.family) existingProv.family = template.family;
9522
- if (!existingProv.baseUrl && template.baseUrl) existingProv.baseUrl = template.baseUrl;
9523
- if (!existingProv.envVars && template.envVars) existingProv.envVars = template.envVars;
9524
- const list = normalizeKeys(existingProv);
9525
- list.push({ label, apiKey, createdAt: nowIso() });
9526
- writeKeysBack(existingProv, list);
9527
- if (!existingProv.activeKey) existingProv.activeKey = label;
9528
- all[providerId] = existingProv;
9529
- });
9530
- deps.renderer.write(
9531
- ` ${color.green("\u2713")} Saved ${color.bold(providerId)}/${color.bold(label)}. ${color.dim("Use `wstack --provider " + providerId + ' "<task>"` to launch.')}
10532
+ if (verb === "B" || verb === "baseurl" || verb === "base-url") {
10533
+ const current = cfg.baseUrl ?? "";
10534
+ const ans = (await deps.reader.readLine(
10535
+ ` ${color.amber("?")} Base URL ${color.dim(`(empty = unset, current: ${current || "unset"})`)}: `
10536
+ )).trim();
10537
+ await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
10538
+ const p = all[providerId];
10539
+ if (!p) return;
10540
+ if (ans === "") delete p.baseUrl;
10541
+ else p.baseUrl = ans;
10542
+ });
10543
+ deps.renderer.write(` ${color.green("\u2713")} baseUrl \u2192 ${ans || "(unset)"}
10544
+ `);
10545
+ return "continue";
10546
+ }
10547
+ if (verb === "m" || verb === "models") {
10548
+ const current = (cfg.models ?? []).join(", ");
10549
+ const ans = (await deps.reader.readLine(
10550
+ ` ${color.amber("?")} Model ids ${color.dim(`(comma-separated, empty = catalog default, current: ${current || "none"})`)}: `
10551
+ )).trim();
10552
+ const list = ans ? ans.split(",").map((s) => s.trim()).filter(Boolean) : [];
10553
+ await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
10554
+ const p = all[providerId];
10555
+ if (!p) return;
10556
+ if (list.length === 0) delete p.models;
10557
+ else p.models = list;
10558
+ });
10559
+ deps.renderer.write(
10560
+ ` ${color.green("\u2713")} models \u2192 ${list.length === 0 ? "(catalog default)" : list.join(", ")}
9532
10561
  `
9533
- );
10562
+ );
10563
+ return "continue";
10564
+ }
10565
+ deps.renderer.writeError(`Unknown action: "${verb}". Type b for back or q to quit.`);
10566
+ return "unknown";
10567
+ }
10568
+ function validKeyIndex(arg, max, deps, verb) {
10569
+ if (!Number.isFinite(arg) || arg < 1 || arg > max) {
10570
+ deps.renderer.writeError(`Usage: ${verb} <1-${max}>`);
10571
+ return false;
10572
+ }
10573
+ return true;
10574
+ }
10575
+
10576
+ // src/auth-menu/top-menu.ts
10577
+ async function runTopMenu(deps) {
10578
+ for (; ; ) {
10579
+ const providers = await loadProviders(deps);
10580
+ renderTopMenu(deps.renderer, providers);
10581
+ const ids = Object.keys(providers).sort();
10582
+ const choice = (await deps.reader.readLine(`
10583
+ ${color.amber("?")} Pick: `)).trim().toLowerCase();
10584
+ if (!choice || choice === "q" || choice === "quit" || choice === "exit") {
10585
+ deps.renderer.write(color.dim("Done.\n"));
10586
+ return 0;
10587
+ }
10588
+ if (choice === "a" || choice === "add") {
10589
+ await addFromCatalog(deps);
10590
+ continue;
10591
+ }
10592
+ if (choice === "c" || choice === "custom") {
10593
+ await addCustomProvider(deps);
10594
+ continue;
10595
+ }
10596
+ const idx = Number.parseInt(choice, 10);
10597
+ if (!Number.isNaN(idx) && idx >= 1 && idx <= ids.length) {
10598
+ const pid = ids[idx - 1];
10599
+ await manageProvider(pid, deps);
10600
+ continue;
10601
+ }
10602
+ const byId = ids.find((id) => id.toLowerCase() === choice);
10603
+ if (byId) {
10604
+ await manageProvider(byId, deps);
10605
+ continue;
10606
+ }
10607
+ deps.renderer.writeError(`Unknown selection: "${choice}"`);
10608
+ }
9534
10609
  }
10610
+
10611
+ // src/auth-menu/direct.ts
10612
+ init_provider_config_utils();
9535
10613
  async function runAuthDirect(deps, opts) {
9536
10614
  const { providerId } = opts;
9537
10615
  const providers = await loadProviders(deps);
@@ -9559,7 +10637,9 @@ async function runAuthDirect(deps, opts) {
9559
10637
  opts.baseUrl ??= knownBase;
9560
10638
  opts.envVars ??= knownEnv;
9561
10639
  }
9562
- const usedLabels = new Set(existing ? normalizeKeys(existing).map((k) => k.label) : []);
10640
+ const usedLabels = new Set(
10641
+ existing ? normalizeKeys(existing).map((k) => k.label) : []
10642
+ );
9563
10643
  let label = opts.label ?? "default";
9564
10644
  if (usedLabels.has(label)) {
9565
10645
  let n = 2;
@@ -9569,7 +10649,7 @@ async function runAuthDirect(deps, opts) {
9569
10649
  }
9570
10650
  const apiKey = await readKeyInput(deps, `API key for ${providerId}/${label}`);
9571
10651
  if (!apiKey) return 1;
9572
- await mutateProviders(deps, (all) => {
10652
+ await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
9573
10653
  const p = all[providerId] ?? { type: providerId };
9574
10654
  if (!p.type) p.type = providerId;
9575
10655
  if (!p.family && opts.family) p.family = opts.family;
@@ -9585,32 +10665,9 @@ async function runAuthDirect(deps, opts) {
9585
10665
  deps.renderer.writeInfo(`Use: wstack --provider ${providerId} "<task>"`);
9586
10666
  return 0;
9587
10667
  }
9588
- async function readKeyInput(deps, intent) {
9589
- const key = (await deps.reader.readSecret(
9590
- ` ${color.amber("?")} ${intent} ${color.dim("(hidden, paste OK)")}: `
9591
- )).trim();
9592
- if (!key) {
9593
- deps.renderer.writeError("No key entered.");
9594
- return void 0;
9595
- }
9596
- return key;
9597
- }
9598
- function loadProviders(deps) {
9599
- return loadConfigProviders(deps.globalConfigPath, deps.vault, {
9600
- warn: (msg) => deps.renderer.writeWarning(msg)
9601
- });
9602
- }
9603
- function mutateProviders(deps, mutator) {
9604
- return mutateConfigProviders(deps.globalConfigPath, deps.vault, mutator);
9605
- }
9606
10668
 
9607
10669
  // src/subcommands/handlers/auth.ts
9608
- function expectDefined8(value) {
9609
- if (value === null || value === void 0) {
9610
- throw new Error("Expected value to be defined");
9611
- }
9612
- return value;
9613
- }
10670
+ init_provider_config_utils();
9614
10671
  var authCmd = async (args, deps) => {
9615
10672
  const flags = parseAuthFlags(args);
9616
10673
  const menuDeps = {
@@ -9620,15 +10677,175 @@ var authCmd = async (args, deps) => {
9620
10677
  vault: deps.vault,
9621
10678
  globalConfigPath: deps.paths.globalConfig
9622
10679
  };
9623
- if (flags.positional.length === 0) return runAuthMenu(menuDeps);
10680
+ if (flags.positional.length === 0) {
10681
+ return runTopMenu(menuDeps);
10682
+ }
10683
+ const first = flags.positional[0];
10684
+ if (first === "list" || first === "ls") {
10685
+ return runAuthList(menuDeps);
10686
+ }
10687
+ if (first === "status") {
10688
+ const pid = flags.positional[1];
10689
+ if (!pid) {
10690
+ deps.renderer.writeError("Usage: wstack auth status <provider>");
10691
+ return 1;
10692
+ }
10693
+ return runAuthStatus(menuDeps, pid);
10694
+ }
10695
+ if (first === "remove" || first === "rm") {
10696
+ const pid = flags.positional[1];
10697
+ if (!pid) {
10698
+ deps.renderer.writeError("Usage: wstack auth remove <provider> [--force]");
10699
+ return 1;
10700
+ }
10701
+ return runAuthRemove(menuDeps, pid);
10702
+ }
9624
10703
  return runAuthDirect(menuDeps, {
9625
- providerId: expectDefined8(flags.positional[0]),
10704
+ providerId: first,
9626
10705
  label: flags.label,
9627
10706
  family: flags.family,
9628
10707
  baseUrl: flags.baseUrl,
9629
10708
  envVars: flags.envVars
9630
10709
  });
9631
10710
  };
10711
+ async function runAuthList(deps) {
10712
+ let providers;
10713
+ try {
10714
+ providers = await loadConfigProviders(deps.globalConfigPath, deps.vault);
10715
+ } catch (err) {
10716
+ deps.renderer.writeError(`Could not read config: ${err.message}`);
10717
+ return 1;
10718
+ }
10719
+ const ids = Object.keys(providers).sort();
10720
+ if (ids.length === 0) {
10721
+ deps.renderer.write(
10722
+ `${color.dim("No providers configured.")}
10723
+ ${color.dim("Run")} ${color.bold("wstack auth")} ${color.dim("to add one.")}
10724
+ `
10725
+ );
10726
+ return 0;
10727
+ }
10728
+ deps.renderer.write(`
10729
+ ${color.bold("Saved providers")} ${color.dim(`(${ids.length})`)}
10730
+
10731
+ `);
10732
+ for (const id of ids) {
10733
+ const cfg = providers[id];
10734
+ if (!cfg) continue;
10735
+ const keys = normalizeKeys(cfg);
10736
+ const active = cfg.activeKey ?? keys[0]?.label;
10737
+ const famTag = cfg.family ? `${cfg.family}` : color.amber("no-family");
10738
+ const aliasHint = cfg.type && cfg.type !== id ? color.dim(` (\u2192 ${cfg.type})`) : "";
10739
+ const modelHint = cfg.models && cfg.models.length > 0 ? color.dim(` [${cfg.models.length} models]`) : "";
10740
+ deps.renderer.write(` ${color.bold(id)}${aliasHint}
10741
+ `);
10742
+ deps.renderer.write(
10743
+ ` family: ${famTag} baseUrl: ${cfg.baseUrl ?? color.dim("unset")}${modelHint}
10744
+ `
10745
+ );
10746
+ if (keys.length === 0) {
10747
+ deps.renderer.write(` ${color.amber("no keys")}
10748
+ `);
10749
+ } else {
10750
+ deps.renderer.write(` ${color.dim(`${keys.length} key${keys.length === 1 ? "" : "s"}:`)}
10751
+ `);
10752
+ for (const k of keys) {
10753
+ const marker = k.label === active ? color.green("\u25CF") : color.dim("\u25CB");
10754
+ deps.renderer.write(
10755
+ ` ${marker} ${k.label.padEnd(18)} ${maskedKey(k.apiKey)} ${color.dim(k.createdAt)}
10756
+ `
10757
+ );
10758
+ }
10759
+ }
10760
+ deps.renderer.write("\n");
10761
+ }
10762
+ deps.renderer.write(
10763
+ color.dim(`Manage: wstack auth Add key: wstack auth <provider>
10764
+ `)
10765
+ );
10766
+ return 0;
10767
+ }
10768
+ async function runAuthStatus(deps, providerId) {
10769
+ let providers;
10770
+ try {
10771
+ providers = await loadConfigProviders(deps.globalConfigPath, deps.vault);
10772
+ } catch (err) {
10773
+ deps.renderer.writeError(`Could not read config: ${err.message}`);
10774
+ return 1;
10775
+ }
10776
+ const cfg = providers[providerId];
10777
+ if (!cfg) {
10778
+ deps.renderer.writeError(`Provider "${providerId}" not found in config.`);
10779
+ deps.renderer.write(
10780
+ color.dim(`Run ${color.bold("wstack auth list")} to see saved providers.
10781
+ `)
10782
+ );
10783
+ return 1;
10784
+ }
10785
+ const keys = normalizeKeys(cfg);
10786
+ const active = cfg.activeKey ?? keys[0]?.label;
10787
+ const lines = [
10788
+ `
10789
+ ${color.bold(providerId)} ${cfg.family ? color.dim(`[${cfg.family}]`) : color.amber("[no family]")}`,
10790
+ "",
10791
+ ` type: ${color.cyan(cfg.type ?? providerId)}`,
10792
+ ` family: ${cfg.family ? color.cyan(cfg.family) : color.dim("unset")}`,
10793
+ ` baseUrl: ${cfg.baseUrl ? color.cyan(cfg.baseUrl) : color.dim("unset")}`
10794
+ ];
10795
+ if (cfg.models?.length) {
10796
+ lines.push(` models: ${color.cyan(cfg.models.join(", "))}`);
10797
+ }
10798
+ if (cfg.envVars?.length) {
10799
+ lines.push(` envVars: ${color.cyan(cfg.envVars.join(", "))}`);
10800
+ }
10801
+ lines.push("");
10802
+ if (keys.length === 0) {
10803
+ lines.push(color.amber(" (no keys saved)"));
10804
+ } else {
10805
+ lines.push(` ${color.dim("Keys:")}`);
10806
+ for (const k of keys) {
10807
+ const marker = k.label === active ? color.green("\u25CF") : color.dim("\u25CB");
10808
+ lines.push(
10809
+ ` ${marker} ${color.bold(k.label.padEnd(18))} ${maskedKey(k.apiKey)} ${color.dim(k.createdAt)}`
10810
+ );
10811
+ }
10812
+ }
10813
+ lines.push("");
10814
+ lines.push(color.dim(`Manage: wstack auth \u2192 pick ${providerId}`));
10815
+ deps.renderer.write(lines.join("\n") + "\n");
10816
+ return 0;
10817
+ }
10818
+ async function runAuthRemove(deps, providerId) {
10819
+ const providers = await loadConfigProviders(deps.globalConfigPath, deps.vault);
10820
+ if (!providers[providerId]) {
10821
+ deps.renderer.writeError(`Provider "${providerId}" not found.`);
10822
+ return 1;
10823
+ }
10824
+ deps.renderer.write(
10825
+ `${color.amber("!")} This will remove "${providerId}" and all its saved keys.
10826
+ `
10827
+ );
10828
+ const answer = (await deps.reader.readLine(
10829
+ ` ${color.amber("?")} Confirm removal? ${color.dim("[y/N]")} `
10830
+ )).trim().toLowerCase();
10831
+ if (answer !== "y" && answer !== "yes") {
10832
+ deps.renderer.write(color.dim("Cancelled.\n"));
10833
+ return 0;
10834
+ }
10835
+ try {
10836
+ await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
10837
+ delete all[providerId];
10838
+ });
10839
+ deps.renderer.write(
10840
+ ` ${color.green("\u2713")} Removed ${color.bold(providerId)}.
10841
+ `
10842
+ );
10843
+ return 0;
10844
+ } catch (err) {
10845
+ deps.renderer.writeError(`Failed to remove: ${err.message}`);
10846
+ return 1;
10847
+ }
10848
+ }
9632
10849
 
9633
10850
  // src/subcommands/handlers/update.ts
9634
10851
  init_update_check();
@@ -9658,7 +10875,8 @@ var updateCmd = async (args, deps) => {
9658
10875
  const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
9659
10876
  const child = spawn(npmCommand, ["install", "-g", "wrongstack@latest"], {
9660
10877
  cwd,
9661
- stdio: "pipe"
10878
+ stdio: "pipe",
10879
+ signal: AbortSignal.timeout(12e4)
9662
10880
  });
9663
10881
  let _stderr = "";
9664
10882
  child.stderr?.on("data", (d) => {
@@ -9845,12 +11063,6 @@ var doctorCmd = async (_args, deps) => {
9845
11063
  deps.renderer.write(color.green("All checks passed.\n"));
9846
11064
  return 0;
9847
11065
  };
9848
- function expectDefined9(value) {
9849
- if (value === null || value === void 0) {
9850
- throw new Error("Expected value to be defined");
9851
- }
9852
- return value;
9853
- }
9854
11066
  var exportCmd = async (args, deps) => {
9855
11067
  if (!deps.sessionStore) {
9856
11068
  deps.renderer.writeError("No session store configured.");
@@ -9862,7 +11074,7 @@ var exportCmd = async (args, deps) => {
9862
11074
  let includeDiagnostics = true;
9863
11075
  let sessionId;
9864
11076
  for (let i = 0; i < args.length; i++) {
9865
- const a = expectDefined9(args[i]);
11077
+ const a = expectDefined(args[i]);
9866
11078
  if (a === "--format" || a === "-f") {
9867
11079
  const v = args[++i];
9868
11080
  if (v !== "markdown" && v !== "json" && v !== "text") {
@@ -10124,12 +11336,6 @@ async function serveMcpStdio(deps) {
10124
11336
  }
10125
11337
 
10126
11338
  // src/subcommands/handlers/mcp.ts
10127
- function expectDefined10(value) {
10128
- if (value === null || value === void 0) {
10129
- throw new Error("Expected value to be defined");
10130
- }
10131
- return value;
10132
- }
10133
11339
  var BUILT_IN_MCP = allServers();
10134
11340
  var mcpCmd = async (args, deps) => {
10135
11341
  const sub = args[0];
@@ -10180,7 +11386,7 @@ async function addMcpServer(args, deps) {
10180
11386
  `);
10181
11387
  if (Object.keys(deps.config.mcpServers ?? {}).length === 0)
10182
11388
  for (const k of Object.keys(BUILT_IN_MCP)) {
10183
- const s = expectDefined10(BUILT_IN_MCP[k]);
11389
+ const s = expectDefined(BUILT_IN_MCP[k]);
10184
11390
  deps.renderer.write(` ${k.padEnd(20)} ${s.description}
10185
11391
  `);
10186
11392
  }
@@ -10458,12 +11664,6 @@ var projectsCmd = async (_args, deps) => {
10458
11664
  return 0;
10459
11665
  }
10460
11666
  };
10461
- function expectDefined11(value) {
10462
- if (value === null || value === void 0) {
10463
- throw new Error("Expected value to be defined");
10464
- }
10465
- return value;
10466
- }
10467
11667
  var providersCmd = async (args, deps) => {
10468
11668
  const showAll = args.includes("--all");
10469
11669
  const showUnsupported = args.includes("--unsupported");
@@ -10511,7 +11711,7 @@ ${color.dim(`Current: ${deps.config.provider ?? "<unset>"} / ${deps.config.model
10511
11711
  function parseFlags2(args) {
10512
11712
  const flags = {};
10513
11713
  for (let i = 0; i < args.length; i++) {
10514
- const a = expectDefined11(args[i]);
11714
+ const a = expectDefined(args[i]);
10515
11715
  if (a.startsWith("--")) {
10516
11716
  const eq = a.indexOf("=");
10517
11717
  if (eq !== -1) {
@@ -10531,7 +11731,7 @@ function parseFlags2(args) {
10531
11731
  function positionals(args) {
10532
11732
  const out = [];
10533
11733
  for (let i = 0; i < args.length; i++) {
10534
- const a = expectDefined11(args[i]);
11734
+ const a = expectDefined(args[i]);
10535
11735
  if (a.startsWith("--")) {
10536
11736
  const eq = a.indexOf("=");
10537
11737
  if (eq === -1) {
@@ -10686,7 +11886,7 @@ function parseSizeFlag(raw) {
10686
11886
  const s = raw.trim().toLowerCase();
10687
11887
  const match = /^(\d+(?:\.\d+)?)\s*(k|m|b)?$/.exec(s);
10688
11888
  if (!match) return void 0;
10689
- const num = Number.parseFloat(expectDefined11(match[1]));
11889
+ const num = Number.parseFloat(expectDefined(match[1]));
10690
11890
  const unit = match[2];
10691
11891
  if (unit === "b") return Math.round(num * 1e9);
10692
11892
  if (unit === "m") return Math.round(num * 1e6);
@@ -11007,12 +12207,6 @@ Fleet Run: ${runId}
11007
12207
  }
11008
12208
 
11009
12209
  // src/subcommands/handlers/sessions-config.ts
11010
- function expectDefined12(value) {
11011
- if (value === null || value === void 0) {
11012
- throw new Error("Expected value to be defined");
11013
- }
11014
- return value;
11015
- }
11016
12210
  var sessionsCmd = async (args, deps) => {
11017
12211
  const sub = args[0];
11018
12212
  if (sub === "fleet") {
@@ -11058,7 +12252,7 @@ var configCmd = async (args, deps) => {
11058
12252
  };
11059
12253
  function extractArg(args, key) {
11060
12254
  const idx = args.indexOf(key);
11061
- if (idx !== -1 && args[idx + 1] !== void 0) return expectDefined12(args[idx + 1]);
12255
+ if (idx !== -1 && args[idx + 1] !== void 0) return expectDefined(args[idx + 1]);
11062
12256
  const eq = key.startsWith("--") ? args.find((a) => a.startsWith(`${key}=`)) : null;
11063
12257
  if (eq) return eq.slice(eq.indexOf("=") + 1);
11064
12258
  return null;
@@ -11134,12 +12328,6 @@ async function runRestore(args, deps) {
11134
12328
  `);
11135
12329
  return 0;
11136
12330
  }
11137
- function expectDefined13(value) {
11138
- if (value === null || value === void 0) {
11139
- throw new Error("Expected value to be defined");
11140
- }
11141
- return value;
11142
- }
11143
12331
  function parseRewindFlags(args) {
11144
12332
  const flags = {};
11145
12333
  for (let i = 0; i < args.length; i++) {
@@ -11154,7 +12342,7 @@ function parseRewindFlags(args) {
11154
12342
  }
11155
12343
  function findSessionId(args) {
11156
12344
  for (let i = 0; i < args.length; i++) {
11157
- const a = expectDefined13(args[i]);
12345
+ const a = expectDefined(args[i]);
11158
12346
  if (a === "--last" || a === "--to") {
11159
12347
  i++;
11160
12348
  continue;
@@ -11406,10 +12594,10 @@ var auditCmd = async (args, deps) => {
11406
12594
  return verify.ok ? 0 : 1;
11407
12595
  };
11408
12596
  async function listAudits(log, dir, deps) {
11409
- const fs26 = await import('fs/promises');
12597
+ const fs27 = await import('fs/promises');
11410
12598
  let entries;
11411
12599
  try {
11412
- entries = await fs26.readdir(dir);
12600
+ entries = await fs27.readdir(dir);
11413
12601
  } catch {
11414
12602
  deps.renderer.write(
11415
12603
  color.dim(`No sessions dir found at ${dir}. Run a session first.`) + "\n"
@@ -11480,6 +12668,10 @@ var helpCmd = async (_args, deps) => {
11480
12668
  " wstack sessions List recent sessions",
11481
12669
  " wstack init Pick provider + model from models.dev",
11482
12670
  " wstack auth Interactive key manager (list/add/update/delete)",
12671
+ " wstack auth list Quick listing of saved providers and keys",
12672
+ " wstack auth status <id> Detailed view of one provider",
12673
+ " wstack auth remove <id> Delete a provider (asks for confirmation)",
12674
+ " wstack auth <provider> Add a key for a provider (--label, --family, \u2026)",
11483
12675
  " wstack config [show|edit] Show or edit effective config",
11484
12676
  " wstack tools List registered tools",
11485
12677
  " wstack skills List discovered skills",
@@ -11560,22 +12752,22 @@ function fmtDuration(ms) {
11560
12752
  const remMin = m - h * 60;
11561
12753
  return `${h}h${remMin}m`;
11562
12754
  }
11563
- function fmtTaskResultLine(r, color52) {
12755
+ function fmtTaskResultLine(r, color56) {
11564
12756
  const stats = `${r.iterations}it ${r.toolCalls}tc ${fmtDuration(r.durationMs)}`;
11565
12757
  const errMsg = typeof r.error === "string" ? r.error : r.error?.message;
11566
12758
  const errKind = typeof r.error === "object" ? r.error?.kind : void 0;
11567
12759
  const errTail = errMsg ? ` \u2014 ${errMsg.replace(/\s+/g, " ").slice(0, 80)}${errMsg.length > 80 ? "\u2026" : ""}` : "";
11568
- const errKindChip = errKind ? color52.dim(` [${errKind}]`) : "";
11569
- const errSnip = errMsg || errKind ? `${errKindChip}${color52.dim(errTail)}` : "";
12760
+ const errKindChip = errKind ? color56.dim(` [${errKind}]`) : "";
12761
+ const errSnip = errMsg || errKind ? `${errKindChip}${color56.dim(errTail)}` : "";
11570
12762
  switch (r.status) {
11571
12763
  case "success":
11572
- return { mark: color52.green("\u2713"), stats, tail: "" };
12764
+ return { mark: color56.green("\u2713"), stats, tail: "" };
11573
12765
  case "timeout":
11574
- return { mark: color52.yellow("\u23F1"), stats: `${color52.yellow("timeout")} ${stats}`, tail: errSnip };
12766
+ return { mark: color56.yellow("\u23F1"), stats: `${color56.yellow("timeout")} ${stats}`, tail: errSnip };
11575
12767
  case "stopped":
11576
- return { mark: color52.dim("\u2298"), stats: `${color52.dim("stopped")} ${stats}`, tail: errSnip };
12768
+ return { mark: color56.dim("\u2298"), stats: `${color56.dim("stopped")} ${stats}`, tail: errSnip };
11577
12769
  case "failed":
11578
- return { mark: color52.red("\u2717"), stats: `${color52.red("failed")} ${stats}`, tail: errSnip };
12770
+ return { mark: color56.red("\u2717"), stats: `${color56.red("failed")} ${stats}`, tail: errSnip };
11579
12771
  }
11580
12772
  }
11581
12773
 
@@ -11697,6 +12889,7 @@ async function boot(argv) {
11697
12889
  const isSingleShot = positional.length > 0 || typeof flags["prompt"] === "string";
11698
12890
  const isInteractiveTTY = isStdinTTY() && !isSingleShot;
11699
12891
  if (isInteractiveTTY) {
12892
+ await checkGitInCwd({ cwd, renderer, reader });
11700
12893
  const cont = await runProjectCheck({ projectRoot, cwd, renderer, reader });
11701
12894
  if (!cont) {
11702
12895
  await reader.close();
@@ -11860,6 +13053,61 @@ async function boot(argv) {
11860
13053
  updateInfo
11861
13054
  };
11862
13055
  }
13056
+ async function checkGitInCwd(opts) {
13057
+ const { cwd, renderer, reader } = opts;
13058
+ const cwdGit = path8.join(cwd, ".git");
13059
+ let hasCwdGit = false;
13060
+ try {
13061
+ await fsp4.access(cwdGit);
13062
+ hasCwdGit = true;
13063
+ } catch {
13064
+ }
13065
+ if (!hasCwdGit) {
13066
+ renderer.write(
13067
+ `
13068
+ ${color.amber("\u25CB")} This folder has no ${color.bold(".git")} repository.
13069
+ `
13070
+ );
13071
+ const answer = (await reader.readLine(
13072
+ ` ${color.amber("?")} Initialize one here? ${color.dim("[y/N]")} `
13073
+ )).trim().toLowerCase();
13074
+ if (answer === "y" || answer === "yes") {
13075
+ try {
13076
+ const { spawn: spawn4 } = await import('child_process');
13077
+ await new Promise((resolve5, reject) => {
13078
+ const child = spawn4("git", ["init"], {
13079
+ cwd,
13080
+ signal: AbortSignal.timeout(1e4)
13081
+ });
13082
+ child.on("error", reject);
13083
+ child.on(
13084
+ "close",
13085
+ (code) => code === 0 ? resolve5() : reject(new Error(`git init failed with ${code}`))
13086
+ );
13087
+ });
13088
+ renderer.write(` ${color.green("\u2713")} Git repository initialized
13089
+ `);
13090
+ hasCwdGit = true;
13091
+ } catch (err) {
13092
+ renderer.writeError(
13093
+ `git init failed: ${err instanceof Error ? err.message : String(err)}
13094
+ `
13095
+ );
13096
+ }
13097
+ }
13098
+ }
13099
+ const parentDir = path8.dirname(cwd);
13100
+ if (parentDir !== cwd) {
13101
+ try {
13102
+ await fsp4.access(path8.join(parentDir, ".git"));
13103
+ renderer.write(
13104
+ ` ${color.dim("\u2139")} A ${color.bold(".git")} repo exists in the parent directory: ${color.dim(parentDir)}
13105
+ `
13106
+ );
13107
+ } catch {
13108
+ }
13109
+ }
13110
+ }
11863
13111
  var CONTEXT_OVERFLOW_RE = /context window|exceeds the context|too many tokens|context.*tokens/i;
11864
13112
  function contextOverflowHint(err) {
11865
13113
  const structured = err.code === ERROR_CODES.PROVIDER_CONTEXT_OVERFLOW || err.code === ERROR_CODES.AGENT_CONTEXT_OVERFLOW;
@@ -12128,7 +13376,7 @@ function parsePredictions(raw, max = 3) {
12128
13376
  }
12129
13377
  return out;
12130
13378
  }
12131
- function extractText(content) {
13379
+ function extractText2(content) {
12132
13380
  if (Array.isArray(content)) {
12133
13381
  return content[0]?.text ?? "";
12134
13382
  }
@@ -12159,7 +13407,7 @@ async function predictNextTasks(input, opts) {
12159
13407
  },
12160
13408
  { signal: internal.signal }
12161
13409
  );
12162
- return parsePredictions(extractText(resp.content), max);
13410
+ return parsePredictions(extractText2(resp.content), max);
12163
13411
  } catch {
12164
13412
  return [];
12165
13413
  } finally {
@@ -12168,12 +13416,6 @@ async function predictNextTasks(input, opts) {
12168
13416
  }
12169
13417
  }
12170
13418
  init_sdd();
12171
- function expectDefined14(value) {
12172
- if (value === null || value === void 0) {
12173
- throw new Error("Expected value to be defined");
12174
- }
12175
- return value;
12176
- }
12177
13419
  async function runRepl(opts) {
12178
13420
  if (opts.banner !== false) printBanner(opts.renderer, opts.projectName);
12179
13421
  await renderGoalBanner(opts);
@@ -12689,7 +13931,7 @@ async function renderGoalBanner(opts) {
12689
13931
  color.dim("Goal: ") + stateColor(summary) + color.dim(` [${goal.goalState}] (iter ${goal.iterations})`) + "\n"
12690
13932
  );
12691
13933
  if (goal.journal.length > 0) {
12692
- const lastEntry = expectDefined14(goal.journal[goal.journal.length - 1]);
13934
+ const lastEntry = expectDefined(goal.journal[goal.journal.length - 1]);
12693
13935
  const statusIcon = lastEntry.status === "success" ? "\u2713" : lastEntry.status === "failure" ? "\u2717" : lastEntry.status === "aborted" ? "\u2298" : lastEntry.status === "skipped" ? "\u229D" : "\xB7";
12694
13936
  opts.renderer.write(
12695
13937
  color.dim(` Last: ${statusIcon} ${lastEntry.task} (${lastEntry.status})`) + "\n"
@@ -13159,8 +14401,8 @@ async function execute(deps) {
13159
14401
  initialGoal: goalFlag,
13160
14402
  initialAsk: askFlag,
13161
14403
  projectRoot,
13162
- getSDDContext: () => {
13163
- const { getActiveSDDContext: getActiveSDDContext2 } = (init_sdd(), __toCommonJS(sdd_exports));
14404
+ getSDDContext: async () => {
14405
+ const { getActiveSDDContext: getActiveSDDContext2 } = await Promise.resolve().then(() => (init_sdd(), sdd_exports));
13164
14406
  return getActiveSDDContext2();
13165
14407
  },
13166
14408
  onSDDOutput: async (output) => {
@@ -13171,7 +14413,7 @@ async function execute(deps) {
13171
14413
  autoDetectTaskCompletion: autoDetectTaskCompletion2,
13172
14414
  getTaskProgress: getTaskProgress2,
13173
14415
  getActiveSDDPhase: getActiveSDDPhase2
13174
- } = (init_sdd(), __toCommonJS(sdd_exports));
14416
+ } = await Promise.resolve().then(() => (init_sdd(), sdd_exports));
13175
14417
  const messages = [];
13176
14418
  const specSaved = await trySaveSpecFromAIOutput2(output);
13177
14419
  if (specSaved)
@@ -13291,12 +14533,6 @@ async function execute(deps) {
13291
14533
  }
13292
14534
  return code;
13293
14535
  }
13294
- function expectDefined15(value) {
13295
- if (value === null || value === void 0) {
13296
- throw new Error("Expected value to be defined");
13297
- }
13298
- return value;
13299
- }
13300
14536
  function buildRoutingRunner(config, host) {
13301
14537
  const standardRunner = makeAgentSubagentRunner({
13302
14538
  factory: host.makeSubagentFactory(config),
@@ -13305,7 +14541,7 @@ function buildRoutingRunner(config, host) {
13305
14541
  return async (task, ctx) => {
13306
14542
  const subCfg = ctx.config;
13307
14543
  if (subCfg.provider === "acp") {
13308
- const cacheKey = subCfg.role ?? subCfg.name ?? expectDefined15(subCfg.id);
14544
+ const cacheKey = subCfg.role ?? subCfg.name ?? expectDefined(subCfg.id);
13309
14545
  return host.buildACPRunner(cacheKey).then((r) => r(task, ctx));
13310
14546
  }
13311
14547
  return standardRunner(task, ctx);
@@ -14163,7 +15399,8 @@ function gitText(args, cwd) {
14163
15399
  const child = spawn("git", args, {
14164
15400
  cwd,
14165
15401
  env: buildChildEnv(),
14166
- stdio: ["ignore", "pipe", "pipe"]
15402
+ stdio: ["ignore", "pipe", "pipe"],
15403
+ signal: AbortSignal.timeout(1e4)
14167
15404
  });
14168
15405
  child.stdout?.on("data", (c) => {
14169
15406
  out += c.toString();
@@ -14186,7 +15423,8 @@ function runCmd(cmd, args, cwd, shell = false) {
14186
15423
  cwd,
14187
15424
  env: buildChildEnv(),
14188
15425
  stdio: ["ignore", "pipe", "pipe"],
14189
- shell: shell || process.platform === "win32"
15426
+ shell: shell || process.platform === "win32",
15427
+ signal: AbortSignal.timeout(3e4)
14190
15428
  });
14191
15429
  child.stdout?.on("data", (c) => {
14192
15430
  out += c.toString();
@@ -14922,10 +16160,9 @@ async function setupCodebaseIndexing(deps) {
14922
16160
  const onError = (err) => logger.debug(`codebase auto-index failed: ${err instanceof Error ? err.message : String(err)}`);
14923
16161
  if (idx.onSessionStart) {
14924
16162
  void runStartupIndex({ projectRoot }).then((r) => {
14925
- const summary = `${color.green("\u2713")} codebase index ready ${color.dim(`\u2014 ${r.symbolsIndexed} symbols \xB7 ${r.filesIndexed} files \xB7 ${r.durationMs}ms`)}`;
14926
- process.stderr.write(`
14927
- ${summary}
14928
- `);
16163
+ logger.info(
16164
+ `codebase index ready: ${r.symbolsIndexed} symbols \xB7 ${r.filesIndexed} files \xB7 ${r.durationMs}ms`
16165
+ );
14929
16166
  }).catch((err) => {
14930
16167
  logger.warn(
14931
16168
  `codebase index (startup) failed: ${err instanceof Error ? err.message : String(err)}`
@@ -14959,7 +16196,7 @@ ${summary}
14959
16196
  let watcher;
14960
16197
  if (idx.watchExternal) {
14961
16198
  try {
14962
- watcher = fs12.watch(projectRoot, { recursive: true }, (_event, filename) => {
16199
+ watcher = fs13.watch(projectRoot, { recursive: true }, (_event, filename) => {
14963
16200
  if (!filename) return;
14964
16201
  const rel = filename.toString();
14965
16202
  if (isIgnored(rel)) return;
@@ -15194,12 +16431,6 @@ Try \`wstack models refresh\` once you have network access, or run with --no-fea
15194
16431
  }
15195
16432
  return { resolvedProvider, provider, providerRegistry };
15196
16433
  }
15197
- function expectDefined16(value) {
15198
- if (value === null || value === void 0) {
15199
- throw new Error("Expected value to be defined");
15200
- }
15201
- return value;
15202
- }
15203
16434
  async function setupSession(params) {
15204
16435
  const {
15205
16436
  config,
@@ -15265,7 +16496,7 @@ async function setupSession(params) {
15265
16496
  const context = new Context({
15266
16497
  systemPrompt,
15267
16498
  provider,
15268
- session: expectDefined16(session),
16499
+ session: expectDefined(session),
15269
16500
  signal: ctxSignal,
15270
16501
  tokenCounter,
15271
16502
  cwd,
@@ -15293,6 +16524,8 @@ async function setupSession(params) {
15293
16524
  );
15294
16525
  const planPath = path8.join(wpaths.projectSessions, `${session?.id}.plan.json`);
15295
16526
  context.state.setMeta("plan.path", planPath);
16527
+ const taskPath = path8.join(wpaths.projectSessions, `${session?.id}.tasks.json`);
16528
+ context.state.setMeta("task.path", taskPath);
15296
16529
  let dirState;
15297
16530
  if (resumeId) {
15298
16531
  try {
@@ -15321,7 +16554,7 @@ async function setupSession(params) {
15321
16554
  }
15322
16555
  }
15323
16556
  return {
15324
- session: expectDefined16(session),
16557
+ session: expectDefined(session),
15325
16558
  sessionRef,
15326
16559
  context,
15327
16560
  restoredMessages,
@@ -15430,12 +16663,6 @@ async function launchEternalFromFlag(deps) {
15430
16663
  }
15431
16664
 
15432
16665
  // src/cli-main.ts
15433
- function expectDefined17(value) {
15434
- if (value === null || value === void 0) {
15435
- throw new Error("Expected value to be defined");
15436
- }
15437
- return value;
15438
- }
15439
16666
  async function main(argv) {
15440
16667
  const ctx = await boot(argv);
15441
16668
  if (typeof ctx === "number") return ctx;
@@ -15455,11 +16682,14 @@ async function main(argv) {
15455
16682
  } = ctx;
15456
16683
  updateInfo = await printUpdateNotice(updateInfo);
15457
16684
  const pathResolver = new DefaultPathResolver(cwd);
16685
+ const events = new EventBus();
16686
+ events.setLogger(logger);
15458
16687
  const container = createDefaultContainer({
15459
16688
  config,
15460
16689
  wpaths,
15461
16690
  logger,
15462
16691
  modelsRegistry,
16692
+ events,
15463
16693
  permission: {
15464
16694
  yolo: config.yolo,
15465
16695
  yoloDestructive: flags["yolo-destructive"] === true || flags["force-all-yolo"] === true,
@@ -15558,9 +16788,9 @@ async function main(argv) {
15558
16788
  if (config.features.memory) {
15559
16789
  toolRegistry.register(rememberTool(memoryStore));
15560
16790
  toolRegistry.register(forgetTool(memoryStore));
16791
+ toolRegistry.register(searchMemoryTool(memoryStore));
16792
+ toolRegistry.register(relatedMemoryTool(memoryStore));
15561
16793
  }
15562
- const events = new EventBus();
15563
- events.setLogger(logger);
15564
16794
  const { metricsSink, healthRegistry } = (() => {
15565
16795
  const ms = setupMetrics({
15566
16796
  flags,
@@ -15574,35 +16804,40 @@ async function main(argv) {
15574
16804
  const tuiOwnsScreen = flags.tui === true && flags["no-tui"] !== true;
15575
16805
  const spinner = new Spinner(process.stderr, { enabled: !tuiOwnsScreen });
15576
16806
  let lastInputTokens = 0;
15577
- events.on("provider.response", (e) => {
16807
+ const teardownHandlers = [];
16808
+ const evOn = (event, handler) => {
16809
+ events.on(event, handler);
16810
+ teardownHandlers.push(() => events.off(event, handler));
16811
+ };
16812
+ evOn("provider.response", (e) => {
15578
16813
  lastInputTokens = e.usage?.input ?? 0;
15579
16814
  updateSpinnerContext();
15580
16815
  });
15581
- events.on("iteration.started", () => {
16816
+ evOn("iteration.started", () => {
15582
16817
  updateSpinnerContext();
15583
16818
  spinner.start(color.dim(`${config.provider}/${config.model} thinking\u2026`));
15584
16819
  });
15585
- events.on("provider.response", () => {
16820
+ evOn("provider.response", () => {
15586
16821
  spinner.stop();
15587
16822
  });
15588
- events.on("error", () => {
16823
+ evOn("error", () => {
15589
16824
  spinner.stop();
15590
16825
  });
15591
16826
  let streamingActive = false;
15592
- events.on("provider.text_delta", (p) => {
16827
+ evOn("provider.text_delta", (p) => {
15593
16828
  if (!streamingActive) {
15594
16829
  spinner.stop();
15595
16830
  streamingActive = true;
15596
16831
  }
15597
16832
  renderer.write(p.text);
15598
16833
  });
15599
- events.on("iteration.completed", () => {
16834
+ evOn("iteration.completed", () => {
15600
16835
  if (streamingActive) {
15601
16836
  renderer.write("\n");
15602
16837
  streamingActive = false;
15603
16838
  }
15604
16839
  });
15605
- events.on("provider.retry", (p) => {
16840
+ evOn("provider.retry", (p) => {
15606
16841
  spinner.stop();
15607
16842
  if (streamingActive) {
15608
16843
  renderer.write("\n");
@@ -15613,7 +16848,7 @@ async function main(argv) {
15613
16848
  `));
15614
16849
  spinner.start(color.dim(`${config.provider}/${config.model} thinking\u2026`));
15615
16850
  });
15616
- events.on("provider.error", (p) => {
16851
+ evOn("provider.error", (p) => {
15617
16852
  spinner.stop();
15618
16853
  if (streamingActive) {
15619
16854
  renderer.write("\n");
@@ -15666,7 +16901,7 @@ async function main(argv) {
15666
16901
  );
15667
16902
  const stats = new SessionStats(events, tokenCounter);
15668
16903
  const errorRing = [];
15669
- events.on("error", (e) => {
16904
+ evOn("error", (e) => {
15670
16905
  const err = e.err;
15671
16906
  const code = err && typeof err === "object" && "code" in err && typeof err.code === "string" ? err.code : "UNKNOWN";
15672
16907
  const message = e.err instanceof Error ? e.err.message : String(e.err);
@@ -15681,7 +16916,7 @@ async function main(argv) {
15681
16916
  }).catch(() => {
15682
16917
  });
15683
16918
  });
15684
- events.on("tool.started", (e) => {
16919
+ evOn("tool.started", (e) => {
15685
16920
  sessionBridge.append({
15686
16921
  type: "tool_call_start",
15687
16922
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -15691,7 +16926,7 @@ async function main(argv) {
15691
16926
  }).catch(() => {
15692
16927
  });
15693
16928
  });
15694
- events.on("tool.executed", (e) => {
16929
+ evOn("tool.executed", (e) => {
15695
16930
  sessionBridge.append({
15696
16931
  type: "tool_call_end",
15697
16932
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -15707,16 +16942,16 @@ async function main(argv) {
15707
16942
  });
15708
16943
  });
15709
16944
  if (!tuiOwnsScreen) {
15710
- events.on("delegate.started", (e) => {
16945
+ evOn("delegate.started", (e) => {
15711
16946
  const task = e.task.length > 100 ? `${e.task.slice(0, 99)}\u2026` : e.task;
15712
16947
  renderer.writeInfo(`\u{1F91D} Delegating \u2192 ${e.target}: ${task}`);
15713
16948
  });
15714
- events.on("delegate.completed", (e) => {
16949
+ evOn("delegate.completed", (e) => {
15715
16950
  const cost = e.costUsd && e.costUsd > 0 ? ` \xB7 $${e.costUsd.toFixed(4)}` : "";
15716
16951
  renderer.writeInfo(`${e.ok ? "\u2705" : "\u274C"} ${e.summary}${cost}`);
15717
16952
  });
15718
16953
  }
15719
- events.on("tool.progress", (e) => {
16954
+ evOn("tool.progress", (e) => {
15720
16955
  sessionBridge.append({
15721
16956
  type: "tool_progress",
15722
16957
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -15726,7 +16961,7 @@ async function main(argv) {
15726
16961
  }).catch(() => {
15727
16962
  });
15728
16963
  });
15729
- events.on("provider.retry", (e) => {
16964
+ evOn("provider.retry", (e) => {
15730
16965
  sessionBridge.append({
15731
16966
  type: "provider_retry",
15732
16967
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -15738,7 +16973,7 @@ async function main(argv) {
15738
16973
  }).catch(() => {
15739
16974
  });
15740
16975
  });
15741
- events.on("provider.error", (e) => {
16976
+ evOn("provider.error", (e) => {
15742
16977
  sessionBridge.append({
15743
16978
  type: "provider_error",
15744
16979
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -15870,6 +17105,13 @@ async function main(argv) {
15870
17105
  logger
15871
17106
  });
15872
17107
  if (fallbackExtension) agent.extensions.register(fallbackExtension);
17108
+ if (config.features.memory && config.features.memoryConsolidation !== false) {
17109
+ agent.extensions.register(
17110
+ new SessionMemoryConsolidator({
17111
+ memoryStore
17112
+ })
17113
+ );
17114
+ }
15873
17115
  const switchProviderAndModel = (providerId, modelId) => {
15874
17116
  try {
15875
17117
  context.provider = buildProviderForId(providerId);
@@ -15916,10 +17158,10 @@ async function main(argv) {
15916
17158
  }
15917
17159
  };
15918
17160
  const fleetRoot = directorMode ? path8.join(wpaths.projectSessions, session.id) : void 0;
15919
- const manifestPath = directorMode ? typeof process.env["WRONGSTACK_FLEET_MANIFEST"] === "string" ? process.env["WRONGSTACK_FLEET_MANIFEST"] : path8.join(expectDefined17(fleetRoot), "fleet.json") : void 0;
15920
- const sharedScratchpadPath = directorMode ? path8.join(expectDefined17(fleetRoot), "shared") : void 0;
15921
- const subagentSessionsRoot = directorMode ? path8.join(expectDefined17(fleetRoot), "subagents") : void 0;
15922
- const stateCheckpointPath = directorMode ? path8.join(expectDefined17(fleetRoot), "director-state.json") : void 0;
17161
+ const manifestPath = directorMode ? typeof process.env["WRONGSTACK_FLEET_MANIFEST"] === "string" ? process.env["WRONGSTACK_FLEET_MANIFEST"] : path8.join(expectDefined(fleetRoot), "fleet.json") : void 0;
17162
+ const sharedScratchpadPath = directorMode ? path8.join(expectDefined(fleetRoot), "shared") : void 0;
17163
+ const subagentSessionsRoot = directorMode ? path8.join(expectDefined(fleetRoot), "subagents") : void 0;
17164
+ const stateCheckpointPath = directorMode ? path8.join(expectDefined(fleetRoot), "director-state.json") : void 0;
15923
17165
  const fleetRootForPromotion = path8.join(wpaths.projectSessions, session.id);
15924
17166
  const brainQueue = new BrainDecisionQueue(events);
15925
17167
  const brain = new ObservableBrainArbiter(
@@ -16338,7 +17580,7 @@ async function main(argv) {
16338
17580
  ...matches.map((m) => ` ${m.subagentId} (${m.runId})`)
16339
17581
  ].join("\n");
16340
17582
  }
16341
- const t = expectDefined17(matches[0]);
17583
+ const t = expectDefined(matches[0]);
16342
17584
  const raw = await fsp4.readFile(t.file, "utf8");
16343
17585
  if (mode === "raw") return raw;
16344
17586
  const lines = raw.split("\n").filter((l) => l.trim());
@@ -16588,6 +17830,8 @@ Restart WrongStack to load or unload plugin code in this session.`;
16588
17830
  parallelEngine?.stop();
16589
17831
  },
16590
17832
  onExit: () => {
17833
+ for (const teardown of teardownHandlers) teardown();
17834
+ teardownHandlers.length = 0;
16591
17835
  brainQueue.dispose();
16592
17836
  void mcpRegistry.stopAll();
16593
17837
  },
@@ -16597,7 +17841,8 @@ Restart WrongStack to load or unload plugin code in this session.`;
16597
17841
  (resolve5, reject) => {
16598
17842
  const child = spawn("git", ["status", "--porcelain"], {
16599
17843
  cwd: cwd2,
16600
- stdio: ["ignore", "pipe", "pipe"]
17844
+ stdio: ["ignore", "pipe", "pipe"],
17845
+ signal: AbortSignal.timeout(5e3)
16601
17846
  });
16602
17847
  let stdout = "";
16603
17848
  child.stdout?.on("data", (d) => {