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
package/src/script_registry.py
CHANGED
|
@@ -591,42 +591,103 @@ def _script_entry(path: Path, meta: dict, *, is_core: bool, classification: str,
|
|
|
591
591
|
|
|
592
592
|
|
|
593
593
|
def classify_scripts_dir() -> dict:
|
|
594
|
-
"""Classify every file in
|
|
594
|
+
"""Classify every file in scripts dirs into personal/core/ignored/non-script buckets.
|
|
595
|
+
|
|
596
|
+
Plan F0.6 — scans every dir returned by paths.all_scripts_dirs():
|
|
597
|
+
core/scripts, personal/scripts, core-dev/scripts. Falls back to the
|
|
598
|
+
legacy ~/.nexo/scripts/ if the new layout has no entries (transition
|
|
599
|
+
safety; remove the fallback once F0.6 has been validated for a week).
|
|
600
|
+
"""
|
|
595
601
|
_apply_legacy_personal_script_backfills()
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
602
|
+
import paths
|
|
603
|
+
candidate_dirs = list(paths.all_scripts_dirs())
|
|
604
|
+
legacy = get_scripts_dir()
|
|
605
|
+
if legacy.is_dir() and legacy not in candidate_dirs:
|
|
606
|
+
candidate_dirs.append(legacy)
|
|
599
607
|
|
|
600
608
|
core_names = load_core_script_names()
|
|
601
609
|
entries: list[dict] = []
|
|
602
|
-
|
|
603
|
-
|
|
610
|
+
seen_keys: set[tuple[str, str]] = set()
|
|
611
|
+
for sdir in candidate_dirs:
|
|
612
|
+
if not sdir.is_dir():
|
|
604
613
|
continue
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
614
|
+
# Decide default classification per directory: a script dropped
|
|
615
|
+
# in core/scripts is core unless its name says otherwise; same
|
|
616
|
+
# for personal and core-dev.
|
|
617
|
+
if "core-dev" in sdir.parts:
|
|
618
|
+
dir_classification = "core-dev"
|
|
619
|
+
elif "personal" in sdir.parts:
|
|
620
|
+
dir_classification = "personal"
|
|
621
|
+
elif "core" in sdir.parts:
|
|
622
|
+
dir_classification = "core"
|
|
623
|
+
else:
|
|
624
|
+
dir_classification = None # legacy — fall back to name-based
|
|
625
|
+
for f in sorted(sdir.iterdir()):
|
|
626
|
+
if not f.is_file():
|
|
627
|
+
continue
|
|
628
|
+
# Dedup by (name, dir-classification) so a personal script with
|
|
629
|
+
# the same filename as a core script doesn't disappear (D2 audit fix).
|
|
630
|
+
dedup_key = (f.name, dir_classification or "legacy")
|
|
631
|
+
if dedup_key in seen_keys:
|
|
632
|
+
continue
|
|
633
|
+
seen_keys.add(dedup_key)
|
|
634
|
+
meta = parse_inline_metadata(f)
|
|
635
|
+
if _is_ignored(f):
|
|
636
|
+
entries.append(_script_entry(f, meta, is_core=False, classification="ignored", reason="internal or hidden artifact"))
|
|
637
|
+
continue
|
|
638
|
+
if not _is_script_candidate(f, meta):
|
|
639
|
+
entries.append(_script_entry(f, meta, is_core=False, classification="non-script", reason="not an executable/script candidate"))
|
|
640
|
+
continue
|
|
641
|
+
if dir_classification:
|
|
642
|
+
cls = dir_classification
|
|
643
|
+
is_core = cls in ("core", "core-dev")
|
|
644
|
+
else:
|
|
645
|
+
is_core = f.name in core_names
|
|
646
|
+
cls = "core" if is_core else "personal"
|
|
647
|
+
entries.append(_script_entry(f, meta, is_core=is_core, classification=cls))
|
|
618
648
|
|
|
619
649
|
summary: dict[str, int] = {}
|
|
620
650
|
for entry in entries:
|
|
621
651
|
summary[entry["classification"]] = summary.get(entry["classification"], 0) + 1
|
|
622
|
-
return {"scripts_dir": str(
|
|
652
|
+
return {"scripts_dir": str(candidate_dirs[0]) if candidate_dirs else "", "entries": entries, "summary": summary}
|
|
623
653
|
|
|
624
654
|
|
|
625
655
|
def list_scripts(include_core: bool = False) -> list[dict]:
|
|
626
656
|
"""List scripts in NEXO_HOME/scripts/.
|
|
627
657
|
|
|
628
658
|
By default only personal scripts. With include_core=True, also shows core/cron scripts.
|
|
659
|
+
|
|
660
|
+
Plan F0.2.4 fix — every entry now carries an `enabled` field
|
|
661
|
+
hydrated from the `personal_scripts` table. Core entries default
|
|
662
|
+
to True (they ship enabled and are not toggleable from this entry
|
|
663
|
+
point); personal entries default to True when no row exists yet
|
|
664
|
+
(sync hasn't run) so the Desktop toggle has a stable starting
|
|
665
|
+
state. The flag is what powers the Settings -> Automatizaciones
|
|
666
|
+
toggle's round-trip.
|
|
629
667
|
"""
|
|
668
|
+
# Build a path -> enabled map once so we don't open a transaction
|
|
669
|
+
# per entry. Personal_scripts rows that don't match anything in
|
|
670
|
+
# classify_scripts_dir() are simply ignored.
|
|
671
|
+
enabled_map: dict[str, bool] = {}
|
|
672
|
+
if include_core:
|
|
673
|
+
# Only the Desktop panel (include_core=True) needs the toggle
|
|
674
|
+
# round-trip. Gating the DB read this way avoids triggering
|
|
675
|
+
# init_db() in default callers (eg `nexo scripts list`), which
|
|
676
|
+
# would surface pre-existing test fixture pollution.
|
|
677
|
+
try:
|
|
678
|
+
from db import init_db
|
|
679
|
+
from db._personal_scripts import list_personal_scripts
|
|
680
|
+
init_db()
|
|
681
|
+
for row in list_personal_scripts(include_disabled=True):
|
|
682
|
+
p = row.get("path")
|
|
683
|
+
if p:
|
|
684
|
+
enabled_map[str(p)] = bool(row.get("enabled", True))
|
|
685
|
+
except Exception:
|
|
686
|
+
# Missing table (older runtime), locked DB, anything else:
|
|
687
|
+
# fall through to enabled=True (the cron wrapper gate is
|
|
688
|
+
# the source of truth at run time).
|
|
689
|
+
enabled_map = {}
|
|
690
|
+
|
|
630
691
|
results = []
|
|
631
692
|
for entry in classify_scripts_dir()["entries"]:
|
|
632
693
|
if entry["classification"] not in {"personal", "core"}:
|
|
@@ -636,6 +697,7 @@ def list_scripts(include_core: bool = False) -> list[dict]:
|
|
|
636
697
|
hidden = _truthy(entry.get("metadata", {}).get("hidden"))
|
|
637
698
|
if hidden and not include_core:
|
|
638
699
|
continue
|
|
700
|
+
entry["enabled"] = enabled_map.get(entry["path"], True)
|
|
639
701
|
results.append(entry)
|
|
640
702
|
return results
|
|
641
703
|
|
|
@@ -1462,6 +1524,78 @@ def remove_personal_script(name_or_path: str, *, keep_file: bool = False) -> dic
|
|
|
1462
1524
|
}
|
|
1463
1525
|
|
|
1464
1526
|
|
|
1527
|
+
def set_personal_script_enabled(name_or_path: str, enabled: bool) -> dict:
|
|
1528
|
+
"""Plan F0.2.2 — flip the `enabled` flag on a personal script.
|
|
1529
|
+
|
|
1530
|
+
Returns ``{ok: bool, name, enabled, changed: bool}``. Refuses to
|
|
1531
|
+
flip packaged core scripts (they ship enabled and the operator
|
|
1532
|
+
should `nexo scripts unschedule` if they want one to stop).
|
|
1533
|
+
|
|
1534
|
+
The cron wrapper (`nexo-cron-wrapper.sh`, F0.2.4) reads this flag
|
|
1535
|
+
on every tick and exits 0 with `summary='[disabled]'` when the
|
|
1536
|
+
script is disabled, so the LaunchAgent can stay loaded but the
|
|
1537
|
+
script itself is dormant.
|
|
1538
|
+
"""
|
|
1539
|
+
from db import init_db, get_personal_script
|
|
1540
|
+
from db._core import get_db
|
|
1541
|
+
|
|
1542
|
+
init_db()
|
|
1543
|
+
sync_personal_scripts()
|
|
1544
|
+
script = get_personal_script(name_or_path) or resolve_script(name_or_path)
|
|
1545
|
+
if not script:
|
|
1546
|
+
return {"ok": False, "error": f"Script not found: {name_or_path}"}
|
|
1547
|
+
if script.get("core") and not _within_scripts_dir(Path(script.get("path", ""))):
|
|
1548
|
+
return {
|
|
1549
|
+
"ok": False,
|
|
1550
|
+
"error": "Refusing to toggle a packaged core script via this entry point — "
|
|
1551
|
+
"use `nexo scripts unschedule` to stop it instead.",
|
|
1552
|
+
}
|
|
1553
|
+
target = 1 if enabled else 0
|
|
1554
|
+
conn = get_db()
|
|
1555
|
+
cur = conn.execute(
|
|
1556
|
+
"UPDATE personal_scripts SET enabled = ?, updated_at = CURRENT_TIMESTAMP WHERE path = ?",
|
|
1557
|
+
(target, script["path"]),
|
|
1558
|
+
)
|
|
1559
|
+
conn.commit()
|
|
1560
|
+
changed = bool(cur.rowcount)
|
|
1561
|
+
return {
|
|
1562
|
+
"ok": True,
|
|
1563
|
+
"name": script.get("name", name_or_path),
|
|
1564
|
+
"path": script.get("path", ""),
|
|
1565
|
+
"enabled": bool(target),
|
|
1566
|
+
"changed": changed,
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
|
|
1570
|
+
def get_personal_script_status(name_or_path: str) -> dict:
|
|
1571
|
+
"""Plan F0.2.2 — read-only view of one personal script for the
|
|
1572
|
+
Desktop panel and the `nexo scripts status` CLI verb."""
|
|
1573
|
+
from db import init_db, get_personal_script
|
|
1574
|
+
from db._core import get_db
|
|
1575
|
+
|
|
1576
|
+
init_db()
|
|
1577
|
+
sync_personal_scripts()
|
|
1578
|
+
script = get_personal_script(name_or_path) or resolve_script(name_or_path)
|
|
1579
|
+
if not script:
|
|
1580
|
+
return {"ok": False, "error": f"Script not found: {name_or_path}"}
|
|
1581
|
+
conn = get_db()
|
|
1582
|
+
last = conn.execute(
|
|
1583
|
+
"SELECT exit_code, started_at, ended_at, summary FROM cron_runs "
|
|
1584
|
+
"WHERE cron_id = ? ORDER BY id DESC LIMIT 1",
|
|
1585
|
+
(script.get("name") or "",),
|
|
1586
|
+
).fetchone()
|
|
1587
|
+
last_run = dict(last) if last else None
|
|
1588
|
+
return {
|
|
1589
|
+
"ok": True,
|
|
1590
|
+
"name": script.get("name"),
|
|
1591
|
+
"path": script.get("path"),
|
|
1592
|
+
"enabled": bool(script.get("enabled", True)),
|
|
1593
|
+
"core": bool(script.get("core")),
|
|
1594
|
+
"classification": script.get("classification", "user"),
|
|
1595
|
+
"last_run": last_run,
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
|
|
1465
1599
|
def doctor_script(path_or_name: str) -> dict:
|
|
1466
1600
|
"""Validate a single script. Returns dict with pass/warn/fail items."""
|
|
1467
1601
|
# Resolve
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# NEXO DB hourly backup — crontab: 0 * * * * $NEXO_HOME/scripts/nexo-backup.sh
|
|
2
|
+
# NEXO DB hourly backup — crontab: 0 * * * * $NEXO_HOME/core/scripts/nexo-backup.sh
|
|
3
3
|
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
4
4
|
NEXO_DIR="$NEXO_HOME"
|
|
5
5
|
BACKUP_DIR="$NEXO_HOME/backups"
|
|
6
6
|
WEEKLY_DIR="$BACKUP_DIR/weekly"
|
|
7
|
-
DB="$NEXO_HOME/data/nexo.db"
|
|
7
|
+
DB="$NEXO_HOME/runtime/data/nexo.db"
|
|
8
8
|
RETENTION_HOURS=48
|
|
9
9
|
|
|
10
10
|
mkdir -p "$BACKUP_DIR" "$WEEKLY_DIR"
|
|
@@ -22,8 +22,8 @@ CRON_ID="${1:?Usage: nexo-cron-wrapper.sh <cron_id> <command...>}"
|
|
|
22
22
|
shift
|
|
23
23
|
|
|
24
24
|
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
25
|
-
DB="$NEXO_HOME/data/nexo.db"
|
|
26
|
-
SPOOL_DIR="$NEXO_HOME/operations/cron-spool"
|
|
25
|
+
DB="$NEXO_HOME/runtime/data/nexo.db"
|
|
26
|
+
SPOOL_DIR="$NEXO_HOME/runtime/operations/cron-spool"
|
|
27
27
|
|
|
28
28
|
# Unlock macOS Keychain so headless Claude Code can read auth tokens.
|
|
29
29
|
# Claude Code stores its API key in the login keychain which auto-locks.
|
|
@@ -185,6 +185,44 @@ trap 'on_signal SIGTERM 143' TERM
|
|
|
185
185
|
trap 'on_signal SIGINT 130' INT
|
|
186
186
|
trap 'on_signal SIGHUP 129' HUP
|
|
187
187
|
|
|
188
|
+
|
|
189
|
+
# Plan F0.2.4 — disabled-script gate. Personal_scripts table holds an
|
|
190
|
+
# `enabled` flag flipped via `nexo scripts enable|disable <name>`. When
|
|
191
|
+
# the operator disables a cron the LaunchAgent stays loaded (no
|
|
192
|
+
# launchctl churn) but this wrapper short-circuits to a clean exit 0
|
|
193
|
+
# with summary='[disabled]'. The corresponding cron_runs row gets an
|
|
194
|
+
# explicit "skipped (disabled)" note so the daily audit can tell apart
|
|
195
|
+
# "didn't run because off" from "ran with exit 0".
|
|
196
|
+
DISABLED_GATE_OUTPUT=$(python3 - "$DB" "$CRON_ID" <<'PYGATE' 2>/dev/null || true
|
|
197
|
+
from __future__ import annotations
|
|
198
|
+
import sqlite3
|
|
199
|
+
import sys
|
|
200
|
+
db_path, cron_id = sys.argv[1:]
|
|
201
|
+
try:
|
|
202
|
+
conn = sqlite3.connect(db_path)
|
|
203
|
+
try:
|
|
204
|
+
row = conn.execute(
|
|
205
|
+
"SELECT enabled FROM personal_scripts WHERE name = ? LIMIT 1",
|
|
206
|
+
(cron_id,),
|
|
207
|
+
).fetchone()
|
|
208
|
+
finally:
|
|
209
|
+
conn.close()
|
|
210
|
+
except Exception:
|
|
211
|
+
sys.exit(0)
|
|
212
|
+
if row is not None and not int(row[0] or 0):
|
|
213
|
+
print("disabled")
|
|
214
|
+
PYGATE
|
|
215
|
+
)
|
|
216
|
+
if [ "$DISABLED_GATE_OUTPUT" = "disabled" ]; then
|
|
217
|
+
EXIT_CODE=0
|
|
218
|
+
SIGNAL_NAME=""
|
|
219
|
+
: > "$OUTPUT_FILE"
|
|
220
|
+
echo "[disabled] $CRON_ID skipped - re-enable with: nexo scripts enable $CRON_ID" > "$OUTPUT_FILE"
|
|
221
|
+
finalize_row
|
|
222
|
+
cleanup
|
|
223
|
+
exit 0
|
|
224
|
+
fi
|
|
225
|
+
|
|
188
226
|
"$@" > "$OUTPUT_FILE" 2>&1 &
|
|
189
227
|
CHILD_PID=$!
|
|
190
228
|
|
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
# Watermark approach: tracks the last processed timestamp so nothing is missed.
|
|
6
6
|
# Sessions from late-night/early-morning work are included in the next run.
|
|
7
7
|
#
|
|
8
|
-
# Logs to $NEXO_HOME/logs/deep-sleep.log
|
|
8
|
+
# Logs to $NEXO_HOME/runtime/logs/deep-sleep.log
|
|
9
9
|
|
|
10
10
|
set -euo pipefail
|
|
11
11
|
|
|
12
12
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
13
13
|
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
14
14
|
LOG_DIR="$NEXO_HOME/logs"
|
|
15
|
-
DEEP_SLEEP_DIR="$NEXO_HOME/operations/deep-sleep"
|
|
15
|
+
DEEP_SLEEP_DIR="$NEXO_HOME/runtime/operations/deep-sleep"
|
|
16
16
|
WATERMARK_FILE="$DEEP_SLEEP_DIR/.watermark"
|
|
17
17
|
RUN_ID=$(date +%Y-%m-%d)
|
|
18
18
|
|
|
@@ -27,7 +27,7 @@ echo "$NOW" > "$DEBOUNCE_FILE"
|
|
|
27
27
|
|
|
28
28
|
# 4. Find NEXO SID mapped to this Claude session_id
|
|
29
29
|
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
30
|
-
DB="$NEXO_HOME/data/nexo.db"
|
|
30
|
+
DB="$NEXO_HOME/runtime/data/nexo.db"
|
|
31
31
|
[ -f "$DB" ] || exit 0
|
|
32
32
|
|
|
33
33
|
NEXO_SID=$(sqlite3 "$DB" "SELECT sid FROM sessions WHERE (external_session_id = '${CLAUDE_SID}' OR claude_session_id = '${CLAUDE_SID}') AND last_update_epoch > (strftime('%s','now') - 900) ORDER BY last_update_epoch DESC LIMIT 1;" 2>/dev/null)
|
|
@@ -6,7 +6,7 @@ set -euo pipefail
|
|
|
6
6
|
SNAP_DIR="${1:?Usage: nexo-snapshot-restore.sh <snapshot-dir>}"
|
|
7
7
|
MANIFEST="$SNAP_DIR/manifest.json"
|
|
8
8
|
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
9
|
-
RESTORE_LOG="$NEXO_HOME/logs/snapshot-restores.log"
|
|
9
|
+
RESTORE_LOG="$NEXO_HOME/runtime/logs/snapshot-restores.log"
|
|
10
10
|
|
|
11
11
|
if [ ! -f "$MANIFEST" ]; then
|
|
12
12
|
echo "ERROR: No manifest.json in $SNAP_DIR" >&2
|
|
@@ -23,8 +23,8 @@ fi
|
|
|
23
23
|
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
24
24
|
TCC_DB="$HOME/Library/Application Support/com.apple.TCC/TCC.db"
|
|
25
25
|
VERSIONS_DIR="$HOME/.local/share/claude/versions"
|
|
26
|
-
MARKER_DIR="$NEXO_HOME/data/.tcc-approved"
|
|
27
|
-
LOG="$NEXO_HOME/logs/tcc-auto-approve.log"
|
|
26
|
+
MARKER_DIR="$NEXO_HOME/runtime/data/.tcc-approved"
|
|
27
|
+
LOG="$NEXO_HOME/runtime/logs/tcc-auto-approve.log"
|
|
28
28
|
|
|
29
29
|
mkdir -p "$MARKER_DIR" "$(dirname "$LOG")"
|
|
30
30
|
|
|
@@ -13,16 +13,16 @@ set -uo pipefail
|
|
|
13
13
|
HOME_DIR="$HOME"
|
|
14
14
|
NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
|
|
15
15
|
NEXO_DIR="$NEXO_HOME"
|
|
16
|
-
CORTEX_DIR="$NEXO_HOME/brain"
|
|
17
|
-
OPS_DIR="$NEXO_HOME/operations"
|
|
18
|
-
LOG_DIR="$NEXO_HOME/logs"
|
|
19
|
-
DB_PATH="$NEXO_HOME/data/nexo.db"
|
|
16
|
+
CORTEX_DIR="$NEXO_HOME/personal/brain"
|
|
17
|
+
OPS_DIR="$NEXO_HOME/runtime/operations"
|
|
18
|
+
LOG_DIR="$NEXO_HOME/runtime/logs"
|
|
19
|
+
DB_PATH="$NEXO_HOME/runtime/data/nexo.db"
|
|
20
20
|
LOG="$LOG_DIR/watchdog.log"
|
|
21
21
|
STATUS_JSON="$OPS_DIR/watchdog-status.json"
|
|
22
22
|
REPORT_TXT="$OPS_DIR/watchdog-report.txt"
|
|
23
23
|
ALERT_FILE="$OPS_DIR/.watchdog-alert"
|
|
24
|
-
HASH_REGISTRY="$NEXO_HOME/scripts/.watchdog-hashes"
|
|
25
|
-
FAIL_COUNT_FILE="$NEXO_HOME/scripts/.watchdog-fails"
|
|
24
|
+
HASH_REGISTRY="$NEXO_HOME/core/scripts/.watchdog-hashes"
|
|
25
|
+
FAIL_COUNT_FILE="$NEXO_HOME/core/scripts/.watchdog-fails"
|
|
26
26
|
MAX_FAILS=3
|
|
27
27
|
|
|
28
28
|
mkdir -p "$LOG_DIR" "$OPS_DIR"
|
|
@@ -47,8 +47,8 @@ log() { echo "[$TS] $1" >> "$LOG"; }
|
|
|
47
47
|
# Add personal (non-manifest) monitors to PERSONAL_MONITORS below.
|
|
48
48
|
NEXO_CODE="${NEXO_CODE:-$(cd "$(dirname "$0")/.." 2>/dev/null && pwd)}"
|
|
49
49
|
# Look for manifest in NEXO_HOME first (packaged install), then NEXO_CODE (dev/repo)
|
|
50
|
-
if [ -f "$NEXO_HOME/crons/manifest.json" ]; then
|
|
51
|
-
MANIFEST_FILE="$NEXO_HOME/crons/manifest.json"
|
|
50
|
+
if [ -f "$NEXO_HOME/runtime/crons/manifest.json" ]; then
|
|
51
|
+
MANIFEST_FILE="$NEXO_HOME/runtime/crons/manifest.json"
|
|
52
52
|
else
|
|
53
53
|
MANIFEST_FILE="$NEXO_CODE/crons/manifest.json"
|
|
54
54
|
fi
|
|
@@ -166,7 +166,7 @@ done < <(_build_monitors_from_manifest)
|
|
|
166
166
|
# Personal (non-manifest) monitors — add yours below.
|
|
167
167
|
# These are NOT in manifest.json and won't be synced by cron-sync.
|
|
168
168
|
PERSONAL_MONITORS=(
|
|
169
|
-
# "My Service|com.nexo.my-service|$NEXO_HOME/logs/my-service.log||3600||Every 30 min|personal"
|
|
169
|
+
# "My Service|com.nexo.my-service|$NEXO_HOME/runtime/logs/my-service.log||3600||Every 30 min|personal"
|
|
170
170
|
)
|
|
171
171
|
MONITORS+=("${PERSONAL_MONITORS[@]+"${PERSONAL_MONITORS[@]}"}")
|
|
172
172
|
|
|
@@ -882,7 +882,7 @@ if [ -f "$HASH_REGISTRY" ]; then
|
|
|
882
882
|
if [ "$ACTUAL" != "$expected_hash" ]; then
|
|
883
883
|
TAMPERED=$((TAMPERED + 1))
|
|
884
884
|
log "CRITICAL: Immutable file modified: $filepath"
|
|
885
|
-
LATEST_SNAP=$(ls -td "$NEXO_HOME/snapshots/"*/ 2>/dev/null | head -1)
|
|
885
|
+
LATEST_SNAP=$(ls -td "$NEXO_HOME/runtime/snapshots/"*/ 2>/dev/null | head -1)
|
|
886
886
|
if [ -n "$LATEST_SNAP" ] && [ -f "${LATEST_SNAP}files/${filepath#$HOME_DIR/}" ]; then
|
|
887
887
|
cp "${LATEST_SNAP}files/${filepath#$HOME_DIR/}" "$filepath"
|
|
888
888
|
log "RESTORED immutable file from snapshot"
|
|
@@ -1075,7 +1075,7 @@ fi
|
|
|
1075
1075
|
# ============================================================================
|
|
1076
1076
|
# Only triggers if: (a) there are FAILs after mechanical repair, (b) no NEXO
|
|
1077
1077
|
# repair is already running, (c) no interactive session is active (avoid conflict)
|
|
1078
|
-
REPAIR_LOCK="$NEXO_HOME/scripts/.watchdog-nexo-repair.lock"
|
|
1078
|
+
REPAIR_LOCK="$NEXO_HOME/core/scripts/.watchdog-nexo-repair.lock"
|
|
1079
1079
|
REPAIR_COOLDOWN=1800 # 30 min between NEXO repair attempts
|
|
1080
1080
|
|
|
1081
1081
|
if [ "$TOTAL_FAIL" -gt 0 ]; then
|
|
@@ -1162,19 +1162,19 @@ STEPS:
|
|
|
1162
1162
|
2. Check stderr/stdout logs for the actual error
|
|
1163
1163
|
3. Fix the root cause (missing file, bad config, dependency issue, etc.)
|
|
1164
1164
|
4. Reload the service and verify it is running (launchctl on macOS, systemctl on Linux)
|
|
1165
|
-
5. Log what you did to $NEXO_HOME/logs/watchdog-repair-result.log
|
|
1165
|
+
5. Log what you did to $NEXO_HOME/runtime/logs/watchdog-repair-result.log
|
|
1166
1166
|
${PROPAGATE_BLOCK}
|
|
1167
1167
|
|
|
1168
1168
|
CONSTRAINTS:
|
|
1169
1169
|
- Do NOT modify CLAUDE.md, AGENTS.md, or any protected file
|
|
1170
1170
|
- Do NOT start interactive conversations
|
|
1171
1171
|
- Keep it under 5 minutes
|
|
1172
|
-
- Log what you did to $NEXO_HOME/logs/watchdog-repair-result.log
|
|
1172
|
+
- Log what you did to $NEXO_HOME/runtime/logs/watchdog-repair-result.log
|
|
1173
1173
|
NEXOPROMPT
|
|
1174
1174
|
|
|
1175
1175
|
# Launch NEXO in background with the configured automation backend.
|
|
1176
1176
|
# Keep the hardened Claude fallback for older runtimes or partial installs.
|
|
1177
|
-
AGENT_RUNNER="$NEXO_HOME/scripts/nexo-agent-run.py"
|
|
1177
|
+
AGENT_RUNNER="$NEXO_HOME/core/scripts/nexo-agent-run.py"
|
|
1178
1178
|
NEXO_PYTHON="$NEXO_HOME/.venv/bin/python3"
|
|
1179
1179
|
if [ ! -x "$NEXO_PYTHON" ]; then
|
|
1180
1180
|
NEXO_PYTHON=$(command -v python3 2>/dev/null || echo "python3")
|
package/src/system_catalog.py
CHANGED
|
@@ -6,6 +6,7 @@ import importlib.util
|
|
|
6
6
|
import inspect
|
|
7
7
|
import json
|
|
8
8
|
import os
|
|
9
|
+
import paths
|
|
9
10
|
import re
|
|
10
11
|
import sys
|
|
11
12
|
from pathlib import Path
|
|
@@ -17,8 +18,8 @@ from script_registry import list_scripts
|
|
|
17
18
|
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
18
19
|
NEXO_CODE = Path(__file__).resolve().parent
|
|
19
20
|
SERVER_PATH = NEXO_CODE / "server.py"
|
|
20
|
-
MANIFEST_PATHS = [NEXO_CODE / "crons" / "manifest.json",
|
|
21
|
-
ATLAS_PATH =
|
|
21
|
+
MANIFEST_PATHS = [NEXO_CODE / "crons" / "manifest.json", paths.crons_dir() / "manifest.json"]
|
|
22
|
+
ATLAS_PATH = paths.brain_dir() / "project-atlas.json"
|
|
22
23
|
|
|
23
24
|
SECTION_ORDER = (
|
|
24
25
|
"core_tools",
|
package/src/tools_sessions.py
CHANGED
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
5
|
import os
|
|
6
|
+
import paths
|
|
6
7
|
import time
|
|
7
8
|
import secrets
|
|
8
9
|
import threading
|
|
@@ -26,7 +27,7 @@ from db import (
|
|
|
26
27
|
|
|
27
28
|
KEEPALIVE_INTERVAL = 600 # 10 min — well inside the 15-min TTL
|
|
28
29
|
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
29
|
-
SESSION_PORTABILITY_DIR =
|
|
30
|
+
SESSION_PORTABILITY_DIR = paths.operations_dir() / "session-portability"
|
|
30
31
|
|
|
31
32
|
_keepalive_threads: dict[str, threading.Event] = {} # sid → stop_event
|
|
32
33
|
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
|
+
import paths
|
|
7
8
|
import shutil
|
|
8
9
|
import sqlite3
|
|
9
10
|
import tarfile
|
|
@@ -17,7 +18,7 @@ from runtime_home import export_resolved_nexo_home
|
|
|
17
18
|
|
|
18
19
|
NEXO_HOME = export_resolved_nexo_home()
|
|
19
20
|
NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent)))
|
|
20
|
-
EXPORTS_DIR =
|
|
21
|
+
EXPORTS_DIR = paths.exports_dir()
|
|
21
22
|
STAGING_DIR = EXPORTS_DIR / ".staging"
|
|
22
23
|
USER_DIRS = ("brain", "coordination", "nexo-email", "assets")
|
|
23
24
|
USER_CONFIG_FILES = ("schedule.json",)
|
|
@@ -182,12 +183,12 @@ def export_user_bundle(output_path: str = "") -> dict:
|
|
|
182
183
|
sections: dict[str, dict] = {}
|
|
183
184
|
|
|
184
185
|
try:
|
|
185
|
-
data_db =
|
|
186
|
+
data_db = paths.db_path()
|
|
186
187
|
if data_db.is_file():
|
|
187
188
|
_sqlite_backup(data_db, bundle_root / "data" / "nexo.db")
|
|
188
189
|
sections["data_db"] = {"path": "data/nexo.db"}
|
|
189
190
|
|
|
190
|
-
brain_dir =
|
|
191
|
+
brain_dir = paths.brain_dir()
|
|
191
192
|
if brain_dir.is_dir():
|
|
192
193
|
copied = _copy_tree_filtered(brain_dir, bundle_root / "brain", exclude_names={"nexo.db"})
|
|
193
194
|
brain_db = brain_dir / "nexo.db"
|
|
@@ -271,7 +272,7 @@ def import_user_bundle(bundle_path: str) -> dict:
|
|
|
271
272
|
if not archive_path.is_file():
|
|
272
273
|
return {"ok": False, "error": f"bundle not found: {archive_path}"}
|
|
273
274
|
|
|
274
|
-
backups_dir =
|
|
275
|
+
backups_dir = paths.backups_dir()
|
|
275
276
|
backups_dir.mkdir(parents=True, exist_ok=True)
|
|
276
277
|
safety_backup = backups_dir / f"pre-import-user-data-{_now_stamp()}.tar.gz"
|
|
277
278
|
safety_result = export_user_bundle(str(safety_backup))
|
|
@@ -300,15 +301,15 @@ def import_user_bundle(bundle_path: str) -> dict:
|
|
|
300
301
|
|
|
301
302
|
data_db = bundle_root / "data" / "nexo.db"
|
|
302
303
|
if data_db.is_file():
|
|
303
|
-
_sqlite_backup(data_db,
|
|
304
|
+
_sqlite_backup(data_db, paths.db_path())
|
|
304
305
|
restored["data_db"] = {"path": "data/nexo.db"}
|
|
305
306
|
|
|
306
307
|
brain_dir = bundle_root / "brain"
|
|
307
308
|
if brain_dir.is_dir():
|
|
308
|
-
copied = _copy_tree_filtered(brain_dir,
|
|
309
|
+
copied = _copy_tree_filtered(brain_dir, paths.brain_dir(), exclude_names={"nexo.db"})
|
|
309
310
|
brain_db = brain_dir / "nexo.db"
|
|
310
311
|
if brain_db.is_file():
|
|
311
|
-
_sqlite_backup(brain_db,
|
|
312
|
+
_sqlite_backup(brain_db, paths.brain_dir() / "nexo.db")
|
|
312
313
|
copied += 1
|
|
313
314
|
restored["brain"] = {"path": "brain", "files": copied}
|
|
314
315
|
|
|
@@ -328,7 +329,7 @@ def import_user_bundle(bundle_path: str) -> dict:
|
|
|
328
329
|
|
|
329
330
|
imported_scripts = 0
|
|
330
331
|
scripts_dir = bundle_root / "personal-scripts"
|
|
331
|
-
target_scripts_dir =
|
|
332
|
+
target_scripts_dir = paths.personal_scripts_dir()
|
|
332
333
|
target_scripts_dir.mkdir(parents=True, exist_ok=True)
|
|
333
334
|
if scripts_dir.is_dir():
|
|
334
335
|
for script_path in sorted(scripts_dir.iterdir()):
|