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
@@ -1,634 +0,0 @@
1
- """NEXO Auto-Update — lightweight startup check for git updates and file-based migrations.
2
-
3
- Called once per server startup. Respects a 1-hour cooldown to avoid redundant checks.
4
- Never blocks startup for more than 5 seconds. Logs errors and continues on failure.
5
-
6
- This is separate from plugins/update.py which handles MANUAL updates with rollback.
7
- """
8
-
9
- import json
10
- import os
11
- import re
12
- import subprocess
13
- import sys
14
- import time
15
- from pathlib import Path
16
-
17
- NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
18
- DATA_DIR = NEXO_HOME / "data"
19
- DATA_DIR.mkdir(parents=True, exist_ok=True)
20
-
21
- # Repo root: go up from src/
22
- SRC_DIR = Path(__file__).resolve().parent
23
- REPO_DIR = SRC_DIR.parent
24
-
25
- LAST_CHECK_FILE = DATA_DIR / "auto_update_last_check.json"
26
- MIGRATION_VERSION_FILE = DATA_DIR / "migration_version"
27
- CLAUDE_MD_VERSION_FILE = DATA_DIR / "claude_md_version.txt"
28
- MIGRATIONS_DIR = REPO_DIR / "migrations"
29
- TEMPLATE_FILE = REPO_DIR / "templates" / "CLAUDE.md.template"
30
-
31
- CHECK_COOLDOWN_SECONDS = 3600 # 1 hour
32
- GIT_TIMEOUT_SECONDS = 4 # stay well under the 5s total budget
33
-
34
-
35
- def _log(msg: str):
36
- """Log to stderr with prefix."""
37
- print(f"[NEXO auto-update] {msg}", file=sys.stderr)
38
-
39
-
40
- def _read_last_check() -> dict:
41
- """Read last check state from disk."""
42
- try:
43
- if LAST_CHECK_FILE.exists():
44
- return json.loads(LAST_CHECK_FILE.read_text())
45
- except Exception:
46
- pass
47
- return {}
48
-
49
-
50
- def _write_last_check(data: dict):
51
- """Persist last check state."""
52
- try:
53
- LAST_CHECK_FILE.write_text(json.dumps(data))
54
- except Exception as e:
55
- _log(f"Failed to write last-check file: {e}")
56
-
57
-
58
- def _is_git_repo() -> bool:
59
- """Check if REPO_DIR is inside a git repository."""
60
- try:
61
- result = subprocess.run(
62
- ["git", "rev-parse", "--is-inside-work-tree"],
63
- cwd=str(REPO_DIR),
64
- capture_output=True,
65
- text=True,
66
- timeout=GIT_TIMEOUT_SECONDS,
67
- )
68
- return result.returncode == 0 and result.stdout.strip() == "true"
69
- except Exception:
70
- return False
71
-
72
-
73
- def _git(*args) -> tuple[int, str, str]:
74
- """Run a git command in REPO_DIR. Returns (returncode, stdout, stderr)."""
75
- result = subprocess.run(
76
- ["git"] + list(args),
77
- cwd=str(REPO_DIR),
78
- capture_output=True,
79
- text=True,
80
- timeout=GIT_TIMEOUT_SECONDS,
81
- )
82
- return result.returncode, result.stdout.strip(), result.stderr.strip()
83
-
84
-
85
- def _read_package_version() -> str:
86
- """Read version from package.json."""
87
- try:
88
- pkg = REPO_DIR / "package.json"
89
- if pkg.exists():
90
- return json.loads(pkg.read_text()).get("version", "unknown")
91
- except Exception:
92
- pass
93
- return "unknown"
94
-
95
-
96
- # ── Git-based auto-update ────────────────────────────────────────────
97
-
98
- def _check_git_updates() -> str | None:
99
- """Fetch remote, compare HEAD, pull if behind. Returns status message or None."""
100
- # Fetch (allow it to fail silently on network issues)
101
- rc, _, fetch_err = _git("fetch", "--quiet")
102
- if rc != 0:
103
- _log(f"git fetch failed (network?): {fetch_err}")
104
- return None # Can't check, skip silently
105
-
106
- # Compare local HEAD vs remote tracking branch
107
- rc, local_head, _ = _git("rev-parse", "HEAD")
108
- if rc != 0:
109
- return None
110
- rc, remote_head, _ = _git("rev-parse", "@{u}")
111
- if rc != 0:
112
- # No upstream configured — skip
113
- return None
114
-
115
- if local_head == remote_head:
116
- return None # Already up to date
117
-
118
- # Check if we're behind (remote has commits we don't)
119
- rc, merge_base, _ = _git("merge-base", "HEAD", "@{u}")
120
- if rc != 0:
121
- return None
122
-
123
- if merge_base == remote_head:
124
- # Local is AHEAD — don't pull
125
- return None
126
- if merge_base != local_head and merge_base != remote_head:
127
- # Diverged — don't auto-pull, too risky
128
- _log("Local and remote have diverged. Skipping auto-pull.")
129
- return "diverged"
130
-
131
- # We're behind — safe to fast-forward pull
132
- old_version = _read_package_version()
133
- rc, pull_out, pull_err = _git("pull", "--ff-only")
134
- if rc != 0:
135
- _log(f"git pull --ff-only failed: {pull_err}")
136
- return None # Don't break anything
137
-
138
- new_version = _read_package_version()
139
-
140
- # Run DB migrations after pull
141
- _run_db_migrations()
142
-
143
- msg = f"Auto-updated: {old_version} -> {new_version}" if old_version != new_version else f"Auto-updated (v{new_version}, new commits)"
144
- _log(msg)
145
- return msg
146
-
147
-
148
- def _run_db_migrations():
149
- """Run NEXO's DB schema migrations (from db._schema) after a pull."""
150
- try:
151
- from db._schema import run_migrations
152
- from db._core import get_db
153
- conn = get_db()
154
- applied = run_migrations(conn)
155
- if applied > 0:
156
- _log(f"Applied {applied} DB migration(s)")
157
- except Exception as e:
158
- _log(f"DB migration error (continuing): {e}")
159
-
160
-
161
- # ── npm version check (notify only) ─────────────────────────────────
162
-
163
- def _check_npm_version() -> str | None:
164
- """For non-git installs: check npm registry for a newer version. Returns notification or None."""
165
- current = _read_package_version()
166
- if current == "unknown":
167
- return None
168
-
169
- pkg_name = "nexo-brain"
170
- try:
171
- pkg = REPO_DIR / "package.json"
172
- if pkg.exists():
173
- data = json.loads(pkg.read_text())
174
- pkg_name = data.get("name", pkg_name)
175
- except Exception:
176
- pass
177
-
178
- try:
179
- result = subprocess.run(
180
- ["npm", "view", pkg_name, "version"],
181
- capture_output=True,
182
- text=True,
183
- timeout=GIT_TIMEOUT_SECONDS,
184
- )
185
- if result.returncode != 0:
186
- return None
187
- latest = result.stdout.strip()
188
- if not latest:
189
- return None
190
- if latest != current and not current.endswith(latest):
191
- return f"NEXO update available: {current} -> {latest}. Run: npm update -g {pkg_name}"
192
- except Exception:
193
- pass
194
- return None
195
-
196
-
197
- # ── File-based migrations (migrations/ directory) ────────────────────
198
-
199
- def _get_applied_migration_version() -> int:
200
- """Read the last applied file-migration version from disk."""
201
- try:
202
- if MIGRATION_VERSION_FILE.exists():
203
- return int(MIGRATION_VERSION_FILE.read_text().strip())
204
- except (ValueError, OSError):
205
- pass
206
- return 0
207
-
208
-
209
- def _set_migration_version(version: int):
210
- """Write the current file-migration version to disk."""
211
- try:
212
- MIGRATION_VERSION_FILE.write_text(str(version))
213
- except Exception as e:
214
- _log(f"Failed to write migration version: {e}")
215
-
216
-
217
- def _discover_migrations() -> list[tuple[int, Path]]:
218
- """Find numbered migration files in migrations/ directory.
219
-
220
- Expected naming: NNN_description.ext where ext is .sql, .py, or .sh
221
- Example: 001_add_index.sql, 002_backfill_data.py, 003_cleanup.sh
222
- """
223
- if not MIGRATIONS_DIR.is_dir():
224
- return []
225
-
226
- migrations = []
227
- for f in MIGRATIONS_DIR.iterdir():
228
- if f.is_file() and f.suffix in (".sql", ".py", ".sh"):
229
- # Extract leading number from filename
230
- parts = f.stem.split("_", 1)
231
- if parts and parts[0].isdigit():
232
- migrations.append((int(parts[0]), f))
233
-
234
- migrations.sort(key=lambda x: x[0])
235
- return migrations
236
-
237
-
238
- def _run_file_migration(path: Path) -> tuple[bool, str]:
239
- """Execute a single migration file. Returns (success, message)."""
240
- ext = path.suffix
241
-
242
- try:
243
- if ext == ".sql":
244
- sql = path.read_text()
245
- from db._core import get_db
246
- conn = get_db()
247
- conn.executescript(sql)
248
- conn.commit()
249
- return True, "OK"
250
-
251
- elif ext == ".py":
252
- result = subprocess.run(
253
- [sys.executable, str(path)],
254
- cwd=str(SRC_DIR),
255
- capture_output=True,
256
- text=True,
257
- timeout=30,
258
- env={**os.environ, "NEXO_HOME": str(NEXO_HOME)},
259
- )
260
- if result.returncode != 0:
261
- return False, result.stderr or result.stdout or "non-zero exit"
262
- return True, "OK"
263
-
264
- elif ext == ".sh":
265
- result = subprocess.run(
266
- ["bash", str(path)],
267
- cwd=str(REPO_DIR),
268
- capture_output=True,
269
- text=True,
270
- timeout=30,
271
- env={**os.environ, "NEXO_HOME": str(NEXO_HOME)},
272
- )
273
- if result.returncode != 0:
274
- return False, result.stderr or result.stdout or "non-zero exit"
275
- return True, "OK"
276
-
277
- else:
278
- return False, f"unknown extension: {ext}"
279
-
280
- except Exception as e:
281
- return False, str(e)
282
-
283
-
284
- def run_file_migrations() -> list[dict]:
285
- """Run any pending file-based migrations from the migrations/ directory.
286
-
287
- Returns list of results: [{"version": N, "file": "...", "status": "ok"|"failed", "message": "..."}]
288
- """
289
- current_version = _get_applied_migration_version()
290
- migrations = _discover_migrations()
291
- results = []
292
-
293
- for version, path in migrations:
294
- if version <= current_version:
295
- continue
296
-
297
- success, message = _run_file_migration(path)
298
-
299
- if success:
300
- _set_migration_version(version)
301
- results.append({
302
- "version": version,
303
- "file": path.name,
304
- "status": "ok",
305
- "message": message,
306
- })
307
- _log(f"Migration {path.name}: OK")
308
- else:
309
- results.append({
310
- "version": version,
311
- "file": path.name,
312
- "status": "failed",
313
- "message": message,
314
- })
315
- _log(f"Migration {path.name}: FAILED — {message}")
316
- # Don't advance version past a failure, but continue trying others
317
- # so independent migrations still run. Version stays at last success.
318
-
319
- return results
320
-
321
-
322
- # ── CLAUDE.md version-tracked migration ─────────────────────────────
323
-
324
-
325
- def _read_template_version() -> str | None:
326
- """Extract version from the <!-- nexo-claude-md-version: X.Y.Z --> comment in the template."""
327
- if not TEMPLATE_FILE.exists():
328
- return None
329
- first_line = TEMPLATE_FILE.read_text().split("\n", 1)[0]
330
- m = re.search(r"nexo-claude-md-version:\s*([\d.]+)", first_line)
331
- return m.group(1) if m else None
332
-
333
-
334
- def _read_installed_claude_md_version() -> str | None:
335
- """Read the CLAUDE.md version currently installed for this user."""
336
- try:
337
- if CLAUDE_MD_VERSION_FILE.exists():
338
- return CLAUDE_MD_VERSION_FILE.read_text().strip()
339
- except OSError:
340
- pass
341
- return None
342
-
343
-
344
- def _write_installed_claude_md_version(version: str):
345
- """Persist the installed CLAUDE.md version."""
346
- try:
347
- CLAUDE_MD_VERSION_FILE.write_text(version)
348
- except Exception as e:
349
- _log(f"Failed to write CLAUDE.md version file: {e}")
350
-
351
-
352
- def _find_user_claude_md() -> Path | None:
353
- """Locate the user's CLAUDE.md (typically ~/.claude/CLAUDE.md)."""
354
- candidate = Path.home() / ".claude" / "CLAUDE.md"
355
- if candidate.exists():
356
- return candidate
357
- # Fallback: check NEXO_HOME
358
- candidate2 = NEXO_HOME / "CLAUDE.md"
359
- if candidate2.exists():
360
- return candidate2
361
- return None
362
-
363
-
364
- def _resolve_placeholders(template_text: str) -> str:
365
- """Fill {{NAME}} and {{NEXO_HOME}} from the user's existing CLAUDE.md or config."""
366
- # Try to read operator name from version.json
367
- name = "NEXO"
368
- try:
369
- vf = NEXO_HOME / "version.json"
370
- if vf.exists():
371
- data = json.loads(vf.read_text())
372
- name = data.get("operator_name", name)
373
- except Exception:
374
- pass
375
-
376
- return (
377
- template_text
378
- .replace("{{NAME}}", name)
379
- .replace("{{NEXO_HOME}}", str(NEXO_HOME))
380
- )
381
-
382
-
383
- def _extract_section(text: str, section_id: str) -> str | None:
384
- """Extract content between <!-- nexo:start:ID --> and <!-- nexo:end:ID --> markers (inclusive)."""
385
- pattern = re.compile(
386
- rf"(<!-- nexo:start:{re.escape(section_id)} -->.*?<!-- nexo:end:{re.escape(section_id)} -->)",
387
- re.DOTALL,
388
- )
389
- m = pattern.search(text)
390
- return m.group(1) if m else None
391
-
392
-
393
- def _list_section_ids(text: str) -> list[str]:
394
- """Return all section IDs found in the text, in order."""
395
- return re.findall(r"<!-- nexo:start:(\w+) -->", text)
396
-
397
-
398
- def _migrate_claude_md() -> str | None:
399
- """Compare template version vs installed version. If newer, update core sections in user's CLAUDE.md.
400
-
401
- Returns a status message or None if no migration was needed.
402
- """
403
- template_version = _read_template_version()
404
- if not template_version:
405
- return None
406
-
407
- installed_version = _read_installed_claude_md_version()
408
- if installed_version == template_version:
409
- return None # Already up to date
410
-
411
- user_md_path = _find_user_claude_md()
412
- if not user_md_path:
413
- _log("CLAUDE.md migration: no user CLAUDE.md found, skipping")
414
- return None
415
-
416
- # Read both files
417
- user_md = user_md_path.read_text()
418
- template_raw = TEMPLATE_FILE.read_text()
419
- template_resolved = _resolve_placeholders(template_raw)
420
-
421
- # Get all section IDs from the template
422
- section_ids = _list_section_ids(template_resolved)
423
- if not section_ids:
424
- _log("CLAUDE.md migration: no section markers in template, skipping")
425
- _write_installed_claude_md_version(template_version)
426
- return None
427
-
428
- updated = user_md
429
- sections_replaced = 0
430
- sections_added = 0
431
-
432
- for sid in section_ids:
433
- new_section = _extract_section(template_resolved, sid)
434
- if not new_section:
435
- continue
436
-
437
- old_section = _extract_section(updated, sid)
438
- if old_section:
439
- if old_section != new_section:
440
- updated = updated.replace(old_section, new_section)
441
- sections_replaced += 1
442
- else:
443
- # Section doesn't exist in user's file — append before the end
444
- # (new sections added by template updates)
445
- updated = updated.rstrip() + "\n\n" + new_section + "\n"
446
- sections_added += 1
447
-
448
- # Update the version comment if present in user's file
449
- updated = re.sub(
450
- r"<!-- nexo-claude-md-version: [\d.]+ -->",
451
- f"<!-- nexo-claude-md-version: {template_version} -->",
452
- updated,
453
- )
454
- # If no version comment existed, add one at the top
455
- if "nexo-claude-md-version:" not in updated:
456
- updated = f"<!-- nexo-claude-md-version: {template_version} -->\n" + updated
457
-
458
- if sections_replaced > 0 or sections_added > 0:
459
- # Backup before writing
460
- backup_path = user_md_path.with_suffix(".md.bak")
461
- try:
462
- backup_path.write_text(user_md)
463
- except Exception:
464
- pass # Non-critical
465
-
466
- user_md_path.write_text(updated)
467
-
468
- _write_installed_claude_md_version(template_version)
469
-
470
- if sections_replaced == 0 and sections_added == 0:
471
- return f"CLAUDE.md v{template_version}: already current (version file updated)"
472
-
473
- msg = f"CLAUDE.md migrated to v{template_version}: {sections_replaced} section(s) updated, {sections_added} new section(s) added"
474
- _log(msg)
475
- return msg
476
-
477
-
478
- # ── Main entry point ─────────────────────────────────────────────────
479
-
480
- def auto_update_check() -> dict:
481
- """Run the full auto-update check at server startup.
482
-
483
- NEVER raises an exception — always returns a dict.
484
-
485
- Phase 1 (local, safe, no network):
486
- - DB schema migrations
487
- - File-based migrations
488
- - CLAUDE.md version migration
489
-
490
- Phase 2 (network, wrapped in try/except):
491
- - git fetch/pull (if git repo)
492
- - npm version check (if non-git install)
493
-
494
- Returns a dict with:
495
- - checked: bool — whether a network check was actually performed
496
- - git_update: str|None — git update status message
497
- - npm_notice: str|None — npm upgrade notice for non-git installs
498
- - claude_md_update: str|None — CLAUDE.md migration status
499
- - migrations: list — file-based migration results
500
- - db_migrations: int — number of DB schema migrations applied
501
- - skipped_reason: str|None — why the network check was skipped (cooldown, etc.)
502
- - error: str|None — error message if something failed (informational only)
503
- """
504
- result = {
505
- "checked": False,
506
- "git_update": None,
507
- "npm_notice": None,
508
- "claude_md_update": None,
509
- "migrations": [],
510
- "db_migrations": 0,
511
- "skipped_reason": None,
512
- "error": None,
513
- }
514
-
515
- # ── Read auto_update flag from schedule.json ────────────────────
516
- auto_update_enabled = True
517
- try:
518
- schedule_file = NEXO_HOME / "config" / "schedule.json"
519
- if schedule_file.exists():
520
- schedule_data = json.loads(schedule_file.read_text())
521
- auto_update_enabled = schedule_data.get("auto_update", True)
522
- except Exception:
523
- pass # Default to enabled on any read error
524
-
525
- # ── Phase 1: Local migrations (safe, no network) ────────────────
526
- # These ALWAYS run, regardless of cooldown, network state, or auto_update flag.
527
-
528
- # DB schema migrations
529
- try:
530
- _run_db_migrations()
531
- except Exception as e:
532
- _log(f"DB migration error (continuing): {e}")
533
-
534
- # File-based migrations
535
- try:
536
- result["migrations"] = run_file_migrations()
537
- except Exception as e:
538
- _log(f"File migration runner error: {e}")
539
-
540
- # Backfill evolution-objective.json for existing installs
541
- try:
542
- evo_obj_path = NEXO_HOME / "brain" / "evolution-objective.json"
543
- if not evo_obj_path.exists():
544
- (NEXO_HOME / "brain").mkdir(parents=True, exist_ok=True)
545
- default_objective = {
546
- "objective": "Improve operational excellence and reduce repeated errors",
547
- "focus_areas": ["error_prevention", "proactivity", "memory_quality"],
548
- "evolution_enabled": True,
549
- "evolution_mode": "review",
550
- "dimensions": {
551
- "episodic_memory": {"current": 0, "target": 90},
552
- "autonomy": {"current": 0, "target": 80},
553
- "proactivity": {"current": 0, "target": 70},
554
- "self_improvement": {"current": 0, "target": 60},
555
- "agi": {"current": 0, "target": 20},
556
- },
557
- "total_evolutions": 0,
558
- "consecutive_failures": 0,
559
- "created_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
560
- }
561
- evo_obj_path.write_text(json.dumps(default_objective, indent=2))
562
- _log("Backfilled evolution-objective.json for existing install")
563
- except Exception as e:
564
- _log(f"evolution-objective.json backfill error: {e}")
565
-
566
- # Backfill NEXO_HOME/scripts/ for existing installs
567
- try:
568
- scripts_dest = NEXO_HOME / "scripts"
569
- # Deduce NEXO_CODE: env var first, then from __file__ (auto_update.py is in src/)
570
- nexo_code = Path(os.environ.get("NEXO_CODE", str(Path(__file__).resolve().parent)))
571
- scripts_src = nexo_code / "scripts" if (nexo_code / "scripts").is_dir() else None
572
- if scripts_src and not scripts_dest.is_dir():
573
- import shutil
574
- scripts_dest.mkdir(parents=True, exist_ok=True)
575
- for f in scripts_src.iterdir():
576
- if f.name.startswith('.') or f.name == '__pycache__':
577
- continue
578
- dest = scripts_dest / f.name
579
- if f.is_file() and not dest.exists():
580
- shutil.copy2(str(f), str(dest))
581
- _log("Backfilled NEXO_HOME/scripts/ from NEXO_CODE for existing install")
582
- except Exception as e:
583
- _log(f"scripts backfill error: {e}")
584
-
585
- # CLAUDE.md version migration
586
- try:
587
- result["claude_md_update"] = _migrate_claude_md()
588
- except Exception as e:
589
- _log(f"CLAUDE.md migration error: {e}")
590
-
591
- # ── Phase 2: Network operations (wrapped, never fatal) ──────────
592
- # Skip entirely if auto_update is disabled in schedule.json
593
- if not auto_update_enabled:
594
- result["skipped_reason"] = "auto_update disabled in schedule.json"
595
- _log("Network updates disabled (auto_update: false in schedule.json)")
596
- return result
597
-
598
- # Check cooldown for git/npm checks
599
- try:
600
- last_check = _read_last_check()
601
- last_ts = last_check.get("timestamp", 0)
602
- now = time.time()
603
-
604
- if now - last_ts < CHECK_COOLDOWN_SECONDS:
605
- result["skipped_reason"] = "cooldown"
606
- return result
607
-
608
- result["checked"] = True
609
-
610
- is_git = _is_git_repo()
611
-
612
- if is_git:
613
- result["git_update"] = _check_git_updates()
614
- else:
615
- # Non-git install — check npm for newer version
616
- version_json = REPO_DIR / "version.json"
617
- pkg_json = REPO_DIR / "package.json"
618
- if version_json.exists() or pkg_json.exists():
619
- result["npm_notice"] = _check_npm_version()
620
-
621
- # Save timestamp
622
- _write_last_check({
623
- "timestamp": now,
624
- "is_git": is_git,
625
- "git_update": result["git_update"],
626
- "npm_notice": result["npm_notice"],
627
- })
628
-
629
- except Exception as e:
630
- error_msg = f"Update check failed: {e}. Running current version."
631
- _log(error_msg)
632
- result["error"] = error_msg
633
-
634
- return result