@tiens.nguyen/gu-cli 1.0.874 → 1.0.876
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/agent-model-command.mjs +48 -0
- package/gu-repl.mjs +84 -7
- package/package.json +1 -1
package/agent-model-command.mjs
CHANGED
|
@@ -389,6 +389,54 @@ export function agentMenuOptions({ host = "", modelCount = 0, needsKey = false }
|
|
|
389
389
|
* rows, current one marked — then "add a new one". Switching to a provider you already set up must
|
|
390
390
|
* not mean typing its URL again. Returns [{ action: "row" | "add", row?, label }].
|
|
391
391
|
*/
|
|
392
|
+
/**
|
|
393
|
+
* EVERY MODEL ALREADY SET UP ON THIS MACHINE, for the agent (#246) — what /model offers the coder.
|
|
394
|
+
*
|
|
395
|
+
* Reported: /model listed several providers and models, and /agent-model showed none of them —
|
|
396
|
+
* it asked to add a provider or enter a key. They are the same servers with the same keys: a key
|
|
397
|
+
* belongs to its server, and the API finds the agent's key by matching agentModelUrl to a saved row
|
|
398
|
+
* (resolveKeyForRole). So a pick here saves that row's URL and the model, and its key comes along.
|
|
399
|
+
*
|
|
400
|
+
* Rows: OpenAI-compatible and Ollama, enabled, with a URL; their default model, then the extra
|
|
401
|
+
* models offered in /model. Embedding models are left out — as the agent they answer nonsense.
|
|
402
|
+
* → [{ rowId, kind, url, host, model, label, current }]
|
|
403
|
+
*/
|
|
404
|
+
export function savedAgentChoices(settings) {
|
|
405
|
+
const rows = Array.isArray(settings?.agentCodingBackends) ? settings.agentCodingBackends : [];
|
|
406
|
+
const currentUrl = ensureV1Root(String(settings?.agentModelUrl ?? "")).toLowerCase();
|
|
407
|
+
const currentModel = currentAgentModel(settings);
|
|
408
|
+
const out = [];
|
|
409
|
+
const seen = new Set();
|
|
410
|
+
const takenKinds = new Set();
|
|
411
|
+
for (const row of rows) {
|
|
412
|
+
if (!row || typeof row !== "object" || row.enabled === false) continue;
|
|
413
|
+
const kind = String(row.kind ?? "");
|
|
414
|
+
if (kind !== "openai" && kind !== "ollama") continue;
|
|
415
|
+
const url = ensureV1Root(String(row.url ?? "").trim());
|
|
416
|
+
if (!url) continue;
|
|
417
|
+
// The API's identity rule: an id-less row is known by its kind, the first of each only.
|
|
418
|
+
const rowId = String(row.id ?? "").trim() || (takenKinds.has(kind) ? "" : kind);
|
|
419
|
+
if (!rowId) continue;
|
|
420
|
+
takenKinds.add(kind);
|
|
421
|
+
let host = url;
|
|
422
|
+
try {
|
|
423
|
+
host = new URL(url).host;
|
|
424
|
+
} catch {
|
|
425
|
+
/* keep the url */
|
|
426
|
+
}
|
|
427
|
+
const models = [String(row.defaultModel ?? "").trim(), ...(Array.isArray(row.models) ? row.models.map((m) => String(m ?? "").trim()) : [])];
|
|
428
|
+
for (const model of models) {
|
|
429
|
+
if (!model || isLikelyEmbedder({ model })) continue;
|
|
430
|
+
const key = `${url.toLowerCase()}\n${model}`;
|
|
431
|
+
if (seen.has(key)) continue;
|
|
432
|
+
seen.add(key);
|
|
433
|
+
const current = url.toLowerCase() === currentUrl && model === currentModel;
|
|
434
|
+
out.push({ rowId, kind, url, host, model, label: `${host}/${model}`, current });
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return out;
|
|
438
|
+
}
|
|
439
|
+
|
|
392
440
|
export function agentProviderChoices(settings) {
|
|
393
441
|
const rows = Array.isArray(settings?.agentCodingBackends) ? settings.agentCodingBackends : [];
|
|
394
442
|
const current = ensureV1Root(String(settings?.agentModelUrl ?? "")).toLowerCase();
|
package/gu-repl.mjs
CHANGED
|
@@ -8957,6 +8957,78 @@ async function chooseAgentProvider({ back = false } = {}) {
|
|
|
8957
8957
|
* The decisions live in agent-model-command.mjs; this is the terminal half only.
|
|
8958
8958
|
*/
|
|
8959
8959
|
async function chooseAgentModel() {
|
|
8960
|
+
const am = await import("./agent-model-command.mjs");
|
|
8961
|
+
const settings = await withPendingStatus("reading your agent model", fetchWorkerSettings);
|
|
8962
|
+
if (!settings) {
|
|
8963
|
+
console.log(red(" couldn't reach the API to read your current agent model.\n"));
|
|
8964
|
+
return;
|
|
8965
|
+
}
|
|
8966
|
+
// WHAT /model ALREADY HAS, FIRST (#246). The providers and models added for the coder are the
|
|
8967
|
+
// same servers with the same keys; listing them here means switching the agent to one of them is
|
|
8968
|
+
// one pick, not "add a provider" or "enter the API key" for something already set up.
|
|
8969
|
+
const saved = am.savedAgentChoices(settings);
|
|
8970
|
+
if (!saved.length) return agentModelOnServer();
|
|
8971
|
+
const current = am.currentAgentModel(settings);
|
|
8972
|
+
const curHost = settings.agentModelUrl ? hostOf(String(settings.agentModelUrl)) : "";
|
|
8973
|
+
for (;;) {
|
|
8974
|
+
const labels = saved.map((c) => c.label + (c.current ? accent(" (current)") : ""));
|
|
8975
|
+
const MORE = labels.length;
|
|
8976
|
+
labels.push("other models, providers and API keys…");
|
|
8977
|
+
const ADD = labels.length;
|
|
8978
|
+
labels.push("add a provider — Claude API, OpenAI, Kimi, Ollama…");
|
|
8979
|
+
// The hint drops to its own row when the header would wrap (80 columns over SSH).
|
|
8980
|
+
const head = ` Agent model — the one that TALKS (now ${current || "unset"}${curHost ? ` on ${curHost}` : ""})`;
|
|
8981
|
+
const hint = `(${PICK_HINT}):`;
|
|
8982
|
+
const cols = Math.max(20, process.stdout.columns || 80);
|
|
8983
|
+
console.log(dim(head.length + 2 + hint.length < cols ? `${head} ${hint}` : `${head}\n ${hint}`));
|
|
8984
|
+
const idx = await pickFromList(labels, Math.max(0, saved.findIndex((c) => c.current)));
|
|
8985
|
+
if (idx < 0) {
|
|
8986
|
+
console.log(dim(" (cancelled)\n"));
|
|
8987
|
+
return;
|
|
8988
|
+
}
|
|
8989
|
+
if (idx === MORE) {
|
|
8990
|
+
if ((await agentModelOnServer({ back: true })) === BACK) continue;
|
|
8991
|
+
return;
|
|
8992
|
+
}
|
|
8993
|
+
if (idx === ADD) {
|
|
8994
|
+
if ((await addServer({ role: "agent", dialect: "openai", back: true })) === BACK) continue;
|
|
8995
|
+
return;
|
|
8996
|
+
}
|
|
8997
|
+
await useSavedAgentChoice(saved[idx]);
|
|
8998
|
+
return;
|
|
8999
|
+
}
|
|
9000
|
+
}
|
|
9001
|
+
|
|
9002
|
+
/**
|
|
9003
|
+
* Point the agent at a model already set up for /model (#246): that row's URL and the model. The
|
|
9004
|
+
* API finds the key by the URL (resolveKeyForRole), so nothing is re-entered — unless the server
|
|
9005
|
+
* needs a key and has none, which is asked here rather than failing on the next question.
|
|
9006
|
+
*/
|
|
9007
|
+
async function useSavedAgentChoice(c) {
|
|
9008
|
+
if (c.current) {
|
|
9009
|
+
console.log(dim(` already using ${c.model} on ${c.host} — nothing changed.\n`));
|
|
9010
|
+
return;
|
|
9011
|
+
}
|
|
9012
|
+
const as = await import("./add-server.mjs");
|
|
9013
|
+
const target = as.serverTarget(c.kind === "ollama" ? "ollama" : "openai", c.url);
|
|
9014
|
+
const keys = (await withPendingStatus(`reading the keys for ${c.host}`, () => fetchServerKeys(c.rowId))) ?? [];
|
|
9015
|
+
if (target.ok && as.keyRequirement(target) === "required" && !keys.length) {
|
|
9016
|
+
console.log(GUTTER + dim(`${c.host} needs an API key and none is saved for it — enter one.`));
|
|
9017
|
+
await addServer({ role: "agent", dialect: target.dialect, url: c.url, newKey: true });
|
|
9018
|
+
return;
|
|
9019
|
+
}
|
|
9020
|
+
// A folder pick of ANOTHER server's key would be sent to this one: this folder follows the
|
|
9021
|
+
// server's own key from now on.
|
|
9022
|
+
if (sessionAgentKeyId && !keys.some((k) => k.id === sessionAgentKeyId)) await saveFolderKeyPick("agent", "");
|
|
9023
|
+
await saveAgentModel(c.url, c.model, c.host);
|
|
9024
|
+
}
|
|
9025
|
+
|
|
9026
|
+
/**
|
|
9027
|
+
* One server's view: its full model list, other providers, its keys — what /agent-model was before
|
|
9028
|
+
* #246, now behind "other models, providers and API keys…" when something is already set up.
|
|
9029
|
+
* `back`: opened from that list, so its first menu offers "← back".
|
|
9030
|
+
*/
|
|
9031
|
+
async function agentModelOnServer({ back = false } = {}) {
|
|
8960
9032
|
const am = await import("./agent-model-command.mjs");
|
|
8961
9033
|
const settings = await withPendingStatus("reading your agent model", fetchWorkerSettings);
|
|
8962
9034
|
if (!settings) {
|
|
@@ -9033,12 +9105,16 @@ async function chooseAgentModel() {
|
|
|
9033
9105
|
const options = am.agentRecoveryOptions({ keyProblem, host: listHost });
|
|
9034
9106
|
const go = await pickFromList(
|
|
9035
9107
|
options.map((o, i) => (i === 0 ? dim(o.label) : o.label)),
|
|
9036
|
-
keyProblem ? 1 : 0
|
|
9108
|
+
keyProblem ? 1 : 0,
|
|
9109
|
+
{ back }
|
|
9037
9110
|
);
|
|
9111
|
+
if (go === PICK_BACK) return BACK;
|
|
9038
9112
|
const action = go >= 0 ? options[go].action : "leave";
|
|
9039
9113
|
if (action === "key") await enterAgentProviderKey(source.row, listHost);
|
|
9040
|
-
else if (action === "provider")
|
|
9041
|
-
|
|
9114
|
+
else if (action === "provider") {
|
|
9115
|
+
// "← back" on "Which provider?" returns to this menu (#247).
|
|
9116
|
+
if ((await addServer({ role: "agent", dialect: "openai", back: true })) === BACK) return agentModelOnServer({ back });
|
|
9117
|
+
} else console.log("");
|
|
9042
9118
|
return;
|
|
9043
9119
|
}
|
|
9044
9120
|
|
|
@@ -9047,7 +9123,8 @@ async function chooseAgentModel() {
|
|
|
9047
9123
|
// are all you can choose". A short menu of what to change comes before any list.
|
|
9048
9124
|
console.log(dim(` Agent model — the one that TALKS: ${current || "unset"} on ${listHost}`));
|
|
9049
9125
|
const menu = am.agentMenuOptions({ host: listHost, modelCount: models.length, needsKey });
|
|
9050
|
-
const first = await pickFromList(menu.map((o) => o.label), 0);
|
|
9126
|
+
const first = await pickFromList(menu.map((o) => o.label), 0, { back });
|
|
9127
|
+
if (first === PICK_BACK) return BACK;
|
|
9051
9128
|
const firstAction = first >= 0 ? menu[first].action : "cancel";
|
|
9052
9129
|
if (firstAction === "cancel") {
|
|
9053
9130
|
console.log(dim(" (cancelled)\n"));
|
|
@@ -9055,7 +9132,7 @@ async function chooseAgentModel() {
|
|
|
9055
9132
|
}
|
|
9056
9133
|
// "← back" in any of these returns to this menu (#242) — read again, as on a first open.
|
|
9057
9134
|
if (firstAction === "provider") {
|
|
9058
|
-
if ((await chooseAgentProvider({ back: true })) === BACK) return
|
|
9135
|
+
if ((await chooseAgentProvider({ back: true })) === BACK) return agentModelOnServer({ back });
|
|
9059
9136
|
return;
|
|
9060
9137
|
}
|
|
9061
9138
|
if (firstAction === "key") {
|
|
@@ -9063,7 +9140,7 @@ async function chooseAgentModel() {
|
|
|
9063
9140
|
const rowId = String(source.row?.id ?? "").trim() || String(source.row?.kind ?? "").trim();
|
|
9064
9141
|
if (!rowId) return enterAgentProviderKey(source.row, listHost);
|
|
9065
9142
|
const r = await manageServerKeys({ role: "agent", backendId: rowId, host: listHost, model: am.currentAgentModel(settings), back: true });
|
|
9066
|
-
if (r === BACK) return
|
|
9143
|
+
if (r === BACK) return agentModelOnServer({ back });
|
|
9067
9144
|
return;
|
|
9068
9145
|
}
|
|
9069
9146
|
|
|
@@ -9096,7 +9173,7 @@ async function chooseAgentModel() {
|
|
|
9096
9173
|
// No other entries here any more (#44): the ways to change the provider are on the menu above,
|
|
9097
9174
|
// where they can be seen, rather than after every model.
|
|
9098
9175
|
const idx = await pickFromList(labels, Math.max(0, rows.findIndex((r) => r.current)), { back: true });
|
|
9099
|
-
if (idx === PICK_BACK) return
|
|
9176
|
+
if (idx === PICK_BACK) return agentModelOnServer({ back });
|
|
9100
9177
|
if (idx < 0) {
|
|
9101
9178
|
console.log(dim(" (cancelled)\n"));
|
|
9102
9179
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiens.nguyen/gu-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.876",
|
|
4
4
|
"description": "Gu CLI — the gu terminal plus the local worker that runs agent / OCR / PDF / embedding jobs on your Mac (Ollama / MLX / OpenAI-compatible).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|