@tiens.nguyen/gu-cli 1.0.695 → 1.0.696

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.
@@ -52,6 +52,34 @@ export function isKeylessCodingHost(url) {
52
52
  return host === "gomarsic.cc" || host.endsWith(".gomarsic.cc");
53
53
  }
54
54
 
55
+ /**
56
+ * TRUE only for the Ollama CLOUD (https://ollama.com, or a subdomain of it). Mirrors
57
+ * isOllamaCloudUrl in api/src/store/settingsTypes.ts, web/src/lib/coderChoices.mjs and
58
+ * _isOllamaCloudUrl in gu-cli.mjs — keep them in step.
59
+ */
60
+ export function isOllamaCloudUrl(url) {
61
+ const v = String(url ?? "").trim();
62
+ if (!/^https:\/\//i.test(v)) return false;
63
+ let host;
64
+ try {
65
+ host = new URL(v).hostname.toLowerCase().replace(/\.$/, "");
66
+ } catch {
67
+ return false;
68
+ }
69
+ return host === "ollama.com" || host.endsWith(".ollama.com");
70
+ }
71
+
72
+ /**
73
+ * DOES THIS CODER NEED AN API KEY? — an OpenAI-compatible provider, or Ollama's paid CLOUD.
74
+ *
75
+ * Reported: /model on kimi-k3 at https://ollama.com said "needs no API key — nothing to set", so
76
+ * there was no way to give it one. Every "needs a key" check had been `kind === "openai"`, on the
77
+ * belief that Ollama never authenticates — true of a local or LAN Ollama, false of ollama.com.
78
+ */
79
+ export function codingNeedsKey(kind, url) {
80
+ return kind === "openai" || (kind === "ollama" && isOllamaCloudUrl(url));
81
+ }
82
+
55
83
  /**
56
84
  * @param payload the /agent-payload `payload` object (reads codingKeyMissing/codingKeyModel).
57
85
  * @param deps.askSecret async (prompt) => string — masked input; "" means the user skipped.
@@ -187,9 +215,10 @@ export function codingKeyFallbackLine(kj, jobId = "") {
187
215
  * when a choice HAS been made, and when only one key exists — with one there is nothing to
188
216
  * choose, and a warning on a correct setup is how warnings stop being read.
189
217
  */
190
- export function unchosenKeyWarning({ codingKind = "", selectedKeyId = "", keys = null } = {}) {
218
+ export function unchosenKeyWarning({ codingKind = "", codingUrl = "", selectedKeyId = "", keys = null } = {}) {
191
219
  const quiet = { warn: false, using: null, why: "" };
192
- if (codingKind !== "openai") return quiet; // Ollama and local MLX need no credential
220
+ // Local Ollama and MLX need no credential; the Ollama cloud does.
221
+ if (!codingNeedsKey(codingKind, codingUrl)) return quiet;
193
222
  if (!Array.isArray(keys) || keys.length < 2) return quiet;
194
223
  if (String(selectedKeyId ?? "").trim()) return quiet;
195
224
  return { warn: true, using: keys[0], why: "none-chosen" };
@@ -200,7 +229,8 @@ export function unchosenKeyWarning({ codingKind = "", selectedKeyId = "", keys =
200
229
  *
201
230
  * REPORTED: the bar's "key not chosen" mark only appeared after running /model. The startup gate
202
231
  * read `payload.codingKind`, and a payload fetched with NO override describes the ACCOUNT DEFAULT
203
- * — on the reporting machine that was kimi-k3 at ollama.com, kind "ollama", which needs no key —
232
+ * — on the reporting machine kimi-k3 at ollama.com, kind "ollama" (which, it turned out later,
233
+ * DOES need a key: see codingNeedsKey) —
204
234
  * while the folder was pointed at gpt-5, kind "openai". So the check answered a question about a
205
235
  * coder the folder was not using, said "nothing to warn about", and stayed silent. /model looked
206
236
  * right because it re-probes WITH the override.
@@ -212,6 +242,17 @@ export function unchosenKeyWarning({ codingKind = "", selectedKeyId = "", keys =
212
242
  * Matched by id OR by bare model name, mirroring that validation: a session saved before ids were
213
243
  * qualified ("<kind>::<model>", task #123) stores the bare form and is still a valid pick.
214
244
  */
245
+ /** The folder's coder — its kind AND its URL, which together decide whether it needs a key. */
246
+ export function effectiveCoder({ sessionCodingModel = "", choices = null, defaultKind = "", defaultUrl = "" } = {}) {
247
+ const want = String(sessionCodingModel ?? "").trim();
248
+ const list = Array.isArray(choices) ? choices : [];
249
+ if (want) {
250
+ const hit = list.find((c) => c?.id === want || c?.model === want);
251
+ if (hit) return { kind: String(hit.kind ?? ""), url: String(hit.url ?? "") };
252
+ }
253
+ return { kind: String(defaultKind ?? ""), url: String(defaultUrl ?? "") };
254
+ }
255
+
215
256
  export function effectiveCoderKind({
216
257
  sessionCodingModel = "",
217
258
  choices = null,
package/gu-cli.mjs CHANGED
@@ -20,7 +20,7 @@ import { execFile as execFileCallback, spawn } from "node:child_process";
20
20
  import { createHash } from "node:crypto";
21
21
  import { createTaskPool, createFolderQueue } from "./job-pools.mjs";
22
22
  import { ragJobConfig } from "./rag-job-config.mjs";
23
- import { codingKeyFallbackLine } from "./coding-key-prompt.mjs";
23
+ import { codingKeyFallbackLine, isOllamaCloudUrl } from "./coding-key-prompt.mjs";
24
24
  // Pure layout helpers — safe to import here (no side effects on load, unlike gu-repl.mjs).
25
25
  // Shared so the mid-run picker and the REPL's post-stop advice cannot word the same budget
26
26
  // differently.
@@ -3265,22 +3265,8 @@ function _isLocalCodingUrl(baseURL) {
3265
3265
  return !host.includes(".") && !host.includes(":");
3266
3266
  }
3267
3267
 
3268
- /** TRUE only for the Ollama CLOUD endpoint (https://ollama.com[/...]) — the one Ollama
3269
- * backend that actually requires an API key. Any other Ollama URL (localhost, a LAN
3270
- * box, a self-hosted remote) has no auth and gets no key. Mirrors isOllamaCloudUrl in
3271
- * web/src/lib/coderChoices.mjs — keep the two in step. */
3272
- function _isOllamaCloudUrl(baseURL) {
3273
- const v = String(baseURL || "").trim();
3274
- if (!/^https:\/\//i.test(v)) return false;
3275
- let host = "";
3276
- try {
3277
- host = new URL(v).hostname.toLowerCase().replace(/\.$/, "");
3278
- } catch {
3279
- return false;
3280
- }
3281
- return host === "ollama.com" || host.endsWith(".ollama.com");
3282
- }
3283
-
3268
+ /** The Ollama cloud check lives in coding-key-prompt.mjs, shared with the terminal. */
3269
+ const _isOllamaCloudUrl = isOllamaCloudUrl;
3284
3270
 
3285
3271
  /** Resolve to "ollama" | "openai" | "local". `kind` is the user's Settings choice
3286
3272
  * ("ollama" | "openai" | "" = Auto). Only Auto costs a probe. */
package/gu-repl.mjs CHANGED
@@ -6779,7 +6779,9 @@ async function coderKeyState(override = "") {
6779
6779
  const active = override
6780
6780
  ? choices.find((c) => c.id === override)
6781
6781
  : choices.find((c) => c.isDefault);
6782
- const needsKey = active?.kind === "openai";
6782
+ // Kind AND url: an Ollama coder at https://ollama.com needs a key; a local one does not.
6783
+ const { codingNeedsKey } = await import("./coding-key-prompt.mjs");
6784
+ const needsKey = codingNeedsKey(active?.kind, active?.url);
6783
6785
  return {
6784
6786
  needsKey,
6785
6787
  model: p.codingKeyModel || active?.model || "the coding model",
@@ -9561,7 +9563,7 @@ async function main() {
9561
9563
  // folder has not chosen, and more than one is stored.
9562
9564
  try {
9563
9565
  if (!keyRes?.saved) {
9564
- const { unchosenKeyWarning, effectiveCoderKind } = await import(
9566
+ const { unchosenKeyWarning, effectiveCoder, codingNeedsKey } = await import(
9565
9567
  "./coding-key-prompt.mjs"
9566
9568
  );
9567
9569
  // THE RULE LIVES IN THE MODULE so it can be tested by calling it. Open-coded here it
@@ -9574,15 +9576,17 @@ async function main() {
9574
9576
  // machine an Ollama coder, which needs no key — while the folder was on gpt-5. Reading
9575
9577
  // it directly made this check answer about a coder nobody was using, and the mark only
9576
9578
  // appeared after /model, which re-probes WITH the override.
9577
- const kind = effectiveCoderKind({
9579
+ const { kind, url } = effectiveCoder({
9578
9580
  sessionCodingModel,
9579
9581
  choices: probe?.codingChoices,
9580
9582
  defaultKind: p?.codingKind,
9583
+ defaultUrl: probe?.codingChoices?.find?.((c) => c?.isDefault)?.url ?? "",
9581
9584
  });
9582
9585
  const allKeys =
9583
- kind === "openai" && !sessionCodingKeyId ? await fetchCodingKeys() : null;
9586
+ codingNeedsKey(kind, url) && !sessionCodingKeyId ? await fetchCodingKeys() : null;
9584
9587
  const w = unchosenKeyWarning({
9585
9588
  codingKind: kind,
9589
+ codingUrl: url,
9586
9590
  selectedKeyId: sessionCodingKeyId,
9587
9591
  keys: allKeys,
9588
9592
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gu-cli",
3
- "version": "1.0.695",
3
+ "version": "1.0.696",
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",