nexo-brain 6.4.0 → 7.0.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 +8 -1
- package/package.json +2 -2
- package/src/auto_update.py +271 -30
- package/src/bootstrap_docs.py +2 -1
- package/src/calibration_migration.py +5 -2
- package/src/cli.py +69 -6
- package/src/cron_recovery.py +4 -3
- package/src/db/_personal_scripts.py +3 -1
- package/src/db/_skills.py +2 -1
- package/src/desktop_bridge.py +4 -2
- package/src/doctor/providers/boot.py +21 -7
- package/src/doctor/providers/deep.py +6 -5
- package/src/doctor/providers/runtime.py +17 -16
- package/src/evolution_cycle.py +7 -6
- package/src/health_check.py +4 -2
- package/src/paths.py +394 -0
- package/src/plugins/personal_plugins.py +2 -1
- package/src/plugins/recover.py +3 -2
- package/src/plugins/update.py +10 -9
- package/src/public_contribution.py +2 -1
- package/src/runtime_power.py +6 -5
- package/src/script_registry.py +154 -20
- package/src/scripts/nexo-backup.sh +2 -2
- package/src/scripts/nexo-cron-wrapper.sh +40 -2
- package/src/scripts/nexo-deep-sleep.sh +2 -2
- package/src/scripts/nexo-inbox-hook.sh +1 -1
- package/src/scripts/nexo-snapshot-restore.sh +1 -1
- package/src/scripts/nexo-tcc-approve.sh +2 -2
- package/src/scripts/nexo-watchdog.sh +14 -14
- package/src/system_catalog.py +3 -2
- package/src/tools_sessions.py +2 -1
- package/src/user_data_portability.py +9 -8
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.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
|
@@ -18,7 +18,14 @@
|
|
|
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 `
|
|
21
|
+
Version `7.0.0` is the current packaged-runtime line — **BREAKING — Plan Consolidado fase F0.6**: physical separation of the runtime tree into `~/.nexo/{core,personal,runtime}/`. The flat layout (`~/.nexo/scripts/`, `brain/`, `data/`, `operations/`, ...) is gone. Operators on v6.x are auto-migrated on first `nexo update`; fresh installs land directly in the new tree. New `paths.py` helpers are transition-aware. The companion NEXO Desktop client (v0.21.0, closed-source distributed separately) updates its hardcoded paths so auto-update keeps working.
|
|
22
|
+
|
|
23
|
+
Previously in `6.5.0`: Plan Consolidado fase F0.2: operators can now `nexo scripts enable|disable|status <name>` any personal automation. The cron wrapper honours the flag at every tick (`exit 0` with `summary='[disabled]'` while the LaunchAgent stays loaded). The companion NEXO Desktop client (a closed-source product, distributed separately) wires the same toggle into its Automatizaciones panel. See [CHANGELOG](CHANGELOG.md) for the full diff.
|
|
24
|
+
|
|
25
|
+
> **About NEXO Desktop.** NEXO Desktop is a separate closed-source companion app distributed at [systeam.es/nexo-desktop](https://systeam.es/nexo-desktop) — its source does not live in this repo. When release notes mention Desktop they describe a coordinated client release that consumes the Brain's CLI / MCP contract; the Brain itself is fully usable on its own (terminal, Codex, Claude Code, or any MCP client).
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
Previously in `6.4.0`: Plan Consolidado fase F1 — multi-tenant email accounts (`email_accounts` table, `nexo email setup` interactive wizard, `nexo email add --password-stdin --json` for machine consumers, idempotent migrator from legacy `~/.nexo/nexo-email/config.json`).
|
|
22
29
|
|
|
23
30
|
Previously in `6.3.1`: privacy hotfix over v6.3.0. The nightly auditor caught that `src/presets/entities_universal.json` in v6.3.0 shipped operator-specific `vhost_mapping` entries (private IPs, hostnames, tenant names). v6.3.1 pulls those out into `src/presets/entities_local.sample.json` (template) + `.gitignore`'d `~/.nexo/brain/presets/entities_local.json` (operator copy), and the installer drops the sample at `nexo init`. No behaviour change on the Guardian side.
|
|
24
31
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
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",
|
|
@@ -92,4 +92,4 @@
|
|
|
92
92
|
"README.md",
|
|
93
93
|
"LICENSE"
|
|
94
94
|
]
|
|
95
|
-
}
|
|
95
|
+
}
|
package/src/auto_update.py
CHANGED
|
@@ -10,6 +10,7 @@ This is separate from plugins/update.py which handles MANUAL updates with rollba
|
|
|
10
10
|
import json
|
|
11
11
|
import hashlib
|
|
12
12
|
import os
|
|
13
|
+
import paths
|
|
13
14
|
import re
|
|
14
15
|
import shutil
|
|
15
16
|
import subprocess
|
|
@@ -32,7 +33,7 @@ except ModuleNotFoundError as exc:
|
|
|
32
33
|
return False
|
|
33
34
|
|
|
34
35
|
NEXO_HOME = export_resolved_nexo_home()
|
|
35
|
-
DATA_DIR =
|
|
36
|
+
DATA_DIR = paths.data_dir()
|
|
36
37
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
37
38
|
|
|
38
39
|
# Repo root: go up from src/
|
|
@@ -212,10 +213,10 @@ def _write_last_check(data: dict):
|
|
|
212
213
|
def _sync_watchdog_hash_registry():
|
|
213
214
|
"""Keep the immutable-hash registry aligned with the installed watchdog script."""
|
|
214
215
|
try:
|
|
215
|
-
watchdog_file =
|
|
216
|
+
watchdog_file = paths.core_scripts_dir() / "nexo-watchdog.sh"
|
|
216
217
|
if not watchdog_file.exists():
|
|
217
218
|
return
|
|
218
|
-
registry_file =
|
|
219
|
+
registry_file = paths.core_scripts_dir() / ".watchdog-hashes"
|
|
219
220
|
entries: dict[str, str] = {}
|
|
220
221
|
if registry_file.exists():
|
|
221
222
|
for line in registry_file.read_text().splitlines():
|
|
@@ -523,7 +524,7 @@ def _refresh_installed_manifest():
|
|
|
523
524
|
try:
|
|
524
525
|
import shutil
|
|
525
526
|
src_crons = SRC_DIR / "crons"
|
|
526
|
-
dst_crons =
|
|
527
|
+
dst_crons = paths.crons_dir()
|
|
527
528
|
if src_crons.exists():
|
|
528
529
|
dst_crons.mkdir(parents=True, exist_ok=True)
|
|
529
530
|
for f in src_crons.iterdir():
|
|
@@ -537,17 +538,17 @@ def _refresh_installed_manifest():
|
|
|
537
538
|
def _cleanup_retired_runtime_files():
|
|
538
539
|
"""Remove retired core files that should not survive updates."""
|
|
539
540
|
retired = [
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
541
|
+
paths.core_scripts_dir() / "nexo-day-orchestrator.sh",
|
|
542
|
+
paths.core_scripts_dir() / "heartbeat-enforcement.py",
|
|
543
|
+
paths.core_scripts_dir() / "heartbeat-posttool.sh",
|
|
544
|
+
paths.core_scripts_dir() / "heartbeat-user-msg.sh",
|
|
545
|
+
paths.core_hooks_dir() / "heartbeat-guard.sh",
|
|
545
546
|
]
|
|
546
547
|
conditional_retired = [
|
|
547
|
-
(
|
|
548
|
-
(
|
|
549
|
-
(
|
|
550
|
-
(
|
|
548
|
+
(paths.core_scripts_dir() / "nexo-postcompact.sh", paths.core_hooks_dir() / "post-compact.sh"),
|
|
549
|
+
(paths.core_scripts_dir() / "nexo-memory-precompact.sh", paths.core_hooks_dir() / "pre-compact.sh"),
|
|
550
|
+
(paths.core_scripts_dir() / "nexo-memory-stop.sh", paths.core_hooks_dir() / "session-stop.sh"),
|
|
551
|
+
(paths.core_scripts_dir() / "nexo-session-briefing.sh", paths.core_hooks_dir() / "session-start.sh"),
|
|
551
552
|
]
|
|
552
553
|
for target in retired:
|
|
553
554
|
try:
|
|
@@ -692,7 +693,7 @@ def _rotate_auto_update_backups(prefix: str, keep: int = AUTO_UPDATE_BACKUP_KEEP
|
|
|
692
693
|
"""
|
|
693
694
|
if keep <= 0:
|
|
694
695
|
return 0
|
|
695
|
-
base =
|
|
696
|
+
base = paths.backups_dir()
|
|
696
697
|
if not base.is_dir():
|
|
697
698
|
return 0
|
|
698
699
|
try:
|
|
@@ -721,7 +722,7 @@ def _rotate_auto_update_backups(prefix: str, keep: int = AUTO_UPDATE_BACKUP_KEEP
|
|
|
721
722
|
return removed
|
|
722
723
|
|
|
723
724
|
|
|
724
|
-
SELF_HEAL_STATE_FILE =
|
|
725
|
+
SELF_HEAL_STATE_FILE = paths.operations_dir() / ".self-heal-state.json"
|
|
725
726
|
SELF_HEAL_COOLDOWN_SECONDS = 6 * 3600 # Never auto-heal twice within 6 h.
|
|
726
727
|
|
|
727
728
|
|
|
@@ -765,7 +766,7 @@ def _self_heal_if_wiped() -> dict | None:
|
|
|
765
766
|
if not db_looks_wiped(primary, CRITICAL_TABLES):
|
|
766
767
|
return None
|
|
767
768
|
reference = find_latest_hourly_backup(
|
|
768
|
-
|
|
769
|
+
paths.backups_dir(),
|
|
769
770
|
max_age_seconds=HOURLY_BACKUP_MAX_AGE,
|
|
770
771
|
)
|
|
771
772
|
if reference is None:
|
|
@@ -813,7 +814,7 @@ def _self_heal_if_wiped() -> dict | None:
|
|
|
813
814
|
time.sleep(0.5)
|
|
814
815
|
|
|
815
816
|
# Snapshot the current (wiped) state so the heal is reversible.
|
|
816
|
-
pre_heal_dir =
|
|
817
|
+
pre_heal_dir = paths.backups_dir() / f"pre-heal-{time.strftime('%Y-%m-%d-%H%M%S')}"
|
|
817
818
|
try:
|
|
818
819
|
import shutil as _shutil
|
|
819
820
|
pre_heal_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -1282,7 +1283,7 @@ def _backup_dbs() -> str | None:
|
|
|
1282
1283
|
# validation and rollback paths. Safe no-op when there are none.
|
|
1283
1284
|
_purge_zero_byte_db_files()
|
|
1284
1285
|
timestamp = _time.strftime("%Y-%m-%d-%H%M%S")
|
|
1285
|
-
backup_dir =
|
|
1286
|
+
backup_dir = paths.backups_dir() / f"pre-autoupdate-{timestamp}"
|
|
1286
1287
|
|
|
1287
1288
|
db_files = list(DATA_DIR.glob("*.db")) if DATA_DIR.is_dir() else []
|
|
1288
1289
|
db_files += [f for f in NEXO_HOME.glob("*.db") if f.is_file()]
|
|
@@ -1353,7 +1354,7 @@ def _sync_hooks():
|
|
|
1353
1354
|
"""Copy hook scripts from src/hooks/ to NEXO_HOME/hooks/ after a git pull."""
|
|
1354
1355
|
import shutil
|
|
1355
1356
|
hooks_src = SRC_DIR / "hooks"
|
|
1356
|
-
hooks_dest =
|
|
1357
|
+
hooks_dest = paths.core_hooks_dir()
|
|
1357
1358
|
if not hooks_src.is_dir():
|
|
1358
1359
|
return
|
|
1359
1360
|
hooks_dest.mkdir(parents=True, exist_ok=True)
|
|
@@ -1526,6 +1527,12 @@ def _run_db_migrations() -> bool:
|
|
|
1526
1527
|
# over automatically so scripts pick up the new source on the
|
|
1527
1528
|
# next cron without the operator running anything manually.
|
|
1528
1529
|
_maybe_migrate_legacy_email_config()
|
|
1530
|
+
# Plan Consolidado F0.6 — one-shot physical layout migration.
|
|
1531
|
+
# If ~/.nexo/.structure-version is absent or pre-F0.6, move the
|
|
1532
|
+
# flat layout into ~/.nexo/{core,personal,runtime}/<X>/, rewrite
|
|
1533
|
+
# personal_scripts.path + LaunchAgent plists, then write the
|
|
1534
|
+
# F0.6 marker. Idempotent: returns immediately when already F0.6.
|
|
1535
|
+
_maybe_migrate_to_f06_layout()
|
|
1529
1536
|
return True
|
|
1530
1537
|
except Exception as e:
|
|
1531
1538
|
_log(f"DB migration error: {e}")
|
|
@@ -1540,7 +1547,7 @@ def _maybe_migrate_legacy_email_config() -> None:
|
|
|
1540
1547
|
except Exception:
|
|
1541
1548
|
return # m46 not applied yet (older runtime), nothing to do
|
|
1542
1549
|
try:
|
|
1543
|
-
legacy =
|
|
1550
|
+
legacy = paths.nexo_email_dir() / "config.json"
|
|
1544
1551
|
if not legacy.exists():
|
|
1545
1552
|
return
|
|
1546
1553
|
if get_email_account("primary"):
|
|
@@ -1571,6 +1578,240 @@ def _maybe_migrate_legacy_email_config() -> None:
|
|
|
1571
1578
|
|
|
1572
1579
|
# ── npm version check (notify only) ─────────────────────────────────
|
|
1573
1580
|
|
|
1581
|
+
|
|
1582
|
+
def _maybe_migrate_to_f06_layout() -> None:
|
|
1583
|
+
"""Plan F0.6 — one-shot physical layout migration. Idempotent.
|
|
1584
|
+
|
|
1585
|
+
Pre-condition: ``~/.nexo/.structure-version`` is absent or its content
|
|
1586
|
+
is not ``F0.6``. After running, every dir from the flat layout
|
|
1587
|
+
(``~/.nexo/scripts/``, ``brain/``, ``data/``, ``operations/``, ...)
|
|
1588
|
+
has been moved to its post-F0.6 home (``~/.nexo/{core,personal,runtime}/<X>/``),
|
|
1589
|
+
``personal_scripts.path`` rows point at the new locations, every
|
|
1590
|
+
``com.nexo.*.plist`` LaunchAgent has been rewritten to use the new
|
|
1591
|
+
paths, and ``~/.nexo/.structure-version`` is set to ``F0.6``.
|
|
1592
|
+
|
|
1593
|
+
A snapshot is taken before the move at ``~/.nexo-pre-f06-snapshot/``
|
|
1594
|
+
so a complete rollback is ``mv ~/.nexo-pre-f06-snapshot ~/.nexo``.
|
|
1595
|
+
|
|
1596
|
+
Failures are non-fatal — the helper logs and returns; the operator
|
|
1597
|
+
can re-run ``nexo update`` later.
|
|
1598
|
+
"""
|
|
1599
|
+
try:
|
|
1600
|
+
marker = NEXO_HOME / ".structure-version"
|
|
1601
|
+
try:
|
|
1602
|
+
current = marker.read_text(encoding="utf-8").strip()
|
|
1603
|
+
except Exception:
|
|
1604
|
+
current = ""
|
|
1605
|
+
if current == "F0.6":
|
|
1606
|
+
return # already migrated
|
|
1607
|
+
# Refuse to migrate from a non-NEXO_HOME or a fresh install
|
|
1608
|
+
# without legacy dirs to move.
|
|
1609
|
+
legacy_anchors = [NEXO_HOME / sub for sub in
|
|
1610
|
+
("scripts", "brain", "data", "operations", "logs", "backups",
|
|
1611
|
+
"memory", "cognitive", "coordination", "exports",
|
|
1612
|
+
"nexo-email", "doctor", "snapshots", "crons",
|
|
1613
|
+
"skills", "plugins", "hooks", "rules")]
|
|
1614
|
+
present = [p for p in legacy_anchors if p.exists() and not p.is_symlink()]
|
|
1615
|
+
if not present:
|
|
1616
|
+
# Fresh install — write marker and return.
|
|
1617
|
+
try:
|
|
1618
|
+
marker.write_text("F0.6\n", encoding="utf-8")
|
|
1619
|
+
except Exception:
|
|
1620
|
+
pass
|
|
1621
|
+
return
|
|
1622
|
+
|
|
1623
|
+
_log("[F0.6] starting layout migration...")
|
|
1624
|
+
import shutil
|
|
1625
|
+
import sqlite3
|
|
1626
|
+
import re as _re
|
|
1627
|
+
# 1. Snapshot
|
|
1628
|
+
snapshot = Path(str(NEXO_HOME) + "-pre-f06-snapshot")
|
|
1629
|
+
if not snapshot.exists():
|
|
1630
|
+
try:
|
|
1631
|
+
shutil.copytree(NEXO_HOME, snapshot, symlinks=True, ignore_dangling_symlinks=True)
|
|
1632
|
+
_log(f"[F0.6] snapshot at {snapshot}")
|
|
1633
|
+
except Exception as e:
|
|
1634
|
+
_log(f"[F0.6] snapshot failed (non-fatal): {e}")
|
|
1635
|
+
|
|
1636
|
+
# 2. Ensure target dirs
|
|
1637
|
+
for sub in ("core/scripts", "core-dev/scripts", "personal/scripts",
|
|
1638
|
+
"personal/brain", "personal/skills",
|
|
1639
|
+
"core/plugins", "core/hooks", "core/rules",
|
|
1640
|
+
"runtime/data", "runtime/logs", "runtime/operations",
|
|
1641
|
+
"runtime/backups", "runtime/memory", "runtime/cognitive",
|
|
1642
|
+
"runtime/coordination", "runtime/exports",
|
|
1643
|
+
"runtime/nexo-email", "runtime/doctor",
|
|
1644
|
+
"runtime/snapshots", "runtime/crons"):
|
|
1645
|
+
(NEXO_HOME / sub).mkdir(parents=True, exist_ok=True)
|
|
1646
|
+
|
|
1647
|
+
# 3. Classify scripts: load core names from packaged manifest
|
|
1648
|
+
core_names: set[str] = set()
|
|
1649
|
+
artifacts = NEXO_HOME / "config" / "runtime-core-artifacts.json"
|
|
1650
|
+
try:
|
|
1651
|
+
data = json.loads(artifacts.read_text(encoding="utf-8"))
|
|
1652
|
+
for name in data.get("script_names", []):
|
|
1653
|
+
core_names.add(name)
|
|
1654
|
+
except Exception:
|
|
1655
|
+
pass
|
|
1656
|
+
for f in (NEXO_HOME / "core" / "scripts").iterdir() if (NEXO_HOME / "core" / "scripts").is_dir() else []:
|
|
1657
|
+
core_names.add(f.name)
|
|
1658
|
+
dev_names: set[str] = set()
|
|
1659
|
+
if (NEXO_HOME / "core-dev" / "scripts").is_dir():
|
|
1660
|
+
for f in (NEXO_HOME / "core-dev" / "scripts").iterdir():
|
|
1661
|
+
dev_names.add(f.name)
|
|
1662
|
+
|
|
1663
|
+
def _classify(name: str) -> str:
|
|
1664
|
+
if name in dev_names:
|
|
1665
|
+
return "core-dev"
|
|
1666
|
+
if name in core_names:
|
|
1667
|
+
return "core"
|
|
1668
|
+
return "personal"
|
|
1669
|
+
|
|
1670
|
+
# 4. Move script files (handle existing F0.3/0.4 symlinks)
|
|
1671
|
+
legacy_scripts = NEXO_HOME / "scripts"
|
|
1672
|
+
if legacy_scripts.is_dir() and not legacy_scripts.is_symlink():
|
|
1673
|
+
for f in sorted(legacy_scripts.iterdir()):
|
|
1674
|
+
if not f.is_file():
|
|
1675
|
+
continue
|
|
1676
|
+
cls = _classify(f.name)
|
|
1677
|
+
root = {
|
|
1678
|
+
"core": NEXO_HOME / "core" / "scripts",
|
|
1679
|
+
"personal": NEXO_HOME / "personal" / "scripts",
|
|
1680
|
+
"core-dev": NEXO_HOME / "core-dev" / "scripts",
|
|
1681
|
+
}[cls]
|
|
1682
|
+
dst = root / f.name
|
|
1683
|
+
if dst.is_symlink():
|
|
1684
|
+
target = dst.resolve()
|
|
1685
|
+
if target == f.resolve():
|
|
1686
|
+
dst.unlink()
|
|
1687
|
+
if dst.exists():
|
|
1688
|
+
if dst.is_file() and f.is_file() and dst.read_bytes() == f.read_bytes():
|
|
1689
|
+
f.unlink()
|
|
1690
|
+
continue
|
|
1691
|
+
dst.rename(dst.with_suffix(dst.suffix + ".f06bak"))
|
|
1692
|
+
shutil.move(str(f), str(dst))
|
|
1693
|
+
|
|
1694
|
+
# 5. Move tree dirs (brain, data, logs, ...)
|
|
1695
|
+
TREE_MAP = [
|
|
1696
|
+
("brain", NEXO_HOME / "personal" / "brain"),
|
|
1697
|
+
("data", NEXO_HOME / "runtime" / "data"),
|
|
1698
|
+
("logs", NEXO_HOME / "runtime" / "logs"),
|
|
1699
|
+
("operations", NEXO_HOME / "runtime" / "operations"),
|
|
1700
|
+
("backups", NEXO_HOME / "runtime" / "backups"),
|
|
1701
|
+
("memory", NEXO_HOME / "runtime" / "memory"),
|
|
1702
|
+
("cognitive", NEXO_HOME / "runtime" / "cognitive"),
|
|
1703
|
+
("coordination", NEXO_HOME / "runtime" / "coordination"),
|
|
1704
|
+
("exports", NEXO_HOME / "runtime" / "exports"),
|
|
1705
|
+
("nexo-email", NEXO_HOME / "runtime" / "nexo-email"),
|
|
1706
|
+
("doctor", NEXO_HOME / "runtime" / "doctor"),
|
|
1707
|
+
("snapshots", NEXO_HOME / "runtime" / "snapshots"),
|
|
1708
|
+
("crons", NEXO_HOME / "runtime" / "crons"),
|
|
1709
|
+
("skills", NEXO_HOME / "personal" / "skills"),
|
|
1710
|
+
("plugins", NEXO_HOME / "core" / "plugins"),
|
|
1711
|
+
("hooks", NEXO_HOME / "core" / "hooks"),
|
|
1712
|
+
("rules", NEXO_HOME / "core" / "rules"),
|
|
1713
|
+
]
|
|
1714
|
+
for legacy_name, new_dir in TREE_MAP:
|
|
1715
|
+
legacy = NEXO_HOME / legacy_name
|
|
1716
|
+
if not legacy.is_dir() or legacy.is_symlink():
|
|
1717
|
+
continue
|
|
1718
|
+
new_dir.parent.mkdir(parents=True, exist_ok=True)
|
|
1719
|
+
# Per-item move so we don't kill files via symlink-loop.
|
|
1720
|
+
for item in list(legacy.iterdir()):
|
|
1721
|
+
target = new_dir / item.name
|
|
1722
|
+
if target.is_symlink() and target.resolve() == item.resolve():
|
|
1723
|
+
target.unlink()
|
|
1724
|
+
if target.exists():
|
|
1725
|
+
if item.is_file() and target.is_file() and item.stat().st_size == target.stat().st_size:
|
|
1726
|
+
try:
|
|
1727
|
+
item.unlink()
|
|
1728
|
+
continue
|
|
1729
|
+
except Exception:
|
|
1730
|
+
pass
|
|
1731
|
+
# Backup conflict and proceed
|
|
1732
|
+
try:
|
|
1733
|
+
target.rename(target.with_suffix(target.suffix + ".f06bak"))
|
|
1734
|
+
except Exception:
|
|
1735
|
+
continue
|
|
1736
|
+
try:
|
|
1737
|
+
shutil.move(str(item), str(target))
|
|
1738
|
+
except Exception as e:
|
|
1739
|
+
_log(f"[F0.6] move {item} -> {target} failed: {e}")
|
|
1740
|
+
try:
|
|
1741
|
+
legacy.rmdir()
|
|
1742
|
+
except OSError:
|
|
1743
|
+
pass
|
|
1744
|
+
|
|
1745
|
+
# 6. UPDATE personal_scripts.path
|
|
1746
|
+
try:
|
|
1747
|
+
db = NEXO_HOME / "runtime" / "data" / "nexo.db"
|
|
1748
|
+
if not db.is_file():
|
|
1749
|
+
db = NEXO_HOME / "data" / "nexo.db"
|
|
1750
|
+
if db.is_file():
|
|
1751
|
+
conn = sqlite3.connect(str(db))
|
|
1752
|
+
rows = conn.execute("SELECT id, path FROM personal_scripts").fetchall()
|
|
1753
|
+
updates = []
|
|
1754
|
+
legacy_root = str(NEXO_HOME / "scripts") + "/"
|
|
1755
|
+
for row_id, old_path in rows:
|
|
1756
|
+
if not old_path or not old_path.startswith(legacy_root):
|
|
1757
|
+
continue
|
|
1758
|
+
name = Path(old_path).name
|
|
1759
|
+
cls = _classify(name)
|
|
1760
|
+
new_root = {
|
|
1761
|
+
"core": NEXO_HOME / "core" / "scripts",
|
|
1762
|
+
"personal": NEXO_HOME / "personal" / "scripts",
|
|
1763
|
+
"core-dev": NEXO_HOME / "core-dev" / "scripts",
|
|
1764
|
+
}[cls]
|
|
1765
|
+
updates.append((str(new_root / name), row_id))
|
|
1766
|
+
if updates:
|
|
1767
|
+
conn.executemany("UPDATE personal_scripts SET path = ? WHERE id = ?", updates)
|
|
1768
|
+
conn.commit()
|
|
1769
|
+
_log(f"[F0.6] UPDATEd {len(updates)} personal_scripts.path rows")
|
|
1770
|
+
conn.close()
|
|
1771
|
+
except Exception as e:
|
|
1772
|
+
_log(f"[F0.6] DB UPDATE failed (non-fatal): {e}")
|
|
1773
|
+
|
|
1774
|
+
# 7. Rewrite LaunchAgent plists
|
|
1775
|
+
try:
|
|
1776
|
+
la_dir = Path.home() / "Library" / "LaunchAgents"
|
|
1777
|
+
count = 0
|
|
1778
|
+
if la_dir.is_dir():
|
|
1779
|
+
for plist in sorted(la_dir.glob("com.nexo.*.plist")):
|
|
1780
|
+
try:
|
|
1781
|
+
text = plist.read_text(encoding="utf-8")
|
|
1782
|
+
except Exception:
|
|
1783
|
+
continue
|
|
1784
|
+
original = text
|
|
1785
|
+
def _repl_script(m):
|
|
1786
|
+
full = m.group(0)
|
|
1787
|
+
name = Path(full).name
|
|
1788
|
+
cls = _classify(name)
|
|
1789
|
+
new_root = {
|
|
1790
|
+
"core": NEXO_HOME / "core" / "scripts",
|
|
1791
|
+
"personal": NEXO_HOME / "personal" / "scripts",
|
|
1792
|
+
"core-dev": NEXO_HOME / "core-dev" / "scripts",
|
|
1793
|
+
}[cls]
|
|
1794
|
+
return str(new_root / name)
|
|
1795
|
+
text = _re.sub(rf"{_re.escape(str(NEXO_HOME))}/scripts/[\w\-\.]+\.(py|sh|js)", _repl_script, text)
|
|
1796
|
+
text = text.replace(f"{NEXO_HOME}/logs/", f"{NEXO_HOME}/runtime/logs/")
|
|
1797
|
+
if text != original:
|
|
1798
|
+
plist.write_text(text, encoding="utf-8")
|
|
1799
|
+
count += 1
|
|
1800
|
+
_log(f"[F0.6] rewrote {count} LaunchAgent plists")
|
|
1801
|
+
except Exception as e:
|
|
1802
|
+
_log(f"[F0.6] plist rewrite failed (non-fatal): {e}")
|
|
1803
|
+
|
|
1804
|
+
# 8. Write marker
|
|
1805
|
+
try:
|
|
1806
|
+
marker.write_text("F0.6\n", encoding="utf-8")
|
|
1807
|
+
_log("[F0.6] marker written: ~/.nexo/.structure-version = F0.6")
|
|
1808
|
+
except Exception as e:
|
|
1809
|
+
_log(f"[F0.6] marker write failed: {e}")
|
|
1810
|
+
_log("[F0.6] migration done")
|
|
1811
|
+
except Exception as e:
|
|
1812
|
+
_log(f"[F0.6] unexpected error (non-fatal, runtime keeps working): {e}")
|
|
1813
|
+
|
|
1814
|
+
|
|
1574
1815
|
def _check_npm_version() -> str | None:
|
|
1575
1816
|
"""For non-git installs: check npm registry for a newer version. Returns notification or None."""
|
|
1576
1817
|
current = _read_package_version()
|
|
@@ -2067,7 +2308,7 @@ def _sync_client_bootstraps(preferences: dict | None = None) -> list[str]:
|
|
|
2067
2308
|
|
|
2068
2309
|
# ── Main entry point ─────────────────────────────────────────────────
|
|
2069
2310
|
|
|
2070
|
-
_AUTO_UPDATE_LOCK_FILE =
|
|
2311
|
+
_AUTO_UPDATE_LOCK_FILE = paths.operations_dir() / ".auto_update.lock"
|
|
2071
2312
|
_AUTO_UPDATE_LOCK_STALE_SECONDS = 600 # 10 minutes
|
|
2072
2313
|
|
|
2073
2314
|
|
|
@@ -2265,10 +2506,10 @@ def _auto_update_check_locked() -> dict:
|
|
|
2265
2506
|
|
|
2266
2507
|
# Backfill evolution-objective.json for existing installs
|
|
2267
2508
|
try:
|
|
2268
|
-
evo_obj_path =
|
|
2509
|
+
evo_obj_path = paths.brain_dir() / "evolution-objective.json"
|
|
2269
2510
|
from evolution_cycle import normalize_objective
|
|
2270
2511
|
if not evo_obj_path.exists():
|
|
2271
|
-
(
|
|
2512
|
+
(paths.brain_dir()).mkdir(parents=True, exist_ok=True)
|
|
2272
2513
|
default_objective = {
|
|
2273
2514
|
"objective": "Improve operational excellence and reduce repeated errors",
|
|
2274
2515
|
"focus_areas": ["error_prevention", "proactivity", "memory_quality"],
|
|
@@ -2298,7 +2539,7 @@ def _auto_update_check_locked() -> dict:
|
|
|
2298
2539
|
|
|
2299
2540
|
# Backfill NEXO_HOME/scripts/ for existing installs
|
|
2300
2541
|
try:
|
|
2301
|
-
scripts_dest =
|
|
2542
|
+
scripts_dest = paths.core_scripts_dir()
|
|
2302
2543
|
# Deduce NEXO_CODE: env var first, then from __file__ (auto_update.py is in src/)
|
|
2303
2544
|
nexo_code = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent)))
|
|
2304
2545
|
scripts_src = nexo_code / "scripts" if (nexo_code / "scripts").is_dir() else None
|
|
@@ -2336,7 +2577,7 @@ def _auto_update_check_locked() -> dict:
|
|
|
2336
2577
|
# Backfill doctor package for existing installs
|
|
2337
2578
|
try:
|
|
2338
2579
|
doctor_src = SRC_DIR / "doctor"
|
|
2339
|
-
doctor_dest =
|
|
2580
|
+
doctor_dest = paths.doctor_dir()
|
|
2340
2581
|
if doctor_src.is_dir():
|
|
2341
2582
|
import shutil
|
|
2342
2583
|
if not doctor_dest.is_dir():
|
|
@@ -2391,7 +2632,7 @@ def _auto_update_check_locked() -> dict:
|
|
|
2391
2632
|
# Backfill MCP doctor plugin so existing installs expose nexo_doctor.
|
|
2392
2633
|
try:
|
|
2393
2634
|
plugin_src = SRC_DIR / "plugins" / "doctor.py"
|
|
2394
|
-
plugin_dest =
|
|
2635
|
+
plugin_dest = paths.core_plugins_dir() / "doctor.py"
|
|
2395
2636
|
plugin_dest.parent.mkdir(parents=True, exist_ok=True)
|
|
2396
2637
|
if plugin_src.is_file() and (not plugin_dest.exists() or plugin_src.stat().st_mtime > plugin_dest.stat().st_mtime):
|
|
2397
2638
|
import shutil
|
|
@@ -2478,8 +2719,8 @@ def _auto_update_check_locked() -> dict:
|
|
|
2478
2719
|
return result
|
|
2479
2720
|
|
|
2480
2721
|
|
|
2481
|
-
UPDATE_SUMMARY_FILE =
|
|
2482
|
-
UPDATE_HISTORY_FILE =
|
|
2722
|
+
UPDATE_SUMMARY_FILE = paths.logs_dir() / "update-last-summary.json"
|
|
2723
|
+
UPDATE_HISTORY_FILE = paths.logs_dir() / "update-history.jsonl"
|
|
2483
2724
|
|
|
2484
2725
|
|
|
2485
2726
|
def _resolve_sync_source() -> tuple[Path | None, Path | None]:
|
|
@@ -2645,7 +2886,7 @@ def _installed_scripts_classification(dest: Path) -> dict[str, str]:
|
|
|
2645
2886
|
|
|
2646
2887
|
def _backup_runtime_tree(dest: Path = NEXO_HOME) -> str:
|
|
2647
2888
|
timestamp = time.strftime("%Y-%m-%d-%H%M%S")
|
|
2648
|
-
backup_dir =
|
|
2889
|
+
backup_dir = paths.backups_dir() / f"runtime-tree-{timestamp}"
|
|
2649
2890
|
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
2650
2891
|
|
|
2651
2892
|
code_dirs = [
|
|
@@ -3090,7 +3331,7 @@ def _runtime_busy_reason() -> str | None:
|
|
|
3090
3331
|
|
|
3091
3332
|
def _write_update_summary(summary: dict):
|
|
3092
3333
|
try:
|
|
3093
|
-
logs_dir =
|
|
3334
|
+
logs_dir = paths.logs_dir()
|
|
3094
3335
|
logs_dir.mkdir(parents=True, exist_ok=True)
|
|
3095
3336
|
payload = dict(summary)
|
|
3096
3337
|
payload.setdefault("timestamp", time.strftime("%Y-%m-%dT%H:%M:%S"))
|
package/src/bootstrap_docs.py
CHANGED
|
@@ -193,7 +193,8 @@ def _target_path(client: str, *, user_home: Path | None = None) -> Path:
|
|
|
193
193
|
|
|
194
194
|
|
|
195
195
|
def _version_tracker_path(nexo_home: Path, client: str) -> Path:
|
|
196
|
-
|
|
196
|
+
import paths
|
|
197
|
+
return paths.data_dir() / BOOTSTRAP_SPECS[client]["version_file"]
|
|
197
198
|
|
|
198
199
|
|
|
199
200
|
def render_bootstrap_template(
|
|
@@ -54,7 +54,8 @@ TOP_LEVEL_KEYS = {"version", "created", "mood_history", "operator_name"}
|
|
|
54
54
|
|
|
55
55
|
def _calibration_path(nexo_home: Path | None = None) -> Path:
|
|
56
56
|
home = nexo_home or Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
57
|
-
|
|
57
|
+
import paths
|
|
58
|
+
return paths.brain_dir() / "calibration.json"
|
|
58
59
|
|
|
59
60
|
|
|
60
61
|
def _is_nested(cal: dict) -> bool:
|
|
@@ -342,7 +343,9 @@ def apply_v6_purge(
|
|
|
342
343
|
"seeded_default_resonance": False,
|
|
343
344
|
}
|
|
344
345
|
|
|
345
|
-
|
|
346
|
+
_brain_new = home / "personal" / "brain"
|
|
347
|
+
_brain_legacy = home / "brain"
|
|
348
|
+
cal_path = (_brain_new if _brain_new.is_dir() else _brain_legacy) / "calibration.json"
|
|
346
349
|
sched_path = home / "config" / "schedule.json"
|
|
347
350
|
|
|
348
351
|
# --- calibration.json ---
|