nexo-brain 5.3.19 → 5.3.21
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/.claude-plugin/plugin.json +1 -1
- package/bin/nexo-brain.js +52 -10
- package/package.json +1 -1
- package/src/auto_update.py +11 -8
- package/src/dashboard/static/favicon 2.svg +32 -0
- package/src/dashboard/static/nexo-logo 2.png +0 -0
- package/src/dashboard/static/nexo-logo 2.svg +40 -0
- package/src/dashboard/static/style 2.css +2458 -0
- package/src/dashboard/templates/adaptive 2.html +118 -0
- package/src/dashboard/templates/artifacts 2.html +133 -0
- package/src/dashboard/templates/backups 2.html +136 -0
- package/src/dashboard/templates/base 2.html +417 -0
- package/src/dashboard/templates/calendar 2.html +591 -0
- package/src/dashboard/templates/chat 2.html +356 -0
- package/src/dashboard/templates/claims 2.html +259 -0
- package/src/dashboard/templates/cortex 2.html +321 -0
- package/src/dashboard/templates/credentials 2.html +128 -0
- package/src/dashboard/templates/crons 2.html +370 -0
- package/src/dashboard/templates/dashboard 2.html +494 -0
- package/src/dashboard/templates/dreams 2.html +252 -0
- package/src/dashboard/templates/email 2.html +160 -0
- package/src/dashboard/templates/evolution 2.html +189 -0
- package/src/dashboard/templates/feed 2.html +249 -0
- package/src/dashboard/templates/followup_health 2.html +170 -0
- package/src/dashboard/templates/graph 2.html +201 -0
- package/src/dashboard/templates/guard 2.html +259 -0
- package/src/dashboard/templates/inbox 2.html +251 -0
- package/src/dashboard/templates/memory 2.html +420 -0
- package/src/dashboard/templates/operations 2.html +608 -0
- package/src/dashboard/templates/plugins 2.html +185 -0
- package/src/dashboard/templates/protocol 2.html +199 -0
- package/src/dashboard/templates/rules 2.html +246 -0
- package/src/dashboard/templates/sentiment 2.html +247 -0
- package/src/dashboard/templates/sessions 2.html +218 -0
- package/src/dashboard/templates/skills 2.html +329 -0
- package/src/dashboard/templates/somatic 2.html +73 -0
- package/src/dashboard/templates/triggers 2.html +133 -0
- package/src/dashboard/templates/trust 2.html +360 -0
- package/src/db/__init__ 2.py +259 -0
- package/src/db/_core 2.py +437 -0
- package/src/db/_credentials 2.py +124 -0
- package/src/db/_episodic 2.py +762 -0
- package/src/db/_evolution 2.py +54 -0
- package/src/db/_fts 2.py +406 -0
- package/src/db/_goal_profiles 2.py +376 -0
- package/src/db/_hot_context 2.py +660 -0
- package/src/db/_outcomes 2.py +800 -0
- package/src/db/_personal_scripts 2.py +582 -0
- package/src/db/_sessions 2.py +330 -0
- package/src/db/_tasks 2.py +91 -0
- package/src/db/_watchers 2.py +173 -0
- package/src/doctor/formatters 2.py +52 -0
- package/src/doctor/models 2.py +69 -0
- package/src/doctor/planes 2.py +87 -0
- package/src/doctor/providers/__init__ 2.py +1 -0
- package/src/doctor/providers/deep 2.py +367 -0
- package/src/evolution_cycle 2.py +519 -0
- package/src/hooks/auto_capture 2.py +208 -0
- package/src/hooks/caffeinate-guard 2.sh +8 -0
- package/src/hooks/capture-session 2.sh +21 -0
- package/src/hooks/capture-tool-logs 2.sh +158 -0
- package/src/hooks/daily-briefing-check 2.sh +33 -0
- package/src/hooks/heartbeat-enforcement 2.py +90 -0
- package/src/hooks/heartbeat-posttool 2.sh +18 -0
- package/src/hooks/inbox-hook 2.sh +76 -0
- package/src/hooks/post-compact 2.sh +152 -0
- package/src/hooks/pre-compact 2.sh +169 -0
- package/src/hooks/protocol-guardrail 2.sh +10 -0
- package/src/hooks/protocol-pretool-guardrail 2.sh +9 -0
- package/src/hooks/session-stop 2.sh +52 -0
- package/src/kg_populate 2.py +292 -0
- package/src/maintenance 2.py +53 -0
- package/src/memory_backends 2.py +71 -0
- package/src/migrate_embeddings 2.py +124 -0
- package/src/nexo_sdk 2.py +103 -0
- package/src/observability 2.py +199 -0
- package/src/plugin_loader 2.py +217 -0
- package/src/plugins/__init__ 2.py +0 -0
- package/src/plugins/artifact_registry 2.py +450 -0
- package/src/plugins/backup 2.py +127 -0
- package/src/plugins/claims_tools 2.py +119 -0
- package/src/plugins/cognitive_memory 2.py +609 -0
- package/src/plugins/core_rules 2.py +252 -0
- package/src/plugins/cortex 2.py +1155 -0
- package/src/plugins/entities 2.py +67 -0
- package/src/plugins/episodic_memory 2.py +560 -0
- package/src/plugins/evolution 2.py +167 -0
- package/src/plugins/goal_engine 2.py +142 -0
- package/src/plugins/guard 2.py +862 -0
- package/src/plugins/impact 2.py +29 -0
- package/src/plugins/knowledge_graph_tools 2.py +137 -0
- package/src/plugins/media_memory_tools 2.py +98 -0
- package/src/plugins/memory_export 2.py +196 -0
- package/src/plugins/outcomes 2.py +130 -0
- package/src/plugins/personal_scripts 2.py +117 -0
- package/src/plugins/preferences 2.py +47 -0
- package/src/plugins/protocol 2.py +1449 -0
- package/src/plugins/simple_api 2.py +106 -0
- package/src/plugins/skills 2.py +341 -0
- package/src/plugins/state_watchers 2.py +79 -0
- package/src/plugins/update 2.py +986 -0
- package/src/plugins/user_state_tools 2.py +43 -0
- package/src/plugins/workflow 2.py +588 -0
- package/src/protocol_settings 2.py +59 -0
- package/src/public_contribution 2.py +466 -0
- package/src/public_evolution_queue 2.py +241 -0
- package/src/requirements 2.txt +14 -0
- package/src/retroactive_learnings 2.py +373 -0
- package/src/rules/__init__ 2.py +0 -0
- package/src/rules/core-rules 2.json +331 -0
- package/src/rules/migrate 2.py +207 -0
- package/src/runtime_power 2.py +874 -0
- package/src/script_registry 2.py +1559 -0
- package/src/scripts/check-context 2.py +272 -0
- package/src/scripts/deep-sleep/apply_findings 2.py +2327 -0
- package/src/scripts/deep-sleep/collect 2.py +928 -0
- package/src/scripts/deep-sleep/extract 2.py +330 -0
- package/src/scripts/deep-sleep/extract-prompt 2.md +285 -0
- package/src/scripts/deep-sleep/synthesize 2.py +312 -0
- package/src/scripts/deep-sleep/synthesize-prompt 2.md +336 -0
- package/src/scripts/nexo-agent-run 2.py +75 -0
- package/src/scripts/nexo-auto-update 2.py +6 -0
- package/src/scripts/nexo-backup 2.sh +25 -0
- package/src/scripts/nexo-brain-activation 2.sh +140 -0
- package/src/scripts/nexo-catchup 2.py +300 -0
- package/src/scripts/nexo-cognitive-decay 2.py +257 -0
- package/src/scripts/nexo-cortex-cycle 2.py +293 -0
- package/src/scripts/nexo-cron-wrapper 2.sh +53 -0
- package/src/scripts/nexo-daily-self-audit 2.py +2161 -0
- package/src/scripts/nexo-dashboard 2.sh +29 -0
- package/src/scripts/nexo-deep-sleep 2.sh +86 -0
- package/src/scripts/nexo-evolution-run 2.py +1664 -0
- package/src/scripts/nexo-followup-hygiene 2.py +139 -0
- package/src/scripts/nexo-hook-record 2.py +42 -0
- package/src/scripts/nexo-immune 2.py +936 -0
- package/src/scripts/nexo-impact-scorer 2.py +117 -0
- package/src/scripts/nexo-inbox-hook 2.sh +74 -0
- package/src/scripts/nexo-install 2.py +6 -0
- package/src/scripts/nexo-learning-housekeep 2.py +401 -0
- package/src/scripts/nexo-learning-validator 2.py +266 -0
- package/src/scripts/nexo-migrate 2.py +260 -0
- package/src/scripts/nexo-outcome-checker 2.py +127 -0
- package/src/scripts/nexo-postmortem-consolidator 2.py +456 -0
- package/src/scripts/nexo-pre-commit 2.py +120 -0
- package/src/scripts/nexo-prevent-sleep 2.sh +35 -0
- package/src/scripts/nexo-proactive-dashboard 2.py +354 -0
- package/src/scripts/nexo-reflection 2.py +256 -0
- package/src/scripts/nexo-runtime-preflight 2.py +274 -0
- package/src/scripts/nexo-sleep 2.py +631 -0
- package/src/scripts/nexo-snapshot-restore 2.sh +35 -0
- package/src/scripts/nexo-sync-clients 2.py +16 -0
- package/src/scripts/nexo-synthesis 2.py +475 -0
- package/src/scripts/nexo-tcc-approve 2.sh +79 -0
- package/src/scripts/nexo-update 2.sh +306 -0
- package/src/scripts/nexo-watchdog 2.sh +1207 -0
- package/src/scripts/nexo-watchdog-smoke 2.py +119 -0
- package/src/scripts/rehydrate_learnings_from_archive 2.py +245 -0
- package/src/server 2.py +1296 -0
- package/src/skills/run-nexo-audit-phase/guide 2.md +43 -0
- package/src/skills/run-nexo-audit-phase/skill 2.json +59 -0
- package/src/skills/run-nexo-core-fix-cycle/guide 2.md +17 -0
- package/src/skills/run-nexo-core-fix-cycle/script 2.py +276 -0
- package/src/skills/run-nexo-core-fix-cycle/skill 2.json +58 -0
- package/src/skills/run-release-final-audit/guide 2.md +16 -0
- package/src/skills/run-release-final-audit/script 2.py +259 -0
- package/src/skills/run-release-final-audit/skill 2.json +77 -0
- package/src/skills/run-runtime-doctor/guide 2.md +12 -0
- package/src/skills/run-runtime-doctor/script 2.py +21 -0
- package/src/skills/run-runtime-doctor/skill 2.json +25 -0
- package/src/skills_runtime 2.py +932 -0
- package/src/state_watchers_runtime 2.py +475 -0
- package/src/storage_router 2.py +32 -0
- package/src/system_catalog 2.py +786 -0
- package/src/tools_coordination 2.py +103 -0
- package/src/tools_credentials 2.py +68 -0
- package/src/tools_drive 2.py +487 -0
- package/src/tools_hot_context 2.py +163 -0
- package/src/tools_learnings 2.py +612 -0
- package/src/tools_menu 2.py +229 -0
- package/src/tools_reminders 2.py +88 -0
- package/src/tools_reminders_crud 2.py +363 -0
- package/src/tools_sessions 2.py +1054 -0
- package/src/tools_system_catalog 2.py +19 -0
- package/src/tools_task_history 2.py +57 -0
- package/src/tools_transcripts 2.py +98 -0
- package/src/transcript_utils 2.py +412 -0
- package/src/user_context 2.py +46 -0
- package/src/user_data_portability 2.py +328 -0
- package/src/user_state_model 2.py +170 -0
- package/templates/CLAUDE.md 2.template +108 -0
- package/templates/CODEX.AGENTS.md 2.template +66 -0
- package/templates/launchagents/README 2.md +132 -0
- package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +39 -0
- package/templates/launchagents/com.nexo.catchup 2.plist +39 -0
- package/templates/launchagents/com.nexo.cognitive-decay 2.plist +40 -0
- package/templates/launchagents/com.nexo.dashboard 2.plist +43 -0
- package/templates/launchagents/com.nexo.deep-sleep 2.plist +43 -0
- package/templates/launchagents/com.nexo.evolution 2.plist +44 -0
- package/templates/launchagents/com.nexo.followup-hygiene 2.plist +45 -0
- package/templates/launchagents/com.nexo.immune 2.plist +41 -0
- package/templates/launchagents/com.nexo.postmortem 2.plist +45 -0
- package/templates/launchagents/com.nexo.self-audit 2.plist +47 -0
- package/templates/launchagents/com.nexo.synthesis 2.plist +45 -0
- package/templates/launchagents/com.nexo.watchdog 2.plist +37 -0
- package/templates/nexo_helper 2.py +301 -0
- package/templates/openclaw 2.json +13 -0
- package/templates/plugin-template 2.py +40 -0
- package/templates/script-template 2.py +59 -0
- package/templates/script-template 2.sh +13 -0
- package/templates/skill-script-template 2.py +48 -0
- package/templates/skill-template 2.md +33 -0
package/src/server 2.py
ADDED
|
@@ -0,0 +1,1296 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
"""NEXO MCP Server — Phase 4: Hot-Reload Plugin System."""
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import signal
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from fastmcp import FastMCP
|
|
9
|
+
from db import init_db, rebuild_fts_index, get_db, close_db, fts_add_dir, fts_remove_dir, fts_list_dirs
|
|
10
|
+
from tools_sessions import (
|
|
11
|
+
handle_startup,
|
|
12
|
+
handle_heartbeat,
|
|
13
|
+
handle_status,
|
|
14
|
+
handle_context_packet,
|
|
15
|
+
handle_smart_startup_query,
|
|
16
|
+
handle_session_portable_context,
|
|
17
|
+
handle_session_export_bundle,
|
|
18
|
+
)
|
|
19
|
+
from tools_hot_context import (
|
|
20
|
+
handle_recent_context_capture,
|
|
21
|
+
handle_recent_context,
|
|
22
|
+
handle_pre_action_context,
|
|
23
|
+
handle_recent_context_resolve,
|
|
24
|
+
handle_hot_context_list,
|
|
25
|
+
)
|
|
26
|
+
from tools_transcripts import (
|
|
27
|
+
handle_transcript_recent,
|
|
28
|
+
handle_transcript_search,
|
|
29
|
+
handle_transcript_read,
|
|
30
|
+
)
|
|
31
|
+
from tools_system_catalog import (
|
|
32
|
+
handle_system_catalog,
|
|
33
|
+
handle_tool_explain,
|
|
34
|
+
)
|
|
35
|
+
from tools_drive import (
|
|
36
|
+
handle_drive_signals,
|
|
37
|
+
handle_drive_reinforce,
|
|
38
|
+
handle_drive_act,
|
|
39
|
+
handle_drive_dismiss,
|
|
40
|
+
)
|
|
41
|
+
from user_context import get_context as _get_ctx
|
|
42
|
+
from tools_coordination import (
|
|
43
|
+
handle_track, handle_untrack, handle_files,
|
|
44
|
+
handle_send, handle_ask, handle_answer, handle_check_answer,
|
|
45
|
+
)
|
|
46
|
+
from tools_reminders import handle_reminders
|
|
47
|
+
from tools_menu import handle_menu
|
|
48
|
+
from tools_reminders_crud import (
|
|
49
|
+
handle_reminder_create, handle_reminder_get, handle_reminder_update,
|
|
50
|
+
handle_reminder_complete, handle_reminder_note, handle_reminder_restore, handle_reminder_delete,
|
|
51
|
+
handle_followup_create, handle_followup_get, handle_followup_update,
|
|
52
|
+
handle_followup_complete, handle_followup_note, handle_followup_restore, handle_followup_delete,
|
|
53
|
+
)
|
|
54
|
+
from tools_learnings import (
|
|
55
|
+
handle_learning_add, handle_learning_search,
|
|
56
|
+
handle_learning_update, handle_learning_delete, handle_learning_list,
|
|
57
|
+
handle_learning_quality,
|
|
58
|
+
)
|
|
59
|
+
from tools_credentials import (
|
|
60
|
+
handle_credential_get, handle_credential_create,
|
|
61
|
+
handle_credential_update, handle_credential_delete, handle_credential_list,
|
|
62
|
+
)
|
|
63
|
+
from tools_task_history import (
|
|
64
|
+
handle_task_log, handle_task_list, handle_task_frequency,
|
|
65
|
+
)
|
|
66
|
+
from plugin_loader import load_all_plugins, load_plugin, remove_plugin, list_plugins
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ── Graceful shutdown: close DB on any termination signal ──────────
|
|
70
|
+
def _shutdown_handler(signum, frame):
|
|
71
|
+
close_db()
|
|
72
|
+
sys.exit(0)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _server_init():
|
|
76
|
+
"""Run all side effects: signals, PID, DB, auto-update, plugins.
|
|
77
|
+
|
|
78
|
+
Called only when the server is actually started (not on import).
|
|
79
|
+
"""
|
|
80
|
+
signal.signal(signal.SIGTERM, _shutdown_handler)
|
|
81
|
+
signal.signal(signal.SIGINT, _shutdown_handler)
|
|
82
|
+
|
|
83
|
+
# ── Write PID file for stale process detection ─────────────────
|
|
84
|
+
_data_dir = os.path.join(os.environ.get("NEXO_HOME", os.path.join(os.path.expanduser("~"), ".nexo")), "data")
|
|
85
|
+
os.makedirs(_data_dir, exist_ok=True)
|
|
86
|
+
_pid_file = os.path.join(_data_dir, "nexo.pid")
|
|
87
|
+
with open(_pid_file, "w") as f:
|
|
88
|
+
f.write(str(os.getpid()))
|
|
89
|
+
|
|
90
|
+
# ── Database initialization with recovery ─────────────────────
|
|
91
|
+
import sqlite3
|
|
92
|
+
try:
|
|
93
|
+
init_db()
|
|
94
|
+
except sqlite3.DatabaseError as exc:
|
|
95
|
+
# Corruption or unreadable DB — attempt restore from backup
|
|
96
|
+
print(f"[NEXO] DB init failed: {exc}", file=sys.stderr)
|
|
97
|
+
_recovered = False
|
|
98
|
+
try:
|
|
99
|
+
from db._core import DB_PATH as _db_path
|
|
100
|
+
import glob as _glob
|
|
101
|
+
_backup_dir = os.path.join(
|
|
102
|
+
os.environ.get("NEXO_HOME", os.path.join(os.path.expanduser("~"), ".nexo")),
|
|
103
|
+
"backups",
|
|
104
|
+
)
|
|
105
|
+
_backups = sorted(_glob.glob(os.path.join(_backup_dir, "nexo-*.db")), reverse=True)
|
|
106
|
+
for _bk in _backups:
|
|
107
|
+
try:
|
|
108
|
+
_test = sqlite3.connect(_bk)
|
|
109
|
+
_result = _test.execute("PRAGMA integrity_check").fetchone()
|
|
110
|
+
_test.close()
|
|
111
|
+
if _result and _result[0] == "ok":
|
|
112
|
+
# Valid backup found — replace corrupt DB
|
|
113
|
+
import shutil
|
|
114
|
+
# Close any open connection before replacing
|
|
115
|
+
try:
|
|
116
|
+
close_db()
|
|
117
|
+
except Exception:
|
|
118
|
+
pass
|
|
119
|
+
shutil.copy2(_bk, _db_path)
|
|
120
|
+
print(f"[NEXO] Restored DB from backup: {os.path.basename(_bk)}", file=sys.stderr)
|
|
121
|
+
init_db()
|
|
122
|
+
_recovered = True
|
|
123
|
+
break
|
|
124
|
+
except Exception:
|
|
125
|
+
continue
|
|
126
|
+
except Exception as restore_exc:
|
|
127
|
+
print(f"[NEXO] Backup restore failed: {restore_exc}", file=sys.stderr)
|
|
128
|
+
|
|
129
|
+
if not _recovered:
|
|
130
|
+
# No valid backup — nuke corrupt file and start fresh
|
|
131
|
+
try:
|
|
132
|
+
close_db()
|
|
133
|
+
except Exception:
|
|
134
|
+
pass
|
|
135
|
+
try:
|
|
136
|
+
from db._core import DB_PATH as _db_path
|
|
137
|
+
if os.path.exists(_db_path):
|
|
138
|
+
_corrupt_path = _db_path + ".corrupt"
|
|
139
|
+
os.rename(_db_path, _corrupt_path)
|
|
140
|
+
print(f"[NEXO] Corrupt DB moved to {os.path.basename(_corrupt_path)}", file=sys.stderr)
|
|
141
|
+
# Remove WAL/SHM files too
|
|
142
|
+
for _ext in (".db-wal", ".db-shm"):
|
|
143
|
+
_wal = _db_path.replace(".db", _ext)
|
|
144
|
+
if os.path.exists(_wal):
|
|
145
|
+
os.remove(_wal)
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
try:
|
|
149
|
+
init_db()
|
|
150
|
+
print("[NEXO] Fresh database created.", file=sys.stderr)
|
|
151
|
+
except Exception as fresh_exc:
|
|
152
|
+
print(f"[NEXO] FATAL: Cannot initialize database: {fresh_exc}", file=sys.stderr)
|
|
153
|
+
print("[NEXO] Check permissions on NEXO_HOME/data/ and disk space.", file=sys.stderr)
|
|
154
|
+
sys.exit(1)
|
|
155
|
+
|
|
156
|
+
# ── Auto-update check (non-blocking, max 5s) ──────────────────
|
|
157
|
+
try:
|
|
158
|
+
from auto_update import startup_preflight
|
|
159
|
+
import threading
|
|
160
|
+
|
|
161
|
+
def _bg_update():
|
|
162
|
+
try:
|
|
163
|
+
result = startup_preflight(entrypoint="server", interactive=False)
|
|
164
|
+
if result.get("updated"):
|
|
165
|
+
print("[NEXO] Startup update applied.", file=sys.stderr)
|
|
166
|
+
if result.get("deferred_reason"):
|
|
167
|
+
print(f"[NEXO] Startup update deferred: {result['deferred_reason']}", file=sys.stderr)
|
|
168
|
+
if result.get("git_update"):
|
|
169
|
+
print(f"[NEXO] {result['git_update']}", file=sys.stderr)
|
|
170
|
+
if result.get("npm_notice"):
|
|
171
|
+
print(f"[NEXO] {result['npm_notice']}", file=sys.stderr)
|
|
172
|
+
if result.get("claude_md_update"):
|
|
173
|
+
print(f"[NEXO] {result['claude_md_update']}", file=sys.stderr)
|
|
174
|
+
for message in result.get("client_bootstrap_updates", []):
|
|
175
|
+
if message != result.get("claude_md_update"):
|
|
176
|
+
print(f"[NEXO] {message}", file=sys.stderr)
|
|
177
|
+
for m in result.get("migrations", []):
|
|
178
|
+
if m["status"] == "failed":
|
|
179
|
+
print(f"[NEXO] Migration {m['file']} FAILED: {m['message']}", file=sys.stderr)
|
|
180
|
+
except Exception as e:
|
|
181
|
+
print(f"[NEXO auto-update] error: {e}", file=sys.stderr)
|
|
182
|
+
|
|
183
|
+
_update_thread = threading.Thread(target=_bg_update, daemon=True)
|
|
184
|
+
_update_thread.start()
|
|
185
|
+
_update_thread.join(timeout=5) # Wait at most 5 seconds
|
|
186
|
+
except Exception:
|
|
187
|
+
pass # Never break startup
|
|
188
|
+
|
|
189
|
+
# ── Load plugins ───────────────────────────────────────────────
|
|
190
|
+
load_all_plugins(mcp)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
mcp = FastMCP(
|
|
194
|
+
name="nexo",
|
|
195
|
+
instructions=(
|
|
196
|
+
f"{_get_ctx().assistant_name} — cognitive co-operator. Save important info from tool results before they clear.\n\n"
|
|
197
|
+
"## CRITICAL — do these or you WILL get corrected\n"
|
|
198
|
+
"- **Protocol (MANDATORY for non-trivial work):** `nexo_task_open(...)` at the start and `nexo_task_close(...)` before saying done. "
|
|
199
|
+
"For edit/execute/delegate tasks this is the default path. Never claim completion without evidence.\n"
|
|
200
|
+
"- **Answer discipline (MANDATORY for non-trivial factual answers):** run `nexo_confidence_check(...)` or use `nexo_task_open(task_type='answer'|'analyze', ...)`. "
|
|
201
|
+
"If the response mode is `verify`, `ask`, or `defer`, follow it instead of answering from memory.\n"
|
|
202
|
+
"- **Workflow runtime (MANDATORY for long multi-step or cross-session work):** open `nexo_goal_open(...)` when the objective must survive sessions, then `nexo_workflow_open(...)`, "
|
|
203
|
+
"update meaningful checkpoints with `nexo_workflow_update(...)`, then use `nexo_workflow_resume(...)` / "
|
|
204
|
+
"`nexo_workflow_replay(...)` instead of restarting blindly.\n"
|
|
205
|
+
"- **Diagnostic plane (MANDATORY before diagnosing NEXO):** fix the plane explicitly first — `product_public`, `runtime_personal`, `installation_live`, `database_real`, or `cooperator`. "
|
|
206
|
+
"Do not mix product, runtime, install, DB, and agent-behavior explanations in the same diagnosis.\n"
|
|
207
|
+
"- **Guard (MANDATORY before ANY code edit):** `nexo_guard_check(files='...', area='...')` BEFORE editing code. "
|
|
208
|
+
"No exceptions. Blocking rules→resolve first. `nexo_track(sid=SID, paths=[...])` before shared files\n"
|
|
209
|
+
"- **Skills (MANDATORY before multi-step tasks):** `nexo_skill_match(task)` to find reusable procedures. "
|
|
210
|
+
"If match found, read it and follow the steps. After completion, `nexo_skill_result(id, success, context)` to record outcome.\n"
|
|
211
|
+
"- **Learnings (MANDATORY on corrections):** When you discover a bug, pattern, or get corrected→`nexo_learning_add` IMMEDIATELY. "
|
|
212
|
+
"Do NOT batch. Do NOT wait until end of session.\n\n"
|
|
213
|
+
"## Rules\n"
|
|
214
|
+
"- **Heartbeat:** `nexo_heartbeat(sid=SID, task='...', context_hint='...')` every user msg. "
|
|
215
|
+
"React: DIARY REMINDER→write diary, VIBE:NEGATIVE→ultra-concise, AUTO-PRIME→read learnings\n"
|
|
216
|
+
"- **Followups:** NEXO tasks, execute silently. 'done'/'all set'→`nexo_followup_complete` NOW. "
|
|
217
|
+
"Reminders=user's, alert when due\n"
|
|
218
|
+
"- **Reminder/followup history:** before update/delete/restore/note, call the corresponding "
|
|
219
|
+
"`nexo_reminder_get` / `nexo_followup_get` first and use its `READ_TOKEN`.\n"
|
|
220
|
+
"- **Observe:** correction→learning. 'tomorrow'→followup. person→entity. open topic→followup 3d\n"
|
|
221
|
+
"- **Trust events:** When user expresses satisfaction/thanks (any language)→`nexo_cognitive_trust(event='explicit_thanks')`. "
|
|
222
|
+
"When user corrects you→`nexo_cognitive_trust(event='correction')`. "
|
|
223
|
+
"When user delegates without micromanaging→`nexo_cognitive_trust(event='delegation')`. "
|
|
224
|
+
"When you catch something the user missed→`nexo_cognitive_trust(event='proactive_action')`. "
|
|
225
|
+
"Detect intent, not keywords — works in ALL languages.\n"
|
|
226
|
+
"- **Delegate:** prefer direct. If needed: `nexo_context_packet(area)` + guard + 'if unsure STOP'\n"
|
|
227
|
+
"- **Memory:** `nexo_recall` searches all. For fresh 24h continuity use `nexo_pre_action_context(query='...')` before acting and "
|
|
228
|
+
"`nexo_recent_context_capture(...)` / `nexo_recent_context_resolve(...)` for important ongoing threads. "
|
|
229
|
+
"If that is not enough, use `nexo_transcript_search(...)` / `nexo_transcript_read(...)` as the raw fallback to full conversations. "
|
|
230
|
+
"Use `nexo_system_catalog(...)` / `nexo_tool_explain(...)` when you need the live map of NEXO itself. "
|
|
231
|
+
"Before the first use of an unfamiliar NEXO tool, call `nexo_tool_explain(name)` to see its signature, examples, workflow notes, and common errors. "
|
|
232
|
+
"Capture: errors→`nexo_learning_add`, prefs, entities, decisions\n"
|
|
233
|
+
"- **Change log:** `nexo_task_close` should be the default closure path. If you bypass it, call `nexo_change_log(...)` after production edits. NOT for config dir\n"
|
|
234
|
+
"- **Diary:** When user signals end of session (any language, any style — 'bye', 'done', 'cierro', etc.), "
|
|
235
|
+
"write `nexo_session_diary_write(...)` with self_critique BEFORE responding. "
|
|
236
|
+
"Detect intent, not keywords. If session closes without diary, auto_close handles it.\n"
|
|
237
|
+
"- **Evolution:** NEXO has a weekly self-improvement cycle. If the user asks how NEXO evolves, inspect "
|
|
238
|
+
"`nexo_evolution_status` / `nexo_evolution_history` instead of assuming this subsystem does not exist. "
|
|
239
|
+
"Use propose/approve/reject only when the user explicitly wants to work on NEXO evolution.\n"
|
|
240
|
+
"- **Outcomes:** When you commit to an action with measurable success (deploy, fix, deadline, "
|
|
241
|
+
"metric target, recurring schedule), call `nexo_outcome_register(action_type, expected_result, "
|
|
242
|
+
"deadline, ...)` so the daily outcome-checker can verify and feed the skill auto-promotion "
|
|
243
|
+
"pipeline. Without this, `outcome_pattern_candidates` stays empty and successful patterns "
|
|
244
|
+
"never become reusable skills.\n"
|
|
245
|
+
"- **Cortex:** `nexo_cortex_check` before budget/campaign/architecture changes\n"
|
|
246
|
+
"- **Dissonance:** user contradicts memory→`nexo_cognitive_dissonance`. Frustrated→force=True\n"
|
|
247
|
+
"- **Trust:** <40=paranoid verify twice, >80=fluid. Check: `nexo_cognitive_trust`"
|
|
248
|
+
),
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _run_kwargs_from_env() -> dict:
|
|
253
|
+
transport = str(os.environ.get("NEXO_MCP_TRANSPORT", "stdio") or "stdio").strip().lower()
|
|
254
|
+
if transport in {"http", "streamable_http"}:
|
|
255
|
+
transport = "streamable-http"
|
|
256
|
+
if transport == "stdio":
|
|
257
|
+
return {"transport": "stdio"}
|
|
258
|
+
|
|
259
|
+
host = str(os.environ.get("NEXO_MCP_HOST", "127.0.0.1") or "127.0.0.1").strip()
|
|
260
|
+
port_text = str(os.environ.get("NEXO_MCP_PORT", "8000") or "8000").strip()
|
|
261
|
+
path = str(os.environ.get("NEXO_MCP_PATH", "/mcp") or "/mcp").strip() or "/mcp"
|
|
262
|
+
try:
|
|
263
|
+
port = int(port_text)
|
|
264
|
+
except Exception:
|
|
265
|
+
port = 8000
|
|
266
|
+
|
|
267
|
+
kwargs = {
|
|
268
|
+
"transport": transport,
|
|
269
|
+
"host": host,
|
|
270
|
+
"port": port,
|
|
271
|
+
}
|
|
272
|
+
if transport == "streamable-http":
|
|
273
|
+
kwargs["path"] = path
|
|
274
|
+
return kwargs
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# ── Session management (3 tools) ──────────────────────────────────
|
|
278
|
+
|
|
279
|
+
@mcp.tool
|
|
280
|
+
def nexo_startup(task: str = "Startup", claude_session_id: str = "", session_token: str = "", session_client: str = "") -> str:
|
|
281
|
+
"""Register new session, clean stale ones, return active sessions + alerts.
|
|
282
|
+
|
|
283
|
+
Call this ONCE at the start of every conversation.
|
|
284
|
+
Returns the session ID (SID) — store it for use in all other nexo_ tools.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
task: Initial task description.
|
|
288
|
+
claude_session_id: Legacy alias for the external client session token.
|
|
289
|
+
session_token: External client session token. Claude Code passes its UUID via hooks;
|
|
290
|
+
other clients may pass a synthetic durable token when useful.
|
|
291
|
+
Pass this to enable automatic inter-terminal inbox detection when available.
|
|
292
|
+
session_client: Optional client label such as `claude_code` or `codex`.
|
|
293
|
+
"""
|
|
294
|
+
return handle_startup(
|
|
295
|
+
task,
|
|
296
|
+
claude_session_id=claude_session_id,
|
|
297
|
+
session_token=session_token,
|
|
298
|
+
session_client=session_client,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@mcp.tool
|
|
303
|
+
def nexo_heartbeat(sid: str, task: str, context_hint: str = '') -> str:
|
|
304
|
+
"""Update session task, check inbox and pending questions. Auto-detects trust events.
|
|
305
|
+
|
|
306
|
+
Call this at the START of every user interaction (before doing work).
|
|
307
|
+
Args:
|
|
308
|
+
sid: Your session ID from nexo_startup.
|
|
309
|
+
task: Brief description of current work (5-10 words).
|
|
310
|
+
context_hint: Last 2-3 sentences from the user or current topic. Used for sentiment detection, trust auto-scoring, and mid-session RAG. ALWAYS provide this for best results.
|
|
311
|
+
"""
|
|
312
|
+
return handle_heartbeat(sid, task, context_hint)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@mcp.tool
|
|
316
|
+
def nexo_stop(sid: str) -> str:
|
|
317
|
+
"""Cleanly close a session. Removes it from active sessions immediately.
|
|
318
|
+
|
|
319
|
+
Call this when ending a conversation to avoid ghost sessions.
|
|
320
|
+
Args:
|
|
321
|
+
sid: Session ID to close."""
|
|
322
|
+
from tools_sessions import handle_stop
|
|
323
|
+
return handle_stop(sid)
|
|
324
|
+
|
|
325
|
+
@mcp.tool
|
|
326
|
+
def nexo_status(keyword: str = "") -> str:
|
|
327
|
+
"""List active sessions. Filter by keyword if provided."""
|
|
328
|
+
return handle_status(keyword if keyword else None)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@mcp.tool
|
|
332
|
+
def nexo_context_packet(area: str, files: str = "") -> str:
|
|
333
|
+
"""Build a context packet for subagent injection. Returns learnings + changes + followups + preferences + cognitive memories for a specific area.
|
|
334
|
+
|
|
335
|
+
MUST call before delegating ANY task to a subagent. Inject the result into the subagent's prompt.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
area: Project/area name (e.g., 'ecommerce', 'shopify', 'backend', 'mobile-app', 'nexo', 'infrastructure').
|
|
339
|
+
files: Optional comma-separated file paths for additional context.
|
|
340
|
+
"""
|
|
341
|
+
return handle_context_packet(area, files)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
@mcp.tool
|
|
345
|
+
def nexo_recent_context_capture(
|
|
346
|
+
title: str,
|
|
347
|
+
summary: str = "",
|
|
348
|
+
details: str = "",
|
|
349
|
+
topic: str = "",
|
|
350
|
+
context_key: str = "",
|
|
351
|
+
state: str = "active",
|
|
352
|
+
owner: str = "",
|
|
353
|
+
source_type: str = "",
|
|
354
|
+
source_id: str = "",
|
|
355
|
+
session_id: str = "",
|
|
356
|
+
actor: str = "nexo",
|
|
357
|
+
ttl_hours: int = 24,
|
|
358
|
+
metadata: str = "",
|
|
359
|
+
) -> str:
|
|
360
|
+
"""Capture/update a recent 24h context item and append an event.
|
|
361
|
+
|
|
362
|
+
Use this for important ongoing threads that should stay mentally fresh across sessions/clients.
|
|
363
|
+
"""
|
|
364
|
+
return handle_recent_context_capture(
|
|
365
|
+
title, summary, details, topic, context_key, state, owner,
|
|
366
|
+
source_type, source_id, session_id, actor, ttl_hours, metadata,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
@mcp.tool
|
|
371
|
+
def nexo_recent_context(query: str = "", context_key: str = "", hours: int = 24, limit: int = 8) -> str:
|
|
372
|
+
"""Read recent hot context and continuity events from the last N hours."""
|
|
373
|
+
return handle_recent_context(query, context_key, hours, limit)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
@mcp.tool
|
|
377
|
+
def nexo_pre_action_context(query: str = "", context_key: str = "", session_id: str = "", hours: int = 24, limit: int = 8) -> str:
|
|
378
|
+
"""Build the 24h recent-context bundle that should be reviewed before acting.
|
|
379
|
+
|
|
380
|
+
Especially useful for emails, orchestrators, and any work where the same topic may reappear hours later.
|
|
381
|
+
"""
|
|
382
|
+
return handle_pre_action_context(query, context_key, session_id, hours, limit)
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
@mcp.tool
|
|
386
|
+
def nexo_recent_context_resolve(
|
|
387
|
+
context_key: str = "",
|
|
388
|
+
topic: str = "",
|
|
389
|
+
resolution: str = "",
|
|
390
|
+
actor: str = "nexo",
|
|
391
|
+
session_id: str = "",
|
|
392
|
+
source_type: str = "",
|
|
393
|
+
source_id: str = "",
|
|
394
|
+
ttl_hours: int = 24,
|
|
395
|
+
) -> str:
|
|
396
|
+
"""Resolve a recent hot-context item and append a resolution event."""
|
|
397
|
+
return handle_recent_context_resolve(
|
|
398
|
+
context_key, topic, resolution, actor, session_id, source_type, source_id, ttl_hours
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
@mcp.tool
|
|
403
|
+
def nexo_hot_context_list(hours: int = 24, limit: int = 10, state: str = "") -> str:
|
|
404
|
+
"""List hot-context items currently alive in the recent continuity window."""
|
|
405
|
+
return handle_hot_context_list(hours, limit, state)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
@mcp.tool
|
|
409
|
+
def nexo_transcript_recent(hours: int = 24, client: str = "", limit: int = 10) -> str:
|
|
410
|
+
"""List recent Claude Code / Codex transcripts visible to NEXO."""
|
|
411
|
+
return handle_transcript_recent(hours, client, limit)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
@mcp.tool
|
|
415
|
+
def nexo_transcript_search(query: str = "", hours: int = 24, client: str = "", limit: int = 10) -> str:
|
|
416
|
+
"""Search recent transcripts directly when recall/hot-context are not enough."""
|
|
417
|
+
return handle_transcript_search(query, hours, client, limit)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
@mcp.tool
|
|
421
|
+
def nexo_transcript_read(session_ref: str = "", transcript_path: str = "", client: str = "", max_messages: int = 80) -> str:
|
|
422
|
+
"""Read a full transcript fallback by session id, transcript display name, session_uid, or exact path."""
|
|
423
|
+
return handle_transcript_read(session_ref, transcript_path, client, max_messages)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
@mcp.tool
|
|
427
|
+
def nexo_system_catalog(section: str = "", query: str = "", limit: int = 20) -> str:
|
|
428
|
+
"""Read NEXO's live system catalog built from core tools, plugins, skills, scripts, crons, projects, and artifacts."""
|
|
429
|
+
return handle_system_catalog(section, query, limit)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
@mcp.tool
|
|
433
|
+
def nexo_tool_explain(name: str) -> str:
|
|
434
|
+
"""Explain a live NEXO tool/capability from the generated system catalog."""
|
|
435
|
+
return handle_tool_explain(name)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@mcp.tool
|
|
439
|
+
def nexo_smart_startup() -> str:
|
|
440
|
+
"""Pre-load relevant cognitive memories based on pending followups, due reminders, and last session topics.
|
|
441
|
+
|
|
442
|
+
Call during startup (after nexo_startup) to ensure the session starts with the right context loaded.
|
|
443
|
+
Returns up to 10 memories matching the current operational state.
|
|
444
|
+
"""
|
|
445
|
+
return handle_smart_startup_query()
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
@mcp.tool
|
|
449
|
+
def nexo_session_portable_context(sid: str = "") -> str:
|
|
450
|
+
"""Build a portable handoff packet for another client/runtime.
|
|
451
|
+
|
|
452
|
+
Use this when another client should continue the same work with explicit
|
|
453
|
+
task/checkpoint/goal/workflow context instead of relying on memory alone.
|
|
454
|
+
"""
|
|
455
|
+
return handle_session_portable_context(sid)
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
@mcp.tool
|
|
459
|
+
def nexo_session_export_bundle(sid: str = "", path: str = "") -> str:
|
|
460
|
+
"""Export a machine-readable session bundle for cross-client handoff or archival."""
|
|
461
|
+
return handle_session_export_bundle(sid, path)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
# ── Session Checkpoints (auto-compaction continuity) ──────────────
|
|
465
|
+
|
|
466
|
+
@mcp.tool
|
|
467
|
+
def nexo_checkpoint_save(
|
|
468
|
+
sid: str,
|
|
469
|
+
task: str = '',
|
|
470
|
+
task_status: str = 'active',
|
|
471
|
+
active_files: str = '[]',
|
|
472
|
+
current_goal: str = '',
|
|
473
|
+
decisions_summary: str = '',
|
|
474
|
+
errors_found: str = '',
|
|
475
|
+
reasoning_thread: str = '',
|
|
476
|
+
next_step: str = ''
|
|
477
|
+
) -> str:
|
|
478
|
+
"""Save a session checkpoint for auto-compaction continuity.
|
|
479
|
+
|
|
480
|
+
Call this BEFORE context compaction to preserve session state.
|
|
481
|
+
The PostCompact hook reads this checkpoint and re-injects it as a
|
|
482
|
+
Core Memory Block, so the session continues seamlessly.
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
sid: Session ID.
|
|
486
|
+
task: Current task description.
|
|
487
|
+
task_status: One of 'active', 'investigating', 'fixing', 'deploying', 'blocked'.
|
|
488
|
+
active_files: JSON array of file paths currently being worked on.
|
|
489
|
+
current_goal: What you're trying to achieve right now (1-2 sentences).
|
|
490
|
+
decisions_summary: Recent decisions with brief reasoning (2-3 lines).
|
|
491
|
+
errors_found: Errors encountered and their status (resolved/open).
|
|
492
|
+
reasoning_thread: Your current chain of thought (1-2 sentences).
|
|
493
|
+
next_step: The concrete next action to take.
|
|
494
|
+
"""
|
|
495
|
+
from db import save_checkpoint
|
|
496
|
+
result = save_checkpoint(
|
|
497
|
+
sid=sid, task=task, task_status=task_status,
|
|
498
|
+
active_files=active_files, current_goal=current_goal,
|
|
499
|
+
decisions_summary=decisions_summary, errors_found=errors_found,
|
|
500
|
+
reasoning_thread=reasoning_thread, next_step=next_step,
|
|
501
|
+
)
|
|
502
|
+
return f"Checkpoint saved for {sid}. Compaction #{result['compaction_count']}. PostCompact will re-inject this as Core Memory Block."
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
@mcp.tool
|
|
506
|
+
def nexo_checkpoint_read(sid: str = '') -> str:
|
|
507
|
+
"""Read the latest session checkpoint. Used by PostCompact hook and for manual recovery.
|
|
508
|
+
|
|
509
|
+
Args:
|
|
510
|
+
sid: Session ID. If empty, returns the most recent checkpoint from any session.
|
|
511
|
+
"""
|
|
512
|
+
from db import read_checkpoint
|
|
513
|
+
cp = read_checkpoint(sid)
|
|
514
|
+
if not cp:
|
|
515
|
+
return "No checkpoint found."
|
|
516
|
+
|
|
517
|
+
lines = [f"CHECKPOINT for {cp['sid']} (compaction #{cp['compaction_count']})"]
|
|
518
|
+
lines.append(f"Task: {cp['task']} ({cp['task_status']})")
|
|
519
|
+
if cp.get('current_goal'):
|
|
520
|
+
lines.append(f"Goal: {cp['current_goal']}")
|
|
521
|
+
if cp.get('active_files') and cp['active_files'] != '[]':
|
|
522
|
+
lines.append(f"Files: {cp['active_files']}")
|
|
523
|
+
if cp.get('decisions_summary'):
|
|
524
|
+
lines.append(f"Decisions: {cp['decisions_summary']}")
|
|
525
|
+
if cp.get('errors_found'):
|
|
526
|
+
lines.append(f"Errors: {cp['errors_found']}")
|
|
527
|
+
if cp.get('reasoning_thread'):
|
|
528
|
+
lines.append(f"Context: {cp['reasoning_thread']}")
|
|
529
|
+
if cp.get('next_step'):
|
|
530
|
+
lines.append(f"Next: {cp['next_step']}")
|
|
531
|
+
lines.append(f"Updated: {cp['updated_at']}")
|
|
532
|
+
return "\n".join(lines)
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
# ── File coordination (3 tools) ───────────────────────────────────
|
|
536
|
+
|
|
537
|
+
@mcp.tool
|
|
538
|
+
def nexo_track(sid: str, paths: list[str]) -> str:
|
|
539
|
+
"""Track files being edited. Detects conflicts with other sessions.
|
|
540
|
+
|
|
541
|
+
MUST call before editing any shared file.
|
|
542
|
+
Args:
|
|
543
|
+
sid: Your session ID.
|
|
544
|
+
paths: List of absolute file paths to track.
|
|
545
|
+
"""
|
|
546
|
+
return handle_track(sid, paths)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
@mcp.tool
|
|
550
|
+
def nexo_untrack(sid: str, paths: list[str] | None = None) -> str:
|
|
551
|
+
"""Stop tracking files. If no paths given, releases all.
|
|
552
|
+
|
|
553
|
+
Args:
|
|
554
|
+
sid: Your session ID.
|
|
555
|
+
paths: File paths to release. Omit to release all.
|
|
556
|
+
"""
|
|
557
|
+
return handle_untrack(sid, paths)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
@mcp.tool
|
|
561
|
+
def nexo_files() -> str:
|
|
562
|
+
"""Show all tracked files across all active sessions with conflict detection."""
|
|
563
|
+
return handle_files()
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
# ── Messaging (4 tools) ───────────────────────────────────────────
|
|
567
|
+
|
|
568
|
+
@mcp.tool
|
|
569
|
+
def nexo_send(from_sid: str, to_sid: str, text: str) -> str:
|
|
570
|
+
"""Send a fire-and-forget message to another session or broadcast.
|
|
571
|
+
|
|
572
|
+
Args:
|
|
573
|
+
from_sid: Your session ID.
|
|
574
|
+
to_sid: Target session ID, or 'all' for broadcast.
|
|
575
|
+
text: Message content.
|
|
576
|
+
"""
|
|
577
|
+
return handle_send(from_sid, to_sid, text)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
@mcp.tool
|
|
581
|
+
def nexo_ask(from_sid: str, to_sid: str, question: str) -> str:
|
|
582
|
+
"""Ask a question to another session (they see it on next heartbeat).
|
|
583
|
+
|
|
584
|
+
Args:
|
|
585
|
+
from_sid: Your session ID.
|
|
586
|
+
to_sid: Target session ID.
|
|
587
|
+
question: The question text.
|
|
588
|
+
Returns: Question ID (qid) for checking the answer later.
|
|
589
|
+
"""
|
|
590
|
+
return handle_ask(from_sid, to_sid, question)
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
@mcp.tool
|
|
594
|
+
def nexo_answer(qid: str, answer: str) -> str:
|
|
595
|
+
"""Answer a pending question from another session.
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
qid: The question ID shown in heartbeat output.
|
|
599
|
+
answer: Your response.
|
|
600
|
+
"""
|
|
601
|
+
return handle_answer(qid, answer)
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
@mcp.tool
|
|
605
|
+
def nexo_check_answer(qid: str) -> str:
|
|
606
|
+
"""Check if a question has been answered.
|
|
607
|
+
|
|
608
|
+
Args:
|
|
609
|
+
qid: The question ID from nexo_ask.
|
|
610
|
+
"""
|
|
611
|
+
return handle_check_answer(qid)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
# ── Operations: Reminders + Menu (2 tools, read-only) ─────────────
|
|
615
|
+
|
|
616
|
+
@mcp.tool
|
|
617
|
+
def nexo_reminders(filter: str = "due") -> str:
|
|
618
|
+
"""Check reminders and followups.
|
|
619
|
+
|
|
620
|
+
Args:
|
|
621
|
+
filter: 'due', 'all', 'followups', 'completed', 'deleted', 'history', or 'any'
|
|
622
|
+
"""
|
|
623
|
+
return handle_reminders(filter)
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
@mcp.tool
|
|
627
|
+
def nexo_menu() -> str:
|
|
628
|
+
"""Generate the NEXO operations center menu with alerts and active sessions.
|
|
629
|
+
|
|
630
|
+
Shows: date, due alerts, all menu items by category, active sessions.
|
|
631
|
+
Uses box-drawing characters for formatting.
|
|
632
|
+
"""
|
|
633
|
+
return handle_menu()
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
# ── Reminders CRUD (7 tools) ──────────────────────────────────────
|
|
637
|
+
|
|
638
|
+
@mcp.tool
|
|
639
|
+
def nexo_reminder_create(id: str, description: str, date: str = "", category: str = "general") -> str:
|
|
640
|
+
"""Create a new reminder for the user.
|
|
641
|
+
|
|
642
|
+
Args:
|
|
643
|
+
id: Unique ID starting with 'R' (e.g., R90).
|
|
644
|
+
description: What needs to be done.
|
|
645
|
+
date: Target date YYYY-MM-DD (optional).
|
|
646
|
+
category: One of: decisions, tasks, waiting, ideas, general.
|
|
647
|
+
"""
|
|
648
|
+
return handle_reminder_create(id, description, date, category)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
@mcp.tool
|
|
652
|
+
def nexo_reminder_get(id: str) -> str:
|
|
653
|
+
"""Read a reminder with its history and usage rules.
|
|
654
|
+
|
|
655
|
+
IMPORTANT: before update/delete/restore/note, call this tool first and use the returned READ_TOKEN.
|
|
656
|
+
"""
|
|
657
|
+
return handle_reminder_get(id)
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
@mcp.tool
|
|
661
|
+
def nexo_reminder_update(
|
|
662
|
+
id: str,
|
|
663
|
+
description: str = "",
|
|
664
|
+
date: str = "",
|
|
665
|
+
status: str = "",
|
|
666
|
+
category: str = "",
|
|
667
|
+
read_token: str = "",
|
|
668
|
+
) -> str:
|
|
669
|
+
"""Update fields of an existing reminder. Only non-empty fields are changed.
|
|
670
|
+
|
|
671
|
+
IMPORTANT: call `nexo_reminder_get` first and pass its READ_TOKEN.
|
|
672
|
+
|
|
673
|
+
Args:
|
|
674
|
+
id: Reminder ID (e.g., R87).
|
|
675
|
+
description: New description (optional).
|
|
676
|
+
date: New date YYYY-MM-DD (optional).
|
|
677
|
+
status: New status (optional).
|
|
678
|
+
category: New category (optional).
|
|
679
|
+
read_token: Token returned by `nexo_reminder_get`.
|
|
680
|
+
"""
|
|
681
|
+
return handle_reminder_update(id, description, date, status, category, read_token)
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
@mcp.tool
|
|
685
|
+
def nexo_reminder_complete(id: str) -> str:
|
|
686
|
+
"""Mark a reminder as completed with today's date.
|
|
687
|
+
|
|
688
|
+
Args:
|
|
689
|
+
id: Reminder ID (e.g., R87).
|
|
690
|
+
"""
|
|
691
|
+
return handle_reminder_complete(id)
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
@mcp.tool
|
|
695
|
+
def nexo_reminder_note(id: str, note: str, read_token: str = "", actor: str = "nexo") -> str:
|
|
696
|
+
"""Append a note to reminder history.
|
|
697
|
+
|
|
698
|
+
IMPORTANT: call `nexo_reminder_get` first and pass its READ_TOKEN.
|
|
699
|
+
|
|
700
|
+
Args:
|
|
701
|
+
id: Reminder ID (e.g., R87).
|
|
702
|
+
note: Operational note to append to history.
|
|
703
|
+
read_token: Token returned by `nexo_reminder_get`.
|
|
704
|
+
actor: Actor label for the history note.
|
|
705
|
+
"""
|
|
706
|
+
return handle_reminder_note(id, note, read_token, actor)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
@mcp.tool
|
|
710
|
+
def nexo_reminder_restore(id: str, read_token: str = "") -> str:
|
|
711
|
+
"""Restore a soft-deleted reminder back to PENDING.
|
|
712
|
+
|
|
713
|
+
IMPORTANT: call `nexo_reminder_get` first and pass its READ_TOKEN.
|
|
714
|
+
|
|
715
|
+
Args:
|
|
716
|
+
id: Reminder ID (e.g., R87).
|
|
717
|
+
read_token: Token returned by `nexo_reminder_get`.
|
|
718
|
+
"""
|
|
719
|
+
return handle_reminder_restore(id, read_token)
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
@mcp.tool
|
|
723
|
+
def nexo_reminder_delete(id: str, read_token: str = "") -> str:
|
|
724
|
+
"""Soft-delete a reminder.
|
|
725
|
+
|
|
726
|
+
IMPORTANT: call `nexo_reminder_get` first and pass its READ_TOKEN.
|
|
727
|
+
|
|
728
|
+
Args:
|
|
729
|
+
id: Reminder ID (e.g., R87).
|
|
730
|
+
read_token: Token returned by `nexo_reminder_get`.
|
|
731
|
+
"""
|
|
732
|
+
return handle_reminder_delete(id, read_token)
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
# ── Followups CRUD (7 tools) ──────────────────────────────────────
|
|
736
|
+
|
|
737
|
+
@mcp.tool
|
|
738
|
+
def nexo_followup_create(id: str, description: str, date: str = "", verification: str = "", reasoning: str = "", recurrence: str = "", priority: str = "medium") -> str:
|
|
739
|
+
"""Create a new NEXO followup (autonomous task).
|
|
740
|
+
|
|
741
|
+
Args:
|
|
742
|
+
id: Unique ID starting with 'NF' (e.g., NF-MCP2).
|
|
743
|
+
description: What to verify/do.
|
|
744
|
+
date: Target date YYYY-MM-DD (optional).
|
|
745
|
+
verification: How to verify completion (optional).
|
|
746
|
+
reasoning: WHY this followup exists — what decision/context led to it (optional).
|
|
747
|
+
recurrence: Auto-regenerate pattern (optional). Formats: 'weekly:monday', 'monthly:1', 'monthly:15', 'quarterly'.
|
|
748
|
+
When completed, a new followup is auto-created with the next date. The completed one is archived with date suffix.
|
|
749
|
+
priority: critical, high, medium, low (default: medium).
|
|
750
|
+
"""
|
|
751
|
+
return handle_followup_create(id, description, date, verification, reasoning, recurrence, priority)
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
@mcp.tool
|
|
755
|
+
def nexo_followup_get(id: str) -> str:
|
|
756
|
+
"""Read a followup with its history and usage rules.
|
|
757
|
+
|
|
758
|
+
IMPORTANT: before update/delete/restore/note, call this tool first and use the returned READ_TOKEN.
|
|
759
|
+
"""
|
|
760
|
+
return handle_followup_get(id)
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
@mcp.tool
|
|
764
|
+
def nexo_followup_update(
|
|
765
|
+
id: str,
|
|
766
|
+
description: str = "",
|
|
767
|
+
date: str = "",
|
|
768
|
+
verification: str = "",
|
|
769
|
+
status: str = "",
|
|
770
|
+
priority: str = "",
|
|
771
|
+
read_token: str = "",
|
|
772
|
+
) -> str:
|
|
773
|
+
"""Update fields of an existing followup. Only non-empty fields are changed.
|
|
774
|
+
|
|
775
|
+
IMPORTANT: call `nexo_followup_get` first and pass its READ_TOKEN.
|
|
776
|
+
|
|
777
|
+
Args:
|
|
778
|
+
id: Followup ID (e.g., NF45).
|
|
779
|
+
description: New description (optional).
|
|
780
|
+
date: New date YYYY-MM-DD (optional).
|
|
781
|
+
verification: New verification text (optional).
|
|
782
|
+
status: New status (optional).
|
|
783
|
+
priority: critical, high, medium, low (optional).
|
|
784
|
+
read_token: Token returned by `nexo_followup_get`.
|
|
785
|
+
"""
|
|
786
|
+
return handle_followup_update(id, description, date, verification, status, priority, read_token)
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
@mcp.tool
|
|
790
|
+
def nexo_followup_complete(id: str, result: str = "") -> str:
|
|
791
|
+
"""Mark a followup as completed. Appends result to verification field.
|
|
792
|
+
|
|
793
|
+
Args:
|
|
794
|
+
id: Followup ID (e.g., NF45).
|
|
795
|
+
result: What was found/done (optional).
|
|
796
|
+
"""
|
|
797
|
+
return handle_followup_complete(id, result)
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
@mcp.tool
|
|
801
|
+
def nexo_followup_note(id: str, note: str, read_token: str = "", actor: str = "nexo") -> str:
|
|
802
|
+
"""Append a note to followup history.
|
|
803
|
+
|
|
804
|
+
IMPORTANT: call `nexo_followup_get` first and pass its READ_TOKEN.
|
|
805
|
+
|
|
806
|
+
Args:
|
|
807
|
+
id: Followup ID (e.g., NF45).
|
|
808
|
+
note: Operational note to append to history.
|
|
809
|
+
read_token: Token returned by `nexo_followup_get`.
|
|
810
|
+
actor: Actor label for the history note.
|
|
811
|
+
"""
|
|
812
|
+
return handle_followup_note(id, note, read_token, actor)
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
@mcp.tool
|
|
816
|
+
def nexo_followup_restore(id: str, read_token: str = "") -> str:
|
|
817
|
+
"""Restore a soft-deleted followup back to PENDING.
|
|
818
|
+
|
|
819
|
+
IMPORTANT: call `nexo_followup_get` first and pass its READ_TOKEN.
|
|
820
|
+
|
|
821
|
+
Args:
|
|
822
|
+
id: Followup ID (e.g., NF45).
|
|
823
|
+
read_token: Token returned by `nexo_followup_get`.
|
|
824
|
+
"""
|
|
825
|
+
return handle_followup_restore(id, read_token)
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
@mcp.tool
|
|
829
|
+
def nexo_followup_delete(id: str, read_token: str = "") -> str:
|
|
830
|
+
"""Soft-delete a followup.
|
|
831
|
+
|
|
832
|
+
IMPORTANT: call `nexo_followup_get` first and pass its READ_TOKEN.
|
|
833
|
+
|
|
834
|
+
Args:
|
|
835
|
+
id: Followup ID (e.g., NF45).
|
|
836
|
+
read_token: Token returned by `nexo_followup_get`.
|
|
837
|
+
"""
|
|
838
|
+
return handle_followup_delete(id, read_token)
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
# ── Learnings CRUD (5 tools) ──────────────────────────────────────
|
|
842
|
+
|
|
843
|
+
@mcp.tool
|
|
844
|
+
def nexo_learning_add(
|
|
845
|
+
category: str,
|
|
846
|
+
title: str,
|
|
847
|
+
content: str,
|
|
848
|
+
reasoning: str = "",
|
|
849
|
+
prevention: str = "",
|
|
850
|
+
applies_to: str = "",
|
|
851
|
+
review_days: int = 30,
|
|
852
|
+
priority: str = "medium",
|
|
853
|
+
supersedes_id: int = 0,
|
|
854
|
+
) -> str:
|
|
855
|
+
"""Add a new learning (resolved error, pattern, gotcha).
|
|
856
|
+
|
|
857
|
+
Args:
|
|
858
|
+
category: Free-form category name (e.g., 'backend', 'frontend', 'devops', 'infrastructure', 'security'). Use consistent names across learnings.
|
|
859
|
+
title: Short title for the learning.
|
|
860
|
+
content: Full description with context and solution.
|
|
861
|
+
reasoning: WHY this matters — what led to discovering this (optional).
|
|
862
|
+
prevention: Concrete rule/check that prevents repeating this mistake (optional).
|
|
863
|
+
applies_to: Files, systems, or areas this learning applies to (optional).
|
|
864
|
+
review_days: Days until this learning should be reviewed again (default 30).
|
|
865
|
+
priority: critical, high, medium, low (default: medium). Critical/high never decay below floor.
|
|
866
|
+
supersedes_id: Existing learning ID this new canonical rule replaces (optional).
|
|
867
|
+
"""
|
|
868
|
+
return handle_learning_add(
|
|
869
|
+
category, title, content, reasoning,
|
|
870
|
+
prevention=prevention, applies_to=applies_to,
|
|
871
|
+
review_days=review_days, priority=priority, supersedes_id=supersedes_id,
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
@mcp.tool
|
|
876
|
+
def nexo_learning_search(query: str, category: str = "") -> str:
|
|
877
|
+
"""Search learnings by keyword. Searches title and content.
|
|
878
|
+
|
|
879
|
+
Args:
|
|
880
|
+
query: Search term.
|
|
881
|
+
category: Filter by category (optional).
|
|
882
|
+
"""
|
|
883
|
+
return handle_learning_search(query, category)
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
@mcp.tool
|
|
887
|
+
def nexo_learning_apply_retroactively(
|
|
888
|
+
learning_id: int,
|
|
889
|
+
lookback_days: int = 14,
|
|
890
|
+
max_matches: int = 5,
|
|
891
|
+
min_score: float = 0.4,
|
|
892
|
+
dry_run: bool = False,
|
|
893
|
+
) -> str:
|
|
894
|
+
"""Scan recent decisions and surface those that conflict with a learning's prevention rule.
|
|
895
|
+
|
|
896
|
+
Closes Fase 2 item 3 of NEXO-AUDIT-2026-04-11. Use this when you add a new
|
|
897
|
+
rule and want to retroactively check whether past decisions still hold.
|
|
898
|
+
Creates deterministic NF-RETRO-L<learning>-D<decision> followups so the
|
|
899
|
+
helper is idempotent across reruns. nexo_learning_add invokes this
|
|
900
|
+
automatically when the new learning has a `prevention` field — call this
|
|
901
|
+
tool manually only when you want to re-scan with a longer window or a
|
|
902
|
+
different threshold.
|
|
903
|
+
|
|
904
|
+
Args:
|
|
905
|
+
learning_id: ID of the learning to apply.
|
|
906
|
+
lookback_days: How many days back to scan decisions (default 14).
|
|
907
|
+
max_matches: Cap on followups created per call (default 5).
|
|
908
|
+
min_score: Match threshold in [0.0, 1.0] (default 0.4).
|
|
909
|
+
dry_run: If True, scores matches but does not create followups.
|
|
910
|
+
"""
|
|
911
|
+
import json as _json
|
|
912
|
+
from retroactive_learnings import apply_learning_retroactively
|
|
913
|
+
|
|
914
|
+
result = apply_learning_retroactively(
|
|
915
|
+
int(learning_id),
|
|
916
|
+
lookback_days=int(lookback_days),
|
|
917
|
+
max_matches=int(max_matches),
|
|
918
|
+
min_score=float(min_score),
|
|
919
|
+
dry_run=bool(dry_run),
|
|
920
|
+
)
|
|
921
|
+
return _json.dumps(result, ensure_ascii=False, indent=2)
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
@mcp.tool
|
|
925
|
+
def nexo_hook_runs(
|
|
926
|
+
hours: int = 24,
|
|
927
|
+
hook_name: str = "",
|
|
928
|
+
status: str = "",
|
|
929
|
+
limit: int = 50,
|
|
930
|
+
summary_only: bool = False,
|
|
931
|
+
) -> str:
|
|
932
|
+
"""List recent hook lifecycle runs and per-hook health summary.
|
|
933
|
+
|
|
934
|
+
Closes Fase 3 item 7 of NEXO-AUDIT-2026-04-11. Each NEXO hook
|
|
935
|
+
(session-start, post-compact, pre-compact, inbox-hook, etc.) writes
|
|
936
|
+
a row to hook_runs when it finishes via scripts/nexo-hook-record.py.
|
|
937
|
+
This tool reads them back so the agent can answer "is the hook
|
|
938
|
+
pipeline healthy?" without needing the dashboard or grepping log files.
|
|
939
|
+
|
|
940
|
+
Args:
|
|
941
|
+
hours: How far back to look (default 24).
|
|
942
|
+
hook_name: Optional substring filter (LIKE %name%).
|
|
943
|
+
status: Optional exact status filter (ok|error|skipped|timeout|blocked).
|
|
944
|
+
limit: Max raw rows to return when summary_only=False (default 50).
|
|
945
|
+
summary_only: If True, return only the per-hook health summary
|
|
946
|
+
(success rate, p50/p95 duration, unhealthy hooks)
|
|
947
|
+
and skip the raw row list.
|
|
948
|
+
"""
|
|
949
|
+
import json as _json
|
|
950
|
+
from hook_observability import list_recent_hook_runs, hook_health_summary
|
|
951
|
+
|
|
952
|
+
summary = hook_health_summary(hours=int(hours))
|
|
953
|
+
if summary_only:
|
|
954
|
+
return _json.dumps(summary, ensure_ascii=False, indent=2)
|
|
955
|
+
|
|
956
|
+
runs = list_recent_hook_runs(
|
|
957
|
+
hours=int(hours),
|
|
958
|
+
hook_name=hook_name,
|
|
959
|
+
status=status,
|
|
960
|
+
limit=int(limit),
|
|
961
|
+
)
|
|
962
|
+
return _json.dumps({"summary": summary, "runs": runs}, ensure_ascii=False, indent=2)
|
|
963
|
+
|
|
964
|
+
|
|
965
|
+
@mcp.tool
|
|
966
|
+
def nexo_learning_update(
|
|
967
|
+
id: int,
|
|
968
|
+
title: str = "",
|
|
969
|
+
content: str = "",
|
|
970
|
+
category: str = "",
|
|
971
|
+
reasoning: str = "",
|
|
972
|
+
prevention: str = "",
|
|
973
|
+
applies_to: str = "",
|
|
974
|
+
status: str = "",
|
|
975
|
+
review_days: int = 0,
|
|
976
|
+
priority: str = "",
|
|
977
|
+
supersedes_id: int = 0,
|
|
978
|
+
) -> str:
|
|
979
|
+
"""Update a learning entry. Only non-empty fields are changed.
|
|
980
|
+
|
|
981
|
+
Args:
|
|
982
|
+
id: Learning ID number.
|
|
983
|
+
title: New title (optional).
|
|
984
|
+
content: New content (optional).
|
|
985
|
+
category: New category (optional).
|
|
986
|
+
reasoning: New reasoning/context (optional).
|
|
987
|
+
prevention: New prevention rule (optional).
|
|
988
|
+
applies_to: New applies_to target(s) (optional).
|
|
989
|
+
status: New status such as active/superseded (optional).
|
|
990
|
+
review_days: New review interval in days (optional).
|
|
991
|
+
priority: critical, high, medium, low (optional).
|
|
992
|
+
supersedes_id: Existing learning ID this updated canonical rule replaces (optional).
|
|
993
|
+
"""
|
|
994
|
+
return handle_learning_update(
|
|
995
|
+
id, title, content, category,
|
|
996
|
+
reasoning=reasoning, prevention=prevention, applies_to=applies_to,
|
|
997
|
+
status=status, review_days=review_days, priority=priority,
|
|
998
|
+
supersedes_id=supersedes_id,
|
|
999
|
+
)
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
@mcp.tool
|
|
1003
|
+
def nexo_learning_delete(id: int) -> str:
|
|
1004
|
+
"""Delete a learning entry.
|
|
1005
|
+
|
|
1006
|
+
Args:
|
|
1007
|
+
id: Learning ID number.
|
|
1008
|
+
"""
|
|
1009
|
+
return handle_learning_delete(id)
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
@mcp.tool
|
|
1013
|
+
def nexo_learning_list(category: str = "") -> str:
|
|
1014
|
+
"""List all learnings, grouped by category.
|
|
1015
|
+
|
|
1016
|
+
Args:
|
|
1017
|
+
category: Filter by category (optional). If empty, shows all grouped.
|
|
1018
|
+
"""
|
|
1019
|
+
return handle_learning_list(category)
|
|
1020
|
+
|
|
1021
|
+
|
|
1022
|
+
@mcp.tool
|
|
1023
|
+
def nexo_learning_quality(id: int = 0, category: str = "", status: str = "active", limit: int = 20) -> str:
|
|
1024
|
+
"""Score learning quality so fragile rules can be strengthened before they mislead guard or retrieval.
|
|
1025
|
+
|
|
1026
|
+
Args:
|
|
1027
|
+
id: Specific learning ID to inspect (optional).
|
|
1028
|
+
category: Filter by category (optional).
|
|
1029
|
+
status: Filter by lifecycle status such as active/superseded (default active).
|
|
1030
|
+
limit: Max learnings to score when listing (default 20).
|
|
1031
|
+
"""
|
|
1032
|
+
return handle_learning_quality(id=id, category=category, status=status, limit=limit)
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
# ── Search index ──────────────────────────────────────────────────
|
|
1036
|
+
|
|
1037
|
+
@mcp.tool
|
|
1038
|
+
def nexo_reindex() -> str:
|
|
1039
|
+
"""Force full rebuild of the FTS5 search index. Use after bulk changes or if search seems stale."""
|
|
1040
|
+
conn = get_db()
|
|
1041
|
+
rebuild_fts_index(conn)
|
|
1042
|
+
count = conn.execute("SELECT COUNT(*) FROM unified_search").fetchone()[0]
|
|
1043
|
+
sources = conn.execute("SELECT source, COUNT(*) as cnt FROM unified_search GROUP BY source ORDER BY cnt DESC").fetchall()
|
|
1044
|
+
lines = [f"Index rebuilt: {count} documentos"]
|
|
1045
|
+
for s in sources:
|
|
1046
|
+
lines.append(f" {s[0]:12s} → {s[1]}")
|
|
1047
|
+
return "\n".join(lines)
|
|
1048
|
+
|
|
1049
|
+
|
|
1050
|
+
@mcp.tool
|
|
1051
|
+
def nexo_index_add_dir(path: str, dir_type: str = "code",
|
|
1052
|
+
patterns: str = "*.php,*.js,*.json,*.py,*.ts,*.tsx",
|
|
1053
|
+
notes: str = "") -> str:
|
|
1054
|
+
"""Register a new directory for FTS5 search indexing. Survives restarts.
|
|
1055
|
+
|
|
1056
|
+
Args:
|
|
1057
|
+
path: Absolute path to directory (supports ~).
|
|
1058
|
+
dir_type: 'code' for source files, 'md' for markdown docs.
|
|
1059
|
+
patterns: Comma-separated glob patterns (only for code type).
|
|
1060
|
+
notes: Description of what this directory contains.
|
|
1061
|
+
"""
|
|
1062
|
+
result = fts_add_dir(path, dir_type, patterns, notes)
|
|
1063
|
+
if "error" in result:
|
|
1064
|
+
return f"ERROR: {result['error']}"
|
|
1065
|
+
return f"Directory registered: {result['path']} ({result['dir_type']}, patterns: {result['patterns']})\nUse nexo_reindex to index now."
|
|
1066
|
+
|
|
1067
|
+
|
|
1068
|
+
@mcp.tool
|
|
1069
|
+
def nexo_index_remove_dir(path: str) -> str:
|
|
1070
|
+
"""Remove a directory from FTS5 indexing and clean up its entries.
|
|
1071
|
+
|
|
1072
|
+
Args:
|
|
1073
|
+
path: Path to directory to remove.
|
|
1074
|
+
"""
|
|
1075
|
+
result = fts_remove_dir(path)
|
|
1076
|
+
if "error" in result:
|
|
1077
|
+
return f"ERROR: {result['error']}"
|
|
1078
|
+
return f"Directory removed from index: {result['removed']}"
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
@mcp.tool
|
|
1082
|
+
def nexo_index_dirs() -> str:
|
|
1083
|
+
"""List all directories being indexed by FTS5 (builtin + dynamic)."""
|
|
1084
|
+
dirs = fts_list_dirs()
|
|
1085
|
+
if not dirs:
|
|
1086
|
+
return "No directories configured."
|
|
1087
|
+
lines = ["INDEXED DIRECTORIES:"]
|
|
1088
|
+
for d in dirs:
|
|
1089
|
+
source_tag = "⚙️" if d["source"] == "builtin" else "➕"
|
|
1090
|
+
notes = f" — {d['notes']}" if d.get("notes") else ""
|
|
1091
|
+
lines.append(f" {source_tag} [{d['type']}] {d['path']}")
|
|
1092
|
+
lines.append(f" patterns: {d['patterns']}{notes}")
|
|
1093
|
+
return "\n".join(lines)
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
# ── Credentials CRUD (5 tools) ────────────────────────────────────
|
|
1097
|
+
|
|
1098
|
+
@mcp.tool
|
|
1099
|
+
def nexo_credential_get(service: str, key: str = "") -> str:
|
|
1100
|
+
"""Get credential value(s) for a service.
|
|
1101
|
+
|
|
1102
|
+
Args:
|
|
1103
|
+
service: Service name (e.g., google-ads, meta-ads, shopify).
|
|
1104
|
+
key: Specific key (optional). If empty, returns all for the service.
|
|
1105
|
+
"""
|
|
1106
|
+
return handle_credential_get(service, key)
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
@mcp.tool
|
|
1110
|
+
def nexo_credential_create(service: str, key: str, value: str, notes: str = "") -> str:
|
|
1111
|
+
"""Store a new credential.
|
|
1112
|
+
|
|
1113
|
+
Args:
|
|
1114
|
+
service: Service name (e.g., google-ads, cloudflare).
|
|
1115
|
+
key: Key name (e.g., api_key, token, ssh).
|
|
1116
|
+
value: The secret value.
|
|
1117
|
+
notes: Description or context (optional).
|
|
1118
|
+
"""
|
|
1119
|
+
return handle_credential_create(service, key, value, notes)
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
@mcp.tool
|
|
1123
|
+
def nexo_credential_update(service: str, key: str, value: str = "", notes: str = "") -> str:
|
|
1124
|
+
"""Update a credential's value and/or notes.
|
|
1125
|
+
|
|
1126
|
+
Args:
|
|
1127
|
+
service: Service name.
|
|
1128
|
+
key: Key name.
|
|
1129
|
+
value: New value (optional).
|
|
1130
|
+
notes: New notes (optional).
|
|
1131
|
+
"""
|
|
1132
|
+
return handle_credential_update(service, key, value, notes)
|
|
1133
|
+
|
|
1134
|
+
|
|
1135
|
+
@mcp.tool
|
|
1136
|
+
def nexo_credential_delete(service: str, key: str = "") -> str:
|
|
1137
|
+
"""Delete credential(s). If no key, deletes all for the service.
|
|
1138
|
+
|
|
1139
|
+
Args:
|
|
1140
|
+
service: Service name.
|
|
1141
|
+
key: Specific key (optional). If empty, deletes ALL for service.
|
|
1142
|
+
"""
|
|
1143
|
+
return handle_credential_delete(service, key)
|
|
1144
|
+
|
|
1145
|
+
|
|
1146
|
+
@mcp.tool
|
|
1147
|
+
def nexo_credential_list(service: str = "") -> str:
|
|
1148
|
+
"""List credentials (names and notes only, no values).
|
|
1149
|
+
|
|
1150
|
+
Args:
|
|
1151
|
+
service: Filter by service (optional). If empty, shows all.
|
|
1152
|
+
"""
|
|
1153
|
+
return handle_credential_list(service)
|
|
1154
|
+
|
|
1155
|
+
|
|
1156
|
+
# ── Task History (3 tools) ────────────────────────────────────────
|
|
1157
|
+
|
|
1158
|
+
@mcp.tool
|
|
1159
|
+
def nexo_task_log(task_num: str, task_name: str, notes: str = "", reasoning: str = "") -> str:
|
|
1160
|
+
"""Record that an operational task was executed.
|
|
1161
|
+
|
|
1162
|
+
Args:
|
|
1163
|
+
task_num: Task number from the checklist (e.g., '7', '7b').
|
|
1164
|
+
task_name: Task name (e.g., 'Google Ads').
|
|
1165
|
+
notes: Execution summary (optional).
|
|
1166
|
+
reasoning: WHY this task was executed now — what triggered it (optional).
|
|
1167
|
+
"""
|
|
1168
|
+
return handle_task_log(task_num, task_name, notes, reasoning)
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
@mcp.tool
|
|
1172
|
+
def nexo_task_list(task_num: str = "", days: int = 30) -> str:
|
|
1173
|
+
"""Show execution history for operational tasks.
|
|
1174
|
+
|
|
1175
|
+
Args:
|
|
1176
|
+
task_num: Filter by task number (optional).
|
|
1177
|
+
days: How many days back to show (default 30).
|
|
1178
|
+
"""
|
|
1179
|
+
return handle_task_list(task_num, days)
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
@mcp.tool
|
|
1183
|
+
def nexo_task_frequency() -> str:
|
|
1184
|
+
"""Check which operational tasks are overdue based on their frequency.
|
|
1185
|
+
|
|
1186
|
+
Compares last execution date vs configured frequency.
|
|
1187
|
+
Returns overdue tasks or 'all tasks up to date'.
|
|
1188
|
+
"""
|
|
1189
|
+
return handle_task_frequency()
|
|
1190
|
+
|
|
1191
|
+
|
|
1192
|
+
# ── Plugin Management (3 tools) ─────────────────────────────────
|
|
1193
|
+
|
|
1194
|
+
@mcp.tool
|
|
1195
|
+
def nexo_plugin_load(filename: str) -> str:
|
|
1196
|
+
"""Load or reload a plugin. Searches repo plugins/ first, then NEXO_HOME/plugins/.
|
|
1197
|
+
|
|
1198
|
+
Args:
|
|
1199
|
+
filename: Plugin filename (e.g., 'entities.py').
|
|
1200
|
+
"""
|
|
1201
|
+
try:
|
|
1202
|
+
n = load_plugin(mcp, filename)
|
|
1203
|
+
return f"Plugin {filename}: {n} tools registered."
|
|
1204
|
+
except Exception as e:
|
|
1205
|
+
return f"Error loading plugin {filename}: {e}"
|
|
1206
|
+
|
|
1207
|
+
|
|
1208
|
+
@mcp.tool
|
|
1209
|
+
def nexo_plugin_list() -> str:
|
|
1210
|
+
"""List all loaded plugins and their tools, showing source (repo/personal)."""
|
|
1211
|
+
plugins = list_plugins()
|
|
1212
|
+
if not plugins:
|
|
1213
|
+
return "No plugins loaded."
|
|
1214
|
+
lines = ["LOADED PLUGINS:"]
|
|
1215
|
+
for p in plugins:
|
|
1216
|
+
names = p["tool_names"] or "(no tools)"
|
|
1217
|
+
source = p.get("source", "repo")
|
|
1218
|
+
lines.append(f" [{source}] {p['filename']} — {p['tools_count']} tools: {names}")
|
|
1219
|
+
return "\n".join(lines)
|
|
1220
|
+
|
|
1221
|
+
|
|
1222
|
+
@mcp.tool
|
|
1223
|
+
def nexo_plugin_remove(filename: str) -> str:
|
|
1224
|
+
"""Unregister a plugin's tools from MCP (does not delete files).
|
|
1225
|
+
|
|
1226
|
+
Args:
|
|
1227
|
+
filename: Plugin filename (e.g., 'entities.py').
|
|
1228
|
+
"""
|
|
1229
|
+
try:
|
|
1230
|
+
removed = remove_plugin(mcp, filename)
|
|
1231
|
+
if removed:
|
|
1232
|
+
return f"Plugin {filename} unregistered. Tools removed: {', '.join(removed)}"
|
|
1233
|
+
return f"Plugin {filename} unregistered (had no registered tools)."
|
|
1234
|
+
except Exception as e:
|
|
1235
|
+
return f"Error removing plugin {filename}: {e}"
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
# ── Drive / Curiosity (4 tools) ──────────────────────────────────
|
|
1239
|
+
|
|
1240
|
+
@mcp.tool
|
|
1241
|
+
def nexo_drive_signals(status: str = "", area: str = "", limit: int = 20) -> str:
|
|
1242
|
+
"""List autonomous drive/curiosity signals.
|
|
1243
|
+
|
|
1244
|
+
Drive signals are observations NEXO accumulates during normal work.
|
|
1245
|
+
When tension crosses threshold, NEXO investigates silently.
|
|
1246
|
+
|
|
1247
|
+
Args:
|
|
1248
|
+
status: Filter by status (latent, rising, ready, acted, dismissed). Default: active only.
|
|
1249
|
+
area: Filter by operational area (shopify, google-ads, wazion, nexo, etc.).
|
|
1250
|
+
limit: Max signals to return (default 20).
|
|
1251
|
+
"""
|
|
1252
|
+
return handle_drive_signals(status, area, limit)
|
|
1253
|
+
|
|
1254
|
+
|
|
1255
|
+
@mcp.tool
|
|
1256
|
+
def nexo_drive_reinforce(signal_id: int, observation: str) -> str:
|
|
1257
|
+
"""Reinforce a drive signal with a new observation.
|
|
1258
|
+
|
|
1259
|
+
Increases tension and may promote the signal status (latent → rising → ready).
|
|
1260
|
+
|
|
1261
|
+
Args:
|
|
1262
|
+
signal_id: Signal ID to reinforce.
|
|
1263
|
+
observation: New observation that supports this signal.
|
|
1264
|
+
"""
|
|
1265
|
+
return handle_drive_reinforce(signal_id, observation)
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
@mcp.tool
|
|
1269
|
+
def nexo_drive_act(signal_id: int, outcome: str) -> str:
|
|
1270
|
+
"""Mark a drive signal as investigated with an outcome.
|
|
1271
|
+
|
|
1272
|
+
Call this after NEXO has autonomously investigated a READY signal.
|
|
1273
|
+
|
|
1274
|
+
Args:
|
|
1275
|
+
signal_id: Signal ID that was investigated.
|
|
1276
|
+
outcome: What was found during investigation.
|
|
1277
|
+
"""
|
|
1278
|
+
return handle_drive_act(signal_id, outcome)
|
|
1279
|
+
|
|
1280
|
+
|
|
1281
|
+
@mcp.tool
|
|
1282
|
+
def nexo_drive_dismiss(signal_id: int, reason: str) -> str:
|
|
1283
|
+
"""Dismiss a drive signal (archived, not deleted).
|
|
1284
|
+
|
|
1285
|
+
Call this when a signal is not worth investigating.
|
|
1286
|
+
|
|
1287
|
+
Args:
|
|
1288
|
+
signal_id: Signal ID to dismiss.
|
|
1289
|
+
reason: Why this signal was dismissed.
|
|
1290
|
+
"""
|
|
1291
|
+
return handle_drive_dismiss(signal_id, reason)
|
|
1292
|
+
|
|
1293
|
+
|
|
1294
|
+
if __name__ == "__main__":
|
|
1295
|
+
_server_init()
|
|
1296
|
+
mcp.run(**_run_kwargs_from_env())
|