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