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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "7.12.3",
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: detect bundled claude-code .tgz and install from it.
1880
- // Path: resources/brain-bundle/claude-code/claude-code-X.Y.Z.tgz (relative
1881
- // to bin/, so __dirname/../claude-code/). Falls back to PyPI/npm if absent.
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 tgzFiles = fs.readdirSync(bundledClaudeDir).filter((f) => f.endsWith(".tgz"));
1885
- if (tgzFiles.length > 0) {
1886
- const tgzPath = path.join(bundledClaudeDir, tgzFiles[0]);
1887
- log(" Installing claude-code from bundled tarball (offline)...");
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
- log("Claude Code install deferred to Desktop final sync.");
2017
- continue;
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
- // macOS: use Homebrew
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
- log("Python 3 not found. Installing via Homebrew...");
2981
- spawnSync("brew", ["install", "python3"], { stdio: "inherit" });
2982
- python = run("which python3");
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
- const useBundle = fs.existsSync(bundledWheelsDir);
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",
3
+ "version": "7.12.6",
4
4
  "mcpName": "io.github.wazionapps/nexo",
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.",
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
- event = {
105
- "id": _next_id(path),
106
- "ts": time.time(),
107
- "type": event_type,
108
- "priority": priority,
109
- "text": text,
110
- "reason": reason,
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
- with open(CONFIG_PATH) as f:
386
- payload = json.load(f)
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