nexo-brain 7.25.2 → 7.25.3

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/bin/nexo-brain.js CHANGED
@@ -57,6 +57,8 @@ const DEFAULT_ASSISTANT_NAME = "Nova";
57
57
  const RESERVED_ASSISTANT_NAME_KEYS = new Set(["nexo", "nexobrain", "nexodesktop"]);
58
58
  const MIN_INSTALLER_PYTHON_MAJOR = 3;
59
59
  const MIN_INSTALLER_PYTHON_MINOR = 10;
60
+ const DESKTOP_BUNDLED_WHEEL_PYTHON_MAJOR = 3;
61
+ const DESKTOP_BUNDLED_WHEEL_PYTHON_MINOR = 12;
60
62
 
61
63
  function normalizeAssistantNameCandidate(value) {
62
64
  return String(value || "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "");
@@ -278,11 +280,25 @@ function pythonVersionMeetsMinimum(versionText) {
278
280
  || (major === MIN_INSTALLER_PYTHON_MAJOR && minor >= MIN_INSTALLER_PYTHON_MINOR);
279
281
  }
280
282
 
283
+ function pythonVersionMatchesDesktopBundle(versionText) {
284
+ const match = String(versionText || "").trim().match(/^(\d+)\.(\d+)(?:\.|$)/);
285
+ if (!match) return false;
286
+ return Number(match[1]) === DESKTOP_BUNDLED_WHEEL_PYTHON_MAJOR
287
+ && Number(match[2]) === DESKTOP_BUNDLED_WHEEL_PYTHON_MINOR;
288
+ }
289
+
290
+ function pythonVersionUsableForInstaller(versionText) {
291
+ if (!pythonVersionMeetsMinimum(versionText)) return false;
292
+ if (isDesktopManagedInstall()) return pythonVersionMatchesDesktopBundle(versionText);
293
+ return true;
294
+ }
295
+
281
296
  function resolveInstallerPython() {
282
297
  const candidates = [
283
298
  process.env.NEXO_BOOTSTRAP_PYTHON,
284
299
  process.env.NEXO_RUNTIME_PYTHON,
285
300
  process.env.NEXO_PYTHON,
301
+ run("which python3.12"),
286
302
  run("which python3"),
287
303
  run("which python"),
288
304
  ].filter(Boolean);
@@ -292,7 +308,7 @@ function resolveInstallerPython() {
292
308
  if (!clean || seen.has(clean)) continue;
293
309
  seen.add(clean);
294
310
  const version = pythonVersion(clean);
295
- if (version && pythonVersionMeetsMinimum(version)) return clean;
311
+ if (version && pythonVersionUsableForInstaller(version)) return clean;
296
312
  }
297
313
  return "";
298
314
  }
@@ -366,9 +382,11 @@ function uniqueBackupPath(targetPath, suffix) {
366
382
  function ensureManagedVenvCompatible(venvPath, venvPython) {
367
383
  if (!fs.existsSync(venvPython)) return;
368
384
  const version = pythonVersion(venvPython);
369
- if (version && pythonVersionMeetsMinimum(version)) return;
385
+ if (version && pythonVersionUsableForInstaller(version)) return;
370
386
 
371
- const reason = version ? `Python ${version}` : "an unreadable Python executable";
387
+ const reason = version
388
+ ? `Python ${version}${isDesktopManagedInstall() ? " (Desktop bundle requires Python 3.12)" : ""}`
389
+ : "an unreadable Python executable";
372
390
  const backupPath = uniqueBackupPath(venvPath, "unsupported-python");
373
391
  log(` Existing Python virtual environment uses ${reason}; moving it aside to recreate.`);
374
392
  try {
@@ -3300,14 +3318,14 @@ async function runSetup() {
3300
3318
  }
3301
3319
  if (!python) {
3302
3320
  log("Python 3 not found and couldn't install automatically.");
3303
- log(platform === "darwin" ? "Install it: brew install python3" : "Install it: sudo apt install python3");
3321
+ log(platform === "darwin" ? "Install it: brew install python@3.12" : "Install it: sudo apt install python3");
3304
3322
  process.exit(1);
3305
3323
  }
3306
3324
  }
3307
3325
  const pyVersion = pythonVersion(python);
3308
- if (!pyVersion || !pythonVersionMeetsMinimum(pyVersion)) {
3326
+ if (!pyVersion || !pythonVersionUsableForInstaller(pyVersion)) {
3309
3327
  log(pyVersion
3310
- ? `Python at ${python} is ${pyVersion}; NEXO Brain requires Python >=${MIN_INSTALLER_PYTHON_MAJOR}.${MIN_INSTALLER_PYTHON_MINOR}.`
3328
+ ? `Python at ${python} is ${pyVersion}; NEXO Brain requires ${isDesktopManagedInstall() ? "Python 3.12 for Desktop bundled wheels" : `Python >=${MIN_INSTALLER_PYTHON_MAJOR}.${MIN_INSTALLER_PYTHON_MINOR}`}.`
3311
3329
  : `Python at ${python || "(not found)"} is not executable.`);
3312
3330
  process.exit(1);
3313
3331
  }
@@ -3618,7 +3636,7 @@ async function runSetup() {
3618
3636
  }
3619
3637
  if (fs.existsSync(venvPython)) {
3620
3638
  const venvVersion = pythonVersion(venvPython);
3621
- if (!venvVersion || !pythonVersionMeetsMinimum(venvVersion)) {
3639
+ if (!venvVersion || !pythonVersionUsableForInstaller(venvVersion)) {
3622
3640
  log(`Python virtual environment is unsupported after creation (${venvVersion || "unknown version"}).`);
3623
3641
  process.exit(1);
3624
3642
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.25.2",
3
+ "version": "7.25.3",
4
4
  "mcpName": "io.github.wazionapps/nexo",
5
5
  "description": "NEXO Brain — Shared brain for AI agents. Persistent memory, semantic RAG, natural forgetting, metacognitive guard, trust scoring, 150+ MCP tools. Works with Claude Code, Codex, Claude Desktop & any MCP client. 100% local, free.",
6
6
  "homepage": "https://nexo-brain.com",
@@ -21,7 +21,7 @@ import time
21
21
  from pathlib import Path
22
22
 
23
23
  try:
24
- from product_mode import enforce_desktop_product_contract
24
+ from product_mode import desktop_product_requested, enforce_desktop_product_contract
25
25
  except ModuleNotFoundError as exc:
26
26
  if getattr(exc, "name", "") != "product_mode":
27
27
  raise
@@ -30,7 +30,7 @@ except ModuleNotFoundError as exc:
30
30
  core_path = str(_core_runtime)
31
31
  if core_path not in sys.path:
32
32
  sys.path.insert(0, core_path)
33
- from product_mode import enforce_desktop_product_contract
33
+ from product_mode import desktop_product_requested, enforce_desktop_product_contract
34
34
  from runtime_home import export_resolved_nexo_home, managed_nexo_home
35
35
 
36
36
  try:
@@ -939,14 +939,94 @@ def _venv_pip_path(runtime_root: Path = NEXO_HOME) -> Path:
939
939
  return runtime_root / ".venv" / "bin" / "pip"
940
940
 
941
941
 
942
+ def _python_version_tuple(python_bin: Path | str) -> tuple[int, int, int] | None:
943
+ try:
944
+ result = subprocess.run(
945
+ [str(python_bin), "-c", "import sys; print('.'.join(map(str, sys.version_info[:3])))"],
946
+ capture_output=True,
947
+ text=True,
948
+ timeout=15,
949
+ )
950
+ except Exception:
951
+ return None
952
+ if result.returncode != 0:
953
+ return None
954
+ match = re.search(r"(\d+)\.(\d+)\.(\d+)", result.stdout or result.stderr or "")
955
+ if not match:
956
+ return None
957
+ return int(match.group(1)), int(match.group(2)), int(match.group(3))
958
+
959
+
960
+ def _managed_venv_python_supported(python_bin: Path | str) -> bool:
961
+ version = _python_version_tuple(python_bin)
962
+ if not version:
963
+ return False
964
+ if desktop_product_requested():
965
+ return version[:2] == (3, 12)
966
+ return version >= (3, 10, 0)
967
+
968
+
969
+ def _resolve_managed_venv_base_python() -> str:
970
+ candidates = [
971
+ os.environ.get("NEXO_BOOTSTRAP_PYTHON", ""),
972
+ os.environ.get("NEXO_RUNTIME_PYTHON", ""),
973
+ os.environ.get("NEXO_PYTHON", ""),
974
+ ]
975
+ if desktop_product_requested():
976
+ candidates.extend([
977
+ "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3.12",
978
+ "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3",
979
+ "/opt/homebrew/bin/python3.12",
980
+ "/usr/local/bin/python3.12",
981
+ shutil.which("python3.12") or "",
982
+ ])
983
+ candidates.append(sys.executable)
984
+ candidates.append(shutil.which("python3") or "")
985
+ for candidate in candidates:
986
+ clean = str(candidate or "").strip()
987
+ if clean and Path(clean).exists() and _managed_venv_python_supported(clean):
988
+ return clean
989
+ return sys.executable
990
+
991
+
992
+ def _archive_incompatible_runtime_venv(runtime_root: Path = NEXO_HOME, reason: str = "incompatible-python") -> Path | None:
993
+ venv_dir = runtime_root / ".venv"
994
+ if not venv_dir.exists():
995
+ return None
996
+ stamp = time.strftime("%Y%m%d-%H%M%S")
997
+ backup_root = runtime_root / "runtime" / "backups"
998
+ try:
999
+ backup_root.mkdir(parents=True, exist_ok=True)
1000
+ target = backup_root / f"venv-{reason}-{stamp}"
1001
+ counter = 2
1002
+ while target.exists():
1003
+ target = backup_root / f"venv-{reason}-{stamp}-{counter}"
1004
+ counter += 1
1005
+ shutil.move(str(venv_dir), str(target))
1006
+ _log(f"Archived incompatible managed venv at {target}")
1007
+ return target
1008
+ except Exception as exc:
1009
+ _log(f"venv archive failed: {exc}")
1010
+ return None
1011
+
1012
+
942
1013
  def _ensure_runtime_venv(runtime_root: Path = NEXO_HOME) -> Path | None:
943
1014
  venv_python = _venv_python_path(runtime_root)
944
1015
  if venv_python.exists():
945
- return venv_python
1016
+ if _managed_venv_python_supported(venv_python):
1017
+ return venv_python
1018
+ version = _python_version_tuple(venv_python)
1019
+ reason = f"python-{'.'.join(map(str, version[:2]))}" if version else "unreadable-python"
1020
+ if _archive_incompatible_runtime_venv(runtime_root, reason=reason) is None:
1021
+ return None
1022
+ base_python = _resolve_managed_venv_base_python()
1023
+ if not _managed_venv_python_supported(base_python):
1024
+ _log(f"no supported base Python found for managed venv: {base_python}")
1025
+ return None
946
1026
  try:
947
1027
  runtime_root.mkdir(parents=True, exist_ok=True)
948
1028
  result = subprocess.run(
949
- [sys.executable, "-m", "venv", str(runtime_root / ".venv")],
1029
+ [base_python, "-m", "venv", str(runtime_root / ".venv")],
950
1030
  capture_output=True,
951
1031
  text=True,
952
1032
  timeout=120,
@@ -965,7 +1045,7 @@ def _reinstall_pip_deps() -> bool:
965
1045
  req_file = SRC_DIR / "requirements.txt"
966
1046
  if not req_file.exists():
967
1047
  return True
968
- _ensure_runtime_venv(NEXO_HOME)
1048
+ venv_python = _ensure_runtime_venv(NEXO_HOME)
969
1049
  venv_pip = _venv_pip_path(NEXO_HOME)
970
1050
  if not venv_pip.exists() and sys.platform != "win32":
971
1051
  alt_pip = NEXO_HOME / ".venv" / "bin" / "pip3"
@@ -977,11 +1057,14 @@ def _reinstall_pip_deps() -> bool:
977
1057
  [str(venv_pip), "install", "--quiet", "-r", str(req_file)],
978
1058
  capture_output=True, text=True, timeout=120,
979
1059
  )
980
- else:
1060
+ elif not desktop_product_requested():
981
1061
  result = subprocess.run(
982
1062
  [sys.executable, "-m", "pip", "install", "--quiet", "-r", str(req_file), "--break-system-packages"],
983
1063
  capture_output=True, text=True, timeout=120,
984
1064
  )
1065
+ else:
1066
+ _log(f"managed venv unavailable for Desktop dependency repair: {venv_python}")
1067
+ return False
985
1068
  if result.returncode != 0:
986
1069
  _log(f"pip install failed (exit {result.returncode}): {result.stderr or result.stdout}")
987
1070
  return False
@@ -4665,7 +4748,7 @@ def _reinstall_runtime_pip_deps(runtime_root: Path = NEXO_HOME) -> bool:
4665
4748
  req_file = runtime_root / "requirements.txt"
4666
4749
  if not req_file.exists():
4667
4750
  return True
4668
- _ensure_runtime_venv(runtime_root)
4751
+ venv_python = _ensure_runtime_venv(runtime_root)
4669
4752
  venv_pip = _venv_pip_path(runtime_root)
4670
4753
  if not venv_pip.exists() and sys.platform != "win32":
4671
4754
  alt_pip = runtime_root / ".venv" / "bin" / "pip3"
@@ -4679,13 +4762,16 @@ def _reinstall_runtime_pip_deps(runtime_root: Path = NEXO_HOME) -> bool:
4679
4762
  text=True,
4680
4763
  timeout=120,
4681
4764
  )
4682
- else:
4765
+ elif not desktop_product_requested():
4683
4766
  result = subprocess.run(
4684
4767
  [sys.executable, "-m", "pip", "install", "--quiet", "-r", str(req_file), "--break-system-packages"],
4685
4768
  capture_output=True,
4686
4769
  text=True,
4687
4770
  timeout=120,
4688
4771
  )
4772
+ else:
4773
+ _log(f"managed venv unavailable for Desktop runtime dependency repair: {venv_python}")
4774
+ return False
4689
4775
  return result.returncode == 0
4690
4776
  except Exception:
4691
4777
  return False
@@ -549,6 +549,111 @@ def check_python_runtime() -> DoctorCheck:
549
549
  )
550
550
 
551
551
 
552
+ def _desktop_product_requested() -> bool:
553
+ try:
554
+ from product_mode import desktop_product_requested
555
+ return bool(desktop_product_requested())
556
+ except Exception:
557
+ return str(os.environ.get("NEXO_DESKTOP_MANAGED", "")).strip() == "1"
558
+
559
+
560
+ def _managed_venv_python_path() -> Path:
561
+ if sys.platform == "win32":
562
+ return NEXO_HOME / ".venv" / "Scripts" / "python.exe"
563
+ return NEXO_HOME / ".venv" / "bin" / "python3"
564
+
565
+
566
+ def _probe_python_version(python_bin: Path | str) -> tuple[int, int, int] | None:
567
+ try:
568
+ result = subprocess.run(
569
+ [str(python_bin), "-c", "import sys; print('.'.join(map(str, sys.version_info[:3])))"],
570
+ capture_output=True,
571
+ text=True,
572
+ timeout=15,
573
+ )
574
+ except Exception:
575
+ return None
576
+ if result.returncode != 0:
577
+ return None
578
+ import re
579
+ match = re.search(r"(\d+)\.(\d+)\.(\d+)", result.stdout or result.stderr or "")
580
+ if not match:
581
+ return None
582
+ return int(match.group(1)), int(match.group(2)), int(match.group(3))
583
+
584
+
585
+ def _managed_venv_version_supported(version: tuple[int, int, int] | None) -> bool:
586
+ if not version:
587
+ return False
588
+ if _desktop_product_requested():
589
+ return version[:2] == (3, 12)
590
+ return version >= (3, 10, 0)
591
+
592
+
593
+ def _repair_managed_venv_python() -> bool:
594
+ try:
595
+ import auto_update
596
+ return bool(auto_update._ensure_runtime_venv(NEXO_HOME)) and bool(auto_update._reinstall_pip_deps())
597
+ except Exception:
598
+ return False
599
+
600
+
601
+ def check_managed_venv_python(fix: bool = False) -> DoctorCheck:
602
+ """Check the managed NEXO venv is compatible with bundled runtime deps."""
603
+ venv_python = _managed_venv_python_path()
604
+ if not venv_python.exists():
605
+ if not _desktop_product_requested():
606
+ return DoctorCheck(
607
+ id="boot.managed_venv_python",
608
+ tier="boot",
609
+ status="healthy",
610
+ severity="info",
611
+ summary="Managed Python venv not present yet",
612
+ evidence=[str(venv_python)],
613
+ )
614
+ return DoctorCheck(
615
+ id="boot.managed_venv_python",
616
+ tier="boot",
617
+ status="degraded",
618
+ severity="warn",
619
+ summary="Managed Python venv missing",
620
+ evidence=[str(venv_python)],
621
+ repair_plan=["Run nexo doctor --tier boot --fix or nexo update to recreate the managed venv"],
622
+ )
623
+
624
+ version = _probe_python_version(venv_python)
625
+ if _managed_venv_version_supported(version):
626
+ return DoctorCheck(
627
+ id="boot.managed_venv_python",
628
+ tier="boot",
629
+ status="healthy",
630
+ severity="info",
631
+ summary=f"Managed venv Python {'.'.join(map(str, version))}",
632
+ evidence=[str(venv_python)],
633
+ )
634
+
635
+ summary = "Managed venv Python is incompatible"
636
+ if version:
637
+ summary = f"Managed venv Python {'.'.join(map(str, version))} is incompatible"
638
+ if _desktop_product_requested():
639
+ summary += " — Desktop bundled wheels require Python 3.12"
640
+ if fix and _repair_managed_venv_python():
641
+ post = check_managed_venv_python(fix=False)
642
+ if post.status == "healthy":
643
+ post.fixed = True
644
+ post.summary += " (fixed)"
645
+ return post
646
+ return DoctorCheck(
647
+ id="boot.managed_venv_python",
648
+ tier="boot",
649
+ status="degraded",
650
+ severity="warn",
651
+ summary=summary,
652
+ evidence=[str(venv_python)],
653
+ repair_plan=["Run nexo doctor --tier boot --fix or nexo update to archive and recreate the managed venv"],
654
+ )
655
+
656
+
552
657
  CRITICAL_CONFIG_FILES = (
553
658
  ("schedule.json", ("config", "schedule.json")),
554
659
  ("optionals.json", ("config", "optionals.json")),
@@ -803,6 +908,7 @@ def run_boot_checks(fix: bool = False) -> list[DoctorCheck]:
803
908
  safe_check(check_disk_space),
804
909
  safe_check(check_wrapper_scripts),
805
910
  safe_check(check_python_runtime),
911
+ safe_check(check_managed_venv_python, fix=fix),
806
912
  safe_check(check_config_parse),
807
913
  safe_check(check_core_dev_packaged_install),
808
914
  safe_check(check_dashboard_desktop_contract),
@@ -12,6 +12,12 @@ import time
12
12
  from pathlib import Path
13
13
 
14
14
  from runtime_home import export_resolved_nexo_home
15
+ try:
16
+ from product_mode import desktop_product_requested
17
+ except Exception: # pragma: no cover - stale packaged runtimes may miss product_mode during update
18
+ def desktop_product_requested() -> bool:
19
+ return str(os.environ.get("NEXO_DESKTOP_MANAGED", "")).strip() == "1"
20
+
15
21
  from runtime_versioning import (
16
22
  activate_versioned_runtime_snapshot,
17
23
  compute_mcp_runtime_fingerprint,
@@ -239,15 +245,94 @@ def _venv_pip_path(runtime_root: Path | None = None) -> Path:
239
245
  return root / ".venv" / "bin" / "pip"
240
246
 
241
247
 
248
+ def _python_version_tuple(python_bin: Path | str) -> tuple[int, int, int] | None:
249
+ try:
250
+ result = subprocess.run(
251
+ [str(python_bin), "-c", "import sys; print('.'.join(map(str, sys.version_info[:3])))"],
252
+ capture_output=True,
253
+ text=True,
254
+ timeout=15,
255
+ )
256
+ except Exception:
257
+ return None
258
+ if result.returncode != 0:
259
+ return None
260
+ match = re.search(r"(\d+)\.(\d+)\.(\d+)", result.stdout or result.stderr or "")
261
+ if not match:
262
+ return None
263
+ return int(match.group(1)), int(match.group(2)), int(match.group(3))
264
+
265
+
266
+ def _managed_venv_python_supported(python_bin: Path | str) -> bool:
267
+ version = _python_version_tuple(python_bin)
268
+ if not version:
269
+ return False
270
+ if desktop_product_requested():
271
+ return version[:2] == (3, 12)
272
+ return version >= (3, 10, 0)
273
+
274
+
275
+ def _resolve_managed_venv_base_python() -> str:
276
+ candidates = [
277
+ os.environ.get("NEXO_BOOTSTRAP_PYTHON", ""),
278
+ os.environ.get("NEXO_RUNTIME_PYTHON", ""),
279
+ os.environ.get("NEXO_PYTHON", ""),
280
+ ]
281
+ if desktop_product_requested():
282
+ candidates.extend([
283
+ "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3.12",
284
+ "/Library/Frameworks/Python.framework/Versions/3.12/bin/python3",
285
+ "/opt/homebrew/bin/python3.12",
286
+ "/usr/local/bin/python3.12",
287
+ shutil.which("python3.12") or "",
288
+ ])
289
+ candidates.extend([sys.executable, shutil.which("python3") or ""])
290
+ for candidate in candidates:
291
+ clean = str(candidate or "").strip()
292
+ if clean and Path(clean).exists() and _managed_venv_python_supported(clean):
293
+ return clean
294
+ return sys.executable
295
+
296
+
297
+ def _archive_incompatible_managed_venv(root: Path, reason: str = "incompatible-python") -> Path | None:
298
+ venv_dir = root / ".venv"
299
+ if not venv_dir.exists():
300
+ return None
301
+ backup_root = root / "runtime" / "backups"
302
+ stamp = time.strftime("%Y%m%d-%H%M%S")
303
+ try:
304
+ backup_root.mkdir(parents=True, exist_ok=True)
305
+ target = backup_root / f"venv-{reason}-{stamp}"
306
+ counter = 2
307
+ while target.exists():
308
+ target = backup_root / f"venv-{reason}-{stamp}-{counter}"
309
+ counter += 1
310
+ shutil.move(str(venv_dir), str(target))
311
+ return target
312
+ except Exception:
313
+ return None
314
+
315
+
242
316
  def _ensure_managed_venv(runtime_root: Path | None = None) -> str | None:
243
317
  root = runtime_root or _nexo_home()
244
318
  venv_python = _venv_python_path(root)
319
+ if venv_python.exists():
320
+ if _managed_venv_python_supported(venv_python):
321
+ return None
322
+ version = _python_version_tuple(venv_python)
323
+ reason = f"python-{'.'.join(map(str, version[:2]))}" if version else "unreadable-python"
324
+ archived = _archive_incompatible_managed_venv(root, reason=reason)
325
+ if archived is None:
326
+ return "managed venv uses an incompatible Python and could not be archived"
327
+ base_python = _resolve_managed_venv_base_python()
328
+ if not _managed_venv_python_supported(base_python):
329
+ return f"no supported Python found for managed venv: {base_python}"
245
330
  if venv_python.exists():
246
331
  return None
247
332
  try:
248
333
  root.mkdir(parents=True, exist_ok=True)
249
334
  result = subprocess.run(
250
- [sys.executable, "-m", "venv", str(root / ".venv")],
335
+ [base_python, "-m", "venv", str(root / ".venv")],
251
336
  capture_output=True,
252
337
  text=True,
253
338
  timeout=120,
@@ -676,6 +761,8 @@ def _reinstall_pip_deps() -> str | None:
676
761
  if alt_pip.exists():
677
762
  venv_pip = alt_pip
678
763
  if not venv_pip.exists():
764
+ if desktop_product_requested():
765
+ return "managed Desktop venv pip is unavailable after repair"
679
766
  # No venv, try system pip with --break-system-packages
680
767
  try:
681
768
  result = subprocess.run(