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,460 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RetentionManager = void 0;
4
+ const uuid_1 = require("uuid");
5
+ class RetentionManager {
6
+ db;
7
+ constructor(db) {
8
+ this.db = db;
9
+ this.initializeTables();
10
+ }
11
+ initializeTables() {
12
+ this.db.getDatabase().exec(`
13
+ CREATE TABLE IF NOT EXISTS retention_policies (
14
+ id TEXT PRIMARY KEY,
15
+ name TEXT NOT NULL,
16
+ enabled BOOLEAN DEFAULT true,
17
+ policy_config TEXT NOT NULL,
18
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
19
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
20
+ last_run TIMESTAMP
21
+ );
22
+
23
+ CREATE TABLE IF NOT EXISTS retention_logs (
24
+ id TEXT PRIMARY KEY,
25
+ policy_id TEXT NOT NULL,
26
+ result TEXT NOT NULL,
27
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
28
+ FOREIGN KEY (policy_id) REFERENCES retention_policies(id)
29
+ );
30
+
31
+ CREATE INDEX IF NOT EXISTS idx_retention_logs_policy ON retention_logs(policy_id);
32
+ CREATE INDEX IF NOT EXISTS idx_retention_logs_created ON retention_logs(created_at);
33
+ `);
34
+ }
35
+ // Parse age strings like "30d", "1y", "6m" into Date objects
36
+ parseAge(ageString) {
37
+ const match = ageString.match(/^(\d+)([dwmy])$/);
38
+ if (!match) {
39
+ throw new Error(`Invalid age format: ${ageString}. Use format like "30d", "1y", "6m"`);
40
+ }
41
+ const value = parseInt(match[1]);
42
+ const unit = match[2];
43
+ const now = new Date();
44
+ switch (unit) {
45
+ case 'd': // days
46
+ return new Date(now.getTime() - value * 24 * 60 * 60 * 1000);
47
+ case 'w': // weeks
48
+ return new Date(now.getTime() - value * 7 * 24 * 60 * 60 * 1000);
49
+ case 'm': // months (approximate: 30 days)
50
+ return new Date(now.getTime() - value * 30 * 24 * 60 * 60 * 1000);
51
+ case 'y': // years (approximate: 365 days)
52
+ return new Date(now.getTime() - value * 365 * 24 * 60 * 60 * 1000);
53
+ default:
54
+ throw new Error(`Unknown age unit: ${unit}`);
55
+ }
56
+ }
57
+ createPolicy(policy) {
58
+ const id = (0, uuid_1.v4)();
59
+ const policyWithId = { enabled: true, ...policy, id };
60
+ this.db
61
+ .getDatabase()
62
+ .prepare(`
63
+ INSERT INTO retention_policies (id, name, enabled, policy_config)
64
+ VALUES (?, ?, ?, ?)
65
+ `)
66
+ .run(id, policy.name, policyWithId.enabled ? 1 : 0, JSON.stringify(policyWithId));
67
+ return id;
68
+ }
69
+ updatePolicy(id, updates) {
70
+ const existing = this.getPolicy(id);
71
+ if (!existing) {
72
+ throw new Error(`Policy not found: ${id}`);
73
+ }
74
+ const updated = { ...existing, ...updates, id };
75
+ this.db
76
+ .getDatabase()
77
+ .prepare(`
78
+ UPDATE retention_policies
79
+ SET policy_config = ?, updated_at = CURRENT_TIMESTAMP
80
+ WHERE id = ?
81
+ `)
82
+ .run(JSON.stringify(updated), id);
83
+ }
84
+ getPolicy(id) {
85
+ const row = this.db
86
+ .getDatabase()
87
+ .prepare(`
88
+ SELECT policy_config FROM retention_policies WHERE id = ?
89
+ `)
90
+ .get(id);
91
+ if (!row)
92
+ return null;
93
+ return JSON.parse(row.policy_config);
94
+ }
95
+ listPolicies() {
96
+ const rows = this.db
97
+ .getDatabase()
98
+ .prepare(`
99
+ SELECT policy_config FROM retention_policies ORDER BY created_at
100
+ `)
101
+ .all();
102
+ return rows.map(row => JSON.parse(row.policy_config));
103
+ }
104
+ deletePolicy(id) {
105
+ this.db
106
+ .getDatabase()
107
+ .prepare(`
108
+ DELETE FROM retention_policies WHERE id = ?
109
+ `)
110
+ .run(id);
111
+ }
112
+ getRetentionStats(sessionId) {
113
+ const sessionFilter = sessionId ? 'WHERE session_id = ?' : '';
114
+ const params = sessionId ? [sessionId] : [];
115
+ // Get overall stats
116
+ const totalStats = this.db
117
+ .getDatabase()
118
+ .prepare(`
119
+ SELECT
120
+ COUNT(*) as total_items,
121
+ SUM(size) as total_size,
122
+ MIN(created_at) as oldest_item,
123
+ MAX(created_at) as newest_item
124
+ FROM context_items ${sessionFilter}
125
+ `)
126
+ .get(...params);
127
+ // Get by category
128
+ const categoryStats = this.db
129
+ .getDatabase()
130
+ .prepare(`
131
+ SELECT
132
+ COALESCE(category, 'uncategorized') as category,
133
+ COUNT(*) as count,
134
+ SUM(size) as size
135
+ FROM context_items ${sessionFilter}
136
+ GROUP BY category
137
+ `)
138
+ .all(...params);
139
+ // Get by priority
140
+ const priorityStats = this.db
141
+ .getDatabase()
142
+ .prepare(`
143
+ SELECT
144
+ COALESCE(priority, 'normal') as priority,
145
+ COUNT(*) as count,
146
+ SUM(size) as size
147
+ FROM context_items ${sessionFilter}
148
+ GROUP BY priority
149
+ `)
150
+ .all(...params);
151
+ // Calculate eligible for retention (older than 30 days)
152
+ const thirtyDaysAgo = new Date();
153
+ thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
154
+ const eligibleStats = this.db
155
+ .getDatabase()
156
+ .prepare(`
157
+ SELECT
158
+ COUNT(*) as eligible_items,
159
+ SUM(size) as eligible_size
160
+ FROM context_items
161
+ WHERE created_at < ? ${sessionId ? 'AND session_id = ?' : ''}
162
+ `)
163
+ .get(thirtyDaysAgo.toISOString(), ...(sessionId ? [sessionId] : []));
164
+ const byCategory = {};
165
+ categoryStats.forEach((stat) => {
166
+ byCategory[stat.category] = {
167
+ count: stat.count,
168
+ size: stat.size || 0,
169
+ };
170
+ });
171
+ const byPriority = {};
172
+ priorityStats.forEach((stat) => {
173
+ byPriority[stat.priority] = {
174
+ count: stat.count,
175
+ size: stat.size || 0,
176
+ };
177
+ });
178
+ const totalSize = totalStats.total_size || 0;
179
+ const eligibleSize = eligibleStats.eligible_size || 0;
180
+ return {
181
+ totalItems: totalStats.total_items || 0,
182
+ totalSize,
183
+ oldestItem: totalStats.oldest_item || '',
184
+ newestItem: totalStats.newest_item || '',
185
+ byCategory,
186
+ byPriority,
187
+ eligibleForRetention: {
188
+ items: eligibleStats.eligible_items || 0,
189
+ size: eligibleSize,
190
+ savings: totalSize > 0 ? Math.round((eligibleSize / totalSize) * 100) : 0,
191
+ },
192
+ };
193
+ }
194
+ async executePolicy(policyId, dryRun = false) {
195
+ const startTime = Date.now();
196
+ const policy = this.getPolicy(policyId);
197
+ if (!policy) {
198
+ throw new Error(`Policy not found: ${policyId}`);
199
+ }
200
+ if (policy.enabled === false) {
201
+ throw new Error(`Policy is disabled: ${policy.name}`);
202
+ }
203
+ const result = {
204
+ policyId,
205
+ policyName: policy.name,
206
+ action: policy.action,
207
+ dryRun,
208
+ processed: { items: 0, size: 0, sessions: [] },
209
+ saved: { size: 0, items: 0 },
210
+ errors: [],
211
+ warnings: [],
212
+ executionTime: 0,
213
+ timestamp: new Date().toISOString(),
214
+ };
215
+ try {
216
+ // Validate age format if specified
217
+ if (policy.maxAge) {
218
+ this.parseAge(policy.maxAge);
219
+ }
220
+ // Find items eligible for retention
221
+ const eligibleItems = this.findEligibleItems(policy);
222
+ if (eligibleItems.length === 0) {
223
+ result.warnings.push('No items eligible for retention');
224
+ return result;
225
+ }
226
+ result.processed.items = eligibleItems.length;
227
+ result.processed.size = eligibleItems.reduce((sum, item) => sum + (item.size || 0), 0);
228
+ result.processed.sessions = [...new Set(eligibleItems.map(item => item.session_id))];
229
+ if (!dryRun) {
230
+ await this.executeRetentionAction(policy, eligibleItems, result);
231
+ // Update last run timestamp
232
+ this.db
233
+ .getDatabase()
234
+ .prepare(`
235
+ UPDATE retention_policies
236
+ SET last_run = CURRENT_TIMESTAMP
237
+ WHERE id = ?
238
+ `)
239
+ .run(policyId);
240
+ }
241
+ result.saved.items = result.processed.items;
242
+ result.saved.size = result.processed.size;
243
+ }
244
+ catch (error) {
245
+ result.errors.push(error.message);
246
+ }
247
+ result.executionTime = Date.now() - startTime;
248
+ // Log the result
249
+ this.db
250
+ .getDatabase()
251
+ .prepare(`
252
+ INSERT INTO retention_logs (id, policy_id, result)
253
+ VALUES (?, ?, ?)
254
+ `)
255
+ .run((0, uuid_1.v4)(), policyId, JSON.stringify(result));
256
+ return result;
257
+ }
258
+ findEligibleItems(policy) {
259
+ let query = 'SELECT * FROM context_items WHERE 1=1';
260
+ const params = [];
261
+ // Age-based filtering
262
+ if (policy.maxAge) {
263
+ const cutoffDate = this.parseAge(policy.maxAge);
264
+ query += ' AND created_at < ?';
265
+ params.push(cutoffDate.toISOString());
266
+ }
267
+ // Category-specific rules
268
+ if (policy.categories) {
269
+ const preserveCategories = Object.entries(policy.categories)
270
+ .filter(([, rules]) => rules.preserve)
271
+ .map(([category]) => category);
272
+ if (preserveCategories.length > 0) {
273
+ const placeholders = preserveCategories.map(() => '?').join(',');
274
+ query += ` AND COALESCE(category, 'uncategorized') NOT IN (${placeholders})`;
275
+ params.push(...preserveCategories);
276
+ }
277
+ }
278
+ // Priority-based filtering
279
+ if (policy.preserveHighPriority) {
280
+ query += " AND priority != 'high'";
281
+ }
282
+ if (policy.preserveCritical) {
283
+ query += " AND priority != 'critical'";
284
+ }
285
+ query += ' ORDER BY created_at ASC';
286
+ let items = this.db
287
+ .getDatabase()
288
+ .prepare(query)
289
+ .all(...params);
290
+ // Size-based limiting
291
+ if (policy.maxItems && items.length > policy.maxItems) {
292
+ items = items.slice(0, items.length - policy.maxItems);
293
+ }
294
+ if (policy.maxSize) {
295
+ let currentSize = 0;
296
+ items = items.filter(item => {
297
+ currentSize += item.size || 0;
298
+ return currentSize <= policy.maxSize;
299
+ });
300
+ }
301
+ return items;
302
+ }
303
+ async executeRetentionAction(policy, items, result) {
304
+ switch (policy.action) {
305
+ case 'delete':
306
+ await this.deleteItems(items, result);
307
+ break;
308
+ case 'archive':
309
+ await this.archiveItems(items, result);
310
+ break;
311
+ case 'compress':
312
+ await this.compressItems(items, result);
313
+ break;
314
+ default:
315
+ throw new Error(`Unknown retention action: ${policy.action}`);
316
+ }
317
+ }
318
+ async deleteItems(items, result) {
319
+ const db = this.db.getDatabase();
320
+ const deleteStmt = db.prepare('DELETE FROM context_items WHERE id = ?');
321
+ for (const item of items) {
322
+ try {
323
+ deleteStmt.run(item.id);
324
+ }
325
+ catch (error) {
326
+ result.errors.push(`Failed to delete item ${item.id}: ${error.message}`);
327
+ }
328
+ }
329
+ }
330
+ async archiveItems(items, result) {
331
+ // Archive to a separate table
332
+ const db = this.db.getDatabase();
333
+ // Create archive table if not exists
334
+ db.exec(`
335
+ CREATE TABLE IF NOT EXISTS context_items_archive (
336
+ id TEXT PRIMARY KEY,
337
+ session_id TEXT NOT NULL,
338
+ key TEXT NOT NULL,
339
+ value TEXT NOT NULL,
340
+ category TEXT,
341
+ priority TEXT,
342
+ metadata TEXT,
343
+ size INTEGER DEFAULT 0,
344
+ created_at TIMESTAMP,
345
+ archived_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
346
+ );
347
+ `);
348
+ const insertStmt = db.prepare(`
349
+ INSERT INTO context_items_archive
350
+ (id, session_id, key, value, category, priority, metadata, size, created_at)
351
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
352
+ `);
353
+ const deleteStmt = db.prepare('DELETE FROM context_items WHERE id = ?');
354
+ for (const item of items) {
355
+ try {
356
+ insertStmt.run(item.id, item.session_id, item.key, item.value, item.category, item.priority, item.metadata, item.size, item.created_at);
357
+ deleteStmt.run(item.id);
358
+ }
359
+ catch (error) {
360
+ result.errors.push(`Failed to archive item ${item.id}: ${error.message}`);
361
+ }
362
+ }
363
+ }
364
+ async compressItems(items, result) {
365
+ // Group items by category and compress
366
+ const itemsByCategory = {};
367
+ for (const item of items) {
368
+ const category = item.category || 'uncategorized';
369
+ if (!itemsByCategory[category]) {
370
+ itemsByCategory[category] = [];
371
+ }
372
+ itemsByCategory[category].push(item);
373
+ }
374
+ const db = this.db.getDatabase();
375
+ const insertCompressed = db.prepare(`
376
+ INSERT INTO compressed_context
377
+ (id, session_id, original_count, compressed_data, compression_ratio, date_range_start, date_range_end)
378
+ VALUES (?, ?, ?, ?, ?, ?, ?)
379
+ `);
380
+ const deleteStmt = db.prepare('DELETE FROM context_items WHERE id = ?');
381
+ for (const [category, categoryItems] of Object.entries(itemsByCategory)) {
382
+ try {
383
+ const compressed = {
384
+ category,
385
+ totalItems: categoryItems.length,
386
+ summary: `${categoryItems.length} items from ${category}`,
387
+ dateRange: {
388
+ start: Math.min(...categoryItems.map(i => new Date(i.created_at).getTime())),
389
+ end: Math.max(...categoryItems.map(i => new Date(i.created_at).getTime())),
390
+ },
391
+ samples: categoryItems.slice(0, 3).map(item => ({
392
+ key: item.key,
393
+ value: item.value.substring(0, 100),
394
+ })),
395
+ };
396
+ const originalSize = JSON.stringify(categoryItems).length;
397
+ const compressedSize = JSON.stringify(compressed).length;
398
+ const ratio = 1 - compressedSize / originalSize;
399
+ insertCompressed.run((0, uuid_1.v4)(), categoryItems[0].session_id, categoryItems.length, JSON.stringify(compressed), ratio, new Date(compressed.dateRange.start).toISOString(), new Date(compressed.dateRange.end).toISOString());
400
+ // Delete original items
401
+ for (const item of categoryItems) {
402
+ deleteStmt.run(item.id);
403
+ }
404
+ }
405
+ catch (error) {
406
+ result.errors.push(`Failed to compress ${category} items: ${error.message}`);
407
+ }
408
+ }
409
+ }
410
+ getRetentionLogs(policyId, limit = 50) {
411
+ const query = policyId
412
+ ? 'SELECT * FROM retention_logs WHERE policy_id = ? ORDER BY created_at DESC LIMIT ?'
413
+ : 'SELECT * FROM retention_logs ORDER BY created_at DESC LIMIT ?';
414
+ const params = policyId ? [policyId, limit] : [limit];
415
+ return this.db
416
+ .getDatabase()
417
+ .prepare(query)
418
+ .all(...params);
419
+ }
420
+ // Predefined retention policies
421
+ static getDefaultPolicies() {
422
+ return [
423
+ {
424
+ name: 'Conservative Cleanup',
425
+ enabled: true,
426
+ maxAge: '90d',
427
+ preserveHighPriority: true,
428
+ preserveCritical: true,
429
+ categories: {
430
+ decision: { preserve: true },
431
+ critical: { preserve: true },
432
+ },
433
+ action: 'archive',
434
+ schedule: 'weekly',
435
+ notifyBeforeAction: true,
436
+ },
437
+ {
438
+ name: 'Aggressive Cleanup',
439
+ enabled: false,
440
+ maxAge: '30d',
441
+ maxItems: 1000,
442
+ preserveCritical: true,
443
+ action: 'compress',
444
+ schedule: 'daily',
445
+ },
446
+ {
447
+ name: 'Development Mode',
448
+ enabled: false,
449
+ maxAge: '7d',
450
+ categories: {
451
+ decision: { maxAge: '30d' },
452
+ critical: { preserve: true },
453
+ },
454
+ action: 'delete',
455
+ schedule: 'daily',
456
+ },
457
+ ];
458
+ }
459
+ }
460
+ exports.RetentionManager = RetentionManager;
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ /**
3
+ * Timestamp utility functions for converting between ISO and SQLite formats
4
+ *
5
+ * SQLite stores timestamps in "YYYY-MM-DD HH:MM:SS" format
6
+ * JavaScript Date objects use ISO format "YYYY-MM-DDTHH:MM:SS.sssZ"
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.toSQLiteTimestamp = toSQLiteTimestamp;
10
+ exports.toISOTimestamp = toISOTimestamp;
11
+ exports.isISOTimestamp = isISOTimestamp;
12
+ exports.isSQLiteTimestamp = isSQLiteTimestamp;
13
+ exports.ensureSQLiteFormat = ensureSQLiteFormat;
14
+ exports.ensureISOFormat = ensureISOFormat;
15
+ /**
16
+ * Convert ISO timestamp to SQLite format
17
+ * @param isoTimestamp - ISO format timestamp (e.g., "2025-06-27T02:07:07.253Z")
18
+ * @returns SQLite format timestamp (e.g., "2025-06-27 02:07:07")
19
+ */
20
+ function toSQLiteTimestamp(isoTimestamp) {
21
+ if (!isoTimestamp) {
22
+ throw new Error('Timestamp cannot be null or empty');
23
+ }
24
+ // Handle already converted timestamps (SQLite format)
25
+ if (!isoTimestamp.includes('T') || !isoTimestamp.includes('Z')) {
26
+ // Already in SQLite format or not an ISO timestamp
27
+ return isoTimestamp;
28
+ }
29
+ try {
30
+ // Convert ISO format to SQLite format
31
+ // "2025-06-27T02:07:07.253Z" -> "2025-06-27 02:07:07"
32
+ return isoTimestamp.replace('T', ' ').replace(/\.\d{3}Z$/, '');
33
+ }
34
+ catch (_error) {
35
+ throw new Error(`Invalid ISO timestamp format: ${isoTimestamp}`);
36
+ }
37
+ }
38
+ /**
39
+ * Convert SQLite timestamp to ISO format
40
+ * @param sqliteTimestamp - SQLite format timestamp (e.g., "2025-06-27 02:07:07")
41
+ * @returns ISO format timestamp (e.g., "2025-06-27T02:07:07.000Z")
42
+ */
43
+ function toISOTimestamp(sqliteTimestamp) {
44
+ if (!sqliteTimestamp) {
45
+ throw new Error('Timestamp cannot be null or empty');
46
+ }
47
+ // Handle already converted timestamps (ISO format)
48
+ if (sqliteTimestamp.includes('T') && sqliteTimestamp.includes('Z')) {
49
+ // Already in ISO format
50
+ return sqliteTimestamp;
51
+ }
52
+ try {
53
+ // Convert SQLite format to ISO format
54
+ // "2025-06-27 02:07:07" -> "2025-06-27T02:07:07.000Z"
55
+ return sqliteTimestamp.replace(' ', 'T') + '.000Z';
56
+ }
57
+ catch (_error) {
58
+ throw new Error(`Invalid SQLite timestamp format: ${sqliteTimestamp}`);
59
+ }
60
+ }
61
+ /**
62
+ * Check if a timestamp is in ISO format
63
+ * @param timestamp - The timestamp to check
64
+ * @returns true if the timestamp is in ISO format
65
+ */
66
+ function isISOTimestamp(timestamp) {
67
+ if (!timestamp)
68
+ return false;
69
+ // ISO format contains 'T' and ends with 'Z'
70
+ return timestamp.includes('T') && timestamp.endsWith('Z');
71
+ }
72
+ /**
73
+ * Check if a timestamp is in SQLite format
74
+ * @param timestamp - The timestamp to check
75
+ * @returns true if the timestamp is in SQLite format
76
+ */
77
+ function isSQLiteTimestamp(timestamp) {
78
+ if (!timestamp)
79
+ return false;
80
+ // SQLite format contains space and doesn't end with 'Z'
81
+ return timestamp.includes(' ') && !timestamp.endsWith('Z');
82
+ }
83
+ /**
84
+ * Ensure a timestamp is in SQLite format, converting if necessary
85
+ * @param timestamp - The timestamp in any supported format
86
+ * @returns SQLite format timestamp
87
+ */
88
+ function ensureSQLiteFormat(timestamp) {
89
+ if (!timestamp) {
90
+ throw new Error('Timestamp cannot be null or empty');
91
+ }
92
+ if (isISOTimestamp(timestamp)) {
93
+ return toSQLiteTimestamp(timestamp);
94
+ }
95
+ // Assume it's already SQLite format or handle as-is
96
+ return timestamp;
97
+ }
98
+ /**
99
+ * Ensure a timestamp is in ISO format, converting if necessary
100
+ * @param timestamp - The timestamp in any supported format
101
+ * @returns ISO format timestamp
102
+ */
103
+ function ensureISOFormat(timestamp) {
104
+ if (!timestamp) {
105
+ throw new Error('Timestamp cannot be null or empty');
106
+ }
107
+ if (isSQLiteTimestamp(timestamp)) {
108
+ return toISOTimestamp(timestamp);
109
+ }
110
+ // Assume it's already ISO format or handle as-is
111
+ return timestamp;
112
+ }