nexo-brain 2.4.0 → 2.5.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 (81) hide show
  1. package/README.md +80 -4
  2. package/bin/nexo-brain.js +238 -12
  3. package/bin/nexo.js +55 -0
  4. package/community/skills/.gitkeep +1 -0
  5. package/package.json +11 -3
  6. package/src/auto_update.py +193 -9
  7. package/src/cli.py +719 -0
  8. package/src/cognitive/_ingest.py +1 -1
  9. package/src/cognitive/_memory.py +4 -4
  10. package/src/crons/manifest.json +8 -0
  11. package/src/dashboard/app.py +700 -35
  12. package/src/dashboard/templates/adaptive.html +112 -218
  13. package/src/dashboard/templates/artifacts.html +133 -0
  14. package/src/dashboard/templates/backups.html +136 -0
  15. package/src/dashboard/templates/base.html +413 -0
  16. package/src/dashboard/templates/calendar.html +523 -654
  17. package/src/dashboard/templates/chat.html +356 -0
  18. package/src/dashboard/templates/claims.html +259 -0
  19. package/src/dashboard/templates/cortex.html +262 -0
  20. package/src/dashboard/templates/credentials.html +128 -0
  21. package/src/dashboard/templates/crons.html +370 -0
  22. package/src/dashboard/templates/dashboard.html +383 -578
  23. package/src/dashboard/templates/dreams.html +252 -0
  24. package/src/dashboard/templates/email.html +160 -0
  25. package/src/dashboard/templates/evolution.html +189 -0
  26. package/src/dashboard/templates/feed.html +249 -0
  27. package/src/dashboard/templates/followup_health.html +170 -0
  28. package/src/dashboard/templates/graph.html +191 -269
  29. package/src/dashboard/templates/guard.html +259 -0
  30. package/src/dashboard/templates/inbox.html +220 -346
  31. package/src/dashboard/templates/memory.html +317 -197
  32. package/src/dashboard/templates/operations.html +521 -698
  33. package/src/dashboard/templates/plugins.html +185 -0
  34. package/src/dashboard/templates/rules.html +246 -0
  35. package/src/dashboard/templates/sentiment.html +247 -0
  36. package/src/dashboard/templates/sessions.html +215 -182
  37. package/src/dashboard/templates/skills.html +329 -0
  38. package/src/dashboard/templates/somatic.html +68 -172
  39. package/src/dashboard/templates/triggers.html +133 -0
  40. package/src/dashboard/templates/trust.html +360 -0
  41. package/src/db/__init__.py +5 -0
  42. package/src/db/_schema.py +16 -1
  43. package/src/db/_sessions.py +22 -0
  44. package/src/db/_skills.py +980 -274
  45. package/src/doctor/__init__.py +1 -0
  46. package/src/doctor/formatters.py +52 -0
  47. package/src/doctor/models.py +44 -0
  48. package/src/doctor/orchestrator.py +42 -0
  49. package/src/doctor/providers/__init__.py +1 -0
  50. package/src/doctor/providers/boot.py +206 -0
  51. package/src/doctor/providers/deep.py +292 -0
  52. package/src/doctor/providers/runtime.py +686 -0
  53. package/src/evolution_cycle.py +86 -6
  54. package/src/hooks/post-compact.sh +5 -1
  55. package/src/hooks/pre-compact.sh +1 -1
  56. package/src/plugins/doctor.py +36 -0
  57. package/src/plugins/evolution.py +11 -3
  58. package/src/plugins/skills.py +135 -175
  59. package/src/requirements.txt +1 -0
  60. package/src/script_registry.py +322 -0
  61. package/src/scripts/deep-sleep/apply_findings.py +63 -48
  62. package/src/scripts/deep-sleep/extract-prompt.md +14 -0
  63. package/src/scripts/deep-sleep/synthesize-prompt.md +36 -0
  64. package/src/scripts/deep-sleep/synthesize.py +37 -1
  65. package/src/scripts/nexo-dashboard.sh +29 -0
  66. package/src/scripts/nexo-day-orchestrator.sh +139 -0
  67. package/src/scripts/nexo-evolution-run.py +141 -54
  68. package/src/scripts/nexo-learning-housekeep.py +1 -1
  69. package/src/scripts/nexo-watchdog.sh +1 -1
  70. package/src/server.py +9 -5
  71. package/src/skills/run-runtime-doctor/guide.md +12 -0
  72. package/src/skills/run-runtime-doctor/script.py +21 -0
  73. package/src/skills/run-runtime-doctor/skill.json +25 -0
  74. package/src/skills_runtime.py +347 -0
  75. package/src/tools_menu.py +3 -2
  76. package/src/tools_sessions.py +126 -0
  77. package/src/user_context.py +46 -0
  78. package/templates/nexo_helper.py +45 -0
  79. package/templates/script-template.py +44 -0
  80. package/templates/skill-script-template.py +39 -0
  81. package/templates/skill-template.md +33 -0
@@ -1,16 +1,15 @@
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.
1
+ """NEXO Brain Dashboard v3.0 Full 23-module cognitive dashboard.
5
2
 
6
3
  Usage:
7
4
  python3 -m dashboard.app [--port 6174] [--no-browser]
8
5
  """
9
6
 
10
7
  import argparse
8
+ import datetime
11
9
  import json
12
10
  import os
13
11
  import platform
12
+ import sqlite3
14
13
  import subprocess
15
14
  import sys
16
15
  import time
@@ -21,6 +20,7 @@ from typing import Optional
21
20
  from fastapi import FastAPI, Query, Request
22
21
  from fastapi.responses import HTMLResponse, JSONResponse
23
22
  from fastapi.staticfiles import StaticFiles
23
+ from jinja2 import Environment, FileSystemLoader
24
24
  from pydantic import BaseModel
25
25
 
26
26
  # Add parent dir to path so we can import NEXO modules
@@ -28,7 +28,7 @@ _PARENT = str(Path(__file__).resolve().parent.parent)
28
28
  if _PARENT not in sys.path:
29
29
  sys.path.insert(0, _PARENT)
30
30
 
31
- app = FastAPI(title="NEXO Brain Dashboard", version="2.0.0")
31
+ app = FastAPI(title="NEXO Brain Dashboard", version="3.0.0")
32
32
 
33
33
  TEMPLATES_DIR = Path(__file__).resolve().parent / "templates"
34
34
  STATIC_DIR = Path(__file__).resolve().parent / "static"
@@ -37,6 +37,12 @@ STATIC_DIR = Path(__file__).resolve().parent / "static"
37
37
  STATIC_DIR.mkdir(exist_ok=True)
38
38
  app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
39
39
 
40
+ # Jinja2 environment
41
+ jinja_env = Environment(
42
+ loader=FileSystemLoader(str(TEMPLATES_DIR)),
43
+ autoescape=True,
44
+ )
45
+
40
46
  # ---------------------------------------------------------------------------
41
47
  # Startup — create dashboard_notes table
42
48
  # ---------------------------------------------------------------------------
@@ -121,66 +127,167 @@ class InboxCreate(BaseModel):
121
127
  content: str
122
128
  reply_to: Optional[int] = None
123
129
 
130
+ class ChatMessage(BaseModel):
131
+ message: str
132
+
124
133
 
125
134
  # ---------------------------------------------------------------------------
126
- # HTML page routes — serve template files
135
+ # Helper DB connections
127
136
  # ---------------------------------------------------------------------------
128
137
 
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"))
138
+ def _cognitive_db():
139
+ """Direct connection to cognitive.db."""
140
+ nexo_home = os.environ.get("NEXO_HOME", str(Path.home() / "claude"))
141
+ db_path = Path(nexo_home) / "data" / "cognitive.db"
142
+ conn = sqlite3.connect(str(db_path))
143
+ conn.row_factory = sqlite3.Row
144
+ return conn
139
145
 
146
+ def _email_db():
147
+ """Direct connection to nexo-email.db."""
148
+ db_path = Path.home() / "claude" / "nexo-email" / "nexo-email.db"
149
+ if not db_path.exists():
150
+ return None
151
+ conn = sqlite3.connect(str(db_path))
152
+ conn.row_factory = sqlite3.Row
153
+ return conn
140
154
 
155
+
156
+ # ---------------------------------------------------------------------------
157
+ # HTML page routes — Jinja2 with fallback to plain file
158
+ # ---------------------------------------------------------------------------
159
+
160
+ def _render(name: str, **ctx) -> HTMLResponse:
161
+ """Render a Jinja2 template with context."""
162
+ try:
163
+ tmpl = jinja_env.get_template(name)
164
+ return HTMLResponse(tmpl.render(**ctx))
165
+ except Exception as exc:
166
+ import logging
167
+ logging.getLogger("dashboard").error("Template render failed for %s: %s", name, exc)
168
+ path = TEMPLATES_DIR / name
169
+ if path.exists():
170
+ return HTMLResponse(path.read_text(encoding="utf-8"))
171
+ return HTMLResponse(f"<h1>Template not found: {name}</h1>", status_code=200)
172
+
173
+ # Overview
141
174
  @app.get("/", response_class=HTMLResponse)
142
175
  async def page_dashboard():
143
- return _render_template("dashboard.html")
176
+ return _render("dashboard.html")
177
+
178
+ @app.get("/feed", response_class=HTMLResponse)
179
+ async def page_feed():
180
+ return _render("feed.html")
144
181
 
182
+ # Operations
183
+ @app.get("/crons", response_class=HTMLResponse)
184
+ async def page_crons():
185
+ return _render("crons.html")
145
186
 
146
187
  @app.get("/ops", response_class=HTMLResponse)
147
188
  async def page_ops():
148
- return _render_template("operations.html")
149
-
189
+ return _render("operations.html")
150
190
 
151
191
  @app.get("/calendar", response_class=HTMLResponse)
152
192
  async def page_calendar():
153
- return _render_template("calendar.html")
193
+ return _render("calendar.html")
194
+
195
+ # Intelligence
196
+ @app.get("/chat", response_class=HTMLResponse)
197
+ async def page_chat():
198
+ return _render("chat.html")
199
+
200
+ @app.get("/memory", response_class=HTMLResponse)
201
+ async def page_memory():
202
+ return _render("memory.html")
203
+
204
+ @app.get("/dreams", response_class=HTMLResponse)
205
+ async def page_dreams():
206
+ return _render("dreams.html")
207
+
208
+ @app.get("/skills", response_class=HTMLResponse)
209
+ async def page_skills():
210
+ return _render("skills.html")
211
+
212
+ @app.get("/trust", response_class=HTMLResponse)
213
+ async def page_trust():
214
+ return _render("trust.html")
215
+
216
+ # Security
217
+ @app.get("/guard", response_class=HTMLResponse)
218
+ async def page_guard():
219
+ return _render("guard.html")
220
+
221
+ @app.get("/cortex", response_class=HTMLResponse)
222
+ async def page_cortex():
223
+ return _render("cortex.html")
154
224
 
225
+ @app.get("/rules", response_class=HTMLResponse)
226
+ async def page_rules():
227
+ return _render("rules.html")
155
228
 
229
+ @app.get("/plugins", response_class=HTMLResponse)
230
+ async def page_plugins():
231
+ return _render("plugins.html")
232
+
233
+ # Advanced
234
+ @app.get("/evolution", response_class=HTMLResponse)
235
+ async def page_evolution():
236
+ return _render("evolution.html")
237
+
238
+ @app.get("/claims", response_class=HTMLResponse)
239
+ async def page_claims():
240
+ return _render("claims.html")
241
+
242
+ @app.get("/sentiment", response_class=HTMLResponse)
243
+ async def page_sentiment():
244
+ return _render("sentiment.html")
245
+
246
+ @app.get("/sessions", response_class=HTMLResponse)
247
+ async def page_sessions():
248
+ return _render("sessions.html")
249
+
250
+ @app.get("/triggers", response_class=HTMLResponse)
251
+ async def page_triggers():
252
+ return _render("triggers.html")
253
+
254
+ @app.get("/artifacts", response_class=HTMLResponse)
255
+ async def page_artifacts():
256
+ return _render("artifacts.html")
257
+
258
+ # System
156
259
  @app.get("/inbox", response_class=HTMLResponse)
157
260
  async def page_inbox():
158
- return _render_template("inbox.html")
261
+ return _render("inbox.html")
159
262
 
263
+ @app.get("/email", response_class=HTMLResponse)
264
+ async def page_email():
265
+ return _render("email.html")
160
266
 
161
- @app.get("/graph", response_class=HTMLResponse)
162
- async def page_graph():
163
- return _render_template("graph.html")
267
+ @app.get("/credentials", response_class=HTMLResponse)
268
+ async def page_credentials():
269
+ return _render("credentials.html")
164
270
 
271
+ @app.get("/backups", response_class=HTMLResponse)
272
+ async def page_backups():
273
+ return _render("backups.html")
165
274
 
166
- @app.get("/memory", response_class=HTMLResponse)
167
- async def page_memory():
168
- return _render_template("memory.html")
275
+ @app.get("/followup-health", response_class=HTMLResponse)
276
+ async def page_followup_health():
277
+ return _render("followup_health.html")
169
278
 
279
+ # Knowledge
280
+ @app.get("/graph", response_class=HTMLResponse)
281
+ async def page_graph():
282
+ return _render("graph.html")
170
283
 
171
284
  @app.get("/somatic", response_class=HTMLResponse)
172
285
  async def page_somatic():
173
- return _render_template("somatic.html")
174
-
286
+ return _render("somatic.html")
175
287
 
176
288
  @app.get("/adaptive", response_class=HTMLResponse)
177
289
  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")
290
+ return _render("adaptive.html")
184
291
 
185
292
 
186
293
  # ---------------------------------------------------------------------------
@@ -769,6 +876,564 @@ async def api_watchdog():
769
876
  return JSONResponse({"error": f"Invalid JSON: {e}"}, status_code=500)
770
877
 
771
878
 
879
+ # ===========================================================================
880
+ # NEW API ENDPOINTS — Dashboard v3.0 modules
881
+ # ===========================================================================
882
+
883
+ # ---------------------------------------------------------------------------
884
+ # Activity Feed
885
+ # ---------------------------------------------------------------------------
886
+
887
+ @app.get("/api/feed")
888
+ async def api_feed(limit: int = Query(50, ge=1, le=200)):
889
+ """Unified activity stream from multiple sources."""
890
+ db = _db()
891
+ conn = db.get_db()
892
+ events = []
893
+
894
+ for row in conn.execute(
895
+ "SELECT 'diary' as type, created_at, summary as content, domain, mental_state "
896
+ "FROM session_diary ORDER BY created_at DESC LIMIT ?", (limit,)
897
+ ).fetchall():
898
+ d = dict(row); d["icon"] = "book"; events.append(d)
899
+
900
+ for row in conn.execute(
901
+ "SELECT 'change' as type, created_at, what_changed as content, files, why "
902
+ "FROM change_log ORDER BY created_at DESC LIMIT ?", (limit,)
903
+ ).fetchall():
904
+ d = dict(row); d["icon"] = "code"; events.append(d)
905
+
906
+ for row in conn.execute(
907
+ "SELECT 'cortex' as type, created_at, goal as content, task_type, mode "
908
+ "FROM cortex_log ORDER BY created_at DESC LIMIT ?", (limit,)
909
+ ).fetchall():
910
+ d = dict(row); d["icon"] = "eye"; events.append(d)
911
+
912
+ for row in conn.execute(
913
+ "SELECT 'cron' as type, started_at as created_at, cron_id as content, "
914
+ "exit_code, duration_secs, summary "
915
+ "FROM cron_runs ORDER BY started_at DESC LIMIT ?", (limit,)
916
+ ).fetchall():
917
+ d = dict(row); d["icon"] = "clock"; events.append(d)
918
+
919
+ for row in conn.execute(
920
+ "SELECT 'decision' as type, created_at, decision as content, domain, confidence "
921
+ "FROM decisions ORDER BY created_at DESC LIMIT ?", (limit,)
922
+ ).fetchall():
923
+ d = dict(row); d["icon"] = "scale"; events.append(d)
924
+
925
+ events.sort(key=lambda e: e.get("created_at") or "", reverse=True)
926
+ return {"count": len(events[:limit]), "events": events[:limit]}
927
+
928
+
929
+ # ---------------------------------------------------------------------------
930
+ # Crons Control Center
931
+ # ---------------------------------------------------------------------------
932
+
933
+ @app.get("/api/crons")
934
+ async def api_crons(hours: int = Query(24, ge=1, le=168)):
935
+ db = _db()
936
+ conn = db.get_db()
937
+ cutoff = (datetime.datetime.now() - datetime.timedelta(hours=hours)).isoformat()
938
+ rows = conn.execute(
939
+ "SELECT * FROM cron_runs WHERE started_at >= ? ORDER BY started_at DESC", (cutoff,)
940
+ ).fetchall()
941
+ runs = [dict(r) for r in rows]
942
+
943
+ cron_summary = {}
944
+ for r in runs:
945
+ cid = r["cron_id"]
946
+ if cid not in cron_summary:
947
+ cron_summary[cid] = {"total": 0, "success": 0, "fail": 0, "last_run": r["started_at"], "durations": []}
948
+ cron_summary[cid]["total"] += 1
949
+ if r.get("exit_code") == 0:
950
+ cron_summary[cid]["success"] += 1
951
+ else:
952
+ cron_summary[cid]["fail"] += 1
953
+ if r.get("duration_secs"):
954
+ cron_summary[cid]["durations"].append(r["duration_secs"])
955
+
956
+ for cid, s in cron_summary.items():
957
+ s["avg_duration"] = round(sum(s["durations"]) / len(s["durations"]), 2) if s["durations"] else 0
958
+ del s["durations"]
959
+
960
+ return {"hours": hours, "total_runs": len(runs), "runs": runs[:100], "summary": cron_summary}
961
+
962
+ @app.get("/api/crons/timeline")
963
+ async def api_crons_timeline():
964
+ db = _db()
965
+ conn = db.get_db()
966
+ cutoff = (datetime.datetime.now() - datetime.timedelta(hours=24)).isoformat()
967
+ rows = conn.execute(
968
+ "SELECT cron_id, started_at, exit_code, duration_secs "
969
+ "FROM cron_runs WHERE started_at >= ? ORDER BY started_at", (cutoff,)
970
+ ).fetchall()
971
+ return {"timeline": [dict(r) for r in rows]}
972
+
973
+
974
+ # ---------------------------------------------------------------------------
975
+ # NEXO Chat
976
+ # ---------------------------------------------------------------------------
977
+
978
+ @app.post("/api/chat")
979
+ async def api_chat(body: ChatMessage):
980
+ msg = body.message.lower().strip()
981
+ db = _db()
982
+ conn = db.get_db()
983
+
984
+ if any(w in msg for w in ["anoche", "noche", "last night", "overnight"]):
985
+ rows = conn.execute(
986
+ "SELECT * FROM session_diary WHERE domain LIKE '%sleep%' OR domain LIKE '%night%' "
987
+ "ORDER BY created_at DESC LIMIT 3"
988
+ ).fetchall()
989
+ if not rows:
990
+ rows = conn.execute("SELECT * FROM session_diary ORDER BY created_at DESC LIMIT 3").fetchall()
991
+ return {"answer": "Recent overnight activity:", "data": [dict(r) for r in rows], "query_type": "diary"}
992
+
993
+ elif any(w in msg for w in ["watchdog", "salud", "health", "status"]):
994
+ nexo_home = os.environ.get("NEXO_HOME", str(Path.home() / ".nexo"))
995
+ wp = Path(nexo_home) / "operations" / "watchdog-status.json"
996
+ if wp.exists():
997
+ return {"answer": "Watchdog status:", "data": json.loads(wp.read_text()), "query_type": "watchdog"}
998
+ return {"answer": "Watchdog not available.", "data": [], "query_type": "watchdog"}
999
+
1000
+ elif any(w in msg for w in ["skill", "habilidad"]):
1001
+ rows = conn.execute(
1002
+ "SELECT id, name, level, trust_score, use_count FROM skills ORDER BY trust_score DESC LIMIT 20"
1003
+ ).fetchall()
1004
+ return {"answer": f"{len(rows)} skills:", "data": [dict(r) for r in rows], "query_type": "skills"}
1005
+
1006
+ elif any(w in msg for w in ["cron", "ejecut", "cycle", "recent cron"]):
1007
+ rows = conn.execute(
1008
+ "SELECT cron_id, started_at, exit_code, duration_secs, summary "
1009
+ "FROM cron_runs ORDER BY started_at DESC LIMIT 10"
1010
+ ).fetchall()
1011
+ return {"answer": "Recent cron runs:", "data": [dict(r) for r in rows], "query_type": "crons"}
1012
+
1013
+ elif any(w in msg for w in ["trust", "confianza"]):
1014
+ cog = _cognitive()
1015
+ return {"answer": f"Trust: {cog.get_trust_score()}", "data": cog.get_trust_history(days=7), "query_type": "trust"}
1016
+
1017
+ elif any(w in msg for w in ["decision", "decidi"]):
1018
+ rows = conn.execute(
1019
+ "SELECT domain, decision, confidence, created_at FROM decisions ORDER BY created_at DESC LIMIT 10"
1020
+ ).fetchall()
1021
+ return {"answer": "Recent decisions:", "data": [dict(r) for r in rows], "query_type": "decisions"}
1022
+
1023
+ elif any(w in msg for w in ["followup", "pendiente", "overdue", "pending"]):
1024
+ rows = conn.execute(
1025
+ "SELECT id, description, date, status FROM followups "
1026
+ "WHERE status NOT IN ('completed','archived','deleted') ORDER BY date ASC LIMIT 20"
1027
+ ).fetchall()
1028
+ return {"answer": f"{len(rows)} pending followups:", "data": [dict(r) for r in rows], "query_type": "followups"}
1029
+
1030
+ elif any(w in msg for w in ["learn", "aprend", "error"]):
1031
+ rows = conn.execute(
1032
+ "SELECT id, category, title, priority, created_at FROM learnings WHERE status='active' ORDER BY created_at DESC LIMIT 15"
1033
+ ).fetchall()
1034
+ return {"answer": f"{len(rows)} learnings:", "data": [dict(r) for r in rows], "query_type": "learnings"}
1035
+
1036
+ elif any(w in msg for w in ["plugin"]):
1037
+ rows = conn.execute("SELECT filename, tools_count, tool_names FROM plugins").fetchall()
1038
+ return {"answer": f"{len(rows)} plugins:", "data": [dict(r) for r in rows], "query_type": "plugins"}
1039
+
1040
+ elif any(w in msg for w in ["memoria", "memory", "stm", "ltm"]):
1041
+ cog = _cognitive()
1042
+ return {"answer": "Cognitive memory:", "data": cog.get_stats(), "query_type": "memory"}
1043
+
1044
+ elif any(w in msg for w in ["resumen", "summary", "semana", "week"]):
1045
+ rows = conn.execute(
1046
+ "SELECT domain, summary, mental_state, created_at FROM session_diary ORDER BY created_at DESC LIMIT 10"
1047
+ ).fetchall()
1048
+ return {"answer": "Recent sessions:", "data": [dict(r) for r in rows], "query_type": "summary"}
1049
+
1050
+ elif any(w in msg for w in ["guard", "riesgo", "risk"]):
1051
+ rows = conn.execute(
1052
+ "SELECT area, files, learnings_returned, blocking_rules_returned, created_at "
1053
+ "FROM guard_checks ORDER BY created_at DESC LIMIT 15"
1054
+ ).fetchall()
1055
+ return {"answer": "Guard checks:", "data": [dict(r) for r in rows], "query_type": "guard"}
1056
+
1057
+ else:
1058
+ rows = conn.execute(
1059
+ "SELECT 'learning' as source, title as content, category, created_at FROM learnings "
1060
+ "WHERE title LIKE ? OR content LIKE ? ORDER BY created_at DESC LIMIT 5",
1061
+ (f"%{msg[:50]}%", f"%{msg[:50]}%")
1062
+ ).fetchall()
1063
+ results = [dict(r) for r in rows]
1064
+ diary = conn.execute(
1065
+ "SELECT 'diary' as source, summary as content, domain, created_at FROM session_diary "
1066
+ "WHERE summary LIKE ? ORDER BY created_at DESC LIMIT 5", (f"%{msg[:50]}%",)
1067
+ ).fetchall()
1068
+ results.extend([dict(r) for r in diary])
1069
+ if results:
1070
+ return {"answer": f"{len(results)} results:", "data": results, "query_type": "search"}
1071
+ return {"answer": "Try: crons, trust, skills, memory, decisions, last night, followups, learnings, guard, plugins, summary.", "data": [], "query_type": "help"}
1072
+
1073
+
1074
+ # ---------------------------------------------------------------------------
1075
+ # Memory Flow
1076
+ # ---------------------------------------------------------------------------
1077
+
1078
+ @app.get("/api/memory/flow")
1079
+ async def api_memory_flow():
1080
+ cog_conn = _cognitive_db()
1081
+ stm = cog_conn.execute("SELECT COUNT(*) FROM stm_memories").fetchone()[0]
1082
+ ltm = cog_conn.execute("SELECT COUNT(*) FROM ltm_memories").fetchone()[0]
1083
+ quar = cog_conn.execute("SELECT COUNT(*) FROM quarantine WHERE status='pending'").fetchone()[0]
1084
+ promoted = cog_conn.execute("SELECT COUNT(*) FROM stm_memories WHERE promoted_to_ltm=1").fetchone()[0]
1085
+ dreamed = cog_conn.execute("SELECT COUNT(*) FROM dreamed_pairs").fetchone()[0]
1086
+
1087
+ stm_recent = [dict(r) for r in cog_conn.execute(
1088
+ "SELECT id, content, source_type, domain, strength, created_at, promoted_to_ltm "
1089
+ "FROM stm_memories ORDER BY created_at DESC LIMIT 20"
1090
+ ).fetchall()]
1091
+ ltm_recent = [dict(r) for r in cog_conn.execute(
1092
+ "SELECT id, content, source_type, domain, strength, access_count, created_at "
1093
+ "FROM ltm_memories ORDER BY created_at DESC LIMIT 20"
1094
+ ).fetchall()]
1095
+ quarantine = [dict(r) for r in cog_conn.execute(
1096
+ "SELECT id, content, source_type, confidence, status, created_at FROM quarantine ORDER BY created_at DESC LIMIT 15"
1097
+ ).fetchall()]
1098
+ cog_conn.close()
1099
+
1100
+ return {"counts": {"stm": stm, "ltm": ltm, "quarantine": quar, "promoted": promoted, "dreamed": dreamed},
1101
+ "stm_recent": stm_recent, "ltm_recent": ltm_recent, "quarantine": quarantine}
1102
+
1103
+
1104
+ # ---------------------------------------------------------------------------
1105
+ # Dream Journal
1106
+ # ---------------------------------------------------------------------------
1107
+
1108
+ @app.get("/api/dreams")
1109
+ async def api_dreams(limit: int = Query(20, ge=1, le=100)):
1110
+ cog_conn = _cognitive_db()
1111
+ pairs = cog_conn.execute(
1112
+ "SELECT dp.id, dp.memory_a_id, dp.memory_b_id, dp.insight_id, dp.created_at, "
1113
+ "ltm.content as insight_content "
1114
+ "FROM dreamed_pairs dp LEFT JOIN ltm_memories ltm ON dp.insight_id = ltm.id "
1115
+ "ORDER BY dp.created_at DESC LIMIT ?", (limit,)
1116
+ ).fetchall()
1117
+ cog_conn.close()
1118
+
1119
+ db = _db()
1120
+ conn = db.get_db()
1121
+ sleep = [dict(r) for r in conn.execute(
1122
+ "SELECT summary, domain, mental_state, created_at FROM session_diary "
1123
+ "WHERE domain LIKE '%sleep%' OR domain LIKE '%dream%' OR domain LIKE '%night%' "
1124
+ "ORDER BY created_at DESC LIMIT 10"
1125
+ ).fetchall()]
1126
+
1127
+ return {"dreams": [dict(r) for r in pairs], "sleep_entries": sleep}
1128
+
1129
+
1130
+ # ---------------------------------------------------------------------------
1131
+ # Skills Lab
1132
+ # ---------------------------------------------------------------------------
1133
+
1134
+ @app.get("/api/skills")
1135
+ async def api_skills():
1136
+ db = _db()
1137
+ conn = db.get_db()
1138
+ skills = [dict(r) for r in conn.execute("SELECT * FROM skills ORDER BY trust_score DESC").fetchall()]
1139
+ for s in skills:
1140
+ for f in ("tags", "trigger_patterns", "source_sessions", "linked_learnings", "steps", "gotchas"):
1141
+ if s.get(f) and isinstance(s[f], str):
1142
+ try: s[f] = json.loads(s[f])
1143
+ except: pass
1144
+
1145
+ usage = [dict(r) for r in conn.execute(
1146
+ "SELECT skill_id, success, context, created_at FROM skill_usage ORDER BY created_at DESC LIMIT 30"
1147
+ ).fetchall()]
1148
+
1149
+ levels = {}
1150
+ for s in skills:
1151
+ lvl = s.get("level", "unknown")
1152
+ levels[lvl] = levels.get(lvl, 0) + 1
1153
+
1154
+ return {"skills": skills, "usage": usage, "levels": levels, "total": len(skills)}
1155
+
1156
+
1157
+ # ---------------------------------------------------------------------------
1158
+ # Trust Events
1159
+ # ---------------------------------------------------------------------------
1160
+
1161
+ @app.get("/api/trust/events")
1162
+ async def api_trust_events(limit: int = Query(50, ge=1, le=200)):
1163
+ cog_conn = _cognitive_db()
1164
+ rows = cog_conn.execute("SELECT * FROM trust_score ORDER BY created_at DESC LIMIT ?", (limit,)).fetchall()
1165
+ cog_conn.close()
1166
+ return {"events": [dict(r) for r in rows]}
1167
+
1168
+
1169
+ # ---------------------------------------------------------------------------
1170
+ # Guard Heatmap
1171
+ # ---------------------------------------------------------------------------
1172
+
1173
+ @app.get("/api/guard")
1174
+ async def api_guard(limit: int = Query(100, ge=1, le=500)):
1175
+ db = _db()
1176
+ conn = db.get_db()
1177
+ checks = [dict(r) for r in conn.execute("SELECT * FROM guard_checks ORDER BY created_at DESC LIMIT ?", (limit,)).fetchall()]
1178
+
1179
+ heatmap = {}
1180
+ for c in checks:
1181
+ area = c.get("area") or "unknown"
1182
+ if area not in heatmap:
1183
+ heatmap[area] = {"count": 0, "blocking": 0, "learnings": 0}
1184
+ heatmap[area]["count"] += 1
1185
+ heatmap[area]["blocking"] += c.get("blocking_rules_returned") or 0
1186
+ heatmap[area]["learnings"] += c.get("learnings_returned") or 0
1187
+
1188
+ cog_conn = _cognitive_db()
1189
+ markers = [dict(r) for r in cog_conn.execute(
1190
+ "SELECT target, target_type, risk_score, incident_count, last_incident "
1191
+ "FROM somatic_markers WHERE risk_score > 0 ORDER BY risk_score DESC LIMIT 30"
1192
+ ).fetchall()]
1193
+ cog_conn.close()
1194
+
1195
+ return {"checks": checks[:50], "heatmap": heatmap, "somatic_markers": markers}
1196
+
1197
+
1198
+ # ---------------------------------------------------------------------------
1199
+ # Cortex Monitor
1200
+ # ---------------------------------------------------------------------------
1201
+
1202
+ @app.get("/api/cortex")
1203
+ async def api_cortex(limit: int = Query(50, ge=1, le=200)):
1204
+ db = _db()
1205
+ conn = db.get_db()
1206
+ logs = [dict(r) for r in conn.execute("SELECT * FROM cortex_log ORDER BY created_at DESC LIMIT ?", (limit,)).fetchall()]
1207
+ decisions = [dict(r) for r in conn.execute("SELECT * FROM decisions ORDER BY created_at DESC LIMIT ?", (limit,)).fetchall()]
1208
+ return {"cortex_logs": logs, "decisions": decisions}
1209
+
1210
+
1211
+ # ---------------------------------------------------------------------------
1212
+ # Core Rules
1213
+ # ---------------------------------------------------------------------------
1214
+
1215
+ @app.get("/api/rules")
1216
+ async def api_rules():
1217
+ db = _db()
1218
+ conn = db.get_db()
1219
+ rules = [dict(r) for r in conn.execute("SELECT * FROM core_rules ORDER BY importance DESC, category").fetchall()]
1220
+ categories = {}
1221
+ for r in rules:
1222
+ cat = r.get("category", "uncategorized")
1223
+ categories.setdefault(cat, []).append(r)
1224
+ return {"rules": rules, "categories": categories, "total": len(rules), "active": sum(1 for r in rules if r.get("is_active"))}
1225
+
1226
+
1227
+ # ---------------------------------------------------------------------------
1228
+ # Plugins
1229
+ # ---------------------------------------------------------------------------
1230
+
1231
+ @app.get("/api/plugins")
1232
+ async def api_plugins():
1233
+ db = _db()
1234
+ conn = db.get_db()
1235
+ plugins = [dict(r) for r in conn.execute("SELECT * FROM plugins").fetchall()]
1236
+ return {"plugins": plugins, "total": len(plugins), "total_tools": sum(p.get("tools_count", 0) for p in plugins)}
1237
+
1238
+
1239
+ # ---------------------------------------------------------------------------
1240
+ # Evolution
1241
+ # ---------------------------------------------------------------------------
1242
+
1243
+ @app.get("/api/evolution")
1244
+ async def api_evolution():
1245
+ db = _db()
1246
+ conn = db.get_db()
1247
+ logs = [dict(r) for r in conn.execute("SELECT * FROM evolution_log ORDER BY created_at DESC LIMIT 50").fetchall()]
1248
+ metrics = [dict(r) for r in conn.execute("SELECT * FROM evolution_metrics ORDER BY measured_at DESC LIMIT 100").fetchall()]
1249
+ dimensions = {}
1250
+ for m in metrics:
1251
+ dim = m["dimension"]
1252
+ if dim not in dimensions:
1253
+ dimensions[dim] = {"score": m["score"], "delta": m.get("delta", 0), "measured_at": m["measured_at"]}
1254
+ return {"logs": logs, "metrics": metrics, "dimensions": dimensions}
1255
+
1256
+
1257
+ # ---------------------------------------------------------------------------
1258
+ # Claims Network
1259
+ # ---------------------------------------------------------------------------
1260
+
1261
+ @app.get("/api/claims")
1262
+ async def api_claims(limit: int = Query(100, ge=1, le=500)):
1263
+ cog_conn = _cognitive_db()
1264
+ claims = [dict(r) for r in cog_conn.execute(
1265
+ "SELECT id, text, source_type, source_id, confidence, verification_status, domain, created_at "
1266
+ "FROM claims ORDER BY created_at DESC LIMIT ?", (limit,)
1267
+ ).fetchall()]
1268
+ links = [dict(r) for r in cog_conn.execute(
1269
+ "SELECT source_claim_id, target_claim_id, relation, confidence FROM claim_links LIMIT ?", (limit * 2,)
1270
+ ).fetchall()]
1271
+ cog_conn.close()
1272
+ status_counts = {}
1273
+ for c in claims:
1274
+ s = c.get("verification_status", "unknown")
1275
+ status_counts[s] = status_counts.get(s, 0) + 1
1276
+ return {"claims": claims, "links": links, "status_counts": status_counts, "total": len(claims)}
1277
+
1278
+
1279
+ # ---------------------------------------------------------------------------
1280
+ # Sentiment
1281
+ # ---------------------------------------------------------------------------
1282
+
1283
+ @app.get("/api/sentiment")
1284
+ async def api_sentiment(days: int = Query(30, ge=1, le=90)):
1285
+ cog_conn = _cognitive_db()
1286
+ cutoff = (datetime.datetime.now() - datetime.timedelta(days=days)).isoformat()
1287
+ logs = [dict(r) for r in cog_conn.execute(
1288
+ "SELECT * FROM sentiment_log WHERE created_at >= ? ORDER BY created_at", (cutoff,)
1289
+ ).fetchall()]
1290
+ cog_conn.close()
1291
+
1292
+ daily = {}
1293
+ for l in logs:
1294
+ day = l["created_at"][:10] if l.get("created_at") else "unknown"
1295
+ if day not in daily:
1296
+ daily[day] = {"positive": 0, "negative": 0, "neutral": 0, "urgent": 0, "count": 0, "total_intensity": 0}
1297
+ s = l.get("sentiment", "neutral")
1298
+ if s in daily[day]: daily[day][s] += 1
1299
+ daily[day]["count"] += 1
1300
+ daily[day]["total_intensity"] += l.get("intensity", 0.5)
1301
+ for d in daily.values():
1302
+ d["avg_intensity"] = round(d["total_intensity"] / d["count"], 2) if d["count"] else 0
1303
+ del d["total_intensity"]
1304
+
1305
+ return {"logs": logs, "daily": daily}
1306
+
1307
+
1308
+ # ---------------------------------------------------------------------------
1309
+ # Triggers
1310
+ # ---------------------------------------------------------------------------
1311
+
1312
+ @app.get("/api/triggers")
1313
+ async def api_triggers():
1314
+ cog_conn = _cognitive_db()
1315
+ triggers = [dict(r) for r in cog_conn.execute("SELECT * FROM prospective_triggers ORDER BY created_at DESC").fetchall()]
1316
+ cog_conn.close()
1317
+ return {"triggers": triggers, "total": len(triggers),
1318
+ "armed": sum(1 for t in triggers if t.get("status") == "armed"),
1319
+ "fired": sum(1 for t in triggers if t.get("status") == "fired")}
1320
+
1321
+
1322
+ # ---------------------------------------------------------------------------
1323
+ # Artifacts
1324
+ # ---------------------------------------------------------------------------
1325
+
1326
+ @app.get("/api/artifacts")
1327
+ async def api_artifacts():
1328
+ db = _db()
1329
+ conn = db.get_db()
1330
+ artifacts = [dict(r) for r in conn.execute("SELECT * FROM artifact_registry ORDER BY last_touched_at DESC").fetchall()]
1331
+ for a in artifacts:
1332
+ for f in ("aliases", "ports", "paths"):
1333
+ if a.get(f) and isinstance(a[f], str):
1334
+ try: a[f] = json.loads(a[f])
1335
+ except: pass
1336
+ if a.get("metadata") and isinstance(a["metadata"], str):
1337
+ try: a["metadata"] = json.loads(a["metadata"])
1338
+ except: pass
1339
+ kinds = {}
1340
+ for a in artifacts:
1341
+ k = a.get("kind", "unknown")
1342
+ kinds[k] = kinds.get(k, 0) + 1
1343
+ return {"artifacts": artifacts, "total": len(artifacts), "kinds": kinds}
1344
+
1345
+
1346
+ # ---------------------------------------------------------------------------
1347
+ # Email Monitor
1348
+ # ---------------------------------------------------------------------------
1349
+
1350
+ @app.get("/api/email")
1351
+ async def api_email_stats():
1352
+ conn = _email_db()
1353
+ if not conn:
1354
+ return {"error": "Email DB not found", "stats": {}}
1355
+ total = conn.execute("SELECT COUNT(*) FROM emails").fetchone()[0]
1356
+ processed = conn.execute("SELECT COUNT(*) FROM emails WHERE status='processed'").fetchone()[0]
1357
+ pending = conn.execute("SELECT COUNT(*) FROM emails WHERE status='pending'").fetchone()[0]
1358
+ recent = [dict(r) for r in conn.execute(
1359
+ "SELECT message_id, from_addr, from_name, subject, received_at, status FROM emails ORDER BY received_at DESC LIMIT 20"
1360
+ ).fetchall()]
1361
+ threads = [dict(r) for r in conn.execute(
1362
+ "SELECT thread_id, COUNT(*) as count, MAX(received_at) as last_email "
1363
+ "FROM emails GROUP BY thread_id ORDER BY last_email DESC LIMIT 15"
1364
+ ).fetchall()]
1365
+ conn.close()
1366
+ return {"stats": {"total": total, "processed": processed, "pending": pending}, "recent": recent, "threads": threads}
1367
+
1368
+
1369
+ # ---------------------------------------------------------------------------
1370
+ # Credentials (names only)
1371
+ # ---------------------------------------------------------------------------
1372
+
1373
+ @app.get("/api/credentials")
1374
+ async def api_credentials():
1375
+ db = _db()
1376
+ conn = db.get_db()
1377
+ rows = conn.execute("SELECT service, key, notes, created_at, updated_at FROM credentials ORDER BY service, key").fetchall()
1378
+ creds = [dict(r) for r in rows]
1379
+ services = {}
1380
+ for c in creds:
1381
+ services.setdefault(c["service"], []).append({"key": c["key"], "notes": c.get("notes", "")})
1382
+ return {"credentials": creds, "services": services, "total": len(creds)}
1383
+
1384
+
1385
+ # ---------------------------------------------------------------------------
1386
+ # Backups
1387
+ # ---------------------------------------------------------------------------
1388
+
1389
+ @app.get("/api/backups")
1390
+ async def api_backups():
1391
+ nexo_home = Path(os.environ.get("NEXO_HOME", str(Path.home() / "claude")))
1392
+ backup_dir = nexo_home / "backups"
1393
+ data_dir = nexo_home / "data"
1394
+ backups = []
1395
+ if backup_dir.exists():
1396
+ for item in sorted(backup_dir.iterdir(), key=lambda p: p.stat().st_mtime, reverse=True)[:20]:
1397
+ stat = item.stat()
1398
+ backups.append({"name": item.name, "size_mb": round(stat.st_size / 1048576, 2) if item.is_file() else None,
1399
+ "modified": datetime.datetime.fromtimestamp(stat.st_mtime).isoformat(), "is_dir": item.is_dir()})
1400
+ db_sizes = {}
1401
+ for dbfile in ["nexo.db", "cognitive.db"]:
1402
+ p = data_dir / dbfile
1403
+ if p.exists():
1404
+ db_sizes[dbfile] = round(p.stat().st_size / 1048576, 2)
1405
+ return {"backups": backups, "db_sizes": db_sizes}
1406
+
1407
+
1408
+ # ---------------------------------------------------------------------------
1409
+ # Followup Health
1410
+ # ---------------------------------------------------------------------------
1411
+
1412
+ @app.get("/api/followup-health")
1413
+ async def api_followup_health():
1414
+ db = _db()
1415
+ conn = db.get_db()
1416
+ all_f = [dict(r) for r in conn.execute("SELECT * FROM followups").fetchall()]
1417
+ today = datetime.date.today().isoformat()
1418
+ pending = [f for f in all_f if f.get("status") not in ("completed", "archived", "deleted")]
1419
+ completed = [f for f in all_f if f.get("status") == "completed"]
1420
+ overdue = [f for f in pending if f.get("date") and f["date"] < today]
1421
+ rate = round(len(completed) / max(len(all_f), 1) * 100, 1)
1422
+ age_buckets = {"0-3d": 0, "4-7d": 0, "8-14d": 0, "15-30d": 0, "30d+": 0}
1423
+ for f in overdue:
1424
+ if f.get("date"):
1425
+ try:
1426
+ age = (datetime.date.today() - datetime.date.fromisoformat(f["date"])).days
1427
+ except: continue
1428
+ if age <= 3: age_buckets["0-3d"] += 1
1429
+ elif age <= 7: age_buckets["4-7d"] += 1
1430
+ elif age <= 14: age_buckets["8-14d"] += 1
1431
+ elif age <= 30: age_buckets["15-30d"] += 1
1432
+ else: age_buckets["30d+"] += 1
1433
+ return {"total": len(all_f), "pending": len(pending), "completed": len(completed),
1434
+ "overdue": len(overdue), "completion_rate": rate, "age_buckets": age_buckets, "overdue_items": overdue[:20]}
1435
+
1436
+
772
1437
  # ---------------------------------------------------------------------------
773
1438
  # Main — run with uvicorn
774
1439
  # ---------------------------------------------------------------------------