wyrm-mcp 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +667 -0
- package/README.md +384 -0
- package/dist/analytics.d.ts +100 -0
- package/dist/analytics.d.ts.map +1 -0
- package/dist/analytics.js +368 -0
- package/dist/analytics.js.map +1 -0
- package/dist/auto-orchestrator.d.ts +118 -0
- package/dist/auto-orchestrator.d.ts.map +1 -0
- package/dist/auto-orchestrator.js +325 -0
- package/dist/auto-orchestrator.js.map +1 -0
- package/dist/autoconfig.d.ts +89 -0
- package/dist/autoconfig.d.ts.map +1 -0
- package/dist/autoconfig.js +576 -0
- package/dist/autoconfig.js.map +1 -0
- package/dist/cli.d.ts +148 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +281 -0
- package/dist/cli.js.map +1 -0
- package/dist/cloud-backup.d.ts +100 -0
- package/dist/cloud-backup.d.ts.map +1 -0
- package/dist/cloud-backup.js +545 -0
- package/dist/cloud-backup.js.map +1 -0
- package/dist/crypto.d.ts +72 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +164 -0
- package/dist/crypto.js.map +1 -0
- package/dist/database.d.ts +218 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +1058 -0
- package/dist/database.js.map +1 -0
- package/dist/http-auth.d.ts +68 -0
- package/dist/http-auth.d.ts.map +1 -0
- package/dist/http-auth.js +296 -0
- package/dist/http-auth.js.map +1 -0
- package/dist/http-fast.d.ts +13 -0
- package/dist/http-fast.d.ts.map +1 -0
- package/dist/http-fast.js +325 -0
- package/dist/http-fast.js.map +1 -0
- package/dist/http-server.d.ts +12 -0
- package/dist/http-server.d.ts.map +1 -0
- package/dist/http-server.js +383 -0
- package/dist/http-server.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1695 -0
- package/dist/index.js.map +1 -0
- package/dist/license.d.ts +177 -0
- package/dist/license.d.ts.map +1 -0
- package/dist/license.js +405 -0
- package/dist/license.js.map +1 -0
- package/dist/logger.d.ts +76 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +195 -0
- package/dist/logger.js.map +1 -0
- package/dist/performance.d.ts +114 -0
- package/dist/performance.d.ts.map +1 -0
- package/dist/performance.js +228 -0
- package/dist/performance.js.map +1 -0
- package/dist/resilience.d.ts +146 -0
- package/dist/resilience.d.ts.map +1 -0
- package/dist/resilience.js +563 -0
- package/dist/resilience.js.map +1 -0
- package/dist/security.d.ts +68 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +215 -0
- package/dist/security.js.map +1 -0
- package/dist/setup.d.ts +21 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +261 -0
- package/dist/setup.js.map +1 -0
- package/dist/summarizer.d.ts +30 -0
- package/dist/summarizer.d.ts.map +1 -0
- package/dist/summarizer.js +139 -0
- package/dist/summarizer.js.map +1 -0
- package/dist/sync.d.ts +39 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +356 -0
- package/dist/sync.js.map +1 -0
- package/dist/types.d.ts +267 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +30 -0
- package/dist/types.js.map +1 -0
- package/dist/vectors.d.ts +103 -0
- package/dist/vectors.d.ts.map +1 -0
- package/dist/vectors.js +311 -0
- package/dist/vectors.js.map +1 -0
- package/package.json +73 -0
package/dist/database.js
ADDED
|
@@ -0,0 +1,1058 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wyrm Database - SQLite storage for infinite memory with data lake support
|
|
3
|
+
*
|
|
4
|
+
* @copyright 2026 Ghost Protocol (Pvt) Ltd. All Rights Reserved.
|
|
5
|
+
* @license Proprietary - See LICENSE file for details.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Auto-discovers projects in configured directories
|
|
9
|
+
* - Handles large datasets with pagination and streaming
|
|
10
|
+
* - Write-Ahead Logging (WAL) for concurrent performance
|
|
11
|
+
* - Full-text search for fast context retrieval
|
|
12
|
+
* - Batch operations for bulk imports
|
|
13
|
+
* - Resilient operations with automatic recovery
|
|
14
|
+
*/
|
|
15
|
+
import Database from 'better-sqlite3';
|
|
16
|
+
import { existsSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
17
|
+
import { homedir } from 'os';
|
|
18
|
+
import { join, basename, resolve, normalize } from 'path';
|
|
19
|
+
import { spawnSync } from 'child_process';
|
|
20
|
+
import { getResilienceManager } from './resilience.js';
|
|
21
|
+
import { WyrmLogger } from './logger.js';
|
|
22
|
+
export class WyrmDB {
|
|
23
|
+
db;
|
|
24
|
+
BATCH_SIZE = 1000;
|
|
25
|
+
resilience;
|
|
26
|
+
logger;
|
|
27
|
+
dbPath;
|
|
28
|
+
constructor(dbPath) {
|
|
29
|
+
const wyrmDir = join(homedir(), '.wyrm');
|
|
30
|
+
if (!existsSync(wyrmDir)) {
|
|
31
|
+
mkdirSync(wyrmDir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
this.dbPath = dbPath || join(wyrmDir, 'wyrm.db');
|
|
34
|
+
this.logger = new WyrmLogger();
|
|
35
|
+
this.resilience = getResilienceManager();
|
|
36
|
+
// Initialize database with resilience
|
|
37
|
+
this.db = this.initializeDatabase(this.dbPath);
|
|
38
|
+
// Enable WAL mode for better concurrent performance and crash recovery
|
|
39
|
+
this.db.pragma('journal_mode = WAL');
|
|
40
|
+
this.db.pragma('synchronous = NORMAL');
|
|
41
|
+
this.db.pragma('cache_size = -64000'); // 64MB cache
|
|
42
|
+
this.db.pragma('temp_store = MEMORY');
|
|
43
|
+
this.db.pragma('busy_timeout = 5000'); // Wait 5s for locks
|
|
44
|
+
this.db.pragma('mmap_size = 268435456'); // 256MB memory-mapped I/O
|
|
45
|
+
this.db.pragma('page_size = 4096'); // Optimal page size
|
|
46
|
+
this.init();
|
|
47
|
+
// Recover any incomplete operations from previous session
|
|
48
|
+
this.recoverIncompleteOperations();
|
|
49
|
+
}
|
|
50
|
+
/** Expose the raw database instance for analytics and other modules */
|
|
51
|
+
getDatabase() {
|
|
52
|
+
return this.db;
|
|
53
|
+
}
|
|
54
|
+
/** Get the database file path */
|
|
55
|
+
getDatabasePath() {
|
|
56
|
+
return this.dbPath;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Initialize database with retry logic for handling corruption/locks
|
|
60
|
+
*/
|
|
61
|
+
initializeDatabase(path) {
|
|
62
|
+
const result = this.resilience.withRetrySync(() => new Database(path), 'database_init', { maxAttempts: 3, baseDelayMs: 500 });
|
|
63
|
+
if (!result.success) {
|
|
64
|
+
this.logger.error('Failed to initialize database', { path, error: result.error?.message });
|
|
65
|
+
throw result.error || new Error('Database initialization failed');
|
|
66
|
+
}
|
|
67
|
+
return result.data;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Recover incomplete operations from previous session
|
|
71
|
+
*/
|
|
72
|
+
recoverIncompleteOperations() {
|
|
73
|
+
const incomplete = this.resilience.getIncompleteOperations();
|
|
74
|
+
for (const op of incomplete) {
|
|
75
|
+
this.logger.warn('Found incomplete operation from previous session', {
|
|
76
|
+
operation: op.operation,
|
|
77
|
+
stage: op.stage,
|
|
78
|
+
id: op.id,
|
|
79
|
+
});
|
|
80
|
+
// For now, just log - specific recovery logic can be added
|
|
81
|
+
// based on operation type
|
|
82
|
+
if (op.operation === 'batch_insert') {
|
|
83
|
+
this.logger.info('Batch insert was incomplete - data may need re-import');
|
|
84
|
+
}
|
|
85
|
+
// Mark as handled
|
|
86
|
+
this.resilience.completeCheckpoint(op.id);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
init() {
|
|
90
|
+
this.db.exec(`
|
|
91
|
+
-- Core tables
|
|
92
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
93
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
94
|
+
name TEXT NOT NULL,
|
|
95
|
+
path TEXT UNIQUE NOT NULL,
|
|
96
|
+
repo TEXT,
|
|
97
|
+
stack TEXT,
|
|
98
|
+
last_commit TEXT,
|
|
99
|
+
branch TEXT,
|
|
100
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
101
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
105
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
106
|
+
project_id INTEGER NOT NULL,
|
|
107
|
+
date TEXT NOT NULL,
|
|
108
|
+
objectives TEXT,
|
|
109
|
+
completed TEXT,
|
|
110
|
+
issues TEXT,
|
|
111
|
+
commits TEXT,
|
|
112
|
+
files_changed TEXT,
|
|
113
|
+
notes TEXT,
|
|
114
|
+
summary TEXT,
|
|
115
|
+
tokens_estimate INTEGER DEFAULT 0,
|
|
116
|
+
is_archived INTEGER DEFAULT 0,
|
|
117
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
118
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
CREATE TABLE IF NOT EXISTS quests (
|
|
122
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
123
|
+
project_id INTEGER NOT NULL,
|
|
124
|
+
title TEXT NOT NULL,
|
|
125
|
+
description TEXT,
|
|
126
|
+
priority TEXT DEFAULT 'medium',
|
|
127
|
+
status TEXT DEFAULT 'pending',
|
|
128
|
+
tags TEXT,
|
|
129
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
130
|
+
completed_at TEXT,
|
|
131
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
CREATE TABLE IF NOT EXISTS context (
|
|
135
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
136
|
+
project_id INTEGER NOT NULL,
|
|
137
|
+
key TEXT NOT NULL,
|
|
138
|
+
value TEXT,
|
|
139
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
140
|
+
UNIQUE(project_id, key),
|
|
141
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
-- Data lake tables for large datasets
|
|
145
|
+
CREATE TABLE IF NOT EXISTS data_lake (
|
|
146
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
147
|
+
project_id INTEGER NOT NULL,
|
|
148
|
+
category TEXT NOT NULL,
|
|
149
|
+
key TEXT NOT NULL,
|
|
150
|
+
value TEXT,
|
|
151
|
+
metadata TEXT,
|
|
152
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
153
|
+
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
-- Watch directories for auto-discovery
|
|
157
|
+
CREATE TABLE IF NOT EXISTS watch_dirs (
|
|
158
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
159
|
+
path TEXT UNIQUE NOT NULL,
|
|
160
|
+
recursive INTEGER DEFAULT 1,
|
|
161
|
+
last_scan TEXT
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
-- Global context (cross-project)
|
|
165
|
+
CREATE TABLE IF NOT EXISTS global_context (
|
|
166
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
167
|
+
key TEXT UNIQUE NOT NULL,
|
|
168
|
+
value TEXT,
|
|
169
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
-- Skills management for Copilot skill integration
|
|
173
|
+
CREATE TABLE IF NOT EXISTS skills (
|
|
174
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
175
|
+
name TEXT UNIQUE NOT NULL,
|
|
176
|
+
description TEXT,
|
|
177
|
+
skill_path TEXT NOT NULL,
|
|
178
|
+
category TEXT,
|
|
179
|
+
author TEXT,
|
|
180
|
+
version TEXT,
|
|
181
|
+
tags TEXT,
|
|
182
|
+
is_active INTEGER DEFAULT 1,
|
|
183
|
+
usage_count INTEGER DEFAULT 0,
|
|
184
|
+
last_used TEXT,
|
|
185
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
186
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
-- Indexes for performance
|
|
190
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project_id);
|
|
191
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_date ON sessions(date);
|
|
192
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_archived ON sessions(is_archived);
|
|
193
|
+
CREATE INDEX IF NOT EXISTS idx_quests_project ON quests(project_id);
|
|
194
|
+
CREATE INDEX IF NOT EXISTS idx_quests_status ON quests(status);
|
|
195
|
+
CREATE INDEX IF NOT EXISTS idx_quests_priority ON quests(priority);
|
|
196
|
+
CREATE INDEX IF NOT EXISTS idx_context_project ON context(project_id);
|
|
197
|
+
CREATE INDEX IF NOT EXISTS idx_data_lake_project ON data_lake(project_id);
|
|
198
|
+
CREATE INDEX IF NOT EXISTS idx_data_lake_category ON data_lake(category);
|
|
199
|
+
CREATE INDEX IF NOT EXISTS idx_data_lake_key ON data_lake(key);
|
|
200
|
+
CREATE INDEX IF NOT EXISTS idx_skills_name ON skills(name);
|
|
201
|
+
CREATE INDEX IF NOT EXISTS idx_skills_category ON skills(category);
|
|
202
|
+
CREATE INDEX IF NOT EXISTS idx_skills_active ON skills(is_active);
|
|
203
|
+
|
|
204
|
+
-- Full-text search for fast queries
|
|
205
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS sessions_fts USING fts5(
|
|
206
|
+
objectives, completed, issues, notes, summary,
|
|
207
|
+
content='sessions', content_rowid='id'
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS quests_fts USING fts5(
|
|
211
|
+
title, description,
|
|
212
|
+
content='quests', content_rowid='id'
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS data_lake_fts USING fts5(
|
|
216
|
+
key, value,
|
|
217
|
+
content='data_lake', content_rowid='id'
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS skills_fts USING fts5(
|
|
221
|
+
name, description, tags,
|
|
222
|
+
content='skills', content_rowid='id'
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
-- Triggers to keep FTS in sync
|
|
226
|
+
CREATE TRIGGER IF NOT EXISTS sessions_ai AFTER INSERT ON sessions BEGIN
|
|
227
|
+
INSERT INTO sessions_fts(rowid, objectives, completed, issues, notes, summary)
|
|
228
|
+
VALUES (new.id, new.objectives, new.completed, new.issues, new.notes, new.summary);
|
|
229
|
+
END;
|
|
230
|
+
|
|
231
|
+
CREATE TRIGGER IF NOT EXISTS sessions_ad AFTER DELETE ON sessions BEGIN
|
|
232
|
+
INSERT INTO sessions_fts(sessions_fts, rowid, objectives, completed, issues, notes, summary)
|
|
233
|
+
VALUES('delete', old.id, old.objectives, old.completed, old.issues, old.notes, old.summary);
|
|
234
|
+
END;
|
|
235
|
+
|
|
236
|
+
CREATE TRIGGER IF NOT EXISTS quests_ai AFTER INSERT ON quests BEGIN
|
|
237
|
+
INSERT INTO quests_fts(rowid, title, description) VALUES (new.id, new.title, new.description);
|
|
238
|
+
END;
|
|
239
|
+
|
|
240
|
+
CREATE TRIGGER IF NOT EXISTS quests_ad AFTER DELETE ON quests BEGIN
|
|
241
|
+
INSERT INTO quests_fts(quests_fts, rowid, title, description)
|
|
242
|
+
VALUES('delete', old.id, old.title, old.description);
|
|
243
|
+
END;
|
|
244
|
+
|
|
245
|
+
CREATE TRIGGER IF NOT EXISTS skills_ai AFTER INSERT ON skills BEGIN
|
|
246
|
+
INSERT INTO skills_fts(rowid, name, description, tags) VALUES (new.id, new.name, new.description, new.tags);
|
|
247
|
+
END;
|
|
248
|
+
|
|
249
|
+
CREATE TRIGGER IF NOT EXISTS skills_ad AFTER DELETE ON skills BEGIN
|
|
250
|
+
INSERT INTO skills_fts(skills_fts, rowid, name, description, tags)
|
|
251
|
+
VALUES('delete', old.id, old.name, old.description, old.tags);
|
|
252
|
+
END;
|
|
253
|
+
|
|
254
|
+
-- UPDATE triggers to keep FTS in sync on updates
|
|
255
|
+
CREATE TRIGGER IF NOT EXISTS sessions_au AFTER UPDATE ON sessions BEGIN
|
|
256
|
+
INSERT INTO sessions_fts(sessions_fts, rowid, objectives, completed, issues, notes, summary)
|
|
257
|
+
VALUES('delete', old.id, old.objectives, old.completed, old.issues, old.notes, old.summary);
|
|
258
|
+
INSERT INTO sessions_fts(rowid, objectives, completed, issues, notes, summary)
|
|
259
|
+
VALUES (new.id, new.objectives, new.completed, new.issues, new.notes, new.summary);
|
|
260
|
+
END;
|
|
261
|
+
|
|
262
|
+
CREATE TRIGGER IF NOT EXISTS quests_au AFTER UPDATE ON quests BEGIN
|
|
263
|
+
INSERT INTO quests_fts(quests_fts, rowid, title, description)
|
|
264
|
+
VALUES('delete', old.id, old.title, old.description);
|
|
265
|
+
INSERT INTO quests_fts(rowid, title, description) VALUES (new.id, new.title, new.description);
|
|
266
|
+
END;
|
|
267
|
+
|
|
268
|
+
CREATE TRIGGER IF NOT EXISTS skills_au AFTER UPDATE ON skills BEGIN
|
|
269
|
+
INSERT INTO skills_fts(skills_fts, rowid, name, description, tags)
|
|
270
|
+
VALUES('delete', old.id, old.name, old.description, old.tags);
|
|
271
|
+
INSERT INTO skills_fts(rowid, name, description, tags) VALUES (new.id, new.name, new.description, new.tags);
|
|
272
|
+
END;
|
|
273
|
+
|
|
274
|
+
-- data_lake FTS triggers (were missing entirely)
|
|
275
|
+
CREATE TRIGGER IF NOT EXISTS data_lake_ai AFTER INSERT ON data_lake BEGIN
|
|
276
|
+
INSERT INTO data_lake_fts(rowid, key, value) VALUES (new.id, new.key, new.value);
|
|
277
|
+
END;
|
|
278
|
+
|
|
279
|
+
CREATE TRIGGER IF NOT EXISTS data_lake_ad AFTER DELETE ON data_lake BEGIN
|
|
280
|
+
INSERT INTO data_lake_fts(data_lake_fts, rowid, key, value)
|
|
281
|
+
VALUES('delete', old.id, old.key, old.value);
|
|
282
|
+
END;
|
|
283
|
+
|
|
284
|
+
CREATE TRIGGER IF NOT EXISTS data_lake_au AFTER UPDATE ON data_lake BEGIN
|
|
285
|
+
INSERT INTO data_lake_fts(data_lake_fts, rowid, key, value)
|
|
286
|
+
VALUES('delete', old.id, old.key, old.value);
|
|
287
|
+
INSERT INTO data_lake_fts(rowid, key, value) VALUES (new.id, new.key, new.value);
|
|
288
|
+
END;
|
|
289
|
+
`);
|
|
290
|
+
}
|
|
291
|
+
// ==================== WATCH DIRECTORIES ====================
|
|
292
|
+
addWatchDir(path, recursive = true) {
|
|
293
|
+
return this.db.prepare(`
|
|
294
|
+
INSERT INTO watch_dirs (path, recursive)
|
|
295
|
+
VALUES (?, ?)
|
|
296
|
+
ON CONFLICT(path) DO UPDATE SET recursive = excluded.recursive
|
|
297
|
+
RETURNING *
|
|
298
|
+
`).get(path, recursive ? 1 : 0);
|
|
299
|
+
}
|
|
300
|
+
getWatchDirs() {
|
|
301
|
+
return this.db.prepare('SELECT * FROM watch_dirs').all();
|
|
302
|
+
}
|
|
303
|
+
removeWatchDir(path) {
|
|
304
|
+
this.db.prepare('DELETE FROM watch_dirs WHERE path = ?').run(path);
|
|
305
|
+
}
|
|
306
|
+
// ==================== AUTO-DISCOVERY ====================
|
|
307
|
+
scanForProjects(rootPath, recursive = true) {
|
|
308
|
+
const discovered = [];
|
|
309
|
+
const scan = (dir, depth = 0) => {
|
|
310
|
+
if (depth > 3 && recursive)
|
|
311
|
+
return; // Max 3 levels deep
|
|
312
|
+
try {
|
|
313
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
314
|
+
for (const entry of entries) {
|
|
315
|
+
if (!entry.isDirectory())
|
|
316
|
+
continue;
|
|
317
|
+
if (entry.name.startsWith('.') && entry.name !== '.git')
|
|
318
|
+
continue;
|
|
319
|
+
const fullPath = join(dir, entry.name);
|
|
320
|
+
// Check if it's a git repo
|
|
321
|
+
const gitDir = join(fullPath, '.git');
|
|
322
|
+
if (existsSync(gitDir)) {
|
|
323
|
+
const project = this.registerProjectFromPath(fullPath);
|
|
324
|
+
if (project)
|
|
325
|
+
discovered.push(project);
|
|
326
|
+
}
|
|
327
|
+
else if (recursive && depth < 3) {
|
|
328
|
+
scan(fullPath, depth + 1);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
// Skip inaccessible directories
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
scan(rootPath);
|
|
337
|
+
// Update last scan time
|
|
338
|
+
this.db.prepare(`
|
|
339
|
+
UPDATE watch_dirs SET last_scan = datetime('now') WHERE path = ?
|
|
340
|
+
`).run(rootPath);
|
|
341
|
+
return discovered;
|
|
342
|
+
}
|
|
343
|
+
scanAllWatchDirs() {
|
|
344
|
+
const dirs = this.getWatchDirs();
|
|
345
|
+
const all = [];
|
|
346
|
+
for (const dir of dirs) {
|
|
347
|
+
const found = this.scanForProjects(dir.path, !!dir.recursive);
|
|
348
|
+
all.push(...found);
|
|
349
|
+
}
|
|
350
|
+
return all;
|
|
351
|
+
}
|
|
352
|
+
registerProjectFromPath(projectPath) {
|
|
353
|
+
try {
|
|
354
|
+
// SECURITY: Validate path is a real directory before any operations
|
|
355
|
+
const normalizedPath = normalize(resolve(projectPath));
|
|
356
|
+
if (!existsSync(normalizedPath) || !statSync(normalizedPath).isDirectory()) {
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
const name = basename(normalizedPath);
|
|
360
|
+
let repo;
|
|
361
|
+
let branch;
|
|
362
|
+
let lastCommit;
|
|
363
|
+
let stack;
|
|
364
|
+
try {
|
|
365
|
+
// SECURITY: Use spawnSync with shell: false to prevent command injection
|
|
366
|
+
const repoResult = spawnSync('git', ['config', '--get', 'remote.origin.url'], {
|
|
367
|
+
cwd: normalizedPath,
|
|
368
|
+
encoding: 'utf-8',
|
|
369
|
+
timeout: 5000,
|
|
370
|
+
shell: false // CRITICAL: No shell interpretation
|
|
371
|
+
});
|
|
372
|
+
if (repoResult.status === 0) {
|
|
373
|
+
repo = repoResult.stdout.trim();
|
|
374
|
+
}
|
|
375
|
+
const branchResult = spawnSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
|
|
376
|
+
cwd: normalizedPath,
|
|
377
|
+
encoding: 'utf-8',
|
|
378
|
+
timeout: 5000,
|
|
379
|
+
shell: false
|
|
380
|
+
});
|
|
381
|
+
if (branchResult.status === 0) {
|
|
382
|
+
branch = branchResult.stdout.trim();
|
|
383
|
+
}
|
|
384
|
+
const commitResult = spawnSync('git', ['log', '-1', '--format=%h %s'], {
|
|
385
|
+
cwd: normalizedPath,
|
|
386
|
+
encoding: 'utf-8',
|
|
387
|
+
timeout: 5000,
|
|
388
|
+
shell: false
|
|
389
|
+
});
|
|
390
|
+
if (commitResult.status === 0) {
|
|
391
|
+
lastCommit = commitResult.stdout.trim();
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
// Not a git repo or git not available
|
|
396
|
+
}
|
|
397
|
+
// Detect stack
|
|
398
|
+
if (existsSync(join(normalizedPath, 'package.json'))) {
|
|
399
|
+
stack = 'Node.js';
|
|
400
|
+
if (existsSync(join(normalizedPath, 'next.config.js')) ||
|
|
401
|
+
existsSync(join(normalizedPath, 'next.config.ts')) ||
|
|
402
|
+
existsSync(join(normalizedPath, 'next.config.mjs'))) {
|
|
403
|
+
stack = 'Next.js';
|
|
404
|
+
}
|
|
405
|
+
else if (existsSync(join(normalizedPath, 'vite.config.ts'))) {
|
|
406
|
+
stack = 'Vite';
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
else if (existsSync(join(normalizedPath, 'requirements.txt')) ||
|
|
410
|
+
existsSync(join(normalizedPath, 'pyproject.toml'))) {
|
|
411
|
+
stack = 'Python';
|
|
412
|
+
}
|
|
413
|
+
else if (existsSync(join(normalizedPath, 'composer.json'))) {
|
|
414
|
+
stack = 'PHP';
|
|
415
|
+
}
|
|
416
|
+
else if (existsSync(join(normalizedPath, 'Cargo.toml'))) {
|
|
417
|
+
stack = 'Rust';
|
|
418
|
+
}
|
|
419
|
+
else if (existsSync(join(normalizedPath, 'go.mod'))) {
|
|
420
|
+
stack = 'Go';
|
|
421
|
+
}
|
|
422
|
+
return this.registerProject(name, normalizedPath, repo, stack, lastCommit, branch);
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
return null;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
// ==================== PROJECTS ====================
|
|
429
|
+
registerProject(name, path, repo, stack, lastCommit, branch) {
|
|
430
|
+
const stmt = this.db.prepare(`
|
|
431
|
+
INSERT INTO projects (name, path, repo, stack, last_commit, branch)
|
|
432
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
433
|
+
ON CONFLICT(path) DO UPDATE SET
|
|
434
|
+
name = excluded.name,
|
|
435
|
+
repo = COALESCE(excluded.repo, projects.repo),
|
|
436
|
+
stack = COALESCE(excluded.stack, projects.stack),
|
|
437
|
+
last_commit = COALESCE(excluded.last_commit, projects.last_commit),
|
|
438
|
+
branch = COALESCE(excluded.branch, projects.branch),
|
|
439
|
+
updated_at = datetime('now')
|
|
440
|
+
RETURNING *
|
|
441
|
+
`);
|
|
442
|
+
return stmt.get(name, path, repo || null, stack || null, lastCommit || null, branch || null);
|
|
443
|
+
}
|
|
444
|
+
getProject(path) {
|
|
445
|
+
return this.db.prepare('SELECT * FROM projects WHERE path = ?').get(path);
|
|
446
|
+
}
|
|
447
|
+
getProjectById(id) {
|
|
448
|
+
return this.db.prepare('SELECT * FROM projects WHERE id = ?').get(id);
|
|
449
|
+
}
|
|
450
|
+
getProjectByName(name) {
|
|
451
|
+
return this.db.prepare('SELECT * FROM projects WHERE name = ?').get(name);
|
|
452
|
+
}
|
|
453
|
+
getAllProjects(limit = 100, offset = 0) {
|
|
454
|
+
return this.db.prepare(`
|
|
455
|
+
SELECT * FROM projects ORDER BY updated_at DESC LIMIT ? OFFSET ?
|
|
456
|
+
`).all(limit, offset);
|
|
457
|
+
}
|
|
458
|
+
searchProjects(query) {
|
|
459
|
+
const pattern = `%${query}%`;
|
|
460
|
+
return this.db.prepare(`
|
|
461
|
+
SELECT * FROM projects
|
|
462
|
+
WHERE name LIKE ? OR stack LIKE ? OR repo LIKE ?
|
|
463
|
+
ORDER BY updated_at DESC
|
|
464
|
+
LIMIT 50
|
|
465
|
+
`).all(pattern, pattern, pattern);
|
|
466
|
+
}
|
|
467
|
+
// ==================== SESSIONS ====================
|
|
468
|
+
createSession(projectId, data) {
|
|
469
|
+
const tokensEstimate = this.estimateTokens((data.objectives || '') + (data.completed || '') + (data.issues || '') + (data.notes || ''));
|
|
470
|
+
const result = this.resilience.withRetrySync(() => {
|
|
471
|
+
const stmt = this.db.prepare(`
|
|
472
|
+
INSERT INTO sessions (project_id, date, objectives, completed, issues, commits, files_changed, notes, tokens_estimate)
|
|
473
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
474
|
+
RETURNING *
|
|
475
|
+
`);
|
|
476
|
+
return stmt.get(projectId, data.date || new Date().toISOString().split('T')[0], data.objectives || '', data.completed || '', data.issues || '', data.commits || '', data.files_changed || '', data.notes || '', tokensEstimate);
|
|
477
|
+
}, 'createSession');
|
|
478
|
+
if (!result.success) {
|
|
479
|
+
throw result.error || new Error('Failed to create session');
|
|
480
|
+
}
|
|
481
|
+
return result.data;
|
|
482
|
+
}
|
|
483
|
+
updateSession(id, data) {
|
|
484
|
+
const updates = [];
|
|
485
|
+
const values = [];
|
|
486
|
+
for (const [key, value] of Object.entries(data)) {
|
|
487
|
+
if (key !== 'id' && key !== 'project_id' && key !== 'created_at') {
|
|
488
|
+
updates.push(`${key} = ?`);
|
|
489
|
+
values.push(value);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (updates.length === 0)
|
|
493
|
+
return this.getSession(id);
|
|
494
|
+
// Recalculate tokens if content changed
|
|
495
|
+
if (data.objectives || data.completed || data.issues || data.notes) {
|
|
496
|
+
const session = this.getSession(id);
|
|
497
|
+
if (session) {
|
|
498
|
+
const newTokens = this.estimateTokens((data.objectives || session.objectives) +
|
|
499
|
+
(data.completed || session.completed) +
|
|
500
|
+
(data.issues || session.issues) +
|
|
501
|
+
(data.notes || session.notes));
|
|
502
|
+
updates.push('tokens_estimate = ?');
|
|
503
|
+
values.push(newTokens);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
values.push(id);
|
|
507
|
+
const result = this.resilience.withRetrySync(() => {
|
|
508
|
+
const stmt = this.db.prepare(`
|
|
509
|
+
UPDATE sessions SET ${updates.join(', ')} WHERE id = ? RETURNING *
|
|
510
|
+
`);
|
|
511
|
+
return stmt.get(...values);
|
|
512
|
+
}, 'updateSession');
|
|
513
|
+
if (!result.success) {
|
|
514
|
+
throw result.error || new Error('Failed to update session');
|
|
515
|
+
}
|
|
516
|
+
return result.data;
|
|
517
|
+
}
|
|
518
|
+
getSession(id) {
|
|
519
|
+
return this.db.prepare('SELECT * FROM sessions WHERE id = ?').get(id);
|
|
520
|
+
}
|
|
521
|
+
getRecentSessions(projectId, limit = 5) {
|
|
522
|
+
return this.db.prepare(`
|
|
523
|
+
SELECT * FROM sessions
|
|
524
|
+
WHERE project_id = ? AND is_archived = 0
|
|
525
|
+
ORDER BY date DESC, id DESC
|
|
526
|
+
LIMIT ?
|
|
527
|
+
`).all(projectId, limit);
|
|
528
|
+
}
|
|
529
|
+
getTodaySession(projectId) {
|
|
530
|
+
const today = new Date().toISOString().split('T')[0];
|
|
531
|
+
return this.db.prepare(`
|
|
532
|
+
SELECT * FROM sessions WHERE project_id = ? AND date = ?
|
|
533
|
+
`).get(projectId, today);
|
|
534
|
+
}
|
|
535
|
+
searchSessions(query, projectId) {
|
|
536
|
+
if (projectId) {
|
|
537
|
+
return this.db.prepare(`
|
|
538
|
+
SELECT s.* FROM sessions s
|
|
539
|
+
JOIN sessions_fts fts ON s.id = fts.rowid
|
|
540
|
+
WHERE sessions_fts MATCH ? AND s.project_id = ?
|
|
541
|
+
ORDER BY s.date DESC
|
|
542
|
+
LIMIT 50
|
|
543
|
+
`).all(query, projectId);
|
|
544
|
+
}
|
|
545
|
+
return this.db.prepare(`
|
|
546
|
+
SELECT s.* FROM sessions s
|
|
547
|
+
JOIN sessions_fts fts ON s.id = fts.rowid
|
|
548
|
+
WHERE sessions_fts MATCH ?
|
|
549
|
+
ORDER BY s.date DESC
|
|
550
|
+
LIMIT 50
|
|
551
|
+
`).all(query);
|
|
552
|
+
}
|
|
553
|
+
archiveOldSessions(projectId, keepRecent = 10) {
|
|
554
|
+
const result = this.db.prepare(`
|
|
555
|
+
UPDATE sessions
|
|
556
|
+
SET is_archived = 1
|
|
557
|
+
WHERE project_id = ?
|
|
558
|
+
AND is_archived = 0
|
|
559
|
+
AND id NOT IN (
|
|
560
|
+
SELECT id FROM sessions
|
|
561
|
+
WHERE project_id = ?
|
|
562
|
+
ORDER BY date DESC, id DESC
|
|
563
|
+
LIMIT ?
|
|
564
|
+
)
|
|
565
|
+
`).run(projectId, projectId, keepRecent);
|
|
566
|
+
return result.changes;
|
|
567
|
+
}
|
|
568
|
+
getSessionTokenUsage(projectId) {
|
|
569
|
+
const result = this.db.prepare(`
|
|
570
|
+
SELECT COALESCE(SUM(tokens_estimate), 0) as total
|
|
571
|
+
FROM sessions WHERE project_id = ? AND is_archived = 0
|
|
572
|
+
`).get(projectId);
|
|
573
|
+
return result.total;
|
|
574
|
+
}
|
|
575
|
+
// ==================== QUESTS ====================
|
|
576
|
+
addQuest(projectId, title, description, priority = 'medium', tags) {
|
|
577
|
+
return this.db.prepare(`
|
|
578
|
+
INSERT INTO quests (project_id, title, description, priority, tags)
|
|
579
|
+
VALUES (?, ?, ?, ?, ?)
|
|
580
|
+
RETURNING *
|
|
581
|
+
`).get(projectId, title, description || '', priority, tags || null);
|
|
582
|
+
}
|
|
583
|
+
updateQuest(id, status) {
|
|
584
|
+
const completedAt = status === 'completed' ? new Date().toISOString() : null;
|
|
585
|
+
return this.db.prepare(`
|
|
586
|
+
UPDATE quests SET status = ?, completed_at = ? WHERE id = ? RETURNING *
|
|
587
|
+
`).get(status, completedAt, id);
|
|
588
|
+
}
|
|
589
|
+
getPendingQuests(projectId) {
|
|
590
|
+
return this.db.prepare(`
|
|
591
|
+
SELECT * FROM quests
|
|
592
|
+
WHERE project_id = ? AND status IN ('pending', 'in_progress')
|
|
593
|
+
ORDER BY
|
|
594
|
+
CASE priority
|
|
595
|
+
WHEN 'critical' THEN 1
|
|
596
|
+
WHEN 'high' THEN 2
|
|
597
|
+
WHEN 'medium' THEN 3
|
|
598
|
+
WHEN 'low' THEN 4
|
|
599
|
+
END,
|
|
600
|
+
created_at ASC
|
|
601
|
+
`).all(projectId);
|
|
602
|
+
}
|
|
603
|
+
getAllPendingQuests() {
|
|
604
|
+
return this.db.prepare(`
|
|
605
|
+
SELECT q.*, p.name as project_name FROM quests q
|
|
606
|
+
JOIN projects p ON q.project_id = p.id
|
|
607
|
+
WHERE q.status IN ('pending', 'in_progress')
|
|
608
|
+
ORDER BY
|
|
609
|
+
CASE q.priority
|
|
610
|
+
WHEN 'critical' THEN 1
|
|
611
|
+
WHEN 'high' THEN 2
|
|
612
|
+
WHEN 'medium' THEN 3
|
|
613
|
+
WHEN 'low' THEN 4
|
|
614
|
+
END,
|
|
615
|
+
q.created_at ASC
|
|
616
|
+
`).all();
|
|
617
|
+
}
|
|
618
|
+
searchQuests(query) {
|
|
619
|
+
return this.db.prepare(`
|
|
620
|
+
SELECT q.* FROM quests q
|
|
621
|
+
JOIN quests_fts fts ON q.id = fts.rowid
|
|
622
|
+
WHERE quests_fts MATCH ?
|
|
623
|
+
ORDER BY q.created_at DESC
|
|
624
|
+
LIMIT 50
|
|
625
|
+
`).all(query);
|
|
626
|
+
}
|
|
627
|
+
getRecentlyCompleted(projectId, limit = 5) {
|
|
628
|
+
return this.db.prepare(`
|
|
629
|
+
SELECT * FROM quests
|
|
630
|
+
WHERE project_id = ? AND status = 'completed'
|
|
631
|
+
ORDER BY completed_at DESC
|
|
632
|
+
LIMIT ?
|
|
633
|
+
`).all(projectId, limit);
|
|
634
|
+
}
|
|
635
|
+
// ==================== CONTEXT ====================
|
|
636
|
+
setContext(projectId, key, value) {
|
|
637
|
+
this.db.prepare(`
|
|
638
|
+
INSERT INTO context (project_id, key, value)
|
|
639
|
+
VALUES (?, ?, ?)
|
|
640
|
+
ON CONFLICT(project_id, key) DO UPDATE SET
|
|
641
|
+
value = excluded.value,
|
|
642
|
+
updated_at = datetime('now')
|
|
643
|
+
`).run(projectId, key, value);
|
|
644
|
+
}
|
|
645
|
+
getContext(projectId, key) {
|
|
646
|
+
const row = this.db.prepare(`
|
|
647
|
+
SELECT value FROM context WHERE project_id = ? AND key = ?
|
|
648
|
+
`).get(projectId, key);
|
|
649
|
+
return row?.value;
|
|
650
|
+
}
|
|
651
|
+
getAllContext(projectId) {
|
|
652
|
+
const rows = this.db.prepare(`
|
|
653
|
+
SELECT key, value FROM context WHERE project_id = ?
|
|
654
|
+
`).all(projectId);
|
|
655
|
+
const result = {};
|
|
656
|
+
for (const row of rows) {
|
|
657
|
+
result[row.key] = row.value;
|
|
658
|
+
}
|
|
659
|
+
return result;
|
|
660
|
+
}
|
|
661
|
+
// ==================== GLOBAL CONTEXT ====================
|
|
662
|
+
setGlobalContext(key, value) {
|
|
663
|
+
this.db.prepare(`
|
|
664
|
+
INSERT INTO global_context (key, value)
|
|
665
|
+
VALUES (?, ?)
|
|
666
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
667
|
+
value = excluded.value,
|
|
668
|
+
updated_at = datetime('now')
|
|
669
|
+
`).run(key, value);
|
|
670
|
+
}
|
|
671
|
+
getGlobalContext(key) {
|
|
672
|
+
const row = this.db.prepare(`
|
|
673
|
+
SELECT value FROM global_context WHERE key = ?
|
|
674
|
+
`).get(key);
|
|
675
|
+
return row?.value;
|
|
676
|
+
}
|
|
677
|
+
getAllGlobalContext() {
|
|
678
|
+
const rows = this.db.prepare('SELECT key, value FROM global_context').all();
|
|
679
|
+
const result = {};
|
|
680
|
+
for (const row of rows) {
|
|
681
|
+
result[row.key] = row.value;
|
|
682
|
+
}
|
|
683
|
+
return result;
|
|
684
|
+
}
|
|
685
|
+
// ==================== SKILLS MANAGEMENT ====================
|
|
686
|
+
registerSkill(name, description, skillPath, category, author, version, tags) {
|
|
687
|
+
const result = this.resilience.withRetrySync(() => this.db.prepare(`
|
|
688
|
+
INSERT INTO skills (name, description, skill_path, category, author, version, tags, is_active, usage_count)
|
|
689
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 1, 0)
|
|
690
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
691
|
+
description = excluded.description,
|
|
692
|
+
skill_path = excluded.skill_path,
|
|
693
|
+
category = excluded.category,
|
|
694
|
+
author = excluded.author,
|
|
695
|
+
version = excluded.version,
|
|
696
|
+
tags = excluded.tags,
|
|
697
|
+
updated_at = datetime('now'),
|
|
698
|
+
is_active = 1
|
|
699
|
+
RETURNING *
|
|
700
|
+
`).get(name, description, skillPath, category || null, author || null, version || null, tags || null), 'registerSkill');
|
|
701
|
+
if (!result.success) {
|
|
702
|
+
throw result.error || new Error('Failed to register skill');
|
|
703
|
+
}
|
|
704
|
+
return result.data;
|
|
705
|
+
}
|
|
706
|
+
getSkill(name) {
|
|
707
|
+
const skill = this.db.prepare('SELECT * FROM skills WHERE name = ?').get(name);
|
|
708
|
+
if (skill) {
|
|
709
|
+
// Update last_used
|
|
710
|
+
this.db.prepare('UPDATE skills SET last_used = datetime(\'now\'), usage_count = usage_count + 1 WHERE id = ?').run(skill.id);
|
|
711
|
+
}
|
|
712
|
+
return skill;
|
|
713
|
+
}
|
|
714
|
+
listSkills(active, category, search) {
|
|
715
|
+
let query = 'SELECT * FROM skills WHERE 1=1';
|
|
716
|
+
const params = [];
|
|
717
|
+
if (active !== undefined) {
|
|
718
|
+
query += ' AND is_active = ?';
|
|
719
|
+
params.push(active ? 1 : 0);
|
|
720
|
+
}
|
|
721
|
+
if (category) {
|
|
722
|
+
query += ' AND category = ?';
|
|
723
|
+
params.push(category);
|
|
724
|
+
}
|
|
725
|
+
if (search) {
|
|
726
|
+
query += ' AND id IN (SELECT rowid FROM skills_fts WHERE skills_fts MATCH ?)';
|
|
727
|
+
params.push(search);
|
|
728
|
+
}
|
|
729
|
+
query += ' ORDER BY updated_at DESC';
|
|
730
|
+
return this.db.prepare(query).all(...params);
|
|
731
|
+
}
|
|
732
|
+
searchSkills(query, limit = 20) {
|
|
733
|
+
return this.db.prepare(`
|
|
734
|
+
SELECT s.* FROM skills s
|
|
735
|
+
JOIN skills_fts fts ON s.id = fts.rowid
|
|
736
|
+
WHERE fts MATCH ?
|
|
737
|
+
ORDER BY rank
|
|
738
|
+
LIMIT ?
|
|
739
|
+
`).all(query, limit);
|
|
740
|
+
}
|
|
741
|
+
updateSkill(name, updates) {
|
|
742
|
+
const setClauses = [];
|
|
743
|
+
const values = [];
|
|
744
|
+
if (updates.description !== undefined) {
|
|
745
|
+
setClauses.push('description = ?');
|
|
746
|
+
values.push(updates.description);
|
|
747
|
+
}
|
|
748
|
+
if (updates.skill_path !== undefined) {
|
|
749
|
+
setClauses.push('skill_path = ?');
|
|
750
|
+
values.push(updates.skill_path);
|
|
751
|
+
}
|
|
752
|
+
if (updates.category !== undefined) {
|
|
753
|
+
setClauses.push('category = ?');
|
|
754
|
+
values.push(updates.category);
|
|
755
|
+
}
|
|
756
|
+
if (updates.is_active !== undefined) {
|
|
757
|
+
setClauses.push('is_active = ?');
|
|
758
|
+
values.push(updates.is_active ? 1 : 0);
|
|
759
|
+
}
|
|
760
|
+
if (updates.tags !== undefined) {
|
|
761
|
+
setClauses.push('tags = ?');
|
|
762
|
+
values.push(updates.tags);
|
|
763
|
+
}
|
|
764
|
+
if (updates.version !== undefined) {
|
|
765
|
+
setClauses.push('version = ?');
|
|
766
|
+
values.push(updates.version);
|
|
767
|
+
}
|
|
768
|
+
if (setClauses.length === 0) {
|
|
769
|
+
return this.getSkill(name);
|
|
770
|
+
}
|
|
771
|
+
setClauses.push('updated_at = datetime(\'now\')');
|
|
772
|
+
values.push(name);
|
|
773
|
+
return this.db.prepare(`
|
|
774
|
+
UPDATE skills SET ${setClauses.join(', ')} WHERE name = ? RETURNING *
|
|
775
|
+
`).get(...values);
|
|
776
|
+
}
|
|
777
|
+
deleteSkill(name) {
|
|
778
|
+
const result = this.db.prepare('DELETE FROM skills WHERE name = ?').run(name);
|
|
779
|
+
return result.changes > 0;
|
|
780
|
+
}
|
|
781
|
+
deactivateSkill(name) {
|
|
782
|
+
return this.updateSkill(name, { is_active: false });
|
|
783
|
+
}
|
|
784
|
+
activateSkill(name) {
|
|
785
|
+
return this.updateSkill(name, { is_active: true });
|
|
786
|
+
}
|
|
787
|
+
getSkillStats() {
|
|
788
|
+
const total = this.db.prepare('SELECT COUNT(*) as count FROM skills').get().count;
|
|
789
|
+
const active = this.db.prepare('SELECT COUNT(*) as count FROM skills WHERE is_active = 1').get().count;
|
|
790
|
+
const byCategoryRows = this.db.prepare(`
|
|
791
|
+
SELECT category, COUNT(*) as count FROM skills WHERE category IS NOT NULL GROUP BY category
|
|
792
|
+
`).all();
|
|
793
|
+
const byCategory = {};
|
|
794
|
+
for (const row of byCategoryRows) {
|
|
795
|
+
byCategory[row.category] = row.count;
|
|
796
|
+
}
|
|
797
|
+
return { total, active, byCategory };
|
|
798
|
+
}
|
|
799
|
+
// ==================== DATA LAKE ====================
|
|
800
|
+
insertData(projectId, category, key, value, metadata) {
|
|
801
|
+
const result = this.resilience.withRetrySync(() => this.db.prepare(`
|
|
802
|
+
INSERT INTO data_lake (project_id, category, key, value, metadata)
|
|
803
|
+
VALUES (?, ?, ?, ?, ?)
|
|
804
|
+
RETURNING *
|
|
805
|
+
`).get(projectId, category, key, value, metadata ? JSON.stringify(metadata) : null), 'insertData');
|
|
806
|
+
if (!result.success) {
|
|
807
|
+
throw result.error || new Error('Insert data failed');
|
|
808
|
+
}
|
|
809
|
+
return result.data;
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Batch insert with resilience - uses checkpointing for large batches
|
|
813
|
+
*/
|
|
814
|
+
insertDataBatch(data) {
|
|
815
|
+
const operationId = this.resilience.generateOperationId('batch_insert');
|
|
816
|
+
const batchSize = this.BATCH_SIZE;
|
|
817
|
+
let totalInserted = 0;
|
|
818
|
+
// Checkpoint for recovery
|
|
819
|
+
this.resilience.createCheckpoint(operationId, 'batch_insert', 'started', {
|
|
820
|
+
totalItems: data.length,
|
|
821
|
+
batchSize,
|
|
822
|
+
});
|
|
823
|
+
const insert = this.db.prepare(`
|
|
824
|
+
INSERT INTO data_lake (project_id, category, key, value, metadata)
|
|
825
|
+
VALUES (?, ?, ?, ?, ?)
|
|
826
|
+
`);
|
|
827
|
+
try {
|
|
828
|
+
// Process in batches for large datasets
|
|
829
|
+
for (let i = 0; i < data.length; i += batchSize) {
|
|
830
|
+
const batch = data.slice(i, i + batchSize);
|
|
831
|
+
const batchNum = Math.floor(i / batchSize) + 1;
|
|
832
|
+
this.resilience.updateCheckpoint(operationId, `batch_${batchNum}`, {
|
|
833
|
+
processed: i,
|
|
834
|
+
currentBatch: batchNum,
|
|
835
|
+
});
|
|
836
|
+
// Transaction for each batch
|
|
837
|
+
const result = this.resilience.withRetrySync(() => {
|
|
838
|
+
const insertBatch = this.db.transaction((items) => {
|
|
839
|
+
let count = 0;
|
|
840
|
+
for (const item of items) {
|
|
841
|
+
insert.run(item.projectId, item.category, item.key, item.value, item.metadata ? JSON.stringify(item.metadata) : null);
|
|
842
|
+
count++;
|
|
843
|
+
}
|
|
844
|
+
return count;
|
|
845
|
+
});
|
|
846
|
+
return insertBatch(batch);
|
|
847
|
+
}, `batch_insert_${batchNum}`, { maxAttempts: 3 });
|
|
848
|
+
if (!result.success) {
|
|
849
|
+
this.logger.error('Batch insert failed', {
|
|
850
|
+
batch: batchNum,
|
|
851
|
+
processed: totalInserted,
|
|
852
|
+
error: result.error?.message,
|
|
853
|
+
});
|
|
854
|
+
// Return what was successfully inserted
|
|
855
|
+
this.resilience.updateCheckpoint(operationId, 'partial_failure', {
|
|
856
|
+
inserted: totalInserted,
|
|
857
|
+
failedAt: i,
|
|
858
|
+
});
|
|
859
|
+
return totalInserted;
|
|
860
|
+
}
|
|
861
|
+
totalInserted += result.data;
|
|
862
|
+
}
|
|
863
|
+
this.resilience.completeCheckpoint(operationId);
|
|
864
|
+
return totalInserted;
|
|
865
|
+
}
|
|
866
|
+
catch (error) {
|
|
867
|
+
this.logger.error('Batch insert exception', {
|
|
868
|
+
inserted: totalInserted,
|
|
869
|
+
error: error.message,
|
|
870
|
+
});
|
|
871
|
+
this.resilience.updateCheckpoint(operationId, 'exception', {
|
|
872
|
+
inserted: totalInserted,
|
|
873
|
+
error: error.message,
|
|
874
|
+
});
|
|
875
|
+
return totalInserted;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
queryData(projectId, category, limit = 100, offset = 0) {
|
|
879
|
+
if (category) {
|
|
880
|
+
return this.db.prepare(`
|
|
881
|
+
SELECT * FROM data_lake
|
|
882
|
+
WHERE project_id = ? AND category = ?
|
|
883
|
+
ORDER BY created_at DESC
|
|
884
|
+
LIMIT ? OFFSET ?
|
|
885
|
+
`).all(projectId, category, limit, offset);
|
|
886
|
+
}
|
|
887
|
+
return this.db.prepare(`
|
|
888
|
+
SELECT * FROM data_lake
|
|
889
|
+
WHERE project_id = ?
|
|
890
|
+
ORDER BY created_at DESC
|
|
891
|
+
LIMIT ? OFFSET ?
|
|
892
|
+
`).all(projectId, limit, offset);
|
|
893
|
+
}
|
|
894
|
+
searchData(query, projectId) {
|
|
895
|
+
if (projectId) {
|
|
896
|
+
return this.db.prepare(`
|
|
897
|
+
SELECT d.* FROM data_lake d
|
|
898
|
+
JOIN data_lake_fts fts ON d.id = fts.rowid
|
|
899
|
+
WHERE data_lake_fts MATCH ? AND d.project_id = ?
|
|
900
|
+
ORDER BY d.created_at DESC
|
|
901
|
+
LIMIT 100
|
|
902
|
+
`).all(query, projectId);
|
|
903
|
+
}
|
|
904
|
+
return this.db.prepare(`
|
|
905
|
+
SELECT d.* FROM data_lake d
|
|
906
|
+
JOIN data_lake_fts fts ON d.id = fts.rowid
|
|
907
|
+
WHERE data_lake_fts MATCH ?
|
|
908
|
+
ORDER BY d.created_at DESC
|
|
909
|
+
LIMIT 100
|
|
910
|
+
`).all(query);
|
|
911
|
+
}
|
|
912
|
+
getDataCategories(projectId) {
|
|
913
|
+
return this.db.prepare(`
|
|
914
|
+
SELECT category, COUNT(*) as count
|
|
915
|
+
FROM data_lake
|
|
916
|
+
WHERE project_id = ?
|
|
917
|
+
GROUP BY category
|
|
918
|
+
ORDER BY count DESC
|
|
919
|
+
`).all(projectId);
|
|
920
|
+
}
|
|
921
|
+
deleteDataCategory(projectId, category) {
|
|
922
|
+
const result = this.db.prepare(`
|
|
923
|
+
DELETE FROM data_lake WHERE project_id = ? AND category = ?
|
|
924
|
+
`).run(projectId, category);
|
|
925
|
+
return result.changes;
|
|
926
|
+
}
|
|
927
|
+
// ==================== STREAMING ====================
|
|
928
|
+
*streamSessions(projectId) {
|
|
929
|
+
const stmt = this.db.prepare(`
|
|
930
|
+
SELECT * FROM sessions WHERE project_id = ? ORDER BY date DESC
|
|
931
|
+
`);
|
|
932
|
+
for (const row of stmt.iterate(projectId)) {
|
|
933
|
+
yield row;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
*streamData(projectId, category) {
|
|
937
|
+
const stmt = category
|
|
938
|
+
? this.db.prepare('SELECT * FROM data_lake WHERE project_id = ? AND category = ?')
|
|
939
|
+
: this.db.prepare('SELECT * FROM data_lake WHERE project_id = ?');
|
|
940
|
+
const params = category ? [projectId, category] : [projectId];
|
|
941
|
+
for (const row of stmt.iterate(...params)) {
|
|
942
|
+
yield row;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
// ==================== STATS & UTILITIES ====================
|
|
946
|
+
getStats() {
|
|
947
|
+
const projects = this.db.prepare('SELECT COUNT(*) as count FROM projects').get();
|
|
948
|
+
const sessions = this.db.prepare('SELECT COUNT(*) as count FROM sessions').get();
|
|
949
|
+
const quests = this.db.prepare('SELECT COUNT(*) as count FROM quests').get();
|
|
950
|
+
const dataPoints = this.db.prepare('SELECT COUNT(*) as count FROM data_lake').get();
|
|
951
|
+
const tokens = this.db.prepare('SELECT COALESCE(SUM(tokens_estimate), 0) as total FROM sessions WHERE is_archived = 0').get();
|
|
952
|
+
const pageCount = this.db.pragma('page_count', { simple: true });
|
|
953
|
+
const pageSize = this.db.pragma('page_size', { simple: true });
|
|
954
|
+
const dbSize = (pageCount * pageSize) / (1024 * 1024);
|
|
955
|
+
return {
|
|
956
|
+
projects: projects.count,
|
|
957
|
+
sessions: sessions.count,
|
|
958
|
+
quests: quests.count,
|
|
959
|
+
dataPoints: dataPoints.count,
|
|
960
|
+
totalTokens: tokens.total,
|
|
961
|
+
dbSize: `${dbSize.toFixed(2)} MB`
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
getProjectStats(projectId) {
|
|
965
|
+
const sessions = this.db.prepare('SELECT COUNT(*) as count FROM sessions WHERE project_id = ?').get(projectId);
|
|
966
|
+
const pendingQuests = this.db.prepare(`SELECT COUNT(*) as count FROM quests WHERE project_id = ? AND status IN ('pending', 'in_progress')`).get(projectId);
|
|
967
|
+
const completedQuests = this.db.prepare(`SELECT COUNT(*) as count FROM quests WHERE project_id = ? AND status = 'completed'`).get(projectId);
|
|
968
|
+
const dataPoints = this.db.prepare('SELECT COUNT(*) as count FROM data_lake WHERE project_id = ?').get(projectId);
|
|
969
|
+
const tokens = this.db.prepare('SELECT COALESCE(SUM(tokens_estimate), 0) as total FROM sessions WHERE project_id = ? AND is_archived = 0').get(projectId);
|
|
970
|
+
return {
|
|
971
|
+
sessions: sessions.count,
|
|
972
|
+
quests: { pending: pendingQuests.count, completed: completedQuests.count },
|
|
973
|
+
dataPoints: dataPoints.count,
|
|
974
|
+
tokens: tokens.total
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
estimateTokens(text) {
|
|
978
|
+
// Rough estimate: ~4 chars per token
|
|
979
|
+
return Math.ceil(text.length / 4);
|
|
980
|
+
}
|
|
981
|
+
vacuum() {
|
|
982
|
+
this.db.exec('VACUUM');
|
|
983
|
+
}
|
|
984
|
+
checkpoint() {
|
|
985
|
+
this.db.pragma('wal_checkpoint(TRUNCATE)');
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Get resilience status for monitoring
|
|
989
|
+
*/
|
|
990
|
+
getResilienceStatus() {
|
|
991
|
+
const circuit = this.resilience.getCircuitStatus();
|
|
992
|
+
const incomplete = this.resilience.getIncompleteOperations();
|
|
993
|
+
return {
|
|
994
|
+
circuitState: circuit.state,
|
|
995
|
+
failures: circuit.failures,
|
|
996
|
+
incompleteOps: incomplete.length,
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Reset circuit breaker (manual recovery)
|
|
1001
|
+
*/
|
|
1002
|
+
resetCircuitBreaker() {
|
|
1003
|
+
this.resilience.resetCircuit();
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* Safe close with WAL checkpoint and cleanup
|
|
1007
|
+
*/
|
|
1008
|
+
close() {
|
|
1009
|
+
try {
|
|
1010
|
+
// Checkpoint WAL to ensure all data is persisted
|
|
1011
|
+
this.checkpoint();
|
|
1012
|
+
this.logger.info('Database checkpoint completed');
|
|
1013
|
+
}
|
|
1014
|
+
catch (error) {
|
|
1015
|
+
this.logger.error('Checkpoint failed during close', {
|
|
1016
|
+
error: error.message,
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
try {
|
|
1020
|
+
this.db.close();
|
|
1021
|
+
this.logger.info('Database closed successfully');
|
|
1022
|
+
}
|
|
1023
|
+
catch (error) {
|
|
1024
|
+
this.logger.error('Database close failed', {
|
|
1025
|
+
error: error.message,
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Check database integrity
|
|
1031
|
+
*/
|
|
1032
|
+
checkIntegrity() {
|
|
1033
|
+
const issues = [];
|
|
1034
|
+
try {
|
|
1035
|
+
const result = this.db.pragma('integrity_check', { simple: true });
|
|
1036
|
+
if (result !== 'ok') {
|
|
1037
|
+
issues.push(`Integrity check failed: ${result}`);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
catch (error) {
|
|
1041
|
+
issues.push(`Integrity check error: ${error.message}`);
|
|
1042
|
+
}
|
|
1043
|
+
try {
|
|
1044
|
+
const fk = this.db.pragma('foreign_key_check');
|
|
1045
|
+
if (fk.length > 0) {
|
|
1046
|
+
issues.push(`Foreign key violations: ${fk.length}`);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
catch (error) {
|
|
1050
|
+
issues.push(`FK check error: ${error.message}`);
|
|
1051
|
+
}
|
|
1052
|
+
return {
|
|
1053
|
+
ok: issues.length === 0,
|
|
1054
|
+
issues,
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
//# sourceMappingURL=database.js.map
|