nexo-brain 2.3.0 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/nexo-brain.js +92 -9
- package/bin/postinstall.js +22 -15
- package/package.json +7 -4
- package/src/auto_update.py +194 -5
- package/src/crons/sync.py +6 -2
- package/src/db/_core.py +1 -0
- package/src/db/_entities.py +1 -0
- package/src/db/_episodic.py +1 -0
- package/src/db/_learnings.py +1 -0
- package/src/db/_reminders.py +1 -0
- package/src/db/_schema.py +11 -1
- package/src/db/_sessions.py +1 -0
- package/src/db/_skills.py +1 -0
- package/src/hooks/capture-tool-logs.sh +23 -6
- package/src/hooks/session-start.sh +4 -3
- package/src/plugin_loader.py +1 -0
- package/src/plugins/update.py +377 -26
- package/src/scripts/deep-sleep/apply_findings.py +1 -0
- package/src/scripts/deep-sleep/collect.py +1 -0
- package/src/scripts/deep-sleep/extract.py +1 -0
- package/src/scripts/deep-sleep/synthesize.py +1 -0
- package/src/scripts/nexo-catchup.py +29 -4
- package/src/scripts/nexo-daily-self-audit.py +21 -1
- package/src/scripts/nexo-evolution-run.py +21 -1
- package/src/scripts/nexo-learning-housekeep.py +1 -0
- package/src/scripts/nexo-postmortem-consolidator.py +34 -9
- package/src/scripts/nexo-sleep.py +32 -10
- package/src/scripts/nexo-synthesis.py +29 -9
- package/src/scripts/nexo-update.sh +109 -7
- package/src/scripts/nexo-watchdog.sh +122 -58
- package/src/server.py +66 -1
- package/src/tools_coordination.py +1 -0
- package/src/tools_sessions.py +1 -0
- package/scripts/migrate-to-unified 2.sh +0 -813
- package/scripts/migrate-to-unified.sh +0 -813
- package/scripts/migrate-v1.5-to-v1.6 2.py +0 -778
- package/scripts/migrate-v1.5-to-v1.6.py +0 -778
- package/scripts/migrate-v1.7-to-v1.8 2.py +0 -214
- package/scripts/migrate-v1.7-to-v1.8.py +0 -214
- package/scripts/nexo-preflight.sh +0 -236
- package/scripts/pre-commit-check 2.sh +0 -55
- package/scripts/pre-commit-check.sh +0 -55
- package/src/__pycache__/auto_close_sessions.cpython-314.pyc +0 -0
- package/src/__pycache__/auto_update.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-310.pyc +0 -0
- package/src/__pycache__/hnsw_index.cpython-314.pyc +0 -0
- package/src/__pycache__/kg_populate.cpython-310.pyc +0 -0
- package/src/__pycache__/knowledge_graph.cpython-310.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-310.pyc +0 -0
- package/src/__pycache__/plugin_loader.cpython-314.pyc +0 -0
- package/src/__pycache__/tools_coordination.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_credentials.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_learnings.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_menu.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_reminders_crud.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_sessions.cpython-310.pyc +0 -0
- package/src/__pycache__/tools_task_history.cpython-310.pyc +0 -0
- package/src/auto_close_sessions 2.py +0 -159
- package/src/auto_update 2.py +0 -634
- package/src/claim_graph 2.py +0 -323
- package/src/cognitive/__init__ 2.py +0 -62
- package/src/cognitive/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_decay.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_ingest.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_memory.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_search.cpython-314.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-310.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-312.pyc +0 -0
- package/src/cognitive/__pycache__/_trust.cpython-314.pyc +0 -0
- package/src/cognitive/_core 2.py +0 -567
- package/src/cognitive/_decay 2.py +0 -382
- package/src/cognitive/_ingest 2.py +0 -892
- package/src/cognitive/_memory 2.py +0 -912
- package/src/cognitive/_search 2.py +0 -949
- package/src/cognitive/_trust 2.py +0 -464
- package/src/crons/__pycache__/sync.cpython-314.pyc +0 -0
- package/src/crons/manifest 2.json +0 -106
- package/src/crons/sync 2.py +0 -217
- package/src/dashboard/__init__ 2.py +0 -0
- package/src/dashboard/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/dashboard/__pycache__/app.cpython-310.pyc +0 -0
- package/src/dashboard/app 2.py +0 -789
- package/src/db/__init__ 2.py +0 -89
- package/src/db/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-312.pyc +0 -0
- package/src/db/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_core.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_credentials.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_cron_runs.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_cron_runs.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_entities.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_episodic.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_evolution.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_fts.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_learnings.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_reminders.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_schema.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_sessions.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_skills.cpython-314.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-310.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-312.pyc +0 -0
- package/src/db/__pycache__/_tasks.cpython-314.pyc +0 -0
- package/src/db/_core 2.py +0 -417
- package/src/db/_credentials 2.py +0 -124
- package/src/db/_entities 2.py +0 -178
- package/src/db/_episodic 2.py +0 -738
- package/src/db/_evolution 2.py +0 -54
- package/src/db/_fts 2.py +0 -406
- package/src/db/_learnings 2.py +0 -168
- package/src/db/_reminders 2.py +0 -338
- package/src/db/_schema 2.py +0 -364
- package/src/db/_sessions 2.py +0 -300
- package/src/db/_tasks 2.py +0 -91
- package/src/evolution_cycle 2.py +0 -266
- package/src/hnsw_index 2.py +0 -254
- package/src/hooks/auto_capture 2.py +0 -208
- package/src/hooks/caffeinate-guard 2.sh +0 -8
- package/src/hooks/capture-session 2.sh +0 -21
- package/src/hooks/capture-tool-logs 2.sh +0 -127
- package/src/hooks/daily-briefing-check 2.sh +0 -33
- package/src/hooks/inbox-hook 2.sh +0 -76
- package/src/hooks/post-compact 2.sh +0 -148
- package/src/hooks/pre-compact 2.sh +0 -151
- package/src/hooks/session-start 2.sh +0 -268
- package/src/hooks/session-stop 2.sh +0 -140
- package/src/kg_populate 2.py +0 -290
- package/src/knowledge_graph 2.py +0 -257
- package/src/maintenance 2.py +0 -59
- package/src/migrate_embeddings 2.py +0 -122
- package/src/plugin_loader 2.py +0 -202
- package/src/plugins/__init__ 2.py +0 -0
- package/src/plugins/__pycache__/__init__ 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/__init__.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/adaptive_mode.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/agents 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/agents.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/artifact_registry.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/backup.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cognitive_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/core_rules.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/cortex.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/entities.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/episodic_memory.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/evolution.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/guard.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/knowledge_graph_tools.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/preferences.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/schedule.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/schedule.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/skills.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/skills.cpython-314.pyc +0 -0
- package/src/plugins/__pycache__/update 2.cpython-310.pyc +0 -0
- package/src/plugins/__pycache__/update.cpython-310.pyc +0 -0
- package/src/plugins/adaptive_mode 2.py +0 -805
- package/src/plugins/agents 2.py +0 -52
- package/src/plugins/artifact_registry 2.py +0 -450
- package/src/plugins/backup 2.py +0 -104
- package/src/plugins/cognitive_memory 2.py +0 -564
- package/src/plugins/core_rules 2.py +0 -252
- package/src/plugins/cortex 2.py +0 -299
- package/src/plugins/entities 2.py +0 -67
- package/src/plugins/episodic_memory 2.py +0 -533
- package/src/plugins/evolution 2.py +0 -115
- package/src/plugins/guard 2.py +0 -746
- package/src/plugins/knowledge_graph_tools 2.py +0 -105
- package/src/plugins/preferences 2.py +0 -47
- package/src/plugins/update 2.py +0 -256
- package/src/requirements 2.txt +0 -12
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/core-rules 2.json +0 -331
- package/src/rules/migrate 2.py +0 -207
- package/src/scripts/__pycache__/nexo-auto-update.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-catchup.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-cognitive-decay.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-daily-self-audit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-evolution-run.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-followup-hygiene.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-immune.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-install.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-housekeep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-learning-validator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-migrate.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-postmortem-consolidator.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-pre-commit.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-proactive-dashboard.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-reflection.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-runtime-preflight.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-email.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-send-reply.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-sleep.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-synthesis.cpython-314.pyc +0 -0
- package/src/scripts/__pycache__/nexo-watchdog-smoke.cpython-314.pyc +0 -0
- package/src/scripts/check-context 2.py +0 -264
- package/src/scripts/nexo-auto-update 2.py +0 -6
- package/src/scripts/nexo-backup 2.sh +0 -25
- package/src/scripts/nexo-brain-activation 2.sh +0 -140
- package/src/scripts/nexo-catchup 2.py +0 -242
- package/src/scripts/nexo-cognitive-decay 2.py +0 -182
- package/src/scripts/nexo-daily-self-audit 2.py +0 -552
- package/src/scripts/nexo-deep-sleep 2.sh +0 -97
- package/src/scripts/nexo-evolution-run 2.py +0 -597
- package/src/scripts/nexo-followup-hygiene 2.py +0 -112
- package/src/scripts/nexo-github-monitor 2.py +0 -256
- package/src/scripts/nexo-immune 2.py +0 -927
- package/src/scripts/nexo-inbox-hook 2.sh +0 -74
- package/src/scripts/nexo-install 2.py +0 -6
- package/src/scripts/nexo-learning-housekeep 2.py +0 -245
- package/src/scripts/nexo-learning-validator 2.py +0 -207
- package/src/scripts/nexo-migrate 2.py +0 -232
- package/src/scripts/nexo-postmortem-consolidator 2.py +0 -421
- package/src/scripts/nexo-pre-commit 2.py +0 -120
- package/src/scripts/nexo-prevent-sleep 2.sh +0 -29
- package/src/scripts/nexo-proactive-dashboard 2.py +0 -345
- package/src/scripts/nexo-reflection 2.py +0 -253
- package/src/scripts/nexo-runtime-preflight 2.py +0 -274
- package/src/scripts/nexo-send-email 2.py +0 -25
- package/src/scripts/nexo-send-email.py +0 -25
- package/src/scripts/nexo-send-reply 2.py +0 -178
- package/src/scripts/nexo-send-reply.py +0 -178
- package/src/scripts/nexo-sleep 2.py +0 -592
- package/src/scripts/nexo-snapshot-restore 2.sh +0 -35
- package/src/scripts/nexo-synthesis 2.py +0 -253
- package/src/scripts/nexo-tcc-approve 2.sh +0 -79
- package/src/scripts/nexo-update 2.sh +0 -161
- package/src/scripts/nexo-watchdog 2.sh +0 -878
- package/src/scripts/nexo-watchdog-smoke 2.py +0 -119
- package/src/server 2.py +0 -733
- package/src/storage_router 2.py +0 -32
- package/src/tools_coordination 2.py +0 -102
- package/src/tools_credentials 2.py +0 -68
- package/src/tools_learnings 2.py +0 -220
- package/src/tools_menu 2.py +0 -227
- package/src/tools_reminders 2.py +0 -86
- package/src/tools_reminders_crud 2.py +0 -159
- package/src/tools_sessions 2.py +0 -476
- package/src/tools_task_history 2.py +0 -57
- package/templates/CLAUDE.md 2.template +0 -63
- package/templates/openclaw 2.json +0 -13
- package/tests/__init__ 2.py +0 -0
- package/tests/__init__.py +0 -0
- package/tests/conftest 2.py +0 -71
- package/tests/conftest.py +0 -71
- package/tests/test_cognitive 2.py +0 -205
- package/tests/test_cognitive.py +0 -205
- package/tests/test_knowledge_graph 2.py +0 -140
- package/tests/test_knowledge_graph.py +0 -140
- package/tests/test_migrations 2.py +0 -137
- package/tests/test_migrations.py +0 -137
package/src/dashboard/app 2.py
DELETED
|
@@ -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()
|