nexo-brain 5.3.13 → 5.3.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bin/nexo-brain.js +52 -1
  3. package/package.json +1 -1
  4. package/src/crons/sync.py +18 -4
  5. package/src/dashboard/static/favicon 2.svg +32 -0
  6. package/src/dashboard/static/nexo-logo 2.png +0 -0
  7. package/src/dashboard/static/nexo-logo 2.svg +40 -0
  8. package/src/dashboard/static/style 2.css +2458 -0
  9. package/src/dashboard/templates/adaptive 2.html +118 -0
  10. package/src/dashboard/templates/artifacts 2.html +133 -0
  11. package/src/dashboard/templates/backups 2.html +136 -0
  12. package/src/dashboard/templates/base 2.html +417 -0
  13. package/src/dashboard/templates/calendar 2.html +591 -0
  14. package/src/dashboard/templates/chat 2.html +356 -0
  15. package/src/dashboard/templates/claims 2.html +259 -0
  16. package/src/dashboard/templates/cortex 2.html +321 -0
  17. package/src/dashboard/templates/credentials 2.html +128 -0
  18. package/src/dashboard/templates/crons 2.html +370 -0
  19. package/src/dashboard/templates/dashboard 2.html +494 -0
  20. package/src/dashboard/templates/dreams 2.html +252 -0
  21. package/src/dashboard/templates/email 2.html +160 -0
  22. package/src/dashboard/templates/evolution 2.html +189 -0
  23. package/src/dashboard/templates/feed 2.html +249 -0
  24. package/src/dashboard/templates/followup_health 2.html +170 -0
  25. package/src/dashboard/templates/graph 2.html +201 -0
  26. package/src/dashboard/templates/guard 2.html +259 -0
  27. package/src/dashboard/templates/inbox 2.html +251 -0
  28. package/src/dashboard/templates/memory 2.html +420 -0
  29. package/src/dashboard/templates/operations 2.html +608 -0
  30. package/src/dashboard/templates/plugins 2.html +185 -0
  31. package/src/dashboard/templates/protocol 2.html +199 -0
  32. package/src/dashboard/templates/rules 2.html +246 -0
  33. package/src/dashboard/templates/sentiment 2.html +247 -0
  34. package/src/dashboard/templates/sessions 2.html +218 -0
  35. package/src/dashboard/templates/skills 2.html +329 -0
  36. package/src/dashboard/templates/somatic 2.html +73 -0
  37. package/src/dashboard/templates/triggers 2.html +133 -0
  38. package/src/dashboard/templates/trust 2.html +360 -0
  39. package/src/db/__init__ 2.py +259 -0
  40. package/src/db/_core 2.py +437 -0
  41. package/src/db/_credentials 2.py +124 -0
  42. package/src/db/_entities.py +1 -1
  43. package/src/db/_episodic 2.py +762 -0
  44. package/src/db/_evolution 2.py +54 -0
  45. package/src/db/_fts 2.py +406 -0
  46. package/src/db/_goal_profiles 2.py +376 -0
  47. package/src/db/_hot_context 2.py +660 -0
  48. package/src/db/_outcomes 2.py +800 -0
  49. package/src/db/_personal_scripts 2.py +582 -0
  50. package/src/db/_sessions 2.py +330 -0
  51. package/src/db/_tasks 2.py +91 -0
  52. package/src/db/_watchers 2.py +173 -0
  53. package/src/doctor/formatters 2.py +52 -0
  54. package/src/doctor/models 2.py +69 -0
  55. package/src/doctor/planes 2.py +87 -0
  56. package/src/doctor/providers/__init__ 2.py +1 -0
  57. package/src/doctor/providers/deep 2.py +367 -0
  58. package/src/evolution_cycle 2.py +519 -0
  59. package/src/hooks/auto_capture 2.py +208 -0
  60. package/src/hooks/caffeinate-guard 2.sh +8 -0
  61. package/src/hooks/capture-session 2.sh +21 -0
  62. package/src/hooks/capture-tool-logs 2.sh +158 -0
  63. package/src/hooks/daily-briefing-check 2.sh +33 -0
  64. package/src/hooks/heartbeat-enforcement 2.py +90 -0
  65. package/src/hooks/heartbeat-posttool 2.sh +18 -0
  66. package/src/hooks/inbox-hook 2.sh +76 -0
  67. package/src/hooks/post-compact 2.sh +152 -0
  68. package/src/hooks/pre-compact 2.sh +169 -0
  69. package/src/hooks/protocol-guardrail 2.sh +10 -0
  70. package/src/hooks/protocol-pretool-guardrail 2.sh +9 -0
  71. package/src/hooks/session-stop 2.sh +52 -0
  72. package/src/kg_populate 2.py +292 -0
  73. package/src/maintenance 2.py +53 -0
  74. package/src/memory_backends 2.py +71 -0
  75. package/src/migrate_embeddings 2.py +124 -0
  76. package/src/nexo_sdk 2.py +103 -0
  77. package/src/observability 2.py +199 -0
  78. package/src/plugin_loader 2.py +217 -0
  79. package/src/plugins/__init__ 2.py +0 -0
  80. package/src/plugins/agents.py +10 -3
  81. package/src/plugins/artifact_registry 2.py +450 -0
  82. package/src/plugins/backup 2.py +127 -0
  83. package/src/plugins/claims_tools 2.py +119 -0
  84. package/src/plugins/cognitive_memory 2.py +609 -0
  85. package/src/plugins/core_rules 2.py +252 -0
  86. package/src/plugins/cortex 2.py +1155 -0
  87. package/src/plugins/entities 2.py +67 -0
  88. package/src/plugins/episodic_memory 2.py +560 -0
  89. package/src/plugins/evolution 2.py +167 -0
  90. package/src/plugins/goal_engine 2.py +142 -0
  91. package/src/plugins/guard 2.py +862 -0
  92. package/src/plugins/impact 2.py +29 -0
  93. package/src/plugins/knowledge_graph_tools 2.py +137 -0
  94. package/src/plugins/media_memory_tools 2.py +98 -0
  95. package/src/plugins/memory_export 2.py +196 -0
  96. package/src/plugins/outcomes 2.py +130 -0
  97. package/src/plugins/personal_scripts 2.py +117 -0
  98. package/src/plugins/preferences 2.py +47 -0
  99. package/src/plugins/protocol 2.py +1449 -0
  100. package/src/plugins/schedule.py +2 -1
  101. package/src/plugins/simple_api 2.py +106 -0
  102. package/src/plugins/skills 2.py +341 -0
  103. package/src/plugins/state_watchers 2.py +79 -0
  104. package/src/plugins/update 2.py +986 -0
  105. package/src/plugins/user_state_tools 2.py +43 -0
  106. package/src/plugins/workflow 2.py +588 -0
  107. package/src/protocol_settings 2.py +59 -0
  108. package/src/public_contribution 2.py +466 -0
  109. package/src/public_evolution_queue 2.py +241 -0
  110. package/src/requirements 2.txt +14 -0
  111. package/src/requirements.txt +1 -1
  112. package/src/retroactive_learnings 2.py +373 -0
  113. package/src/rules/__init__ 2.py +0 -0
  114. package/src/rules/core-rules 2.json +331 -0
  115. package/src/rules/migrate 2.py +207 -0
  116. package/src/runtime_power 2.py +874 -0
  117. package/src/runtime_power.py +18 -1
  118. package/src/script_registry 2.py +1559 -0
  119. package/src/scripts/check-context 2.py +272 -0
  120. package/src/scripts/deep-sleep/apply_findings 2.py +2327 -0
  121. package/src/scripts/deep-sleep/collect 2.py +928 -0
  122. package/src/scripts/deep-sleep/extract 2.py +330 -0
  123. package/src/scripts/deep-sleep/extract-prompt 2.md +285 -0
  124. package/src/scripts/deep-sleep/synthesize 2.py +312 -0
  125. package/src/scripts/deep-sleep/synthesize-prompt 2.md +336 -0
  126. package/src/scripts/nexo-agent-run 2.py +75 -0
  127. package/src/scripts/nexo-auto-update 2.py +6 -0
  128. package/src/scripts/nexo-backup 2.sh +25 -0
  129. package/src/scripts/nexo-brain-activation 2.sh +140 -0
  130. package/src/scripts/nexo-catchup 2.py +300 -0
  131. package/src/scripts/nexo-cognitive-decay 2.py +257 -0
  132. package/src/scripts/nexo-cortex-cycle 2.py +293 -0
  133. package/src/scripts/nexo-cron-wrapper 2.sh +53 -0
  134. package/src/scripts/nexo-cron-wrapper.sh +7 -0
  135. package/src/scripts/nexo-daily-self-audit 2.py +2161 -0
  136. package/src/scripts/nexo-dashboard 2.sh +29 -0
  137. package/src/scripts/nexo-deep-sleep 2.sh +86 -0
  138. package/src/scripts/nexo-evolution-run 2.py +1664 -0
  139. package/src/scripts/nexo-followup-hygiene 2.py +139 -0
  140. package/src/scripts/nexo-hook-record 2.py +42 -0
  141. package/src/scripts/nexo-immune 2.py +936 -0
  142. package/src/scripts/nexo-impact-scorer 2.py +117 -0
  143. package/src/scripts/nexo-inbox-hook 2.sh +74 -0
  144. package/src/scripts/nexo-install 2.py +6 -0
  145. package/src/scripts/nexo-learning-housekeep 2.py +401 -0
  146. package/src/scripts/nexo-learning-validator 2.py +266 -0
  147. package/src/scripts/nexo-migrate 2.py +260 -0
  148. package/src/scripts/nexo-outcome-checker 2.py +127 -0
  149. package/src/scripts/nexo-postmortem-consolidator 2.py +456 -0
  150. package/src/scripts/nexo-pre-commit 2.py +120 -0
  151. package/src/scripts/nexo-prevent-sleep 2.sh +35 -0
  152. package/src/scripts/nexo-proactive-dashboard 2.py +354 -0
  153. package/src/scripts/nexo-reflection 2.py +256 -0
  154. package/src/scripts/nexo-runtime-preflight 2.py +274 -0
  155. package/src/scripts/nexo-sleep 2.py +631 -0
  156. package/src/scripts/nexo-snapshot-restore 2.sh +35 -0
  157. package/src/scripts/nexo-sync-clients 2.py +16 -0
  158. package/src/scripts/nexo-synthesis 2.py +475 -0
  159. package/src/scripts/nexo-tcc-approve 2.sh +79 -0
  160. package/src/scripts/nexo-update 2.sh +306 -0
  161. package/src/scripts/nexo-watchdog 2.sh +1207 -0
  162. package/src/scripts/nexo-watchdog-smoke 2.py +119 -0
  163. package/src/scripts/rehydrate_learnings_from_archive 2.py +245 -0
  164. package/src/server 2.py +1296 -0
  165. package/src/skills/run-nexo-audit-phase/guide 2.md +43 -0
  166. package/src/skills/run-nexo-audit-phase/skill 2.json +59 -0
  167. package/src/skills/run-nexo-core-fix-cycle/guide 2.md +17 -0
  168. package/src/skills/run-nexo-core-fix-cycle/script 2.py +276 -0
  169. package/src/skills/run-nexo-core-fix-cycle/skill 2.json +58 -0
  170. package/src/skills/run-release-final-audit/guide 2.md +16 -0
  171. package/src/skills/run-release-final-audit/script 2.py +259 -0
  172. package/src/skills/run-release-final-audit/skill 2.json +77 -0
  173. package/src/skills/run-runtime-doctor/guide 2.md +12 -0
  174. package/src/skills/run-runtime-doctor/script 2.py +21 -0
  175. package/src/skills/run-runtime-doctor/skill 2.json +25 -0
  176. package/src/skills_runtime 2.py +932 -0
  177. package/src/state_watchers_runtime 2.py +475 -0
  178. package/src/storage_router 2.py +32 -0
  179. package/src/system_catalog 2.py +786 -0
  180. package/src/tools_coordination 2.py +103 -0
  181. package/src/tools_credentials 2.py +68 -0
  182. package/src/tools_drive 2.py +487 -0
  183. package/src/tools_hot_context 2.py +163 -0
  184. package/src/tools_learnings 2.py +612 -0
  185. package/src/tools_menu 2.py +229 -0
  186. package/src/tools_reminders 2.py +88 -0
  187. package/src/tools_reminders_crud 2.py +363 -0
  188. package/src/tools_sessions 2.py +1054 -0
  189. package/src/tools_system_catalog 2.py +19 -0
  190. package/src/tools_task_history 2.py +57 -0
  191. package/src/tools_transcripts 2.py +98 -0
  192. package/src/transcript_utils 2.py +412 -0
  193. package/src/user_context 2.py +46 -0
  194. package/src/user_data_portability 2.py +328 -0
  195. package/src/user_state_model 2.py +170 -0
  196. package/templates/CLAUDE.md 2.template +108 -0
  197. package/templates/CODEX.AGENTS.md 2.template +66 -0
  198. package/templates/launchagents/README 2.md +132 -0
  199. package/templates/launchagents/com.nexo.auto-close-sessions 2.plist +39 -0
  200. package/templates/launchagents/com.nexo.auto-close-sessions.plist +1 -1
  201. package/templates/launchagents/com.nexo.catchup 2.plist +39 -0
  202. package/templates/launchagents/com.nexo.catchup.plist +1 -1
  203. package/templates/launchagents/com.nexo.cognitive-decay 2.plist +40 -0
  204. package/templates/launchagents/com.nexo.dashboard 2.plist +43 -0
  205. package/templates/launchagents/com.nexo.dashboard.plist +1 -1
  206. package/templates/launchagents/com.nexo.deep-sleep 2.plist +43 -0
  207. package/templates/launchagents/com.nexo.deep-sleep.plist +1 -1
  208. package/templates/launchagents/com.nexo.evolution 2.plist +44 -0
  209. package/templates/launchagents/com.nexo.evolution.plist +1 -1
  210. package/templates/launchagents/com.nexo.followup-hygiene 2.plist +45 -0
  211. package/templates/launchagents/com.nexo.followup-hygiene.plist +1 -1
  212. package/templates/launchagents/com.nexo.immune 2.plist +41 -0
  213. package/templates/launchagents/com.nexo.immune.plist +1 -1
  214. package/templates/launchagents/com.nexo.postmortem 2.plist +45 -0
  215. package/templates/launchagents/com.nexo.postmortem.plist +1 -1
  216. package/templates/launchagents/com.nexo.self-audit 2.plist +47 -0
  217. package/templates/launchagents/com.nexo.self-audit.plist +1 -1
  218. package/templates/launchagents/com.nexo.synthesis 2.plist +45 -0
  219. package/templates/launchagents/com.nexo.synthesis.plist +1 -1
  220. package/templates/launchagents/com.nexo.watchdog 2.plist +37 -0
  221. package/templates/launchagents/com.nexo.watchdog.plist +1 -1
  222. package/templates/nexo_helper 2.py +301 -0
  223. package/templates/openclaw 2.json +13 -0
  224. package/templates/plugin-template 2.py +40 -0
  225. package/templates/script-template 2.py +59 -0
  226. package/templates/script-template 2.sh +13 -0
  227. package/templates/script-template.py +5 -4
  228. package/templates/skill-script-template 2.py +48 -0
  229. package/templates/skill-script-template.py +2 -1
  230. 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
+
@@ -125,7 +125,7 @@ def delete_preference(key: str) -> bool:
125
125
 
126
126
  # ── Agents ────────────────────────────────────────────────────────
127
127
 
128
- def create_agent(id: str, name: str, specialization: str, model: str = "sonnet",
128
+ def create_agent(id: str, name: str, specialization: str, model: str = "",
129
129
  tools: str = "", context_files: str = "", rules: str = "") -> dict:
130
130
  """Register a new agent. Uses INSERT OR REPLACE to allow re-registration."""
131
131
  conn = get_db()