nexo-brain 5.2.1 → 5.3.0

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": "5.2.1",
3
+ "version": "5.3.0",
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
@@ -87,7 +87,9 @@ Versions `3.1.7` through `3.2.0` close the recent-memory gap:
87
87
  - when even that misses, NEXO now exposes raw transcript fallback tools for Claude Code and Codex session stores
88
88
  - NEXO can now inspect itself through a live system catalog derived from canonical sources instead of relying only on stale docs or operator memory
89
89
 
90
- Version `5.2.0` closes two focused gaps in the Cortex layer that were left open by the v5.1 audit the high-stakes response-contract detector was English-only, and the `nexo-cortex-cycle` cron was writing a quality snapshot that no reader ever consumed:
90
+ Version `5.3.0` adds `nexo uninstall` a CLI command that cleanly separates runtime from user data. It stops all crons, removes the MCP server config, and preserves databases, learnings, and personal scripts for safe reinstall.
91
+
92
+ Version `5.2.1` closes two focused gaps in the Cortex layer that were left open by the v5.1 audit — the high-stakes response-contract detector was English-only, and the `nexo-cortex-cycle` cron was writing a quality snapshot that no reader ever consumed:
91
93
 
92
94
  - `HIGH_STAKES_KEYWORDS_ES` adds ~45 Spanish keywords to the high-stakes detector with accented and unaccented variants, so a goal written in Spanish (`migrar la base de datos de producción`) trips the same gate as its English twin.
93
95
  - `NEGATION_PATTERNS` suppresses false positives when the user explicitly disclaims touching the sensitive area (`sin afectar producción`, `no tocar prod`, `without touching production`, `don't modify`). The raw keyword being present is no longer enough to flag the task.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexo-brain",
3
- "version": "5.2.1",
3
+ "version": "5.3.0",
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",
package/src/cli.py CHANGED
@@ -25,6 +25,7 @@ Entry points:
25
25
  nexo clients sync [--json]
26
26
  nexo contributor status|on|off [--json]
27
27
  nexo doctor [--tier boot|runtime|deep|all] [--json] [--fix]
28
+ nexo uninstall [--dry-run] [--delete-data] [--json]
28
29
  """
29
30
  from __future__ import annotations
30
31
 
@@ -1281,6 +1282,218 @@ def _skills_compose(args):
1281
1282
  return 0 if result.get("ok") else 1
1282
1283
 
1283
1284
 
1285
+ def _uninstall(args):
1286
+ """Stop all crons, remove MCP config and hooks, preserve user data."""
1287
+ from pathlib import Path
1288
+
1289
+ nexo_home = Path(os.environ.get("NEXO_HOME", Path.home() / ".nexo"))
1290
+ dry_run = args.dry_run
1291
+ delete_data = args.delete_data
1292
+ use_json = args.json
1293
+ platform = sys.platform
1294
+
1295
+ actions: list[dict] = []
1296
+ errors: list[str] = []
1297
+
1298
+ def log_action(category: str, detail: str, path: str = ""):
1299
+ actions.append({"category": category, "detail": detail, "path": path})
1300
+ if not use_json:
1301
+ tag = "[DRY-RUN] " if dry_run else ""
1302
+ print(f" {tag}{category}: {detail}")
1303
+
1304
+ # ── 1. Stop and remove LaunchAgents (macOS) ──
1305
+ if platform == "darwin":
1306
+ la_dir = Path.home() / "Library" / "LaunchAgents"
1307
+ if la_dir.exists():
1308
+ uid = os.getuid()
1309
+ for plist in sorted(la_dir.glob("com.nexo.*.plist")):
1310
+ label = plist.stem
1311
+ # Stop the agent
1312
+ if not dry_run:
1313
+ subprocess.run(
1314
+ ["launchctl", "bootout", f"gui/{uid}", str(plist)],
1315
+ capture_output=True,
1316
+ )
1317
+ log_action("stop-cron", f"launchctl bootout {label}", str(plist))
1318
+ # Remove plist file
1319
+ if not dry_run:
1320
+ plist.unlink(missing_ok=True)
1321
+ log_action("remove-plist", label, str(plist))
1322
+ # ── systemd (Linux) ──
1323
+ elif platform == "linux":
1324
+ systemd_dir = Path.home() / ".config" / "systemd" / "user"
1325
+ if systemd_dir.exists():
1326
+ for unit in sorted(list(systemd_dir.glob("nexo-*.timer")) + list(systemd_dir.glob("nexo-*.service"))):
1327
+ if not dry_run:
1328
+ if unit.suffix == ".timer":
1329
+ subprocess.run(["systemctl", "--user", "stop", unit.name], capture_output=True)
1330
+ subprocess.run(["systemctl", "--user", "disable", unit.name], capture_output=True)
1331
+ unit.unlink(missing_ok=True)
1332
+ log_action("remove-systemd", unit.name, str(unit))
1333
+ if not dry_run:
1334
+ subprocess.run(["systemctl", "--user", "daemon-reload"], capture_output=True)
1335
+
1336
+ # ── 2. Remove MCP server and hooks from Claude Code settings ──
1337
+ claude_settings = Path.home() / ".claude" / "settings.json"
1338
+ if claude_settings.exists():
1339
+ try:
1340
+ settings = json.loads(claude_settings.read_text())
1341
+ changed = False
1342
+
1343
+ # Remove nexo MCP server
1344
+ mcp = settings.get("mcpServers", {})
1345
+ if "nexo" in mcp:
1346
+ if not dry_run:
1347
+ del mcp["nexo"]
1348
+ changed = True
1349
+ log_action("remove-mcp", "nexo server from settings.json", str(claude_settings))
1350
+
1351
+ # Remove NEXO hooks (hooks referencing NEXO_HOME)
1352
+ hooks = settings.get("hooks", {})
1353
+ nexo_home_str = str(nexo_home)
1354
+ for event_name in list(hooks.keys()):
1355
+ hook_list = hooks[event_name]
1356
+ if isinstance(hook_list, list):
1357
+ original_len = len(hook_list)
1358
+ filtered = [
1359
+ h for h in hook_list
1360
+ if not (
1361
+ isinstance(h, dict)
1362
+ and nexo_home_str in (h.get("command", "") + " ".join(h.get("args", [])))
1363
+ )
1364
+ ]
1365
+ if len(filtered) < original_len:
1366
+ if not dry_run:
1367
+ hooks[event_name] = filtered
1368
+ changed = True
1369
+ removed_count = original_len - len(filtered)
1370
+ log_action("remove-hooks", f"{removed_count} hook(s) from {event_name}", str(claude_settings))
1371
+ # Clean up empty hook lists
1372
+ if isinstance(hooks.get(event_name), list) and len(hooks.get(event_name, [])) == 0:
1373
+ if not dry_run:
1374
+ del hooks[event_name]
1375
+ changed = True
1376
+
1377
+ if changed and not dry_run:
1378
+ claude_settings.write_text(json.dumps(settings, indent=2, ensure_ascii=False))
1379
+ except Exception as exc:
1380
+ errors.append(f"Failed to clean settings.json: {exc}")
1381
+
1382
+ # ── 3. Remove Codex AGENTS.md bootstrap ──
1383
+ codex_agents = Path.home() / "AGENTS.md"
1384
+ if codex_agents.exists():
1385
+ try:
1386
+ content = codex_agents.read_text()
1387
+ if "NEXO" in content or "nexo" in content:
1388
+ log_action("preserve-note", "AGENTS.md contains NEXO references — remove manually if desired", str(codex_agents))
1389
+ except Exception:
1390
+ pass
1391
+
1392
+ # ── 4. Remove runtime files, PRESERVE user data ──
1393
+ # User data directories that are NEVER deleted (unless --delete-data)
1394
+ user_data_dirs = {"data", "brain", "operations", "coordination", "config", "logs", "backups"}
1395
+ user_data_files = {"version.json"} # keeps reinstall detection working
1396
+
1397
+ # Runtime directories that get removed
1398
+ runtime_dirs = {"plugins", "hooks", "dashboard", "cognitive", "db", "rules", "crons", "doctor", "skills"}
1399
+ # Runtime flat files: any .py/.txt at NEXO_HOME root is core runtime.
1400
+ # User data always lives in subdirectories (data/, brain/, scripts/, etc.)
1401
+ # so this is safe and doesn't need updating when new core files are added.
1402
+ runtime_file_extensions = {".py", ".txt"}
1403
+
1404
+ if nexo_home.exists():
1405
+ # Remove runtime directories
1406
+ for d in sorted(runtime_dirs):
1407
+ dir_path = nexo_home / d
1408
+ if dir_path.is_dir():
1409
+ if not dry_run:
1410
+ shutil.rmtree(dir_path, ignore_errors=True)
1411
+ log_action("remove-runtime-dir", d, str(dir_path))
1412
+
1413
+ # Remove runtime flat files (any .py/.txt at root level)
1414
+ for file_path in sorted(nexo_home.iterdir()):
1415
+ if file_path.is_file() and file_path.suffix in runtime_file_extensions:
1416
+ if not dry_run:
1417
+ file_path.unlink(missing_ok=True)
1418
+ log_action("remove-runtime-file", file_path.name, str(file_path))
1419
+
1420
+ # Remove core scripts (nexo-*.py/sh in scripts/)
1421
+ scripts_dir = nexo_home / "scripts"
1422
+ if scripts_dir.is_dir():
1423
+ for script in sorted(scripts_dir.glob("nexo-*")):
1424
+ if script.is_file():
1425
+ if not dry_run:
1426
+ script.unlink(missing_ok=True)
1427
+ log_action("remove-core-script", script.name, str(script))
1428
+ # Remove deep-sleep directory (core)
1429
+ ds_dir = scripts_dir / "deep-sleep"
1430
+ if ds_dir.is_dir():
1431
+ if not dry_run:
1432
+ shutil.rmtree(ds_dir, ignore_errors=True)
1433
+ log_action("remove-runtime-dir", "scripts/deep-sleep", str(ds_dir))
1434
+
1435
+ # List preserved user data
1436
+ for d in sorted(user_data_dirs):
1437
+ dir_path = nexo_home / d
1438
+ if dir_path.is_dir():
1439
+ if delete_data:
1440
+ if not dry_run:
1441
+ shutil.rmtree(dir_path, ignore_errors=True)
1442
+ log_action("DELETE-user-data", d, str(dir_path))
1443
+ else:
1444
+ log_action("preserve-data", d, str(dir_path))
1445
+
1446
+ # Preserve personal scripts (non nexo-* files in scripts/)
1447
+ if scripts_dir.is_dir():
1448
+ personal = [f.name for f in scripts_dir.iterdir() if f.is_file() and not f.name.startswith("nexo-")]
1449
+ if personal:
1450
+ log_action("preserve-scripts", f"{len(personal)} personal script(s)", str(scripts_dir))
1451
+
1452
+ # Preserve templates/
1453
+ templates_dir = nexo_home / "templates"
1454
+ if templates_dir.is_dir():
1455
+ log_action("preserve-data", "templates", str(templates_dir))
1456
+
1457
+ # ── 5. Write uninstall marker for reinstall detection ──
1458
+ if not dry_run and nexo_home.exists():
1459
+ marker = nexo_home / ".uninstalled"
1460
+ marker.write_text(json.dumps({
1461
+ "uninstalled_at": __import__("datetime").datetime.now().isoformat(),
1462
+ "nexo_home": str(nexo_home),
1463
+ "data_preserved": not delete_data,
1464
+ }, indent=2))
1465
+ log_action("write-marker", ".uninstalled marker for reinstall detection", str(marker))
1466
+
1467
+ # ── Summary ──
1468
+ result = {
1469
+ "ok": len(errors) == 0,
1470
+ "dry_run": dry_run,
1471
+ "nexo_home": str(nexo_home),
1472
+ "actions": actions,
1473
+ "errors": errors,
1474
+ "data_preserved": not delete_data,
1475
+ }
1476
+
1477
+ if use_json:
1478
+ print(json.dumps(result, indent=2, ensure_ascii=False))
1479
+ else:
1480
+ print()
1481
+ if dry_run:
1482
+ print(f" DRY RUN complete. {len(actions)} action(s) would be taken.")
1483
+ print(" Run without --dry-run to execute.")
1484
+ else:
1485
+ print(f" Uninstall complete. {len(actions)} action(s) taken.")
1486
+ if not delete_data:
1487
+ print(f"\n Your data is preserved in: {nexo_home}")
1488
+ print(" To reinstall: npm install -g nexo-brain && nexo-brain")
1489
+ if errors:
1490
+ print(f"\n {len(errors)} error(s):")
1491
+ for e in errors:
1492
+ print(f" - {e}")
1493
+
1494
+ return 1 if errors else 0
1495
+
1496
+
1284
1497
  def _print_help():
1285
1498
  v = _get_version()
1286
1499
  print(f"""NEXO Runtime CLI v{v}
@@ -1293,6 +1506,7 @@ Commands:
1293
1506
  nexo skills list|apply|sync|approve Executable skills
1294
1507
  nexo clients sync Sync Claude/Codex shared-brain configs and bootstrap files
1295
1508
  nexo update Update installed runtime
1509
+ nexo uninstall [--dry-run] [--delete-data] Stop crons, remove runtime (keeps data)
1296
1510
  nexo contributor status|on|off Public Draft PR contribution mode
1297
1511
  nexo dashboard on|off|status Web dashboard control
1298
1512
 
@@ -1477,6 +1691,12 @@ def main():
1477
1691
  skills_compose_p.add_argument("--trigger-patterns", default="[]", help="JSON array or comma-separated trigger patterns")
1478
1692
  skills_compose_p.add_argument("--json", action="store_true", help="JSON output")
1479
1693
 
1694
+ # -- uninstall --
1695
+ uninstall_parser = sub.add_parser("uninstall", help="Stop all crons, remove runtime, keep user data")
1696
+ uninstall_parser.add_argument("--dry-run", action="store_true", help="Show what would be done without doing it")
1697
+ uninstall_parser.add_argument("--delete-data", action="store_true", help="Also delete databases and user data (DESTRUCTIVE)")
1698
+ uninstall_parser.add_argument("--json", action="store_true", help="JSON output")
1699
+
1480
1700
  # -- dashboard --
1481
1701
  dashboard_parser = sub.add_parser("dashboard", help="Web dashboard control")
1482
1702
  dashboard_parser.add_argument("action", choices=["on", "off", "status"], help="Start, stop, or check dashboard")
@@ -1566,6 +1786,8 @@ def main():
1566
1786
  else:
1567
1787
  skills_parser.print_help()
1568
1788
  return 0
1789
+ elif args.command == "uninstall":
1790
+ return _uninstall(args)
1569
1791
  elif args.command == "dashboard":
1570
1792
  return _dashboard(args)
1571
1793
  else: