@wrongstack/cli 0.301.0 → 0.302.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/data/providers.json +41 -155
  2. package/dist/{acp-DATHHPNT.js → acp-XCE4JJF3.js} +4 -4
  3. package/dist/{audit-QRCLEHHW.js → audit-BBR22QH3.js} +2 -2
  4. package/dist/{auth-JASC4K3F.js → auth-K7CYVVKH.js} +121 -78
  5. package/dist/auth-menu/index.d.ts +5 -5
  6. package/dist/auth-menu/local.d.ts +4 -5
  7. package/dist/auth-menu/openai-codex-oauth.d.ts +1 -1
  8. package/dist/{auto-discover-providers-DRW4OZHG.js → auto-discover-providers-MHFLDSD3.js} +2 -2
  9. package/dist/{bench-7ZDWSNEF.js → bench-XNDUJOSZ.js} +10 -11
  10. package/dist/boot/auto-discover-providers.d.ts +1 -1
  11. package/dist/boot/container-wiring.d.ts +3 -3
  12. package/dist/boot/dispatch-webui.d.ts +3 -16
  13. package/dist/boot/system-prompt.d.ts +1 -2
  14. package/dist/boot/tool-registry.d.ts +1 -1
  15. package/dist/boot/tui-coordinator-setup.d.ts +1 -1
  16. package/dist/boot/tui-project-picker-callback.d.ts +1 -1
  17. package/dist/boot/webui-session-child.d.ts +91 -0
  18. package/dist/boot-config.d.ts +2 -2
  19. package/dist/boot.d.ts +1 -1
  20. package/dist/{chunk-ETOEGL3V.js → chunk-3IC4IEZC.js} +7 -2
  21. package/dist/chunk-3JPTNADJ.js +128 -0
  22. package/dist/{chunk-4GVH7V3S.js → chunk-52C7PD2H.js} +1 -1
  23. package/dist/{chunk-OP47B3IX.js → chunk-7YLN7YUA.js} +7 -2
  24. package/dist/{chunk-263G3FMB.js → chunk-B4JLSXZB.js} +11 -14
  25. package/dist/{chunk-I5MEUFDI.js → chunk-BXQG3H2Y.js} +8 -3
  26. package/dist/{chunk-MHF2IJDE.js → chunk-C5NXM2SI.js} +21 -6
  27. package/dist/{chunk-XTIXVJXP.js → chunk-DGXNL7OT.js} +1 -1
  28. package/dist/{chunk-O7XNGCRT.js → chunk-FJVCVZIV.js} +974 -935
  29. package/dist/{chunk-MRLYDHQ7.js → chunk-JQURRYU4.js} +4 -6
  30. package/dist/{chunk-ZECLGUTF.js → chunk-KV6DNQT6.js} +2 -2
  31. package/dist/{chunk-F4PEGAJZ.js → chunk-NUIHRGPY.js} +1 -1
  32. package/dist/{chunk-GUHQQG63.js → chunk-RK7E3IXT.js} +2 -4
  33. package/dist/{chunk-PYTFS4MC.js → chunk-SSSJQVUY.js} +27 -16
  34. package/dist/{chunk-PJHET45A.js → chunk-SZ42FYPT.js} +3 -3
  35. package/dist/{chunk-2KDOFTTM.js → chunk-WOXUE2CN.js} +1067 -900
  36. package/dist/{chunk-A6Q5KGLV.js → chunk-YKBRFSGL.js} +5 -5
  37. package/dist/cli-context.d.ts +3 -1
  38. package/dist/cli-eternal-flag.d.ts +1 -2
  39. package/dist/{cli-main-XWC57MGE.js → cli-main-PJMDZHDK.js} +2683 -2134
  40. package/dist/context-limit.d.ts +11 -5
  41. package/dist/{diag-doctor-OQ3AA63O.js → diag-doctor-ZF5BF2MK.js} +2 -3
  42. package/dist/execute-deps.d.ts +3 -0
  43. package/dist/{execution-LHMSMDHB.js → execution-LZRYRGDD.js} +343 -177
  44. package/dist/{export-DOBGPY23.js → export-CCUXRJOU.js} +3 -11
  45. package/dist/fleet/host-supervisor.d.ts +1 -1
  46. package/dist/fleet/host.d.ts +1 -1
  47. package/dist/fleet/routing.d.ts +1 -2
  48. package/dist/goal-host.d.ts +2 -2
  49. package/dist/hooks-wiring.d.ts +1 -1
  50. package/dist/{hq-LN5CJHCM.js → hq-AHPAE7EN.js} +104 -55
  51. package/dist/hq-command-controller.d.ts +1 -1
  52. package/dist/hq-server/routes/system-handlers.d.ts +1 -2
  53. package/dist/hq-server/routes.d.ts +7 -7
  54. package/dist/{hq-server-S7HZG3PA.js → hq-server-KU2Z54QU.js} +2 -2
  55. package/dist/index.d.ts +1 -1
  56. package/dist/index.js +2712 -2642
  57. package/dist/mailbox-bridge-bootstrap.d.ts +2 -2
  58. package/dist/{mcp-OHPJH73U.js → mcp-E5EBGOHF.js} +6 -6
  59. package/dist/mcp-serve.d.ts +1 -1
  60. package/dist/{modeldiag-3DOK4BWW.js → modeldiag-SIVUOX2Y.js} +18 -13
  61. package/dist/{permissions-FNYLZIL4.js → permissions-2PWBL4YU.js} +5 -3
  62. package/dist/picker.d.ts +1 -1
  63. package/dist/plugin-management.d.ts +1 -1
  64. package/dist/{plugin-usage-YEVLOR33.js → plugin-usage-PTEETL3J.js} +2 -2
  65. package/dist/{plugins-K3BAUP4D.js → plugins-XRGWC3NJ.js} +3 -3
  66. package/dist/pre-launch.d.ts +2 -2
  67. package/dist/{providers-models-WEZ4EGLL.js → providers-models-VV74TRQ2.js} +86 -37
  68. package/dist/repl.d.ts +1 -1
  69. package/dist/{replay-FCBNHW3K.js → replay-WBRMEWQ5.js} +2 -2
  70. package/dist/{rewind-AOD3JIYX.js → rewind-7RE6ONIC.js} +17 -17
  71. package/dist/{sdd-runtime-AIJCJGWC.js → sdd-runtime-GFZ3NJKS.js} +2 -2
  72. package/dist/{sessions-config-XXN7267N.js → sessions-config-YX2ZPAFM.js} +36 -32
  73. package/dist/{simpleui-dist-JIFGUJO7.js → simpleui-dist-33VO3QHF.js} +9 -2
  74. package/dist/slash-commands/agents.d.ts +14 -0
  75. package/dist/slash-commands/command-context.d.ts +2 -3
  76. package/dist/slash-commands/helpers.d.ts +1 -1
  77. package/dist/slash-commands/index.d.ts +1 -1
  78. package/dist/slash-commands/intake.d.ts +10 -0
  79. package/dist/slash-commands/memory-triage.d.ts +1 -1
  80. package/dist/slash-commands/provider-status.d.ts +1 -1
  81. package/dist/subcommands/handlers/hq.d.ts +20 -0
  82. package/dist/subcommands/handlers/per-subcommand-help-data.d.ts +1 -1
  83. package/dist/{update-MV6TUACD.js → update-LTDJGAFU.js} +3 -3
  84. package/dist/{update-check-UMYTOH7Z.js → update-check-WARHSVZA.js} +2 -2
  85. package/dist/webui-server/connection-handler.d.ts +1 -1
  86. package/dist/webui-server/context-breakdown.d.ts +1 -1
  87. package/dist/webui-server/cost-helpers.d.ts +1 -1
  88. package/dist/webui-server/lifecycle.d.ts +1 -1
  89. package/dist/webui-server/prefs-seeding.d.ts +1 -1
  90. package/dist/webui-server/setup-events.d.ts +1 -1
  91. package/dist/webui-server/static-serve.d.ts +1 -1
  92. package/dist/{webui-server-DD35QFNO.js → webui-server-E3OPEKU5.js} +66 -25
  93. package/dist/webui-server-options.d.ts +7 -0
  94. package/dist/wiring/command-host-state.d.ts +2 -2
  95. package/dist/wiring/controllers.d.ts +1 -2
  96. package/dist/wiring/dep-watcher.d.ts +1 -1
  97. package/dist/wiring/director-setup.d.ts +3 -3
  98. package/dist/wiring/eternal-command-handlers.d.ts +2 -2
  99. package/dist/wiring/management-tools.d.ts +2 -2
  100. package/dist/wiring/provider-runtime.d.ts +1 -1
  101. package/dist/wiring/provider-status.d.ts +1 -1
  102. package/dist/wiring/provider-utility-tools.d.ts +3 -3
  103. package/dist/wiring/provider.d.ts +1 -1
  104. package/dist/wiring/replay.d.ts +1 -1
  105. package/dist/wiring/runtime-controller-deps.d.ts +1 -1
  106. package/dist/wiring/runtime-dispatch-state.d.ts +1 -2
  107. package/dist/wiring/runtime-lifecycle-deps.d.ts +2 -2
  108. package/dist/wiring/runtime-picker-deps.d.ts +1 -2
  109. package/dist/wiring/sage.d.ts +1 -1
  110. package/dist/wiring/session-command-handlers.d.ts +1 -2
  111. package/dist/wiring/session.d.ts +6 -1
  112. package/dist/wiring/slash-commands.d.ts +2 -3
  113. package/package.json +23 -23
@@ -1,12 +1,12 @@
1
- import {
2
- LOCAL_LLM_PRESETS,
3
- runLiveProviderPicker
4
- } from "./chunk-MHF2IJDE.js";
5
1
  import {
6
2
  openBrowser,
7
3
  runCodexOAuthLogin,
8
4
  startLoopbackServer
9
- } from "./chunk-PYTFS4MC.js";
5
+ } from "./chunk-SSSJQVUY.js";
6
+ import {
7
+ LOCAL_LLM_PRESETS,
8
+ runLiveProviderPicker
9
+ } from "./chunk-C5NXM2SI.js";
10
10
  import {
11
11
  activeLabel,
12
12
  loadConfigProviders,
@@ -15,432 +15,717 @@ import {
15
15
  normalizeKeys,
16
16
  nowIso,
17
17
  writeKeysBack
18
- } from "./chunk-PJHET45A.js";
18
+ } from "./chunk-SZ42FYPT.js";
19
19
 
20
- // src/auth-menu/oauth-menu.ts
21
- import { color as color3 } from "@wrongstack/core/utils";
20
+ // src/auth-menu/local.ts
21
+ import { DefaultSecretScrubber as DefaultSecretScrubber2 } from "@wrongstack/core/security";
22
+ import { color as color2 } from "@wrongstack/core/utils";
23
+ import { probeLocalLlm } from "@wrongstack/runtime/probe";
22
24
 
23
- // src/auth-menu/anthropic-oauth.ts
24
- import { createHash, randomBytes } from "node:crypto";
25
- import { color } from "@wrongstack/core/utils";
26
- import { FetchError, ParseError } from "@wrongstack/core/types";
27
- var CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
28
- var AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
29
- var TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
30
- var REDIRECT_PORT = 53692;
31
- var REDIRECT_HOST = "127.0.0.1";
32
- var REDIRECT_PATH = "/callback";
33
- var REDIRECT_URI = `http://localhost:${REDIRECT_PORT}${REDIRECT_PATH}`;
34
- var SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
35
- var CLAUDE_PROVIDER_ID = "anthropic-oauth";
36
- var CLAUDE_BASE_URL = "https://api.anthropic.com";
37
- function base64url(buf) {
38
- return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
39
- }
40
- function generatePkce() {
41
- const verifier = base64url(randomBytes(32));
42
- const challenge = base64url(createHash("sha256").update(verifier).digest());
43
- return { verifier, challenge };
44
- }
45
- function buildAuthorizeUrl(challenge, verifier) {
46
- const params = new URLSearchParams({
47
- code: "true",
48
- client_id: CLIENT_ID,
49
- response_type: "code",
50
- redirect_uri: REDIRECT_URI,
51
- scope: SCOPES,
52
- code_challenge: challenge,
53
- code_challenge_method: "S256",
54
- state: verifier
55
- });
56
- return `${AUTHORIZE_URL}?${params.toString()}`;
57
- }
58
- function parseAuthorizationInput(input) {
59
- const value = input.trim();
60
- if (!value) return {};
61
- try {
62
- const url = new URL(value);
63
- return {
64
- code: url.searchParams.get("code") ?? void 0,
65
- state: url.searchParams.get("state") ?? void 0
66
- };
67
- } catch {
25
+ // src/auth-menu/auth-menu-audit.ts
26
+ import { DefaultSecretScrubber } from "@wrongstack/core/security";
27
+ var NOOP_AUDIT_LOGGER = {
28
+ emit: () => {
68
29
  }
69
- if (value.includes("#")) {
70
- const [code, state] = value.split("#", 2);
71
- return { code, state };
30
+ };
31
+ function decideAuthLocalEvents(opts) {
32
+ const events = [];
33
+ if (opts.probeSkip) {
34
+ events.push({
35
+ type: "auth.local.probe_skip",
36
+ providerId: opts.providerId,
37
+ baseUrl: opts.baseUrl,
38
+ requestedShape: opts.probeSkip.shape
39
+ });
72
40
  }
73
- if (value.includes("code=")) {
74
- const params = new URLSearchParams(value);
75
- return {
76
- code: params.get("code") ?? void 0,
77
- state: params.get("state") ?? void 0
78
- };
41
+ if (opts.probeFailedSave) {
42
+ events.push({
43
+ type: "auth.local.probe_failed_save",
44
+ providerId: opts.providerId,
45
+ baseUrl: opts.baseUrl,
46
+ probeStatus: opts.probeFailedSave.status,
47
+ ...opts.probeFailedSave.detail !== void 0 ? { probeDetail: opts.probeFailedSave.detail } : {}
48
+ });
79
49
  }
80
- return { code: value };
81
- }
82
- async function readTokens(res, op, url) {
83
- if (!res.ok) {
84
- const text = await res.text().catch(() => "");
85
- throw new FetchError({
86
- message: `Claude token ${op} failed (${res.status}): ${text || res.statusText}`,
87
- status: res.status,
88
- context: { provider: "anthropic-oauth", op, url }
50
+ if (opts.newModels === null) return events;
51
+ const hadPriorAllowlist = opts.previousModels !== void 0 && opts.previousModels.length > 0;
52
+ if (hadPriorAllowlist && opts.newModels.length === 0) {
53
+ events.push({
54
+ type: "auth.local.clear",
55
+ providerId: opts.providerId,
56
+ baseUrl: opts.baseUrl,
57
+ previousModels: opts.previousModels
89
58
  });
59
+ return events;
90
60
  }
91
- const json = await res.json();
92
- if (!json?.access_token || !json.refresh_token || typeof json.expires_in !== "number") {
93
- throw new ParseError({
94
- message: `Claude token ${op} response missing fields`,
95
- source: "anthropic-oauth-token-response",
96
- context: { op }
61
+ if (!hadPriorAllowlist && opts.newModels.length > 0) {
62
+ if (opts.previousModels !== void 0) {
63
+ events.push({
64
+ type: "auth.local.undo",
65
+ providerId: opts.providerId,
66
+ baseUrl: opts.baseUrl,
67
+ restoredModels: [...opts.newModels]
68
+ });
69
+ } else {
70
+ events.push({
71
+ type: "auth.local.add",
72
+ providerId: opts.providerId,
73
+ baseUrl: opts.baseUrl,
74
+ models: [...opts.newModels],
75
+ ...opts.keyLabel !== void 0 ? { keyLabel: opts.keyLabel } : {}
76
+ });
77
+ }
78
+ return events;
79
+ }
80
+ if (hadPriorAllowlist && opts.newModels.length > 0) {
81
+ events.push({
82
+ type: "auth.local.add",
83
+ providerId: opts.providerId,
84
+ baseUrl: opts.baseUrl,
85
+ models: [...opts.newModels],
86
+ ...opts.keyLabel !== void 0 ? { keyLabel: opts.keyLabel } : {}
97
87
  });
88
+ return events;
98
89
  }
99
- return {
100
- access: json.access_token,
101
- refresh: json.refresh_token,
102
- expires: Date.now() + json.expires_in * 1e3
103
- };
90
+ return events;
104
91
  }
105
- async function exchangeAuthorizationCode(code, state, verifier, signal) {
106
- const res = await fetch(TOKEN_URL, {
107
- method: "POST",
108
- headers: { "content-type": "application/json", accept: "application/json" },
109
- body: JSON.stringify({
110
- grant_type: "authorization_code",
111
- client_id: CLIENT_ID,
112
- code,
113
- state,
114
- redirect_uri: REDIRECT_URI,
115
- code_verifier: verifier
116
- }),
117
- signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(3e4)]) : AbortSignal.timeout(3e4)
92
+
93
+ // src/auth-menu/helpers.ts
94
+ async function loadProviders(deps) {
95
+ return loadConfigProviders(deps.profileConfigPath, deps.vault, {
96
+ warn: (msg) => deps.renderer.writeWarning(msg),
97
+ profileConfigPath: deps.profileConfigPath
118
98
  });
119
- return readTokens(res, "exchange", TOKEN_URL);
120
99
  }
121
- async function fetchClaudeModels(accessToken, signal) {
122
- try {
123
- const res = await fetch(`${CLAUDE_BASE_URL}/v1/models?limit=100`, {
124
- headers: {
125
- accept: "application/json",
126
- authorization: `Bearer ${accessToken}`,
127
- "anthropic-version": "2023-06-01",
128
- "anthropic-beta": "claude-code-20250219,oauth-2025-04-20"
129
- },
130
- signal: AbortSignal.any([signal, AbortSignal.timeout(8e3)])
131
- });
132
- if (!res.ok) return [];
133
- const json = await res.json();
134
- const ids = (json?.data ?? []).map((m) => m.id).filter((id) => typeof id === "string" && id.startsWith("claude-"));
135
- return ids;
136
- } catch {
137
- return [];
100
+
101
+ // src/auth-menu/shared.ts
102
+ import { color } from "@wrongstack/core/utils";
103
+ function renderProviderLine(renderer, id, cfg, idx) {
104
+ const keys = normalizeKeys(cfg);
105
+ const active = activeLabel(cfg, keys);
106
+ const firstKey = keys[0];
107
+ let summary;
108
+ if (keys.length === 0) {
109
+ summary = color.dim("(no keys)");
110
+ } else if (keys.length === 1) {
111
+ summary = maskedKey(firstKey?.apiKey ?? "");
112
+ } else {
113
+ const activeKeyObj = active != null ? keys.find((k) => k.label === active) : void 0;
114
+ summary = `${color.dim(`${keys.length} keys`)} ${color.dim("active:")} ${color.bold(active ?? "?")} ` + maskedKey(activeKeyObj?.apiKey ?? firstKey?.apiKey ?? "");
138
115
  }
116
+ const fam = cfg.family ? color.dim(`[${cfg.family}]`) : "";
117
+ const aliasHint = cfg.type && cfg.type !== id ? color.dim(`\u2192 ${cfg.type}`) : "";
118
+ renderer.write(
119
+ ` ${color.dim(`${idx}.`.padStart(4))} ${id.padEnd(22)} ${fam} ${aliasHint} ${summary}
120
+ `
121
+ );
139
122
  }
140
- async function runClaudeOAuthLogin(deps, opts = {}) {
141
- const providerId = opts.providerId ?? CLAUDE_PROVIDER_ID;
142
- const { verifier, challenge } = generatePkce();
143
- const state = verifier;
144
- const authorizeUrl = buildAuthorizeUrl(challenge, verifier);
145
- const osc8 = (url) => `\x1B]8;;${url}\x1B\\${url}\x1B]8;;\x1B\\`;
146
- const ac = new AbortController();
147
- const onSig = () => ac.abort();
148
- const external = opts.signal;
149
- const onExternalAbort = () => ac.abort();
150
- if (external) {
151
- if (external.aborted) ac.abort();
152
- else external.addEventListener("abort", onExternalAbort, { once: true });
123
+ function renderProviderHeader(renderer, providerId, cfg) {
124
+ const keys = normalizeKeys(cfg);
125
+ const active = activeLabel(cfg, keys);
126
+ renderer.write(
127
+ `
128
+ ${color.bold(providerId)} ${cfg.family ? color.dim(`[${cfg.family}]`) : color.amber("[no family]")}
129
+ `
130
+ );
131
+ const details = [
132
+ color.dim(` type: ${cfg.type ?? providerId}`),
133
+ color.dim(` family: ${cfg.family ?? "(unset \u2192 resolved from models.dev when type matches)"}`),
134
+ color.dim(` baseUrl: ${cfg.baseUrl ?? "(unset \u2192 catalog default)"}`)
135
+ ];
136
+ if (cfg.envVars && cfg.envVars.length > 0) {
137
+ details.push(color.dim(` envVars: ${cfg.envVars.join(", ")}`));
138
+ }
139
+ if (cfg.models && cfg.models.length > 0) {
140
+ details.push(color.dim(` models: ${cfg.models.join(", ")}`));
141
+ }
142
+ renderer.write(details.join("\n") + "\n");
143
+ if (keys.length === 0) {
144
+ renderer.write(color.dim(" (no keys saved)\n"));
153
145
  } else {
154
- process.on("SIGINT", onSig);
146
+ for (let i = 0; i < keys.length; i++) {
147
+ const key = keys[i];
148
+ if (key) renderKeyLine(renderer, key, i + 1, active);
149
+ }
155
150
  }
156
- const server = await startLoopbackServer(state, [REDIRECT_PORT], ac.signal, REDIRECT_PATH, REDIRECT_HOST);
157
- deps.renderer.write(
158
- color.bold(`
159
- Sign in with Claude \u2014 ${color.cyan(providerId)}
160
- `) + color.dim(" Uses your Claude Pro/Max subscription (not an API key).\n") + color.amber(" \u26A0 Using a subscription outside the official Claude Code client is against\n") + color.amber(" Anthropic\u2019s Terms \u2014 your account could be rate-limited or banned.\n") + color.dim(" Sanctioned programmatic use = an API key: ") + color.bold("wstack auth anthropic") + color.dim("\n\n") + color.bold(` ${"\u2500".repeat(56)}
161
- `) + color.bold(" Open this URL in your browser to sign in:\n") + color.cyan(` ${osc8(authorizeUrl)}
162
- `) + color.bold(` ${"\u2500".repeat(56)}
163
-
164
- `)
151
+ }
152
+ function renderKeyLine(renderer, key, idx, active) {
153
+ const marker = key.label === active ? color.green("\u25CF") : color.dim("\u25CB");
154
+ renderer.write(
155
+ ` ${color.dim(`${idx}.`.padStart(4))} ${marker} ${key.label.padEnd(20)} ${maskedKey(key.apiKey)} ${color.dim(key.createdAt)}
156
+ `
165
157
  );
166
- if (server.bound) {
167
- openBrowser(authorizeUrl);
168
- deps.renderer.write(
169
- color.dim(" A browser window should open. Waiting for you to finish signing in...\n") + color.dim(" (Listening on http://localhost:53692 \u2014 press Ctrl+C to cancel.)\n")
170
- );
171
- } else {
172
- deps.renderer.write(
173
- color.amber(" \u26A0 Could not start the local callback listener (port 53692 in use).\n") + color.dim(" After signing in, paste the full redirect URL (or the code) below.\n")
174
- );
158
+ }
159
+ function renderActions(renderer, keysLength) {
160
+ renderer.write(`
161
+ ${color.dim("Actions:")}
162
+ `);
163
+ renderer.write(` ${color.bold("a")} Add another key
164
+ `);
165
+ if (keysLength > 0) {
166
+ renderer.write(` ${color.bold("u")} <n> Update key <n>
167
+ `);
168
+ renderer.write(` ${color.bold("d")} <n> Delete key <n>
169
+ `);
170
+ renderer.write(` ${color.bold("s")} <n> Set key <n> as active
171
+ `);
175
172
  }
176
- let code;
177
- try {
178
- if (server.bound) {
179
- const got = await server.waitForCode();
180
- if (ac.signal.aborted) {
181
- deps.renderer.write(color.dim(" Cancelled.\n"));
182
- return 1;
183
- }
184
- if (got) code = got.code;
173
+ renderer.write(` ${color.bold("f")} Edit family
174
+ `);
175
+ renderer.write(
176
+ ` ${color.bold("B")} Edit baseUrl ${color.dim("(capital B \u2014 b is Back)")}
177
+ `
178
+ );
179
+ renderer.write(` ${color.bold("m")} Edit visible model list
180
+ `);
181
+ renderer.write(` ${color.bold("x")} Remove this provider entirely
182
+ `);
183
+ renderer.write(` ${color.bold("b")} Back
184
+ `);
185
+ renderer.write(` ${color.bold("q")} Quit
186
+ `);
187
+ }
188
+ function renderTopMenu(renderer, providers) {
189
+ renderer.write(`
190
+ ${color.bold("WrongStack")} ${color.dim("\u2014 API key manager")}
191
+
192
+ `);
193
+ const ids = Object.keys(providers).sort();
194
+ if (ids.length === 0) {
195
+ renderer.write(color.dim(" No providers configured yet.\n"));
196
+ renderer.write(
197
+ color.dim(
198
+ " Use (a) to add one from the models.dev catalog, (l) for a local server, or (c) for a custom provider.\n"
199
+ )
200
+ );
201
+ } else {
202
+ renderer.write(` ${color.dim("Saved providers:")}
203
+ `);
204
+ let idx = 1;
205
+ for (const id of ids) {
206
+ const cfg = providers[id];
207
+ if (!cfg) continue;
208
+ renderProviderLine(renderer, id, cfg, idx);
209
+ idx++;
185
210
  }
186
- if (!code) {
187
- const input = (await deps.reader.readLine(
188
- `
189
- ${color.amber("?")} Paste the redirect URL or code ${color.dim("(or q to cancel)")}: `
190
- )).trim();
191
- if (input.toLowerCase() === "q" || input === "") {
192
- deps.renderer.write(color.dim(" Cancelled.\n"));
193
- return 1;
194
- }
195
- const parsed = parseAuthorizationInput(input);
196
- if (parsed.state && parsed.state !== state) {
197
- deps.renderer.writeError(" State mismatch \u2014 please restart the login flow.");
198
- return 1;
199
- }
200
- code = parsed.code;
211
+ }
212
+ renderer.write(`
213
+ ${color.dim("Actions:")}
214
+ `);
215
+ renderer.write(` ${color.bold("a")} Add a provider (from catalog)
216
+ `);
217
+ renderer.write(
218
+ ` ${color.bold("l")} Add a local server ${color.dim("(OmniRoute / Ollama / vLLM / LM Studio)")}
219
+ `
220
+ );
221
+ renderer.write(` ${color.bold("c")} Add a custom provider
222
+ `);
223
+ renderer.write(
224
+ ` ${color.bold("s")} Login with OAuth ${color.dim("(ChatGPT / Claude / Copilot)")}
225
+ `
226
+ );
227
+ if (ids.length > 0) {
228
+ renderer.write(
229
+ ` ${color.dim("1-")}${color.dim(String(ids.length))} ${color.bold("Manage a provider")}
230
+ `
231
+ );
232
+ }
233
+ renderer.write(` ${color.bold("q")} Quit
234
+ `);
235
+ }
236
+ async function readKeyInput(deps, intent) {
237
+ const key = (await deps.reader.readSecret(
238
+ ` ${color.amber("?")} ${intent} ${color.dim("(hidden, paste OK)")}: `
239
+ )).trim();
240
+ if (!key) {
241
+ deps.renderer.writeError("No key entered.");
242
+ return void 0;
243
+ }
244
+ return key;
245
+ }
246
+ async function confirm(deps, question) {
247
+ const answer = (await deps.reader.readLine(` ${color.amber("?")} ${question} ${color.dim("[y/N/q]")} `)).trim().toLowerCase();
248
+ if (answer === "q" || answer === "quit") return null;
249
+ return answer === "y" || answer === "yes";
250
+ }
251
+ function suggestLabel(usedLabels) {
252
+ const candidate = "default";
253
+ if (!usedLabels.has(candidate)) return candidate;
254
+ let n = 2;
255
+ while (usedLabels.has(`key${n}`)) n++;
256
+ return `key${n}`;
257
+ }
258
+ function validateFamily(raw) {
259
+ const valid = ["anthropic", "openai", "openai-compatible", "google"];
260
+ return valid.includes(raw) ? raw : null;
261
+ }
262
+
263
+ // src/auth-menu/local.ts
264
+ var PRESET_BY_ID = new Map(LOCAL_LLM_PRESETS.map((p) => [p.id, p]));
265
+ async function runAuthLocal(deps, opts = {}) {
266
+ const scrubber = deps.secretScrubber ?? new DefaultSecretScrubber2();
267
+ const audit = opts.audit ?? NOOP_AUDIT_LOGGER;
268
+ const preset = opts.name ? PRESET_BY_ID.get(opts.name.toLowerCase()) : void 0;
269
+ if (opts.name && !preset) {
270
+ deps.renderer.writeError(
271
+ `Unknown local server "${opts.name}". Valid: ${LOCAL_LLM_PRESETS.map((p) => p.id).join(", ")}.`
272
+ );
273
+ return 1;
274
+ }
275
+ const chosen = preset ?? await pickLocalPreset(deps);
276
+ if (!chosen) {
277
+ return 0;
278
+ }
279
+ const baseUrl = opts.baseUrl?.trim() || chosen.defaultBaseUrl;
280
+ let apiKey;
281
+ if (opts.apiKey !== void 0) {
282
+ apiKey = opts.apiKey.trim() || void 0;
283
+ } else if (chosen.noAuth) {
284
+ apiKey = void 0;
285
+ } else if (opts.skipKey) {
286
+ apiKey = void 0;
287
+ } else {
288
+ apiKey = await promptOptionalKey(deps, chosen);
289
+ }
290
+ let probe;
291
+ let probeFailedAndSaved = false;
292
+ if (!opts.noProbe) {
293
+ probe = await probeLocalLlm({
294
+ baseUrl,
295
+ apiKey,
296
+ noAuth: chosen.noAuth,
297
+ presetLabel: chosen.label,
298
+ scrubber,
299
+ fetchImpl: opts.fetchImpl,
300
+ timeoutMs: opts.probeTimeoutMs
301
+ });
302
+ renderProbeResult(deps, probe);
303
+ if (opts.probeOnly) {
304
+ return 0;
201
305
  }
202
- if (!code) {
203
- deps.renderer.writeError(" No authorization code received.");
204
- return 1;
306
+ if (!probe.ok) {
307
+ if (chosen.noAuth) {
308
+ deps.renderer.write(
309
+ color2.dim(
310
+ ` Saving ${chosen.label} anyway \u2014 keyless local gateway needs no key and discovers models when it comes up.
311
+ `
312
+ )
313
+ );
314
+ probeFailedAndSaved = true;
315
+ } else {
316
+ const saveAnyway = await promptSaveAnyway(deps);
317
+ if (!saveAnyway) {
318
+ deps.renderer.write(color2.dim(" Cancelled. Nothing saved.\n"));
319
+ return 0;
320
+ }
321
+ probeFailedAndSaved = true;
322
+ }
205
323
  }
206
- deps.renderer.write(color.dim("\n Exchanging authorization code for tokens...\n"));
207
- const tokens = await exchangeAuthorizationCode(code, state, verifier, ac.signal);
208
- const models = await fetchClaudeModels(tokens.access, ac.signal);
209
- const saved = await saveClaudeTokens(deps, providerId, tokens, models);
210
- if (!saved) return 1;
211
- deps.renderer.write(color.green("\n \u2713 Signed in with Claude!\n"));
212
- const modelHint = models[0];
213
- deps.renderer.writeInfo(
214
- ` Saved as provider ${color.bold(providerId)}${models.length ? ` (${models.length} models)` : ""}.
215
- ` + (modelHint ? ` Use: ${color.bold(`wstack --provider ${providerId} --model ${modelHint}`)} "<task>"
216
- ` : ` No model list was discovered; choose a model before starting a run.
217
- `) + color.dim(" Tokens refresh automatically before they expire.\n")
324
+ }
325
+ const resolvedModels = resolveModelList(opts.models, probe, scrubber);
326
+ const label = opts.label?.trim() || "default";
327
+ const providers = await loadProviders(deps);
328
+ const existing = providers[chosen.id];
329
+ const previousModels = existing?.models ? [...existing.models] : existing?.models === void 0 ? void 0 : [...existing.models];
330
+ const usedLabels = new Set(existing ? normalizeKeys(existing).map((k) => k.label) : []);
331
+ let finalLabel = label;
332
+ if (usedLabels.has(finalLabel)) {
333
+ let n = 2;
334
+ while (usedLabels.has(`${label}-${n}`)) n++;
335
+ finalLabel = `${label}-${n}`;
336
+ deps.renderer.writeInfo(`Label collided; saving as "${finalLabel}".`);
337
+ }
338
+ try {
339
+ await mutateConfigProviders(
340
+ deps.profileConfigPath,
341
+ deps.vault,
342
+ (all) => {
343
+ const p = all[chosen.id] ?? { type: chosen.id };
344
+ if (!p.type) p.type = chosen.id;
345
+ if (!p.family) p.family = "openai-compatible";
346
+ if (!p.baseUrl) p.baseUrl = baseUrl;
347
+ if (!p.envVars) p.envVars = [];
348
+ if (resolvedModels !== null) {
349
+ p.models = [...resolvedModels];
350
+ }
351
+ if (apiKey) {
352
+ const list = normalizeKeys(p);
353
+ list.push({ label: finalLabel, apiKey, createdAt: nowIso() });
354
+ writeKeysBack(p, list);
355
+ if (!p.activeKey) p.activeKey = finalLabel;
356
+ }
357
+ all[chosen.id] = p;
358
+ },
359
+ deps.profileConfigPath
218
360
  );
219
- return 0;
220
361
  } catch (err) {
221
- const msg = err instanceof DOMException && err.name === "AbortError" ? "Login cancelled." : err.message;
222
- deps.renderer.writeError(` Login failed: ${msg}`);
362
+ deps.renderer.writeError(`Failed to save ${chosen.id}: ${err.message}`);
223
363
  return 1;
224
- } finally {
225
- server.close();
226
- if (external) external.removeEventListener("abort", onExternalAbort);
227
- else process.off("SIGINT", onSig);
228
364
  }
365
+ const events = decideAuthLocalEvents({
366
+ providerId: chosen.id,
367
+ baseUrl,
368
+ previousModels,
369
+ newModels: resolvedModels,
370
+ keyLabel: apiKey ? finalLabel : void 0,
371
+ probeFailedSave: probeFailedAndSaved && probe ? {
372
+ status: probe.status,
373
+ ...probe.detail !== void 0 ? { detail: probe.detail } : {}
374
+ } : void 0
375
+ });
376
+ for (const event of events) {
377
+ audit.emit(event);
378
+ }
379
+ if (apiKey) {
380
+ deps.renderer.write(
381
+ ` ${color2.green("\u2713")} Saved ${color2.bold(chosen.id)}/${color2.bold(finalLabel)} \u2192 ${color2.cyan(baseUrl)}
382
+ `
383
+ );
384
+ } else {
385
+ deps.renderer.write(
386
+ ` ${color2.green("\u2713")} Saved ${color2.bold(chosen.id)} (no key) \u2192 ${color2.cyan(baseUrl)}
387
+ `
388
+ );
389
+ }
390
+ const firstModel = resolvedModels && resolvedModels.length > 0 ? resolvedModels[0] : null;
391
+ const modelHint = firstModel ? color2.cyan(firstModel) : color2.dim("<model-id>");
392
+ deps.renderer.write(
393
+ color2.dim(` Launch: wstack --provider ${chosen.id} --model ${modelHint} "<task>"
394
+ `)
395
+ );
396
+ return 0;
229
397
  }
230
- async function saveClaudeTokens(deps, providerId, tokens, models) {
231
- const entry = {
232
- label: "oauth-default",
233
- apiKey: tokens.access,
234
- createdAt: nowIso(),
235
- authMethod: "oauth",
236
- expiresAt: new Date(tokens.expires).toISOString(),
237
- refreshToken: tokens.refresh,
238
- tokenType: "bearer",
239
- scope: SCOPES
240
- };
241
- try {
242
- await mutateConfigProviders(deps.profileConfigPath, deps.vault, (all) => {
243
- const existing = all[providerId];
244
- const p = existing ? { ...existing } : { type: providerId };
245
- p.family = "anthropic-oauth";
246
- if (!p.baseUrl) p.baseUrl = CLAUDE_BASE_URL;
247
- if (models.length > 0) p.models = models;
248
- const keys = normalizeKeys(p).filter((k) => k.label !== entry.label);
249
- keys.push(entry);
250
- writeKeysBack(p, keys);
251
- p.activeKey = entry.label;
252
- all[providerId] = p;
253
- }, deps.profileConfigPath);
254
- return true;
255
- } catch (err) {
256
- deps.renderer.writeError(` Failed to save tokens: ${err.message}`);
257
- return false;
398
+ function resolveModelList(models, probe, scrubber) {
399
+ if (models === void 0) return null;
400
+ const trimmed = models.trim();
401
+ if (trimmed.length === 0) return [];
402
+ const hasProbeIds = probe?.status === "ok" && (probe.modelIds?.length ?? 0) > 0;
403
+ const probeIds = hasProbeIds ? probe?.modelIds ?? [] : [];
404
+ if (trimmed === "first") {
405
+ if (probeIds.length === 0) return null;
406
+ return [scrubber.scrub(probeIds[0])];
258
407
  }
408
+ if (/^\d+$/.test(trimmed)) {
409
+ if (probeIds.length === 0) return null;
410
+ const n = Math.min(Number.parseInt(trimmed, 10), probeIds.length);
411
+ return probeIds.slice(0, n).map((id) => scrubber.scrub(id));
412
+ }
413
+ const seen = /* @__PURE__ */ new Set();
414
+ const out = [];
415
+ for (const raw of trimmed.split(",")) {
416
+ const id = scrubber.scrub(raw.trim());
417
+ if (id.length === 0) continue;
418
+ if (seen.has(id)) continue;
419
+ seen.add(id);
420
+ out.push(id);
421
+ }
422
+ return out;
259
423
  }
424
+ function renderProbeResult(deps, probe, opts = {}) {
425
+ const label = opts.presetLabel ? ` ${color2.bold(opts.presetLabel)}` : "";
426
+ const elapsed = probe.elapsedMs !== void 0 ? ` ${color2.dim(`(${probe.elapsedMs}ms)`)}` : "";
427
+ switch (probe.status) {
428
+ case "ok": {
429
+ const models = probe.modelCount !== void 0 ? color2.dim(`, ${probe.modelCount} model${probe.modelCount === 1 ? "" : "s"}`) : "";
430
+ deps.renderer.write(` ${color2.green("\u25CF")} ${label} health probe ok${models}${elapsed}
431
+ `);
432
+ break;
433
+ }
434
+ case "timeout": {
435
+ deps.renderer.write(
436
+ ` ${color2.red("\u25CF")} ${label} health probe timed out${elapsed}
437
+ ${color2.dim("The server did not respond within the timeout.")}
438
+ ${color2.dim("Is the local server running? Check the port and try again.")}
439
+ `
440
+ );
441
+ break;
442
+ }
443
+ case "unreachable": {
444
+ const detail = probe.detail ? ` ${color2.dim(`(${probe.detail})`)}` : "";
445
+ deps.renderer.write(
446
+ ` ${color2.red("\u25CF")} ${label} server unreachable${detail}
447
+ ${color2.dim("Could not connect. Common causes:")}
448
+ ${color2.dim(" - The local server is not running")}
449
+ ${color2.dim(" - The port is blocked by a firewall")}
450
+ ${color2.dim(" - The base URL is wrong")}
451
+ `
452
+ );
453
+ break;
454
+ }
455
+ case "http_error": {
456
+ const status = probe.httpStatus !== void 0 ? ` HTTP ${probe.httpStatus}` : "";
457
+ const detail = probe.detail ? ` ${color2.dim(`\u2014 ${probe.detail}`)}` : "";
458
+ deps.renderer.write(` ${color2.yellow("\u25CF")} ${label} health probe got${status}${detail}
459
+ `);
460
+ break;
461
+ }
462
+ case "invalid_response": {
463
+ deps.renderer.write(
464
+ ` ${color2.yellow("\u25CF")} ${label} health probe returned an unexpected shape
465
+ ` + (probe.detail ? ` ${color2.dim(probe.detail)}
466
+ ` : "")
467
+ );
468
+ break;
469
+ }
470
+ case "skipped": {
471
+ break;
472
+ }
473
+ }
474
+ }
475
+ async function promptSaveAnyway(deps) {
476
+ const answer = (await deps.reader.readLine(
477
+ `
478
+ ${color2.amber("?")} ${color2.bold("Save anyway?")} ${color2.dim("[y/N]")}: `
479
+ )).trim().toLowerCase();
480
+ return answer === "y" || answer === "yes";
481
+ }
482
+ async function pickLocalPreset(deps) {
483
+ deps.renderer.write(
484
+ `
485
+ ${color2.bold("Local LLM servers")} ${color2.dim("\u2014 pick one to pre-fill the base URL")}
260
486
 
261
- // src/auth-menu/github-copilot-oauth.ts
262
- import { color as color2 } from "@wrongstack/core/utils";
263
- import { FetchError as FetchError2, ParseError as ParseError2 } from "@wrongstack/core/types";
264
- import { copilotBaseUrlFromToken, refreshCopilotToken } from "@wrongstack/providers";
265
- var CLIENT_ID2 = "Iv1.b507a08c87ecfe98";
266
- var DEVICE_CODE_URL = "https://github.com/login/device/code";
267
- var ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
268
- var COPILOT_HEADERS = {
269
- "User-Agent": "GitHubCopilotChat/0.35.0",
270
- "Editor-Version": "vscode/1.107.0",
271
- "Editor-Plugin-Version": "copilot-chat/0.35.0",
272
- "Copilot-Integration-Id": "vscode-chat"
273
- };
274
- var COPILOT_API_VERSION = "2026-06-01";
275
- var COPILOT_PROVIDER_ID = "github-copilot";
276
- function sleep(ms, signal) {
277
- return new Promise((resolve, reject) => {
278
- if (signal.aborted) return reject(new DOMException("Aborted", "AbortError"));
279
- const t = setTimeout(resolve, ms);
280
- signal.addEventListener(
281
- "abort",
282
- () => {
283
- clearTimeout(t);
284
- reject(new DOMException("Aborted", "AbortError"));
285
- },
286
- { once: true }
487
+ `
488
+ );
489
+ let idx = 1;
490
+ for (const entry of LOCAL_LLM_PRESETS) {
491
+ const auth = entry.noAuth ? color2.dim("(no auth)") : color2.dim("(optional auth)");
492
+ deps.renderer.write(
493
+ ` ${color2.dim(`${idx}.`.padStart(4))} ${color2.bold(entry.label.padEnd(12))} ${color2.cyan(entry.defaultBaseUrl.padEnd(34))} ${auth}
494
+ `
287
495
  );
288
- });
496
+ deps.renderer.write(` ${color2.dim(entry.hint)}
497
+ `);
498
+ idx++;
499
+ }
500
+ const answer = (await deps.reader.readLine(
501
+ `
502
+ ${color2.amber("?")} Pick ${color2.dim(`(1-${LOCAL_LLM_PRESETS.length}, q to quit)`)}: `
503
+ )).trim().toLowerCase();
504
+ if (!answer || answer === "q" || answer === "quit") return void 0;
505
+ const num = Number.parseInt(answer, 10);
506
+ if (!Number.isNaN(num) && num >= 1 && num <= LOCAL_LLM_PRESETS.length) {
507
+ return LOCAL_LLM_PRESETS[num - 1];
508
+ }
509
+ const byId = LOCAL_LLM_PRESETS.find((p) => p.id === answer);
510
+ if (byId) return byId;
511
+ deps.renderer.writeError(`Unknown selection: "${answer}"`);
512
+ return void 0;
289
513
  }
290
- async function startDeviceFlow(signal) {
291
- const res = await fetch(DEVICE_CODE_URL, {
292
- method: "POST",
293
- headers: {
294
- accept: "application/json",
295
- "content-type": "application/x-www-form-urlencoded",
296
- "user-agent": COPILOT_HEADERS["User-Agent"]
297
- },
298
- body: new URLSearchParams({ client_id: CLIENT_ID2, scope: "read:user" }).toString(),
299
- signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(15e3)]) : AbortSignal.timeout(15e3)
514
+ async function promptOptionalKey(deps, preset) {
515
+ const answer = (await deps.reader.readSecret(
516
+ ` ${color2.amber("?")} ${color2.bold(preset.label)} API key ${color2.dim("(hidden \u2014 Enter to skip if auth is disabled on the server)")}: `
517
+ )).trim();
518
+ if (!answer) return void 0;
519
+ return answer;
520
+ }
521
+
522
+ // src/auth-menu/oauth-menu.ts
523
+ import { color as color5 } from "@wrongstack/core/utils";
524
+
525
+ // src/auth-menu/anthropic-oauth.ts
526
+ import { createHash, randomBytes } from "node:crypto";
527
+ import {
528
+ FetchError,
529
+ ParseError
530
+ } from "@wrongstack/core/types";
531
+ import { color as color3 } from "@wrongstack/core/utils";
532
+ var CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
533
+ var AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
534
+ var TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
535
+ var REDIRECT_PORT = 53692;
536
+ var REDIRECT_HOST = "127.0.0.1";
537
+ var REDIRECT_PATH = "/callback";
538
+ var REDIRECT_URI = `http://localhost:${REDIRECT_PORT}${REDIRECT_PATH}`;
539
+ var SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
540
+ var CLAUDE_PROVIDER_ID = "anthropic-oauth";
541
+ var CLAUDE_BASE_URL = "https://api.anthropic.com";
542
+ function base64url(buf) {
543
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
544
+ }
545
+ function generatePkce() {
546
+ const verifier = base64url(randomBytes(32));
547
+ const challenge = base64url(createHash("sha256").update(verifier).digest());
548
+ return { verifier, challenge };
549
+ }
550
+ function buildAuthorizeUrl(challenge, verifier) {
551
+ const params = new URLSearchParams({
552
+ code: "true",
553
+ client_id: CLIENT_ID,
554
+ response_type: "code",
555
+ redirect_uri: REDIRECT_URI,
556
+ scope: SCOPES,
557
+ code_challenge: challenge,
558
+ code_challenge_method: "S256",
559
+ state: verifier
300
560
  });
561
+ return `${AUTHORIZE_URL}?${params.toString()}`;
562
+ }
563
+ function parseAuthorizationInput(input) {
564
+ const value = input.trim();
565
+ if (!value) return {};
566
+ try {
567
+ const url = new URL(value);
568
+ return {
569
+ code: url.searchParams.get("code") ?? void 0,
570
+ state: url.searchParams.get("state") ?? void 0
571
+ };
572
+ } catch {
573
+ }
574
+ if (value.includes("#")) {
575
+ const [code, state] = value.split("#", 2);
576
+ return { code, state };
577
+ }
578
+ if (value.includes("code=")) {
579
+ const params = new URLSearchParams(value);
580
+ return {
581
+ code: params.get("code") ?? void 0,
582
+ state: params.get("state") ?? void 0
583
+ };
584
+ }
585
+ return { code: value };
586
+ }
587
+ async function readTokens(res, op, url) {
301
588
  if (!res.ok) {
302
- throw new FetchError2({
303
- message: `GitHub device-code request failed (${res.status})`,
589
+ const text = await res.text().catch(() => "");
590
+ throw new FetchError({
591
+ message: `Claude token ${op} failed (${res.status}): ${text || res.statusText}`,
304
592
  status: res.status,
305
- context: { provider: "github-copilot", op: "device-code", url: DEVICE_CODE_URL }
593
+ context: { provider: "anthropic-oauth", op, url }
306
594
  });
307
595
  }
308
596
  const json = await res.json();
309
- if (!json?.device_code || !json.user_code || !json.verification_uri || typeof json.expires_in !== "number") {
310
- throw new ParseError2({
311
- message: "Invalid device-code response",
312
- source: "github-copilot-device-code-response"
597
+ if (!json?.access_token || !json.refresh_token || typeof json.expires_in !== "number") {
598
+ throw new ParseError({
599
+ message: `Claude token ${op} response missing fields`,
600
+ source: "anthropic-oauth-token-response",
601
+ context: { op }
313
602
  });
314
603
  }
315
604
  return {
316
- device_code: json.device_code,
317
- user_code: json.user_code,
318
- verification_uri: json.verification_uri,
319
- interval: json.interval ?? 5,
320
- expires_in: json.expires_in
605
+ access: json.access_token,
606
+ refresh: json.refresh_token,
607
+ expires: Date.now() + json.expires_in * 1e3
321
608
  };
322
609
  }
323
- async function pollForGitHubToken(device, signal) {
324
- let intervalMs = device.interval * 1e3;
325
- const expiresAt = Date.now() + device.expires_in * 1e3;
326
- while (Date.now() < expiresAt) {
327
- await sleep(intervalMs, signal);
328
- const res = await fetch(ACCESS_TOKEN_URL, {
329
- method: "POST",
330
- headers: {
331
- accept: "application/json",
332
- "content-type": "application/x-www-form-urlencoded",
333
- "user-agent": COPILOT_HEADERS["User-Agent"]
334
- },
335
- body: new URLSearchParams({
336
- client_id: CLIENT_ID2,
337
- device_code: device.device_code,
338
- grant_type: "urn:ietf:params:oauth:grant-type:device_code"
339
- }).toString(),
340
- signal: AbortSignal.any([signal, AbortSignal.timeout(15e3)])
341
- });
342
- const json = await res.json().catch(() => ({}));
343
- if (json.access_token) return json.access_token;
344
- if (json.error === "authorization_pending") continue;
345
- if (json.error === "slow_down") {
346
- intervalMs += 5e3;
347
- continue;
348
- }
349
- throw new Error(`Device flow failed: ${json.error ?? "unknown error"}`);
350
- }
351
- throw new FetchError2({
352
- message: "Device code expired \u2014 please restart the login.",
353
- status: 408,
354
- // Request Timeout
355
- context: { provider: "github-copilot", op: "device-code-poll", reason: "expired" }
610
+ async function exchangeAuthorizationCode(code, state, verifier, signal) {
611
+ const res = await fetch(TOKEN_URL, {
612
+ method: "POST",
613
+ headers: { "content-type": "application/json", accept: "application/json" },
614
+ body: JSON.stringify({
615
+ grant_type: "authorization_code",
616
+ client_id: CLIENT_ID,
617
+ code,
618
+ state,
619
+ redirect_uri: REDIRECT_URI,
620
+ code_verifier: verifier
621
+ }),
622
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(3e4)]) : AbortSignal.timeout(3e4)
356
623
  });
624
+ return readTokens(res, "exchange", TOKEN_URL);
357
625
  }
358
- function isUsableCopilotChatModel(item) {
359
- if (typeof item.id !== "string" || item.id.length === 0) return false;
360
- const cap = item.capabilities;
361
- if (cap?.type !== "chat") return false;
362
- if (cap.supports?.tool_calls !== true) return false;
363
- const eps = item.supported_endpoints;
364
- if (Array.isArray(eps) && !eps.includes("/chat/completions")) return false;
365
- if (item.policy?.state === "disabled") return false;
366
- if (item.vendor === "Experimental") return false;
367
- return true;
368
- }
369
- async function fetchCopilotModels(copilotToken, signal) {
626
+ async function fetchClaudeModels(accessToken, signal) {
370
627
  try {
371
- const base = copilotBaseUrlFromToken(copilotToken);
372
- const res = await fetch(`${base}/models`, {
628
+ const res = await fetch(`${CLAUDE_BASE_URL}/v1/models?limit=100`, {
373
629
  headers: {
374
630
  accept: "application/json",
375
- authorization: `Bearer ${copilotToken}`,
376
- "X-GitHub-Api-Version": COPILOT_API_VERSION,
377
- ...COPILOT_HEADERS
631
+ authorization: `Bearer ${accessToken}`,
632
+ "anthropic-version": "2023-06-01",
633
+ "anthropic-beta": "claude-code-20250219,oauth-2025-04-20"
378
634
  },
379
635
  signal: AbortSignal.any([signal, AbortSignal.timeout(8e3)])
380
636
  });
381
637
  if (!res.ok) return [];
382
638
  const json = await res.json();
383
- const data = json?.data;
384
- if (!Array.isArray(data)) return [];
385
- const usable = data.filter(isUsableCopilotChatModel);
386
- usable.sort((a, b) => copilotModelRank(a) - copilotModelRank(b));
387
- return usable.map((m) => m.id);
639
+ const ids = (json?.data ?? []).map((m) => m.id).filter((id) => typeof id === "string" && id.startsWith("claude-"));
640
+ return ids;
388
641
  } catch {
389
642
  return [];
390
643
  }
391
644
  }
392
- function copilotModelRank(item) {
393
- if (item.is_chat_default === true) return 0;
394
- if (item.is_chat_fallback === true) return 1;
395
- return 2;
396
- }
397
- async function runCopilotOAuthLogin(deps, opts = {}) {
398
- const providerId = opts.providerId ?? COPILOT_PROVIDER_ID;
645
+ async function runClaudeOAuthLogin(deps, opts = {}) {
646
+ const providerId = opts.providerId ?? CLAUDE_PROVIDER_ID;
647
+ const { verifier, challenge } = generatePkce();
648
+ const state = verifier;
649
+ const authorizeUrl = buildAuthorizeUrl(challenge, verifier);
650
+ const osc8 = (url) => `\x1B]8;;${url}\x1B\\${url}\x1B]8;;\x1B\\`;
399
651
  const ac = new AbortController();
400
652
  const onSig = () => ac.abort();
401
- const onExternalAbort = () => ac.abort();
402
653
  const external = opts.signal;
654
+ const onExternalAbort = () => ac.abort();
403
655
  if (external) {
404
656
  if (external.aborted) ac.abort();
405
657
  else external.addEventListener("abort", onExternalAbort, { once: true });
406
658
  } else {
407
659
  process.on("SIGINT", onSig);
408
660
  }
409
- try {
410
- deps.renderer.write(
411
- color2.bold(`
412
- Sign in with GitHub Copilot \u2014 ${color2.cyan(providerId)}
413
- `) + color2.dim(" Uses your GitHub Copilot subscription (not an API key).\n") + color2.amber(" \u26A0 Using Copilot outside its official editor integrations may violate\n") + color2.amber(" GitHub\u2019s Terms \u2014 your account could be rate-limited or banned.\n\n")
414
- );
415
- const device = await startDeviceFlow(ac.signal);
416
- deps.renderer.write(
417
- color2.bold(` ${"\u2500".repeat(56)}
418
- `) + color2.bold(" Open this URL and enter the code:\n") + color2.cyan(` ${device.verification_uri}
419
- `) + color2.bold(" Code: ") + color2.green(color2.bold(device.user_code)) + "\n" + color2.bold(` ${"\u2500".repeat(56)}
661
+ const server = await startLoopbackServer(
662
+ state,
663
+ [REDIRECT_PORT],
664
+ ac.signal,
665
+ REDIRECT_PATH,
666
+ REDIRECT_HOST
667
+ );
668
+ deps.renderer.write(
669
+ color3.bold(`
670
+ Sign in with Claude \u2014 ${color3.cyan(providerId)}
671
+ `) + 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)}
672
+ `) + color3.bold(" Open this URL in your browser to sign in:\n") + color3.cyan(` ${osc8(authorizeUrl)}
673
+ `) + color3.bold(` ${"\u2500".repeat(56)}
420
674
 
421
675
  `)
676
+ );
677
+ if (server.bound) {
678
+ openBrowser(authorizeUrl);
679
+ deps.renderer.write(
680
+ 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")
422
681
  );
423
- openBrowser(device.verification_uri);
424
- deps.renderer.write(color2.dim(" Waiting for you to authorize in the browser...\n"));
425
- const githubToken = await pollForGitHubToken(device, ac.signal);
426
- deps.renderer.write(color2.dim(" Authorized. Fetching your Copilot token...\n"));
427
- const copilot = await refreshCopilotToken(githubToken, ac.signal);
428
- const models = await fetchCopilotModels(copilot.token, ac.signal);
429
- const saved = await saveCopilotTokens(
430
- deps,
431
- providerId,
432
- copilot.token,
433
- githubToken,
434
- copilot.expires,
435
- models
682
+ } else {
683
+ deps.renderer.write(
684
+ 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")
436
685
  );
686
+ }
687
+ let code;
688
+ try {
689
+ if (server.bound) {
690
+ const got = await server.waitForCode();
691
+ if (ac.signal.aborted) {
692
+ deps.renderer.write(color3.dim(" Cancelled.\n"));
693
+ return 1;
694
+ }
695
+ if (got) code = got.code;
696
+ }
697
+ if (!code) {
698
+ const input = (await deps.reader.readLine(
699
+ `
700
+ ${color3.amber("?")} Paste the redirect URL or code ${color3.dim("(or q to cancel)")}: `
701
+ )).trim();
702
+ if (input.toLowerCase() === "q" || input === "") {
703
+ deps.renderer.write(color3.dim(" Cancelled.\n"));
704
+ return 1;
705
+ }
706
+ const parsed = parseAuthorizationInput(input);
707
+ if (parsed.state && parsed.state !== state) {
708
+ deps.renderer.writeError(" State mismatch \u2014 please restart the login flow.");
709
+ return 1;
710
+ }
711
+ code = parsed.code;
712
+ }
713
+ if (!code) {
714
+ deps.renderer.writeError(" No authorization code received.");
715
+ return 1;
716
+ }
717
+ deps.renderer.write(color3.dim("\n Exchanging authorization code for tokens...\n"));
718
+ const tokens = await exchangeAuthorizationCode(code, state, verifier, ac.signal);
719
+ const models = await fetchClaudeModels(tokens.access, ac.signal);
720
+ const saved = await saveClaudeTokens(deps, providerId, tokens, models);
437
721
  if (!saved) return 1;
438
- deps.renderer.write(color2.green("\n \u2713 Signed in with GitHub Copilot!\n"));
439
- const modelHint = models[0] ?? "gpt-4o";
722
+ deps.renderer.write(color3.green("\n \u2713 Signed in with Claude!\n"));
723
+ const modelHint = models[0];
440
724
  deps.renderer.writeInfo(
441
- ` Saved as provider ${color2.bold(providerId)}${models.length ? ` (${models.length} models)` : ""}.
442
- Use: ${color2.bold(`wstack --provider ${providerId} --model ${modelHint}`)} "<task>"
443
- ` + color2.dim(" The Copilot token refreshes automatically.\n")
725
+ ` Saved as provider ${color3.bold(providerId)}${models.length ? ` (${models.length} models)` : ""}.
726
+ ` + (modelHint ? ` Use: ${color3.bold(`wstack --provider ${providerId} --model ${modelHint}`)} "<task>"
727
+ ` : ` No model list was discovered; choose a model before starting a run.
728
+ `) + color3.dim(" Tokens refresh automatically before they expire.\n")
444
729
  );
445
730
  return 0;
446
731
  } catch (err) {
@@ -448,602 +733,351 @@ async function runCopilotOAuthLogin(deps, opts = {}) {
448
733
  deps.renderer.writeError(` Login failed: ${msg}`);
449
734
  return 1;
450
735
  } finally {
451
- if (external) {
452
- external.removeEventListener("abort", onExternalAbort);
453
- } else {
454
- process.off("SIGINT", onSig);
455
- }
736
+ server.close();
737
+ if (external) external.removeEventListener("abort", onExternalAbort);
738
+ else process.off("SIGINT", onSig);
456
739
  }
457
740
  }
458
- async function saveCopilotTokens(deps, providerId, copilotToken, githubToken, expires, models) {
741
+ async function saveClaudeTokens(deps, providerId, tokens, models) {
459
742
  const entry = {
460
743
  label: "oauth-default",
461
- apiKey: copilotToken,
744
+ apiKey: tokens.access,
462
745
  createdAt: nowIso(),
463
746
  authMethod: "oauth",
464
- expiresAt: new Date(expires).toISOString(),
465
- refreshToken: githubToken,
466
- tokenType: "bearer"
747
+ expiresAt: new Date(tokens.expires).toISOString(),
748
+ refreshToken: tokens.refresh,
749
+ tokenType: "bearer",
750
+ scope: SCOPES
467
751
  };
468
752
  try {
469
- await mutateConfigProviders(deps.profileConfigPath, deps.vault, (all) => {
470
- const existing = all[providerId];
471
- const p = existing ? { ...existing } : { type: providerId };
472
- p.family = "github-copilot";
473
- if (!p.baseUrl) p.baseUrl = copilotBaseUrlFromToken(copilotToken);
474
- if (models.length > 0) p.models = models;
475
- else if (!p.models || p.models.length === 0) p.models = ["gpt-4o"];
476
- const keys = normalizeKeys(p).filter((k) => k.label !== entry.label);
477
- keys.push(entry);
478
- writeKeysBack(p, keys);
479
- p.activeKey = entry.label;
480
- all[providerId] = p;
481
- }, deps.profileConfigPath);
482
- return true;
483
- } catch (err) {
484
- deps.renderer.writeError(` Failed to save tokens: ${err.message}`);
485
- return false;
486
- }
487
- }
488
-
489
- // src/auth-menu/oauth-menu.ts
490
- function renderOAuthLoginOptions(deps, indent = " ") {
491
- deps.renderer.write(
492
- `${indent}${color3.bold("OAuth login options")} ${color3.dim("(subscription sign-in)")}
493
- ${indent}${color3.bold("chatgpt")} ChatGPT Plus/Pro ${color3.dim("(\u2192 openai-codex)")}
494
- ${indent}${color3.bold("claude")} Claude Pro/Max ${color3.dim("(\u2192 anthropic-oauth)")}
495
- ${indent}${color3.bold("copilot")} GitHub Copilot ${color3.dim("(\u2192 github-copilot)")}
496
- `
497
- );
498
- }
499
- var OAUTH_ALIASES = {
500
- chatgpt: ["chatgpt", "openai", "codex", "codex-cli", "openai-codex", "chatgpt-plus", "plus", "pro"],
501
- claude: ["claude", "anthropic", "claude-pro", "claude-max", "anthropic-oauth", "max"],
502
- copilot: ["copilot", "github", "gh", "github-copilot"]
503
- };
504
- var OAUTH_NUMERIC = { "1": "chatgpt", "2": "claude", "3": "copilot" };
505
- function resolveOAuthKind(choice, opts = {}) {
506
- const pick = choice.trim().toLowerCase();
507
- if (!pick) return void 0;
508
- if (opts.allowNumeric !== false && OAUTH_NUMERIC[pick]) return OAUTH_NUMERIC[pick];
509
- for (const [kind, aliases] of Object.entries(OAUTH_ALIASES)) {
510
- if (aliases.includes(pick)) return kind;
511
- }
512
- return void 0;
513
- }
514
- async function runOAuthLoginChoice(deps, choice, opts = {}) {
515
- const kind = resolveOAuthKind(choice, opts);
516
- if (!kind) return false;
517
- await runOAuthLoginKind(deps, kind);
518
- return true;
519
- }
520
- async function runOAuthLoginKind(deps, kind) {
521
- if (kind === "claude") return runClaudeOAuthLogin(deps);
522
- if (kind === "copilot") return runCopilotOAuthLogin(deps);
523
- return runCodexOAuthLogin(deps);
524
- }
525
- async function runOAuthLoginMenu(deps) {
526
- deps.renderer.write(
527
- `
528
- ${color3.bold("Login with OAuth:")}
529
- ` + color3.amber(" \u26A0 Subscription tokens used outside official clients may violate provider\n") + color3.amber(" Terms \u2014 your account could be rate-limited or banned. An API key is the\n") + color3.dim(" sanctioned path for programmatic use.\n") + ` ${color3.bold("1")} ChatGPT Plus/Pro ${color3.dim("(\u2192 openai-codex)")}
530
- ${color3.bold("2")} Claude Pro/Max ${color3.dim("(\u2192 anthropic-oauth)")}
531
- ${color3.bold("3")} GitHub Copilot ${color3.dim("(\u2192 github-copilot)")}
532
- `
533
- );
534
- const pick = await deps.reader.readLine(` ${color3.amber("?")} Pick ${color3.dim("(or b to go back)")}: `);
535
- await runOAuthLoginChoice(deps, pick);
536
- }
537
-
538
- // src/auth-menu/local.ts
539
- import { color as color5 } from "@wrongstack/core/utils";
540
- import { DefaultSecretScrubber as DefaultSecretScrubber2 } from "@wrongstack/core/security";
541
-
542
- // src/auth-menu/helpers.ts
543
- async function loadProviders(deps) {
544
- return loadConfigProviders(deps.profileConfigPath, deps.vault, {
545
- warn: (msg) => deps.renderer.writeWarning(msg),
546
- profileConfigPath: deps.profileConfigPath
547
- });
548
- }
549
-
550
- // src/auth-menu/shared.ts
551
- import { color as color4 } from "@wrongstack/core/utils";
552
- function renderProviderLine(renderer, id, cfg, idx) {
553
- const keys = normalizeKeys(cfg);
554
- const active = activeLabel(cfg, keys);
555
- const firstKey = keys[0];
556
- let summary;
557
- if (keys.length === 0) {
558
- summary = color4.dim("(no keys)");
559
- } else if (keys.length === 1) {
560
- summary = maskedKey(firstKey?.apiKey ?? "");
561
- } else {
562
- const activeKeyObj = active != null ? keys.find((k) => k.label === active) : void 0;
563
- summary = `${color4.dim(`${keys.length} keys`)} ${color4.dim("active:")} ${color4.bold(active ?? "?")} ` + maskedKey(activeKeyObj?.apiKey ?? firstKey?.apiKey ?? "");
564
- }
565
- const fam = cfg.family ? color4.dim(`[${cfg.family}]`) : "";
566
- const aliasHint = cfg.type && cfg.type !== id ? color4.dim(`\u2192 ${cfg.type}`) : "";
567
- renderer.write(
568
- ` ${color4.dim(`${idx}.`.padStart(4))} ${id.padEnd(22)} ${fam} ${aliasHint} ${summary}
569
- `
570
- );
571
- }
572
- function renderProviderHeader(renderer, providerId, cfg) {
573
- const keys = normalizeKeys(cfg);
574
- const active = activeLabel(cfg, keys);
575
- renderer.write(
576
- `
577
- ${color4.bold(providerId)} ${cfg.family ? color4.dim(`[${cfg.family}]`) : color4.amber("[no family]")}
578
- `
579
- );
580
- const details = [
581
- color4.dim(` type: ${cfg.type ?? providerId}`),
582
- color4.dim(` family: ${cfg.family ?? "(unset \u2192 resolved from models.dev when type matches)"}`),
583
- color4.dim(` baseUrl: ${cfg.baseUrl ?? "(unset \u2192 catalog default)"}`)
584
- ];
585
- if (cfg.envVars && cfg.envVars.length > 0) {
586
- details.push(color4.dim(` envVars: ${cfg.envVars.join(", ")}`));
587
- }
588
- if (cfg.models && cfg.models.length > 0) {
589
- details.push(color4.dim(` models: ${cfg.models.join(", ")}`));
590
- }
591
- renderer.write(details.join("\n") + "\n");
592
- if (keys.length === 0) {
593
- renderer.write(color4.dim(" (no keys saved)\n"));
594
- } else {
595
- for (let i = 0; i < keys.length; i++) {
596
- const key = keys[i];
597
- if (key) renderKeyLine(renderer, key, i + 1, active);
598
- }
599
- }
600
- }
601
- function renderKeyLine(renderer, key, idx, active) {
602
- const marker = key.label === active ? color4.green("\u25CF") : color4.dim("\u25CB");
603
- renderer.write(
604
- ` ${color4.dim(`${idx}.`.padStart(4))} ${marker} ${key.label.padEnd(20)} ${maskedKey(key.apiKey)} ${color4.dim(key.createdAt)}
605
- `
606
- );
607
- }
608
- function renderActions(renderer, keysLength) {
609
- renderer.write(`
610
- ${color4.dim("Actions:")}
611
- `);
612
- renderer.write(` ${color4.bold("a")} Add another key
613
- `);
614
- if (keysLength > 0) {
615
- renderer.write(` ${color4.bold("u")} <n> Update key <n>
616
- `);
617
- renderer.write(` ${color4.bold("d")} <n> Delete key <n>
618
- `);
619
- renderer.write(` ${color4.bold("s")} <n> Set key <n> as active
620
- `);
621
- }
622
- renderer.write(` ${color4.bold("f")} Edit family
623
- `);
624
- renderer.write(` ${color4.bold("B")} Edit baseUrl ${color4.dim("(capital B \u2014 b is Back)")}
625
- `);
626
- renderer.write(` ${color4.bold("m")} Edit visible model list
627
- `);
628
- renderer.write(` ${color4.bold("x")} Remove this provider entirely
629
- `);
630
- renderer.write(` ${color4.bold("b")} Back
631
- `);
632
- renderer.write(` ${color4.bold("q")} Quit
633
- `);
634
- }
635
- function renderTopMenu(renderer, providers) {
636
- renderer.write(`
637
- ${color4.bold("WrongStack")} ${color4.dim("\u2014 API key manager")}
638
-
639
- `);
640
- const ids = Object.keys(providers).sort();
641
- if (ids.length === 0) {
642
- renderer.write(color4.dim(" No providers configured yet.\n"));
643
- renderer.write(
644
- color4.dim(
645
- " Use (a) to add one from the models.dev catalog, (l) for a local server, or (c) for a custom provider.\n"
646
- )
647
- );
648
- } else {
649
- renderer.write(` ${color4.dim("Saved providers:")}
650
- `);
651
- let idx = 1;
652
- for (const id of ids) {
653
- const cfg = providers[id];
654
- if (!cfg) continue;
655
- renderProviderLine(renderer, id, cfg, idx);
656
- idx++;
657
- }
658
- }
659
- renderer.write(`
660
- ${color4.dim("Actions:")}
661
- `);
662
- renderer.write(` ${color4.bold("a")} Add a provider (from catalog)
663
- `);
664
- renderer.write(
665
- ` ${color4.bold("l")} Add a local server ${color4.dim("(OmniRoute / Ollama / vLLM / LM Studio)")}
666
- `
667
- );
668
- renderer.write(` ${color4.bold("c")} Add a custom provider
669
- `);
670
- renderer.write(
671
- ` ${color4.bold("s")} Login with OAuth ${color4.dim("(ChatGPT / Claude / Copilot)")}
672
- `
673
- );
674
- if (ids.length > 0) {
675
- renderer.write(
676
- ` ${color4.dim("1-")}${color4.dim(String(ids.length))} ${color4.bold("Manage a provider")}
677
- `
753
+ await mutateConfigProviders(
754
+ deps.profileConfigPath,
755
+ deps.vault,
756
+ (all) => {
757
+ const existing = all[providerId];
758
+ const p = existing ? { ...existing } : { type: providerId };
759
+ p.family = "anthropic-oauth";
760
+ if (!p.baseUrl) p.baseUrl = CLAUDE_BASE_URL;
761
+ if (models.length > 0) p.models = models;
762
+ const keys = normalizeKeys(p).filter((k) => k.label !== entry.label);
763
+ keys.push(entry);
764
+ writeKeysBack(p, keys);
765
+ p.activeKey = entry.label;
766
+ all[providerId] = p;
767
+ },
768
+ deps.profileConfigPath
678
769
  );
770
+ return true;
771
+ } catch (err) {
772
+ deps.renderer.writeError(` Failed to save tokens: ${err.message}`);
773
+ return false;
679
774
  }
680
- renderer.write(` ${color4.bold("q")} Quit
681
- `);
682
- }
683
- async function readKeyInput(deps, intent) {
684
- const key = (await deps.reader.readSecret(
685
- ` ${color4.amber("?")} ${intent} ${color4.dim("(hidden, paste OK)")}: `
686
- )).trim();
687
- if (!key) {
688
- deps.renderer.writeError("No key entered.");
689
- return void 0;
690
- }
691
- return key;
692
- }
693
- async function confirm(deps, question) {
694
- const answer = (await deps.reader.readLine(` ${color4.amber("?")} ${question} ${color4.dim("[y/N/q]")} `)).trim().toLowerCase();
695
- if (answer === "q" || answer === "quit") return null;
696
- return answer === "y" || answer === "yes";
697
- }
698
- function suggestLabel(usedLabels) {
699
- const candidate = "default";
700
- if (!usedLabels.has(candidate)) return candidate;
701
- let n = 2;
702
- while (usedLabels.has(`key${n}`)) n++;
703
- return `key${n}`;
704
- }
705
- function validateFamily(raw) {
706
- const valid = ["anthropic", "openai", "openai-compatible", "google"];
707
- return valid.includes(raw) ? raw : null;
708
775
  }
709
776
 
710
- // src/auth-menu/local.ts
711
- import { probeLocalLlm } from "@wrongstack/runtime/probe";
712
-
713
- // src/auth-menu/auth-menu-audit.ts
714
- import { DefaultSecretScrubber } from "@wrongstack/core/security";
715
- var NOOP_AUDIT_LOGGER = {
716
- emit: () => {
717
- }
777
+ // src/auth-menu/github-copilot-oauth.ts
778
+ import {
779
+ FetchError as FetchError2,
780
+ ParseError as ParseError2
781
+ } from "@wrongstack/core/types";
782
+ import { color as color4 } from "@wrongstack/core/utils";
783
+ import { copilotBaseUrlFromToken, refreshCopilotToken } from "@wrongstack/providers";
784
+ var CLIENT_ID2 = "Iv1.b507a08c87ecfe98";
785
+ var DEVICE_CODE_URL = "https://github.com/login/device/code";
786
+ var ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
787
+ var COPILOT_HEADERS = {
788
+ "User-Agent": "GitHubCopilotChat/0.35.0",
789
+ "Editor-Version": "vscode/1.107.0",
790
+ "Editor-Plugin-Version": "copilot-chat/0.35.0",
791
+ "Copilot-Integration-Id": "vscode-chat"
718
792
  };
719
- function decideAuthLocalEvents(opts) {
720
- const events = [];
721
- if (opts.probeSkip) {
722
- events.push({
723
- type: "auth.local.probe_skip",
724
- providerId: opts.providerId,
725
- baseUrl: opts.baseUrl,
726
- requestedShape: opts.probeSkip.shape
727
- });
728
- }
729
- if (opts.probeFailedSave) {
730
- events.push({
731
- type: "auth.local.probe_failed_save",
732
- providerId: opts.providerId,
733
- baseUrl: opts.baseUrl,
734
- probeStatus: opts.probeFailedSave.status,
735
- ...opts.probeFailedSave.detail !== void 0 ? { probeDetail: opts.probeFailedSave.detail } : {}
736
- });
737
- }
738
- if (opts.newModels === null) return events;
739
- const hadPriorAllowlist = opts.previousModels !== void 0 && opts.previousModels.length > 0;
740
- if (hadPriorAllowlist && opts.newModels.length === 0) {
741
- events.push({
742
- type: "auth.local.clear",
743
- providerId: opts.providerId,
744
- baseUrl: opts.baseUrl,
745
- previousModels: opts.previousModels
793
+ var COPILOT_API_VERSION = "2026-06-01";
794
+ var COPILOT_PROVIDER_ID = "github-copilot";
795
+ function sleep(ms, signal) {
796
+ return new Promise((resolve, reject) => {
797
+ if (signal.aborted) return reject(new DOMException("Aborted", "AbortError"));
798
+ const t = setTimeout(resolve, ms);
799
+ signal.addEventListener(
800
+ "abort",
801
+ () => {
802
+ clearTimeout(t);
803
+ reject(new DOMException("Aborted", "AbortError"));
804
+ },
805
+ { once: true }
806
+ );
807
+ });
808
+ }
809
+ async function startDeviceFlow(signal) {
810
+ const res = await fetch(DEVICE_CODE_URL, {
811
+ method: "POST",
812
+ headers: {
813
+ accept: "application/json",
814
+ "content-type": "application/x-www-form-urlencoded",
815
+ "user-agent": COPILOT_HEADERS["User-Agent"]
816
+ },
817
+ body: new URLSearchParams({ client_id: CLIENT_ID2, scope: "read:user" }).toString(),
818
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(15e3)]) : AbortSignal.timeout(15e3)
819
+ });
820
+ if (!res.ok) {
821
+ throw new FetchError2({
822
+ message: `GitHub device-code request failed (${res.status})`,
823
+ status: res.status,
824
+ context: { provider: "github-copilot", op: "device-code", url: DEVICE_CODE_URL }
746
825
  });
747
- return events;
748
- }
749
- if (!hadPriorAllowlist && opts.newModels.length > 0) {
750
- if (opts.previousModels !== void 0) {
751
- events.push({
752
- type: "auth.local.undo",
753
- providerId: opts.providerId,
754
- baseUrl: opts.baseUrl,
755
- restoredModels: [...opts.newModels]
756
- });
757
- } else {
758
- events.push({
759
- type: "auth.local.add",
760
- providerId: opts.providerId,
761
- baseUrl: opts.baseUrl,
762
- models: [...opts.newModels],
763
- ...opts.keyLabel !== void 0 ? { keyLabel: opts.keyLabel } : {}
764
- });
765
- }
766
- return events;
767
826
  }
768
- if (hadPriorAllowlist && opts.newModels.length > 0) {
769
- events.push({
770
- type: "auth.local.add",
771
- providerId: opts.providerId,
772
- baseUrl: opts.baseUrl,
773
- models: [...opts.newModels],
774
- ...opts.keyLabel !== void 0 ? { keyLabel: opts.keyLabel } : {}
827
+ const json = await res.json();
828
+ if (!json?.device_code || !json.user_code || !json.verification_uri || typeof json.expires_in !== "number") {
829
+ throw new ParseError2({
830
+ message: "Invalid device-code response",
831
+ source: "github-copilot-device-code-response"
775
832
  });
776
- return events;
777
833
  }
778
- return events;
834
+ return {
835
+ device_code: json.device_code,
836
+ user_code: json.user_code,
837
+ verification_uri: json.verification_uri,
838
+ interval: json.interval ?? 5,
839
+ expires_in: json.expires_in
840
+ };
779
841
  }
780
-
781
- // src/auth-menu/local.ts
782
- var PRESET_BY_ID = new Map(LOCAL_LLM_PRESETS.map((p) => [p.id, p]));
783
- async function runAuthLocal(deps, opts = {}) {
784
- const scrubber = deps.secretScrubber ?? new DefaultSecretScrubber2();
785
- const audit = opts.audit ?? NOOP_AUDIT_LOGGER;
786
- const preset = opts.name ? PRESET_BY_ID.get(opts.name.toLowerCase()) : void 0;
787
- if (opts.name && !preset) {
788
- deps.renderer.writeError(
789
- `Unknown local server "${opts.name}". Valid: ${LOCAL_LLM_PRESETS.map((p) => p.id).join(", ")}.`
790
- );
791
- return 1;
792
- }
793
- const chosen = preset ?? await pickLocalPreset(deps);
794
- if (!chosen) {
795
- return 0;
796
- }
797
- const baseUrl = opts.baseUrl?.trim() || chosen.defaultBaseUrl;
798
- let apiKey;
799
- if (opts.apiKey !== void 0) {
800
- apiKey = opts.apiKey.trim() || void 0;
801
- } else if (chosen.noAuth) {
802
- apiKey = void 0;
803
- } else if (opts.skipKey) {
804
- apiKey = void 0;
805
- } else {
806
- apiKey = await promptOptionalKey(deps, chosen);
807
- }
808
- let probe;
809
- let probeFailedAndSaved = false;
810
- if (!opts.noProbe) {
811
- probe = await probeLocalLlm({
812
- baseUrl,
813
- apiKey,
814
- noAuth: chosen.noAuth,
815
- presetLabel: chosen.label,
816
- scrubber,
817
- fetchImpl: opts.fetchImpl,
818
- timeoutMs: opts.probeTimeoutMs
842
+ async function pollForGitHubToken(device, signal) {
843
+ let intervalMs = device.interval * 1e3;
844
+ const expiresAt = Date.now() + device.expires_in * 1e3;
845
+ while (Date.now() < expiresAt) {
846
+ await sleep(intervalMs, signal);
847
+ const res = await fetch(ACCESS_TOKEN_URL, {
848
+ method: "POST",
849
+ headers: {
850
+ accept: "application/json",
851
+ "content-type": "application/x-www-form-urlencoded",
852
+ "user-agent": COPILOT_HEADERS["User-Agent"]
853
+ },
854
+ body: new URLSearchParams({
855
+ client_id: CLIENT_ID2,
856
+ device_code: device.device_code,
857
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code"
858
+ }).toString(),
859
+ signal: AbortSignal.any([signal, AbortSignal.timeout(15e3)])
819
860
  });
820
- renderProbeResult(deps, probe);
821
- if (opts.probeOnly) {
822
- return 0;
823
- }
824
- if (!probe.ok) {
825
- if (chosen.noAuth) {
826
- deps.renderer.write(
827
- color5.dim(
828
- ` Saving ${chosen.label} anyway \u2014 keyless local gateway needs no key and discovers models when it comes up.
829
- `
830
- )
831
- );
832
- probeFailedAndSaved = true;
833
- } else {
834
- const saveAnyway = await promptSaveAnyway(deps);
835
- if (!saveAnyway) {
836
- deps.renderer.write(color5.dim(" Cancelled. Nothing saved.\n"));
837
- return 0;
838
- }
839
- probeFailedAndSaved = true;
840
- }
861
+ const json = await res.json().catch(() => ({}));
862
+ if (json.access_token) return json.access_token;
863
+ if (json.error === "authorization_pending") continue;
864
+ if (json.error === "slow_down") {
865
+ intervalMs += 5e3;
866
+ continue;
841
867
  }
868
+ throw new Error(`Device flow failed: ${json.error ?? "unknown error"}`);
842
869
  }
843
- const resolvedModels = resolveModelList(opts.models, probe, scrubber);
844
- const label = opts.label?.trim() || "default";
845
- const providers = await loadProviders(deps);
846
- const existing = providers[chosen.id];
847
- const previousModels = existing?.models ? [...existing.models] : existing?.models === void 0 ? void 0 : [...existing.models];
848
- const usedLabels = new Set(existing ? normalizeKeys(existing).map((k) => k.label) : []);
849
- let finalLabel = label;
850
- if (usedLabels.has(finalLabel)) {
851
- let n = 2;
852
- while (usedLabels.has(`${label}-${n}`)) n++;
853
- finalLabel = `${label}-${n}`;
854
- deps.renderer.writeInfo(`Label collided; saving as "${finalLabel}".`);
855
- }
870
+ throw new FetchError2({
871
+ message: "Device code expired \u2014 please restart the login.",
872
+ status: 408,
873
+ // Request Timeout
874
+ context: { provider: "github-copilot", op: "device-code-poll", reason: "expired" }
875
+ });
876
+ }
877
+ function isUsableCopilotChatModel(item) {
878
+ if (typeof item.id !== "string" || item.id.length === 0) return false;
879
+ const cap = item.capabilities;
880
+ if (cap?.type !== "chat") return false;
881
+ if (cap.supports?.tool_calls !== true) return false;
882
+ const eps = item.supported_endpoints;
883
+ if (Array.isArray(eps) && !eps.includes("/chat/completions")) return false;
884
+ if (item.policy?.state === "disabled") return false;
885
+ if (item.vendor === "Experimental") return false;
886
+ return true;
887
+ }
888
+ async function fetchCopilotModels(copilotToken, signal) {
856
889
  try {
857
- await mutateConfigProviders(deps.profileConfigPath, deps.vault, (all) => {
858
- const p = all[chosen.id] ?? { type: chosen.id };
859
- if (!p.type) p.type = chosen.id;
860
- if (!p.family) p.family = "openai-compatible";
861
- if (!p.baseUrl) p.baseUrl = baseUrl;
862
- if (!p.envVars) p.envVars = [];
863
- if (resolvedModels !== null) {
864
- p.models = [...resolvedModels];
865
- }
866
- if (apiKey) {
867
- const list = normalizeKeys(p);
868
- list.push({ label: finalLabel, apiKey, createdAt: nowIso() });
869
- writeKeysBack(p, list);
870
- if (!p.activeKey) p.activeKey = finalLabel;
871
- }
872
- all[chosen.id] = p;
873
- }, deps.profileConfigPath);
874
- } catch (err) {
875
- deps.renderer.writeError(`Failed to save ${chosen.id}: ${err.message}`);
876
- return 1;
890
+ const base = copilotBaseUrlFromToken(copilotToken);
891
+ const res = await fetch(`${base}/models`, {
892
+ headers: {
893
+ accept: "application/json",
894
+ authorization: `Bearer ${copilotToken}`,
895
+ "X-GitHub-Api-Version": COPILOT_API_VERSION,
896
+ ...COPILOT_HEADERS
897
+ },
898
+ signal: AbortSignal.any([signal, AbortSignal.timeout(8e3)])
899
+ });
900
+ if (!res.ok) return [];
901
+ const json = await res.json();
902
+ const data = json?.data;
903
+ if (!Array.isArray(data)) return [];
904
+ const usable = data.filter(isUsableCopilotChatModel);
905
+ usable.sort((a, b) => copilotModelRank(a) - copilotModelRank(b));
906
+ return usable.map((m) => m.id);
907
+ } catch {
908
+ return [];
877
909
  }
878
- const events = decideAuthLocalEvents({
879
- providerId: chosen.id,
880
- baseUrl,
881
- previousModels,
882
- newModels: resolvedModels,
883
- keyLabel: apiKey ? finalLabel : void 0,
884
- probeFailedSave: probeFailedAndSaved && probe ? {
885
- status: probe.status,
886
- ...probe.detail !== void 0 ? { detail: probe.detail } : {}
887
- } : void 0
888
- });
889
- for (const event of events) {
890
- audit.emit(event);
910
+ }
911
+ function copilotModelRank(item) {
912
+ if (item.is_chat_default === true) return 0;
913
+ if (item.is_chat_fallback === true) return 1;
914
+ return 2;
915
+ }
916
+ async function runCopilotOAuthLogin(deps, opts = {}) {
917
+ const providerId = opts.providerId ?? COPILOT_PROVIDER_ID;
918
+ const ac = new AbortController();
919
+ const onSig = () => ac.abort();
920
+ const onExternalAbort = () => ac.abort();
921
+ const external = opts.signal;
922
+ if (external) {
923
+ if (external.aborted) ac.abort();
924
+ else external.addEventListener("abort", onExternalAbort, { once: true });
925
+ } else {
926
+ process.on("SIGINT", onSig);
891
927
  }
892
- if (apiKey) {
928
+ try {
893
929
  deps.renderer.write(
894
- ` ${color5.green("\u2713")} Saved ${color5.bold(chosen.id)}/${color5.bold(finalLabel)} \u2192 ${color5.cyan(baseUrl)}
895
- `
930
+ color4.bold(`
931
+ Sign in with GitHub Copilot \u2014 ${color4.cyan(providerId)}
932
+ `) + 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")
896
933
  );
897
- } else {
934
+ const device = await startDeviceFlow(ac.signal);
898
935
  deps.renderer.write(
899
- ` ${color5.green("\u2713")} Saved ${color5.bold(chosen.id)} (no key) \u2192 ${color5.cyan(baseUrl)}
900
- `
936
+ color4.bold(` ${"\u2500".repeat(56)}
937
+ `) + color4.bold(" Open this URL and enter the code:\n") + color4.cyan(` ${device.verification_uri}
938
+ `) + color4.bold(" Code: ") + color4.green(color4.bold(device.user_code)) + "\n" + color4.bold(` ${"\u2500".repeat(56)}
939
+
940
+ `)
941
+ );
942
+ openBrowser(device.verification_uri);
943
+ deps.renderer.write(color4.dim(" Waiting for you to authorize in the browser...\n"));
944
+ const githubToken = await pollForGitHubToken(device, ac.signal);
945
+ deps.renderer.write(color4.dim(" Authorized. Fetching your Copilot token...\n"));
946
+ const copilot = await refreshCopilotToken(githubToken, ac.signal);
947
+ const models = await fetchCopilotModels(copilot.token, ac.signal);
948
+ const saved = await saveCopilotTokens(
949
+ deps,
950
+ providerId,
951
+ copilot.token,
952
+ githubToken,
953
+ copilot.expires,
954
+ models
955
+ );
956
+ if (!saved) return 1;
957
+ deps.renderer.write(color4.green("\n \u2713 Signed in with GitHub Copilot!\n"));
958
+ const modelHint = models[0] ?? "gpt-4o";
959
+ deps.renderer.writeInfo(
960
+ ` Saved as provider ${color4.bold(providerId)}${models.length ? ` (${models.length} models)` : ""}.
961
+ Use: ${color4.bold(`wstack --provider ${providerId} --model ${modelHint}`)} "<task>"
962
+ ` + color4.dim(" The Copilot token refreshes automatically.\n")
963
+ );
964
+ return 0;
965
+ } catch (err) {
966
+ const msg = err instanceof DOMException && err.name === "AbortError" ? "Login cancelled." : err.message;
967
+ deps.renderer.writeError(` Login failed: ${msg}`);
968
+ return 1;
969
+ } finally {
970
+ if (external) {
971
+ external.removeEventListener("abort", onExternalAbort);
972
+ } else {
973
+ process.off("SIGINT", onSig);
974
+ }
975
+ }
976
+ }
977
+ async function saveCopilotTokens(deps, providerId, copilotToken, githubToken, expires, models) {
978
+ const entry = {
979
+ label: "oauth-default",
980
+ apiKey: copilotToken,
981
+ createdAt: nowIso(),
982
+ authMethod: "oauth",
983
+ expiresAt: new Date(expires).toISOString(),
984
+ refreshToken: githubToken,
985
+ tokenType: "bearer"
986
+ };
987
+ try {
988
+ await mutateConfigProviders(
989
+ deps.profileConfigPath,
990
+ deps.vault,
991
+ (all) => {
992
+ const existing = all[providerId];
993
+ const p = existing ? { ...existing } : { type: providerId };
994
+ p.family = "github-copilot";
995
+ if (!p.baseUrl) p.baseUrl = copilotBaseUrlFromToken(copilotToken);
996
+ if (models.length > 0) p.models = models;
997
+ else if (!p.models || p.models.length === 0) p.models = ["gpt-4o"];
998
+ const keys = normalizeKeys(p).filter((k) => k.label !== entry.label);
999
+ keys.push(entry);
1000
+ writeKeysBack(p, keys);
1001
+ p.activeKey = entry.label;
1002
+ all[providerId] = p;
1003
+ },
1004
+ deps.profileConfigPath
901
1005
  );
1006
+ return true;
1007
+ } catch (err) {
1008
+ deps.renderer.writeError(` Failed to save tokens: ${err.message}`);
1009
+ return false;
902
1010
  }
903
- const firstModel = resolvedModels && resolvedModels.length > 0 ? resolvedModels[0] : null;
904
- const modelHint = firstModel ? color5.cyan(firstModel) : color5.dim("<model-id>");
1011
+ }
1012
+
1013
+ // src/auth-menu/oauth-menu.ts
1014
+ function renderOAuthLoginOptions(deps, indent = " ") {
905
1015
  deps.renderer.write(
906
- color5.dim(
907
- ` Launch: wstack --provider ${chosen.id} --model ${modelHint} "<task>"
1016
+ `${indent}${color5.bold("OAuth login options")} ${color5.dim("(subscription sign-in)")}
1017
+ ${indent}${color5.bold("chatgpt")} ChatGPT Plus/Pro ${color5.dim("(\u2192 openai-codex)")}
1018
+ ${indent}${color5.bold("claude")} Claude Pro/Max ${color5.dim("(\u2192 anthropic-oauth)")}
1019
+ ${indent}${color5.bold("copilot")} GitHub Copilot ${color5.dim("(\u2192 github-copilot)")}
908
1020
  `
909
- )
910
1021
  );
911
- return 0;
912
1022
  }
913
- function resolveModelList(models, probe, scrubber) {
914
- if (models === void 0) return null;
915
- const trimmed = models.trim();
916
- if (trimmed.length === 0) return [];
917
- const hasProbeIds = probe?.status === "ok" && (probe.modelIds?.length ?? 0) > 0;
918
- const probeIds = hasProbeIds ? probe?.modelIds ?? [] : [];
919
- if (trimmed === "first") {
920
- if (probeIds.length === 0) return null;
921
- return [scrubber.scrub(probeIds[0])];
922
- }
923
- if (/^\d+$/.test(trimmed)) {
924
- if (probeIds.length === 0) return null;
925
- const n = Math.min(Number.parseInt(trimmed, 10), probeIds.length);
926
- return probeIds.slice(0, n).map((id) => scrubber.scrub(id));
927
- }
928
- const seen = /* @__PURE__ */ new Set();
929
- const out = [];
930
- for (const raw of trimmed.split(",")) {
931
- const id = scrubber.scrub(raw.trim());
932
- if (id.length === 0) continue;
933
- if (seen.has(id)) continue;
934
- seen.add(id);
935
- out.push(id);
1023
+ var OAUTH_ALIASES = {
1024
+ chatgpt: [
1025
+ "chatgpt",
1026
+ "openai",
1027
+ "codex",
1028
+ "codex-cli",
1029
+ "openai-codex",
1030
+ "chatgpt-plus",
1031
+ "plus",
1032
+ "pro"
1033
+ ],
1034
+ claude: ["claude", "anthropic", "claude-pro", "claude-max", "anthropic-oauth", "max"],
1035
+ copilot: ["copilot", "github", "gh", "github-copilot"]
1036
+ };
1037
+ var OAUTH_NUMERIC = {
1038
+ "1": "chatgpt",
1039
+ "2": "claude",
1040
+ "3": "copilot"
1041
+ };
1042
+ function resolveOAuthKind(choice, opts = {}) {
1043
+ const pick = choice.trim().toLowerCase();
1044
+ if (!pick) return void 0;
1045
+ if (opts.allowNumeric !== false && OAUTH_NUMERIC[pick]) return OAUTH_NUMERIC[pick];
1046
+ for (const [kind, aliases] of Object.entries(OAUTH_ALIASES)) {
1047
+ if (aliases.includes(pick)) return kind;
936
1048
  }
937
- return out;
1049
+ return void 0;
938
1050
  }
939
- function renderProbeResult(deps, probe, opts = {}) {
940
- const label = opts.presetLabel ? ` ${color5.bold(opts.presetLabel)}` : "";
941
- const elapsed = probe.elapsedMs !== void 0 ? ` ${color5.dim(`(${probe.elapsedMs}ms)`)}` : "";
942
- switch (probe.status) {
943
- case "ok": {
944
- const models = probe.modelCount !== void 0 ? color5.dim(`, ${probe.modelCount} model${probe.modelCount === 1 ? "" : "s"}`) : "";
945
- deps.renderer.write(
946
- ` ${color5.green("\u25CF")} ${label} health probe ok${models}${elapsed}
947
- `
948
- );
949
- break;
950
- }
951
- case "timeout": {
952
- deps.renderer.write(
953
- ` ${color5.red("\u25CF")} ${label} health probe timed out${elapsed}
954
- ${color5.dim("The server did not respond within the timeout.")}
955
- ${color5.dim("Is the local server running? Check the port and try again.")}
956
- `
957
- );
958
- break;
959
- }
960
- case "unreachable": {
961
- const detail = probe.detail ? ` ${color5.dim(`(${probe.detail})`)}` : "";
962
- deps.renderer.write(
963
- ` ${color5.red("\u25CF")} ${label} server unreachable${detail}
964
- ${color5.dim("Could not connect. Common causes:")}
965
- ${color5.dim(" - The local server is not running")}
966
- ${color5.dim(" - The port is blocked by a firewall")}
967
- ${color5.dim(" - The base URL is wrong")}
968
- `
969
- );
970
- break;
971
- }
972
- case "http_error": {
973
- const status = probe.httpStatus !== void 0 ? ` HTTP ${probe.httpStatus}` : "";
974
- const detail = probe.detail ? ` ${color5.dim(`\u2014 ${probe.detail}`)}` : "";
975
- deps.renderer.write(
976
- ` ${color5.yellow("\u25CF")} ${label} health probe got${status}${detail}
977
- `
978
- );
979
- break;
980
- }
981
- case "invalid_response": {
982
- deps.renderer.write(
983
- ` ${color5.yellow("\u25CF")} ${label} health probe returned an unexpected shape
984
- ` + (probe.detail ? ` ${color5.dim(probe.detail)}
985
- ` : "")
986
- );
987
- break;
988
- }
989
- case "skipped": {
990
- break;
991
- }
992
- }
1051
+ async function runOAuthLoginChoice(deps, choice, opts = {}) {
1052
+ const kind = resolveOAuthKind(choice, opts);
1053
+ if (!kind) return false;
1054
+ await runOAuthLoginKind(deps, kind);
1055
+ return true;
993
1056
  }
994
- async function promptSaveAnyway(deps) {
995
- const answer = (await deps.reader.readLine(
996
- `
997
- ${color5.amber("?")} ${color5.bold("Save anyway?")} ${color5.dim("[y/N]")}: `
998
- )).trim().toLowerCase();
999
- return answer === "y" || answer === "yes";
1057
+ async function runOAuthLoginKind(deps, kind) {
1058
+ if (kind === "claude") return runClaudeOAuthLogin(deps);
1059
+ if (kind === "copilot") return runCopilotOAuthLogin(deps);
1060
+ return runCodexOAuthLogin(deps);
1000
1061
  }
1001
- async function pickLocalPreset(deps) {
1062
+ async function runOAuthLoginMenu(deps) {
1002
1063
  deps.renderer.write(
1003
1064
  `
1004
- ${color5.bold("Local LLM servers")} ${color5.dim("\u2014 pick one to pre-fill the base URL")}
1005
-
1065
+ ${color5.bold("Login with OAuth:")}
1066
+ ` + 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)")}
1067
+ ${color5.bold("2")} Claude Pro/Max ${color5.dim("(\u2192 anthropic-oauth)")}
1068
+ ${color5.bold("3")} GitHub Copilot ${color5.dim("(\u2192 github-copilot)")}
1006
1069
  `
1007
1070
  );
1008
- let idx = 1;
1009
- for (const entry of LOCAL_LLM_PRESETS) {
1010
- const auth = entry.noAuth ? color5.dim("(no auth)") : color5.dim("(optional auth)");
1011
- deps.renderer.write(
1012
- ` ${color5.dim(`${idx}.`.padStart(4))} ${color5.bold(entry.label.padEnd(12))} ${color5.cyan(entry.defaultBaseUrl.padEnd(34))} ${auth}
1013
- `
1014
- );
1015
- deps.renderer.write(
1016
- ` ${color5.dim(entry.hint)}
1017
- `
1018
- );
1019
- idx++;
1020
- }
1021
- const answer = (await deps.reader.readLine(
1022
- `
1023
- ${color5.amber("?")} Pick ${color5.dim(`(1-${LOCAL_LLM_PRESETS.length}, q to quit)`)}: `
1024
- )).trim().toLowerCase();
1025
- if (!answer || answer === "q" || answer === "quit") return void 0;
1026
- const num = Number.parseInt(answer, 10);
1027
- if (!Number.isNaN(num) && num >= 1 && num <= LOCAL_LLM_PRESETS.length) {
1028
- return LOCAL_LLM_PRESETS[num - 1];
1029
- }
1030
- const byId = LOCAL_LLM_PRESETS.find((p) => p.id === answer);
1031
- if (byId) return byId;
1032
- deps.renderer.writeError(`Unknown selection: "${answer}"`);
1033
- return void 0;
1034
- }
1035
- async function promptOptionalKey(deps, preset) {
1036
- const answer = (await deps.reader.readSecret(
1037
- ` ${color5.amber("?")} ${color5.bold(preset.label)} API key ${color5.dim("(hidden \u2014 Enter to skip if auth is disabled on the server)")}: `
1038
- )).trim();
1039
- if (!answer) return void 0;
1040
- return answer;
1071
+ const pick = await deps.reader.readLine(
1072
+ ` ${color5.amber("?")} Pick ${color5.dim("(or b to go back)")}: `
1073
+ );
1074
+ await runOAuthLoginChoice(deps, pick);
1041
1075
  }
1042
1076
 
1043
1077
  // src/auth-menu/add-provider.ts
1078
+ import * as fs from "node:fs/promises";
1044
1079
  import { atomicWrite, color as color6 } from "@wrongstack/core/utils";
1045
1080
  import { catalogProviderIdFor, PROVIDER_DEFINITIONS } from "@wrongstack/providers";
1046
- import * as fs from "node:fs/promises";
1047
1081
 
1048
1082
  // src/provider-id.ts
1049
1083
  function nextCustomProviderId(existing) {
@@ -1245,27 +1279,32 @@ async function addKeyForProvider(providerId, deps, template) {
1245
1279
  if (!label) return false;
1246
1280
  const apiKey = await readKeyInput(deps, `API key for ${providerId}/${label}`);
1247
1281
  if (!apiKey) return false;
1248
- await mutateConfigProviders(deps.profileConfigPath, deps.vault, (all) => {
1249
- const existingProv = all[providerId] ?? {
1250
- type: providerId,
1251
- ...template
1252
- };
1253
- if (!existingProv.type) existingProv.type = providerId;
1254
- if (!existingProv.family && template.family) {
1255
- existingProv.family = template.family;
1256
- }
1257
- if (!existingProv.baseUrl && template.baseUrl) {
1258
- existingProv.baseUrl = template.baseUrl;
1259
- }
1260
- if (!existingProv.envVars && template.envVars) {
1261
- existingProv.envVars = template.envVars;
1262
- }
1263
- const list = normalizeKeys(existingProv);
1264
- list.push({ label, apiKey, createdAt: nowIso() });
1265
- writeKeysBack(existingProv, list);
1266
- if (!existingProv.activeKey) existingProv.activeKey = label;
1267
- all[providerId] = existingProv;
1268
- }, deps.profileConfigPath);
1282
+ await mutateConfigProviders(
1283
+ deps.profileConfigPath,
1284
+ deps.vault,
1285
+ (all) => {
1286
+ const existingProv = all[providerId] ?? {
1287
+ type: providerId,
1288
+ ...template
1289
+ };
1290
+ if (!existingProv.type) existingProv.type = providerId;
1291
+ if (!existingProv.family && template.family) {
1292
+ existingProv.family = template.family;
1293
+ }
1294
+ if (!existingProv.baseUrl && template.baseUrl) {
1295
+ existingProv.baseUrl = template.baseUrl;
1296
+ }
1297
+ if (!existingProv.envVars && template.envVars) {
1298
+ existingProv.envVars = template.envVars;
1299
+ }
1300
+ const list = normalizeKeys(existingProv);
1301
+ list.push({ label, apiKey, createdAt: nowIso() });
1302
+ writeKeysBack(existingProv, list);
1303
+ if (!existingProv.activeKey) existingProv.activeKey = label;
1304
+ all[providerId] = existingProv;
1305
+ },
1306
+ deps.profileConfigPath
1307
+ );
1269
1308
  deps.renderer.write(
1270
1309
  ` ${color6.green("\u2713")} Saved ${color6.bold(providerId)}/${color6.bold(label)}.
1271
1310
  `
@@ -1316,6 +1355,7 @@ async function promptForLabel(deps, usedLabels) {
1316
1355
  }
1317
1356
 
1318
1357
  export {
1358
+ runClaudeOAuthLogin,
1319
1359
  loadProviders,
1320
1360
  renderProviderHeader,
1321
1361
  renderActions,
@@ -1323,8 +1363,8 @@ export {
1323
1363
  readKeyInput,
1324
1364
  confirm,
1325
1365
  validateFamily,
1326
- runClaudeOAuthLogin,
1327
1366
  runCopilotOAuthLogin,
1367
+ runAuthLocal,
1328
1368
  resolveOAuthKind,
1329
1369
  runOAuthLoginKind,
1330
1370
  runOAuthLoginMenu,
@@ -1332,7 +1372,6 @@ export {
1332
1372
  addFromCatalog,
1333
1373
  addKeyForCatalogProvider,
1334
1374
  addCustomProvider,
1335
- addKeyForProvider,
1336
- runAuthLocal
1375
+ addKeyForProvider
1337
1376
  };
1338
- //# sourceMappingURL=chunk-O7XNGCRT.js.map
1377
+ //# sourceMappingURL=chunk-FJVCVZIV.js.map