aiden-runtime 4.0.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +11 -7
  2. package/config/hardware.json +2 -2
  3. package/dist/api/server.js +50 -52
  4. package/dist/cli/v4/aidenCLI.js +513 -14
  5. package/dist/cli/v4/aidenPrompt.js +317 -0
  6. package/dist/cli/v4/box.js +105 -39
  7. package/dist/cli/v4/callbacks.js +39 -6
  8. package/dist/cli/v4/chatSession.js +269 -52
  9. package/dist/cli/v4/citationFooter.js +97 -0
  10. package/dist/cli/v4/commands/channel.js +656 -0
  11. package/dist/cli/v4/commands/clear.js +1 -1
  12. package/dist/cli/v4/commands/compress.js +1 -1
  13. package/dist/cli/v4/commands/cron.js +44 -16
  14. package/dist/cli/v4/commands/fanout.js +236 -0
  15. package/dist/cli/v4/commands/help.js +15 -4
  16. package/dist/cli/v4/commands/history.js +84 -0
  17. package/dist/cli/v4/commands/index.js +19 -1
  18. package/dist/cli/v4/commands/mcp.js +358 -0
  19. package/dist/cli/v4/commands/setup.js +34 -0
  20. package/dist/cli/v4/commands/show.js +43 -0
  21. package/dist/cli/v4/commands/skills.js +169 -4
  22. package/dist/cli/v4/commands/status.js +84 -0
  23. package/dist/cli/v4/commands/subagent.js +78 -0
  24. package/dist/cli/v4/commands/verbose.js +1 -1
  25. package/dist/cli/v4/commands/voice.js +218 -0
  26. package/dist/cli/v4/cronCli.js +103 -0
  27. package/dist/cli/v4/display.js +300 -14
  28. package/dist/cli/v4/doctor.js +41 -0
  29. package/dist/cli/v4/envSources.js +105 -0
  30. package/dist/cli/v4/ghostMatch.js +74 -0
  31. package/dist/cli/v4/historyStore.js +163 -0
  32. package/dist/cli/v4/pasteCompression.js +124 -0
  33. package/dist/cli/v4/pasteIntercept.js +203 -0
  34. package/dist/cli/v4/replyRenderer.js +209 -0
  35. package/dist/cli/v4/resizeGuard.js +92 -0
  36. package/dist/cli/v4/setupWizard.js +466 -232
  37. package/dist/cli/v4/shellInterpolation.js +139 -0
  38. package/dist/cli/v4/skinEngine.js +21 -1
  39. package/dist/cli/v4/streamingPrefix.js +121 -0
  40. package/dist/cli/v4/syntaxHighlight.js +345 -0
  41. package/dist/cli/v4/table.js +216 -0
  42. package/dist/cli/v4/themeDetect.js +81 -0
  43. package/dist/cli/v4/uiBuild.js +74 -0
  44. package/dist/cli/v4/voiceCli.js +113 -0
  45. package/dist/cli/v4/voicePromptApi.js +196 -0
  46. package/dist/core/channels/discord.js +16 -10
  47. package/dist/core/channels/email.js +13 -9
  48. package/dist/core/channels/imessage.js +13 -9
  49. package/dist/core/channels/manager.js +25 -7
  50. package/dist/core/channels/pdf-extract.js +180 -0
  51. package/dist/core/channels/photo-vision.js +157 -0
  52. package/dist/core/channels/signal.js +11 -7
  53. package/dist/core/channels/slack.js +13 -10
  54. package/dist/core/channels/telegram-commands.js +154 -0
  55. package/dist/core/channels/telegram-groups.js +198 -0
  56. package/dist/core/channels/telegram-rate-limit.js +124 -0
  57. package/dist/core/channels/telegram.js +1980 -0
  58. package/dist/core/channels/twilio.js +11 -7
  59. package/dist/core/channels/webhook.js +9 -5
  60. package/dist/core/channels/whatsapp.js +15 -11
  61. package/dist/core/channels/whisper-transcribe.js +163 -0
  62. package/dist/core/cronManager.js +33 -294
  63. package/dist/core/gateway.js +29 -8
  64. package/dist/core/playwrightBridge.js +90 -0
  65. package/dist/core/v4/aidenAgent.js +35 -0
  66. package/dist/core/v4/auxiliaryClient.js +2 -2
  67. package/dist/core/v4/cron/atomicWrite.js +18 -4
  68. package/dist/core/v4/cron/cronExecute.js +300 -0
  69. package/dist/core/v4/cron/cronManager.js +502 -0
  70. package/dist/core/v4/cron/cronState.js +314 -0
  71. package/dist/core/v4/cron/cronTick.js +90 -0
  72. package/dist/core/v4/cron/diagnostics.js +104 -0
  73. package/dist/core/v4/cron/graceWindow.js +79 -0
  74. package/dist/core/v4/firstRun/providerDetection.js +287 -0
  75. package/dist/core/v4/logger/factory.js +110 -0
  76. package/dist/core/v4/logger/index.js +22 -0
  77. package/dist/core/v4/logger/logger.js +101 -0
  78. package/dist/core/v4/logger/sinks/fileSink.js +110 -0
  79. package/dist/core/v4/logger/sinks/multiSink.js +43 -0
  80. package/dist/core/v4/logger/sinks/nullSink.js +53 -0
  81. package/dist/core/v4/logger/sinks/stdSink.js +81 -0
  82. package/dist/core/v4/mcp/server/diagnostics.js +40 -0
  83. package/dist/core/v4/mcp/server/skillBridge.js +94 -0
  84. package/dist/core/v4/mcp/server/stdioServer.js +119 -0
  85. package/dist/core/v4/mcp/server/toolBridge.js +168 -0
  86. package/dist/core/v4/platformPaths.js +105 -0
  87. package/dist/core/v4/providerFallback.js +25 -0
  88. package/dist/core/v4/skillLoader.js +21 -5
  89. package/dist/core/v4/skillMining/candidateStore.js +164 -0
  90. package/dist/core/v4/skillMining/extractorPrompt.js +111 -0
  91. package/dist/core/v4/skillMining/proposalBuilder.js +139 -0
  92. package/dist/core/v4/skillMining/skillMiner.js +191 -0
  93. package/dist/core/v4/skillMining/traceFingerprint.js +51 -0
  94. package/dist/core/v4/subagent/budget.js +76 -0
  95. package/dist/core/v4/subagent/diagnostics.js +22 -0
  96. package/dist/core/v4/subagent/fanout.js +216 -0
  97. package/dist/core/v4/subagent/merger.js +148 -0
  98. package/dist/core/v4/subagent/providerRotation.js +54 -0
  99. package/dist/core/v4/voice/audioStream.js +373 -0
  100. package/dist/core/v4/voice/cliVoice.js +393 -0
  101. package/dist/core/v4/voice/diagnostics.js +66 -0
  102. package/dist/core/v4/voice/ttsStream.js +193 -0
  103. package/dist/core/version.js +1 -1
  104. package/dist/core/visionAnalyze.js +291 -90
  105. package/dist/core/voice/audio.js +61 -5
  106. package/dist/core/voice/audioBackend.js +134 -0
  107. package/dist/core/voice/stt.js +61 -6
  108. package/dist/core/voice/tts.js +19 -3
  109. package/dist/providers/v4/nullAdapter.js +58 -0
  110. package/dist/tools/v4/index.js +32 -1
  111. package/dist/tools/v4/subagent/subagentFanout.js +166 -0
  112. package/package.json +11 -2
@@ -42,41 +42,91 @@ const keyValidator_1 = require("./keyValidator");
42
42
  const providerAuth_1 = require("../../core/v4/auth/providerAuth");
43
43
  const loadProvider_1 = require("./auth/loadProvider");
44
44
  const box_1 = require("./box");
45
+ // Phase 30.2.1 — provider order optimised for new-user time-to-first-chat.
46
+ // Free providers first (Groq → Gemini → OpenRouter → NVIDIA → Ollama),
47
+ // paid providers next (Anthropic, OpenAI, Together), subscription
48
+ // sign-ins last. The legacy entries (deepseek, mistral, zai, kimi,
49
+ // minimax, huggingface, vercel, nous, custom) trail the top 10 — still
50
+ // pickable for power users, never the default.
51
+ //
52
+ // Plain-English descriptions per spec — "TPM cap" replaced with
53
+ // "limited messages per minute", "tier 1 paid" with "best for complex
54
+ // tasks", etc. Subsequent prompts use `shortLabel` (e.g. just "Groq")
55
+ // to avoid restating the description.
45
56
  exports.PROVIDERS = [
46
- { id: 'claude-pro', label: 'Use my Claude Pro/Max subscription', kind: 'pro' },
47
- { id: 'chatgpt-plus', label: 'Use my ChatGPT Plus subscription', kind: 'pro' },
57
+ // ── Free tier / no-cost ──
48
58
  {
49
- id: 'nous',
50
- label: 'Nous Portal (subscription, zero-config)',
51
- kind: 'subscription',
52
- defaultModel: 'hermes-3-llama-3.1-405b',
59
+ id: 'groq',
60
+ shortLabel: 'Groq',
61
+ label: 'Groq — free, fast, limited messages per minute',
62
+ kind: 'key',
63
+ envVar: 'GROQ_API_KEY',
64
+ keyUrl: 'https://console.groq.com/keys',
65
+ defaultModel: 'llama-3.3-70b-versatile',
66
+ models: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant', 'mixtral-8x7b-32768'],
67
+ },
68
+ {
69
+ id: 'gemini',
70
+ shortLabel: 'Google Gemini',
71
+ label: 'Google Gemini — free, generous limits',
72
+ kind: 'key',
73
+ envVar: 'GEMINI_API_KEY',
74
+ keyUrl: 'https://aistudio.google.com/apikey',
75
+ defaultModel: 'gemini-2.0-flash',
76
+ },
77
+ {
78
+ id: 'openrouter',
79
+ shortLabel: 'OpenRouter',
80
+ label: 'OpenRouter — free credits, then paid',
81
+ kind: 'key',
82
+ envVar: 'OPENROUTER_API_KEY',
83
+ keyUrl: 'https://openrouter.ai/keys',
84
+ defaultModel: 'anthropic/claude-opus-4',
85
+ },
86
+ {
87
+ id: 'nvidia',
88
+ shortLabel: 'NVIDIA NIM',
89
+ label: 'NVIDIA NIM — free, but rate-limited',
90
+ kind: 'key',
91
+ envVar: 'NVIDIA_API_KEY',
92
+ keyUrl: 'https://build.nvidia.com',
93
+ defaultModel: 'meta/llama-3.3-70b-instruct',
53
94
  },
95
+ {
96
+ id: 'ollama',
97
+ shortLabel: 'Ollama',
98
+ label: 'Ollama — fully offline, no key needed (requires Ollama install)',
99
+ kind: 'local',
100
+ defaultModel: 'llama3.1:8b',
101
+ },
102
+ // ── Paid (best quality) ──
54
103
  {
55
104
  id: 'anthropic',
56
- label: 'Anthropic API key',
105
+ shortLabel: 'Anthropic',
106
+ label: 'Anthropic — paid, best for complex tasks',
57
107
  kind: 'key',
58
108
  envVar: 'ANTHROPIC_API_KEY',
109
+ keyUrl: 'https://console.anthropic.com/settings/keys',
59
110
  defaultModel: 'claude-opus-4-7',
60
111
  models: ['claude-opus-4-7', 'claude-sonnet-4-5', 'claude-haiku-4-5'],
61
112
  },
62
113
  {
63
114
  id: 'openai',
64
- label: 'OpenAI API key',
115
+ shortLabel: 'OpenAI',
116
+ label: 'OpenAI — paid, GPT models',
65
117
  kind: 'key',
66
118
  envVar: 'OPENAI_API_KEY',
119
+ keyUrl: 'https://platform.openai.com/api-keys',
67
120
  defaultModel: 'gpt-5',
68
121
  models: ['gpt-5', 'gpt-4o', 'gpt-4o-mini'],
69
122
  },
70
123
  {
71
124
  id: 'together',
72
- // Phase 16f: Together + Qwen3 is the recommended primary — strong tool
73
- // calling, $0.20/M throughput tier, 131k context. Free $5-10 credit on
74
- // signup covers a few hundred turns. Replaces Groq free tier as the
75
- // first recommendation after the user's Groq slots kept hammering
76
- // simultaneous 429s within 2 turns of normal use.
77
- label: 'Together AI (recommended — Qwen3-235B, paid throughput tier)',
125
+ shortLabel: 'Together AI',
126
+ label: 'Together AI paid, fast & reliable',
78
127
  kind: 'key',
79
128
  envVar: 'TOGETHER_API_KEY',
129
+ keyUrl: 'https://api.together.xyz/settings/api-keys',
80
130
  defaultModel: 'Qwen/Qwen3-235B-A22B-Instruct-2507-tput',
81
131
  models: [
82
132
  'Qwen/Qwen3-235B-A22B-Instruct-2507-tput',
@@ -84,74 +134,91 @@ exports.PROVIDERS = [
84
134
  'deepseek-ai/DeepSeek-V3',
85
135
  ],
86
136
  },
137
+ // ── Subscription sign-ins ──
87
138
  {
88
- id: 'groq',
89
- label: 'Groq (free tier — fast but tight TPM cap)',
90
- kind: 'key',
91
- envVar: 'GROQ_API_KEY',
92
- defaultModel: 'llama-3.3-70b-versatile',
93
- models: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant', 'mixtral-8x7b-32768'],
139
+ id: 'claude-pro',
140
+ shortLabel: 'Claude Pro',
141
+ label: 'Claude Pro — use your existing Claude subscription',
142
+ kind: 'pro',
94
143
  },
95
144
  {
96
- id: 'openrouter',
97
- label: 'OpenRouter (200+ models)',
98
- kind: 'key',
99
- envVar: 'OPENROUTER_API_KEY',
100
- defaultModel: 'anthropic/claude-opus-4',
101
- },
102
- {
103
- id: 'gemini',
104
- label: 'Google Gemini (free tier)',
105
- kind: 'key',
106
- envVar: 'GEMINI_API_KEY',
107
- defaultModel: 'gemini-2.0-flash',
145
+ id: 'chatgpt-plus',
146
+ shortLabel: 'ChatGPT Plus',
147
+ label: 'ChatGPT Plus — use your existing ChatGPT subscription',
148
+ kind: 'pro',
108
149
  },
150
+ // ── Legacy / power-user entries (kept to preserve env-var detection
151
+ // for users who already configured these out-of-band) ──
109
152
  {
110
153
  id: 'deepseek',
111
- label: 'DeepSeek',
154
+ shortLabel: 'DeepSeek',
155
+ label: 'DeepSeek — paid',
112
156
  kind: 'key',
113
157
  envVar: 'DEEPSEEK_API_KEY',
114
158
  defaultModel: 'deepseek-chat',
115
159
  },
116
160
  {
117
161
  id: 'mistral',
118
- label: 'Mistral',
162
+ shortLabel: 'Mistral',
163
+ label: 'Mistral — paid',
119
164
  kind: 'key',
120
165
  envVar: 'MISTRAL_API_KEY',
121
166
  defaultModel: 'mistral-large-latest',
122
167
  },
123
- { id: 'zai', label: 'Z.AI / GLM', kind: 'key', envVar: 'ZAI_API_KEY', defaultModel: 'glm-4-plus' },
168
+ {
169
+ id: 'zai',
170
+ shortLabel: 'Z.AI',
171
+ label: 'Z.AI / GLM — paid',
172
+ kind: 'key',
173
+ envVar: 'ZAI_API_KEY',
174
+ defaultModel: 'glm-4-plus',
175
+ },
124
176
  {
125
177
  id: 'kimi',
126
- label: 'Kimi / Moonshot',
178
+ shortLabel: 'Kimi',
179
+ label: 'Kimi / Moonshot — paid',
127
180
  kind: 'key',
128
181
  envVar: 'MOONSHOT_API_KEY',
129
182
  defaultModel: 'moonshot-v1-128k',
130
183
  },
131
- { id: 'minimax', label: 'MiniMax', kind: 'key', envVar: 'MINIMAX_API_KEY', defaultModel: 'abab6.5s-chat' },
132
184
  {
133
- id: 'nvidia',
134
- label: 'NVIDIA NIM (free tier)',
185
+ id: 'minimax',
186
+ shortLabel: 'MiniMax',
187
+ label: 'MiniMax — paid',
135
188
  kind: 'key',
136
- envVar: 'NVIDIA_API_KEY',
137
- defaultModel: 'meta/llama-3.3-70b-instruct',
189
+ envVar: 'MINIMAX_API_KEY',
190
+ defaultModel: 'abab6.5s-chat',
138
191
  },
139
192
  {
140
193
  id: 'huggingface',
141
- label: 'Hugging Face (free tier)',
194
+ shortLabel: 'Hugging Face',
195
+ label: 'Hugging Face — free tier',
142
196
  kind: 'key',
143
197
  envVar: 'HF_API_KEY',
144
198
  defaultModel: 'meta-llama/Llama-3.3-70B-Instruct',
145
199
  },
146
200
  {
147
201
  id: 'vercel',
148
- label: 'Vercel AI Gateway',
202
+ shortLabel: 'Vercel AI Gateway',
203
+ label: 'Vercel AI Gateway — paid',
149
204
  kind: 'key',
150
205
  envVar: 'VERCEL_AI_GATEWAY_KEY',
151
206
  defaultModel: 'anthropic/claude-opus-4',
152
207
  },
153
- { id: 'custom', label: 'Custom OpenAI-compatible endpoint', kind: 'custom', envVar: 'CUSTOM_API_KEY' },
154
- { id: 'ollama', label: 'Local (Ollama, no internet)', kind: 'local', defaultModel: 'llama3.1:8b' },
208
+ {
209
+ id: 'nous',
210
+ shortLabel: 'Nous Portal',
211
+ label: 'Nous Portal — subscription',
212
+ kind: 'subscription',
213
+ defaultModel: 'hermes-3-llama-3.1-405b',
214
+ },
215
+ {
216
+ id: 'custom',
217
+ shortLabel: 'custom endpoint',
218
+ label: 'Custom OpenAI-compatible endpoint',
219
+ kind: 'custom',
220
+ envVar: 'CUSTOM_API_KEY',
221
+ },
155
222
  ];
156
223
  /** Lazy-load @inquirer/prompts so unit tests don't need a TTY. */
157
224
  async function defaultPrompts() {
@@ -345,6 +412,66 @@ async function probeOllama(opts) {
345
412
  clearTimeout(timer);
346
413
  }
347
414
  }
415
+ /**
416
+ * Phase 30.2.1 — open `url` in the user's default browser. Used by the
417
+ * recovery menu's "Get a key from <provider URL>" branch. Best-effort
418
+ * — failure is non-fatal (we still print the URL so the user can copy
419
+ * it manually).
420
+ */
421
+ async function openUrlInBrowser(url, display) {
422
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
423
+ const { exec } = require('node:child_process');
424
+ const platform = process.platform;
425
+ let cmd;
426
+ if (platform === 'win32') {
427
+ // `start ""` swallows the URL into the title arg; double-empty-arg
428
+ // form keeps cmd.exe happy and leaves the URL as the actual target.
429
+ cmd = `cmd /c start "" "${url}"`;
430
+ }
431
+ else if (platform === 'darwin') {
432
+ cmd = `open "${url}"`;
433
+ }
434
+ else {
435
+ cmd = `xdg-open "${url}"`;
436
+ }
437
+ await new Promise((resolve) => {
438
+ exec(cmd, (err) => {
439
+ if (err) {
440
+ display.write(`${kleur_1.default.dim(`(could not auto-open browser — visit ${url} manually)`)}\n`);
441
+ }
442
+ resolve();
443
+ });
444
+ });
445
+ }
446
+ async function runRecoveryMenu(provider, prompts, display) {
447
+ const choices = [
448
+ 'Try a different provider',
449
+ provider.keyUrl
450
+ ? `Get a key from ${provider.keyUrl}`
451
+ : 'Get a key from the provider website (URL printed when picked)',
452
+ 'Save without validation (writes config; key untested)',
453
+ 'Skip — explore Aiden first (no chat, but / commands work)',
454
+ 'Exit (try again later)',
455
+ ];
456
+ const idx = await prompts.choose('What would you like to do?', choices,
457
+ /* default= */ 1);
458
+ switch (idx) {
459
+ case 1: return { kind: 'try-different' };
460
+ case 2: {
461
+ // Provider may not carry a keyUrl (legacy entries). Fall through
462
+ // to "try-different" so the user isn't stuck staring at nothing.
463
+ if (!provider.keyUrl) {
464
+ display.write(`${kleur_1.default.dim(`(no key URL on file for ${provider.shortLabel}; pick a different provider.)`)}\n`);
465
+ return { kind: 'try-different' };
466
+ }
467
+ return { kind: 'get-key', url: provider.keyUrl };
468
+ }
469
+ case 3: return { kind: 'save-anyway' };
470
+ case 4: return { kind: 'skip' };
471
+ case 5: return { kind: 'exit' };
472
+ default: return { kind: 'exit' };
473
+ }
474
+ }
348
475
  /**
349
476
  * Append (or overwrite) an entry in `.env`. Existing entries with the same
350
477
  * key are replaced. New entries are appended to the bottom. Keys are
@@ -381,59 +508,283 @@ async function runSetupWizard(opts = {}) {
381
508
  const prompts = opts.prompts ?? (await defaultPrompts());
382
509
  const fetchImpl = opts.fetchImpl ?? fetch;
383
510
  if (!opts.force && !(await isFreshInstall(paths))) {
384
- return { ran: false, skipReason: 'config.yaml already exists; pass force=true to re-run' };
511
+ // Existing config wizard wasn't needed. Treat as already-configured
512
+ // so the boot path proceeds to resolver (which will surface real
513
+ // credential issues with its own error contract).
514
+ return {
515
+ status: 'configured',
516
+ ran: false,
517
+ skipReason: 'config.yaml already exists; pass force=true to re-run',
518
+ };
385
519
  }
386
520
  await (0, paths_1.ensureAidenDirsExist)(paths);
387
521
  display.printBanner();
388
522
  display.write('\nWelcome — let\'s pick a provider.\n');
389
- display.write(`${kleur_1.default.dim('(Press Enter to accept the recommended Together AI option — fastest path to a working REPL.)')}\n\n`);
390
- // Step 1: provider selection. Phase 22 Task 1: pre-select Together as
391
- // the recommended default. Together's signup is fast, the free credit
392
- // covers a few hundred turns, and Qwen3-235B has strong tool calling —
393
- // optimises for time-to-first-tool-call.
394
- const togetherDefaultIdx = exports.PROVIDERS.findIndex((p) => p.id === 'together') + 1;
395
- const providerIndex = await prompts.choose('Which provider would you like to use?', exports.PROVIDERS.map((p) => p.label), togetherDefaultIdx > 0 ? togetherDefaultIdx : undefined);
396
- const provider = exports.PROVIDERS[providerIndex - 1];
397
- if (!provider)
398
- throw new Error(`invalid provider selection: ${providerIndex}`);
399
- // Phase 18: real OAuth flow for kind: 'pro' providers (claude-pro,
400
- // chatgpt-plus). The flow is the same one /auth login uses (Task 5);
401
- // single entry point.
402
- if (provider.kind === 'pro') {
403
- // 1-line explainer up-front so the user knows what they're agreeing to.
404
- const explainer = PRO_EXPLAINERS[provider.id] ??
405
- 'This connects your subscription via OAuth. No API key needed.';
406
- display.write(`\n${explainer}\n`);
407
- // Phase 18.1: honest beta framing per diagnostic 292c7cd. Some
408
- // upstream errors are account-state-specific and have no client-side
409
- // fix. We show this every time so a user who hits a wall has clear
410
- // remediation: rerun setup with an API-key provider.
411
- display.write('Note: OAuth flows are beta in v4.0. If signin fails, rerun `aiden setup` and pick an API-key provider instead.\n\n');
412
- const proceed = await prompts.confirm(`Continue with ${provider.label}?`, true);
413
- if (!proceed) {
414
- display.write('\nSkipped. Run `aiden setup` again to retry, or pick a different provider.\n');
415
- return { ran: false, skipReason: 'oauth-skipped' };
416
- }
417
- let oauthProvider;
523
+ display.write(`${kleur_1.default.dim('(Press Enter to accept Groq free + fastest setup.)')}\n\n`);
524
+ // Phase 30.2.1 Groq is the new recommended default for first-time
525
+ // users: free tier, fastest signup, and avoids the surprise charge
526
+ // path of paid providers. Together AI moved to position [8] paid.
527
+ const groqDefaultIdx = exports.PROVIDERS.findIndex((p) => p.id === 'groq') + 1;
528
+ // outer: provider-pick loop. Recovery option [1] "Try a different
529
+ // provider" jumps back to this prompt without losing progress on
530
+ // global state (display, paths, ensureAidenDirsExist already ran).
531
+ // Try/catch wraps inquirer to convert Ctrl+C ("User force closed
532
+ // the prompt") into the same "skipped" exit state as recovery [4]
533
+ // the user clearly didn't want to finish, but we still want them
534
+ // to land in REPL "explore mode" rather than crash.
535
+ // eslint-disable-next-line no-constant-condition
536
+ outer: while (true) {
537
+ let providerIndex;
418
538
  try {
419
- oauthProvider = await (0, loadProvider_1.loadOAuthProvider)(provider.id);
539
+ providerIndex = await prompts.choose('Which provider would you like to use?', exports.PROVIDERS.map((p) => p.label), groqDefaultIdx > 0 ? groqDefaultIdx : undefined);
420
540
  }
421
541
  catch (err) {
422
- display.write(display.error(`Could not load OAuth plugin for ${provider.label}: ${err.message}`));
423
- return { ran: false, skipReason: 'oauth-plugin-missing' };
542
+ const msg = err?.message ?? '';
543
+ if (/force closed|cancel/i.test(msg)) {
544
+ display.write('\nWizard cancelled — entering explore mode.\n');
545
+ return {
546
+ status: 'skipped',
547
+ ran: false,
548
+ skipReason: 'cancelled-at-provider-pick',
549
+ };
550
+ }
551
+ throw err;
424
552
  }
425
- const ua = wizardUserAgent(prompts, display);
426
- const runtime = new providerAuth_1.OAuthProviderRuntime(oauthProvider, paths);
427
- let tokens;
428
- try {
429
- tokens = await runtime.login(ua);
553
+ const provider = exports.PROVIDERS[providerIndex - 1];
554
+ if (!provider)
555
+ throw new Error(`invalid provider selection: ${providerIndex}`);
556
+ // Phase 18: real OAuth flow for kind: 'pro' providers (claude-pro,
557
+ // chatgpt-plus). The flow is the same one /auth login uses (Task 5);
558
+ // single entry point.
559
+ if (provider.kind === 'pro') {
560
+ // 1-line explainer up-front so the user knows what they're agreeing to.
561
+ const explainer = PRO_EXPLAINERS[provider.id] ??
562
+ 'This connects your subscription via OAuth. No API key needed.';
563
+ display.write(`\n${explainer}\n`);
564
+ // Phase 18.1: honest beta framing per diagnostic 292c7cd. Some
565
+ // upstream errors are account-state-specific and have no client-side
566
+ // fix. We show this every time so a user who hits a wall has clear
567
+ // remediation: rerun setup with an API-key provider.
568
+ display.write('Note: OAuth flows are beta in v4.0. If signin fails, rerun `aiden setup` and pick an API-key provider instead.\n\n');
569
+ const proceed = await prompts.confirm(`Continue with ${provider.shortLabel}?`, true);
570
+ if (!proceed) {
571
+ // Phase 30.2.1: don't dead-end. Loop back to provider pick so
572
+ // the user can choose another option without re-launching aiden.
573
+ display.write('\nNo problem — pick another provider.\n');
574
+ continue outer;
575
+ }
576
+ let oauthProvider;
577
+ try {
578
+ oauthProvider = await (0, loadProvider_1.loadOAuthProvider)(provider.id);
579
+ }
580
+ catch (err) {
581
+ display.write(display.error(`Could not load OAuth plugin for ${provider.shortLabel}: ${err.message}`));
582
+ // Plugin missing — let the user pick another provider.
583
+ continue outer;
584
+ }
585
+ const ua = wizardUserAgent(prompts, display);
586
+ const runtime = new providerAuth_1.OAuthProviderRuntime(oauthProvider, paths);
587
+ let tokens;
588
+ try {
589
+ tokens = await runtime.login(ua);
590
+ }
591
+ catch (err) {
592
+ display.write(display.error(`${provider.shortLabel} sign-in failed: ${err.message}`));
593
+ // OAuth failures are recoverable — loop back so the user can
594
+ // pick an API-key provider as a fallback.
595
+ continue outer;
596
+ }
597
+ // Pick a default model from the registry's known list; user can /model later.
598
+ const modelId = oauthProvider.defaultModels?.[0] ?? '';
599
+ const config = {
600
+ ...config_1.DEFAULT_CONFIG,
601
+ model: { provider: provider.id, modelId },
602
+ agent: { ...config_1.DEFAULT_CONFIG.agent, max_turns: config_1.DEFAULT_CONFIG.agent.max_turns },
603
+ display: { ...config_1.DEFAULT_CONFIG.display, skin: 'default' },
604
+ memory: { ...config_1.DEFAULT_CONFIG.memory },
605
+ providers: {
606
+ ...(config_1.DEFAULT_CONFIG.providers ?? {}),
607
+ // Marker for /providers + future tooling. The actual bearer
608
+ // lives in tokenStore — config.yaml does NOT carry the secret.
609
+ [provider.id]: { auth: 'oauth' },
610
+ },
611
+ terminal: { backend: 'auto' },
612
+ };
613
+ if (opts.smokeTest) {
614
+ display.write('\n✓ Smoke test complete — would have saved this config:\n');
615
+ display.write(`${JSON.stringify(config, null, 2)}\n`);
616
+ display.write(`(would have saved tokens to ${node_path_1.default.join(paths.root, 'auth', `${provider.id}.json`)})\n`);
617
+ return {
618
+ status: 'configured',
619
+ ran: false,
620
+ skipReason: 'smoke-test',
621
+ config,
622
+ envFile: paths.envFile,
623
+ };
624
+ }
625
+ const cm = new config_1.ConfigManager(paths);
626
+ await cm.save(config);
627
+ // Confirmation surface — what's wired now.
628
+ const expIso = new Date(tokens.expiresAtMs).toISOString();
629
+ display.write(`\n✓ ${provider.shortLabel} authed.\n`);
630
+ if (tokens.account)
631
+ display.write(` Account: ${tokens.account}\n`);
632
+ if (oauthProvider.defaultModels?.length) {
633
+ display.write(` Models: ${oauthProvider.defaultModels.join(', ')}\n`);
634
+ }
635
+ display.write(` Tokens stored at: ${node_path_1.default.join(paths.root, 'auth', `${provider.id}.json`)}\n`);
636
+ display.write(` Expires: ${expIso}\n`);
637
+ display.write(`\nTokens encrypted with a machine-derived key. Protects against casual ` +
638
+ `file inspection but NOT against code execution on this machine. ` +
639
+ `Real OS keychain integration in v4.1.\n`);
640
+ printPostWizardTutorial(display, AIDEN_VERSION);
641
+ return { status: 'configured', ran: true, config, envFile: paths.envFile };
430
642
  }
431
- catch (err) {
432
- display.write(display.error(`${provider.label} sign-in failed: ${err.message}`));
433
- return { ran: false, skipReason: 'oauth-failed' };
643
+ // Step 2: model selection. Phase 30.2.1: subsequent prompts use
644
+ // `shortLabel` instead of the full picker `label` to drop the
645
+ // parenthetical description from the wall of text.
646
+ let modelId = provider.defaultModel ?? '';
647
+ if (provider.models && provider.models.length > 1) {
648
+ const modelIndex = await prompts.choose(`Pick a model for ${provider.shortLabel}`, provider.models);
649
+ modelId = provider.models[modelIndex - 1];
650
+ }
651
+ else if (provider.kind === 'local') {
652
+ modelId = await prompts.input('Ollama model id', { default: provider.defaultModel ?? 'llama3.1:8b' });
653
+ }
654
+ else if (!modelId) {
655
+ modelId = await prompts.input('Model id', { default: '' });
656
+ }
657
+ // Step 3: credentials
658
+ let apiKey;
659
+ let baseUrl;
660
+ if (provider.kind === 'local') {
661
+ const reachable = await probeOllama({ fetchImpl });
662
+ if (!reachable) {
663
+ display.write(display.error('Ollama not reachable on http://localhost:11434', 'install from https://ollama.com, run `ollama serve`, then re-run `aiden setup`.'));
664
+ // Phase 30.2.1: Ollama unreachable is recoverable — loop back
665
+ // so the user can pick a different provider without restart.
666
+ continue outer;
667
+ }
668
+ }
669
+ else if (provider.kind === 'custom') {
670
+ baseUrl = await prompts.input('Base URL (e.g. https://api.example.com/v1)');
671
+ apiKey = await prompts.input('API key', { mask: true });
672
+ }
673
+ else if (provider.kind === 'key' || provider.kind === 'subscription') {
674
+ if (provider.envVar) {
675
+ apiKey = await prompts.input(`API key for ${provider.shortLabel}`, { mask: true });
676
+ }
677
+ }
678
+ // Step 3.5: validate the API key against the provider endpoint.
679
+ // Bypassed when smokeTest or skipValidation is set, or when there's no key
680
+ // to validate (Ollama, or a subscription provider without an env var).
681
+ const shouldValidate = !opts.smokeTest &&
682
+ !opts.skipValidation &&
683
+ typeof apiKey === 'string' &&
684
+ apiKey.length > 0;
685
+ if (shouldValidate) {
686
+ const validate = opts.validator ?? keyValidator_1.validateProviderKey;
687
+ const maxAttempts = 3;
688
+ let attempt = 1;
689
+ let validated = false;
690
+ let skipValidationForSave = false;
691
+ // Validation loop: at most 3 attempts before falling through to
692
+ // the recovery menu. First attempt uses the already-collected key;
693
+ // subsequent attempts re-prompt for a fresh key (and baseUrl, for
694
+ // custom). This loop labelled `validation` so the recovery flow
695
+ // can `continue validation` to retry with fresh attempts after
696
+ // option [2] "Get a key" opens the browser.
697
+ validation: while (attempt <= maxAttempts) {
698
+ const spinner = display.startSpinner(`Validating ${provider.shortLabel} API key…`);
699
+ let result;
700
+ try {
701
+ result = await validate(provider.id, apiKey, baseUrl, fetchImpl);
702
+ }
703
+ finally {
704
+ spinner.stop();
705
+ }
706
+ if (result.valid) {
707
+ if (result.skipped) {
708
+ display.write(`${kleur_1.default.dim(`Skipped validation: ${result.skipReason ?? 'no validation endpoint'}. The key will be tested on first call.`)}\n`);
709
+ }
710
+ else {
711
+ display.write(`${kleur_1.default.green(`✓ ${provider.shortLabel} API key validated`)}\n`);
712
+ }
713
+ validated = true;
714
+ break;
715
+ }
716
+ // Invalid — show error, re-prompt if we have attempts left.
717
+ display.write(display.error(`Validation failed: ${result.reason ?? 'unknown error'}`, attempt < maxAttempts
718
+ ? 'Re-enter the key, or press Ctrl+C to exit.'
719
+ : 'Three attempts used.'));
720
+ if (attempt >= maxAttempts) {
721
+ // Phase 30.2.1 — recovery menu replaces the prior dead-end
722
+ // `throw new Error(...)`. Five paths for the user to pick from:
723
+ // [1] try-different → loop back to provider picker
724
+ // [2] get-key (URL) → open browser, fresh 3 attempts
725
+ // [3] save-anyway → write config without validation
726
+ // [4] skip → boot REPL in explore mode
727
+ // [5] exit → clean exit
728
+ const choice = await runRecoveryMenu(provider, prompts, display);
729
+ if (choice.kind === 'try-different') {
730
+ continue outer;
731
+ }
732
+ if (choice.kind === 'get-key') {
733
+ display.write(`\nOpening ${choice.url} in your browser…\n`);
734
+ await openUrlInBrowser(choice.url, display);
735
+ display.write(`${kleur_1.default.dim('Paste the new key when prompted. You have 3 fresh attempts.')}\n`);
736
+ // Reset the attempt counter and re-prompt for a fresh key.
737
+ attempt = 1;
738
+ if (provider.kind === 'custom') {
739
+ baseUrl = await prompts.input('Base URL (e.g. https://api.example.com/v1)', { default: baseUrl });
740
+ apiKey = await prompts.input('API key', { mask: true });
741
+ }
742
+ else {
743
+ apiKey = await prompts.input(`API key for ${provider.shortLabel}`, { mask: true });
744
+ }
745
+ continue validation;
746
+ }
747
+ if (choice.kind === 'save-anyway') {
748
+ display.write(`${kleur_1.default.yellow('Saving without validation. The key will be tested on your first chat.')}\n`);
749
+ skipValidationForSave = true;
750
+ break validation;
751
+ }
752
+ if (choice.kind === 'skip') {
753
+ display.write('\nEntering explore mode — chat is disabled but slash commands work.\n');
754
+ return {
755
+ status: 'skipped',
756
+ ran: false,
757
+ skipReason: 'recovery-explore-mode',
758
+ };
759
+ }
760
+ // choice.kind === 'exit'
761
+ display.write('\nExited. Run `aiden setup` to try again.\n');
762
+ return { status: 'exited', ran: false, skipReason: 'recovery-exited' };
763
+ }
764
+ // Re-prompt for credentials (only reached when attempt < maxAttempts).
765
+ if (provider.kind === 'custom') {
766
+ baseUrl = await prompts.input('Base URL (e.g. https://api.example.com/v1)', {
767
+ default: baseUrl,
768
+ });
769
+ apiKey = await prompts.input('API key', { mask: true });
770
+ }
771
+ else {
772
+ apiKey = await prompts.input(`API key for ${provider.shortLabel}`, { mask: true });
773
+ }
774
+ attempt += 1;
775
+ }
776
+ // Tag the outer scope so the post-validation save path knows
777
+ // whether to print the "untested" warning. (validated is read
778
+ // by the smoke-test branch below; skipValidationForSave is
779
+ // currently unused outside this block but documented for
780
+ // post-save UX hooks.)
781
+ void validated;
782
+ void skipValidationForSave;
434
783
  }
435
- // Pick a default model from the registry's known list; user can /model later.
436
- const modelId = oauthProvider.defaultModels?.[0] ?? '';
784
+ // Step 4: terminal backend (basic keeps wizard in scope for 14a).
785
+ // Default to "auto" full picker lands in 14b.
786
+ const terminalBackend = 'auto';
787
+ // Step 5: save
437
788
  const config = {
438
789
  ...config_1.DEFAULT_CONFIG,
439
790
  model: { provider: provider.id, modelId },
@@ -442,17 +793,25 @@ async function runSetupWizard(opts = {}) {
442
793
  memory: { ...config_1.DEFAULT_CONFIG.memory },
443
794
  providers: {
444
795
  ...(config_1.DEFAULT_CONFIG.providers ?? {}),
445
- // Marker for /providers + future tooling. The actual bearer
446
- // lives in tokenStore config.yaml does NOT carry the secret.
447
- [provider.id]: { auth: 'oauth' },
796
+ [provider.id]: {
797
+ ...(baseUrl ? { baseUrl } : {}),
798
+ ...(provider.envVar ? { apiKey: `\${${provider.envVar}}` } : {}),
799
+ },
448
800
  },
449
- terminal: { backend: 'auto' },
801
+ terminal: { backend: terminalBackend },
450
802
  };
451
803
  if (opts.smokeTest) {
452
804
  display.write('\n✓ Smoke test complete — would have saved this config:\n');
453
805
  display.write(`${JSON.stringify(config, null, 2)}\n`);
454
- display.write(`(would have saved tokens to ${node_path_1.default.join(paths.root, 'auth', `${provider.id}.json`)})\n`);
806
+ if (apiKey && provider.envVar) {
807
+ display.write(`(would have written ${provider.envVar}=*** to ${paths.envFile})\n`);
808
+ }
809
+ if (baseUrl && provider.kind === 'custom') {
810
+ display.write(`(would have written CUSTOM_BASE_URL=${baseUrl} to ${paths.envFile})\n`);
811
+ }
812
+ display.write('(no files written because --smoke-test was passed)\n');
455
813
  return {
814
+ status: 'configured',
456
815
  ran: false,
457
816
  skipReason: 'smoke-test',
458
817
  config,
@@ -461,145 +820,19 @@ async function runSetupWizard(opts = {}) {
461
820
  }
462
821
  const cm = new config_1.ConfigManager(paths);
463
822
  await cm.save(config);
464
- // Confirmation surface — what's wired now.
465
- const expIso = new Date(tokens.expiresAtMs).toISOString();
466
- display.write(`\n✓ ${provider.label} authed.\n`);
467
- if (tokens.account)
468
- display.write(` Account: ${tokens.account}\n`);
469
- if (oauthProvider.defaultModels?.length) {
470
- display.write(` Models: ${oauthProvider.defaultModels.join(', ')}\n`);
471
- }
472
- display.write(` Tokens stored at: ${node_path_1.default.join(paths.root, 'auth', `${provider.id}.json`)}\n`);
473
- display.write(` Expires: ${expIso}\n`);
474
- display.write(`\nTokens encrypted with a machine-derived key. Protects against casual ` +
475
- `file inspection but NOT against code execution on this machine. ` +
476
- `Real OS keychain integration in v4.1.\n`);
477
- printPostWizardTutorial(display, AIDEN_VERSION);
478
- return { ran: true, config, envFile: paths.envFile };
479
- }
480
- // Step 2: model selection
481
- let modelId = provider.defaultModel ?? '';
482
- if (provider.models && provider.models.length > 1) {
483
- const modelIndex = await prompts.choose(`Pick a model for ${provider.label}`, provider.models);
484
- modelId = provider.models[modelIndex - 1];
485
- }
486
- else if (provider.kind === 'local') {
487
- modelId = await prompts.input('Ollama model id', { default: provider.defaultModel ?? 'llama3.1:8b' });
488
- }
489
- else if (!modelId) {
490
- modelId = await prompts.input('Model id', { default: '' });
491
- }
492
- // Step 3: credentials
493
- let apiKey;
494
- let baseUrl;
495
- if (provider.kind === 'local') {
496
- const reachable = await probeOllama({ fetchImpl });
497
- if (!reachable) {
498
- display.write(display.error('Ollama not reachable on http://localhost:11434', 'install from https://ollama.com, run `ollama serve`, then re-run `aiden setup`.'));
499
- return { ran: false, skipReason: 'ollama-not-reachable' };
500
- }
501
- }
502
- else if (provider.kind === 'custom') {
503
- baseUrl = await prompts.input('Base URL (e.g. https://api.example.com/v1)');
504
- apiKey = await prompts.input('API key', { mask: true });
505
- }
506
- else if (provider.kind === 'key' || provider.kind === 'subscription') {
507
- if (provider.envVar) {
508
- apiKey = await prompts.input(`API key for ${provider.label}`, { mask: true });
509
- }
510
- }
511
- // Step 3.5: validate the API key against the provider endpoint.
512
- // Bypassed when smokeTest or skipValidation is set, or when there's no key
513
- // to validate (Ollama, or a subscription provider without an env var).
514
- const shouldValidate = !opts.smokeTest &&
515
- !opts.skipValidation &&
516
- typeof apiKey === 'string' &&
517
- apiKey.length > 0;
518
- if (shouldValidate) {
519
- const validate = opts.validator ?? keyValidator_1.validateProviderKey;
520
- const maxAttempts = 3;
521
- let attempt = 1;
522
- // First attempt uses the key already collected. Subsequent attempts
523
- // re-prompt for a fresh key (and baseUrl, for custom).
524
- while (attempt <= maxAttempts) {
525
- const spinner = display.startSpinner(`Validating ${provider.label} API key…`);
526
- let result;
527
- try {
528
- result = await validate(provider.id, apiKey, baseUrl, fetchImpl);
529
- }
530
- finally {
531
- spinner.stop();
532
- }
533
- if (result.valid) {
534
- if (result.skipped) {
535
- display.write(`${kleur_1.default.dim(`Skipped validation: ${result.skipReason ?? 'no validation endpoint'}. The key will be tested on first call.`)}\n`);
536
- }
537
- else {
538
- display.write(`${kleur_1.default.green(`✓ ${provider.label} API key validated`)}\n`);
539
- }
540
- break;
541
- }
542
- // Invalid — show error, re-prompt if we have attempts left.
543
- display.write(display.error(`Validation failed: ${result.reason ?? 'unknown error'}`, 'Re-enter the key, or press Ctrl+C to exit.'));
544
- if (attempt >= maxAttempts) {
545
- throw new Error('Could not validate key after 3 attempts. Run `aiden setup --skip-validation` to bypass.');
546
- }
547
- // Re-prompt for credentials.
548
- if (provider.kind === 'custom') {
549
- baseUrl = await prompts.input('Base URL (e.g. https://api.example.com/v1)', {
550
- default: baseUrl,
551
- });
552
- apiKey = await prompts.input('API key', { mask: true });
553
- }
554
- else {
555
- apiKey = await prompts.input(`API key for ${provider.label}`, { mask: true });
556
- }
557
- attempt += 1;
558
- }
559
- }
560
- // Step 4: terminal backend (basic — keeps wizard in scope for 14a).
561
- // Default to "auto" — full picker lands in 14b.
562
- const terminalBackend = 'auto';
563
- // Step 5: save
564
- const config = {
565
- ...config_1.DEFAULT_CONFIG,
566
- model: { provider: provider.id, modelId },
567
- agent: { ...config_1.DEFAULT_CONFIG.agent, max_turns: config_1.DEFAULT_CONFIG.agent.max_turns },
568
- display: { ...config_1.DEFAULT_CONFIG.display, skin: 'default' },
569
- memory: { ...config_1.DEFAULT_CONFIG.memory },
570
- providers: {
571
- ...(config_1.DEFAULT_CONFIG.providers ?? {}),
572
- [provider.id]: {
573
- ...(baseUrl ? { baseUrl } : {}),
574
- ...(provider.envVar ? { apiKey: `\${${provider.envVar}}` } : {}),
575
- },
576
- },
577
- terminal: { backend: terminalBackend },
578
- };
579
- if (opts.smokeTest) {
580
- display.write('\n✓ Smoke test complete — would have saved this config:\n');
581
- display.write(`${JSON.stringify(config, null, 2)}\n`);
582
823
  if (apiKey && provider.envVar) {
583
- display.write(`(would have written ${provider.envVar}=*** to ${paths.envFile})\n`);
824
+ await upsertEnvVar(paths.envFile, provider.envVar, apiKey);
584
825
  }
585
826
  if (baseUrl && provider.kind === 'custom') {
586
- display.write(`(would have written CUSTOM_BASE_URL=${baseUrl} to ${paths.envFile})\n`);
827
+ await upsertEnvVar(paths.envFile, 'CUSTOM_BASE_URL', baseUrl);
587
828
  }
588
- display.write('(no files written because --smoke-test was passed)\n');
589
- return { ran: false, skipReason: 'smoke-test', config, envFile: paths.envFile };
590
- }
591
- const cm = new config_1.ConfigManager(paths);
592
- await cm.save(config);
593
- if (apiKey && provider.envVar) {
594
- await upsertEnvVar(paths.envFile, provider.envVar, apiKey);
595
- }
596
- if (baseUrl && provider.kind === 'custom') {
597
- await upsertEnvVar(paths.envFile, 'CUSTOM_BASE_URL', baseUrl);
598
- }
599
- // Step 6: tutorial
600
- display.write(`\n${kleur_1.default.green(`✓ ${provider.label}`)} configured with model ${kleur_1.default.cyan(modelId)}.\n`);
601
- printPostWizardTutorial(display, AIDEN_VERSION);
602
- return { ran: true, config, envFile: paths.envFile };
829
+ // Step 6: tutorial
830
+ display.write(`\n${kleur_1.default.green(`✓ ${provider.shortLabel}`)} configured with model ${kleur_1.default.cyan(modelId)}.\n`);
831
+ printPostWizardTutorial(display, AIDEN_VERSION);
832
+ return { status: 'configured', ran: true, config, envFile: paths.envFile };
833
+ } // end of outer: while (true) — every path inside either continues,
834
+ // returns, or breaks. Reaching this `}` is impossible (guarded by
835
+ // the no-constant-condition eslint comment above the outer label).
603
836
  }
604
837
  // ---------------------------------------------------------------------------
605
838
  // Direct invocation: `npx tsx cli/v4/setupWizard.ts [--smoke-test] [--force]`
@@ -611,10 +844,11 @@ if (require.main === module) {
611
844
  const skipValidation = argv.includes('--skip-validation');
612
845
  runSetupWizard({ smokeTest, force, skipValidation })
613
846
  .then((result) => {
614
- if (!result.ran && result.skipReason && result.skipReason !== 'smoke-test') {
615
- // Skipped for a reason that's already been displayed; non-zero so callers can detect.
616
- process.exit(0);
617
- }
847
+ // Phase 30.2.1: status is the new authoritative signal. Direct
848
+ // CLI invocation always exits 0 the wizard already printed
849
+ // its own outcome lines, and a non-zero exit confuses shell
850
+ // wrappers that pipe the wizard's output into other tools.
851
+ void result.status;
618
852
  process.exit(0);
619
853
  })
620
854
  .catch((err) => {