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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +3 -1
- package/package.json +1 -1
- package/src/cli.py +222 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "5.
|
|
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.
|
|
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.
|
|
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:
|