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,789 +0,0 @@
1
- """NEXO Brain Dashboard — FastAPI app for inspecting cognitive state.
2
-
3
- Local dashboard: graphs, memories, somatic markers, trust, adaptive personality.
4
- Runs on-demand (not embedded in MCP stdio). Opens browser automatically.
5
-
6
- Usage:
7
- python3 -m dashboard.app [--port 6174] [--no-browser]
8
- """
9
-
10
- import argparse
11
- import json
12
- import os
13
- import platform
14
- import subprocess
15
- import sys
16
- import time
17
- import webbrowser
18
- from pathlib import Path
19
- from typing import Optional
20
-
21
- from fastapi import FastAPI, Query, Request
22
- from fastapi.responses import HTMLResponse, JSONResponse
23
- from fastapi.staticfiles import StaticFiles
24
- from pydantic import BaseModel
25
-
26
- # Add parent dir to path so we can import NEXO modules
27
- _PARENT = str(Path(__file__).resolve().parent.parent)
28
- if _PARENT not in sys.path:
29
- sys.path.insert(0, _PARENT)
30
-
31
- app = FastAPI(title="NEXO Brain Dashboard", version="2.0.0")
32
-
33
- TEMPLATES_DIR = Path(__file__).resolve().parent / "templates"
34
- STATIC_DIR = Path(__file__).resolve().parent / "static"
35
-
36
- # Mount static files
37
- STATIC_DIR.mkdir(exist_ok=True)
38
- app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
39
-
40
- # ---------------------------------------------------------------------------
41
- # Startup — create dashboard_notes table
42
- # ---------------------------------------------------------------------------
43
-
44
- @app.on_event("startup")
45
- async def create_tables():
46
- db = _db()
47
- conn = db.get_db()
48
- conn.execute("""
49
- CREATE TABLE IF NOT EXISTS dashboard_notes (
50
- id INTEGER PRIMARY KEY AUTOINCREMENT,
51
- direction TEXT NOT NULL,
52
- content TEXT NOT NULL,
53
- read INTEGER DEFAULT 0,
54
- reply_to INTEGER DEFAULT NULL,
55
- created_at TEXT DEFAULT (datetime('now'))
56
- )
57
- """)
58
- # Migration: add reply_to if missing
59
- try:
60
- conn.execute("SELECT reply_to FROM dashboard_notes LIMIT 1")
61
- except Exception:
62
- conn.execute("ALTER TABLE dashboard_notes ADD COLUMN reply_to INTEGER DEFAULT NULL")
63
- conn.commit()
64
-
65
-
66
- # ---------------------------------------------------------------------------
67
- # Lazy imports — modules live in the parent source directory
68
- # ---------------------------------------------------------------------------
69
-
70
- def _cognitive():
71
- import cognitive
72
- return cognitive
73
-
74
- def _knowledge_graph():
75
- import knowledge_graph as kg
76
- return kg
77
-
78
- def _db():
79
- import db as nexo_db
80
- return nexo_db
81
-
82
- def _adaptive():
83
- from plugins import adaptive_mode
84
- return adaptive_mode
85
-
86
-
87
- # ---------------------------------------------------------------------------
88
- # Pydantic models for request bodies
89
- # ---------------------------------------------------------------------------
90
-
91
- class ReminderCreate(BaseModel):
92
- description: str
93
- date: Optional[str] = None
94
- category: Optional[str] = "general"
95
-
96
- class ReminderUpdate(BaseModel):
97
- description: Optional[str] = None
98
- date: Optional[str] = None
99
- status: Optional[str] = None
100
- category: Optional[str] = None
101
-
102
- class FollowupCreate(BaseModel):
103
- description: str
104
- date: Optional[str] = None
105
- verification: Optional[str] = None
106
- reasoning: Optional[str] = None
107
-
108
- class FollowupUpdate(BaseModel):
109
- description: Optional[str] = None
110
- date: Optional[str] = None
111
- status: Optional[str] = None
112
- verification: Optional[str] = None
113
- reasoning: Optional[str] = None
114
-
115
- class MoveRequest(BaseModel):
116
- id: str
117
- direction: str # "to_followup" | "to_reminder"
118
-
119
- class InboxCreate(BaseModel):
120
- direction: str # "to_nexo" | "to_user"
121
- content: str
122
- reply_to: Optional[int] = None
123
-
124
-
125
- # ---------------------------------------------------------------------------
126
- # HTML page routes — serve template files
127
- # ---------------------------------------------------------------------------
128
-
129
- def _render_template(name: str) -> HTMLResponse:
130
- """Read a template file and return as HTML."""
131
- path = TEMPLATES_DIR / name
132
- if not path.exists():
133
- return HTMLResponse(
134
- f"<html><body><h1>Template not found: {name}</h1>"
135
- f"<p>Create it at <code>{path}</code></p></body></html>",
136
- status_code=200,
137
- )
138
- return HTMLResponse(path.read_text(encoding="utf-8"))
139
-
140
-
141
- @app.get("/", response_class=HTMLResponse)
142
- async def page_dashboard():
143
- return _render_template("dashboard.html")
144
-
145
-
146
- @app.get("/ops", response_class=HTMLResponse)
147
- async def page_ops():
148
- return _render_template("operations.html")
149
-
150
-
151
- @app.get("/calendar", response_class=HTMLResponse)
152
- async def page_calendar():
153
- return _render_template("calendar.html")
154
-
155
-
156
- @app.get("/inbox", response_class=HTMLResponse)
157
- async def page_inbox():
158
- return _render_template("inbox.html")
159
-
160
-
161
- @app.get("/graph", response_class=HTMLResponse)
162
- async def page_graph():
163
- return _render_template("graph.html")
164
-
165
-
166
- @app.get("/memory", response_class=HTMLResponse)
167
- async def page_memory():
168
- return _render_template("memory.html")
169
-
170
-
171
- @app.get("/somatic", response_class=HTMLResponse)
172
- async def page_somatic():
173
- return _render_template("somatic.html")
174
-
175
-
176
- @app.get("/adaptive", response_class=HTMLResponse)
177
- async def page_adaptive():
178
- return _render_template("adaptive.html")
179
-
180
-
181
- @app.get("/sessions", response_class=HTMLResponse)
182
- async def page_sessions():
183
- return _render_template("sessions.html")
184
-
185
-
186
- # ---------------------------------------------------------------------------
187
- # API endpoints — JSON (existing)
188
- # ---------------------------------------------------------------------------
189
-
190
- @app.get("/api/stats")
191
- async def api_stats():
192
- """Overview: trust score, memory counts, KG stats."""
193
- cog = _cognitive()
194
- kg = _knowledge_graph()
195
-
196
- trust = cog.get_trust_score()
197
- cog_stats = cog.get_stats()
198
- kg_stats = kg.stats()
199
- gate_stats = cog.get_gate_stats()
200
-
201
- return {
202
- "trust_score": trust,
203
- "cognitive": cog_stats,
204
- "knowledge_graph": kg_stats,
205
- "prediction_gate": gate_stats,
206
- }
207
-
208
-
209
- @app.get("/api/graph")
210
- async def api_graph(
211
- center: int = Query(None, description="Center node ID for subgraph"),
212
- depth: int = Query(2, ge=1, le=5, description="Traversal depth"),
213
- node_type: str = Query(None, description="Filter by node type"),
214
- node_ref: str = Query(None, description="Find node by type+ref"),
215
- ):
216
- """Subgraph for D3 visualization."""
217
- kg = _knowledge_graph()
218
-
219
- # If node_type+node_ref given, resolve to center ID
220
- if center is None and node_type and node_ref:
221
- node = kg.get_node(node_type, node_ref)
222
- # Fallback: try with type prefix (refs stored as "area:project-a", "file:path")
223
- if not node:
224
- node = kg.get_node(node_type, f"{node_type}:{node_ref}")
225
- if node:
226
- center = node["id"]
227
-
228
- if center is None:
229
- # Return full graph stats + top connected nodes as starting points
230
- s = kg.stats()
231
- return {
232
- "nodes": [],
233
- "edges": [],
234
- "hints": s.get("most_connected", []),
235
- "stats": {
236
- "total_nodes": s["nodes"],
237
- "total_edges": s["edges_active"],
238
- },
239
- }
240
-
241
- subgraph = kg.extract_subgraph(center, depth=depth)
242
- return subgraph
243
-
244
-
245
- @app.get("/api/memories")
246
- async def api_memories(
247
- q: str = Query("", description="Search query"),
248
- store: str = Query("both", description="stm, ltm, or both"),
249
- limit: int = Query(20, ge=1, le=100),
250
- ):
251
- """Memory search via cognitive engine."""
252
- cog = _cognitive()
253
-
254
- if not q:
255
- return {"results": [], "message": "Provide ?q= parameter to search"}
256
-
257
- results = cog.search(q, top_k=limit, stores=store)
258
- # Serialize — results may contain numpy arrays or sqlite Rows
259
- serialized = []
260
- for r in results:
261
- item = dict(r) if hasattr(r, "keys") else r
262
- # Remove embedding blob if present
263
- item.pop("embedding", None)
264
- item.pop("vec", None)
265
- serialized.append(item)
266
- return {"query": q, "store": store, "count": len(serialized), "results": serialized}
267
-
268
-
269
- @app.get("/api/somatic")
270
- async def api_somatic():
271
- """Somatic marker risk scores."""
272
- cog = _cognitive()
273
- top_risks = cog.somatic_top_risks(limit=20)
274
- return {"risks": top_risks}
275
-
276
-
277
- @app.get("/api/trust")
278
- async def api_trust():
279
- """Trust score history (last 30 days)."""
280
- cog = _cognitive()
281
- current = cog.get_trust_score()
282
- history = cog.get_trust_history(days=30)
283
- return {
284
- "current_score": current,
285
- "history": history,
286
- }
287
-
288
-
289
- @app.get("/api/adaptive")
290
- async def api_adaptive():
291
- """Adaptive personality: current weight state + mode history."""
292
- adp = _adaptive()
293
- state = adp._load_state()
294
- # Get recent history from DB
295
- db = _db()
296
- conn = db.get_db()
297
- rows = conn.execute(
298
- "SELECT * FROM adaptive_log ORDER BY timestamp DESC LIMIT 50"
299
- ).fetchall()
300
- history = [dict(r) for r in rows]
301
- return {
302
- "state": state,
303
- "weights": adp.WEIGHTS,
304
- "modes": {k: v["description"] for k, v in adp.MODES.items()},
305
- "history": history,
306
- }
307
-
308
-
309
- @app.get("/api/sessions")
310
- async def api_sessions(limit: int = Query(10, ge=1, le=50)):
311
- """Recent session diaries + active sessions from sessions table."""
312
- db = _db()
313
- conn = db.get_db()
314
- # Active sessions (from sessions table, not diaries)
315
- active_rows = conn.execute(
316
- "SELECT sid as session_id, task, last_update_epoch, claude_session_id "
317
- "FROM sessions WHERE last_update_epoch > (strftime('%s','now') - 900) "
318
- "ORDER BY last_update_epoch DESC"
319
- ).fetchall()
320
- active = [dict(r) for r in active_rows]
321
- # Add last_heartbeat as ISO string for frontend
322
- for a in active:
323
- epoch = a.get("last_update_epoch", 0)
324
- if epoch:
325
- import datetime
326
- a["last_heartbeat"] = datetime.datetime.fromtimestamp(epoch).isoformat()
327
- # Recent diaries
328
- rows = conn.execute(
329
- "SELECT * FROM session_diary ORDER BY created_at DESC LIMIT ?",
330
- (limit,),
331
- ).fetchall()
332
- diaries = [dict(r) for r in rows]
333
- return {"count": len(diaries), "sessions": active, "diaries": diaries}
334
-
335
-
336
- @app.get("/api/kg/nodes")
337
- async def api_kg_nodes(
338
- node_type: str = Query(None, description="Filter by node type"),
339
- limit: int = Query(100, ge=1, le=500),
340
- ):
341
- """List KG nodes, optionally filtered by type."""
342
- kg = _knowledge_graph()
343
- db = kg._get_db()
344
- if node_type:
345
- rows = db.execute(
346
- "SELECT * FROM kg_nodes WHERE node_type = ? ORDER BY id DESC LIMIT ?",
347
- (node_type, limit),
348
- ).fetchall()
349
- else:
350
- rows = db.execute(
351
- "SELECT * FROM kg_nodes ORDER BY id DESC LIMIT ?", (limit,)
352
- ).fetchall()
353
- nodes = [dict(r) for r in rows]
354
- return {"count": len(nodes), "nodes": nodes}
355
-
356
-
357
- # ---------------------------------------------------------------------------
358
- # Reminders CRUD
359
- # ---------------------------------------------------------------------------
360
-
361
- def _next_reminder_id(conn) -> str:
362
- """Generate next R-prefixed ID."""
363
- row = conn.execute(
364
- "SELECT id FROM reminders WHERE id LIKE 'R%' ORDER BY CAST(SUBSTR(id,2) AS INTEGER) DESC LIMIT 1"
365
- ).fetchone()
366
- if row:
367
- try:
368
- num = int(str(row[0])[1:]) + 1
369
- except (ValueError, IndexError):
370
- num = 1
371
- else:
372
- num = 1
373
- return f"R{num}"
374
-
375
-
376
- @app.get("/api/reminders")
377
- async def api_reminders_list(
378
- status: str = Query(None, description="Filter by status"),
379
- category: str = Query(None, description="Filter by category"),
380
- ):
381
- """List reminders."""
382
- db = _db()
383
- conn = db.get_db()
384
- query = "SELECT * FROM reminders WHERE 1=1"
385
- params = []
386
- if status:
387
- query += " AND status = ?"
388
- params.append(status)
389
- if category:
390
- query += " AND category = ?"
391
- params.append(category)
392
- query += " ORDER BY created_at DESC"
393
- rows = conn.execute(query, params).fetchall()
394
- reminders = [dict(r) for r in rows]
395
- return {"count": len(reminders), "reminders": reminders}
396
-
397
-
398
- @app.post("/api/reminders")
399
- async def api_reminders_create(body: ReminderCreate):
400
- """Create a reminder."""
401
- db = _db()
402
- conn = db.get_db()
403
- rid = _next_reminder_id(conn)
404
- now = time.time()
405
- conn.execute(
406
- "INSERT INTO reminders (id, description, date, status, category, created_at, updated_at) VALUES (?,?,?,?,?,?,?)",
407
- (rid, body.description, body.date, "PENDING", body.category or "general", now, now),
408
- )
409
- conn.commit()
410
- row = conn.execute("SELECT * FROM reminders WHERE id = ?", (rid,)).fetchone()
411
- return {"success": True, "reminder": dict(row)}
412
-
413
-
414
- @app.put("/api/reminders/{rid}")
415
- async def api_reminders_update(rid: str, body: ReminderUpdate):
416
- """Update a reminder."""
417
- db = _db()
418
- conn = db.get_db()
419
- row = conn.execute("SELECT * FROM reminders WHERE id = ?", (rid,)).fetchone()
420
- if not row:
421
- return JSONResponse({"error": f"Reminder {rid} not found"}, status_code=404)
422
- fields = []
423
- params = []
424
- if body.description is not None:
425
- fields.append("description = ?")
426
- params.append(body.description)
427
- if body.date is not None:
428
- fields.append("date = ?")
429
- params.append(body.date)
430
- if body.status is not None:
431
- fields.append("status = ?")
432
- params.append(body.status)
433
- if body.category is not None:
434
- fields.append("category = ?")
435
- params.append(body.category)
436
- if not fields:
437
- return {"success": True, "reminder": dict(row)}
438
- fields.append("updated_at = ?")
439
- params.append(time.time())
440
- params.append(rid)
441
- conn.execute(f"UPDATE reminders SET {', '.join(fields)} WHERE id = ?", params)
442
- conn.commit()
443
- row = conn.execute("SELECT * FROM reminders WHERE id = ?", (rid,)).fetchone()
444
- return {"success": True, "reminder": dict(row)}
445
-
446
-
447
- @app.delete("/api/reminders/{rid}")
448
- async def api_reminders_delete(rid: str):
449
- """Delete a reminder."""
450
- db = _db()
451
- conn = db.get_db()
452
- row = conn.execute("SELECT * FROM reminders WHERE id = ?", (rid,)).fetchone()
453
- if not row:
454
- return JSONResponse({"error": f"Reminder {rid} not found"}, status_code=404)
455
- conn.execute("DELETE FROM reminders WHERE id = ?", (rid,))
456
- conn.commit()
457
- return {"success": True, "deleted_id": rid}
458
-
459
-
460
- # ---------------------------------------------------------------------------
461
- # Followups CRUD
462
- # ---------------------------------------------------------------------------
463
-
464
- def _next_followup_id(conn) -> str:
465
- """Generate next NF-prefixed ID."""
466
- row = conn.execute(
467
- "SELECT id FROM followups WHERE id LIKE 'NF%' ORDER BY CAST(SUBSTR(id,3) AS INTEGER) DESC LIMIT 1"
468
- ).fetchone()
469
- if row:
470
- try:
471
- num = int(str(row[0])[2:]) + 1
472
- except (ValueError, IndexError):
473
- num = 1
474
- else:
475
- num = 1
476
- return f"NF{num}"
477
-
478
-
479
- @app.get("/api/followups")
480
- async def api_followups_list(
481
- status: str = Query(None, description="Filter by status"),
482
- ):
483
- """List followups."""
484
- db = _db()
485
- conn = db.get_db()
486
- query = "SELECT * FROM followups WHERE 1=1"
487
- params = []
488
- if status:
489
- query += " AND status = ?"
490
- params.append(status)
491
- query += " ORDER BY created_at DESC"
492
- rows = conn.execute(query, params).fetchall()
493
- followups = [dict(r) for r in rows]
494
- return {"count": len(followups), "followups": followups}
495
-
496
-
497
- @app.post("/api/followups")
498
- async def api_followups_create(body: FollowupCreate):
499
- """Create a followup."""
500
- db = _db()
501
- conn = db.get_db()
502
- fid = _next_followup_id(conn)
503
- now = time.time()
504
- conn.execute(
505
- "INSERT INTO followups (id, description, date, verification, status, reasoning, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)",
506
- (fid, body.description, body.date, body.verification, "PENDING", body.reasoning, now, now),
507
- )
508
- conn.commit()
509
- row = conn.execute("SELECT * FROM followups WHERE id = ?", (fid,)).fetchone()
510
- return {"success": True, "followup": dict(row)}
511
-
512
-
513
- @app.put("/api/followups/{fid}")
514
- async def api_followups_update(fid: str, body: FollowupUpdate):
515
- """Update a followup."""
516
- db = _db()
517
- conn = db.get_db()
518
- row = conn.execute("SELECT * FROM followups WHERE id = ?", (fid,)).fetchone()
519
- if not row:
520
- return JSONResponse({"error": f"Followup {fid} not found"}, status_code=404)
521
- fields = []
522
- params = []
523
- if body.description is not None:
524
- fields.append("description = ?")
525
- params.append(body.description)
526
- if body.date is not None:
527
- fields.append("date = ?")
528
- params.append(body.date)
529
- if body.status is not None:
530
- fields.append("status = ?")
531
- params.append(body.status)
532
- if body.verification is not None:
533
- fields.append("verification = ?")
534
- params.append(body.verification)
535
- if body.reasoning is not None:
536
- fields.append("reasoning = ?")
537
- params.append(body.reasoning)
538
- if not fields:
539
- return {"success": True, "followup": dict(row)}
540
- fields.append("updated_at = ?")
541
- params.append(time.time())
542
- params.append(fid)
543
- conn.execute(f"UPDATE followups SET {', '.join(fields)} WHERE id = ?", params)
544
- conn.commit()
545
- row = conn.execute("SELECT * FROM followups WHERE id = ?", (fid,)).fetchone()
546
- return {"success": True, "followup": dict(row)}
547
-
548
-
549
- @app.delete("/api/followups/{fid}")
550
- async def api_followups_delete(fid: str):
551
- """Delete a followup."""
552
- db = _db()
553
- conn = db.get_db()
554
- row = conn.execute("SELECT * FROM followups WHERE id = ?", (fid,)).fetchone()
555
- if not row:
556
- return JSONResponse({"error": f"Followup {fid} not found"}, status_code=404)
557
- conn.execute("DELETE FROM followups WHERE id = ?", (fid,))
558
- conn.commit()
559
- return {"success": True, "deleted_id": fid}
560
-
561
-
562
- # ---------------------------------------------------------------------------
563
- # Ops: Move and Execute
564
- # ---------------------------------------------------------------------------
565
-
566
- @app.post("/api/ops/move")
567
- async def api_ops_move(body: MoveRequest):
568
- """Move an item between reminders and followups."""
569
- db = _db()
570
- conn = db.get_db()
571
- now = time.time()
572
-
573
- if body.direction == "to_followup":
574
- # Read from reminders
575
- row = conn.execute("SELECT * FROM reminders WHERE id = ?", (body.id,)).fetchone()
576
- if not row:
577
- return JSONResponse({"error": f"Reminder {body.id} not found"}, status_code=404)
578
- item = dict(row)
579
- fid = _next_followup_id(conn)
580
- conn.execute(
581
- "INSERT INTO followups (id, description, date, status, created_at, updated_at) VALUES (?,?,?,?,?,?)",
582
- (fid, item["description"], item.get("date"), "PENDING", now, now),
583
- )
584
- conn.execute("DELETE FROM reminders WHERE id = ?", (body.id,))
585
- conn.commit()
586
- return {"success": True, "new_id": fid, "direction": "to_followup"}
587
-
588
- elif body.direction == "to_reminder":
589
- # Read from followups
590
- row = conn.execute("SELECT * FROM followups WHERE id = ?", (body.id,)).fetchone()
591
- if not row:
592
- return JSONResponse({"error": f"Followup {body.id} not found"}, status_code=404)
593
- item = dict(row)
594
- rid = _next_reminder_id(conn)
595
- conn.execute(
596
- "INSERT INTO reminders (id, description, date, status, category, created_at, updated_at) VALUES (?,?,?,?,?,?,?)",
597
- (rid, item["description"], item.get("date"), "PENDING", "general", now, now),
598
- )
599
- conn.execute("DELETE FROM followups WHERE id = ?", (body.id,))
600
- conn.commit()
601
- return {"success": True, "new_id": rid, "direction": "to_reminder"}
602
-
603
- else:
604
- return JSONResponse(
605
- {"error": f"Invalid direction: {body.direction}. Use 'to_followup' or 'to_reminder'"},
606
- status_code=400,
607
- )
608
-
609
-
610
- @app.post("/api/ops/execute/{fid}")
611
- async def api_ops_execute(fid: str):
612
- """Execute a followup by opening Terminal with claude command."""
613
- db = _db()
614
- conn = db.get_db()
615
- row = conn.execute("SELECT * FROM followups WHERE id = ?", (fid,)).fetchone()
616
- if not row:
617
- return JSONResponse({"error": f"Followup {fid} not found"}, status_code=404)
618
- item = dict(row)
619
- description = item["description"].replace('"', '\\"').replace("'", "\\'")
620
- if platform.system() != "Darwin":
621
- return JSONResponse(
622
- {"error": "This operation requires macOS (uses osascript to open Terminal)"},
623
- status_code=501,
624
- )
625
- script = f'tell application "Terminal" to do script "claude \\"NEXO: execute followup #{fid} — {description}\\""'
626
- subprocess.Popen(["osascript", "-e", script])
627
- return {"success": True, "followup_id": fid}
628
-
629
-
630
- # ---------------------------------------------------------------------------
631
- # Inbox endpoints
632
- # ---------------------------------------------------------------------------
633
-
634
- @app.get("/api/inbox")
635
- async def api_inbox_list(
636
- limit: int = Query(50, ge=1, le=200),
637
- unread_only: bool = Query(False),
638
- ):
639
- """List inbox notes."""
640
- db = _db()
641
- conn = db.get_db()
642
- query = "SELECT * FROM dashboard_notes WHERE 1=1"
643
- params = []
644
- if unread_only:
645
- query += " AND read = 0"
646
- query += " ORDER BY created_at DESC LIMIT ?"
647
- params.append(limit)
648
- rows = conn.execute(query, params).fetchall()
649
- notes = [dict(r) for r in rows]
650
- return {"count": len(notes), "notes": notes}
651
-
652
-
653
- @app.post("/api/inbox")
654
- async def api_inbox_create(body: InboxCreate):
655
- """Create an inbox note."""
656
- if body.direction not in ("to_nexo", "to_user"):
657
- return JSONResponse(
658
- {"error": "direction must be 'to_nexo' or 'to_user'"},
659
- status_code=400,
660
- )
661
- db = _db()
662
- conn = db.get_db()
663
- conn.execute(
664
- "INSERT INTO dashboard_notes (direction, content, reply_to) VALUES (?, ?, ?)",
665
- (body.direction, body.content, body.reply_to),
666
- )
667
- conn.commit()
668
- row = conn.execute("SELECT * FROM dashboard_notes ORDER BY id DESC LIMIT 1").fetchone()
669
- return {"success": True, "note": dict(row)}
670
-
671
-
672
- @app.put("/api/inbox/{nid}/read")
673
- async def api_inbox_mark_read(nid: int):
674
- """Mark a note as read."""
675
- db = _db()
676
- conn = db.get_db()
677
- row = conn.execute("SELECT * FROM dashboard_notes WHERE id = ?", (nid,)).fetchone()
678
- if not row:
679
- return JSONResponse({"error": f"Note {nid} not found"}, status_code=404)
680
- conn.execute("UPDATE dashboard_notes SET read = 1 WHERE id = ?", (nid,))
681
- conn.commit()
682
- row = conn.execute("SELECT * FROM dashboard_notes WHERE id = ?", (nid,)).fetchone()
683
- return {"success": True, "note": dict(row)}
684
-
685
-
686
- @app.get("/api/inbox/unread")
687
- async def api_inbox_unread():
688
- """Count unread notes per direction."""
689
- db = _db()
690
- conn = db.get_db()
691
- rows = conn.execute(
692
- "SELECT direction, COUNT(*) as count FROM dashboard_notes WHERE read = 0 GROUP BY direction"
693
- ).fetchall()
694
- counts = {r["direction"]: r["count"] for r in rows}
695
- return {
696
- "to_nexo": counts.get("to_nexo", 0),
697
- "to_user": counts.get("to_user", 0),
698
- "total": sum(counts.values()),
699
- }
700
-
701
-
702
- # ---------------------------------------------------------------------------
703
- # Calendar endpoint
704
- # ---------------------------------------------------------------------------
705
-
706
- @app.get("/api/calendar")
707
- async def api_calendar(
708
- year: int = Query(..., description="Year (e.g. 2026)"),
709
- month: int = Query(..., ge=1, le=12, description="Month (1-12)"),
710
- ):
711
- """Return all reminders and followups with dates in the given month."""
712
- db = _db()
713
- conn = db.get_db()
714
-
715
- # Format month prefix for LIKE query (dates stored as text YYYY-MM-DD or similar)
716
- month_prefix = f"{year}-{month:02d}%"
717
-
718
- reminder_rows = conn.execute(
719
- "SELECT *, 'reminder' as item_type FROM reminders WHERE date LIKE ? ORDER BY date ASC",
720
- (month_prefix,),
721
- ).fetchall()
722
-
723
- followup_rows = conn.execute(
724
- "SELECT *, 'followup' as item_type FROM followups WHERE date LIKE ? ORDER BY date ASC",
725
- (month_prefix,),
726
- ).fetchall()
727
-
728
- reminders = [dict(r) for r in reminder_rows]
729
- followups = [dict(r) for r in followup_rows]
730
-
731
- # Merge and sort by date
732
- all_items = sorted(reminders + followups, key=lambda x: x.get("date") or "")
733
-
734
- return {
735
- "year": year,
736
- "month": month,
737
- "count": len(all_items),
738
- "items": all_items,
739
- "reminders": reminders,
740
- "followups": followups,
741
- }
742
-
743
-
744
- # ---------------------------------------------------------------------------
745
- # Watchdog endpoint
746
- # ---------------------------------------------------------------------------
747
-
748
- @app.get("/api/watchdog")
749
- async def api_watchdog():
750
- """Read watchdog status from file."""
751
- nexo_home = os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))
752
- watchdog_path = Path(nexo_home) / "operations" / "watchdog-status.json"
753
- if not watchdog_path.exists():
754
- return JSONResponse(
755
- {"error": "watchdog-status.json not found", "path": str(watchdog_path)},
756
- status_code=404,
757
- )
758
- try:
759
- data = json.loads(watchdog_path.read_text(encoding="utf-8"))
760
- return data
761
- except json.JSONDecodeError as e:
762
- return JSONResponse({"error": f"Invalid JSON: {e}"}, status_code=500)
763
-
764
-
765
- # ---------------------------------------------------------------------------
766
- # Main — run with uvicorn
767
- # ---------------------------------------------------------------------------
768
-
769
- def main():
770
- parser = argparse.ArgumentParser(description="NEXO Brain Dashboard")
771
- parser.add_argument("--port", type=int, default=6174, help="Port (default: 6174)")
772
- parser.add_argument("--no-browser", action="store_true", help="Don't open browser")
773
- args = parser.parse_args()
774
-
775
- if not args.no_browser:
776
- # Open browser after a short delay (uvicorn will be starting)
777
- import threading
778
- def _open():
779
- import time
780
- time.sleep(1.2)
781
- webbrowser.open(f"http://localhost:{args.port}")
782
- threading.Thread(target=_open, daemon=True).start()
783
-
784
- import uvicorn
785
- uvicorn.run(app, host="127.0.0.1", port=args.port, log_level="info")
786
-
787
-
788
- if __name__ == "__main__":
789
- main()