@wrongstack/cli 1.0.0 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/data/providers.json +21 -21
  2. package/dist/{acp-3LARESEO.js → acp-JK4ZQO3R.js} +4 -3
  3. package/dist/{auth-KIBJZZIE.js → auth-XD3GN5RJ.js} +80 -56
  4. package/dist/auth-menu/index.d.ts +1 -0
  5. package/dist/auth-menu/oauth-menu.d.ts +3 -1
  6. package/dist/auth-menu/panel-service.d.ts +2 -0
  7. package/dist/auth-menu/provider-auth-login.d.ts +12 -0
  8. package/dist/auth-menu/types.d.ts +3 -0
  9. package/dist/auto-discover-providers-DXR25GTG.js +7 -0
  10. package/dist/boot/dispatch-webui.d.ts +1 -0
  11. package/dist/boot/system-prompt-builder.d.ts +7 -0
  12. package/dist/boot/system-prompt-menu.d.ts +7 -0
  13. package/dist/boot/system-prompt.d.ts +2 -1
  14. package/dist/chimera-reviewer-policy.d.ts +1 -1
  15. package/dist/{chunk-4HV5RKZH.js → chunk-4NHD5OXJ.js} +3 -3
  16. package/dist/chunk-D5GB5MHA.js +87 -0
  17. package/dist/chunk-DAKVBMD4.js +36 -0
  18. package/dist/{chunk-Z7A5MXKJ.js → chunk-E6RFXHWD.js} +1 -2
  19. package/dist/{chunk-ZSZ3EYWH.js → chunk-GMY45LO6.js} +9 -5
  20. package/dist/{chunk-ETYHZPXP.js → chunk-IUEKNHPT.js} +12 -10
  21. package/dist/{chunk-MNQQ4ZNF.js → chunk-ONOT5OJ2.js} +290 -687
  22. package/dist/chunk-SIGUWXAA.js +43 -0
  23. package/dist/{chunk-TEBMCYZN.js → chunk-SIS37L3W.js} +58 -2
  24. package/dist/{chunk-5IA5337I.js → chunk-U6KCWRMC.js} +4 -2
  25. package/dist/chunk-UQUYBDMG.js +51 -0
  26. package/dist/{cli-context-CYMKNOK3.js → cli-context-Q6QP534K.js} +20 -9
  27. package/dist/{cli-main-TTIVR4DI.js → cli-main-JQXCDZEA.js} +2979 -2279
  28. package/dist/config-transfer-2YF3CRTN.js +99 -0
  29. package/dist/execute-deps.d.ts +1 -0
  30. package/dist/{execution-3Y55IWBK.js → execution-KH7WJNAX.js} +26 -19
  31. package/dist/index.js +4 -4
  32. package/dist/live-settings-input.d.ts +2 -0
  33. package/dist/{mailbox-serve-VUCWZ2SJ.js → mailbox-serve-X3CR6VFW.js} +4 -3
  34. package/dist/{modeldiag-FMJT37SN.js → modeldiag-46CMWUNM.js} +31 -24
  35. package/dist/{plugin-usage-HFBI3TFT.js → plugin-usage-M6KET4OZ.js} +2 -2
  36. package/dist/{plugins-PZ2VZL7V.js → plugins-U7KGQAUG.js} +3 -3
  37. package/dist/{providers-models-YVMVYWRS.js → providers-models-6MOGAOVK.js} +2 -2
  38. package/dist/settings-behavior-sections.d.ts +20 -0
  39. package/dist/{short-circuit-flags-6PSP56L7.js → short-circuit-flags-ZNZX44MC.js} +3 -3
  40. package/dist/slash-commands/codebase-map.d.ts +11 -0
  41. package/dist/slash-commands/command-context.d.ts +7 -0
  42. package/dist/slash-commands/context.d.ts +10 -0
  43. package/dist/slash-commands/openai-quota.d.ts +31 -0
  44. package/dist/subcommands/handlers/config-transfer.d.ts +6 -0
  45. package/dist/{subcommands-HZQKA4G2.js → subcommands-CUTGBDKP.js} +2 -2
  46. package/dist/{webui-server-T6AMOXJJ.js → webui-server-QWBUT7BO.js} +22 -5
  47. package/dist/webui-server-options.d.ts +1 -0
  48. package/dist/wiring/atlas-prompt-contributor.d.ts +54 -0
  49. package/dist/wiring/cli-execute-builder.d.ts +1 -0
  50. package/dist/wiring/codebase-embeddings.d.ts +31 -0
  51. package/dist/wiring/command-host-state.d.ts +2 -0
  52. package/dist/wiring/concept-summarizer.d.ts +32 -0
  53. package/dist/wiring/external-plugins.d.ts +1 -0
  54. package/dist/wiring/lifecycle-plugins.d.ts +2 -1
  55. package/dist/wiring/plugins.d.ts +2 -1
  56. package/dist/wiring/provider.d.ts +2 -1
  57. package/package.json +29 -29
  58. package/dist/auto-discover-providers-VQU3IZMR.js +0 -7
  59. package/dist/chunk-52C7PD2H.js +0 -61
  60. package/dist/chunk-TNK6237M.js +0 -262
@@ -8,10 +8,8 @@ import {
8
8
  visibleModelIds
9
9
  } from "./chunk-VUVOMXWP.js";
10
10
  import {
11
- openBrowser,
12
- runCodexOAuthLogin,
13
- startLoopbackServer
14
- } from "./chunk-TNK6237M.js";
11
+ openBrowser
12
+ } from "./chunk-DAKVBMD4.js";
15
13
  import {
16
14
  activeLabel,
17
15
  loadConfigProviders,
@@ -22,9 +20,123 @@ import {
22
20
  writeKeysBack
23
21
  } from "./chunk-DR2OXU46.js";
24
22
 
23
+ // src/auth-menu/provider-auth-login.ts
24
+ import { color } from "@wrongstack/core/utils";
25
+ import {
26
+ applyProviderAuthOutcome,
27
+ createBuiltinProviderAuthRegistry
28
+ } from "@wrongstack/providers/oauth";
29
+ var defaultRegistry;
30
+ function providerAuthRegistryFor(deps) {
31
+ if (deps.providerAuthRegistry) return deps.providerAuthRegistry;
32
+ defaultRegistry ??= createBuiltinProviderAuthRegistry();
33
+ return defaultRegistry;
34
+ }
35
+ function providerAuthStrategiesFor(deps) {
36
+ return providerAuthRegistryFor(deps).list();
37
+ }
38
+ function hyperlink(url) {
39
+ return `\x1B]8;;${url}\x1B\\${url}\x1B]8;;\x1B\\`;
40
+ }
41
+ async function runProviderAuthLogin(deps, strategyId, opts = {}) {
42
+ const registry = providerAuthRegistryFor(deps);
43
+ const strategy = registry.get(strategyId);
44
+ if (!strategy) {
45
+ deps.renderer.writeError(`Unknown OAuth login "${strategyId}".`);
46
+ return 1;
47
+ }
48
+ const controller = new AbortController();
49
+ let sigintCount = 0;
50
+ const onSigint = () => {
51
+ sigintCount += 1;
52
+ if (sigintCount === 1) {
53
+ deps.renderer.write(
54
+ color.dim("\n Cancelling sign-in\u2026 (press Ctrl+C again to force-quit)\n")
55
+ );
56
+ controller.abort();
57
+ } else {
58
+ process.exit(130);
59
+ }
60
+ };
61
+ const onExternalAbort = () => controller.abort();
62
+ if (opts.signal) {
63
+ if (opts.signal.aborted) controller.abort();
64
+ else opts.signal.addEventListener("abort", onExternalAbort, { once: true });
65
+ } else {
66
+ process.on("SIGINT", onSigint);
67
+ }
68
+ let session;
69
+ try {
70
+ deps.renderer.write(
71
+ color.bold(`
72
+ Sign in with ${strategy.label}`) + color.dim(` \u2192 ${opts.providerId ?? strategy.providerId}
73
+ `)
74
+ );
75
+ session = await registry.begin(
76
+ strategy.id,
77
+ { modelsRegistry: deps.modelsRegistry },
78
+ controller.signal
79
+ );
80
+ let outcome;
81
+ if (session.interaction.type === "device_code") {
82
+ const { verificationUri, userCode } = session.interaction;
83
+ deps.renderer.write(
84
+ color.bold("\n Open this URL and enter the code:\n") + color.cyan(` ${hyperlink(verificationUri)}
85
+ `) + color.bold(` Code: ${userCode}
86
+ `) + color.dim(" Waiting for authorization\u2026\n")
87
+ );
88
+ openBrowser(verificationUri);
89
+ outcome = await session.waitForCompletion(controller.signal);
90
+ } else {
91
+ const { authorizeUrl, bound } = session.interaction;
92
+ deps.renderer.write(
93
+ color.bold("\n Open this URL in your browser to sign in:\n") + color.cyan(` ${hyperlink(authorizeUrl)}
94
+ `)
95
+ );
96
+ if (bound) {
97
+ openBrowser(authorizeUrl);
98
+ deps.renderer.write(color.dim(" Waiting for browser authorization\u2026\n"));
99
+ outcome = await session.waitForCompletion(controller.signal);
100
+ } else {
101
+ const input = (await deps.reader.readLine(
102
+ `
103
+ ${color.amber("?")} Paste the redirect URL or code ${color.dim("(or q to cancel)")}: `
104
+ )).trim();
105
+ if (!input || input.toLowerCase() === "q") return 1;
106
+ outcome = await session.completeWithCode(input, controller.signal);
107
+ }
108
+ }
109
+ if (!outcome) {
110
+ deps.renderer.write(color.dim(" Sign-in cancelled or timed out.\n"));
111
+ return 1;
112
+ }
113
+ await mutateConfigProviders(
114
+ deps.profileConfigPath,
115
+ deps.vault,
116
+ (providers) => {
117
+ applyProviderAuthOutcome(providers, outcome, { targetProviderId: opts.providerId });
118
+ },
119
+ deps.profileConfigPath
120
+ );
121
+ const savedId = opts.providerId ?? outcome.providerId;
122
+ deps.renderer.write(color.green(`
123
+ \u2713 Signed in \u2014 saved as ${savedId}.
124
+ `));
125
+ return 0;
126
+ } catch (error) {
127
+ const message = error instanceof DOMException && error.name === "AbortError" ? "Login cancelled." : error instanceof Error ? error.message : String(error);
128
+ deps.renderer.writeError(` Login failed: ${message}`);
129
+ return 1;
130
+ } finally {
131
+ session?.close();
132
+ if (opts.signal) opts.signal.removeEventListener("abort", onExternalAbort);
133
+ else process.off("SIGINT", onSigint);
134
+ }
135
+ }
136
+
25
137
  // src/auth-menu/local.ts
26
138
  import { DefaultSecretScrubber as DefaultSecretScrubber2 } from "@wrongstack/core/security";
27
- import { color as color2 } from "@wrongstack/core/utils";
139
+ import { color as color3 } from "@wrongstack/core/utils";
28
140
  import { probeLocalLlm } from "@wrongstack/runtime/probe";
29
141
 
30
142
  // src/auth-menu/auth-menu-audit.ts
@@ -108,24 +220,24 @@ import { projectLocalProviderPresets } from "@wrongstack/providers/definitions";
108
220
  var LOCAL_LLM_PRESETS = projectLocalProviderPresets();
109
221
 
110
222
  // src/auth-menu/shared.ts
111
- import { color } from "@wrongstack/core/utils";
223
+ import { color as color2 } from "@wrongstack/core/utils";
112
224
  function renderProviderLine(renderer, id, cfg, idx) {
113
225
  const keys = normalizeKeys(cfg);
114
226
  const active = activeLabel(cfg, keys);
115
227
  const firstKey = keys[0];
116
228
  let summary;
117
229
  if (keys.length === 0) {
118
- summary = color.dim("(no keys)");
230
+ summary = color2.dim("(no keys)");
119
231
  } else if (keys.length === 1) {
120
232
  summary = maskedKey(firstKey?.apiKey ?? "");
121
233
  } else {
122
234
  const activeKeyObj = active != null ? keys.find((k) => k.label === active) : void 0;
123
- summary = `${color.dim(`${keys.length} keys`)} ${color.dim("active:")} ${color.bold(active ?? "?")} ` + maskedKey(activeKeyObj?.apiKey ?? firstKey?.apiKey ?? "");
235
+ summary = `${color2.dim(`${keys.length} keys`)} ${color2.dim("active:")} ${color2.bold(active ?? "?")} ` + maskedKey(activeKeyObj?.apiKey ?? firstKey?.apiKey ?? "");
124
236
  }
125
- const fam = cfg.family ? color.dim(`[${cfg.family}]`) : "";
126
- const aliasHint = cfg.type && cfg.type !== id ? color.dim(`\u2192 ${cfg.type}`) : "";
237
+ const fam = cfg.family ? color2.dim(`[${cfg.family}]`) : "";
238
+ const aliasHint = cfg.type && cfg.type !== id ? color2.dim(`\u2192 ${cfg.type}`) : "";
127
239
  renderer.write(
128
- ` ${color.dim(`${idx}.`.padStart(4))} ${id.padEnd(22)} ${fam} ${aliasHint} ${summary}
240
+ ` ${color2.dim(`${idx}.`.padStart(4))} ${id.padEnd(22)} ${fam} ${aliasHint} ${summary}
129
241
  `
130
242
  );
131
243
  }
@@ -134,23 +246,23 @@ function renderProviderHeader(renderer, providerId, cfg) {
134
246
  const active = activeLabel(cfg, keys);
135
247
  renderer.write(
136
248
  `
137
- ${color.bold(providerId)} ${cfg.family ? color.dim(`[${cfg.family}]`) : color.amber("[no family]")}
249
+ ${color2.bold(providerId)} ${cfg.family ? color2.dim(`[${cfg.family}]`) : color2.amber("[no family]")}
138
250
  `
139
251
  );
140
252
  const details = [
141
- color.dim(` type: ${cfg.type ?? providerId}`),
142
- color.dim(` family: ${cfg.family ?? "(unset \u2192 resolved from models.dev when type matches)"}`),
143
- color.dim(` baseUrl: ${cfg.baseUrl ?? "(unset \u2192 catalog default)"}`)
253
+ color2.dim(` type: ${cfg.type ?? providerId}`),
254
+ color2.dim(` family: ${cfg.family ?? "(unset \u2192 resolved from models.dev when type matches)"}`),
255
+ color2.dim(` baseUrl: ${cfg.baseUrl ?? "(unset \u2192 catalog default)"}`)
144
256
  ];
145
257
  if (cfg.envVars && cfg.envVars.length > 0) {
146
- details.push(color.dim(` envVars: ${cfg.envVars.join(", ")}`));
258
+ details.push(color2.dim(` envVars: ${cfg.envVars.join(", ")}`));
147
259
  }
148
260
  if (cfg.models && cfg.models.length > 0) {
149
- details.push(color.dim(` models: ${cfg.models.join(", ")}`));
261
+ details.push(color2.dim(` models: ${cfg.models.join(", ")}`));
150
262
  }
151
263
  renderer.write(details.join("\n") + "\n");
152
264
  if (keys.length === 0) {
153
- renderer.write(color.dim(" (no keys saved)\n"));
265
+ renderer.write(color2.dim(" (no keys saved)\n"));
154
266
  } else {
155
267
  for (let i = 0; i < keys.length; i++) {
156
268
  const key = keys[i];
@@ -159,56 +271,56 @@ ${color.bold(providerId)} ${cfg.family ? color.dim(`[${cfg.family}]`) : color.am
159
271
  }
160
272
  }
161
273
  function renderKeyLine(renderer, key, idx, active) {
162
- const marker = key.label === active ? color.green("\u25CF") : color.dim("\u25CB");
274
+ const marker = key.label === active ? color2.green("\u25CF") : color2.dim("\u25CB");
163
275
  renderer.write(
164
- ` ${color.dim(`${idx}.`.padStart(4))} ${marker} ${key.label.padEnd(20)} ${maskedKey(key.apiKey)} ${color.dim(key.createdAt)}
276
+ ` ${color2.dim(`${idx}.`.padStart(4))} ${marker} ${key.label.padEnd(20)} ${maskedKey(key.apiKey)} ${color2.dim(key.createdAt)}
165
277
  `
166
278
  );
167
279
  }
168
280
  function renderActions(renderer, keysLength) {
169
281
  renderer.write(`
170
- ${color.dim("Actions:")}
282
+ ${color2.dim("Actions:")}
171
283
  `);
172
- renderer.write(` ${color.bold("a")} Add another key
284
+ renderer.write(` ${color2.bold("a")} Add another key
173
285
  `);
174
286
  if (keysLength > 0) {
175
- renderer.write(` ${color.bold("u")} <n> Update key <n>
287
+ renderer.write(` ${color2.bold("u")} <n> Update key <n>
176
288
  `);
177
- renderer.write(` ${color.bold("d")} <n> Delete key <n>
289
+ renderer.write(` ${color2.bold("d")} <n> Delete key <n>
178
290
  `);
179
- renderer.write(` ${color.bold("s")} <n> Set key <n> as active
291
+ renderer.write(` ${color2.bold("s")} <n> Set key <n> as active
180
292
  `);
181
293
  }
182
- renderer.write(` ${color.bold("f")} Edit family
294
+ renderer.write(` ${color2.bold("f")} Edit family
183
295
  `);
184
296
  renderer.write(
185
- ` ${color.bold("B")} Edit baseUrl ${color.dim("(capital B \u2014 b is Back)")}
297
+ ` ${color2.bold("B")} Edit baseUrl ${color2.dim("(capital B \u2014 b is Back)")}
186
298
  `
187
299
  );
188
- renderer.write(` ${color.bold("m")} Edit visible model list
300
+ renderer.write(` ${color2.bold("m")} Edit visible model list
189
301
  `);
190
- renderer.write(` ${color.bold("x")} Remove this provider entirely
302
+ renderer.write(` ${color2.bold("x")} Remove this provider entirely
191
303
  `);
192
- renderer.write(` ${color.bold("b")} Back
304
+ renderer.write(` ${color2.bold("b")} Back
193
305
  `);
194
- renderer.write(` ${color.bold("q")} Quit
306
+ renderer.write(` ${color2.bold("q")} Quit
195
307
  `);
196
308
  }
197
309
  function renderTopMenu(renderer, providers) {
198
310
  renderer.write(`
199
- ${color.bold("WrongStack")} ${color.dim("\u2014 API key manager")}
311
+ ${color2.bold("WrongStack")} ${color2.dim("\u2014 API key manager")}
200
312
 
201
313
  `);
202
314
  const ids = Object.keys(providers).sort();
203
315
  if (ids.length === 0) {
204
- renderer.write(color.dim(" No providers configured yet.\n"));
316
+ renderer.write(color2.dim(" No providers configured yet.\n"));
205
317
  renderer.write(
206
- color.dim(
318
+ color2.dim(
207
319
  " Use (a) to add one from the models.dev catalog, (l) for a local server, or (c) for a custom provider.\n"
208
320
  )
209
321
  );
210
322
  } else {
211
- renderer.write(` ${color.dim("Saved providers:")}
323
+ renderer.write(` ${color2.dim("Saved providers:")}
212
324
  `);
213
325
  let idx = 1;
214
326
  for (const id of ids) {
@@ -219,32 +331,32 @@ ${color.bold("WrongStack")} ${color.dim("\u2014 API key manager")}
219
331
  }
220
332
  }
221
333
  renderer.write(`
222
- ${color.dim("Actions:")}
334
+ ${color2.dim("Actions:")}
223
335
  `);
224
- renderer.write(` ${color.bold("a")} Add a provider (from catalog)
336
+ renderer.write(` ${color2.bold("a")} Add a provider (from catalog)
225
337
  `);
226
338
  renderer.write(
227
- ` ${color.bold("l")} Add a local server ${color.dim("(OmniRoute / Ollama / vLLM / LM Studio)")}
339
+ ` ${color2.bold("l")} Add a local server ${color2.dim("(OmniRoute / Ollama / vLLM / LM Studio)")}
228
340
  `
229
341
  );
230
- renderer.write(` ${color.bold("c")} Add a custom provider
342
+ renderer.write(` ${color2.bold("c")} Add a custom provider
231
343
  `);
232
344
  renderer.write(
233
- ` ${color.bold("s")} Login with OAuth ${color.dim("(ChatGPT / Claude / Copilot)")}
345
+ ` ${color2.bold("s")} Login with provider OAuth
234
346
  `
235
347
  );
236
348
  if (ids.length > 0) {
237
349
  renderer.write(
238
- ` ${color.dim("1-")}${color.dim(String(ids.length))} ${color.bold("Manage a provider")}
350
+ ` ${color2.dim("1-")}${color2.dim(String(ids.length))} ${color2.bold("Manage a provider")}
239
351
  `
240
352
  );
241
353
  }
242
- renderer.write(` ${color.bold("q")} Quit
354
+ renderer.write(` ${color2.bold("q")} Quit
243
355
  `);
244
356
  }
245
357
  async function readKeyInput(deps, intent) {
246
358
  const key = (await deps.reader.readSecret(
247
- ` ${color.amber("?")} ${intent} ${color.dim("(hidden, paste OK)")}: `
359
+ ` ${color2.amber("?")} ${intent} ${color2.dim("(hidden, paste OK)")}: `
248
360
  )).trim();
249
361
  if (!key) {
250
362
  deps.renderer.writeError("No key entered.");
@@ -253,7 +365,7 @@ async function readKeyInput(deps, intent) {
253
365
  return key;
254
366
  }
255
367
  async function confirm(deps, question) {
256
- const answer = (await deps.reader.readLine(` ${color.amber("?")} ${question} ${color.dim("[y/N/q]")} `)).trim().toLowerCase();
368
+ const answer = (await deps.reader.readLine(` ${color2.amber("?")} ${question} ${color2.dim("[y/N/q]")} `)).trim().toLowerCase();
257
369
  if (answer === "q" || answer === "quit") return null;
258
370
  return answer === "y" || answer === "yes";
259
371
  }
@@ -315,7 +427,7 @@ async function runAuthLocal(deps, opts = {}) {
315
427
  if (!probe.ok) {
316
428
  if (chosen.noAuth) {
317
429
  deps.renderer.write(
318
- color2.dim(
430
+ color3.dim(
319
431
  ` Saving ${chosen.label} anyway \u2014 keyless local gateway needs no key and discovers models when it comes up.
320
432
  `
321
433
  )
@@ -324,7 +436,7 @@ async function runAuthLocal(deps, opts = {}) {
324
436
  } else {
325
437
  const saveAnyway = await promptSaveAnyway(deps);
326
438
  if (!saveAnyway) {
327
- deps.renderer.write(color2.dim(" Cancelled. Nothing saved.\n"));
439
+ deps.renderer.write(color3.dim(" Cancelled. Nothing saved.\n"));
328
440
  return 0;
329
441
  }
330
442
  probeFailedAndSaved = true;
@@ -387,19 +499,19 @@ async function runAuthLocal(deps, opts = {}) {
387
499
  }
388
500
  if (apiKey) {
389
501
  deps.renderer.write(
390
- ` ${color2.green("\u2713")} Saved ${color2.bold(chosen.id)}/${color2.bold(finalLabel)} \u2192 ${color2.cyan(baseUrl)}
502
+ ` ${color3.green("\u2713")} Saved ${color3.bold(chosen.id)}/${color3.bold(finalLabel)} \u2192 ${color3.cyan(baseUrl)}
391
503
  `
392
504
  );
393
505
  } else {
394
506
  deps.renderer.write(
395
- ` ${color2.green("\u2713")} Saved ${color2.bold(chosen.id)} (no key) \u2192 ${color2.cyan(baseUrl)}
507
+ ` ${color3.green("\u2713")} Saved ${color3.bold(chosen.id)} (no key) \u2192 ${color3.cyan(baseUrl)}
396
508
  `
397
509
  );
398
510
  }
399
511
  const firstModel = resolvedModels && resolvedModels.length > 0 ? resolvedModels[0] : null;
400
- const modelHint = firstModel ? color2.cyan(firstModel) : color2.dim("<model-id>");
512
+ const modelHint = firstModel ? color3.cyan(firstModel) : color3.dim("<model-id>");
401
513
  deps.renderer.write(
402
- color2.dim(` Launch: wstack --provider ${chosen.id} --model ${modelHint} "<task>"
514
+ color3.dim(` Launch: wstack --provider ${chosen.id} --model ${modelHint} "<task>"
403
515
  `)
404
516
  );
405
517
  return 0;
@@ -431,47 +543,47 @@ function resolveModelList(models, probe, scrubber) {
431
543
  return out;
432
544
  }
433
545
  function renderProbeResult(deps, probe, opts = {}) {
434
- const label = opts.presetLabel ? ` ${color2.bold(opts.presetLabel)}` : "";
435
- const elapsed = probe.elapsedMs !== void 0 ? ` ${color2.dim(`(${probe.elapsedMs}ms)`)}` : "";
546
+ const label = opts.presetLabel ? ` ${color3.bold(opts.presetLabel)}` : "";
547
+ const elapsed = probe.elapsedMs !== void 0 ? ` ${color3.dim(`(${probe.elapsedMs}ms)`)}` : "";
436
548
  switch (probe.status) {
437
549
  case "ok": {
438
- const models = probe.modelCount !== void 0 ? color2.dim(`, ${probe.modelCount} model${probe.modelCount === 1 ? "" : "s"}`) : "";
439
- deps.renderer.write(` ${color2.green("\u25CF")} ${label} health probe ok${models}${elapsed}
550
+ const models = probe.modelCount !== void 0 ? color3.dim(`, ${probe.modelCount} model${probe.modelCount === 1 ? "" : "s"}`) : "";
551
+ deps.renderer.write(` ${color3.green("\u25CF")} ${label} health probe ok${models}${elapsed}
440
552
  `);
441
553
  break;
442
554
  }
443
555
  case "timeout": {
444
556
  deps.renderer.write(
445
- ` ${color2.red("\u25CF")} ${label} health probe timed out${elapsed}
446
- ${color2.dim("The server did not respond within the timeout.")}
447
- ${color2.dim("Is the local server running? Check the port and try again.")}
557
+ ` ${color3.red("\u25CF")} ${label} health probe timed out${elapsed}
558
+ ${color3.dim("The server did not respond within the timeout.")}
559
+ ${color3.dim("Is the local server running? Check the port and try again.")}
448
560
  `
449
561
  );
450
562
  break;
451
563
  }
452
564
  case "unreachable": {
453
- const detail = probe.detail ? ` ${color2.dim(`(${probe.detail})`)}` : "";
565
+ const detail = probe.detail ? ` ${color3.dim(`(${probe.detail})`)}` : "";
454
566
  deps.renderer.write(
455
- ` ${color2.red("\u25CF")} ${label} server unreachable${detail}
456
- ${color2.dim("Could not connect. Common causes:")}
457
- ${color2.dim(" - The local server is not running")}
458
- ${color2.dim(" - The port is blocked by a firewall")}
459
- ${color2.dim(" - The base URL is wrong")}
567
+ ` ${color3.red("\u25CF")} ${label} server unreachable${detail}
568
+ ${color3.dim("Could not connect. Common causes:")}
569
+ ${color3.dim(" - The local server is not running")}
570
+ ${color3.dim(" - The port is blocked by a firewall")}
571
+ ${color3.dim(" - The base URL is wrong")}
460
572
  `
461
573
  );
462
574
  break;
463
575
  }
464
576
  case "http_error": {
465
577
  const status = probe.httpStatus !== void 0 ? ` HTTP ${probe.httpStatus}` : "";
466
- const detail = probe.detail ? ` ${color2.dim(`\u2014 ${probe.detail}`)}` : "";
467
- deps.renderer.write(` ${color2.yellow("\u25CF")} ${label} health probe got${status}${detail}
578
+ const detail = probe.detail ? ` ${color3.dim(`\u2014 ${probe.detail}`)}` : "";
579
+ deps.renderer.write(` ${color3.yellow("\u25CF")} ${label} health probe got${status}${detail}
468
580
  `);
469
581
  break;
470
582
  }
471
583
  case "invalid_response": {
472
584
  deps.renderer.write(
473
- ` ${color2.yellow("\u25CF")} ${label} health probe returned an unexpected shape
474
- ` + (probe.detail ? ` ${color2.dim(probe.detail)}
585
+ ` ${color3.yellow("\u25CF")} ${label} health probe returned an unexpected shape
586
+ ` + (probe.detail ? ` ${color3.dim(probe.detail)}
475
587
  ` : "")
476
588
  );
477
589
  break;
@@ -484,31 +596,31 @@ function renderProbeResult(deps, probe, opts = {}) {
484
596
  async function promptSaveAnyway(deps) {
485
597
  const answer = (await deps.reader.readLine(
486
598
  `
487
- ${color2.amber("?")} ${color2.bold("Save anyway?")} ${color2.dim("[y/N]")}: `
599
+ ${color3.amber("?")} ${color3.bold("Save anyway?")} ${color3.dim("[y/N]")}: `
488
600
  )).trim().toLowerCase();
489
601
  return answer === "y" || answer === "yes";
490
602
  }
491
603
  async function pickLocalPreset(deps) {
492
604
  deps.renderer.write(
493
605
  `
494
- ${color2.bold("Local LLM servers")} ${color2.dim("\u2014 pick one to pre-fill the base URL")}
606
+ ${color3.bold("Local LLM servers")} ${color3.dim("\u2014 pick one to pre-fill the base URL")}
495
607
 
496
608
  `
497
609
  );
498
610
  let idx = 1;
499
611
  for (const entry of LOCAL_LLM_PRESETS) {
500
- const auth = entry.noAuth ? color2.dim("(no auth)") : color2.dim("(optional auth)");
612
+ const auth = entry.noAuth ? color3.dim("(no auth)") : color3.dim("(optional auth)");
501
613
  deps.renderer.write(
502
- ` ${color2.dim(`${idx}.`.padStart(4))} ${color2.bold(entry.label.padEnd(12))} ${color2.cyan(entry.defaultBaseUrl.padEnd(34))} ${auth}
614
+ ` ${color3.dim(`${idx}.`.padStart(4))} ${color3.bold(entry.label.padEnd(12))} ${color3.cyan(entry.defaultBaseUrl.padEnd(34))} ${auth}
503
615
  `
504
616
  );
505
- deps.renderer.write(` ${color2.dim(entry.hint)}
617
+ deps.renderer.write(` ${color3.dim(entry.hint)}
506
618
  `);
507
619
  idx++;
508
620
  }
509
621
  const answer = (await deps.reader.readLine(
510
622
  `
511
- ${color2.amber("?")} Pick ${color2.dim(`(1-${LOCAL_LLM_PRESETS.length}, q to quit)`)}: `
623
+ ${color3.amber("?")} Pick ${color3.dim(`(1-${LOCAL_LLM_PRESETS.length}, q to quit)`)}: `
512
624
  )).trim().toLowerCase();
513
625
  if (!answer || answer === "q" || answer === "quit") return void 0;
514
626
  const num = Number.parseInt(answer, 10);
@@ -522,563 +634,54 @@ ${color2.amber("?")} Pick ${color2.dim(`(1-${LOCAL_LLM_PRESETS.length}, q to qui
522
634
  }
523
635
  async function promptOptionalKey(deps, preset) {
524
636
  const answer = (await deps.reader.readSecret(
525
- ` ${color2.amber("?")} ${color2.bold(preset.label)} API key ${color2.dim("(hidden \u2014 Enter to skip if auth is disabled on the server)")}: `
637
+ ` ${color3.amber("?")} ${color3.bold(preset.label)} API key ${color3.dim("(hidden \u2014 Enter to skip if auth is disabled on the server)")}: `
526
638
  )).trim();
527
639
  if (!answer) return void 0;
528
640
  return answer;
529
641
  }
530
642
 
531
643
  // src/auth-menu/oauth-menu.ts
532
- import { color as color5 } from "@wrongstack/core/utils";
533
-
534
- // src/auth-menu/anthropic-oauth.ts
535
- import { createHash, randomBytes } from "node:crypto";
536
- import {
537
- FetchError,
538
- ParseError
539
- } from "@wrongstack/core/types";
540
- import { color as color3 } from "@wrongstack/core/utils";
541
- var CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
542
- var AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
543
- var TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
544
- var REDIRECT_PORT = 53692;
545
- var REDIRECT_HOST = "127.0.0.1";
546
- var REDIRECT_PATH = "/callback";
547
- var REDIRECT_URI = `http://localhost:${REDIRECT_PORT}${REDIRECT_PATH}`;
548
- var SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
549
- var CLAUDE_PROVIDER_ID = "anthropic-oauth";
550
- var CLAUDE_BASE_URL = "https://api.anthropic.com";
551
- function base64url(buf) {
552
- return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
553
- }
554
- function generatePkce() {
555
- const verifier = base64url(randomBytes(32));
556
- const challenge = base64url(createHash("sha256").update(verifier).digest());
557
- return { verifier, challenge };
558
- }
559
- function buildAuthorizeUrl(challenge, verifier) {
560
- const params = new URLSearchParams({
561
- code: "true",
562
- client_id: CLIENT_ID,
563
- response_type: "code",
564
- redirect_uri: REDIRECT_URI,
565
- scope: SCOPES,
566
- code_challenge: challenge,
567
- code_challenge_method: "S256",
568
- state: verifier
569
- });
570
- return `${AUTHORIZE_URL}?${params.toString()}`;
571
- }
572
- function parseAuthorizationInput(input) {
573
- const value = input.trim();
574
- if (!value) return {};
575
- try {
576
- const url = new URL(value);
577
- return {
578
- code: url.searchParams.get("code") ?? void 0,
579
- state: url.searchParams.get("state") ?? void 0
580
- };
581
- } catch {
582
- }
583
- if (value.includes("#")) {
584
- const [code, state] = value.split("#", 2);
585
- return { code, state };
586
- }
587
- if (value.includes("code=")) {
588
- const params = new URLSearchParams(value);
589
- return {
590
- code: params.get("code") ?? void 0,
591
- state: params.get("state") ?? void 0
592
- };
593
- }
594
- return { code: value };
595
- }
596
- async function readTokens(res, op, url) {
597
- if (!res.ok) {
598
- const text = await res.text().catch(() => "");
599
- throw new FetchError({
600
- message: `Claude token ${op} failed (${res.status}): ${text || res.statusText}`,
601
- status: res.status,
602
- context: { provider: "anthropic-oauth", op, url }
603
- });
604
- }
605
- const json = await res.json();
606
- if (!json?.access_token || !json.refresh_token || typeof json.expires_in !== "number") {
607
- throw new ParseError({
608
- message: `Claude token ${op} response missing fields`,
609
- source: "anthropic-oauth-token-response",
610
- context: { op }
611
- });
612
- }
613
- return {
614
- access: json.access_token,
615
- refresh: json.refresh_token,
616
- expires: Date.now() + json.expires_in * 1e3
617
- };
618
- }
619
- async function exchangeAuthorizationCode(code, state, verifier, signal) {
620
- const res = await fetch(TOKEN_URL, {
621
- method: "POST",
622
- headers: { "content-type": "application/json", accept: "application/json" },
623
- body: JSON.stringify({
624
- grant_type: "authorization_code",
625
- client_id: CLIENT_ID,
626
- code,
627
- state,
628
- redirect_uri: REDIRECT_URI,
629
- code_verifier: verifier
630
- }),
631
- signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(3e4)]) : AbortSignal.timeout(3e4)
632
- });
633
- return readTokens(res, "exchange", TOKEN_URL);
634
- }
635
- async function fetchClaudeModels(accessToken, signal) {
636
- try {
637
- const res = await fetch(`${CLAUDE_BASE_URL}/v1/models?limit=100`, {
638
- headers: {
639
- accept: "application/json",
640
- authorization: `Bearer ${accessToken}`,
641
- "anthropic-version": "2023-06-01",
642
- "anthropic-beta": "claude-code-20250219,oauth-2025-04-20"
643
- },
644
- signal: AbortSignal.any([signal, AbortSignal.timeout(8e3)])
645
- });
646
- if (!res.ok) return [];
647
- const json = await res.json();
648
- const ids = (json?.data ?? []).map((m) => m.id).filter((id) => typeof id === "string" && id.startsWith("claude-"));
649
- return ids;
650
- } catch {
651
- return [];
652
- }
653
- }
654
- async function runClaudeOAuthLogin(deps, opts = {}) {
655
- const providerId = opts.providerId ?? CLAUDE_PROVIDER_ID;
656
- const { verifier, challenge } = generatePkce();
657
- const state = verifier;
658
- const authorizeUrl = buildAuthorizeUrl(challenge, verifier);
659
- const osc8 = (url) => `\x1B]8;;${url}\x1B\\${url}\x1B]8;;\x1B\\`;
660
- const ac = new AbortController();
661
- const onSig = () => ac.abort();
662
- const external = opts.signal;
663
- const onExternalAbort = () => ac.abort();
664
- if (external) {
665
- if (external.aborted) ac.abort();
666
- else external.addEventListener("abort", onExternalAbort, { once: true });
667
- } else {
668
- process.on("SIGINT", onSig);
669
- }
670
- const server = await startLoopbackServer(
671
- state,
672
- [REDIRECT_PORT],
673
- ac.signal,
674
- REDIRECT_PATH,
675
- REDIRECT_HOST
676
- );
677
- deps.renderer.write(
678
- color3.bold(`
679
- Sign in with Claude \u2014 ${color3.cyan(providerId)}
680
- `) + color3.dim(" Uses your Claude Pro/Max subscription (not an API key).\n") + color3.amber(" \u26A0 Using a subscription outside the official Claude Code client is against\n") + color3.amber(" Anthropic\u2019s Terms \u2014 your account could be rate-limited or banned.\n") + color3.dim(" Sanctioned programmatic use = an API key: ") + color3.bold("wstack auth anthropic") + color3.dim("\n\n") + color3.bold(` ${"\u2500".repeat(56)}
681
- `) + color3.bold(" Open this URL in your browser to sign in:\n") + color3.cyan(` ${osc8(authorizeUrl)}
682
- `) + color3.bold(` ${"\u2500".repeat(56)}
683
-
684
- `)
685
- );
686
- if (server.bound) {
687
- openBrowser(authorizeUrl);
688
- deps.renderer.write(
689
- color3.dim(" A browser window should open. Waiting for you to finish signing in...\n") + color3.dim(" (Listening on http://localhost:53692 \u2014 press Ctrl+C to cancel.)\n")
690
- );
691
- } else {
692
- deps.renderer.write(
693
- color3.amber(" \u26A0 Could not start the local callback listener (port 53692 in use).\n") + color3.dim(" After signing in, paste the full redirect URL (or the code) below.\n")
694
- );
695
- }
696
- let code;
697
- try {
698
- if (server.bound) {
699
- const got = await server.waitForCode();
700
- if (ac.signal.aborted) {
701
- deps.renderer.write(color3.dim(" Cancelled.\n"));
702
- return 1;
703
- }
704
- if (got) code = got.code;
705
- }
706
- if (!code) {
707
- const input = (await deps.reader.readLine(
708
- `
709
- ${color3.amber("?")} Paste the redirect URL or code ${color3.dim("(or q to cancel)")}: `
710
- )).trim();
711
- if (input.toLowerCase() === "q" || input === "") {
712
- deps.renderer.write(color3.dim(" Cancelled.\n"));
713
- return 1;
714
- }
715
- const parsed = parseAuthorizationInput(input);
716
- if (parsed.state && parsed.state !== state) {
717
- deps.renderer.writeError(" State mismatch \u2014 please restart the login flow.");
718
- return 1;
719
- }
720
- code = parsed.code;
721
- }
722
- if (!code) {
723
- deps.renderer.writeError(" No authorization code received.");
724
- return 1;
725
- }
726
- deps.renderer.write(color3.dim("\n Exchanging authorization code for tokens...\n"));
727
- const tokens = await exchangeAuthorizationCode(code, state, verifier, ac.signal);
728
- const models = await fetchClaudeModels(tokens.access, ac.signal);
729
- const saved = await saveClaudeTokens(deps, providerId, tokens, models);
730
- if (!saved) return 1;
731
- deps.renderer.write(color3.green("\n \u2713 Signed in with Claude!\n"));
732
- const modelHint = models[0];
733
- deps.renderer.writeInfo(
734
- ` Saved as provider ${color3.bold(providerId)}${models.length ? ` (${models.length} models)` : ""}.
735
- ` + (modelHint ? ` Use: ${color3.bold(`wstack --provider ${providerId} --model ${modelHint}`)} "<task>"
736
- ` : ` No model list was discovered; choose a model before starting a run.
737
- `) + color3.dim(" Tokens refresh automatically before they expire.\n")
738
- );
739
- return 0;
740
- } catch (err) {
741
- const msg = err instanceof DOMException && err.name === "AbortError" ? "Login cancelled." : err.message;
742
- deps.renderer.writeError(` Login failed: ${msg}`);
743
- return 1;
744
- } finally {
745
- server.close();
746
- if (external) external.removeEventListener("abort", onExternalAbort);
747
- else process.off("SIGINT", onSig);
748
- }
749
- }
750
- async function saveClaudeTokens(deps, providerId, tokens, models) {
751
- const entry = {
752
- label: "oauth-default",
753
- apiKey: tokens.access,
754
- createdAt: nowIso(),
755
- authMethod: "oauth",
756
- expiresAt: new Date(tokens.expires).toISOString(),
757
- refreshToken: tokens.refresh,
758
- tokenType: "bearer",
759
- scope: SCOPES
760
- };
761
- try {
762
- await mutateConfigProviders(
763
- deps.profileConfigPath,
764
- deps.vault,
765
- (all) => {
766
- const existing = all[providerId];
767
- const p = existing ? { ...existing } : { type: providerId };
768
- p.family = "anthropic-oauth";
769
- if (!p.baseUrl) p.baseUrl = CLAUDE_BASE_URL;
770
- if (models.length > 0) p.models = models;
771
- const keys = normalizeKeys(p).filter((k) => k.label !== entry.label);
772
- keys.push(entry);
773
- writeKeysBack(p, keys);
774
- p.activeKey = entry.label;
775
- all[providerId] = p;
776
- },
777
- deps.profileConfigPath
778
- );
779
- return true;
780
- } catch (err) {
781
- deps.renderer.writeError(` Failed to save tokens: ${err.message}`);
782
- return false;
783
- }
784
- }
785
-
786
- // src/auth-menu/github-copilot-oauth.ts
787
- import {
788
- FetchError as FetchError2,
789
- ParseError as ParseError2
790
- } from "@wrongstack/core/types";
791
644
  import { color as color4 } from "@wrongstack/core/utils";
792
- import { copilotBaseUrlFromToken, refreshCopilotToken } from "@wrongstack/providers";
793
- var CLIENT_ID2 = "Iv1.b507a08c87ecfe98";
794
- var DEVICE_CODE_URL = "https://github.com/login/device/code";
795
- var ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
796
- var COPILOT_HEADERS = {
797
- "User-Agent": "GitHubCopilotChat/0.35.0",
798
- "Editor-Version": "vscode/1.107.0",
799
- "Editor-Plugin-Version": "copilot-chat/0.35.0",
800
- "Copilot-Integration-Id": "vscode-chat"
801
- };
802
- var COPILOT_API_VERSION = "2026-06-01";
803
- var COPILOT_PROVIDER_ID = "github-copilot";
804
- function sleep(ms, signal) {
805
- return new Promise((resolve, reject) => {
806
- if (signal.aborted) return reject(new DOMException("Aborted", "AbortError"));
807
- const t = setTimeout(() => {
808
- signal.removeEventListener("abort", onAbort);
809
- resolve();
810
- }, ms);
811
- function onAbort() {
812
- clearTimeout(t);
813
- reject(new DOMException("Aborted", "AbortError"));
814
- }
815
- signal.addEventListener("abort", onAbort, { once: true });
816
- });
817
- }
818
- async function startDeviceFlow(signal) {
819
- const res = await fetch(DEVICE_CODE_URL, {
820
- method: "POST",
821
- headers: {
822
- accept: "application/json",
823
- "content-type": "application/x-www-form-urlencoded",
824
- "user-agent": COPILOT_HEADERS["User-Agent"]
825
- },
826
- body: new URLSearchParams({ client_id: CLIENT_ID2, scope: "read:user" }).toString(),
827
- signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(15e3)]) : AbortSignal.timeout(15e3)
828
- });
829
- if (!res.ok) {
830
- throw new FetchError2({
831
- message: `GitHub device-code request failed (${res.status})`,
832
- status: res.status,
833
- context: { provider: "github-copilot", op: "device-code", url: DEVICE_CODE_URL }
834
- });
835
- }
836
- const json = await res.json();
837
- if (!json?.device_code || !json.user_code || !json.verification_uri || typeof json.expires_in !== "number") {
838
- throw new ParseError2({
839
- message: "Invalid device-code response",
840
- source: "github-copilot-device-code-response"
841
- });
842
- }
843
- return {
844
- device_code: json.device_code,
845
- user_code: json.user_code,
846
- verification_uri: json.verification_uri,
847
- interval: json.interval ?? 5,
848
- expires_in: json.expires_in
849
- };
850
- }
851
- async function pollForGitHubToken(device, signal) {
852
- let intervalMs = device.interval * 1e3;
853
- const expiresAt = Date.now() + device.expires_in * 1e3;
854
- while (Date.now() < expiresAt) {
855
- await sleep(intervalMs, signal);
856
- const res = await fetch(ACCESS_TOKEN_URL, {
857
- method: "POST",
858
- headers: {
859
- accept: "application/json",
860
- "content-type": "application/x-www-form-urlencoded",
861
- "user-agent": COPILOT_HEADERS["User-Agent"]
862
- },
863
- body: new URLSearchParams({
864
- client_id: CLIENT_ID2,
865
- device_code: device.device_code,
866
- grant_type: "urn:ietf:params:oauth:grant-type:device_code"
867
- }).toString(),
868
- signal: AbortSignal.any([signal, AbortSignal.timeout(15e3)])
869
- });
870
- const json = await res.json().catch(() => ({}));
871
- if (json.access_token) return json.access_token;
872
- if (json.error === "authorization_pending") continue;
873
- if (json.error === "slow_down") {
874
- intervalMs += 5e3;
875
- continue;
876
- }
877
- throw new Error(`Device flow failed: ${json.error ?? "unknown error"}`);
878
- }
879
- throw new FetchError2({
880
- message: "Device code expired \u2014 please restart the login.",
881
- status: 408,
882
- // Request Timeout
883
- context: { provider: "github-copilot", op: "device-code-poll", reason: "expired" }
884
- });
885
- }
886
- function isUsableCopilotChatModel(item) {
887
- if (typeof item.id !== "string" || item.id.length === 0) return false;
888
- const cap = item.capabilities;
889
- if (cap?.type !== "chat") return false;
890
- if (cap.supports?.tool_calls !== true) return false;
891
- const eps = item.supported_endpoints;
892
- if (Array.isArray(eps) && !eps.includes("/chat/completions")) return false;
893
- if (item.policy?.state === "disabled") return false;
894
- if (item.vendor === "Experimental") return false;
895
- return true;
896
- }
897
- async function fetchCopilotModels(copilotToken, signal) {
898
- try {
899
- const base = copilotBaseUrlFromToken(copilotToken);
900
- const res = await fetch(`${base}/models`, {
901
- headers: {
902
- accept: "application/json",
903
- authorization: `Bearer ${copilotToken}`,
904
- "X-GitHub-Api-Version": COPILOT_API_VERSION,
905
- ...COPILOT_HEADERS
906
- },
907
- signal: AbortSignal.any([signal, AbortSignal.timeout(8e3)])
908
- });
909
- if (!res.ok) return [];
910
- const json = await res.json();
911
- const data = json?.data;
912
- if (!Array.isArray(data)) return [];
913
- const usable = data.filter(isUsableCopilotChatModel);
914
- usable.sort((a, b) => copilotModelRank(a) - copilotModelRank(b));
915
- return usable.map((m) => m.id);
916
- } catch {
917
- return [];
918
- }
919
- }
920
- function copilotModelRank(item) {
921
- if (item.is_chat_default === true) return 0;
922
- if (item.is_chat_fallback === true) return 1;
923
- return 2;
924
- }
925
- async function runCopilotOAuthLogin(deps, opts = {}) {
926
- const providerId = opts.providerId ?? COPILOT_PROVIDER_ID;
927
- const ac = new AbortController();
928
- const onSig = () => ac.abort();
929
- const onExternalAbort = () => ac.abort();
930
- const external = opts.signal;
931
- if (external) {
932
- if (external.aborted) ac.abort();
933
- else external.addEventListener("abort", onExternalAbort, { once: true });
934
- } else {
935
- process.on("SIGINT", onSig);
936
- }
937
- try {
938
- deps.renderer.write(
939
- color4.bold(`
940
- Sign in with GitHub Copilot \u2014 ${color4.cyan(providerId)}
941
- `) + color4.dim(" Uses your GitHub Copilot subscription (not an API key).\n") + color4.amber(" \u26A0 Using Copilot outside its official editor integrations may violate\n") + color4.amber(" GitHub\u2019s Terms \u2014 your account could be rate-limited or banned.\n\n")
942
- );
943
- const device = await startDeviceFlow(ac.signal);
944
- deps.renderer.write(
945
- color4.bold(` ${"\u2500".repeat(56)}
946
- `) + color4.bold(" Open this URL and enter the code:\n") + color4.cyan(` ${device.verification_uri}
947
- `) + color4.bold(" Code: ") + color4.green(color4.bold(device.user_code)) + "\n" + color4.bold(` ${"\u2500".repeat(56)}
948
-
949
- `)
950
- );
951
- openBrowser(device.verification_uri);
952
- deps.renderer.write(color4.dim(" Waiting for you to authorize in the browser...\n"));
953
- const githubToken = await pollForGitHubToken(device, ac.signal);
954
- deps.renderer.write(color4.dim(" Authorized. Fetching your Copilot token...\n"));
955
- const copilot = await refreshCopilotToken(githubToken, ac.signal);
956
- const models = await fetchCopilotModels(copilot.token, ac.signal);
957
- const saved = await saveCopilotTokens(
958
- deps,
959
- providerId,
960
- copilot.token,
961
- githubToken,
962
- copilot.expires,
963
- models
964
- );
965
- if (!saved) return 1;
966
- deps.renderer.write(color4.green("\n \u2713 Signed in with GitHub Copilot!\n"));
967
- const modelHint = models[0] ?? "gpt-4o";
968
- deps.renderer.writeInfo(
969
- ` Saved as provider ${color4.bold(providerId)}${models.length ? ` (${models.length} models)` : ""}.
970
- Use: ${color4.bold(`wstack --provider ${providerId} --model ${modelHint}`)} "<task>"
971
- ` + color4.dim(" The Copilot token refreshes automatically.\n")
972
- );
973
- return 0;
974
- } catch (err) {
975
- const msg = err instanceof DOMException && err.name === "AbortError" ? "Login cancelled." : err.message;
976
- deps.renderer.writeError(` Login failed: ${msg}`);
977
- return 1;
978
- } finally {
979
- if (external) {
980
- external.removeEventListener("abort", onExternalAbort);
981
- } else {
982
- process.off("SIGINT", onSig);
983
- }
984
- }
985
- }
986
- async function saveCopilotTokens(deps, providerId, copilotToken, githubToken, expires, models) {
987
- const entry = {
988
- label: "oauth-default",
989
- apiKey: copilotToken,
990
- createdAt: nowIso(),
991
- authMethod: "oauth",
992
- expiresAt: new Date(expires).toISOString(),
993
- refreshToken: githubToken,
994
- tokenType: "bearer"
995
- };
996
- try {
997
- await mutateConfigProviders(
998
- deps.profileConfigPath,
999
- deps.vault,
1000
- (all) => {
1001
- const existing = all[providerId];
1002
- const p = existing ? { ...existing } : { type: providerId };
1003
- p.family = "github-copilot";
1004
- if (!p.baseUrl) p.baseUrl = copilotBaseUrlFromToken(copilotToken);
1005
- if (models.length > 0) p.models = models;
1006
- else if (!p.models || p.models.length === 0) p.models = ["gpt-4o"];
1007
- const keys = normalizeKeys(p).filter((k) => k.label !== entry.label);
1008
- keys.push(entry);
1009
- writeKeysBack(p, keys);
1010
- p.activeKey = entry.label;
1011
- all[providerId] = p;
1012
- },
1013
- deps.profileConfigPath
1014
- );
1015
- return true;
1016
- } catch (err) {
1017
- deps.renderer.writeError(` Failed to save tokens: ${err.message}`);
1018
- return false;
1019
- }
1020
- }
1021
-
1022
- // src/auth-menu/oauth-menu.ts
1023
645
  function renderOAuthLoginOptions(deps, indent = " ") {
646
+ const entries = providerAuthRegistryFor(deps).list();
1024
647
  deps.renderer.write(
1025
- `${indent}${color5.bold("OAuth login options")} ${color5.dim("(subscription sign-in)")}
1026
- ${indent}${color5.bold("chatgpt")} ChatGPT Plus/Pro ${color5.dim("(\u2192 openai-codex)")}
1027
- ${indent}${color5.bold("claude")} Claude Pro/Max ${color5.dim("(\u2192 anthropic-oauth)")}
1028
- ${indent}${color5.bold("copilot")} GitHub Copilot ${color5.dim("(\u2192 github-copilot)")}
648
+ `${indent}${color4.bold("OAuth login options")} ${color4.dim("(browser / device sign-in)")}
649
+ ` + entries.map(
650
+ (entry) => `${indent}${color4.bold(entry.id.padEnd(10))} ${entry.label} ${color4.dim(`(\u2192 ${entry.providerId})`)}
1029
651
  `
652
+ ).join("")
1030
653
  );
1031
654
  }
1032
- var OAUTH_ALIASES = {
1033
- chatgpt: [
1034
- "chatgpt",
1035
- "openai",
1036
- "codex",
1037
- "codex-cli",
1038
- "openai-codex",
1039
- "chatgpt-plus",
1040
- "plus",
1041
- "pro"
1042
- ],
1043
- claude: ["claude", "anthropic", "claude-pro", "claude-max", "anthropic-oauth", "max"],
1044
- copilot: ["copilot", "github", "gh", "github-copilot"]
1045
- };
1046
- var OAUTH_NUMERIC = {
1047
- "1": "chatgpt",
1048
- "2": "claude",
1049
- "3": "copilot"
1050
- };
1051
655
  function resolveOAuthKind(choice, opts = {}) {
1052
656
  const pick = choice.trim().toLowerCase();
1053
657
  if (!pick) return void 0;
1054
- if (opts.allowNumeric !== false && OAUTH_NUMERIC[pick]) return OAUTH_NUMERIC[pick];
1055
- for (const [kind, aliases] of Object.entries(OAUTH_ALIASES)) {
1056
- if (aliases.includes(pick)) return kind;
658
+ const registry = opts.deps ? providerAuthRegistryFor(opts.deps) : providerAuthRegistryFor({});
659
+ if (opts.allowNumeric !== false && /^\d+$/.test(pick)) {
660
+ return registry.list()[Number(pick) - 1]?.id;
1057
661
  }
1058
- return void 0;
662
+ return registry.resolveId(pick);
1059
663
  }
1060
664
  async function runOAuthLoginChoice(deps, choice, opts = {}) {
1061
- const kind = resolveOAuthKind(choice, opts);
665
+ const kind = resolveOAuthKind(choice, { ...opts, deps });
1062
666
  if (!kind) return false;
1063
667
  await runOAuthLoginKind(deps, kind);
1064
668
  return true;
1065
669
  }
1066
670
  async function runOAuthLoginKind(deps, kind) {
1067
- if (kind === "claude") return runClaudeOAuthLogin(deps);
1068
- if (kind === "copilot") return runCopilotOAuthLogin(deps);
1069
- return runCodexOAuthLogin(deps);
671
+ return runProviderAuthLogin(deps, kind);
1070
672
  }
1071
673
  async function runOAuthLoginMenu(deps) {
674
+ const entries = providerAuthRegistryFor(deps).list();
1072
675
  deps.renderer.write(
1073
676
  `
1074
- ${color5.bold("Login with OAuth:")}
1075
- ` + color5.amber(" \u26A0 Subscription tokens used outside official clients may violate provider\n") + color5.amber(" Terms \u2014 your account could be rate-limited or banned. An API key is the\n") + color5.dim(" sanctioned path for programmatic use.\n") + ` ${color5.bold("1")} ChatGPT Plus/Pro ${color5.dim("(\u2192 openai-codex)")}
1076
- ${color5.bold("2")} Claude Pro/Max ${color5.dim("(\u2192 anthropic-oauth)")}
1077
- ${color5.bold("3")} GitHub Copilot ${color5.dim("(\u2192 github-copilot)")}
677
+ ${color4.bold("Login with OAuth:")}
678
+ ` + color4.amber(" \u26A0 Subscription authentication may be governed by provider-specific terms.\n") + entries.map(
679
+ (entry, index) => ` ${color4.bold(String(index + 1))} ${entry.label} ${color4.dim(`(\u2192 ${entry.providerId})`)}
1078
680
  `
681
+ ).join("")
1079
682
  );
1080
683
  const pick = await deps.reader.readLine(
1081
- ` ${color5.amber("?")} Pick ${color5.dim("(or b to go back)")}: `
684
+ ` ${color4.amber("?")} Pick ${color4.dim("(or b to go back)")}: `
1082
685
  );
1083
686
  await runOAuthLoginChoice(deps, pick);
1084
687
  }
@@ -1086,7 +689,7 @@ async function runOAuthLoginMenu(deps) {
1086
689
  // src/picker.ts
1087
690
  import os from "node:os";
1088
691
  import {
1089
- color as color8,
692
+ color as color7,
1090
693
  expectDefined as expectDefined2,
1091
694
  setOutputLineGuard as setOutputLineGuard2,
1092
695
  setRawMode as setRawMode2,
@@ -1130,7 +733,7 @@ function applyPickerKey(state, chunk, matchCount) {
1130
733
 
1131
734
  // src/picker-model-picker.ts
1132
735
  import {
1133
- color as color7,
736
+ color as color6,
1134
737
  expectDefined,
1135
738
  setOutputLineGuard,
1136
739
  setRawMode,
@@ -1138,14 +741,14 @@ import {
1138
741
  } from "@wrongstack/core/utils";
1139
742
 
1140
743
  // src/picker-ui.ts
1141
- import { color as color6, stripAnsi } from "@wrongstack/core/utils";
744
+ import { color as color5, stripAnsi } from "@wrongstack/core/utils";
1142
745
  var theme = {
1143
- accent: color6.amber,
1144
- chrome: color6.gray,
1145
- cursor: (s) => color6.amber(color6.bold(s)),
1146
- ctx: color6.cyan,
1147
- cost: color6.green,
1148
- caps: color6.magenta
746
+ accent: color5.amber,
747
+ chrome: color5.gray,
748
+ cursor: (s) => color5.amber(color5.bold(s)),
749
+ ctx: color5.cyan,
750
+ cost: color5.green,
751
+ caps: color5.magenta
1149
752
  };
1150
753
  var BOX = {
1151
754
  tl: "\u256D",
@@ -1179,7 +782,7 @@ function boxTop(title) {
1179
782
  const label = ` ${title} `;
1180
783
  const pad = Math.max(0, width - visibleWidth(label) - 1);
1181
784
  return theme.chrome(
1182
- `${BOX.tl}${BOX.h}${theme.accent(color6.bold(label))}${theme.chrome(BOX.h.repeat(pad))}${BOX.tr}`
785
+ `${BOX.tl}${BOX.h}${theme.accent(color5.bold(label))}${theme.chrome(BOX.h.repeat(pad))}${BOX.tr}`
1183
786
  );
1184
787
  }
1185
788
  function boxBottom() {
@@ -1192,15 +795,15 @@ function boxDivider() {
1192
795
  return theme.chrome(`${BOX.v}${BOX.h.repeat(pickerWidth())}${BOX.v}`);
1193
796
  }
1194
797
  function keyHints(pairs) {
1195
- return pairs.map(([k, label]) => `${theme.accent(k)} ${color6.dim(label)}`).join(color6.dim(" \xB7 "));
798
+ return pairs.map(([k, label]) => `${theme.accent(k)} ${color5.dim(label)}`).join(color5.dim(" \xB7 "));
1196
799
  }
1197
800
  var CODEX_PROVIDER_ID = "openai-codex";
1198
801
  var CODEX_PICKER_HEADER = "Select Model and Effort";
1199
802
  var CODEX_LEGACY_NOTE = "Access legacy models by running `wstack -m <model_name>` or in your `config.json`.";
1200
803
  function codexPickerPreamble(provider) {
1201
804
  if (provider.id !== CODEX_PROVIDER_ID) return "";
1202
- return ` ${color6.bold(CODEX_PICKER_HEADER)}
1203
- ${color6.dim(` ${CODEX_LEGACY_NOTE}`)}
805
+ return ` ${color5.bold(CODEX_PICKER_HEADER)}
806
+ ${color5.dim(` ${CODEX_LEGACY_NOTE}`)}
1204
807
 
1205
808
  `;
1206
809
  }
@@ -1219,9 +822,9 @@ function renderLiveModelList(query, filtered, selectedIdx, header) {
1219
822
  const visible = ordered.slice(0, LIVE_PICKER_MAX_VISIBLE);
1220
823
  const lines = [];
1221
824
  lines.push(boxTop("Select a model"));
1222
- lines.push(boxRow(color7.dim(header)));
1223
- const caret = query.length > 0 ? color7.dim("\u258F") : theme.cursor("\u258F");
1224
- lines.push(boxRow(`${theme.accent("\u276F")} ${color7.dim("Select model:")} ${query}${caret}`));
825
+ lines.push(boxRow(color6.dim(header)));
826
+ const caret = query.length > 0 ? color6.dim("\u258F") : theme.cursor("\u258F");
827
+ lines.push(boxRow(`${theme.accent("\u276F")} ${color6.dim("Select model:")} ${query}${caret}`));
1225
828
  lines.push(boxDivider());
1226
829
  let flat = 0;
1227
830
  for (const m of visible) {
@@ -1235,14 +838,14 @@ function renderLiveModelList(query, filtered, selectedIdx, header) {
1235
838
  if (m.tool_call) capTags.push("tools");
1236
839
  if (m.reasoning) capTags.push("reason");
1237
840
  if (m.modalities?.input?.includes("image")) capTags.push("vision");
1238
- const caps = capTags.map((c) => theme.caps(c)).join(color7.dim(" "));
1239
- const id = selected ? theme.accent(color7.bold(m.id)) : m.id;
841
+ const caps = capTags.map((c) => theme.caps(c)).join(color6.dim(" "));
842
+ const id = selected ? theme.accent(color6.bold(m.id)) : m.id;
1240
843
  lines.push(boxRow(`${marker} ${padVisible(id, 34)} ${ctx} ${padVisible(cost, 12)} ${caps}`));
1241
844
  flat++;
1242
845
  }
1243
846
  if (ordered.length > LIVE_PICKER_MAX_VISIBLE) {
1244
847
  lines.push(
1245
- boxRow(color7.dim(` \u2026 ${ordered.length - LIVE_PICKER_MAX_VISIBLE} more \u2014 type to filter`))
848
+ boxRow(color6.dim(` \u2026 ${ordered.length - LIVE_PICKER_MAX_VISIBLE} more \u2014 type to filter`))
1246
849
  );
1247
850
  }
1248
851
  lines.push(boxDivider());
@@ -1394,37 +997,37 @@ function renderLiveProviderList(query, filtered, selectedIdx, savedSet) {
1394
997
  const visible = ordered.slice(scrollOffset, scrollOffset + LIVE_PICKER_MAX_VISIBLE);
1395
998
  const lines = [];
1396
999
  lines.push(boxTop("Select a provider"));
1397
- const caret = query.length > 0 ? color8.dim("\u258F") : theme.cursor("\u258F");
1398
- lines.push(boxRow(`${theme.accent("\u276F")} ${color8.dim("Select provider:")} ${query}${caret}`));
1000
+ const caret = query.length > 0 ? color7.dim("\u258F") : theme.cursor("\u258F");
1001
+ lines.push(boxRow(`${theme.accent("\u276F")} ${color7.dim("Select provider:")} ${query}${caret}`));
1399
1002
  lines.push(boxDivider());
1400
1003
  if (scrollOffset > 0) {
1401
- lines.push(boxRow(color8.dim(` ${scrollOffset} more above \u2191`)));
1004
+ lines.push(boxRow(color7.dim(` ${scrollOffset} more above \u2191`)));
1402
1005
  }
1403
1006
  let flat = 0;
1404
1007
  let lastFamily = "";
1405
1008
  for (const p of visible) {
1406
1009
  if (p.family !== lastFamily) {
1407
- lines.push(boxRow(`${theme.accent("\xB7")} ${color8.dim(p.family.toUpperCase())}`));
1010
+ lines.push(boxRow(`${theme.accent("\xB7")} ${color7.dim(p.family.toUpperCase())}`));
1408
1011
  lastFamily = p.family;
1409
1012
  }
1410
1013
  const selected = flat === selectedIdx - scrollOffset;
1411
1014
  const sel = selected ? theme.cursor("\u25B6") : " ";
1412
1015
  let marker;
1413
1016
  if (savedSet) {
1414
- const mark = savedSet.has(p.id) ? color8.cyan("\u25C9") : color8.dim("\u25CB");
1017
+ const mark = savedSet.has(p.id) ? color7.cyan("\u25C9") : color7.dim("\u25CB");
1415
1018
  marker = `${sel} ${mark}`;
1416
1019
  } else {
1417
1020
  marker = `${sel}`;
1418
1021
  }
1419
- const id = selected ? theme.accent(color8.bold(p.id)) : p.id;
1420
- const name = selected ? p.name : color8.dim(p.name);
1022
+ const id = selected ? theme.accent(color7.bold(p.id)) : p.id;
1023
+ const name = selected ? p.name : color7.dim(p.name);
1421
1024
  lines.push(boxRow(`${marker} ${padVisible(id, 24)} ${name}`));
1422
1025
  flat++;
1423
1026
  }
1424
1027
  if (scrollOffset + LIVE_PICKER_MAX_VISIBLE < ordered.length) {
1425
1028
  lines.push(
1426
1029
  boxRow(
1427
- color8.dim(` ${ordered.length - scrollOffset - LIVE_PICKER_MAX_VISIBLE} more below \u2193`)
1030
+ color7.dim(` ${ordered.length - scrollOffset - LIVE_PICKER_MAX_VISIBLE} more below \u2193`)
1428
1031
  )
1429
1032
  );
1430
1033
  }
@@ -1575,10 +1178,10 @@ async function runPicker(deps) {
1575
1178
  const { modelsRegistry, renderer, reader, config, defaultProvider, defaultModel } = deps;
1576
1179
  renderer.write(
1577
1180
  `
1578
- ${theme.accent(color8.bold("\u258C WrongStack"))} ${color8.dim("Provider & Model Selection")}
1181
+ ${theme.accent(color7.bold("\u258C WrongStack"))} ${color7.dim("Provider & Model Selection")}
1579
1182
  `
1580
1183
  );
1581
- renderer.write(color8.dim(`${theme.chrome("\xB7")} Loading provider catalog\u2026
1184
+ renderer.write(color7.dim(`${theme.chrome("\xB7")} Loading provider catalog\u2026
1582
1185
  `));
1583
1186
  let providers;
1584
1187
  try {
@@ -1625,7 +1228,7 @@ ${theme.accent(color8.bold("\u258C WrongStack"))} ${color8.dim("Provider & Mode
1625
1228
  const inherited = catalogType ? catalogById.get(catalogType) : void 0;
1626
1229
  merged.push({
1627
1230
  id,
1628
- name: inherited ? `${inherited.name} ${color8.dim("(alias)")}` : id,
1231
+ name: inherited ? `${inherited.name} ${color7.dim("(alias)")}` : id,
1629
1232
  family: cfg.family,
1630
1233
  apiBase: cfg.baseUrl ?? inherited?.apiBase,
1631
1234
  envVars: cfg.envVars ?? inherited?.envVars ?? [],
@@ -1653,7 +1256,7 @@ ${theme.accent(color8.bold("\u258C WrongStack"))} ${color8.dim("Provider & Mode
1653
1256
  if (process.stdin.isTTY) {
1654
1257
  const chosen2 = await runLiveProviderPicker(displayList);
1655
1258
  if (!chosen2) {
1656
- renderer.write(color8.dim("Cancelled.\n"));
1259
+ renderer.write(color7.dim("Cancelled.\n"));
1657
1260
  return void 0;
1658
1261
  }
1659
1262
  return pickModel(chosen2, modelsRegistry, renderer, reader, defaultModel);
@@ -1688,19 +1291,19 @@ ${theme.accent(color8.bold("\u258C WrongStack"))} ${color8.dim("Provider & Mode
1688
1291
  (a, b) => a.id.toLowerCase().localeCompare(b.id.toLowerCase())
1689
1292
  );
1690
1293
  if (!list || list.length === 0) continue;
1691
- renderer.write(`${boxRow(`${theme.accent("\xB7")} ${color8.dim(fam.toUpperCase())}`)}
1294
+ renderer.write(`${boxRow(`${theme.accent("\xB7")} ${color7.dim(fam.toUpperCase())}`)}
1692
1295
  `);
1693
1296
  for (const p of list) {
1694
1297
  const envFound = p.envVars.some((v) => !!process.env[v]);
1695
1298
  const entry = config?.providers?.[p.id];
1696
1299
  const configKey = typeof entry?.apiKey === "string" && entry.apiKey.length > 0 || Array.isArray(entry?.apiKeys) && entry?.apiKeys?.some((k) => k?.apiKey);
1697
- const marker = envFound ? color8.green("\u25CF") : configKey ? color8.cyan("\u25C9") : color8.dim("\u25CB");
1300
+ const marker = envFound ? color7.green("\u25CF") : configKey ? color7.cyan("\u25C9") : color7.dim("\u25CB");
1698
1301
  const isDefault = p.id === defaultProvider;
1699
1302
  if (isDefault) defaultIdx = idx;
1700
- const idLabel = isDefault ? theme.accent(color8.bold(p.id)) : p.id;
1701
- const suffix = isDefault ? color8.dim(" (default)") : "";
1303
+ const idLabel = isDefault ? theme.accent(color7.bold(p.id)) : p.id;
1304
+ const suffix = isDefault ? color7.dim(" (default)") : "";
1702
1305
  renderer.write(
1703
- `${boxRow(`${color8.dim(`${idx}.`.padStart(4))} ${marker} ${padVisible(idLabel, 22)} ${color8.dim(p.name)}${suffix}`)}
1306
+ `${boxRow(`${color7.dim(`${idx}.`.padStart(4))} ${marker} ${padVisible(idLabel, 22)} ${color7.dim(p.name)}${suffix}`)}
1704
1307
  `
1705
1308
  );
1706
1309
  ordered.push({ provider: p, index: idx });
@@ -1711,22 +1314,22 @@ ${theme.accent(color8.bold("\u258C WrongStack"))} ${color8.dim("Provider & Mode
1711
1314
  `);
1712
1315
  if (showingFallback) {
1713
1316
  renderer.write(
1714
- `${boxRow(`${color8.yellow("\u26A0 No API keys detected.")} ${color8.dim("Run `wstack auth <provider>` after picking.")}`)}
1317
+ `${boxRow(`${color7.yellow("\u26A0 No API keys detected.")} ${color7.dim("Run `wstack auth <provider>` after picking.")}`)}
1715
1318
  `
1716
1319
  );
1717
1320
  } else {
1718
- renderer.write(`${boxRow(color8.dim("\u25CF env key \u25C9 stored in config \u25CB no key"))}
1321
+ renderer.write(`${boxRow(color7.dim("\u25CF env key \u25C9 stored in config \u25CB no key"))}
1719
1322
  `);
1720
1323
  }
1721
1324
  renderer.write(`${boxBottom()}
1722
1325
  `);
1723
- const defaultHint = defaultIdx !== void 0 && defaultProvider ? ` ${color8.dim(`[Enter = ${defaultProvider}]`)}` : "";
1326
+ const defaultHint = defaultIdx !== void 0 && defaultProvider ? ` ${color7.dim(`[Enter = ${defaultProvider}]`)}` : "";
1724
1327
  const providerAnswer = (await reader.readLine(
1725
1328
  `
1726
- ${color8.amber("?")} Select provider (1-${ordered.length})${defaultHint} ${color8.dim("[q to quit]")}: `
1329
+ ${color7.amber("?")} Select provider (1-${ordered.length})${defaultHint} ${color7.dim("[q to quit]")}: `
1727
1330
  )).trim();
1728
1331
  if (providerAnswer.toLowerCase() === "q") {
1729
- renderer.write(color8.dim("Cancelled.\n"));
1332
+ renderer.write(color7.dim("Cancelled.\n"));
1730
1333
  return void 0;
1731
1334
  }
1732
1335
  if (!providerAnswer) {
@@ -1734,7 +1337,7 @@ ${color8.amber("?")} Select provider (1-${ordered.length})${defaultHint} ${color
1734
1337
  const def = ordered[defaultIdx - 1];
1735
1338
  if (def) return pickModel(def.provider, modelsRegistry, renderer, reader, defaultModel);
1736
1339
  }
1737
- renderer.write(color8.dim("Cancelled.\n"));
1340
+ renderer.write(color7.dim("Cancelled.\n"));
1738
1341
  return void 0;
1739
1342
  }
1740
1343
  const providerIdx = Number.parseInt(providerAnswer, 10);
@@ -1756,7 +1359,7 @@ async function pickModel(provider, registry, renderer, reader, defaultModel) {
1756
1359
  ${boxTop("Select a model")}
1757
1360
  `);
1758
1361
  renderer.write(
1759
- `${boxRow(`${color8.bold(provider.name)} ${color8.dim(`(${provider.id})`)} models:`)}
1362
+ `${boxRow(`${color7.bold(provider.name)} ${color7.dim(`(${provider.id})`)} models:`)}
1760
1363
  `
1761
1364
  );
1762
1365
  renderer.write(`${boxBottom()}
@@ -1768,18 +1371,18 @@ ${boxTop("Select a model")}
1768
1371
  );
1769
1372
  if (models.length === 0) {
1770
1373
  renderer.write(
1771
- color8.dim(
1374
+ color7.dim(
1772
1375
  " No models listed yet for this provider \u2014 type a model id to use it (local servers auto-discover models at launch).\n"
1773
1376
  )
1774
1377
  );
1775
1378
  const typed = (await reader.readLine(`
1776
- ${color8.amber("?")} Model id ${color8.dim("(or Enter to cancel)")}: `)).trim();
1379
+ ${color7.amber("?")} Model id ${color7.dim("(or Enter to cancel)")}: `)).trim();
1777
1380
  if (!typed) {
1778
- renderer.write(color8.dim("Cancelled.\n"));
1381
+ renderer.write(color7.dim("Cancelled.\n"));
1779
1382
  return void 0;
1780
1383
  }
1781
1384
  renderer.write(`
1782
- ${color8.green("\u2713")} ${color8.bold(provider.id)} / ${color8.bold(typed)}
1385
+ ${color7.green("\u2713")} ${color7.bold(provider.id)} / ${color7.bold(typed)}
1783
1386
 
1784
1387
  `);
1785
1388
  return { provider: provider.id, model: typed };
@@ -1787,12 +1390,12 @@ ${color8.amber("?")} Model id ${color8.dim("(or Enter to cancel)")}: `)).trim();
1787
1390
  if (process.stdin.isTTY) {
1788
1391
  const chosen = await runLiveModelPicker(provider, defaultModel);
1789
1392
  if (!chosen) {
1790
- renderer.write(color8.dim("Cancelled.\n"));
1393
+ renderer.write(color7.dim("Cancelled.\n"));
1791
1394
  return void 0;
1792
1395
  }
1793
1396
  renderer.write(
1794
1397
  `
1795
- ${color8.green("\u2713")} ${color8.bold(provider.id)} / ${color8.bold(chosen.id)}
1398
+ ${color7.green("\u2713")} ${color7.bold(provider.id)} / ${color7.bold(chosen.id)}
1796
1399
 
1797
1400
  `
1798
1401
  );
@@ -1818,11 +1421,11 @@ ${color8.amber("?")} Model id ${color8.dim("(or Enter to cancel)")}: `)).trim();
1818
1421
  if (m.tool_call) capTags.push("tools");
1819
1422
  if (m.reasoning) capTags.push("reason");
1820
1423
  if (m.modalities?.input?.includes("image")) capTags.push("vision");
1821
- const capStr = capTags.map((c) => theme.caps(c)).join(color8.dim(" "));
1424
+ const capStr = capTags.map((c) => theme.caps(c)).join(color7.dim(" "));
1822
1425
  const isDefault = m.id === defaultModel;
1823
- const idLabel = isDefault ? theme.accent(color8.bold(m.id)) : m.id;
1824
- const suffix = isDefault ? color8.dim(" (default)") : "";
1825
- const num5 = color8.dim(`${num}.`.padStart(5));
1426
+ const idLabel = isDefault ? theme.accent(color7.bold(m.id)) : m.id;
1427
+ const suffix = isDefault ? color7.dim(" (default)") : "";
1428
+ const num5 = color7.dim(`${num}.`.padStart(5));
1826
1429
  renderer.write(
1827
1430
  `${boxRow(`${num5} ${padVisible(idLabel, 34)} ${ctx} ${padVisible(costCol, 11)} ${capStr}${suffix}`)}
1828
1431
  `
@@ -1834,36 +1437,36 @@ ${color8.amber("?")} Model id ${color8.dim("(or Enter to cancel)")}: `)).trim();
1834
1437
  if (offset < models.length) {
1835
1438
  const more = (await reader.readLine(
1836
1439
  `
1837
- ${color8.amber("?")} Showing ${Math.min(offset, models.length)}/${models.length} \u2014 Enter number, ${color8.dim("Enter")} for more, or ${color8.dim("q")} to quit: `
1440
+ ${color7.amber("?")} Showing ${Math.min(offset, models.length)}/${models.length} \u2014 Enter number, ${color7.dim("Enter")} for more, or ${color7.dim("q")} to quit: `
1838
1441
  )).trim();
1839
1442
  if (more.toLowerCase() === "q") {
1840
- renderer.write(color8.dim("Cancelled.\n"));
1443
+ renderer.write(color7.dim("Cancelled.\n"));
1841
1444
  return void 0;
1842
1445
  }
1843
1446
  if (!more) continue;
1844
1447
  return resolveModelSelection(more, models, provider, registry, renderer, reader);
1845
1448
  }
1846
1449
  }
1847
- const defaultHint = defaultIdxInModels >= 0 && defaultModel ? ` ${color8.dim(`[Enter = ${defaultModel}]`)}` : "";
1450
+ const defaultHint = defaultIdxInModels >= 0 && defaultModel ? ` ${color7.dim(`[Enter = ${defaultModel}]`)}` : "";
1848
1451
  const answer = (await reader.readLine(
1849
1452
  `
1850
- ${color8.amber("?")} Select model (1-${models.length})${defaultHint} ${color8.dim("[q to quit]")}: `
1453
+ ${color7.amber("?")} Select model (1-${models.length})${defaultHint} ${color7.dim("[q to quit]")}: `
1851
1454
  )).trim();
1852
1455
  if (answer.toLowerCase() === "q") {
1853
- renderer.write(color8.dim("Cancelled.\n"));
1456
+ renderer.write(color7.dim("Cancelled.\n"));
1854
1457
  return void 0;
1855
1458
  }
1856
1459
  if (!answer) {
1857
1460
  if (defaultIdxInModels >= 0 && defaultModel) {
1858
1461
  renderer.write(
1859
1462
  `
1860
- ${color8.green("\u2713")} ${color8.bold(provider.id)} / ${color8.bold(defaultModel)}
1463
+ ${color7.green("\u2713")} ${color7.bold(provider.id)} / ${color7.bold(defaultModel)}
1861
1464
 
1862
1465
  `
1863
1466
  );
1864
1467
  return { provider: provider.id, model: defaultModel };
1865
1468
  }
1866
- renderer.write(color8.dim("Cancelled.\n"));
1469
+ renderer.write(color7.dim("Cancelled.\n"));
1867
1470
  return void 0;
1868
1471
  }
1869
1472
  return resolveModelSelection(answer, models, provider, registry, renderer, reader);
@@ -1892,14 +1495,14 @@ async function resolveModelSelection(answer, models, provider, _registry, render
1892
1495
  modelId = answer;
1893
1496
  }
1894
1497
  renderer.write(`
1895
- ${color8.green("\u2713")} ${color8.bold(provider.id)} / ${color8.bold(modelId)}
1498
+ ${color7.green("\u2713")} ${color7.bold(provider.id)} / ${color7.bold(modelId)}
1896
1499
 
1897
1500
  `);
1898
1501
  return { provider: provider.id, model: modelId };
1899
1502
  }
1900
1503
 
1901
1504
  // src/auth-menu/add-provider.ts
1902
- import { color as color9 } from "@wrongstack/core/utils";
1505
+ import { color as color8 } from "@wrongstack/core/utils";
1903
1506
  import { catalogProviderIdFor, PROVIDER_DEFINITIONS } from "@wrongstack/providers";
1904
1507
 
1905
1508
  // src/provider-id.ts
@@ -1938,12 +1541,12 @@ async function addFromCatalog(deps) {
1938
1541
  catalog = [...catalog, ...ownDefinitionsAsCatalog(new Set(catalog.map((p) => p.id)))];
1939
1542
  const saved = new Set(Object.keys(await loadProviders(deps)));
1940
1543
  deps.renderer.write(
1941
- color9.dim(
1544
+ color8.dim(
1942
1545
  ` Catalog: ${catalog.length} providers. Type to filter, \u2191/\u2193 navigate, Enter to select.
1943
1546
  `
1944
1547
  )
1945
1548
  );
1946
- deps.renderer.write(` ${color9.dim("\u25C9 already saved \u25CB no key yet")}
1549
+ deps.renderer.write(` ${color8.dim("\u25C9 already saved \u25CB no key yet")}
1947
1550
 
1948
1551
  `);
1949
1552
  renderOAuthLoginOptions(deps);
@@ -1952,11 +1555,11 @@ async function addFromCatalog(deps) {
1952
1555
  if (!chosen) {
1953
1556
  deps.renderer.write(
1954
1557
  `
1955
- ${color9.dim("OAuth login options:")} ${color9.dim("chatgpt")}, ${color9.dim("claude")}, or ${color9.dim("copilot")}?
1558
+ ${color8.dim("OAuth login options: enter a registered strategy name, or q to quit.")}
1956
1559
  `
1957
1560
  );
1958
1561
  const answer = (await deps.reader.readLine(
1959
- ` ${color9.amber("?")} OAuth or q to quit ${color9.dim("[chatgpt/claude/copilot]")}: `
1562
+ ` ${color8.amber("?")} OAuth strategy ${color8.dim("(or q)")}: `
1960
1563
  )).trim();
1961
1564
  if (answer && await runOAuthLoginChoice(deps, answer, { allowNumeric: false })) {
1962
1565
  return true;
@@ -1967,12 +1570,12 @@ async function addFromCatalog(deps) {
1967
1570
  }
1968
1571
  async function addKeyForCatalogProvider(deps, chosen) {
1969
1572
  deps.renderer.write(
1970
- color9.dim(`
1573
+ color8.dim(`
1971
1574
  Defaults from models.dev \u2014 press Enter to keep, or type overrides.
1972
1575
  `)
1973
1576
  );
1974
1577
  const famRaw = (await deps.reader.readLine(
1975
- ` ${color9.amber("?")} Family ${color9.dim(`[${chosen.family}]`)} ${color9.dim("(q to quit)")}: `
1578
+ ` ${color8.amber("?")} Family ${color8.dim(`[${chosen.family}]`)} ${color8.dim("(q to quit)")}: `
1976
1579
  )).trim();
1977
1580
  if (famRaw === "q") return false;
1978
1581
  let family = chosen.family;
@@ -1987,7 +1590,7 @@ async function addKeyForCatalogProvider(deps, chosen) {
1987
1590
  family = validated;
1988
1591
  }
1989
1592
  const baseRaw = (await deps.reader.readLine(
1990
- ` ${color9.amber("?")} Base URL ${color9.dim(`[${chosen.apiBase ?? "unset"}]`)} ${color9.dim("(q to quit)")}: `
1593
+ ` ${color8.amber("?")} Base URL ${color8.dim(`[${chosen.apiBase ?? "unset"}]`)} ${color8.dim("(q to quit)")}: `
1991
1594
  )).trim();
1992
1595
  if (baseRaw === "q") return false;
1993
1596
  const baseUrl = baseRaw || chosen.apiBase;
@@ -2003,7 +1606,7 @@ async function addKeyForCatalogProvider(deps, chosen) {
2003
1606
  suggestedAlias = candidate;
2004
1607
  }
2005
1608
  const aliasRaw = (await deps.reader.readLine(
2006
- ` ${color9.amber("?")} Save as alias ${color9.dim(`[${suggestedAlias}]`)} ${color9.dim("(used with --provider <alias>)")}: `
1609
+ ` ${color8.amber("?")} Save as alias ${color8.dim(`[${suggestedAlias}]`)} ${color8.dim("(used with --provider <alias>)")}: `
2007
1610
  )).trim();
2008
1611
  const alias = aliasRaw || suggestedAlias;
2009
1612
  const existing = providersNow[alias];
@@ -2030,25 +1633,25 @@ async function addKeyForCatalogProvider(deps, chosen) {
2030
1633
  async function addCustomProvider(deps) {
2031
1634
  deps.renderer.write(
2032
1635
  `
2033
- ${color9.bold("Custom provider")} ${color9.dim("\u2014 for local models or proxies not in the catalog.")}
1636
+ ${color8.bold("Custom provider")} ${color8.dim("\u2014 for local models or proxies not in the catalog.")}
2034
1637
  `
2035
1638
  );
2036
1639
  const providersNow = await loadProviders(deps);
2037
1640
  const idRaw = (await deps.reader.readLine(
2038
- ` ${color9.amber("?")} Provider id ${color9.dim('(e.g. "local-llama", "my-proxy"; blank = auto custom-N, q to quit)')}: `
1641
+ ` ${color8.amber("?")} Provider id ${color8.dim('(e.g. "local-llama", "my-proxy"; blank = auto custom-N, q to quit)')}: `
2039
1642
  )).trim();
2040
1643
  if (idRaw === "q") return false;
2041
1644
  let type = idRaw;
2042
1645
  if (!type) {
2043
1646
  type = nextCustomProviderId(Object.keys(providersNow));
2044
- deps.renderer.write(` ${color9.dim("Using generated id:")} ${color9.bold(type)}
1647
+ deps.renderer.write(` ${color8.dim("Using generated id:")} ${color8.bold(type)}
2045
1648
  `);
2046
1649
  } else if (providersNow[type]) {
2047
1650
  deps.renderer.writeWarning(`"${type}" already exists. Pick it from the main menu to edit.`);
2048
1651
  return false;
2049
1652
  }
2050
1653
  const familyRaw = (await deps.reader.readLine(
2051
- ` ${color9.amber("?")} Wire family ${color9.dim("(anthropic | openai | openai-compatible | google)")} ${color9.dim("(q to quit)")}: `
1654
+ ` ${color8.amber("?")} Wire family ${color8.dim("(anthropic | openai | openai-compatible | google)")} ${color8.dim("(q to quit)")}: `
2052
1655
  )).trim();
2053
1656
  if (familyRaw === "q") return false;
2054
1657
  const family = validateFamily(familyRaw);
@@ -2057,14 +1660,14 @@ ${color9.bold("Custom provider")} ${color9.dim("\u2014 for local models or proxi
2057
1660
  return false;
2058
1661
  }
2059
1662
  const baseUrl = (await deps.reader.readLine(
2060
- ` ${color9.amber("?")} Base URL ${color9.dim("(e.g. http://localhost:11434/v1, optional)")}: `
1663
+ ` ${color8.amber("?")} Base URL ${color8.dim("(e.g. http://localhost:11434/v1, optional)")}: `
2061
1664
  )).trim();
2062
1665
  const modelsRaw = (await deps.reader.readLine(
2063
- ` ${color9.amber("?")} Model ids ${color9.dim("(comma-separated, optional)")}: `
1666
+ ` ${color8.amber("?")} Model ids ${color8.dim("(comma-separated, optional)")}: `
2064
1667
  )).trim();
2065
1668
  const models = modelsRaw ? modelsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
2066
1669
  const envVarsRaw = (await deps.reader.readLine(
2067
- ` ${color9.amber("?")} Env var names ${color9.dim("(comma-separated, optional)")}: `
1670
+ ` ${color8.amber("?")} Env var names ${color8.dim("(comma-separated, optional)")}: `
2068
1671
  )).trim();
2069
1672
  const envVars = envVarsRaw ? envVarsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
2070
1673
  return addKeyForProvider(type, deps, {
@@ -2076,17 +1679,17 @@ ${color9.bold("Custom provider")} ${color9.dim("\u2014 for local models or proxi
2076
1679
  });
2077
1680
  }
2078
1681
  async function addManualEntry(deps) {
2079
- const pid = (await deps.reader.readLine(` ${color9.amber("?")} Provider id ${color9.dim("[q to quit]")}: `)).trim();
1682
+ const pid = (await deps.reader.readLine(` ${color8.amber("?")} Provider id ${color8.dim("[q to quit]")}: `)).trim();
2080
1683
  if (!pid || pid === "q") return false;
2081
1684
  const famRaw = (await deps.reader.readLine(
2082
- ` ${color9.amber("?")} Family ${color9.dim("(anthropic/openai/openai-compatible/google)")}: `
1685
+ ` ${color8.amber("?")} Family ${color8.dim("(anthropic/openai/openai-compatible/google)")}: `
2083
1686
  )).trim();
2084
1687
  const family = validateFamily(famRaw);
2085
1688
  if (!family) {
2086
1689
  deps.renderer.writeError(`Invalid family: "${famRaw}"`);
2087
1690
  return false;
2088
1691
  }
2089
- const baseUrl = (await deps.reader.readLine(` ${color9.amber("?")} Base URL ${color9.dim("(optional)")}: `)).trim();
1692
+ const baseUrl = (await deps.reader.readLine(` ${color8.amber("?")} Base URL ${color8.dim("(optional)")}: `)).trim();
2090
1693
  return addKeyForProvider(pid, deps, {
2091
1694
  type: pid,
2092
1695
  family,
@@ -2129,10 +1732,10 @@ async function addKeyForProvider(providerId, deps, template) {
2129
1732
  deps.profileConfigPath
2130
1733
  );
2131
1734
  deps.renderer.write(
2132
- ` ${color9.green("\u2713")} Saved ${color9.bold(providerId)}/${color9.bold(label)}.
1735
+ ` ${color8.green("\u2713")} Saved ${color8.bold(providerId)}/${color8.bold(label)}.
2133
1736
  `
2134
1737
  );
2135
- deps.renderer.write(color9.dim(` Launch: wstack --provider ${providerId} "<task>"
1738
+ deps.renderer.write(color8.dim(` Launch: wstack --provider ${providerId} "<task>"
2136
1739
  `));
2137
1740
  try {
2138
1741
  await adoptAsDefaultIfUnset(deps, providerId, template);
@@ -2163,7 +1766,7 @@ async function adoptAsDefaultIfUnset(deps, providerId, template) {
2163
1766
  );
2164
1767
  if (adopted) {
2165
1768
  deps.renderer.write(
2166
- ` ${color9.green("\u2713")} Set ${color9.bold(providerId)}/${color9.bold(modelId)} as the default.
1769
+ ` ${color8.green("\u2713")} Set ${color8.bold(providerId)}/${color8.bold(modelId)} as the default.
2167
1770
  `
2168
1771
  );
2169
1772
  }
@@ -2171,7 +1774,7 @@ async function adoptAsDefaultIfUnset(deps, providerId, template) {
2171
1774
  async function promptForLabel(deps, usedLabels) {
2172
1775
  const defaultLabel = suggestLabel(usedLabels);
2173
1776
  const labelRaw = (await deps.reader.readLine(
2174
- ` ${color9.amber("?")} Label for this key ${color9.dim(`[${defaultLabel}]`)}: `
1777
+ ` ${color8.amber("?")} Label for this key ${color8.dim(`[${defaultLabel}]`)}: `
2175
1778
  )).trim();
2176
1779
  const label = labelRaw || defaultLabel;
2177
1780
  if (usedLabels.has(label)) {
@@ -2182,7 +1785,6 @@ async function promptForLabel(deps, usedLabels) {
2182
1785
  }
2183
1786
 
2184
1787
  export {
2185
- runClaudeOAuthLogin,
2186
1788
  loadProviders,
2187
1789
  renderProviderHeader,
2188
1790
  renderActions,
@@ -2190,7 +1792,8 @@ export {
2190
1792
  readKeyInput,
2191
1793
  confirm,
2192
1794
  validateFamily,
2193
- runCopilotOAuthLogin,
1795
+ providerAuthStrategiesFor,
1796
+ runProviderAuthLogin,
2194
1797
  LOCAL_LLM_PRESETS,
2195
1798
  runAuthLocal,
2196
1799
  resolveOAuthKind,
@@ -2210,4 +1813,4 @@ export {
2210
1813
  addCustomProvider,
2211
1814
  addKeyForProvider
2212
1815
  };
2213
- //# sourceMappingURL=chunk-MNQQ4ZNF.js.map
1816
+ //# sourceMappingURL=chunk-ONOT5OJ2.js.map