nexo-brain 2.3.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 (287) hide show
  1. package/README.md +1 -1
  2. package/package.json +6 -3
  3. package/src/auto_update.py +1 -0
  4. package/src/crons/sync.py +1 -2
  5. package/src/db/_core.py +1 -0
  6. package/src/db/_entities.py +1 -0
  7. package/src/db/_episodic.py +1 -0
  8. package/src/db/_learnings.py +1 -0
  9. package/src/db/_reminders.py +1 -0
  10. package/src/db/_sessions.py +1 -0
  11. package/src/db/_skills.py +1 -0
  12. package/src/plugin_loader.py +1 -0
  13. package/src/plugins/update.py +1 -0
  14. package/src/scripts/deep-sleep/apply_findings.py +1 -0
  15. package/src/scripts/deep-sleep/collect.py +1 -0
  16. package/src/scripts/deep-sleep/extract.py +1 -0
  17. package/src/scripts/deep-sleep/synthesize.py +1 -0
  18. package/src/scripts/nexo-learning-housekeep.py +1 -0
  19. package/src/scripts/nexo-watchdog.sh +19 -11
  20. package/src/server.py +1 -0
  21. package/src/tools_coordination.py +1 -0
  22. package/src/tools_sessions.py +1 -0
  23. package/scripts/migrate-to-unified 2.sh +0 -813
  24. package/scripts/migrate-to-unified.sh +0 -813
  25. package/scripts/migrate-v1.5-to-v1.6 2.py +0 -778
  26. package/scripts/migrate-v1.5-to-v1.6.py +0 -778
  27. package/scripts/migrate-v1.7-to-v1.8 2.py +0 -214
  28. package/scripts/migrate-v1.7-to-v1.8.py +0 -214
  29. package/scripts/nexo-preflight.sh +0 -236
  30. package/scripts/pre-commit-check 2.sh +0 -55
  31. package/scripts/pre-commit-check.sh +0 -55
  32. package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
  33. package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
  34. package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
  35. package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
  36. package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
  37. package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
  38. package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
  39. package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
  40. package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
  41. package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
  42. package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
  43. package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
  44. package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
  45. package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
  46. package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
  47. package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
  48. package/src/auto_close_sessions 2.py +0 -159
  49. package/src/auto_update 2.py +0 -634
  50. package/src/claim_graph 2.py +0 -323
  51. package/src/cognitive/__init__ 2.py +0 -62
  52. package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
  53. package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
  54. package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  55. package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
  56. package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
  57. package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
  58. package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
  59. package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
  60. package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
  61. package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
  62. package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
  63. package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
  64. package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
  65. package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
  66. package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
  67. package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
  68. package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
  69. package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
  70. package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
  71. package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
  72. package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
  73. package/src/cognitive/_core 2.py +0 -567
  74. package/src/cognitive/_decay 2.py +0 -382
  75. package/src/cognitive/_ingest 2.py +0 -892
  76. package/src/cognitive/_memory 2.py +0 -912
  77. package/src/cognitive/_search 2.py +0 -949
  78. package/src/cognitive/_trust 2.py +0 -464
  79. package/src/crons/__pycache__/sync.cpython-314.pyc +0 -0
  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__/_cron_runs.cpython-310.pyc +0 -0
  97. package/src/db/__pycache__/_cron_runs.cpython-314.pyc +0 -0
  98. package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
  99. package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
  100. package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
  101. package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
  102. package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
  103. package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
  104. package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
  105. package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
  106. package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
  107. package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
  108. package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
  109. package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
  110. package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
  111. package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
  112. package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
  113. package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
  114. package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
  115. package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
  116. package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
  117. package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
  118. package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
  119. package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
  120. package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
  121. package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
  122. package/src/db/__pycache__/_skills.cpython-310.pyc +0 -0
  123. package/src/db/__pycache__/_skills.cpython-312.pyc +0 -0
  124. package/src/db/__pycache__/_skills.cpython-314.pyc +0 -0
  125. package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
  126. package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
  127. package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
  128. package/src/db/_core 2.py +0 -417
  129. package/src/db/_credentials 2.py +0 -124
  130. package/src/db/_entities 2.py +0 -178
  131. package/src/db/_episodic 2.py +0 -738
  132. package/src/db/_evolution 2.py +0 -54
  133. package/src/db/_fts 2.py +0 -406
  134. package/src/db/_learnings 2.py +0 -168
  135. package/src/db/_reminders 2.py +0 -338
  136. package/src/db/_schema 2.py +0 -364
  137. package/src/db/_sessions 2.py +0 -300
  138. package/src/db/_tasks 2.py +0 -91
  139. package/src/evolution_cycle 2.py +0 -266
  140. package/src/hnsw_index 2.py +0 -254
  141. package/src/hooks/auto_capture 2.py +0 -208
  142. package/src/hooks/caffeinate-guard 2.sh +0 -8
  143. package/src/hooks/capture-session 2.sh +0 -21
  144. package/src/hooks/capture-tool-logs 2.sh +0 -127
  145. package/src/hooks/daily-briefing-check 2.sh +0 -33
  146. package/src/hooks/inbox-hook 2.sh +0 -76
  147. package/src/hooks/post-compact 2.sh +0 -148
  148. package/src/hooks/pre-compact 2.sh +0 -151
  149. package/src/hooks/session-start 2.sh +0 -268
  150. package/src/hooks/session-stop 2.sh +0 -140
  151. package/src/kg_populate 2.py +0 -290
  152. package/src/knowledge_graph 2.py +0 -257
  153. package/src/maintenance 2.py +0 -59
  154. package/src/migrate_embeddings 2.py +0 -122
  155. package/src/plugin_loader 2.py +0 -202
  156. package/src/plugins/__init__ 2.py +0 -0
  157. package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
  158. package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
  159. package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
  160. package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
  161. package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
  162. package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
  163. package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
  164. package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
  165. package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
  166. package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
  167. package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
  168. package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
  169. package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
  170. package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
  171. package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
  172. package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
  173. package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
  174. package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
  175. package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
  176. package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
  177. package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
  178. package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
  179. package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
  180. package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
  181. package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
  182. package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
  183. package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
  184. package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
  185. package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
  186. package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
  187. package/src/plugins/__pycache__/schedule.cpython-310.pyc +0 -0
  188. package/src/plugins/__pycache__/schedule.cpython-314.pyc +0 -0
  189. package/src/plugins/__pycache__/skills.cpython-310.pyc +0 -0
  190. package/src/plugins/__pycache__/skills.cpython-314.pyc +0 -0
  191. package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
  192. package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
  193. package/src/plugins/adaptive_mode 2.py +0 -805
  194. package/src/plugins/agents 2.py +0 -52
  195. package/src/plugins/artifact_registry 2.py +0 -450
  196. package/src/plugins/backup 2.py +0 -104
  197. package/src/plugins/cognitive_memory 2.py +0 -564
  198. package/src/plugins/core_rules 2.py +0 -252
  199. package/src/plugins/cortex 2.py +0 -299
  200. package/src/plugins/entities 2.py +0 -67
  201. package/src/plugins/episodic_memory 2.py +0 -533
  202. package/src/plugins/evolution 2.py +0 -115
  203. package/src/plugins/guard 2.py +0 -746
  204. package/src/plugins/knowledge_graph_tools 2.py +0 -105
  205. package/src/plugins/preferences 2.py +0 -47
  206. package/src/plugins/update 2.py +0 -256
  207. package/src/requirements 2.txt +0 -12
  208. package/src/rules/__init__ 2.py +0 -0
  209. package/src/rules/core-rules 2.json +0 -331
  210. package/src/rules/migrate 2.py +0 -207
  211. package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
  212. package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
  213. package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
  214. package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
  215. package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
  216. package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
  217. package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
  218. package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
  219. package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
  220. package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
  221. package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
  222. package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
  223. package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
  224. package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
  225. package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
  226. package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
  227. package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
  228. package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
  229. package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
  230. package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
  231. package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
  232. package/src/scripts/check-context 2.py +0 -264
  233. package/src/scripts/nexo-auto-update 2.py +0 -6
  234. package/src/scripts/nexo-backup 2.sh +0 -25
  235. package/src/scripts/nexo-brain-activation 2.sh +0 -140
  236. package/src/scripts/nexo-catchup 2.py +0 -242
  237. package/src/scripts/nexo-cognitive-decay 2.py +0 -182
  238. package/src/scripts/nexo-daily-self-audit 2.py +0 -552
  239. package/src/scripts/nexo-deep-sleep 2.sh +0 -97
  240. package/src/scripts/nexo-evolution-run 2.py +0 -597
  241. package/src/scripts/nexo-followup-hygiene 2.py +0 -112
  242. package/src/scripts/nexo-github-monitor 2.py +0 -256
  243. package/src/scripts/nexo-immune 2.py +0 -927
  244. package/src/scripts/nexo-inbox-hook 2.sh +0 -74
  245. package/src/scripts/nexo-install 2.py +0 -6
  246. package/src/scripts/nexo-learning-housekeep 2.py +0 -245
  247. package/src/scripts/nexo-learning-validator 2.py +0 -207
  248. package/src/scripts/nexo-migrate 2.py +0 -232
  249. package/src/scripts/nexo-postmortem-consolidator 2.py +0 -421
  250. package/src/scripts/nexo-pre-commit 2.py +0 -120
  251. package/src/scripts/nexo-prevent-sleep 2.sh +0 -29
  252. package/src/scripts/nexo-proactive-dashboard 2.py +0 -345
  253. package/src/scripts/nexo-reflection 2.py +0 -253
  254. package/src/scripts/nexo-runtime-preflight 2.py +0 -274
  255. package/src/scripts/nexo-send-email 2.py +0 -25
  256. package/src/scripts/nexo-send-email.py +0 -25
  257. package/src/scripts/nexo-send-reply 2.py +0 -178
  258. package/src/scripts/nexo-send-reply.py +0 -178
  259. package/src/scripts/nexo-sleep 2.py +0 -592
  260. package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
  261. package/src/scripts/nexo-synthesis 2.py +0 -253
  262. package/src/scripts/nexo-tcc-approve 2.sh +0 -79
  263. package/src/scripts/nexo-update 2.sh +0 -161
  264. package/src/scripts/nexo-watchdog 2.sh +0 -878
  265. package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
  266. package/src/server 2.py +0 -733
  267. package/src/storage_router 2.py +0 -32
  268. package/src/tools_coordination 2.py +0 -102
  269. package/src/tools_credentials 2.py +0 -68
  270. package/src/tools_learnings 2.py +0 -220
  271. package/src/tools_menu 2.py +0 -227
  272. package/src/tools_reminders 2.py +0 -86
  273. package/src/tools_reminders_crud 2.py +0 -159
  274. package/src/tools_sessions 2.py +0 -476
  275. package/src/tools_task_history 2.py +0 -57
  276. package/templates/CLAUDE.md 2.template +0 -63
  277. package/templates/openclaw 2.json +0 -13
  278. package/tests/__init__ 2.py +0 -0
  279. package/tests/__init__.py +0 -0
  280. package/tests/conftest 2.py +0 -71
  281. package/tests/conftest.py +0 -71
  282. package/tests/test_cognitive 2.py +0 -205
  283. package/tests/test_cognitive.py +0 -205
  284. package/tests/test_knowledge_graph 2.py +0 -140
  285. package/tests/test_knowledge_graph.py +0 -140
  286. package/tests/test_migrations 2.py +0 -137
  287. 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)