nemoris 0.1.19 → 0.1.20
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/package.json +4 -2
- package/src/onboarding/auth/api-key.js +52 -12
- package/src/onboarding/clack-prompter.js +35 -6
- package/src/onboarding/phases/auth.js +266 -89
- package/src/onboarding/phases/build.js +61 -35
- package/src/onboarding/setup-checklist.js +30 -9
- package/src/onboarding/wizard.js +624 -160
- package/src/shadow/bridge.js +13 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nemoris",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Personal AI agent runtime — persistent memory, delivery guarantees, task contracts, self-healing. Local-first, no cloud.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -73,7 +73,9 @@
|
|
|
73
73
|
"publish:check": "node scripts/check-publish-dry-run.js",
|
|
74
74
|
"status": "node src/cli.js runtime-status",
|
|
75
75
|
"test": "node --test",
|
|
76
|
-
"test:e2e": "node tests/e2e/run-report.js"
|
|
76
|
+
"test:e2e": "node tests/e2e/run-report.js",
|
|
77
|
+
"test:e2e:interactive": "node --test tests/e2e/onboarding-interactive.test.js",
|
|
78
|
+
"test:e2e:setup-sweep": "node scripts/e2e-setup-sweep.js"
|
|
77
79
|
},
|
|
78
80
|
"engines": {
|
|
79
81
|
"node": ">=22.5.0"
|
|
@@ -143,19 +143,59 @@ export async function validateApiKey(provider, key, options = {}) {
|
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
const fetch = options.fetchImpl || globalThis.fetch;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
146
|
+
if (provider === "anthropic") {
|
|
147
|
+
const probes = [
|
|
148
|
+
{
|
|
149
|
+
url: "https://api.anthropic.com/v1/models",
|
|
150
|
+
method: "GET",
|
|
151
|
+
headers: {
|
|
152
|
+
"x-api-key": key,
|
|
153
|
+
"anthropic-version": "2023-06-01"
|
|
154
|
+
}
|
|
153
155
|
},
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
156
|
+
{
|
|
157
|
+
url: "https://api.anthropic.com/v1/models",
|
|
158
|
+
method: "GET",
|
|
159
|
+
headers: {
|
|
160
|
+
Authorization: `Bearer ${key}`,
|
|
161
|
+
"anthropic-version": "2023-06-01"
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
url: "https://api.anthropic.com/v1/messages/count_tokens",
|
|
166
|
+
method: "POST",
|
|
167
|
+
headers: {
|
|
168
|
+
"content-type": "application/json",
|
|
169
|
+
...buildAnthropicAuthHeaders(key)
|
|
170
|
+
},
|
|
171
|
+
body: JSON.stringify({
|
|
172
|
+
model: "claude-haiku-4-5",
|
|
173
|
+
messages: [{ role: "user", content: "ping" }]
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
let lastFailure = { ok: false, error: "request failed" };
|
|
179
|
+
for (const probe of probes) {
|
|
180
|
+
try {
|
|
181
|
+
const response = await fetch(probe.url, {
|
|
182
|
+
method: probe.method,
|
|
183
|
+
headers: probe.headers,
|
|
184
|
+
body: probe.body,
|
|
185
|
+
signal: AbortSignal.timeout(10000)
|
|
186
|
+
});
|
|
187
|
+
if (response.ok) {
|
|
188
|
+
return { ok: true, status: response.status };
|
|
189
|
+
}
|
|
190
|
+
lastFailure = { ok: false, status: response.status };
|
|
191
|
+
} catch (error) {
|
|
192
|
+
lastFailure = { ok: false, error: error.message };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return lastFailure;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const providerTargets = {
|
|
159
199
|
openai: {
|
|
160
200
|
url: "https://api.openai.com/v1/models",
|
|
161
201
|
method: "GET",
|
|
@@ -23,6 +23,27 @@ function normalizeOptions(options = []) {
|
|
|
23
23
|
}));
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function normalizeSearchTokens(search = "") {
|
|
27
|
+
return String(search)
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.split(/\s+/)
|
|
30
|
+
.map((token) => token.trim())
|
|
31
|
+
.filter(Boolean);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildOptionSearchText(option = {}) {
|
|
35
|
+
return `${option.label || ""} ${option.hint || ""} ${option.value || ""}`.toLowerCase();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function tokenizedOptionFilter(search, option) {
|
|
39
|
+
const tokens = normalizeSearchTokens(search);
|
|
40
|
+
if (tokens.length === 0) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
const haystack = buildOptionSearchText(option);
|
|
44
|
+
return tokens.every((token) => haystack.includes(token));
|
|
45
|
+
}
|
|
46
|
+
|
|
26
47
|
export function createClackPrompter() {
|
|
27
48
|
return {
|
|
28
49
|
intro(message) {
|
|
@@ -48,12 +69,20 @@ export function createClackPrompter() {
|
|
|
48
69
|
}));
|
|
49
70
|
},
|
|
50
71
|
async multiselect({ message, options, initialValues = [], required = false }) {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
72
|
+
const normalized = normalizeOptions(options);
|
|
73
|
+
const value = guardCancel(await (required || !p.autocompleteMultiselect
|
|
74
|
+
? p.multiselect({
|
|
75
|
+
message,
|
|
76
|
+
options: normalized,
|
|
77
|
+
initialValues,
|
|
78
|
+
required,
|
|
79
|
+
})
|
|
80
|
+
: p.autocompleteMultiselect({
|
|
81
|
+
message,
|
|
82
|
+
options: normalized,
|
|
83
|
+
initialValues,
|
|
84
|
+
filter: tokenizedOptionFilter,
|
|
85
|
+
})));
|
|
57
86
|
return Array.isArray(value) ? value : [];
|
|
58
87
|
},
|
|
59
88
|
async text({ message, placeholder, initialValue, validate }) {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import fs from "node:fs";
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { providerTemplate, routerTemplate } from "../templates.js";
|
|
12
|
+
import { parseToml } from "../../config/toml-lite.js";
|
|
12
13
|
import {
|
|
13
14
|
detectExistingKeys,
|
|
14
15
|
validateApiKey,
|
|
@@ -57,6 +58,10 @@ const PROVIDER_CONFIGS = {
|
|
|
57
58
|
};
|
|
58
59
|
|
|
59
60
|
const MODEL_ROLE_ORDER = ["cheap_interactive", "fallback", "manual_bump"];
|
|
61
|
+
const MODEL_ROLE_INDEX = new Map(MODEL_ROLE_ORDER.map((role, index) => [role, index]));
|
|
62
|
+
const KEEP_VALUE = "__keep__";
|
|
63
|
+
const MANUAL_VALUE = "__manual__";
|
|
64
|
+
const SHOW_ALL_VALUE = "__show_all__";
|
|
60
65
|
const PROVIDER_MODEL_PRESETS = {
|
|
61
66
|
anthropic: [
|
|
62
67
|
{
|
|
@@ -209,6 +214,28 @@ function providerDisplayName(provider) {
|
|
|
209
214
|
return provider;
|
|
210
215
|
}
|
|
211
216
|
|
|
217
|
+
function readConfiguredProviderModels(installDir, provider) {
|
|
218
|
+
const providerId = PROVIDER_CONFIGS[provider]?.providerId || provider;
|
|
219
|
+
const providerPath = path.join(installDir, "config", "providers", `${providerId}.toml`);
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const parsed = parseToml(fs.readFileSync(providerPath, "utf8"));
|
|
223
|
+
return Object.values(parsed?.models || {})
|
|
224
|
+
.sort((left, right) => {
|
|
225
|
+
const leftRank = MODEL_ROLE_INDEX.get(left?.role) ?? Number.MAX_SAFE_INTEGER;
|
|
226
|
+
const rightRank = MODEL_ROLE_INDEX.get(right?.role) ?? Number.MAX_SAFE_INTEGER;
|
|
227
|
+
if (leftRank !== rightRank) {
|
|
228
|
+
return leftRank - rightRank;
|
|
229
|
+
}
|
|
230
|
+
return String(left?.id || "").localeCompare(String(right?.id || ""));
|
|
231
|
+
})
|
|
232
|
+
.map((entry) => String(entry?.id || "").trim())
|
|
233
|
+
.filter(Boolean);
|
|
234
|
+
} catch {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
212
239
|
const OPENAI_CONTEXT_WINDOWS = {
|
|
213
240
|
"gpt-4.1": 1000000,
|
|
214
241
|
"gpt-4o": 128000,
|
|
@@ -380,15 +407,77 @@ async function buildProviderSelectionOptions(provider, key, { fetchImpl = global
|
|
|
380
407
|
return { options, fetchedModels, extra };
|
|
381
408
|
}
|
|
382
409
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
410
|
+
function normalizePickerOption(option) {
|
|
411
|
+
return {
|
|
412
|
+
value: option.value,
|
|
413
|
+
label: option.label,
|
|
414
|
+
description: option.description || option.hint || "",
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function buildExpandedPickerOptions(options, extra = []) {
|
|
419
|
+
return [
|
|
420
|
+
...options.filter((option) => !String(option.value).startsWith("__")).map(normalizePickerOption),
|
|
421
|
+
...extra.map(normalizePickerOption),
|
|
422
|
+
];
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function buildCurrentModelLabel(provider, modelId) {
|
|
426
|
+
const stripped = stripProviderPrefix(provider, modelId);
|
|
427
|
+
return stripped || modelId;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function ensureFallbackOption(options, provider, modelId) {
|
|
431
|
+
if (!modelId || options.some((option) => option.value === modelId)) {
|
|
432
|
+
return options;
|
|
433
|
+
}
|
|
434
|
+
return [
|
|
435
|
+
...options,
|
|
436
|
+
{
|
|
437
|
+
value: modelId,
|
|
438
|
+
label: buildCurrentModelLabel(provider, modelId),
|
|
439
|
+
description: "current (not in catalog)",
|
|
440
|
+
},
|
|
441
|
+
];
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async function promptManualProviderModel(provider, key, tui, fetchedModels, {
|
|
445
|
+
fetchImpl = globalThis.fetch,
|
|
446
|
+
initialValue = "",
|
|
447
|
+
} = {}) {
|
|
448
|
+
const { prompt } = tui;
|
|
449
|
+
if (!prompt) {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
while (true) {
|
|
454
|
+
const custom = await prompt("Model id", stripProviderPrefix(provider, initialValue));
|
|
455
|
+
const modelId = ensureProviderModelPrefix(provider, custom);
|
|
456
|
+
if (!modelId) {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const check = await validateModelId(provider, modelId, key, { fetchImpl, fetchedModels });
|
|
461
|
+
if (check.ok) {
|
|
462
|
+
return modelId;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
console.log(` \u274c ${check.error || "Model not found."} Try again or press Enter to cancel.`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async function promptForProviderModels(provider, key, tui, {
|
|
470
|
+
fetchImpl = globalThis.fetch,
|
|
471
|
+
currentModels = [],
|
|
472
|
+
} = {}) {
|
|
473
|
+
const { select, multiselect, dim, cyan } = tui;
|
|
474
|
+
if (!select) return { chosen: currentModels.slice(0, 3), fetchedModels: [] };
|
|
386
475
|
|
|
387
476
|
const { options, fetchedModels, extra } = await buildProviderSelectionOptions(provider, key, { fetchImpl });
|
|
388
|
-
let activeOptions = options;
|
|
477
|
+
let activeOptions = options.map(normalizePickerOption);
|
|
389
478
|
const chosen = [];
|
|
390
|
-
const
|
|
391
|
-
|
|
479
|
+
const currentDefault = currentModels[0] || "";
|
|
480
|
+
const currentFallbacks = currentModels.slice(1);
|
|
392
481
|
const defaultModelValue = options.find((item) => {
|
|
393
482
|
const v = String(item.value);
|
|
394
483
|
return !v.startsWith("__") && !v.startsWith("\u2605");
|
|
@@ -397,81 +486,111 @@ async function promptForProviderModels(provider, key, tui, { fetchImpl = globalT
|
|
|
397
486
|
console.log(`\n ${cyan(`Choose ${provider === "openrouter" ? "OpenRouter" : provider === "openai" ? "OpenAI" : "Anthropic"} models`)}`);
|
|
398
487
|
console.log(` ${dim("Pick up to three models. The first is your default. The others are fallbacks when the default is slow or unavailable.")}`);
|
|
399
488
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
// Insert "Done" before the custom-entry option at the end
|
|
410
|
-
const customIndex = pickerOptions.findIndex((item) => item.value === "__custom__");
|
|
411
|
-
const doneOption = { label: "Done", value: "__done__", description: "Continue setup with the models already selected." };
|
|
412
|
-
if (customIndex >= 0) {
|
|
413
|
-
pickerOptions.splice(customIndex, 0, doneOption);
|
|
414
|
-
} else {
|
|
415
|
-
pickerOptions.push(doneOption);
|
|
416
|
-
}
|
|
489
|
+
let defaultModel = "";
|
|
490
|
+
while (!defaultModel) {
|
|
491
|
+
const defaultOptions = [];
|
|
492
|
+
if (currentDefault) {
|
|
493
|
+
defaultOptions.push({
|
|
494
|
+
value: KEEP_VALUE,
|
|
495
|
+
label: `Keep current (${buildCurrentModelLabel(provider, currentDefault)})`,
|
|
496
|
+
description: "Use the current configured default model.",
|
|
497
|
+
});
|
|
417
498
|
}
|
|
499
|
+
defaultOptions.push({
|
|
500
|
+
value: MANUAL_VALUE,
|
|
501
|
+
label: "Enter model manually",
|
|
502
|
+
description: "Use a specific model id not shown in the list.",
|
|
503
|
+
});
|
|
504
|
+
defaultOptions.push(...activeOptions.filter((option) =>
|
|
505
|
+
option.value !== MANUAL_VALUE && option.value !== "__custom__",
|
|
506
|
+
));
|
|
418
507
|
|
|
419
|
-
const picked = await select(
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
);
|
|
423
|
-
|
|
424
|
-
if (picked === "__done__") {
|
|
508
|
+
const picked = await select("Default model:", defaultOptions);
|
|
509
|
+
if (picked === KEEP_VALUE) {
|
|
510
|
+
defaultModel = currentDefault;
|
|
425
511
|
break;
|
|
426
512
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
// Expand to full filtered list + custom entry
|
|
430
|
-
activeOptions = [
|
|
431
|
-
...activeOptions.filter((o) => !o.value.startsWith("__")),
|
|
432
|
-
...(extra || []),
|
|
433
|
-
{ value: "__custom__", label: "Enter a different model name...", description: "Use a specific model id not shown in the list." },
|
|
434
|
-
];
|
|
513
|
+
if (picked === SHOW_ALL_VALUE) {
|
|
514
|
+
activeOptions = buildExpandedPickerOptions(options, extra);
|
|
435
515
|
continue;
|
|
436
516
|
}
|
|
517
|
+
if (picked === MANUAL_VALUE || picked === "__custom__") {
|
|
518
|
+
defaultModel = await promptManualProviderModel(provider, key, tui, fetchedModels, {
|
|
519
|
+
fetchImpl,
|
|
520
|
+
initialValue: currentDefault || defaultModelValue,
|
|
521
|
+
});
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
defaultModel = picked;
|
|
525
|
+
}
|
|
437
526
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
} else {
|
|
462
|
-
console.log(` \u274c ${recheck.error || "Model not found."}`);
|
|
463
|
-
modelId = null;
|
|
464
|
-
break;
|
|
465
|
-
}
|
|
466
|
-
}
|
|
527
|
+
if (defaultModel) {
|
|
528
|
+
chosen.push(defaultModel);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const fullCatalogOptions = buildExpandedPickerOptions(options, extra)
|
|
532
|
+
.filter((option) => option.value !== chosen[0]);
|
|
533
|
+
let fallbackOptions = fullCatalogOptions;
|
|
534
|
+
for (const fallback of currentFallbacks) {
|
|
535
|
+
fallbackOptions = ensureFallbackOption(fallbackOptions, provider, fallback);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (typeof multiselect === "function" && fallbackOptions.length > 0) {
|
|
539
|
+
const initialValues = currentFallbacks.filter((modelId) =>
|
|
540
|
+
fallbackOptions.some((option) => option.value === modelId),
|
|
541
|
+
);
|
|
542
|
+
const selectedFallbacks = await multiselect(
|
|
543
|
+
"Backup models (optional)",
|
|
544
|
+
fallbackOptions,
|
|
545
|
+
initialValues,
|
|
546
|
+
);
|
|
547
|
+
for (const modelId of selectedFallbacks) {
|
|
548
|
+
if (modelId && !chosen.includes(modelId)) {
|
|
549
|
+
chosen.push(modelId);
|
|
467
550
|
}
|
|
468
|
-
if (
|
|
469
|
-
|
|
551
|
+
if (chosen.length === 3) {
|
|
552
|
+
break;
|
|
470
553
|
}
|
|
471
554
|
}
|
|
555
|
+
return { chosen, fetchedModels };
|
|
556
|
+
}
|
|
472
557
|
|
|
473
|
-
|
|
474
|
-
|
|
558
|
+
while (chosen.length < 3) {
|
|
559
|
+
const remaining = fallbackOptions.filter((option) => !chosen.includes(option.value));
|
|
560
|
+
if (remaining.length === 0) {
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const pickerOptions = [
|
|
565
|
+
...remaining,
|
|
566
|
+
{
|
|
567
|
+
label: "Done",
|
|
568
|
+
value: "__done__",
|
|
569
|
+
description: "Continue setup with the models already selected.",
|
|
570
|
+
},
|
|
571
|
+
{
|
|
572
|
+
label: "Enter model manually",
|
|
573
|
+
value: MANUAL_VALUE,
|
|
574
|
+
description: "Use a specific model id not shown in the list.",
|
|
575
|
+
},
|
|
576
|
+
];
|
|
577
|
+
|
|
578
|
+
const picked = await select(`Add another model (${chosen.length}/3 selected):`, pickerOptions);
|
|
579
|
+
if (picked === "__done__") {
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
if (picked === MANUAL_VALUE) {
|
|
583
|
+
const manualModel = await promptManualProviderModel(provider, key, tui, fetchedModels, {
|
|
584
|
+
fetchImpl,
|
|
585
|
+
initialValue: chosen.at(-1) || defaultModelValue,
|
|
586
|
+
});
|
|
587
|
+
if (manualModel && !chosen.includes(manualModel)) {
|
|
588
|
+
chosen.push(manualModel);
|
|
589
|
+
}
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
if (picked && !chosen.includes(picked)) {
|
|
593
|
+
chosen.push(picked);
|
|
475
594
|
}
|
|
476
595
|
}
|
|
477
596
|
|
|
@@ -566,6 +685,16 @@ async function promptForProviderKey(provider, tui, validateApiKeyImpl = validate
|
|
|
566
685
|
}
|
|
567
686
|
}
|
|
568
687
|
|
|
688
|
+
function formatProviderValidationFailure(provider, result = {}) {
|
|
689
|
+
if (result.status === 401 || result.status === 403) {
|
|
690
|
+
return "authentication failed";
|
|
691
|
+
}
|
|
692
|
+
if (result.status) {
|
|
693
|
+
return `HTTP ${result.status}`;
|
|
694
|
+
}
|
|
695
|
+
return result.error || `${providerDisplayName(provider)} key verification failed`;
|
|
696
|
+
}
|
|
697
|
+
|
|
569
698
|
async function confirmLocalOnlySetup(tui) {
|
|
570
699
|
const { confirm, yellow = (value) => value, dim = (value) => value } = tui;
|
|
571
700
|
if (!confirm) {
|
|
@@ -579,6 +708,7 @@ async function confirmLocalOnlySetup(tui) {
|
|
|
579
708
|
|
|
580
709
|
async function maybeResolveOpenAIOAuth({
|
|
581
710
|
tui,
|
|
711
|
+
authMethod = null,
|
|
582
712
|
enableOpenAIOAuthChoice = false,
|
|
583
713
|
openAIOAuthImpl = initiateOpenAICodexOAuthFlow,
|
|
584
714
|
}) {
|
|
@@ -597,22 +727,29 @@ async function maybeResolveOpenAIOAuth({
|
|
|
597
727
|
};
|
|
598
728
|
}
|
|
599
729
|
|
|
600
|
-
if (
|
|
730
|
+
if (authMethod === "api_key") {
|
|
601
731
|
return null;
|
|
602
732
|
}
|
|
603
733
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
734
|
+
let method = authMethod;
|
|
735
|
+
if (!method) {
|
|
736
|
+
if (!enableOpenAIOAuthChoice || !tui?.select) {
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
method = await tui.select("OpenAI auth method:", [
|
|
741
|
+
{
|
|
742
|
+
label: "API key",
|
|
743
|
+
value: "api_key",
|
|
744
|
+
description: "Paste an OpenAI API key from platform.openai.com.",
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
label: "ChatGPT OAuth",
|
|
748
|
+
value: "oauth",
|
|
749
|
+
description: "Open browser login and store a refreshable local profile.",
|
|
750
|
+
},
|
|
751
|
+
]);
|
|
752
|
+
}
|
|
616
753
|
|
|
617
754
|
if (method !== "oauth") {
|
|
618
755
|
return null;
|
|
@@ -642,6 +779,7 @@ export async function runAuthPhase(installDir, options = {}) {
|
|
|
642
779
|
const {
|
|
643
780
|
tui,
|
|
644
781
|
detectionCache,
|
|
782
|
+
authMethod = null,
|
|
645
783
|
validateApiKeyImpl = validateApiKey,
|
|
646
784
|
fetchImpl = globalThis.fetch,
|
|
647
785
|
providerOrder = ["openrouter", "anthropic", "openai"],
|
|
@@ -655,6 +793,7 @@ export async function runAuthPhase(installDir, options = {}) {
|
|
|
655
793
|
const selectedModels = {};
|
|
656
794
|
const providerAuthRefs = {};
|
|
657
795
|
const providerSecrets = {};
|
|
796
|
+
const validationFailures = {};
|
|
658
797
|
|
|
659
798
|
if (tui) {
|
|
660
799
|
let promptingComplete = false;
|
|
@@ -664,6 +803,7 @@ export async function runAuthPhase(installDir, options = {}) {
|
|
|
664
803
|
if (provider === "openai") {
|
|
665
804
|
const oauthResult = await maybeResolveOpenAIOAuth({
|
|
666
805
|
tui,
|
|
806
|
+
authMethod,
|
|
667
807
|
enableOpenAIOAuthChoice,
|
|
668
808
|
openAIOAuthImpl,
|
|
669
809
|
});
|
|
@@ -697,17 +837,51 @@ export async function runAuthPhase(installDir, options = {}) {
|
|
|
697
837
|
const validatedKeys = {};
|
|
698
838
|
const providerFlags = { anthropic: false, openrouter: false, openai: false, ollama: ollamaResult.ok };
|
|
699
839
|
|
|
700
|
-
for (const
|
|
840
|
+
for (const provider of Object.keys(keys)) {
|
|
841
|
+
let key = keys[provider];
|
|
701
842
|
if (!key) continue;
|
|
702
|
-
|
|
703
|
-
if (!format.ok)
|
|
704
|
-
|
|
843
|
+
let format = validateApiKeyFormat(provider, key);
|
|
844
|
+
if (!format.ok) {
|
|
845
|
+
validationFailures[provider] = format.error;
|
|
846
|
+
if (tui && providerOrder.includes(provider)) {
|
|
847
|
+
console.log(` ${tui.yellow("!")} Found a ${providerDisplayName(provider)} key, but it looks malformed: ${format.error}`);
|
|
848
|
+
const replacement = await promptForProviderKey(provider, tui, validateApiKeyImpl);
|
|
849
|
+
if (!replacement) {
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
key = replacement;
|
|
853
|
+
keys[provider] = replacement;
|
|
854
|
+
prevalidatedKeys.add(provider);
|
|
855
|
+
format = validateApiKeyFormat(provider, key);
|
|
856
|
+
if (!format.ok) {
|
|
857
|
+
validationFailures[provider] = format.error;
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
} else {
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
let result = prevalidatedKeys.has(provider)
|
|
705
866
|
? { ok: true, status: 200 }
|
|
706
867
|
: await validateApiKeyImpl(provider, key);
|
|
868
|
+
if (!result.ok && tui && providerOrder.includes(provider)) {
|
|
869
|
+
validationFailures[provider] = formatProviderValidationFailure(provider, result);
|
|
870
|
+
console.log(` ${tui.yellow("!")} Found a ${providerDisplayName(provider)} key, but Nemoris could not verify it (${validationFailures[provider]}).`);
|
|
871
|
+
const replacement = await promptForProviderKey(provider, tui, validateApiKeyImpl);
|
|
872
|
+
if (!replacement) {
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
key = replacement;
|
|
876
|
+
keys[provider] = replacement;
|
|
877
|
+
prevalidatedKeys.add(provider);
|
|
878
|
+
result = { ok: true, status: 200 };
|
|
879
|
+
}
|
|
707
880
|
if (!result.ok) continue;
|
|
708
881
|
|
|
709
882
|
validatedKeys[provider] = key;
|
|
710
883
|
providerSecrets[provider] = key;
|
|
884
|
+
delete validationFailures[provider];
|
|
711
885
|
if (provider in providerFlags) {
|
|
712
886
|
providerFlags[provider] = true;
|
|
713
887
|
}
|
|
@@ -722,7 +896,10 @@ export async function runAuthPhase(installDir, options = {}) {
|
|
|
722
896
|
for (const provider of ["openrouter", "anthropic", "openai"]) {
|
|
723
897
|
const providerToken = providerSecrets[provider];
|
|
724
898
|
if (!providerToken) continue;
|
|
725
|
-
const { chosen, fetchedModels } = await promptForProviderModels(provider, providerToken, tui, {
|
|
899
|
+
const { chosen, fetchedModels } = await promptForProviderModels(provider, providerToken, tui, {
|
|
900
|
+
fetchImpl,
|
|
901
|
+
currentModels: readConfiguredProviderModels(installDir, provider),
|
|
902
|
+
});
|
|
726
903
|
selectedModels[provider] = chosen;
|
|
727
904
|
fetchedModelsCache[provider] = fetchedModels;
|
|
728
905
|
}
|
|
@@ -777,5 +954,5 @@ export async function runAuthPhase(installDir, options = {}) {
|
|
|
777
954
|
}])));
|
|
778
955
|
writeRouter(installDir, providerFlags, selectedModels);
|
|
779
956
|
|
|
780
|
-
return { providers: providerIds, providerFlags, keys: validatedKeys, selectedModels };
|
|
957
|
+
return { providers: providerIds, providerFlags, keys: validatedKeys, selectedModels, validationFailures };
|
|
781
958
|
}
|