nexo-brain 2.3.0 → 2.3.2
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/README.md +1 -1
- package/bin/nexo-brain.js +92 -9
- package/bin/postinstall.js +22 -15
- package/package.json +7 -4
- package/src/auto_update.py +194 -5
- package/src/crons/sync.py +6 -2
- package/src/db/_core.py +1 -0
- package/src/db/_entities.py +1 -0
- package/src/db/_episodic.py +1 -0
- package/src/db/_learnings.py +1 -0
- package/src/db/_reminders.py +1 -0
- package/src/db/_schema.py +11 -1
- package/src/db/_sessions.py +1 -0
- package/src/db/_skills.py +1 -0
- package/src/hooks/capture-tool-logs.sh +23 -6
- package/src/hooks/session-start.sh +4 -3
- package/src/plugin_loader.py +1 -0
- package/src/plugins/update.py +377 -26
- package/src/scripts/deep-sleep/apply_findings.py +1 -0
- package/src/scripts/deep-sleep/collect.py +1 -0
- package/src/scripts/deep-sleep/extract.py +1 -0
- package/src/scripts/deep-sleep/synthesize.py +1 -0
- package/src/scripts/nexo-catchup.py +29 -4
- package/src/scripts/nexo-daily-self-audit.py +21 -1
- package/src/scripts/nexo-evolution-run.py +21 -1
- package/src/scripts/nexo-learning-housekeep.py +1 -0
- package/src/scripts/nexo-postmortem-consolidator.py +34 -9
- package/src/scripts/nexo-sleep.py +32 -10
- package/src/scripts/nexo-synthesis.py +29 -9
- package/src/scripts/nexo-update.sh +109 -7
- package/src/scripts/nexo-watchdog.sh +122 -58
- package/src/server.py +66 -1
- package/src/tools_coordination.py +1 -0
- package/src/tools_sessions.py +1 -0
- package/scripts/migrate-to-unified 2.sh +0 -813
- package/scripts/migrate-to-unified.sh +0 -813
- package/scripts/migrate-v1.5-to-v1.6 2.py +0 -778
- package/scripts/migrate-v1.5-to-v1.6.py +0 -778
- package/scripts/migrate-v1.7-to-v1.8 2.py +0 -214
- package/scripts/migrate-v1.7-to-v1.8.py +0 -214
- package/scripts/nexo-preflight.sh +0 -236
- package/scripts/pre-commit-check 2.sh +0 -55
- package/scripts/pre-commit-check.sh +0 -55
- package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
- package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
- package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
- package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
- package/src/auto_close_sessions 2.py +0 -159
- package/src/auto_update 2.py +0 -634
- package/src/claim_graph 2.py +0 -323
- package/src/cognitive/__init__ 2.py +0 -62
- package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
- package/src/cognitive/_core 2.py +0 -567
- package/src/cognitive/_decay 2.py +0 -382
- package/src/cognitive/_ingest 2.py +0 -892
- package/src/cognitive/_memory 2.py +0 -912
- package/src/cognitive/_search 2.py +0 -949
- package/src/cognitive/_trust 2.py +0 -464
- package/src/crons/__pycache__/sync.cpython-314.pyc +0 -0
- package/src/crons/manifest 2.json +0 -106
- package/src/crons/sync 2.py +0 -217
- package/src/dashboard/__init__ 2.py +0 -0
- package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
- package/src/dashboard/app 2.py +0 -789
- package/src/db/__init__ 2.py +0 -89
- package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_cron_runs.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_cron_runs.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
- package/src/db/_core 2.py +0 -417
- package/src/db/_credentials 2.py +0 -124
- package/src/db/_entities 2.py +0 -178
- package/src/db/_episodic 2.py +0 -738
- package/src/db/_evolution 2.py +0 -54
- package/src/db/_fts 2.py +0 -406
- package/src/db/_learnings 2.py +0 -168
- package/src/db/_reminders 2.py +0 -338
- package/src/db/_schema 2.py +0 -364
- package/src/db/_sessions 2.py +0 -300
- package/src/db/_tasks 2.py +0 -91
- package/src/evolution_cycle 2.py +0 -266
- package/src/hnsw_index 2.py +0 -254
- package/src/hooks/auto_capture 2.py +0 -208
- package/src/hooks/caffeinate-guard 2.sh +0 -8
- package/src/hooks/capture-session 2.sh +0 -21
- package/src/hooks/capture-tool-logs 2.sh +0 -127
- package/src/hooks/daily-briefing-check 2.sh +0 -33
- package/src/hooks/inbox-hook 2.sh +0 -76
- package/src/hooks/post-compact 2.sh +0 -148
- package/src/hooks/pre-compact 2.sh +0 -151
- package/src/hooks/session-start 2.sh +0 -268
- package/src/hooks/session-stop 2.sh +0 -140
- package/src/kg_populate 2.py +0 -290
- package/src/knowledge_graph 2.py +0 -257
- package/src/maintenance 2.py +0 -59
- package/src/migrate_embeddings 2.py +0 -122
- package/src/plugin_loader 2.py +0 -202
- package/src/plugins/__init__ 2.py +0 -0
- package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/schedule.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/schedule.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/skills.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/skills.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
- package/src/plugins/adaptive_mode 2.py +0 -805
- package/src/plugins/agents 2.py +0 -52
- package/src/plugins/artifact_registry 2.py +0 -450
- package/src/plugins/backup 2.py +0 -104
- package/src/plugins/cognitive_memory 2.py +0 -564
- package/src/plugins/core_rules 2.py +0 -252
- package/src/plugins/cortex 2.py +0 -299
- package/src/plugins/entities 2.py +0 -67
- package/src/plugins/episodic_memory 2.py +0 -533
- package/src/plugins/evolution 2.py +0 -115
- package/src/plugins/guard 2.py +0 -746
- package/src/plugins/knowledge_graph_tools 2.py +0 -105
- package/src/plugins/preferences 2.py +0 -47
- package/src/plugins/update 2.py +0 -256
- package/src/requirements 2.txt +0 -12
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/core-rules 2.json +0 -331
- package/src/rules/migrate 2.py +0 -207
- package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
- package/src/scripts/check-context 2.py +0 -264
- package/src/scripts/nexo-auto-update 2.py +0 -6
- package/src/scripts/nexo-backup 2.sh +0 -25
- package/src/scripts/nexo-brain-activation 2.sh +0 -140
- package/src/scripts/nexo-catchup 2.py +0 -242
- package/src/scripts/nexo-cognitive-decay 2.py +0 -182
- package/src/scripts/nexo-daily-self-audit 2.py +0 -552
- package/src/scripts/nexo-deep-sleep 2.sh +0 -97
- package/src/scripts/nexo-evolution-run 2.py +0 -597
- package/src/scripts/nexo-followup-hygiene 2.py +0 -112
- package/src/scripts/nexo-github-monitor 2.py +0 -256
- package/src/scripts/nexo-immune 2.py +0 -927
- package/src/scripts/nexo-inbox-hook 2.sh +0 -74
- package/src/scripts/nexo-install 2.py +0 -6
- package/src/scripts/nexo-learning-housekeep 2.py +0 -245
- package/src/scripts/nexo-learning-validator 2.py +0 -207
- package/src/scripts/nexo-migrate 2.py +0 -232
- package/src/scripts/nexo-postmortem-consolidator 2.py +0 -421
- package/src/scripts/nexo-pre-commit 2.py +0 -120
- package/src/scripts/nexo-prevent-sleep 2.sh +0 -29
- package/src/scripts/nexo-proactive-dashboard 2.py +0 -345
- package/src/scripts/nexo-reflection 2.py +0 -253
- package/src/scripts/nexo-runtime-preflight 2.py +0 -274
- package/src/scripts/nexo-send-email 2.py +0 -25
- package/src/scripts/nexo-send-email.py +0 -25
- package/src/scripts/nexo-send-reply 2.py +0 -178
- package/src/scripts/nexo-send-reply.py +0 -178
- package/src/scripts/nexo-sleep 2.py +0 -592
- package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
- package/src/scripts/nexo-synthesis 2.py +0 -253
- package/src/scripts/nexo-tcc-approve 2.sh +0 -79
- package/src/scripts/nexo-update 2.sh +0 -161
- package/src/scripts/nexo-watchdog 2.sh +0 -878
- package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
- package/src/server 2.py +0 -733
- package/src/storage_router 2.py +0 -32
- package/src/tools_coordination 2.py +0 -102
- package/src/tools_credentials 2.py +0 -68
- package/src/tools_learnings 2.py +0 -220
- package/src/tools_menu 2.py +0 -227
- package/src/tools_reminders 2.py +0 -86
- package/src/tools_reminders_crud 2.py +0 -159
- package/src/tools_sessions 2.py +0 -476
- package/src/tools_task_history 2.py +0 -57
- package/templates/CLAUDE.md 2.template +0 -63
- package/templates/openclaw 2.json +0 -13
- package/tests/__init__ 2.py +0 -0
- package/tests/__init__.py +0 -0
- package/tests/conftest 2.py +0 -71
- package/tests/conftest.py +0 -71
- package/tests/test_cognitive 2.py +0 -205
- package/tests/test_cognitive.py +0 -205
- package/tests/test_knowledge_graph 2.py +0 -140
- package/tests/test_knowledge_graph.py +0 -140
- package/tests/test_migrations 2.py +0 -137
- package/tests/test_migrations.py +0 -137
|
@@ -41,8 +41,12 @@ record = {
|
|
|
41
41
|
print(json.dumps(record))
|
|
42
42
|
" >> "$LOG_FILE" 2>/dev/null
|
|
43
43
|
|
|
44
|
-
# ── Layer 1: Auto-diary every 10 tool calls
|
|
45
|
-
|
|
44
|
+
# ── Layer 1: Auto-diary every 10 tool calls (session-scoped) ─────────
|
|
45
|
+
# Extract session_id for per-session counters (prevents cross-terminal contamination)
|
|
46
|
+
SESSION_ID=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('session_id','global'))" 2>/dev/null || echo "global")
|
|
47
|
+
COUNTER_DIR="$NEXO_HOME/operations/counters"
|
|
48
|
+
mkdir -p "$COUNTER_DIR"
|
|
49
|
+
COUNTER_FILE="$COUNTER_DIR/.tool-call-count-${SESSION_ID}"
|
|
46
50
|
NEXO_DB="$NEXO_HOME/data/nexo.db"
|
|
47
51
|
|
|
48
52
|
# Increment counter (atomic: read+write in one step)
|
|
@@ -86,12 +90,25 @@ if not entries:
|
|
|
86
90
|
|
|
87
91
|
tools_summary = ', '.join(entries[-10:])
|
|
88
92
|
|
|
89
|
-
# Get
|
|
93
|
+
# Get session by claude session_id (scoped), fallback to most recent
|
|
94
|
+
session_id = '$SESSION_ID'
|
|
90
95
|
conn = sqlite3.connect(db_path, timeout=2)
|
|
91
96
|
conn.row_factory = sqlite3.Row
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
97
|
+
|
|
98
|
+
# Try to find NEXO SID mapped to this claude session_id
|
|
99
|
+
row = None
|
|
100
|
+
if session_id and session_id != 'global':
|
|
101
|
+
row = conn.execute(
|
|
102
|
+
'SELECT sid, task FROM sessions WHERE claude_session_id = ? LIMIT 1',
|
|
103
|
+
(session_id,)
|
|
104
|
+
).fetchone()
|
|
105
|
+
|
|
106
|
+
# Fallback: most recent active session
|
|
107
|
+
if not row:
|
|
108
|
+
row = conn.execute(
|
|
109
|
+
'SELECT sid, task FROM sessions ORDER BY last_update_epoch DESC LIMIT 1'
|
|
110
|
+
).fetchone()
|
|
111
|
+
|
|
95
112
|
if not row:
|
|
96
113
|
conn.close()
|
|
97
114
|
sys.exit(0)
|
|
@@ -92,10 +92,11 @@ try:
|
|
|
92
92
|
|
|
93
93
|
try:
|
|
94
94
|
rows = db.execute(
|
|
95
|
-
'SELECT sid, task,
|
|
96
|
-
'WHERE
|
|
95
|
+
'SELECT sid, task, started_epoch FROM sessions '
|
|
96
|
+
'WHERE (strftime(\"%s\",\"now\") - last_update_epoch) < 900'
|
|
97
97
|
).fetchall()
|
|
98
|
-
|
|
98
|
+
from datetime import datetime as _dt
|
|
99
|
+
sessions = [{'sid': r['sid'], 'task': r['task'], 'started': _dt.fromtimestamp(r['started_epoch']).strftime('%Y-%m-%d %H:%M') if r['started_epoch'] else '?'} for r in rows]
|
|
99
100
|
except Exception:
|
|
100
101
|
pass
|
|
101
102
|
|
package/src/plugin_loader.py
CHANGED
package/src/plugins/update.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
"""Update plugin — pull latest code, backup DBs, run migrations, verify."""
|
|
2
3
|
import json
|
|
3
4
|
import os
|
|
@@ -12,19 +13,71 @@ from pathlib import Path
|
|
|
12
13
|
_THIS_DIR = Path(__file__).resolve().parent
|
|
13
14
|
REPO_DIR = _THIS_DIR.parent.parent
|
|
14
15
|
PACKAGE_JSON = REPO_DIR / "package.json"
|
|
15
|
-
SRC_DIR = REPO_DIR / "src"
|
|
16
16
|
|
|
17
17
|
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
|
|
18
18
|
DATA_DIR = NEXO_HOME / "data"
|
|
19
19
|
BACKUP_BASE = NEXO_HOME / "backups"
|
|
20
20
|
|
|
21
|
+
# In packaged installs, update.py lives at ~/.nexo/plugins/update.py
|
|
22
|
+
# so REPO_DIR would be ~/ (wrong). Detect this and fix paths.
|
|
23
|
+
_PACKAGED_INSTALL = not (REPO_DIR / ".git").exists() and not (REPO_DIR / ".git").is_file()
|
|
24
|
+
|
|
25
|
+
if _PACKAGED_INSTALL:
|
|
26
|
+
# In packaged mode, core .py files live directly in NEXO_HOME
|
|
27
|
+
SRC_DIR = NEXO_HOME
|
|
28
|
+
else:
|
|
29
|
+
SRC_DIR = REPO_DIR / "src"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _find_npm_pkg_src() -> Path | None:
|
|
33
|
+
"""Locate the nexo-brain npm package's src/ directory for requirements.txt."""
|
|
34
|
+
try:
|
|
35
|
+
result = subprocess.run(
|
|
36
|
+
["npm", "root", "-g"],
|
|
37
|
+
capture_output=True, text=True, timeout=10,
|
|
38
|
+
)
|
|
39
|
+
if result.returncode == 0:
|
|
40
|
+
npm_src = Path(result.stdout.strip()) / "nexo-brain" / "src"
|
|
41
|
+
if npm_src.is_dir():
|
|
42
|
+
return npm_src
|
|
43
|
+
except Exception:
|
|
44
|
+
pass
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
def _is_git_repo() -> bool:
|
|
48
|
+
"""Check if REPO_DIR is a valid git repository."""
|
|
49
|
+
return (REPO_DIR / ".git").exists() or (REPO_DIR / ".git").is_file()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _refresh_installed_manifest():
|
|
53
|
+
"""Copy source crons/ to NEXO_HOME/crons/ so catchup & watchdog stay current."""
|
|
54
|
+
try:
|
|
55
|
+
src_crons = SRC_DIR / "crons"
|
|
56
|
+
dst_crons = NEXO_HOME / "crons"
|
|
57
|
+
if src_crons.exists():
|
|
58
|
+
dst_crons.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
for f in src_crons.iterdir():
|
|
60
|
+
if f.is_file():
|
|
61
|
+
shutil.copy2(str(f), str(dst_crons / f.name))
|
|
62
|
+
except Exception:
|
|
63
|
+
pass
|
|
64
|
+
|
|
21
65
|
|
|
22
66
|
def _read_version() -> str:
|
|
23
|
-
"""Read version from package.json."""
|
|
67
|
+
"""Read version from package.json or NEXO_HOME/version.json (packaged installs)."""
|
|
24
68
|
try:
|
|
25
|
-
|
|
69
|
+
if PACKAGE_JSON.exists():
|
|
70
|
+
return json.loads(PACKAGE_JSON.read_text()).get("version", "unknown")
|
|
26
71
|
except Exception:
|
|
27
|
-
|
|
72
|
+
pass
|
|
73
|
+
# Packaged installs don't ship package.json — check version.json in NEXO_HOME
|
|
74
|
+
try:
|
|
75
|
+
version_file = NEXO_HOME / "version.json"
|
|
76
|
+
if version_file.exists():
|
|
77
|
+
return json.loads(version_file.read_text()).get("version", "unknown")
|
|
78
|
+
except Exception:
|
|
79
|
+
pass
|
|
80
|
+
return "unknown"
|
|
28
81
|
|
|
29
82
|
|
|
30
83
|
def _git(*args, cwd=None) -> tuple[int, str, str]:
|
|
@@ -39,13 +92,28 @@ def _git(*args, cwd=None) -> tuple[int, str, str]:
|
|
|
39
92
|
return result.returncode, result.stdout.strip(), result.stderr.strip()
|
|
40
93
|
|
|
41
94
|
|
|
95
|
+
def _requirements_hash() -> str:
|
|
96
|
+
"""Return a content hash of requirements.txt, or empty string if missing."""
|
|
97
|
+
import hashlib
|
|
98
|
+
req_file = SRC_DIR / "requirements.txt"
|
|
99
|
+
if not req_file.exists() and _PACKAGED_INSTALL:
|
|
100
|
+
npm_src = _find_npm_pkg_src()
|
|
101
|
+
if npm_src:
|
|
102
|
+
req_file = npm_src / "requirements.txt"
|
|
103
|
+
if req_file.exists():
|
|
104
|
+
return hashlib.sha256(req_file.read_bytes()).hexdigest()
|
|
105
|
+
return ""
|
|
106
|
+
|
|
107
|
+
|
|
42
108
|
def _check_dirty() -> str | None:
|
|
43
|
-
"""Return error message if
|
|
44
|
-
|
|
109
|
+
"""Return error message if worktree has uncommitted changes, else None."""
|
|
110
|
+
if not _is_git_repo():
|
|
111
|
+
return None # Not a git repo, skip dirty check
|
|
112
|
+
rc, out, _ = _git("status", "--porcelain")
|
|
45
113
|
if rc != 0:
|
|
46
114
|
return "Failed to check git status."
|
|
47
115
|
if out:
|
|
48
|
-
return f"Uncommitted changes
|
|
116
|
+
return f"Uncommitted changes:\n{out}\nCommit or stash before updating."
|
|
49
117
|
return None
|
|
50
118
|
|
|
51
119
|
|
|
@@ -101,12 +169,51 @@ def _restore_databases(backup_dir: str):
|
|
|
101
169
|
break
|
|
102
170
|
|
|
103
171
|
|
|
172
|
+
def _reinstall_pip_deps() -> str | None:
|
|
173
|
+
"""Reinstall Python dependencies from requirements.txt into the managed venv."""
|
|
174
|
+
req_file = SRC_DIR / "requirements.txt"
|
|
175
|
+
if not req_file.exists() and _PACKAGED_INSTALL:
|
|
176
|
+
# In packaged mode, requirements.txt lives in the npm package's src/ dir
|
|
177
|
+
npm_src = _find_npm_pkg_src()
|
|
178
|
+
if npm_src:
|
|
179
|
+
req_file = npm_src / "requirements.txt"
|
|
180
|
+
if not req_file.exists():
|
|
181
|
+
return None # No requirements file, skip
|
|
182
|
+
venv_pip = NEXO_HOME / ".venv" / "bin" / "pip"
|
|
183
|
+
if not venv_pip.exists():
|
|
184
|
+
venv_pip = NEXO_HOME / ".venv" / "bin" / "pip3"
|
|
185
|
+
if not venv_pip.exists():
|
|
186
|
+
# No venv, try system pip with --break-system-packages
|
|
187
|
+
try:
|
|
188
|
+
result = subprocess.run(
|
|
189
|
+
[sys.executable, "-m", "pip", "install", "--quiet", "-r", str(req_file), "--break-system-packages"],
|
|
190
|
+
capture_output=True, text=True, timeout=120,
|
|
191
|
+
)
|
|
192
|
+
if result.returncode != 0:
|
|
193
|
+
return f"pip install failed: {result.stderr or result.stdout}"
|
|
194
|
+
except Exception as e:
|
|
195
|
+
return f"pip install error: {e}"
|
|
196
|
+
return None
|
|
197
|
+
try:
|
|
198
|
+
result = subprocess.run(
|
|
199
|
+
[str(venv_pip), "install", "--quiet", "-r", str(req_file)],
|
|
200
|
+
capture_output=True, text=True, timeout=120,
|
|
201
|
+
)
|
|
202
|
+
if result.returncode != 0:
|
|
203
|
+
return f"pip install failed: {result.stderr or result.stdout}"
|
|
204
|
+
except Exception as e:
|
|
205
|
+
return f"pip install error: {e}"
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
|
|
104
209
|
def _run_migrations() -> str | None:
|
|
105
210
|
"""Run init_db() to apply pending migrations. Returns error or None."""
|
|
211
|
+
# In packaged mode, db/ lives in NEXO_HOME; in dev mode, in SRC_DIR
|
|
212
|
+
cwd = str(NEXO_HOME) if _PACKAGED_INSTALL else str(SRC_DIR)
|
|
106
213
|
try:
|
|
107
214
|
result = subprocess.run(
|
|
108
215
|
[sys.executable, "-c", "import db; db.init_db()"],
|
|
109
|
-
cwd=
|
|
216
|
+
cwd=cwd,
|
|
110
217
|
capture_output=True,
|
|
111
218
|
text=True,
|
|
112
219
|
timeout=30,
|
|
@@ -120,10 +227,12 @@ def _run_migrations() -> str | None:
|
|
|
120
227
|
|
|
121
228
|
def _verify_import() -> str | None:
|
|
122
229
|
"""Verify server.py can be imported successfully."""
|
|
230
|
+
# In packaged mode, server.py lives in NEXO_HOME; in dev mode, in SRC_DIR
|
|
231
|
+
cwd = str(NEXO_HOME) if _PACKAGED_INSTALL else str(SRC_DIR)
|
|
123
232
|
try:
|
|
124
233
|
result = subprocess.run(
|
|
125
234
|
[sys.executable, "-c", "import server"],
|
|
126
|
-
cwd=
|
|
235
|
+
cwd=cwd,
|
|
127
236
|
capture_output=True,
|
|
128
237
|
text=True,
|
|
129
238
|
timeout=15,
|
|
@@ -135,27 +244,235 @@ def _verify_import() -> str | None:
|
|
|
135
244
|
return None
|
|
136
245
|
|
|
137
246
|
|
|
247
|
+
def _sync_hooks_to_home():
|
|
248
|
+
"""Copy hook scripts from src/hooks/ to NEXO_HOME/hooks/ after update."""
|
|
249
|
+
import shutil
|
|
250
|
+
hooks_src = SRC_DIR / "hooks"
|
|
251
|
+
hooks_dest = NEXO_HOME / "hooks"
|
|
252
|
+
if not hooks_src.is_dir():
|
|
253
|
+
return
|
|
254
|
+
hooks_dest.mkdir(parents=True, exist_ok=True)
|
|
255
|
+
synced = 0
|
|
256
|
+
for f in hooks_src.iterdir():
|
|
257
|
+
if f.is_file() and f.suffix == ".sh":
|
|
258
|
+
dest = hooks_dest / f.name
|
|
259
|
+
shutil.copy2(str(f), str(dest))
|
|
260
|
+
os.chmod(str(dest), 0o755)
|
|
261
|
+
synced += 1
|
|
262
|
+
if synced:
|
|
263
|
+
print(f"[NEXO update] Synced {synced} hook(s) to {hooks_dest}", file=sys.stderr)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _backup_code_tree() -> tuple[str | None, str | None]:
|
|
267
|
+
"""Snapshot NEXO_HOME code dirs before npm update. Returns (backup_dir, error)."""
|
|
268
|
+
timestamp = time.strftime("%Y-%m-%d-%H%M%S")
|
|
269
|
+
backup_dir = BACKUP_BASE / f"code-tree-{timestamp}"
|
|
270
|
+
# Directories and flat files that postinstall copies into NEXO_HOME
|
|
271
|
+
code_dirs = ["hooks", "plugins", "db", "cognitive", "dashboard", "rules", "crons", "scripts"]
|
|
272
|
+
code_files_glob = ["*.py", "requirements.txt"]
|
|
273
|
+
try:
|
|
274
|
+
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
275
|
+
# Backup directories
|
|
276
|
+
for d in code_dirs:
|
|
277
|
+
src = NEXO_HOME / d
|
|
278
|
+
if src.is_dir():
|
|
279
|
+
shutil.copytree(src, backup_dir / d, dirs_exist_ok=True)
|
|
280
|
+
# Backup flat code files in NEXO_HOME root
|
|
281
|
+
for pattern in code_files_glob:
|
|
282
|
+
for f in NEXO_HOME.glob(pattern):
|
|
283
|
+
if f.is_file():
|
|
284
|
+
shutil.copy2(f, backup_dir / f.name)
|
|
285
|
+
# Backup version.json
|
|
286
|
+
vf = NEXO_HOME / "version.json"
|
|
287
|
+
if vf.is_file():
|
|
288
|
+
shutil.copy2(vf, backup_dir / "version.json")
|
|
289
|
+
except Exception as e:
|
|
290
|
+
return None, f"Code tree backup failed: {e}"
|
|
291
|
+
return str(backup_dir), None
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _restore_code_tree(backup_dir: str) -> str | None:
|
|
295
|
+
"""Restore NEXO_HOME code dirs from a backup snapshot. Returns error or None."""
|
|
296
|
+
bdir = Path(backup_dir)
|
|
297
|
+
if not bdir.is_dir():
|
|
298
|
+
return f"Code tree backup dir not found: {backup_dir}"
|
|
299
|
+
try:
|
|
300
|
+
for item in bdir.iterdir():
|
|
301
|
+
dest = NEXO_HOME / item.name
|
|
302
|
+
if item.is_dir():
|
|
303
|
+
if dest.is_dir():
|
|
304
|
+
shutil.rmtree(dest)
|
|
305
|
+
shutil.copytree(item, dest)
|
|
306
|
+
elif item.is_file():
|
|
307
|
+
shutil.copy2(item, dest)
|
|
308
|
+
except Exception as e:
|
|
309
|
+
return f"Code tree restore failed: {e}"
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _rollback_npm_package(target_version: str) -> str | None:
|
|
314
|
+
"""Rollback nexo-brain npm package to a specific version.
|
|
315
|
+
|
|
316
|
+
Uses NEXO_SKIP_POSTINSTALL because we restore the code tree
|
|
317
|
+
from our own pre-update backup — no need for postinstall migration.
|
|
318
|
+
"""
|
|
319
|
+
try:
|
|
320
|
+
result = subprocess.run(
|
|
321
|
+
["npm", "install", "-g", f"nexo-brain@{target_version}"],
|
|
322
|
+
capture_output=True, text=True, timeout=120,
|
|
323
|
+
env={**os.environ, "NEXO_SKIP_POSTINSTALL": "1", "NEXO_HOME": str(NEXO_HOME)},
|
|
324
|
+
)
|
|
325
|
+
if result.returncode != 0:
|
|
326
|
+
return f"npm rollback failed: {result.stderr or result.stdout}"
|
|
327
|
+
except Exception as e:
|
|
328
|
+
return f"npm rollback error: {e}"
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _handle_packaged_update() -> str:
|
|
333
|
+
"""Update a packaged (npm) install — no git repo available."""
|
|
334
|
+
old_version = _read_version()
|
|
335
|
+
|
|
336
|
+
# 1. Backup databases BEFORE any changes
|
|
337
|
+
backup_dir, backup_err = _backup_databases()
|
|
338
|
+
if backup_err:
|
|
339
|
+
return f"ABORTED at backup: {backup_err}"
|
|
340
|
+
|
|
341
|
+
# 2. Backup NEXO_HOME code tree BEFORE npm update
|
|
342
|
+
# postinstall copies hooks/core/plugins/scripts into NEXO_HOME,
|
|
343
|
+
# so we need a full snapshot to restore on failure.
|
|
344
|
+
code_backup_dir, code_err = _backup_code_tree()
|
|
345
|
+
if code_err:
|
|
346
|
+
return f"ABORTED at code tree backup: {code_err}"
|
|
347
|
+
|
|
348
|
+
# 3. Run npm update (postinstall.js will migrate NEXO_HOME in-place)
|
|
349
|
+
try:
|
|
350
|
+
result = subprocess.run(
|
|
351
|
+
["npm", "update", "-g", "nexo-brain"],
|
|
352
|
+
capture_output=True, text=True, timeout=120,
|
|
353
|
+
env={**os.environ, "NEXO_HOME": str(NEXO_HOME)},
|
|
354
|
+
)
|
|
355
|
+
if result.returncode != 0:
|
|
356
|
+
# npm failed (including postinstall failures) — full rollback
|
|
357
|
+
if backup_dir:
|
|
358
|
+
_restore_databases(backup_dir)
|
|
359
|
+
if code_backup_dir:
|
|
360
|
+
_restore_code_tree(code_backup_dir)
|
|
361
|
+
# Reinstall pip deps from restored old requirements.txt
|
|
362
|
+
_reinstall_pip_deps()
|
|
363
|
+
rollback_err = _rollback_npm_package(old_version)
|
|
364
|
+
msg = f"ABORTED: npm update failed: {result.stderr or result.stdout}"
|
|
365
|
+
if rollback_err:
|
|
366
|
+
msg += f"\n WARNING: npm rollback also failed: {rollback_err}"
|
|
367
|
+
msg += f"\n Manual rollback: npm install -g nexo-brain@{old_version}"
|
|
368
|
+
return msg
|
|
369
|
+
except FileNotFoundError:
|
|
370
|
+
return "ABORTED: npm not found. Install Node.js to update packaged installs."
|
|
371
|
+
except Exception as e:
|
|
372
|
+
if backup_dir:
|
|
373
|
+
_restore_databases(backup_dir)
|
|
374
|
+
if code_backup_dir:
|
|
375
|
+
_restore_code_tree(code_backup_dir)
|
|
376
|
+
# Reinstall pip deps from restored old requirements.txt
|
|
377
|
+
_reinstall_pip_deps()
|
|
378
|
+
rollback_err = _rollback_npm_package(old_version)
|
|
379
|
+
msg = f"ABORTED: npm update error: {e}"
|
|
380
|
+
if rollback_err:
|
|
381
|
+
msg += f"\n WARNING: npm rollback also failed: {rollback_err}"
|
|
382
|
+
msg += f"\n Manual rollback: npm install -g nexo-brain@{old_version}"
|
|
383
|
+
return msg
|
|
384
|
+
|
|
385
|
+
new_version = _read_version()
|
|
386
|
+
if old_version == new_version:
|
|
387
|
+
return f"Already up to date (v{old_version}). No changes."
|
|
388
|
+
|
|
389
|
+
# 4. Post-npm verification steps
|
|
390
|
+
errors = []
|
|
391
|
+
|
|
392
|
+
# Reinstall pip deps for new version
|
|
393
|
+
pip_err = _reinstall_pip_deps()
|
|
394
|
+
if pip_err:
|
|
395
|
+
errors.append(f"pip deps: {pip_err}")
|
|
396
|
+
|
|
397
|
+
# Run migrations
|
|
398
|
+
mig_err = _run_migrations()
|
|
399
|
+
if mig_err:
|
|
400
|
+
errors.append(f"migrations: {mig_err}")
|
|
401
|
+
|
|
402
|
+
# Verify server can still import
|
|
403
|
+
verify_err = _verify_import()
|
|
404
|
+
if verify_err:
|
|
405
|
+
errors.append(f"verification: {verify_err}")
|
|
406
|
+
|
|
407
|
+
if errors:
|
|
408
|
+
# 5. Full rollback: restore code tree + DBs + pip deps + rollback npm package
|
|
409
|
+
if code_backup_dir:
|
|
410
|
+
tree_err = _restore_code_tree(code_backup_dir)
|
|
411
|
+
else:
|
|
412
|
+
tree_err = "no code tree backup available"
|
|
413
|
+
if backup_dir:
|
|
414
|
+
_restore_databases(backup_dir)
|
|
415
|
+
# Reinstall pip deps from the restored (old) requirements.txt
|
|
416
|
+
# so the venv matches the rolled-back code tree
|
|
417
|
+
pip_rollback_err = _reinstall_pip_deps() if not tree_err else None
|
|
418
|
+
rollback_err = _rollback_npm_package(old_version)
|
|
419
|
+
lines = [f"UPDATE FAILED (packaged install, v{old_version} -> v{new_version})"]
|
|
420
|
+
for err in errors:
|
|
421
|
+
lines.append(f" ERROR: {err}")
|
|
422
|
+
lines.append(f" Databases restored from: {backup_dir}")
|
|
423
|
+
if tree_err:
|
|
424
|
+
lines.append(f" WARNING: code tree restore failed: {tree_err}")
|
|
425
|
+
else:
|
|
426
|
+
lines.append(f" Code tree restored from: {code_backup_dir}")
|
|
427
|
+
if pip_rollback_err:
|
|
428
|
+
lines.append(f" WARNING: pip deps rollback failed: {pip_rollback_err}")
|
|
429
|
+
elif not tree_err:
|
|
430
|
+
lines.append(" Python deps: reinstalled from old requirements.txt")
|
|
431
|
+
if rollback_err:
|
|
432
|
+
lines.append(f" WARNING: npm rollback failed: {rollback_err}")
|
|
433
|
+
lines.append(f" Manual rollback: npm install -g nexo-brain@{old_version}")
|
|
434
|
+
else:
|
|
435
|
+
lines.append(f" npm package rolled back to v{old_version}")
|
|
436
|
+
lines.append("")
|
|
437
|
+
lines.append("Fix the errors above, then run nexo_update again.")
|
|
438
|
+
return "\n".join(lines)
|
|
439
|
+
|
|
440
|
+
lines = ["UPDATE SUCCESSFUL (packaged install)"]
|
|
441
|
+
lines.append(f" Version: {old_version} -> {new_version}")
|
|
442
|
+
lines.append(f" Backup: {backup_dir}")
|
|
443
|
+
lines.append("")
|
|
444
|
+
lines.append("MCP server restart needed to load new code.")
|
|
445
|
+
return "\n".join(lines)
|
|
446
|
+
|
|
447
|
+
|
|
138
448
|
def handle_update(remote: str = "origin", branch: str = "main") -> str:
|
|
139
449
|
"""Pull latest NEXO code, backup databases, run migrations, and verify.
|
|
140
450
|
|
|
141
|
-
|
|
142
|
-
|
|
451
|
+
Supports both git checkouts and packaged (npm) installs.
|
|
452
|
+
|
|
453
|
+
Full update flow (git):
|
|
454
|
+
1. Check for uncommitted changes in entire worktree
|
|
143
455
|
2. Backup all .db files
|
|
144
456
|
3. git pull
|
|
145
|
-
4.
|
|
146
|
-
5.
|
|
147
|
-
6.
|
|
457
|
+
4. Reinstall Python dependencies if version changed
|
|
458
|
+
5. Run migrations if version changed
|
|
459
|
+
6. Verify server.py imports
|
|
460
|
+
7. Rollback on failure (git reset --hard to saved commit)
|
|
148
461
|
|
|
149
462
|
Args:
|
|
150
463
|
remote: Git remote name (default: origin)
|
|
151
464
|
branch: Git branch to pull (default: main)
|
|
152
465
|
"""
|
|
466
|
+
# Packaged install — no git repo
|
|
467
|
+
if not _is_git_repo():
|
|
468
|
+
return _handle_packaged_update()
|
|
469
|
+
|
|
153
470
|
steps_done = []
|
|
154
471
|
old_commit = None
|
|
155
472
|
backup_dir = None
|
|
156
473
|
|
|
157
474
|
try:
|
|
158
|
-
# Step 1: Check dirty
|
|
475
|
+
# Step 1: Check dirty (full worktree)
|
|
159
476
|
dirty_err = _check_dirty()
|
|
160
477
|
if dirty_err:
|
|
161
478
|
return f"ABORTED: {dirty_err}"
|
|
@@ -163,6 +480,7 @@ def handle_update(remote: str = "origin", branch: str = "main") -> str:
|
|
|
163
480
|
|
|
164
481
|
# Record current state
|
|
165
482
|
old_version = _read_version()
|
|
483
|
+
old_req_hash = _requirements_hash()
|
|
166
484
|
rc, old_commit, _ = _git("rev-parse", "HEAD")
|
|
167
485
|
if rc != 0:
|
|
168
486
|
return "ABORTED: Not a git repository or git not available."
|
|
@@ -179,39 +497,59 @@ def handle_update(remote: str = "origin", branch: str = "main") -> str:
|
|
|
179
497
|
return f"ABORTED at git pull: {pull_err or pull_out}"
|
|
180
498
|
steps_done.append("git-pull")
|
|
181
499
|
|
|
182
|
-
# Step 4: Check version
|
|
500
|
+
# Step 4: Check version and dependency changes
|
|
183
501
|
new_version = _read_version()
|
|
184
502
|
version_changed = old_version != new_version
|
|
503
|
+
new_req_hash = _requirements_hash()
|
|
504
|
+
deps_changed = old_req_hash != new_req_hash
|
|
185
505
|
|
|
186
|
-
# Step 5:
|
|
506
|
+
# Step 5: Reinstall pip dependencies if requirements.txt changed
|
|
507
|
+
if deps_changed or version_changed:
|
|
508
|
+
pip_err = _reinstall_pip_deps()
|
|
509
|
+
if pip_err:
|
|
510
|
+
raise RuntimeError(f"Pip install failed: {pip_err}")
|
|
511
|
+
steps_done.append("pip-deps")
|
|
512
|
+
|
|
513
|
+
# Step 6: Run migrations if version changed
|
|
187
514
|
if version_changed:
|
|
188
515
|
mig_err = _run_migrations()
|
|
189
516
|
if mig_err:
|
|
190
517
|
raise RuntimeError(f"Migration failed: {mig_err}")
|
|
191
518
|
steps_done.append("migrations")
|
|
192
519
|
|
|
193
|
-
# Step
|
|
520
|
+
# Step 7: Verify import
|
|
194
521
|
verify_err = _verify_import()
|
|
195
522
|
if verify_err:
|
|
196
523
|
raise RuntimeError(f"Verification failed: {verify_err}")
|
|
197
524
|
steps_done.append("verify")
|
|
198
525
|
|
|
199
|
-
# Step
|
|
526
|
+
# Step 8: Sync crons with manifest
|
|
200
527
|
cron_sync_result = ""
|
|
201
528
|
try:
|
|
202
|
-
cron_sync_path =
|
|
529
|
+
cron_sync_path = SRC_DIR / "crons" / "sync.py"
|
|
203
530
|
if cron_sync_path.exists():
|
|
204
|
-
|
|
205
|
-
r = _sp.run(
|
|
531
|
+
r = subprocess.run(
|
|
206
532
|
[sys.executable, str(cron_sync_path)],
|
|
207
533
|
capture_output=True, text=True, timeout=30,
|
|
208
|
-
env={**os.environ, "NEXO_HOME": str(NEXO_HOME), "NEXO_CODE": str(
|
|
534
|
+
env={**os.environ, "NEXO_HOME": str(NEXO_HOME), "NEXO_CODE": str(SRC_DIR)},
|
|
209
535
|
)
|
|
210
536
|
cron_sync_result = r.stdout.strip()
|
|
211
|
-
|
|
537
|
+
if r.returncode == 0:
|
|
538
|
+
steps_done.append("cron-sync")
|
|
539
|
+
# Refresh installed manifest only after successful sync
|
|
540
|
+
_refresh_installed_manifest()
|
|
541
|
+
else:
|
|
542
|
+
cron_sync_result = f"Cron sync failed (exit {r.returncode}): {r.stderr or r.stdout}"
|
|
212
543
|
except Exception as e:
|
|
213
544
|
cron_sync_result = f"Cron sync warning: {e}"
|
|
214
545
|
|
|
546
|
+
# Step 9: Sync hooks to NEXO_HOME
|
|
547
|
+
try:
|
|
548
|
+
_sync_hooks_to_home()
|
|
549
|
+
steps_done.append("hook-sync")
|
|
550
|
+
except Exception as e:
|
|
551
|
+
pass # Non-critical, log in function
|
|
552
|
+
|
|
215
553
|
# Build result
|
|
216
554
|
if pull_out == "Already up to date.":
|
|
217
555
|
return f"Already up to date (v{old_version}). No changes pulled."
|
|
@@ -223,22 +561,35 @@ def handle_update(remote: str = "origin", branch: str = "main") -> str:
|
|
|
223
561
|
lines.append(f" Version: {old_version} (unchanged)")
|
|
224
562
|
lines.append(f" Branch: {remote}/{branch}")
|
|
225
563
|
lines.append(f" Backup: {backup_dir}")
|
|
564
|
+
if "pip-deps" in steps_done:
|
|
565
|
+
lines.append(" Python deps: reinstalled")
|
|
226
566
|
if version_changed:
|
|
227
567
|
lines.append(" Migrations: applied")
|
|
228
568
|
if "cron-sync" in steps_done:
|
|
229
569
|
lines.append(" Crons: synced with manifest")
|
|
570
|
+
if "hook-sync" in steps_done:
|
|
571
|
+
lines.append(" Hooks: synced to NEXO_HOME")
|
|
230
572
|
lines.append("")
|
|
231
573
|
lines.append("MCP server restart needed to load new code.")
|
|
232
574
|
return "\n".join(lines)
|
|
233
575
|
|
|
234
576
|
except Exception as e:
|
|
235
|
-
# Rollback
|
|
577
|
+
# Rollback — use git checkout to saved commit (safer than reset --hard)
|
|
236
578
|
rollback_lines = [f"UPDATE FAILED: {e}", "", "Rolling back..."]
|
|
237
579
|
|
|
238
580
|
if old_commit and "git-pull" in steps_done:
|
|
581
|
+
# Full rollback: reset HEAD + index + worktree to old commit
|
|
239
582
|
rc, _, err = _git("reset", "--hard", old_commit)
|
|
240
583
|
if rc == 0:
|
|
241
|
-
rollback_lines.append(f" Git:
|
|
584
|
+
rollback_lines.append(f" Git: restored files to {old_commit[:8]}")
|
|
585
|
+
# Reinstall pip deps from the restored old requirements.txt
|
|
586
|
+
# so the venv matches the rolled-back code
|
|
587
|
+
if "pip-deps" in steps_done:
|
|
588
|
+
pip_rb_err = _reinstall_pip_deps()
|
|
589
|
+
if pip_rb_err:
|
|
590
|
+
rollback_lines.append(f" WARNING: pip deps rollback failed: {pip_rb_err}")
|
|
591
|
+
else:
|
|
592
|
+
rollback_lines.append(" Python deps: reinstalled from old requirements.txt")
|
|
242
593
|
else:
|
|
243
594
|
rollback_lines.append(f" Git rollback FAILED: {err}")
|
|
244
595
|
|
|
@@ -18,10 +18,32 @@ import sys
|
|
|
18
18
|
from datetime import datetime, timedelta
|
|
19
19
|
from pathlib import Path
|
|
20
20
|
|
|
21
|
-
CLAUDE_CLI = Path.home() / ".local" / "bin" / "claude"
|
|
22
|
-
|
|
23
21
|
HOME = Path.home()
|
|
24
|
-
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(
|
|
22
|
+
NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(HOME / ".nexo")))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _resolve_claude_cli() -> Path:
|
|
26
|
+
"""Find claude CLI: saved path > PATH > common locations."""
|
|
27
|
+
saved = NEXO_HOME / "config" / "claude-cli-path"
|
|
28
|
+
if saved.exists():
|
|
29
|
+
p = Path(saved.read_text().strip())
|
|
30
|
+
if p.exists():
|
|
31
|
+
return p
|
|
32
|
+
import shutil
|
|
33
|
+
found = shutil.which("claude")
|
|
34
|
+
if found:
|
|
35
|
+
return Path(found)
|
|
36
|
+
for candidate in [
|
|
37
|
+
HOME / ".local" / "bin" / "claude",
|
|
38
|
+
HOME / ".npm-global" / "bin" / "claude",
|
|
39
|
+
Path("/usr/local/bin/claude"),
|
|
40
|
+
]:
|
|
41
|
+
if candidate.exists():
|
|
42
|
+
return candidate
|
|
43
|
+
return HOME / ".local" / "bin" / "claude" # last resort
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
CLAUDE_CLI = _resolve_claude_cli()
|
|
25
47
|
LOG_DIR = NEXO_HOME / "logs"
|
|
26
48
|
LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
27
49
|
LOG_FILE = LOG_DIR / "catchup.log"
|
|
@@ -47,7 +69,10 @@ def _resolve_python() -> str:
|
|
|
47
69
|
|
|
48
70
|
NEXO_PYTHON = _resolve_python()
|
|
49
71
|
NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent.parent)))
|
|
50
|
-
|
|
72
|
+
# Look for manifest in NEXO_HOME first (packaged install), then NEXO_CODE (dev/repo)
|
|
73
|
+
_manifest_home = NEXO_HOME / "crons" / "manifest.json"
|
|
74
|
+
_manifest_code = NEXO_CODE / "crons" / "manifest.json"
|
|
75
|
+
MANIFEST = _manifest_home if _manifest_home.exists() else _manifest_code
|
|
51
76
|
|
|
52
77
|
|
|
53
78
|
def _load_tasks_from_manifest() -> list[tuple]:
|