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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +5 -1
- package/bin/nexo-brain.js +185 -20
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.13.
|
|
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.
|
|
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 =
|
|
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
|
-
[
|
|
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
|
-
[
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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.
|
|
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",
|