nexo-brain 2.4.0 → 2.5.0
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.
- package/README.md +65 -2
- package/bin/nexo-brain.js +208 -11
- package/bin/nexo.js +55 -0
- package/community/skills/.gitkeep +1 -0
- package/package.json +5 -2
- package/src/auto_update.py +158 -8
- package/src/cli.py +605 -0
- package/src/cognitive/_ingest.py +1 -1
- package/src/cognitive/_memory.py +4 -4
- package/src/crons/manifest.json +8 -0
- package/src/dashboard/app.py +700 -35
- package/src/dashboard/templates/adaptive.html +112 -218
- package/src/dashboard/templates/artifacts.html +133 -0
- package/src/dashboard/templates/backups.html +136 -0
- package/src/dashboard/templates/base.html +413 -0
- package/src/dashboard/templates/calendar.html +523 -654
- package/src/dashboard/templates/chat.html +356 -0
- package/src/dashboard/templates/claims.html +259 -0
- package/src/dashboard/templates/cortex.html +262 -0
- package/src/dashboard/templates/credentials.html +128 -0
- package/src/dashboard/templates/crons.html +370 -0
- package/src/dashboard/templates/dashboard.html +383 -578
- package/src/dashboard/templates/dreams.html +252 -0
- package/src/dashboard/templates/email.html +160 -0
- package/src/dashboard/templates/evolution.html +189 -0
- package/src/dashboard/templates/feed.html +249 -0
- package/src/dashboard/templates/followup_health.html +170 -0
- package/src/dashboard/templates/graph.html +191 -269
- package/src/dashboard/templates/guard.html +259 -0
- package/src/dashboard/templates/inbox.html +220 -346
- package/src/dashboard/templates/memory.html +317 -197
- package/src/dashboard/templates/operations.html +521 -698
- package/src/dashboard/templates/plugins.html +185 -0
- package/src/dashboard/templates/rules.html +246 -0
- package/src/dashboard/templates/sentiment.html +247 -0
- package/src/dashboard/templates/sessions.html +215 -182
- package/src/dashboard/templates/skills.html +329 -0
- package/src/dashboard/templates/somatic.html +68 -172
- package/src/dashboard/templates/triggers.html +133 -0
- package/src/dashboard/templates/trust.html +360 -0
- package/src/db/__init__.py +5 -0
- package/src/db/_schema.py +16 -1
- package/src/db/_sessions.py +22 -0
- package/src/db/_skills.py +980 -274
- package/src/doctor/__init__.py +1 -0
- package/src/doctor/formatters.py +52 -0
- package/src/doctor/models.py +44 -0
- package/src/doctor/orchestrator.py +42 -0
- package/src/doctor/providers/__init__.py +1 -0
- package/src/doctor/providers/boot.py +206 -0
- package/src/doctor/providers/deep.py +292 -0
- package/src/doctor/providers/runtime.py +686 -0
- package/src/hooks/post-compact.sh +5 -1
- package/src/hooks/pre-compact.sh +1 -1
- package/src/plugins/doctor.py +36 -0
- package/src/plugins/evolution.py +2 -1
- package/src/plugins/skills.py +135 -175
- package/src/requirements.txt +1 -0
- package/src/script_registry.py +322 -0
- package/src/scripts/deep-sleep/apply_findings.py +63 -48
- package/src/scripts/deep-sleep/extract-prompt.md +14 -0
- package/src/scripts/deep-sleep/synthesize-prompt.md +36 -0
- package/src/scripts/deep-sleep/synthesize.py +37 -1
- package/src/scripts/nexo-dashboard.sh +29 -0
- package/src/scripts/nexo-day-orchestrator.sh +139 -0
- package/src/scripts/nexo-evolution-run.py +2 -1
- package/src/scripts/nexo-learning-housekeep.py +1 -1
- package/src/scripts/nexo-watchdog.sh +1 -1
- package/src/server.py +9 -5
- package/src/skills/run-runtime-doctor/guide.md +12 -0
- package/src/skills/run-runtime-doctor/script.py +21 -0
- package/src/skills/run-runtime-doctor/skill.json +25 -0
- package/src/skills_runtime.py +347 -0
- package/src/tools_menu.py +3 -2
- package/src/tools_sessions.py +126 -0
- package/src/user_context.py +46 -0
- package/templates/nexo_helper.py +45 -0
- package/templates/script-template.py +44 -0
- package/templates/skill-script-template.py +39 -0
- package/templates/skill-template.md +33 -0
package/src/dashboard/app.py
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
"""NEXO Brain Dashboard —
|
|
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="
|
|
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
|
-
#
|
|
135
|
+
# Helper DB connections
|
|
127
136
|
# ---------------------------------------------------------------------------
|
|
128
137
|
|
|
129
|
-
def
|
|
130
|
-
"""
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
|
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
|
|
149
|
-
|
|
189
|
+
return _render("operations.html")
|
|
150
190
|
|
|
151
191
|
@app.get("/calendar", response_class=HTMLResponse)
|
|
152
192
|
async def page_calendar():
|
|
153
|
-
return
|
|
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
|
|
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("/
|
|
162
|
-
async def
|
|
163
|
-
return
|
|
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("/
|
|
167
|
-
async def
|
|
168
|
-
return
|
|
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
|
|
174
|
-
|
|
286
|
+
return _render("somatic.html")
|
|
175
287
|
|
|
176
288
|
@app.get("/adaptive", response_class=HTMLResponse)
|
|
177
289
|
async def page_adaptive():
|
|
178
|
-
return
|
|
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
|
# ---------------------------------------------------------------------------
|