nexo-brain 7.12.3 → 7.12.6
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/bin/nexo-brain.js +111 -14
- package/package.json +2 -2
- package/src/events_bus.py +18 -13
- package/src/runtime_versioning.py +11 -0
- package/src/scripts/nexo-email-monitor.py +26 -2
- package/src/scripts/nexo-immune.py +16 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.12.
|
|
3
|
+
"version": "7.12.4",
|
|
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/bin/nexo-brain.js
CHANGED
|
@@ -1876,15 +1876,55 @@ function installClaudeCodeCli(platform) {
|
|
|
1876
1876
|
const managedPrefix = managedClaudePrefix();
|
|
1877
1877
|
const desktopManaged = isDesktopManagedInstall();
|
|
1878
1878
|
|
|
1879
|
-
// OFFLINE-FIRST:
|
|
1880
|
-
// Path: resources/brain-bundle/claude-code
|
|
1881
|
-
//
|
|
1879
|
+
// OFFLINE-FIRST v0.32.4: install claude-code wrapper + ALL its native packs
|
|
1880
|
+
// from bundled tarballs. Path: resources/brain-bundle/claude-code/*.tgz.
|
|
1881
|
+
//
|
|
1882
|
+
// Bug que arregla este fix (encontrado 2026-05-02):
|
|
1883
|
+
// claude-code 2.1.x ships como wrapper + 4 native packs por arquitectura
|
|
1884
|
+
// (@anthropic-ai/claude-code-linux-x64, -darwin-arm64, -darwin-x64,
|
|
1885
|
+
// -linux-arm64). Antes solo bundeaba el wrapper (.tgz 13 KB) y pasaba SOLO
|
|
1886
|
+
// ese al npm install. npm intentaba resolver los `optionalDependencies` del
|
|
1887
|
+
// registry online → fallo offline → wrapper instala SIN binario claude →
|
|
1888
|
+
// `command -v claude` exit 127 → bootstrap "claude-runtime-missing-soft".
|
|
1889
|
+
// Ahora bundeamos los 5 .tgz (wrapper + 4 native) y se los pasamos TODOS
|
|
1890
|
+
// a npm en un solo install. npm los ve como dependencias pre-resueltas
|
|
1891
|
+
// y skip el registry lookup. claude binary ejecutable resultante.
|
|
1882
1892
|
const bundledClaudeDir = path.join(__dirname, "..", "claude-code");
|
|
1883
1893
|
if (fs.existsSync(bundledClaudeDir)) {
|
|
1884
|
-
const
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1894
|
+
const allTgz = fs.readdirSync(bundledClaudeDir).filter((f) => f.endsWith(".tgz"));
|
|
1895
|
+
// Ordenar: el wrapper primero (sin sufijo de plataforma), después los packs.
|
|
1896
|
+
const wrapper = allTgz.find((f) => /^anthropic-ai-claude-code-\d/.test(f));
|
|
1897
|
+
// v7.12.6 — Filter native packs to ONLY the current platform/arch.
|
|
1898
|
+
// Bug: passing all 4 native packs to `npm install` triggers EBADPLATFORM
|
|
1899
|
+
// when npm refuses to install e.g. `claude-code-darwin-arm64` on linux/x64
|
|
1900
|
+
// (`{"os":"darwin","cpu":"arm64"}` vs current `{"os":"linux","cpu":"x64"}`).
|
|
1901
|
+
// The whole install aborts → `command -v claude` fails → bootstrap stalls
|
|
1902
|
+
// at "Preparando NEXO..." and never reaches Brain config. First seen
|
|
1903
|
+
// 2026-05-03 on Inma's Win11 PC running the Linux x64 WSL distro.
|
|
1904
|
+
const platformSlug = `${process.platform}-${process.arch}`;
|
|
1905
|
+
const nativePacks = allTgz.filter((f) => {
|
|
1906
|
+
if (f === wrapper) return false;
|
|
1907
|
+
// Native pack filenames look like `anthropic-ai-claude-code-<os>-<cpu>-<version>.tgz`.
|
|
1908
|
+
// Match the `-<os>-<cpu>-` segment against the runtime platform slug.
|
|
1909
|
+
return f.includes(`-${platformSlug}-`);
|
|
1910
|
+
});
|
|
1911
|
+
if (wrapper && nativePacks.length > 0) {
|
|
1912
|
+
const tgzPaths = [path.join(bundledClaudeDir, wrapper), ...nativePacks.map((p) => path.join(bundledClaudeDir, p))];
|
|
1913
|
+
log(" Installing claude-code from bundled tarballs (offline, " + (1 + nativePacks.length) + " packs)...");
|
|
1914
|
+
spawnSync(
|
|
1915
|
+
"npm",
|
|
1916
|
+
["install", "-g", "--prefix", managedPrefix, "--offline", "--no-audit", "--no-fund", ...tgzPaths],
|
|
1917
|
+
{ stdio: "inherit", env: installEnv },
|
|
1918
|
+
);
|
|
1919
|
+
claudeInstalled = detectInstalledClients().claude_code.path || "";
|
|
1920
|
+
if (claudeInstalled) {
|
|
1921
|
+
persistClaudeCliPath(claudeInstalled);
|
|
1922
|
+
return { installed: true, path: claudeInstalled };
|
|
1923
|
+
}
|
|
1924
|
+
} else if (wrapper) {
|
|
1925
|
+
// Fallback: solo wrapper (legacy bundle 0.32.3 y anteriores).
|
|
1926
|
+
const tgzPath = path.join(bundledClaudeDir, wrapper);
|
|
1927
|
+
log(" Installing claude-code from bundled wrapper only (legacy bundle, may need network for native pack)...");
|
|
1888
1928
|
spawnSync(
|
|
1889
1929
|
"npm",
|
|
1890
1930
|
["install", "-g", "--prefix", managedPrefix, tgzPath],
|
|
@@ -2013,8 +2053,18 @@ async function configureClientSetup({ lang, useDefaults, autoInstall, detected }
|
|
|
2013
2053
|
for (const client of required) {
|
|
2014
2054
|
if (detected[client] && detected[client].installed) continue;
|
|
2015
2055
|
if (desktopManaged && client === "claude_code") {
|
|
2016
|
-
|
|
2017
|
-
|
|
2056
|
+
const bundledClaudeDir = path.join(__dirname, "..", "claude-code");
|
|
2057
|
+
let hasBundle = false;
|
|
2058
|
+
try {
|
|
2059
|
+
if (fs.existsSync(bundledClaudeDir)) {
|
|
2060
|
+
hasBundle = fs.readdirSync(bundledClaudeDir).some((f) => f.endsWith(".tgz"));
|
|
2061
|
+
}
|
|
2062
|
+
} catch (_) {}
|
|
2063
|
+
if (!hasBundle) {
|
|
2064
|
+
log("Claude Code install deferred to Desktop final sync.");
|
|
2065
|
+
continue;
|
|
2066
|
+
}
|
|
2067
|
+
log("Bundled Claude Code tarball detected — installing offline now.");
|
|
2018
2068
|
}
|
|
2019
2069
|
let shouldInstall = useDefaults || autoInstall === "auto";
|
|
2020
2070
|
if (!shouldInstall && process.stdin.isTTY && process.stdout.isTTY) {
|
|
@@ -2956,6 +3006,22 @@ async function runSetup() {
|
|
|
2956
3006
|
logMacPermissionsNotice(NEXO_HOME, syncPython);
|
|
2957
3007
|
|
|
2958
3008
|
log(`Already at v${currentVersion}. No migration needed.`);
|
|
3009
|
+
|
|
3010
|
+
// Ensure bundled Claude Code is installed even when migration is skipped.
|
|
3011
|
+
try {
|
|
3012
|
+
const _claudeCheck = detectInstalledClients().claude_code;
|
|
3013
|
+
if (!_claudeCheck.installed) {
|
|
3014
|
+
const _bundledClaudeDir = path.join(__dirname, "..", "claude-code");
|
|
3015
|
+
if (fs.existsSync(_bundledClaudeDir)) {
|
|
3016
|
+
const _tgzFiles = fs.readdirSync(_bundledClaudeDir).filter((f) => f.endsWith(".tgz"));
|
|
3017
|
+
if (_tgzFiles.length > 0) {
|
|
3018
|
+
log("Bundled Claude Code tarball detected after migration-skip — installing offline.");
|
|
3019
|
+
installClaudeCodeCli(process.platform);
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
} catch (_e) {}
|
|
3024
|
+
|
|
2959
3025
|
closeReadline();
|
|
2960
3026
|
return;
|
|
2961
3027
|
} catch (e) {
|
|
@@ -2967,8 +3033,29 @@ async function runSetup() {
|
|
|
2967
3033
|
let python = run("which python3");
|
|
2968
3034
|
if (!python) {
|
|
2969
3035
|
if (platform === "darwin") {
|
|
2970
|
-
//
|
|
3036
|
+
// v0.32.5 — Mac vanilla NO trae python3. La auto-instalación de
|
|
3037
|
+
// Homebrew vía `curl install.sh` requiere TTY interactivo + sudo +
|
|
3038
|
+
// user accept license. Cuando este script se invoca desde Electron
|
|
3039
|
+
// sandbox, NO hay TTY → curl pipe cuelga sin progreso → bootstrap
|
|
3040
|
+
// se queda silencioso. Mejor: detectar la ausencia de Python y
|
|
3041
|
+
// surface un error CLARO al user con instrucciones manuales que
|
|
3042
|
+
// funcionan siempre. Si hay TTY (corriendo desde terminal), seguimos
|
|
3043
|
+
// con el path automático.
|
|
3044
|
+
const isTty = !!(process.stdin && process.stdin.isTTY);
|
|
2971
3045
|
let hasBrew = run("which brew");
|
|
3046
|
+
if (!hasBrew && !isTty) {
|
|
3047
|
+
log("ERROR: Python 3.12 is not installed and the auto-installer requires an interactive terminal.");
|
|
3048
|
+
log("");
|
|
3049
|
+
log("Please install Python 3.12 manually:");
|
|
3050
|
+
log(" 1. Open Terminal.app");
|
|
3051
|
+
log(" 2. Run: xcode-select --install");
|
|
3052
|
+
log(" 3. Run: /bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"");
|
|
3053
|
+
log(" 4. Run: brew install python@3.12");
|
|
3054
|
+
log(" 5. Reopen NEXO Desktop");
|
|
3055
|
+
log("");
|
|
3056
|
+
log("More help: https://nexo-desktop.com/help/python-install");
|
|
3057
|
+
process.exit(1);
|
|
3058
|
+
}
|
|
2972
3059
|
if (!hasBrew) {
|
|
2973
3060
|
log("Homebrew not found. Installing...");
|
|
2974
3061
|
spawnSync("/bin/bash", ["-c", '$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)'], {
|
|
@@ -2977,9 +3064,14 @@ async function runSetup() {
|
|
|
2977
3064
|
hasBrew = run("which brew") || run("eval $(/opt/homebrew/bin/brew shellenv) && which brew");
|
|
2978
3065
|
}
|
|
2979
3066
|
if (hasBrew) {
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
3067
|
+
// v0.32.5 — explicit @3.12 pin: el Brain `requirements.txt` y los
|
|
3068
|
+
// wheels manylinux están compilados contra cp312. `brew install
|
|
3069
|
+
// python3` instala el `python3` formula que actualmente apunta a
|
|
3070
|
+
// 3.13 → wheels rechazados → numpy/cffi/cryptography/onnxruntime
|
|
3071
|
+
// fallan al import. Pinning a `python@3.12` evita el drift.
|
|
3072
|
+
log("Python 3.12 not found. Installing via Homebrew...");
|
|
3073
|
+
spawnSync("brew", ["install", "python@3.12"], { stdio: "inherit" });
|
|
3074
|
+
python = run("which python3.12") || run("which python3");
|
|
2983
3075
|
}
|
|
2984
3076
|
} else if (platform === "linux") {
|
|
2985
3077
|
// Linux: try apt or yum
|
|
@@ -3451,7 +3543,12 @@ async function runSetup() {
|
|
|
3451
3543
|
// present, pip uses --no-index --find-links to install without internet.
|
|
3452
3544
|
// Falls back to PyPI if bundle not found.
|
|
3453
3545
|
const bundledWheelsDir = path.join(__dirname, "..", "python-wheels");
|
|
3454
|
-
|
|
3546
|
+
// v0.32.5 — el bundle empaca wheels manylinux (cp312 x86_64) porque
|
|
3547
|
+
// en Win Brain corre dentro de WSL Ubuntu noble. En Mac, Brain corre
|
|
3548
|
+
// nativo macOS y NO acepta esos wheels (ABI distinto). Si gateamos
|
|
3549
|
+
// useBundle a !linux, pip cae al PyPI online — bien. macOS y Win
|
|
3550
|
+
// (host nativo) deben tener red la primera vez.
|
|
3551
|
+
const useBundle = process.platform === "linux" && fs.existsSync(bundledWheelsDir);
|
|
3455
3552
|
const pipArgs = useBundle
|
|
3456
3553
|
? ["-m", "pip", "install", "--no-index", "--find-links", bundledWheelsDir, "--progress-bar", "off", "-r", requirementsFile]
|
|
3457
3554
|
: ["-m", "pip", "install", "-v", "--progress-bar", "off", "--default-timeout=60", "-r", requirementsFile];
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "7.12.
|
|
3
|
+
"version": "7.12.6",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
|
-
"description": "NEXO Brain
|
|
5
|
+
"description": "NEXO Brain \u2014 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",
|
|
7
7
|
"bin": {
|
|
8
8
|
"nexo-brain": "bin/nexo-brain.js",
|
package/src/events_bus.py
CHANGED
|
@@ -101,22 +101,27 @@ def emit(
|
|
|
101
101
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
102
102
|
_rotate_if_needed(path)
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
"source": source,
|
|
112
|
-
"extra": extra or {},
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
line = json.dumps(event, ensure_ascii=False) + "\n"
|
|
116
|
-
# fcntl flock for cross-process safety on macOS/Linux
|
|
104
|
+
# v0.32.5 — antes `_next_id(path)` se calculaba ANTES de adquirir el
|
|
105
|
+
# flock → dos emitters concurrentes leían el mismo tail, computaban
|
|
106
|
+
# el mismo id, y escribían dos eventos con el mismo id. El renderer
|
|
107
|
+
# dedup descartaba el segundo silently → eventos perdidos. Ahora
|
|
108
|
+
# `_next_id` se llama DENTRO del lock, garantizando monotonía.
|
|
109
|
+
line = None
|
|
110
|
+
event = None
|
|
117
111
|
with path.open("a", encoding="utf-8") as fh:
|
|
118
112
|
try:
|
|
119
113
|
fcntl.flock(fh, fcntl.LOCK_EX)
|
|
114
|
+
event = {
|
|
115
|
+
"id": _next_id(path),
|
|
116
|
+
"ts": time.time(),
|
|
117
|
+
"type": event_type,
|
|
118
|
+
"priority": priority,
|
|
119
|
+
"text": text,
|
|
120
|
+
"reason": reason,
|
|
121
|
+
"source": source,
|
|
122
|
+
"extra": extra or {},
|
|
123
|
+
}
|
|
124
|
+
line = json.dumps(event, ensure_ascii=False) + "\n"
|
|
120
125
|
fh.write(line)
|
|
121
126
|
fh.flush()
|
|
122
127
|
finally:
|
|
@@ -57,6 +57,17 @@ RESTART_ALLOWLIST = {
|
|
|
57
57
|
"nexo_continuity_snapshot_read",
|
|
58
58
|
"nexo_continuity_resume_bundle",
|
|
59
59
|
"nexo_continuity_audit",
|
|
60
|
+
# v0.32.5 — añadidas las read-only tools que el protocolo CORE llama
|
|
61
|
+
# justo después de `nexo_startup` (memory recall, reminders, followups,
|
|
62
|
+
# context, doctor). Antes quedaban bloqueadas por mcp_restart_required
|
|
63
|
+
# tras `nexo update` con sesión activa → Nero parecía amnésico hasta
|
|
64
|
+
# que el cliente se cerraba/reabría.
|
|
65
|
+
"nexo_smart_startup",
|
|
66
|
+
"nexo_session_diary_read",
|
|
67
|
+
"nexo_reminders",
|
|
68
|
+
"nexo_followups",
|
|
69
|
+
"nexo_recent_context",
|
|
70
|
+
"nexo_doctor",
|
|
60
71
|
}
|
|
61
72
|
|
|
62
73
|
|
|
@@ -382,8 +382,19 @@ def load_config():
|
|
|
382
382
|
return cfg
|
|
383
383
|
except Exception:
|
|
384
384
|
pass
|
|
385
|
-
|
|
386
|
-
|
|
385
|
+
# v0.32.5 — graceful return None when no email setup yet. Antes esto
|
|
386
|
+
# lanzaba FileNotFoundError y el cron de cada minuto generaba 1440
|
|
387
|
+
# filas de error/día por cliente sin email configurado. 50 clientes
|
|
388
|
+
# pagados sin email setup → 72k filas de error/día → watchdog L2
|
|
389
|
+
# alertas falsas → ruido en logs y posible token burn. Ahora el
|
|
390
|
+
# cron retorna sin error cuando no hay config.
|
|
391
|
+
try:
|
|
392
|
+
with open(CONFIG_PATH) as f:
|
|
393
|
+
payload = json.load(f)
|
|
394
|
+
except FileNotFoundError:
|
|
395
|
+
return None
|
|
396
|
+
except (OSError, json.JSONDecodeError):
|
|
397
|
+
return None
|
|
387
398
|
if isinstance(payload, dict):
|
|
388
399
|
payload.pop("automation_task_profile", None)
|
|
389
400
|
return payload
|
|
@@ -1694,6 +1705,13 @@ def _run_worker_job(job_path):
|
|
|
1694
1705
|
return 1
|
|
1695
1706
|
|
|
1696
1707
|
config = load_config()
|
|
1708
|
+
if config is None:
|
|
1709
|
+
# v0.32.5 — worker invoked but email setup gone (config deleted
|
|
1710
|
+
# between scheduling and execution). Drop the job silently
|
|
1711
|
+
# rather than spamming exception logs.
|
|
1712
|
+
log.warning(f"Worker job {job_file.name}: no email config, dropping.")
|
|
1713
|
+
job_file.unlink(missing_ok=True)
|
|
1714
|
+
return 0
|
|
1697
1715
|
|
|
1698
1716
|
log.info(
|
|
1699
1717
|
f"Worker job started: {job_file.name} "
|
|
@@ -2190,6 +2208,12 @@ def main():
|
|
|
2190
2208
|
return
|
|
2191
2209
|
|
|
2192
2210
|
config = load_config()
|
|
2211
|
+
# v0.32.5 — exit cleanly cuando no hay email setup.
|
|
2212
|
+
# Antes el FileNotFoundError propagaba a 1440 errores/día por
|
|
2213
|
+
# cliente Win sin email. Ahora salimos en silencio.
|
|
2214
|
+
if config is None:
|
|
2215
|
+
log.info("No email config — skipping monitor check.")
|
|
2216
|
+
return
|
|
2193
2217
|
base_interval_seconds = max(60, _safe_int(config.get("check_interval_seconds"), 300))
|
|
2194
2218
|
backoff_state = load_empty_inbox_backoff_state()
|
|
2195
2219
|
debt_block = scan_debt()
|
|
@@ -323,6 +323,22 @@ def check_tokens():
|
|
|
323
323
|
|
|
324
324
|
def check_launch_agents():
|
|
325
325
|
"""Check that expected LaunchAgents are loaded. Auto-repair if not."""
|
|
326
|
+
# v0.32.5 — antes esto siempre invocaba `launchctl list` (Mac-only).
|
|
327
|
+
# Cuando el cron immune corría dentro de WSL Ubuntu (clientes Win),
|
|
328
|
+
# `launchctl` no existe → rc != 0 → loaded_labels vacío → cada agente
|
|
329
|
+
# marcado missing → cada immune tick disparaba watchdog L2 con LLM
|
|
330
|
+
# repair loop → quemaba tokens. Ahora detectamos plataforma y skip
|
|
331
|
+
# graceful en Linux/Win-WSL: no hay LaunchAgents en esas plataformas;
|
|
332
|
+
# los crons se materializan con cron / systemd y los chequea otra
|
|
333
|
+
# función separada.
|
|
334
|
+
import platform as _platform
|
|
335
|
+
if _platform.system() != "Darwin":
|
|
336
|
+
return [{
|
|
337
|
+
"name": "launchd",
|
|
338
|
+
"status": "OK",
|
|
339
|
+
"detail": "skipped: launchd is macOS-only",
|
|
340
|
+
"repaired": False,
|
|
341
|
+
}]
|
|
326
342
|
results = []
|
|
327
343
|
|
|
328
344
|
# Get list of loaded agents
|