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.
Files changed (299) hide show
  1. package/README.md +1 -1
  2. package/bin/nexo-brain.js +92 -9
  3. package/bin/postinstall.js +22 -15
  4. package/package.json +7 -4
  5. package/src/auto_update.py +194 -5
  6. package/src/crons/sync.py +6 -2
  7. package/src/db/_core.py +1 -0
  8. package/src/db/_entities.py +1 -0
  9. package/src/db/_episodic.py +1 -0
  10. package/src/db/_learnings.py +1 -0
  11. package/src/db/_reminders.py +1 -0
  12. package/src/db/_schema.py +11 -1
  13. package/src/db/_sessions.py +1 -0
  14. package/src/db/_skills.py +1 -0
  15. package/src/hooks/capture-tool-logs.sh +23 -6
  16. package/src/hooks/session-start.sh +4 -3
  17. package/src/plugin_loader.py +1 -0
  18. package/src/plugins/update.py +377 -26
  19. package/src/scripts/deep-sleep/apply_findings.py +1 -0
  20. package/src/scripts/deep-sleep/collect.py +1 -0
  21. package/src/scripts/deep-sleep/extract.py +1 -0
  22. package/src/scripts/deep-sleep/synthesize.py +1 -0
  23. package/src/scripts/nexo-catchup.py +29 -4
  24. package/src/scripts/nexo-daily-self-audit.py +21 -1
  25. package/src/scripts/nexo-evolution-run.py +21 -1
  26. package/src/scripts/nexo-learning-housekeep.py +1 -0
  27. package/src/scripts/nexo-postmortem-consolidator.py +34 -9
  28. package/src/scripts/nexo-sleep.py +32 -10
  29. package/src/scripts/nexo-synthesis.py +29 -9
  30. package/src/scripts/nexo-update.sh +109 -7
  31. package/src/scripts/nexo-watchdog.sh +122 -58
  32. package/src/server.py +66 -1
  33. package/src/tools_coordination.py +1 -0
  34. package/src/tools_sessions.py +1 -0
  35. package/scripts/migrate-to-unified 2.sh +0 -813
  36. package/scripts/migrate-to-unified.sh +0 -813
  37. package/scripts/migrate-v1.5-to-v1.6 2.py +0 -778
  38. package/scripts/migrate-v1.5-to-v1.6.py +0 -778
  39. package/scripts/migrate-v1.7-to-v1.8 2.py +0 -214
  40. package/scripts/migrate-v1.7-to-v1.8.py +0 -214
  41. package/scripts/nexo-preflight.sh +0 -236
  42. package/scripts/pre-commit-check 2.sh +0 -55
  43. package/scripts/pre-commit-check.sh +0 -55
  44. package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
  45. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  46. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  47. package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
  48. package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
  49. package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
  50. package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
  51. package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
  52. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  53. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  54. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  55. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  56. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  57. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  58. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  59. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  60. package/src/auto_close_sessions 2.py +0 -159
  61. package/src/auto_update 2.py +0 -634
  62. package/src/claim_graph 2.py +0 -323
  63. package/src/cognitive/__init__ 2.py +0 -62
  64. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  65. package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
  66. package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  67. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  68. package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
  69. package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
  70. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  71. package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
  72. package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
  73. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  74. package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
  75. package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
  76. package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
  77. package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
  78. package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
  79. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  80. package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
  81. package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
  82. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  83. package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
  84. package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
  85. package/src/cognitive/_core 2.py +0 -567
  86. package/src/cognitive/_decay 2.py +0 -382
  87. package/src/cognitive/_ingest 2.py +0 -892
  88. package/src/cognitive/_memory 2.py +0 -912
  89. package/src/cognitive/_search 2.py +0 -949
  90. package/src/cognitive/_trust 2.py +0 -464
  91. package/src/crons/__pycache__/sync.cpython-314.pyc +0 -0
  92. package/src/crons/manifest 2.json +0 -106
  93. package/src/crons/sync 2.py +0 -217
  94. package/src/dashboard/__init__ 2.py +0 -0
  95. package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
  96. package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
  97. package/src/dashboard/app 2.py +0 -789
  98. package/src/db/__init__ 2.py +0 -89
  99. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  100. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  101. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  102. package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
  103. package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
  104. package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
  105. package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
  106. package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
  107. package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
  108. package/src/db/__pycache__/_cron_runs.cpython-310.pyc +0 -0
  109. package/src/db/__pycache__/_cron_runs.cpython-314.pyc +0 -0
  110. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  111. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  112. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  113. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  114. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  115. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  116. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  117. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  118. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  119. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  120. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  121. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  122. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  123. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  124. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  125. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  126. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  127. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  128. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  129. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  130. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  131. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  132. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  133. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  134. package/src/db/__pycache__/_skills.cpython-310.pyc +0 -0
  135. package/src/db/__pycache__/_skills.cpython-312.pyc +0 -0
  136. package/src/db/__pycache__/_skills.cpython-314.pyc +0 -0
  137. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  138. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  139. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  140. package/src/db/_core 2.py +0 -417
  141. package/src/db/_credentials 2.py +0 -124
  142. package/src/db/_entities 2.py +0 -178
  143. package/src/db/_episodic 2.py +0 -738
  144. package/src/db/_evolution 2.py +0 -54
  145. package/src/db/_fts 2.py +0 -406
  146. package/src/db/_learnings 2.py +0 -168
  147. package/src/db/_reminders 2.py +0 -338
  148. package/src/db/_schema 2.py +0 -364
  149. package/src/db/_sessions 2.py +0 -300
  150. package/src/db/_tasks 2.py +0 -91
  151. package/src/evolution_cycle 2.py +0 -266
  152. package/src/hnsw_index 2.py +0 -254
  153. package/src/hooks/auto_capture 2.py +0 -208
  154. package/src/hooks/caffeinate-guard 2.sh +0 -8
  155. package/src/hooks/capture-session 2.sh +0 -21
  156. package/src/hooks/capture-tool-logs 2.sh +0 -127
  157. package/src/hooks/daily-briefing-check 2.sh +0 -33
  158. package/src/hooks/inbox-hook 2.sh +0 -76
  159. package/src/hooks/post-compact 2.sh +0 -148
  160. package/src/hooks/pre-compact 2.sh +0 -151
  161. package/src/hooks/session-start 2.sh +0 -268
  162. package/src/hooks/session-stop 2.sh +0 -140
  163. package/src/kg_populate 2.py +0 -290
  164. package/src/knowledge_graph 2.py +0 -257
  165. package/src/maintenance 2.py +0 -59
  166. package/src/migrate_embeddings 2.py +0 -122
  167. package/src/plugin_loader 2.py +0 -202
  168. package/src/plugins/__init__ 2.py +0 -0
  169. package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
  170. package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
  171. package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
  172. package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
  173. package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
  174. package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
  175. package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
  176. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  177. package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
  178. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  179. package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
  180. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  181. package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
  182. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  183. package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
  184. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  185. package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
  186. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  187. package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
  188. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  189. package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
  190. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  191. package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
  192. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  193. package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
  194. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  195. package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
  196. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  197. package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
  198. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  199. package/src/plugins/__pycache__/schedule.cpython-310.pyc +0 -0
  200. package/src/plugins/__pycache__/schedule.cpython-314.pyc +0 -0
  201. package/src/plugins/__pycache__/skills.cpython-310.pyc +0 -0
  202. package/src/plugins/__pycache__/skills.cpython-314.pyc +0 -0
  203. package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
  204. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  205. package/src/plugins/adaptive_mode 2.py +0 -805
  206. package/src/plugins/agents 2.py +0 -52
  207. package/src/plugins/artifact_registry 2.py +0 -450
  208. package/src/plugins/backup 2.py +0 -104
  209. package/src/plugins/cognitive_memory 2.py +0 -564
  210. package/src/plugins/core_rules 2.py +0 -252
  211. package/src/plugins/cortex 2.py +0 -299
  212. package/src/plugins/entities 2.py +0 -67
  213. package/src/plugins/episodic_memory 2.py +0 -533
  214. package/src/plugins/evolution 2.py +0 -115
  215. package/src/plugins/guard 2.py +0 -746
  216. package/src/plugins/knowledge_graph_tools 2.py +0 -105
  217. package/src/plugins/preferences 2.py +0 -47
  218. package/src/plugins/update 2.py +0 -256
  219. package/src/requirements 2.txt +0 -12
  220. package/src/rules/__init__ 2.py +0 -0
  221. package/src/rules/core-rules 2.json +0 -331
  222. package/src/rules/migrate 2.py +0 -207
  223. package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
  224. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  225. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  226. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  227. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  228. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
  229. package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
  230. package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
  231. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
  232. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  233. package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
  234. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
  235. package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
  236. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
  237. package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
  238. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
  239. package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
  240. package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
  241. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  242. package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
  243. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
  244. package/src/scripts/check-context 2.py +0 -264
  245. package/src/scripts/nexo-auto-update 2.py +0 -6
  246. package/src/scripts/nexo-backup 2.sh +0 -25
  247. package/src/scripts/nexo-brain-activation 2.sh +0 -140
  248. package/src/scripts/nexo-catchup 2.py +0 -242
  249. package/src/scripts/nexo-cognitive-decay 2.py +0 -182
  250. package/src/scripts/nexo-daily-self-audit 2.py +0 -552
  251. package/src/scripts/nexo-deep-sleep 2.sh +0 -97
  252. package/src/scripts/nexo-evolution-run 2.py +0 -597
  253. package/src/scripts/nexo-followup-hygiene 2.py +0 -112
  254. package/src/scripts/nexo-github-monitor 2.py +0 -256
  255. package/src/scripts/nexo-immune 2.py +0 -927
  256. package/src/scripts/nexo-inbox-hook 2.sh +0 -74
  257. package/src/scripts/nexo-install 2.py +0 -6
  258. package/src/scripts/nexo-learning-housekeep 2.py +0 -245
  259. package/src/scripts/nexo-learning-validator 2.py +0 -207
  260. package/src/scripts/nexo-migrate 2.py +0 -232
  261. package/src/scripts/nexo-postmortem-consolidator 2.py +0 -421
  262. package/src/scripts/nexo-pre-commit 2.py +0 -120
  263. package/src/scripts/nexo-prevent-sleep 2.sh +0 -29
  264. package/src/scripts/nexo-proactive-dashboard 2.py +0 -345
  265. package/src/scripts/nexo-reflection 2.py +0 -253
  266. package/src/scripts/nexo-runtime-preflight 2.py +0 -274
  267. package/src/scripts/nexo-send-email 2.py +0 -25
  268. package/src/scripts/nexo-send-email.py +0 -25
  269. package/src/scripts/nexo-send-reply 2.py +0 -178
  270. package/src/scripts/nexo-send-reply.py +0 -178
  271. package/src/scripts/nexo-sleep 2.py +0 -592
  272. package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
  273. package/src/scripts/nexo-synthesis 2.py +0 -253
  274. package/src/scripts/nexo-tcc-approve 2.sh +0 -79
  275. package/src/scripts/nexo-update 2.sh +0 -161
  276. package/src/scripts/nexo-watchdog 2.sh +0 -878
  277. package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
  278. package/src/server 2.py +0 -733
  279. package/src/storage_router 2.py +0 -32
  280. package/src/tools_coordination 2.py +0 -102
  281. package/src/tools_credentials 2.py +0 -68
  282. package/src/tools_learnings 2.py +0 -220
  283. package/src/tools_menu 2.py +0 -227
  284. package/src/tools_reminders 2.py +0 -86
  285. package/src/tools_reminders_crud 2.py +0 -159
  286. package/src/tools_sessions 2.py +0 -476
  287. package/src/tools_task_history 2.py +0 -57
  288. package/templates/CLAUDE.md 2.template +0 -63
  289. package/templates/openclaw 2.json +0 -13
  290. package/tests/__init__ 2.py +0 -0
  291. package/tests/__init__.py +0 -0
  292. package/tests/conftest 2.py +0 -71
  293. package/tests/conftest.py +0 -71
  294. package/tests/test_cognitive 2.py +0 -205
  295. package/tests/test_cognitive.py +0 -205
  296. package/tests/test_knowledge_graph 2.py +0 -140
  297. package/tests/test_knowledge_graph.py +0 -140
  298. package/tests/test_migrations 2.py +0 -137
  299. 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. Zero personal data in the repo.
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 = path.join(__dirname, "..");
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
- // No timer for runAtLoad runs via MCP startup
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 === "interval") {
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") || run("npx -y @anthropic-ai/claude-code --version");
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 aliasLine = `alias ${aliasName}='claude --dangerously-skip-permissions "."'`;
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
@@ -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
- try {
18
- const installed = JSON.parse(fs.readFileSync(VERSION_FILE, "utf8"));
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
- if (installed.version === pkg.version) {
22
- // Same version, nothing to do
23
- process.exit(0);
24
- }
25
+ if (installed.version === pkg.version) {
26
+ // Same version, nothing to do
27
+ process.exit(0);
28
+ }
25
29
 
26
- console.log(`\n NEXO Brain: upgrading v${installed.version} → v${pkg.version}...`);
30
+ console.log(`\n NEXO Brain: upgrading v${installed.version} → v${pkg.version}...`);
27
31
 
28
- // Run the main installer in --yes mode (non-interactive)
29
- // It will detect the existing version and do migration only
30
- const { execSync } = require("child_process");
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(` NEXO Brain: migration warning — ${e.message}`);
37
- console.log(" Run 'nexo-brain' manually to complete setup.");
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.0",
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 2>/dev/null || true"
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
- "scripts/",
61
- "tests/"
63
+ "README.md",
64
+ "LICENSE"
62
65
  ]
63
66
  }
@@ -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 _run_db_migrations():
174
- """Run NEXO's DB schema migrations (from db._schema) after a pull."""
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 (continuing): {e}")
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 typically ~/claude/scripts/
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
@@ -1,3 +1,4 @@
1
+ from __future__ import annotations
1
2
  """SQLite database for NEXO session coordination."""
2
3
 
3
4
  import sqlite3
@@ -1,3 +1,4 @@
1
+ from __future__ import annotations
1
2
  """NEXO DB — Entities module."""
2
3
  import time
3
4
  from db._core import get_db, _multi_word_like
@@ -1,3 +1,4 @@
1
+ from __future__ import annotations
1
2
  """NEXO DB — Episodic module."""
2
3
  import datetime, time, json
3
4
  from db._core import get_db, now_epoch, _multi_word_like
@@ -1,3 +1,4 @@
1
+ from __future__ import annotations
1
2
  """NEXO DB — Learnings module."""
2
3
  import re, time
3
4
  from db._core import get_db, now_epoch
@@ -1,3 +1,4 @@
1
+ from __future__ import annotations
1
2
  """NEXO DB — Reminders module."""
2
3
  import sqlite3, time, datetime
3
4
  from datetime import timedelta
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
- # Log but don't crash — partial migration is better than no server
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
 
@@ -1,3 +1,4 @@
1
+ from __future__ import annotations
1
2
  """NEXO DB — Sessions module."""
2
3
  import time, secrets, string, sqlite3
3
4
  from datetime import datetime
package/src/db/_skills.py CHANGED
@@ -1,3 +1,4 @@
1
+ from __future__ import annotations
1
2
  """NEXO DB — Skills module.
2
3
 
3
4
  Skill Auto-Creation system: reusable procedures extracted from complex tasks.