mcp-memory-keeper 0.10.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 (98) hide show
  1. package/CHANGELOG.md +433 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1051 -0
  4. package/bin/mcp-memory-keeper +52 -0
  5. package/dist/__tests__/helpers/database-test-helper.js +160 -0
  6. package/dist/__tests__/helpers/test-server.js +92 -0
  7. package/dist/__tests__/integration/advanced-features.test.js +614 -0
  8. package/dist/__tests__/integration/backward-compatibility.test.js +245 -0
  9. package/dist/__tests__/integration/batchOperationsE2E.test.js +396 -0
  10. package/dist/__tests__/integration/batchOperationsHandler.test.js +1230 -0
  11. package/dist/__tests__/integration/channelManagementHandler.test.js +1291 -0
  12. package/dist/__tests__/integration/channels.test.js +376 -0
  13. package/dist/__tests__/integration/checkpoint.test.js +251 -0
  14. package/dist/__tests__/integration/concurrent-access.test.js +190 -0
  15. package/dist/__tests__/integration/context-operations.test.js +243 -0
  16. package/dist/__tests__/integration/contextDiff.test.js +852 -0
  17. package/dist/__tests__/integration/contextDiffHandler.test.js +976 -0
  18. package/dist/__tests__/integration/contextExportHandler.test.js +510 -0
  19. package/dist/__tests__/integration/contextGetPaginationDefaults.test.js +298 -0
  20. package/dist/__tests__/integration/contextReassignChannelHandler.test.js +908 -0
  21. package/dist/__tests__/integration/contextRelationshipsHandler.test.js +1151 -0
  22. package/dist/__tests__/integration/contextSearch.test.js +938 -0
  23. package/dist/__tests__/integration/contextSearchHandler.test.js +552 -0
  24. package/dist/__tests__/integration/contextWatchActual.test.js +165 -0
  25. package/dist/__tests__/integration/contextWatchHandler.test.js +1500 -0
  26. package/dist/__tests__/integration/cross-session-sharing.test.js +302 -0
  27. package/dist/__tests__/integration/database-initialization.test.js +134 -0
  28. package/dist/__tests__/integration/enhanced-context-operations.test.js +1082 -0
  29. package/dist/__tests__/integration/enhancedContextGetHandler.test.js +915 -0
  30. package/dist/__tests__/integration/enhancedContextTimelineHandler.test.js +716 -0
  31. package/dist/__tests__/integration/error-cases.test.js +407 -0
  32. package/dist/__tests__/integration/export-import.test.js +367 -0
  33. package/dist/__tests__/integration/feature-flags.test.js +542 -0
  34. package/dist/__tests__/integration/file-operations.test.js +264 -0
  35. package/dist/__tests__/integration/git-integration.test.js +237 -0
  36. package/dist/__tests__/integration/index-tools.test.js +496 -0
  37. package/dist/__tests__/integration/issue11-actual-bug-demo.test.js +304 -0
  38. package/dist/__tests__/integration/issue11-search-filters-bug.test.js +561 -0
  39. package/dist/__tests__/integration/issue12-checkpoint-restore-behavior.test.js +621 -0
  40. package/dist/__tests__/integration/issue13-key-validation.test.js +433 -0
  41. package/dist/__tests__/integration/knowledge-graph.test.js +338 -0
  42. package/dist/__tests__/integration/migrations.test.js +528 -0
  43. package/dist/__tests__/integration/multi-agent.test.js +546 -0
  44. package/dist/__tests__/integration/pagination-critical-fix.test.js +296 -0
  45. package/dist/__tests__/integration/paginationDefaultsHandler.test.js +600 -0
  46. package/dist/__tests__/integration/project-directory.test.js +283 -0
  47. package/dist/__tests__/integration/resource-cleanup.test.js +149 -0
  48. package/dist/__tests__/integration/retention.test.js +513 -0
  49. package/dist/__tests__/integration/search.test.js +333 -0
  50. package/dist/__tests__/integration/semantic-search.test.js +266 -0
  51. package/dist/__tests__/integration/server-initialization.test.js +307 -0
  52. package/dist/__tests__/integration/session-management.test.js +219 -0
  53. package/dist/__tests__/integration/simplified-sharing.test.js +346 -0
  54. package/dist/__tests__/integration/smart-compaction.test.js +230 -0
  55. package/dist/__tests__/integration/summarization.test.js +308 -0
  56. package/dist/__tests__/integration/watcher-migration-validation.test.js +544 -0
  57. package/dist/__tests__/security/input-validation.test.js +115 -0
  58. package/dist/__tests__/utils/agents.test.js +473 -0
  59. package/dist/__tests__/utils/database.test.js +177 -0
  60. package/dist/__tests__/utils/git.test.js +122 -0
  61. package/dist/__tests__/utils/knowledge-graph.test.js +297 -0
  62. package/dist/__tests__/utils/migrationHealthCheck.test.js +302 -0
  63. package/dist/__tests__/utils/project-directory-messages.test.js +188 -0
  64. package/dist/__tests__/utils/timezone-safe-dates.js +119 -0
  65. package/dist/__tests__/utils/validation.test.js +200 -0
  66. package/dist/__tests__/utils/vector-store.test.js +231 -0
  67. package/dist/handlers/contextWatchHandlers.js +206 -0
  68. package/dist/index.js +4310 -0
  69. package/dist/index.phase1.backup.js +410 -0
  70. package/dist/index.phase2.backup.js +704 -0
  71. package/dist/migrations/003_add_channels.js +174 -0
  72. package/dist/migrations/004_add_context_watch.js +151 -0
  73. package/dist/migrations/005_add_context_watch.js +98 -0
  74. package/dist/migrations/simplify-sharing.js +117 -0
  75. package/dist/repositories/BaseRepository.js +30 -0
  76. package/dist/repositories/CheckpointRepository.js +140 -0
  77. package/dist/repositories/ContextRepository.js +1873 -0
  78. package/dist/repositories/FileRepository.js +104 -0
  79. package/dist/repositories/RepositoryManager.js +62 -0
  80. package/dist/repositories/SessionRepository.js +66 -0
  81. package/dist/repositories/WatcherRepository.js +252 -0
  82. package/dist/repositories/index.js +15 -0
  83. package/dist/server.js +384 -0
  84. package/dist/test-helpers/database-helper.js +128 -0
  85. package/dist/types/entities.js +3 -0
  86. package/dist/utils/agents.js +791 -0
  87. package/dist/utils/channels.js +150 -0
  88. package/dist/utils/database.js +731 -0
  89. package/dist/utils/feature-flags.js +476 -0
  90. package/dist/utils/git.js +145 -0
  91. package/dist/utils/knowledge-graph.js +264 -0
  92. package/dist/utils/migrationHealthCheck.js +373 -0
  93. package/dist/utils/migrations.js +452 -0
  94. package/dist/utils/retention.js +460 -0
  95. package/dist/utils/timestamps.js +112 -0
  96. package/dist/utils/validation.js +296 -0
  97. package/dist/utils/vector-store.js +247 -0
  98. package/package.json +84 -0
@@ -0,0 +1,614 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const database_1 = require("../../utils/database");
37
+ const os = __importStar(require("os"));
38
+ const path = __importStar(require("path"));
39
+ const fs = __importStar(require("fs"));
40
+ const uuid_1 = require("uuid");
41
+ describe('Advanced Features Integration Tests', () => {
42
+ let dbManager;
43
+ let tempDbPath;
44
+ let db;
45
+ let testSessionId;
46
+ beforeEach(() => {
47
+ tempDbPath = path.join(os.tmpdir(), `test-advanced-${Date.now()}.db`);
48
+ dbManager = new database_1.DatabaseManager({
49
+ filename: tempDbPath,
50
+ maxSize: 10 * 1024 * 1024,
51
+ walMode: true,
52
+ });
53
+ db = dbManager.getDatabase();
54
+ // Create all necessary tables including Phase 4.4 enhancements
55
+ db.exec(`
56
+ -- Sessions table with parent_id for branching
57
+ CREATE TABLE IF NOT EXISTS sessions (
58
+ id TEXT PRIMARY KEY,
59
+ name TEXT,
60
+ description TEXT,
61
+ branch TEXT,
62
+ parent_id TEXT,
63
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
64
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
65
+ FOREIGN KEY (parent_id) REFERENCES sessions(id)
66
+ );
67
+
68
+ -- Context items table
69
+ CREATE TABLE IF NOT EXISTS context_items (
70
+ id TEXT PRIMARY KEY,
71
+ session_id TEXT NOT NULL,
72
+ key TEXT NOT NULL,
73
+ value TEXT NOT NULL,
74
+ category TEXT,
75
+ priority TEXT DEFAULT 'normal',
76
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
77
+ FOREIGN KEY (session_id) REFERENCES sessions(id),
78
+ UNIQUE(session_id, key)
79
+ );
80
+
81
+ -- File cache table
82
+ CREATE TABLE IF NOT EXISTS file_cache (
83
+ id TEXT PRIMARY KEY,
84
+ session_id TEXT NOT NULL,
85
+ file_path TEXT NOT NULL,
86
+ content TEXT,
87
+ hash TEXT,
88
+ last_read TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
89
+ FOREIGN KEY (session_id) REFERENCES sessions(id),
90
+ UNIQUE(session_id, file_path)
91
+ );
92
+
93
+ -- Journal entries table
94
+ CREATE TABLE IF NOT EXISTS journal_entries (
95
+ id TEXT PRIMARY KEY,
96
+ session_id TEXT NOT NULL,
97
+ entry TEXT NOT NULL,
98
+ tags TEXT,
99
+ mood TEXT,
100
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
101
+ FOREIGN KEY (session_id) REFERENCES sessions(id)
102
+ );
103
+
104
+ -- Compressed context table
105
+ CREATE TABLE IF NOT EXISTS compressed_context (
106
+ id TEXT PRIMARY KEY,
107
+ session_id TEXT NOT NULL,
108
+ original_count INTEGER NOT NULL,
109
+ compressed_data TEXT NOT NULL,
110
+ compression_ratio REAL NOT NULL,
111
+ date_range_start TIMESTAMP,
112
+ date_range_end TIMESTAMP,
113
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
114
+ FOREIGN KEY (session_id) REFERENCES sessions(id)
115
+ );
116
+
117
+ -- Cross-tool integration events
118
+ CREATE TABLE IF NOT EXISTS tool_events (
119
+ id TEXT PRIMARY KEY,
120
+ session_id TEXT NOT NULL,
121
+ tool_name TEXT NOT NULL,
122
+ event_type TEXT NOT NULL,
123
+ data TEXT,
124
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
125
+ FOREIGN KEY (session_id) REFERENCES sessions(id)
126
+ );
127
+ `);
128
+ // Create test session
129
+ testSessionId = (0, uuid_1.v4)();
130
+ db.prepare('INSERT INTO sessions (id, name, description) VALUES (?, ?, ?)').run(testSessionId, 'Advanced Features Test', 'Testing Phase 4.4 features');
131
+ });
132
+ afterEach(() => {
133
+ dbManager.close();
134
+ try {
135
+ fs.unlinkSync(tempDbPath);
136
+ fs.unlinkSync(`${tempDbPath}-wal`);
137
+ fs.unlinkSync(`${tempDbPath}-shm`);
138
+ }
139
+ catch (_e) {
140
+ // Ignore
141
+ }
142
+ });
143
+ describe('Session Branching', () => {
144
+ beforeEach(() => {
145
+ // Add test data to source session
146
+ const items = [
147
+ { key: 'task_1', value: 'High priority task', category: 'task', priority: 'high' },
148
+ { key: 'task_2', value: 'Normal priority task', category: 'task', priority: 'normal' },
149
+ {
150
+ key: 'decision_1',
151
+ value: 'Architecture decision',
152
+ category: 'decision',
153
+ priority: 'high',
154
+ },
155
+ { key: 'note_1', value: 'Implementation note', category: 'note', priority: 'normal' },
156
+ ];
157
+ for (const item of items) {
158
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, category, priority) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, item.key, item.value, item.category, item.priority);
159
+ }
160
+ // Add file cache
161
+ db.prepare('INSERT INTO file_cache (id, session_id, file_path, content, hash) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), testSessionId, '/test/file.js', 'console.log("test");', 'abc123');
162
+ });
163
+ it('should create shallow branch with only high priority items', () => {
164
+ // Create branch
165
+ const branchName = 'feature-branch';
166
+ const branchId = (0, uuid_1.v4)();
167
+ db.prepare(`
168
+ INSERT INTO sessions (id, name, description, parent_id)
169
+ VALUES (?, ?, ?, ?)
170
+ `).run(branchId, branchName, `Branch of test session`, testSessionId);
171
+ // Copy high priority items
172
+ const highPriorityItems = db
173
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND priority = ?')
174
+ .all(testSessionId, 'high');
175
+ expect(highPriorityItems.length).toBe(2);
176
+ for (const item of highPriorityItems) {
177
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, category, priority) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), branchId, item.key, item.value, item.category, item.priority);
178
+ }
179
+ // Verify branch has only high priority items
180
+ const branchItems = db
181
+ .prepare('SELECT * FROM context_items WHERE session_id = ?')
182
+ .all(branchId);
183
+ expect(branchItems.length).toBe(2);
184
+ expect(branchItems.every((item) => item.priority === 'high')).toBe(true);
185
+ });
186
+ it('should create deep branch with all items and files', () => {
187
+ const branchName = 'full-branch';
188
+ const branchId = (0, uuid_1.v4)();
189
+ db.prepare(`
190
+ INSERT INTO sessions (id, name, description, parent_id)
191
+ VALUES (?, ?, ?, ?)
192
+ `).run(branchId, branchName, `Branch of test session`, testSessionId);
193
+ // Copy all items
194
+ const allItems = db
195
+ .prepare('SELECT * FROM context_items WHERE session_id = ?')
196
+ .all(testSessionId);
197
+ for (const item of allItems) {
198
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, category, priority) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), branchId, item.key, item.value, item.category, item.priority);
199
+ }
200
+ // Copy files
201
+ const files = db
202
+ .prepare('SELECT * FROM file_cache WHERE session_id = ?')
203
+ .all(testSessionId);
204
+ for (const file of files) {
205
+ db.prepare('INSERT INTO file_cache (id, session_id, file_path, content, hash) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), branchId, file.file_path, file.content, file.hash);
206
+ }
207
+ // Verify
208
+ const branchItems = db
209
+ .prepare('SELECT * FROM context_items WHERE session_id = ?')
210
+ .all(branchId);
211
+ const branchFiles = db
212
+ .prepare('SELECT * FROM file_cache WHERE session_id = ?')
213
+ .all(branchId);
214
+ expect(branchItems.length).toBe(4);
215
+ expect(branchFiles.length).toBe(1);
216
+ });
217
+ it('should track parent-child relationship', () => {
218
+ const branchId = (0, uuid_1.v4)();
219
+ db.prepare(`
220
+ INSERT INTO sessions (id, name, parent_id)
221
+ VALUES (?, ?, ?)
222
+ `).run(branchId, 'child-branch', testSessionId);
223
+ const branch = db.prepare('SELECT * FROM sessions WHERE id = ?').get(branchId);
224
+ expect(branch.parent_id).toBe(testSessionId);
225
+ });
226
+ });
227
+ describe('Session Merging', () => {
228
+ let sourceSessionId;
229
+ let targetSessionId;
230
+ beforeEach(() => {
231
+ // Create source and target sessions
232
+ sourceSessionId = (0, uuid_1.v4)();
233
+ targetSessionId = (0, uuid_1.v4)();
234
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(sourceSessionId, 'Source Session');
235
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(targetSessionId, 'Target Session');
236
+ // Add items to source
237
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), sourceSessionId, 'unique_item', 'Only in source');
238
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), sourceSessionId, 'shared_item', 'Source version');
239
+ // Add items to target
240
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), targetSessionId, 'shared_item', 'Target version');
241
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), targetSessionId, 'target_only', 'Only in target');
242
+ });
243
+ it('should merge with keep_current conflict resolution', () => {
244
+ const sourceItems = db
245
+ .prepare('SELECT * FROM context_items WHERE session_id = ?')
246
+ .all(sourceSessionId);
247
+ for (const item of sourceItems) {
248
+ const existing = db
249
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND key = ?')
250
+ .get(targetSessionId, item.key);
251
+ if (!existing) {
252
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), targetSessionId, item.key, item.value);
253
+ }
254
+ // keep_current means we don't update existing items
255
+ }
256
+ const finalItems = db
257
+ .prepare('SELECT * FROM context_items WHERE session_id = ?')
258
+ .all(targetSessionId);
259
+ const sharedItem = finalItems.find((i) => i.key === 'shared_item');
260
+ expect(finalItems.length).toBe(3);
261
+ expect(sharedItem.value).toBe('Target version'); // Kept current
262
+ });
263
+ it('should merge with keep_source conflict resolution', () => {
264
+ const sourceItems = db
265
+ .prepare('SELECT * FROM context_items WHERE session_id = ?')
266
+ .all(sourceSessionId);
267
+ for (const item of sourceItems) {
268
+ const existing = db
269
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND key = ?')
270
+ .get(targetSessionId, item.key);
271
+ if (existing) {
272
+ db.prepare('UPDATE context_items SET value = ? WHERE session_id = ? AND key = ?').run(item.value, targetSessionId, item.key);
273
+ }
274
+ else {
275
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), targetSessionId, item.key, item.value);
276
+ }
277
+ }
278
+ const finalItems = db
279
+ .prepare('SELECT * FROM context_items WHERE session_id = ?')
280
+ .all(targetSessionId);
281
+ const sharedItem = finalItems.find((i) => i.key === 'shared_item');
282
+ expect(finalItems.length).toBe(3);
283
+ expect(sharedItem.value).toBe('Source version'); // Replaced with source
284
+ });
285
+ it('should merge with keep_newest conflict resolution', () => {
286
+ // First, create the target item with an older timestamp
287
+ const oldDate = new Date();
288
+ oldDate.setHours(oldDate.getHours() - 1); // 1 hour ago
289
+ // Update target item to be older
290
+ db.prepare('UPDATE context_items SET created_at = ? WHERE session_id = ? AND key = ?').run(oldDate.toISOString(), targetSessionId, 'shared_item');
291
+ // Source item has current timestamp, so it's newer
292
+ const sourceItems = db
293
+ .prepare('SELECT * FROM context_items WHERE session_id = ?')
294
+ .all(sourceSessionId);
295
+ for (const item of sourceItems) {
296
+ const existing = db
297
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND key = ?')
298
+ .get(targetSessionId, item.key);
299
+ if (existing) {
300
+ if (new Date(item.created_at) > new Date(existing.created_at)) {
301
+ db.prepare('UPDATE context_items SET value = ? WHERE session_id = ? AND key = ?').run(item.value, targetSessionId, item.key);
302
+ }
303
+ }
304
+ else {
305
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), targetSessionId, item.key, item.value);
306
+ }
307
+ }
308
+ const finalItems = db
309
+ .prepare('SELECT * FROM context_items WHERE session_id = ?')
310
+ .all(targetSessionId);
311
+ const sharedItem = finalItems.find((i) => i.key === 'shared_item');
312
+ expect(finalItems.length).toBe(3);
313
+ expect(sharedItem.value).toBe('Source version'); // Source was newer
314
+ });
315
+ });
316
+ describe('Journal Entries', () => {
317
+ it('should create journal entry with tags and mood', () => {
318
+ const entry = 'Had a productive day working on the authentication module';
319
+ const tags = ['productivity', 'authentication', 'backend'];
320
+ const mood = 'accomplished';
321
+ const id = (0, uuid_1.v4)();
322
+ db.prepare(`
323
+ INSERT INTO journal_entries (id, session_id, entry, tags, mood)
324
+ VALUES (?, ?, ?, ?, ?)
325
+ `).run(id, testSessionId, entry, JSON.stringify(tags), mood);
326
+ const saved = db.prepare('SELECT * FROM journal_entries WHERE id = ?').get(id);
327
+ expect(saved.entry).toBe(entry);
328
+ expect(JSON.parse(saved.tags)).toEqual(tags);
329
+ expect(saved.mood).toBe(mood);
330
+ });
331
+ it('should retrieve journal entries by session', () => {
332
+ // Add multiple entries
333
+ const entries = [
334
+ { entry: 'Morning standup went well', mood: 'positive', tags: ['meeting'] },
335
+ { entry: 'Debugging session was challenging', mood: 'frustrated', tags: ['debugging'] },
336
+ { entry: 'Fixed the bug!', mood: 'excited', tags: ['debugging', 'success'] },
337
+ ];
338
+ for (const e of entries) {
339
+ db.prepare(`
340
+ INSERT INTO journal_entries (id, session_id, entry, tags, mood)
341
+ VALUES (?, ?, ?, ?, ?)
342
+ `).run((0, uuid_1.v4)(), testSessionId, e.entry, JSON.stringify(e.tags), e.mood);
343
+ }
344
+ const journalEntries = db
345
+ .prepare('SELECT * FROM journal_entries WHERE session_id = ? ORDER BY created_at')
346
+ .all(testSessionId);
347
+ expect(journalEntries.length).toBe(3);
348
+ expect(journalEntries[0].entry).toBe('Morning standup went well');
349
+ expect(journalEntries[2].mood).toBe('excited');
350
+ });
351
+ it('should filter journal entries by date range', () => {
352
+ const yesterday = new Date();
353
+ yesterday.setDate(yesterday.getDate() - 1);
354
+ const tomorrow = new Date();
355
+ tomorrow.setDate(tomorrow.getDate() + 1);
356
+ // Add entry
357
+ db.prepare(`
358
+ INSERT INTO journal_entries (id, session_id, entry)
359
+ VALUES (?, ?, ?)
360
+ `).run((0, uuid_1.v4)(), testSessionId, 'Today entry');
361
+ const entries = db
362
+ .prepare('SELECT * FROM journal_entries WHERE session_id = ? AND created_at >= ? AND created_at <= ?')
363
+ .all(testSessionId, yesterday.toISOString(), tomorrow.toISOString());
364
+ expect(entries.length).toBe(1);
365
+ });
366
+ });
367
+ describe('Timeline Generation', () => {
368
+ beforeEach(() => {
369
+ // Create items across different dates
370
+ const dates = [
371
+ new Date('2024-01-15T10:00:00'),
372
+ new Date('2024-01-15T14:00:00'),
373
+ new Date('2024-01-16T09:00:00'),
374
+ new Date('2024-01-16T11:00:00'),
375
+ new Date('2024-01-16T15:00:00'),
376
+ ];
377
+ const categories = ['task', 'decision', 'task', 'progress', 'note'];
378
+ dates.forEach((date, index) => {
379
+ db.prepare(`
380
+ INSERT INTO context_items (id, session_id, key, value, category, created_at)
381
+ VALUES (?, ?, ?, ?, ?, ?)
382
+ `).run((0, uuid_1.v4)(), testSessionId, `item_${index}`, `Item ${index}`, categories[index], date.toISOString());
383
+ });
384
+ });
385
+ it('should group timeline by day', () => {
386
+ const timeline = db
387
+ .prepare(`
388
+ SELECT
389
+ strftime('%Y-%m-%d', created_at) as date,
390
+ COUNT(*) as count,
391
+ category
392
+ FROM context_items
393
+ WHERE session_id = ?
394
+ GROUP BY date, category
395
+ ORDER BY date
396
+ `)
397
+ .all(testSessionId);
398
+ const dateGroups = {};
399
+ for (const item of timeline) {
400
+ if (!dateGroups[item.date]) {
401
+ dateGroups[item.date] = { total: 0, categories: {} };
402
+ }
403
+ dateGroups[item.date].categories[item.category] = item.count;
404
+ dateGroups[item.date].total += item.count;
405
+ }
406
+ expect(Object.keys(dateGroups).length).toBe(2); // 2 different days
407
+ expect(dateGroups['2024-01-15'].total).toBe(2);
408
+ expect(dateGroups['2024-01-16'].total).toBe(3);
409
+ });
410
+ it('should group timeline by hour', () => {
411
+ const timeline = db
412
+ .prepare(`
413
+ SELECT
414
+ strftime('%Y-%m-%d', created_at) as date,
415
+ strftime('%H', created_at) as hour,
416
+ COUNT(*) as count
417
+ FROM context_items
418
+ WHERE session_id = ?
419
+ GROUP BY date, hour
420
+ ORDER BY date, hour
421
+ `)
422
+ .all(testSessionId);
423
+ expect(timeline.length).toBe(5); // 5 different hours
424
+ // Just verify we have different hours, don't check specific values due to timezone
425
+ const hours = timeline.map((t) => parseInt(t.hour));
426
+ expect(new Set(hours).size).toBe(5); // All hours are different
427
+ });
428
+ it('should include journal entries in timeline', () => {
429
+ // Add journal entry
430
+ db.prepare(`
431
+ INSERT INTO journal_entries (id, session_id, entry, created_at)
432
+ VALUES (?, ?, ?, ?)
433
+ `).run((0, uuid_1.v4)(), testSessionId, 'Daily reflection', '2024-01-15T20:00:00');
434
+ const journals = db
435
+ .prepare('SELECT * FROM journal_entries WHERE session_id = ? ORDER BY created_at')
436
+ .all(testSessionId);
437
+ expect(journals.length).toBe(1);
438
+ expect(journals[0].entry).toBe('Daily reflection');
439
+ });
440
+ });
441
+ describe('Progressive Compression', () => {
442
+ beforeEach(() => {
443
+ // Create old and new items
444
+ const oldDate = new Date();
445
+ oldDate.setDate(oldDate.getDate() - 30);
446
+ const items = [
447
+ {
448
+ key: 'old_task_1',
449
+ value: 'Old task 1',
450
+ created_at: oldDate.toISOString(),
451
+ category: 'task',
452
+ },
453
+ {
454
+ key: 'old_task_2',
455
+ value: 'Old task 2',
456
+ created_at: oldDate.toISOString(),
457
+ category: 'task',
458
+ },
459
+ {
460
+ key: 'old_decision',
461
+ value: 'Old decision',
462
+ created_at: oldDate.toISOString(),
463
+ category: 'decision',
464
+ },
465
+ {
466
+ key: 'recent_task',
467
+ value: 'Recent task',
468
+ created_at: new Date().toISOString(),
469
+ category: 'task',
470
+ },
471
+ {
472
+ key: 'preserve_me',
473
+ value: 'Important decision',
474
+ created_at: oldDate.toISOString(),
475
+ category: 'critical',
476
+ },
477
+ ];
478
+ for (const item of items) {
479
+ db.prepare(`
480
+ INSERT INTO context_items (id, session_id, key, value, category, created_at)
481
+ VALUES (?, ?, ?, ?, ?, ?)
482
+ `).run((0, uuid_1.v4)(), testSessionId, item.key, item.value, item.category, item.created_at);
483
+ }
484
+ });
485
+ it('should compress old items', () => {
486
+ const cutoffDate = new Date();
487
+ cutoffDate.setDate(cutoffDate.getDate() - 7);
488
+ const itemsToCompress = db
489
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND created_at < ?')
490
+ .all(testSessionId, cutoffDate.toISOString());
491
+ expect(itemsToCompress.length).toBe(4); // All old items
492
+ // Group by category
493
+ const categoryGroups = {};
494
+ for (const item of itemsToCompress) {
495
+ const category = item.category || 'uncategorized';
496
+ if (!categoryGroups[category]) {
497
+ categoryGroups[category] = [];
498
+ }
499
+ categoryGroups[category].push(item);
500
+ }
501
+ expect(Object.keys(categoryGroups).length).toBe(3); // task, decision, critical
502
+ expect(categoryGroups.task.length).toBe(2);
503
+ });
504
+ it('should preserve specified categories', () => {
505
+ const cutoffDate = new Date();
506
+ cutoffDate.setDate(cutoffDate.getDate() - 7);
507
+ const preserveCategories = ['critical', 'decision'];
508
+ const query = `
509
+ SELECT * FROM context_items
510
+ WHERE session_id = ?
511
+ AND created_at < ?
512
+ AND category NOT IN (${preserveCategories.map(() => '?').join(',')})
513
+ `;
514
+ const itemsToCompress = db
515
+ .prepare(query)
516
+ .all(testSessionId, cutoffDate.toISOString(), ...preserveCategories);
517
+ expect(itemsToCompress.length).toBe(2); // Only old tasks
518
+ expect(itemsToCompress.every((item) => item.category === 'task')).toBe(true);
519
+ });
520
+ it('should calculate compression ratio', () => {
521
+ const items = db
522
+ .prepare('SELECT * FROM context_items WHERE session_id = ?')
523
+ .all(testSessionId);
524
+ const compressed = {
525
+ categories: {
526
+ task: { count: 3, samples: [] },
527
+ decision: { count: 1, samples: [] },
528
+ critical: { count: 1, samples: [] },
529
+ },
530
+ };
531
+ const originalSize = JSON.stringify(items).length;
532
+ const compressedSize = JSON.stringify(compressed).length;
533
+ const compressionRatio = 1 - compressedSize / originalSize;
534
+ expect(compressionRatio).toBeGreaterThan(0.5); // Should achieve >50% compression
535
+ });
536
+ it('should store compressed data', () => {
537
+ const compressedData = JSON.stringify({
538
+ categories: { task: { count: 2 } },
539
+ });
540
+ const id = (0, uuid_1.v4)();
541
+ const now = new Date().toISOString();
542
+ db.prepare(`
543
+ INSERT INTO compressed_context (id, session_id, original_count, compressed_data, compression_ratio, date_range_start, date_range_end)
544
+ VALUES (?, ?, ?, ?, ?, ?, ?)
545
+ `).run(id, testSessionId, 2, compressedData, 0.75, now, now);
546
+ const saved = db.prepare('SELECT * FROM compressed_context WHERE id = ?').get(id);
547
+ expect(saved.original_count).toBe(2);
548
+ expect(saved.compression_ratio).toBe(0.75);
549
+ expect(JSON.parse(saved.compressed_data).categories.task.count).toBe(2);
550
+ });
551
+ });
552
+ describe('Cross-Tool Integration', () => {
553
+ it('should record tool events', () => {
554
+ const toolName = 'code-analyzer';
555
+ const eventType = 'analysis-complete';
556
+ const data = { files: 10, issues: 3 };
557
+ const id = (0, uuid_1.v4)();
558
+ db.prepare(`
559
+ INSERT INTO tool_events (id, session_id, tool_name, event_type, data)
560
+ VALUES (?, ?, ?, ?, ?)
561
+ `).run(id, testSessionId, toolName, eventType, JSON.stringify(data));
562
+ const saved = db.prepare('SELECT * FROM tool_events WHERE id = ?').get(id);
563
+ expect(saved.tool_name).toBe(toolName);
564
+ expect(saved.event_type).toBe(eventType);
565
+ expect(JSON.parse(saved.data)).toEqual(data);
566
+ });
567
+ it('should create context item for important events', () => {
568
+ const toolName = 'security-scanner';
569
+ const eventType = 'vulnerability-found';
570
+ const data = {
571
+ severity: 'high',
572
+ file: 'auth.js',
573
+ important: true,
574
+ };
575
+ // Record event
576
+ const eventId = (0, uuid_1.v4)();
577
+ db.prepare(`
578
+ INSERT INTO tool_events (id, session_id, tool_name, event_type, data)
579
+ VALUES (?, ?, ?, ?, ?)
580
+ `).run(eventId, testSessionId, toolName, eventType, JSON.stringify(data));
581
+ // Create context item for important event
582
+ db.prepare(`
583
+ INSERT INTO context_items (id, session_id, key, value, category, priority)
584
+ VALUES (?, ?, ?, ?, ?, ?)
585
+ `).run((0, uuid_1.v4)(), testSessionId, `${toolName}_${eventType}_${Date.now()}`, `Tool event: ${toolName} - ${eventType}: ${JSON.stringify(data)}`, 'tool_event', 'high');
586
+ const contextItems = db
587
+ .prepare('SELECT * FROM context_items WHERE session_id = ? AND category = ?')
588
+ .all(testSessionId, 'tool_event');
589
+ expect(contextItems.length).toBe(1);
590
+ expect(contextItems[0].priority).toBe('high');
591
+ expect(contextItems[0].value).toContain('vulnerability-found');
592
+ });
593
+ it('should handle events from multiple tools', () => {
594
+ const events = [
595
+ { tool: 'linter', type: 'scan-complete', data: { warnings: 5 } },
596
+ { tool: 'test-runner', type: 'tests-passed', data: { total: 100, passed: 100 } },
597
+ { tool: 'build-tool', type: 'build-success', data: { duration: '45s' } },
598
+ ];
599
+ for (const event of events) {
600
+ db.prepare(`
601
+ INSERT INTO tool_events (id, session_id, tool_name, event_type, data)
602
+ VALUES (?, ?, ?, ?, ?)
603
+ `).run((0, uuid_1.v4)(), testSessionId, event.tool, event.type, JSON.stringify(event.data));
604
+ }
605
+ const allEvents = db
606
+ .prepare('SELECT * FROM tool_events WHERE session_id = ?')
607
+ .all(testSessionId);
608
+ expect(allEvents.length).toBe(3);
609
+ expect(allEvents.map((e) => e.tool_name)).toContain('linter');
610
+ expect(allEvents.map((e) => e.tool_name)).toContain('test-runner');
611
+ expect(allEvents.map((e) => e.tool_name)).toContain('build-tool');
612
+ });
613
+ });
614
+ });