nexo-brain 1.7.0 → 2.0.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 (247) hide show
  1. package/README.md +25 -24
  2. package/bin/nexo-brain.js +680 -381
  3. package/package.json +4 -1
  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_close_sessions.cpython-310.pyc +0 -0
  8. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  9. package/src/__pycache__/auto_update.cpython-314.pyc +0 -0
  10. package/src/__pycache__/claim_graph.cpython-310.pyc +0 -0
  11. package/src/__pycache__/evolution_cycle.cpython-310.pyc +0 -0
  12. package/src/__pycache__/evolution_cycle.cpython-314.pyc +0 -0
  13. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  14. package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
  15. package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
  16. package/src/__pycache__/knowledge_graph.cpython-314.pyc +0 -0
  17. package/src/__pycache__/maintenance.cpython-310.pyc +0 -0
  18. package/src/__pycache__/migrate_embeddings.cpython-310.pyc +0 -0
  19. package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
  20. package/src/__pycache__/server.cpython-310.pyc +0 -0
  21. package/src/__pycache__/server.cpython-314.pyc +0 -0
  22. package/src/__pycache__/storage_router.cpython-310.pyc +0 -0
  23. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  24. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  25. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  26. package/src/__pycache__/tools_learnings.cpython-314.pyc +0 -0
  27. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  28. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  29. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  30. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  31. package/src/__pycache__/tools_sessions.cpython-314.pyc +0 -0
  32. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  33. package/src/auto_close_sessions.py +1 -1
  34. package/src/auto_update.py +634 -0
  35. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  36. package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  37. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  38. package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
  39. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  40. package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
  41. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  42. package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
  43. package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
  44. package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
  45. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  46. package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
  47. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  48. package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
  49. package/src/cognitive/_core.py +7 -3
  50. package/src/cognitive/_decay.py +1 -1
  51. package/src/cognitive/_search.py +1 -0
  52. package/src/cognitive/_trust.py +3 -3
  53. package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
  54. package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
  55. package/src/dashboard/app.py +8 -2
  56. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  57. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  58. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  59. package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
  60. package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
  61. package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
  62. package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
  63. package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
  64. package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
  65. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  66. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  67. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  68. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  69. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  70. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  71. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  72. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  73. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  74. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  75. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  76. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  77. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  78. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  79. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  80. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  81. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  82. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  83. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  84. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  85. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  86. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  87. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  88. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  89. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  90. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  91. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  92. package/src/db/_core.py +5 -1
  93. package/src/db/_episodic.py +1 -3
  94. package/src/db/_reminders.py +36 -1
  95. package/src/db/_schema.py +31 -0
  96. package/src/evolution_cycle.py +33 -11
  97. package/src/hooks/__pycache__/auto_capture.cpython-310.pyc +0 -0
  98. package/src/hooks/auto_capture.py +1 -1
  99. package/src/hooks/capture-tool-logs.sh +76 -0
  100. package/src/hooks/inbox-hook.sh +2 -1
  101. package/src/hooks/post-compact.sh +2 -1
  102. package/src/hooks/pre-compact.sh +104 -2
  103. package/src/hooks/session-start.sh +6 -2
  104. package/src/hooks/session-stop.sh +2 -1
  105. package/src/kg_populate.py +4 -1
  106. package/src/migrate_embeddings.py +4 -1
  107. package/src/plugin_loader.py +100 -34
  108. package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
  109. package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
  110. package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
  111. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  112. package/src/plugins/__pycache__/agents.cpython-314.pyc +0 -0
  113. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  114. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  115. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  116. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  117. package/src/plugins/__pycache__/core_rules.cpython-314.pyc +0 -0
  118. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  119. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  120. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  121. package/src/plugins/__pycache__/episodic_memory.cpython-314.pyc +0 -0
  122. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  123. package/src/plugins/__pycache__/evolution.cpython-314.pyc +0 -0
  124. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  125. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  126. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  127. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  128. package/src/plugins/agents.py +2 -2
  129. package/src/plugins/backup.py +5 -4
  130. package/src/plugins/core_rules.py +5 -1
  131. package/src/plugins/episodic_memory.py +14 -5
  132. package/src/plugins/evolution.py +6 -2
  133. package/src/plugins/guard.py +20 -11
  134. package/src/plugins/update.py +238 -0
  135. package/src/requirements.txt +12 -0
  136. package/src/rules/__pycache__/__init__.cpython-310.pyc +0 -0
  137. package/src/rules/__pycache__/migrate.cpython-310.pyc +0 -0
  138. package/src/scripts/__pycache__/check-context.cpython-310.pyc +0 -0
  139. package/src/scripts/__pycache__/check-context.cpython-314.pyc +0 -0
  140. package/src/scripts/__pycache__/nexo-auto-update.cpython-310.pyc +0 -0
  141. package/src/scripts/__pycache__/nexo-catchup.cpython-310.pyc +0 -0
  142. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  143. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-310.pyc +0 -0
  144. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  145. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-310.pyc +0 -0
  146. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  147. package/src/scripts/__pycache__/nexo-evolution-run.cpython-310.pyc +0 -0
  148. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  149. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-310.pyc +0 -0
  150. package/src/scripts/__pycache__/nexo-github-monitor.cpython-310.pyc +0 -0
  151. package/src/scripts/__pycache__/nexo-github-monitor.cpython-314.pyc +0 -0
  152. package/src/scripts/__pycache__/nexo-immune.cpython-310.pyc +0 -0
  153. package/src/scripts/__pycache__/nexo-install.cpython-310.pyc +0 -0
  154. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-310.pyc +0 -0
  155. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
  156. package/src/scripts/__pycache__/nexo-learning-validator.cpython-310.pyc +0 -0
  157. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  158. package/src/scripts/__pycache__/nexo-migrate.cpython-310.pyc +0 -0
  159. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-310.pyc +0 -0
  160. package/src/scripts/__pycache__/nexo-pre-commit.cpython-310.pyc +0 -0
  161. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-310.pyc +0 -0
  162. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
  163. package/src/scripts/__pycache__/nexo-reflection.cpython-310.pyc +0 -0
  164. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-310.pyc +0 -0
  165. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
  166. package/src/scripts/__pycache__/nexo-send-email.cpython-310.pyc +0 -0
  167. package/src/scripts/__pycache__/nexo-send-reply.cpython-310.pyc +0 -0
  168. package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
  169. package/src/scripts/__pycache__/nexo-sleep.cpython-310.pyc +0 -0
  170. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  171. package/src/scripts/__pycache__/nexo-synthesis.cpython-310.pyc +0 -0
  172. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-310.pyc +0 -0
  173. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
  174. package/src/scripts/check-context.py +9 -1
  175. package/src/scripts/deep-sleep/__pycache__/analyze_session.cpython-310.pyc +0 -0
  176. package/src/scripts/deep-sleep/__pycache__/analyze_session.cpython-314.pyc +0 -0
  177. package/src/scripts/deep-sleep/__pycache__/apply_findings.cpython-310.pyc +0 -0
  178. package/src/scripts/deep-sleep/__pycache__/apply_findings.cpython-314.pyc +0 -0
  179. package/src/scripts/deep-sleep/__pycache__/collect_transcripts.cpython-310.pyc +0 -0
  180. package/src/scripts/deep-sleep/apply_findings.py +3 -3
  181. package/src/scripts/nexo-auto-update.py +4 -211
  182. package/src/scripts/nexo-backup.sh +5 -13
  183. package/src/scripts/nexo-brain-activation.sh +26 -26
  184. package/src/scripts/nexo-catchup.py +36 -22
  185. package/src/scripts/nexo-cognitive-decay.py +7 -3
  186. package/src/scripts/nexo-daily-self-audit.py +30 -10
  187. package/src/scripts/nexo-evolution-run.py +35 -10
  188. package/src/scripts/nexo-followup-hygiene.py +2 -2
  189. package/src/scripts/nexo-github-monitor.py +11 -4
  190. package/src/scripts/nexo-immune.py +17 -2
  191. package/src/scripts/nexo-inbox-hook.sh +2 -1
  192. package/src/scripts/nexo-install.py +4 -225
  193. package/src/scripts/nexo-learning-housekeep.py +7 -3
  194. package/src/scripts/nexo-learning-validator.py +10 -2
  195. package/src/scripts/nexo-migrate.py +9 -3
  196. package/src/scripts/nexo-postmortem-consolidator.py +22 -4
  197. package/src/scripts/nexo-pre-commit.py +3 -1
  198. package/src/scripts/nexo-prevent-sleep.sh +29 -0
  199. package/src/scripts/nexo-proactive-dashboard.py +4 -4
  200. package/src/scripts/nexo-runtime-preflight.py +59 -55
  201. package/src/scripts/nexo-send-email.py +1 -1
  202. package/src/scripts/nexo-send-reply.py +3 -1
  203. package/src/scripts/nexo-sleep.py +19 -5
  204. package/src/scripts/nexo-snapshot-restore.sh +2 -1
  205. package/src/scripts/nexo-synthesis.py +17 -2
  206. package/src/scripts/nexo-tcc-approve.sh +79 -0
  207. package/src/scripts/nexo-update.sh +161 -0
  208. package/src/scripts/nexo-watchdog-smoke.py +18 -13
  209. package/src/scripts/nexo-watchdog.sh +22 -13
  210. package/src/server.py +77 -28
  211. package/src/storage_router.py +6 -2
  212. package/src/tools_learnings.py +6 -6
  213. package/src/tools_reminders_crud.py +10 -8
  214. package/src/tools_sessions.py +9 -4
  215. package/templates/CLAUDE.md.template +14 -80
  216. package/templates/launchagents/README.md +7 -7
  217. package/templates/launchagents/com.nexo.auto-close-sessions.plist +5 -1
  218. package/templates/launchagents/com.nexo.catchup.plist +4 -0
  219. package/templates/launchagents/com.nexo.cognitive-decay.plist +7 -0
  220. package/templates/launchagents/com.nexo.dashboard.plist +5 -1
  221. package/templates/launchagents/com.nexo.deep-sleep.plist +4 -0
  222. package/templates/launchagents/com.nexo.evolution.plist +4 -0
  223. package/templates/launchagents/com.nexo.followup-hygiene.plist +4 -0
  224. package/templates/launchagents/com.nexo.github-monitor.plist +3 -1
  225. package/templates/launchagents/com.nexo.immune.plist +4 -0
  226. package/templates/launchagents/com.nexo.postmortem.plist +4 -0
  227. package/templates/launchagents/com.nexo.self-audit.plist +4 -0
  228. package/templates/launchagents/com.nexo.synthesis.plist +4 -0
  229. package/templates/launchagents/com.nexo.watchdog.plist +4 -0
  230. package/templates/openclaw.json +1 -1
  231. package/tests/__pycache__/__init__.cpython-310.pyc +0 -0
  232. package/tests/__pycache__/__init__.cpython-314.pyc +0 -0
  233. package/tests/__pycache__/conftest.cpython-310-pytest-9.0.2.pyc +0 -0
  234. package/tests/__pycache__/conftest.cpython-310.pyc +0 -0
  235. package/tests/__pycache__/conftest.cpython-314-pytest-9.0.2.pyc +0 -0
  236. package/tests/__pycache__/test_cognitive.cpython-310-pytest-9.0.2.pyc +0 -0
  237. package/tests/__pycache__/test_cognitive.cpython-310.pyc +0 -0
  238. package/tests/__pycache__/test_cognitive.cpython-314-pytest-9.0.2.pyc +0 -0
  239. package/tests/__pycache__/test_knowledge_graph.cpython-310-pytest-9.0.2.pyc +0 -0
  240. package/tests/__pycache__/test_knowledge_graph.cpython-310.pyc +0 -0
  241. package/tests/__pycache__/test_knowledge_graph.cpython-314-pytest-9.0.2.pyc +0 -0
  242. package/tests/__pycache__/test_migrations.cpython-310-pytest-9.0.2.pyc +0 -0
  243. package/tests/__pycache__/test_migrations.cpython-310.pyc +0 -0
  244. package/tests/__pycache__/test_migrations.cpython-314-pytest-9.0.2.pyc +0 -0
  245. package/tests/conftest.py +2 -2
  246. package/tests/test_cognitive.py +7 -6
  247. package/tests/test_migrations.py +26 -0
@@ -48,7 +48,7 @@ from datetime import date
48
48
  today_str = '$TODAY'
49
49
  weekday = '$WEEKDAY'
50
50
  nexo_home = os.environ.get('NEXO_HOME', os.path.expanduser('~/.nexo'))
51
- db_path = os.path.join(nexo_home, 'nexo.db')
51
+ db_path = os.path.join(nexo_home, 'data', 'nexo.db')
52
52
 
53
53
  lines = []
54
54
  lines.append(f'## Date: {today_str} ({weekday})')
@@ -212,7 +212,11 @@ except Exception as e:
212
212
  fi
213
213
 
214
214
  # ─── Cortex Report: what happened while user was away ───
215
- CORTEX_BRIEFING="$NEXO_HOME/cortex/last-briefing.json"
215
+ # Check brain/ (canonical) first, fall back to cortex/ (legacy)
216
+ CORTEX_BRIEFING="$NEXO_HOME/brain/last-briefing.json"
217
+ if [ ! -f "$CORTEX_BRIEFING" ] && [ -f "$NEXO_HOME/cortex/last-briefing.json" ]; then
218
+ CORTEX_BRIEFING="$NEXO_HOME/cortex/last-briefing.json"
219
+ fi
216
220
  if [ -f "$CORTEX_BRIEFING" ]; then
217
221
  CORTEX_SECTION=$(python3 -c "
218
222
  import json
@@ -25,7 +25,8 @@ TOOL_LOG="$NEXO_HOME/operations/tool-logs/${TODAY}.jsonl"
25
25
  python3 -c "
26
26
  import sys, json, os
27
27
  nexo_home = os.environ.get('NEXO_HOME', os.path.expanduser('~/.nexo'))
28
- sys.path.insert(0, os.path.join(nexo_home, 'nexo-mcp'))
28
+ nexo_code = os.environ.get('NEXO_CODE', nexo_home)
29
+ sys.path.insert(0, nexo_code)
29
30
  os.environ['NEXO_SKIP_FS_INDEX'] = '1'
30
31
  from db import init_db, get_db, get_active_sessions, upsert_diary_draft, get_diary_draft
31
32
  init_db()
@@ -13,7 +13,10 @@ from db import get_db
13
13
 
14
14
  def _cognitive_db():
15
15
  """Direct cognitive.db connection (for somatic_markers)."""
16
- path = os.path.join(os.path.dirname(__file__), "cognitive.db")
16
+ nexo_home = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
17
+ data_dir = os.path.join(nexo_home, "data")
18
+ os.makedirs(data_dir, exist_ok=True)
19
+ path = os.path.join(data_dir, "cognitive.db")
17
20
  conn = sqlite3.connect(path)
18
21
  conn.row_factory = sqlite3.Row
19
22
  return conn
@@ -15,7 +15,10 @@ import sys
15
15
  import time
16
16
  import numpy as np
17
17
 
18
- DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "cognitive.db")
18
+ NEXO_HOME = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
19
+ _data_dir = os.path.join(NEXO_HOME, "data")
20
+ os.makedirs(_data_dir, exist_ok=True)
21
+ DB_PATH = os.path.join(_data_dir, "cognitive.db")
19
22
  BACKUP_PATH = DB_PATH + ".bak-384dims-pre-upgrade"
20
23
 
21
24
  MODELS = {
@@ -1,6 +1,7 @@
1
1
  """Dynamic plugin loader for NEXO MCP server."""
2
2
 
3
3
  import importlib
4
+ import importlib.util
4
5
  import os
5
6
  import signal
6
7
  import sys
@@ -9,7 +10,12 @@ import time
9
10
  from db import get_db
10
11
  from fastmcp.tools import Tool
11
12
 
12
- PLUGINS_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "plugins")
13
+ SERVER_DIR = os.path.dirname(os.path.abspath(__file__))
14
+ PLUGINS_DIR = os.path.join(SERVER_DIR, "plugins")
15
+
16
+ # Personal plugins directory: NEXO_HOME/plugins/ (env var, defaults to ~/.nexo/)
17
+ NEXO_HOME = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
18
+ PERSONAL_PLUGINS_DIR = os.path.join(NEXO_HOME, "plugins")
13
19
 
14
20
  PLUGIN_LOAD_TIMEOUT = 10 # seconds per plugin
15
21
 
@@ -22,41 +28,95 @@ def _timeout_handler(signum, frame):
22
28
  raise _PluginTimeout("Plugin loading timed out")
23
29
 
24
30
 
31
+ def _ensure_src_in_path():
32
+ """Ensure server src/ is in sys.path so personal plugins can import db, cognitive, etc."""
33
+ if SERVER_DIR not in sys.path:
34
+ sys.path.insert(0, SERVER_DIR)
35
+
36
+
25
37
  def load_all_plugins(mcp) -> int:
26
- """Load all plugins from plugins/ directory at startup. Returns total tools loaded."""
27
- if not os.path.isdir(PLUGINS_DIR):
28
- return 0
38
+ """Load all plugins from repo and personal directories at startup. Returns total tools loaded."""
39
+ _ensure_src_in_path()
29
40
  total = 0
30
- for f in sorted(os.listdir(PLUGINS_DIR)):
31
- if f.endswith(".py") and f != "__init__.py":
41
+
42
+ # Collect plugins: repo first, personal overrides
43
+ plugin_map = {} # filename -> (dir_path, source_label)
44
+
45
+ # 1. Repo plugins (base)
46
+ if os.path.isdir(PLUGINS_DIR):
47
+ for f in sorted(os.listdir(PLUGINS_DIR)):
48
+ if f.endswith(".py") and f != "__init__.py":
49
+ plugin_map[f] = (PLUGINS_DIR, "repo")
50
+
51
+ # 2. Personal plugins (override if same filename)
52
+ if os.path.isdir(PERSONAL_PLUGINS_DIR):
53
+ for f in sorted(os.listdir(PERSONAL_PLUGINS_DIR)):
54
+ if f.endswith(".py") and f != "__init__.py":
55
+ source = "personal (override)" if f in plugin_map else "personal"
56
+ plugin_map[f] = (PERSONAL_PLUGINS_DIR, source)
57
+
58
+ # Load all in sorted order
59
+ for f in sorted(plugin_map):
60
+ plugins_dir, source_label = plugin_map[f]
61
+ try:
62
+ old_handler = signal.signal(signal.SIGALRM, _timeout_handler)
63
+ signal.alarm(PLUGIN_LOAD_TIMEOUT)
32
64
  try:
33
- old_handler = signal.signal(signal.SIGALRM, _timeout_handler)
34
- signal.alarm(PLUGIN_LOAD_TIMEOUT)
35
- try:
36
- n = load_plugin(mcp, f)
37
- total += n
38
- finally:
39
- signal.alarm(0)
40
- signal.signal(signal.SIGALRM, old_handler)
41
- except _PluginTimeout:
42
- print(f"[PLUGIN TIMEOUT] {f}: skipped after {PLUGIN_LOAD_TIMEOUT}s", file=sys.stderr)
43
- except Exception as e:
44
- print(f"[PLUGIN ERROR] {f}: {e}", file=sys.stderr)
65
+ n = load_plugin(mcp, f, plugins_dir=plugins_dir)
66
+ total += n
67
+ print(f"[PLUGIN LOADED] {f} ({n} tools) from {source_label}: {plugins_dir}", file=sys.stderr)
68
+ finally:
69
+ signal.alarm(0)
70
+ signal.signal(signal.SIGALRM, old_handler)
71
+ except _PluginTimeout:
72
+ print(f"[PLUGIN TIMEOUT] {f}: skipped after {PLUGIN_LOAD_TIMEOUT}s", file=sys.stderr)
73
+ except Exception as e:
74
+ print(f"[PLUGIN ERROR] {f}: {e}", file=sys.stderr)
45
75
  return total
46
76
 
47
77
 
48
- def load_plugin(mcp, filename: str) -> int:
49
- """Load or reload a single plugin. Returns number of tools registered."""
78
+ def load_plugin(mcp, filename: str, plugins_dir: str | None = None) -> int:
79
+ """Load or reload a single plugin. Returns number of tools registered.
80
+
81
+ Args:
82
+ plugins_dir: Directory to load from. If None, searches repo PLUGINS_DIR first,
83
+ then PERSONAL_PLUGINS_DIR. Personal plugins are loaded via
84
+ importlib.util.spec_from_file_location.
85
+ """
50
86
  if not filename.endswith(".py"):
51
87
  filename += ".py"
52
88
 
53
- filepath = os.path.join(PLUGINS_DIR, filename)
54
- if not os.path.isfile(filepath):
55
- raise FileNotFoundError(f"Plugin not found: {filepath}")
89
+ if plugins_dir is not None:
90
+ filepath = os.path.join(plugins_dir, filename)
91
+ if not os.path.isfile(filepath):
92
+ raise FileNotFoundError(f"Plugin not found: {filepath}")
93
+ else:
94
+ # Search repo first, then personal
95
+ repo_path = os.path.join(PLUGINS_DIR, filename)
96
+ personal_path = os.path.join(PERSONAL_PLUGINS_DIR, filename)
97
+ if os.path.isfile(repo_path):
98
+ plugins_dir = PLUGINS_DIR
99
+ filepath = repo_path
100
+ elif os.path.isfile(personal_path):
101
+ plugins_dir = PERSONAL_PLUGINS_DIR
102
+ filepath = personal_path
103
+ else:
104
+ raise FileNotFoundError(
105
+ f"Plugin not found in repo ({PLUGINS_DIR}) or personal ({PERSONAL_PLUGINS_DIR}): {filename}"
106
+ )
56
107
 
57
108
  module_name = f"plugins.{filename[:-3]}"
58
109
 
59
- if module_name in sys.modules:
110
+ # For personal plugins (outside repo), use spec_from_file_location
111
+ if plugins_dir != PLUGINS_DIR:
112
+ _ensure_src_in_path()
113
+ spec = importlib.util.spec_from_file_location(module_name, filepath)
114
+ if spec is None or spec.loader is None:
115
+ raise ImportError(f"Cannot create module spec for {filepath}")
116
+ mod = importlib.util.module_from_spec(spec)
117
+ sys.modules[module_name] = mod
118
+ spec.loader.exec_module(mod)
119
+ elif module_name in sys.modules:
60
120
  mod = importlib.reload(sys.modules[module_name])
61
121
  else:
62
122
  mod = importlib.import_module(module_name)
@@ -73,13 +133,18 @@ def load_plugin(mcp, filename: str) -> int:
73
133
  mcp.add_tool(t)
74
134
  tool_names.append(name)
75
135
 
76
- _update_registry(filename, len(tool_names), ",".join(tool_names), "manual")
136
+ source_label = "personal" if plugins_dir != PLUGINS_DIR else "repo"
137
+ _update_registry(filename, len(tool_names), ",".join(tool_names), source_label)
77
138
 
78
139
  return len(tool_names)
79
140
 
80
141
 
81
142
  def remove_plugin(mcp, filename: str) -> list[str]:
82
- """Remove a plugin: unregister its tools, delete file, clean registry."""
143
+ """Unregister a plugin's tools from MCP and clean the registry.
144
+
145
+ Does NOT delete plugin files — only unregisters tools to avoid
146
+ accidental deletion of code from repo or personal directories.
147
+ """
83
148
  if not filename.endswith(".py"):
84
149
  filename += ".py"
85
150
 
@@ -100,10 +165,6 @@ def remove_plugin(mcp, filename: str) -> list[str]:
100
165
  module_name = f"plugins.{filename[:-3]}"
101
166
  sys.modules.pop(module_name, None)
102
167
 
103
- filepath = os.path.join(PLUGINS_DIR, filename)
104
- if os.path.isfile(filepath):
105
- os.remove(filepath)
106
-
107
168
  conn = get_db()
108
169
  conn.execute("DELETE FROM plugins WHERE filename = ?", (filename,))
109
170
  conn.commit()
@@ -112,12 +173,17 @@ def remove_plugin(mcp, filename: str) -> list[str]:
112
173
 
113
174
 
114
175
  def list_plugins() -> list[dict]:
115
- """List all registered plugins."""
176
+ """List all registered plugins with source info (repo/personal)."""
116
177
  conn = get_db()
117
178
  rows = conn.execute(
118
179
  "SELECT filename, tools_count, tool_names, loaded_at, created_by FROM plugins ORDER BY filename"
119
180
  ).fetchall()
120
- return [dict(r) for r in rows]
181
+ result = []
182
+ for r in rows:
183
+ d = dict(r)
184
+ d["source"] = d.get("created_by", "repo")
185
+ result.append(d)
186
+ return result
121
187
 
122
188
 
123
189
  def _update_registry(filename: str, tools_count: int, tool_names: str, created_by: str):
@@ -128,8 +194,8 @@ def _update_registry(filename: str, tools_count: int, tool_names: str, created_b
128
194
  conn.execute(
129
195
  "INSERT INTO plugins (filename, tools_count, tool_names, loaded_at, created_by) "
130
196
  "VALUES (?, ?, ?, ?, ?) "
131
- "ON CONFLICT(filename) DO UPDATE SET tools_count=?, tool_names=?, loaded_at=?",
132
- (filename, tools_count, tool_names, now, created_by, tools_count, tool_names, now),
197
+ "ON CONFLICT(filename) DO UPDATE SET tools_count=?, tool_names=?, loaded_at=?, created_by=?",
198
+ (filename, tools_count, tool_names, now, created_by, tools_count, tool_names, now, created_by),
133
199
  )
134
200
  conn.commit()
135
201
  except Exception as e:
@@ -15,7 +15,7 @@ def handle_agent_create(id: str, name: str, specialization: str, model: str = "s
15
15
  tools: str = "", context_files: str = "", rules: str = "") -> str:
16
16
  """Register a new agent in the registry."""
17
17
  create_agent(id, name, specialization, model, tools, context_files, rules)
18
- return f"Agente '{id}' ({name}) registered. Model: {model}"
18
+ return f"Agent '{id}' ({name}) registered. Model: {model}"
19
19
 
20
20
  def handle_agent_update(id: str, name: str = "", specialization: str = "", model: str = "",
21
21
  tools: str = "", context_files: str = "", rules: str = "") -> str:
@@ -32,7 +32,7 @@ def handle_agent_list() -> str:
32
32
  """List all registered agents."""
33
33
  agents = list_agents()
34
34
  if not agents: return "No agents registered."
35
- lines = ["AGENTES REGISTRADOS:"]
35
+ lines = ["REGISTERED AGENTS:"]
36
36
  for a in agents:
37
37
  lines.append(f" {a['id']} — {a['name']} ({a['model']}) — {a['specialization'][:60]}")
38
38
  return "\n".join(lines)
@@ -5,8 +5,9 @@ import time
5
5
  import glob
6
6
  from db import get_db
7
7
 
8
- DB_PATH = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "nexo.db"))
9
- BACKUP_DIR = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "backups"))
8
+ NEXO_HOME = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
9
+ DB_PATH = os.path.join(NEXO_HOME, "data", "nexo.db")
10
+ BACKUP_DIR = os.path.join(NEXO_HOME, "backups")
10
11
 
11
12
  RETENTION_DAYS = 7
12
13
 
@@ -33,10 +34,10 @@ def handle_backup_now() -> str:
33
34
  def handle_backup_list() -> str:
34
35
  """List available backups with dates and sizes."""
35
36
  if not os.path.isdir(BACKUP_DIR):
36
- return "Sin backups."
37
+ return "No backups."
37
38
  files = sorted(glob.glob(os.path.join(BACKUP_DIR, "nexo-*.db")), reverse=True)
38
39
  if not files:
39
- return "Sin backups."
40
+ return "No backups."
40
41
  lines = [f"BACKUPS ({len(files)}):"]
41
42
  total_size = 0
42
43
  for f in files:
@@ -12,7 +12,11 @@ def _get_db():
12
12
  def _seed_if_empty():
13
13
  """Seed rules from JSON if table is empty (first run after migration)."""
14
14
  conn = _get_db()
15
- count = conn.execute("SELECT COUNT(*) FROM core_rules WHERE is_active = 1").fetchone()[0]
15
+ try:
16
+ count = conn.execute("SELECT COUNT(*) FROM core_rules WHERE is_active = 1").fetchone()[0]
17
+ except Exception:
18
+ # Table doesn't exist yet — migrations haven't run. Bail gracefully.
19
+ return
16
20
  if count > 0:
17
21
  return
18
22
 
@@ -39,7 +39,7 @@ def handle_decision_log(domain: str, decision: str, alternatives: str = '',
39
39
  if domain not in valid_domains:
40
40
  return f"ERROR: domain must be one of: {', '.join(sorted(valid_domains))}"
41
41
  if confidence not in ('high', 'medium', 'low'):
42
- return f"ERROR: confidence debe ser high, medium, o low"
42
+ return f"ERROR: confidence must be high, medium, or low"
43
43
 
44
44
  sid = session_id or 'unknown'
45
45
  result = log_decision(sid, domain, decision, alternatives, based_on, confidence, context_ref)
@@ -84,7 +84,7 @@ def handle_decision_outcome(id: int, outcome: str) -> str:
84
84
  (id,)
85
85
  )
86
86
  conn.commit()
87
- return f"Decision #{id} outcome registrado: {outcome[:100]}"
87
+ return f"Decision #{id} outcome recorded: {outcome[:100]}"
88
88
 
89
89
 
90
90
  def handle_decision_search(query: str = '', domain: str = '', days: int = 30) -> str:
@@ -297,7 +297,7 @@ def handle_change_log(files: str, what_changed: str, why: str,
297
297
  session_id: Current session ID
298
298
  """
299
299
  if not files or not what_changed or not why:
300
- return "ERROR: files, what_changed, y why son obligatorios"
300
+ return "ERROR: files, what_changed, and why are required"
301
301
  sid = session_id or 'unknown'
302
302
  result = log_change(sid, files, what_changed, why, triggered_by, affects, risks, verify, commit_ref)
303
303
  if "error" in result:
@@ -313,7 +313,7 @@ def handle_change_log(files: str, what_changed: str, why: str,
313
313
  on_change_log(change_id, files, "")
314
314
  except Exception:
315
315
  pass
316
- msg = f"Change #{change_id} registrado: {files[:60]} — {what_changed[:60]}"
316
+ msg = f"Change #{change_id} recorded: {files[:60]} — {what_changed[:60]}"
317
317
  if not commit_ref:
318
318
  msg += f"\n⚠ NO COMMIT. Use nexo_change_commit({change_id}, 'hash') after push, or 'server-direct' if it was a direct server edit."
319
319
  return msg
@@ -351,6 +351,9 @@ def handle_change_search(query: str = '', files: str = '', days: int = 30) -> st
351
351
  def handle_change_commit(id: int, commit_ref: str) -> str:
352
352
  """Link a change log entry to its git commit hash after committing.
353
353
 
354
+ After linking, automatically resolves any open followups that match
355
+ the change (by file overlap, keyword similarity, or explicit ID reference).
356
+
354
357
  Args:
355
358
  id: Change log entry ID
356
359
  commit_ref: Git commit hash
@@ -358,7 +361,13 @@ def handle_change_commit(id: int, commit_ref: str) -> str:
358
361
  result = update_change_commit(id, commit_ref)
359
362
  if "error" in result:
360
363
  return f"ERROR: {result['error']}"
361
- return f"Change #{id} vinculado a commit {commit_ref[:8]}"
364
+
365
+ msg = f"Change #{id} vinculado a commit {commit_ref[:8]}"
366
+ auto_resolved = result.get("_auto_resolved", [])
367
+ if auto_resolved:
368
+ ids = ", ".join(auto_resolved)
369
+ msg += f"\n✅ AUTO-RESOLVED followups: {ids}"
370
+ return msg
362
371
 
363
372
 
364
373
  def handle_recall(query: str, days: int = 30) -> str:
@@ -1,5 +1,6 @@
1
1
  """Evolution plugin — NEXO self-improvement tools for interactive sessions."""
2
2
 
3
+ import os
3
4
  from db import get_latest_metrics, get_evolution_history, update_evolution_log_status, get_db
4
5
 
5
6
 
@@ -59,9 +60,12 @@ def handle_evolution_propose() -> str:
59
60
  import json
60
61
  from pathlib import Path
61
62
  nexo_home = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
62
- obj_file = nexo_home / "cortex" / "evolution-objective.json"
63
+ # Check brain/ (canonical) first, fall back to cortex/ (legacy)
64
+ obj_file = nexo_home / "brain" / "evolution-objective.json"
63
65
  if not obj_file.exists():
64
- return "ERROR: evolution-objective.json not found"
66
+ obj_file = nexo_home / "cortex" / "evolution-objective.json"
67
+ if not obj_file.exists():
68
+ return "ERROR: evolution-objective.json not found. Run the installer or create one in ~/.nexo/brain/"
65
69
  try:
66
70
  obj = json.loads(obj_file.read_text())
67
71
  if not obj.get("evolution_enabled", True):
@@ -110,16 +110,22 @@ def handle_guard_check(files: str = "", area: str = "", include_schemas: str = "
110
110
  w = r["weight"] or 0.5
111
111
  result["learnings"].append({"id": r["id"], "category": r["category"], "rule": r["title"], "priority": pri, "weight": w})
112
112
 
113
- # 3. Universal rules (SIEMPRE, NUNCA, ANTES, always, never)
113
+ # 3. Universal rules only from matching area or nexo-ops (not ALL learnings)
114
+ universal_categories = {"nexo-ops"}
115
+ if area:
116
+ universal_categories.add(area)
117
+ placeholders = ",".join("?" for _ in universal_categories)
114
118
  rows = conn.execute(
115
- "SELECT id, category, title, content FROM learnings WHERE "
116
- "content LIKE '%SIEMPRE%' OR content LIKE '%NUNCA%' OR content LIKE '%ANTES%' "
117
- "OR content LIKE '%always%' OR content LIKE '%never%'"
119
+ f"SELECT id, category, title, content, priority FROM learnings WHERE "
120
+ f"category IN ({placeholders}) AND ("
121
+ f"content LIKE '%SIEMPRE%' OR content LIKE '%NUNCA%' OR content LIKE '%ANTES%' "
122
+ f"OR content LIKE '%always%' OR content LIKE '%never%')",
123
+ tuple(universal_categories)
118
124
  ).fetchall()
119
125
  for r in rows:
120
126
  if r["id"] not in seen_ids:
121
127
  seen_ids.add(r["id"])
122
- result["universal_rules"].append({"id": r["id"], "rule": r["title"], "category": r["category"]})
128
+ result["universal_rules"].append({"id": r["id"], "rule": r["title"], "category": r["category"], "priority": r["priority"] or "medium"})
123
129
 
124
130
  # 4. DB schemas if files contain SQL keywords
125
131
  if include_schemas_bool and file_list:
@@ -173,8 +179,9 @@ def handle_guard_check(files: str = "", area: str = "", include_schemas: str = "
173
179
  })
174
180
  continue
175
181
 
176
- # Path (b): Aggressive learning TITLE contains prohibition keywords
177
- if BLOCKING_KEYWORDS.search(learning["rule"]):
182
+ # Path (b): Only promote to blocking if high/critical priority AND title has prohibition keyword
183
+ pri = learning.get("priority", "medium")
184
+ if pri in ("critical", "high") and BLOCKING_KEYWORDS.search(learning["rule"]):
178
185
  blocking_seen.add(lid)
179
186
  result["blocking_rules"].append({
180
187
  "id": lid, "rule": learning["rule"], "repetitions": rep_count,
@@ -297,8 +304,9 @@ def handle_guard_check(files: str = "", area: str = "", include_schemas: str = "
297
304
  lines.append("")
298
305
 
299
306
  if result["learnings"]:
300
- lines.append(f"RELEVANT LEARNINGS ({len(result['learnings'])}):")
301
- for l in result["learnings"][:15]:
307
+ shown = result["learnings"][:10] # Cap at 10, not 15
308
+ lines.append(f"RELEVANT LEARNINGS ({len(result['learnings'])}, showing {len(shown)}):")
309
+ for l in shown:
302
310
  lines.append(f" #{l['id']} [{l['category']}] {l['rule']}")
303
311
  lines.append("")
304
312
 
@@ -310,8 +318,9 @@ def handle_guard_check(files: str = "", area: str = "", include_schemas: str = "
310
318
  lines.append("")
311
319
 
312
320
  if result["universal_rules"]:
313
- lines.append(f"UNIVERSAL RULES ({len(result['universal_rules'])}):")
314
- for r in result["universal_rules"][:10]:
321
+ shown_u = result["universal_rules"][:5] # Cap at 5
322
+ lines.append(f"UNIVERSAL RULES ({len(result['universal_rules'])}, showing {len(shown_u)}):")
323
+ for r in shown_u:
315
324
  lines.append(f" #{r['id']} {r['rule']}")
316
325
  lines.append("")
317
326