@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.
- package/agent-model-command.mjs +0 -67
- package/client-expert-api.mjs +0 -36
- package/doctor-check.mjs +9 -1
- package/gonext_agent_chat.py +22 -4
- package/gu-cli.mjs +28 -0
- package/gu-repl.mjs +178 -450
- package/hints.mjs +67 -0
- package/model-doctor.mjs +2 -7
- package/package.json +4 -1
- package/platform-tools.mjs +30 -2
- package/quick-models.mjs +64 -0
- package/service-check.mjs +181 -0
package/agent-model-command.mjs
CHANGED
|
@@ -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
|
// ---------------------------------------------------------------------------------------------
|
package/client-expert-api.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/gonext_agent_chat.py
CHANGED
|
@@ -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
|
}
|