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.
Files changed (87) hide show
  1. package/LICENSE +667 -0
  2. package/README.md +384 -0
  3. package/dist/analytics.d.ts +100 -0
  4. package/dist/analytics.d.ts.map +1 -0
  5. package/dist/analytics.js +368 -0
  6. package/dist/analytics.js.map +1 -0
  7. package/dist/auto-orchestrator.d.ts +118 -0
  8. package/dist/auto-orchestrator.d.ts.map +1 -0
  9. package/dist/auto-orchestrator.js +325 -0
  10. package/dist/auto-orchestrator.js.map +1 -0
  11. package/dist/autoconfig.d.ts +89 -0
  12. package/dist/autoconfig.d.ts.map +1 -0
  13. package/dist/autoconfig.js +576 -0
  14. package/dist/autoconfig.js.map +1 -0
  15. package/dist/cli.d.ts +148 -0
  16. package/dist/cli.d.ts.map +1 -0
  17. package/dist/cli.js +281 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/cloud-backup.d.ts +100 -0
  20. package/dist/cloud-backup.d.ts.map +1 -0
  21. package/dist/cloud-backup.js +545 -0
  22. package/dist/cloud-backup.js.map +1 -0
  23. package/dist/crypto.d.ts +72 -0
  24. package/dist/crypto.d.ts.map +1 -0
  25. package/dist/crypto.js +164 -0
  26. package/dist/crypto.js.map +1 -0
  27. package/dist/database.d.ts +218 -0
  28. package/dist/database.d.ts.map +1 -0
  29. package/dist/database.js +1058 -0
  30. package/dist/database.js.map +1 -0
  31. package/dist/http-auth.d.ts +68 -0
  32. package/dist/http-auth.d.ts.map +1 -0
  33. package/dist/http-auth.js +296 -0
  34. package/dist/http-auth.js.map +1 -0
  35. package/dist/http-fast.d.ts +13 -0
  36. package/dist/http-fast.d.ts.map +1 -0
  37. package/dist/http-fast.js +325 -0
  38. package/dist/http-fast.js.map +1 -0
  39. package/dist/http-server.d.ts +12 -0
  40. package/dist/http-server.d.ts.map +1 -0
  41. package/dist/http-server.js +383 -0
  42. package/dist/http-server.js.map +1 -0
  43. package/dist/index.d.ts +19 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +1695 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/license.d.ts +177 -0
  48. package/dist/license.d.ts.map +1 -0
  49. package/dist/license.js +405 -0
  50. package/dist/license.js.map +1 -0
  51. package/dist/logger.d.ts +76 -0
  52. package/dist/logger.d.ts.map +1 -0
  53. package/dist/logger.js +195 -0
  54. package/dist/logger.js.map +1 -0
  55. package/dist/performance.d.ts +114 -0
  56. package/dist/performance.d.ts.map +1 -0
  57. package/dist/performance.js +228 -0
  58. package/dist/performance.js.map +1 -0
  59. package/dist/resilience.d.ts +146 -0
  60. package/dist/resilience.d.ts.map +1 -0
  61. package/dist/resilience.js +563 -0
  62. package/dist/resilience.js.map +1 -0
  63. package/dist/security.d.ts +68 -0
  64. package/dist/security.d.ts.map +1 -0
  65. package/dist/security.js +215 -0
  66. package/dist/security.js.map +1 -0
  67. package/dist/setup.d.ts +21 -0
  68. package/dist/setup.d.ts.map +1 -0
  69. package/dist/setup.js +261 -0
  70. package/dist/setup.js.map +1 -0
  71. package/dist/summarizer.d.ts +30 -0
  72. package/dist/summarizer.d.ts.map +1 -0
  73. package/dist/summarizer.js +139 -0
  74. package/dist/summarizer.js.map +1 -0
  75. package/dist/sync.d.ts +39 -0
  76. package/dist/sync.d.ts.map +1 -0
  77. package/dist/sync.js +356 -0
  78. package/dist/sync.js.map +1 -0
  79. package/dist/types.d.ts +267 -0
  80. package/dist/types.d.ts.map +1 -0
  81. package/dist/types.js +30 -0
  82. package/dist/types.js.map +1 -0
  83. package/dist/vectors.d.ts +103 -0
  84. package/dist/vectors.d.ts.map +1 -0
  85. package/dist/vectors.js +311 -0
  86. package/dist/vectors.js.map +1 -0
  87. package/package.json +73 -0
@@ -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