nexo-brain 1.7.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/README.md +160 -60
  2. package/bin/nexo-brain.js +680 -381
  3. package/package.json +18 -3
  4. package/scripts/migrate-to-unified.sh +813 -0
  5. package/scripts/migrate-v1.7-to-v1.8.py +214 -0
  6. package/scripts/pre-commit-check.sh +1 -1
  7. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  8. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  9. package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
  10. package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
  11. package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
  12. package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
  13. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  14. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  15. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  16. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  17. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  18. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  19. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  20. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  21. package/src/auto_close_sessions.py +1 -1
  22. package/src/auto_update.py +634 -0
  23. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  24. package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
  25. package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  26. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  27. package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
  28. package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
  29. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  30. package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
  31. package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
  32. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  33. package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
  34. package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
  35. package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
  36. package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
  37. package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
  38. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  39. package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
  40. package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
  41. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  42. package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
  43. package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
  44. package/src/cognitive/_core.py +7 -3
  45. package/src/cognitive/_decay.py +1 -1
  46. package/src/cognitive/_search.py +1 -0
  47. package/src/cognitive/_trust.py +3 -3
  48. package/src/crons/manifest.json +106 -0
  49. package/src/crons/sync.py +217 -0
  50. package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
  51. package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
  52. package/src/dashboard/app.py +24 -4
  53. package/src/dashboard/templates/dashboard.html +3 -2
  54. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  55. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  56. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  57. package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
  58. package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
  59. package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
  60. package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
  61. package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
  62. package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
  63. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  64. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  65. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  66. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  67. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  68. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  69. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  70. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  71. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  72. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  73. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  74. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  75. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  76. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  77. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  78. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  79. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  80. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  81. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  82. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  83. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  84. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  85. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  86. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  87. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  88. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  89. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  90. package/src/db/_core.py +5 -1
  91. package/src/db/_episodic.py +2 -4
  92. package/src/db/_reminders.py +45 -6
  93. package/src/db/_schema.py +31 -0
  94. package/src/evolution_cycle.py +33 -11
  95. package/src/hooks/auto_capture.py +1 -1
  96. package/src/hooks/capture-tool-logs.sh +76 -0
  97. package/src/hooks/inbox-hook.sh +2 -1
  98. package/src/hooks/post-compact.sh +2 -1
  99. package/src/hooks/pre-compact.sh +104 -2
  100. package/src/hooks/session-start.sh +6 -2
  101. package/src/hooks/session-stop.sh +4 -2
  102. package/src/kg_populate.py +4 -1
  103. package/src/migrate_embeddings.py +4 -1
  104. package/src/plugin_loader.py +100 -34
  105. package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
  106. package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
  107. package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
  108. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  109. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  110. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  111. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  112. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  113. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  114. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  115. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  116. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  117. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  118. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  119. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  120. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  121. package/src/plugins/agents.py +2 -2
  122. package/src/plugins/backup.py +5 -4
  123. package/src/plugins/core_rules.py +37 -16
  124. package/src/plugins/episodic_memory.py +14 -5
  125. package/src/plugins/evolution.py +6 -2
  126. package/src/plugins/guard.py +20 -11
  127. package/src/plugins/update.py +256 -0
  128. package/src/requirements.txt +12 -0
  129. package/src/scripts/check-context.py +8 -3
  130. package/src/scripts/deep-sleep/__pycache__/extract.cpython-314.pyc +0 -0
  131. package/src/scripts/deep-sleep/apply_findings.py +514 -169
  132. package/src/scripts/deep-sleep/collect.py +480 -0
  133. package/src/scripts/deep-sleep/extract-prompt.md +233 -0
  134. package/src/scripts/deep-sleep/extract.py +249 -0
  135. package/src/scripts/deep-sleep/synthesize-prompt.md +168 -0
  136. package/src/scripts/deep-sleep/synthesize.py +191 -0
  137. package/src/scripts/nexo-auto-update.py +4 -211
  138. package/src/scripts/nexo-backup.sh +5 -13
  139. package/src/scripts/nexo-brain-activation.sh +26 -26
  140. package/src/scripts/nexo-catchup.py +36 -25
  141. package/src/scripts/nexo-cognitive-decay.py +7 -3
  142. package/src/scripts/nexo-daily-self-audit.py +44 -15
  143. package/src/scripts/nexo-deep-sleep.sh +31 -16
  144. package/src/scripts/nexo-evolution-run.py +21 -11
  145. package/src/scripts/nexo-followup-hygiene.py +6 -4
  146. package/src/scripts/nexo-github-monitor.py +12 -8
  147. package/src/scripts/nexo-immune.py +6 -4
  148. package/src/scripts/nexo-inbox-hook.sh +2 -1
  149. package/src/scripts/nexo-install.py +4 -225
  150. package/src/scripts/nexo-learning-housekeep.py +7 -3
  151. package/src/scripts/nexo-learning-validator.py +1 -22
  152. package/src/scripts/nexo-migrate.py +9 -3
  153. package/src/scripts/nexo-postmortem-consolidator.py +17 -10
  154. package/src/scripts/nexo-pre-commit.py +3 -1
  155. package/src/scripts/nexo-prevent-sleep.sh +29 -0
  156. package/src/scripts/nexo-proactive-dashboard.py +5 -4
  157. package/src/scripts/nexo-runtime-preflight.py +59 -55
  158. package/src/scripts/nexo-send-email.py +1 -1
  159. package/src/scripts/nexo-send-reply.py +3 -1
  160. package/src/scripts/nexo-sleep.py +13 -9
  161. package/src/scripts/nexo-snapshot-restore.sh +2 -1
  162. package/src/scripts/nexo-synthesis.py +11 -7
  163. package/src/scripts/nexo-tcc-approve.sh +79 -0
  164. package/src/scripts/nexo-update.sh +161 -0
  165. package/src/scripts/nexo-watchdog-smoke.py +18 -13
  166. package/src/scripts/nexo-watchdog.sh +22 -13
  167. package/src/server.py +77 -28
  168. package/src/storage_router.py +6 -2
  169. package/src/tools_learnings.py +6 -6
  170. package/src/tools_menu.py +1 -1
  171. package/src/tools_reminders_crud.py +10 -8
  172. package/src/tools_sessions.py +76 -4
  173. package/templates/CLAUDE.md.template +14 -80
  174. package/templates/launchagents/README.md +7 -7
  175. package/templates/launchagents/com.nexo.auto-close-sessions.plist +5 -1
  176. package/templates/launchagents/com.nexo.catchup.plist +4 -0
  177. package/templates/launchagents/com.nexo.cognitive-decay.plist +7 -0
  178. package/templates/launchagents/com.nexo.dashboard.plist +5 -1
  179. package/templates/launchagents/com.nexo.deep-sleep.plist +4 -0
  180. package/templates/launchagents/com.nexo.evolution.plist +4 -0
  181. package/templates/launchagents/com.nexo.followup-hygiene.plist +4 -0
  182. package/templates/launchagents/com.nexo.github-monitor.plist +3 -1
  183. package/templates/launchagents/com.nexo.immune.plist +4 -0
  184. package/templates/launchagents/com.nexo.postmortem.plist +4 -0
  185. package/templates/launchagents/com.nexo.self-audit.plist +4 -0
  186. package/templates/launchagents/com.nexo.synthesis.plist +4 -0
  187. package/templates/launchagents/com.nexo.watchdog.plist +4 -0
  188. package/templates/openclaw.json +1 -1
  189. package/tests/conftest.py +2 -2
  190. package/tests/test_cognitive.py +7 -6
  191. package/tests/test_migrations.py +26 -0
  192. package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
  193. package/src/__pycache__/claim_graph.cpython-314.pyc +0 -0
  194. package/src/__pycache__/evolution_cycle.cpython-314.pyc +0 -0
  195. package/src/__pycache__/kg_populate.cpython-314.pyc +0 -0
  196. package/src/__pycache__/knowledge_graph.cpython-314.pyc +0 -0
  197. package/src/__pycache__/maintenance.cpython-314.pyc +0 -0
  198. package/src/__pycache__/migrate_embeddings.cpython-314.pyc +0 -0
  199. package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
  200. package/src/__pycache__/server.cpython-314.pyc +0 -0
  201. package/src/__pycache__/storage_router.cpython-314.pyc +0 -0
  202. package/src/__pycache__/tools_coordination.cpython-314.pyc +0 -0
  203. package/src/__pycache__/tools_credentials.cpython-314.pyc +0 -0
  204. package/src/__pycache__/tools_learnings.cpython-314.pyc +0 -0
  205. package/src/__pycache__/tools_menu.cpython-314.pyc +0 -0
  206. package/src/__pycache__/tools_reminders.cpython-314.pyc +0 -0
  207. package/src/__pycache__/tools_reminders_crud.cpython-314.pyc +0 -0
  208. package/src/__pycache__/tools_sessions.cpython-314.pyc +0 -0
  209. package/src/__pycache__/tools_task_history.cpython-314.pyc +0 -0
  210. package/src/dashboard/__pycache__/__init__.cpython-314.pyc +0 -0
  211. package/src/dashboard/__pycache__/app.cpython-314.pyc +0 -0
  212. package/src/hooks/__pycache__/auto_capture.cpython-314.pyc +0 -0
  213. package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
  214. package/src/plugins/__pycache__/agents.cpython-314.pyc +0 -0
  215. package/src/plugins/__pycache__/artifact_registry.cpython-314.pyc +0 -0
  216. package/src/plugins/__pycache__/backup.cpython-314.pyc +0 -0
  217. package/src/plugins/__pycache__/cognitive_memory.cpython-314.pyc +0 -0
  218. package/src/plugins/__pycache__/core_rules.cpython-314.pyc +0 -0
  219. package/src/plugins/__pycache__/cortex.cpython-314.pyc +0 -0
  220. package/src/plugins/__pycache__/entities.cpython-314.pyc +0 -0
  221. package/src/plugins/__pycache__/episodic_memory.cpython-314.pyc +0 -0
  222. package/src/plugins/__pycache__/evolution.cpython-314.pyc +0 -0
  223. package/src/plugins/__pycache__/guard.cpython-314.pyc +0 -0
  224. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-314.pyc +0 -0
  225. package/src/plugins/__pycache__/preferences.cpython-314.pyc +0 -0
  226. package/src/rules/__pycache__/__init__.cpython-314.pyc +0 -0
  227. package/src/rules/__pycache__/migrate.cpython-314.pyc +0 -0
  228. package/src/scripts/__pycache__/check-context.cpython-314.pyc +0 -0
  229. package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
  230. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  231. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  232. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  233. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  234. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
  235. package/src/scripts/__pycache__/nexo-github-monitor.cpython-314.pyc +0 -0
  236. package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
  237. package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
  238. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
  239. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  240. package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
  241. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
  242. package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
  243. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
  244. package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
  245. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
  246. package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
  247. package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
  248. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  249. package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
  250. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
  251. package/src/scripts/deep-sleep/__pycache__/analyze_session.cpython-314.pyc +0 -0
  252. package/src/scripts/deep-sleep/__pycache__/apply_findings.cpython-314.pyc +0 -0
  253. package/src/scripts/deep-sleep/__pycache__/collect_transcripts.cpython-314.pyc +0 -0
  254. package/src/scripts/deep-sleep/analyze_session.py +0 -217
  255. package/src/scripts/deep-sleep/collect_transcripts.py +0 -145
  256. package/src/scripts/deep-sleep/prompt.md +0 -109
  257. package/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  258. package/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  259. package/tests/__pycache__/test_cognitive.cpython-314-pytest-9.0.2.pyc +0 -0
  260. package/tests/__pycache__/test_knowledge_graph.cpython-314-pytest-9.0.2.pyc +0 -0
  261. package/tests/__pycache__/test_migrations.cpython-314-pytest-9.0.2.pyc +0 -0
package/bin/nexo-brain.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * create-nexo — Interactive installer for NEXO cognitive co-operator.
3
+ * nexo-brain — Interactive installer for NEXO cognitive co-operator.
4
4
  *
5
- * Usage: npx create-nexo
5
+ * Usage: npx nexo-brain
6
6
  *
7
7
  * What it does:
8
8
  * 1. Asks for the co-operator's name
@@ -19,7 +19,7 @@ const fs = require("fs");
19
19
  const path = require("path");
20
20
  const readline = require("readline");
21
21
 
22
- const NEXO_HOME = path.join(require("os").homedir(), ".nexo");
22
+ let NEXO_HOME = path.join(require("os").homedir(), ".nexo");
23
23
  const CLAUDE_SETTINGS = path.join(
24
24
  require("os").homedir(),
25
25
  ".claude",
@@ -52,6 +52,456 @@ function log(msg) {
52
52
  console.log(` ${msg}`);
53
53
  }
54
54
 
55
+ // ══════════════════════════════════════════════════════════════════════════════
56
+ // CORE PROCESS & HOOK DEFINITIONS
57
+ // All 13 nightly/periodic processes and all 7 core hooks that make NEXO functional.
58
+ // ══════════════════════════════════════════════════════════════════════════════
59
+
60
+ /**
61
+ * Complete definition of all 13 NEXO automated processes.
62
+ * Each entry specifies the script, its interpreter ("python" or "bash"),
63
+ * the schedule type, and default schedule values.
64
+ */
65
+ const ALL_PROCESSES = [
66
+ // --- Every 5 minutes ---
67
+ { name: "auto-close-sessions", script: "auto_close_sessions.py", interpreter: "python", scriptDir: "root",
68
+ type: "interval", intervalMinutes: 5, purpose: "Clean stale sessions" },
69
+ { name: "watchdog", script: "nexo-watchdog.sh", interpreter: "bash", scriptDir: "scripts",
70
+ type: "interval", intervalMinutes: 5, purpose: "Health monitoring" },
71
+ // --- Every 30 minutes ---
72
+ { name: "immune", script: "nexo-immune.py", interpreter: "python", scriptDir: "scripts",
73
+ type: "interval", intervalMinutes: 30, purpose: "System immunity checks" },
74
+ // --- Every 2 hours ---
75
+ { name: "synthesis", script: "nexo-synthesis.py", interpreter: "python", scriptDir: "scripts",
76
+ type: "interval", intervalMinutes: 120, purpose: "Memory synthesis" },
77
+ // --- Every hour ---
78
+ { name: "backup", script: "nexo-backup.sh", interpreter: "bash", scriptDir: "scripts",
79
+ type: "interval", intervalMinutes: 60, purpose: "DB backups" },
80
+ // --- RunAtLoad (once on boot) ---
81
+ { name: "catchup", script: "nexo-catchup.py", interpreter: "python", scriptDir: "scripts",
82
+ type: "runAtLoad", purpose: "Session catchup" },
83
+ { name: "tcc-approve", script: "nexo-tcc-approve.sh", interpreter: "bash", scriptDir: "scripts",
84
+ type: "runAtLoad", macOnly: true, watchPaths: ["~/.local/share/claude/versions"],
85
+ purpose: "Auto-approve macOS permissions for Claude updates" },
86
+ // --- KeepAlive (persistent daemon) ---
87
+ { name: "prevent-sleep", script: "nexo-prevent-sleep.sh", interpreter: "bash", scriptDir: "scripts",
88
+ type: "keepAlive", purpose: "Keep machine awake for nocturnal processes" },
89
+ // --- Daily (times from schedule.json) ---
90
+ { name: "cognitive-decay", script: "nexo-cognitive-decay.py", interpreter: "python", scriptDir: "scripts",
91
+ type: "daily", defaultHour: 3, defaultMinute: 0, purpose: "Memory decay" },
92
+ { name: "postmortem", script: "nexo-postmortem-consolidator.py", interpreter: "python", scriptDir: "scripts",
93
+ type: "daily", defaultHour: 23, defaultMinute: 30, purpose: "Session consolidation" },
94
+ { name: "self-audit", script: "nexo-daily-self-audit.py", interpreter: "python", scriptDir: "scripts",
95
+ type: "daily", defaultHour: 7, defaultMinute: 0, purpose: "Self-diagnostic" },
96
+ { name: "sleep", script: "nexo-sleep.py", interpreter: "python", scriptDir: "scripts",
97
+ type: "daily", defaultHour: 4, defaultMinute: 0, purpose: "Sleep cycle" },
98
+ { name: "deep-sleep", script: "nexo-deep-sleep.sh", interpreter: "bash", scriptDir: "scripts",
99
+ type: "daily", defaultHour: 4, defaultMinute: 30, purpose: "Deep sleep analysis" },
100
+ // --- Weekly (day + time from schedule.json) ---
101
+ { name: "evolution", script: "nexo-evolution-run.py", interpreter: "python", scriptDir: "scripts",
102
+ type: "weekly", defaultDay: "sunday", defaultHour: 3, defaultMinute: 0, purpose: "Self-evolution" },
103
+ { name: "followup-hygiene", script: "nexo-followup-hygiene.py", interpreter: "python", scriptDir: "scripts",
104
+ type: "weekly", defaultDay: "sunday", defaultHour: 5, defaultMinute: 0, purpose: "Cleanup stale followups" },
105
+ ];
106
+
107
+ /**
108
+ * Complete definition of all 7 core hooks.
109
+ * event: Claude Code hook event name
110
+ * matcher: glob matcher for the hook
111
+ * script: script filename inside NEXO_HOME/hooks/ (or a raw command template)
112
+ * key: unique identifier to detect if already registered (avoids duplicates)
113
+ */
114
+ const ALL_CORE_HOOKS = [
115
+ { event: "SessionStart", key: "session-start-ts", commandTemplate: (nexoHome) =>
116
+ `date +%s > ${path.join(nexoHome, "operations", ".session-start-ts")}`,
117
+ purpose: "Session timing" },
118
+ { event: "SessionStart", key: "session-start.sh", script: "session-start.sh",
119
+ purpose: "Briefing + context" },
120
+ { event: "Stop", key: "session-stop.sh", script: "session-stop.sh",
121
+ purpose: "POSTMORTEM — the most important" },
122
+ { event: "PostToolUse", key: "capture-tool-logs.sh", script: "capture-tool-logs.sh",
123
+ purpose: "Operation capture" },
124
+ { event: "PostToolUse", key: "inbox-hook.sh", script: "inbox-hook.sh",
125
+ purpose: "Inter-session messaging" },
126
+ { event: "PreCompact", key: "pre-compact.sh", script: "pre-compact.sh",
127
+ purpose: "Memory preservation" },
128
+ { event: "PostCompact", key: "post-compact.sh", script: "post-compact.sh",
129
+ purpose: "Memory restoration" },
130
+ ];
131
+
132
+ /**
133
+ * Register all 7 core hooks in settings.hooks.
134
+ * Additive: adds missing hooks, never removes user's custom ones.
135
+ */
136
+ function registerAllCoreHooks(settings, hooksDir, nexoHome) {
137
+ if (!settings.hooks) settings.hooks = {};
138
+
139
+ // Ensure operations dir exists for timestamp file
140
+ const opsDir = path.join(nexoHome, "operations");
141
+ fs.mkdirSync(opsDir, { recursive: true });
142
+
143
+ for (const hook of ALL_CORE_HOOKS) {
144
+ if (!settings.hooks[hook.event]) settings.hooks[hook.event] = [];
145
+
146
+ // Check if this specific hook is already registered (by key)
147
+ const alreadyExists = settings.hooks[hook.event].some(
148
+ (h) => h.command && h.command.includes(hook.key)
149
+ );
150
+ if (alreadyExists) continue;
151
+
152
+ // Build the command
153
+ let command;
154
+ if (hook.commandTemplate) {
155
+ command = hook.commandTemplate(nexoHome);
156
+ } else {
157
+ command = `NEXO_HOME=${nexoHome} bash ${path.join(hooksDir, hook.script)}`;
158
+ }
159
+
160
+ settings.hooks[hook.event].push({
161
+ type: "command",
162
+ command: command,
163
+ });
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Load schedule.json if it exists, or create it with defaults on fresh install.
169
+ * NEVER overwrites an existing schedule.json (user customization).
170
+ */
171
+ function loadOrCreateSchedule(nexoHome) {
172
+ const configDir = path.join(nexoHome, "config");
173
+ fs.mkdirSync(configDir, { recursive: true });
174
+ const scheduleFile = path.join(configDir, "schedule.json");
175
+
176
+ if (fs.existsSync(scheduleFile)) {
177
+ try {
178
+ return JSON.parse(fs.readFileSync(scheduleFile, "utf8"));
179
+ } catch {
180
+ // Corrupt file — return defaults but don't overwrite
181
+ return getDefaultSchedule();
182
+ }
183
+ }
184
+
185
+ // Fresh install: detect timezone and create schedule.json
186
+ const detectedTz = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
187
+ const schedule = getDefaultSchedule(detectedTz);
188
+ fs.writeFileSync(scheduleFile, JSON.stringify(schedule, null, 2));
189
+ return schedule;
190
+ }
191
+
192
+ function getDefaultSchedule(timezone) {
193
+ return {
194
+ timezone: timezone || "UTC",
195
+ auto_update: true,
196
+ processes: {
197
+ "cognitive-decay": { hour: 3, minute: 0 },
198
+ "postmortem": { hour: 23, minute: 30 },
199
+ "self-audit": { hour: 7, minute: 0 },
200
+ "sleep": { hour: 4, minute: 0 },
201
+ "deep-sleep": { hour: 4, minute: 30 },
202
+ "evolution": { day: "sunday", hour: 3, minute: 0 },
203
+ "followup-hygiene": { day: "sunday", hour: 5, minute: 0 },
204
+ },
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Resolve the venv python path for an existing NEXO_HOME installation.
210
+ */
211
+ function findVenvPython(nexoHome) {
212
+ const venvPy = path.join(nexoHome, ".venv", "bin", "python3");
213
+ if (fs.existsSync(venvPy)) return venvPy;
214
+ return null;
215
+ }
216
+
217
+ /**
218
+ * Map day name to systemd OnCalendar day abbreviation and crontab day number.
219
+ */
220
+ const DAY_MAP = {
221
+ sunday: { systemd: "Sun", cron: 0 },
222
+ monday: { systemd: "Mon", cron: 1 },
223
+ tuesday: { systemd: "Tue", cron: 2 },
224
+ wednesday: { systemd: "Wed", cron: 3 },
225
+ thursday: { systemd: "Thu", cron: 4 },
226
+ friday: { systemd: "Fri", cron: 5 },
227
+ saturday: { systemd: "Sat", cron: 6 },
228
+ };
229
+
230
+ /**
231
+ * Install all 13 processes on the current platform.
232
+ * macOS: LaunchAgents (.plist)
233
+ * Linux+systemd: .service + .timer files
234
+ * Linux fallback: crontab entries
235
+ */
236
+ function installAllProcesses(platform, pythonPath, nexoHome, schedule, launchAgentsDir) {
237
+ const home = require("os").homedir();
238
+ const nexoCode = path.join(__dirname, "..");
239
+ const logsDir = path.join(nexoHome, "logs");
240
+ fs.mkdirSync(logsDir, { recursive: true });
241
+
242
+ // Resolve script path: "root" means NEXO_HOME directly, "scripts" means NEXO_HOME/scripts/
243
+ function scriptPath(proc) {
244
+ const dir = proc.scriptDir === "root" ? nexoHome : path.join(nexoHome, "scripts");
245
+ return path.join(dir, proc.script);
246
+ }
247
+
248
+ // Resolve interpreter
249
+ function interpreterPath(proc) {
250
+ return proc.interpreter === "bash" ? "/bin/bash" : pythonPath;
251
+ }
252
+
253
+ // Get schedule overrides for daily/weekly processes
254
+ function getSchedule(proc) {
255
+ const sched = schedule.processes || {};
256
+ const override = sched[proc.name] || {};
257
+ return {
258
+ hour: override.hour !== undefined ? override.hour : (proc.defaultHour || 0),
259
+ minute: override.minute !== undefined ? override.minute : (proc.defaultMinute || 0),
260
+ day: override.day || proc.defaultDay || null,
261
+ };
262
+ }
263
+
264
+ if (platform === "darwin") {
265
+ // ──── macOS: LaunchAgents ────
266
+ fs.mkdirSync(launchAgentsDir, { recursive: true });
267
+ let count = 0;
268
+
269
+ for (const proc of ALL_PROCESSES) {
270
+ // Skip macOnly processes on Linux
271
+ if (proc.macOnly && platform !== "darwin") continue;
272
+
273
+ const plistName = `com.nexo.${proc.name}.plist`;
274
+ const plistPath = path.join(launchAgentsDir, plistName);
275
+ const sPath = scriptPath(proc);
276
+ const interp = interpreterPath(proc);
277
+ const s = getSchedule(proc);
278
+
279
+ let scheduleBlock = "";
280
+ if (proc.type === "keepAlive") {
281
+ scheduleBlock = ` <key>RunAtLoad</key>
282
+ <true/>
283
+ <key>KeepAlive</key>
284
+ <true/>`;
285
+ } else if (proc.type === "runAtLoad") {
286
+ let extra = "";
287
+ if (proc.watchPaths) {
288
+ const paths = proc.watchPaths.map(p => p.replace("~", home));
289
+ extra = `\n <key>WatchPaths</key>\n <array>\n${paths.map(p => ` <string>${p}</string>`).join("\n")}\n </array>`;
290
+ }
291
+ scheduleBlock = ` <key>RunAtLoad</key>
292
+ <true/>${extra}`;
293
+ } else if (proc.type === "interval") {
294
+ scheduleBlock = ` <key>StartInterval</key>
295
+ <integer>${proc.intervalMinutes * 60}</integer>
296
+ <key>RunAtLoad</key>
297
+ <false/>`;
298
+ } else if (proc.type === "daily") {
299
+ scheduleBlock = ` <key>StartCalendarInterval</key>
300
+ <dict>
301
+ <key>Hour</key>
302
+ <integer>${s.hour}</integer>
303
+ <key>Minute</key>
304
+ <integer>${s.minute}</integer>
305
+ </dict>
306
+ <key>RunAtLoad</key>
307
+ <false/>`;
308
+ } else if (proc.type === "weekly") {
309
+ // macOS uses Weekday 0=Sunday
310
+ const dayNum = s.day ? (DAY_MAP[s.day.toLowerCase()] || { cron: 0 }).cron : 0;
311
+ scheduleBlock = ` <key>StartCalendarInterval</key>
312
+ <dict>
313
+ <key>Weekday</key>
314
+ <integer>${dayNum}</integer>
315
+ <key>Hour</key>
316
+ <integer>${s.hour}</integer>
317
+ <key>Minute</key>
318
+ <integer>${s.minute}</integer>
319
+ </dict>
320
+ <key>RunAtLoad</key>
321
+ <false/>`;
322
+ }
323
+
324
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
325
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
326
+ <plist version="1.0">
327
+ <dict>
328
+ <key>Label</key>
329
+ <string>com.nexo.${proc.name}</string>
330
+ <key>ProgramArguments</key>
331
+ <array>
332
+ <string>${interp}</string>
333
+ <string>${sPath}</string>
334
+ </array>
335
+ ${scheduleBlock}
336
+ <key>StandardOutPath</key>
337
+ <string>${path.join(logsDir, `${proc.name}-stdout.log`)}</string>
338
+ <key>StandardErrorPath</key>
339
+ <string>${path.join(logsDir, `${proc.name}-stderr.log`)}</string>
340
+ <key>EnvironmentVariables</key>
341
+ <dict>
342
+ <key>HOME</key>
343
+ <string>${home}</string>
344
+ <key>NEXO_HOME</key>
345
+ <string>${nexoHome}</string>
346
+ <key>NEXO_CODE</key>
347
+ <string>${nexoCode}</string>
348
+ <key>PATH</key>
349
+ <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
350
+ </dict>
351
+ </dict>
352
+ </plist>`;
353
+
354
+ fs.writeFileSync(plistPath, plist);
355
+ try {
356
+ execSync(
357
+ `launchctl bootout gui/$(id -u) "${plistPath}" 2>/dev/null; launchctl bootstrap gui/$(id -u) "${plistPath}"`,
358
+ { stdio: "pipe" }
359
+ );
360
+ } catch {
361
+ // May fail if not previously loaded, that's OK
362
+ }
363
+ count++;
364
+ }
365
+ log(`${count} automated processes configured (LaunchAgents).`);
366
+
367
+ } else if (platform === "linux") {
368
+ // ──── Linux: systemd user timers (preferred) or crontab fallback ────
369
+ const systemdDir = path.join(home, ".config", "systemd", "user");
370
+ const hasSystemd = run("which systemctl") && run("systemctl --user status 2>/dev/null");
371
+
372
+ if (hasSystemd) {
373
+ fs.mkdirSync(systemdDir, { recursive: true });
374
+ let count = 0;
375
+
376
+ for (const proc of ALL_PROCESSES) {
377
+ if (proc.macOnly) continue; // tcc-approve is macOS only
378
+ const serviceName = `nexo-${proc.name}`;
379
+ const serviceFile = path.join(systemdDir, `${serviceName}.service`);
380
+ const timerFile = path.join(systemdDir, `${serviceName}.timer`);
381
+ const sPath = scriptPath(proc);
382
+ const interp = interpreterPath(proc);
383
+ const s = getSchedule(proc);
384
+
385
+ const serviceType = proc.type === "keepAlive" ? "simple" : "oneshot";
386
+ const restartPolicy = proc.type === "keepAlive" ? "Restart=always\nRestartSec=5" : "";
387
+ const service = `[Unit]
388
+ Description=NEXO Brain — ${proc.name} (${proc.purpose})
389
+
390
+ [Service]
391
+ Type=${serviceType}
392
+ ExecStart=${interp} ${sPath}
393
+ Environment=HOME=${home}
394
+ Environment=NEXO_HOME=${nexoHome}
395
+ Environment=NEXO_CODE=${nexoCode}
396
+ StandardOutput=append:${path.join(logsDir, `${proc.name}-stdout.log`)}
397
+ StandardError=append:${path.join(logsDir, `${proc.name}-stderr.log`)}
398
+ ${restartPolicy}
399
+ `;
400
+
401
+ // Build calendar spec
402
+ let onCalendar = "";
403
+ let persistent = "true";
404
+ if (proc.type === "keepAlive") {
405
+ // KeepAlive = persistent service, no timer needed
406
+ fs.writeFileSync(serviceFile, service + `\n[Install]\nWantedBy=default.target\n`);
407
+ run(`systemctl --user enable ${serviceName}.service`);
408
+ run(`systemctl --user start ${serviceName}.service`);
409
+ count++;
410
+ continue;
411
+ } else if (proc.type === "runAtLoad") {
412
+ // No timer for runAtLoad — runs via MCP startup
413
+ fs.writeFileSync(serviceFile, service);
414
+ count++;
415
+ continue;
416
+ } else if (proc.type === "interval") {
417
+ onCalendar = `*:0/${proc.intervalMinutes}`;
418
+ } else if (proc.type === "daily") {
419
+ onCalendar = `*-*-* ${String(s.hour).padStart(2, "0")}:${String(s.minute).padStart(2, "0")}:00`;
420
+ } else if (proc.type === "weekly") {
421
+ const dayAbbr = s.day ? (DAY_MAP[s.day.toLowerCase()] || { systemd: "Sun" }).systemd : "Sun";
422
+ onCalendar = `${dayAbbr} *-*-* ${String(s.hour).padStart(2, "0")}:${String(s.minute).padStart(2, "0")}:00`;
423
+ }
424
+
425
+ const timer = `[Unit]
426
+ Description=NEXO Brain — ${proc.name} timer
427
+
428
+ [Timer]
429
+ OnCalendar=${onCalendar}
430
+ Persistent=${persistent}
431
+
432
+ [Install]
433
+ WantedBy=timers.target
434
+ `;
435
+
436
+ fs.writeFileSync(serviceFile, service);
437
+ fs.writeFileSync(timerFile, timer);
438
+ try {
439
+ execSync(`systemctl --user enable --now ${serviceName}.timer 2>/dev/null`, { stdio: "pipe" });
440
+ } catch {}
441
+ count++;
442
+ }
443
+ log(`${count} systemd user timers configured.`);
444
+
445
+ } else {
446
+ // ──── Fallback: crontab ────
447
+ log("systemd not available, configuring crontab...");
448
+ const cronLines = [];
449
+ const envLine = `NEXO_HOME=${nexoHome}`;
450
+ const envLine2 = `NEXO_CODE=${nexoCode}`;
451
+
452
+ for (const proc of ALL_PROCESSES) {
453
+ if (proc.type === "runAtLoad") continue; // No cron for runAtLoad
454
+ const sPath = scriptPath(proc);
455
+ const interp = interpreterPath(proc);
456
+ const s = getSchedule(proc);
457
+ const logPath = path.join(logsDir, `${proc.name}-stdout.log`);
458
+
459
+ let cronSpec = "";
460
+ if (proc.type === "interval") {
461
+ cronSpec = `*/${proc.intervalMinutes} * * * *`;
462
+ } else if (proc.type === "daily") {
463
+ cronSpec = `${s.minute} ${s.hour} * * *`;
464
+ } else if (proc.type === "weekly") {
465
+ const dayNum = s.day ? (DAY_MAP[s.day.toLowerCase()] || { cron: 0 }).cron : 0;
466
+ cronSpec = `${s.minute} ${s.hour} * * ${dayNum}`;
467
+ }
468
+
469
+ cronLines.push(`${cronSpec} ${interp} ${sPath} >> ${logPath} 2>&1`);
470
+ }
471
+
472
+ try {
473
+ const existingCron = run("crontab -l 2>/dev/null") || "";
474
+ const nexoCronMarker = "# NEXO Brain automated processes";
475
+ const nexoCronEnd = "# END NEXO Brain";
476
+
477
+ // Remove old NEXO cron block if present, then add fresh one
478
+ let baseCron = existingCron;
479
+ if (existingCron.includes(nexoCronMarker)) {
480
+ const startIdx = existingCron.indexOf(nexoCronMarker);
481
+ const endIdx = existingCron.indexOf(nexoCronEnd);
482
+ if (endIdx > startIdx) {
483
+ baseCron = existingCron.substring(0, startIdx) + existingCron.substring(endIdx + nexoCronEnd.length);
484
+ } else {
485
+ baseCron = existingCron.substring(0, startIdx);
486
+ }
487
+ }
488
+
489
+ const newCron = baseCron.trimEnd() + "\n" + nexoCronMarker + "\n" + envLine + "\n" + envLine2 + "\n" + cronLines.join("\n") + "\n" + nexoCronEnd + "\n";
490
+ const tmpCron = path.join(nexoHome, ".crontab-tmp");
491
+ fs.writeFileSync(tmpCron, newCron);
492
+ execSync(`crontab ${tmpCron}`, { stdio: "pipe" });
493
+ fs.unlinkSync(tmpCron);
494
+ log(`${cronLines.length} cron jobs configured.`);
495
+ } catch (e) {
496
+ log(`Could not configure crontab: ${e.message}`);
497
+ log("Background tasks will run via catch-up on startup.");
498
+ }
499
+ }
500
+ } else {
501
+ log("Unsupported platform for background tasks. Maintenance runs on MCP startup.");
502
+ }
503
+ }
504
+
55
505
  async function main() {
56
506
  // Non-interactive mode: --defaults or --yes skips all prompts
57
507
  const useDefaults = process.argv.includes("--defaults") || process.argv.includes("--yes") || process.argv.includes("-y");
@@ -107,35 +557,60 @@ async function main() {
107
557
  log(`Existing installation detected: v${installedVersion} → v${currentVersion}`);
108
558
  log("Running auto-migration...");
109
559
 
110
- // Update hooks
111
- const hooksSrc = path.join(__dirname, "..", "src", "hooks");
560
+ // Recursive copy helper (skips __pycache__, .pyc, .db files)
561
+ const srcDir = path.join(__dirname, "..", "src");
562
+ const copyDirRec = (src, dest) => {
563
+ fs.mkdirSync(dest, { recursive: true });
564
+ fs.readdirSync(src).forEach(item => {
565
+ if (item === "__pycache__" || item.endsWith(".pyc") || item.endsWith(".db")) return;
566
+ const srcPath = path.join(src, item);
567
+ const destPath = path.join(dest, item);
568
+ if (fs.statSync(srcPath).isDirectory()) {
569
+ copyDirRec(srcPath, destPath);
570
+ } else {
571
+ fs.copyFileSync(srcPath, destPath);
572
+ }
573
+ });
574
+ };
575
+
576
+ // Update hooks (entire directory)
577
+ const hooksSrc = path.join(srcDir, "hooks");
112
578
  const hooksDest = path.join(NEXO_HOME, "hooks");
113
- fs.mkdirSync(hooksDest, { recursive: true });
114
- ["session-start.sh", "capture-session.sh", "session-stop.sh", "pre-compact.sh", "caffeinate-guard.sh"].forEach((h) => {
115
- const src = path.join(hooksSrc, h);
116
- const dest = path.join(hooksDest, h);
117
- if (fs.existsSync(src)) {
118
- fs.copyFileSync(src, dest);
119
- fs.chmodSync(dest, "755");
120
- }
121
- });
579
+ if (fs.existsSync(hooksSrc)) {
580
+ copyDirRec(hooksSrc, hooksDest);
581
+ // Make .sh files executable
582
+ fs.readdirSync(hooksDest).filter(f => f.endsWith(".sh")).forEach(f => {
583
+ fs.chmodSync(path.join(hooksDest, f), "755");
584
+ });
585
+ }
122
586
  log(" Hooks updated.");
123
587
 
124
- // Update core Python files
125
- const srcDir = path.join(__dirname, "..", "src");
126
- ["server.py", "db.py", "plugin_loader.py", "cognitive.py",
127
- "knowledge_graph.py", "kg_populate.py", "maintenance.py", "storage_router.py",
128
- "tools_sessions.py", "tools_coordination.py", "tools_reminders.py",
129
- "tools_reminders_crud.py", "tools_learnings.py", "tools_credentials.py",
130
- "tools_task_history.py", "tools_menu.py"].forEach((f) => {
588
+ // Update core Python files (flat .py files in src/)
589
+ const coreFlatFiles = [
590
+ "server.py", "plugin_loader.py",
591
+ "knowledge_graph.py", "kg_populate.py", "maintenance.py", "storage_router.py",
592
+ "claim_graph.py", "hnsw_index.py", "evolution_cycle.py", "migrate_embeddings.py",
593
+ "auto_close_sessions.py",
594
+ "tools_sessions.py", "tools_coordination.py", "tools_reminders.py",
595
+ "tools_reminders_crud.py", "tools_learnings.py", "tools_credentials.py",
596
+ "tools_task_history.py", "tools_menu.py",
597
+ ];
598
+ coreFlatFiles.forEach((f) => {
131
599
  const src = path.join(srcDir, f);
132
600
  if (fs.existsSync(src)) {
133
601
  fs.copyFileSync(src, path.join(NEXO_HOME, f));
134
602
  }
135
603
  });
604
+ // Update core packages (db/, cognitive/) — full directory copy
605
+ ["db", "cognitive"].forEach(pkg => {
606
+ const pkgSrc = path.join(srcDir, pkg);
607
+ if (fs.existsSync(pkgSrc)) {
608
+ copyDirRec(pkgSrc, path.join(NEXO_HOME, pkg));
609
+ }
610
+ });
136
611
  log(" Core files updated.");
137
612
 
138
- // Update plugins
613
+ // Update plugins (all .py files in plugins/)
139
614
  const pluginsSrc = path.join(srcDir, "plugins");
140
615
  const pluginsDest = path.join(NEXO_HOME, "plugins");
141
616
  fs.mkdirSync(pluginsDest, { recursive: true });
@@ -146,57 +621,51 @@ async function main() {
146
621
  }
147
622
  log(" Plugins updated.");
148
623
 
149
- // Update dashboard
624
+ // Update dashboard (recursive — includes static/, templates/)
150
625
  const dashSrc = path.join(srcDir, "dashboard");
151
626
  const dashDest = path.join(NEXO_HOME, "dashboard");
152
627
  if (fs.existsSync(dashSrc)) {
153
- fs.mkdirSync(dashDest, { recursive: true });
154
- const copyDir = (src, dest) => {
155
- fs.readdirSync(src).forEach(item => {
156
- const srcPath = path.join(src, item);
157
- const destPath = path.join(dest, item);
158
- if (fs.statSync(srcPath).isDirectory()) {
159
- fs.mkdirSync(destPath, { recursive: true });
160
- copyDir(srcPath, destPath);
161
- } else {
162
- fs.copyFileSync(srcPath, destPath);
163
- }
164
- });
165
- };
166
- copyDir(dashSrc, dashDest);
628
+ copyDirRec(dashSrc, dashDest);
167
629
  log(" Dashboard updated.");
168
630
  }
169
631
 
170
- // Update scripts
632
+ // Update rules (directory with core-rules.json, __init__.py, migrate.py)
633
+ const rulesSrc = path.join(srcDir, "rules");
634
+ const rulesDest = path.join(NEXO_HOME, "rules");
635
+ if (fs.existsSync(rulesSrc)) {
636
+ copyDirRec(rulesSrc, rulesDest);
637
+ log(" Rules updated.");
638
+ }
639
+
640
+ // Update scripts (all .py, .sh files + subdirectories like deep-sleep/)
171
641
  const scriptsSrc = path.join(srcDir, "scripts");
172
642
  const scriptsDest = path.join(NEXO_HOME, "scripts");
173
- fs.mkdirSync(scriptsDest, { recursive: true });
174
643
  if (fs.existsSync(scriptsSrc)) {
175
- fs.readdirSync(scriptsSrc).filter(f => f.endsWith(".py") || f.endsWith(".sh")).forEach((f) => {
176
- fs.copyFileSync(path.join(scriptsSrc, f), path.join(scriptsDest, f));
644
+ copyDirRec(scriptsSrc, scriptsDest);
645
+ // Make .sh files executable
646
+ fs.readdirSync(scriptsDest).filter(f => f.endsWith(".sh")).forEach(f => {
647
+ fs.chmodSync(path.join(scriptsDest, f), "755");
177
648
  });
178
649
  }
179
650
  log(" Scripts updated.");
180
651
 
181
- // Add PreCompact hook to settings.json if missing
652
+ // Register ALL 7 core hooks in settings.json (additive — don't remove user's custom hooks)
182
653
  let settings = {};
183
654
  if (fs.existsSync(CLAUDE_SETTINGS)) {
184
655
  try { settings = JSON.parse(fs.readFileSync(CLAUDE_SETTINGS, "utf8")); } catch {}
185
656
  }
186
- if (settings.hooks && !settings.hooks.PreCompact) {
187
- settings.hooks.PreCompact = [];
188
- }
189
- if (settings.hooks && settings.hooks.PreCompact) {
190
- const hookPath = path.join(hooksDest, "pre-compact.sh");
191
- if (!settings.hooks.PreCompact.some((h) => h.command && h.command.includes("pre-compact.sh"))) {
192
- settings.hooks.PreCompact.push({
193
- type: "command",
194
- command: `bash ${hookPath}`,
195
- });
196
- fs.writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2));
197
- log(" PreCompact hook added to Claude Code settings.");
198
- }
199
- }
657
+ if (!settings.hooks) settings.hooks = {};
658
+ const migHooksDest = path.join(NEXO_HOME, "hooks");
659
+ registerAllCoreHooks(settings, migHooksDest, NEXO_HOME);
660
+ fs.mkdirSync(path.dirname(CLAUDE_SETTINGS), { recursive: true });
661
+ fs.writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2));
662
+ log(" All 7 core hooks registered in Claude Code settings.");
663
+
664
+ // Regenerate ALL 13 LaunchAgents / systemd timers
665
+ const migSchedule = loadOrCreateSchedule(NEXO_HOME);
666
+ const migPython = findVenvPython(NEXO_HOME) || "python3";
667
+ installAllProcesses(platform, migPython, NEXO_HOME, migSchedule, LAUNCH_AGENTS);
668
+ log(" All 13 automated processes updated.");
200
669
 
201
670
  // Update version file
202
671
  fs.writeFileSync(versionFile, JSON.stringify({
@@ -215,7 +684,20 @@ async function main() {
215
684
  .replace(/\{\{NEXO_HOME\}\}/g, NEXO_HOME);
216
685
  fs.writeFileSync(path.join(NEXO_HOME, "CLAUDE.md.updated"), claudeMd);
217
686
  log(` Updated CLAUDE.md template saved to ~/.nexo/CLAUDE.md.updated`);
218
- log(` Review and merge changes into your ~/.claude/CLAUDE.md if desired.`);
687
+
688
+ // Update CLAUDE.md version tracker (auto_update.py will handle section migration on next server start)
689
+ const migClaudeMdVerMatch = claudeMd.match(/nexo-claude-md-version:\s*([\d.]+)/);
690
+ if (migClaudeMdVerMatch) {
691
+ const migDataDir = path.join(NEXO_HOME, "data");
692
+ fs.mkdirSync(migDataDir, { recursive: true });
693
+ // Don't write the version yet — let auto_update.py detect the diff and migrate sections
694
+ // Only write if no version file exists (first time with version tracking)
695
+ const migVerFile = path.join(migDataDir, "claude_md_version.txt");
696
+ if (!fs.existsSync(migVerFile)) {
697
+ fs.writeFileSync(migVerFile, "0.0.0");
698
+ log(` CLAUDE.md version tracker initialized (will migrate on next server start)`);
699
+ }
700
+ }
219
701
  }
220
702
 
221
703
  console.log("");
@@ -298,6 +780,8 @@ async function main() {
298
780
  const i18n = {
299
781
  en: {
300
782
  langConfirm: "English it is.",
783
+ askDataDir: ` Where should I store my data? (databases, backups, personal plugins)\n Default: ~/.nexo/\n > `,
784
+ dataDirConfirm: (p) => `Data directory: ${p}`,
301
785
  askUserName: " What's your name? > ",
302
786
  userGreet: (n) => `Nice to meet you, ${n}.`,
303
787
  askAgentName: " What should I call myself? (default: NEXO) > ",
@@ -325,6 +809,8 @@ async function main() {
325
809
  },
326
810
  es: {
327
811
  langConfirm: "Español, perfecto.",
812
+ askDataDir: ` ¿Dónde quieres que guarde mis datos? (bases de datos, backups, plugins personales)\n Por defecto: ~/.nexo/\n > `,
813
+ dataDirConfirm: (p) => `Directorio de datos: ${p}`,
328
814
  askUserName: " ¿Cómo te llamas? > ",
329
815
  userGreet: (n) => `Encantado, ${n}.`,
330
816
  askAgentName: " ¿Cómo quieres que me llame? (default: NEXO) > ",
@@ -352,6 +838,8 @@ async function main() {
352
838
  },
353
839
  fr: {
354
840
  langConfirm: "Français, parfait.",
841
+ askDataDir: ` Où stocker mes données ? (bases de données, sauvegardes, plugins)\n Par défaut : ~/.nexo/\n > `,
842
+ dataDirConfirm: (p) => `Répertoire de données : ${p}`,
355
843
  askUserName: " Comment tu t'appelles ? > ",
356
844
  userGreet: (n) => `Enchanté, ${n}.`,
357
845
  askAgentName: " Comment veux-tu m'appeler ? (défaut: NEXO) > ",
@@ -379,6 +867,8 @@ async function main() {
379
867
  },
380
868
  de: {
381
869
  langConfirm: "Deutsch, perfekt.",
870
+ askDataDir: ` Wo sollen meine Daten gespeichert werden? (Datenbanken, Backups, Plugins)\n Standard: ~/.nexo/\n > `,
871
+ dataDirConfirm: (p) => `Datenverzeichnis: ${p}`,
382
872
  askUserName: " Wie heißt du? > ",
383
873
  userGreet: (n) => `Freut mich, ${n}.`,
384
874
  askAgentName: " Wie soll ich heißen? (Standard: NEXO) > ",
@@ -406,6 +896,8 @@ async function main() {
406
896
  },
407
897
  it: {
408
898
  langConfirm: "Italiano, perfetto.",
899
+ askDataDir: ` Dove salvare i miei dati? (database, backup, plugin)\n Default: ~/.nexo/\n > `,
900
+ dataDirConfirm: (p) => `Directory dati: ${p}`,
409
901
  askUserName: " Come ti chiami? > ",
410
902
  userGreet: (n) => `Piacere, ${n}.`,
411
903
  askAgentName: " Come vuoi chiamarmi? (default: NEXO) > ",
@@ -433,6 +925,8 @@ async function main() {
433
925
  },
434
926
  pt: {
435
927
  langConfirm: "Português, perfeito.",
928
+ askDataDir: ` Onde guardar os meus dados? (bases de dados, backups, plugins)\n Padrão: ~/.nexo/\n > `,
929
+ dataDirConfirm: (p) => `Diretório de dados: ${p}`,
436
930
  askUserName: " Como te chamas? > ",
437
931
  userGreet: (n) => `Prazer, ${n}.`,
438
932
  askAgentName: " Como queres que eu me chame? (padrão: NEXO) > ",
@@ -486,6 +980,20 @@ async function main() {
486
980
  console.log("");
487
981
  }
488
982
 
983
+ // Step 1b: Data directory
984
+ if (!useDefaults) {
985
+ const dataDirInput = await ask(t.askDataDir);
986
+ const dataDirTrimmed = dataDirInput.trim();
987
+ if (dataDirTrimmed) {
988
+ // Expand ~ to home dir
989
+ NEXO_HOME = dataDirTrimmed.replace(/^~/, require("os").homedir());
990
+ // Resolve to absolute path
991
+ NEXO_HOME = path.resolve(NEXO_HOME);
992
+ }
993
+ log(t.dataDirConfirm(NEXO_HOME));
994
+ console.log("");
995
+ }
996
+
489
997
  // Step 2: User's name (P2)
490
998
  let userName = "";
491
999
  if (!useDefaults) {
@@ -598,7 +1106,8 @@ async function main() {
598
1106
 
599
1107
  // Use venv python if available, otherwise fall back to system python with --break-system-packages
600
1108
  const pipPython = fs.existsSync(venvPython) ? venvPython : python;
601
- const pipArgs = ["-m", "pip", "install", "--quiet", "fastembed", "numpy", "mcp[cli]"];
1109
+ const requirementsFile = path.join(__dirname, "..", "src", "requirements.txt");
1110
+ const pipArgs = ["-m", "pip", "install", "--quiet", "-r", requirementsFile];
602
1111
  if (!fs.existsSync(venvPython)) {
603
1112
  pipArgs.push("--break-system-packages"); // Fallback for systems without venv
604
1113
  }
@@ -606,7 +1115,7 @@ async function main() {
606
1115
  const pipInstall = spawnSync(pipPython, pipArgs, { stdio: "inherit" });
607
1116
  if (pipInstall.status !== 0) {
608
1117
  log("Failed to install Python dependencies.");
609
- log("Try manually: python3 -m venv ~/.nexo/.venv && ~/.nexo/.venv/bin/pip install fastembed numpy 'mcp[cli]'");
1118
+ log("Try manually: python3 -m venv ~/.nexo/.venv && ~/.nexo/.venv/bin/pip install -r src/requirements.txt");
610
1119
  process.exit(1);
611
1120
  }
612
1121
  // Update python reference to use venv python for the rest of setup
@@ -625,9 +1134,33 @@ async function main() {
625
1134
  path.join(NEXO_HOME, "backups"),
626
1135
  path.join(NEXO_HOME, "coordination"),
627
1136
  path.join(NEXO_HOME, "brain"),
1137
+ path.join(NEXO_HOME, "config"),
1138
+ path.join(NEXO_HOME, "operations"),
628
1139
  ];
629
1140
  dirs.forEach((d) => fs.mkdirSync(d, { recursive: true }));
630
1141
 
1142
+ // Create default evolution-objective.json in brain/ if it doesn't exist
1143
+ const evoObjectivePath = path.join(NEXO_HOME, "brain", "evolution-objective.json");
1144
+ if (!fs.existsSync(evoObjectivePath)) {
1145
+ fs.writeFileSync(evoObjectivePath, JSON.stringify({
1146
+ objective: "Improve operational excellence and reduce repeated errors",
1147
+ focus_areas: ["error_prevention", "proactivity", "memory_quality"],
1148
+ evolution_enabled: true,
1149
+ evolution_mode: "review",
1150
+ dimensions: {
1151
+ episodic_memory: { current: 0, target: 90 },
1152
+ autonomy: { current: 0, target: 80 },
1153
+ proactivity: { current: 0, target: 70 },
1154
+ self_improvement: { current: 0, target: 60 },
1155
+ agi: { current: 0, target: 20 },
1156
+ },
1157
+ total_evolutions: 0,
1158
+ consecutive_failures: 0,
1159
+ created_at: new Date().toISOString(),
1160
+ }, null, 2));
1161
+ log(" Created default evolution-objective.json in brain/");
1162
+ }
1163
+
631
1164
  // Write version file for auto-update tracking
632
1165
  const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
633
1166
  fs.writeFileSync(
@@ -644,16 +1177,38 @@ async function main() {
644
1177
 
645
1178
  // Copy source files
646
1179
  const srcDir = path.join(__dirname, "..", "src");
647
- const scriptsSrcDir = path.join(__dirname, "..", "src", "scripts");
648
- const pluginsSrcDir = path.join(__dirname, "..", "src", "plugins");
1180
+ const pluginsSrcDir = path.join(srcDir, "plugins");
1181
+ const scriptsSrcDir = path.join(srcDir, "scripts");
649
1182
  const templateDir = path.join(__dirname, "..", "templates");
650
1183
 
651
- // Core files
1184
+ // Recursive copy helper (skips __pycache__, .pyc, .db files)
1185
+ const copyDirRecursive = (src, dest) => {
1186
+ fs.mkdirSync(dest, { recursive: true });
1187
+ fs.readdirSync(src).forEach(item => {
1188
+ if (item === "__pycache__" || item.endsWith(".pyc") || item.endsWith(".db")) return;
1189
+ const srcPath = path.join(src, item);
1190
+ const destPath = path.join(dest, item);
1191
+ if (fs.statSync(srcPath).isDirectory()) {
1192
+ copyDirRecursive(srcPath, destPath);
1193
+ } else {
1194
+ fs.copyFileSync(srcPath, destPath);
1195
+ }
1196
+ });
1197
+ };
1198
+
1199
+ // Core flat files (single .py files in src/)
652
1200
  const coreFiles = [
653
1201
  "server.py",
654
- "db.py",
655
1202
  "plugin_loader.py",
656
- "cognitive.py",
1203
+ "knowledge_graph.py",
1204
+ "kg_populate.py",
1205
+ "maintenance.py",
1206
+ "storage_router.py",
1207
+ "claim_graph.py",
1208
+ "hnsw_index.py",
1209
+ "evolution_cycle.py",
1210
+ "migrate_embeddings.py",
1211
+ "auto_close_sessions.py",
657
1212
  "tools_sessions.py",
658
1213
  "tools_coordination.py",
659
1214
  "tools_reminders.py",
@@ -662,12 +1217,6 @@ async function main() {
662
1217
  "tools_credentials.py",
663
1218
  "tools_task_history.py",
664
1219
  "tools_menu.py",
665
- "knowledge_graph.py",
666
- "kg_populate.py",
667
- "maintenance.py",
668
- "storage_router.py",
669
- "migrate_embeddings.py",
670
- "auto_close_sessions.py",
671
1220
  ];
672
1221
  coreFiles.forEach((f) => {
673
1222
  const src = path.join(srcDir, f);
@@ -676,58 +1225,58 @@ async function main() {
676
1225
  }
677
1226
  });
678
1227
 
679
- // Plugins
680
- const pluginFiles = [
681
- "__init__.py",
682
- "guard.py",
683
- "episodic_memory.py",
684
- "cognitive_memory.py",
685
- "entities.py",
686
- "preferences.py",
687
- "agents.py",
688
- "backup.py",
689
- "evolution.py",
690
- "adaptive_mode.py",
691
- "knowledge_graph_tools.py",
692
- ];
693
- pluginFiles.forEach((f) => {
694
- const src = path.join(pluginsSrcDir, f);
695
- if (fs.existsSync(src)) {
696
- fs.copyFileSync(src, path.join(NEXO_HOME, "plugins", f));
1228
+ // Core packages (directories with __init__.py)
1229
+ ["db", "cognitive"].forEach(pkg => {
1230
+ const pkgSrc = path.join(srcDir, pkg);
1231
+ if (fs.existsSync(pkgSrc)) {
1232
+ copyDirRecursive(pkgSrc, path.join(NEXO_HOME, pkg));
697
1233
  }
698
1234
  });
699
1235
 
700
- // Scripts
701
- const scriptFiles = fs.existsSync(scriptsSrcDir)
702
- ? fs.readdirSync(scriptsSrcDir).filter((f) => f.endsWith(".py"))
703
- : [];
704
- scriptFiles.forEach((f) => {
705
- const src = path.join(scriptsSrcDir, f);
706
- if (fs.existsSync(src)) {
707
- fs.copyFileSync(src, path.join(NEXO_HOME, "scripts", f));
708
- }
709
- });
1236
+ // Plugins (all .py files in plugins/)
1237
+ fs.mkdirSync(path.join(NEXO_HOME, "plugins"), { recursive: true });
1238
+ if (fs.existsSync(pluginsSrcDir)) {
1239
+ fs.readdirSync(pluginsSrcDir).filter(f => f.endsWith(".py")).forEach((f) => {
1240
+ fs.copyFileSync(path.join(pluginsSrcDir, f), path.join(NEXO_HOME, "plugins", f));
1241
+ });
1242
+ }
710
1243
 
711
- // Dashboard
1244
+ // Scripts (all files + subdirectories like deep-sleep/)
1245
+ if (fs.existsSync(scriptsSrcDir)) {
1246
+ copyDirRecursive(scriptsSrcDir, path.join(NEXO_HOME, "scripts"));
1247
+ // Make .sh files executable
1248
+ const scriptsDest = path.join(NEXO_HOME, "scripts");
1249
+ fs.readdirSync(scriptsDest).filter(f => f.endsWith(".sh")).forEach(f => {
1250
+ fs.chmodSync(path.join(scriptsDest, f), "755");
1251
+ });
1252
+ }
1253
+
1254
+ // Dashboard (recursive — includes static/, templates/)
712
1255
  const dashSrcDir = path.join(srcDir, "dashboard");
713
- const dashDestDir = path.join(NEXO_HOME, "dashboard");
714
1256
  if (fs.existsSync(dashSrcDir)) {
715
- const copyDirRecursive = (src, dest) => {
716
- fs.mkdirSync(dest, { recursive: true });
717
- fs.readdirSync(src).forEach(item => {
718
- const srcPath = path.join(src, item);
719
- const destPath = path.join(dest, item);
720
- if (fs.statSync(srcPath).isDirectory()) {
721
- copyDirRecursive(srcPath, destPath);
722
- } else {
723
- fs.copyFileSync(srcPath, destPath);
724
- }
725
- });
726
- };
727
- copyDirRecursive(dashSrcDir, dashDestDir);
1257
+ copyDirRecursive(dashSrcDir, path.join(NEXO_HOME, "dashboard"));
728
1258
  log(" Dashboard installed.");
729
1259
  }
730
1260
 
1261
+ // Rules directory
1262
+ const rulesSrcDir = path.join(srcDir, "rules");
1263
+ if (fs.existsSync(rulesSrcDir)) {
1264
+ copyDirRecursive(rulesSrcDir, path.join(NEXO_HOME, "rules"));
1265
+ log(" Rules installed.");
1266
+ }
1267
+
1268
+ // Hooks directory
1269
+ const hooksSrcDir = path.join(srcDir, "hooks");
1270
+ if (fs.existsSync(hooksSrcDir)) {
1271
+ const hooksDest = path.join(NEXO_HOME, "hooks");
1272
+ copyDirRecursive(hooksSrcDir, hooksDest);
1273
+ // Make .sh files executable
1274
+ fs.readdirSync(hooksDest).filter(f => f.endsWith(".sh")).forEach(f => {
1275
+ fs.chmodSync(path.join(hooksDest, f), "755");
1276
+ });
1277
+ log(" Hooks installed.");
1278
+ }
1279
+
731
1280
  // Generate personality
732
1281
  const personality = `# ${operatorName} — Personality
733
1282
 
@@ -1192,285 +1741,26 @@ ${doScan ? `- Stack: ${Object.keys(profileData.code.languages || {}).slice(0, 5)
1192
1741
  },
1193
1742
  };
1194
1743
 
1195
- // Configure hooks for session capture (Sensory Register)
1744
+ // Configure ALL 7 core hooks for session capture (Sensory Register)
1196
1745
  if (!settings.hooks) settings.hooks = {};
1197
1746
 
1198
- // Copy hook scripts to NEXO_HOME
1199
- const hooksSrcDir = path.join(__dirname, "..", "src", "hooks");
1747
+ // Hook scripts already copied above — just reference the dest dir
1200
1748
  const hooksDestDir = path.join(NEXO_HOME, "hooks");
1201
- fs.mkdirSync(hooksDestDir, { recursive: true });
1202
- ["session-start.sh", "capture-session.sh", "session-stop.sh", "pre-compact.sh"].forEach((h) => {
1203
- const src = path.join(hooksSrcDir, h);
1204
- const dest = path.join(hooksDestDir, h);
1205
- if (fs.existsSync(src)) {
1206
- fs.copyFileSync(src, dest);
1207
- fs.chmodSync(dest, "755");
1208
- }
1209
- });
1210
1749
 
1211
- // SessionStart hook
1212
- if (!settings.hooks.SessionStart) settings.hooks.SessionStart = [];
1213
- const startHook = {
1214
- type: "command",
1215
- command: `bash ${path.join(hooksDestDir, "session-start.sh")}`,
1216
- };
1217
- if (!settings.hooks.SessionStart.some((h) => h.command && h.command.includes("session-start.sh"))) {
1218
- settings.hooks.SessionStart.push(startHook);
1219
- }
1220
-
1221
- // PostToolUse hook (captures tool usage to session_buffer)
1222
- if (!settings.hooks.PostToolUse) settings.hooks.PostToolUse = [];
1223
- const captureHook = {
1224
- type: "command",
1225
- command: `bash ${path.join(hooksDestDir, "capture-session.sh")}`,
1226
- };
1227
- if (!settings.hooks.PostToolUse.some((h) => h.command && h.command.includes("capture-session.sh"))) {
1228
- settings.hooks.PostToolUse.push(captureHook);
1229
- }
1230
-
1231
- // Stop hook (session end)
1232
- if (!settings.hooks.Stop) settings.hooks.Stop = [];
1233
- const stopHook = {
1234
- type: "command",
1235
- command: `bash ${path.join(hooksDestDir, "session-stop.sh")}`,
1236
- };
1237
- if (!settings.hooks.Stop.some((h) => h.command && h.command.includes("session-stop.sh"))) {
1238
- settings.hooks.Stop.push(stopHook);
1239
- }
1240
-
1241
- // PreCompact hook (saves context before conversation compression)
1242
- if (!settings.hooks.PreCompact) settings.hooks.PreCompact = [];
1243
- const preCompactHook = {
1244
- type: "command",
1245
- command: `bash ${path.join(hooksDestDir, "pre-compact.sh")}`,
1246
- };
1247
- if (!settings.hooks.PreCompact.some((h) => h.command && h.command.includes("pre-compact.sh"))) {
1248
- settings.hooks.PreCompact.push(preCompactHook);
1249
- }
1750
+ registerAllCoreHooks(settings, hooksDestDir, NEXO_HOME);
1250
1751
 
1251
1752
  const settingsDir = path.dirname(CLAUDE_SETTINGS);
1252
1753
  fs.mkdirSync(settingsDir, { recursive: true });
1253
1754
  fs.writeFileSync(CLAUDE_SETTINGS, JSON.stringify(settings, null, 2));
1254
- log("MCP server + hooks configured in Claude Code settings.");
1755
+ log("MCP server + 7 core hooks configured in Claude Code settings.");
1255
1756
 
1256
- // Step 7: Install LaunchAgents (macOS only)
1757
+ // Step 7: Create schedule.json (only on fresh install) and install ALL 13 processes
1257
1758
  log("Setting up automated processes...");
1258
- if (platform === "darwin") {
1259
- fs.mkdirSync(LAUNCH_AGENTS, { recursive: true });
1260
-
1261
- const agents = [
1262
- {
1263
- name: "cognitive-decay",
1264
- script: "nexo-cognitive-decay.py",
1265
- hour: 3,
1266
- minute: 0,
1267
- },
1268
- {
1269
- name: "postmortem",
1270
- script: "nexo-postmortem-consolidator.py",
1271
- hour: 23,
1272
- minute: 30,
1273
- },
1274
- {
1275
- name: "sleep",
1276
- script: "nexo-sleep.py",
1277
- hour: 4,
1278
- minute: 0,
1279
- },
1280
- {
1281
- name: "self-audit",
1282
- script: "nexo-daily-self-audit.py",
1283
- hour: 7,
1284
- minute: 0,
1285
- },
1286
- { name: "catchup", script: "nexo-catchup.py", runAtLoad: true },
1287
- ];
1759
+ const schedule = loadOrCreateSchedule(NEXO_HOME);
1760
+ installAllProcesses(platform, python, NEXO_HOME, schedule, LAUNCH_AGENTS);
1288
1761
 
1289
- agents.forEach((agent) => {
1290
- const plistName = `com.nexo.${agent.name}.plist`;
1291
- const plistPath = path.join(LAUNCH_AGENTS, plistName);
1292
-
1293
- let scheduleBlock = "";
1294
- if (agent.runAtLoad) {
1295
- scheduleBlock = ` <key>RunAtLoad</key>
1296
- <true/>`;
1297
- } else {
1298
- scheduleBlock = ` <key>StartCalendarInterval</key>
1299
- <dict>
1300
- <key>Hour</key>
1301
- <integer>${agent.hour}</integer>
1302
- <key>Minute</key>
1303
- <integer>${agent.minute}</integer>
1304
- </dict>
1305
- <key>RunAtLoad</key>
1306
- <false/>`;
1307
- }
1308
-
1309
- const plist = `<?xml version="1.0" encoding="UTF-8"?>
1310
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1311
- <plist version="1.0">
1312
- <dict>
1313
- <key>Label</key>
1314
- <string>com.nexo.${agent.name}</string>
1315
- <key>ProgramArguments</key>
1316
- <array>
1317
- <string>${python}</string>
1318
- <string>${path.join(NEXO_HOME, "scripts", agent.script)}</string>
1319
- </array>
1320
- ${scheduleBlock}
1321
- <key>StandardOutPath</key>
1322
- <string>${path.join(NEXO_HOME, "logs", `${agent.name}-stdout.log`)}</string>
1323
- <key>StandardErrorPath</key>
1324
- <string>${path.join(NEXO_HOME, "logs", `${agent.name}-stderr.log`)}</string>
1325
- <key>EnvironmentVariables</key>
1326
- <dict>
1327
- <key>HOME</key>
1328
- <string>${require("os").homedir()}</string>
1329
- <key>NEXO_HOME</key>
1330
- <string>${NEXO_HOME}</string>
1331
- <key>PATH</key>
1332
- <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
1333
- </dict>
1334
- </dict>
1335
- </plist>`;
1336
-
1337
- fs.writeFileSync(plistPath, plist);
1338
- // Register the agent
1339
- try {
1340
- execSync(
1341
- `launchctl bootout gui/$(id -u) "${plistPath}" 2>/dev/null; launchctl bootstrap gui/$(id -u) "${plistPath}"`,
1342
- { stdio: "pipe" }
1343
- );
1344
- } catch {
1345
- // May fail if not previously loaded, that's OK
1346
- }
1347
- });
1348
- log(`${agents.length} automated processes configured.`);
1349
-
1350
- // Caffeinate: keep Mac awake for nocturnal processes
1351
- if (doCaffeinate) {
1352
- const caffHookSrc = path.join(__dirname, "..", "src", "hooks", "caffeinate-guard.sh");
1353
- const caffHookDest = path.join(NEXO_HOME, "hooks", "caffeinate-guard.sh");
1354
- if (fs.existsSync(caffHookSrc)) {
1355
- fs.copyFileSync(caffHookSrc, caffHookDest);
1356
- fs.chmodSync(caffHookDest, "755");
1357
- }
1358
-
1359
- const caffPlist = `<?xml version="1.0" encoding="UTF-8"?>
1360
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1361
- <plist version="1.0">
1362
- <dict>
1363
- <key>Label</key>
1364
- <string>com.nexo.caffeinate</string>
1365
- <key>ProgramArguments</key>
1366
- <array>
1367
- <string>/bin/bash</string>
1368
- <string>${caffHookDest}</string>
1369
- </array>
1370
- <key>RunAtLoad</key>
1371
- <true/>
1372
- <key>KeepAlive</key>
1373
- <true/>
1374
- <key>StandardOutPath</key>
1375
- <string>${path.join(NEXO_HOME, "logs", "caffeinate-stdout.log")}</string>
1376
- <key>StandardErrorPath</key>
1377
- <string>${path.join(NEXO_HOME, "logs", "caffeinate-stderr.log")}</string>
1378
- </dict>
1379
- </plist>`;
1380
-
1381
- const caffPlistPath = path.join(LAUNCH_AGENTS, "com.nexo.caffeinate.plist");
1382
- fs.writeFileSync(caffPlistPath, caffPlist);
1383
- try {
1384
- execSync(
1385
- `launchctl bootout gui/$(id -u) "${caffPlistPath}" 2>/dev/null; launchctl bootstrap gui/$(id -u) "${caffPlistPath}"`,
1386
- { stdio: "pipe" }
1387
- );
1388
- } catch {}
1389
- log("Caffeinate enabled — Mac will stay awake for cognitive processes.");
1390
- }
1391
- } else if (platform === "linux") {
1392
- // Linux: use systemd user timers (preferred) or crontab as fallback
1393
- const systemdDir = path.join(require("os").homedir(), ".config", "systemd", "user");
1394
- const hasSystemd = run("which systemctl") && run("systemctl --user status 2>/dev/null");
1395
-
1396
- if (hasSystemd) {
1397
- fs.mkdirSync(systemdDir, { recursive: true });
1398
-
1399
- const linuxAgents = [
1400
- { name: "cognitive-decay", script: "nexo-cognitive-decay.py", calendar: "*-*-* 03:00:00" },
1401
- { name: "postmortem", script: "nexo-postmortem-consolidator.py", calendar: "*-*-* 23:30:00" },
1402
- { name: "sleep", script: "nexo-sleep.py", calendar: "*-*-* 04:00:00" },
1403
- { name: "self-audit", script: "nexo-daily-self-audit.py", calendar: "*-*-* 07:00:00" },
1404
- ];
1405
-
1406
- linuxAgents.forEach((agent) => {
1407
- const serviceName = `nexo-${agent.name}`;
1408
- const serviceFile = path.join(systemdDir, `${serviceName}.service`);
1409
- const timerFile = path.join(systemdDir, `${serviceName}.timer`);
1410
-
1411
- const service = `[Unit]
1412
- Description=NEXO Brain — ${agent.name}
1413
-
1414
- [Service]
1415
- Type=oneshot
1416
- ExecStart=${venvPython} ${path.join(NEXO_HOME, "scripts", agent.script)}
1417
- Environment=HOME=${require("os").homedir()}
1418
- Environment=NEXO_HOME=${NEXO_HOME}
1419
- StandardOutput=append:${path.join(NEXO_HOME, "logs", `${agent.name}-stdout.log`)}
1420
- StandardError=append:${path.join(NEXO_HOME, "logs", `${agent.name}-stderr.log`)}
1421
- `;
1422
-
1423
- const timer = `[Unit]
1424
- Description=NEXO Brain — ${agent.name} timer
1425
-
1426
- [Timer]
1427
- OnCalendar=${agent.calendar}
1428
- Persistent=true
1429
-
1430
- [Install]
1431
- WantedBy=timers.target
1432
- `;
1433
-
1434
- fs.writeFileSync(serviceFile, service);
1435
- fs.writeFileSync(timerFile, timer);
1436
- try {
1437
- execSync(`systemctl --user enable --now ${serviceName}.timer 2>/dev/null`, { stdio: "pipe" });
1438
- } catch {}
1439
- });
1440
-
1441
- // Catchup runs at startup via MCP — no timer needed
1442
- log(`${linuxAgents.length} systemd user timers configured.`);
1443
- } else {
1444
- // Fallback: crontab
1445
- log("systemd not available, configuring crontab...");
1446
- const cronLines = [
1447
- `0 3 * * * ${venvPython} ${path.join(NEXO_HOME, "scripts", "nexo-cognitive-decay.py")} >> ${path.join(NEXO_HOME, "logs", "cognitive-decay-stdout.log")} 2>&1`,
1448
- `30 23 * * * ${venvPython} ${path.join(NEXO_HOME, "scripts", "nexo-postmortem-consolidator.py")} >> ${path.join(NEXO_HOME, "logs", "postmortem-stdout.log")} 2>&1`,
1449
- `0 4 * * * ${venvPython} ${path.join(NEXO_HOME, "scripts", "nexo-sleep.py")} >> ${path.join(NEXO_HOME, "logs", "sleep-stdout.log")} 2>&1`,
1450
- `0 7 * * * ${venvPython} ${path.join(NEXO_HOME, "scripts", "nexo-daily-self-audit.py")} >> ${path.join(NEXO_HOME, "logs", "self-audit-stdout.log")} 2>&1`,
1451
- ];
1452
-
1453
- try {
1454
- const existingCron = run("crontab -l 2>/dev/null") || "";
1455
- const nexoCronMarker = "# NEXO Brain automated processes";
1456
- if (!existingCron.includes(nexoCronMarker)) {
1457
- const newCron = existingCron + "\n" + nexoCronMarker + "\n" + cronLines.join("\n") + "\n";
1458
- const tmpCron = path.join(NEXO_HOME, ".crontab-tmp");
1459
- fs.writeFileSync(tmpCron, newCron);
1460
- execSync(`crontab ${tmpCron}`, { stdio: "pipe" });
1461
- fs.unlinkSync(tmpCron);
1462
- log(`${cronLines.length} cron jobs configured.`);
1463
- } else {
1464
- log("NEXO cron jobs already configured.");
1465
- }
1466
- } catch (e) {
1467
- log(`Could not configure crontab: ${e.message}`);
1468
- log("Background tasks will run via catch-up on startup.");
1469
- }
1470
- }
1471
- } else {
1472
- log("Unsupported platform for background tasks. Maintenance runs on MCP startup.");
1473
- }
1762
+ // Note: prevent-sleep and tcc-approve are now part of ALL_PROCESSES
1763
+ // and installed by installAllProcesses() above. No separate caffeinate block needed.
1474
1764
 
1475
1765
  // Step 8: Create shell alias so user can just type the operator's name
1476
1766
  log("Creating shell alias...");
@@ -1528,6 +1818,15 @@ See ~/.nexo/ for configuration.
1528
1818
  );
1529
1819
  }
1530
1820
 
1821
+ // Write initial CLAUDE.md version tracker
1822
+ const claudeMdVersionMatch = claudeMd.match(/nexo-claude-md-version:\s*([\d.]+)/);
1823
+ if (claudeMdVersionMatch) {
1824
+ const dataDir = path.join(NEXO_HOME, "data");
1825
+ fs.mkdirSync(dataDir, { recursive: true });
1826
+ fs.writeFileSync(path.join(dataDir, "claude_md_version.txt"), claudeMdVersionMatch[1]);
1827
+ log(`CLAUDE.md version tracker initialized: v${claudeMdVersionMatch[1]}`);
1828
+ }
1829
+
1531
1830
  console.log("");
1532
1831
  const readyMsg = t.ready(operatorName, aliasName);
1533
1832
  const readySub = t.readySubtext;