@tiens.nguyen/gu-cli 1.0.868 → 1.0.870
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/gonext_agent_chat.py +22 -4
- package/gu-repl.mjs +130 -13
- package/model-doctor.mjs +2 -7
- package/package.json +2 -1
- package/platform-tools.mjs +30 -2
- package/quick-models.mjs +64 -0
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-repl.mjs
CHANGED
|
@@ -157,6 +157,7 @@ import { globalPrefixOf } from "./install-path.mjs";
|
|
|
157
157
|
import { indentBlock, commandBlockRows } from "./terminal-layout.mjs"; // every line of an Allow-prompt detail indented (#189); wrapped + collapsible command (#204)
|
|
158
158
|
import { firstGuOnPath, pathGuMismatch, guOnPathIsCurrent, updateAttemptRecord, repeatUpdateNotice } from "./update-verify.mjs";
|
|
159
159
|
import { embedNotice, embedState, setupNoticeRows } from "./embed-model-command.mjs";
|
|
160
|
+
import { normalizeQuick, quickReady, quickTarget, quickBarHint, quickWarning, QUICK_MAX } from "./quick-models.mjs";
|
|
160
161
|
import {
|
|
161
162
|
MODELFALLBACK_PREFIX, fallbackAfterTurn, fallbackHistoryLine, fallbackNotice, keyMissingNotice,
|
|
162
163
|
parseModelFallback,
|
|
@@ -991,6 +992,7 @@ const COMMANDS = [
|
|
|
991
992
|
// "/model" has meant the CODING model since task #44, and "agent model" vs "agent coding model"
|
|
992
993
|
// has already confused the person who built both. The descriptions carry the distinction.
|
|
993
994
|
{ name: "/model", desc: "switch the CODING model (the one that writes code) for this session" },
|
|
995
|
+
{ name: "/quick-models", desc: "pick up to 2 coding models to flip between with ← / → at the prompt" },
|
|
994
996
|
// /provider IS NOT LISTED HERE ANY MORE — see HIDDEN_COMMANDS. It was promoted to a top-level
|
|
995
997
|
// command when editing a provider was genuinely unreachable, and by the very next change
|
|
996
998
|
// /model carried "add or change a provider — Claude API, OpenAI, Kimi…" as a row in EVERY branch of
|
|
@@ -1855,6 +1857,7 @@ const fallbackNoticeRows = () => {
|
|
|
1855
1857
|
...(codingKeyMissingKnown === true
|
|
1856
1858
|
? [keyMissingNotice({ coder: activeCodingModelId(), width, gutter: GUTTER })]
|
|
1857
1859
|
: []),
|
|
1860
|
+
...[quickWarning(quickModels, quickActiveId(), { width, gutter: GUTTER })].filter(Boolean),
|
|
1858
1861
|
];
|
|
1859
1862
|
};
|
|
1860
1863
|
|
|
@@ -2364,7 +2367,7 @@ function slashCloseSeq(line, cursor, cols) {
|
|
|
2364
2367
|
let editMode = "accept"; // "accept" | "manual"
|
|
2365
2368
|
const modeLineText = () =>
|
|
2366
2369
|
white("✎ " + (editMode === "accept" ? "accept edits on" : "manually edit")) +
|
|
2367
|
-
dim(" (shift+tab to switch)");
|
|
2370
|
+
dim(" (shift+tab to switch)") + (quickBarHint(quickModels) ? dim(" " + quickBarHint(quickModels)) : "");
|
|
2368
2371
|
// Token usage for the bar's middle row (task #104): the LAST turn's code-model input +
|
|
2369
2372
|
// output, then the coder's lifetime output total. Unlike the old per-turn print, this row is
|
|
2370
2373
|
// ALWAYS on screen — a turn that did no code-model work honestly reads "0", and the lifetime
|
|
@@ -4732,6 +4735,14 @@ if (process.stdin.isTTY) {
|
|
|
4732
4735
|
if (key && key.ctrl && key.name === "t" && !slashOverlayActive) {
|
|
4733
4736
|
if (toggleNewestThought()) return;
|
|
4734
4737
|
}
|
|
4738
|
+
// ← / → at the EMPTY prompt flips between the two quick models (#233). Only at an idle prompt:
|
|
4739
|
+
// not in a question (askWaiting), not during a command or a turn (inputVisible is false then),
|
|
4740
|
+
// not over the slash menu, and never on a line with text — there the arrows move the caret.
|
|
4741
|
+
if (key && (key.name === "left" || key.name === "right") && !key.ctrl && !key.meta && !key.shift
|
|
4742
|
+
&& rl.line === "" && inputVisible && !askWaiting && !slashOverlayActive && quickReady(quickModels)) {
|
|
4743
|
+
void quickSwitch();
|
|
4744
|
+
return;
|
|
4745
|
+
}
|
|
4735
4746
|
// Shift+Tab cycles the edit mode shown in the bottom bar (display-only). Skip while the
|
|
4736
4747
|
// slash overlay owns the screen so we don't fight its redraw.
|
|
4737
4748
|
if (key && key.name === "tab" && key.shift) {
|
|
@@ -5779,6 +5790,25 @@ async function loadSessionCoding(cwd) {
|
|
|
5779
5790
|
}
|
|
5780
5791
|
}
|
|
5781
5792
|
|
|
5793
|
+
async function loadQuickModels(cwd) {
|
|
5794
|
+
try {
|
|
5795
|
+
return normalizeQuick(JSON.parse(await readFile(sessionFilePath(cwd), "utf8"))?.quickModels);
|
|
5796
|
+
} catch {
|
|
5797
|
+
return [];
|
|
5798
|
+
}
|
|
5799
|
+
}
|
|
5800
|
+
|
|
5801
|
+
async function saveQuickModels(cwd) {
|
|
5802
|
+
try {
|
|
5803
|
+
await updateSessionFile(cwd, (raw) => {
|
|
5804
|
+
raw.quickModels = quickModels;
|
|
5805
|
+
raw.updatedAt = new Date().toISOString();
|
|
5806
|
+
});
|
|
5807
|
+
} catch (err) {
|
|
5808
|
+
console.error(dim(`(quick models save failed: ${err.message})`));
|
|
5809
|
+
}
|
|
5810
|
+
}
|
|
5811
|
+
|
|
5782
5812
|
/** This folder's stored choice — the SHAPE reading lives in terminal-layout, where it is
|
|
5783
5813
|
* testable; only the file I/O is here. */
|
|
5784
5814
|
async function loadSessionRagStorage(cwd) {
|
|
@@ -7275,6 +7305,76 @@ async function useCoderInThisFolder({ ps, rows, beforeChoices, url, host, locali
|
|
|
7275
7305
|
{ const g = pickGlobalTotal(t); if (g !== null) globalOutputTokens = g; }
|
|
7276
7306
|
}
|
|
7277
7307
|
|
|
7308
|
+
|
|
7309
|
+
/**
|
|
7310
|
+
* The picker rows for a list of coding choices, and the tags that tell same-named ones apart.
|
|
7311
|
+
* Shared by /model and /quick-models (#233) so the two lists read identically.
|
|
7312
|
+
*/
|
|
7313
|
+
function codingChoiceLabels(choices, def) {
|
|
7314
|
+
const { tags, show: showTag } = codingChoiceTags(choices);
|
|
7315
|
+
const labels = choices.map((c, i) => {
|
|
7316
|
+
// Every row says WHERE it runs in front of the name ("ollama.com/kimi-k3",
|
|
7317
|
+
// "api.anthropic.com/claude-opus-5" — #263), so the kind tag is only for a row with no server.
|
|
7318
|
+
const where = coderWhere(c.kind, c.url);
|
|
7319
|
+
const tag = showTag && !where ? ` ${dim(`· ${tags[i]}`)}` : "";
|
|
7320
|
+
return `${coderDisplayName(c.model, where)}${tag}${c.id === def ? " (default)" : ""}`;
|
|
7321
|
+
});
|
|
7322
|
+
return { labels, tags, showTag };
|
|
7323
|
+
}
|
|
7324
|
+
|
|
7325
|
+
/**
|
|
7326
|
+
* /quick-models — tick up to two coding models to flip between with ← / → (#233). The list is the
|
|
7327
|
+
* same (backend, model) choices /model offers, pre-ticked from what is saved; Enter saves, Esc leaves
|
|
7328
|
+
* it as it was.
|
|
7329
|
+
*/
|
|
7330
|
+
async function chooseQuickModels() {
|
|
7331
|
+
let probe;
|
|
7332
|
+
try {
|
|
7333
|
+
probe = await withPendingStatus("loading models", () => fetchAgentPayload([{ role: "user", content: "ping" }]));
|
|
7334
|
+
} catch (err) {
|
|
7335
|
+
console.log(red(` ${describeApiFailure(err, "couldn't load models")}\n`));
|
|
7336
|
+
return;
|
|
7337
|
+
}
|
|
7338
|
+
const def = (probe?.codingDefault ?? "").trim();
|
|
7339
|
+
const choices = Array.isArray(probe?.codingChoices) && probe.codingChoices.length
|
|
7340
|
+
? probe.codingChoices
|
|
7341
|
+
: (Array.isArray(probe?.codingAllowed) ? probe.codingAllowed : []).map((id) => ({ id, model: id, kind: "" }));
|
|
7342
|
+
if (choices.length < 2) {
|
|
7343
|
+
console.log(dim(" Quick models need at least two coding models — add one with /model → add or change a provider.\n"));
|
|
7344
|
+
return;
|
|
7345
|
+
}
|
|
7346
|
+
const { labels } = codingChoiceLabels(choices, def);
|
|
7347
|
+
const pre = quickModels.map((q) => choices.findIndex((c) => c.id === q.id)).filter((i) => i >= 0);
|
|
7348
|
+
console.log(dim(` Tick up to ${QUICK_MAX} models to flip between with ← / → (space ticks · Enter saves · Esc cancels):`));
|
|
7349
|
+
const picked = await pickManyFromList(labels, pre, { max: QUICK_MAX });
|
|
7350
|
+
if (picked === null) {
|
|
7351
|
+
console.log(dim(" (cancelled)\n"));
|
|
7352
|
+
return;
|
|
7353
|
+
}
|
|
7354
|
+
quickModels = normalizeQuick(picked.map((i) => choices[i]));
|
|
7355
|
+
await saveQuickModels(resolve(process.cwd()));
|
|
7356
|
+
if (quickModels.length === 0) console.log(dim(" quick models cleared — ← / → does nothing.\n"));
|
|
7357
|
+
else if (quickModels.length === 1) console.log(accent(` ✓ 1 quick model: ${quickModels[0].model}`) + dim(" — pick a second to switch with ← / →\n"));
|
|
7358
|
+
else console.log(accent(` ✓ quick models: ${quickModels.map((c) => c.model).join(" ⇄ ")}`) + dim(" (← / → at the prompt)\n"));
|
|
7359
|
+
paintBar();
|
|
7360
|
+
}
|
|
7361
|
+
|
|
7362
|
+
/** ← / → at the empty prompt: flip to the other quick model, through the same steps as /model. */
|
|
7363
|
+
async function quickSwitch() {
|
|
7364
|
+
if (quickSwitching) return;
|
|
7365
|
+
const target = quickTarget(quickModels, quickActiveId());
|
|
7366
|
+
if (!target) return;
|
|
7367
|
+
quickSwitching = true;
|
|
7368
|
+
try {
|
|
7369
|
+
await applyChosenCoder(target, { quick: true });
|
|
7370
|
+
} catch (e) {
|
|
7371
|
+
if (!wasPromptCancelled(e)) console.log(red(` quick switch failed: ${e?.message ?? e}\n`));
|
|
7372
|
+
} finally {
|
|
7373
|
+
quickSwitching = false;
|
|
7374
|
+
paintBar();
|
|
7375
|
+
}
|
|
7376
|
+
}
|
|
7377
|
+
|
|
7278
7378
|
async function chooseModel() {
|
|
7279
7379
|
let probe;
|
|
7280
7380
|
try {
|
|
@@ -7345,14 +7445,7 @@ async function chooseModel() {
|
|
|
7345
7445
|
// Which tag tells these rows apart — the kind, or the provider the API derived from each
|
|
7346
7446
|
// URL. The rule and its reasons live in coding-choice-tags.mjs (task #223): two
|
|
7347
7447
|
// OpenAI-compatible providers are both "OpenAI-compatible", so the kind names neither.
|
|
7348
|
-
const { tags,
|
|
7349
|
-
const labels = choices.map((c, i) => {
|
|
7350
|
-
// Every row says WHERE it runs in front of the name ("ollama.com/kimi-k3",
|
|
7351
|
-
// "api.anthropic.com/claude-opus-5" — #263), so the kind tag is only for a row with no server.
|
|
7352
|
-
const where = coderWhere(c.kind, c.url);
|
|
7353
|
-
const tag = showTag && !where ? ` ${dim(`· ${tags[i]}`)}` : "";
|
|
7354
|
-
return `${coderDisplayName(c.model, where)}${tag}${c.id === def ? " (default)" : ""}`;
|
|
7355
|
-
});
|
|
7448
|
+
const { labels, tags, showTag } = codingChoiceLabels(choices, def);
|
|
7356
7449
|
const activeIdx = choices.findIndex((c) => c.id === active);
|
|
7357
7450
|
labels.push("change the Ollama server this machine uses…");
|
|
7358
7451
|
// Task #223. Appended LAST, after the #220 entry, for the same reason that one is appended
|
|
@@ -7384,7 +7477,16 @@ async function chooseModel() {
|
|
|
7384
7477
|
else await manageCodingProviders();
|
|
7385
7478
|
return;
|
|
7386
7479
|
}
|
|
7387
|
-
|
|
7480
|
+
await applyChosenCoder(choices[idx], { def, tag: showTag ? tags[idx] : "" });
|
|
7481
|
+
}
|
|
7482
|
+
|
|
7483
|
+
/**
|
|
7484
|
+
* Make `chosen` ({ id, model, kind, url }) the coding model: session state, saved choice, the key
|
|
7485
|
+
* check and the lifetime token total. The tail of /model, extracted so /quick-models' ← / → (#233)
|
|
7486
|
+
* runs EXACTLY the same steps — a quick switch that skipped the key prompt would leave gu on a
|
|
7487
|
+
* coder it cannot use (#181). `quick` only quietens the output: one ✓ line, no key essay.
|
|
7488
|
+
*/
|
|
7489
|
+
async function applyChosenCoder(chosen, { def = "", tag = "", quick = false } = {}) {
|
|
7388
7490
|
// Empty override means "use the account default" — so don't send an override when the
|
|
7389
7491
|
// user picks the default (keeps the payload clean and lets a later default change win).
|
|
7390
7492
|
// GitHub #130: what to go back to if the key prompt for the new coder is cancelled (Esc).
|
|
@@ -7409,7 +7511,7 @@ async function chooseModel() {
|
|
|
7409
7511
|
// WRITTEN NOW, not at the end of the next turn: `/model` then `/exit` is how the choice is
|
|
7410
7512
|
// made and then tested, and that path never reaches saveSession.
|
|
7411
7513
|
await saveSessionCoding(resolve(process.cwd()));
|
|
7412
|
-
const where =
|
|
7514
|
+
const where = tag && !activeCoderWhere ? ` (${tag})` : "";
|
|
7413
7515
|
console.log(
|
|
7414
7516
|
accent(
|
|
7415
7517
|
` ✓ coding model → ${coderDisplayName(chosen.model, activeCoderWhere)}${where}${chosen.id === def ? " (default)" : ""}\n`
|
|
@@ -7480,7 +7582,7 @@ async function chooseModel() {
|
|
|
7480
7582
|
backendId: ks.backendId,
|
|
7481
7583
|
host: ks.provider,
|
|
7482
7584
|
});
|
|
7483
|
-
console.log(
|
|
7585
|
+
if (!quick) console.log(
|
|
7484
7586
|
dim(" API key ") + accent(line.text) +
|
|
7485
7587
|
(line.note ? orange(` · ${line.note}`) : "") +
|
|
7486
7588
|
dim(` · ctrl+k to ${line.verb} it`)
|
|
@@ -7490,7 +7592,7 @@ async function chooseModel() {
|
|
|
7490
7592
|
// the more common one — "how do I change the model" — with no visible answer at all. Reported
|
|
7491
7593
|
// three times from exactly this view, twice after being told where the picker entry was.
|
|
7492
7594
|
// /provider is hidden (see HIDDEN_COMMANDS): point at the row of /model that does the same.
|
|
7493
|
-
console.log(dim(" to change this provider's model or URL, or remove it: /model → add or change a provider\n"));
|
|
7595
|
+
if (!quick) console.log(dim(" to change this provider's model or URL, or remove it: /model → add or change a provider\n"));
|
|
7494
7596
|
}
|
|
7495
7597
|
// Task #125: the GLOBAL lifetime total is per-backend, so re-fetch it for the newly
|
|
7496
7598
|
// selected backend NOW — otherwise the footer would keep showing the previous backend's
|
|
@@ -11524,6 +11626,16 @@ let jobIdForApprovalRace = "";
|
|
|
11524
11626
|
// Task #123: with several backends configured this is a QUALIFIED id ("<kind>::<model>"),
|
|
11525
11627
|
// which resolves to a whole backend — URL, kind and API key — not just a model name.
|
|
11526
11628
|
let sessionCodingModel = "";
|
|
11629
|
+
// QUICK MODELS (#233): up to two coding choices ({ id, model, kind, url }) that ← / → flips between at
|
|
11630
|
+
// the empty prompt. Per folder, in the session file, like the /model pick.
|
|
11631
|
+
let quickModels = [];
|
|
11632
|
+
let quickSwitching = false;
|
|
11633
|
+
/** The id the quick list compares against: the folder's pick, else the account default's choice id. */
|
|
11634
|
+
const quickActiveId = () => {
|
|
11635
|
+
if (sessionCodingModel) return sessionCodingModel;
|
|
11636
|
+
const d = quickModels.find((c) => c.id === defaultCodingModel) ?? quickModels.find((c) => c.model === defaultCodingModel);
|
|
11637
|
+
return d ? d.id : defaultCodingModel;
|
|
11638
|
+
};
|
|
11527
11639
|
/**
|
|
11528
11640
|
* WHICH STORED API KEY THIS FOLDER USES for the coding model (task #222, phase 3).
|
|
11529
11641
|
*
|
|
@@ -13823,6 +13935,7 @@ async function main() {
|
|
|
13823
13935
|
sessionEmbedKeyId = await loadSessionFolderKeyId(resolve(process.cwd()), "embed");
|
|
13824
13936
|
}
|
|
13825
13937
|
}
|
|
13938
|
+
quickModels = await loadQuickModels(resolve(process.cwd())); // #233 — folder file only, whatever the database said
|
|
13826
13939
|
// Fetch the API key tail after loading the session, if a key is configured
|
|
13827
13940
|
if (sessionCodingKeyId) {
|
|
13828
13941
|
try {
|
|
@@ -14765,6 +14878,10 @@ async function main() {
|
|
|
14765
14878
|
paintBar();
|
|
14766
14879
|
continue;
|
|
14767
14880
|
}
|
|
14881
|
+
if (line === "/quick-models") {
|
|
14882
|
+
await chooseQuickModels();
|
|
14883
|
+
continue;
|
|
14884
|
+
}
|
|
14768
14885
|
if (line === "/model") {
|
|
14769
14886
|
await chooseModel();
|
|
14770
14887
|
await refreshNoModelAtAll(); // #188: a coding model may have just been set → drop its row
|
package/model-doctor.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { open, mkdir, readdir, writeFile } from "node:fs/promises";
|
|
|
12
12
|
import { arch, homedir, platform, totalmem } from "node:os";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
|
-
import { ensureSupportedPython, isBuildToolsMissing } from "./platform-tools.mjs";
|
|
15
|
+
import { ensureSupportedPython, isBuildToolsMissing, buildFailureMessage } from "./platform-tools.mjs";
|
|
16
16
|
|
|
17
17
|
// Minimum RAM to run the default 14B-4bit chat model comfortably on Apple Silicon.
|
|
18
18
|
const MIN_CHAT_RAM_GB = 16;
|
|
@@ -207,12 +207,7 @@ async function pipInstall(pkgs, log) {
|
|
|
207
207
|
log(` installing ${pkgs.join(" ")} … (pip, this can take a minute)`);
|
|
208
208
|
let r = await runPip(python3Bin(), pkgs);
|
|
209
209
|
if (r.code === 0) return;
|
|
210
|
-
if (isBuildToolsMissing(r.err))
|
|
211
|
-
throw new Error(
|
|
212
|
-
"pip had to compile a package and this machine has no C++ compiler. Use Python 3.13 or 3.12 (64-bit), which " +
|
|
213
|
-
"has prebuilt packages: " + pythonInstallHint() + ". Then run gu doctor again."
|
|
214
|
-
);
|
|
215
|
-
}
|
|
210
|
+
if (isBuildToolsMissing(r.err)) throw new Error(buildFailureMessage(r.err));
|
|
216
211
|
const noPip = isPipMissing(r.err);
|
|
217
212
|
if (!noPip && !isExternallyManaged(r.err)) {
|
|
218
213
|
throw new Error(`pip install ${pkgs.join(" ")} exited ${r.code}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiens.nguyen/gu-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.870",
|
|
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",
|
|
@@ -122,6 +122,7 @@
|
|
|
122
122
|
"model-doctor.mjs",
|
|
123
123
|
"coding-key-prompt.mjs",
|
|
124
124
|
"model-fallback.mjs",
|
|
125
|
+
"quick-models.mjs",
|
|
125
126
|
"terminal-title.mjs",
|
|
126
127
|
"gu-relaunch.mjs",
|
|
127
128
|
"server-keys.mjs",
|
package/platform-tools.mjs
CHANGED
|
@@ -314,9 +314,37 @@ export async function ensureSupportedPython({
|
|
|
314
314
|
};
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
-
/**
|
|
317
|
+
/**
|
|
318
|
+
* pip's "I had to compile and could not" failure. Newer pip words it "error: failed-wheel-build-for-install /
|
|
319
|
+
* Failed to build installable wheels for some pyproject.toml based projects" (reported on a Windows 11
|
|
320
|
+
* customer installing smolagents + openai, #232), older pip "Failed building wheel for X".
|
|
321
|
+
*/
|
|
318
322
|
export function isBuildToolsMissing(output) {
|
|
319
|
-
return /Microsoft Visual C\+\+ 14\.0 or greater is required|Failed building wheel for|error: command '(?:gcc|cl\.exe)' failed/i.test(String(output ?? ""));
|
|
323
|
+
return /Microsoft Visual C\+\+ 14\.0 or greater is required|Failed building wheel for|failed-wheel-build-for-install|Failed to build installable wheels|error: command '(?:gcc|cl\.exe)' failed|could not find (?:`?cargo`?|rustc)|metadata-generation-failed/i.test(String(output ?? ""));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** The packages pip could not build: ["pydantic-core", "jiter"], from either pip wording. */
|
|
327
|
+
export function failedBuildPackages(output) {
|
|
328
|
+
const text = String(output ?? "");
|
|
329
|
+
const names = new Set();
|
|
330
|
+
for (const m of text.matchAll(/Failed building wheel for ([A-Za-z0-9_.-]+)/g)) names.add(m[1]);
|
|
331
|
+
// New pip: "╰─> pydantic-core" (or "-> pydantic-core") under the failed-wheel-build summary.
|
|
332
|
+
const at = text.search(/Failed to build installable wheels/i);
|
|
333
|
+
if (at >= 0) for (const m of text.slice(at).matchAll(/^\s*(?:╰─>|\+->|->)\s*([A-Za-z0-9_.-]+)\s*$/gm)) names.add(m[1]);
|
|
334
|
+
return [...names];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** What to tell the user when pip could not build wheels: which package, and the fix that works. */
|
|
338
|
+
export function buildFailureMessage(output, { platform = process.platform } = {}) {
|
|
339
|
+
const names = failedBuildPackages(output);
|
|
340
|
+
const what = names.length ? `pip could not build ${names.join(", ")}` : "pip had to compile a package and could not";
|
|
341
|
+
const rust = /rustc|cargo|maturin|pydantic[-_]core|jiter|rpds/i.test(String(output ?? "")) || names.some((n) => /pydantic[-_]core|jiter|rpds/i.test(n));
|
|
342
|
+
const how = platform === "win32" ? "winget install Python.Python.3.13" : "your package manager, e.g. python3.13";
|
|
343
|
+
return (
|
|
344
|
+
`${what}. That happens when this Python has no prebuilt package for it (too new, 32-bit, or ARM) and the machine ` +
|
|
345
|
+
`has no compiler${rust ? " (these packages need the Rust toolchain)" : ""}. Install 64-bit Python 3.13 (${how}), ` +
|
|
346
|
+
`open a new terminal, then run gu doctor again.`
|
|
347
|
+
);
|
|
320
348
|
}
|
|
321
349
|
|
|
322
350
|
/** How to SAY the resolved interpreter in a log line or error message. */
|
package/quick-models.mjs
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QUICK MODELS (#233) — two coding models to flip between with ← / → at the prompt.
|
|
3
|
+
*
|
|
4
|
+
* Pure helpers only (no I/O, no terminal): the picker, the key and the bar live in gu-repl.mjs and
|
|
5
|
+
* call these, so the rules below are testable. A quick model is the same (backend, model) PAIR
|
|
6
|
+
* /model offers — { id, model, kind, url } — stored whole, so a switch needs no network round trip
|
|
7
|
+
* to find out what "claude-opus" meant. At most TWO: the point is a flip, not a menu.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const QUICK_MAX = 2;
|
|
11
|
+
|
|
12
|
+
/** At most QUICK_MAX distinct choices, each with an id; anything else in the saved list is dropped. */
|
|
13
|
+
export function normalizeQuick(list) {
|
|
14
|
+
const out = [];
|
|
15
|
+
for (const c of Array.isArray(list) ? list : []) {
|
|
16
|
+
const id = typeof c?.id === "string" ? c.id.trim() : "";
|
|
17
|
+
if (!id || out.some((o) => o.id === id)) continue;
|
|
18
|
+
out.push({
|
|
19
|
+
id,
|
|
20
|
+
model: typeof c.model === "string" && c.model ? c.model : id,
|
|
21
|
+
kind: typeof c.kind === "string" ? c.kind : "",
|
|
22
|
+
url: typeof c.url === "string" ? c.url : "",
|
|
23
|
+
});
|
|
24
|
+
if (out.length === QUICK_MAX) break;
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The ← / → flip needs exactly two. One quick model is a pin, not a flip. */
|
|
30
|
+
export const quickReady = (quick) => normalizeQuick(quick).length === QUICK_MAX;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Where ← / → goes. Active is one of the two → the OTHER one. Active is neither (the user picked
|
|
34
|
+
* something else with /model) → the first one, so a key press always lands somewhere defined.
|
|
35
|
+
* null when fewer than two are chosen.
|
|
36
|
+
*/
|
|
37
|
+
export function quickTarget(quick, activeId) {
|
|
38
|
+
const q = normalizeQuick(quick);
|
|
39
|
+
if (q.length < QUICK_MAX) return null;
|
|
40
|
+
const at = q.findIndex((c) => c.id === activeId);
|
|
41
|
+
return at < 0 ? q[0] : q[1 - at];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Does a model id match a quick choice? The session stores "" for the account default. */
|
|
45
|
+
export const isQuickActive = (quick, activeId) => normalizeQuick(quick).some((c) => c.id === activeId);
|
|
46
|
+
|
|
47
|
+
/** The dim hint beside the mode on the bar: only when the keys do something. */
|
|
48
|
+
export const quickBarHint = (quick) => (quickReady(quick) ? "← → switch model" : "");
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The yellow warning row: quick models are chosen but the model in use is not one of them, so ← / →
|
|
52
|
+
* would not toggle what the user thinks it toggles. Plain text, cut to `width` with an ellipsis.
|
|
53
|
+
*/
|
|
54
|
+
export function quickWarning(quick, activeId, { width = 80, gutter = " " } = {}) {
|
|
55
|
+
const q = normalizeQuick(quick);
|
|
56
|
+
if (q.length === 0 || isQuickActive(q, activeId)) return "";
|
|
57
|
+
const full = q.length === QUICK_MAX
|
|
58
|
+
? `⚠ not on a quick model — ← → goes to ${q[0].model} · /quick-models`
|
|
59
|
+
: "⚠ not on your quick model · pick 2 with /quick-models to use ← →";
|
|
60
|
+
const short = "⚠ not on a quick model · /quick-models";
|
|
61
|
+
const room = Math.max(8, (width || 80) - 1 - gutter.length);
|
|
62
|
+
const pick = full.length <= room ? full : short;
|
|
63
|
+
return pick.length <= room ? pick : pick.slice(0, Math.max(1, room - 1)) + "…";
|
|
64
|
+
}
|