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
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
"""SQLite database for NEXO session coordination."""
|
|
3
|
+
|
|
4
|
+
import sqlite3
|
|
5
|
+
import time
|
|
6
|
+
import os
|
|
7
|
+
import secrets
|
|
8
|
+
import string
|
|
9
|
+
import datetime
|
|
10
|
+
import pathlib
|
|
11
|
+
import threading
|
|
12
|
+
|
|
13
|
+
NEXO_HOME = os.environ.get("NEXO_HOME", os.path.expanduser("~/.nexo"))
|
|
14
|
+
_data_dir = os.path.join(NEXO_HOME, "data")
|
|
15
|
+
os.makedirs(_data_dir, exist_ok=True)
|
|
16
|
+
|
|
17
|
+
DB_PATH = os.environ.get(
|
|
18
|
+
"NEXO_TEST_DB",
|
|
19
|
+
os.environ.get(
|
|
20
|
+
"NEXO_DB",
|
|
21
|
+
os.path.join(_data_dir, "nexo.db"),
|
|
22
|
+
),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# TTLs in seconds (match session-coord.sh behavior)
|
|
26
|
+
SESSION_STALE_SECONDS = 900 # 15 min (documented TTL)
|
|
27
|
+
MESSAGE_TTL_SECONDS = 3600 # 1 hour
|
|
28
|
+
QUESTION_TTL_SECONDS = 600 # 10 min
|
|
29
|
+
|
|
30
|
+
# Single shared connection per process with write serialization.
|
|
31
|
+
# SQLite allows only one writer at a time. Using a shared connection with
|
|
32
|
+
# check_same_thread=False and a write lock ensures:
|
|
33
|
+
# - No FTS5 corruption from concurrent write connections
|
|
34
|
+
# - Reads can happen freely (WAL allows concurrent readers)
|
|
35
|
+
# - Writes are serialized via _write_lock to prevent 'database is locked' errors
|
|
36
|
+
_shared_conn: sqlite3.Connection | None = None
|
|
37
|
+
_write_lock = threading.RLock() # RLock allows re-entrant locking (function A calls B, both serialize)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_db() -> sqlite3.Connection:
|
|
41
|
+
"""Get shared database connection with WAL mode.
|
|
42
|
+
|
|
43
|
+
Returns a _SerializedConnection wrapper that serializes all execute
|
|
44
|
+
calls via _write_lock, preventing race conditions and FTS5 corruption
|
|
45
|
+
under concurrent thread access.
|
|
46
|
+
"""
|
|
47
|
+
global _shared_conn
|
|
48
|
+
if _shared_conn is None:
|
|
49
|
+
raw = sqlite3.connect(
|
|
50
|
+
DB_PATH, timeout=30, check_same_thread=False,
|
|
51
|
+
isolation_level=None, # autocommit — no implicit BEGIN holding locks
|
|
52
|
+
)
|
|
53
|
+
raw.execute("PRAGMA journal_mode=WAL")
|
|
54
|
+
raw.execute("PRAGMA busy_timeout=30000")
|
|
55
|
+
raw.execute("PRAGMA foreign_keys=ON")
|
|
56
|
+
raw.execute("PRAGMA wal_autocheckpoint=1000")
|
|
57
|
+
raw.row_factory = sqlite3.Row
|
|
58
|
+
_shared_conn = _SerializedConnection(raw)
|
|
59
|
+
return _shared_conn
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def close_db():
|
|
63
|
+
"""Close the shared database connection. Called on shutdown signals."""
|
|
64
|
+
global _shared_conn
|
|
65
|
+
if _shared_conn is not None:
|
|
66
|
+
try:
|
|
67
|
+
_shared_conn.close()
|
|
68
|
+
except Exception:
|
|
69
|
+
pass
|
|
70
|
+
_shared_conn = None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _get_raw_conn() -> sqlite3.Connection:
|
|
74
|
+
"""Get the raw unwrapped connection (for PRAGMA queries that need direct access)."""
|
|
75
|
+
conn = get_db()
|
|
76
|
+
if isinstance(conn, _SerializedConnection):
|
|
77
|
+
return conn._conn
|
|
78
|
+
return conn
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class _SerializedConnection:
|
|
82
|
+
"""Wrapper around sqlite3.Connection that serializes all execute calls.
|
|
83
|
+
|
|
84
|
+
SQLite with a single shared connection and check_same_thread=False needs
|
|
85
|
+
serialization to prevent:
|
|
86
|
+
- Stale lastrowid when concurrent INSERTs happen
|
|
87
|
+
- FTS5 index corruption from concurrent writes
|
|
88
|
+
- 'NoneType' errors from interleaved INSERT+SELECT sequences
|
|
89
|
+
|
|
90
|
+
All execute/executemany/executescript calls go through _write_lock.
|
|
91
|
+
Property access (row_factory etc.) passes through directly.
|
|
92
|
+
"""
|
|
93
|
+
def __init__(self, conn: sqlite3.Connection):
|
|
94
|
+
self._conn = conn
|
|
95
|
+
|
|
96
|
+
def execute(self, *args, **kwargs):
|
|
97
|
+
with _write_lock:
|
|
98
|
+
return self._conn.execute(*args, **kwargs)
|
|
99
|
+
|
|
100
|
+
def executemany(self, *args, **kwargs):
|
|
101
|
+
with _write_lock:
|
|
102
|
+
return self._conn.executemany(*args, **kwargs)
|
|
103
|
+
|
|
104
|
+
def executescript(self, *args, **kwargs):
|
|
105
|
+
with _write_lock:
|
|
106
|
+
return self._conn.executescript(*args, **kwargs)
|
|
107
|
+
|
|
108
|
+
def commit(self):
|
|
109
|
+
with _write_lock:
|
|
110
|
+
return self._conn.commit()
|
|
111
|
+
|
|
112
|
+
def close(self):
|
|
113
|
+
return self._conn.close()
|
|
114
|
+
|
|
115
|
+
def __getattr__(self, name):
|
|
116
|
+
return getattr(self._conn, name)
|
|
117
|
+
|
|
118
|
+
def __setattr__(self, name, value):
|
|
119
|
+
if name == '_conn':
|
|
120
|
+
super().__setattr__(name, value)
|
|
121
|
+
else:
|
|
122
|
+
setattr(self._conn, name, value)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def init_db():
|
|
126
|
+
"""Create tables if they don't exist."""
|
|
127
|
+
conn = get_db()
|
|
128
|
+
conn.executescript("""
|
|
129
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
130
|
+
sid TEXT PRIMARY KEY,
|
|
131
|
+
task TEXT NOT NULL DEFAULT '',
|
|
132
|
+
started_epoch REAL NOT NULL,
|
|
133
|
+
last_update_epoch REAL NOT NULL,
|
|
134
|
+
local_time TEXT NOT NULL DEFAULT ''
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
CREATE TABLE IF NOT EXISTS tracked_files (
|
|
138
|
+
sid TEXT NOT NULL,
|
|
139
|
+
path TEXT NOT NULL,
|
|
140
|
+
tracked_at REAL NOT NULL,
|
|
141
|
+
PRIMARY KEY (sid, path),
|
|
142
|
+
FOREIGN KEY (sid) REFERENCES sessions(sid) ON DELETE CASCADE
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
146
|
+
id TEXT PRIMARY KEY,
|
|
147
|
+
from_sid TEXT NOT NULL,
|
|
148
|
+
to_sid TEXT NOT NULL,
|
|
149
|
+
text TEXT NOT NULL,
|
|
150
|
+
created_epoch REAL NOT NULL
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
CREATE TABLE IF NOT EXISTS message_reads (
|
|
154
|
+
message_id TEXT NOT NULL,
|
|
155
|
+
sid TEXT NOT NULL,
|
|
156
|
+
PRIMARY KEY (message_id, sid),
|
|
157
|
+
FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
CREATE TABLE IF NOT EXISTS questions (
|
|
161
|
+
qid TEXT PRIMARY KEY,
|
|
162
|
+
from_sid TEXT NOT NULL,
|
|
163
|
+
to_sid TEXT NOT NULL,
|
|
164
|
+
question TEXT NOT NULL,
|
|
165
|
+
answer TEXT,
|
|
166
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
167
|
+
created_epoch REAL NOT NULL,
|
|
168
|
+
answered_epoch REAL
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
CREATE TABLE IF NOT EXISTS reminders (
|
|
173
|
+
id TEXT PRIMARY KEY,
|
|
174
|
+
date TEXT,
|
|
175
|
+
description TEXT NOT NULL,
|
|
176
|
+
status TEXT NOT NULL DEFAULT 'PENDING',
|
|
177
|
+
category TEXT DEFAULT 'general',
|
|
178
|
+
created_at REAL NOT NULL,
|
|
179
|
+
updated_at REAL NOT NULL
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
CREATE TABLE IF NOT EXISTS followups (
|
|
183
|
+
id TEXT PRIMARY KEY,
|
|
184
|
+
date TEXT,
|
|
185
|
+
description TEXT NOT NULL,
|
|
186
|
+
verification TEXT DEFAULT '',
|
|
187
|
+
status TEXT NOT NULL DEFAULT 'PENDING',
|
|
188
|
+
recurrence TEXT DEFAULT NULL,
|
|
189
|
+
created_at REAL NOT NULL,
|
|
190
|
+
updated_at REAL NOT NULL
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
CREATE TABLE IF NOT EXISTS item_history (
|
|
194
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
195
|
+
item_type TEXT NOT NULL,
|
|
196
|
+
item_id TEXT NOT NULL,
|
|
197
|
+
event_type TEXT NOT NULL,
|
|
198
|
+
note TEXT DEFAULT '',
|
|
199
|
+
actor TEXT DEFAULT '',
|
|
200
|
+
metadata TEXT DEFAULT '{}',
|
|
201
|
+
created_at REAL NOT NULL
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
CREATE TABLE IF NOT EXISTS item_read_tokens (
|
|
205
|
+
token TEXT PRIMARY KEY,
|
|
206
|
+
item_type TEXT NOT NULL,
|
|
207
|
+
item_id TEXT NOT NULL,
|
|
208
|
+
history_seq INTEGER DEFAULT 0,
|
|
209
|
+
issued_at REAL NOT NULL,
|
|
210
|
+
expires_at REAL NOT NULL
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
CREATE TABLE IF NOT EXISTS learnings (
|
|
214
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
215
|
+
category TEXT NOT NULL,
|
|
216
|
+
title TEXT NOT NULL,
|
|
217
|
+
content TEXT NOT NULL,
|
|
218
|
+
created_at REAL NOT NULL,
|
|
219
|
+
updated_at REAL NOT NULL
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
CREATE TABLE IF NOT EXISTS credentials (
|
|
223
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
224
|
+
service TEXT NOT NULL,
|
|
225
|
+
key TEXT NOT NULL,
|
|
226
|
+
value TEXT NOT NULL,
|
|
227
|
+
notes TEXT DEFAULT '',
|
|
228
|
+
created_at REAL NOT NULL,
|
|
229
|
+
updated_at REAL NOT NULL,
|
|
230
|
+
UNIQUE(service, key)
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
CREATE TABLE IF NOT EXISTS task_history (
|
|
234
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
235
|
+
task_num TEXT NOT NULL,
|
|
236
|
+
task_name TEXT NOT NULL,
|
|
237
|
+
executed_at REAL NOT NULL,
|
|
238
|
+
notes TEXT DEFAULT ''
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
CREATE TABLE IF NOT EXISTS task_frequencies (
|
|
242
|
+
task_num TEXT PRIMARY KEY,
|
|
243
|
+
task_name TEXT NOT NULL,
|
|
244
|
+
frequency_days INTEGER NOT NULL,
|
|
245
|
+
description TEXT DEFAULT ''
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
CREATE TABLE IF NOT EXISTS plugins (
|
|
249
|
+
filename TEXT PRIMARY KEY,
|
|
250
|
+
tools_count INTEGER DEFAULT 0,
|
|
251
|
+
tool_names TEXT DEFAULT '',
|
|
252
|
+
loaded_at REAL,
|
|
253
|
+
created_by TEXT DEFAULT 'manual'
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
257
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
258
|
+
name TEXT NOT NULL,
|
|
259
|
+
type TEXT NOT NULL DEFAULT 'general',
|
|
260
|
+
value TEXT NOT NULL,
|
|
261
|
+
notes TEXT DEFAULT '',
|
|
262
|
+
created_at REAL NOT NULL,
|
|
263
|
+
updated_at REAL NOT NULL
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
CREATE TABLE IF NOT EXISTS preferences (
|
|
267
|
+
key TEXT PRIMARY KEY,
|
|
268
|
+
value TEXT NOT NULL,
|
|
269
|
+
category TEXT DEFAULT 'general',
|
|
270
|
+
updated_at REAL NOT NULL
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
274
|
+
id TEXT PRIMARY KEY,
|
|
275
|
+
name TEXT NOT NULL,
|
|
276
|
+
specialization TEXT NOT NULL,
|
|
277
|
+
model TEXT DEFAULT 'sonnet',
|
|
278
|
+
tools TEXT DEFAULT '',
|
|
279
|
+
context_files TEXT DEFAULT '',
|
|
280
|
+
rules TEXT DEFAULT '',
|
|
281
|
+
created_at REAL NOT NULL,
|
|
282
|
+
updated_at REAL NOT NULL
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
CREATE TABLE IF NOT EXISTS change_log (
|
|
286
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
287
|
+
session_id TEXT NOT NULL,
|
|
288
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
289
|
+
files TEXT NOT NULL,
|
|
290
|
+
what_changed TEXT NOT NULL,
|
|
291
|
+
why TEXT NOT NULL,
|
|
292
|
+
triggered_by TEXT DEFAULT '',
|
|
293
|
+
affects TEXT DEFAULT '',
|
|
294
|
+
risks TEXT DEFAULT '',
|
|
295
|
+
verify TEXT DEFAULT '',
|
|
296
|
+
commit_ref TEXT DEFAULT ''
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
CREATE TABLE IF NOT EXISTS decisions (
|
|
300
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
301
|
+
session_id TEXT NOT NULL,
|
|
302
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
303
|
+
domain TEXT NOT NULL,
|
|
304
|
+
decision TEXT NOT NULL,
|
|
305
|
+
alternatives TEXT,
|
|
306
|
+
based_on TEXT,
|
|
307
|
+
confidence TEXT DEFAULT 'medium',
|
|
308
|
+
context_ref TEXT,
|
|
309
|
+
outcome TEXT,
|
|
310
|
+
outcome_at TEXT
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
CREATE TABLE IF NOT EXISTS session_diary (
|
|
314
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
315
|
+
session_id TEXT NOT NULL,
|
|
316
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
317
|
+
decisions TEXT NOT NULL,
|
|
318
|
+
discarded TEXT,
|
|
319
|
+
pending TEXT,
|
|
320
|
+
context_next TEXT,
|
|
321
|
+
mental_state TEXT,
|
|
322
|
+
domain TEXT,
|
|
323
|
+
user_signals TEXT,
|
|
324
|
+
summary TEXT NOT NULL
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
CREATE TABLE IF NOT EXISTS evolution_metrics (
|
|
328
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
329
|
+
dimension TEXT NOT NULL,
|
|
330
|
+
score INTEGER NOT NULL CHECK(score >= 0 AND score <= 100),
|
|
331
|
+
measured_at TEXT DEFAULT (datetime('now')),
|
|
332
|
+
evidence TEXT NOT NULL,
|
|
333
|
+
delta INTEGER DEFAULT 0
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
CREATE TABLE IF NOT EXISTS evolution_log (
|
|
337
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
338
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
339
|
+
cycle_number INTEGER NOT NULL,
|
|
340
|
+
dimension TEXT NOT NULL,
|
|
341
|
+
proposal TEXT NOT NULL,
|
|
342
|
+
classification TEXT NOT NULL DEFAULT 'auto',
|
|
343
|
+
status TEXT DEFAULT 'pending',
|
|
344
|
+
files_changed TEXT,
|
|
345
|
+
snapshot_ref TEXT,
|
|
346
|
+
test_result TEXT,
|
|
347
|
+
impact INTEGER DEFAULT 0,
|
|
348
|
+
reasoning TEXT NOT NULL
|
|
349
|
+
);
|
|
350
|
+
""")
|
|
351
|
+
# foreign_keys=ON is set in get_db() per-connection
|
|
352
|
+
|
|
353
|
+
# ── Run formal migrations ────────────────────────────────────
|
|
354
|
+
from db._schema import run_migrations
|
|
355
|
+
run_migrations(conn)
|
|
356
|
+
|
|
357
|
+
# ── FTS5 unified search index ────────────────────────────────
|
|
358
|
+
conn.execute("""
|
|
359
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS unified_search USING fts5(
|
|
360
|
+
source,
|
|
361
|
+
source_id,
|
|
362
|
+
title,
|
|
363
|
+
body,
|
|
364
|
+
category,
|
|
365
|
+
updated_at UNINDEXED,
|
|
366
|
+
tokenize='unicode61 remove_diacritics 2'
|
|
367
|
+
)
|
|
368
|
+
""")
|
|
369
|
+
|
|
370
|
+
# Dynamic directory registry for FTS indexing
|
|
371
|
+
conn.execute("""
|
|
372
|
+
CREATE TABLE IF NOT EXISTS fts_dirs (
|
|
373
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
374
|
+
path TEXT NOT NULL UNIQUE,
|
|
375
|
+
dir_type TEXT NOT NULL DEFAULT 'code',
|
|
376
|
+
patterns TEXT NOT NULL DEFAULT '*.php,*.js,*.json,*.py,*.ts,*.tsx',
|
|
377
|
+
added_at REAL NOT NULL,
|
|
378
|
+
notes TEXT DEFAULT ''
|
|
379
|
+
)
|
|
380
|
+
""")
|
|
381
|
+
conn.commit()
|
|
382
|
+
|
|
383
|
+
if os.environ.get("NEXO_SKIP_FS_INDEX", "0") != "1":
|
|
384
|
+
# FTS refresh in background thread — never block server startup
|
|
385
|
+
import threading
|
|
386
|
+
|
|
387
|
+
def _bg_fts():
|
|
388
|
+
try:
|
|
389
|
+
bg_conn = sqlite3.connect(DB_PATH, timeout=30)
|
|
390
|
+
bg_conn.execute("PRAGMA journal_mode=WAL")
|
|
391
|
+
bg_conn.execute("PRAGMA busy_timeout=30000")
|
|
392
|
+
bg_conn.row_factory = sqlite3.Row
|
|
393
|
+
row = bg_conn.execute("SELECT COUNT(*) FROM unified_search").fetchone()
|
|
394
|
+
from db._fts import rebuild_fts_index, _refresh_fts_files
|
|
395
|
+
if row[0] == 0:
|
|
396
|
+
rebuild_fts_index(bg_conn)
|
|
397
|
+
else:
|
|
398
|
+
_refresh_fts_files(bg_conn)
|
|
399
|
+
bg_conn.close()
|
|
400
|
+
except Exception:
|
|
401
|
+
pass
|
|
402
|
+
|
|
403
|
+
threading.Thread(target=_bg_fts, daemon=True).start()
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _gen_id(prefix: str, length: int = 8) -> str:
|
|
408
|
+
"""Generate a random ID like 'msg-a1b2c3' or 'q-x9y8z7w6'."""
|
|
409
|
+
chars = string.ascii_lowercase + string.digits
|
|
410
|
+
suffix = ''.join(secrets.choice(chars) for _ in range(length))
|
|
411
|
+
return f"{prefix}-{suffix}"
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
# ── Session operations ──────────────────────────────────────────────
|
|
415
|
+
|
|
416
|
+
def now_epoch() -> float:
|
|
417
|
+
return time.time()
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def local_time_str() -> str:
|
|
421
|
+
from datetime import datetime
|
|
422
|
+
return datetime.now().strftime("%H:%M")
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _multi_word_like(query: str, columns: list[str]) -> tuple[str, list]:
|
|
426
|
+
"""Build AND-ed LIKE conditions: every word must appear in at least one column."""
|
|
427
|
+
words = query.strip().split()
|
|
428
|
+
if not words:
|
|
429
|
+
return "1=1", []
|
|
430
|
+
word_conditions = []
|
|
431
|
+
params = []
|
|
432
|
+
for word in words:
|
|
433
|
+
pattern = f"%{word}%"
|
|
434
|
+
col_or = " OR ".join(f"{c} LIKE ?" for c in columns)
|
|
435
|
+
word_conditions.append(f"({col_or})")
|
|
436
|
+
params.extend([pattern] * len(columns))
|
|
437
|
+
return " AND ".join(word_conditions), params
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""NEXO DB — Credentials module."""
|
|
2
|
+
import sqlite3, time
|
|
3
|
+
from db._core import get_db, now_epoch
|
|
4
|
+
|
|
5
|
+
# ── Credentials ────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
def create_credential(service: str, key: str, value: str, notes: str = '') -> dict:
|
|
8
|
+
"""Create a new credential entry."""
|
|
9
|
+
conn = get_db()
|
|
10
|
+
now = now_epoch()
|
|
11
|
+
try:
|
|
12
|
+
conn.execute(
|
|
13
|
+
"INSERT INTO credentials (service, key, value, notes, created_at, updated_at) "
|
|
14
|
+
"VALUES (?, ?, ?, ?, ?, ?)",
|
|
15
|
+
(service, key, value, notes, now, now)
|
|
16
|
+
)
|
|
17
|
+
conn.commit()
|
|
18
|
+
except sqlite3.IntegrityError:
|
|
19
|
+
return {"error": f"Credential {service}/{key} already exists. Use update instead."}
|
|
20
|
+
row = conn.execute(
|
|
21
|
+
"SELECT * FROM credentials WHERE service = ? AND key = ?", (service, key)
|
|
22
|
+
).fetchone()
|
|
23
|
+
return dict(row)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def update_credential(service: str, key: str, value: str = None, notes: str = None) -> dict:
|
|
27
|
+
"""Update value and/or notes for a credential."""
|
|
28
|
+
conn = get_db()
|
|
29
|
+
row = conn.execute(
|
|
30
|
+
"SELECT * FROM credentials WHERE service = ? AND key = ?", (service, key)
|
|
31
|
+
).fetchone()
|
|
32
|
+
if not row:
|
|
33
|
+
return {"error": f"Credential {service}/{key} not found"}
|
|
34
|
+
updates = {"updated_at": now_epoch()}
|
|
35
|
+
if value is not None:
|
|
36
|
+
updates["value"] = value
|
|
37
|
+
if notes is not None:
|
|
38
|
+
updates["notes"] = notes
|
|
39
|
+
set_clause = ", ".join(f"{k} = ?" for k in updates)
|
|
40
|
+
values = list(updates.values()) + [service, key]
|
|
41
|
+
conn.execute(
|
|
42
|
+
f"UPDATE credentials SET {set_clause} WHERE service = ? AND key = ?", values
|
|
43
|
+
)
|
|
44
|
+
conn.commit()
|
|
45
|
+
row = conn.execute(
|
|
46
|
+
"SELECT * FROM credentials WHERE service = ? AND key = ?", (service, key)
|
|
47
|
+
).fetchone()
|
|
48
|
+
return dict(row)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def delete_credential(service: str, key: str = None) -> bool:
|
|
52
|
+
"""Delete credential(s). If key=None, delete all for the service."""
|
|
53
|
+
conn = get_db()
|
|
54
|
+
if key:
|
|
55
|
+
result = conn.execute(
|
|
56
|
+
"DELETE FROM credentials WHERE service = ? AND key = ?", (service, key)
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
result = conn.execute(
|
|
60
|
+
"DELETE FROM credentials WHERE service = ?", (service,)
|
|
61
|
+
)
|
|
62
|
+
conn.commit()
|
|
63
|
+
deleted = result.rowcount > 0
|
|
64
|
+
return deleted
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def get_credential(service: str, key: str = None) -> list[dict]:
|
|
68
|
+
"""Get credential(s). If key=None, return all for the service.
|
|
69
|
+
|
|
70
|
+
When exact match fails, performs fuzzy search across service, key,
|
|
71
|
+
value and notes fields. Returns results tagged with _fuzzy=True so
|
|
72
|
+
the caller can differentiate suggestions from exact hits.
|
|
73
|
+
"""
|
|
74
|
+
conn = get_db()
|
|
75
|
+
if key:
|
|
76
|
+
rows = conn.execute(
|
|
77
|
+
"SELECT * FROM credentials WHERE service = ? AND key = ?", (service, key)
|
|
78
|
+
).fetchall()
|
|
79
|
+
else:
|
|
80
|
+
rows = conn.execute(
|
|
81
|
+
"SELECT * FROM credentials WHERE service = ?", (service,)
|
|
82
|
+
).fetchall()
|
|
83
|
+
if rows:
|
|
84
|
+
return [dict(r) for r in rows]
|
|
85
|
+
|
|
86
|
+
# Fuzzy fallback: search term in service, key and notes (not value — too noisy)
|
|
87
|
+
# Prioritize: service/key matches first, notes-only matches second
|
|
88
|
+
term = f"%{service}%"
|
|
89
|
+
fuzzy_rows = conn.execute(
|
|
90
|
+
"SELECT *, "
|
|
91
|
+
"CASE WHEN service LIKE ? THEN 0 "
|
|
92
|
+
" WHEN key LIKE ? THEN 1 "
|
|
93
|
+
" ELSE 2 END AS _rank "
|
|
94
|
+
"FROM credentials WHERE "
|
|
95
|
+
"service LIKE ? OR key LIKE ? OR notes LIKE ? "
|
|
96
|
+
"ORDER BY _rank ASC, service ASC, key ASC",
|
|
97
|
+
(term, term, term, term, term),
|
|
98
|
+
).fetchall()
|
|
99
|
+
results = []
|
|
100
|
+
for r in fuzzy_rows:
|
|
101
|
+
d = dict(r)
|
|
102
|
+
d["_fuzzy"] = True
|
|
103
|
+
d.pop("_rank", None)
|
|
104
|
+
results.append(d)
|
|
105
|
+
return results
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def list_credentials(service: str = None) -> list[dict]:
|
|
109
|
+
"""List service+key only (NO values) for security."""
|
|
110
|
+
conn = get_db()
|
|
111
|
+
if service:
|
|
112
|
+
rows = conn.execute(
|
|
113
|
+
"SELECT id, service, key, notes, created_at, updated_at "
|
|
114
|
+
"FROM credentials WHERE service = ? ORDER BY key ASC",
|
|
115
|
+
(service,)
|
|
116
|
+
).fetchall()
|
|
117
|
+
else:
|
|
118
|
+
rows = conn.execute(
|
|
119
|
+
"SELECT id, service, key, notes, created_at, updated_at "
|
|
120
|
+
"FROM credentials ORDER BY service ASC, key ASC"
|
|
121
|
+
).fetchall()
|
|
122
|
+
return [dict(r) for r in rows]
|
|
123
|
+
|
|
124
|
+
|