@wrongstack/cli 0.317.2 → 0.319.0
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 +1 -22
- package/dist/{acp-CEQINXWB.js → acp-5ANIAOZC.js} +4 -4
- package/dist/arg-parser.d.ts +4 -0
- package/dist/{auth-3OSF6BMU.js → auth-KF5TAD7V.js} +10 -10
- package/dist/{chunk-GKHUHAIK.js → chunk-42JD25KU.js} +5 -5
- package/dist/{chunk-QYIIOMLE.js → chunk-E4I2TF5S.js} +2 -2
- package/dist/{chunk-37SPPLUD.js → chunk-F5GA2QW2.js} +15 -2
- package/dist/chunk-GGGUZSAU.js +235 -0
- package/dist/{chunk-QC5I4VWZ.js → chunk-H7GABTGS.js} +6 -2
- package/dist/chunk-OZDE3OWD.js +52 -0
- package/dist/chunk-P2OKBDCH.js +1166 -0
- package/dist/chunk-PCBJSXYV.js +65 -0
- package/dist/{chunk-KG4X5F7Z.js → chunk-VFCJOWJB.js} +10 -12
- package/dist/chunk-VWCZL4OH.js +46 -0
- package/dist/{chunk-YX6KUIDF.js → chunk-XHIEA6LG.js} +861 -29
- package/dist/{chunk-BXFYBQKR.js → chunk-XPX432BZ.js} +8 -1
- package/dist/cli-context-LH4QP4W2.js +3369 -0
- package/dist/{cli-main-ILRTNPDP.js → cli-main-72XUFUCL.js} +50 -44
- package/dist/{execution-BN4QIFH6.js → execution-DKJDNPYF.js} +17 -17
- package/dist/index.js +20 -4440
- package/dist/{modeldiag-4NYBRSFP.js → modeldiag-5FRMT6FQ.js} +2 -2
- package/dist/per-subcommand-help-6VCIKDUW.js +23 -0
- package/dist/{plugin-usage-AP7SJCDA.js → plugin-usage-CYUORYM6.js} +2 -2
- package/dist/{plugins-HQ5FU2AM.js → plugins-RV3OBBND.js} +3 -3
- package/dist/pre-launch/first-run.d.ts +51 -0
- package/dist/pre-launch.d.ts +1 -0
- package/dist/{providers-models-663NDPRQ.js → providers-models-OKKQZIWU.js} +2 -2
- package/dist/{sessions-config-P2LK2L32.js → sessions-config-KAMEBVOX.js} +4 -4
- package/dist/short-circuit-flags-U6CJAF6J.js +11 -0
- package/dist/subcommands-J4JMWC5W.js +10 -0
- package/dist/{webui-server-OJLLM6PE.js → webui-server-SY4EQCKD.js} +5 -5
- package/package.json +28 -28
- package/dist/chunk-L6ZNCTRJ.js +0 -1067
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
} from "./chunk-
|
|
2
|
+
appendHistory,
|
|
3
|
+
backupCurrent
|
|
4
|
+
} from "./chunk-B4JLSXZB.js";
|
|
5
|
+
import {
|
|
6
|
+
hasApiKey,
|
|
7
|
+
isKeylessLocalProvider,
|
|
8
|
+
visibleModelIds
|
|
9
|
+
} from "./chunk-VUVOMXWP.js";
|
|
5
10
|
import {
|
|
6
11
|
openBrowser,
|
|
7
12
|
runCodexOAuthLogin,
|
|
8
13
|
startLoopbackServer
|
|
9
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-E4I2TF5S.js";
|
|
10
15
|
import {
|
|
11
16
|
activeLabel,
|
|
12
17
|
loadConfigProviders,
|
|
@@ -15,7 +20,7 @@ import {
|
|
|
15
20
|
normalizeKeys,
|
|
16
21
|
nowIso,
|
|
17
22
|
writeKeysBack
|
|
18
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-XPX432BZ.js";
|
|
19
24
|
|
|
20
25
|
// src/auth-menu/local.ts
|
|
21
26
|
import { DefaultSecretScrubber as DefaultSecretScrubber2 } from "@wrongstack/core/security";
|
|
@@ -98,6 +103,10 @@ async function loadProviders(deps) {
|
|
|
98
103
|
});
|
|
99
104
|
}
|
|
100
105
|
|
|
106
|
+
// src/auth-menu/local-presets.ts
|
|
107
|
+
import { projectLocalProviderPresets } from "@wrongstack/providers/definitions";
|
|
108
|
+
var LOCAL_LLM_PRESETS = projectLocalProviderPresets();
|
|
109
|
+
|
|
101
110
|
// src/auth-menu/shared.ts
|
|
102
111
|
import { color } from "@wrongstack/core/utils";
|
|
103
112
|
function renderProviderLine(renderer, id, cfg, idx) {
|
|
@@ -1074,9 +1083,824 @@ async function runOAuthLoginMenu(deps) {
|
|
|
1074
1083
|
await runOAuthLoginChoice(deps, pick);
|
|
1075
1084
|
}
|
|
1076
1085
|
|
|
1086
|
+
// src/picker.ts
|
|
1087
|
+
import os from "node:os";
|
|
1088
|
+
import {
|
|
1089
|
+
color as color8,
|
|
1090
|
+
expectDefined as expectDefined2,
|
|
1091
|
+
setOutputLineGuard as setOutputLineGuard2,
|
|
1092
|
+
setRawMode as setRawMode2,
|
|
1093
|
+
stripAnsi as stripAnsi2,
|
|
1094
|
+
toErrorMessage,
|
|
1095
|
+
writeOut as writeOut2
|
|
1096
|
+
} from "@wrongstack/core/utils";
|
|
1097
|
+
|
|
1098
|
+
// src/picker-key-state.ts
|
|
1099
|
+
function applyPickerKey(state, chunk, matchCount) {
|
|
1100
|
+
if (chunk === "\x1B[A") return { ...state, selected: Math.max(0, state.selected - 1) };
|
|
1101
|
+
if (chunk === "\x1B[B")
|
|
1102
|
+
return { ...state, selected: Math.min(Math.max(0, matchCount - 1), state.selected + 1) };
|
|
1103
|
+
if (chunk === "\x1B") return { ...state, query: "", selected: 0, status: "typing" };
|
|
1104
|
+
if (chunk.charCodeAt(0) === 27) return state;
|
|
1105
|
+
let query = state.query;
|
|
1106
|
+
let selected = state.selected;
|
|
1107
|
+
const status = "typing";
|
|
1108
|
+
for (const ch of chunk) {
|
|
1109
|
+
if (ch === "\r" || ch === "\n") {
|
|
1110
|
+
return { ...state, status: matchCount > 0 ? "submitted" : "typing" };
|
|
1111
|
+
}
|
|
1112
|
+
if (ch === "") return { ...state, status: "cancelled" };
|
|
1113
|
+
if (ch === "") {
|
|
1114
|
+
query = "";
|
|
1115
|
+
selected = 0;
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
if (ch === "\x7F" || ch === "\b") {
|
|
1119
|
+
query = query.slice(0, -1);
|
|
1120
|
+
selected = 0;
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
if (ch >= " " && ch !== "\x7F") {
|
|
1124
|
+
query += ch;
|
|
1125
|
+
selected = 0;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return { query, selected, status };
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// src/picker-model-picker.ts
|
|
1132
|
+
import {
|
|
1133
|
+
color as color7,
|
|
1134
|
+
expectDefined,
|
|
1135
|
+
setOutputLineGuard,
|
|
1136
|
+
setRawMode,
|
|
1137
|
+
writeOut
|
|
1138
|
+
} from "@wrongstack/core/utils";
|
|
1139
|
+
|
|
1140
|
+
// src/picker-ui.ts
|
|
1141
|
+
import { color as color6, stripAnsi } from "@wrongstack/core/utils";
|
|
1142
|
+
var theme = {
|
|
1143
|
+
accent: color6.amber,
|
|
1144
|
+
chrome: color6.gray,
|
|
1145
|
+
cursor: (s) => color6.amber(color6.bold(s)),
|
|
1146
|
+
ctx: color6.cyan,
|
|
1147
|
+
cost: color6.green,
|
|
1148
|
+
caps: color6.magenta
|
|
1149
|
+
};
|
|
1150
|
+
var BOX = {
|
|
1151
|
+
tl: "\u256D",
|
|
1152
|
+
tr: "\u256E",
|
|
1153
|
+
bl: "\u2570",
|
|
1154
|
+
br: "\u256F",
|
|
1155
|
+
h: "\u2500",
|
|
1156
|
+
v: "\u2502"
|
|
1157
|
+
};
|
|
1158
|
+
var PICKER_MIN_WIDTH = 74;
|
|
1159
|
+
var PICKER_MAX_WIDTH = 100;
|
|
1160
|
+
function pickerWidth() {
|
|
1161
|
+
const cols = process.stdout.columns;
|
|
1162
|
+
if (typeof cols !== "number" || !Number.isFinite(cols) || cols <= 0) {
|
|
1163
|
+
return PICKER_MIN_WIDTH;
|
|
1164
|
+
}
|
|
1165
|
+
return Math.max(PICKER_MIN_WIDTH, Math.min(PICKER_MAX_WIDTH, cols - 2));
|
|
1166
|
+
}
|
|
1167
|
+
function visibleWidth(s) {
|
|
1168
|
+
return stripAnsi(s).length;
|
|
1169
|
+
}
|
|
1170
|
+
function padVisible(s, width) {
|
|
1171
|
+
const w = visibleWidth(s);
|
|
1172
|
+
if (w === width) return s;
|
|
1173
|
+
if (w < width) return s + " ".repeat(width - w);
|
|
1174
|
+
const plain = stripAnsi(s);
|
|
1175
|
+
return `${plain.slice(0, Math.max(0, width - 1))}\u2026`;
|
|
1176
|
+
}
|
|
1177
|
+
function boxTop(title) {
|
|
1178
|
+
const width = pickerWidth();
|
|
1179
|
+
const label = ` ${title} `;
|
|
1180
|
+
const pad = Math.max(0, width - visibleWidth(label) - 1);
|
|
1181
|
+
return theme.chrome(
|
|
1182
|
+
`${BOX.tl}${BOX.h}${theme.accent(color6.bold(label))}${theme.chrome(BOX.h.repeat(pad))}${BOX.tr}`
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
function boxBottom() {
|
|
1186
|
+
return theme.chrome(`${BOX.bl}${BOX.h.repeat(pickerWidth())}${BOX.br}`);
|
|
1187
|
+
}
|
|
1188
|
+
function boxRow(content) {
|
|
1189
|
+
return `${theme.chrome(BOX.v)} ${padVisible(content, pickerWidth() - 2)} ${theme.chrome(BOX.v)}`;
|
|
1190
|
+
}
|
|
1191
|
+
function boxDivider() {
|
|
1192
|
+
return theme.chrome(`${BOX.v}${BOX.h.repeat(pickerWidth())}${BOX.v}`);
|
|
1193
|
+
}
|
|
1194
|
+
function keyHints(pairs) {
|
|
1195
|
+
return pairs.map(([k, label]) => `${theme.accent(k)} ${color6.dim(label)}`).join(color6.dim(" \xB7 "));
|
|
1196
|
+
}
|
|
1197
|
+
var CODEX_PROVIDER_ID = "openai-codex";
|
|
1198
|
+
var CODEX_PICKER_HEADER = "Select Model and Effort";
|
|
1199
|
+
var CODEX_LEGACY_NOTE = "Access legacy models by running `wstack -m <model_name>` or in your `config.json`.";
|
|
1200
|
+
function codexPickerPreamble(provider) {
|
|
1201
|
+
if (provider.id !== CODEX_PROVIDER_ID) return "";
|
|
1202
|
+
return ` ${color6.bold(CODEX_PICKER_HEADER)}
|
|
1203
|
+
${color6.dim(` ${CODEX_LEGACY_NOTE}`)}
|
|
1204
|
+
|
|
1205
|
+
`;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// src/picker-model-picker.ts
|
|
1209
|
+
var LIVE_PICKER_MAX_VISIBLE = 15;
|
|
1210
|
+
function filterModels(query, models) {
|
|
1211
|
+
const q = query.trim().toLowerCase();
|
|
1212
|
+
if (!q) return [...models];
|
|
1213
|
+
return models.filter((m) => m.id.toLowerCase().includes(q) || m.name.toLowerCase().includes(q));
|
|
1214
|
+
}
|
|
1215
|
+
function renderLiveModelList(query, filtered, selectedIdx, header) {
|
|
1216
|
+
const ordered = [...filtered].sort(
|
|
1217
|
+
(a, b) => (b.release_date ?? "").localeCompare(a.release_date ?? "")
|
|
1218
|
+
);
|
|
1219
|
+
const visible = ordered.slice(0, LIVE_PICKER_MAX_VISIBLE);
|
|
1220
|
+
const lines = [];
|
|
1221
|
+
lines.push(boxTop("Select a model"));
|
|
1222
|
+
lines.push(boxRow(color7.dim(header)));
|
|
1223
|
+
const caret = query.length > 0 ? color7.dim("\u258F") : theme.cursor("\u258F");
|
|
1224
|
+
lines.push(boxRow(`${theme.accent("\u276F")} ${color7.dim("Select model:")} ${query}${caret}`));
|
|
1225
|
+
lines.push(boxDivider());
|
|
1226
|
+
let flat = 0;
|
|
1227
|
+
for (const m of visible) {
|
|
1228
|
+
const selected = flat === selectedIdx;
|
|
1229
|
+
const marker = selected ? theme.cursor("\u25B6") : " ";
|
|
1230
|
+
const ctxRaw = m.limit?.context ? `${(m.limit.context / 1e3).toFixed(0)}k` : "?";
|
|
1231
|
+
const ctx = theme.ctx(ctxRaw.padStart(6));
|
|
1232
|
+
const costRaw = m.cost?.input !== void 0 ? `$${m.cost.input}/$${m.cost.output ?? "?"}` : "";
|
|
1233
|
+
const cost = costRaw ? theme.cost(costRaw) : "";
|
|
1234
|
+
const capTags = [];
|
|
1235
|
+
if (m.tool_call) capTags.push("tools");
|
|
1236
|
+
if (m.reasoning) capTags.push("reason");
|
|
1237
|
+
if (m.modalities?.input?.includes("image")) capTags.push("vision");
|
|
1238
|
+
const caps = capTags.map((c) => theme.caps(c)).join(color7.dim(" "));
|
|
1239
|
+
const id = selected ? theme.accent(color7.bold(m.id)) : m.id;
|
|
1240
|
+
lines.push(boxRow(`${marker} ${padVisible(id, 34)} ${ctx} ${padVisible(cost, 12)} ${caps}`));
|
|
1241
|
+
flat++;
|
|
1242
|
+
}
|
|
1243
|
+
if (ordered.length > LIVE_PICKER_MAX_VISIBLE) {
|
|
1244
|
+
lines.push(
|
|
1245
|
+
boxRow(color7.dim(` \u2026 ${ordered.length - LIVE_PICKER_MAX_VISIBLE} more \u2014 type to filter`))
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
lines.push(boxDivider());
|
|
1249
|
+
lines.push(
|
|
1250
|
+
boxRow(
|
|
1251
|
+
keyHints([
|
|
1252
|
+
["\u2191\u2193", "move"],
|
|
1253
|
+
["Enter", "select"],
|
|
1254
|
+
["Esc", "clear"],
|
|
1255
|
+
["Ctrl+C", "quit"]
|
|
1256
|
+
])
|
|
1257
|
+
)
|
|
1258
|
+
);
|
|
1259
|
+
lines.push(boxBottom());
|
|
1260
|
+
return lines.join("\n");
|
|
1261
|
+
}
|
|
1262
|
+
async function runLiveModelPicker(provider, defaultModel) {
|
|
1263
|
+
const stdin = process.stdin;
|
|
1264
|
+
const out = process.stdout;
|
|
1265
|
+
if (!stdin.isTTY || !out.isTTY) return void 0;
|
|
1266
|
+
const header = `${provider.name} (${provider.id}) models:`;
|
|
1267
|
+
const byNewest = (a, b) => (b.release_date ?? "").localeCompare(a.release_date ?? "");
|
|
1268
|
+
const ranked = [...provider.models].sort(byNewest);
|
|
1269
|
+
const defaultIdx = defaultModel !== void 0 ? ranked.findIndex((m) => m.id === defaultModel) : -1;
|
|
1270
|
+
setOutputLineGuard(null);
|
|
1271
|
+
let state = {
|
|
1272
|
+
query: "",
|
|
1273
|
+
selected: defaultIdx >= 0 ? defaultIdx : 0,
|
|
1274
|
+
status: "typing"
|
|
1275
|
+
};
|
|
1276
|
+
const order = (filtered) => [...filtered].sort(byNewest);
|
|
1277
|
+
let ordered = order(filterModels(state.query, provider.models));
|
|
1278
|
+
const visibleCount = () => Math.min(ordered.length, LIVE_PICKER_MAX_VISIBLE);
|
|
1279
|
+
const clamp = () => {
|
|
1280
|
+
if (state.selected >= visibleCount()) state.selected = Math.max(0, visibleCount() - 1);
|
|
1281
|
+
};
|
|
1282
|
+
clamp();
|
|
1283
|
+
const preamble = codexPickerPreamble(provider);
|
|
1284
|
+
if (preamble) writeOut(preamble);
|
|
1285
|
+
let frame = renderLiveModelList(state.query, ordered, state.selected, header);
|
|
1286
|
+
writeOut(frame);
|
|
1287
|
+
return new Promise((resolve) => {
|
|
1288
|
+
const wasRaw = stdin.isRaw;
|
|
1289
|
+
const wasPaused = stdin.isPaused();
|
|
1290
|
+
setRawMode(stdin, true);
|
|
1291
|
+
stdin.resume();
|
|
1292
|
+
stdin.setEncoding("utf8");
|
|
1293
|
+
const cleanup = () => {
|
|
1294
|
+
stdin.off("data", onData);
|
|
1295
|
+
setRawMode(stdin, wasRaw);
|
|
1296
|
+
if (wasPaused) stdin.pause();
|
|
1297
|
+
};
|
|
1298
|
+
const repaint = () => {
|
|
1299
|
+
const ups = (frame.match(/\n/g) ?? []).length;
|
|
1300
|
+
writeOut(`\x1B[${ups}A\r\x1B[J`);
|
|
1301
|
+
ordered = order(filterModels(state.query, provider.models));
|
|
1302
|
+
clamp();
|
|
1303
|
+
frame = renderLiveModelList(state.query, ordered, state.selected, header);
|
|
1304
|
+
writeOut(frame);
|
|
1305
|
+
};
|
|
1306
|
+
const onData = (chunk) => {
|
|
1307
|
+
ordered = order(filterModels(state.query, provider.models));
|
|
1308
|
+
state = applyPickerKey(state, chunk, visibleCount());
|
|
1309
|
+
ordered = order(filterModels(state.query, provider.models));
|
|
1310
|
+
clamp();
|
|
1311
|
+
if (state.status === "cancelled") {
|
|
1312
|
+
cleanup();
|
|
1313
|
+
writeOut("\n");
|
|
1314
|
+
resolve(void 0);
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
if (state.status === "submitted") {
|
|
1318
|
+
if (ordered.length === 0) {
|
|
1319
|
+
state = { ...state, status: "typing" };
|
|
1320
|
+
repaint();
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
const pick = ordered[state.selected] ?? expectDefined(ordered[0]);
|
|
1324
|
+
cleanup();
|
|
1325
|
+
writeOut("\n");
|
|
1326
|
+
resolve(pick);
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
repaint();
|
|
1330
|
+
};
|
|
1331
|
+
stdin.on("data", onData);
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// src/picker.ts
|
|
1336
|
+
function appendLocalPresetProviders(merged) {
|
|
1337
|
+
const existing = new Set(merged.map((p) => p.id));
|
|
1338
|
+
for (const preset of LOCAL_LLM_PRESETS) {
|
|
1339
|
+
if (existing.has(preset.id)) continue;
|
|
1340
|
+
merged.push({
|
|
1341
|
+
id: preset.id,
|
|
1342
|
+
name: preset.label,
|
|
1343
|
+
family: "openai-compatible",
|
|
1344
|
+
apiBase: preset.defaultBaseUrl,
|
|
1345
|
+
// No env vars → with a loopback apiBase this reads as a keyless
|
|
1346
|
+
// local gateway, so the picker offers it without a key.
|
|
1347
|
+
envVars: [],
|
|
1348
|
+
// No catalog models yet; OmniRoute/LM Studio/etc. auto-discover at
|
|
1349
|
+
// boot or the user picks `m` to set a list. An empty list still
|
|
1350
|
+
// lets the provider be chosen — pickModel surfaces the "no models
|
|
1351
|
+
// listed" hint and the user can type an id directly.
|
|
1352
|
+
models: []
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
return merged;
|
|
1356
|
+
}
|
|
1357
|
+
function filterProviders(query, providers) {
|
|
1358
|
+
const q = query.trim().toLowerCase();
|
|
1359
|
+
if (!q) return [...providers];
|
|
1360
|
+
return providers.filter(
|
|
1361
|
+
(p) => p.id.toLowerCase().includes(q) || p.name.toLowerCase().includes(q)
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1364
|
+
var PROVIDER_FAMILY_PREFERRED_ORDER = [
|
|
1365
|
+
"anthropic",
|
|
1366
|
+
"anthropic-oauth",
|
|
1367
|
+
"openai",
|
|
1368
|
+
"openai-codex",
|
|
1369
|
+
"github-copilot",
|
|
1370
|
+
"google",
|
|
1371
|
+
"openai-compatible"
|
|
1372
|
+
];
|
|
1373
|
+
function orderProvidersForDisplay(filtered) {
|
|
1374
|
+
const families = /* @__PURE__ */ new Map();
|
|
1375
|
+
for (const p of filtered) {
|
|
1376
|
+
const arr = families.get(p.family) ?? [];
|
|
1377
|
+
arr.push(p);
|
|
1378
|
+
families.set(p.family, arr);
|
|
1379
|
+
}
|
|
1380
|
+
const order = [
|
|
1381
|
+
...PROVIDER_FAMILY_PREFERRED_ORDER.filter((f) => families.has(f)),
|
|
1382
|
+
...[...families.keys()].filter((f) => !PROVIDER_FAMILY_PREFERRED_ORDER.includes(f))
|
|
1383
|
+
];
|
|
1384
|
+
const flat = [];
|
|
1385
|
+
for (const fam of order) {
|
|
1386
|
+
const arr = (families.get(fam) ?? []).slice().sort((a, b) => a.id.toLowerCase().localeCompare(b.id.toLowerCase()));
|
|
1387
|
+
flat.push(...arr);
|
|
1388
|
+
}
|
|
1389
|
+
return flat;
|
|
1390
|
+
}
|
|
1391
|
+
function renderLiveProviderList(query, filtered, selectedIdx, savedSet) {
|
|
1392
|
+
const ordered = orderProvidersForDisplay(filtered);
|
|
1393
|
+
const scrollOffset = Math.max(0, selectedIdx - LIVE_PICKER_MAX_VISIBLE + 1);
|
|
1394
|
+
const visible = ordered.slice(scrollOffset, scrollOffset + LIVE_PICKER_MAX_VISIBLE);
|
|
1395
|
+
const lines = [];
|
|
1396
|
+
lines.push(boxTop("Select a provider"));
|
|
1397
|
+
const caret = query.length > 0 ? color8.dim("\u258F") : theme.cursor("\u258F");
|
|
1398
|
+
lines.push(boxRow(`${theme.accent("\u276F")} ${color8.dim("Select provider:")} ${query}${caret}`));
|
|
1399
|
+
lines.push(boxDivider());
|
|
1400
|
+
if (scrollOffset > 0) {
|
|
1401
|
+
lines.push(boxRow(color8.dim(` ${scrollOffset} more above \u2191`)));
|
|
1402
|
+
}
|
|
1403
|
+
let flat = 0;
|
|
1404
|
+
let lastFamily = "";
|
|
1405
|
+
for (const p of visible) {
|
|
1406
|
+
if (p.family !== lastFamily) {
|
|
1407
|
+
lines.push(boxRow(`${theme.accent("\xB7")} ${color8.dim(p.family.toUpperCase())}`));
|
|
1408
|
+
lastFamily = p.family;
|
|
1409
|
+
}
|
|
1410
|
+
const selected = flat === selectedIdx - scrollOffset;
|
|
1411
|
+
const sel = selected ? theme.cursor("\u25B6") : " ";
|
|
1412
|
+
let marker;
|
|
1413
|
+
if (savedSet) {
|
|
1414
|
+
const mark = savedSet.has(p.id) ? color8.cyan("\u25C9") : color8.dim("\u25CB");
|
|
1415
|
+
marker = `${sel} ${mark}`;
|
|
1416
|
+
} else {
|
|
1417
|
+
marker = `${sel}`;
|
|
1418
|
+
}
|
|
1419
|
+
const id = selected ? theme.accent(color8.bold(p.id)) : p.id;
|
|
1420
|
+
const name = selected ? p.name : color8.dim(p.name);
|
|
1421
|
+
lines.push(boxRow(`${marker} ${padVisible(id, 24)} ${name}`));
|
|
1422
|
+
flat++;
|
|
1423
|
+
}
|
|
1424
|
+
if (scrollOffset + LIVE_PICKER_MAX_VISIBLE < ordered.length) {
|
|
1425
|
+
lines.push(
|
|
1426
|
+
boxRow(
|
|
1427
|
+
color8.dim(` ${ordered.length - scrollOffset - LIVE_PICKER_MAX_VISIBLE} more below \u2193`)
|
|
1428
|
+
)
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
lines.push(boxDivider());
|
|
1432
|
+
lines.push(
|
|
1433
|
+
boxRow(
|
|
1434
|
+
keyHints([
|
|
1435
|
+
["\u2191\u2193", "scroll"],
|
|
1436
|
+
["Enter", "select"],
|
|
1437
|
+
["Esc", "clear"],
|
|
1438
|
+
["Ctrl+C", "quit"]
|
|
1439
|
+
])
|
|
1440
|
+
)
|
|
1441
|
+
);
|
|
1442
|
+
lines.push(boxBottom());
|
|
1443
|
+
return lines.join("\n");
|
|
1444
|
+
}
|
|
1445
|
+
async function saveToGlobalConfig(configPath, provider, model, homeFn = () => process.env.HOME ?? os.homedir()) {
|
|
1446
|
+
try {
|
|
1447
|
+
const { atomicWrite: atomicWrite2 } = await import("@wrongstack/core/utils");
|
|
1448
|
+
const fs2 = await import("node:fs/promises");
|
|
1449
|
+
let existing = {};
|
|
1450
|
+
try {
|
|
1451
|
+
const raw = await fs2.readFile(configPath, "utf8");
|
|
1452
|
+
existing = JSON.parse(raw);
|
|
1453
|
+
} catch {
|
|
1454
|
+
}
|
|
1455
|
+
const oldCfg = { ...existing };
|
|
1456
|
+
existing.provider = provider;
|
|
1457
|
+
existing.model = model;
|
|
1458
|
+
try {
|
|
1459
|
+
await backupCurrent(homeFn, configPath);
|
|
1460
|
+
} catch (err) {
|
|
1461
|
+
console.warn(
|
|
1462
|
+
JSON.stringify({
|
|
1463
|
+
level: "warn",
|
|
1464
|
+
event: "picker.backup_failed",
|
|
1465
|
+
message: toErrorMessage(err),
|
|
1466
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1467
|
+
})
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
await atomicWrite2(configPath, JSON.stringify(existing, null, 2), { mode: 384 });
|
|
1471
|
+
try {
|
|
1472
|
+
await appendHistory(
|
|
1473
|
+
oldCfg,
|
|
1474
|
+
existing,
|
|
1475
|
+
`Provider/model changed: ${oldCfg.provider ?? "(none)"} \u2192 ${provider}, ${oldCfg.model ?? "(none)"} \u2192 ${model}`,
|
|
1476
|
+
homeFn,
|
|
1477
|
+
configPath
|
|
1478
|
+
);
|
|
1479
|
+
} catch {
|
|
1480
|
+
}
|
|
1481
|
+
return true;
|
|
1482
|
+
} catch (err) {
|
|
1483
|
+
console.warn(
|
|
1484
|
+
JSON.stringify({
|
|
1485
|
+
level: "warn",
|
|
1486
|
+
event: "picker.save_failed",
|
|
1487
|
+
message: toErrorMessage(err),
|
|
1488
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1489
|
+
})
|
|
1490
|
+
);
|
|
1491
|
+
return false;
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
var QUERY_MARKER = "Select provider:";
|
|
1495
|
+
function cursorToQuery(frame, query) {
|
|
1496
|
+
const rows = frame.split("\n");
|
|
1497
|
+
const lastIdx = rows.length - 1;
|
|
1498
|
+
const queryIdx = rows.findIndex((r) => r.includes(QUERY_MARKER));
|
|
1499
|
+
if (queryIdx < 0) return;
|
|
1500
|
+
const up = lastIdx - queryIdx;
|
|
1501
|
+
const queryLine = rows[queryIdx] ?? "";
|
|
1502
|
+
const plain = stripAnsi2(queryLine);
|
|
1503
|
+
const markerAt = plain.indexOf(QUERY_MARKER);
|
|
1504
|
+
const col = markerAt + QUERY_MARKER.length + 1 + query.length + 1;
|
|
1505
|
+
if (up > 0) writeOut2(`\x1B[${up}A`);
|
|
1506
|
+
writeOut2(`\x1B[${col}G`);
|
|
1507
|
+
}
|
|
1508
|
+
async function runLiveProviderPicker(displayList, savedSet) {
|
|
1509
|
+
const stdin = process.stdin;
|
|
1510
|
+
const out = process.stdout;
|
|
1511
|
+
if (!stdin.isTTY || !out.isTTY) return void 0;
|
|
1512
|
+
setOutputLineGuard2(null);
|
|
1513
|
+
let state = { query: "", selected: 0, status: "typing" };
|
|
1514
|
+
let ordered = orderProvidersForDisplay(filterProviders(state.query, displayList));
|
|
1515
|
+
const visibleCount = () => ordered.length;
|
|
1516
|
+
const clamp = () => {
|
|
1517
|
+
if (state.selected >= visibleCount()) state.selected = Math.max(0, visibleCount() - 1);
|
|
1518
|
+
};
|
|
1519
|
+
clamp();
|
|
1520
|
+
let frame = renderLiveProviderList(state.query, ordered, state.selected, savedSet);
|
|
1521
|
+
writeOut2(frame);
|
|
1522
|
+
writeOut2("\x1B7");
|
|
1523
|
+
cursorToQuery(frame, state.query);
|
|
1524
|
+
return new Promise((resolve) => {
|
|
1525
|
+
const wasRaw = stdin.isRaw;
|
|
1526
|
+
const wasPaused = stdin.isPaused();
|
|
1527
|
+
setRawMode2(stdin, true);
|
|
1528
|
+
stdin.resume();
|
|
1529
|
+
stdin.setEncoding("utf8");
|
|
1530
|
+
const cleanup = () => {
|
|
1531
|
+
stdin.off("data", onData);
|
|
1532
|
+
setRawMode2(stdin, wasRaw);
|
|
1533
|
+
if (wasPaused) stdin.pause();
|
|
1534
|
+
};
|
|
1535
|
+
const repaint = () => {
|
|
1536
|
+
writeOut2("\x1B8");
|
|
1537
|
+
const ups = (frame.match(/\n/g) ?? []).length;
|
|
1538
|
+
writeOut2(`\x1B[${ups}A\r\x1B[J`);
|
|
1539
|
+
ordered = orderProvidersForDisplay(filterProviders(state.query, displayList));
|
|
1540
|
+
clamp();
|
|
1541
|
+
frame = renderLiveProviderList(state.query, ordered, state.selected, savedSet);
|
|
1542
|
+
writeOut2(frame);
|
|
1543
|
+
writeOut2("\x1B7");
|
|
1544
|
+
cursorToQuery(frame, state.query);
|
|
1545
|
+
};
|
|
1546
|
+
const onData = (chunk) => {
|
|
1547
|
+
ordered = orderProvidersForDisplay(filterProviders(state.query, displayList));
|
|
1548
|
+
state = applyPickerKey(state, chunk, visibleCount());
|
|
1549
|
+
ordered = orderProvidersForDisplay(filterProviders(state.query, displayList));
|
|
1550
|
+
clamp();
|
|
1551
|
+
if (state.status === "cancelled") {
|
|
1552
|
+
cleanup();
|
|
1553
|
+
writeOut2("\n");
|
|
1554
|
+
resolve(void 0);
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
if (state.status === "submitted") {
|
|
1558
|
+
if (ordered.length === 0) {
|
|
1559
|
+
state = { ...state, status: "typing" };
|
|
1560
|
+
repaint();
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
const pick = ordered[state.selected] ?? ordered[0];
|
|
1564
|
+
cleanup();
|
|
1565
|
+
writeOut2("\n");
|
|
1566
|
+
resolve(pick);
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
repaint();
|
|
1570
|
+
};
|
|
1571
|
+
stdin.on("data", onData);
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
async function runPicker(deps) {
|
|
1575
|
+
const { modelsRegistry, renderer, reader, config, defaultProvider, defaultModel } = deps;
|
|
1576
|
+
renderer.write(
|
|
1577
|
+
`
|
|
1578
|
+
${theme.accent(color8.bold("\u258C WrongStack"))} ${color8.dim("Provider & Model Selection")}
|
|
1579
|
+
`
|
|
1580
|
+
);
|
|
1581
|
+
renderer.write(color8.dim(`${theme.chrome("\xB7")} Loading provider catalog\u2026
|
|
1582
|
+
`));
|
|
1583
|
+
let providers;
|
|
1584
|
+
try {
|
|
1585
|
+
providers = await modelsRegistry.listProviders();
|
|
1586
|
+
} catch {
|
|
1587
|
+
renderer.writeError(
|
|
1588
|
+
"Failed to load provider catalog. Pass --provider and --model to skip the picker."
|
|
1589
|
+
);
|
|
1590
|
+
return void 0;
|
|
1591
|
+
}
|
|
1592
|
+
const supported = providers.filter((p) => p.family !== "unsupported");
|
|
1593
|
+
const catalogById = new Map(supported.map((p) => [p.id, p]));
|
|
1594
|
+
const overlay = config?.providers ?? {};
|
|
1595
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1596
|
+
const merged = [];
|
|
1597
|
+
for (const p of supported) {
|
|
1598
|
+
const cfg = overlay[p.id];
|
|
1599
|
+
seen.add(p.id);
|
|
1600
|
+
if (cfg) {
|
|
1601
|
+
merged.push({
|
|
1602
|
+
...p,
|
|
1603
|
+
family: cfg.family ?? p.family,
|
|
1604
|
+
apiBase: cfg.baseUrl ?? p.apiBase,
|
|
1605
|
+
envVars: cfg.envVars && cfg.envVars.length > 0 ? cfg.envVars : p.envVars,
|
|
1606
|
+
// When the user has saved an explicit model list, it wins — they
|
|
1607
|
+
// know which models their endpoint actually serves (e.g. LM
|
|
1608
|
+
// Studio, vLLM, or a proxy with custom model ids). Otherwise the
|
|
1609
|
+
// catalog list keeps providing suggestions.
|
|
1610
|
+
models: visibleModelIds(
|
|
1611
|
+
p.id,
|
|
1612
|
+
config ?? { providers: {} },
|
|
1613
|
+
p.models.map((m) => m.id),
|
|
1614
|
+
cfg
|
|
1615
|
+
).map((m) => p.models.find((pm) => pm.id === m) ?? { id: m, name: m })
|
|
1616
|
+
});
|
|
1617
|
+
} else {
|
|
1618
|
+
merged.push(p);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
for (const [id, cfg] of Object.entries(overlay)) {
|
|
1622
|
+
if (seen.has(id)) continue;
|
|
1623
|
+
if (!cfg?.family || cfg.family === "unsupported") continue;
|
|
1624
|
+
const catalogType = cfg.type && cfg.type !== id ? cfg.type : void 0;
|
|
1625
|
+
const inherited = catalogType ? catalogById.get(catalogType) : void 0;
|
|
1626
|
+
merged.push({
|
|
1627
|
+
id,
|
|
1628
|
+
name: inherited ? `${inherited.name} ${color8.dim("(alias)")}` : id,
|
|
1629
|
+
family: cfg.family,
|
|
1630
|
+
apiBase: cfg.baseUrl ?? inherited?.apiBase,
|
|
1631
|
+
envVars: cfg.envVars ?? inherited?.envVars ?? [],
|
|
1632
|
+
models: visibleModelIds(
|
|
1633
|
+
id,
|
|
1634
|
+
config ?? { providers: {} },
|
|
1635
|
+
(inherited?.models ?? []).map((m) => m.id),
|
|
1636
|
+
cfg
|
|
1637
|
+
).map((m) => inherited?.models.find((pm) => pm.id === m) ?? { id: m, name: m }),
|
|
1638
|
+
npm: inherited?.npm
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
const keyed = merged.filter((p) => hasApiKey(p, config) || isKeylessLocalProvider(p));
|
|
1642
|
+
let displayList = keyed;
|
|
1643
|
+
let showingFallback = false;
|
|
1644
|
+
if (keyed.length === 0) {
|
|
1645
|
+
displayList = merged;
|
|
1646
|
+
showingFallback = merged.length > 0;
|
|
1647
|
+
}
|
|
1648
|
+
appendLocalPresetProviders(displayList);
|
|
1649
|
+
if (displayList.length === 0) {
|
|
1650
|
+
renderer.writeError("No supported providers found in catalog.");
|
|
1651
|
+
return void 0;
|
|
1652
|
+
}
|
|
1653
|
+
if (process.stdin.isTTY) {
|
|
1654
|
+
const chosen2 = await runLiveProviderPicker(displayList);
|
|
1655
|
+
if (!chosen2) {
|
|
1656
|
+
renderer.write(color8.dim("Cancelled.\n"));
|
|
1657
|
+
return void 0;
|
|
1658
|
+
}
|
|
1659
|
+
return pickModel(chosen2, modelsRegistry, renderer, reader, defaultModel);
|
|
1660
|
+
}
|
|
1661
|
+
const families = /* @__PURE__ */ new Map();
|
|
1662
|
+
for (const p of displayList) {
|
|
1663
|
+
const list = families.get(p.family) ?? [];
|
|
1664
|
+
list.push(p);
|
|
1665
|
+
families.set(p.family, list);
|
|
1666
|
+
}
|
|
1667
|
+
const ordered = [];
|
|
1668
|
+
const preferredOrder = [
|
|
1669
|
+
"anthropic",
|
|
1670
|
+
"anthropic-oauth",
|
|
1671
|
+
"openai",
|
|
1672
|
+
"openai-codex",
|
|
1673
|
+
"github-copilot",
|
|
1674
|
+
"google",
|
|
1675
|
+
"openai-compatible"
|
|
1676
|
+
];
|
|
1677
|
+
const familyOrder = [
|
|
1678
|
+
...preferredOrder.filter((f) => families.has(f)),
|
|
1679
|
+
...[...families.keys()].filter((f) => !preferredOrder.includes(f))
|
|
1680
|
+
];
|
|
1681
|
+
let idx = 1;
|
|
1682
|
+
let defaultIdx;
|
|
1683
|
+
renderer.write("\n");
|
|
1684
|
+
renderer.write(`${boxTop("Select a provider")}
|
|
1685
|
+
`);
|
|
1686
|
+
for (const fam of familyOrder) {
|
|
1687
|
+
const list = [...families.get(fam) ?? []].sort(
|
|
1688
|
+
(a, b) => a.id.toLowerCase().localeCompare(b.id.toLowerCase())
|
|
1689
|
+
);
|
|
1690
|
+
if (!list || list.length === 0) continue;
|
|
1691
|
+
renderer.write(`${boxRow(`${theme.accent("\xB7")} ${color8.dim(fam.toUpperCase())}`)}
|
|
1692
|
+
`);
|
|
1693
|
+
for (const p of list) {
|
|
1694
|
+
const envFound = p.envVars.some((v) => !!process.env[v]);
|
|
1695
|
+
const entry = config?.providers?.[p.id];
|
|
1696
|
+
const configKey = typeof entry?.apiKey === "string" && entry.apiKey.length > 0 || Array.isArray(entry?.apiKeys) && entry?.apiKeys?.some((k) => k?.apiKey);
|
|
1697
|
+
const marker = envFound ? color8.green("\u25CF") : configKey ? color8.cyan("\u25C9") : color8.dim("\u25CB");
|
|
1698
|
+
const isDefault = p.id === defaultProvider;
|
|
1699
|
+
if (isDefault) defaultIdx = idx;
|
|
1700
|
+
const idLabel = isDefault ? theme.accent(color8.bold(p.id)) : p.id;
|
|
1701
|
+
const suffix = isDefault ? color8.dim(" (default)") : "";
|
|
1702
|
+
renderer.write(
|
|
1703
|
+
`${boxRow(`${color8.dim(`${idx}.`.padStart(4))} ${marker} ${padVisible(idLabel, 22)} ${color8.dim(p.name)}${suffix}`)}
|
|
1704
|
+
`
|
|
1705
|
+
);
|
|
1706
|
+
ordered.push({ provider: p, index: idx });
|
|
1707
|
+
idx++;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
renderer.write(`${boxDivider()}
|
|
1711
|
+
`);
|
|
1712
|
+
if (showingFallback) {
|
|
1713
|
+
renderer.write(
|
|
1714
|
+
`${boxRow(`${color8.yellow("\u26A0 No API keys detected.")} ${color8.dim("Run `wstack auth <provider>` after picking.")}`)}
|
|
1715
|
+
`
|
|
1716
|
+
);
|
|
1717
|
+
} else {
|
|
1718
|
+
renderer.write(`${boxRow(color8.dim("\u25CF env key \u25C9 stored in config \u25CB no key"))}
|
|
1719
|
+
`);
|
|
1720
|
+
}
|
|
1721
|
+
renderer.write(`${boxBottom()}
|
|
1722
|
+
`);
|
|
1723
|
+
const defaultHint = defaultIdx !== void 0 && defaultProvider ? ` ${color8.dim(`[Enter = ${defaultProvider}]`)}` : "";
|
|
1724
|
+
const providerAnswer = (await reader.readLine(
|
|
1725
|
+
`
|
|
1726
|
+
${color8.amber("?")} Select provider (1-${ordered.length})${defaultHint} ${color8.dim("[q to quit]")}: `
|
|
1727
|
+
)).trim();
|
|
1728
|
+
if (providerAnswer.toLowerCase() === "q") {
|
|
1729
|
+
renderer.write(color8.dim("Cancelled.\n"));
|
|
1730
|
+
return void 0;
|
|
1731
|
+
}
|
|
1732
|
+
if (!providerAnswer) {
|
|
1733
|
+
if (defaultIdx !== void 0) {
|
|
1734
|
+
const def = ordered[defaultIdx - 1];
|
|
1735
|
+
if (def) return pickModel(def.provider, modelsRegistry, renderer, reader, defaultModel);
|
|
1736
|
+
}
|
|
1737
|
+
renderer.write(color8.dim("Cancelled.\n"));
|
|
1738
|
+
return void 0;
|
|
1739
|
+
}
|
|
1740
|
+
const providerIdx = Number.parseInt(providerAnswer, 10);
|
|
1741
|
+
if (Number.isNaN(providerIdx) || providerIdx < 1 || providerIdx > ordered.length) {
|
|
1742
|
+
const byId = ordered.find((o) => o.provider.id.toLowerCase() === providerAnswer.toLowerCase());
|
|
1743
|
+
if (!byId) {
|
|
1744
|
+
renderer.writeError(`Invalid selection: "${providerAnswer}"`);
|
|
1745
|
+
return void 0;
|
|
1746
|
+
}
|
|
1747
|
+
return pickModel(byId.provider, modelsRegistry, renderer, reader, defaultModel);
|
|
1748
|
+
}
|
|
1749
|
+
const chosen = ordered[providerIdx - 1];
|
|
1750
|
+
if (!chosen) return void 0;
|
|
1751
|
+
const modelHint = chosen.provider.id === defaultProvider ? defaultModel : void 0;
|
|
1752
|
+
return pickModel(chosen.provider, modelsRegistry, renderer, reader, modelHint);
|
|
1753
|
+
}
|
|
1754
|
+
async function pickModel(provider, registry, renderer, reader, defaultModel) {
|
|
1755
|
+
renderer.write(`
|
|
1756
|
+
${boxTop("Select a model")}
|
|
1757
|
+
`);
|
|
1758
|
+
renderer.write(
|
|
1759
|
+
`${boxRow(`${color8.bold(provider.name)} ${color8.dim(`(${provider.id})`)} models:`)}
|
|
1760
|
+
`
|
|
1761
|
+
);
|
|
1762
|
+
renderer.write(`${boxBottom()}
|
|
1763
|
+
`);
|
|
1764
|
+
const codexBlock = codexPickerPreamble(provider);
|
|
1765
|
+
if (codexBlock) renderer.write(codexBlock);
|
|
1766
|
+
const models = [...provider.models].sort(
|
|
1767
|
+
(a, b) => (b.release_date ?? "").localeCompare(a.release_date ?? "")
|
|
1768
|
+
);
|
|
1769
|
+
if (models.length === 0) {
|
|
1770
|
+
renderer.write(
|
|
1771
|
+
color8.dim(
|
|
1772
|
+
" No models listed yet for this provider \u2014 type a model id to use it (local servers auto-discover models at launch).\n"
|
|
1773
|
+
)
|
|
1774
|
+
);
|
|
1775
|
+
const typed = (await reader.readLine(`
|
|
1776
|
+
${color8.amber("?")} Model id ${color8.dim("(or Enter to cancel)")}: `)).trim();
|
|
1777
|
+
if (!typed) {
|
|
1778
|
+
renderer.write(color8.dim("Cancelled.\n"));
|
|
1779
|
+
return void 0;
|
|
1780
|
+
}
|
|
1781
|
+
renderer.write(`
|
|
1782
|
+
${color8.green("\u2713")} ${color8.bold(provider.id)} / ${color8.bold(typed)}
|
|
1783
|
+
|
|
1784
|
+
`);
|
|
1785
|
+
return { provider: provider.id, model: typed };
|
|
1786
|
+
}
|
|
1787
|
+
if (process.stdin.isTTY) {
|
|
1788
|
+
const chosen = await runLiveModelPicker(provider, defaultModel);
|
|
1789
|
+
if (!chosen) {
|
|
1790
|
+
renderer.write(color8.dim("Cancelled.\n"));
|
|
1791
|
+
return void 0;
|
|
1792
|
+
}
|
|
1793
|
+
renderer.write(
|
|
1794
|
+
`
|
|
1795
|
+
${color8.green("\u2713")} ${color8.bold(provider.id)} / ${color8.bold(chosen.id)}
|
|
1796
|
+
|
|
1797
|
+
`
|
|
1798
|
+
);
|
|
1799
|
+
return { provider: provider.id, model: chosen.id };
|
|
1800
|
+
}
|
|
1801
|
+
const defaultIdxInModels = defaultModel !== void 0 ? models.findIndex((m) => m.id === defaultModel) : -1;
|
|
1802
|
+
const pageSize = 30;
|
|
1803
|
+
let offset = 0;
|
|
1804
|
+
while (offset < models.length) {
|
|
1805
|
+
const page = models.slice(offset, offset + pageSize);
|
|
1806
|
+
const pageFrom = offset + 1;
|
|
1807
|
+
const pageTo = Math.min(offset + page.length, models.length);
|
|
1808
|
+
renderer.write(`${boxTop(`Models ${pageFrom}\u2013${pageTo} of ${models.length}`)}
|
|
1809
|
+
`);
|
|
1810
|
+
for (let i = 0; i < page.length; i++) {
|
|
1811
|
+
const m = expectDefined2(page[i]);
|
|
1812
|
+
const num = offset + i + 1;
|
|
1813
|
+
const ctxRaw = m.limit?.context ? `${(m.limit.context / 1e3).toFixed(0)}k` : "?";
|
|
1814
|
+
const ctx = theme.ctx(ctxRaw.padStart(6));
|
|
1815
|
+
const cost = m.cost?.input !== void 0 ? `$${m.cost.input}/$${m.cost.output ?? "?"}` : "";
|
|
1816
|
+
const costCol = cost ? theme.cost(cost) : "";
|
|
1817
|
+
const capTags = [];
|
|
1818
|
+
if (m.tool_call) capTags.push("tools");
|
|
1819
|
+
if (m.reasoning) capTags.push("reason");
|
|
1820
|
+
if (m.modalities?.input?.includes("image")) capTags.push("vision");
|
|
1821
|
+
const capStr = capTags.map((c) => theme.caps(c)).join(color8.dim(" "));
|
|
1822
|
+
const isDefault = m.id === defaultModel;
|
|
1823
|
+
const idLabel = isDefault ? theme.accent(color8.bold(m.id)) : m.id;
|
|
1824
|
+
const suffix = isDefault ? color8.dim(" (default)") : "";
|
|
1825
|
+
const num5 = color8.dim(`${num}.`.padStart(5));
|
|
1826
|
+
renderer.write(
|
|
1827
|
+
`${boxRow(`${num5} ${padVisible(idLabel, 34)} ${ctx} ${padVisible(costCol, 11)} ${capStr}${suffix}`)}
|
|
1828
|
+
`
|
|
1829
|
+
);
|
|
1830
|
+
}
|
|
1831
|
+
renderer.write(`${boxBottom()}
|
|
1832
|
+
`);
|
|
1833
|
+
offset += pageSize;
|
|
1834
|
+
if (offset < models.length) {
|
|
1835
|
+
const more = (await reader.readLine(
|
|
1836
|
+
`
|
|
1837
|
+
${color8.amber("?")} Showing ${Math.min(offset, models.length)}/${models.length} \u2014 Enter number, ${color8.dim("Enter")} for more, or ${color8.dim("q")} to quit: `
|
|
1838
|
+
)).trim();
|
|
1839
|
+
if (more.toLowerCase() === "q") {
|
|
1840
|
+
renderer.write(color8.dim("Cancelled.\n"));
|
|
1841
|
+
return void 0;
|
|
1842
|
+
}
|
|
1843
|
+
if (!more) continue;
|
|
1844
|
+
return resolveModelSelection(more, models, provider, registry, renderer, reader);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
const defaultHint = defaultIdxInModels >= 0 && defaultModel ? ` ${color8.dim(`[Enter = ${defaultModel}]`)}` : "";
|
|
1848
|
+
const answer = (await reader.readLine(
|
|
1849
|
+
`
|
|
1850
|
+
${color8.amber("?")} Select model (1-${models.length})${defaultHint} ${color8.dim("[q to quit]")}: `
|
|
1851
|
+
)).trim();
|
|
1852
|
+
if (answer.toLowerCase() === "q") {
|
|
1853
|
+
renderer.write(color8.dim("Cancelled.\n"));
|
|
1854
|
+
return void 0;
|
|
1855
|
+
}
|
|
1856
|
+
if (!answer) {
|
|
1857
|
+
if (defaultIdxInModels >= 0 && defaultModel) {
|
|
1858
|
+
renderer.write(
|
|
1859
|
+
`
|
|
1860
|
+
${color8.green("\u2713")} ${color8.bold(provider.id)} / ${color8.bold(defaultModel)}
|
|
1861
|
+
|
|
1862
|
+
`
|
|
1863
|
+
);
|
|
1864
|
+
return { provider: provider.id, model: defaultModel };
|
|
1865
|
+
}
|
|
1866
|
+
renderer.write(color8.dim("Cancelled.\n"));
|
|
1867
|
+
return void 0;
|
|
1868
|
+
}
|
|
1869
|
+
return resolveModelSelection(answer, models, provider, registry, renderer, reader);
|
|
1870
|
+
}
|
|
1871
|
+
async function resolveModelSelection(answer, models, provider, _registry, renderer, _reader) {
|
|
1872
|
+
const idx = Number.parseInt(answer, 10);
|
|
1873
|
+
let modelId;
|
|
1874
|
+
if (!Number.isNaN(idx) && idx >= 1 && idx <= models.length) {
|
|
1875
|
+
modelId = models[idx - 1]?.id;
|
|
1876
|
+
} else {
|
|
1877
|
+
const lower = answer.toLowerCase();
|
|
1878
|
+
const match = models.find((m) => m.id.toLowerCase() === lower);
|
|
1879
|
+
if (match) {
|
|
1880
|
+
modelId = match.id;
|
|
1881
|
+
} else {
|
|
1882
|
+
const partial = models.filter((m) => m.id.toLowerCase().includes(lower));
|
|
1883
|
+
if (partial.length === 1) {
|
|
1884
|
+
modelId = partial[0]?.id;
|
|
1885
|
+
} else if (partial.length > 1) {
|
|
1886
|
+
renderer.writeError(`"${answer}" matches multiple models. Be more specific.`);
|
|
1887
|
+
return void 0;
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
if (!modelId) {
|
|
1892
|
+
modelId = answer;
|
|
1893
|
+
}
|
|
1894
|
+
renderer.write(`
|
|
1895
|
+
${color8.green("\u2713")} ${color8.bold(provider.id)} / ${color8.bold(modelId)}
|
|
1896
|
+
|
|
1897
|
+
`);
|
|
1898
|
+
return { provider: provider.id, model: modelId };
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1077
1901
|
// src/auth-menu/add-provider.ts
|
|
1078
1902
|
import * as fs from "node:fs/promises";
|
|
1079
|
-
import { atomicWrite, color as
|
|
1903
|
+
import { atomicWrite, color as color9 } from "@wrongstack/core/utils";
|
|
1080
1904
|
import { catalogProviderIdFor, PROVIDER_DEFINITIONS } from "@wrongstack/providers";
|
|
1081
1905
|
|
|
1082
1906
|
// src/provider-id.ts
|
|
@@ -1115,12 +1939,12 @@ async function addFromCatalog(deps) {
|
|
|
1115
1939
|
catalog = [...catalog, ...ownDefinitionsAsCatalog(new Set(catalog.map((p) => p.id)))];
|
|
1116
1940
|
const saved = new Set(Object.keys(await loadProviders(deps)));
|
|
1117
1941
|
deps.renderer.write(
|
|
1118
|
-
|
|
1942
|
+
color9.dim(
|
|
1119
1943
|
` Catalog: ${catalog.length} providers. Type to filter, \u2191/\u2193 navigate, Enter to select.
|
|
1120
1944
|
`
|
|
1121
1945
|
)
|
|
1122
1946
|
);
|
|
1123
|
-
deps.renderer.write(` ${
|
|
1947
|
+
deps.renderer.write(` ${color9.dim("\u25C9 already saved \u25CB no key yet")}
|
|
1124
1948
|
|
|
1125
1949
|
`);
|
|
1126
1950
|
renderOAuthLoginOptions(deps);
|
|
@@ -1129,11 +1953,11 @@ async function addFromCatalog(deps) {
|
|
|
1129
1953
|
if (!chosen) {
|
|
1130
1954
|
deps.renderer.write(
|
|
1131
1955
|
`
|
|
1132
|
-
${
|
|
1956
|
+
${color9.dim("OAuth login options:")} ${color9.dim("chatgpt")}, ${color9.dim("claude")}, or ${color9.dim("copilot")}?
|
|
1133
1957
|
`
|
|
1134
1958
|
);
|
|
1135
1959
|
const answer = (await deps.reader.readLine(
|
|
1136
|
-
` ${
|
|
1960
|
+
` ${color9.amber("?")} OAuth or q to quit ${color9.dim("[chatgpt/claude/copilot]")}: `
|
|
1137
1961
|
)).trim();
|
|
1138
1962
|
if (answer && await runOAuthLoginChoice(deps, answer, { allowNumeric: false })) {
|
|
1139
1963
|
return true;
|
|
@@ -1144,12 +1968,12 @@ async function addFromCatalog(deps) {
|
|
|
1144
1968
|
}
|
|
1145
1969
|
async function addKeyForCatalogProvider(deps, chosen) {
|
|
1146
1970
|
deps.renderer.write(
|
|
1147
|
-
|
|
1971
|
+
color9.dim(`
|
|
1148
1972
|
Defaults from models.dev \u2014 press Enter to keep, or type overrides.
|
|
1149
1973
|
`)
|
|
1150
1974
|
);
|
|
1151
1975
|
const famRaw = (await deps.reader.readLine(
|
|
1152
|
-
` ${
|
|
1976
|
+
` ${color9.amber("?")} Family ${color9.dim(`[${chosen.family}]`)} ${color9.dim("(q to quit)")}: `
|
|
1153
1977
|
)).trim();
|
|
1154
1978
|
if (famRaw === "q") return false;
|
|
1155
1979
|
let family = chosen.family;
|
|
@@ -1164,7 +1988,7 @@ async function addKeyForCatalogProvider(deps, chosen) {
|
|
|
1164
1988
|
family = validated;
|
|
1165
1989
|
}
|
|
1166
1990
|
const baseRaw = (await deps.reader.readLine(
|
|
1167
|
-
` ${
|
|
1991
|
+
` ${color9.amber("?")} Base URL ${color9.dim(`[${chosen.apiBase ?? "unset"}]`)} ${color9.dim("(q to quit)")}: `
|
|
1168
1992
|
)).trim();
|
|
1169
1993
|
if (baseRaw === "q") return false;
|
|
1170
1994
|
const baseUrl = baseRaw || chosen.apiBase;
|
|
@@ -1180,7 +2004,7 @@ async function addKeyForCatalogProvider(deps, chosen) {
|
|
|
1180
2004
|
suggestedAlias = candidate;
|
|
1181
2005
|
}
|
|
1182
2006
|
const aliasRaw = (await deps.reader.readLine(
|
|
1183
|
-
` ${
|
|
2007
|
+
` ${color9.amber("?")} Save as alias ${color9.dim(`[${suggestedAlias}]`)} ${color9.dim("(used with --provider <alias>)")}: `
|
|
1184
2008
|
)).trim();
|
|
1185
2009
|
const alias = aliasRaw || suggestedAlias;
|
|
1186
2010
|
const existing = providersNow[alias];
|
|
@@ -1207,25 +2031,25 @@ async function addKeyForCatalogProvider(deps, chosen) {
|
|
|
1207
2031
|
async function addCustomProvider(deps) {
|
|
1208
2032
|
deps.renderer.write(
|
|
1209
2033
|
`
|
|
1210
|
-
${
|
|
2034
|
+
${color9.bold("Custom provider")} ${color9.dim("\u2014 for local models or proxies not in the catalog.")}
|
|
1211
2035
|
`
|
|
1212
2036
|
);
|
|
1213
2037
|
const providersNow = await loadProviders(deps);
|
|
1214
2038
|
const idRaw = (await deps.reader.readLine(
|
|
1215
|
-
` ${
|
|
2039
|
+
` ${color9.amber("?")} Provider id ${color9.dim('(e.g. "local-llama", "my-proxy"; blank = auto custom-N, q to quit)')}: `
|
|
1216
2040
|
)).trim();
|
|
1217
2041
|
if (idRaw === "q") return false;
|
|
1218
2042
|
let type = idRaw;
|
|
1219
2043
|
if (!type) {
|
|
1220
2044
|
type = nextCustomProviderId(Object.keys(providersNow));
|
|
1221
|
-
deps.renderer.write(` ${
|
|
2045
|
+
deps.renderer.write(` ${color9.dim("Using generated id:")} ${color9.bold(type)}
|
|
1222
2046
|
`);
|
|
1223
2047
|
} else if (providersNow[type]) {
|
|
1224
2048
|
deps.renderer.writeWarning(`"${type}" already exists. Pick it from the main menu to edit.`);
|
|
1225
2049
|
return false;
|
|
1226
2050
|
}
|
|
1227
2051
|
const familyRaw = (await deps.reader.readLine(
|
|
1228
|
-
` ${
|
|
2052
|
+
` ${color9.amber("?")} Wire family ${color9.dim("(anthropic | openai | openai-compatible | google)")} ${color9.dim("(q to quit)")}: `
|
|
1229
2053
|
)).trim();
|
|
1230
2054
|
if (familyRaw === "q") return false;
|
|
1231
2055
|
const family = validateFamily(familyRaw);
|
|
@@ -1234,14 +2058,14 @@ ${color6.bold("Custom provider")} ${color6.dim("\u2014 for local models or proxi
|
|
|
1234
2058
|
return false;
|
|
1235
2059
|
}
|
|
1236
2060
|
const baseUrl = (await deps.reader.readLine(
|
|
1237
|
-
` ${
|
|
2061
|
+
` ${color9.amber("?")} Base URL ${color9.dim("(e.g. http://localhost:11434/v1, optional)")}: `
|
|
1238
2062
|
)).trim();
|
|
1239
2063
|
const modelsRaw = (await deps.reader.readLine(
|
|
1240
|
-
` ${
|
|
2064
|
+
` ${color9.amber("?")} Model ids ${color9.dim("(comma-separated, optional)")}: `
|
|
1241
2065
|
)).trim();
|
|
1242
2066
|
const models = modelsRaw ? modelsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
1243
2067
|
const envVarsRaw = (await deps.reader.readLine(
|
|
1244
|
-
` ${
|
|
2068
|
+
` ${color9.amber("?")} Env var names ${color9.dim("(comma-separated, optional)")}: `
|
|
1245
2069
|
)).trim();
|
|
1246
2070
|
const envVars = envVarsRaw ? envVarsRaw.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
1247
2071
|
return addKeyForProvider(type, deps, {
|
|
@@ -1253,17 +2077,17 @@ ${color6.bold("Custom provider")} ${color6.dim("\u2014 for local models or proxi
|
|
|
1253
2077
|
});
|
|
1254
2078
|
}
|
|
1255
2079
|
async function addManualEntry(deps) {
|
|
1256
|
-
const pid = (await deps.reader.readLine(` ${
|
|
2080
|
+
const pid = (await deps.reader.readLine(` ${color9.amber("?")} Provider id ${color9.dim("[q to quit]")}: `)).trim();
|
|
1257
2081
|
if (!pid || pid === "q") return false;
|
|
1258
2082
|
const famRaw = (await deps.reader.readLine(
|
|
1259
|
-
` ${
|
|
2083
|
+
` ${color9.amber("?")} Family ${color9.dim("(anthropic/openai/openai-compatible/google)")}: `
|
|
1260
2084
|
)).trim();
|
|
1261
2085
|
const family = validateFamily(famRaw);
|
|
1262
2086
|
if (!family) {
|
|
1263
2087
|
deps.renderer.writeError(`Invalid family: "${famRaw}"`);
|
|
1264
2088
|
return false;
|
|
1265
2089
|
}
|
|
1266
|
-
const baseUrl = (await deps.reader.readLine(` ${
|
|
2090
|
+
const baseUrl = (await deps.reader.readLine(` ${color9.amber("?")} Base URL ${color9.dim("(optional)")}: `)).trim();
|
|
1267
2091
|
return addKeyForProvider(pid, deps, {
|
|
1268
2092
|
type: pid,
|
|
1269
2093
|
family,
|
|
@@ -1306,10 +2130,10 @@ async function addKeyForProvider(providerId, deps, template) {
|
|
|
1306
2130
|
deps.profileConfigPath
|
|
1307
2131
|
);
|
|
1308
2132
|
deps.renderer.write(
|
|
1309
|
-
` ${
|
|
2133
|
+
` ${color9.green("\u2713")} Saved ${color9.bold(providerId)}/${color9.bold(label)}.
|
|
1310
2134
|
`
|
|
1311
2135
|
);
|
|
1312
|
-
deps.renderer.write(
|
|
2136
|
+
deps.renderer.write(color9.dim(` Launch: wstack --provider ${providerId} "<task>"
|
|
1313
2137
|
`));
|
|
1314
2138
|
try {
|
|
1315
2139
|
await adoptAsDefaultIfUnset(deps, providerId, template);
|
|
@@ -1337,14 +2161,14 @@ async function adoptAsDefaultIfUnset(deps, providerId, template) {
|
|
|
1337
2161
|
cfg["model"] = modelId;
|
|
1338
2162
|
await atomicWrite(targetPath, JSON.stringify(cfg, null, 2), { mode: 384 });
|
|
1339
2163
|
deps.renderer.write(
|
|
1340
|
-
` ${
|
|
2164
|
+
` ${color9.green("\u2713")} Set ${color9.bold(providerId)}/${color9.bold(modelId)} as the default.
|
|
1341
2165
|
`
|
|
1342
2166
|
);
|
|
1343
2167
|
}
|
|
1344
2168
|
async function promptForLabel(deps, usedLabels) {
|
|
1345
2169
|
const defaultLabel = suggestLabel(usedLabels);
|
|
1346
2170
|
const labelRaw = (await deps.reader.readLine(
|
|
1347
|
-
` ${
|
|
2171
|
+
` ${color9.amber("?")} Label for this key ${color9.dim(`[${defaultLabel}]`)}: `
|
|
1348
2172
|
)).trim();
|
|
1349
2173
|
const label = labelRaw || defaultLabel;
|
|
1350
2174
|
if (usedLabels.has(label)) {
|
|
@@ -1364,14 +2188,22 @@ export {
|
|
|
1364
2188
|
confirm,
|
|
1365
2189
|
validateFamily,
|
|
1366
2190
|
runCopilotOAuthLogin,
|
|
2191
|
+
LOCAL_LLM_PRESETS,
|
|
1367
2192
|
runAuthLocal,
|
|
1368
2193
|
resolveOAuthKind,
|
|
1369
2194
|
runOAuthLoginKind,
|
|
1370
2195
|
runOAuthLoginMenu,
|
|
2196
|
+
theme,
|
|
2197
|
+
boxTop,
|
|
2198
|
+
boxBottom,
|
|
2199
|
+
boxRow,
|
|
2200
|
+
boxDivider,
|
|
2201
|
+
saveToGlobalConfig,
|
|
2202
|
+
runPicker,
|
|
1371
2203
|
nextCustomProviderId,
|
|
1372
2204
|
addFromCatalog,
|
|
1373
2205
|
addKeyForCatalogProvider,
|
|
1374
2206
|
addCustomProvider,
|
|
1375
2207
|
addKeyForProvider
|
|
1376
2208
|
};
|
|
1377
|
-
//# sourceMappingURL=chunk-
|
|
2209
|
+
//# sourceMappingURL=chunk-XHIEA6LG.js.map
|