@tiens.nguyen/gu-cli 1.0.893 → 1.0.895
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-deps-preflight.mjs +5 -2
- package/browser-command.mjs +21 -11
- package/client-expert-run.mjs +32 -6
- package/embed-model-command.mjs +5 -5
- package/gonext_agent_chat.py +269 -42
- package/gu-cli.mjs +14 -4
- package/gu-repl.mjs +80 -27
- package/model-doctor.mjs +35 -22
- package/model-fallback.mjs +4 -4
- package/mongo-windows.mjs +63 -0
- package/package.json +2 -1
- package/preset-apply.mjs +1 -1
- package/python-windows.mjs +91 -0
- package/quick-models.mjs +3 -3
- package/terminal-layout.mjs +114 -2
- package/theme.mjs +9 -3
package/gu-cli.mjs
CHANGED
|
@@ -486,7 +486,7 @@ if (["start", "stop", "status", "logs", "restart"].includes(args[0])) {
|
|
|
486
486
|
if (!cfg.url) {
|
|
487
487
|
lines.push([` ${label}`, cfg.model || "(none)", "(not set)", "NOT CONFIGURED"]);
|
|
488
488
|
notes.push(
|
|
489
|
-
|
|
489
|
+
`EMBEDDING MODEL NOT FOUND — no ${label} is configured, and there is no fallback\n` +
|
|
490
490
|
" (chat/code model servers cannot embed). Set the embedding model folder in the web\n" +
|
|
491
491
|
` app (${tab}), or run \`gu-cli doctor\` to download and start it.`
|
|
492
492
|
);
|
|
@@ -508,7 +508,7 @@ if (["start", "stop", "status", "logs", "restart"].includes(args[0])) {
|
|
|
508
508
|
]);
|
|
509
509
|
if (!r.reachable) {
|
|
510
510
|
notes.push(
|
|
511
|
-
|
|
511
|
+
`The ${label} (${tab}) is NOT reachable from this machine:\n` +
|
|
512
512
|
` ${cfg.url} — ${r.error}.\n` +
|
|
513
513
|
" Check the address, the network/tunnel, and that the server is listening on a\n" +
|
|
514
514
|
" routable interface (127.0.0.1 on another machine is not reachable from here)."
|
|
@@ -532,7 +532,7 @@ if (["start", "stop", "status", "logs", "restart"].includes(args[0])) {
|
|
|
532
532
|
const ours = !clientHere &&
|
|
533
533
|
(cfg.configured === false || cfg.port === md.DEFAULT_CHAT_PORT || cfg.port === md.EMBED_PORT);
|
|
534
534
|
notes.push(
|
|
535
|
-
|
|
535
|
+
`The ${label} (${tab}) is NOT answering on this machine:\n` +
|
|
536
536
|
` ${cfg.model ? `${cfg.model} ` : ""}should be at ${cfg.url}, but nothing responded there.\n` +
|
|
537
537
|
(cfg.source.startsWith("not set")
|
|
538
538
|
? ` Note: no ${label} URL is set, so the agent ${cfg.source.replace("not set — falls back to", "falls back to")}.\n`
|
|
@@ -1823,7 +1823,17 @@ if (args[0] === "deps") {
|
|
|
1823
1823
|
// The agent's Python packages (smolagents + openai), installed in a plain process — what the installer and the
|
|
1824
1824
|
// first start run, and what to run by hand when either said it could not (GH #308).
|
|
1825
1825
|
const md = await import("./model-doctor.mjs");
|
|
1826
|
-
|
|
1826
|
+
// Running `gu-cli deps` agrees to the agent framework; installing Python itself (Windows, GH #363) is asked on its own.
|
|
1827
|
+
const { askLine } = await import("./agent-deps-preflight.mjs");
|
|
1828
|
+
const out = await md.ensureAgentDeps({
|
|
1829
|
+
log: (l) => console.log(l),
|
|
1830
|
+
confirm: async (q, defYes = true) => {
|
|
1831
|
+
const typed = String(await askLine(" " + q + (defYes ? " [Y/n]: " : " [y/N]: "))).trim().toLowerCase();
|
|
1832
|
+
return typed === "" ? defYes : /^y(es)?$/.test(typed);
|
|
1833
|
+
},
|
|
1834
|
+
modelHost: false,
|
|
1835
|
+
agreed: ["agentLibs"],
|
|
1836
|
+
});
|
|
1827
1837
|
const ok = out.agentLibs === "installed" || out.agentLibs === "present";
|
|
1828
1838
|
console.log(ok ? "\n ✓ the agent framework is ready." : "\n ✗ the agent framework is not installed — see the message above and " + md.PIP_LOG_FILE);
|
|
1829
1839
|
process.exit(ok ? 0 : 1);
|
package/gu-repl.mjs
CHANGED
|
@@ -73,6 +73,8 @@ import {
|
|
|
73
73
|
tokenRowWithPrice,
|
|
74
74
|
visibleLen,
|
|
75
75
|
createProbeReplyReader,
|
|
76
|
+
createMouseReportReader,
|
|
77
|
+
stripLeakedMouseReports,
|
|
76
78
|
agentPickerItems,
|
|
77
79
|
agentsIndicator,
|
|
78
80
|
classifyCliArg,
|
|
@@ -115,6 +117,7 @@ import {
|
|
|
115
117
|
syncPrevRowsForResize,
|
|
116
118
|
isToolStepSummary,
|
|
117
119
|
bulletKind,
|
|
120
|
+
stripWarnMark,
|
|
118
121
|
workspaceScopeWarning,
|
|
119
122
|
isLocalApiBase,
|
|
120
123
|
apiVersionSuffix,
|
|
@@ -597,6 +600,9 @@ const BULLET_STYLE = {
|
|
|
597
600
|
// wait for approval — so its text is white. Look-ups and finished commands are activity and go
|
|
598
601
|
// into the dim counted group instead (step-group.mjs); they never reach this style.
|
|
599
602
|
info: { mark: BULLET, markColor: dim, textColor: brightText },
|
|
603
|
+
// GH #378: a warning is YELLOW TEXT and no icon — "⚠" draws as a broken wide emoji on Windows
|
|
604
|
+
// 11. The mark slot stays (a space), so the text column does not move.
|
|
605
|
+
warn: { mark: " ", markColor: (s) => s, textColor: yellow },
|
|
600
606
|
};
|
|
601
607
|
const classifyBullet = (text) => BULLET_STYLE[bulletKind(text)];
|
|
602
608
|
|
|
@@ -709,7 +715,17 @@ if (REPL_LOG) {
|
|
|
709
715
|
const md0 = await import("./model-doctor.mjs");
|
|
710
716
|
await pre.agentDepsPreflight({
|
|
711
717
|
hasLibs: () => md0.hasAgentLibs(),
|
|
712
|
-
|
|
718
|
+
// The "Install it now?" above agreed to the agent framework ONLY (agreed: agentLibs). Anything else this install
|
|
719
|
+
// would do — installing Python itself on Windows (GH #363) — is a new decision and gets its own question.
|
|
720
|
+
install: () => md0.ensureAgentDeps({
|
|
721
|
+
log: (l) => console.log(l),
|
|
722
|
+
confirm: async (q, defYes = true) => {
|
|
723
|
+
const typed = String(await pre.askLine(" " + q + (defYes ? " [Y/n]: " : " [y/N]: "))).trim().toLowerCase();
|
|
724
|
+
return typed === "" ? defYes : /^y(es)?$/.test(typed);
|
|
725
|
+
},
|
|
726
|
+
modelHost: false,
|
|
727
|
+
agreed: ["agentLibs"],
|
|
728
|
+
}),
|
|
713
729
|
markDeclined: pre.markDeclinedOnDisk,
|
|
714
730
|
});
|
|
715
731
|
}
|
|
@@ -1304,7 +1320,30 @@ const PASTE_MODE_OFF = "\x1b[?2004l";
|
|
|
1304
1320
|
// short paste goes in literally, a long one becomes "[Pasted text #N +M lines]" with the real
|
|
1305
1321
|
// text kept in pasteStore and expanded when the turn is sent.
|
|
1306
1322
|
let pasteBuffer = null;
|
|
1307
|
-
|
|
1323
|
+
// Mouse reports in every shape node can deliver them (GH #365) — see createMouseReportReader.
|
|
1324
|
+
const mouseReader = createMouseReportReader();
|
|
1325
|
+
let _mouseTimer = null;
|
|
1326
|
+
let _mouseReplaying = false; // replayed keys skip the mouse reader, and only the mouse reader
|
|
1327
|
+
function mouseClearTimer() {
|
|
1328
|
+
if (_mouseTimer) {
|
|
1329
|
+
clearTimeout(_mouseTimer);
|
|
1330
|
+
_mouseTimer = null;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
/** Not a report after all: give the keys back to the rest of the input path, in order. */
|
|
1334
|
+
function mouseReplay(keys) {
|
|
1335
|
+
_mouseReplaying = true;
|
|
1336
|
+
try {
|
|
1337
|
+
for (const [s, k] of keys) rl._ttyWrite(s, k);
|
|
1338
|
+
} finally {
|
|
1339
|
+
_mouseReplaying = false;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
/** Timer fired: what is still held (a lone Esc, a "[" with nothing after it) is a keystroke. */
|
|
1343
|
+
function mouseFlush() {
|
|
1344
|
+
_mouseTimer = null;
|
|
1345
|
+
mouseReplay(mouseReader.flush());
|
|
1346
|
+
}
|
|
1308
1347
|
const _origTtyWrite = rl._ttyWrite.bind(rl);
|
|
1309
1348
|
rl._ttyWrite = (str, key) => {
|
|
1310
1349
|
// A LIST PICKER OWNS EVERY KEY WHILE IT IS UP, and this has to happen HERE rather than in the
|
|
@@ -1384,20 +1423,26 @@ rl._ttyWrite = (str, key) => {
|
|
|
1384
1423
|
// readline parses an SGR mouse report only as far as its "\x1b[<" prefix, then hands the
|
|
1385
1424
|
// REST through as individual characters — which is how "0;62;21M" ended up typed into the
|
|
1386
1425
|
// line. Buffer from the prefix to the M/m terminator so nothing leaks, then move the caret.
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
if (
|
|
1392
|
-
|
|
1393
|
-
if (
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1426
|
+
//
|
|
1427
|
+
// GH #365: by EVERY shape, not only node's "[<" key — an Esc that timed out (a stalled event
|
|
1428
|
+
// loop, e.g. the repaint after restoring a window on Windows) sends "[<35;114;22M" through as
|
|
1429
|
+
// plain characters, which is how hovering typed junk into the line. See createMouseReportReader.
|
|
1430
|
+
if (!_mouseReplaying) {
|
|
1431
|
+
const verdict = mouseReader.feed(str, key, { plainLeads: _clickEnabled || _mouseEnabled });
|
|
1432
|
+
if (verdict.action === "hold") {
|
|
1433
|
+
if (!_mouseTimer) _mouseTimer = setTimeout(mouseFlush, 300);
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
if (verdict.action === "consume") {
|
|
1437
|
+
mouseClearTimer();
|
|
1438
|
+
applyMouseReport(verdict.report);
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
if (verdict.action === "replay") {
|
|
1442
|
+
if (!verdict.hold) mouseClearTimer();
|
|
1443
|
+
mouseReplay(verdict.keys);
|
|
1444
|
+
return;
|
|
1399
1445
|
}
|
|
1400
|
-
return;
|
|
1401
1446
|
}
|
|
1402
1447
|
// The terminal's answer to our \x1b[6n probe (ESC [ row ; col R) — the caret's ABSOLUTE row,
|
|
1403
1448
|
// which is what mouse reports are measured against. It is INPUT: any byte of it we fail to
|
|
@@ -2119,6 +2164,8 @@ let slashSel = -1; // highlighted row in the current filtered list (-1 = none)
|
|
|
2119
2164
|
let slashNames = []; // command names in the current filtered list (for Enter → run)
|
|
2120
2165
|
let slashPrefix = ""; // the "/…" text the user typed (restored when ↑/↓ recall history)
|
|
2121
2166
|
rl.on("line", (l) => {
|
|
2167
|
+
// GH #365: a mouse report that got past every reader is junk, not part of the question.
|
|
2168
|
+
l = stripLeakedMouseReports(l);
|
|
2122
2169
|
// Enter ends any drag: if a release was lost (focus left the window mid-drag) the highlight
|
|
2123
2170
|
// would otherwise stay painted over text nobody is selecting — see endDrag.
|
|
2124
2171
|
endDrag();
|
|
@@ -2987,7 +3034,7 @@ const footerBuilt = () =>
|
|
|
2987
3034
|
modeHint: pasteHint({ images: coderTakesImages() }),
|
|
2988
3035
|
// Its own row (#54), sized to the window so the fix it names is never the part cut off.
|
|
2989
3036
|
notice: imageNotice
|
|
2990
|
-
? orange(
|
|
3037
|
+
? orange(fitImageNotice(imageNotice, (process.stdout.columns || 80) - visibleLen(GUTTER) - 1))
|
|
2991
3038
|
: stopNotice
|
|
2992
3039
|
? dim(clipVisible(stopNotice, (process.stdout.columns || 80) - visibleLen(GUTTER) - 1))
|
|
2993
3040
|
: "",
|
|
@@ -6992,7 +7039,7 @@ async function useCoderInThisFolder({ ps, rows, beforeChoices, url, host, locali
|
|
|
6992
7039
|
])
|
|
6993
7040
|
);
|
|
6994
7041
|
if (lost.length) {
|
|
6995
|
-
console.log(yellow(`
|
|
7042
|
+
console.log(yellow(` this API dropped ${lost.length === 1 ? "a provider" : `${lost.length} providers`} when saving:`));
|
|
6996
7043
|
for (const u of lost) console.log(GUTTER + dim(` ${u}`));
|
|
6997
7044
|
if (ps.apiKeysBackendsByKind(after?.codingChoices)) {
|
|
6998
7045
|
console.log(GUTTER + dim("this API identifies a backend by its KIND, so it holds ONE"));
|
|
@@ -10151,7 +10198,7 @@ async function showBrowser() {
|
|
|
10151
10198
|
const st = await bridgeStatus().catch(() => ({ running: false, connected: false }));
|
|
10152
10199
|
const lines = browserReport(st, { allowed: allowedIn(resolve(process.cwd())) });
|
|
10153
10200
|
console.log("");
|
|
10154
|
-
for (const [i, l] of lines.entries()) console.log(GUTTER + (i === 0 && st.connected ? accent(l) :
|
|
10201
|
+
for (const [i, l] of lines.entries()) console.log(GUTTER + (i === 0 && st.connected ? accent(l) : lines.warnings?.has(l) ? yellow(l) : dim(l)));
|
|
10155
10202
|
console.log("");
|
|
10156
10203
|
paintBar();
|
|
10157
10204
|
}
|
|
@@ -11171,7 +11218,7 @@ function drawApprovalOptions(redraw) {
|
|
|
11171
11218
|
/** #204: the command block of the open Yes/No prompt — Ctrl+O toggles it (a click does too). */
|
|
11172
11219
|
let approvalCommandBlock = null;
|
|
11173
11220
|
/**
|
|
11174
|
-
* Print "
|
|
11221
|
+
* Print "Allow running this command?" (yellow, no icon — GH #378) and the command (#204): every line indented, long lines
|
|
11175
11222
|
* wrapped under that indent instead of at column 0, and a tall command collapsed to its first
|
|
11176
11223
|
* rows with "click to expand". Registered as a clickable block only when it has two forms and
|
|
11177
11224
|
* `toggle` is set (a list picker below cannot survive the repaint); otherwise shown whole.
|
|
@@ -11180,7 +11227,7 @@ function printApprovalCommand(shown, { toggle = true } = {}) {
|
|
|
11180
11227
|
const canToggle = toggle && barPinned();
|
|
11181
11228
|
const block = { kind: "command", from: 0, to: 0, text: shown, collapsed: canToggle };
|
|
11182
11229
|
const painted = paintThoughtRows(block, false);
|
|
11183
|
-
process.stdout.write("\n" + markLine(
|
|
11230
|
+
process.stdout.write("\n" + markLine(" ") + yellow("Allow running this command?") + "\n" + painted.join("\n") + "\n");
|
|
11184
11231
|
if (!canToggle || !blockCanToggle(block)) return null;
|
|
11185
11232
|
block.to = transcript.dropped() + transcript.length() - 2;
|
|
11186
11233
|
block.from = block.to - painted.length + 1;
|
|
@@ -12222,7 +12269,7 @@ function announceScaffold() {
|
|
|
12222
12269
|
// ONE yellow line (#222): the steps are the token question's own, behind "?" there.
|
|
12223
12270
|
console.log(GUTTER + yellow("! " + text));
|
|
12224
12271
|
} else {
|
|
12225
|
-
console.log(GUTTER + (kind === "ok" ? accent("✓ " + text) : kind === "warn" ? yellow(
|
|
12272
|
+
console.log(GUTTER + (kind === "ok" ? accent("✓ " + text) : kind === "warn" ? yellow(text) : red("✗ " + text)));
|
|
12226
12273
|
}
|
|
12227
12274
|
if (barPinned()) paintBar();
|
|
12228
12275
|
}
|
|
@@ -12485,7 +12532,7 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
|
|
|
12485
12532
|
// warn nobody, which is the bug agent-payload-shape.test.mjs exists to catch.
|
|
12486
12533
|
if (!skipAsk && codingKeyMissingKnown === true) {
|
|
12487
12534
|
console.log(
|
|
12488
|
-
GUTTER + orange("
|
|
12535
|
+
GUTTER + orange("no API key for the coding model") +
|
|
12489
12536
|
dim(" — coding is running on your chat model. ctrl+k to set it.\n")
|
|
12490
12537
|
);
|
|
12491
12538
|
}
|
|
@@ -13428,15 +13475,18 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
|
|
|
13428
13475
|
// hangs in the gutter (markLine); a long bullet wraps with every continuation row
|
|
13429
13476
|
// re-indented to the SAME gutter, so it shares one text column with the prompt/answer.
|
|
13430
13477
|
// Paths under this folder are shown relative to it (#326): the folder is on the bar.
|
|
13431
|
-
const
|
|
13478
|
+
const rawStep = relativizePaths(line.trim(), turnCwd);
|
|
13479
|
+
// GH #378: the agent's "⚠" marks a warning; it is shown as colour, never as the icon.
|
|
13480
|
+
const stepWarn = bulletKind(rawStep) === "warn";
|
|
13481
|
+
const stepText = stepWarn ? stripWarnMark(rawStep) : rawStep;
|
|
13432
13482
|
// A LOOK-UP joins the turn's open group — one counted row, a click away from the lines
|
|
13433
13483
|
// it stands for — instead of printing a line of its own.
|
|
13434
|
-
if (isGroupableStep(stepText, bulletKind(stepText))) {
|
|
13484
|
+
if (!stepWarn && isGroupableStep(stepText, bulletKind(stepText))) {
|
|
13435
13485
|
addLookupStep(stepText);
|
|
13436
13486
|
lastWasBlank = false;
|
|
13437
13487
|
continue;
|
|
13438
13488
|
}
|
|
13439
|
-
const st = classifyBullet(stepText);
|
|
13489
|
+
const st = stepWarn ? BULLET_STYLE.warn : classifyBullet(stepText);
|
|
13440
13490
|
// A finished/failed command fits ONE row, trimmed at a word boundary with "…" (task #20).
|
|
13441
13491
|
const mCmd = /^(Command (?:finished|failed) → )(.+)$/.exec(stepText);
|
|
13442
13492
|
const shown = mCmd
|
|
@@ -13830,7 +13880,7 @@ async function runAgentTurn(history, { attachJobId = "", ownJob = false } = {})
|
|
|
13830
13880
|
localDaemonRunning
|
|
13831
13881
|
? GUTTER +
|
|
13832
13882
|
dim("waiting for a free slot — the worker is busy with another folder's turn\n")
|
|
13833
|
-
: red("\
|
|
13883
|
+
: red("\nno worker has claimed the job — is `gu-cli` running?\n")
|
|
13834
13884
|
);
|
|
13835
13885
|
}
|
|
13836
13886
|
await sleep(700);
|
|
@@ -14425,6 +14475,9 @@ async function main() {
|
|
|
14425
14475
|
// The yes two lines up IS the answer — do not ask the same thing again (#333).
|
|
14426
14476
|
agreed: ["agentLibs"],
|
|
14427
14477
|
});
|
|
14478
|
+
if (depsOut?.python3 === "missing") {
|
|
14479
|
+
console.log(yellow(" Without Python the agent cannot answer. Setup goes on, but install Python 3 and run `gu doctor` before your first question.\n"));
|
|
14480
|
+
}
|
|
14428
14481
|
// The worker reads worker.env ONCE at startup, so a daemon that is already
|
|
14429
14482
|
// running still has the OLD python — the packages would look "not installed"
|
|
14430
14483
|
// for the rest of its life. Restart it here rather than leaving the user to
|
|
@@ -14969,7 +15022,7 @@ async function main() {
|
|
|
14969
15022
|
if (!entries.length) continue;
|
|
14970
15023
|
shellRecap = true;
|
|
14971
15024
|
alignOutputToLineStart();
|
|
14972
|
-
console.log("\n" + GUTTER + dim(
|
|
15025
|
+
console.log("\n" + GUTTER + dim(`background ${entries.length === 1 ? "shell" : "shells"} finished — ${entries.map((e) => `${e.rec.id} ${shellStatusText(e.st)}`).join(", ")} · gu asks the agent to recap`));
|
|
14973
15026
|
lineRaw = recapQuestion(entries);
|
|
14974
15027
|
}
|
|
14975
15028
|
// /resume (#285): show where the last unfinished session stopped; on "yes" the line becomes the
|
package/model-doctor.mjs
CHANGED
|
@@ -237,7 +237,7 @@ async function ensureVenv(log) {
|
|
|
237
237
|
});
|
|
238
238
|
if (made && existsSync(py)) return py;
|
|
239
239
|
// Debian/Ubuntu split venv into its own package, so this is a real and common outcome.
|
|
240
|
-
log("
|
|
240
|
+
log(yellow(" could not create a virtualenv (on Debian/Ubuntu: sudo apt install python3-venv)"));
|
|
241
241
|
return "";
|
|
242
242
|
}
|
|
243
243
|
|
|
@@ -282,7 +282,7 @@ async function pipInstall(pkgs, log) {
|
|
|
282
282
|
// (GU_PIP_SYSTEM=1 keeps the old behaviour.)
|
|
283
283
|
if (process.platform === "win32" && process.env.GU_PIP_SYSTEM !== "1") {
|
|
284
284
|
if (await intoVenv()) return;
|
|
285
|
-
log("
|
|
285
|
+
log(yellow(" could not use a private environment — trying the system Python instead"));
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
log(` installing ${pkgs.join(" ")} … (pip, this can take a minute)`);
|
|
@@ -327,7 +327,7 @@ export function chatRamWarningLines(repo, totalBytes) {
|
|
|
327
327
|
const ramGb = Math.round((Number(totalBytes) || 0) / 1024 ** 3);
|
|
328
328
|
const name = String(repo).split("/").pop();
|
|
329
329
|
const out = [
|
|
330
|
-
yellow(`
|
|
330
|
+
yellow(` ${name} needs a Mac with at least ${MIN_CHAT_RAM_GB} GB of RAM to run well.`),
|
|
331
331
|
];
|
|
332
332
|
if (ramGb > 0 && ramGb < MIN_CHAT_RAM_GB) {
|
|
333
333
|
out.push(
|
|
@@ -1121,7 +1121,7 @@ export async function runDoctor({ apiBase, workerKey, log, confirm, prompt, mode
|
|
|
1121
1121
|
// Only a REAL configured name is an expectation. "default_model" is mlx_lm's "whatever I
|
|
1122
1122
|
// loaded" sentinel, so it agrees with anything by construction.
|
|
1123
1123
|
if (namesAModel(modelId) && !sameModelName(modelId, served.name)) {
|
|
1124
|
-
log(`
|
|
1124
|
+
log(yellow(` your account asks for "${modelId}", but that server is running`));
|
|
1125
1125
|
log(` "${served.name}". Stop it and re-run doctor, or change the model in the web app.`);
|
|
1126
1126
|
}
|
|
1127
1127
|
} else {
|
|
@@ -1132,7 +1132,7 @@ export async function runDoctor({ apiBase, workerKey, log, confirm, prompt, mode
|
|
|
1132
1132
|
chat = "already-running";
|
|
1133
1133
|
} else if (configuredChat && !isLocalModelUrl(chatUrl)) {
|
|
1134
1134
|
// Configured but REMOTE and not answering — we can't start a model on another machine.
|
|
1135
|
-
log(`
|
|
1135
|
+
log(yellow(` your chat model server (${modelRoot(chatUrl)}) isn't answering.`));
|
|
1136
1136
|
log(" It's remote, so start it there — this machine can't launch it.");
|
|
1137
1137
|
chat = "remote";
|
|
1138
1138
|
} else {
|
|
@@ -1142,7 +1142,7 @@ export async function runDoctor({ apiBase, workerKey, log, confirm, prompt, mode
|
|
|
1142
1142
|
if (!dirReady && !chatRepo) {
|
|
1143
1143
|
// A specifically-named, non-default model we don't know how to fetch — DON'T
|
|
1144
1144
|
// substitute the default. Tell the user how to bring their own (review H2).
|
|
1145
|
-
log(`
|
|
1145
|
+
log(yellow(` your account uses the chat model "${modelId}", which isn't the built-in`));
|
|
1146
1146
|
log(` default, so I can't auto-download it. Put it at ${dir} (or any dir) and run:`);
|
|
1147
1147
|
log(` mlx_lm.server --model <that dir> --port ${chatPort}`);
|
|
1148
1148
|
chat = "unknown-model";
|
|
@@ -1163,7 +1163,7 @@ export async function runDoctor({ apiBase, workerKey, log, confirm, prompt, mode
|
|
|
1163
1163
|
await downloadModel({ repo: chatRepo, dir, log });
|
|
1164
1164
|
dirReady = true;
|
|
1165
1165
|
} catch (e) {
|
|
1166
|
-
log(`
|
|
1166
|
+
log(yellow(` ${e instanceof Error ? e.message : e}`));
|
|
1167
1167
|
chat = "download-failed";
|
|
1168
1168
|
}
|
|
1169
1169
|
}
|
|
@@ -1200,7 +1200,7 @@ export async function runDoctor({ apiBase, workerKey, log, confirm, prompt, mode
|
|
|
1200
1200
|
log(` the first answer may take a minute). Watch it: gu-cli logs`);
|
|
1201
1201
|
chat = "loading";
|
|
1202
1202
|
} else {
|
|
1203
|
-
log(`
|
|
1203
|
+
log(yellow(` ${r.reason}`));
|
|
1204
1204
|
chat = "start-failed";
|
|
1205
1205
|
}
|
|
1206
1206
|
}
|
|
@@ -1224,12 +1224,12 @@ export async function runDoctor({ apiBase, workerKey, log, confirm, prompt, mode
|
|
|
1224
1224
|
if (codeOk) {
|
|
1225
1225
|
log(` ✓ code model server is up at ${modelRoot(codeUrl)}.`);
|
|
1226
1226
|
} else if (codeIsLocal) {
|
|
1227
|
-
log(`
|
|
1227
|
+
log(yellow(` local code model server (${modelRoot(codeUrl)}) isn't answering.`));
|
|
1228
1228
|
if (codeModel) log(` Start Ollama and pull it: ollama serve & ollama pull ${codeModel}`);
|
|
1229
1229
|
} else {
|
|
1230
1230
|
// Unreachable AND remote: this machine can't start it, and `ollama pull` is only
|
|
1231
1231
|
// meaningful if that server is actually Ollama — don't assume it is.
|
|
1232
|
-
log(`
|
|
1232
|
+
log(yellow(` your code model (${modelRoot(codeUrl)}) is not reachable from this machine.`));
|
|
1233
1233
|
log(" It's remote, so start it there / check the network — this machine can't launch it.");
|
|
1234
1234
|
}
|
|
1235
1235
|
}
|
|
@@ -1354,13 +1354,26 @@ export async function ensureClaudeSdk({ log, confirm }) {
|
|
|
1354
1354
|
* (smolagents + openai) — required to run the agent?" — the same question twice in a row on
|
|
1355
1355
|
* every first install. A yes that was just given is not asked for again.
|
|
1356
1356
|
*/
|
|
1357
|
-
export async function ensureAgentDeps({ log, confirm, modelHost = canHostMlxModels(), agreed = [] }) {
|
|
1357
|
+
export async function ensureAgentDeps({ log, confirm, modelHost = canHostMlxModels(), agreed = [], os = platform(), installPython = null }) {
|
|
1358
1358
|
const out = {};
|
|
1359
1359
|
log("");
|
|
1360
1360
|
log(" Checking the agent's dependencies…");
|
|
1361
|
-
// python3 — the whole agent runs on it
|
|
1361
|
+
// python3 — the whole agent runs on it. On Windows gu offers to install it for this user (GH #363); elsewhere it can't.
|
|
1362
|
+
if (!(await pyHasModule("sys")) && os === "win32") {
|
|
1363
|
+
const yes = await confirm(
|
|
1364
|
+
"Python 3 is not installed, and the agent runs on it. Install Python 3.12 for your user only? (about 25 MB from python.org; no administrator needed)",
|
|
1365
|
+
true
|
|
1366
|
+
);
|
|
1367
|
+
if (yes) {
|
|
1368
|
+
log(" Installing Python 3.12 …");
|
|
1369
|
+
const install = installPython ?? (await import("./python-windows.mjs")).installPythonWindows;
|
|
1370
|
+
const r = await install({});
|
|
1371
|
+
if (r.ok) log(" ✓ Python 3.12 installed");
|
|
1372
|
+
else log(yellow(` could not install Python: ${r.error ?? "unknown error"}`));
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1362
1375
|
if (!(await pyHasModule("sys"))) {
|
|
1363
|
-
log(`
|
|
1376
|
+
log(yellow(` ${python3Bin()} not found. Install Python 3 — ${pythonInstallHint()} —`));
|
|
1364
1377
|
log(" then run `gu-cli doctor` again.");
|
|
1365
1378
|
out.python3 = "missing";
|
|
1366
1379
|
return out; // nothing else can be installed without python
|
|
@@ -1381,7 +1394,7 @@ export async function ensureAgentDeps({ log, confirm, modelHost = canHostMlxMode
|
|
|
1381
1394
|
await pipInstall(pkgs, log);
|
|
1382
1395
|
out[key] = "installed";
|
|
1383
1396
|
} catch (e) {
|
|
1384
|
-
log(`
|
|
1397
|
+
log(yellow(` ${e instanceof Error ? e.message : e}`));
|
|
1385
1398
|
out[key] = "failed";
|
|
1386
1399
|
}
|
|
1387
1400
|
};
|
|
@@ -1412,7 +1425,7 @@ export async function ensureAgentDeps({ log, confirm, modelHost = canHostMlxMode
|
|
|
1412
1425
|
"Install the agent framework (smolagents + openai) — required to run the agent?"
|
|
1413
1426
|
);
|
|
1414
1427
|
if (out.agentLibs === "skipped" || out.agentLibs === "failed") {
|
|
1415
|
-
log(yellow("
|
|
1428
|
+
log(yellow(" Without smolagents + openai the agent can't run — install them before asking a question."));
|
|
1416
1429
|
}
|
|
1417
1430
|
if (out.agentLibs === "installed") {
|
|
1418
1431
|
// Make the daemon use the interpreter these packages actually landed in. ONLY after a
|
|
@@ -1491,7 +1504,7 @@ export async function ensureRag({
|
|
|
1491
1504
|
await pipInstall(["boto3"], log);
|
|
1492
1505
|
out.boto3 = "installed";
|
|
1493
1506
|
} catch (e) {
|
|
1494
|
-
log(`
|
|
1507
|
+
log(yellow(` ${e instanceof Error ? e.message : e}`));
|
|
1495
1508
|
out.boto3 = "failed";
|
|
1496
1509
|
}
|
|
1497
1510
|
} else out.boto3 = "skipped";
|
|
@@ -1525,7 +1538,7 @@ export async function ensureRag({
|
|
|
1525
1538
|
// A LOCAL embedder that is down is the opposite case and must still fall through: setting
|
|
1526
1539
|
// one up is precisely what the doctor is for.
|
|
1527
1540
|
if (configuredEmbedUrl && !isLocalModelUrl(configuredEmbedUrl)) {
|
|
1528
|
-
log(`
|
|
1541
|
+
log(yellow(` your embeddings server (${modelRoot(configuredEmbedUrl)}) is remote and isn't`));
|
|
1529
1542
|
log(" answering. Start it there — this machine can't launch it.");
|
|
1530
1543
|
log(" Leaving the setting alone — nothing here can serve a remote embedder.");
|
|
1531
1544
|
out.embed = "remote";
|
|
@@ -1557,7 +1570,7 @@ export async function ensureRag({
|
|
|
1557
1570
|
log(
|
|
1558
1571
|
savedToAccount
|
|
1559
1572
|
? ` ✓ saved to your account — visible in the web app under Settings → Agent → RAG.`
|
|
1560
|
-
: `
|
|
1573
|
+
: yellow(` couldn't save that path to your account (using it for this machine anyway).`)
|
|
1561
1574
|
);
|
|
1562
1575
|
}
|
|
1563
1576
|
const embedDir = configuredDir || EMBED_DIR;
|
|
@@ -1576,7 +1589,7 @@ export async function ensureRag({
|
|
|
1576
1589
|
// local, so probing embedUrl first let a locally-running server mask a dead remote one
|
|
1577
1590
|
// and report "already running" for a machine the user can't even reach.
|
|
1578
1591
|
if (configuredEmbed && !isLocalModelUrl(configuredEmbed)) {
|
|
1579
|
-
log(`
|
|
1592
|
+
log(yellow(` your embeddings server (${modelRoot(configuredEmbed)}) is remote and isn't`));
|
|
1580
1593
|
log(" answering. Start it there — this machine can't launch it.");
|
|
1581
1594
|
out.embed = "remote";
|
|
1582
1595
|
return out;
|
|
@@ -1606,7 +1619,7 @@ export async function ensureRag({
|
|
|
1606
1619
|
// don't know what it is — so say what's wrong instead of silently serving a different model.
|
|
1607
1620
|
if (!(await modelDirReady(embedDir))) {
|
|
1608
1621
|
if (embedIsCustom) {
|
|
1609
|
-
log(`
|
|
1622
|
+
log(yellow(` the embedding model folder you set in the web app doesn't exist here:`));
|
|
1610
1623
|
log(` ${embedDir}`);
|
|
1611
1624
|
log(" Fix the path in Settings → Agent → RAG, or download the model into it:");
|
|
1612
1625
|
log(` hf download <repo> --local-dir ${embedDir}`);
|
|
@@ -1623,7 +1636,7 @@ export async function ensureRag({
|
|
|
1623
1636
|
try {
|
|
1624
1637
|
await downloadModel({ repo: EMBED_REPO, dir: embedDir, log });
|
|
1625
1638
|
} catch (e) {
|
|
1626
|
-
log(`
|
|
1639
|
+
log(yellow(` ${e instanceof Error ? e.message : e}`));
|
|
1627
1640
|
out.embed = "download-failed";
|
|
1628
1641
|
return out;
|
|
1629
1642
|
}
|
|
@@ -1644,7 +1657,7 @@ export async function ensureRag({
|
|
|
1644
1657
|
log(` … ${modelDisplayName(embedDir)} is loading in the background on port ${embedPort}.`);
|
|
1645
1658
|
out.embed = "loading";
|
|
1646
1659
|
} else {
|
|
1647
|
-
log(`
|
|
1660
|
+
log(yellow(` ${r.reason}`));
|
|
1648
1661
|
out.embed = "start-failed";
|
|
1649
1662
|
}
|
|
1650
1663
|
return out;
|
package/model-fallback.mjs
CHANGED
|
@@ -67,7 +67,7 @@ function parts(fb, { fromMax = Infinity, toMax = Infinity } = {}) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
const join = ({ subject, verb, outcome }, reason) =>
|
|
70
|
-
|
|
70
|
+
`${subject} ${verb}${reason ? ` (${reason})` : ""} — ${outcome}`;
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
73
|
* The bar's row for a fallback, FITTED to `width` columns (after the gutter): every bar row must
|
|
@@ -109,11 +109,11 @@ export function fallbackHistoryLine(fb) {
|
|
|
109
109
|
export function keyMissingNotice({ coder = "", width = 80, gutter = " " } = {}) {
|
|
110
110
|
const room = Math.max(20, (width || 80) - 1 - gutter.length);
|
|
111
111
|
const tail = " — coding with the chat model · ctrl+k";
|
|
112
|
-
const named =
|
|
112
|
+
const named = `no API key for coder ${text(coder)}${tail}`;
|
|
113
113
|
if (text(coder) && named.length <= room) return named;
|
|
114
|
-
const plain =
|
|
114
|
+
const plain = `no API key for the coding model${tail}`;
|
|
115
115
|
if (plain.length <= room) return plain;
|
|
116
|
-
return clipName("
|
|
116
|
+
return clipName("no coder API key — on the chat model · ctrl+k", room);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
/**
|
package/mongo-windows.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import { mkdir, rename, writeFile, rm, stat } from "node:fs/promises";
|
|
|
17
17
|
import { homedir } from "node:os";
|
|
18
18
|
import { join, dirname } from "node:path";
|
|
19
19
|
import { inflateRawSync } from "node:zlib";
|
|
20
|
+
import { execFile } from "node:child_process";
|
|
20
21
|
|
|
21
22
|
// Bump on purpose, like MONGO_WINGET_VERSION: 9.0.4 is what fastdl serves today (winget has 9.0.2).
|
|
22
23
|
export const MONGO_WIN_VERSION = "9.0.4";
|
|
@@ -244,3 +245,65 @@ export async function startPortableMongod({ port = 27017, paths = mongoWinPaths(
|
|
|
244
245
|
taskName: "gu-local-mongod",
|
|
245
246
|
});
|
|
246
247
|
}
|
|
248
|
+
|
|
249
|
+
// ------------------------------------------------------------------- the Visual C++ runtime (GH #363)
|
|
250
|
+
//
|
|
251
|
+
// mongod.exe needs Microsoft's Visual C++ runtime (vcruntime140.dll, msvcp140.dll). A brand-new Windows 11
|
|
252
|
+
// has none, so mongod stops at once with 0xC0000135 (STATUS_DLL_NOT_FOUND) and — started detached — nobody
|
|
253
|
+
// sees why: gu only said "not answering yet" and the user was sent round in a circle.
|
|
254
|
+
|
|
255
|
+
export const VC_REDIST_URL = "https://aka.ms/vs/17/release/vc_redist.x64.exe";
|
|
256
|
+
|
|
257
|
+
/** The exit code Windows gives a program whose DLL is missing: 0xC0000135, as unsigned or signed. */
|
|
258
|
+
export function isDllNotFoundExit(code) {
|
|
259
|
+
return code === 0xC0000135 || code === -1073741515;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Can this mongod.exe start at all? → { ok, code }. Runs `mongod --version` (it prints and exits). */
|
|
263
|
+
export function mongodRuns(paths = mongoWinPaths(), { execFn = execFile, timeoutMs = 20000 } = {}) {
|
|
264
|
+
return new Promise((resolve) => {
|
|
265
|
+
try {
|
|
266
|
+
execFn(paths.exe, ["--version"], { timeout: timeoutMs, windowsHide: true }, (err) => {
|
|
267
|
+
if (!err) return resolve({ ok: true, code: 0 });
|
|
268
|
+
const code = typeof err.code === "number" ? err.code : null;
|
|
269
|
+
// ENOENT / killed by the timeout carry a string or signal, not an exit code: not "a DLL is missing".
|
|
270
|
+
resolve({ ok: false, code });
|
|
271
|
+
});
|
|
272
|
+
} catch {
|
|
273
|
+
resolve({ ok: false, code: null });
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Exit codes of vc_redist.x64.exe that mean "it is there now": done, reboot wanted, a newer one already installed. */
|
|
279
|
+
export function vcRedistInstalled(code) {
|
|
280
|
+
return code === 0 || code === 3010 || code === 1638;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Download Microsoft's runtime installer and run it quietly. Installing it needs administrator rights, so it is
|
|
285
|
+
* started with `-Verb RunAs` (one Windows permission prompt); a non-elevated child_process could not ask.
|
|
286
|
+
* → { ok, code, error? }. `download` and `run` are injectable so tests never touch the net or the machine.
|
|
287
|
+
*/
|
|
288
|
+
export async function installVcRuntime({ dir = join(homedir(), ".gonext"), fetchFn = fetch, run = null } = {}) {
|
|
289
|
+
const exe = join(dir, "vc_redist.x64.exe");
|
|
290
|
+
try {
|
|
291
|
+
await mkdir(dir, { recursive: true });
|
|
292
|
+
const res = await fetchFn(VC_REDIST_URL, { redirect: "follow" });
|
|
293
|
+
if (!res.ok) return { ok: false, code: null, error: `download failed (HTTP ${res.status})` };
|
|
294
|
+
await writeFile(exe, Buffer.from(await res.arrayBuffer()));
|
|
295
|
+
} catch (e) {
|
|
296
|
+
return { ok: false, code: null, error: `download failed: ${e?.message ?? e}` };
|
|
297
|
+
}
|
|
298
|
+
const runner = run ?? ((file) => new Promise((resolve) => {
|
|
299
|
+
const script = `$p = Start-Process -FilePath '${file.replace(/'/g, "''")}' -ArgumentList '/install','/quiet','/norestart' -Verb RunAs -Wait -PassThru; exit $p.ExitCode`;
|
|
300
|
+
execFile("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { windowsHide: true, timeout: 600000 }, (err, _o, stderr) => {
|
|
301
|
+
resolve({ code: err ? (typeof err.code === "number" ? err.code : null) : 0, stderr: String(stderr ?? "") });
|
|
302
|
+
});
|
|
303
|
+
}));
|
|
304
|
+
const r = await runner(exe);
|
|
305
|
+
await rm(exe, { force: true }).catch(() => {});
|
|
306
|
+
if (vcRedistInstalled(r.code)) return { ok: true, code: r.code };
|
|
307
|
+
const declined = /canceled by the user|operation was canceled/i.test(r.stderr ?? "");
|
|
308
|
+
return { ok: false, code: r.code, error: declined ? "the administrator permission prompt was declined" : `the installer stopped with code ${r.code}` };
|
|
309
|
+
}
|
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.895",
|
|
4
4
|
"description": "gu: the coding agent in your terminal. It reads your repo, makes the change and runs the tests, with your own models (Ollama or any OpenAI-compatible provider). Free installs run entirely on your machine. macOS, Linux and Windows.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -75,6 +75,7 @@
|
|
|
75
75
|
"workspace-revert.mjs",
|
|
76
76
|
"mongo-native.mjs",
|
|
77
77
|
"mongo-windows.mjs",
|
|
78
|
+
"python-windows.mjs",
|
|
78
79
|
"terminal-hover.mjs",
|
|
79
80
|
"terminal-layout.mjs",
|
|
80
81
|
"terminal-viewport.mjs",
|
package/preset-apply.mjs
CHANGED
|
@@ -86,7 +86,7 @@ export async function applyTransfer(value, { api, io, fromLine = "", carriesSecr
|
|
|
86
86
|
: plan.lines;
|
|
87
87
|
for (const l of lines) say(l);
|
|
88
88
|
if (summary && plan.replaces) say(`replaces the ${plan.replaces} coding provider${plan.replaces === 1 ? "" : "s"} on this account.`);
|
|
89
|
-
for (const w of new Set(warnings)) say(
|
|
89
|
+
for (const w of new Set(warnings)) say(w, "warn");
|
|
90
90
|
if (carriesSecrets) say("this file contains API keys in plain text — delete it when you are done.", "warn");
|
|
91
91
|
const go = String((await io.ask("apply this? [y/N] ")) ?? "").trim().toLowerCase();
|
|
92
92
|
if (go !== "y" && go !== "yes") {
|