@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,510 @@
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('Enhanced context_export Handler Tests', () => {
42
+ let dbManager;
43
+ let tempDbPath;
44
+ let tempExportPath;
45
+ let db;
46
+ let currentSessionId;
47
+ // Mock the handler function - this will be replaced with actual implementation
48
+ const contextExportHandler = async (args, db, currentSessionId) => {
49
+ // This is a mock implementation that represents the expected behavior
50
+ // In TDD, this will be replaced with the actual implementation
51
+ const { sessionId: specificSessionId, format = 'json', includeStats = false } = args;
52
+ const targetSessionId = specificSessionId || currentSessionId;
53
+ // Phase 1: Validation
54
+ if (!targetSessionId) {
55
+ throw new Error('No session ID provided and no current session active');
56
+ }
57
+ // Check if session exists
58
+ const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(targetSessionId);
59
+ if (!session) {
60
+ throw new Error(`Session not found: ${targetSessionId}`);
61
+ }
62
+ // Get session data
63
+ const contextItems = db
64
+ .prepare('SELECT * FROM context_items WHERE session_id = ?')
65
+ .all(targetSessionId);
66
+ const fileCache = db
67
+ .prepare('SELECT * FROM file_cache WHERE session_id = ?')
68
+ .all(targetSessionId);
69
+ const checkpoints = db
70
+ .prepare('SELECT * FROM checkpoints WHERE session_id = ?')
71
+ .all(targetSessionId);
72
+ // Check if session is empty
73
+ const isEmpty = contextItems.length === 0 && fileCache.length === 0 && checkpoints.length === 0;
74
+ if (isEmpty && !args.confirmEmpty) {
75
+ return {
76
+ content: [
77
+ {
78
+ type: 'text',
79
+ text: 'Warning: Session appears to be empty. No context items, files, or checkpoints found.\n\nTo export anyway, use confirmEmpty: true',
80
+ },
81
+ ],
82
+ isEmpty: true,
83
+ requiresConfirmation: true,
84
+ };
85
+ }
86
+ const exportData = {
87
+ version: '0.4.0',
88
+ exported: new Date().toISOString(),
89
+ session,
90
+ contextItems,
91
+ fileCache,
92
+ checkpoints,
93
+ metadata: {
94
+ itemCount: contextItems.length,
95
+ fileCount: fileCache.length,
96
+ checkpointCount: checkpoints.length,
97
+ totalSize: JSON.stringify({ contextItems, fileCache, checkpoints }).length,
98
+ },
99
+ };
100
+ if (format === 'json') {
101
+ const exportPath = path.join(os.tmpdir(), `memory-keeper-export-${targetSessionId.substring(0, 8)}.json`);
102
+ // Check write permissions
103
+ try {
104
+ fs.writeFileSync(exportPath, JSON.stringify(exportData, null, 2));
105
+ }
106
+ catch (error) {
107
+ if (error.code === 'EACCES') {
108
+ throw new Error(`Permission denied: Cannot write to ${exportPath}`);
109
+ }
110
+ throw error;
111
+ }
112
+ const stats = {
113
+ items: contextItems.length,
114
+ files: fileCache.length,
115
+ checkpoints: checkpoints.length,
116
+ size: fs.statSync(exportPath).size,
117
+ };
118
+ return {
119
+ content: [
120
+ {
121
+ type: 'text',
122
+ text: includeStats
123
+ ? `✅ Successfully exported session "${session.name}" to: ${exportPath}
124
+
125
+ 📊 Export Statistics:
126
+ - Context Items: ${stats.items}
127
+ - Cached Files: ${stats.files}
128
+ - Checkpoints: ${stats.checkpoints}
129
+ - Export Size: ${(stats.size / 1024).toFixed(2)} KB
130
+
131
+ Session ID: ${targetSessionId}`
132
+ : `Exported session to: ${exportPath}
133
+ Items: ${stats.items}
134
+ Files: ${stats.files}`,
135
+ },
136
+ ],
137
+ exportPath,
138
+ statistics: stats,
139
+ };
140
+ }
141
+ // Inline format
142
+ return {
143
+ content: [
144
+ {
145
+ type: 'text',
146
+ text: JSON.stringify(exportData, null, 2),
147
+ },
148
+ ],
149
+ statistics: {
150
+ items: contextItems.length,
151
+ files: fileCache.length,
152
+ checkpoints: checkpoints.length,
153
+ },
154
+ };
155
+ };
156
+ beforeEach(() => {
157
+ tempDbPath = path.join(os.tmpdir(), `test-export-handler-${Date.now()}.db`);
158
+ tempExportPath = path.join(os.tmpdir(), `test-exports-handler-${Date.now()}`);
159
+ dbManager = new database_1.DatabaseManager({
160
+ filename: tempDbPath,
161
+ maxSize: 10 * 1024 * 1024,
162
+ walMode: true,
163
+ });
164
+ db = dbManager.getDatabase();
165
+ // Create export directory
166
+ fs.mkdirSync(tempExportPath, { recursive: true });
167
+ // Set current session
168
+ currentSessionId = (0, uuid_1.v4)();
169
+ db.prepare('INSERT INTO sessions (id, name, description) VALUES (?, ?, ?)').run(currentSessionId, 'Current Session', 'Test session');
170
+ });
171
+ afterEach(() => {
172
+ dbManager.close();
173
+ try {
174
+ fs.unlinkSync(tempDbPath);
175
+ fs.unlinkSync(`${tempDbPath}-wal`);
176
+ fs.unlinkSync(`${tempDbPath}-shm`);
177
+ fs.rmSync(tempExportPath, { recursive: true, force: true });
178
+ // Clean up any export files from temp directory
179
+ const tempFiles = fs
180
+ .readdirSync(os.tmpdir())
181
+ .filter(f => f.startsWith('memory-keeper-export-'));
182
+ tempFiles.forEach(f => {
183
+ try {
184
+ fs.unlinkSync(path.join(os.tmpdir(), f));
185
+ }
186
+ catch (_e) {
187
+ // Ignore
188
+ }
189
+ });
190
+ }
191
+ catch (_e) {
192
+ // Ignore
193
+ }
194
+ });
195
+ describe('Validation Tests', () => {
196
+ it('should throw error when exporting with invalid session ID', async () => {
197
+ const invalidSessionId = (0, uuid_1.v4)();
198
+ await expect(contextExportHandler({ sessionId: invalidSessionId }, db, currentSessionId)).rejects.toThrow(`Session not found: ${invalidSessionId}`);
199
+ });
200
+ it('should throw error when no session ID provided and no current session', async () => {
201
+ await expect(contextExportHandler({}, db, null)).rejects.toThrow('No session ID provided and no current session active');
202
+ });
203
+ it('should warn when exporting empty session without confirmation', async () => {
204
+ // Create empty session
205
+ const emptySessionId = (0, uuid_1.v4)();
206
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(emptySessionId, 'Empty Session');
207
+ const result = await contextExportHandler({ sessionId: emptySessionId }, db, currentSessionId);
208
+ expect(result.isEmpty).toBe(true);
209
+ expect(result.requiresConfirmation).toBe(true);
210
+ expect(result.content[0].text).toContain('Warning: Session appears to be empty');
211
+ expect(result.content[0].text).toContain('confirmEmpty: true');
212
+ });
213
+ it('should allow exporting empty session with confirmation', async () => {
214
+ // Create empty session
215
+ const emptySessionId = (0, uuid_1.v4)();
216
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(emptySessionId, 'Empty Session');
217
+ const result = await contextExportHandler({ sessionId: emptySessionId, confirmEmpty: true }, db, currentSessionId);
218
+ expect(result.isEmpty).toBeUndefined();
219
+ expect(result.requiresConfirmation).toBeUndefined();
220
+ expect(result.exportPath).toBeDefined();
221
+ expect(result.statistics.items).toBe(0);
222
+ expect(result.statistics.files).toBe(0);
223
+ });
224
+ });
225
+ describe('Success Path Tests', () => {
226
+ it('should export session with statistics when includeStats is true', async () => {
227
+ // Add test data
228
+ const items = [
229
+ { key: 'task1', value: 'Complete feature', category: 'task', priority: 'high' },
230
+ { key: 'note1', value: 'Important note', category: 'note', priority: 'normal' },
231
+ ];
232
+ items.forEach(item => {
233
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, category, priority) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, item.key, item.value, item.category, item.priority);
234
+ });
235
+ // Add file cache
236
+ db.prepare('INSERT INTO file_cache (id, session_id, file_path, content, hash) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, '/test.ts', 'test content', 'hash123');
237
+ // Add checkpoint
238
+ db.prepare('INSERT INTO checkpoints (id, session_id, name) VALUES (?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, 'Test Checkpoint');
239
+ const result = await contextExportHandler({ includeStats: true }, db, currentSessionId);
240
+ expect(result.content[0].text).toContain('✅ Successfully exported session');
241
+ expect(result.content[0].text).toContain('📊 Export Statistics:');
242
+ expect(result.content[0].text).toContain('Context Items: 2');
243
+ expect(result.content[0].text).toContain('Cached Files: 1');
244
+ expect(result.content[0].text).toContain('Checkpoints: 1');
245
+ expect(result.content[0].text).toContain('Export Size:');
246
+ expect(result.content[0].text).toContain(`Session ID: ${currentSessionId}`);
247
+ expect(result.statistics).toEqual({
248
+ items: 2,
249
+ files: 1,
250
+ checkpoints: 1,
251
+ size: expect.any(Number),
252
+ });
253
+ });
254
+ it('should export in JSON format by default', async () => {
255
+ // Add minimal data
256
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, 'key1', 'value1');
257
+ const result = await contextExportHandler({}, db, currentSessionId);
258
+ expect(result.exportPath).toBeDefined();
259
+ expect(result.exportPath).toContain('memory-keeper-export-');
260
+ expect(result.exportPath).toMatch(/\.json$/);
261
+ // Verify file exists and has correct structure
262
+ const exportData = JSON.parse(fs.readFileSync(result.exportPath, 'utf-8'));
263
+ expect(exportData.version).toBe('0.4.0');
264
+ expect(exportData.exported).toBeDefined();
265
+ expect(exportData.session).toBeDefined();
266
+ expect(exportData.contextItems).toHaveLength(1);
267
+ expect(exportData.metadata).toEqual({
268
+ itemCount: 1,
269
+ fileCount: 0,
270
+ checkpointCount: 0,
271
+ totalSize: expect.any(Number),
272
+ });
273
+ });
274
+ it('should export in inline format when requested', async () => {
275
+ // Add test data
276
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, 'key1', 'value1');
277
+ const result = await contextExportHandler({ format: 'inline' }, db, currentSessionId);
278
+ expect(result.exportPath).toBeUndefined();
279
+ expect(result.content[0].type).toBe('text');
280
+ const exportData = JSON.parse(result.content[0].text);
281
+ expect(exportData.version).toBe('0.4.0');
282
+ expect(exportData.contextItems).toHaveLength(1);
283
+ expect(result.statistics).toEqual({
284
+ items: 1,
285
+ files: 0,
286
+ checkpoints: 0,
287
+ });
288
+ });
289
+ it('should use absolute file paths for exports', async () => {
290
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, 'key1', 'value1');
291
+ const result = await contextExportHandler({}, db, currentSessionId);
292
+ expect(path.isAbsolute(result.exportPath)).toBe(true);
293
+ expect(result.exportPath).toMatch(/^(\/|[A-Z]:\\)/); // Unix or Windows absolute path
294
+ });
295
+ it('should handle large exports with many items', async () => {
296
+ // Add many items
297
+ for (let i = 0; i < 100; i++) {
298
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, category, priority) VALUES (?, ?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, `key${i}`, `This is a test value for item ${i} with some additional content to make it larger`, i % 2 === 0 ? 'task' : 'note', i % 3 === 0 ? 'high' : 'normal');
299
+ }
300
+ const result = await contextExportHandler({ includeStats: true }, db, currentSessionId);
301
+ expect(result.statistics.items).toBe(100);
302
+ expect(result.content[0].text).toContain('Context Items: 100');
303
+ // Verify file size is reasonable
304
+ const stats = fs.statSync(result.exportPath);
305
+ expect(stats.size).toBeGreaterThan(10000); // Should be at least 10KB
306
+ expect(result.content[0].text).toMatch(/Export Size: \d+\.\d+ KB/);
307
+ });
308
+ });
309
+ describe('Error Handling Tests', () => {
310
+ it('should handle file system errors gracefully', async () => {
311
+ // Add test data
312
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, 'key1', 'value1');
313
+ // Create a mock handler that simulates fs errors
314
+ const errorHandler = async (_args, _db, _currentSessionId) => {
315
+ throw new Error('Disk full');
316
+ };
317
+ await expect(errorHandler({}, db, currentSessionId)).rejects.toThrow('Disk full');
318
+ });
319
+ it('should handle permission errors with specific message', async () => {
320
+ // Add test data
321
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, 'key1', 'value1');
322
+ // Create a mock handler that simulates permission errors
323
+ const permissionErrorHandler = async (args, _db, currentSessionId) => {
324
+ const { sessionId: specificSessionId } = args;
325
+ const targetSessionId = specificSessionId || currentSessionId;
326
+ if (!targetSessionId) {
327
+ throw new Error('No session ID provided and no current session active');
328
+ }
329
+ // Simulating permission check without using db
330
+ if (!targetSessionId) {
331
+ throw new Error(`Session not found: ${targetSessionId}`);
332
+ }
333
+ const exportPath = path.join(os.tmpdir(), `memory-keeper-export-${targetSessionId.substring(0, 8)}.json`);
334
+ const error = new Error('EACCES: permission denied');
335
+ error.code = 'EACCES';
336
+ throw new Error(`Permission denied: Cannot write to ${exportPath}`);
337
+ };
338
+ await expect(permissionErrorHandler({}, db, currentSessionId)).rejects.toThrow(/Permission denied: Cannot write to/);
339
+ });
340
+ it('should handle invalid export paths', async () => {
341
+ // This test would be implemented when path validation is added
342
+ // For now, we'll skip it as the current implementation doesn't validate paths
343
+ expect(true).toBe(true);
344
+ });
345
+ it('should handle database errors during export', async () => {
346
+ // Mock database error
347
+ const mockDb = {
348
+ prepare: jest.fn().mockImplementation((query) => {
349
+ if (query.includes('SELECT * FROM sessions')) {
350
+ return {
351
+ get: () => {
352
+ throw new Error('Database locked');
353
+ },
354
+ };
355
+ }
356
+ return {
357
+ get: () => null,
358
+ all: () => [],
359
+ };
360
+ }),
361
+ };
362
+ await expect(contextExportHandler({}, mockDb, currentSessionId)).rejects.toThrow('Database locked');
363
+ });
364
+ });
365
+ describe('Backward Compatibility Tests', () => {
366
+ it('should maintain existing behavior when no new options provided', async () => {
367
+ // Add test data
368
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, 'key1', 'value1');
369
+ db.prepare('INSERT INTO file_cache (id, session_id, file_path, content, hash) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, '/test.ts', 'content', 'hash');
370
+ // Call with no options (existing behavior)
371
+ const result = await contextExportHandler({}, db, currentSessionId);
372
+ // Should return in the old format
373
+ expect(result.content[0].text).toMatch(/^Exported session to: .+\nItems: 1\nFiles: 1$/);
374
+ expect(result.content[0].text).not.toContain('✅');
375
+ expect(result.content[0].text).not.toContain('📊');
376
+ });
377
+ it('should support existing sessionId parameter', async () => {
378
+ // Create another session
379
+ const otherSessionId = (0, uuid_1.v4)();
380
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(otherSessionId, 'Other Session');
381
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), otherSessionId, 'other_key', 'other_value');
382
+ const result = await contextExportHandler({ sessionId: otherSessionId }, db, currentSessionId);
383
+ expect(result.exportPath).toContain(otherSessionId.substring(0, 8));
384
+ const exportData = JSON.parse(fs.readFileSync(result.exportPath, 'utf-8'));
385
+ expect(exportData.session.name).toBe('Other Session');
386
+ expect(exportData.contextItems[0].key).toBe('other_key');
387
+ });
388
+ it('should support existing format parameter', async () => {
389
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, 'key1', 'value1');
390
+ // Test JSON format (default)
391
+ const jsonResult = await contextExportHandler({ format: 'json' }, db, currentSessionId);
392
+ expect(jsonResult.exportPath).toBeDefined();
393
+ // Test inline format
394
+ const inlineResult = await contextExportHandler({ format: 'inline' }, db, currentSessionId);
395
+ expect(inlineResult.exportPath).toBeUndefined();
396
+ expect(inlineResult.content[0].text).toContain('"version": "0.4.0"');
397
+ });
398
+ });
399
+ describe('Edge Cases', () => {
400
+ it('should handle session with only context items', async () => {
401
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, 'key1', 'value1');
402
+ const result = await contextExportHandler({ includeStats: true }, db, currentSessionId);
403
+ expect(result.statistics).toEqual({
404
+ items: 1,
405
+ files: 0,
406
+ checkpoints: 0,
407
+ size: expect.any(Number),
408
+ });
409
+ });
410
+ it('should handle session with only file cache', async () => {
411
+ db.prepare('INSERT INTO file_cache (id, session_id, file_path, content, hash) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, '/file.ts', 'content', 'hash');
412
+ const result = await contextExportHandler({ includeStats: true }, db, currentSessionId);
413
+ expect(result.statistics).toEqual({
414
+ items: 0,
415
+ files: 1,
416
+ checkpoints: 0,
417
+ size: expect.any(Number),
418
+ });
419
+ });
420
+ it('should handle session with only checkpoints', async () => {
421
+ db.prepare('INSERT INTO checkpoints (id, session_id, name) VALUES (?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, 'Checkpoint');
422
+ const result = await contextExportHandler({ includeStats: true }, db, currentSessionId);
423
+ expect(result.statistics).toEqual({
424
+ items: 0,
425
+ files: 0,
426
+ checkpoints: 1,
427
+ size: expect.any(Number),
428
+ });
429
+ });
430
+ it('should handle very long session names', async () => {
431
+ const longName = 'A'.repeat(500);
432
+ const longSessionId = (0, uuid_1.v4)();
433
+ db.prepare('INSERT INTO sessions (id, name) VALUES (?, ?)').run(longSessionId, longName);
434
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), longSessionId, 'key1', 'value1');
435
+ const result = await contextExportHandler({ sessionId: longSessionId, includeStats: true }, db, currentSessionId);
436
+ expect(result.content[0].text).toContain(`Successfully exported session "${longName}"`);
437
+ expect(result.exportPath).toBeDefined();
438
+ });
439
+ it('should handle special characters in session data', async () => {
440
+ const specialChars = 'Test with "quotes", \'apostrophes\', \n newlines, \t tabs, and unicode: 😀';
441
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, 'special_key', specialChars);
442
+ const result = await contextExportHandler({}, db, currentSessionId);
443
+ // Verify the exported file contains properly escaped special characters
444
+ const exportData = JSON.parse(fs.readFileSync(result.exportPath, 'utf-8'));
445
+ expect(exportData.contextItems[0].value).toBe(specialChars);
446
+ });
447
+ });
448
+ describe('Integration Tests', () => {
449
+ it('should work with complete session data including all components', async () => {
450
+ // Create comprehensive test data
451
+ const itemIds = [];
452
+ // Add context items with various categories and priorities
453
+ ['task', 'decision', 'progress', 'note', 'error', 'warning'].forEach((category, idx) => {
454
+ const itemId = (0, uuid_1.v4)();
455
+ itemIds.push(itemId);
456
+ db.prepare('INSERT INTO context_items (id, session_id, key, value, category, priority) VALUES (?, ?, ?, ?, ?, ?)').run(itemId, currentSessionId, `${category}_item`, `This is a ${category} item`, category, idx % 3 === 0 ? 'high' : idx % 3 === 1 ? 'normal' : 'low');
457
+ });
458
+ // Add file cache entries
459
+ ['/src/index.ts', '/src/utils.ts', '/tests/test.spec.ts'].forEach(filePath => {
460
+ db.prepare('INSERT INTO file_cache (id, session_id, file_path, content, hash) VALUES (?, ?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, filePath, `Content of ${filePath}`, `hash_${filePath.replace(/\//g, '_')}`);
461
+ });
462
+ // Add checkpoints with linked items
463
+ const checkpointId = (0, uuid_1.v4)();
464
+ db.prepare('INSERT INTO checkpoints (id, session_id, name, description) VALUES (?, ?, ?, ?)').run(checkpointId, currentSessionId, 'Major Milestone', 'Checkpoint after completing major feature');
465
+ // Link some items to checkpoint
466
+ itemIds.slice(0, 3).forEach(itemId => {
467
+ db.prepare('INSERT INTO checkpoint_items (id, checkpoint_id, context_item_id) VALUES (?, ?, ?)').run((0, uuid_1.v4)(), checkpointId, itemId);
468
+ });
469
+ // Export with full statistics
470
+ const result = await contextExportHandler({ includeStats: true }, db, currentSessionId);
471
+ // Verify comprehensive export
472
+ expect(result.statistics).toEqual({
473
+ items: 6,
474
+ files: 3,
475
+ checkpoints: 1,
476
+ size: expect.any(Number),
477
+ });
478
+ expect(result.content[0].text).toContain('Context Items: 6');
479
+ expect(result.content[0].text).toContain('Cached Files: 3');
480
+ expect(result.content[0].text).toContain('Checkpoints: 1');
481
+ // Verify exported data structure
482
+ const exportData = JSON.parse(fs.readFileSync(result.exportPath, 'utf-8'));
483
+ expect(exportData.contextItems).toHaveLength(6);
484
+ expect(exportData.fileCache).toHaveLength(3);
485
+ expect(exportData.checkpoints).toHaveLength(1);
486
+ expect(exportData.metadata).toBeDefined();
487
+ expect(exportData.metadata.totalSize).toBeGreaterThan(0);
488
+ });
489
+ it('should handle concurrent export requests gracefully', async () => {
490
+ // Add test data
491
+ db.prepare('INSERT INTO context_items (id, session_id, key, value) VALUES (?, ?, ?, ?)').run((0, uuid_1.v4)(), currentSessionId, 'key1', 'value1');
492
+ // Simulate concurrent exports
493
+ const promises = Array(5)
494
+ .fill(null)
495
+ .map(() => contextExportHandler({ includeStats: true }, db, currentSessionId));
496
+ const results = await Promise.all(promises);
497
+ // All exports should succeed
498
+ results.forEach(result => {
499
+ expect(result.exportPath).toBeDefined();
500
+ expect(result.statistics.items).toBe(1);
501
+ });
502
+ // Clean up export files
503
+ results.forEach(result => {
504
+ if (result.exportPath && fs.existsSync(result.exportPath)) {
505
+ fs.unlinkSync(result.exportPath);
506
+ }
507
+ });
508
+ });
509
+ });
510
+ });