@smallironman/mcp-memory-keeper 0.12.2-fork1

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 (110) hide show
  1. package/CHANGELOG.md +542 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1281 -0
  4. package/bin/mcp-memory-keeper +54 -0
  5. package/dist/__tests__/e2e/issue33-reproduce.test.js +234 -0
  6. package/dist/__tests__/e2e/server-e2e.test.js +341 -0
  7. package/dist/__tests__/helpers/database-test-helper.js +160 -0
  8. package/dist/__tests__/helpers/test-server.js +92 -0
  9. package/dist/__tests__/integration/advanced-features.test.js +614 -0
  10. package/dist/__tests__/integration/backward-compatibility.test.js +245 -0
  11. package/dist/__tests__/integration/batchOperationsE2E.test.js +396 -0
  12. package/dist/__tests__/integration/batchOperationsHandler.test.js +1230 -0
  13. package/dist/__tests__/integration/channelManagementHandler.test.js +1291 -0
  14. package/dist/__tests__/integration/channels.test.js +376 -0
  15. package/dist/__tests__/integration/checkpoint.test.js +251 -0
  16. package/dist/__tests__/integration/concurrent-access.test.js +190 -0
  17. package/dist/__tests__/integration/context-operations.test.js +243 -0
  18. package/dist/__tests__/integration/contextDiff.test.js +852 -0
  19. package/dist/__tests__/integration/contextDiffHandler.test.js +976 -0
  20. package/dist/__tests__/integration/contextExportHandler.test.js +510 -0
  21. package/dist/__tests__/integration/contextGetPaginationDefaults.test.js +298 -0
  22. package/dist/__tests__/integration/contextReassignChannelHandler.test.js +908 -0
  23. package/dist/__tests__/integration/contextRelationshipsHandler.test.js +1151 -0
  24. package/dist/__tests__/integration/contextSearch.test.js +1054 -0
  25. package/dist/__tests__/integration/contextSearchHandler.test.js +552 -0
  26. package/dist/__tests__/integration/contextWatchActual.test.js +165 -0
  27. package/dist/__tests__/integration/contextWatchHandler.test.js +1500 -0
  28. package/dist/__tests__/integration/database-initialization.test.js +134 -0
  29. package/dist/__tests__/integration/enhanced-context-operations.test.js +1082 -0
  30. package/dist/__tests__/integration/enhancedContextGetHandler.test.js +915 -0
  31. package/dist/__tests__/integration/enhancedContextTimelineHandler.test.js +716 -0
  32. package/dist/__tests__/integration/error-cases.test.js +411 -0
  33. package/dist/__tests__/integration/export-import.test.js +367 -0
  34. package/dist/__tests__/integration/feature-flags.test.js +542 -0
  35. package/dist/__tests__/integration/file-operations.test.js +264 -0
  36. package/dist/__tests__/integration/filterBySessionId.test.js +251 -0
  37. package/dist/__tests__/integration/git-integration.test.js +241 -0
  38. package/dist/__tests__/integration/index-tools.test.js +496 -0
  39. package/dist/__tests__/integration/issue11-actual-bug-demo.test.js +304 -0
  40. package/dist/__tests__/integration/issue11-search-filters-bug.test.js +561 -0
  41. package/dist/__tests__/integration/issue12-checkpoint-restore-behavior.test.js +621 -0
  42. package/dist/__tests__/integration/issue13-key-validation.test.js +433 -0
  43. package/dist/__tests__/integration/issue24-final-fix.test.js +241 -0
  44. package/dist/__tests__/integration/issue24-fix-validation.test.js +158 -0
  45. package/dist/__tests__/integration/issue24-reproduce.test.js +225 -0
  46. package/dist/__tests__/integration/issue24-token-limit.test.js +199 -0
  47. package/dist/__tests__/integration/issue33-array-items-schema.test.js +165 -0
  48. package/dist/__tests__/integration/knowledge-graph.test.js +338 -0
  49. package/dist/__tests__/integration/migrations.test.js +528 -0
  50. package/dist/__tests__/integration/multi-agent.test.js +546 -0
  51. package/dist/__tests__/integration/pagination-critical-fix.test.js +296 -0
  52. package/dist/__tests__/integration/paginationDefaultsHandler.test.js +600 -0
  53. package/dist/__tests__/integration/project-directory.test.js +291 -0
  54. package/dist/__tests__/integration/resource-cleanup.test.js +149 -0
  55. package/dist/__tests__/integration/retention.test.js +513 -0
  56. package/dist/__tests__/integration/search.test.js +333 -0
  57. package/dist/__tests__/integration/semantic-search.test.js +266 -0
  58. package/dist/__tests__/integration/server-initialization.test.js +305 -0
  59. package/dist/__tests__/integration/session-management.test.js +219 -0
  60. package/dist/__tests__/integration/simplified-sharing.test.js +346 -0
  61. package/dist/__tests__/integration/smart-compaction.test.js +230 -0
  62. package/dist/__tests__/integration/summarization.test.js +308 -0
  63. package/dist/__tests__/integration/tokenLimitEnforcement.test.js +134 -0
  64. package/dist/__tests__/integration/tool-profiles-integration.test.js +150 -0
  65. package/dist/__tests__/integration/watcher-migration-validation.test.js +544 -0
  66. package/dist/__tests__/security/input-validation.test.js +115 -0
  67. package/dist/__tests__/utils/agents.test.js +473 -0
  68. package/dist/__tests__/utils/database.test.js +177 -0
  69. package/dist/__tests__/utils/git.test.js +122 -0
  70. package/dist/__tests__/utils/knowledge-graph.test.js +297 -0
  71. package/dist/__tests__/utils/migrationHealthCheck.test.js +302 -0
  72. package/dist/__tests__/utils/project-directory-messages.test.js +192 -0
  73. package/dist/__tests__/utils/timezone-safe-dates.js +119 -0
  74. package/dist/__tests__/utils/token-limits.test.js +225 -0
  75. package/dist/__tests__/utils/tool-profiles.test.js +374 -0
  76. package/dist/__tests__/utils/validation.test.js +200 -0
  77. package/dist/__tests__/utils/vector-store.test.js +231 -0
  78. package/dist/handlers/contextWatchHandlers.js +206 -0
  79. package/dist/index.js +4425 -0
  80. package/dist/migrations/003_add_channels.js +174 -0
  81. package/dist/migrations/004_add_context_watch.js +151 -0
  82. package/dist/migrations/005_add_context_watch.js +98 -0
  83. package/dist/migrations/simplify-sharing.js +117 -0
  84. package/dist/repositories/BaseRepository.js +30 -0
  85. package/dist/repositories/CheckpointRepository.js +140 -0
  86. package/dist/repositories/ContextRepository.js +2017 -0
  87. package/dist/repositories/FileRepository.js +104 -0
  88. package/dist/repositories/RepositoryManager.js +62 -0
  89. package/dist/repositories/SessionRepository.js +66 -0
  90. package/dist/repositories/WatcherRepository.js +252 -0
  91. package/dist/repositories/index.js +15 -0
  92. package/dist/test-helpers/database-helper.js +128 -0
  93. package/dist/types/entities.js +3 -0
  94. package/dist/utils/agents.js +791 -0
  95. package/dist/utils/channels.js +150 -0
  96. package/dist/utils/database.js +780 -0
  97. package/dist/utils/feature-flags.js +476 -0
  98. package/dist/utils/git.js +145 -0
  99. package/dist/utils/knowledge-graph.js +264 -0
  100. package/dist/utils/migrationHealthCheck.js +373 -0
  101. package/dist/utils/migrations.js +452 -0
  102. package/dist/utils/retention.js +460 -0
  103. package/dist/utils/timestamps.js +112 -0
  104. package/dist/utils/token-limits.js +350 -0
  105. package/dist/utils/tool-profiles.js +242 -0
  106. package/dist/utils/validation.js +296 -0
  107. package/dist/utils/vector-store.js +247 -0
  108. package/examples/config.json +31 -0
  109. package/examples/project-directory-setup.md +114 -0
  110. package/package.json +85 -0
@@ -0,0 +1,780 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DatabaseManager = void 0;
7
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
8
+ const migrationHealthCheck_1 = require("./migrationHealthCheck");
9
+ class DatabaseManager {
10
+ db;
11
+ config;
12
+ constructor(config) {
13
+ this.config = {
14
+ filename: config.filename,
15
+ maxSize: config.maxSize || 100 * 1024 * 1024, // 100MB default
16
+ walMode: config.walMode !== false, // WAL mode enabled by default
17
+ };
18
+ this.db = new better_sqlite3_1.default(this.config.filename);
19
+ this.initialize();
20
+ }
21
+ initialize() {
22
+ // Enable WAL mode for better concurrency
23
+ if (this.config.walMode) {
24
+ this.db.pragma('journal_mode = WAL');
25
+ }
26
+ // Set busy timeout to handle concurrent access
27
+ this.db.pragma('busy_timeout = 5000'); // 5 seconds
28
+ // Enable foreign keys
29
+ this.db.pragma('foreign_keys = ON');
30
+ // First, check if this is an existing database that might need migration
31
+ const tables = this.db
32
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
33
+ .all();
34
+ if (tables.length > 0) {
35
+ // Existing database - run health check first before creating tables
36
+ const healthCheck = new migrationHealthCheck_1.MigrationHealthCheck(this);
37
+ healthCheck.runAutoFix();
38
+ }
39
+ // Create tables (will use CREATE TABLE IF NOT EXISTS, so safe to run after migrations)
40
+ this.createTables();
41
+ // Apply watcher migrations if needed
42
+ this.applyWatcherMigrations();
43
+ // Apply FTS5 full-text search index (optional — gracefully skipped if unavailable)
44
+ this.applyFts5Migration();
45
+ // Set up maintenance triggers
46
+ this.setupMaintenanceTriggers();
47
+ }
48
+ createTables() {
49
+ this.db.exec(`
50
+ -- Sessions table
51
+ CREATE TABLE IF NOT EXISTS sessions (
52
+ id TEXT PRIMARY KEY,
53
+ name TEXT,
54
+ description TEXT,
55
+ branch TEXT,
56
+ working_directory TEXT,
57
+ parent_id TEXT,
58
+ default_channel TEXT DEFAULT 'general',
59
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
60
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
61
+ FOREIGN KEY (parent_id) REFERENCES sessions(id)
62
+ );
63
+
64
+ -- Enhanced context_items table with size tracking and simplified sharing
65
+ CREATE TABLE IF NOT EXISTS context_items (
66
+ id TEXT PRIMARY KEY,
67
+ session_id TEXT NOT NULL,
68
+ key TEXT NOT NULL,
69
+ value TEXT NOT NULL,
70
+ category TEXT,
71
+ priority TEXT DEFAULT 'normal',
72
+ metadata TEXT,
73
+ size INTEGER DEFAULT 0,
74
+ is_private INTEGER DEFAULT 0,
75
+ channel TEXT DEFAULT 'general',
76
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
77
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
78
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE,
79
+ UNIQUE(session_id, key)
80
+ );
81
+
82
+ -- File cache table with size tracking
83
+ CREATE TABLE IF NOT EXISTS file_cache (
84
+ id TEXT PRIMARY KEY,
85
+ session_id TEXT NOT NULL,
86
+ file_path TEXT NOT NULL,
87
+ content TEXT,
88
+ hash TEXT,
89
+ size INTEGER DEFAULT 0,
90
+ last_read TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
91
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
92
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE,
93
+ UNIQUE(session_id, file_path)
94
+ );
95
+
96
+ -- Checkpoints table (Phase 2)
97
+ CREATE TABLE IF NOT EXISTS checkpoints (
98
+ id TEXT PRIMARY KEY,
99
+ session_id TEXT NOT NULL,
100
+ name TEXT NOT NULL,
101
+ description TEXT,
102
+ metadata TEXT,
103
+ git_status TEXT,
104
+ git_branch TEXT,
105
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
106
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
107
+ );
108
+
109
+ -- Checkpoint items table (Phase 2)
110
+ CREATE TABLE IF NOT EXISTS checkpoint_items (
111
+ id TEXT PRIMARY KEY,
112
+ checkpoint_id TEXT NOT NULL,
113
+ context_item_id TEXT NOT NULL,
114
+ FOREIGN KEY (checkpoint_id) REFERENCES checkpoints(id) ON DELETE CASCADE,
115
+ FOREIGN KEY (context_item_id) REFERENCES context_items(id) ON DELETE CASCADE
116
+ );
117
+
118
+ -- Checkpoint files table (Phase 2)
119
+ CREATE TABLE IF NOT EXISTS checkpoint_files (
120
+ id TEXT PRIMARY KEY,
121
+ checkpoint_id TEXT NOT NULL,
122
+ file_cache_id TEXT NOT NULL,
123
+ FOREIGN KEY (checkpoint_id) REFERENCES checkpoints(id) ON DELETE CASCADE,
124
+ FOREIGN KEY (file_cache_id) REFERENCES file_cache(id) ON DELETE CASCADE
125
+ );
126
+
127
+ -- Create indexes for better performance
128
+ CREATE INDEX IF NOT EXISTS idx_context_items_session ON context_items(session_id);
129
+ CREATE INDEX IF NOT EXISTS idx_context_items_category ON context_items(category);
130
+ CREATE INDEX IF NOT EXISTS idx_context_items_priority ON context_items(priority);
131
+ CREATE INDEX IF NOT EXISTS idx_context_items_private ON context_items(is_private);
132
+ CREATE INDEX IF NOT EXISTS idx_context_items_channel ON context_items(channel);
133
+ CREATE INDEX IF NOT EXISTS idx_context_items_created ON context_items(created_at);
134
+ CREATE INDEX IF NOT EXISTS idx_context_items_session_created ON context_items(session_id, created_at);
135
+ CREATE INDEX IF NOT EXISTS idx_file_cache_session ON file_cache(session_id);
136
+ CREATE INDEX IF NOT EXISTS idx_checkpoints_session ON checkpoints(session_id);
137
+
138
+ -- Knowledge Graph tables (Phase 4.1)
139
+ CREATE TABLE IF NOT EXISTS entities (
140
+ id TEXT PRIMARY KEY,
141
+ session_id TEXT NOT NULL,
142
+ type TEXT NOT NULL,
143
+ name TEXT NOT NULL,
144
+ attributes TEXT,
145
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
146
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
147
+ );
148
+
149
+ CREATE TABLE IF NOT EXISTS relations (
150
+ id TEXT PRIMARY KEY,
151
+ session_id TEXT NOT NULL,
152
+ subject_id TEXT NOT NULL,
153
+ predicate TEXT NOT NULL,
154
+ object_id TEXT NOT NULL,
155
+ confidence REAL DEFAULT 1.0,
156
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
157
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE,
158
+ FOREIGN KEY (subject_id) REFERENCES entities(id) ON DELETE CASCADE,
159
+ FOREIGN KEY (object_id) REFERENCES entities(id) ON DELETE CASCADE
160
+ );
161
+
162
+ CREATE TABLE IF NOT EXISTS observations (
163
+ id TEXT PRIMARY KEY,
164
+ entity_id TEXT NOT NULL,
165
+ observation TEXT NOT NULL,
166
+ source TEXT,
167
+ timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
168
+ FOREIGN KEY (entity_id) REFERENCES entities(id) ON DELETE CASCADE
169
+ );
170
+
171
+ -- Vector Storage tables (Phase 4.2)
172
+ CREATE TABLE IF NOT EXISTS embeddings (
173
+ id TEXT PRIMARY KEY,
174
+ content_id TEXT NOT NULL,
175
+ content_type TEXT NOT NULL, -- 'context_item' or 'file_cache'
176
+ embedding BLOB NOT NULL,
177
+ metadata TEXT,
178
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
179
+ UNIQUE(content_id, content_type)
180
+ );
181
+
182
+ CREATE INDEX IF NOT EXISTS idx_embeddings_content ON embeddings(content_id, content_type);
183
+
184
+ -- Multi-Agent System tables (Phase 4.3)
185
+ CREATE TABLE IF NOT EXISTS agent_tasks (
186
+ id TEXT PRIMARY KEY,
187
+ type TEXT NOT NULL,
188
+ input TEXT NOT NULL,
189
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
190
+ );
191
+
192
+ CREATE TABLE IF NOT EXISTS agent_results (
193
+ id TEXT PRIMARY KEY,
194
+ task_id TEXT NOT NULL,
195
+ agent_type TEXT NOT NULL,
196
+ output TEXT NOT NULL,
197
+ confidence REAL DEFAULT 0.0,
198
+ reasoning TEXT,
199
+ processing_time INTEGER,
200
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
201
+ FOREIGN KEY (task_id) REFERENCES agent_tasks(id) ON DELETE CASCADE
202
+ );
203
+
204
+ CREATE INDEX IF NOT EXISTS idx_agent_results_task ON agent_results(task_id);
205
+
206
+ -- Journal table (Phase 4.4)
207
+ CREATE TABLE IF NOT EXISTS journal_entries (
208
+ id TEXT PRIMARY KEY,
209
+ session_id TEXT NOT NULL,
210
+ entry TEXT NOT NULL,
211
+ mood TEXT,
212
+ tags TEXT, -- JSON array of tags
213
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
214
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
215
+ );
216
+
217
+ CREATE INDEX IF NOT EXISTS idx_journal_session ON journal_entries(session_id);
218
+ CREATE INDEX IF NOT EXISTS idx_journal_created ON journal_entries(created_at);
219
+
220
+ -- Context Relationships table
221
+ CREATE TABLE IF NOT EXISTS context_relationships (
222
+ id TEXT PRIMARY KEY,
223
+ session_id TEXT NOT NULL,
224
+ from_key TEXT NOT NULL,
225
+ to_key TEXT NOT NULL,
226
+ relationship_type TEXT NOT NULL,
227
+ metadata TEXT,
228
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
229
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE,
230
+ UNIQUE(session_id, from_key, to_key, relationship_type)
231
+ );
232
+
233
+ CREATE INDEX IF NOT EXISTS idx_relationships_from ON context_relationships(session_id, from_key);
234
+ CREATE INDEX IF NOT EXISTS idx_relationships_to ON context_relationships(session_id, to_key);
235
+ CREATE INDEX IF NOT EXISTS idx_relationships_type ON context_relationships(relationship_type);
236
+
237
+ -- Compaction history table (Phase 4.4)
238
+ CREATE TABLE IF NOT EXISTS compaction_history (
239
+ id TEXT PRIMARY KEY,
240
+ session_id TEXT NOT NULL,
241
+ items_before INTEGER NOT NULL,
242
+ items_after INTEGER NOT NULL,
243
+ size_before INTEGER NOT NULL,
244
+ size_after INTEGER NOT NULL,
245
+ summary TEXT,
246
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
247
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
248
+ );
249
+
250
+ -- Retention Policies table (Phase 5.1)
251
+ CREATE TABLE IF NOT EXISTS retention_policies (
252
+ id TEXT PRIMARY KEY,
253
+ name TEXT NOT NULL UNIQUE,
254
+ description TEXT,
255
+ enabled BOOLEAN DEFAULT 1,
256
+ policy_config TEXT NOT NULL, -- JSON configuration
257
+ last_run TIMESTAMP,
258
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
259
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
260
+ );
261
+
262
+ CREATE TABLE IF NOT EXISTS retention_executions (
263
+ id TEXT PRIMARY KEY,
264
+ policy_id TEXT NOT NULL,
265
+ session_id TEXT,
266
+ dry_run BOOLEAN DEFAULT 1,
267
+ items_affected INTEGER DEFAULT 0,
268
+ size_freed INTEGER DEFAULT 0,
269
+ execution_log TEXT,
270
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
271
+ FOREIGN KEY (policy_id) REFERENCES retention_policies(id) ON DELETE CASCADE,
272
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE SET NULL
273
+ );
274
+
275
+ CREATE TABLE IF NOT EXISTS retention_logs (
276
+ id TEXT PRIMARY KEY,
277
+ policy_id TEXT NOT NULL,
278
+ result TEXT NOT NULL,
279
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
280
+ FOREIGN KEY (policy_id) REFERENCES retention_policies(id) ON DELETE CASCADE
281
+ );
282
+
283
+ -- Feature Flags table (Phase 5.2)
284
+ CREATE TABLE IF NOT EXISTS feature_flags (
285
+ id TEXT PRIMARY KEY,
286
+ key TEXT NOT NULL UNIQUE,
287
+ name TEXT NOT NULL,
288
+ description TEXT,
289
+ enabled BOOLEAN DEFAULT 0,
290
+ category TEXT,
291
+ environments TEXT, -- JSON array
292
+ users TEXT, -- JSON array
293
+ percentage INTEGER DEFAULT 0,
294
+ enabled_from TEXT,
295
+ enabled_until TEXT,
296
+ tags TEXT, -- JSON array
297
+ created_by TEXT,
298
+ last_modified_by TEXT,
299
+ config TEXT, -- JSON configuration
300
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
301
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
302
+ );
303
+
304
+ CREATE TABLE IF NOT EXISTS feature_flag_history (
305
+ id TEXT PRIMARY KEY,
306
+ flag_id TEXT NOT NULL,
307
+ user_id TEXT,
308
+ action TEXT NOT NULL, -- 'created', 'updated', 'evaluated'
309
+ details TEXT,
310
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
311
+ FOREIGN KEY (flag_id) REFERENCES feature_flags(id) ON DELETE CASCADE
312
+ );
313
+
314
+ -- Compressed context table for retention policies
315
+ CREATE TABLE IF NOT EXISTS compressed_context (
316
+ id TEXT PRIMARY KEY,
317
+ session_id TEXT NOT NULL,
318
+ original_count INTEGER NOT NULL,
319
+ compressed_data TEXT NOT NULL,
320
+ compression_ratio REAL NOT NULL,
321
+ date_range_start TIMESTAMP NOT NULL,
322
+ date_range_end TIMESTAMP NOT NULL,
323
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
324
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
325
+ );
326
+
327
+ CREATE INDEX IF NOT EXISTS idx_compressed_session ON compressed_context(session_id);
328
+
329
+ -- Database Migrations table (Phase 5.3)
330
+ CREATE TABLE IF NOT EXISTS migrations (
331
+ id TEXT PRIMARY KEY,
332
+ version TEXT NOT NULL UNIQUE,
333
+ name TEXT NOT NULL,
334
+ description TEXT,
335
+ up_sql TEXT NOT NULL,
336
+ down_sql TEXT,
337
+ dependencies TEXT, -- JSON array of version strings
338
+ requires_backup BOOLEAN DEFAULT 0,
339
+ checksum TEXT,
340
+ applied_at TIMESTAMP,
341
+ rolled_back_at TIMESTAMP,
342
+ rollback_at TIMESTAMP,
343
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
344
+ );
345
+
346
+ CREATE TABLE IF NOT EXISTS migration_log (
347
+ id TEXT PRIMARY KEY,
348
+ migration_id TEXT NOT NULL,
349
+ version TEXT NOT NULL,
350
+ action TEXT NOT NULL, -- 'apply', 'rollback', 'backup'
351
+ success BOOLEAN NOT NULL,
352
+ errors TEXT, -- JSON array
353
+ warnings TEXT, -- JSON array
354
+ error_message TEXT,
355
+ execution_time INTEGER, -- milliseconds
356
+ rows_affected INTEGER,
357
+ backup_path TEXT,
358
+ duration_ms INTEGER,
359
+ timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
360
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
361
+ FOREIGN KEY (migration_id) REFERENCES migrations(id) ON DELETE CASCADE
362
+ );
363
+
364
+ CREATE INDEX IF NOT EXISTS idx_migrations_version ON migrations(version);
365
+ CREATE INDEX IF NOT EXISTS idx_migrations_applied ON migrations(applied_at);
366
+
367
+ -- Migration for existing databases - add new columns if they don't exist
368
+ ${this.getMigrationSQL()}
369
+ `);
370
+ }
371
+ getMigrationSQL() {
372
+ // The shared columns are already defined in the CREATE TABLE statement above
373
+ // This method is kept for potential future migrations
374
+ return '';
375
+ }
376
+ addColumnIfNotExists(table, column, definition) {
377
+ // SQLite doesn't support IF NOT EXISTS for columns, so we need to check first
378
+ const hasColumn = this.db
379
+ .prepare(`
380
+ SELECT COUNT(*) as count FROM pragma_table_info(?) WHERE name = ?
381
+ `)
382
+ .get(table, column);
383
+ if (hasColumn.count === 0) {
384
+ return `ALTER TABLE ${table} ADD COLUMN ${column} ${definition};`;
385
+ }
386
+ return '';
387
+ }
388
+ setupMaintenanceTriggers() {
389
+ // Update timestamp trigger
390
+ this.db.exec(`
391
+ CREATE TRIGGER IF NOT EXISTS update_context_items_timestamp
392
+ AFTER UPDATE ON context_items
393
+ BEGIN
394
+ UPDATE context_items SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
395
+ END;
396
+
397
+ CREATE TRIGGER IF NOT EXISTS update_sessions_timestamp
398
+ AFTER UPDATE ON sessions
399
+ BEGIN
400
+ UPDATE sessions SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
401
+ END;
402
+
403
+ CREATE TRIGGER IF NOT EXISTS update_retention_policies_timestamp
404
+ AFTER UPDATE ON retention_policies
405
+ BEGIN
406
+ UPDATE retention_policies SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
407
+ END;
408
+
409
+ CREATE TRIGGER IF NOT EXISTS update_feature_flags_timestamp
410
+ AFTER UPDATE ON feature_flags
411
+ BEGIN
412
+ UPDATE feature_flags SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
413
+ END;
414
+ `);
415
+ }
416
+ getDatabase() {
417
+ return this.db;
418
+ }
419
+ close() {
420
+ this.db.close();
421
+ }
422
+ getDatabaseSize() {
423
+ const result = this.db
424
+ .prepare('SELECT page_count * page_size as size FROM pragma_page_count(), pragma_page_size()')
425
+ .get();
426
+ return result.size;
427
+ }
428
+ getTableSizes() {
429
+ const tables = this.db
430
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
431
+ .all();
432
+ const sizes = {};
433
+ for (const table of tables) {
434
+ const result = this.db.prepare(`SELECT COUNT(*) as count FROM ${table.name}`).get();
435
+ sizes[table.name] = result.count;
436
+ }
437
+ return sizes;
438
+ }
439
+ vacuum() {
440
+ this.db.exec('VACUUM');
441
+ }
442
+ runInTransaction(fn) {
443
+ const transaction = this.db.transaction(fn);
444
+ return transaction();
445
+ }
446
+ transaction(fn) {
447
+ return this.runInTransaction(fn);
448
+ }
449
+ isDatabaseFull() {
450
+ const currentSize = this.getDatabaseSize();
451
+ return currentSize >= this.config.maxSize;
452
+ }
453
+ getSessionSize(sessionId) {
454
+ const itemsResult = this.db
455
+ .prepare(`
456
+ SELECT COUNT(*) as count, COALESCE(SUM(size), 0) as totalSize
457
+ FROM context_items
458
+ WHERE session_id = ?
459
+ `)
460
+ .get(sessionId);
461
+ const filesResult = this.db
462
+ .prepare(`
463
+ SELECT COUNT(*) as count, COALESCE(SUM(size), 0) as totalSize
464
+ FROM file_cache
465
+ WHERE session_id = ?
466
+ `)
467
+ .get(sessionId);
468
+ return {
469
+ items: itemsResult?.count || 0,
470
+ files: filesResult?.count || 0,
471
+ totalSize: (itemsResult?.totalSize || 0) + (filesResult?.totalSize || 0),
472
+ };
473
+ }
474
+ cleanupOldSessions(daysToKeep = 30) {
475
+ const cutoffDate = new Date();
476
+ cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
477
+ const oldSessions = this.db
478
+ .prepare(`
479
+ SELECT id FROM sessions
480
+ WHERE updated_at < ?
481
+ ORDER BY updated_at ASC
482
+ `)
483
+ .all(cutoffDate.toISOString());
484
+ let deletedCount = 0;
485
+ for (const session of oldSessions) {
486
+ try {
487
+ this.db.prepare('DELETE FROM sessions WHERE id = ?').run(session.id);
488
+ deletedCount++;
489
+ }
490
+ catch (error) {
491
+ console.error(`Failed to delete session ${session.id}:`, error);
492
+ }
493
+ }
494
+ return deletedCount;
495
+ }
496
+ applyWatcherMigrations() {
497
+ try {
498
+ // Check if watcher tables are missing using the needsMigration logic
499
+ const result = this.db
500
+ .prepare(`
501
+ SELECT COUNT(*) as count FROM sqlite_master
502
+ WHERE type='table' AND name IN ('context_changes', 'context_watchers')
503
+ `)
504
+ .get();
505
+ const needsMigration = result.count < 2;
506
+ if (needsMigration) {
507
+ // console.log('Applying watcher migrations...');
508
+ // Apply migration 004 - context watch functionality
509
+ this.db.transaction(() => {
510
+ // Create change tracking table
511
+ this.db.exec(`
512
+ CREATE TABLE IF NOT EXISTS context_changes (
513
+ sequence_id INTEGER PRIMARY KEY AUTOINCREMENT,
514
+ session_id TEXT NOT NULL,
515
+ item_id TEXT NOT NULL,
516
+ key TEXT NOT NULL,
517
+ operation TEXT NOT NULL CHECK (operation IN ('CREATE', 'UPDATE', 'DELETE')),
518
+ old_value TEXT,
519
+ new_value TEXT,
520
+ old_metadata TEXT,
521
+ new_metadata TEXT,
522
+ category TEXT,
523
+ priority TEXT,
524
+ channel TEXT,
525
+ size_delta INTEGER DEFAULT 0,
526
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
527
+ created_by TEXT,
528
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
529
+ );
530
+ `);
531
+ // Create watchers registry table
532
+ this.db.exec(`
533
+ CREATE TABLE IF NOT EXISTS context_watchers (
534
+ id TEXT PRIMARY KEY,
535
+ session_id TEXT,
536
+ filter_keys TEXT,
537
+ filter_categories TEXT,
538
+ filter_channels TEXT,
539
+ filter_priorities TEXT,
540
+ last_sequence INTEGER DEFAULT 0,
541
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
542
+ last_poll_at TIMESTAMP,
543
+ expires_at TIMESTAMP,
544
+ metadata TEXT,
545
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
546
+ );
547
+ `);
548
+ // Create indexes for performance
549
+ this.db.exec(`
550
+ CREATE INDEX IF NOT EXISTS idx_changes_sequence ON context_changes(sequence_id);
551
+ CREATE INDEX IF NOT EXISTS idx_changes_session_seq ON context_changes(session_id, sequence_id);
552
+ CREATE INDEX IF NOT EXISTS idx_changes_created ON context_changes(created_at);
553
+ CREATE INDEX IF NOT EXISTS idx_watchers_expires ON context_watchers(expires_at);
554
+ CREATE INDEX IF NOT EXISTS idx_watchers_session ON context_watchers(session_id);
555
+ `);
556
+ // Create triggers for change tracking
557
+ this.db.exec(`
558
+ -- Trigger for INSERT operations on context_items
559
+ CREATE TRIGGER IF NOT EXISTS track_context_insert
560
+ AFTER INSERT ON context_items
561
+ BEGIN
562
+ INSERT INTO context_changes (
563
+ session_id, item_id, key, operation,
564
+ new_value, new_metadata, category, priority, channel,
565
+ size_delta, created_by
566
+ ) VALUES (
567
+ NEW.session_id, NEW.id, NEW.key, 'CREATE',
568
+ NEW.value, NEW.metadata, NEW.category, NEW.priority, NEW.channel,
569
+ NEW.size, 'context_save'
570
+ );
571
+ END;
572
+
573
+ -- Trigger for UPDATE operations on context_items
574
+ CREATE TRIGGER IF NOT EXISTS track_context_update
575
+ AFTER UPDATE ON context_items
576
+ WHEN OLD.value != NEW.value OR
577
+ IFNULL(OLD.metadata, '') != IFNULL(NEW.metadata, '') OR
578
+ IFNULL(OLD.category, '') != IFNULL(NEW.category, '') OR
579
+ IFNULL(OLD.priority, '') != IFNULL(NEW.priority, '') OR
580
+ IFNULL(OLD.channel, '') != IFNULL(NEW.channel, '')
581
+ BEGIN
582
+ INSERT INTO context_changes (
583
+ session_id, item_id, key, operation,
584
+ old_value, new_value, old_metadata, new_metadata,
585
+ category, priority, channel, size_delta, created_by
586
+ ) VALUES (
587
+ NEW.session_id, NEW.id, NEW.key, 'UPDATE',
588
+ OLD.value, NEW.value, OLD.metadata, NEW.metadata,
589
+ NEW.category, NEW.priority, NEW.channel,
590
+ NEW.size - OLD.size, 'context_save'
591
+ );
592
+ END;
593
+
594
+ -- Trigger for DELETE operations on context_items
595
+ CREATE TRIGGER IF NOT EXISTS track_context_delete
596
+ AFTER DELETE ON context_items
597
+ BEGIN
598
+ INSERT INTO context_changes (
599
+ session_id, item_id, key, operation,
600
+ old_value, old_metadata, category, priority, channel,
601
+ size_delta, created_by
602
+ ) VALUES (
603
+ OLD.session_id, OLD.id, OLD.key, 'DELETE',
604
+ OLD.value, OLD.metadata, OLD.category, OLD.priority, OLD.channel,
605
+ -OLD.size, 'context_delete'
606
+ );
607
+ END;
608
+ `);
609
+ })();
610
+ // Apply migration 005 - additional watcher functionality
611
+ this.db.transaction(() => {
612
+ // Add is_active column to context_watchers if not exists
613
+ const watchers_columns = this.db
614
+ .prepare('PRAGMA table_info(context_watchers)')
615
+ .all();
616
+ if (!watchers_columns.some((col) => col.name === 'is_active')) {
617
+ this.db.exec('ALTER TABLE context_watchers ADD COLUMN is_active INTEGER DEFAULT 1');
618
+ this.db.exec('CREATE INDEX IF NOT EXISTS idx_watchers_active ON context_watchers(is_active)');
619
+ }
620
+ // Add sequence_number column to context_items if not exists
621
+ const columns = this.db.prepare('PRAGMA table_info(context_items)').all();
622
+ if (!columns.some((col) => col.name === 'sequence_number')) {
623
+ this.db.exec('ALTER TABLE context_items ADD COLUMN sequence_number INTEGER DEFAULT 0');
624
+ // Update existing rows with sequence numbers
625
+ this.db.exec(`
626
+ UPDATE context_items
627
+ SET sequence_number = (
628
+ SELECT COUNT(*)
629
+ FROM context_items c2
630
+ WHERE c2.session_id = context_items.session_id
631
+ AND c2.created_at <= context_items.created_at
632
+ )
633
+ WHERE sequence_number = 0
634
+ `);
635
+ // Create trigger to auto-increment sequence numbers for new inserts
636
+ this.db.exec(`
637
+ CREATE TRIGGER IF NOT EXISTS increment_sequence_insert
638
+ AFTER INSERT ON context_items
639
+ FOR EACH ROW
640
+ WHEN NEW.sequence_number = 0
641
+ BEGIN
642
+ UPDATE context_items
643
+ SET sequence_number = (
644
+ SELECT COALESCE(MAX(sequence_number), 0) + 1
645
+ FROM context_items
646
+ WHERE session_id = NEW.session_id
647
+ )
648
+ WHERE id = NEW.id;
649
+ END
650
+ `);
651
+ // Create trigger to update sequence numbers on updates
652
+ this.db.exec(`
653
+ CREATE TRIGGER IF NOT EXISTS increment_sequence_update
654
+ AFTER UPDATE OF value, metadata, category, priority, channel ON context_items
655
+ FOR EACH ROW
656
+ WHEN OLD.value != NEW.value OR
657
+ IFNULL(OLD.metadata, '') != IFNULL(NEW.metadata, '') OR
658
+ IFNULL(OLD.category, '') != IFNULL(NEW.category, '') OR
659
+ IFNULL(OLD.priority, '') != IFNULL(NEW.priority, '') OR
660
+ IFNULL(OLD.channel, '') != IFNULL(NEW.channel, '')
661
+ BEGIN
662
+ UPDATE context_items
663
+ SET sequence_number = (
664
+ SELECT COALESCE(MAX(sequence_number), 0) + 1
665
+ FROM context_items
666
+ WHERE session_id = NEW.session_id
667
+ )
668
+ WHERE id = NEW.id;
669
+ END
670
+ `);
671
+ }
672
+ // Create table for tracking deleted items (needed for tests)
673
+ this.db.exec(`
674
+ CREATE TABLE IF NOT EXISTS deleted_items (
675
+ id TEXT PRIMARY KEY,
676
+ session_id TEXT NOT NULL,
677
+ key TEXT NOT NULL,
678
+ category TEXT,
679
+ channel TEXT,
680
+ sequence_number INTEGER NOT NULL,
681
+ deleted_at TEXT DEFAULT (datetime('now')),
682
+ FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
683
+ )
684
+ `);
685
+ this.db.exec('CREATE INDEX IF NOT EXISTS idx_deleted_items_session ON deleted_items(session_id)');
686
+ this.db.exec('CREATE INDEX IF NOT EXISTS idx_deleted_items_key ON deleted_items(key)');
687
+ })();
688
+ // Record migrations in the migrations table for tracking
689
+ try {
690
+ // Check if migrations table exists
691
+ const migrationTableExists = this.db
692
+ .prepare(`SELECT COUNT(*) as count FROM sqlite_master WHERE type='table' AND name='migrations'`)
693
+ .get();
694
+ if (migrationTableExists.count > 0) {
695
+ // Record migration 004
696
+ const migration004Exists = this.db
697
+ .prepare('SELECT COUNT(*) as count FROM migrations WHERE version = ?')
698
+ .get('0.4.0');
699
+ if (migration004Exists.count === 0) {
700
+ this.db
701
+ .prepare(`INSERT INTO migrations (id, version, name, description, up_sql, applied_at)
702
+ VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`)
703
+ .run('004_add_context_watch', '0.4.0', '004_add_context_watch', 'Add context watch functionality with change tracking', 'See migration file for full SQL');
704
+ }
705
+ // Record migration 005
706
+ const migration005Exists = this.db
707
+ .prepare('SELECT COUNT(*) as count FROM migrations WHERE version = ?')
708
+ .get('0.5.0');
709
+ if (migration005Exists.count === 0) {
710
+ this.db
711
+ .prepare(`INSERT INTO migrations (id, version, name, description, up_sql, applied_at)
712
+ VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`)
713
+ .run('005_add_context_watch', '0.5.0', '005_add_context_watch', 'Add missing context watch functionality', 'See migration file for full SQL');
714
+ }
715
+ }
716
+ }
717
+ catch (error) {
718
+ console.warn('Could not record migrations in tracking table:', error);
719
+ }
720
+ // console.log('Watcher migrations applied successfully');
721
+ }
722
+ else {
723
+ // console.log('Watcher migrations already applied, skipping.');
724
+ }
725
+ }
726
+ catch (error) {
727
+ console.error('Failed to apply watcher migrations:', error);
728
+ // For production, we should fail here since watcher functionality is critical
729
+ throw new Error(`Critical error: Watcher migrations failed - ${error}`);
730
+ }
731
+ }
732
+ applyFts5Migration() {
733
+ try {
734
+ const exists = this.db
735
+ .prepare("SELECT COUNT(*) as count FROM sqlite_master WHERE type='table' AND name='context_items_fts'")
736
+ .get();
737
+ if (exists.count > 0)
738
+ return;
739
+ this.db.transaction(() => {
740
+ // External-content FTS5 table backed by context_items; trigram tokenizer
741
+ // enables substring matching for both ASCII and CJK (≥3-char terms).
742
+ this.db.exec(`
743
+ CREATE VIRTUAL TABLE IF NOT EXISTS context_items_fts USING fts5(
744
+ key, value,
745
+ content='context_items',
746
+ content_rowid='rowid',
747
+ tokenize='trigram case_sensitive 0'
748
+ );
749
+
750
+ -- Sync triggers (all three required to keep FTS consistent)
751
+ CREATE TRIGGER IF NOT EXISTS fts_ai AFTER INSERT ON context_items BEGIN
752
+ INSERT INTO context_items_fts(rowid, key, value)
753
+ VALUES (new.rowid, new.key, new.value);
754
+ END;
755
+
756
+ CREATE TRIGGER IF NOT EXISTS fts_ad AFTER DELETE ON context_items BEGIN
757
+ INSERT INTO context_items_fts(context_items_fts, rowid, key, value)
758
+ VALUES ('delete', old.rowid, old.key, old.value);
759
+ END;
760
+
761
+ CREATE TRIGGER IF NOT EXISTS fts_au AFTER UPDATE ON context_items BEGIN
762
+ INSERT INTO context_items_fts(context_items_fts, rowid, key, value)
763
+ VALUES ('delete', old.rowid, old.key, old.value);
764
+ INSERT INTO context_items_fts(rowid, key, value)
765
+ VALUES (new.rowid, new.key, new.value);
766
+ END;
767
+
768
+ INSERT INTO context_items_fts(rowid, key, value)
769
+ SELECT rowid, key, value FROM context_items;
770
+ `);
771
+ })();
772
+ }
773
+ catch (e) {
774
+ // Non-fatal: FTS5 trigram is an optional enhancement.
775
+ // LIKE-based search continues to work if this migration is skipped.
776
+ console.warn('FTS5 migration skipped (trigram tokenizer may not be available):', e);
777
+ }
778
+ }
779
+ }
780
+ exports.DatabaseManager = DatabaseManager;