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,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)