@tiens.nguyen/gu-cli 1.0.866 → 1.0.868

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/gu-repl.mjs CHANGED
@@ -2935,7 +2935,7 @@ const footerBuilt = () =>
2935
2935
  identity: identityLineText(),
2936
2936
  token: tokenLineText(),
2937
2937
  mode: modeLineText(),
2938
- // Task #34: "rag: embedder down → grep fallback" as a soft yellow note beside the mode, not an
2938
+ // Task #34: "rag: using grep" as a soft yellow note beside the mode, not an
2939
2939
  // alarming red segment on the token row. yellow goes through paint(), so NO_COLOR turns it off.
2940
2940
  // Issue #120: an auto-compaction this turn outranks the rag note (same soft yellow slot).
2941
2941
  modeNote: compactLockNote() || compactBarNote(lastPromptBreakdown) || ragBarNote(lastPromptBreakdown),
package/model-doctor.mjs CHANGED
@@ -12,6 +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
16
 
16
17
  // Minimum RAM to run the default 14B-4bit chat model comfortably on Apple Silicon.
17
18
  const MIN_CHAT_RAM_GB = 16;
@@ -144,7 +145,8 @@ export function isPipMissing(output) {
144
145
  /** Run pip once. Resolves { code, err } — never rejects, so the caller owns the policy. */
145
146
  function runPip(python, pkgs, extraArgs = []) {
146
147
  return new Promise((resolve) => {
147
- const child = spawn(python, ["-m", "pip", "install", "-U", ...extraArgs, ...pkgs], {
148
+ // --prefer-binary: take an older wheel over a newer source build (a compiler is not a given, #225).
149
+ const child = spawn(python, ["-m", "pip", "install", "-U", "--prefer-binary", ...extraArgs, ...pkgs], {
148
150
  // stderr is PIPED so the PEP 668 marker can be recognised, and echoed as it arrives so
149
151
  // the user still watches a real install rather than a frozen line.
150
152
  stdio: ["ignore", "inherit", "pipe"],
@@ -198,9 +200,19 @@ async function ensureVenv(log) {
198
200
  * venv cannot be built, --break-system-packages, which is announced rather than done quietly.
199
201
  */
200
202
  async function pipInstall(pkgs, log) {
203
+ // A Python newer than the wheels (3.15 -> PyYAML builds from source -> "Visual C++ 14.0 is required"): #225.
204
+ const sp = await ensureSupportedPython({ log });
205
+ if (!sp.ok) throw new Error(sp.message);
206
+ if (sp.switched) await pinPythonForDaemon({ log });
201
207
  log(` installing ${pkgs.join(" ")} … (pip, this can take a minute)`);
202
208
  let r = await runPip(python3Bin(), pkgs);
203
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
+ }
204
216
  const noPip = isPipMissing(r.err);
205
217
  if (!noPip && !isExternallyManaged(r.err)) {
206
218
  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.866",
3
+ "version": "1.0.868",
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",
@@ -233,6 +233,92 @@ export async function resolvePython() {
233
233
  : { cmd: "python3", args: [] };
234
234
  }
235
235
 
236
+ /* ------------------------------------------- a Python new enough to lack wheels (#225) ---- */
237
+
238
+ /**
239
+ * The newest Python that gu's packages (PyYAML via smolagents, pillow, ...) ship prebuilt wheels for.
240
+ * Above it pip BUILDS FROM SOURCE, which on Windows asks for Visual C++ 14 and ends in
241
+ * "Microsoft Visual C++ 14.0 or greater is required" (reported with Python 3.15). Raise it as wheels land.
242
+ */
243
+ export const PYTHON_WHEELS_MAX = [3, 14];
244
+
245
+ /** "3.15.0" / "3.15" -> [3, 15], or null when it is not a version. */
246
+ export function parsePyVersion(out) {
247
+ const m = /^\s*(\d+)\.(\d+)/.exec(String(out ?? ""));
248
+ return m ? [Number(m[1]), Number(m[2])] : null;
249
+ }
250
+
251
+ /** Is this [major, minor] newer than the newest Python with wheels? */
252
+ export function pythonTooNew(v, max = PYTHON_WHEELS_MAX) {
253
+ return Array.isArray(v) && (v[0] > max[0] || (v[0] === max[0] && v[1] > max[1]));
254
+ }
255
+
256
+ /** Older interpreters to fall back to, best first (3.13, 3.12 are the most widely wheeled). */
257
+ export function altPythonCandidates(platform = process.platform) {
258
+ const minors = [13, 12, 14, 11, 10];
259
+ return platform === "win32"
260
+ ? minors.map((m) => ({ cmd: "py", args: [`-3.${m}`] }))
261
+ : minors.map((m) => ({ cmd: `python3.${m}`, args: [] }));
262
+ }
263
+
264
+ const EXE_PROBE = ["-c", "import sys; print(sys.executable)"];
265
+ const VER_PROBE = ["-c", "import sys; print('%d.%d' % sys.version_info[:2])"];
266
+
267
+ /**
268
+ * The first older interpreter that runs, as { cmd, args, exe } (exe = its real path), or null.
269
+ * `probe(cmd, args)` resolves stdout or null; injectable for the tests.
270
+ */
271
+ export async function findSupportedPython({ platform = process.platform, probe = (c, a) => run(c, a, { timeout: 8000 }) } = {}) {
272
+ for (const c of altPythonCandidates(platform)) {
273
+ const exe = String((await probe(c.cmd, [...c.args, ...EXE_PROBE])) ?? "").trim();
274
+ if (exe && !/\n/.test(exe)) return { ...c, exe };
275
+ }
276
+ return null;
277
+ }
278
+
279
+ /**
280
+ * Make sure the Python gu will install packages into is one that has wheels.
281
+ * - GONEXT_PROBE_PYTHON set: the user chose; leave it.
282
+ * - default Python fine: nothing to do.
283
+ * - default too new: switch to an older one (sets GONEXT_PROBE_PYTHON in `env`, so every spawn in
284
+ * this process follows) or say plainly what to install. Never starts a build that cannot succeed.
285
+ * Resolves { ok, switched?, from?, message? }.
286
+ */
287
+ export async function ensureSupportedPython({
288
+ platform = process.platform,
289
+ env = process.env,
290
+ probe = (c, a) => run(c, a, { timeout: 8000 }),
291
+ log = () => {},
292
+ } = {}) {
293
+ if ((env.GONEXT_PROBE_PYTHON ?? env.GONEXT_MLX_LM_PYTHON ?? "").trim()) return { ok: true };
294
+ let version = null;
295
+ for (const c of pythonCandidates(platform)) {
296
+ version = parsePyVersion(await probe(c.cmd, [...c.args, ...VER_PROBE]));
297
+ if (version) break;
298
+ }
299
+ if (!version || !pythonTooNew(version)) return { ok: true };
300
+ const from = version.join(".");
301
+ const alt = await findSupportedPython({ platform, probe });
302
+ if (alt) {
303
+ env.GONEXT_PROBE_PYTHON = alt.exe;
304
+ log(` Python ${from} is too new for gu's packages (no prebuilt wheels yet) - using ${alt.exe} instead.`);
305
+ return { ok: true, switched: alt.exe, from };
306
+ }
307
+ const how = platform === "win32" ? "winget install Python.Python.3.13, or python.org" : "your package manager, or python.org";
308
+ return {
309
+ ok: false,
310
+ from,
311
+ message:
312
+ `Python ${from} is too new: gu's packages (PyYAML and others) have no prebuilt ${platform === "win32" ? "Windows " : ""}` +
313
+ `wheels for it yet, so pip would try to compile them. Install Python 3.13 (${how}), open a new terminal, then run gu doctor again.`,
314
+ };
315
+ }
316
+
317
+ /** pip's "I had to compile and there is no compiler" failure. */
318
+ 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 ?? ""));
320
+ }
321
+
236
322
  /** How to SAY the resolved interpreter in a log line or error message. */
237
323
  export const pythonLabel = (py) => [py.cmd, ...py.args].join(" ");
238
324
 
@@ -148,7 +148,7 @@ export function ragSegmentLabel(b) {
148
148
  switch (b.ragState) {
149
149
  // Task #34: no "✗" — search still works through grep, so this is a notice, not an error.
150
150
  case "unreachable":
151
- return "rag: embedder down → grep fallback";
151
+ return "rag: using grep";
152
152
  case "no-index":
153
153
  return "rag — not indexed";
154
154
  case "unavailable":