@wrongstack/cli 0.89.3 → 0.107.2

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, 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, 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';
@@ -18,7 +18,7 @@ import { createDefaultContainer, routeImagesForModel, readClipboardImage } from
18
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';
@@ -36,6 +36,102 @@ var __export = (target, all) => {
36
36
  for (var name in all)
37
37
  __defProp(target, name, { get: all[name], enumerable: true });
38
38
  };
39
+ function normalizeKeys(cfg) {
40
+ if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
41
+ return cfg.apiKeys.map((k) => ({ ...k }));
42
+ }
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
+ });
39
135
  function getSessionState(ctx) {
40
136
  if (!ctx) return sddState;
41
137
  let state = ctx.meta[SDD_META_KEY];
@@ -1496,102 +1592,6 @@ var init_sdd = __esm({
1496
1592
  init_rendering();
1497
1593
  }
1498
1594
  });
1499
- function normalizeKeys(cfg) {
1500
- if (Array.isArray(cfg.apiKeys) && cfg.apiKeys.length > 0) {
1501
- return cfg.apiKeys.map((k) => ({ ...k }));
1502
- }
1503
- if (typeof cfg.apiKey === "string" && cfg.apiKey.length > 0) {
1504
- return [{ label: "default", apiKey: cfg.apiKey, createdAt: "" }];
1505
- }
1506
- return [];
1507
- }
1508
- function writeKeysBack(cfg, keys) {
1509
- if (keys.length === 0) {
1510
- delete cfg.apiKeys;
1511
- delete cfg.apiKey;
1512
- delete cfg.activeKey;
1513
- return;
1514
- }
1515
- cfg.apiKeys = keys;
1516
- const active = keys.find((k) => k.label === cfg.activeKey) ?? expectDefined(keys[0]);
1517
- cfg.apiKey = active.apiKey;
1518
- if (!cfg.activeKey || !keys.some((k) => k.label === cfg.activeKey)) {
1519
- cfg.activeKey = active.label;
1520
- }
1521
- }
1522
- function activeLabel(cfg, keys) {
1523
- if (cfg.activeKey && keys.some((k) => k.label === cfg.activeKey)) return cfg.activeKey;
1524
- return keys[0]?.label;
1525
- }
1526
- function maskedKey(key) {
1527
- if (!key) return color.dim("\u2014");
1528
- if (key.length <= 8) return color.dim("\u2022".repeat(key.length));
1529
- const head = key.slice(0, 4);
1530
- const tail = key.slice(-4);
1531
- return `${color.dim(head + "\u2026")}${tail}`;
1532
- }
1533
- function nowIso() {
1534
- return (/* @__PURE__ */ new Date()).toISOString();
1535
- }
1536
- async function loadConfigProviders(configPath2, vault, opts) {
1537
- const warn = opts?.warn;
1538
- let raw;
1539
- try {
1540
- raw = await fsp4.readFile(configPath2, "utf8");
1541
- } catch (err) {
1542
- if (err.code !== "ENOENT") {
1543
- warn?.(`Could not read ${configPath2}: ${err.message}. Treating as empty.`);
1544
- }
1545
- return {};
1546
- }
1547
- let parsed;
1548
- try {
1549
- parsed = JSON.parse(raw);
1550
- } catch (err) {
1551
- warn?.(`Config at ${configPath2} is not valid JSON: ${err.message}`);
1552
- return {};
1553
- }
1554
- const decrypted = decryptConfigSecrets(parsed, vault);
1555
- return decrypted.providers ?? {};
1556
- }
1557
- async function mutateConfigProviders(configPath2, vault, mutator) {
1558
- let raw;
1559
- let fileExists = true;
1560
- try {
1561
- raw = await fsp4.readFile(configPath2, "utf8");
1562
- } catch (err) {
1563
- if (err.code !== "ENOENT") {
1564
- throw new Error(
1565
- `Refusing to mutate ${configPath2}: ${err.message}`,
1566
- { cause: err }
1567
- );
1568
- }
1569
- fileExists = false;
1570
- raw = "{}";
1571
- }
1572
- let parsed;
1573
- try {
1574
- parsed = JSON.parse(raw);
1575
- } catch (err) {
1576
- if (fileExists) {
1577
- throw new Error(
1578
- `Refusing to overwrite corrupt config at ${configPath2} (${err.message}). Fix or move the file aside before retrying.`,
1579
- { cause: err }
1580
- );
1581
- }
1582
- parsed = {};
1583
- }
1584
- const decrypted = decryptConfigSecrets(parsed, vault);
1585
- const providers = decrypted.providers ?? {};
1586
- mutator(providers);
1587
- decrypted.providers = providers;
1588
- const encrypted = encryptConfigSecrets(decrypted, vault);
1589
- await atomicWrite(configPath2, JSON.stringify(encrypted, null, 2), { mode: 384 });
1590
- }
1591
- var init_provider_config_utils = __esm({
1592
- "src/provider-config-utils.ts"() {
1593
- }
1594
- });
1595
1595
 
1596
1596
  // src/update-check.ts
1597
1597
  var update_check_exports = {};
@@ -1749,7 +1749,7 @@ async function runWebUI(opts) {
1749
1749
  const distDir = path8.resolve(path8.dirname(serverEntry), "..");
1750
1750
  httpServer = createHttpServer({ host, distDir, wsPort });
1751
1751
  const openUrl = `http://${host}:${httpPort}`;
1752
- httpServer.listen(httpPort, host, () => {
1752
+ httpServer?.listen(httpPort, host, () => {
1753
1753
  console.log(
1754
1754
  `
1755
1755
  \u25B8 WebUI ready \u2014 open \x1B[1m${openUrl}\x1B[0m in your browser
@@ -2888,212 +2888,147 @@ function estimateTokens(messages) {
2888
2888
  }
2889
2889
  return total;
2890
2890
  }
2891
- 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");
2892
2903
  return {
2893
- name: "autonomy",
2894
- category: "Agent",
2895
- description: "Toggle or query autonomy mode (self-driving agent).",
2896
- help: [
2897
- "Usage:",
2898
- " /autonomy Show current autonomy status",
2899
- " /autonomy off Disabled \u2014 agent stops after each turn (default)",
2900
- " /autonomy suggest Show next-step suggestions after each turn",
2901
- " /autonomy on Auto-continue \u2014 agent picks next step and proceeds",
2902
- " /autonomy eternal Goal-driven loop \u2014 runs forever against /goal",
2903
- " (prompts to confirm an existing goal; `--keep` to skip prompt)",
2904
- " /autonomy parallel Parallel mode \u2014 4-8 agents per tick, fan-out parallelism",
2905
- " (prompts to confirm an existing goal; `--keep` to skip prompt)",
2906
- " /autonomy stop Stop eternal mode (no-op for other modes)",
2907
- " /autonomy toggle Cycle: off \u2192 suggest \u2192 auto \u2192 eternal \u2192 parallel \u2192 off",
2908
- "",
2909
- "Modes:",
2910
- " off \u2014 Normal interactive mode. Agent stops and waits.",
2911
- " suggest \u2014 After each turn, agent suggests next steps. You pick.",
2912
- " auto \u2014 After each turn, agent picks the best next step and continues.",
2913
- " Runs indefinitely until you press Esc or Ctrl+C.",
2914
- " eternal \u2014 Goal-driven sense/decide/execute/reflect loop. Requires /goal.",
2915
- " Force-enables regular YOLO; destructive-gated calls still use",
2916
- " the permission flow. Runs until /autonomy stop or Ctrl+C twice.",
2917
- " parallel \u2014 Fan-out 4\u20138 subagents per tick. Each tick decomposes the goal,",
2918
- " spawns N agents, awaits results, aggregates. Requires /goal.",
2919
- " Force-enables regular YOLO; destructive-gated calls still use",
2920
- " the permission flow. Runs until /autonomy stop or Ctrl+C twice.",
2921
- "",
2922
- "Eternal stage flow: decide \u2192 execute \u2192 reflect \u2192 sleep | paused | stopped",
2923
- "Stage shown in real-time. Use /goal pause to pause, /goal resume to continue.",
2924
- "",
2925
- "In auto/eternal/parallel modes the agent works autonomously. Press Esc to redirect,",
2926
- "Ctrl+C to stop the active iteration. /autonomy stop ends the eternal loop."
2927
- ].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,
2928
2908
  async run(args) {
2929
- const parts = args.trim().toLowerCase().split(/\s+/).filter(Boolean);
2930
- const arg = parts[0] ?? "";
2931
- const modifiers = parts.slice(1);
2932
- if (!opts.onAutonomy) {
2933
- const msg2 = "Autonomy mode is not available in this session.";
2934
- opts.renderer.writeWarning(msg2);
2935
- return { message: msg2 };
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 ?? "" };
2936
2913
  }
2937
- if (!arg || arg === "status") {
2938
- const current = opts.onAutonomy();
2939
- const labels2 = {
2940
- off: `${color.green("OFF")} ${color.dim("(agent stops after each turn)")}`,
2941
- suggest: `${color.cyan("SUGGEST")} ${color.dim("(shows next-step suggestions)")}`,
2942
- auto: `${color.yellow("AUTO")} ${color.dim("(self-driving \u2014 Esc to redirect, Ctrl+C to stop)")}`,
2943
- eternal: `${color.red("ETERNAL")} ${color.dim("(goal-driven loop \u2014 YOLO, until /autonomy stop)")}`,
2944
- "eternal-parallel": `${color.magenta("PARALLEL")} ${color.dim("(4-8 subagents per tick \u2014 fan-out, until /autonomy stop)")}`
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")
2945
2931
  };
2946
- const lines = [`Autonomy mode: ${labels2[current] ?? current}`];
2947
- try {
2948
- const goal = await loadGoal(goalFilePath(opts.projectRoot));
2949
- if (goal) {
2950
- const u = summarizeUsage(goal);
2951
- lines.push(
2952
- color.dim(
2953
- ` Goal: ${goal.goal.length > 80 ? `${goal.goal.slice(0, 77)}\u2026` : goal.goal}`
2954
- )
2955
- );
2956
- lines.push(
2957
- color.dim(
2958
- ` Engine state: ${goal.engineState} \xB7 iterations: ${goal.iterations} \xB7 journal: ${goal.journal.length}`
2959
- )
2960
- );
2961
- if (u.iterationsWithUsage > 0) {
2962
- lines.push(
2963
- color.dim(
2964
- ` Spent: $${u.totalCostUsd.toFixed(4)} \xB7 ${u.totalInputTokens} in / ${u.totalOutputTokens} out tokens`
2965
- )
2966
- );
2967
- }
2968
- const recent = goal.journal.slice(-10);
2969
- const failed = recent.filter((e) => e.status === "failure").length;
2970
- if (failed > 0) {
2971
- lines.push(
2972
- color.amber(` Recent failures: ${failed} of last ${recent.length} iterations`)
2973
- );
2974
- }
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
2975
2944
  }
2976
- } catch {
2977
- }
2978
- const msg2 = lines.join("\n");
2979
- opts.renderer.write(msg2);
2980
- return { message: msg2 };
2945
+ );
2946
+ } catch {
2947
+ return { message: `${color.red("Error")} could not read config file.` };
2981
2948
  }
2982
- if (arg === "stop" || arg === "halt" || arg === "kill") {
2983
- if (!opts.onEternalStop) {
2984
- const msg3 = "No eternal-mode controller wired in this session.";
2985
- opts.renderer.writeWarning(msg3);
2986
- return { message: msg3 };
2949
+ if (sub === "status") {
2950
+ const pid = parts[1];
2951
+ if (!pid) {
2952
+ return { message: `${color.amber("Usage:")} /auth status <provider>` };
2987
2953
  }
2988
- opts.getEternalEngine?.()?.stop();
2989
- opts.getParallelEngine?.()?.stop();
2990
- opts.onEternalStop();
2991
- opts.onAutonomy("off");
2992
- let summaryLine = "";
2993
- try {
2994
- const goal = await loadGoal(goalFilePath(opts.projectRoot));
2995
- if (goal) {
2996
- const u = summarizeUsage(goal);
2997
- if (u.iterationsWithUsage > 0) {
2998
- summaryLine = "\n" + color.dim(
2999
- ` Spent so far: $${u.totalCostUsd.toFixed(4)} \xB7 ${u.totalInputTokens} in / ${u.totalOutputTokens} out tokens \xB7 ${goal.iterations} total iterations.`
3000
- );
3001
- } else if (goal.iterations > 0) {
3002
- summaryLine = "\n" + color.dim(` Total iterations: ${goal.iterations}.`);
3003
- }
3004
- }
3005
- } catch {
2954
+ const cfg = providers[pid];
2955
+ if (!cfg) {
2956
+ return { message: `${color.yellow("Provider")} "${pid}" not found in saved config.` };
3006
2957
  }
3007
- 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}`;
3008
- opts.renderer.write(msg2);
3009
- return { message: msg2 };
3010
- }
3011
- let newMode;
3012
- if (arg === "on" || arg === "enable" || arg === "true" || arg === "auto") {
3013
- newMode = "auto";
3014
- } else if (arg === "off" || arg === "disable" || arg === "false") {
3015
- newMode = "off";
3016
- } else if (arg === "suggest" || arg === "suggestions") {
3017
- newMode = "suggest";
3018
- } else if (arg === "eternal" || arg === "forever" || arg === "infinite" || arg === "sittinsene") {
3019
- newMode = "eternal";
3020
- } else if (arg === "parallel" || arg === "eternal-parallel" || arg === "fanout") {
3021
- newMode = "eternal-parallel";
3022
- } else if (arg === "toggle" || arg === "cycle") {
3023
- const current = opts.onAutonomy() ?? "off";
3024
- const cycle = ["off", "suggest", "auto", "eternal", "eternal-parallel"];
3025
- newMode = cycle[(cycle.indexOf(current) + 1) % cycle.length] ?? "off";
3026
- } else {
3027
- const msg2 = `Unknown argument: ${arg}. Use /autonomy on, off, suggest, eternal, parallel, stop, or toggle.`;
3028
- opts.renderer.writeWarning(msg2);
3029
- return { message: msg2 };
3030
- }
3031
- if (newMode === "eternal" || newMode === "eternal-parallel") {
3032
- const wantKeep = modifiers.includes("--keep") || modifiers.includes("keep");
3033
- const wantNew = modifiers.includes("--new") || modifiers.includes("new");
3034
- const goal = await loadGoal(goalFilePath(opts.projectRoot));
3035
- if (!goal) {
3036
- const msg3 = `${color.red("Eternal/parallel mode requires a goal.")} Run \`/goal set <mission>\` first.`;
3037
- opts.renderer.writeWarning(msg3);
3038
- 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(", "))}`);
3039
2971
  }
3040
- if (wantNew) {
3041
- 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}`)}.`;
3042
- opts.renderer.writeWarning(msg3);
3043
- return { message: msg3 };
2972
+ if (cfg.envVars?.length) {
2973
+ lines2.push(` envVars: ${color.cyan(cfg.envVars.join(", "))}`);
3044
2974
  }
3045
- const isStale = goal.iterations > 0 || goal.engineState === "running";
3046
- if (!wantKeep) {
3047
- if (opts.confirm) {
3048
- const goalPreview = goal.goal.length > 80 ? `${goal.goal.slice(0, 77)}\u2026` : goal.goal;
3049
- const detail = isStale ? `${color.amber("Stale goal")} (${goal.iterations} iterations, engineState: ${goal.engineState}): "${goalPreview}". Continue with this mission?` : `Existing goal: "${goalPreview}". Use this mission?`;
3050
- const defaultYes = !isStale;
3051
- const answer = await opts.confirm(detail, defaultYes);
3052
- if (answer === null) {
3053
- const msg3 = `${color.dim("Cancelled.")} Autonomy mode unchanged.`;
3054
- opts.renderer.write(msg3);
3055
- return { message: msg3 };
3056
- }
3057
- if (!answer) {
3058
- 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`)}.`;
3059
- opts.renderer.write(msg3);
3060
- return { message: msg3 };
3061
- }
3062
- } else if (isStale) {
3063
- 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>")}.`;
3064
- opts.renderer.writeWarning(msg3);
3065
- 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
+ );
3066
2986
  }
3067
2987
  }
3068
- if (!opts.onEternalStart) {
3069
- const msg3 = "Eternal mode controller is not wired in this session.";
3070
- opts.renderer.writeWarning(msg3);
3071
- return { message: msg3 };
3072
- }
3073
- if (opts.onYolo) opts.onYolo(true);
3074
- opts.onAutonomy(newMode);
3075
- opts.onEternalStart(newMode);
3076
- const modeLabel = newMode === "eternal-parallel" ? `${color.magenta("PARALLEL")} mode` : `${color.red("ETERNAL")} mode`;
3077
- const msg2 = `Autonomy mode: ${modeLabel} \u2014 engine launching against goal: ${color.bold(goal.goal)}
3078
- ${color.dim("Regular YOLO enabled; destructive-gated calls still use the permission flow. Use /autonomy stop to end. Journal at /goal journal.")}`;
3079
- opts.renderer.write(msg2);
3080
- return { message: msg2 };
2988
+ lines2.push("", color.dim(` Manage: wstack auth \u2192 pick ${pid}`));
2989
+ return { message: lines2.join("\n") };
3081
2990
  }
3082
- const previous = opts.onAutonomy();
3083
- if ((previous === "eternal" || previous === "eternal-parallel") && opts.onEternalStop) {
3084
- 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
+ };
3085
3006
  }
3086
- opts.onAutonomy(newMode);
3087
- const labels = {
3088
- off: `${color.green("OFF")} \u2014 agent stops after each turn`,
3089
- suggest: `${color.cyan("SUGGEST")} \u2014 shows next-step suggestions after each turn`,
3090
- auto: `${color.yellow("AUTO")} \u2014 self-driving, agent continues automatically`,
3091
- eternal: `${color.red("ETERNAL")} \u2014 goal-driven sittin-sene loop`,
3092
- "eternal-parallel": `${color.magenta("PARALLEL")} \u2014 fan-out 4-8 subagents per tick`
3093
- };
3094
- const msg = `Autonomy mode: ${labels[newMode]}`;
3095
- opts.renderer.write(msg);
3096
- 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") };
3097
3032
  }
3098
3033
  };
3099
3034
  }
@@ -3255,43 +3190,252 @@ function buildAutoPhaseCommand(opts) {
3255
3190
  }
3256
3191
  };
3257
3192
  }
3258
- function buildBtwCommand(opts) {
3193
+ function buildAutonomyCommand(opts) {
3259
3194
  return {
3260
- name: "btw",
3195
+ name: "autonomy",
3261
3196
  category: "Agent",
3262
- description: 'Drop a "by the way" note for the running agent without interrupting it \u2014 delivered at the next step',
3263
- argsHint: "<note>",
3197
+ description: "Toggle or query autonomy mode (self-driving agent).",
3264
3198
  help: [
3265
- "/btw <note> Stash a note; the agent reads it at the start of its next",
3266
- " iteration (between tool calls) without restarting.",
3267
- "/btw Show how many notes are pending.",
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",
3268
3210
  "",
3269
- "Use `/steer` instead when you need to abort the current work immediately."
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."
3270
3229
  ].join("\n"),
3271
3230
  async run(args) {
3272
- const ctx = opts.context;
3273
- if (!ctx) {
3274
- return { message: "No active session \u2014 start a turn first, then use /btw to nudge it." };
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 };
3275
3238
  }
3276
- const text = args.trim();
3277
- if (!text) {
3278
- const n = pendingBtwCount(ctx);
3279
- return {
3280
- message: n === 0 ? "No notes pending. Usage: /btw <note>" : `${n} note(s) pending \u2014 will reach the agent at its next step.`
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)")}`
3281
3247
  };
3282
- }
3283
- const pending = setBtwNote(ctx, text);
3284
- return {
3285
- message: `\u21AF Noted (${pending} pending) \u2014 the agent will fold this in at its next step:
3286
- ${text}`
3287
- };
3288
- }
3289
- };
3290
- }
3291
-
3292
- // src/slash-commands/clear.ts
3293
- function buildClearCommand(opts) {
3294
- return {
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
+ }
3402
+ function buildBtwCommand(opts) {
3403
+ return {
3404
+ name: "btw",
3405
+ category: "Agent",
3406
+ description: 'Drop a "by the way" note for the running agent without interrupting it \u2014 delivered at the next step',
3407
+ argsHint: "<note>",
3408
+ help: [
3409
+ "/btw <note> Stash a note; the agent reads it at the start of its next",
3410
+ " iteration (between tool calls) without restarting.",
3411
+ "/btw Show how many notes are pending.",
3412
+ "",
3413
+ "Use `/steer` instead when you need to abort the current work immediately."
3414
+ ].join("\n"),
3415
+ async run(args) {
3416
+ const ctx = opts.context;
3417
+ if (!ctx) {
3418
+ return { message: "No active session \u2014 start a turn first, then use /btw to nudge it." };
3419
+ }
3420
+ const text = args.trim();
3421
+ if (!text) {
3422
+ const n = pendingBtwCount(ctx);
3423
+ return {
3424
+ message: n === 0 ? "No notes pending. Usage: /btw <note>" : `${n} note(s) pending \u2014 will reach the agent at its next step.`
3425
+ };
3426
+ }
3427
+ const pending = setBtwNote(ctx, text);
3428
+ return {
3429
+ message: `\u21AF Noted (${pending} pending) \u2014 the agent will fold this in at its next step:
3430
+ ${text}`
3431
+ };
3432
+ }
3433
+ };
3434
+ }
3435
+
3436
+ // src/slash-commands/clear.ts
3437
+ function buildClearCommand(opts) {
3438
+ return {
3295
3439
  name: "clear",
3296
3440
  category: "Session",
3297
3441
  description: "Reset the session and start a new one.",
@@ -5131,6 +5275,112 @@ function buildFleetCommand(opts) {
5131
5275
  }
5132
5276
  };
5133
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
5134
5384
  var KNOWN_VERBS = /* @__PURE__ */ new Set([
5135
5385
  "",
5136
5386
  "show",
@@ -5142,25 +5392,31 @@ var KNOWN_VERBS = /* @__PURE__ */ new Set([
5142
5392
  "journal",
5143
5393
  "log",
5144
5394
  "pause",
5145
- "resume"
5395
+ "resume",
5396
+ "refine"
5146
5397
  ]);
5147
5398
  function buildGoalCommand(opts) {
5148
5399
  return {
5149
5400
  name: "goal",
5150
5401
  category: "Agent",
5151
- 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.",
5152
5403
  help: [
5153
5404
  "Usage:",
5154
- " /goal Show current goal + recent journal",
5155
- " /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",
5156
5408
  " /goal clear Clear the goal (stops eternal mode if running)",
5157
- " /goal pause Pause at end of current iteration (no-op if already paused)",
5158
- " /goal resume Resume a paused goal (no-op if not paused)",
5159
- " /goal status Same as /goal (alias)",
5409
+ " /goal pause Pause at end of current iteration",
5410
+ " /goal resume Resume a paused goal",
5160
5411
  " /goal journal [N] Show last N journal entries (default 25)",
5161
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
+ "",
5162
5418
  "Stage flow: decide \u2192 execute \u2192 reflect \u2192 sleep | paused | stopped",
5163
- "Pausing stops after current iteration completes. Resume continues from next iteration.",
5419
+ "The engine updates progress after each iteration toward the deliverable list.",
5164
5420
  "",
5165
5421
  "Goals live in ~/.wrongstack/projects/<hash>/goal.json and persist across sessions.",
5166
5422
  "A goal is the prerequisite for /autonomy eternal \u2014 the engine consults it on",
@@ -5196,24 +5452,93 @@ function buildGoalCommand(opts) {
5196
5452
  opts.renderer.writeWarning(msg2);
5197
5453
  return { message: msg2 };
5198
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
+ }
5199
5463
  const existing = await loadGoal(goalPath);
5200
- 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
+ };
5201
5480
  await saveGoal(goalPath, next);
5202
- const shortGoal = setText.length > 80 ? `${setText.slice(0, 80)}\u2026` : setText;
5203
- const msg = `\u{1F3AF} ${color.green("Goal locked:")} ${shortGoal}
5204
- ${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.`;
5205
5528
  opts.renderer.write(msg);
5206
- return { message: msg, runText: buildGoalPreamble(setText) };
5529
+ return { message: `${msg}
5530
+
5531
+ ${formatGoal(updated)}` };
5207
5532
  }
5208
5533
  case "clear":
5209
5534
  case "reset": {
5210
- const existing = await loadGoal(goalPath);
5211
- if (!existing) {
5535
+ const current = await loadGoal(goalPath);
5536
+ if (!current) {
5212
5537
  const msg2 = "No goal to clear.";
5213
5538
  opts.renderer.write(msg2);
5214
5539
  return { message: msg2 };
5215
5540
  }
5216
- const abandoned = { ...existing, goalState: "abandoned" };
5541
+ const abandoned = { ...current, goalState: "abandoned" };
5217
5542
  await saveGoal(goalPath, abandoned);
5218
5543
  const { unlink: unlink4 } = await import('fs/promises');
5219
5544
  try {
@@ -5288,7 +5613,7 @@ ${lines.join("\n")}`;
5288
5613
  return { message: msg };
5289
5614
  }
5290
5615
  default: {
5291
- 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]`;
5292
5617
  opts.renderer.writeWarning(msg);
5293
5618
  return { message: msg };
5294
5619
  }
@@ -6751,12 +7076,14 @@ async function patchGlobalConfig2(globalConfigPath, mutate) {
6751
7076
  function buildSetModelCommand(opts) {
6752
7077
  const help = [
6753
7078
  "Usage:",
6754
- " /setmodel Show leader model + the task\u2192model matrix",
7079
+ " /setmodel Show leader model + matrix + resolution summary",
6755
7080
  " /setmodel list List keyed providers, their models, and valid keys",
6756
- " /setmodel leader <provider> <model> Set the main (leader) model",
7081
+ " /setmodel leader <provider> <model> Set the main (leader / brain) model",
6757
7082
  " /setmodel set <key> <provider>/<model> Pin a role/phase/* to a model",
6758
7083
  " /setmodel set <key> <model> Pin to a model on the leader provider",
6759
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)",
6760
7087
  "",
6761
7088
  "Keys: a catalog role (e.g. security-scanner), a phase (" + MATRIX_PHASE_KEYS.join(", ") + "),",
6762
7089
  "or * for the fleet-wide default. Precedence at spawn: role \u2192 phase \u2192 * \u2192 leader.",
@@ -6770,24 +7097,57 @@ function buildSetModelCommand(opts) {
6770
7097
  const lines = [
6771
7098
  `${color.bold("WrongStack")} ${color.dim("\u2014 Models")}`,
6772
7099
  "",
6773
- ` leader: ${color.cyan(`${config.provider}/${config.model}`)} ${color.dim("change: /setmodel leader <provider> <model>")}`,
6774
- "",
6775
- ` ${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
+ ""
6776
7102
  ];
6777
7103
  if (keys.length === 0) {
6778
7104
  lines.push(
6779
- ` ${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>")}`
6780
7108
  );
6781
7109
  } else {
7110
+ lines.push(` ${color.bold("matrix")} ${color.dim("(role \u2192 phase \u2192 * \u2192 leader)")}`);
6782
7111
  for (const k of keys.sort()) {
6783
7112
  const kind = matrixKeyKind(k);
6784
7113
  const tag = kind === "unknown" ? color.red("?") : color.dim(kind);
6785
7114
  lines.push(` ${color.amber(k.padEnd(22))} \u2192 ${fmtEntry(expectDefined(matrix[k]))} ${tag}`);
6786
7115
  }
6787
7116
  }
6788
- lines.push("", color.dim(" /setmodel list for valid keys \xB7 /setmodel help for usage"));
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)}`);
7127
+ }
7128
+ }
7129
+ lines.push("", color.dim(" /setmodel list \xB7 resolve <role> \xB7 doctor \xB7 help"));
6789
7130
  return lines.join("\n");
6790
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
+ }
6791
7151
  return {
6792
7152
  name: "setmodel",
6793
7153
  category: "Config",
@@ -6804,6 +7164,7 @@ function buildSetModelCommand(opts) {
6804
7164
  const config = opts.configStore.get();
6805
7165
  const keyed = keyedProviderIds(config);
6806
7166
  const globalConfigPath = opts.paths.globalConfig;
7167
+ const matrix = config.modelMatrix ?? {};
6807
7168
  if (sub === "list") {
6808
7169
  const provLines = keyed.map((id) => {
6809
7170
  const models = config.providers?.[id]?.models ?? [];
@@ -6824,6 +7185,119 @@ function buildSetModelCommand(opts) {
6824
7185
  ].join("\n")
6825
7186
  };
6826
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
+ }
6827
7301
  try {
6828
7302
  if (sub === "leader") {
6829
7303
  const provider = parts[1];
@@ -6868,9 +7342,9 @@ function buildSetModelCommand(opts) {
6868
7342
  };
6869
7343
  }
6870
7344
  const decrypted = await patchGlobalConfig2(globalConfigPath, (cfg) => {
6871
- const matrix = { ...cfg.modelMatrix ?? {} };
6872
- matrix[key] = parsed.provider ? { provider: parsed.provider, model: parsed.model } : { model: parsed.model };
6873
- 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;
6874
7348
  });
6875
7349
  opts.configStore.update({
6876
7350
  modelMatrix: decrypted.modelMatrix
@@ -6885,9 +7359,9 @@ function buildSetModelCommand(opts) {
6885
7359
  return { message: `${color.amber("No matrix entry")} for "${key}".` };
6886
7360
  }
6887
7361
  const decrypted = await patchGlobalConfig2(globalConfigPath, (cfg) => {
6888
- const matrix = { ...cfg.modelMatrix ?? {} };
6889
- delete matrix[key];
6890
- cfg.modelMatrix = matrix;
7362
+ const matrix2 = { ...cfg.modelMatrix ?? {} };
7363
+ delete matrix2[key];
7364
+ cfg.modelMatrix = matrix2;
6891
7365
  });
6892
7366
  opts.configStore.update({
6893
7367
  modelMatrix: decrypted.modelMatrix
@@ -7456,6 +7930,198 @@ function buildTodosCommand(opts) {
7456
7930
  }
7457
7931
  };
7458
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
+ }
7459
8125
  function buildToolsCommand(opts) {
7460
8126
  return {
7461
8127
  name: "tools",
@@ -7583,6 +8249,7 @@ function buildBuiltinSlashCommands(opts) {
7583
8249
  buildPluginCommand(opts),
7584
8250
  buildPruneCommand(opts),
7585
8251
  buildMcpSlashCommand(opts),
8252
+ buildAuthCommand(opts),
7586
8253
  buildDiagCommand(opts),
7587
8254
  buildStatsCommand(opts),
7588
8255
  buildSpawnCommand(opts),
@@ -7592,6 +8259,7 @@ function buildBuiltinSlashCommands(opts) {
7592
8259
  buildEnhanceCommand(opts),
7593
8260
  buildMemoryCommand(opts),
7594
8261
  buildTodosCommand(opts),
8262
+ buildTasksCommand(),
7595
8263
  buildSddCommand(opts),
7596
8264
  buildSaveCommand(opts),
7597
8265
  buildLoadCommand(opts),
@@ -8769,12 +9437,12 @@ function pickGroupIndex(opts) {
8769
9437
  try {
8770
9438
  let current = 0;
8771
9439
  try {
8772
- const parsed = Number.parseInt(fs12.readFileSync(opts.cursorFile, "utf8").trim(), 10);
9440
+ const parsed = Number.parseInt(fs13.readFileSync(opts.cursorFile, "utf8").trim(), 10);
8773
9441
  if (Number.isFinite(parsed)) current = wrap(parsed);
8774
9442
  } catch {
8775
9443
  }
8776
- fs12.mkdirSync(path8.dirname(opts.cursorFile), { recursive: true });
8777
- 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)));
8778
9446
  return current;
8779
9447
  } catch {
8780
9448
  }
@@ -9282,49 +9950,114 @@ async function spawnACPAgent(args, deps) {
9282
9950
  }
9283
9951
  }
9284
9952
 
9285
- // src/auth-menu.ts
9953
+ // src/auth-menu/add-provider.ts
9286
9954
  init_provider_config_utils();
9287
- async function runAuthMenu(deps) {
9288
- for (; ; ) {
9289
- const providers = await loadProviders(deps);
9290
- renderTopMenu(deps.renderer, providers);
9291
- const ids = Object.keys(providers).sort();
9292
- const choice = (await deps.reader.readLine(`
9293
- ${color.amber("?")} Pick: `)).trim().toLowerCase();
9294
- if (!choice || choice === "q" || choice === "quit" || choice === "exit") {
9295
- deps.renderer.write(color.dim("Done.\n"));
9296
- return 0;
9297
- }
9298
- if (choice === "a" || choice === "add") {
9299
- await addForNewProvider(deps);
9300
- continue;
9301
- }
9302
- if (choice === "c" || choice === "custom") {
9303
- await addCustomProvider(deps);
9304
- continue;
9305
- }
9306
- const idx = Number.parseInt(choice, 10);
9307
- if (!Number.isNaN(idx) && idx >= 1 && idx <= ids.length) {
9308
- const pid = expectDefined(ids[idx - 1]);
9309
- await manageProvider(pid, deps);
9310
- continue;
9311
- }
9312
- const byId = ids.find((id) => id.toLowerCase() === choice);
9313
- if (byId) {
9314
- await manageProvider(byId, deps);
9315
- continue;
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
+ });
9962
+ }
9963
+
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 ?? "");
9975
+ } else {
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 ?? "");
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
+ );
9985
+ }
9986
+ function renderProviderHeader(renderer, providerId, cfg) {
9987
+ const keys = normalizeKeys(cfg);
9988
+ const active = activeLabel(cfg, keys);
9989
+ renderer.write(
9990
+ `
9991
+ ${color.bold(providerId)} ${cfg.family ? color.dim(`[${cfg.family}]`) : color.amber("[no family]")}
9992
+ `
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);
9316
10013
  }
9317
- deps.renderer.writeError(`Unknown selection: "${choice}"`);
9318
10014
  }
9319
10015
  }
9320
- function renderTopMenu(renderer, providers) {
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)}
10020
+ `
10021
+ );
10022
+ }
10023
+ function renderActions(renderer, keysLength) {
9321
10024
  renderer.write(`
9322
- ${color.bold("WrongStack")} ${color.dim("\u2014 API keys")}
9323
-
10025
+ ${color.dim("Actions:")}
10026
+ `);
10027
+ renderer.write(` ${color.bold("a")} Add another key
10028
+ `);
10029
+ if (keysLength > 0) {
10030
+ renderer.write(` ${color.bold("u")} <n> Update key <n>
10031
+ `);
10032
+ renderer.write(` ${color.bold("d")} <n> Delete key <n>
10033
+ `);
10034
+ renderer.write(` ${color.bold("s")} <n> Set key <n> as active
10035
+ `);
10036
+ }
10037
+ renderer.write(` ${color.bold("f")} Edit family
10038
+ `);
10039
+ renderer.write(` ${color.bold("B")} Edit baseUrl
9324
10040
  `);
10041
+ renderer.write(` ${color.bold("m")} Edit visible model list
10042
+ `);
10043
+ renderer.write(` ${color.bold("x")} Remove this provider entirely
10044
+ `);
10045
+ renderer.write(` ${color.bold("b")} Back
10046
+ `);
10047
+ renderer.write(` ${color.bold("q")} Quit
10048
+ `);
10049
+ }
10050
+ function renderTopMenu(renderer, providers) {
10051
+ renderer.write(
10052
+ `
10053
+ ${color.bold("WrongStack")} ${color.dim("\u2014 API key manager")}
10054
+
10055
+ `
10056
+ );
9325
10057
  const ids = Object.keys(providers).sort();
9326
10058
  if (ids.length === 0) {
9327
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"));
9328
10061
  } else {
9329
10062
  renderer.write(` ${color.dim("Saved providers:")}
9330
10063
  `);
@@ -9332,306 +10065,116 @@ ${color.bold("WrongStack")} ${color.dim("\u2014 API keys")}
9332
10065
  for (const id of ids) {
9333
10066
  const cfg = providers[id];
9334
10067
  if (!cfg) continue;
9335
- const keys = normalizeKeys(cfg);
9336
- const active = activeLabel(cfg, keys);
9337
- const firstKey = keys[0];
9338
- 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 ?? "")}`;
9339
- const fam = cfg.family ? color.dim(`[${cfg.family}]`) : "";
9340
- const aliasHint = cfg.type && cfg.type !== id ? color.dim(`\u2192 ${cfg.type}`) : "";
9341
- renderer.write(
9342
- ` ${color.dim(`${idx}.`.padStart(4))} ${id.padEnd(22)} ${fam} ${aliasHint} ${summary}
9343
- `
9344
- );
10068
+ renderProviderLine(renderer, id, cfg, idx);
9345
10069
  idx++;
9346
10070
  }
9347
10071
  }
9348
10072
  renderer.write(`
9349
10073
  ${color.dim("Actions:")}
9350
10074
  `);
9351
- renderer.write(` ${color.bold("a")} Add key for a new provider (from catalog)
10075
+ renderer.write(` ${color.bold("a")} Add a provider (from catalog)
9352
10076
  `);
9353
- renderer.write(` ${color.bold("c")} Add custom provider (type + family + baseUrl)
10077
+ renderer.write(` ${color.bold("c")} Add a custom provider
10078
+ `);
10079
+ if (ids.length > 0) {
10080
+ renderer.write(` ${color.dim("1-")}${color.dim(String(ids.length))} ${color.bold("Manage a provider")}
9354
10081
  `);
10082
+ }
9355
10083
  renderer.write(` ${color.bold("q")} Quit
9356
10084
  `);
9357
- if (ids.length > 0) {
9358
- renderer.write(color.dim(`
9359
- Pick a number to manage that provider's keys.
9360
- `));
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;
9361
10093
  }
10094
+ return key;
9362
10095
  }
9363
- async function manageProvider(providerId, deps) {
9364
- for (; ; ) {
9365
- const providers = await loadProviders(deps);
9366
- const cfg = providers[providerId];
9367
- if (!cfg) {
9368
- deps.renderer.writeError(`Provider "${providerId}" no longer in config.`);
9369
- return;
9370
- }
9371
- const keys = normalizeKeys(cfg);
9372
- const active = activeLabel(cfg, keys);
9373
- deps.renderer.write(
9374
- `
9375
- ${color.bold(providerId)} ${cfg.family ? color.dim(`[${cfg.family}]`) : color.amber("[no family]")}
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;
10113
+ }
10114
+
10115
+ // src/auth-menu/add-provider.ts
10116
+ async function addFromCatalog(deps) {
10117
+ let catalog = [];
10118
+ try {
10119
+ catalog = (await deps.modelsRegistry.listProviders()).filter(
10120
+ (p) => p.family !== "unsupported"
10121
+ );
10122
+ } catch {
10123
+ deps.renderer.writeWarning(
10124
+ "Catalog unavailable \u2014 falling back to manual entry.\n"
10125
+ );
10126
+ }
10127
+ if (catalog.length === 0) {
10128
+ return addManualEntry(deps);
10129
+ }
10130
+ const saved = new Set(Object.keys(await loadProviders(deps)));
10131
+ deps.renderer.write(
10132
+ color.dim(
10133
+ ` Catalog: ${catalog.length} providers. Filter to narrow, "s" for unsaved-only, or enter to show all.
9376
10134
  `
10135
+ )
10136
+ );
10137
+ const filterRaw = (await deps.reader.readLine(
10138
+ ` ${color.amber("?")} Filter ${color.dim('(substring / "s" / q to quit)')}: `
10139
+ )).trim();
10140
+ if (filterRaw === "q") return false;
10141
+ const filterLc = filterRaw.toLowerCase();
10142
+ const showUnsavedOnly = filterLc === "s" || filterLc === "unsaved";
10143
+ function matches(p) {
10144
+ if (showUnsavedOnly) return !saved.has(p.id);
10145
+ if (!filterLc) return true;
10146
+ return p.id.toLowerCase().includes(filterLc) || p.name.toLowerCase().includes(filterLc);
10147
+ }
10148
+ const byFamily = /* @__PURE__ */ new Map();
10149
+ let filteredCount = 0;
10150
+ for (const p of catalog) {
10151
+ if (!matches(p)) continue;
10152
+ filteredCount++;
10153
+ const list = byFamily.get(p.family) ?? [];
10154
+ list.push(p);
10155
+ byFamily.set(p.family, list);
10156
+ }
10157
+ if (filteredCount === 0) {
10158
+ deps.renderer.writeError(
10159
+ `No providers match "${filterRaw}". Try a shorter substring or check \`wstack providers\`.`
9377
10160
  );
10161
+ return false;
10162
+ }
10163
+ if (filterRaw && !showUnsavedOnly) {
9378
10164
  deps.renderer.write(
9379
- color.dim(` type: ${cfg.type ?? providerId}
9380
- `) + color.dim(
9381
- ` family: ${cfg.family ?? "(unset \u2192 resolved from models.dev when type matches)"}
10165
+ color.dim(
10166
+ ` ${filteredCount} match${filteredCount === 1 ? "" : "es"} for "${filterRaw}".
9382
10167
  `
9383
- ) + color.dim(` baseUrl: ${cfg.baseUrl ?? "(unset \u2192 catalog default)"}
9384
- `)
9385
- );
9386
- if (cfg.envVars && cfg.envVars.length > 0) {
9387
- deps.renderer.write(color.dim(` envVars: ${cfg.envVars.join(", ")}
9388
- `));
9389
- }
9390
- if (cfg.models && cfg.models.length > 0) {
9391
- deps.renderer.write(color.dim(` models: ${cfg.models.join(", ")}
9392
- `));
9393
- }
9394
- if (keys.length === 0) {
9395
- deps.renderer.write(color.dim(" (no keys saved)\n"));
9396
- } else {
9397
- for (let i = 0; i < keys.length; i++) {
9398
- const k = expectDefined(keys[i]);
9399
- const marker = k.label === active ? color.green("\u25CF") : color.dim("\u25CB");
9400
- deps.renderer.write(
9401
- ` ${color.dim(`${i + 1}.`.padStart(4))} ${marker} ${k.label.padEnd(20)} ${maskedKey(k.apiKey)} ${color.dim(k.createdAt)}
9402
- `
9403
- );
9404
- }
9405
- }
9406
- deps.renderer.write(`
9407
- ${color.dim("Actions:")}
9408
- `);
9409
- deps.renderer.write(` ${color.bold("a")} Add another key
9410
- `);
9411
- if (keys.length > 0) {
9412
- deps.renderer.write(` ${color.bold("u")} <n> Update key <n>
9413
- `);
9414
- deps.renderer.write(` ${color.bold("d")} <n> Delete key <n>
9415
- `);
9416
- deps.renderer.write(` ${color.bold("s")} <n> Set key <n> as active
9417
- `);
9418
- }
9419
- deps.renderer.write(` ${color.bold("f")} Edit family
9420
- `);
9421
- deps.renderer.write(` ${color.bold("B")} Edit baseUrl
9422
- `);
9423
- deps.renderer.write(` ${color.bold("m")} Edit visible model list
9424
- `);
9425
- deps.renderer.write(` ${color.bold("x")} Remove this provider entirely
9426
- `);
9427
- deps.renderer.write(` ${color.bold("b")} Back
9428
- `);
9429
- deps.renderer.write(` ${color.bold("q")} Quit
9430
- `);
9431
- const raw = (await deps.reader.readLine(`
9432
- ${color.amber("?")} ${providerId} > `)).trim();
9433
- if (!raw || raw === "b" || raw === "back" || raw === "q" || raw === "quit") return;
9434
- const [verb, argRaw] = raw.split(/\s+/, 2);
9435
- const arg = argRaw ? Number.parseInt(argRaw, 10) : Number.NaN;
9436
- if (verb === "a" || verb === "add") {
9437
- await addKeyForProvider(providerId, deps, cfg);
9438
- continue;
9439
- }
9440
- if (verb === "x" || verb === "remove") {
9441
- const confirm = (await deps.reader.readLine(
9442
- ` ${color.amber("?")} Remove provider "${providerId}" and ${keys.length} key(s)? ${color.dim("[y/N/q]")} `
9443
- )).trim().toLowerCase();
9444
- if (confirm === "q") continue;
9445
- if (confirm === "y" || confirm === "yes") {
9446
- await mutateProviders(deps, (all) => {
9447
- delete all[providerId];
9448
- });
9449
- deps.renderer.write(` ${color.green("\u2713")} Removed ${providerId}.
9450
- `);
9451
- return;
9452
- }
9453
- continue;
9454
- }
9455
- if (verb === "u" || verb === "update") {
9456
- if (!Number.isFinite(arg) || arg < 1 || arg > keys.length) {
9457
- deps.renderer.writeError(`Usage: u <1-${keys.length}>`);
9458
- continue;
9459
- }
9460
- const target = expectDefined(keys[arg - 1]);
9461
- const newKey = await readKeyInput(deps, `Updated key for ${target.label}`);
9462
- if (!newKey) continue;
9463
- await mutateProviders(deps, (all) => {
9464
- const p = all[providerId];
9465
- if (!p) return;
9466
- const list = normalizeKeys(p).map(
9467
- (k) => k.label === target.label ? { ...k, apiKey: newKey, createdAt: nowIso() } : k
9468
- );
9469
- writeKeysBack(p, list);
9470
- });
9471
- deps.renderer.write(` ${color.green("\u2713")} Updated ${providerId}/${target.label}.
9472
- `);
9473
- continue;
9474
- }
9475
- if (verb === "d" || verb === "delete" || verb === "rm") {
9476
- if (!Number.isFinite(arg) || arg < 1 || arg > keys.length) {
9477
- deps.renderer.writeError(`Usage: d <1-${keys.length}>`);
9478
- continue;
9479
- }
9480
- const target = expectDefined(keys[arg - 1]);
9481
- const confirm = (await deps.reader.readLine(
9482
- ` ${color.amber("?")} Delete key "${target.label}" (${maskedKey(target.apiKey)})? ${color.dim("[y/N/q]")} `
9483
- )).trim().toLowerCase();
9484
- if (confirm === "q") continue;
9485
- if (confirm !== "y" && confirm !== "yes") continue;
9486
- await mutateProviders(deps, (all) => {
9487
- const p = all[providerId];
9488
- if (!p) return;
9489
- const list = normalizeKeys(p).filter((k) => k.label !== target.label);
9490
- writeKeysBack(p, list);
9491
- if (p.activeKey === target.label) {
9492
- p.activeKey = list[0]?.label;
9493
- }
9494
- });
9495
- deps.renderer.write(` ${color.green("\u2713")} Deleted ${providerId}/${target.label}.
9496
- `);
9497
- continue;
9498
- }
9499
- if (verb === "f" || verb === "family") {
9500
- const current = cfg.family ?? "";
9501
- const ans = (await deps.reader.readLine(
9502
- ` ${color.amber("?")} Family ${color.dim(`(anthropic | openai | openai-compatible | google, empty = unset, current: ${current || "unset"})`)}: `
9503
- )).trim();
9504
- if (ans !== "" && !["anthropic", "openai", "openai-compatible", "google"].includes(ans)) {
9505
- deps.renderer.writeError(`Invalid family: "${ans}"`);
9506
- continue;
9507
- }
9508
- await mutateProviders(deps, (all) => {
9509
- const p = all[providerId];
9510
- if (!p) return;
9511
- if (ans === "") delete p.family;
9512
- else p.family = ans;
9513
- });
9514
- deps.renderer.write(` ${color.green("\u2713")} family \u2192 ${ans || "(unset)"}
9515
- `);
9516
- continue;
9517
- }
9518
- if (verb === "B" || verb === "baseurl" || verb === "base-url") {
9519
- const current = cfg.baseUrl ?? "";
9520
- const ans = (await deps.reader.readLine(
9521
- ` ${color.amber("?")} Base URL ${color.dim(`(empty = unset, current: ${current || "unset"})`)}: `
9522
- )).trim();
9523
- await mutateProviders(deps, (all) => {
9524
- const p = all[providerId];
9525
- if (!p) return;
9526
- if (ans === "") delete p.baseUrl;
9527
- else p.baseUrl = ans;
9528
- });
9529
- deps.renderer.write(` ${color.green("\u2713")} baseUrl \u2192 ${ans || "(unset)"}
9530
- `);
9531
- continue;
9532
- }
9533
- if (verb === "m" || verb === "models") {
9534
- const current = (cfg.models ?? []).join(", ");
9535
- const ans = (await deps.reader.readLine(
9536
- ` ${color.amber("?")} Model ids ${color.dim(`(comma-separated, empty = catalog default, current: ${current || "none"})`)}: `
9537
- )).trim();
9538
- const list = ans ? ans.split(",").map((s) => s.trim()).filter(Boolean) : [];
9539
- await mutateProviders(deps, (all) => {
9540
- const p = all[providerId];
9541
- if (!p) return;
9542
- if (list.length === 0) delete p.models;
9543
- else p.models = list;
9544
- });
9545
- deps.renderer.write(
9546
- ` ${color.green("\u2713")} models \u2192 ${list.length === 0 ? "(catalog default)" : list.join(", ")}
9547
- `
9548
- );
9549
- continue;
9550
- }
9551
- if (verb === "s" || verb === "set" || verb === "active") {
9552
- if (!Number.isFinite(arg) || arg < 1 || arg > keys.length) {
9553
- deps.renderer.writeError(`Usage: s <1-${keys.length}>`);
9554
- continue;
9555
- }
9556
- const target = expectDefined(keys[arg - 1]);
9557
- await mutateProviders(deps, (all) => {
9558
- const p = all[providerId];
9559
- if (!p) return;
9560
- const list = normalizeKeys(p);
9561
- writeKeysBack(p, list);
9562
- p.activeKey = target.label;
9563
- });
9564
- deps.renderer.write(
9565
- ` ${color.green("\u2713")} Active key for ${providerId} \u2192 ${color.bold(target.label)}.
9566
- `
9567
- );
9568
- continue;
9569
- }
9570
- deps.renderer.writeError(`Unknown action: "${raw}"`);
9571
- }
9572
- }
9573
- async function addForNewProvider(deps) {
9574
- let catalog = [];
9575
- try {
9576
- catalog = (await deps.modelsRegistry.listProviders()).filter((p) => p.family !== "unsupported");
9577
- } catch {
9578
- deps.renderer.writeWarning("Catalog unavailable \u2014 falling back to manual entry.");
9579
- }
9580
- if (catalog.length === 0) {
9581
- const pid = (await deps.reader.readLine(` ${color.amber("?")} Provider id ${color.dim("[q to quit]")}: `)).trim();
9582
- if (!pid || pid === "q") return;
9583
- const fam = (await deps.reader.readLine(
9584
- ` ${color.amber("?")} Family (anthropic/openai/openai-compatible/google): `
9585
- )).trim();
9586
- const baseUrl2 = (await deps.reader.readLine(` ${color.amber("?")} Base URL ${color.dim("(optional)")}: `)).trim();
9587
- await addKeyForProvider(pid, deps, {
9588
- type: pid,
9589
- family: fam || void 0,
9590
- ...baseUrl2 ? { baseUrl: baseUrl2 } : {}
9591
- });
9592
- return;
9593
- }
9594
- const saved = new Set(Object.keys(await loadProviders(deps)));
9595
- deps.renderer.write(
9596
- color.dim(
9597
- ` Catalog has ${catalog.length} providers. Filter by name to narrow, or "s" for unsaved-only.
9598
- `
9599
- )
9600
- );
9601
- const filterRaw = (await deps.reader.readLine(
9602
- ` ${color.amber("?")} Filter ${color.dim('(substring, "s" for unsaved-only, q to quit)')}: `
9603
- )).trim();
9604
- if (filterRaw === "q") return;
9605
- const filterLc = filterRaw.toLowerCase();
9606
- const showUnsavedOnly = filterLc === "s" || filterLc === "unsaved";
9607
- const matches = (p) => {
9608
- if (showUnsavedOnly) return !saved.has(p.id);
9609
- if (!filterLc) return true;
9610
- return p.id.toLowerCase().includes(filterLc) || p.name.toLowerCase().includes(filterLc);
9611
- };
9612
- const byFamily = /* @__PURE__ */ new Map();
9613
- let filteredCount = 0;
9614
- for (const p of catalog) {
9615
- if (!matches(p)) continue;
9616
- filteredCount++;
9617
- const list = byFamily.get(p.family) ?? [];
9618
- list.push(p);
9619
- byFamily.set(p.family, list);
9620
- }
9621
- if (filteredCount === 0) {
9622
- deps.renderer.writeError(
9623
- `No providers match "${filterRaw}". Try a shorter substring or check \`wstack providers\` for valid ids.`
9624
- );
9625
- return;
9626
- }
9627
- if (filterRaw && !showUnsavedOnly) {
9628
- deps.renderer.write(
9629
- color.dim(` ${filteredCount} match${filteredCount === 1 ? "" : "es"} for "${filterRaw}".
9630
- `)
10168
+ )
9631
10169
  );
9632
10170
  }
9633
10171
  const ordered = [];
9634
- const familyOrder = ["anthropic", "openai", "google", "openai-compatible"];
10172
+ const familyOrder = [
10173
+ "anthropic",
10174
+ "openai",
10175
+ "google",
10176
+ "openai-compatible"
10177
+ ];
9635
10178
  let idx = 1;
9636
10179
  deps.renderer.write("\n");
9637
10180
  for (const fam of familyOrder) {
@@ -9657,7 +10200,7 @@ async function addForNewProvider(deps) {
9657
10200
  `
9658
10201
  ${color.amber("?")} Pick (1-${ordered.length}) or type provider id ${color.dim("[q to quit]")}: `
9659
10202
  )).trim();
9660
- if (!answer || answer === "q") return;
10203
+ if (!answer || answer === "q") return false;
9661
10204
  let chosen;
9662
10205
  const num = Number.parseInt(answer, 10);
9663
10206
  if (!Number.isNaN(num) && num >= 1 && num <= ordered.length) {
@@ -9667,29 +10210,35 @@ ${color.amber("?")} Pick (1-${ordered.length}) or type provider id ${color.dim("
9667
10210
  }
9668
10211
  if (!chosen) {
9669
10212
  deps.renderer.writeError(`No such provider: "${answer}"`);
9670
- return;
10213
+ return false;
9671
10214
  }
10215
+ return addKeyForCatalogProvider(deps, chosen);
10216
+ }
10217
+ async function addKeyForCatalogProvider(deps, chosen) {
9672
10218
  deps.renderer.write(
9673
10219
  color.dim(`
9674
- 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.
9675
10221
  `)
9676
10222
  );
9677
- const famRaw = (await deps.reader.readLine(` ${color.amber("?")} Family ${color.dim(`[${chosen.family}]`)} ${color.dim("(q to quit)")}: `)).trim();
9678
- 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;
9679
10227
  let family = chosen.family;
9680
10228
  if (famRaw) {
9681
- if (!["anthropic", "openai", "openai-compatible", "google"].includes(famRaw)) {
10229
+ const validated = validateFamily(famRaw);
10230
+ if (!validated) {
9682
10231
  deps.renderer.writeError(
9683
- `Invalid family: "${famRaw}" (must be anthropic | openai | openai-compatible | google).`
10232
+ `Invalid family: "${famRaw}" (must be: anthropic, openai, openai-compatible, google).`
9684
10233
  );
9685
- return;
10234
+ return false;
9686
10235
  }
9687
- family = famRaw;
10236
+ family = validated;
9688
10237
  }
9689
10238
  const baseRaw = (await deps.reader.readLine(
9690
10239
  ` ${color.amber("?")} Base URL ${color.dim(`[${chosen.apiBase ?? "unset"}]`)} ${color.dim("(q to quit)")}: `
9691
10240
  )).trim();
9692
- if (baseRaw === "q") return;
10241
+ if (baseRaw === "q") return false;
9693
10242
  const baseUrl = baseRaw || chosen.apiBase;
9694
10243
  const providersNow = await loadProviders(deps);
9695
10244
  let suggestedAlias = chosen.id;
@@ -9703,7 +10252,7 @@ ${color.amber("?")} Pick (1-${ordered.length}) or type provider id ${color.dim("
9703
10252
  suggestedAlias = candidate;
9704
10253
  }
9705
10254
  const aliasRaw = (await deps.reader.readLine(
9706
- ` ${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>)")}: `
9707
10256
  )).trim();
9708
10257
  const alias = aliasRaw || suggestedAlias;
9709
10258
  const existing = providersNow[alias];
@@ -9717,10 +10266,10 @@ ${color.amber("?")} Pick (1-${ordered.length}) or type provider id ${color.dim("
9717
10266
  New: family=${family}, baseUrl=${baseUrl ?? "(unset)"}
9718
10267
  Pick a different alias to keep them separate.`
9719
10268
  );
9720
- return;
10269
+ return false;
9721
10270
  }
9722
10271
  }
9723
- await addKeyForProvider(alias, deps, {
10272
+ return addKeyForProvider(alias, deps, {
9724
10273
  type: chosen.id,
9725
10274
  family,
9726
10275
  baseUrl,
@@ -9730,39 +10279,41 @@ ${color.amber("?")} Pick (1-${ordered.length}) or type provider id ${color.dim("
9730
10279
  async function addCustomProvider(deps) {
9731
10280
  deps.renderer.write(
9732
10281
  `
9733
- ${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.")}
9734
10283
  `
9735
10284
  );
9736
10285
  const type = (await deps.reader.readLine(
9737
10286
  ` ${color.amber("?")} Provider id ${color.dim('(e.g. "local-llama", "my-proxy", q to quit)')}: `
9738
10287
  )).trim();
9739
- if (!type || type === "q") return;
10288
+ if (!type || type === "q") return false;
9740
10289
  const existing = (await loadProviders(deps))[type];
9741
10290
  if (existing) {
9742
- deps.renderer.writeWarning(`"${type}" already exists. Pick it from the main menu to edit.`);
9743
- return;
10291
+ deps.renderer.writeWarning(
10292
+ `"${type}" already exists. Pick it from the main menu to edit.`
10293
+ );
10294
+ return false;
9744
10295
  }
9745
10296
  const familyRaw = (await deps.reader.readLine(
9746
10297
  ` ${color.amber("?")} Wire family ${color.dim("(anthropic | openai | openai-compatible | google)")} ${color.dim("(q to quit)")}: `
9747
10298
  )).trim();
9748
- if (familyRaw === "q") return;
9749
- if (!["anthropic", "openai", "openai-compatible", "google"].includes(familyRaw)) {
10299
+ if (familyRaw === "q") return false;
10300
+ const family = validateFamily(familyRaw);
10301
+ if (!family) {
9750
10302
  deps.renderer.writeError(`Invalid family: "${familyRaw}"`);
9751
- return;
10303
+ return false;
9752
10304
  }
9753
- const family = familyRaw;
9754
10305
  const baseUrl = (await deps.reader.readLine(
9755
- ` ${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)")}: `
9756
10307
  )).trim();
9757
10308
  const modelsRaw = (await deps.reader.readLine(
9758
10309
  ` ${color.amber("?")} Model ids ${color.dim("(comma-separated, optional)")}: `
9759
10310
  )).trim();
9760
10311
  const models = modelsRaw ? modelsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
9761
10312
  const envVarsRaw = (await deps.reader.readLine(
9762
- ` ${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)")}: `
9763
10314
  )).trim();
9764
10315
  const envVars = envVarsRaw ? envVarsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
9765
- await addKeyForProvider(type, deps, {
10316
+ return addKeyForProvider(type, deps, {
9766
10317
  type,
9767
10318
  family,
9768
10319
  ...baseUrl ? { baseUrl } : {},
@@ -9770,38 +10321,52 @@ ${color.bold("Custom provider")} ${color.dim("\u2014 for local models or proxies
9770
10321
  ...envVars ? { envVars } : {}
9771
10322
  });
9772
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
+ }
9773
10346
  async function addKeyForProvider(providerId, deps, template) {
9774
10347
  const providers = await loadProviders(deps);
9775
10348
  const existing = providers[providerId];
9776
10349
  const existingKeys = existing ? normalizeKeys(existing) : [];
9777
10350
  const usedLabels = new Set(existingKeys.map((k) => k.label));
9778
- let defaultLabel = "default";
9779
- if (usedLabels.has(defaultLabel)) {
9780
- let n = 2;
9781
- while (usedLabels.has(`key${n}`)) n++;
9782
- defaultLabel = `key${n}`;
9783
- }
9784
- const labelRaw = (await deps.reader.readLine(
9785
- ` ${color.amber("?")} Label for this key ${color.dim(`[${defaultLabel}]`)}: `
9786
- )).trim();
9787
- const label = labelRaw || defaultLabel;
9788
- if (usedLabels.has(label)) {
9789
- deps.renderer.writeError(
9790
- `Label "${label}" already used for ${providerId}. Use update (u) instead.`
9791
- );
9792
- return;
9793
- }
10351
+ const label = await promptForLabel(deps, usedLabels);
10352
+ if (!label) return false;
9794
10353
  const apiKey = await readKeyInput(deps, `API key for ${providerId}/${label}`);
9795
- if (!apiKey) {
9796
- deps.renderer.writeError("No key entered. Nothing saved.");
9797
- return;
9798
- }
9799
- await mutateProviders(deps, (all) => {
9800
- const existingProv = all[providerId] ?? { type: providerId, ...template };
10354
+ if (!apiKey) return false;
10355
+ await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
10356
+ const existingProv = all[providerId] ?? {
10357
+ type: providerId,
10358
+ ...template
10359
+ };
9801
10360
  if (!existingProv.type) existingProv.type = providerId;
9802
- if (!existingProv.family && template.family) existingProv.family = template.family;
9803
- if (!existingProv.baseUrl && template.baseUrl) existingProv.baseUrl = template.baseUrl;
9804
- if (!existingProv.envVars && template.envVars) existingProv.envVars = template.envVars;
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
+ }
9805
10370
  const list = normalizeKeys(existingProv);
9806
10371
  list.push({ label, apiKey, createdAt: nowIso() });
9807
10372
  writeKeysBack(existingProv, list);
@@ -9809,10 +10374,242 @@ async function addKeyForProvider(providerId, deps, template) {
9809
10374
  all[providerId] = existingProv;
9810
10375
  });
9811
10376
  deps.renderer.write(
9812
- ` ${color.green("\u2713")} Saved ${color.bold(providerId)}/${color.bold(label)}. ${color.dim("Use `wstack --provider " + providerId + ' "<task>"` to launch.')}
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>"
9813
10383
  `
10384
+ )
9814
10385
  );
10386
+ return true;
10387
+ }
10388
+ async function promptForLabel(deps, usedLabels) {
10389
+ const defaultLabel = suggestLabel(usedLabels);
10390
+ const labelRaw = (await deps.reader.readLine(
10391
+ ` ${color.amber("?")} Label for this key ${color.dim(`[${defaultLabel}]`)}: `
10392
+ )).trim();
10393
+ const label = labelRaw || defaultLabel;
10394
+ if (usedLabels.has(label)) {
10395
+ deps.renderer.writeError(
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)})?`
10471
+ );
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";
10502
+ }
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";
10531
+ }
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(", ")}
10561
+ `
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;
9815
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
+ }
10609
+ }
10610
+
10611
+ // src/auth-menu/direct.ts
10612
+ init_provider_config_utils();
9816
10613
  async function runAuthDirect(deps, opts) {
9817
10614
  const { providerId } = opts;
9818
10615
  const providers = await loadProviders(deps);
@@ -9840,7 +10637,9 @@ async function runAuthDirect(deps, opts) {
9840
10637
  opts.baseUrl ??= knownBase;
9841
10638
  opts.envVars ??= knownEnv;
9842
10639
  }
9843
- 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
+ );
9844
10643
  let label = opts.label ?? "default";
9845
10644
  if (usedLabels.has(label)) {
9846
10645
  let n = 2;
@@ -9850,7 +10649,7 @@ async function runAuthDirect(deps, opts) {
9850
10649
  }
9851
10650
  const apiKey = await readKeyInput(deps, `API key for ${providerId}/${label}`);
9852
10651
  if (!apiKey) return 1;
9853
- await mutateProviders(deps, (all) => {
10652
+ await mutateConfigProviders(deps.globalConfigPath, deps.vault, (all) => {
9854
10653
  const p = all[providerId] ?? { type: providerId };
9855
10654
  if (!p.type) p.type = providerId;
9856
10655
  if (!p.family && opts.family) p.family = opts.family;
@@ -9866,26 +10665,9 @@ async function runAuthDirect(deps, opts) {
9866
10665
  deps.renderer.writeInfo(`Use: wstack --provider ${providerId} "<task>"`);
9867
10666
  return 0;
9868
10667
  }
9869
- async function readKeyInput(deps, intent) {
9870
- const key = (await deps.reader.readSecret(
9871
- ` ${color.amber("?")} ${intent} ${color.dim("(hidden, paste OK)")}: `
9872
- )).trim();
9873
- if (!key) {
9874
- deps.renderer.writeError("No key entered.");
9875
- return void 0;
9876
- }
9877
- return key;
9878
- }
9879
- function loadProviders(deps) {
9880
- return loadConfigProviders(deps.globalConfigPath, deps.vault, {
9881
- warn: (msg) => deps.renderer.writeWarning(msg)
9882
- });
9883
- }
9884
- function mutateProviders(deps, mutator) {
9885
- return mutateConfigProviders(deps.globalConfigPath, deps.vault, mutator);
9886
- }
9887
10668
 
9888
10669
  // src/subcommands/handlers/auth.ts
10670
+ init_provider_config_utils();
9889
10671
  var authCmd = async (args, deps) => {
9890
10672
  const flags = parseAuthFlags(args);
9891
10673
  const menuDeps = {
@@ -9895,15 +10677,175 @@ var authCmd = async (args, deps) => {
9895
10677
  vault: deps.vault,
9896
10678
  globalConfigPath: deps.paths.globalConfig
9897
10679
  };
9898
- 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
+ }
9899
10703
  return runAuthDirect(menuDeps, {
9900
- providerId: expectDefined(flags.positional[0]),
10704
+ providerId: first,
9901
10705
  label: flags.label,
9902
10706
  family: flags.family,
9903
10707
  baseUrl: flags.baseUrl,
9904
10708
  envVars: flags.envVars
9905
10709
  });
9906
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
+ }
9907
10849
 
9908
10850
  // src/subcommands/handlers/update.ts
9909
10851
  init_update_check();
@@ -11726,6 +12668,10 @@ var helpCmd = async (_args, deps) => {
11726
12668
  " wstack sessions List recent sessions",
11727
12669
  " wstack init Pick provider + model from models.dev",
11728
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)",
11729
12675
  " wstack config [show|edit] Show or edit effective config",
11730
12676
  " wstack tools List registered tools",
11731
12677
  " wstack skills List discovered skills",
@@ -11806,22 +12752,22 @@ function fmtDuration(ms) {
11806
12752
  const remMin = m - h * 60;
11807
12753
  return `${h}h${remMin}m`;
11808
12754
  }
11809
- function fmtTaskResultLine(r, color51) {
12755
+ function fmtTaskResultLine(r, color56) {
11810
12756
  const stats = `${r.iterations}it ${r.toolCalls}tc ${fmtDuration(r.durationMs)}`;
11811
12757
  const errMsg = typeof r.error === "string" ? r.error : r.error?.message;
11812
12758
  const errKind = typeof r.error === "object" ? r.error?.kind : void 0;
11813
12759
  const errTail = errMsg ? ` \u2014 ${errMsg.replace(/\s+/g, " ").slice(0, 80)}${errMsg.length > 80 ? "\u2026" : ""}` : "";
11814
- const errKindChip = errKind ? color51.dim(` [${errKind}]`) : "";
11815
- const errSnip = errMsg || errKind ? `${errKindChip}${color51.dim(errTail)}` : "";
12760
+ const errKindChip = errKind ? color56.dim(` [${errKind}]`) : "";
12761
+ const errSnip = errMsg || errKind ? `${errKindChip}${color56.dim(errTail)}` : "";
11816
12762
  switch (r.status) {
11817
12763
  case "success":
11818
- return { mark: color51.green("\u2713"), stats, tail: "" };
12764
+ return { mark: color56.green("\u2713"), stats, tail: "" };
11819
12765
  case "timeout":
11820
- return { mark: color51.yellow("\u23F1"), stats: `${color51.yellow("timeout")} ${stats}`, tail: errSnip };
12766
+ return { mark: color56.yellow("\u23F1"), stats: `${color56.yellow("timeout")} ${stats}`, tail: errSnip };
11821
12767
  case "stopped":
11822
- return { mark: color51.dim("\u2298"), stats: `${color51.dim("stopped")} ${stats}`, tail: errSnip };
12768
+ return { mark: color56.dim("\u2298"), stats: `${color56.dim("stopped")} ${stats}`, tail: errSnip };
11823
12769
  case "failed":
11824
- return { mark: color51.red("\u2717"), stats: `${color51.red("failed")} ${stats}`, tail: errSnip };
12770
+ return { mark: color56.red("\u2717"), stats: `${color56.red("failed")} ${stats}`, tail: errSnip };
11825
12771
  }
11826
12772
  }
11827
12773
 
@@ -12084,7 +13030,8 @@ async function boot(argv) {
12084
13030
  if (choices.director) flags["director"] = true;
12085
13031
  flags["autonomy"] = choices.autonomy;
12086
13032
  try {
12087
- await persistLaunchChoices(wpaths.globalConfig, choices);
13033
+ const toPersist = flags["webui"] ? { ...choices, mode: lastChoices?.mode ?? config.launch?.mode ?? "tui" } : choices;
13034
+ await persistLaunchChoices(wpaths.globalConfig, toPersist);
12088
13035
  } catch {
12089
13036
  }
12090
13037
  printLaunchHints(renderer, flags, {
@@ -12430,7 +13377,7 @@ function parsePredictions(raw, max = 3) {
12430
13377
  }
12431
13378
  return out;
12432
13379
  }
12433
- function extractText(content) {
13380
+ function extractText2(content) {
12434
13381
  if (Array.isArray(content)) {
12435
13382
  return content[0]?.text ?? "";
12436
13383
  }
@@ -12461,7 +13408,7 @@ async function predictNextTasks(input, opts) {
12461
13408
  },
12462
13409
  { signal: internal.signal }
12463
13410
  );
12464
- return parsePredictions(extractText(resp.content), max);
13411
+ return parsePredictions(extractText2(resp.content), max);
12465
13412
  } catch {
12466
13413
  return [];
12467
13414
  } finally {
@@ -15250,7 +16197,7 @@ async function setupCodebaseIndexing(deps) {
15250
16197
  let watcher;
15251
16198
  if (idx.watchExternal) {
15252
16199
  try {
15253
- watcher = fs12.watch(projectRoot, { recursive: true }, (_event, filename) => {
16200
+ watcher = fs13.watch(projectRoot, { recursive: true }, (_event, filename) => {
15254
16201
  if (!filename) return;
15255
16202
  const rel = filename.toString();
15256
16203
  if (isIgnored(rel)) return;
@@ -15578,6 +16525,8 @@ async function setupSession(params) {
15578
16525
  );
15579
16526
  const planPath = path8.join(wpaths.projectSessions, `${session?.id}.plan.json`);
15580
16527
  context.state.setMeta("plan.path", planPath);
16528
+ const taskPath = path8.join(wpaths.projectSessions, `${session?.id}.tasks.json`);
16529
+ context.state.setMeta("task.path", taskPath);
15581
16530
  let dirState;
15582
16531
  if (resumeId) {
15583
16532
  try {