nexo-brain 2.2.0 → 2.3.1

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 (256) hide show
  1. package/README.md +5 -5
  2. package/package.json +6 -3
  3. package/src/auto_update.py +26 -0
  4. package/src/crons/manifest.json +6 -13
  5. package/src/crons/sync.py +150 -6
  6. package/src/db/__init__.py +13 -0
  7. package/src/db/_core.py +1 -0
  8. package/src/db/_cron_runs.py +74 -0
  9. package/src/db/_entities.py +1 -0
  10. package/src/db/_episodic.py +41 -6
  11. package/src/db/_learnings.py +1 -0
  12. package/src/db/_reminders.py +1 -0
  13. package/src/db/_schema.py +64 -0
  14. package/src/db/_sessions.py +1 -0
  15. package/src/db/_skills.py +515 -0
  16. package/src/hooks/session-stop.sh +13 -101
  17. package/src/plugin_loader.py +1 -0
  18. package/src/plugins/episodic_memory.py +5 -3
  19. package/src/plugins/schedule.py +212 -0
  20. package/src/plugins/skills.py +264 -0
  21. package/src/plugins/update.py +1 -0
  22. package/src/scripts/deep-sleep/apply_findings.py +111 -8
  23. package/src/scripts/deep-sleep/collect.py +34 -11
  24. package/src/scripts/deep-sleep/extract-prompt.md +38 -0
  25. package/src/scripts/deep-sleep/extract.py +81 -8
  26. package/src/scripts/deep-sleep/synthesize-prompt.md +29 -1
  27. package/src/scripts/deep-sleep/synthesize.py +4 -1
  28. package/src/scripts/nexo-catchup.py +65 -29
  29. package/src/scripts/nexo-cron-wrapper.sh +53 -0
  30. package/src/scripts/nexo-daily-self-audit.py +4 -2
  31. package/src/scripts/nexo-deep-sleep.sh +66 -77
  32. package/src/scripts/nexo-evolution-run.py +13 -0
  33. package/src/scripts/nexo-learning-housekeep.py +157 -1
  34. package/src/scripts/nexo-learning-validator.py +19 -0
  35. package/src/scripts/nexo-postmortem-consolidator.py +3 -2
  36. package/src/scripts/nexo-sleep.py +16 -11
  37. package/src/scripts/nexo-synthesis.py +46 -3
  38. package/src/scripts/nexo-watchdog.sh +91 -30
  39. package/src/server.py +6 -1
  40. package/src/tools_coordination.py +1 -0
  41. package/src/tools_sessions.py +1 -0
  42. package/scripts/migrate-to-unified 2.sh +0 -813
  43. package/scripts/migrate-to-unified.sh +0 -813
  44. package/scripts/migrate-v1.5-to-v1.6 2.py +0 -778
  45. package/scripts/migrate-v1.5-to-v1.6.py +0 -778
  46. package/scripts/migrate-v1.7-to-v1.8 2.py +0 -214
  47. package/scripts/migrate-v1.7-to-v1.8.py +0 -214
  48. package/scripts/pre-commit-check 2.sh +0 -55
  49. package/scripts/pre-commit-check.sh +0 -55
  50. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  51. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  52. package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
  53. package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
  54. package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
  55. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  56. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  57. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  58. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  59. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  60. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  61. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  62. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  63. package/src/auto_close_sessions 2.py +0 -159
  64. package/src/auto_update 2.py +0 -634
  65. package/src/claim_graph 2.py +0 -323
  66. package/src/cognitive/__init__ 2.py +0 -62
  67. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  68. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  69. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  70. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  71. package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
  72. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  73. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  74. package/src/cognitive/_core 2.py +0 -567
  75. package/src/cognitive/_decay 2.py +0 -382
  76. package/src/cognitive/_ingest 2.py +0 -892
  77. package/src/cognitive/_memory 2.py +0 -912
  78. package/src/cognitive/_search 2.py +0 -949
  79. package/src/cognitive/_trust 2.py +0 -464
  80. package/src/crons/manifest 2.json +0 -106
  81. package/src/crons/sync 2.py +0 -217
  82. package/src/dashboard/__init__ 2.py +0 -0
  83. package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
  84. package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
  85. package/src/dashboard/app 2.py +0 -789
  86. package/src/db/__init__ 2.py +0 -89
  87. package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
  88. package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
  89. package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
  90. package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
  91. package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
  92. package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
  93. package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
  94. package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
  95. package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
  96. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  97. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  98. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  99. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  100. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  101. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  102. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  103. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  104. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  105. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  106. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  107. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  108. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  109. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  110. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  111. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  112. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  113. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  114. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  115. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  116. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  117. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  118. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  119. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  120. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  121. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  122. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  123. package/src/db/_core 2.py +0 -417
  124. package/src/db/_credentials 2.py +0 -124
  125. package/src/db/_entities 2.py +0 -178
  126. package/src/db/_episodic 2.py +0 -738
  127. package/src/db/_evolution 2.py +0 -54
  128. package/src/db/_fts 2.py +0 -406
  129. package/src/db/_learnings 2.py +0 -168
  130. package/src/db/_reminders 2.py +0 -338
  131. package/src/db/_schema 2.py +0 -364
  132. package/src/db/_sessions 2.py +0 -300
  133. package/src/db/_tasks 2.py +0 -91
  134. package/src/evolution_cycle 2.py +0 -266
  135. package/src/hnsw_index 2.py +0 -254
  136. package/src/hooks/auto_capture 2.py +0 -208
  137. package/src/hooks/caffeinate-guard 2.sh +0 -8
  138. package/src/hooks/capture-session 2.sh +0 -21
  139. package/src/hooks/capture-tool-logs 2.sh +0 -127
  140. package/src/hooks/daily-briefing-check 2.sh +0 -33
  141. package/src/hooks/inbox-hook 2.sh +0 -76
  142. package/src/hooks/post-compact 2.sh +0 -148
  143. package/src/hooks/pre-compact 2.sh +0 -151
  144. package/src/hooks/session-start 2.sh +0 -268
  145. package/src/hooks/session-stop 2.sh +0 -140
  146. package/src/kg_populate 2.py +0 -290
  147. package/src/knowledge_graph 2.py +0 -257
  148. package/src/maintenance 2.py +0 -59
  149. package/src/migrate_embeddings 2.py +0 -122
  150. package/src/plugin_loader 2.py +0 -202
  151. package/src/plugins/__init__ 2.py +0 -0
  152. package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
  153. package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
  154. package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
  155. package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
  156. package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
  157. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  158. package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
  159. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  160. package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
  161. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  162. package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
  163. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  164. package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
  165. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  166. package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
  167. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  168. package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
  169. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  170. package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
  171. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  172. package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
  173. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  174. package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
  175. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  176. package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
  177. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  178. package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
  179. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  180. package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
  181. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  182. package/src/plugins/adaptive_mode 2.py +0 -805
  183. package/src/plugins/agents 2.py +0 -52
  184. package/src/plugins/artifact_registry 2.py +0 -450
  185. package/src/plugins/backup 2.py +0 -104
  186. package/src/plugins/cognitive_memory 2.py +0 -564
  187. package/src/plugins/core_rules 2.py +0 -252
  188. package/src/plugins/cortex 2.py +0 -299
  189. package/src/plugins/entities 2.py +0 -67
  190. package/src/plugins/episodic_memory 2.py +0 -533
  191. package/src/plugins/evolution 2.py +0 -115
  192. package/src/plugins/guard 2.py +0 -746
  193. package/src/plugins/knowledge_graph_tools 2.py +0 -105
  194. package/src/plugins/preferences 2.py +0 -47
  195. package/src/plugins/update 2.py +0 -256
  196. package/src/requirements 2.txt +0 -12
  197. package/src/rules/__init__ 2.py +0 -0
  198. package/src/rules/core-rules 2.json +0 -331
  199. package/src/rules/migrate 2.py +0 -207
  200. package/src/scripts/check-context 2.py +0 -264
  201. package/src/scripts/nexo-auto-update 2.py +0 -6
  202. package/src/scripts/nexo-backup 2.sh +0 -25
  203. package/src/scripts/nexo-brain-activation 2.sh +0 -140
  204. package/src/scripts/nexo-catchup 2.py +0 -242
  205. package/src/scripts/nexo-cognitive-decay 2.py +0 -182
  206. package/src/scripts/nexo-daily-self-audit 2.py +0 -552
  207. package/src/scripts/nexo-deep-sleep 2.sh +0 -97
  208. package/src/scripts/nexo-evolution-run 2.py +0 -597
  209. package/src/scripts/nexo-followup-hygiene 2.py +0 -112
  210. package/src/scripts/nexo-github-monitor 2.py +0 -256
  211. package/src/scripts/nexo-github-monitor.py +0 -256
  212. package/src/scripts/nexo-immune 2.py +0 -927
  213. package/src/scripts/nexo-inbox-hook 2.sh +0 -74
  214. package/src/scripts/nexo-install 2.py +0 -6
  215. package/src/scripts/nexo-learning-housekeep 2.py +0 -245
  216. package/src/scripts/nexo-learning-validator 2.py +0 -207
  217. package/src/scripts/nexo-migrate 2.py +0 -232
  218. package/src/scripts/nexo-postmortem-consolidator 2.py +0 -421
  219. package/src/scripts/nexo-pre-commit 2.py +0 -120
  220. package/src/scripts/nexo-prevent-sleep 2.sh +0 -29
  221. package/src/scripts/nexo-proactive-dashboard 2.py +0 -345
  222. package/src/scripts/nexo-reflection 2.py +0 -253
  223. package/src/scripts/nexo-runtime-preflight 2.py +0 -274
  224. package/src/scripts/nexo-send-email 2.py +0 -25
  225. package/src/scripts/nexo-send-email.py +0 -25
  226. package/src/scripts/nexo-send-reply 2.py +0 -178
  227. package/src/scripts/nexo-send-reply.py +0 -178
  228. package/src/scripts/nexo-sleep 2.py +0 -592
  229. package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
  230. package/src/scripts/nexo-synthesis 2.py +0 -253
  231. package/src/scripts/nexo-tcc-approve 2.sh +0 -79
  232. package/src/scripts/nexo-update 2.sh +0 -161
  233. package/src/scripts/nexo-watchdog 2.sh +0 -878
  234. package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
  235. package/src/server 2.py +0 -733
  236. package/src/storage_router 2.py +0 -32
  237. package/src/tools_coordination 2.py +0 -102
  238. package/src/tools_credentials 2.py +0 -68
  239. package/src/tools_learnings 2.py +0 -220
  240. package/src/tools_menu 2.py +0 -227
  241. package/src/tools_reminders 2.py +0 -86
  242. package/src/tools_reminders_crud 2.py +0 -159
  243. package/src/tools_sessions 2.py +0 -476
  244. package/src/tools_task_history 2.py +0 -57
  245. package/templates/CLAUDE.md 2.template +0 -63
  246. package/templates/openclaw 2.json +0 -13
  247. package/tests/__init__ 2.py +0 -0
  248. package/tests/__init__.py +0 -0
  249. package/tests/conftest 2.py +0 -71
  250. package/tests/conftest.py +0 -71
  251. package/tests/test_cognitive 2.py +0 -205
  252. package/tests/test_cognitive.py +0 -205
  253. package/tests/test_knowledge_graph 2.py +0 -140
  254. package/tests/test_knowledge_graph.py +0 -140
  255. package/tests/test_migrations 2.py +0 -137
  256. package/tests/test_migrations.py +0 -137
@@ -1,32 +0,0 @@
1
- """Storage Router — DB path abstraction for future multi-tenant support."""
2
-
3
- import os
4
-
5
- NEXO_HOME = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
6
-
7
-
8
- class StorageRouter:
9
- def __init__(self, tenant_id: str = "default"):
10
- self.tenant_id = tenant_id
11
-
12
- def nexo_db_path(self) -> str:
13
- if self.tenant_id == "default":
14
- data_dir = os.path.join(NEXO_HOME, "data")
15
- os.makedirs(data_dir, exist_ok=True)
16
- return os.path.join(data_dir, "nexo.db")
17
- return os.path.join(NEXO_HOME, "tenants", self.tenant_id, "nexo.db")
18
-
19
- def cognitive_db_path(self) -> str:
20
- if self.tenant_id == "default":
21
- data_dir = os.path.join(NEXO_HOME, "data")
22
- os.makedirs(data_dir, exist_ok=True)
23
- return os.path.join(data_dir, "cognitive.db")
24
- return os.path.join(NEXO_HOME, "tenants", self.tenant_id, "cognitive.db")
25
-
26
-
27
- _default_router = StorageRouter("default")
28
-
29
- def get_router(tenant_id: str = "default") -> StorageRouter:
30
- if tenant_id == "default":
31
- return _default_router
32
- return StorageRouter(tenant_id)
@@ -1,102 +0,0 @@
1
- """Coordination tools: file tracking, messaging, Q&A."""
2
-
3
- from db import (
4
- track_files, untrack_files, get_all_tracked_files,
5
- send_message, get_inbox,
6
- ask_question, answer_question, get_pending_questions, check_answer,
7
- now_epoch,
8
- )
9
- from tools_sessions import _format_age
10
-
11
-
12
- def handle_track(sid: str, paths: list[str]) -> str:
13
- """Track files being edited. Reports conflicts immediately."""
14
- result = track_files(sid, paths)
15
- if "error" in result:
16
- return f"ERROR: {result['error']}"
17
-
18
- lines = [f"Tracked: {', '.join(result['tracked'])}"]
19
-
20
- if result["conflicts"]:
21
- lines.append("")
22
- lines.append("FILE CONFLICTS:")
23
- for c in result["conflicts"]:
24
- lines.append(f" {c['sid']} ({c['task']}):")
25
- for f in c["files"]:
26
- lines.append(f" {f}")
27
- lines.append("")
28
- lines.append("STOP and inform the user before editing.")
29
-
30
- return "\n".join(lines)
31
-
32
-
33
- def handle_untrack(sid: str, paths: list[str] | None = None) -> str:
34
- """Untrack files. If no paths given, untrack all."""
35
- untrack_files(sid, paths)
36
- if paths:
37
- return f"Untracked: {', '.join(paths)}"
38
- return "All files released."
39
-
40
-
41
- def handle_files() -> str:
42
- """Show all tracked files across sessions."""
43
- data = get_all_tracked_files()
44
- if not data:
45
- return "No tracked files."
46
-
47
- lines = ["TRACKED FILES:"]
48
- all_paths = {}
49
- for sid, info in data.items():
50
- for path in info["files"]:
51
- all_paths.setdefault(path, []).append(sid)
52
- lines.append(f" {sid} ({info['task']}):")
53
- for path in info["files"]:
54
- lines.append(f" {path}")
55
-
56
- conflicts = {p: sids for p, sids in all_paths.items() if len(sids) > 1}
57
- if conflicts:
58
- lines.append("")
59
- lines.append("CONFLICTS:")
60
- for path, sids in conflicts.items():
61
- lines.append(f" {path} -> {', '.join(sids)}")
62
-
63
- return "\n".join(lines)
64
-
65
-
66
- def handle_send(from_sid: str, to_sid: str, text: str) -> str:
67
- """Send a message. to_sid='all' for broadcast."""
68
- msg_id = send_message(from_sid, to_sid, text)
69
- target = "all sessions" if to_sid == "all" else to_sid
70
- return f"Message {msg_id} sent to {target}."
71
-
72
-
73
- def handle_ask(from_sid: str, to_sid: str, question: str) -> str:
74
- """Create a question to another session (non-blocking)."""
75
- qid = ask_question(from_sid, to_sid, question)
76
- return (
77
- f"Question sent: {qid}\n"
78
- f"To: {to_sid}\n"
79
- f"Question: {question}\n\n"
80
- f"The other session will see this question on its next nexo_heartbeat.\n"
81
- f"Use nexo_check_answer(qid='{qid}') to check for a response."
82
- )
83
-
84
-
85
- def handle_answer(qid: str, answer_text: str) -> str:
86
- """Answer a pending question."""
87
- result = answer_question(qid, answer_text)
88
- if "error" in result:
89
- return f"ERROR: {result['error']}"
90
- return f"Answered {qid}: {answer_text}"
91
-
92
-
93
- def handle_check_answer(qid: str) -> str:
94
- """Check if a question has been answered."""
95
- result = check_answer(qid)
96
- if not result:
97
- return f"Question {qid} not found."
98
- if result["status"] == "answered":
99
- return f"ANSWER for {qid}: {result['answer']}"
100
- elif result["status"] == "expired":
101
- return f"Question {qid} expired without answer."
102
- return f"Question still pending. Retry in a few seconds."
@@ -1,68 +0,0 @@
1
- """Credentials CRUD tools: get, create, update, delete, list."""
2
-
3
- from db import create_credential, update_credential, delete_credential, get_credential, list_credentials
4
-
5
-
6
- def handle_credential_get(service: str, key: str = '') -> str:
7
- """Retrieve credential(s) including their values. Use for reading secrets."""
8
- results = get_credential(service, key if key else None)
9
- if not results:
10
- target = f"{service}/{key}" if key else service
11
- return f"ERROR: No credentials found for '{target}'."
12
- is_fuzzy = any(r.get("_fuzzy") for r in results)
13
- lines = []
14
- if is_fuzzy:
15
- lines.append(f"⚠ No exact match for '{service}'. Similar results ({len(results)}):")
16
- lines.append("")
17
- for r in results:
18
- lines.append(f"CREDENTIAL {r['service']}/{r['key']}:")
19
- lines.append(f" Value: {r['value']}")
20
- notes = r.get("notes") or ""
21
- lines.append(f" Notes: {notes if notes else '—'}")
22
- return "\n".join(lines)
23
-
24
-
25
- def handle_credential_create(service: str, key: str, value: str, notes: str = '') -> str:
26
- """Create a new credential entry."""
27
- result = create_credential(service, key, value, notes)
28
- if "error" in result:
29
- return f"ERROR: {result['error']}"
30
- return f"Credential {service}/{key} created."
31
-
32
-
33
- def handle_credential_update(service: str, key: str, value: str = '', notes: str = '') -> str:
34
- """Update the value and/or notes of an existing credential."""
35
- result = update_credential(
36
- service,
37
- key,
38
- value if value else None,
39
- notes if notes else None,
40
- )
41
- if "error" in result:
42
- return f"ERROR: {result['error']}"
43
- return f"Credential {service}/{key} updated."
44
-
45
-
46
- def handle_credential_delete(service: str, key: str = '') -> str:
47
- """Delete a credential or all credentials for a service."""
48
- deleted = delete_credential(service, key if key else None)
49
- if not deleted:
50
- target = f"{service}/{key}" if key else service
51
- return f"ERROR: No credentials found for '{target}'."
52
- if key:
53
- return f"Credential deleted."
54
- return f"All credentials for service deleted."
55
-
56
-
57
- def handle_credential_list(service: str = '') -> str:
58
- """List credential service/key names and notes — values are never shown."""
59
- results = list_credentials(service if service else None)
60
- label = service if service else "ALL"
61
- if not results:
62
- return f"CREDENTIALS {label.upper()}: No entries."
63
- lines = [f"CREDENTIALS {label.upper()} ({len(results)}):"]
64
- for r in results:
65
- notes = r.get("notes") or ""
66
- suffix = f" — {notes}" if notes else ""
67
- lines.append(f" {r['service']}/{r['key']}{suffix}")
68
- return "\n".join(lines)
@@ -1,220 +0,0 @@
1
- """Learnings CRUD tools: add, search, update, delete, list."""
2
-
3
- from db import (create_learning, update_learning, delete_learning, search_learnings,
4
- list_learnings, find_similar_learnings, get_db, now_epoch)
5
-
6
- def handle_learning_add(category: str, title: str, content: str, reasoning: str = '',
7
- prevention: str = '', applies_to: str = '', review_days: int = 30,
8
- priority: str = 'medium') -> str:
9
- """Add a new learning entry to the specified category.
10
-
11
- Args:
12
- category: Free-form category name (e.g., 'backend', 'frontend', 'devops', 'infrastructure', 'security', 'nexo-ops'). Use consistent names — learnings are grouped and searched by category.
13
- title: Short title for the learning
14
- content: Full description of what was learned
15
- reasoning: WHY this matters — what led to discovering this, what was the context
16
- prevention: Concrete rule/check that prevents repeating this mistake
17
- applies_to: Files, systems, or areas this learning applies to
18
- review_days: Days until this learning should be reviewed again
19
- priority: critical, high, medium, low (default: medium)
20
- """
21
- if priority not in ('critical', 'high', 'medium', 'low'):
22
- priority = 'medium'
23
- category = category.lower().strip()
24
- if not category:
25
- return "ERROR: Category cannot be empty."
26
- result = create_learning(
27
- category, title, content, reasoning=reasoning
28
- )
29
- if "error" in result:
30
- return f"ERROR: {result['error']}"
31
- if prevention or applies_to or review_days > 0 or priority != 'medium':
32
- initial_weight = {'critical': 0.9, 'high': 0.7, 'medium': 0.5, 'low': 0.3}[priority]
33
- conn = get_db()
34
- conn.execute(
35
- "UPDATE learnings SET prevention = ?, applies_to = ?, status = COALESCE(status, 'active'), "
36
- "review_due_at = ?, updated_at = ?, priority = ?, weight = ? WHERE id = ?",
37
- (prevention, applies_to, now_epoch() + (max(1, int(review_days)) * 86400), now_epoch(),
38
- priority, initial_weight, result["id"])
39
- )
40
- conn.commit()
41
- result = conn.execute("SELECT * FROM learnings WHERE id = ?", (result["id"],)).fetchone()
42
- result = dict(result)
43
-
44
- # Cognitive ingest — embed learning for semantic search
45
- new_id = result["id"]
46
- try:
47
- import cognitive
48
- cognitive.ingest(f"{title}: {content}", "learning", f"L{new_id}", title, category)
49
- except Exception:
50
- pass
51
-
52
- # Similarity check — detect repeated errors
53
- matches = find_similar_learnings(new_id, title, content, category)
54
- repetition_msg = ""
55
- if matches:
56
- conn = get_db()
57
- for original_id, similarity in matches:
58
- conn.execute(
59
- "INSERT INTO error_repetitions (new_learning_id, original_learning_id, similarity, area) VALUES (?,?,?,?)",
60
- (new_id, original_id, similarity, category)
61
- )
62
- conn.commit()
63
- repetition_msg = f"\n⚠️ REPETITION WARNING: Similar to {len(matches)} existing learning(s): " + \
64
- ", ".join(f"#{m[0]} ({m[1]:.0%})" for m in matches[:3])
65
-
66
- # Somatic event logging (append-only in nexo.db, projected to cognitive.db nightly)
67
- try:
68
- if applies_to:
69
- for file_path in [f.strip() for f in applies_to.split(",") if f.strip()]:
70
- get_db().execute(
71
- "INSERT INTO somatic_events (target, target_type, event_type, delta, source) VALUES (?, ?, ?, ?, ?)",
72
- (file_path, "file", "learning_add", 0.15, f"learning:{new_id}")
73
- )
74
- # Area + extra file pain ONLY for repeated errors
75
- if matches:
76
- get_db().execute(
77
- "INSERT INTO somatic_events (target, target_type, event_type, delta, source) VALUES (?, ?, ?, ?, ?)",
78
- (category, "area", "error_repetition", 0.15, f"learning:{new_id}")
79
- )
80
- if applies_to:
81
- for file_path in [f.strip() for f in applies_to.split(",") if f.strip()]:
82
- get_db().execute(
83
- "INSERT INTO somatic_events (target, target_type, event_type, delta, source) VALUES (?, ?, ?, ?, ?)",
84
- (file_path, "file", "error_repetition", 0.25, f"learning:{new_id}")
85
- )
86
- get_db().commit()
87
- except Exception:
88
- pass # Somatic event logging is best-effort
89
-
90
- # Knowledge graph incremental population
91
- try:
92
- from kg_populate import on_learning_add
93
- on_learning_add(new_id, category, title, applies_to)
94
- except Exception:
95
- pass
96
-
97
- meta = []
98
- if prevention:
99
- meta.append("with prevention")
100
- if applies_to:
101
- meta.append(f"applies_to={applies_to}")
102
- meta_str = f" ({', '.join(meta)})" if meta else ""
103
- return f"Learning #{result['id']} added in {category}: {title}{meta_str}{repetition_msg}"
104
-
105
-
106
- def handle_learning_search(query: str, category: str = '') -> str:
107
- """Search learnings by query string, optionally filtered by category."""
108
- results = search_learnings(query, category if category else None)
109
- if not results:
110
- return f"No results for '{query}'."
111
- lines = [f"RESULTS ({len(results)}):"]
112
- for r in results:
113
- snippet = r["content"][:100] + "..." if len(r["content"]) > 100 else r["content"]
114
- status = r.get("status", "active")
115
- review_due = r.get("review_due_at")
116
- review_note = f" | review_due={review_due:.0f}" if isinstance(review_due, (int, float)) and review_due else ""
117
- pri = r.get("priority", "medium") or "medium"
118
- w = r.get("weight", 0.5) or 0.5
119
- pri_icon = {"critical": "🔴", "high": "🟠", "medium": "🟡", "low": "⚪"}.get(pri, "🟡")
120
- lines.append(f" #{r['id']} [{r['category']}] [{status}] {pri_icon}{pri} w={w:.2f} {r['title']}{review_note}")
121
- lines.append(f" {snippet}")
122
- if r.get("prevention"):
123
- lines.append(f" Prevention: {r['prevention'][:100]}")
124
-
125
- # v1.2: Passive rehearsal — strengthen matching cognitive memories
126
- try:
127
- import cognitive
128
- for r in results[:5]:
129
- cognitive.rehearse_by_content(f"{r.get('title', '')} {r.get('content', '')[:200]}")
130
- except Exception:
131
- pass
132
-
133
- return "\n".join(lines)
134
-
135
-
136
- def handle_learning_update(id: int, title: str = '', content: str = '', category: str = '',
137
- reasoning: str = '', prevention: str = '', applies_to: str = '',
138
- status: str = '', review_days: int = 0, priority: str = '') -> str:
139
- """Update an existing learning, including review metadata and priority."""
140
- kwargs = {}
141
- if title:
142
- kwargs["title"] = title
143
- if content:
144
- kwargs["content"] = content
145
- if category:
146
- kwargs["category"] = category.lower().strip()
147
- if reasoning:
148
- kwargs["reasoning"] = reasoning
149
- if prevention:
150
- kwargs["prevention"] = prevention
151
- if applies_to:
152
- kwargs["applies_to"] = applies_to
153
- if status:
154
- kwargs["status"] = status
155
- if review_days > 0:
156
- kwargs["review_days"] = review_days
157
- if not kwargs:
158
- return "ERROR: Nothing to update. Provide new fields."
159
- basic_kwargs = {k: v for k, v in kwargs.items() if k in {"title", "content", "category", "reasoning"}}
160
- result = update_learning(id, **basic_kwargs)
161
- if "error" in result:
162
- return f"ERROR: {result['error']}"
163
- extra_updates = {}
164
- if prevention:
165
- extra_updates["prevention"] = prevention
166
- if applies_to:
167
- extra_updates["applies_to"] = applies_to
168
- if status:
169
- extra_updates["status"] = status
170
- if priority and priority in ('critical', 'high', 'medium', 'low'):
171
- extra_updates["priority"] = priority
172
- extra_updates["weight"] = {'critical': 0.9, 'high': 0.7, 'medium': 0.5, 'low': 0.3}[priority]
173
- if review_days > 0:
174
- extra_updates["review_due_at"] = now_epoch() + (max(1, int(review_days)) * 86400)
175
- if extra_updates:
176
- extra_updates["updated_at"] = now_epoch()
177
- set_clause = ", ".join(f"{k} = ?" for k in extra_updates)
178
- values = list(extra_updates.values()) + [id]
179
- conn = get_db()
180
- conn.execute(f"UPDATE learnings SET {set_clause} WHERE id = ?", values)
181
- conn.commit()
182
- return f"Learning #{id} updated."
183
-
184
-
185
- def handle_learning_delete(id: int) -> str:
186
- """Delete a learning entry by ID."""
187
- deleted = delete_learning(id)
188
- if not deleted:
189
- return f"ERROR: Learning #{id} not found."
190
- return f"Learning #{id} deleted."
191
-
192
-
193
- def handle_learning_list(category: str = '') -> str:
194
- """List all learnings, grouped by category if no filter given."""
195
- results = list_learnings(category if category else None)
196
- if not results:
197
- label = category if category else "ALL"
198
- return f"LEARNINGS {label} (0): No entries."
199
-
200
- if category:
201
- label = category.upper()
202
- lines = [f"LEARNINGS {label} ({len(results)}):"]
203
- for r in results:
204
- pri = r.get("priority", "medium") or "medium"
205
- w = r.get("weight", 0.5) or 0.5
206
- pri_icon = {"critical": "🔴", "high": "🟠", "medium": "🟡", "low": "⚪"}.get(pri, "🟡")
207
- lines.append(f" #{r['id']} [{r.get('status','active')}] {pri_icon}{pri} w={w:.2f} {r['title']}")
208
- else:
209
- lines = [f"LEARNINGS ALL ({len(results)}):"]
210
- current_cat = None
211
- for r in results:
212
- if r["category"] != current_cat:
213
- current_cat = r["category"]
214
- lines.append(f"\n [{current_cat.upper()}]")
215
- pri = r.get("priority", "medium") or "medium"
216
- w = r.get("weight", 0.5) or 0.5
217
- pri_icon = {"critical": "🔴", "high": "🟠", "medium": "🟡", "low": "⚪"}.get(pri, "🟡")
218
- lines.append(f" #{r['id']} [{r.get('status','active')}] {pri_icon}{pri} w={w:.2f} {r['title']}")
219
-
220
- return "\n".join(lines)
@@ -1,227 +0,0 @@
1
- """Menu generator — NEXO operations center."""
2
-
3
- from datetime import datetime, timedelta
4
- import json
5
- import subprocess
6
- import sys
7
- from pathlib import Path
8
- from tools_sessions import handle_status
9
- from tools_reminders import handle_reminders
10
- from db import get_db
11
-
12
-
13
- def _get_date_str() -> str:
14
- """Get formatted date in Madrid timezone."""
15
- try:
16
- result = subprocess.run(
17
- ["date", "+%A %d %B %Y, %H:%M"],
18
- capture_output=True, text=True,
19
- env={"PATH": "/usr/bin:/bin"}
20
- )
21
- return result.stdout.strip()
22
- except Exception:
23
- return datetime.now().strftime("%Y-%m-%d %H:%M")
24
-
25
-
26
- MENU_ITEMS = [
27
- ("Projects", [
28
- ("1", "Projects - Review project status"),
29
- ("9", "Claude Agent VPS - Review autonomous changes"),
30
- ]),
31
- ("Advertising", [
32
- ("7", "Google Ads - Manage campaigns"),
33
- ("7b", "Meta Ads - Manage Facebook/Instagram campaigns"),
34
- ("7c", "Ads Tracking - Combined Google+Meta review"),
35
- ]),
36
- ("Shopify", [
37
- ("4", "Shopify Theme Sync - Sync theme"),
38
- ("5", "Shopify Scripts - Run periodic scripts"),
39
- ("6", "Change Shopify Promotion"),
40
- ]),
41
- ("Server & Infrastructure", [
42
- ("2", "Server - Health check your-server.example.com"),
43
- ("3", "WhatsApp Logs - Review logs your-whatsapp-account"),
44
- ("11", "File Tracker - PHP file report"),
45
- ("12", "Google Cloud - Spend, usage and GCP status"),
46
- ]),
47
- ("Communication & Monitoring", [
48
- ("8", "Recovery Optimizer - Weekly AI analysis (MONDAY)"),
49
- ("10", "Recovery Monitor - Email/WA recovery status (24h)"),
50
- ("13", "Review Monitor - Email/WA review status"),
51
- ("14", "WhatsApp Full Analysis - Global statistics"),
52
- ("15", "Google Analytics - Review web analytics"),
53
- ("16", "Email Review - Check inboxes and spam"),
54
- ]),
55
- ("Reports & SEO", [
56
- ("17", "Search Console Audit (every 2 weeks)"),
57
- ("18", "Sitemap resubmission (every 30 days)"),
58
- ("19", "SEO meta verification"),
59
- ("20", "Weekly Email Report (Sundays)"),
60
- ]),
61
- ]
62
-
63
-
64
- def _get_dashboard_alerts() -> list[dict]:
65
- """Run proactive dashboard and return alerts."""
66
- try:
67
- nexo_home = Path(os.environ.get("NEXO_HOME", str(Path.home() / ".nexo")))
68
- script = nexo_home / "scripts" / "nexo-proactive-dashboard.py"
69
- if not script.exists():
70
- return []
71
- result = subprocess.run(
72
- [sys.executable, str(script), "--json"],
73
- capture_output=True, text=True, timeout=10
74
- )
75
- if result.stdout.strip():
76
- return json.loads(result.stdout)
77
- except Exception:
78
- pass
79
- return []
80
-
81
-
82
- def _get_memory_review_summary() -> dict:
83
- """Return counts of due memory reviews."""
84
- try:
85
- conn = get_db()
86
- now_epoch = datetime.now().timestamp()
87
- now_iso = datetime.now().isoformat(timespec="seconds")
88
- due_learnings = conn.execute(
89
- "SELECT COUNT(*) FROM learnings WHERE review_due_at IS NOT NULL AND status != 'superseded' AND review_due_at <= ?",
90
- (now_epoch,)
91
- ).fetchone()[0]
92
- due_decisions = conn.execute(
93
- "SELECT COUNT(*) FROM decisions WHERE review_due_at IS NOT NULL AND status != 'reviewed' AND review_due_at <= ?",
94
- (now_iso,)
95
- ).fetchone()[0]
96
- return {
97
- "learnings": due_learnings,
98
- "decisions": due_decisions,
99
- "total": due_learnings + due_decisions,
100
- }
101
- except Exception:
102
- return {"learnings": 0, "decisions": 0, "total": 0}
103
-
104
-
105
- def handle_menu() -> str:
106
- """Generate the full operations menu with alerts."""
107
- date_str = _get_date_str()
108
- W = 56 # inner width
109
-
110
- lines = []
111
- lines.append("╔" + "═" * W + "╗")
112
- lines.append("║" + "NEXO — OPERATIONS CENTER".center(W) + "║")
113
- lines.append("║" + date_str.center(W) + "║")
114
- lines.append("╠" + "═" * W + "╣")
115
-
116
- # Proactive dashboard alerts
117
- dashboard_alerts = _get_dashboard_alerts()
118
- memory_reviews = _get_memory_review_summary()
119
- due = handle_reminders("due")
120
- has_alerts = dashboard_alerts or memory_reviews["total"] > 0 or (due and "No reminders" not in due)
121
-
122
- if has_alerts:
123
- lines.append("║" + " PROACTIVE ALERTS".ljust(W) + "║")
124
- lines.append("╠" + "═" * W + "╣")
125
-
126
- if dashboard_alerts:
127
- for alert in dashboard_alerts[:10]: # Top 10
128
- sev = alert.get("severity", "low")
129
- icon = {"high": "!!!", "medium": " ! ", "low": " . "}.get(sev, " . ")
130
- text = alert.get("title", "")[:W - 8]
131
- lines.append("║" + f" {icon} {text}".ljust(W) + "║")
132
- if len(dashboard_alerts) > 10:
133
- more = len(dashboard_alerts) - 10
134
- lines.append("║" + f" ... and {more} more alerts".ljust(W) + "║")
135
-
136
- if memory_reviews["total"] > 0:
137
- text = (
138
- f"MEMORY: {memory_reviews['total']} pending reviews "
139
- f"({memory_reviews['decisions']} decisions, {memory_reviews['learnings']} learnings)"
140
- )[:W - 4]
141
- lines.append("║" + f" ! {text}".ljust(W) + "║")
142
-
143
- if due and "No reminders" not in due:
144
- for reminder_line in due.split("\n"):
145
- if reminder_line.strip():
146
- truncated = reminder_line[:W - 2]
147
- lines.append("║" + f" {truncated}".ljust(W) + "║")
148
-
149
- lines.append("╠" + "═" * W + "╣")
150
-
151
- # Menu categories
152
- for category, items in MENU_ITEMS:
153
- lines.append("║" + f" {category.upper()}".ljust(W) + "║")
154
- lines.append("║" + "─" * W + "║")
155
- for num, desc in items:
156
- entry = f" {num:>3}. {desc}"
157
- lines.append("║" + entry.ljust(W) + "║")
158
- lines.append("╠" + "═" * W + "╣")
159
-
160
- # Backlog: ideas, future projects, undated or distant tasks
161
- try:
162
- conn = get_db()
163
- cutoff = (datetime.now() + timedelta(days=7)).strftime("%Y-%m-%d")
164
- # Reminders without date (backlog/ideas)
165
- no_date = conn.execute(
166
- "SELECT id, description, category FROM reminders WHERE status LIKE 'PENDING%' AND (date IS NULL OR date='') ORDER BY category, id"
167
- ).fetchall()
168
- # Reminders with date > 7 days ahead (future)
169
- future = conn.execute(
170
- "SELECT id, description, date, category FROM reminders WHERE status LIKE 'PENDING%' AND date > ? ORDER BY date",
171
- (cutoff,)
172
- ).fetchall()
173
- # Followups without date
174
- nf_no_date = conn.execute(
175
- "SELECT id, description FROM followups WHERE status NOT LIKE 'COMPLETED%' AND status NOT IN ('DELETED','archived','blocked','waiting') AND (date IS NULL OR date='') ORDER BY id"
176
- ).fetchall()
177
-
178
- if no_date or future or nf_no_date:
179
- lines.append("║" + " BACKLOG / IDEAS / FUTURE".ljust(W) + "║")
180
- lines.append("║" + "─" * W + "║")
181
-
182
- if no_date:
183
- by_cat = {}
184
- for r in no_date:
185
- cat = (r["category"] or "general").capitalize()
186
- by_cat.setdefault(cat, []).append(r)
187
- for cat, items in by_cat.items():
188
- lines.append("║" + f" [{cat}]".ljust(W) + "║")
189
- for r in items:
190
- short = r["description"][:W - 10]
191
- lines.append("║" + f" {r['id']}: {short}".ljust(W) + "║")
192
-
193
- if future:
194
- lines.append("║" + f" [Scheduled]".ljust(W) + "║")
195
- for r in future:
196
- short = r["description"][:W - 18]
197
- lines.append("║" + f" {r['id']} ({r['date']}): {short}".ljust(W) + "║")
198
-
199
- if nf_no_date:
200
- lines.append("║" + f" [Pending followups]".ljust(W) + "║")
201
- for r in nf_no_date:
202
- short = r["description"][:W - 12]
203
- lines.append("║" + f" {r['id']}: {short}".ljust(W) + "║")
204
-
205
- lines.append("╠" + "═" * W + "╣")
206
- except Exception as e:
207
- lines.append("║" + f" ⚠ Error backlog: {e}".ljust(W) + "║")
208
- lines.append("╠" + "═" * W + "╣")
209
-
210
- # Active sessions
211
- sessions = handle_status()
212
- if "No sessions" not in sessions:
213
- lines.append("║" + " ACTIVE SESSIONS".ljust(W) + "║")
214
- lines.append("║" + "─" * W + "║")
215
- for s_line in sessions.split("\n"):
216
- if s_line.strip() and "ACTIVE SESSIONS" not in s_line:
217
- truncated = s_line[:W - 2]
218
- lines.append("║" + f" {truncated}".ljust(W) + "║")
219
- lines.append("╠" + "═" * W + "╣")
220
-
221
- # Replace last ╠═╣ with bottom border
222
- if lines[-1].startswith("╠"):
223
- lines[-1] = "╚" + "═" * W + "╝"
224
- else:
225
- lines.append("╚" + "═" * W + "╝")
226
-
227
- return "\n".join(lines)