theclawbay 0.3.77 → 0.3.79
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/README.md +14 -1
- package/dist/commands/backup.d.ts +10 -0
- package/dist/commands/backup.js +51 -0
- package/dist/commands/setup.d.ts +5 -0
- package/dist/commands/setup.js +550 -14
- package/dist/lib/backups.d.ts +30 -0
- package/dist/lib/backups.js +286 -0
- package/dist/lib/codex-model-cache-migration.js +5 -1
- package/dist/lib/config/paths.d.ts +1 -0
- package/dist/lib/config/paths.js +2 -1
- package/dist/lib/supported-models.d.ts +3 -0
- package/dist/lib/supported-models.js +9 -0
- package/package.json +1 -1
- package/theclawbay-supported-models.json +32 -63
package/dist/commands/setup.js
CHANGED
|
@@ -13,6 +13,7 @@ const node_child_process_1 = require("node:child_process");
|
|
|
13
13
|
const core_1 = require("@oclif/core");
|
|
14
14
|
const yaml_1 = require("yaml");
|
|
15
15
|
const base_command_1 = require("../lib/base-command");
|
|
16
|
+
const backups_1 = require("../lib/backups");
|
|
16
17
|
const codex_auth_seeding_1 = require("../lib/codex-auth-seeding");
|
|
17
18
|
const codex_history_migration_1 = require("../lib/codex-history-migration");
|
|
18
19
|
const codex_model_cache_migration_1 = require("../lib/codex-model-cache-migration");
|
|
@@ -672,7 +673,66 @@ function powerShellQuote(value) {
|
|
|
672
673
|
return `'${value.replace(/'/g, "''")}'`;
|
|
673
674
|
}
|
|
674
675
|
function modelDisplayName(modelId) {
|
|
675
|
-
return MODEL_DISPLAY_NAMES[modelId] ?? modelId;
|
|
676
|
+
return MODEL_DISPLAY_NAMES[modelId] ?? prettyModelId(modelId);
|
|
677
|
+
}
|
|
678
|
+
function supportedModelProviderMap() {
|
|
679
|
+
return new Map((0, supported_models_1.getSupportedModels)().map((model) => [model.id, model.provider]));
|
|
680
|
+
}
|
|
681
|
+
function groupForModelId(modelId) {
|
|
682
|
+
const knownProvider = supportedModelProviderMap().get(modelId);
|
|
683
|
+
if (knownProvider === "openai") {
|
|
684
|
+
return { id: "openai", label: "OpenAI / Codex", icon: "◎", order: 0 };
|
|
685
|
+
}
|
|
686
|
+
if (knownProvider === "google") {
|
|
687
|
+
return { id: "google", label: "Google Gemini", icon: "◈", order: 1 };
|
|
688
|
+
}
|
|
689
|
+
if (knownProvider === "deepseek") {
|
|
690
|
+
return { id: "deepseek", label: "DeepSeek", icon: "◉", order: 2 };
|
|
691
|
+
}
|
|
692
|
+
if (modelId.startsWith("claude-")) {
|
|
693
|
+
return { id: "anthropic", label: "Anthropic Claude", icon: "✳", order: 3 };
|
|
694
|
+
}
|
|
695
|
+
if (modelId.startsWith("gemini-")) {
|
|
696
|
+
return { id: "google", label: "Google Gemini", icon: "◈", order: 1 };
|
|
697
|
+
}
|
|
698
|
+
if (modelId.startsWith("deepseek-")) {
|
|
699
|
+
return { id: "deepseek", label: "DeepSeek", icon: "◉", order: 2 };
|
|
700
|
+
}
|
|
701
|
+
if (modelId.startsWith("gpt-") || modelId.startsWith("codex-")) {
|
|
702
|
+
return { id: "openai", label: "OpenAI / Codex", icon: "◎", order: 0 };
|
|
703
|
+
}
|
|
704
|
+
return { id: "other", label: "Other models", icon: "•", order: 9 };
|
|
705
|
+
}
|
|
706
|
+
function groupModelOptions(models) {
|
|
707
|
+
return models.map((model) => ({
|
|
708
|
+
...model,
|
|
709
|
+
provider: groupForModelId(model.id),
|
|
710
|
+
}));
|
|
711
|
+
}
|
|
712
|
+
function groupModelSections(models) {
|
|
713
|
+
const sections = new Map();
|
|
714
|
+
for (const model of groupModelOptions(models)) {
|
|
715
|
+
const existing = sections.get(model.provider.id);
|
|
716
|
+
if (existing) {
|
|
717
|
+
existing.models.push(model);
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
sections.set(model.provider.id, { provider: model.provider, models: [model] });
|
|
721
|
+
}
|
|
722
|
+
return [...sections.values()]
|
|
723
|
+
.sort((left, right) => left.provider.order - right.provider.order || left.provider.label.localeCompare(right.provider.label))
|
|
724
|
+
.map((section) => ({
|
|
725
|
+
provider: section.provider,
|
|
726
|
+
models: [...section.models].sort((left, right) => left.name.localeCompare(right.name)),
|
|
727
|
+
}));
|
|
728
|
+
}
|
|
729
|
+
function prettyModelId(modelId) {
|
|
730
|
+
return modelId
|
|
731
|
+
.replace(/^claude-/, "Claude ")
|
|
732
|
+
.replace(/^gemini-/, "Gemini ")
|
|
733
|
+
.replace(/^deepseek-/, "DeepSeek ")
|
|
734
|
+
.replace(/-/g, " ")
|
|
735
|
+
.replace(/\b\w/g, (match) => match.toUpperCase());
|
|
676
736
|
}
|
|
677
737
|
function publicApiOriginForBackendUrl(backendUrl) {
|
|
678
738
|
const trimmed = trimTrailingSlash(backendUrl);
|
|
@@ -690,6 +750,39 @@ function publicApiOriginForBackendUrl(backendUrl) {
|
|
|
690
750
|
return trimmed;
|
|
691
751
|
}
|
|
692
752
|
}
|
|
753
|
+
function isPrivateHostname(hostname) {
|
|
754
|
+
const normalized = hostname.trim().toLowerCase();
|
|
755
|
+
if (!normalized)
|
|
756
|
+
return false;
|
|
757
|
+
if (normalized === "localhost" ||
|
|
758
|
+
normalized === "127.0.0.1" ||
|
|
759
|
+
normalized === "::1" ||
|
|
760
|
+
normalized === "[::1]" ||
|
|
761
|
+
normalized === "0.0.0.0") {
|
|
762
|
+
return true;
|
|
763
|
+
}
|
|
764
|
+
if (normalized.startsWith("10.") || normalized.startsWith("192.168.") || normalized.startsWith("127.")) {
|
|
765
|
+
return true;
|
|
766
|
+
}
|
|
767
|
+
const match = normalized.match(/^172\.(\d{1,3})\./);
|
|
768
|
+
if (!match)
|
|
769
|
+
return false;
|
|
770
|
+
const secondOctet = Number.parseInt(match[1] ?? "", 10);
|
|
771
|
+
return Number.isInteger(secondOctet) && secondOctet >= 16 && secondOctet <= 31;
|
|
772
|
+
}
|
|
773
|
+
function shouldPreferPublicSetupBackend(backendUrl) {
|
|
774
|
+
const trimmed = backendUrl?.trim();
|
|
775
|
+
if (!trimmed)
|
|
776
|
+
return false;
|
|
777
|
+
try {
|
|
778
|
+
const parsed = new URL(trimmed);
|
|
779
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
780
|
+
return hostname !== THECLAWBAY_CANONICAL_API_HOST && !THECLAWBAY_WEBSITE_HOSTS.has(hostname) && isPrivateHostname(hostname);
|
|
781
|
+
}
|
|
782
|
+
catch {
|
|
783
|
+
return false;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
693
786
|
function isTheClawBayChatgptBaseUrl(value) {
|
|
694
787
|
const trimmed = trimTrailingSlash(value.trim());
|
|
695
788
|
if (!trimmed)
|
|
@@ -1228,6 +1321,319 @@ async function resolveDeviceLabel(params) {
|
|
|
1228
1321
|
rl.close();
|
|
1229
1322
|
}
|
|
1230
1323
|
}
|
|
1324
|
+
async function promptForGroupedModelSelection(params) {
|
|
1325
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY || params.models.length <= 1) {
|
|
1326
|
+
return uniqueStrings(params.defaultSelectedIds);
|
|
1327
|
+
}
|
|
1328
|
+
const sections = groupModelSections(params.models);
|
|
1329
|
+
const knownIds = new Set(params.models.map((model) => model.id));
|
|
1330
|
+
const selected = new Set(uniqueStrings(params.defaultSelectedIds).filter((modelId) => knownIds.has(modelId)));
|
|
1331
|
+
const rows = [];
|
|
1332
|
+
for (const [sectionIndex, section] of sections.entries()) {
|
|
1333
|
+
rows.push({ kind: "group", sectionIndex });
|
|
1334
|
+
for (const [modelIndex] of section.models.entries()) {
|
|
1335
|
+
rows.push({ kind: "model", sectionIndex, modelIndex });
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
rows.push({ kind: "apply" });
|
|
1339
|
+
const stdin = process.stdin;
|
|
1340
|
+
const stdout = process.stdout;
|
|
1341
|
+
const wasRaw = stdin.isRaw;
|
|
1342
|
+
const ansi = {
|
|
1343
|
+
reset: "\x1b[0m",
|
|
1344
|
+
bold: "\x1b[1m",
|
|
1345
|
+
dim: "\x1b[2m",
|
|
1346
|
+
inverse: "\x1b[7m",
|
|
1347
|
+
green: "\x1b[32m",
|
|
1348
|
+
yellow: "\x1b[33m",
|
|
1349
|
+
gray: "\x1b[90m",
|
|
1350
|
+
};
|
|
1351
|
+
const paint = (text, ...codes) => `${codes.join("")}${text}${ansi.reset}`;
|
|
1352
|
+
const clearScreen = () => {
|
|
1353
|
+
stdout.write("\x1b[2J\x1b[H");
|
|
1354
|
+
};
|
|
1355
|
+
const rowForModel = (sectionIndex, modelIndex) => rows.findIndex((row) => row.kind === "model" && row.sectionIndex === sectionIndex && row.modelIndex === modelIndex);
|
|
1356
|
+
let settled = false;
|
|
1357
|
+
let hint = "Use ↑/↓ to move. Press Enter or Space to toggle a model or a provider header.";
|
|
1358
|
+
let cursor = Math.max(0, rows.findIndex((row) => {
|
|
1359
|
+
if (row.kind !== "model")
|
|
1360
|
+
return false;
|
|
1361
|
+
const model = sections[row.sectionIndex]?.models[row.modelIndex];
|
|
1362
|
+
return model?.id === params.defaultSelectedIds[0];
|
|
1363
|
+
}));
|
|
1364
|
+
const groupState = (sectionIndex) => {
|
|
1365
|
+
const models = sections[sectionIndex]?.models ?? [];
|
|
1366
|
+
const selectedCount = models.filter((model) => selected.has(model.id)).length;
|
|
1367
|
+
if (selectedCount === 0)
|
|
1368
|
+
return "[ ]";
|
|
1369
|
+
if (selectedCount === models.length)
|
|
1370
|
+
return "[x]";
|
|
1371
|
+
return "[-]";
|
|
1372
|
+
};
|
|
1373
|
+
const setGroupSelection = (sectionIndex, nextValue) => {
|
|
1374
|
+
for (const model of sections[sectionIndex]?.models ?? []) {
|
|
1375
|
+
if (nextValue)
|
|
1376
|
+
selected.add(model.id);
|
|
1377
|
+
else
|
|
1378
|
+
selected.delete(model.id);
|
|
1379
|
+
}
|
|
1380
|
+
};
|
|
1381
|
+
const toggleCurrent = () => {
|
|
1382
|
+
const current = rows[cursor];
|
|
1383
|
+
if (!current)
|
|
1384
|
+
return;
|
|
1385
|
+
if (current.kind === "apply") {
|
|
1386
|
+
finish();
|
|
1387
|
+
return;
|
|
1388
|
+
}
|
|
1389
|
+
if (current.kind === "group") {
|
|
1390
|
+
const state = groupState(current.sectionIndex);
|
|
1391
|
+
const nextValue = state !== "[x]";
|
|
1392
|
+
setGroupSelection(current.sectionIndex, nextValue);
|
|
1393
|
+
hint = `${sections[current.sectionIndex]?.provider.label ?? "Group"} ${nextValue ? "selected" : "cleared"}.`;
|
|
1394
|
+
render();
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
const model = sections[current.sectionIndex]?.models[current.modelIndex];
|
|
1398
|
+
if (!model)
|
|
1399
|
+
return;
|
|
1400
|
+
if (selected.has(model.id))
|
|
1401
|
+
selected.delete(model.id);
|
|
1402
|
+
else
|
|
1403
|
+
selected.add(model.id);
|
|
1404
|
+
hint = `${model.name} ${selected.has(model.id) ? "selected" : "cleared"}.`;
|
|
1405
|
+
render();
|
|
1406
|
+
};
|
|
1407
|
+
const finish = () => {
|
|
1408
|
+
if (settled)
|
|
1409
|
+
return;
|
|
1410
|
+
settled = true;
|
|
1411
|
+
stdin.off("keypress", onKeypress);
|
|
1412
|
+
if (stdin.isTTY)
|
|
1413
|
+
stdin.setRawMode(Boolean(wasRaw));
|
|
1414
|
+
stdout.write("\x1b[?25h");
|
|
1415
|
+
clearScreen();
|
|
1416
|
+
resolvePromise([...selected].filter((modelId) => knownIds.has(modelId)));
|
|
1417
|
+
};
|
|
1418
|
+
const render = () => {
|
|
1419
|
+
clearScreen();
|
|
1420
|
+
stdout.write(`${paint(params.title, ansi.bold)}\n`);
|
|
1421
|
+
stdout.write(`${paint("Each provider header toggles that provider on or off. Press A to toggle all models.", ansi.dim, ansi.gray)}\n`);
|
|
1422
|
+
stdout.write(`${paint("Move to Apply and press Enter when you're ready.", ansi.dim, ansi.gray)}\n\n`);
|
|
1423
|
+
for (const [sectionIndex, section] of sections.entries()) {
|
|
1424
|
+
const currentRow = rows[cursor];
|
|
1425
|
+
const groupCursor = currentRow?.kind === "group" && currentRow.sectionIndex === sectionIndex;
|
|
1426
|
+
const groupPointer = groupCursor ? paint("→", ansi.bold, ansi.yellow) : " ";
|
|
1427
|
+
const groupMark = groupState(sectionIndex);
|
|
1428
|
+
stdout.write(`${groupPointer} ${paint(groupMark, groupMark === "[x]" ? ansi.green : ansi.gray)} ${section.provider.icon} ${paint(`${section.provider.label} — Select all`, ansi.bold)}\n`);
|
|
1429
|
+
for (const [modelIndex, model] of section.models.entries()) {
|
|
1430
|
+
const activeRow = rows[cursor];
|
|
1431
|
+
const isActive = activeRow?.kind === "model" &&
|
|
1432
|
+
activeRow.sectionIndex === sectionIndex &&
|
|
1433
|
+
activeRow.modelIndex === modelIndex;
|
|
1434
|
+
const pointer = isActive ? paint("→", ansi.bold, ansi.yellow) : " ";
|
|
1435
|
+
const mark = selected.has(model.id) ? paint("[x]", ansi.green) : paint("[ ]", ansi.gray);
|
|
1436
|
+
stdout.write(`${pointer} ${mark} ${model.name} ${paint(`(${model.id})`, ansi.dim, ansi.gray)}\n`);
|
|
1437
|
+
}
|
|
1438
|
+
stdout.write("\n");
|
|
1439
|
+
}
|
|
1440
|
+
const applyActive = rows[cursor]?.kind === "apply";
|
|
1441
|
+
const applyStyled = applyActive
|
|
1442
|
+
? paint(` ${params.applyLabel} (${selected.size} selected) `, ansi.inverse, ansi.bold, ansi.green)
|
|
1443
|
+
: paint(`${params.applyLabel} (${selected.size} selected)`, ansi.bold, ansi.green);
|
|
1444
|
+
stdout.write(`${applyActive ? "→" : " "} ${applyStyled}\n`);
|
|
1445
|
+
stdout.write(`\n${paint(hint, ansi.dim, ansi.gray)}\n`);
|
|
1446
|
+
};
|
|
1447
|
+
const onKeypress = (_str, key) => {
|
|
1448
|
+
if (key.ctrl && key.name === "c") {
|
|
1449
|
+
stdout.write("\x1b[?25h");
|
|
1450
|
+
process.exit(130);
|
|
1451
|
+
}
|
|
1452
|
+
if (key.name === "up" || key.name === "k") {
|
|
1453
|
+
cursor = (cursor - 1 + rows.length) % rows.length;
|
|
1454
|
+
render();
|
|
1455
|
+
return;
|
|
1456
|
+
}
|
|
1457
|
+
if (key.name === "down" || key.name === "j") {
|
|
1458
|
+
cursor = (cursor + 1) % rows.length;
|
|
1459
|
+
render();
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
if (key.name === "space" || key.name === "return" || key.name === "enter") {
|
|
1463
|
+
toggleCurrent();
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1466
|
+
if (key.name === "a") {
|
|
1467
|
+
const nextValue = sections.some((section) => section.models.some((model) => !selected.has(model.id)));
|
|
1468
|
+
for (const [sectionIndex] of sections.entries())
|
|
1469
|
+
setGroupSelection(sectionIndex, nextValue);
|
|
1470
|
+
hint = `${nextValue ? "Selected" : "Cleared"} all available models.`;
|
|
1471
|
+
render();
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
if (key.name === "c") {
|
|
1475
|
+
finish();
|
|
1476
|
+
}
|
|
1477
|
+
};
|
|
1478
|
+
let resolvePromise = () => { };
|
|
1479
|
+
const result = new Promise((resolve) => {
|
|
1480
|
+
resolvePromise = resolve;
|
|
1481
|
+
});
|
|
1482
|
+
(0, node_readline_1.emitKeypressEvents)(stdin);
|
|
1483
|
+
if (stdin.isTTY)
|
|
1484
|
+
stdin.setRawMode(true);
|
|
1485
|
+
stdout.write("\x1b[?25l");
|
|
1486
|
+
stdin.on("keypress", onKeypress);
|
|
1487
|
+
render();
|
|
1488
|
+
return result;
|
|
1489
|
+
}
|
|
1490
|
+
async function promptForPrimaryModel(models, defaultModel) {
|
|
1491
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY || models.length <= 1) {
|
|
1492
|
+
return defaultModel;
|
|
1493
|
+
}
|
|
1494
|
+
const sections = groupModelSections(models);
|
|
1495
|
+
const flattened = sections.flatMap((section) => section.models);
|
|
1496
|
+
const stdin = process.stdin;
|
|
1497
|
+
const stdout = process.stdout;
|
|
1498
|
+
const wasRaw = stdin.isRaw;
|
|
1499
|
+
const ansi = {
|
|
1500
|
+
reset: "\x1b[0m",
|
|
1501
|
+
bold: "\x1b[1m",
|
|
1502
|
+
dim: "\x1b[2m",
|
|
1503
|
+
green: "\x1b[32m",
|
|
1504
|
+
yellow: "\x1b[33m",
|
|
1505
|
+
gray: "\x1b[90m",
|
|
1506
|
+
};
|
|
1507
|
+
const paint = (text, ...codes) => `${codes.join("")}${text}${ansi.reset}`;
|
|
1508
|
+
const clearScreen = () => {
|
|
1509
|
+
stdout.write("\x1b[2J\x1b[H");
|
|
1510
|
+
};
|
|
1511
|
+
let cursor = Math.max(0, flattened.findIndex((model) => model.id === defaultModel));
|
|
1512
|
+
let settled = false;
|
|
1513
|
+
let resolvePromise = () => { };
|
|
1514
|
+
const finish = (modelId) => {
|
|
1515
|
+
if (settled)
|
|
1516
|
+
return;
|
|
1517
|
+
settled = true;
|
|
1518
|
+
stdin.off("keypress", onKeypress);
|
|
1519
|
+
if (stdin.isTTY)
|
|
1520
|
+
stdin.setRawMode(Boolean(wasRaw));
|
|
1521
|
+
stdout.write("\x1b[?25h");
|
|
1522
|
+
clearScreen();
|
|
1523
|
+
resolvePromise(modelId);
|
|
1524
|
+
};
|
|
1525
|
+
const render = () => {
|
|
1526
|
+
clearScreen();
|
|
1527
|
+
stdout.write(`${paint("Choose the default model for local setup", ansi.bold)}\n`);
|
|
1528
|
+
stdout.write(`${paint("Use ↑/↓ to move and Enter to select the default model.", ansi.dim, ansi.gray)}\n\n`);
|
|
1529
|
+
let index = 0;
|
|
1530
|
+
for (const section of sections) {
|
|
1531
|
+
stdout.write(`${paint(`${section.provider.icon} ${section.provider.label}`, ansi.bold)}\n`);
|
|
1532
|
+
for (const model of section.models) {
|
|
1533
|
+
const active = index === cursor;
|
|
1534
|
+
const pointer = active ? paint("→", ansi.bold, ansi.yellow) : " ";
|
|
1535
|
+
const defaultSuffix = model.id === defaultModel ? paint(" default", ansi.dim, ansi.gray) : "";
|
|
1536
|
+
stdout.write(`${pointer} ${model.name} ${paint(`(${model.id})`, ansi.dim, ansi.gray)}${defaultSuffix}\n`);
|
|
1537
|
+
index += 1;
|
|
1538
|
+
}
|
|
1539
|
+
stdout.write("\n");
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1542
|
+
const onKeypress = (_str, key) => {
|
|
1543
|
+
if (key.ctrl && key.name === "c") {
|
|
1544
|
+
stdout.write("\x1b[?25h");
|
|
1545
|
+
process.exit(130);
|
|
1546
|
+
}
|
|
1547
|
+
if (key.name === "up" || key.name === "k") {
|
|
1548
|
+
cursor = (cursor - 1 + flattened.length) % flattened.length;
|
|
1549
|
+
render();
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
if (key.name === "down" || key.name === "j") {
|
|
1553
|
+
cursor = (cursor + 1) % flattened.length;
|
|
1554
|
+
render();
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
if (key.name === "return" || key.name === "enter" || key.name === "space") {
|
|
1558
|
+
finish(flattened[cursor]?.id ?? defaultModel);
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
const result = new Promise((resolve) => {
|
|
1562
|
+
resolvePromise = resolve;
|
|
1563
|
+
});
|
|
1564
|
+
(0, node_readline_1.emitKeypressEvents)(stdin);
|
|
1565
|
+
if (stdin.isTTY)
|
|
1566
|
+
stdin.setRawMode(true);
|
|
1567
|
+
stdout.write("\x1b[?25l");
|
|
1568
|
+
stdin.on("keypress", onKeypress);
|
|
1569
|
+
render();
|
|
1570
|
+
return result;
|
|
1571
|
+
}
|
|
1572
|
+
async function resolveConfiguredModelSelection(params) {
|
|
1573
|
+
const requestedModel = params.requestedModel?.trim();
|
|
1574
|
+
const requestedModels = params.requestedModels
|
|
1575
|
+
?.split(",")
|
|
1576
|
+
.map((entry) => entry.trim())
|
|
1577
|
+
.filter(Boolean);
|
|
1578
|
+
const availableIds = new Set(params.resolved.models.map((entry) => entry.id));
|
|
1579
|
+
if (requestedModel && availableIds.size > 0 && !availableIds.has(requestedModel)) {
|
|
1580
|
+
throw new Error(`Requested model "${requestedModel}" is not advertised by the live backend.`);
|
|
1581
|
+
}
|
|
1582
|
+
if (requestedModels?.length) {
|
|
1583
|
+
for (const modelId of requestedModels) {
|
|
1584
|
+
if (availableIds.size > 0 && !availableIds.has(modelId)) {
|
|
1585
|
+
throw new Error(`Requested model "${modelId}" is not advertised by the live backend.`);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
let selectedModels;
|
|
1590
|
+
if (requestedModels?.length) {
|
|
1591
|
+
selectedModels = params.resolved.models.filter((entry) => requestedModels.includes(entry.id));
|
|
1592
|
+
}
|
|
1593
|
+
else if (params.skipPrompt || !process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1594
|
+
selectedModels = params.resolved.models;
|
|
1595
|
+
}
|
|
1596
|
+
else if (params.resolved.models.length > 1) {
|
|
1597
|
+
const selectedIds = new Set(await promptForGroupedModelSelection({
|
|
1598
|
+
title: "Choose the OpenAI-compatible models to save into local tools",
|
|
1599
|
+
models: params.resolved.models,
|
|
1600
|
+
defaultSelectedIds: params.resolved.models.map((entry) => entry.id),
|
|
1601
|
+
applyLabel: "Apply model selection",
|
|
1602
|
+
}));
|
|
1603
|
+
selectedModels = params.resolved.models.filter((entry) => selectedIds.has(entry.id));
|
|
1604
|
+
}
|
|
1605
|
+
else {
|
|
1606
|
+
selectedModels = params.resolved.models;
|
|
1607
|
+
}
|
|
1608
|
+
const fallbackModels = selectedModels.length > 0 ? selectedModels : params.resolved.models;
|
|
1609
|
+
const selectedModelId = requestedModel && fallbackModels.some((entry) => entry.id === requestedModel)
|
|
1610
|
+
? requestedModel
|
|
1611
|
+
: params.skipPrompt || !process.stdin.isTTY || !process.stdout.isTTY
|
|
1612
|
+
? fallbackModels.some((entry) => entry.id === params.resolved.model)
|
|
1613
|
+
? params.resolved.model
|
|
1614
|
+
: fallbackModels[0]?.id ?? params.resolved.model
|
|
1615
|
+
: await promptForPrimaryModel(fallbackModels, fallbackModels.some((entry) => entry.id === params.resolved.model)
|
|
1616
|
+
? params.resolved.model
|
|
1617
|
+
: (fallbackModels[0]?.id ?? params.resolved.model));
|
|
1618
|
+
const noteParts = [];
|
|
1619
|
+
if (requestedModels?.length) {
|
|
1620
|
+
noteParts.push(`Configured ${requestedModels.length} OpenAI-compatible models.`);
|
|
1621
|
+
}
|
|
1622
|
+
else if (selectedModels.length > 0 && selectedModels.length < params.resolved.models.length) {
|
|
1623
|
+
noteParts.push(`Configured ${selectedModels.length} OpenAI-compatible models for local tools.`);
|
|
1624
|
+
}
|
|
1625
|
+
if (selectedModelId !== params.resolved.model) {
|
|
1626
|
+
noteParts.push(`Using ${selectedModelId} as the default configured model for local tools.`);
|
|
1627
|
+
}
|
|
1628
|
+
if (availableIds.size === 0 && requestedModel && params.resolved.failure) {
|
|
1629
|
+
noteParts.push(`Could not verify requested model ${requestedModel} against the live backend (${params.resolved.failure}).`);
|
|
1630
|
+
}
|
|
1631
|
+
return {
|
|
1632
|
+
model: selectedModelId,
|
|
1633
|
+
models: fallbackModels,
|
|
1634
|
+
note: noteParts.join(" ").trim() || undefined,
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1231
1637
|
function summarizeModelFetchFailure(detail) {
|
|
1232
1638
|
const normalized = detail.replace(/\s+/g, " ").trim();
|
|
1233
1639
|
if (!normalized)
|
|
@@ -1540,6 +1946,12 @@ async function detectHermesClient() {
|
|
|
1540
1946
|
}
|
|
1541
1947
|
return false;
|
|
1542
1948
|
}
|
|
1949
|
+
function isOpenAiCompatibleSetupModelId(modelId) {
|
|
1950
|
+
const trimmed = modelId.trim();
|
|
1951
|
+
if (!trimmed)
|
|
1952
|
+
return false;
|
|
1953
|
+
return !trimmed.startsWith("claude-");
|
|
1954
|
+
}
|
|
1543
1955
|
function formatDotEnvValue(value) {
|
|
1544
1956
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
1545
1957
|
}
|
|
@@ -1621,7 +2033,8 @@ async function writeHermesConfig(params) {
|
|
|
1621
2033
|
}
|
|
1622
2034
|
async function resolveModels(backendUrl, apiKey) {
|
|
1623
2035
|
const { ids, failure } = await fetchBackendModelIds(backendUrl, apiKey);
|
|
1624
|
-
const
|
|
2036
|
+
const filteredIds = (ids ?? []).filter(isOpenAiCompatibleSetupModelId);
|
|
2037
|
+
const available = new Set(filteredIds);
|
|
1625
2038
|
let selected = DEFAULT_CODEX_MODEL;
|
|
1626
2039
|
let note;
|
|
1627
2040
|
const authRejected = isCredentialRejectedFailure(failure);
|
|
@@ -1631,29 +2044,29 @@ async function resolveModels(backendUrl, apiKey) {
|
|
|
1631
2044
|
break;
|
|
1632
2045
|
}
|
|
1633
2046
|
}
|
|
1634
|
-
if (
|
|
1635
|
-
selected =
|
|
2047
|
+
if (filteredIds.length && !available.has(selected)) {
|
|
2048
|
+
selected = filteredIds[0] ?? DEFAULT_CODEX_MODEL;
|
|
1636
2049
|
note = "No preferred Codex model advertised by backend; selected first available model.";
|
|
1637
2050
|
}
|
|
1638
|
-
else if (!
|
|
2051
|
+
else if (!filteredIds.length) {
|
|
1639
2052
|
note = failure
|
|
1640
2053
|
? `Unable to query backend model list (${failure}); defaulted to ${DEFAULT_CODEX_MODEL}.`
|
|
1641
2054
|
: `Unable to query backend model list; defaulted to ${DEFAULT_CODEX_MODEL}.`;
|
|
1642
2055
|
}
|
|
1643
2056
|
const unique = [];
|
|
1644
2057
|
const pushUnique = (modelId) => {
|
|
1645
|
-
if (!modelId || unique.includes(modelId))
|
|
2058
|
+
if (!modelId || !isOpenAiCompatibleSetupModelId(modelId) || unique.includes(modelId))
|
|
1646
2059
|
return;
|
|
1647
2060
|
unique.push(modelId);
|
|
1648
2061
|
};
|
|
1649
2062
|
pushUnique(selected);
|
|
1650
2063
|
for (const modelId of DEFAULT_MODELS)
|
|
1651
2064
|
pushUnique(modelId);
|
|
1652
|
-
for (const modelId of
|
|
2065
|
+
for (const modelId of filteredIds)
|
|
1653
2066
|
pushUnique(modelId);
|
|
1654
2067
|
return {
|
|
1655
2068
|
model: selected,
|
|
1656
|
-
models: unique.map((modelId) => ({ id: modelId, name: modelDisplayName(modelId) })),
|
|
2069
|
+
models: unique.map((modelId) => ({ id: modelId, name: modelDisplayName(modelId) || prettyModelId(modelId) })),
|
|
1657
2070
|
note,
|
|
1658
2071
|
failure,
|
|
1659
2072
|
authRejected,
|
|
@@ -1678,6 +2091,45 @@ async function resolveClaudeAccess(backendUrl, apiKey) {
|
|
|
1678
2091
|
authRejected,
|
|
1679
2092
|
};
|
|
1680
2093
|
}
|
|
2094
|
+
async function resolveConfiguredClaudeModels(params) {
|
|
2095
|
+
const requestedModels = params.requestedModels
|
|
2096
|
+
?.split(",")
|
|
2097
|
+
.map((entry) => entry.trim())
|
|
2098
|
+
.filter(Boolean);
|
|
2099
|
+
if (!requestedModels?.length || !params.access.enabled) {
|
|
2100
|
+
const selectedModels = !requestedModels?.length &&
|
|
2101
|
+
params.access.enabled &&
|
|
2102
|
+
!params.skipPrompt &&
|
|
2103
|
+
process.stdin.isTTY &&
|
|
2104
|
+
process.stdout.isTTY &&
|
|
2105
|
+
params.access.models.length > 1
|
|
2106
|
+
? await promptForGroupedModelSelection({
|
|
2107
|
+
title: "Choose the Claude models to save into Claude-compatible tools",
|
|
2108
|
+
models: params.access.models.map((modelId) => ({
|
|
2109
|
+
id: modelId,
|
|
2110
|
+
name: prettyModelId(modelId),
|
|
2111
|
+
})),
|
|
2112
|
+
defaultSelectedIds: params.access.models,
|
|
2113
|
+
applyLabel: "Apply Claude selection",
|
|
2114
|
+
})
|
|
2115
|
+
: params.access.models;
|
|
2116
|
+
return {
|
|
2117
|
+
...params.access,
|
|
2118
|
+
selectedModels,
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
const available = new Set(params.access.models);
|
|
2122
|
+
for (const modelId of requestedModels) {
|
|
2123
|
+
if (!available.has(modelId)) {
|
|
2124
|
+
throw new Error(`Requested Claude model "${modelId}" is not advertised by the live Claude endpoint.`);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
return {
|
|
2128
|
+
...params.access,
|
|
2129
|
+
selectedModels: requestedModels,
|
|
2130
|
+
note: `Configured Claude-compatible clients with ${requestedModels.join(", ")}.`,
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
1681
2133
|
async function writeCodexConfig(params) {
|
|
1682
2134
|
const configPath = node_path_1.default.join(paths_1.codexDir, "config.toml");
|
|
1683
2135
|
await promises_1.default.mkdir(paths_1.codexDir, { recursive: true });
|
|
@@ -2993,13 +3445,25 @@ class SetupCommand extends base_command_1.BaseCommand {
|
|
|
2993
3445
|
let deviceLabel = managedAuthType === "device-session"
|
|
2994
3446
|
? managed?.deviceLabel ?? null
|
|
2995
3447
|
: null;
|
|
3448
|
+
const inferredBackend = explicitApiKey || managedAuthType === "api-key"
|
|
3449
|
+
? (0, api_key_1.tryInferBackendUrlFromApiKey)(explicitApiKey || managedCredential)
|
|
3450
|
+
: null;
|
|
3451
|
+
const managedBackendUrl = managed?.backendUrl ?? null;
|
|
2996
3452
|
const backendRaw = flags.backend ??
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
: null) ??
|
|
3000
|
-
managed?.backendUrl ??
|
|
3453
|
+
inferredBackend ??
|
|
3454
|
+
(shouldPreferPublicSetupBackend(managedBackendUrl) ? DEFAULT_BACKEND_URL : managedBackendUrl) ??
|
|
3001
3455
|
DEFAULT_BACKEND_URL;
|
|
3002
3456
|
const backendUrl = normalizeUrl(backendRaw, "--backend");
|
|
3457
|
+
const reusingLocalManagedCredential = !explicitApiKey &&
|
|
3458
|
+
!flags.backend &&
|
|
3459
|
+
shouldPreferPublicSetupBackend(managedBackendUrl) &&
|
|
3460
|
+
authCredential.length > 0;
|
|
3461
|
+
if (reusingLocalManagedCredential) {
|
|
3462
|
+
authType = "device-session";
|
|
3463
|
+
authCredential = "";
|
|
3464
|
+
deviceSessionId = null;
|
|
3465
|
+
deviceLabel = null;
|
|
3466
|
+
}
|
|
3003
3467
|
const [codexDetected, claudeDetected, continueDetected, clineDetected, gsdDetected, openCodeDetected, kiloDetected, rooDetected, traeDetected, aiderDetected, zoDetected, hermesDetected] = await Promise.all([
|
|
3004
3468
|
detectCodexClient(),
|
|
3005
3469
|
detectClaudeClient(),
|
|
@@ -3176,6 +3640,7 @@ class SetupCommand extends base_command_1.BaseCommand {
|
|
|
3176
3640
|
const progress = this.createProgressHandle(!debugOutput);
|
|
3177
3641
|
let resolved = null;
|
|
3178
3642
|
let claudeAccess = null;
|
|
3643
|
+
let backupSummary = null;
|
|
3179
3644
|
let updatedShellFiles = [];
|
|
3180
3645
|
let updatedFishFiles = [];
|
|
3181
3646
|
let updatedPowerShellProfiles = [];
|
|
@@ -3208,14 +3673,56 @@ class SetupCommand extends base_command_1.BaseCommand {
|
|
|
3208
3673
|
if (resolved.authRejected) {
|
|
3209
3674
|
throw new Error(describeCredentialRejection(authType, resolved.failure));
|
|
3210
3675
|
}
|
|
3676
|
+
progress.stop();
|
|
3677
|
+
const configuredModel = await resolveConfiguredModelSelection({
|
|
3678
|
+
resolved,
|
|
3679
|
+
requestedModel: flags.model,
|
|
3680
|
+
requestedModels: flags.models,
|
|
3681
|
+
skipPrompt: flags.yes,
|
|
3682
|
+
});
|
|
3683
|
+
resolved = {
|
|
3684
|
+
...resolved,
|
|
3685
|
+
model: configuredModel.model,
|
|
3686
|
+
models: configuredModel.models,
|
|
3687
|
+
note: [resolved.note, configuredModel.note].filter(Boolean).join(" ").trim() || undefined,
|
|
3688
|
+
};
|
|
3211
3689
|
}
|
|
3212
3690
|
progress.update("Checking Claude access");
|
|
3213
3691
|
claudeAccess = await resolveClaudeAccess(backendUrl, authCredential);
|
|
3214
3692
|
if (!resolved?.authRejected && claudeAccess.authRejected) {
|
|
3215
3693
|
throw new Error(describeCredentialRejection(authType, claudeAccess.failure));
|
|
3216
3694
|
}
|
|
3695
|
+
progress.stop();
|
|
3696
|
+
claudeAccess = await resolveConfiguredClaudeModels({
|
|
3697
|
+
access: claudeAccess,
|
|
3698
|
+
requestedModels: flags["claude-models"],
|
|
3699
|
+
skipPrompt: flags.yes,
|
|
3700
|
+
});
|
|
3701
|
+
if (flags["list-models"]) {
|
|
3702
|
+
if (resolved) {
|
|
3703
|
+
this.log("\nOpenAI-compatible models:");
|
|
3704
|
+
for (const section of groupModelSections(resolved.models)) {
|
|
3705
|
+
this.log(`${section.provider.icon} ${section.provider.label}`);
|
|
3706
|
+
for (const model of section.models) {
|
|
3707
|
+
const marker = model.id === resolved.model ? " (default)" : "";
|
|
3708
|
+
this.log(` - ${model.id}${marker}`);
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
if (claudeAccess.models.length > 0) {
|
|
3713
|
+
this.log("\nClaude models:");
|
|
3714
|
+
for (const modelId of claudeAccess.models) {
|
|
3715
|
+
const marker = claudeAccess.selectedModels?.includes(modelId) ? " (selected)" : "";
|
|
3716
|
+
this.log(` - ${modelId}${marker}`);
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3217
3720
|
const claudeSelected = selectedSetupClients.has("claude");
|
|
3218
3721
|
const claudeEnvEnabled = claudeSelected && claudeAccess.enabled;
|
|
3722
|
+
if (flags.backup) {
|
|
3723
|
+
progress.update("Creating local config backup");
|
|
3724
|
+
backupSummary = await (0, backups_1.createBackup)("pre-setup backup");
|
|
3725
|
+
}
|
|
3219
3726
|
progress.update("Saving shared machine config");
|
|
3220
3727
|
await (0, config_1.writeManagedConfig)({
|
|
3221
3728
|
backendUrl,
|
|
@@ -3257,7 +3764,7 @@ class SetupCommand extends base_command_1.BaseCommand {
|
|
|
3257
3764
|
claudeDesktop3pConfigPathManaged = await writeClaudeDesktop3pConfig({
|
|
3258
3765
|
backendUrl,
|
|
3259
3766
|
apiKey: authCredential,
|
|
3260
|
-
claudeModels: claudeAccess?.enabled ? claudeAccess.models : [],
|
|
3767
|
+
claudeModels: claudeAccess?.enabled ? (claudeAccess.selectedModels ?? claudeAccess.models) : [],
|
|
3261
3768
|
});
|
|
3262
3769
|
}
|
|
3263
3770
|
else {
|
|
@@ -3345,7 +3852,7 @@ class SetupCommand extends base_command_1.BaseCommand {
|
|
|
3345
3852
|
backendUrl,
|
|
3346
3853
|
model: resolved?.model ?? DEFAULT_CODEX_MODEL,
|
|
3347
3854
|
models: resolved?.models ?? [{ id: DEFAULT_CODEX_MODEL, name: modelDisplayName(DEFAULT_CODEX_MODEL) }],
|
|
3348
|
-
claudeModels: claudeAccess?.enabled ? claudeAccess.models : [],
|
|
3855
|
+
claudeModels: claudeAccess?.enabled ? (claudeAccess.selectedModels ?? claudeAccess.models) : [],
|
|
3349
3856
|
apiKey: authCredential,
|
|
3350
3857
|
});
|
|
3351
3858
|
}
|
|
@@ -3413,6 +3920,9 @@ class SetupCommand extends base_command_1.BaseCommand {
|
|
|
3413
3920
|
const summaryNotes = new Set();
|
|
3414
3921
|
if (resolved?.note)
|
|
3415
3922
|
summaryNotes.add(resolved.note);
|
|
3923
|
+
if (backupSummary) {
|
|
3924
|
+
summaryNotes.add(`Created a local backup snapshot (${backupSummary.id}) before changing client configs.`);
|
|
3925
|
+
}
|
|
3416
3926
|
if (selectedSetupClients.has("claude") && claudeAccess?.enabled) {
|
|
3417
3927
|
summaryNotes.add("Claude Code: exported ANTHROPIC_BASE_URL and ANTHROPIC_API_KEY.");
|
|
3418
3928
|
if (claudeDesktop3pConfigPathManaged) {
|
|
@@ -3467,6 +3977,9 @@ class SetupCommand extends base_command_1.BaseCommand {
|
|
|
3467
3977
|
this.log(`- Managed config: ${paths_1.managedConfigPath}`);
|
|
3468
3978
|
this.log(`- Backend: ${backendUrl}`);
|
|
3469
3979
|
this.log(`- Auth mode: ${authType}`);
|
|
3980
|
+
if (backupSummary) {
|
|
3981
|
+
this.log(`- Backup: ${backupSummary.id} (${backupSummary.directory})`);
|
|
3982
|
+
}
|
|
3470
3983
|
if (authType === "device-session") {
|
|
3471
3984
|
this.log(`- Device session id: ${deviceSessionId ?? "n/a"}`);
|
|
3472
3985
|
this.log(`- Device label: ${deviceLabel ?? "n/a"}`);
|
|
@@ -3724,6 +4237,29 @@ SetupCommand.flags = {
|
|
|
3724
4237
|
required: false,
|
|
3725
4238
|
description: "Detected local clients to configure: codex, claude, continue, cline, gsd, openclaw, opencode, kilo, roo, trae, aider, zo, hermes",
|
|
3726
4239
|
}),
|
|
4240
|
+
model: core_1.Flags.string({
|
|
4241
|
+
required: false,
|
|
4242
|
+
description: "Default OpenAI-compatible model to configure for local tools",
|
|
4243
|
+
}),
|
|
4244
|
+
models: core_1.Flags.string({
|
|
4245
|
+
required: false,
|
|
4246
|
+
description: "Comma-separated OpenAI-compatible model ids to save into local tools",
|
|
4247
|
+
}),
|
|
4248
|
+
"claude-models": core_1.Flags.string({
|
|
4249
|
+
required: false,
|
|
4250
|
+
description: "Comma-separated Claude model ids to configure for Claude-compatible tools",
|
|
4251
|
+
}),
|
|
4252
|
+
"list-models": core_1.Flags.boolean({
|
|
4253
|
+
required: false,
|
|
4254
|
+
default: false,
|
|
4255
|
+
description: "Print the live OpenAI-compatible and Claude model ids discovered during setup",
|
|
4256
|
+
}),
|
|
4257
|
+
backup: core_1.Flags.boolean({
|
|
4258
|
+
required: false,
|
|
4259
|
+
allowNo: true,
|
|
4260
|
+
default: true,
|
|
4261
|
+
description: "Create a local backup before changing client configs",
|
|
4262
|
+
}),
|
|
3727
4263
|
yes: core_1.Flags.boolean({
|
|
3728
4264
|
required: false,
|
|
3729
4265
|
char: "y",
|