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,74 +0,0 @@
1
- #!/bin/bash
2
- # nexo-inbox-hook.sh — PostToolUse: automatic inter-terminal inbox check (D+)
3
- #
4
- # Zero output when no messages = zero tokens consumed in Claude's context.
5
- # Reads SQLite directly (no MCP overhead). Write-only: INSERT OR IGNORE for mark-as-read.
6
- # Debounce: skips if last check was <2 seconds ago.
7
-
8
- INPUT=$(cat)
9
-
10
- # 1. Skip read-only tools (same logic as capture-tool-logs.sh)
11
- TOOL_NAME=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_name',''))" 2>/dev/null)
12
- case "$TOOL_NAME" in
13
- Read|Grep|Glob|LS|Skill|ToolSearch|Agent) exit 0 ;;
14
- esac
15
-
16
- # 2. Extract Claude Code session_id
17
- CLAUDE_SID=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('session_id',''))" 2>/dev/null)
18
- [ -z "$CLAUDE_SID" ] && exit 0
19
-
20
- # 3. Debounce: skip if last check <2s ago
21
- DEBOUNCE_FILE="/tmp/nexo-inbox-ts-${CLAUDE_SID}"
22
- NOW=$(date +%s)
23
- LAST=$(cat "$DEBOUNCE_FILE" 2>/dev/null || echo 0)
24
- DIFF=$((NOW - LAST))
25
- [ "$DIFF" -lt 2 ] && exit 0
26
- echo "$NOW" > "$DEBOUNCE_FILE"
27
-
28
- # 4. Find NEXO SID mapped to this Claude session_id
29
- NEXO_HOME="${NEXO_HOME:-$HOME/.nexo}"
30
- DB="$NEXO_HOME/data/nexo.db"
31
- [ -f "$DB" ] || exit 0
32
-
33
- NEXO_SID=$(sqlite3 "$DB" "SELECT sid FROM sessions WHERE claude_session_id = '${CLAUDE_SID}' AND last_update_epoch > (strftime('%s','now') - 900) ORDER BY last_update_epoch DESC LIMIT 1;" 2>/dev/null)
34
- [ -z "$NEXO_SID" ] && exit 0
35
-
36
- # 5. Check inbox — messages addressed to this session or broadcast
37
- MESSAGES=$(sqlite3 -separator '|' "$DB" "
38
- SELECT m.id, m.from_sid, m.text FROM messages m
39
- WHERE (m.to_sid = 'all' OR m.to_sid = '${NEXO_SID}')
40
- AND m.from_sid != '${NEXO_SID}'
41
- AND m.id NOT IN (SELECT message_id FROM message_reads WHERE sid = '${NEXO_SID}')
42
- LIMIT 5;
43
- " 2>/dev/null)
44
-
45
- # 6. Check pending questions
46
- QUESTIONS=$(sqlite3 -separator '|' "$DB" "
47
- SELECT qid, from_sid, question FROM questions
48
- WHERE to_sid = '${NEXO_SID}' AND answer IS NULL
49
- LIMIT 3;
50
- " 2>/dev/null)
51
-
52
- # 7. If empty → silent exit (0 tokens consumed)
53
- [ -z "$MESSAGES" ] && [ -z "$QUESTIONS" ] && exit 0
54
-
55
- # 8. Format and output (injected into Claude's context)
56
- echo ""
57
- echo "📨 INTER-TERMINAL MESSAGE (auto-detected):"
58
-
59
- if [ -n "$MESSAGES" ]; then
60
- echo "$MESSAGES" | while IFS='|' read -r mid from text; do
61
- echo " [$from]: $text"
62
- # Mark as read (lightweight INSERT, WAL mode, no lock contention)
63
- sqlite3 "$DB" "INSERT OR IGNORE INTO message_reads (message_id, sid) VALUES ('${mid}', '${NEXO_SID}');" 2>/dev/null
64
- done
65
- fi
66
-
67
- if [ -n "$QUESTIONS" ]; then
68
- echo " ⚠ PREGUNTAS de otra terminal — responder con nexo_answer:"
69
- echo "$QUESTIONS" | while IFS='|' read -r qid from question; do
70
- echo " Q[$qid] de [$from]: $question"
71
- done
72
- fi
73
-
74
- exit 0
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env python3
2
- """DEPRECATED: Use 'npx nexo-brain' instead. This installer is no longer maintained."""
3
- import sys
4
- print("This installer is deprecated. Please use: npx nexo-brain")
5
- print("See: https://github.com/wazionapps/nexo#installation")
6
- sys.exit(1)
@@ -1,245 +0,0 @@
1
- #!/usr/bin/env python3
2
- """NEXO Learning Housekeeping — Nightly dedup, weight adjustment, and review.
3
-
4
- Runs daily. Adjusts learning weights based on usage (guard_hits),
5
- detects duplicates via semantic similarity, and archives stale learnings.
6
- """
7
-
8
- import json
9
- import os
10
- import sqlite3
11
- import sys
12
- import time
13
- from datetime import datetime, timedelta
14
- from pathlib import Path
15
-
16
- NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
17
- # Auto-detect: if running from repo (src/scripts/), use src/ as NEXO_CODE
18
- _script_dir = Path(__file__).resolve().parent
19
- _repo_src = _script_dir.parent # src/scripts/ -> src/
20
- NEXO_CODE = Path(os.environ.get("NEXO_CODE", str(_repo_src) if (_repo_src / "server.py").exists() else str(NEXO_HOME)))
21
-
22
- sys.path.insert(0, str(NEXO_CODE))
23
-
24
- DB_PATH = NEXO_HOME / "data" / "nexo.db"
25
- STATE_FILE = NEXO_HOME / "operations" / ".catchup-state.json"
26
-
27
- # Weight adjustment rates
28
- GUARD_HIT_BOOST = 0.02 # per guard hit since last run
29
- DECAY_RATE = 0.005 # daily decay for unused learnings
30
- MIN_WEIGHT = 0.05
31
- MAX_WEIGHT = 1.0
32
- DEDUP_THRESHOLD = 0.85 # cosine similarity for duplicate detection
33
- ARCHIVE_AFTER_DAYS = 90 # archive if weight < 0.1 and no hits in this many days
34
-
35
-
36
- def get_db():
37
- conn = sqlite3.connect(str(DB_PATH))
38
- conn.row_factory = sqlite3.Row
39
- return conn
40
-
41
-
42
- def update_catchup_state():
43
- try:
44
- state = json.loads(STATE_FILE.read_text()) if STATE_FILE.exists() else {}
45
- except Exception:
46
- state = {}
47
- state["learning-housekeep"] = datetime.now().isoformat()
48
- STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
49
- STATE_FILE.write_text(json.dumps(state, indent=2))
50
-
51
-
52
- def adjust_weights(conn):
53
- """Boost weight for frequently-used learnings, decay unused ones."""
54
- ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
55
- now = time.time()
56
- one_day_ago = now - 86400
57
-
58
- learnings = conn.execute(
59
- "SELECT id, weight, guard_hits, last_guard_hit_at, priority, created_at "
60
- "FROM learnings WHERE status = 'active'"
61
- ).fetchall()
62
-
63
- adjusted = 0
64
- for l in learnings:
65
- old_weight = l["weight"] or 0.5
66
- hits = l["guard_hits"] or 0
67
- last_hit = l["last_guard_hit_at"] or 0
68
- priority = l["priority"] or "medium"
69
-
70
- # Priority floor — critical learnings never drop below 0.5
71
- priority_floor = {"critical": 0.5, "high": 0.3, "medium": 0.1, "low": 0.05}[priority]
72
-
73
- new_weight = old_weight
74
-
75
- if last_hit > one_day_ago:
76
- # Recent guard hit — boost
77
- recent_hits = 1 # Simplified: at least 1 hit today
78
- new_weight = min(MAX_WEIGHT, old_weight + (GUARD_HIT_BOOST * recent_hits))
79
- else:
80
- # No recent hits — decay
81
- new_weight = max(priority_floor, old_weight - DECAY_RATE)
82
-
83
- new_weight = max(MIN_WEIGHT, min(MAX_WEIGHT, new_weight))
84
-
85
- if abs(new_weight - old_weight) > 0.001:
86
- conn.execute("UPDATE learnings SET weight = ? WHERE id = ?", (round(new_weight, 4), l["id"]))
87
- adjusted += 1
88
-
89
- conn.commit()
90
- print(f"[{ts}] Weight adjustment: {adjusted}/{len(learnings)} learnings adjusted")
91
- return adjusted
92
-
93
-
94
- def auto_prioritize(conn):
95
- """Auto-upgrade priority based on guard hits and repetitions."""
96
- ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
97
-
98
- # Learnings with 10+ guard hits that are still medium → upgrade to high
99
- upgraded = conn.execute(
100
- "UPDATE learnings SET priority = 'high', weight = MAX(weight, 0.7) "
101
- "WHERE status = 'active' AND priority = 'medium' AND guard_hits >= 10"
102
- ).rowcount
103
-
104
- # Learnings with repetitions (same error happened again) → upgrade to high
105
- repeated = conn.execute(
106
- """UPDATE learnings SET priority = 'high', weight = MAX(weight, 0.7)
107
- WHERE status = 'active' AND priority IN ('medium', 'low')
108
- AND id IN (SELECT original_learning_id FROM error_repetitions)"""
109
- ).rowcount
110
-
111
- conn.commit()
112
- total = upgraded + repeated
113
- if total > 0:
114
- print(f"[{ts}] Auto-prioritize: {upgraded} by guard_hits, {repeated} by repetitions")
115
- return total
116
-
117
-
118
- def detect_duplicates(conn):
119
- """Find semantically similar learnings using fastembed."""
120
- ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
121
- try:
122
- from fastembed import TextEmbedding
123
- import numpy as np
124
- except ImportError:
125
- print(f"[{ts}] Dedup skipped: fastembed not available")
126
- return []
127
-
128
- learnings = conn.execute(
129
- "SELECT id, title, content, weight, guard_hits FROM learnings WHERE status = 'active'"
130
- ).fetchall()
131
-
132
- if len(learnings) < 2:
133
- return []
134
-
135
- model = TextEmbedding("BAAI/bge-base-en-v1.5")
136
- texts = [f"{l['title']}: {l['content'][:300]}" for l in learnings]
137
- embeddings = list(model.embed(texts))
138
- embeddings = np.array(embeddings)
139
-
140
- # Normalize
141
- norms = np.linalg.norm(embeddings, axis=1, keepdims=True)
142
- norms[norms == 0] = 1
143
- embeddings = embeddings / norms
144
-
145
- duplicates = []
146
- for i in range(len(learnings)):
147
- for j in range(i + 1, len(learnings)):
148
- sim = float(np.dot(embeddings[i], embeddings[j]))
149
- if sim >= DEDUP_THRESHOLD:
150
- # Keep the one with higher weight/hits
151
- a, b = learnings[i], learnings[j]
152
- score_a = (a["weight"] or 0.5) + (a["guard_hits"] or 0) * 0.01
153
- score_b = (b["weight"] or 0.5) + (b["guard_hits"] or 0) * 0.01
154
- keep, drop = (a, b) if score_a >= score_b else (b, a)
155
- duplicates.append({
156
- "keep_id": keep["id"], "keep_title": keep["title"],
157
- "drop_id": drop["id"], "drop_title": drop["title"],
158
- "similarity": round(sim, 3)
159
- })
160
-
161
- if duplicates:
162
- print(f"[{ts}] Duplicates found: {len(duplicates)} pairs (>= {DEDUP_THRESHOLD})")
163
- for d in duplicates[:10]:
164
- print(f"[{ts}] [{d['similarity']}] keep #{d['keep_id']} '{d['keep_title'][:40]}', archive #{d['drop_id']} '{d['drop_title'][:40]}'")
165
- # Archive the duplicate (don't delete — just mark inactive)
166
- conn.execute("UPDATE learnings SET status = 'archived' WHERE id = ?", (d["drop_id"],))
167
- conn.commit()
168
- else:
169
- print(f"[{ts}] No duplicates found ({len(learnings)} learnings scanned)")
170
-
171
- return duplicates
172
-
173
-
174
- def archive_stale(conn):
175
- """Archive learnings with very low weight and no recent guard hits."""
176
- ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
177
- cutoff = time.time() - (ARCHIVE_AFTER_DAYS * 86400)
178
-
179
- stale = conn.execute(
180
- "SELECT id, title, weight, last_guard_hit_at FROM learnings "
181
- "WHERE status = 'active' AND weight < 0.1 AND priority NOT IN ('critical', 'high') "
182
- "AND (last_guard_hit_at IS NULL OR last_guard_hit_at < ?)",
183
- (cutoff,)
184
- ).fetchall()
185
-
186
- if stale:
187
- for s in stale:
188
- conn.execute("UPDATE learnings SET status = 'archived' WHERE id = ?", (s["id"],))
189
- print(f"[{ts}] Archived #{s['id']} '{s['title'][:50]}' (weight={s['weight']:.2f})")
190
- conn.commit()
191
- print(f"[{ts}] Archived {len(stale)} stale learnings")
192
- else:
193
- print(f"[{ts}] No stale learnings to archive")
194
-
195
- return len(stale)
196
-
197
-
198
- def print_summary(conn):
199
- """Print summary stats."""
200
- ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
201
- stats = conn.execute(
202
- """SELECT
203
- COUNT(*) as total,
204
- SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active,
205
- SUM(CASE WHEN status = 'archived' THEN 1 ELSE 0 END) as archived,
206
- SUM(CASE WHEN priority = 'critical' THEN 1 ELSE 0 END) as critical,
207
- SUM(CASE WHEN priority = 'high' THEN 1 ELSE 0 END) as high,
208
- SUM(CASE WHEN priority = 'medium' THEN 1 ELSE 0 END) as medium,
209
- SUM(CASE WHEN priority = 'low' THEN 1 ELSE 0 END) as low,
210
- printf('%.2f', AVG(CASE WHEN status = 'active' THEN weight END)) as avg_weight
211
- FROM learnings"""
212
- ).fetchone()
213
- print(f"[{ts}] Summary: {stats['active']} active, {stats['archived']} archived | "
214
- f"Priority: {stats['critical']}C {stats['high']}H {stats['medium']}M {stats['low']}L | "
215
- f"Avg weight: {stats['avg_weight']}")
216
-
217
-
218
- def main():
219
- ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
220
- print(f"[{ts}] Learning housekeeping starting...")
221
-
222
- conn = get_db()
223
-
224
- # 1. Adjust weights based on usage
225
- adjust_weights(conn)
226
-
227
- # 2. Auto-prioritize based on guard hits and repetitions
228
- auto_prioritize(conn)
229
-
230
- # 3. Detect and archive duplicates
231
- detect_duplicates(conn)
232
-
233
- # 4. Archive stale learnings
234
- archive_stale(conn)
235
-
236
- # 5. Summary
237
- print_summary(conn)
238
-
239
- conn.close()
240
- update_catchup_state()
241
- print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Done.")
242
-
243
-
244
- if __name__ == "__main__":
245
- main()
@@ -1,207 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- NEXO Learning Validator — Cross-checks findings against existing learnings.
4
-
5
- Wrapper collects the finding + all learnings from SQLite, then passes
6
- to Claude CLI (opus) to make an intelligent determination of whether
7
- the finding is known, related, or genuinely new.
8
-
9
- Usage as CLI:
10
- python3 nexo-learning-validator.py "finding text to validate"
11
- python3 nexo-learning-validator.py --category project "finding text"
12
-
13
- Usage as library:
14
- from nexo_learning_validator import validate_finding
15
- result = validate_finding("CRITICAL: message_id column is NULL")
16
- if result["known"]:
17
- print(f"Already known: {result['matching_learnings']}")
18
-
19
- Exit codes:
20
- 0 = Finding is NEW (not known)
21
- 1 = Finding is KNOWN (matches existing learning)
22
- """
23
-
24
- import json
25
- import os
26
- import sqlite3
27
- import subprocess
28
- import sys
29
- from pathlib import Path
30
-
31
- NEXO_HOME = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
32
-
33
- NEXO_DB = NEXO_HOME / "data" / "nexo.db"
34
- CLAUDE_CLI = Path.home() / ".local" / "bin" / "claude"
35
-
36
-
37
- def get_all_learnings(category: str = None) -> list[dict]:
38
- """Fetch all learnings from nexo.db."""
39
- conn = sqlite3.connect(str(NEXO_DB), timeout=10)
40
- conn.row_factory = sqlite3.Row
41
- if category:
42
- rows = conn.execute(
43
- "SELECT id, category, title, content FROM learnings WHERE category = ?",
44
- (category,)
45
- ).fetchall()
46
- else:
47
- rows = conn.execute(
48
- "SELECT id, category, title, content FROM learnings"
49
- ).fetchall()
50
- conn.close()
51
- return [dict(r) for r in rows]
52
-
53
-
54
- def validate_finding(finding: str, category: str = None) -> dict:
55
- """
56
- Validate a finding against existing learnings using Claude CLI.
57
-
58
- Returns:
59
- {
60
- "known": bool,
61
- "confidence": float (0-1),
62
- "matching_learnings": [{"id": int, "title": str, "similarity": float}],
63
- "recommendation": str
64
- }
65
- """
66
- learnings = get_all_learnings(category)
67
-
68
- if not learnings:
69
- return {
70
- "known": False,
71
- "confidence": 0,
72
- "matching_learnings": [],
73
- "recommendation": "No learnings in DB — finding is new by default"
74
- }
75
-
76
- # Build compact learnings reference for CLI
77
- learnings_ref = []
78
- for l in learnings:
79
- learnings_ref.append({
80
- "id": l["id"],
81
- "cat": l["category"],
82
- "title": l["title"],
83
- "content": (l["content"] or "")[:300],
84
- })
85
-
86
- prompt = f"""You are a finding deduplication engine. Compare a new finding against existing learnings and determine if it's already known.
87
-
88
- NEW FINDING:
89
- {finding}
90
-
91
- EXISTING LEARNINGS ({len(learnings_ref)} total):
92
- {json.dumps(learnings_ref, indent=1)}
93
-
94
- Respond with ONLY valid JSON (no markdown, no code fences):
95
- {{
96
- "known": true/false,
97
- "confidence": 0.0-1.0,
98
- "matching_learnings": [
99
- {{"id": <learning_id>, "title": "<title>", "similarity": 0.0-1.0}}
100
- ],
101
- "recommendation": "<one line: KNOWN/LIKELY KNOWN/POSSIBLY RELATED/NEW>"
102
- }}
103
-
104
- Rules:
105
- - confidence >= 0.7 and same root cause = known: true
106
- - confidence 0.55-0.7 and related topic = known: true, say LIKELY KNOWN
107
- - confidence < 0.55 = known: false
108
- - Max 5 matching_learnings, sorted by similarity descending
109
- - If the finding describes the SAME bug/issue/pattern as a learning, it's known even if worded differently
110
- - Be strict: different symptoms of different bugs are NOT the same even if they mention the same file"""
111
-
112
- # Try CLI first, fall back to mechanical similarity
113
- if CLAUDE_CLI.exists():
114
- # Fallback: mechanical SequenceMatcher (original logic)
115
- return _mechanical_validate(finding, learnings)
116
-
117
-
118
- def _mechanical_validate(finding: str, learnings: list[dict]) -> dict:
119
- """Fallback validation using SequenceMatcher when CLI is unavailable."""
120
- from difflib import SequenceMatcher
121
-
122
- threshold = 0.45
123
- finding_kw = _extract_keywords(finding)
124
- matches = []
125
-
126
- for learning in learnings:
127
- title_sim = SequenceMatcher(None, finding.lower(), learning["title"].lower()).ratio()
128
- content_sim = SequenceMatcher(None, finding.lower(), (learning["content"] or "").lower()).ratio()
129
-
130
- learning_text = f"{learning['title']} {learning['content'] or ''}"
131
- learning_kw = _extract_keywords(learning_text)
132
- kw_overlap = len(finding_kw & learning_kw) / len(finding_kw) if finding_kw and learning_kw else 0
133
-
134
- combined = max(title_sim, content_sim) * 0.6 + kw_overlap * 0.4
135
-
136
- if combined >= threshold:
137
- matches.append({
138
- "id": learning["id"],
139
- "category": learning["category"],
140
- "title": learning["title"],
141
- "similarity": round(combined, 3),
142
- })
143
-
144
- matches.sort(key=lambda x: x["similarity"], reverse=True)
145
- top = matches[:5]
146
-
147
- if not top:
148
- return {"known": False, "confidence": 0, "matching_learnings": [], "recommendation": "NEW finding"}
149
-
150
- best = top[0]["similarity"]
151
- if best >= 0.7:
152
- return {"known": True, "confidence": best, "matching_learnings": top,
153
- "recommendation": f"KNOWN issue (learning #{top[0]['id']})"}
154
- elif best >= 0.55:
155
- return {"known": True, "confidence": best, "matching_learnings": top,
156
- "recommendation": f"LIKELY KNOWN (learning #{top[0]['id']})"}
157
- else:
158
- return {"known": False, "confidence": best, "matching_learnings": top,
159
- "recommendation": "POSSIBLY RELATED but different enough to report"}
160
-
161
-
162
- def _extract_keywords(text: str) -> set:
163
- """Extract meaningful keywords from text."""
164
- stop_words = {
165
- 'the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being',
166
- 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could',
167
- 'should', 'may', 'might', 'must', 'shall', 'can', 'need', 'dare',
168
- 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from', 'as',
169
- 'and', 'but', 'or', 'nor', 'not', 'so', 'yet', 'both', 'either',
170
- 'error', 'critical', 'warning', 'bug', 'issue', 'problem', 'fix',
171
- 'el', 'la', 'los', 'las', 'un', 'una', 'de', 'en', 'que', 'por',
172
- }
173
- words = set()
174
- for word in text.lower().split():
175
- clean = ''.join(c for c in word if c.isalnum() or c == '_')
176
- if clean and len(clean) > 2 and clean not in stop_words:
177
- words.add(clean)
178
- return words
179
-
180
-
181
- def main():
182
- import argparse
183
- parser = argparse.ArgumentParser(description="Validate findings against existing NEXO learnings")
184
- parser.add_argument("finding", help="The finding text to validate")
185
- parser.add_argument("--category", "-c", help="Filter learnings by category")
186
- parser.add_argument("--json", "-j", action="store_true", help="Output as JSON")
187
- args = parser.parse_args()
188
-
189
- result = validate_finding(args.finding, args.category)
190
-
191
- if args.json:
192
- print(json.dumps(result, indent=2))
193
- else:
194
- status = "KNOWN" if result["known"] else "NEW"
195
- print(f"Status: {status} (confidence: {result['confidence']:.0%})")
196
- print(f"Recommendation: {result['recommendation']}")
197
- if result["matching_learnings"]:
198
- print(f"Related learnings:")
199
- for m in result["matching_learnings"]:
200
- cat = m.get('category', '?')
201
- print(f" #{m['id']} [{cat}] {m['title']} ({m['similarity']:.0%})")
202
-
203
- sys.exit(1 if result["known"] else 0)
204
-
205
-
206
- if __name__ == "__main__":
207
- main()