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
package/README.md
CHANGED
|
@@ -755,7 +755,7 @@ If NEXO Brain is useful to you, consider:
|
|
|
755
755
|
- **Auto-resolve followups**: Change log entries automatically cross-reference and complete matching open followups.
|
|
756
756
|
- **Free-form learning categories**: No more hardcoded category validation — use any category name.
|
|
757
757
|
- **CLAUDE.md template rewrite**: 494 to 127 lines, compact procedural format with full heartbeat signal reactions.
|
|
758
|
-
- **Complete sanitization**: All hardcoded paths use `NEXO_HOME` env var.
|
|
758
|
+
- **Complete sanitization**: All hardcoded paths use `NEXO_HOME` env var. No credentials or personal data in the distributed package. Migration scripts and maintainer tooling use configurable paths.
|
|
759
759
|
|
|
760
760
|
### v1.6.0 — Nervous System + Dashboard v2 (2026-03-30)
|
|
761
761
|
- **Nervous System**: 11 autonomous scripts (decay, deep sleep, self-audit, catchup, evolution, followup hygiene, immune, watchdog, github monitor, learning validator)
|
package/bin/nexo-brain.js
CHANGED
|
@@ -19,7 +19,7 @@ const fs = require("fs");
|
|
|
19
19
|
const path = require("path");
|
|
20
20
|
const readline = require("readline");
|
|
21
21
|
|
|
22
|
-
let NEXO_HOME = path.join(require("os").homedir(), ".nexo");
|
|
22
|
+
let NEXO_HOME = process.env.NEXO_HOME || path.join(require("os").homedir(), ".nexo");
|
|
23
23
|
const CLAUDE_SETTINGS = path.join(
|
|
24
24
|
require("os").homedir(),
|
|
25
25
|
".claude",
|
|
@@ -124,6 +124,8 @@ const ALL_CORE_HOOKS = [
|
|
|
124
124
|
timeout: 10, purpose: "POSTMORTEM — the most important" },
|
|
125
125
|
{ event: "PostToolUse", key: "capture-tool-logs.sh", script: "capture-tool-logs.sh",
|
|
126
126
|
timeout: 5, purpose: "Operation capture" },
|
|
127
|
+
{ event: "PostToolUse", key: "capture-session.sh", script: "capture-session.sh",
|
|
128
|
+
timeout: 3, purpose: "Sensory register (session_buffer.jsonl)" },
|
|
127
129
|
{ event: "PostToolUse", key: "inbox-hook.sh", script: "inbox-hook.sh",
|
|
128
130
|
timeout: 5, purpose: "Inter-session messaging" },
|
|
129
131
|
{ event: "PreCompact", key: "pre-compact.sh", script: "pre-compact.sh",
|
|
@@ -262,7 +264,7 @@ const DAY_MAP = {
|
|
|
262
264
|
*/
|
|
263
265
|
function installAllProcesses(platform, pythonPath, nexoHome, schedule, launchAgentsDir) {
|
|
264
266
|
const home = require("os").homedir();
|
|
265
|
-
const nexoCode =
|
|
267
|
+
const nexoCode = nexoHome;
|
|
266
268
|
const logsDir = path.join(nexoHome, "logs");
|
|
267
269
|
fs.mkdirSync(logsDir, { recursive: true });
|
|
268
270
|
|
|
@@ -436,8 +438,10 @@ ${restartPolicy}
|
|
|
436
438
|
count++;
|
|
437
439
|
continue;
|
|
438
440
|
} else if (proc.type === "runAtLoad") {
|
|
439
|
-
//
|
|
440
|
-
fs.writeFileSync(serviceFile, service);
|
|
441
|
+
// runAtLoad: enable as a boot-time oneshot service (like macOS RunAtLoad)
|
|
442
|
+
fs.writeFileSync(serviceFile, service + `\n[Install]\nWantedBy=default.target\n`);
|
|
443
|
+
run(`systemctl --user enable ${serviceName}.service`);
|
|
444
|
+
run(`systemctl --user start ${serviceName}.service`);
|
|
441
445
|
count++;
|
|
442
446
|
continue;
|
|
443
447
|
} else if (proc.type === "interval") {
|
|
@@ -477,14 +481,15 @@ WantedBy=timers.target
|
|
|
477
481
|
const envLine2 = `NEXO_CODE=${nexoCode}`;
|
|
478
482
|
|
|
479
483
|
for (const proc of ALL_PROCESSES) {
|
|
480
|
-
if (proc.type === "runAtLoad") continue; // No cron for runAtLoad
|
|
481
484
|
const sPath = scriptPath(proc);
|
|
482
485
|
const interp = interpreterPath(proc);
|
|
483
486
|
const s = getSchedule(proc);
|
|
484
487
|
const logPath = path.join(logsDir, `${proc.name}-stdout.log`);
|
|
485
488
|
|
|
486
489
|
let cronSpec = "";
|
|
487
|
-
if (proc.type === "
|
|
490
|
+
if (proc.type === "runAtLoad") {
|
|
491
|
+
cronSpec = "@reboot";
|
|
492
|
+
} else if (proc.type === "interval") {
|
|
488
493
|
cronSpec = `*/${proc.intervalMinutes} * * * *`;
|
|
489
494
|
} else if (proc.type === "daily") {
|
|
490
495
|
cronSpec = `${s.minute} ${s.hour} * * *`;
|
|
@@ -617,10 +622,11 @@ async function main() {
|
|
|
617
622
|
"server.py", "plugin_loader.py",
|
|
618
623
|
"knowledge_graph.py", "kg_populate.py", "maintenance.py", "storage_router.py",
|
|
619
624
|
"claim_graph.py", "hnsw_index.py", "evolution_cycle.py", "migrate_embeddings.py",
|
|
620
|
-
"auto_close_sessions.py",
|
|
625
|
+
"auto_close_sessions.py", "auto_update.py",
|
|
621
626
|
"tools_sessions.py", "tools_coordination.py", "tools_reminders.py",
|
|
622
627
|
"tools_reminders_crud.py", "tools_learnings.py", "tools_credentials.py",
|
|
623
628
|
"tools_task_history.py", "tools_menu.py",
|
|
629
|
+
"requirements.txt",
|
|
624
630
|
];
|
|
625
631
|
coreFlatFiles.forEach((f) => {
|
|
626
632
|
const src = path.join(srcDir, f);
|
|
@@ -637,6 +643,30 @@ async function main() {
|
|
|
637
643
|
});
|
|
638
644
|
log(" Core files updated.");
|
|
639
645
|
|
|
646
|
+
// Reconcile Python dependencies after updating code (mirrors fresh-install logic)
|
|
647
|
+
const migReqFile = path.join(srcDir, "requirements.txt");
|
|
648
|
+
if (fs.existsSync(migReqFile)) {
|
|
649
|
+
const migVenvPy = findVenvPython(NEXO_HOME);
|
|
650
|
+
const migPipPy = migVenvPy || "python3";
|
|
651
|
+
const migPipArgs = ["-m", "pip", "install", "--quiet", "-r", migReqFile];
|
|
652
|
+
if (!migVenvPy) migPipArgs.push("--break-system-packages");
|
|
653
|
+
log(" Reconciling Python dependencies...");
|
|
654
|
+
const migPipResult = spawnSync(migPipPy, migPipArgs, { stdio: "inherit", timeout: 120000 });
|
|
655
|
+
if (migPipResult.status !== 0) {
|
|
656
|
+
log(" WARNING: Failed to reconcile Python deps. Rolling back version...");
|
|
657
|
+
// Restore previous version so next boot retries migration
|
|
658
|
+
fs.writeFileSync(versionFile, JSON.stringify({
|
|
659
|
+
version: installedVersion,
|
|
660
|
+
installed_at: installed.installed_at,
|
|
661
|
+
updated_at: new Date().toISOString(),
|
|
662
|
+
migration_failed: currentVersion,
|
|
663
|
+
}, null, 2));
|
|
664
|
+
log(" Run manually: " + migPipPy + " -m pip install -r src/requirements.txt");
|
|
665
|
+
process.exit(1);
|
|
666
|
+
}
|
|
667
|
+
log(" Python dependencies reconciled.");
|
|
668
|
+
}
|
|
669
|
+
|
|
640
670
|
// Update plugins (all .py files in plugins/)
|
|
641
671
|
const pluginsSrc = path.join(srcDir, "plugins");
|
|
642
672
|
const pluginsDest = path.join(NEXO_HOME, "plugins");
|
|
@@ -664,6 +694,14 @@ async function main() {
|
|
|
664
694
|
log(" Rules updated.");
|
|
665
695
|
}
|
|
666
696
|
|
|
697
|
+
// Update crons (manifest.json + sync.py — needed by catchup & watchdog)
|
|
698
|
+
const cronsMigSrc = path.join(srcDir, "crons");
|
|
699
|
+
const cronsMigDest = path.join(NEXO_HOME, "crons");
|
|
700
|
+
if (fs.existsSync(cronsMigSrc)) {
|
|
701
|
+
copyDirRec(cronsMigSrc, cronsMigDest);
|
|
702
|
+
log(" Crons updated.");
|
|
703
|
+
}
|
|
704
|
+
|
|
667
705
|
// Update scripts (all .py, .sh files + subdirectories like deep-sleep/)
|
|
668
706
|
const scriptsSrc = path.join(srcDir, "scripts");
|
|
669
707
|
const scriptsDest = path.join(NEXO_HOME, "scripts");
|
|
@@ -734,6 +772,28 @@ async function main() {
|
|
|
734
772
|
rl.close();
|
|
735
773
|
return;
|
|
736
774
|
}
|
|
775
|
+
|
|
776
|
+
// Same version — backfill crons/ if missing (for installs before crons was shipped)
|
|
777
|
+
const cronsDest = path.join(NEXO_HOME, "crons");
|
|
778
|
+
const cronsSrc = path.join(__dirname, "..", "src", "crons");
|
|
779
|
+
if (!fs.existsSync(path.join(cronsDest, "manifest.json")) && fs.existsSync(cronsSrc)) {
|
|
780
|
+
const copyDirRec2 = (src, dest) => {
|
|
781
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
782
|
+
fs.readdirSync(src).forEach(item => {
|
|
783
|
+
if (item === "__pycache__" || item.endsWith(".pyc") || item.endsWith(".db")) return;
|
|
784
|
+
const srcP = path.join(src, item);
|
|
785
|
+
const destP = path.join(dest, item);
|
|
786
|
+
if (fs.statSync(srcP).isDirectory()) copyDirRec2(srcP, destP);
|
|
787
|
+
else fs.copyFileSync(srcP, destP);
|
|
788
|
+
});
|
|
789
|
+
};
|
|
790
|
+
copyDirRec2(cronsSrc, cronsDest);
|
|
791
|
+
log("Backfilled crons/ directory (catchup & watchdog need it).");
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
log(`Already at v${currentVersion}. No migration needed.`);
|
|
795
|
+
rl.close();
|
|
796
|
+
return;
|
|
737
797
|
} catch (e) {
|
|
738
798
|
// Version file corrupt — proceed with fresh install
|
|
739
799
|
}
|
|
@@ -782,7 +842,7 @@ async function main() {
|
|
|
782
842
|
log("Claude Code not found. Installing...");
|
|
783
843
|
// Try npx first (no sudo needed), then npm -g as fallback
|
|
784
844
|
spawnSync("npx", ["-y", "@anthropic-ai/claude-code", "--version"], { stdio: "pipe", timeout: 60000 });
|
|
785
|
-
claudeInstalled = run("which claude")
|
|
845
|
+
claudeInstalled = run("which claude");
|
|
786
846
|
if (!claudeInstalled) {
|
|
787
847
|
// Fallback: npm -g (may need sudo on Linux)
|
|
788
848
|
const npmCmd = platform === "linux" ? "sudo" : "npm";
|
|
@@ -800,6 +860,15 @@ async function main() {
|
|
|
800
860
|
} else {
|
|
801
861
|
log("Claude Code detected.");
|
|
802
862
|
}
|
|
863
|
+
|
|
864
|
+
// Persist the discovered claude CLI path for scheduled scripts
|
|
865
|
+
const claudeCliPath = run("which claude") || "";
|
|
866
|
+
if (claudeCliPath) {
|
|
867
|
+
const cliPathFile = path.join(NEXO_HOME, "config", "claude-cli-path");
|
|
868
|
+
fs.mkdirSync(path.join(NEXO_HOME, "config"), { recursive: true });
|
|
869
|
+
fs.writeFileSync(cliPathFile, claudeCliPath.trim());
|
|
870
|
+
log(`Claude CLI path saved: ${claudeCliPath.trim()}`);
|
|
871
|
+
}
|
|
803
872
|
console.log("");
|
|
804
873
|
|
|
805
874
|
// Step 1: Language (P1)
|
|
@@ -1236,6 +1305,7 @@ async function main() {
|
|
|
1236
1305
|
"evolution_cycle.py",
|
|
1237
1306
|
"migrate_embeddings.py",
|
|
1238
1307
|
"auto_close_sessions.py",
|
|
1308
|
+
"auto_update.py",
|
|
1239
1309
|
"tools_sessions.py",
|
|
1240
1310
|
"tools_coordination.py",
|
|
1241
1311
|
"tools_reminders.py",
|
|
@@ -1244,6 +1314,7 @@ async function main() {
|
|
|
1244
1314
|
"tools_credentials.py",
|
|
1245
1315
|
"tools_task_history.py",
|
|
1246
1316
|
"tools_menu.py",
|
|
1317
|
+
"requirements.txt",
|
|
1247
1318
|
];
|
|
1248
1319
|
coreFiles.forEach((f) => {
|
|
1249
1320
|
const src = path.join(srcDir, f);
|
|
@@ -1292,6 +1363,13 @@ async function main() {
|
|
|
1292
1363
|
log(" Rules installed.");
|
|
1293
1364
|
}
|
|
1294
1365
|
|
|
1366
|
+
// Crons directory (manifest.json + sync.py — needed by catchup & watchdog)
|
|
1367
|
+
const cronsSrcDir = path.join(srcDir, "crons");
|
|
1368
|
+
if (fs.existsSync(cronsSrcDir)) {
|
|
1369
|
+
copyDirRecursive(cronsSrcDir, path.join(NEXO_HOME, "crons"));
|
|
1370
|
+
log(" Crons installed.");
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1295
1373
|
// Hooks directory
|
|
1296
1374
|
const hooksSrcDir = path.join(srcDir, "hooks");
|
|
1297
1375
|
if (fs.existsSync(hooksSrcDir)) {
|
|
@@ -1792,7 +1870,12 @@ ${doScan ? `- Stack: ${Object.keys(profileData.code.languages || {}).slice(0, 5)
|
|
|
1792
1870
|
// Step 8: Create shell alias so user can just type the operator's name
|
|
1793
1871
|
log("Creating shell alias...");
|
|
1794
1872
|
const aliasName = operatorName.toLowerCase();
|
|
1795
|
-
const
|
|
1873
|
+
const savedCliPath = (() => {
|
|
1874
|
+
const p = path.join(NEXO_HOME, "config", "claude-cli-path");
|
|
1875
|
+
try { return fs.readFileSync(p, "utf8").trim(); } catch { return ""; }
|
|
1876
|
+
})();
|
|
1877
|
+
const claudeBin = savedCliPath || run("which claude") || "claude";
|
|
1878
|
+
const aliasLine = `alias ${aliasName}='${claudeBin} --dangerously-skip-permissions "."'`;
|
|
1796
1879
|
const aliasComment = `# ${operatorName} — start Claude Code with ${operatorName} speaking first`;
|
|
1797
1880
|
|
|
1798
1881
|
// Detect shell and add alias
|
package/bin/postinstall.js
CHANGED
|
@@ -9,32 +9,39 @@
|
|
|
9
9
|
const fs = require("fs");
|
|
10
10
|
const path = require("path");
|
|
11
11
|
|
|
12
|
-
const NEXO_HOME = path.join(require("os").homedir(), ".nexo");
|
|
12
|
+
const NEXO_HOME = process.env.NEXO_HOME || path.join(require("os").homedir(), ".nexo");
|
|
13
13
|
const VERSION_FILE = path.join(NEXO_HOME, "version.json");
|
|
14
14
|
|
|
15
|
+
if (process.env.NEXO_SKIP_POSTINSTALL === "1") {
|
|
16
|
+
// Called during rollback — skip migration to avoid loops
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
if (fs.existsSync(VERSION_FILE)) {
|
|
16
21
|
// Existing installation — run auto-migration silently
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
|
|
22
|
+
const installed = JSON.parse(fs.readFileSync(VERSION_FILE, "utf8"));
|
|
23
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
if (installed.version === pkg.version) {
|
|
26
|
+
// Same version, nothing to do
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
25
29
|
|
|
26
|
-
|
|
30
|
+
console.log(`\n NEXO Brain: upgrading v${installed.version} → v${pkg.version}...`);
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
// Run the main installer in --yes mode (non-interactive)
|
|
33
|
+
// It will detect the existing version and do migration only
|
|
34
|
+
// Let errors propagate so npm reports the failure correctly
|
|
35
|
+
const { execSync } = require("child_process");
|
|
36
|
+
try {
|
|
31
37
|
execSync(`node ${path.join(__dirname, "nexo-brain.js")} --yes`, {
|
|
32
38
|
stdio: "inherit",
|
|
33
|
-
env: { ...process.env, NEXO_POSTINSTALL: "1" }
|
|
39
|
+
env: { ...process.env, NEXO_POSTINSTALL: "1", NEXO_HOME: NEXO_HOME }
|
|
34
40
|
});
|
|
35
41
|
} catch (e) {
|
|
36
|
-
console.error(
|
|
37
|
-
console.
|
|
42
|
+
console.error(`\n NEXO Brain: migration FAILED — ${e.message}`);
|
|
43
|
+
console.error(" Run 'nexo-brain' manually to complete setup.");
|
|
44
|
+
process.exit(1);
|
|
38
45
|
}
|
|
39
46
|
} else {
|
|
40
47
|
// Fresh install — just show instructions
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexo-brain",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.2",
|
|
4
4
|
"mcpName": "io.github.wazionapps/nexo",
|
|
5
5
|
"description": "NEXO — Cognitive co-operator for Claude Code. Memory, emotional intelligence, overnight learning (Deep Sleep), cron management, trust scoring, and adaptive calibration.",
|
|
6
6
|
"bin": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"url": "git+https://github.com/wazionapps/nexo.git"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
|
-
"postinstall": "node bin/postinstall.js
|
|
50
|
+
"postinstall": "node bin/postinstall.js"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|
|
53
53
|
"node": ">=18"
|
|
@@ -56,8 +56,11 @@
|
|
|
56
56
|
"bin/nexo-brain.js",
|
|
57
57
|
"bin/postinstall.js",
|
|
58
58
|
"src/",
|
|
59
|
+
"!src/**/__pycache__",
|
|
60
|
+
"!src/**/*.pyc",
|
|
61
|
+
"!src/**/*.pyo",
|
|
59
62
|
"templates/",
|
|
60
|
-
"
|
|
61
|
-
"
|
|
63
|
+
"README.md",
|
|
64
|
+
"LICENSE"
|
|
62
65
|
]
|
|
63
66
|
}
|
package/src/auto_update.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
"""NEXO Auto-Update — lightweight startup check for git updates and file-based migrations.
|
|
2
3
|
|
|
3
4
|
Called once per server startup. Respects a 1-hour cooldown to avoid redundant checks.
|
|
@@ -95,6 +96,130 @@ def _read_package_version() -> str:
|
|
|
95
96
|
|
|
96
97
|
# ── Hook sync ────────────────────────────────────────────────────────
|
|
97
98
|
|
|
99
|
+
def _requirements_hash() -> str:
|
|
100
|
+
"""Return a content hash of requirements.txt, or empty string if missing."""
|
|
101
|
+
import hashlib
|
|
102
|
+
req_file = SRC_DIR / "requirements.txt"
|
|
103
|
+
if req_file.exists():
|
|
104
|
+
return hashlib.sha256(req_file.read_bytes()).hexdigest()
|
|
105
|
+
return ""
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _reinstall_pip_deps() -> bool:
|
|
109
|
+
"""Reinstall Python deps from requirements.txt. Returns True on success."""
|
|
110
|
+
req_file = SRC_DIR / "requirements.txt"
|
|
111
|
+
if not req_file.exists():
|
|
112
|
+
return True
|
|
113
|
+
venv_pip = NEXO_HOME / ".venv" / "bin" / "pip"
|
|
114
|
+
if not venv_pip.exists():
|
|
115
|
+
venv_pip = NEXO_HOME / ".venv" / "bin" / "pip3"
|
|
116
|
+
try:
|
|
117
|
+
if venv_pip.exists():
|
|
118
|
+
result = subprocess.run(
|
|
119
|
+
[str(venv_pip), "install", "--quiet", "-r", str(req_file)],
|
|
120
|
+
capture_output=True, text=True, timeout=120,
|
|
121
|
+
)
|
|
122
|
+
else:
|
|
123
|
+
result = subprocess.run(
|
|
124
|
+
[sys.executable, "-m", "pip", "install", "--quiet", "-r", str(req_file), "--break-system-packages"],
|
|
125
|
+
capture_output=True, text=True, timeout=120,
|
|
126
|
+
)
|
|
127
|
+
if result.returncode != 0:
|
|
128
|
+
_log(f"pip install failed (exit {result.returncode}): {result.stderr or result.stdout}")
|
|
129
|
+
return False
|
|
130
|
+
_log("Reinstalled Python dependencies after update")
|
|
131
|
+
return True
|
|
132
|
+
except Exception as e:
|
|
133
|
+
_log(f"pip reinstall failed: {e}")
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _refresh_installed_manifest():
|
|
138
|
+
"""Copy source crons/ to NEXO_HOME/crons/ so catchup & watchdog stay current."""
|
|
139
|
+
try:
|
|
140
|
+
import shutil
|
|
141
|
+
src_crons = SRC_DIR / "crons"
|
|
142
|
+
dst_crons = NEXO_HOME / "crons"
|
|
143
|
+
if src_crons.exists():
|
|
144
|
+
dst_crons.mkdir(parents=True, exist_ok=True)
|
|
145
|
+
for f in src_crons.iterdir():
|
|
146
|
+
if f.is_file():
|
|
147
|
+
shutil.copy2(str(f), str(dst_crons / f.name))
|
|
148
|
+
_log("Refreshed installed crons manifest")
|
|
149
|
+
except Exception as e:
|
|
150
|
+
_log(f"Manifest refresh warning: {e}")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _sync_crons():
|
|
154
|
+
"""Sync cron definitions with manifest after a git pull."""
|
|
155
|
+
try:
|
|
156
|
+
cron_sync_path = SRC_DIR / "crons" / "sync.py"
|
|
157
|
+
if cron_sync_path.exists():
|
|
158
|
+
result = subprocess.run(
|
|
159
|
+
[sys.executable, str(cron_sync_path)],
|
|
160
|
+
capture_output=True, text=True, timeout=30,
|
|
161
|
+
env={**os.environ, "NEXO_HOME": str(NEXO_HOME), "NEXO_CODE": str(SRC_DIR)},
|
|
162
|
+
)
|
|
163
|
+
if result.returncode != 0:
|
|
164
|
+
_log(f"Cron sync failed (exit {result.returncode}): {result.stderr or result.stdout}")
|
|
165
|
+
return # Don't refresh manifest if timers weren't actually updated
|
|
166
|
+
_log("Synced cron definitions with manifest")
|
|
167
|
+
# Refresh the installed manifest only after successful sync
|
|
168
|
+
_refresh_installed_manifest()
|
|
169
|
+
except Exception as e:
|
|
170
|
+
_log(f"Cron sync warning: {e}")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _backup_dbs() -> str | None:
|
|
174
|
+
"""Snapshot all .db files before migration. Returns backup dir or None."""
|
|
175
|
+
import sqlite3
|
|
176
|
+
import time as _time
|
|
177
|
+
timestamp = _time.strftime("%Y-%m-%d-%H%M%S")
|
|
178
|
+
backup_dir = NEXO_HOME / "backups" / f"pre-autoupdate-{timestamp}"
|
|
179
|
+
|
|
180
|
+
db_files = list(DATA_DIR.glob("*.db")) if DATA_DIR.is_dir() else []
|
|
181
|
+
db_files += [f for f in NEXO_HOME.glob("*.db") if f.is_file()]
|
|
182
|
+
src_db = SRC_DIR / "nexo.db"
|
|
183
|
+
if src_db.is_file() and src_db not in db_files:
|
|
184
|
+
db_files.append(src_db)
|
|
185
|
+
|
|
186
|
+
if not db_files:
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
190
|
+
for db_file in db_files:
|
|
191
|
+
try:
|
|
192
|
+
src_conn = sqlite3.connect(str(db_file))
|
|
193
|
+
dst_conn = sqlite3.connect(str(backup_dir / db_file.name))
|
|
194
|
+
src_conn.backup(dst_conn)
|
|
195
|
+
dst_conn.close()
|
|
196
|
+
src_conn.close()
|
|
197
|
+
except Exception as e:
|
|
198
|
+
_log(f"DB backup warning ({db_file.name}): {e}")
|
|
199
|
+
return str(backup_dir)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _restore_dbs(backup_dir: str):
|
|
203
|
+
"""Restore .db files from a backup directory."""
|
|
204
|
+
import sqlite3
|
|
205
|
+
bdir = Path(backup_dir)
|
|
206
|
+
if not bdir.is_dir():
|
|
207
|
+
return
|
|
208
|
+
for db_backup in bdir.glob("*.db"):
|
|
209
|
+
for candidate in [DATA_DIR / db_backup.name, NEXO_HOME / db_backup.name, SRC_DIR / db_backup.name]:
|
|
210
|
+
if candidate.is_file():
|
|
211
|
+
try:
|
|
212
|
+
src_conn = sqlite3.connect(str(db_backup))
|
|
213
|
+
dst_conn = sqlite3.connect(str(candidate))
|
|
214
|
+
src_conn.backup(dst_conn)
|
|
215
|
+
dst_conn.close()
|
|
216
|
+
src_conn.close()
|
|
217
|
+
_log(f"Restored DB: {db_backup.name}")
|
|
218
|
+
except Exception as e:
|
|
219
|
+
_log(f"DB restore warning ({db_backup.name}): {e}")
|
|
220
|
+
break
|
|
221
|
+
|
|
222
|
+
|
|
98
223
|
def _sync_hooks():
|
|
99
224
|
"""Copy hook scripts from src/hooks/ to NEXO_HOME/hooks/ after a git pull."""
|
|
100
225
|
import shutil
|
|
@@ -151,27 +276,89 @@ def _check_git_updates() -> str | None:
|
|
|
151
276
|
|
|
152
277
|
# We're behind — safe to fast-forward pull
|
|
153
278
|
old_version = _read_package_version()
|
|
279
|
+
old_req_hash = _requirements_hash()
|
|
280
|
+
|
|
281
|
+
# Save old HEAD for rollback
|
|
282
|
+
rc, old_head, _ = _git("rev-parse", "HEAD")
|
|
283
|
+
if rc != 0:
|
|
284
|
+
return None
|
|
285
|
+
|
|
154
286
|
rc, pull_out, pull_err = _git("pull", "--ff-only")
|
|
155
287
|
if rc != 0:
|
|
156
288
|
_log(f"git pull --ff-only failed: {pull_err}")
|
|
157
289
|
return None # Don't break anything
|
|
158
290
|
|
|
159
291
|
new_version = _read_package_version()
|
|
292
|
+
new_req_hash = _requirements_hash()
|
|
293
|
+
|
|
294
|
+
# Backup databases before any changes that might run migrations
|
|
295
|
+
db_backup_dir = _backup_dbs()
|
|
296
|
+
|
|
297
|
+
# Reinstall pip deps if requirements.txt content changed (not just version)
|
|
298
|
+
if old_req_hash != new_req_hash:
|
|
299
|
+
if not _reinstall_pip_deps():
|
|
300
|
+
# pip failed — rollback git + DBs to old HEAD
|
|
301
|
+
_log("pip install failed after pull, rolling back git...")
|
|
302
|
+
_git("reset", "--hard", old_head)
|
|
303
|
+
_reinstall_pip_deps() # restore old deps (best-effort)
|
|
304
|
+
if db_backup_dir:
|
|
305
|
+
_restore_dbs(db_backup_dir)
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
# Verify the new code can be imported before proceeding
|
|
309
|
+
if not _verify_import():
|
|
310
|
+
_log("Import verification failed after pull, rolling back git...")
|
|
311
|
+
_git("reset", "--hard", old_head)
|
|
312
|
+
if old_req_hash != new_req_hash:
|
|
313
|
+
_reinstall_pip_deps() # restore old deps (best-effort)
|
|
314
|
+
if db_backup_dir:
|
|
315
|
+
_restore_dbs(db_backup_dir)
|
|
316
|
+
return None
|
|
160
317
|
|
|
161
|
-
# Run DB migrations after pull
|
|
162
|
-
_run_db_migrations()
|
|
318
|
+
# Run DB migrations after pull — rollback if they fail
|
|
319
|
+
if not _run_db_migrations():
|
|
320
|
+
_log("DB migration failed after pull, rolling back git + DB...")
|
|
321
|
+
_git("reset", "--hard", old_head)
|
|
322
|
+
if old_req_hash != new_req_hash:
|
|
323
|
+
_reinstall_pip_deps()
|
|
324
|
+
if db_backup_dir:
|
|
325
|
+
_restore_dbs(db_backup_dir)
|
|
326
|
+
return None
|
|
163
327
|
|
|
164
328
|
# Sync hooks to NEXO_HOME (nexo-brain.js copies them on install,
|
|
165
329
|
# but auto-update via git pull bypasses nexo-brain.js)
|
|
166
330
|
_sync_hooks()
|
|
167
331
|
|
|
332
|
+
# Sync cron definitions with manifest
|
|
333
|
+
_sync_crons()
|
|
334
|
+
|
|
168
335
|
msg = f"Auto-updated: {old_version} -> {new_version}" if old_version != new_version else f"Auto-updated (v{new_version}, new commits)"
|
|
169
336
|
_log(msg)
|
|
170
337
|
return msg
|
|
171
338
|
|
|
172
339
|
|
|
173
|
-
def
|
|
174
|
-
"""
|
|
340
|
+
def _verify_import() -> bool:
|
|
341
|
+
"""Verify that the new code can be imported. Returns True on success."""
|
|
342
|
+
try:
|
|
343
|
+
result = subprocess.run(
|
|
344
|
+
[sys.executable, "-c", "import server"],
|
|
345
|
+
cwd=str(SRC_DIR),
|
|
346
|
+
capture_output=True,
|
|
347
|
+
text=True,
|
|
348
|
+
timeout=15,
|
|
349
|
+
)
|
|
350
|
+
if result.returncode != 0:
|
|
351
|
+
_log(f"Import verification failed: {result.stderr or result.stdout}")
|
|
352
|
+
return False
|
|
353
|
+
return True
|
|
354
|
+
except Exception as e:
|
|
355
|
+
_log(f"Import verification error: {e}")
|
|
356
|
+
return False
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _run_db_migrations() -> bool:
|
|
360
|
+
"""Run NEXO's DB schema migrations (from db._schema) after a pull.
|
|
361
|
+
Returns True on success, False on failure."""
|
|
175
362
|
try:
|
|
176
363
|
from db._schema import run_migrations
|
|
177
364
|
from db._core import get_db
|
|
@@ -179,8 +366,10 @@ def _run_db_migrations():
|
|
|
179
366
|
applied = run_migrations(conn)
|
|
180
367
|
if applied > 0:
|
|
181
368
|
_log(f"Applied {applied} DB migration(s)")
|
|
369
|
+
return True
|
|
182
370
|
except Exception as e:
|
|
183
|
-
_log(f"DB migration error
|
|
371
|
+
_log(f"DB migration error: {e}")
|
|
372
|
+
return False
|
|
184
373
|
|
|
185
374
|
|
|
186
375
|
# ── npm version check (notify only) ─────────────────────────────────
|
package/src/crons/sync.py
CHANGED
|
@@ -46,8 +46,7 @@ def _copy_script_to_nexo_home(src: Path) -> Path:
|
|
|
46
46
|
"""Copy a script from NEXO_CODE to NEXO_HOME/scripts/ for Sandbox compatibility.
|
|
47
47
|
|
|
48
48
|
macOS Sandbox blocks LaunchAgents from executing scripts in ~/Documents/.
|
|
49
|
-
We copy scripts to NEXO_HOME/scripts/ which is
|
|
50
|
-
or ~/.nexo/scripts/ — both outside the Sandbox restricted paths.
|
|
49
|
+
We copy scripts to NEXO_HOME/scripts/ which is outside the Sandbox restricted paths.
|
|
51
50
|
"""
|
|
52
51
|
dest_dir = NEXO_HOME / "scripts"
|
|
53
52
|
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
@@ -297,6 +296,9 @@ def sync_linux(dry_run: bool = False):
|
|
|
297
296
|
service_path = unit_dir / f"nexo-{cron_id}.service"
|
|
298
297
|
timer_path = unit_dir / f"nexo-{cron_id}.timer"
|
|
299
298
|
|
|
299
|
+
stdout_log = LOG_DIR / f"{cron_id}-stdout.log"
|
|
300
|
+
stderr_log = LOG_DIR / f"{cron_id}-stderr.log"
|
|
301
|
+
|
|
300
302
|
service_content = f"""[Unit]
|
|
301
303
|
Description=NEXO: {cron.get('description', cron_id)}
|
|
302
304
|
|
|
@@ -306,6 +308,8 @@ ExecStart={exec_cmd}
|
|
|
306
308
|
Environment=NEXO_HOME={NEXO_HOME}
|
|
307
309
|
Environment=NEXO_CODE={NEXO_CODE}
|
|
308
310
|
Environment=HOME={Path.home()}
|
|
311
|
+
StandardOutput=append:{stdout_log}
|
|
312
|
+
StandardError=append:{stderr_log}
|
|
309
313
|
"""
|
|
310
314
|
|
|
311
315
|
if cron.get("run_at_load"):
|
package/src/db/_core.py
CHANGED
package/src/db/_entities.py
CHANGED
package/src/db/_episodic.py
CHANGED
package/src/db/_learnings.py
CHANGED
package/src/db/_reminders.py
CHANGED
package/src/db/_schema.py
CHANGED
|
@@ -399,6 +399,7 @@ def run_migrations(conn=None):
|
|
|
399
399
|
|
|
400
400
|
applied = {r[0] for r in conn.execute("SELECT version FROM schema_migrations").fetchall()}
|
|
401
401
|
|
|
402
|
+
failed = []
|
|
402
403
|
for version, name, fn in MIGRATIONS:
|
|
403
404
|
if version not in applied:
|
|
404
405
|
try:
|
|
@@ -409,9 +410,18 @@ def run_migrations(conn=None):
|
|
|
409
410
|
)
|
|
410
411
|
conn.commit()
|
|
411
412
|
except Exception as e:
|
|
412
|
-
|
|
413
|
+
conn.rollback()
|
|
413
414
|
import sys
|
|
414
415
|
print(f"[MIGRATION] v{version} ({name}) failed: {e}", file=sys.stderr)
|
|
416
|
+
failed.append((version, name, str(e)))
|
|
417
|
+
# Stop on first failure — don't run subsequent migrations
|
|
418
|
+
# against a potentially inconsistent schema
|
|
419
|
+
break
|
|
420
|
+
|
|
421
|
+
if failed:
|
|
422
|
+
raise RuntimeError(
|
|
423
|
+
f"Migration failed: v{failed[0][0]} ({failed[0][1]}): {failed[0][2]}"
|
|
424
|
+
)
|
|
415
425
|
|
|
416
426
|
return len(MIGRATIONS) - len(applied)
|
|
417
427
|
|
package/src/db/_sessions.py
CHANGED
package/src/db/_skills.py
CHANGED