nexo-brain 7.13.7 → 7.13.9

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.13.7",
3
+ "version": "7.13.9",
4
4
  "description": "Local cognitive runtime for Claude Code \u2014 persistent memory, overnight learning, doctor diagnostics, personal scripts, recovery-aware jobs, startup preflight, and optional dashboard/power helper.",
5
5
  "author": {
6
6
  "name": "NEXO Brain",
package/README.md CHANGED
@@ -18,7 +18,11 @@
18
18
 
19
19
  [Watch the overview video](https://nexo-brain.com/watch/) · [Watch on YouTube](https://www.youtube.com/watch?v=i2lkGhKyVqI) · [Open the infographic](https://nexo-brain.com/assets/nexo-brain-infographic-v5.png)
20
20
 
21
- Version `7.13.7` is the current packaged-runtime line. Patch release over v7.13.6 — Brain adds an authenticated official protocol-card client (`nexo_card_catalog`, `nexo_card_get`, `nexo_card_match`) so agents can ask the NEXO Desktop backend for the right task protocol at runtime. The protocol corpus stays private on the server; this open-source package ships only the client, tool map, and agent guidance.
21
+ Version `7.13.9` is the current packaged-runtime line. Patch release over v7.13.8 — Brain now moves aside an existing managed `.venv` when it was created with unsupported Python <3.10, then recreates it with the supported interpreter prepared by Desktop.
22
+
23
+ Previously in `7.13.8`: patch release — Brain rejects Python <3.10 during Desktop-managed fresh installs, honors the Python interpreter prepared by Desktop, and fails clearly before dependency resolution if an unsupported Apple Python 3.9 reaches the installer.
24
+
25
+ Previously in `7.13.7`: patch release — Brain adds an authenticated official protocol-card client (`nexo_card_catalog`, `nexo_card_get`, `nexo_card_match`) so agents can ask the NEXO Desktop backend for the right task protocol at runtime. The protocol corpus stays private on the server; this open-source package ships only the client, tool map, and agent guidance.
22
26
 
23
27
  Previously in `7.13.6`: patch release — Codex hook sync now renders the managed `PreToolUse` shell/exec_command guard with native Windows `cmd.exe` syntax while preserving the existing POSIX command on macOS/Linux. Result: coordinated Desktop bundles can ship the fixed Brain without changing the Mac/Windows installation contract.
24
28
 
package/bin/nexo-brain.js CHANGED
@@ -36,6 +36,8 @@ if (process.platform === "win32") {
36
36
  let NEXO_HOME = process.env.NEXO_HOME || path.join(require("os").homedir(), ".nexo");
37
37
  const DEFAULT_ASSISTANT_NAME = "Nova";
38
38
  const RESERVED_ASSISTANT_NAME_KEYS = new Set(["nexo", "nexobrain", "nexodesktop"]);
39
+ const MIN_INSTALLER_PYTHON_MAJOR = 3;
40
+ const MIN_INSTALLER_PYTHON_MINOR = 10;
39
41
 
40
42
  function normalizeAssistantNameCandidate(value) {
41
43
  return String(value || "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "");
@@ -225,6 +227,57 @@ function run(cmd, opts = {}) {
225
227
  }
226
228
  }
227
229
 
230
+ function shSingleQuote(value) {
231
+ return "'" + String(value || "").replace(/'/g, "'\\''") + "'";
232
+ }
233
+
234
+ function runPythonProbe(pythonBin, args, timeout = 15000) {
235
+ if (!pythonBin) return null;
236
+ try {
237
+ const result = spawnSync(pythonBin, args, {
238
+ encoding: "utf8",
239
+ stdio: ["ignore", "pipe", "pipe"],
240
+ timeout,
241
+ });
242
+ if (result.status !== 0) return null;
243
+ return String(result.stdout || result.stderr || "").trim();
244
+ } catch {
245
+ return null;
246
+ }
247
+ }
248
+
249
+ function pythonVersion(pythonBin) {
250
+ return runPythonProbe(pythonBin, ["-c", "import sys; print(sys.version.split()[0])"]);
251
+ }
252
+
253
+ function pythonVersionMeetsMinimum(versionText) {
254
+ const match = String(versionText || "").trim().match(/^(\d+)\.(\d+)(?:\.|$)/);
255
+ if (!match) return false;
256
+ const major = Number(match[1]);
257
+ const minor = Number(match[2]);
258
+ return major > MIN_INSTALLER_PYTHON_MAJOR
259
+ || (major === MIN_INSTALLER_PYTHON_MAJOR && minor >= MIN_INSTALLER_PYTHON_MINOR);
260
+ }
261
+
262
+ function resolveInstallerPython() {
263
+ const candidates = [
264
+ process.env.NEXO_BOOTSTRAP_PYTHON,
265
+ process.env.NEXO_RUNTIME_PYTHON,
266
+ process.env.NEXO_PYTHON,
267
+ run("which python3"),
268
+ run("which python"),
269
+ ].filter(Boolean);
270
+ const seen = new Set();
271
+ for (const candidate of candidates) {
272
+ const clean = String(candidate || "").trim();
273
+ if (!clean || seen.has(clean)) continue;
274
+ seen.add(clean);
275
+ const version = pythonVersion(clean);
276
+ if (version && pythonVersionMeetsMinimum(version)) return clean;
277
+ }
278
+ return "";
279
+ }
280
+
228
281
  function findBundledWheel(wheelsDir, prefix) {
229
282
  try {
230
283
  const normalizedPrefix = String(prefix || "").toLowerCase() + "-";
@@ -250,6 +303,46 @@ function pythonHasPip(pythonBin) {
250
303
  }
251
304
  }
252
305
 
306
+ function managedVenvPythonPath(nexoHome = NEXO_HOME) {
307
+ const venvPath = path.join(nexoHome, ".venv");
308
+ return process.platform === "win32"
309
+ ? path.join(venvPath, "Scripts", "python.exe")
310
+ : path.join(venvPath, "bin", "python3");
311
+ }
312
+
313
+ function safeTimestampForPath() {
314
+ return new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14);
315
+ }
316
+
317
+ function uniqueBackupPath(targetPath, suffix) {
318
+ const dir = path.dirname(targetPath);
319
+ const base = path.basename(targetPath);
320
+ const stamp = safeTimestampForPath();
321
+ let candidate = path.join(dir, `${base}.${suffix}-${stamp}`);
322
+ if (!fs.existsSync(candidate)) return candidate;
323
+ for (let i = 2; i < 100; i += 1) {
324
+ candidate = path.join(dir, `${base}.${suffix}-${stamp}-${i}`);
325
+ if (!fs.existsSync(candidate)) return candidate;
326
+ }
327
+ return path.join(dir, `${base}.${suffix}-${stamp}-${process.pid}`);
328
+ }
329
+
330
+ function ensureManagedVenvCompatible(venvPath, venvPython) {
331
+ if (!fs.existsSync(venvPython)) return;
332
+ const version = pythonVersion(venvPython);
333
+ if (version && pythonVersionMeetsMinimum(version)) return;
334
+
335
+ const reason = version ? `Python ${version}` : "an unreadable Python executable";
336
+ const backupPath = uniqueBackupPath(venvPath, "unsupported-python");
337
+ log(` Existing Python virtual environment uses ${reason}; moving it aside to recreate.`);
338
+ try {
339
+ fs.renameSync(venvPath, backupPath);
340
+ } catch (err) {
341
+ throw new Error(`Existing NEXO Python virtual environment is incompatible and could not be moved aside: ${err.message || err}`);
342
+ }
343
+ log(` Previous Python virtual environment moved to ${backupPath}`);
344
+ }
345
+
253
346
  function seedPipFromBundledWheels(venvPython, bundledWheelsDir) {
254
347
  if (!fs.existsSync(venvPython) || !fs.existsSync(bundledWheelsDir)) return false;
255
348
  if (pythonHasPip(venvPython)) return true;
@@ -523,15 +616,13 @@ function resolveSystemPython() {
523
616
  }
524
617
 
525
618
  function ensureWarmupPython(nexoHome = NEXO_HOME) {
526
- const existing = findVenvPython(nexoHome);
527
- if (existing) return existing;
528
-
529
- const basePython = resolveSystemPython();
530
619
  const venvPath = path.join(nexoHome, ".venv");
531
- const venvPython = process.platform === "win32"
532
- ? path.join(venvPath, "Scripts", "python.exe")
533
- : path.join(venvPath, "bin", "python3");
620
+ const venvPython = managedVenvPythonPath(nexoHome);
534
621
  fs.mkdirSync(nexoHome, { recursive: true });
622
+ ensureManagedVenvCompatible(venvPath, venvPython);
623
+ if (fs.existsSync(venvPython)) return venvPython;
624
+
625
+ const basePython = resolveInstallerPython() || resolveSystemPython();
535
626
  if (!fs.existsSync(venvPython)) {
536
627
  log(" Creating Python virtual environment for model warmup...");
537
628
  const result = spawnSync(basePython, ["-m", "venv", venvPath], { stdio: "inherit", timeout: 120000 });
@@ -1710,6 +1801,48 @@ function buildManagedCliEnv(extraEnv = {}) {
1710
1801
  };
1711
1802
  }
1712
1803
 
1804
+ function ensureDesktopNodeShim(desktopNode) {
1805
+ const clean = String(desktopNode || "").trim();
1806
+ if (!clean) return "";
1807
+ const shimDir = path.join(NEXO_HOME, "runtime", "bootstrap", "node-shim");
1808
+ fs.mkdirSync(shimDir, { recursive: true });
1809
+ if (process.platform === "win32") {
1810
+ const shimPath = path.join(shimDir, "node.cmd");
1811
+ fs.writeFileSync(
1812
+ shimPath,
1813
+ `@echo off\r\nset ELECTRON_RUN_AS_NODE=1\r\n"${clean}" %*\r\n`,
1814
+ );
1815
+ return shimDir;
1816
+ }
1817
+ const shimPath = path.join(shimDir, "node");
1818
+ fs.writeFileSync(
1819
+ shimPath,
1820
+ [
1821
+ "#!/bin/sh",
1822
+ "export ELECTRON_RUN_AS_NODE=1",
1823
+ `exec ${shSingleQuote(clean)} "$@"`,
1824
+ "",
1825
+ ].join("\n"),
1826
+ );
1827
+ fs.chmodSync(shimPath, 0o755);
1828
+ return shimDir;
1829
+ }
1830
+
1831
+ function withDesktopNodeShim(env, desktopNode) {
1832
+ try {
1833
+ const shimDir = ensureDesktopNodeShim(desktopNode);
1834
+ if (!shimDir) return env;
1835
+ return {
1836
+ ...env,
1837
+ ELECTRON_RUN_AS_NODE: "1",
1838
+ PATH: [shimDir, env.PATH || ""].filter(Boolean).join(path.delimiter),
1839
+ };
1840
+ } catch (err) {
1841
+ log(`Desktop Node shim could not be created: ${String(err && err.message || err)}`);
1842
+ return env;
1843
+ }
1844
+ }
1845
+
1713
1846
  function resolveManagedClaudeBinary() {
1714
1847
  const prefix = managedClaudePrefix();
1715
1848
  const candidates = process.platform === "win32"
@@ -1921,11 +2054,13 @@ function installClaudeCodeCli(platform) {
1921
2054
  return { installed: true, path: claudeInstalled };
1922
2055
  }
1923
2056
 
1924
- const installEnv = buildManagedCliEnv();
1925
2057
  const desktopNode = String(process.env.NEXO_DESKTOP_NODE || "").trim();
1926
2058
  const bundledNpmCli = String(process.env.NEXO_DESKTOP_NPM_CLI || "").trim();
1927
2059
  const managedPrefix = managedClaudePrefix();
1928
2060
  const desktopManaged = isDesktopManagedInstall();
2061
+ const npmViaDesktop = desktopNode && bundledNpmCli;
2062
+ let installEnv = buildManagedCliEnv();
2063
+ if (desktopNode) installEnv = withDesktopNodeShim(installEnv, desktopNode);
1929
2064
 
1930
2065
  // OFFLINE-FIRST v0.32.4: install claude-code wrapper + ALL its native packs
1931
2066
  // from bundled tarballs. Path: resources/brain-bundle/claude-code/*.tgz.
@@ -1963,8 +2098,18 @@ function installClaudeCodeCli(platform) {
1963
2098
  const tgzPaths = [path.join(bundledClaudeDir, wrapper), ...nativePacks.map((p) => path.join(bundledClaudeDir, p))];
1964
2099
  log(" Installing claude-code from bundled tarballs (offline, " + (1 + nativePacks.length) + " packs)...");
1965
2100
  spawnSync(
1966
- "npm",
1967
- ["install", "-g", "--prefix", managedPrefix, "--offline", "--no-audit", "--no-fund", ...tgzPaths],
2101
+ npmViaDesktop ? desktopNode : "npm",
2102
+ [
2103
+ ...(npmViaDesktop ? [bundledNpmCli] : []),
2104
+ "install",
2105
+ "-g",
2106
+ "--prefix",
2107
+ managedPrefix,
2108
+ "--offline",
2109
+ "--no-audit",
2110
+ "--no-fund",
2111
+ ...tgzPaths,
2112
+ ],
1968
2113
  { stdio: "inherit", env: installEnv },
1969
2114
  );
1970
2115
  claudeInstalled = detectInstalledClients().claude_code.path || "";
@@ -1977,8 +2122,15 @@ function installClaudeCodeCli(platform) {
1977
2122
  const tgzPath = path.join(bundledClaudeDir, wrapper);
1978
2123
  log(" Installing claude-code from bundled wrapper only (legacy bundle, may need network for native pack)...");
1979
2124
  spawnSync(
1980
- "npm",
1981
- ["install", "-g", "--prefix", managedPrefix, tgzPath],
2125
+ npmViaDesktop ? desktopNode : "npm",
2126
+ [
2127
+ ...(npmViaDesktop ? [bundledNpmCli] : []),
2128
+ "install",
2129
+ "-g",
2130
+ "--prefix",
2131
+ managedPrefix,
2132
+ tgzPath,
2133
+ ],
1982
2134
  { stdio: "inherit", env: installEnv },
1983
2135
  );
1984
2136
  claudeInstalled = detectInstalledClients().claude_code.path || "";
@@ -2284,7 +2436,7 @@ async function maybeConfigurePublicContribution(schedule, useDefaults) {
2284
2436
  * Resolve the venv python path for an existing NEXO_HOME installation.
2285
2437
  */
2286
2438
  function findVenvPython(nexoHome) {
2287
- const venvPy = path.join(nexoHome, ".venv", "bin", "python3");
2439
+ const venvPy = managedVenvPythonPath(nexoHome);
2288
2440
  if (fs.existsSync(venvPy)) return venvPy;
2289
2441
  return null;
2290
2442
  }
@@ -3081,7 +3233,7 @@ async function runSetup() {
3081
3233
  }
3082
3234
 
3083
3235
  // Find or install Python (platform-aware)
3084
- let python = run("which python3");
3236
+ let python = resolveInstallerPython();
3085
3237
  if (!python) {
3086
3238
  if (platform === "darwin") {
3087
3239
  // v0.32.5 — Mac vanilla NO trae python3. La auto-instalación de
@@ -3122,7 +3274,7 @@ async function runSetup() {
3122
3274
  // fallan al import. Pinning a `python@3.12` evita el drift.
3123
3275
  log("Python 3.12 not found. Installing via Homebrew...");
3124
3276
  spawnSync("brew", ["install", "python@3.12"], { stdio: "inherit" });
3125
- python = run("which python3.12") || run("which python3");
3277
+ python = resolveInstallerPython() || run("which python3.12") || run("which python3");
3126
3278
  }
3127
3279
  } else if (platform === "linux") {
3128
3280
  // Linux: try apt or yum
@@ -3132,7 +3284,7 @@ async function runSetup() {
3132
3284
  } else if (run("which yum")) {
3133
3285
  spawnSync("sudo", ["yum", "install", "-y", "python3", "python3-pip"], { stdio: "inherit" });
3134
3286
  }
3135
- python = run("which python3");
3287
+ python = resolveInstallerPython();
3136
3288
  }
3137
3289
  if (!python) {
3138
3290
  log("Python 3 not found and couldn't install automatically.");
@@ -3140,7 +3292,13 @@ async function runSetup() {
3140
3292
  process.exit(1);
3141
3293
  }
3142
3294
  }
3143
- const pyVersion = run(`${python} --version`);
3295
+ const pyVersion = pythonVersion(python);
3296
+ if (!pyVersion || !pythonVersionMeetsMinimum(pyVersion)) {
3297
+ log(pyVersion
3298
+ ? `Python at ${python} is ${pyVersion}; NEXO Brain requires Python >=${MIN_INSTALLER_PYTHON_MAJOR}.${MIN_INSTALLER_PYTHON_MINOR}.`
3299
+ : `Python at ${python || "(not found)"} is not executable.`);
3300
+ process.exit(1);
3301
+ }
3144
3302
  log(`Found ${pyVersion} at ${python}`);
3145
3303
  logMacPermissionsNotice(NEXO_HOME, python);
3146
3304
 
@@ -3582,11 +3740,11 @@ async function runSetup() {
3582
3740
  log("Installing cognitive engine dependencies...");
3583
3741
  fs.mkdirSync(NEXO_HOME, { recursive: true });
3584
3742
  const venvPath = path.join(NEXO_HOME, ".venv");
3585
- const venvPython = platform === "win32"
3586
- ? path.join(venvPath, "Scripts", "python.exe")
3587
- : path.join(venvPath, "bin", "python3");
3743
+ const venvPython = managedVenvPythonPath(NEXO_HOME);
3588
3744
  const bundledWheelsDir = path.join(__dirname, "..", "python-wheels");
3589
3745
 
3746
+ ensureManagedVenvCompatible(venvPath, venvPython);
3747
+
3590
3748
  // Create venv if it doesn't exist
3591
3749
  if (!fs.existsSync(venvPython)) {
3592
3750
  log(" Creating Python virtual environment...");
@@ -3604,6 +3762,13 @@ async function runSetup() {
3604
3762
  }
3605
3763
  }
3606
3764
  }
3765
+ if (fs.existsSync(venvPython)) {
3766
+ const venvVersion = pythonVersion(venvPython);
3767
+ if (!venvVersion || !pythonVersionMeetsMinimum(venvVersion)) {
3768
+ log(`Python virtual environment is unsupported after creation (${venvVersion || "unknown version"}).`);
3769
+ process.exit(1);
3770
+ }
3771
+ }
3607
3772
  if (fs.existsSync(venvPython) && !pythonHasPip(venvPython)) {
3608
3773
  seedPipFromBundledWheels(venvPython, bundledWheelsDir);
3609
3774
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.13.7",
3
+ "version": "7.13.9",
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",