@tiens.nguyen/gu-cli 1.0.874 → 1.0.875

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.
@@ -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,8 +9105,10 @@ 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
9114
  else if (action === "provider") await addServer({ role: "agent", dialect: "openai" });
@@ -9047,7 +9121,8 @@ async function chooseAgentModel() {
9047
9121
  // are all you can choose". A short menu of what to change comes before any list.
9048
9122
  console.log(dim(` Agent model — the one that TALKS: ${current || "unset"} on ${listHost}`));
9049
9123
  const menu = am.agentMenuOptions({ host: listHost, modelCount: models.length, needsKey });
9050
- const first = await pickFromList(menu.map((o) => o.label), 0);
9124
+ const first = await pickFromList(menu.map((o) => o.label), 0, { back });
9125
+ if (first === PICK_BACK) return BACK;
9051
9126
  const firstAction = first >= 0 ? menu[first].action : "cancel";
9052
9127
  if (firstAction === "cancel") {
9053
9128
  console.log(dim(" (cancelled)\n"));
@@ -9055,7 +9130,7 @@ async function chooseAgentModel() {
9055
9130
  }
9056
9131
  // "← back" in any of these returns to this menu (#242) — read again, as on a first open.
9057
9132
  if (firstAction === "provider") {
9058
- if ((await chooseAgentProvider({ back: true })) === BACK) return chooseAgentModel();
9133
+ if ((await chooseAgentProvider({ back: true })) === BACK) return agentModelOnServer({ back });
9059
9134
  return;
9060
9135
  }
9061
9136
  if (firstAction === "key") {
@@ -9063,7 +9138,7 @@ async function chooseAgentModel() {
9063
9138
  const rowId = String(source.row?.id ?? "").trim() || String(source.row?.kind ?? "").trim();
9064
9139
  if (!rowId) return enterAgentProviderKey(source.row, listHost);
9065
9140
  const r = await manageServerKeys({ role: "agent", backendId: rowId, host: listHost, model: am.currentAgentModel(settings), back: true });
9066
- if (r === BACK) return chooseAgentModel();
9141
+ if (r === BACK) return agentModelOnServer({ back });
9067
9142
  return;
9068
9143
  }
9069
9144
 
@@ -9096,7 +9171,7 @@ async function chooseAgentModel() {
9096
9171
  // No other entries here any more (#44): the ways to change the provider are on the menu above,
9097
9172
  // where they can be seen, rather than after every model.
9098
9173
  const idx = await pickFromList(labels, Math.max(0, rows.findIndex((r) => r.current)), { back: true });
9099
- if (idx === PICK_BACK) return chooseAgentModel();
9174
+ if (idx === PICK_BACK) return agentModelOnServer({ back });
9100
9175
  if (idx < 0) {
9101
9176
  console.log(dim(" (cancelled)\n"));
9102
9177
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gu-cli",
3
- "version": "1.0.874",
3
+ "version": "1.0.875",
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",