@tiens.nguyen/gu-cli 1.0.869 → 1.0.871

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.
@@ -355,73 +355,6 @@ export function agentRecoveryOptions({ keyProblem = false, host = "" } = {}) {
355
355
  ];
356
356
  }
357
357
 
358
- // ---------------------------------------------------------------------------------------------
359
- // #43 — /model's "change the Ollama server" flow, when the agent is NOT on an Ollama.
360
- // ---------------------------------------------------------------------------------------------
361
-
362
- /**
363
- * Is this machine's agent model on an OpenAI-compatible PROVIDER rather than an Ollama?
364
- *
365
- * Reported (#43): client mode, /model → change the Ollama server offered the agent's URL —
366
- * https://api.openai.com — as the "agent + embedding server", probed it for Ollama's /api/tags, got
367
- * 404, said "check that Ollama is listening", and asked again, with no way forward but typing "no".
368
- * A provider is known two ways: a provider row for that URL says so, or the URL is one of the hosted
369
- * APIs everyone knows is not an Ollama.
370
- */
371
- const KNOWN_PROVIDER_HOSTS = [
372
- "api.openai.com",
373
- "openrouter.ai",
374
- "api.together.xyz",
375
- "api.moonshot.ai",
376
- "api.groq.com",
377
- "api.deepseek.com",
378
- "api.mistral.ai",
379
- "generativelanguage.googleapis.com",
380
- ];
381
-
382
- export function agentIsOnProvider(settings) {
383
- const source = agentListSource(settings);
384
- if (!source.url) return false;
385
- if (source.dialect === "openai") return true;
386
- let host = "";
387
- try {
388
- host = new URL(String(settings?.agentModelUrl ?? "")).hostname.toLowerCase();
389
- } catch {
390
- return false;
391
- }
392
- return KNOWN_PROVIDER_HOSTS.some((h) => host === h || host.endsWith(`.${h}`));
393
- }
394
-
395
- /**
396
- * The address to offer for the CODING server: the coder's own URL when it is not a known provider,
397
- * else this machine's Ollama. Never the agent's provider URL — that is what put api.openai.com in an
398
- * Ollama prompt.
399
- */
400
- export function coderServerDefault(settings) {
401
- const coder = String(settings?.agentCodingModelUrl ?? "").trim();
402
- if (coder && !agentIsOnProvider({ ...settings, agentModelUrl: coder })) return coder;
403
- const agent = String(settings?.agentModelUrl ?? "").trim();
404
- if (agent && !agentIsOnProvider(settings)) return agent;
405
- return "http://127.0.0.1:11434";
406
- }
407
-
408
- /**
409
- * A server that ANSWERED but has no /api/tags is not an Ollama — an OpenAI-compatible API answers 404
410
- * there. That is a different fix from "not listening", and saying the wrong one sends people to check
411
- * a server that is working fine.
412
- */
413
- export function describeNotOllama(host, role) {
414
- return role === "agent"
415
- ? [
416
- `${host} answers, but it is not an Ollama server (no /api/tags).`,
417
- "an OpenAI-compatible agent is changed with /agent-model — or type 'no' to skip this role.",
418
- ]
419
- : [
420
- `${host} answers, but it is not an Ollama server (no /api/tags).`,
421
- "for an OpenAI-compatible coder use /model → add or change an OpenAI-compatible provider — or type 'no' to skip.",
422
- ];
423
- }
424
-
425
358
  // ---------------------------------------------------------------------------------------------
426
359
  // #44 — WHAT TO CHANGE comes first.
427
360
  // ---------------------------------------------------------------------------------------------
@@ -1138,39 +1138,3 @@ export async function saveSetupKeys({ apiBase = LOCAL_API_BASE, workerKey, model
1138
1138
  return out;
1139
1139
  }
1140
1140
 
1141
- /**
1142
- * THE NEW OLLAMA SERVER'S ROWS, MERGED INTO WHAT IS ALREADY THERE — never replacing it (#43).
1143
- *
1144
- * Found while fixing #43: choosing a coding server in /model → "change the Ollama server" saved
1145
- * codingBackendsForClientExpert(...) AS the whole list — one Ollama row and the Kimi row — deleting
1146
- * every provider the user had added: an api.openai.com row the AGENT's key is found through, an
1147
- * ollama.com cloud row. Their keys, bound to those row ids, were orphaned.
1148
- *
1149
- * Replaced: the local/LAN Ollama row(s) — that is what "the Ollama server this machine uses" means —
1150
- * with the new one, which keeps the first replaced row's id so anything bound to it still resolves.
1151
- * Kept, ids and all: every OpenAI-compatible row, and any Ollama CLOUD row (a paid provider, not
1152
- * "this machine's server"). The template's default provider row is added only when no row already
1153
- * has its URL.
1154
- */
1155
- export function mergeOllamaServerBackends(existing, fresh, { isCloud } = {}) {
1156
- const cloud = typeof isCloud === "function" ? isCloud : () => false;
1157
- const norm = (u) => String(u ?? "").trim().replace(/\/+$/, "").toLowerCase();
1158
- const rows = Array.isArray(existing) ? existing.filter(Boolean) : [];
1159
- const freshOllama = (Array.isArray(fresh) ? fresh : []).filter((f) => f.kind === "ollama");
1160
- const newUrls = new Set(freshOllama.map((f) => norm(f.url)));
1161
- // A row already at the NEW server's URL is the same provider — the new server takes it over, id and
1162
- // all, so a key bound to it (the Ollama cloud's own key) keeps working and no twin row appears.
1163
- const sameServer = rows.filter((b) => newUrls.has(norm(b.url)));
1164
- const replaced = [...sameServer, ...rows.filter((b) => b.kind === "ollama" && !cloud(b.url) && !sameServer.includes(b))];
1165
- const kept = rows.filter((b) => !replaced.includes(b));
1166
- const out = [];
1167
- for (const f of freshOllama) {
1168
- const id = (sameServer.find((b) => b.id && norm(b.url) === norm(f.url)) ?? replaced.find((b) => b.id))?.id;
1169
- out.push(id ? { ...f, id } : f);
1170
- }
1171
- out.push(...kept);
1172
- for (const f of Array.isArray(fresh) ? fresh : []) {
1173
- if (f.kind !== "ollama" && !out.some((b) => norm(b.url) === norm(f.url))) out.push(f);
1174
- }
1175
- return out;
1176
- }
package/doctor-check.mjs CHANGED
@@ -220,6 +220,13 @@ export async function runDoctor(settings, opts = {}) {
220
220
  return { results, ok: results.every((r) => r.status === "ok") };
221
221
  }
222
222
 
223
+ /** Where an unset role is set from the terminal. */
224
+ const SET_WITH = {
225
+ chat: "/agent-model",
226
+ code: "/model → add a provider",
227
+ embed: "/embed-model",
228
+ };
229
+
223
230
  /** One line per role, for the terminal. Pure, so the wording is testable. */
224
231
  export function doctorLines(results) {
225
232
  const mark = { ok: "✓", unset: "·", timeout: "✗", unreachable: "✗", error: "✗" };
@@ -233,7 +240,8 @@ export function doctorLines(results) {
233
240
  out.push(" could not be scheduled. This is not a network problem.");
234
241
  if (r.busy?.hint) out.push(` ${r.busy.hint}`);
235
242
  }
236
- if (r.status === "unset") out.push(" set it with /model → change the Ollama server");
243
+ // Each role has its own command since #412 removed /model's all-in-one Ollama flow.
244
+ if (r.status === "unset") out.push(` set it with ${SET_WITH[r.role] ?? "/model"}`);
237
245
  }
238
246
  return out;
239
247
  }
@@ -10442,10 +10442,7 @@ def run_agent_chat(cfg):
10442
10442
  # wrong python" — indistinguishable, and both were hit while getting a client working.
10443
10443
  import platform as _plat # noqa: PLC0415
10444
10444
  _where = f"{_plat.node()} · {sys.executable}"
10445
- _emit({"type": "final",
10446
- "text": f"[smolagents not installed on {_where}: {e}]\n"
10447
- f"Fix on THAT machine: {sys.executable} -m pip install -U smolagents openai\n"
10448
- f"then: gu stop && gu start"})
10445
+ _emit({"type": "final", "text": _smolagents_missing_text(_where, e, sys.executable, sys.version_info)})
10449
10446
  _log(f"smolagents import FAILED on {_where}: {e}")
10450
10447
  return
10451
10448
  _patch_dotted_imports()
@@ -17077,6 +17074,27 @@ def _request_dump_range(n_msgs: int) -> tuple:
17077
17074
  _EMIT_LOCK = threading.Lock()
17078
17075
 
17079
17076
 
17077
+ def _smolagents_missing_text(where, err, exe, ver):
17078
+ """The turn's message when `import smolagents` fails (reported on a Windows 11 customer, #232).
17079
+
17080
+ Names the exception TYPE (a missing module and a broken dependency need different fixes), says
17081
+ when this Python is too new for prebuilt wheels (> 3.14: pip then tries to compile PyYAML and
17082
+ fails without Visual C++), and leads with `gu doctor`, which installs into the interpreter the
17083
+ background worker actually uses and pins it - a bare pip line can put the packages somewhere
17084
+ else."""
17085
+ kind = type(err).__name__
17086
+ lines = [f"[smolagents could not be loaded on {where}: {kind}: {err}]"]
17087
+ if (ver[0], ver[1]) > (3, 14):
17088
+ lines.append(f"This Python is {ver[0]}.{ver[1]}: too new for the packages' prebuilt wheels, so pip "
17089
+ f"cannot install them. Install Python 3.13 (Windows: winget install Python.Python.3.13), "
17090
+ f"open a new terminal, then run: gu doctor")
17091
+ else:
17092
+ lines.append("Fix: run `gu doctor` (it installs smolagents + openai into the Python the worker uses), "
17093
+ "then: gu stop && gu start")
17094
+ lines.append(f"By hand, on THAT machine: {exe} -m pip install -U smolagents openai")
17095
+ return "\n".join(lines)
17096
+
17097
+
17080
17098
  def _emit(obj):
17081
17099
  """Write one NDJSON line to the real stdout and flush immediately.
17082
17100
 
package/gu-cli.mjs CHANGED
@@ -1799,6 +1799,25 @@ if (args[0] === "mongo") {
1799
1799
  process.exit(0);
1800
1800
  }
1801
1801
 
1802
+ /** The gu-service check (#413), as lines — shared by `gu doctor` and the terminal's /doctor. */
1803
+ async function guServiceLines() {
1804
+ const sc = await import("./service-check.mjs");
1805
+ const { hostedApiBase } = await import("./client-expert-api.mjs");
1806
+ const { isLocalApiBase } = await import("./terminal-layout.mjs");
1807
+ const { loadProxy } = await import("./proxy-store.mjs");
1808
+ const { dispatcherFor } = await import("./proxy-dispatcher.mjs");
1809
+ const apiBase = String(process.env.GONEXT_API_BASE ?? "").trim();
1810
+ const { lines } = await sc.checkGuService({
1811
+ hostedBase: hostedApiBase(),
1812
+ apiBase,
1813
+ workerKey: String(process.env.GONEXT_WORKER_KEY ?? "").trim(),
1814
+ localApi: isLocalApiBase(apiBase),
1815
+ proxy: await loadProxy().catch(() => null),
1816
+ dispatcherFor,
1817
+ });
1818
+ return lines;
1819
+ }
1820
+
1802
1821
  if (args[0] === "doctor") {
1803
1822
  // Task #304: a Mac whose MongoDB runs in Docker is told the native option, once, up front —
1804
1823
  // a working Docker MongoDB is left alone (it is a choice), a silent one is named as stuck.
@@ -2001,6 +2020,15 @@ if (args[0] === "doctor") {
2001
2020
  console.log(` (local stack check skipped: ${e.message})`);
2002
2021
  }
2003
2022
  }
2023
+ // CAN A CODING TURN START? (#413) Last, after the local API: its instructions come from the
2024
+ // gu service, through that API. Reported on a Windows VM where doctor passed everything, "hi"
2025
+ // worked, and every coding turn failed — this was the one thing doctor never looked at.
2026
+ try {
2027
+ console.log("\n Checking the gu service (a coding turn needs it)…");
2028
+ for (const l of await guServiceLines()) console.log(` ${l}`);
2029
+ } catch (e) {
2030
+ console.log(` (gu service check skipped: ${e?.message ?? e})`);
2031
+ }
2004
2032
  } finally {
2005
2033
  rl0.close();
2006
2034
  }