@wrongstack/cli 1.0.1 → 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.
- package/data/providers.json +21 -21
- package/dist/{acp-3LARESEO.js → acp-JK4ZQO3R.js} +4 -3
- package/dist/{auth-ZUMCDNE5.js → auth-XD3GN5RJ.js} +78 -54
- package/dist/auth-menu/index.d.ts +1 -0
- package/dist/auth-menu/oauth-menu.d.ts +3 -1
- package/dist/auth-menu/panel-service.d.ts +2 -0
- package/dist/auth-menu/provider-auth-login.d.ts +12 -0
- package/dist/auth-menu/types.d.ts +3 -0
- package/dist/auto-discover-providers-DXR25GTG.js +7 -0
- package/dist/boot/dispatch-webui.d.ts +1 -0
- package/dist/boot/system-prompt-builder.d.ts +7 -0
- package/dist/boot/system-prompt.d.ts +2 -1
- package/dist/chimera-reviewer-policy.d.ts +1 -1
- package/dist/{chunk-KHWDTNBZ.js → chunk-4NHD5OXJ.js} +2 -2
- package/dist/chunk-D5GB5MHA.js +87 -0
- package/dist/chunk-DAKVBMD4.js +36 -0
- package/dist/{chunk-ZSZ3EYWH.js → chunk-GMY45LO6.js} +9 -5
- package/dist/{chunk-D2VCUN7M.js → chunk-IUEKNHPT.js} +10 -10
- package/dist/{chunk-MNQQ4ZNF.js → chunk-ONOT5OJ2.js} +290 -687
- package/dist/chunk-SIGUWXAA.js +43 -0
- package/dist/{chunk-TEBMCYZN.js → chunk-SIS37L3W.js} +58 -2
- package/dist/{chunk-5IA5337I.js → chunk-U6KCWRMC.js} +4 -2
- package/dist/{cli-context-7X3M6U73.js → cli-context-Q6QP534K.js} +18 -7
- package/dist/{cli-main-53KG6HAV.js → cli-main-JQXCDZEA.js} +2909 -2277
- package/dist/execute-deps.d.ts +1 -0
- package/dist/{execution-MIBWBYB6.js → execution-KH7WJNAX.js} +11 -4
- package/dist/index.js +3 -3
- package/dist/live-settings-input.d.ts +2 -0
- package/dist/{mailbox-serve-VUCWZ2SJ.js → mailbox-serve-X3CR6VFW.js} +4 -3
- package/dist/{modeldiag-FMJT37SN.js → modeldiag-46CMWUNM.js} +31 -24
- package/dist/{plugin-usage-HFBI3TFT.js → plugin-usage-M6KET4OZ.js} +2 -2
- package/dist/{plugins-PZ2VZL7V.js → plugins-U7KGQAUG.js} +3 -3
- package/dist/{providers-models-YVMVYWRS.js → providers-models-6MOGAOVK.js} +2 -2
- package/dist/{short-circuit-flags-UREFSYBL.js → short-circuit-flags-ZNZX44MC.js} +2 -2
- package/dist/slash-commands/codebase-map.d.ts +11 -0
- package/dist/slash-commands/command-context.d.ts +7 -0
- package/dist/slash-commands/context.d.ts +10 -0
- package/dist/slash-commands/openai-quota.d.ts +31 -0
- package/dist/{subcommands-WJLBHVKC.js → subcommands-CUTGBDKP.js} +2 -2
- package/dist/{webui-server-5R35BHWZ.js → webui-server-QWBUT7BO.js} +19 -2
- package/dist/webui-server-options.d.ts +1 -0
- package/dist/wiring/atlas-prompt-contributor.d.ts +54 -0
- package/dist/wiring/cli-execute-builder.d.ts +1 -0
- package/dist/wiring/codebase-embeddings.d.ts +31 -0
- package/dist/wiring/command-host-state.d.ts +2 -0
- package/dist/wiring/concept-summarizer.d.ts +32 -0
- package/dist/wiring/external-plugins.d.ts +1 -0
- package/dist/wiring/lifecycle-plugins.d.ts +2 -1
- package/dist/wiring/plugins.d.ts +2 -1
- package/dist/wiring/provider.d.ts +2 -1
- package/package.json +29 -29
- package/dist/auto-discover-providers-VQU3IZMR.js +0 -7
- package/dist/chunk-52C7PD2H.js +0 -61
- 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
|
-
|
|
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
|
|
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 =
|
|
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 = `${
|
|
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 ?
|
|
126
|
-
const aliasHint = cfg.type && cfg.type !== id ?
|
|
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
|
-
` ${
|
|
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
|
-
${
|
|
249
|
+
${color2.bold(providerId)} ${cfg.family ? color2.dim(`[${cfg.family}]`) : color2.amber("[no family]")}
|
|
138
250
|
`
|
|
139
251
|
);
|
|
140
252
|
const details = [
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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(
|
|
258
|
+
details.push(color2.dim(` envVars: ${cfg.envVars.join(", ")}`));
|
|
147
259
|
}
|
|
148
260
|
if (cfg.models && cfg.models.length > 0) {
|
|
149
|
-
details.push(
|
|
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(
|
|
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 ?
|
|
274
|
+
const marker = key.label === active ? color2.green("\u25CF") : color2.dim("\u25CB");
|
|
163
275
|
renderer.write(
|
|
164
|
-
` ${
|
|
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
|
-
${
|
|
282
|
+
${color2.dim("Actions:")}
|
|
171
283
|
`);
|
|
172
|
-
renderer.write(` ${
|
|
284
|
+
renderer.write(` ${color2.bold("a")} Add another key
|
|
173
285
|
`);
|
|
174
286
|
if (keysLength > 0) {
|
|
175
|
-
renderer.write(` ${
|
|
287
|
+
renderer.write(` ${color2.bold("u")} <n> Update key <n>
|
|
176
288
|
`);
|
|
177
|
-
renderer.write(` ${
|
|
289
|
+
renderer.write(` ${color2.bold("d")} <n> Delete key <n>
|
|
178
290
|
`);
|
|
179
|
-
renderer.write(` ${
|
|
291
|
+
renderer.write(` ${color2.bold("s")} <n> Set key <n> as active
|
|
180
292
|
`);
|
|
181
293
|
}
|
|
182
|
-
renderer.write(` ${
|
|
294
|
+
renderer.write(` ${color2.bold("f")} Edit family
|
|
183
295
|
`);
|
|
184
296
|
renderer.write(
|
|
185
|
-
` ${
|
|
297
|
+
` ${color2.bold("B")} Edit baseUrl ${color2.dim("(capital B \u2014 b is Back)")}
|
|
186
298
|
`
|
|
187
299
|
);
|
|
188
|
-
renderer.write(` ${
|
|
300
|
+
renderer.write(` ${color2.bold("m")} Edit visible model list
|
|
189
301
|
`);
|
|
190
|
-
renderer.write(` ${
|
|
302
|
+
renderer.write(` ${color2.bold("x")} Remove this provider entirely
|
|
191
303
|
`);
|
|
192
|
-
renderer.write(` ${
|
|
304
|
+
renderer.write(` ${color2.bold("b")} Back
|
|
193
305
|
`);
|
|
194
|
-
renderer.write(` ${
|
|
306
|
+
renderer.write(` ${color2.bold("q")} Quit
|
|
195
307
|
`);
|
|
196
308
|
}
|
|
197
309
|
function renderTopMenu(renderer, providers) {
|
|
198
310
|
renderer.write(`
|
|
199
|
-
${
|
|
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(
|
|
316
|
+
renderer.write(color2.dim(" No providers configured yet.\n"));
|
|
205
317
|
renderer.write(
|
|
206
|
-
|
|
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(` ${
|
|
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
|
-
${
|
|
334
|
+
${color2.dim("Actions:")}
|
|
223
335
|
`);
|
|
224
|
-
renderer.write(` ${
|
|
336
|
+
renderer.write(` ${color2.bold("a")} Add a provider (from catalog)
|
|
225
337
|
`);
|
|
226
338
|
renderer.write(
|
|
227
|
-
` ${
|
|
339
|
+
` ${color2.bold("l")} Add a local server ${color2.dim("(OmniRoute / Ollama / vLLM / LM Studio)")}
|
|
228
340
|
`
|
|
229
341
|
);
|
|
230
|
-
renderer.write(` ${
|
|
342
|
+
renderer.write(` ${color2.bold("c")} Add a custom provider
|
|
231
343
|
`);
|
|
232
344
|
renderer.write(
|
|
233
|
-
` ${
|
|
345
|
+
` ${color2.bold("s")} Login with provider OAuth
|
|
234
346
|
`
|
|
235
347
|
);
|
|
236
348
|
if (ids.length > 0) {
|
|
237
349
|
renderer.write(
|
|
238
|
-
` ${
|
|
350
|
+
` ${color2.dim("1-")}${color2.dim(String(ids.length))} ${color2.bold("Manage a provider")}
|
|
239
351
|
`
|
|
240
352
|
);
|
|
241
353
|
}
|
|
242
|
-
renderer.write(` ${
|
|
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
|
-
` ${
|
|
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(` ${
|
|
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
|
-
|
|
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(
|
|
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
|
-
` ${
|
|
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
|
-
` ${
|
|
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 ?
|
|
512
|
+
const modelHint = firstModel ? color3.cyan(firstModel) : color3.dim("<model-id>");
|
|
401
513
|
deps.renderer.write(
|
|
402
|
-
|
|
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 ? ` ${
|
|
435
|
-
const elapsed = probe.elapsedMs !== void 0 ? ` ${
|
|
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 ?
|
|
439
|
-
deps.renderer.write(` ${
|
|
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
|
-
` ${
|
|
446
|
-
${
|
|
447
|
-
${
|
|
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 ? ` ${
|
|
565
|
+
const detail = probe.detail ? ` ${color3.dim(`(${probe.detail})`)}` : "";
|
|
454
566
|
deps.renderer.write(
|
|
455
|
-
` ${
|
|
456
|
-
${
|
|
457
|
-
${
|
|
458
|
-
${
|
|
459
|
-
${
|
|
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 ? ` ${
|
|
467
|
-
deps.renderer.write(` ${
|
|
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
|
-
` ${
|
|
474
|
-
` + (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
|
-
${
|
|
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
|
-
${
|
|
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 ?
|
|
612
|
+
const auth = entry.noAuth ? color3.dim("(no auth)") : color3.dim("(optional auth)");
|
|
501
613
|
deps.renderer.write(
|
|
502
|
-
` ${
|
|
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(` ${
|
|
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
|
-
${
|
|
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
|
-
` ${
|
|
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}${
|
|
1026
|
-
|
|
1027
|
-
|
|
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
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
${
|
|
1075
|
-
` +
|
|
1076
|
-
${
|
|
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
|
-
` ${
|
|
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
|
|
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
|
|
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
|
|
744
|
+
import { color as color5, stripAnsi } from "@wrongstack/core/utils";
|
|
1142
745
|
var theme = {
|
|
1143
|
-
accent:
|
|
1144
|
-
chrome:
|
|
1145
|
-
cursor: (s) =>
|
|
1146
|
-
ctx:
|
|
1147
|
-
cost:
|
|
1148
|
-
caps:
|
|
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(
|
|
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)} ${
|
|
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 ` ${
|
|
1203
|
-
${
|
|
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(
|
|
1223
|
-
const caret = query.length > 0 ?
|
|
1224
|
-
lines.push(boxRow(`${theme.accent("\u276F")} ${
|
|
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(
|
|
1239
|
-
const id = selected ? theme.accent(
|
|
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(
|
|
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 ?
|
|
1398
|
-
lines.push(boxRow(`${theme.accent("\u276F")} ${
|
|
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(
|
|
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")} ${
|
|
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) ?
|
|
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(
|
|
1420
|
-
const name = selected ? 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
|
-
|
|
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(
|
|
1181
|
+
${theme.accent(color7.bold("\u258C WrongStack"))} ${color7.dim("Provider & Model Selection")}
|
|
1579
1182
|
`
|
|
1580
1183
|
);
|
|
1581
|
-
renderer.write(
|
|
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} ${
|
|
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(
|
|
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")} ${
|
|
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 ?
|
|
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(
|
|
1701
|
-
const suffix = isDefault ?
|
|
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(`${
|
|
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(`${
|
|
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(
|
|
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 ? ` ${
|
|
1326
|
+
const defaultHint = defaultIdx !== void 0 && defaultProvider ? ` ${color7.dim(`[Enter = ${defaultProvider}]`)}` : "";
|
|
1724
1327
|
const providerAnswer = (await reader.readLine(
|
|
1725
1328
|
`
|
|
1726
|
-
${
|
|
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(
|
|
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(
|
|
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(`${
|
|
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
|
-
|
|
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
|
-
${
|
|
1379
|
+
${color7.amber("?")} Model id ${color7.dim("(or Enter to cancel)")}: `)).trim();
|
|
1777
1380
|
if (!typed) {
|
|
1778
|
-
renderer.write(
|
|
1381
|
+
renderer.write(color7.dim("Cancelled.\n"));
|
|
1779
1382
|
return void 0;
|
|
1780
1383
|
}
|
|
1781
1384
|
renderer.write(`
|
|
1782
|
-
${
|
|
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(
|
|
1393
|
+
renderer.write(color7.dim("Cancelled.\n"));
|
|
1791
1394
|
return void 0;
|
|
1792
1395
|
}
|
|
1793
1396
|
renderer.write(
|
|
1794
1397
|
`
|
|
1795
|
-
${
|
|
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(
|
|
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(
|
|
1824
|
-
const suffix = isDefault ?
|
|
1825
|
-
const num5 =
|
|
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
|
-
${
|
|
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(
|
|
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 ? ` ${
|
|
1450
|
+
const defaultHint = defaultIdxInModels >= 0 && defaultModel ? ` ${color7.dim(`[Enter = ${defaultModel}]`)}` : "";
|
|
1848
1451
|
const answer = (await reader.readLine(
|
|
1849
1452
|
`
|
|
1850
|
-
${
|
|
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(
|
|
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
|
-
${
|
|
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(
|
|
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
|
-
${
|
|
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
|
|
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
|
-
|
|
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(` ${
|
|
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
|
-
${
|
|
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
|
-
` ${
|
|
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
|
-
|
|
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
|
-
` ${
|
|
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
|
-
` ${
|
|
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
|
-
` ${
|
|
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
|
-
${
|
|
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
|
-
` ${
|
|
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(` ${
|
|
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
|
-
` ${
|
|
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
|
-
` ${
|
|
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
|
-
` ${
|
|
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
|
-
` ${
|
|
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(` ${
|
|
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
|
-
` ${
|
|
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(` ${
|
|
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
|
-
` ${
|
|
1735
|
+
` ${color8.green("\u2713")} Saved ${color8.bold(providerId)}/${color8.bold(label)}.
|
|
2133
1736
|
`
|
|
2134
1737
|
);
|
|
2135
|
-
deps.renderer.write(
|
|
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
|
-
` ${
|
|
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
|
-
` ${
|
|
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
|
-
|
|
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-
|
|
1816
|
+
//# sourceMappingURL=chunk-ONOT5OJ2.js.map
|