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.
- package/CHANGELOG.md +433 -0
- package/LICENSE +21 -0
- package/README.md +1051 -0
- package/bin/mcp-memory-keeper +52 -0
- package/dist/__tests__/helpers/database-test-helper.js +160 -0
- package/dist/__tests__/helpers/test-server.js +92 -0
- package/dist/__tests__/integration/advanced-features.test.js +614 -0
- package/dist/__tests__/integration/backward-compatibility.test.js +245 -0
- package/dist/__tests__/integration/batchOperationsE2E.test.js +396 -0
- package/dist/__tests__/integration/batchOperationsHandler.test.js +1230 -0
- package/dist/__tests__/integration/channelManagementHandler.test.js +1291 -0
- package/dist/__tests__/integration/channels.test.js +376 -0
- package/dist/__tests__/integration/checkpoint.test.js +251 -0
- package/dist/__tests__/integration/concurrent-access.test.js +190 -0
- package/dist/__tests__/integration/context-operations.test.js +243 -0
- package/dist/__tests__/integration/contextDiff.test.js +852 -0
- package/dist/__tests__/integration/contextDiffHandler.test.js +976 -0
- package/dist/__tests__/integration/contextExportHandler.test.js +510 -0
- package/dist/__tests__/integration/contextGetPaginationDefaults.test.js +298 -0
- package/dist/__tests__/integration/contextReassignChannelHandler.test.js +908 -0
- package/dist/__tests__/integration/contextRelationshipsHandler.test.js +1151 -0
- package/dist/__tests__/integration/contextSearch.test.js +938 -0
- package/dist/__tests__/integration/contextSearchHandler.test.js +552 -0
- package/dist/__tests__/integration/contextWatchActual.test.js +165 -0
- package/dist/__tests__/integration/contextWatchHandler.test.js +1500 -0
- package/dist/__tests__/integration/cross-session-sharing.test.js +302 -0
- package/dist/__tests__/integration/database-initialization.test.js +134 -0
- package/dist/__tests__/integration/enhanced-context-operations.test.js +1082 -0
- package/dist/__tests__/integration/enhancedContextGetHandler.test.js +915 -0
- package/dist/__tests__/integration/enhancedContextTimelineHandler.test.js +716 -0
- package/dist/__tests__/integration/error-cases.test.js +407 -0
- package/dist/__tests__/integration/export-import.test.js +367 -0
- package/dist/__tests__/integration/feature-flags.test.js +542 -0
- package/dist/__tests__/integration/file-operations.test.js +264 -0
- package/dist/__tests__/integration/git-integration.test.js +237 -0
- package/dist/__tests__/integration/index-tools.test.js +496 -0
- package/dist/__tests__/integration/issue11-actual-bug-demo.test.js +304 -0
- package/dist/__tests__/integration/issue11-search-filters-bug.test.js +561 -0
- package/dist/__tests__/integration/issue12-checkpoint-restore-behavior.test.js +621 -0
- package/dist/__tests__/integration/issue13-key-validation.test.js +433 -0
- package/dist/__tests__/integration/knowledge-graph.test.js +338 -0
- package/dist/__tests__/integration/migrations.test.js +528 -0
- package/dist/__tests__/integration/multi-agent.test.js +546 -0
- package/dist/__tests__/integration/pagination-critical-fix.test.js +296 -0
- package/dist/__tests__/integration/paginationDefaultsHandler.test.js +600 -0
- package/dist/__tests__/integration/project-directory.test.js +283 -0
- package/dist/__tests__/integration/resource-cleanup.test.js +149 -0
- package/dist/__tests__/integration/retention.test.js +513 -0
- package/dist/__tests__/integration/search.test.js +333 -0
- package/dist/__tests__/integration/semantic-search.test.js +266 -0
- package/dist/__tests__/integration/server-initialization.test.js +307 -0
- package/dist/__tests__/integration/session-management.test.js +219 -0
- package/dist/__tests__/integration/simplified-sharing.test.js +346 -0
- package/dist/__tests__/integration/smart-compaction.test.js +230 -0
- package/dist/__tests__/integration/summarization.test.js +308 -0
- package/dist/__tests__/integration/watcher-migration-validation.test.js +544 -0
- package/dist/__tests__/security/input-validation.test.js +115 -0
- package/dist/__tests__/utils/agents.test.js +473 -0
- package/dist/__tests__/utils/database.test.js +177 -0
- package/dist/__tests__/utils/git.test.js +122 -0
- package/dist/__tests__/utils/knowledge-graph.test.js +297 -0
- package/dist/__tests__/utils/migrationHealthCheck.test.js +302 -0
- package/dist/__tests__/utils/project-directory-messages.test.js +188 -0
- package/dist/__tests__/utils/timezone-safe-dates.js +119 -0
- package/dist/__tests__/utils/validation.test.js +200 -0
- package/dist/__tests__/utils/vector-store.test.js +231 -0
- package/dist/handlers/contextWatchHandlers.js +206 -0
- package/dist/index.js +4310 -0
- package/dist/index.phase1.backup.js +410 -0
- package/dist/index.phase2.backup.js +704 -0
- package/dist/migrations/003_add_channels.js +174 -0
- package/dist/migrations/004_add_context_watch.js +151 -0
- package/dist/migrations/005_add_context_watch.js +98 -0
- package/dist/migrations/simplify-sharing.js +117 -0
- package/dist/repositories/BaseRepository.js +30 -0
- package/dist/repositories/CheckpointRepository.js +140 -0
- package/dist/repositories/ContextRepository.js +1873 -0
- package/dist/repositories/FileRepository.js +104 -0
- package/dist/repositories/RepositoryManager.js +62 -0
- package/dist/repositories/SessionRepository.js +66 -0
- package/dist/repositories/WatcherRepository.js +252 -0
- package/dist/repositories/index.js +15 -0
- package/dist/server.js +384 -0
- package/dist/test-helpers/database-helper.js +128 -0
- package/dist/types/entities.js +3 -0
- package/dist/utils/agents.js +791 -0
- package/dist/utils/channels.js +150 -0
- package/dist/utils/database.js +731 -0
- package/dist/utils/feature-flags.js +476 -0
- package/dist/utils/git.js +145 -0
- package/dist/utils/knowledge-graph.js +264 -0
- package/dist/utils/migrationHealthCheck.js +373 -0
- package/dist/utils/migrations.js +452 -0
- package/dist/utils/retention.js +460 -0
- package/dist/utils/timestamps.js +112 -0
- package/dist/utils/validation.js +296 -0
- package/dist/utils/vector-store.js +247 -0
- 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
|
+
}
|