@melaya/runner 1.1.30 → 1.1.31

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/dist/cli.js CHANGED
@@ -53,13 +53,25 @@ async function main() {
53
53
  const opts = program.opts();
54
54
  console.log(BANNER);
55
55
  // Detect Python
56
- const python = await findPython();
57
- if (!python) {
58
- console.log(chalk.red(" ✗ Python not found in PATH"));
59
- console.log(chalk.gray(" Install Python 3.11+ from https://python.org"));
56
+ const py = await findPython();
57
+ if (!py.ok) {
58
+ if (py.foundVersion) {
59
+ console.log(chalk.red(` ✗ ${py.foundVersion} found, but Melaya needs Python 3.${PY_MIN_MINOR}–3.${PY_MAX_MINOR}`));
60
+ console.log(chalk.gray(" Python 3.13+ has no wheels yet for the assistant's Python deps (agentscope + native"));
61
+ console.log(chalk.gray(" extensions), so the runner's venv can't build and every assistant turn fails."));
62
+ console.log(chalk.gray(" Install a supported Python, then restart the runner:"));
63
+ console.log(chalk.gray(" macOS: brew install python@3.12"));
64
+ console.log(chalk.gray(" Linux: sudo apt install python3.12 python3.12-venv (or: pyenv install 3.12)"));
65
+ console.log(chalk.gray(" Windows: winget install Python.Python.3.12"));
66
+ }
67
+ else {
68
+ console.log(chalk.red(" ✗ Python not found in PATH"));
69
+ console.log(chalk.gray(" Install Python 3.12 from https://python.org"));
70
+ }
60
71
  process.exit(1);
61
72
  }
62
- console.log(chalk.green(` ✓ Python: ${python}`));
73
+ const python = py.cmd;
74
+ console.log(chalk.green(` ✓ Python: ${python} (3.${py.minor})`));
63
75
  // Detect local models
64
76
  const models = await detectModels();
65
77
  if (models.length === 0) {
@@ -81,17 +93,67 @@ async function main() {
81
93
  verbose: opts.verbose,
82
94
  });
83
95
  }
84
- async function findPython() {
96
+ // Supported CPython range for the runner's venv. agentscope and its native
97
+ // deps (tiktoken, numpy, pydantic-core, curl_cffi via scrapling, sentence-
98
+ // transformers, …) only ship wheels for 3.10–3.12. Building the venv from a
99
+ // 3.13 interpreter — which is now Homebrew's default `python3` — leaves those
100
+ // deps unbuilt, so `import agentscope` fails the probe and every assistant /
101
+ // pipeline turn dies with "agentscope import probe failed". So we must NOT
102
+ // grab the first `python3` on PATH: we rank interpreters and pick a supported
103
+ // minor, only reporting the unsupported one (with install guidance) if that's
104
+ // all that exists. Bump PY_MAX_MINOR once the dep set is validated on a newer
105
+ // CPython.
106
+ const PY_MIN_MINOR = 10;
107
+ const PY_MAX_MINOR = 12;
108
+ async function probePyVersion(cmd) {
85
109
  const { execSync } = await import("child_process");
86
- for (const cmd of ["python3", "python"]) {
87
- try {
88
- const version = execSync(`${cmd} --version`, { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] }).trim();
89
- if (version.includes("Python 3."))
90
- return cmd;
110
+ try {
111
+ const out = execSync(`"${cmd}" --version`, { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] }).trim();
112
+ const m = out.match(/Python (\d+)\.(\d+)/);
113
+ if (!m)
114
+ return null;
115
+ return { major: Number(m[1]), minor: Number(m[2]) };
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ }
121
+ async function findPython() {
122
+ const candidates = [];
123
+ // 1. Explicit supported-minor binaries first, newest supported first. Homebrew
124
+ // (`brew install python@3.12`) and pyenv both expose these names on PATH,
125
+ // so this catches a supported interpreter even when bare `python3` is 3.13.
126
+ for (let m = PY_MAX_MINOR; m >= PY_MIN_MINOR; m--)
127
+ candidates.push(`python3.${m}`);
128
+ // 2. Common absolute install locations, in case PATH points `python3` at 3.13
129
+ // but a supported keg is installed and simply not linked onto PATH.
130
+ for (let m = PY_MAX_MINOR; m >= PY_MIN_MINOR; m--) {
131
+ if (process.platform === "darwin") {
132
+ candidates.push(`/opt/homebrew/opt/python@3.${m}/bin/python3.${m}`, // Apple-silicon brew keg
133
+ `/usr/local/opt/python@3.${m}/bin/python3.${m}`, // Intel brew keg
134
+ `/opt/homebrew/bin/python3.${m}`, `/usr/local/bin/python3.${m}`);
91
135
  }
92
- catch { /* not found */ }
136
+ else if (process.platform === "linux") {
137
+ candidates.push(`/usr/bin/python3.${m}`, `/usr/local/bin/python3.${m}`);
138
+ }
139
+ }
140
+ // 3. Generic names last — accepted only if they resolve into the supported range.
141
+ candidates.push("python3", "python");
142
+ let fallback; // a real 3.x that exists but is out of range
143
+ const seen = new Set();
144
+ for (const cmd of candidates) {
145
+ if (seen.has(cmd))
146
+ continue;
147
+ seen.add(cmd);
148
+ const v = await probePyVersion(cmd);
149
+ if (!v || v.major !== 3)
150
+ continue;
151
+ if (v.minor >= PY_MIN_MINOR && v.minor <= PY_MAX_MINOR)
152
+ return { ok: true, cmd, minor: v.minor };
153
+ if (!fallback)
154
+ fallback = `Python ${v.major}.${v.minor}`;
93
155
  }
94
- return null;
156
+ return { ok: false, foundVersion: fallback };
95
157
  }
96
158
  // `melaya-runner copilot login` — GitHub Copilot device-flow sign-in, cached
97
159
  // locally. Handled before the runner arg-parse so it needs no --token.
package/dist/pythonEnv.js CHANGED
@@ -10,7 +10,7 @@
10
10
  * shared-bundle version. Re-bootstraps when the bundle version changes.
11
11
  */
12
12
  import { spawn } from "child_process";
13
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
13
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from "fs";
14
14
  import { createHash } from "crypto";
15
15
  import { join } from "path";
16
16
  import { homedir, platform } from "os";
@@ -19,6 +19,29 @@ const CACHE_DIR = join(homedir(), ".melaya-runner");
19
19
  const VENV_DIR = join(CACHE_DIR, "venv");
20
20
  const VENV_MARK = join(CACHE_DIR, "venv-version.txt");
21
21
  const AGENTSCOPE = join(CACHE_DIR, "agentscope");
22
+ // Supported CPython range for the venv — MUST match cli.ts's findPython(). A
23
+ // venv is a thin wrapper frozen to the interpreter that created it, so one
24
+ // built from an unsupported minor (e.g. a pre-fix runner that grabbed
25
+ // Homebrew's python3.13) can never install agentscope's deps and its import
26
+ // probe always fails. We detect that from pyvenv.cfg and rebuild automatically.
27
+ const PY_SUPPORTED_MIN = 10;
28
+ const PY_SUPPORTED_MAX = 12;
29
+ // The minor version a venv was built from, read from its pyvenv.cfg
30
+ // (`version = 3.12.4`). Returns null if the file is missing/unparseable.
31
+ function venvMinorVersion() {
32
+ try {
33
+ const cfg = readFileSync(join(VENV_DIR, "pyvenv.cfg"), "utf-8");
34
+ const m = cfg.match(/version(?:_info)?\s*=\s*3\.(\d+)/i);
35
+ return m ? Number(m[1]) : null;
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ }
41
+ function venvVersionSupported() {
42
+ const minor = venvMinorVersion();
43
+ return minor !== null && minor >= PY_SUPPORTED_MIN && minor <= PY_SUPPORTED_MAX;
44
+ }
22
45
  // Explicit dependency list. The shared-bundle endpoint ships only
23
46
  // `*.py` files — no pyproject.toml — so `pip install -e
24
47
  // ~/.melaya-runner/agentscope` does NOT work (pip can't find a project
@@ -243,6 +266,11 @@ function venvIsValid(expectedVersion) {
243
266
  return false;
244
267
  if (!existsSync(VENV_MARK))
245
268
  return false;
269
+ // A venv built from an unsupported Python is never "valid", regardless of the
270
+ // marker — this forces the rebuild path (which recreates it from a supported
271
+ // interpreter) instead of self-healing a doomed venv.
272
+ if (!venvVersionSupported())
273
+ return false;
246
274
  try {
247
275
  const cached = readFileSync(VENV_MARK, "utf-8").trim();
248
276
  return cached === venvMarkerValue(expectedVersion);
@@ -376,6 +404,28 @@ export async function ensurePythonEnv(systemPython, expectedVersion, onProgress
376
404
  };
377
405
  }
378
406
  mkdirSync(CACHE_DIR, { recursive: true });
407
+ // Step 0: heal a venv left behind by a runner that built it from an
408
+ // unsupported interpreter (the classic case: a pre-fix runner grabbed
409
+ // Homebrew's python3.13, so agentscope's deps never installed and the import
410
+ // probe fails forever). A venv's Python is frozen at creation, so the only
411
+ // fix is to delete and recreate it from `systemPython` — which the CLI now
412
+ // guarantees is a supported 3.10–3.12. Fully automatic: no "delete the venv
413
+ // and restart" by hand.
414
+ if (existsSync(venvPython()) && !venvVersionSupported()) {
415
+ const minor = venvMinorVersion();
416
+ onProgress(`existing venv is Python ${minor === null ? "unknown" : `3.${minor}`} ` +
417
+ `(unsupported — need 3.${PY_SUPPORTED_MIN}–3.${PY_SUPPORTED_MAX}); rebuilding it`);
418
+ try {
419
+ rmSync(VENV_DIR, { recursive: true, force: true });
420
+ }
421
+ catch (e) {
422
+ return {
423
+ ok: false,
424
+ pythonPath: systemPython,
425
+ reason: `could not remove the stale venv at ${VENV_DIR} (${e?.message}). Delete it manually and restart the runner.`,
426
+ };
427
+ }
428
+ }
379
429
  // Step 1: create the venv if missing.
380
430
  if (!existsSync(venvPython())) {
381
431
  onProgress(`creating venv at ${VENV_DIR} (one-time, ~5s)`);
@@ -439,19 +489,22 @@ export async function ensurePythonEnv(systemPython, expectedVersion, onProgress
439
489
  // identical whether the venv is fresh or already valid.
440
490
  await ensureNltkData(onProgress);
441
491
  await _resolveAndCacheCertBundle(onProgress);
442
- writeFileSync(VENV_MARK, venvMarkerValue(expectedVersion), "utf-8");
443
492
  // sanity: confirm shortuuid + agentscope (via PYTHONPATH) resolve now.
444
493
  // Probe must mirror the spawn env so PYTHONPATH=CACHE_DIR points at
445
494
  // the cached agentscope source — without this the probe ImportError's
446
- // even though the real spawn would work.
495
+ // even though the real spawn would work. The marker is written ONLY after
496
+ // this passes, so a half-built venv is never cached as valid: the next
497
+ // launch re-enters this rebuild path and self-heals instead of returning a
498
+ // broken venv as ok.
447
499
  const probe = await runProc(venvPython(), ["-c", "import shortuuid, agentscope, anthropic, openai"], onProgress, { PYTHONPATH: CACHE_DIR });
448
500
  if (probe !== 0) {
449
501
  return {
450
502
  ok: false,
451
503
  pythonPath: systemPython,
452
- reason: "venv created but agentscope import probe failed. Delete ~/.melaya-runner/venv and restart the runner.",
504
+ reason: `agentscope import probe failed on the ${venvMinorVersion() === null ? "" : `Python 3.${venvMinorVersion()} `}venv the deps above did not import. See the pip lines above for the cause; the venv will be rebuilt on next launch.`,
453
505
  };
454
506
  }
507
+ writeFileSync(VENV_MARK, venvMarkerValue(expectedVersion), "utf-8");
455
508
  onProgress(`✓ venv ready (python=${venvPython()})`);
456
509
  return { ok: true, pythonPath: venvPython() };
457
510
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@melaya/runner",
3
- "version": "1.1.30",
3
+ "version": "1.1.31",
4
4
  "description": "Run Melaya AI pipelines locally with your own LM Studio or Ollama models",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,