@tiens.nguyen/gu-cli 1.0.696 → 1.0.698

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.
@@ -67,3 +67,36 @@ export function codingChoiceTags(choices) {
67
67
  });
68
68
  return { tags, show: new Set(tags).size > 1 };
69
69
  }
70
+
71
+ /**
72
+ * WHERE AN OLLAMA CODER RUNS — "ollama.com", "127.0.0.1", "ollama1.gomarsic.cc" — or "" for any
73
+ * other kind.
74
+ *
75
+ * Reported: "/model ✓ coding model → kimi-k3 (ollama)". The tag was the API's provider label, the
76
+ * first word of the host, so https://ollama.com read "ollama" — the same word as the KIND — and a
77
+ * paid cloud model looked exactly like one on this machine. Whether it is local or cloud decides
78
+ * whether it costs money and needs a key, so it is shown as a prefix wherever the coder is named.
79
+ *
80
+ * The Ollama default port (11434) is left off; any other port is kept, because two local servers on
81
+ * different ports are different servers.
82
+ */
83
+ export function ollamaWhere(kind, url) {
84
+ if (kind !== "ollama") return "";
85
+ let u;
86
+ try {
87
+ u = new URL(String(url ?? ""));
88
+ } catch {
89
+ return "";
90
+ }
91
+ const host = u.hostname.toLowerCase().replace(/\.$/, "").replace(/^\[|\]$/g, "");
92
+ if (host === "ollama.com" || host.endsWith(".ollama.com")) return "ollama.com";
93
+ const local = host === "localhost" || host === "127.0.0.1" || host === "::1";
94
+ const port = u.port && u.port !== "11434" ? `:${u.port}` : "";
95
+ return (local ? "127.0.0.1" : host) + port;
96
+ }
97
+
98
+ /** The coder's name as shown: "ollama.com/kimi-k3" for an Ollama coder, the bare name otherwise. */
99
+ export function coderDisplayName(name, where) {
100
+ const n = String(name ?? "").trim();
101
+ return where && n ? `${where}/${n}` : n;
102
+ }
@@ -141,8 +141,10 @@ export async function promptCodingKeyIfMissing(payload, { askSecret, saveCodingK
141
141
  *
142
142
  * There is a coder either way; what differs is whether it can be USED yet. Name it, and say so.
143
143
  */
144
- export function codingBannerLabel(payload, { dim = identity, override = "" } = {}) {
144
+ export function codingBannerLabel(payload, { dim = identity, override = "", where = "" } = {}) {
145
145
  const p = payload ?? {};
146
+ // "ollama.com/kimi-k3" rather than "kimi-k3": local or cloud, visible in the header too.
147
+ const named = (n) => (where && n ? `${where}/${n}` : n);
146
148
  // A `/model` PICK WINS OVER THE ACCOUNT DEFAULT, because it is what the next turn will
147
149
  // actually use. The payload carries the account's coder; the override is this folder's,
148
150
  // restored from its session file at startup. Reading only the payload put two different
@@ -152,15 +154,15 @@ export function codingBannerLabel(payload, { dim = identity, override = "" } = {
152
154
  // Only possible since the pick started surviving an exit; before that the override was always
153
155
  // empty at startup and the two could not disagree.
154
156
  const pick = String(override ?? "").trim();
155
- if (pick) return pick;
156
- if (p.codingModelId) return String(p.codingModelId);
157
+ if (pick) return named(pick);
158
+ if (p.codingModelId) return named(String(p.codingModelId));
157
159
  const name = String(p.codingKeyMissing ? p.codingKeyModel || "" : "").trim();
158
160
  if (!name) return dim("—");
159
161
  // A keyless host is never asked for anything — the prompt stores the placeholder itself and
160
162
  // the coder is live moments later, so "(needs a key)" would be a lie with a shelf life of
161
163
  // about one line.
162
- if (isKeylessCodingHost(p.codingKeyUrl)) return name;
163
- return name + dim(" (needs a key)");
164
+ if (isKeylessCodingHost(p.codingKeyUrl)) return named(name);
165
+ return named(name) + dim(" (needs a key)");
164
166
  }
165
167
 
166
168
  /**
package/gu-repl.mjs CHANGED
@@ -113,7 +113,7 @@ import {
113
113
  } from "./terminal-layout.mjs";
114
114
  import { completedFlush } from "./completed-flush.mjs";
115
115
  import { codingBannerLabel, SECRET_KEY_HINT } from "./coding-key-prompt.mjs";
116
- import { codingChoiceTags } from "./coding-choice-tags.mjs";
116
+ import { codingChoiceTags, coderDisplayName, ollamaWhere } from "./coding-choice-tags.mjs";
117
117
  import {
118
118
  isDoubleClick,
119
119
  clickRun,
@@ -1973,11 +1973,14 @@ let keyChoicePending = false;
1973
1973
  const identityLineText = () =>
1974
1974
  identityRow(
1975
1975
  {
1976
- coder: activeCoderName({
1977
- sessionLabel: sessionCodingLabel,
1978
- sessionModel: sessionCodingModel,
1979
- accountDefault: defaultCodingModel,
1980
- }),
1976
+ coder: coderDisplayName(
1977
+ activeCoderName({
1978
+ sessionLabel: sessionCodingLabel,
1979
+ sessionModel: sessionCodingModel,
1980
+ accountDefault: defaultCodingModel,
1981
+ }),
1982
+ activeCoderWhere,
1983
+ ),
1981
1984
  folder: folderLabel(),
1982
1985
  keyUnchosen: keyChoicePending,
1983
1986
  },
@@ -3480,7 +3483,15 @@ if (process.stdin.isTTY) {
3480
3483
  // a running turn owns the screen, and a masked prompt drawn over either corrupts both.
3481
3484
  if (key && key.ctrl && key.name === "k" && !slashOverlayActive && !following
3482
3485
  && !listPickerActive && !approvalActive) {
3483
- void replaceCodingKey();
3486
+ // NOT a bare `void`: this runs outside the command loop that turns Esc into "(cancelled)",
3487
+ // so an Esc at the key's name or secret escaped as an unhandled rejection and ENDED THE
3488
+ // PROCESS (reported: "PromptCancelled: cancelled … Node.js v22"). Cancel is caught here; any
3489
+ // other failure is reported and the session goes on — a keypress must not kill the REPL.
3490
+ replaceCodingKey().catch((e) => {
3491
+ if (wasPromptCancelled(e)) console.log(dim(" (cancelled)\n"));
3492
+ else console.log(red(` ctrl+k failed: ${e?.message ?? e}\n`));
3493
+ paintBar();
3494
+ });
3484
3495
  return;
3485
3496
  }
3486
3497
  if (key && key.ctrl && key.name === "t" && !slashOverlayActive) {
@@ -5780,8 +5791,11 @@ async function chooseModel() {
5780
5791
  // OpenAI-compatible providers are both "OpenAI-compatible", so the kind names neither.
5781
5792
  const { tags, show: showTag } = codingChoiceTags(choices);
5782
5793
  const labels = choices.map((c, i) => {
5783
- const tag = showTag ? ` ${dim(`· ${tags[i]}`)}` : "";
5784
- return `${c.model}${tag}${c.id === def ? " (default)" : ""}`;
5794
+ // An Ollama row says WHERE it runs in front of the name ("ollama.com/kimi-k3"), so the kind tag
5795
+ // beside it would only repeat "Ollama". Other rows keep the tag rule.
5796
+ const where = ollamaWhere(c.kind, c.url);
5797
+ const tag = showTag && !where ? ` ${dim(`· ${tags[i]}`)}` : "";
5798
+ return `${coderDisplayName(c.model, where)}${tag}${c.id === def ? " (default)" : ""}`;
5785
5799
  });
5786
5800
  const activeIdx = choices.findIndex((c) => c.id === active);
5787
5801
  labels.push("change the Ollama server this machine uses…");
@@ -5827,9 +5841,12 @@ async function chooseModel() {
5827
5841
  // WRITTEN NOW, not at the end of the next turn: `/model` then `/exit` is how the choice is
5828
5842
  // made and then tested, and that path never reaches saveSession.
5829
5843
  await saveSessionCoding(resolve(process.cwd()));
5830
- const where = showTag ? ` (${tags[idx]})` : "";
5844
+ activeCoderWhere = ollamaWhere(chosen.kind, chosen.url);
5845
+ const where = showTag && !activeCoderWhere ? ` (${tags[idx]})` : "";
5831
5846
  console.log(
5832
- accent(` ✓ coding model → ${chosen.model}${where}${chosen.id === def ? " (default)" : ""}\n`)
5847
+ accent(
5848
+ ` ✓ coding model → ${coderDisplayName(chosen.model, activeCoderWhere)}${where}${chosen.id === def ? " (default)" : ""}\n`
5849
+ )
5833
5850
  );
5834
5851
  // DOES THIS ONE NEED AN API KEY? (task #181) Reported live on Windows 11 / Client Expert:
5835
5852
  // picking kimi-k3 here and asking a question just span — the coder needs a key, none was
@@ -7669,6 +7686,8 @@ let defaultCodingModel = "";
7669
7686
  // another backend must not keep showing the previous backend's total). Empty = let the API
7670
7687
  // fall back to the account-default URL from settings (older behaviour).
7671
7688
  let sessionCodingUrl = "";
7689
+ /** "ollama.com" / "127.0.0.1" / a host — where the ACTIVE coder runs, when it is an Ollama one. */
7690
+ let activeCoderWhere = "";
7672
7691
  // The account-default backend's URL (from the startup probe's codingChoices), used when no
7673
7692
  // /model override is active. Set once at startup alongside defaultCodingModel.
7674
7693
  let defaultCodingUrl = "";
@@ -9329,6 +9348,17 @@ async function main() {
9329
9348
  sessionCodingUrl = "";
9330
9349
  }
9331
9350
  }
9351
+ {
9352
+ // WHERE THE CODER RUNS, for the banner and the bar: the folder's pick, else the default.
9353
+ const choices = Array.isArray(probe?.codingChoices) ? probe.codingChoices : [];
9354
+ const active =
9355
+ (sessionCodingModel && choices.find((c) => c.id === sessionCodingModel || c.model === sessionCodingModel)) ||
9356
+ choices.find((c) => c.isDefault) ||
9357
+ null;
9358
+ // No choice list (an older API): no prefix, rather than guess from the ACCOUNT default's raw
9359
+ // payload fields, which key-choice-regression.test.mjs forbids for exactly that reason.
9360
+ activeCoderWhere = active ? ollamaWhere(active.kind, active.url) : "";
9361
+ }
9332
9362
  // Task #133: no RAG storage question any more — the terminal ALWAYS keeps the index on
9333
9363
  // this machine. `/rag-cloud` opts into additionally mirroring it to the user's S3 bucket.
9334
9364
  // Startup banner (task #131): the gu logo on the left, compact status facts on the
@@ -9377,6 +9407,7 @@ async function main() {
9377
9407
  sessionLabel: sessionCodingLabel,
9378
9408
  sessionModel: sessionCodingModel,
9379
9409
  }),
9410
+ where: activeCoderWhere,
9380
9411
  }),
9381
9412
  lbl("RAG") +
9382
9413
  (p.ragEnabled
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gu-cli",
3
- "version": "1.0.696",
3
+ "version": "1.0.698",
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",