@danielsimonjr/memory-mcp 0.7.2 → 0.41.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 (61) hide show
  1. package/dist/__tests__/edge-cases/edge-cases.test.js +406 -0
  2. package/dist/__tests__/file-path.test.js +5 -5
  3. package/dist/__tests__/integration/workflows.test.js +449 -0
  4. package/dist/__tests__/knowledge-graph.test.js +8 -3
  5. package/dist/__tests__/performance/benchmarks.test.js +411 -0
  6. package/dist/__tests__/unit/core/EntityManager.test.js +334 -0
  7. package/dist/__tests__/unit/core/GraphStorage.test.js +205 -0
  8. package/dist/__tests__/unit/core/RelationManager.test.js +274 -0
  9. package/dist/__tests__/unit/features/CompressionManager.test.js +350 -0
  10. package/dist/__tests__/unit/search/BasicSearch.test.js +311 -0
  11. package/dist/__tests__/unit/search/BooleanSearch.test.js +432 -0
  12. package/dist/__tests__/unit/search/FuzzySearch.test.js +448 -0
  13. package/dist/__tests__/unit/search/RankedSearch.test.js +379 -0
  14. package/dist/__tests__/unit/utils/levenshtein.test.js +77 -0
  15. package/dist/core/EntityManager.js +554 -0
  16. package/dist/core/GraphStorage.js +172 -0
  17. package/dist/core/KnowledgeGraphManager.js +400 -0
  18. package/dist/core/ObservationManager.js +129 -0
  19. package/dist/core/RelationManager.js +186 -0
  20. package/dist/core/TransactionManager.js +389 -0
  21. package/dist/core/index.js +9 -0
  22. package/dist/features/AnalyticsManager.js +222 -0
  23. package/dist/features/ArchiveManager.js +74 -0
  24. package/dist/features/BackupManager.js +311 -0
  25. package/dist/features/CompressionManager.js +310 -0
  26. package/dist/features/ExportManager.js +305 -0
  27. package/dist/features/HierarchyManager.js +219 -0
  28. package/dist/features/ImportExportManager.js +50 -0
  29. package/dist/features/ImportManager.js +328 -0
  30. package/dist/features/TagManager.js +210 -0
  31. package/dist/features/index.js +12 -0
  32. package/dist/index.js +13 -996
  33. package/dist/memory.jsonl +225 -0
  34. package/dist/search/BasicSearch.js +161 -0
  35. package/dist/search/BooleanSearch.js +304 -0
  36. package/dist/search/FuzzySearch.js +115 -0
  37. package/dist/search/RankedSearch.js +206 -0
  38. package/dist/search/SavedSearchManager.js +145 -0
  39. package/dist/search/SearchManager.js +305 -0
  40. package/dist/search/SearchSuggestions.js +57 -0
  41. package/dist/search/TFIDFIndexManager.js +217 -0
  42. package/dist/search/index.js +10 -0
  43. package/dist/server/MCPServer.js +889 -0
  44. package/dist/types/analytics.types.js +6 -0
  45. package/dist/types/entity.types.js +7 -0
  46. package/dist/types/import-export.types.js +7 -0
  47. package/dist/types/index.js +12 -0
  48. package/dist/types/search.types.js +7 -0
  49. package/dist/types/tag.types.js +6 -0
  50. package/dist/utils/constants.js +127 -0
  51. package/dist/utils/dateUtils.js +89 -0
  52. package/dist/utils/errors.js +121 -0
  53. package/dist/utils/index.js +13 -0
  54. package/dist/utils/levenshtein.js +62 -0
  55. package/dist/utils/logger.js +33 -0
  56. package/dist/utils/pathUtils.js +115 -0
  57. package/dist/utils/schemas.js +184 -0
  58. package/dist/utils/searchCache.js +209 -0
  59. package/dist/utils/tfidf.js +90 -0
  60. package/dist/utils/validationUtils.js +109 -0
  61. package/package.json +50 -48
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Analytics Manager
3
+ *
4
+ * Provides graph validation and analytics capabilities.
5
+ *
6
+ * @module features/AnalyticsManager
7
+ */
8
+ /**
9
+ * Performs validation and analytics on the knowledge graph.
10
+ */
11
+ export class AnalyticsManager {
12
+ storage;
13
+ constructor(storage) {
14
+ this.storage = storage;
15
+ }
16
+ /**
17
+ * Validate the knowledge graph structure and data integrity.
18
+ *
19
+ * Checks for:
20
+ * - Orphaned relations (pointing to non-existent entities)
21
+ * - Duplicate entity names
22
+ * - Invalid entity data (missing name/type, invalid observations)
23
+ * - Isolated entities (no relations)
24
+ * - Empty observations
25
+ * - Missing metadata (createdAt, lastModified)
26
+ *
27
+ * @returns Validation report with errors, warnings, and summary
28
+ */
29
+ async validateGraph() {
30
+ const graph = await this.storage.loadGraph();
31
+ const errors = [];
32
+ const warnings = [];
33
+ // Create a set of all entity names for fast lookup
34
+ const entityNames = new Set(graph.entities.map(e => e.name));
35
+ // Check for orphaned relations (relations pointing to non-existent entities)
36
+ for (const relation of graph.relations) {
37
+ if (!entityNames.has(relation.from)) {
38
+ errors.push({
39
+ type: 'orphaned_relation',
40
+ message: `Relation has non-existent source entity: "${relation.from}"`,
41
+ details: { relation, missingEntity: relation.from },
42
+ });
43
+ }
44
+ if (!entityNames.has(relation.to)) {
45
+ errors.push({
46
+ type: 'orphaned_relation',
47
+ message: `Relation has non-existent target entity: "${relation.to}"`,
48
+ details: { relation, missingEntity: relation.to },
49
+ });
50
+ }
51
+ }
52
+ // Check for duplicate entity names
53
+ const entityNameCounts = new Map();
54
+ for (const entity of graph.entities) {
55
+ const count = entityNameCounts.get(entity.name) || 0;
56
+ entityNameCounts.set(entity.name, count + 1);
57
+ }
58
+ for (const [name, count] of entityNameCounts.entries()) {
59
+ if (count > 1) {
60
+ errors.push({
61
+ type: 'duplicate_entity',
62
+ message: `Duplicate entity name found: "${name}" (${count} instances)`,
63
+ details: { entityName: name, count },
64
+ });
65
+ }
66
+ }
67
+ // Check for entities with invalid data
68
+ for (const entity of graph.entities) {
69
+ if (!entity.name || entity.name.trim() === '') {
70
+ errors.push({
71
+ type: 'invalid_data',
72
+ message: 'Entity has empty or missing name',
73
+ details: { entity },
74
+ });
75
+ }
76
+ if (!entity.entityType || entity.entityType.trim() === '') {
77
+ errors.push({
78
+ type: 'invalid_data',
79
+ message: `Entity "${entity.name}" has empty or missing entityType`,
80
+ details: { entity },
81
+ });
82
+ }
83
+ if (!Array.isArray(entity.observations)) {
84
+ errors.push({
85
+ type: 'invalid_data',
86
+ message: `Entity "${entity.name}" has invalid observations (not an array)`,
87
+ details: { entity },
88
+ });
89
+ }
90
+ }
91
+ // Warnings: Check for isolated entities (no relations)
92
+ const entitiesInRelations = new Set();
93
+ for (const relation of graph.relations) {
94
+ entitiesInRelations.add(relation.from);
95
+ entitiesInRelations.add(relation.to);
96
+ }
97
+ for (const entity of graph.entities) {
98
+ if (!entitiesInRelations.has(entity.name) && graph.relations.length > 0) {
99
+ warnings.push({
100
+ type: 'isolated_entity',
101
+ message: `Entity "${entity.name}" has no relations to other entities`,
102
+ details: { entityName: entity.name },
103
+ });
104
+ }
105
+ }
106
+ // Warnings: Check for entities with empty observations
107
+ for (const entity of graph.entities) {
108
+ if (entity.observations.length === 0) {
109
+ warnings.push({
110
+ type: 'empty_observations',
111
+ message: `Entity "${entity.name}" has no observations`,
112
+ details: { entityName: entity.name },
113
+ });
114
+ }
115
+ }
116
+ // Warnings: Check for missing metadata (createdAt, lastModified)
117
+ for (const entity of graph.entities) {
118
+ if (!entity.createdAt) {
119
+ warnings.push({
120
+ type: 'missing_metadata',
121
+ message: `Entity "${entity.name}" is missing createdAt timestamp`,
122
+ details: { entityName: entity.name, field: 'createdAt' },
123
+ });
124
+ }
125
+ if (!entity.lastModified) {
126
+ warnings.push({
127
+ type: 'missing_metadata',
128
+ message: `Entity "${entity.name}" is missing lastModified timestamp`,
129
+ details: { entityName: entity.name, field: 'lastModified' },
130
+ });
131
+ }
132
+ }
133
+ // Count specific issues
134
+ const orphanedRelationsCount = errors.filter(e => e.type === 'orphaned_relation').length;
135
+ const entitiesWithoutRelationsCount = warnings.filter(w => w.type === 'isolated_entity').length;
136
+ return {
137
+ isValid: errors.length === 0,
138
+ errors,
139
+ warnings,
140
+ summary: {
141
+ totalErrors: errors.length,
142
+ totalWarnings: warnings.length,
143
+ orphanedRelationsCount,
144
+ entitiesWithoutRelationsCount,
145
+ },
146
+ };
147
+ }
148
+ /**
149
+ * Get comprehensive statistics about the knowledge graph.
150
+ *
151
+ * Provides metrics including:
152
+ * - Total counts of entities and relations
153
+ * - Entity and relation type distributions
154
+ * - Oldest and newest entities/relations
155
+ * - Date ranges for entities and relations
156
+ *
157
+ * @returns Graph statistics object
158
+ */
159
+ async getGraphStats() {
160
+ const graph = await this.storage.loadGraph();
161
+ // Calculate entity type counts
162
+ const entityTypesCounts = {};
163
+ graph.entities.forEach(e => {
164
+ entityTypesCounts[e.entityType] = (entityTypesCounts[e.entityType] || 0) + 1;
165
+ });
166
+ // Calculate relation type counts
167
+ const relationTypesCounts = {};
168
+ graph.relations.forEach(r => {
169
+ relationTypesCounts[r.relationType] = (relationTypesCounts[r.relationType] || 0) + 1;
170
+ });
171
+ // Find oldest and newest entities
172
+ let oldestEntity;
173
+ let newestEntity;
174
+ let earliestEntityDate = null;
175
+ let latestEntityDate = null;
176
+ graph.entities.forEach(e => {
177
+ const date = new Date(e.createdAt || '');
178
+ if (!earliestEntityDate || date < earliestEntityDate) {
179
+ earliestEntityDate = date;
180
+ oldestEntity = { name: e.name, date: e.createdAt || '' };
181
+ }
182
+ if (!latestEntityDate || date > latestEntityDate) {
183
+ latestEntityDate = date;
184
+ newestEntity = { name: e.name, date: e.createdAt || '' };
185
+ }
186
+ });
187
+ // Find oldest and newest relations
188
+ let oldestRelation;
189
+ let newestRelation;
190
+ let earliestRelationDate = null;
191
+ let latestRelationDate = null;
192
+ graph.relations.forEach(r => {
193
+ const date = new Date(r.createdAt || '');
194
+ if (!earliestRelationDate || date < earliestRelationDate) {
195
+ earliestRelationDate = date;
196
+ oldestRelation = { from: r.from, to: r.to, relationType: r.relationType, date: r.createdAt || '' };
197
+ }
198
+ if (!latestRelationDate || date > latestRelationDate) {
199
+ latestRelationDate = date;
200
+ newestRelation = { from: r.from, to: r.to, relationType: r.relationType, date: r.createdAt || '' };
201
+ }
202
+ });
203
+ return {
204
+ totalEntities: graph.entities.length,
205
+ totalRelations: graph.relations.length,
206
+ entityTypesCounts,
207
+ relationTypesCounts,
208
+ oldestEntity,
209
+ newestEntity,
210
+ oldestRelation,
211
+ newestRelation,
212
+ entityDateRange: earliestEntityDate && latestEntityDate ? {
213
+ earliest: earliestEntityDate.toISOString(),
214
+ latest: latestEntityDate.toISOString()
215
+ } : undefined,
216
+ relationDateRange: earliestRelationDate && latestRelationDate ? {
217
+ earliest: earliestRelationDate.toISOString(),
218
+ latest: latestRelationDate.toISOString()
219
+ } : undefined,
220
+ };
221
+ }
222
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Archive Manager
3
+ *
4
+ * Archives old or low-importance entities to reduce active graph size.
5
+ *
6
+ * @module features/ArchiveManager
7
+ */
8
+ /**
9
+ * Manages entity archival based on age, importance, and tags.
10
+ */
11
+ export class ArchiveManager {
12
+ storage;
13
+ constructor(storage) {
14
+ this.storage = storage;
15
+ }
16
+ /**
17
+ * Archive old or low-importance entities.
18
+ *
19
+ * Entities matching ANY of the criteria are archived:
20
+ * - lastModified older than olderThan date
21
+ * - importance less than importanceLessThan
22
+ * - has at least one tag from tags array
23
+ *
24
+ * Archived entities and their relations are removed from the graph.
25
+ *
26
+ * @param criteria - Archiving criteria
27
+ * @param dryRun - If true, preview what would be archived without making changes
28
+ * @returns Archive result with count and entity names
29
+ */
30
+ async archiveEntities(criteria, dryRun = false) {
31
+ const graph = await this.storage.loadGraph();
32
+ const toArchive = [];
33
+ for (const entity of graph.entities) {
34
+ let shouldArchive = false;
35
+ // Check age criteria
36
+ if (criteria.olderThan && entity.lastModified) {
37
+ const entityDate = new Date(entity.lastModified);
38
+ const cutoffDate = new Date(criteria.olderThan);
39
+ if (entityDate < cutoffDate) {
40
+ shouldArchive = true;
41
+ }
42
+ }
43
+ // Check importance criteria
44
+ if (criteria.importanceLessThan !== undefined) {
45
+ if (entity.importance === undefined || entity.importance < criteria.importanceLessThan) {
46
+ shouldArchive = true;
47
+ }
48
+ }
49
+ // Check tag criteria (must have at least one matching tag)
50
+ if (criteria.tags && criteria.tags.length > 0) {
51
+ const normalizedCriteriaTags = criteria.tags.map(t => t.toLowerCase());
52
+ const entityTags = (entity.tags || []).map(t => t.toLowerCase());
53
+ const hasMatchingTag = normalizedCriteriaTags.some(tag => entityTags.includes(tag));
54
+ if (hasMatchingTag) {
55
+ shouldArchive = true;
56
+ }
57
+ }
58
+ if (shouldArchive) {
59
+ toArchive.push(entity);
60
+ }
61
+ }
62
+ if (!dryRun && toArchive.length > 0) {
63
+ // Remove archived entities from main graph
64
+ const archiveNames = new Set(toArchive.map(e => e.name));
65
+ graph.entities = graph.entities.filter(e => !archiveNames.has(e.name));
66
+ graph.relations = graph.relations.filter(r => !archiveNames.has(r.from) && !archiveNames.has(r.to));
67
+ await this.storage.saveGraph(graph);
68
+ }
69
+ return {
70
+ archived: toArchive.length,
71
+ entityNames: toArchive.map(e => e.name),
72
+ };
73
+ }
74
+ }
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Backup Manager
3
+ *
4
+ * Manages backup and restore operations for the knowledge graph.
5
+ * Provides point-in-time recovery and data protection.
6
+ *
7
+ * @module features/BackupManager
8
+ */
9
+ import { promises as fs } from 'fs';
10
+ import { dirname, join } from 'path';
11
+ import { FileOperationError } from '../utils/errors.js';
12
+ /**
13
+ * Manages backup and restore operations for the knowledge graph.
14
+ *
15
+ * Backup files are stored in a `.backups` directory next to the main graph file.
16
+ * Each backup includes the graph data and metadata about the backup.
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * const storage = new GraphStorage('/data/memory.jsonl');
21
+ * const backup = new BackupManager(storage);
22
+ *
23
+ * // Create a backup
24
+ * const backupPath = await backup.createBackup('Before compression');
25
+ *
26
+ * // List available backups
27
+ * const backups = await backup.listBackups();
28
+ *
29
+ * // Restore from backup
30
+ * await backup.restoreFromBackup(backupPath);
31
+ *
32
+ * // Clean old backups (keep last 10)
33
+ * await backup.cleanOldBackups(10);
34
+ * ```
35
+ */
36
+ export class BackupManager {
37
+ storage;
38
+ backupDir;
39
+ constructor(storage) {
40
+ this.storage = storage;
41
+ const filePath = this.storage.getFilePath();
42
+ const dir = dirname(filePath);
43
+ this.backupDir = join(dir, '.backups');
44
+ }
45
+ /**
46
+ * Ensure backup directory exists.
47
+ *
48
+ * @private
49
+ */
50
+ async ensureBackupDir() {
51
+ try {
52
+ await fs.mkdir(this.backupDir, { recursive: true });
53
+ }
54
+ catch (error) {
55
+ throw new FileOperationError('create backup directory', this.backupDir, error);
56
+ }
57
+ }
58
+ /**
59
+ * Generate backup file name with timestamp.
60
+ *
61
+ * Format: backup_YYYY-MM-DD_HH-MM-SS-mmm.jsonl
62
+ *
63
+ * @private
64
+ */
65
+ generateBackupFileName() {
66
+ const now = new Date();
67
+ const timestamp = now.toISOString()
68
+ .replace(/:/g, '-')
69
+ .replace(/\./g, '-')
70
+ .replace('T', '_')
71
+ .replace('Z', '');
72
+ return `backup_${timestamp}.jsonl`;
73
+ }
74
+ /**
75
+ * Create a backup of the current knowledge graph.
76
+ *
77
+ * Backup includes:
78
+ * - Complete graph data (entities and relations)
79
+ * - Metadata (timestamp, counts, file size, description)
80
+ *
81
+ * @param description - Optional description for this backup
82
+ * @returns Promise resolving to the backup file path
83
+ * @throws {FileOperationError} If backup creation fails
84
+ *
85
+ * @example
86
+ * ```typescript
87
+ * // Create backup with description
88
+ * const backupPath = await manager.createBackup('Before merging duplicates');
89
+ *
90
+ * // Create backup without description
91
+ * const backupPath = await manager.createBackup();
92
+ * ```
93
+ */
94
+ async createBackup(description) {
95
+ await this.ensureBackupDir();
96
+ // Load current graph
97
+ const graph = await this.storage.loadGraph();
98
+ const fileName = this.generateBackupFileName();
99
+ const backupPath = join(this.backupDir, fileName);
100
+ try {
101
+ // Read the current file content to preserve exact formatting
102
+ const originalPath = this.storage.getFilePath();
103
+ let fileContent;
104
+ try {
105
+ fileContent = await fs.readFile(originalPath, 'utf-8');
106
+ }
107
+ catch (error) {
108
+ // File doesn't exist yet - generate content from graph
109
+ const lines = [
110
+ ...graph.entities.map(e => JSON.stringify({ type: 'entity', ...e })),
111
+ ...graph.relations.map(r => JSON.stringify({ type: 'relation', ...r })),
112
+ ];
113
+ fileContent = lines.join('\n');
114
+ }
115
+ // Write backup file
116
+ await fs.writeFile(backupPath, fileContent);
117
+ // Get file stats
118
+ const stats = await fs.stat(backupPath);
119
+ // Create metadata
120
+ const metadata = {
121
+ timestamp: new Date().toISOString(),
122
+ entityCount: graph.entities.length,
123
+ relationCount: graph.relations.length,
124
+ fileSize: stats.size,
125
+ description,
126
+ };
127
+ // Write metadata file
128
+ const metadataPath = `${backupPath}.meta.json`;
129
+ await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
130
+ return backupPath;
131
+ }
132
+ catch (error) {
133
+ throw new FileOperationError('create backup', backupPath, error);
134
+ }
135
+ }
136
+ /**
137
+ * List all available backups, sorted by timestamp (newest first).
138
+ *
139
+ * @returns Promise resolving to array of backup information
140
+ * @throws {FileOperationError} If listing fails
141
+ *
142
+ * @example
143
+ * ```typescript
144
+ * const backups = await manager.listBackups();
145
+ * console.log(`Found ${backups.length} backups`);
146
+ *
147
+ * for (const backup of backups) {
148
+ * console.log(`${backup.fileName}: ${backup.metadata.entityCount} entities`);
149
+ * }
150
+ * ```
151
+ */
152
+ async listBackups() {
153
+ try {
154
+ // Check if backup directory exists
155
+ try {
156
+ await fs.access(this.backupDir);
157
+ }
158
+ catch {
159
+ // Directory doesn't exist - no backups
160
+ return [];
161
+ }
162
+ const files = await fs.readdir(this.backupDir);
163
+ const backupFiles = files.filter(f => f.startsWith('backup_') && f.endsWith('.jsonl'));
164
+ const backups = [];
165
+ for (const fileName of backupFiles) {
166
+ const filePath = join(this.backupDir, fileName);
167
+ const metadataPath = `${filePath}.meta.json`;
168
+ try {
169
+ // Read metadata if it exists
170
+ const metadataContent = await fs.readFile(metadataPath, 'utf-8');
171
+ const metadata = JSON.parse(metadataContent);
172
+ backups.push({
173
+ fileName,
174
+ filePath,
175
+ metadata,
176
+ });
177
+ }
178
+ catch {
179
+ // Metadata file doesn't exist or is corrupt - skip this backup
180
+ continue;
181
+ }
182
+ }
183
+ // Sort by timestamp (newest first)
184
+ backups.sort((a, b) => new Date(b.metadata.timestamp).getTime() - new Date(a.metadata.timestamp).getTime());
185
+ return backups;
186
+ }
187
+ catch (error) {
188
+ throw new FileOperationError('list backups', this.backupDir, error);
189
+ }
190
+ }
191
+ /**
192
+ * Restore the knowledge graph from a backup file.
193
+ *
194
+ * CAUTION: This operation overwrites the current graph with backup data.
195
+ * Consider creating a backup before restoring.
196
+ *
197
+ * @param backupPath - Path to the backup file to restore from
198
+ * @returns Promise that resolves when restore is complete
199
+ * @throws {FileOperationError} If restore fails
200
+ *
201
+ * @example
202
+ * ```typescript
203
+ * // List backups and restore the most recent
204
+ * const backups = await manager.listBackups();
205
+ * if (backups.length > 0) {
206
+ * await manager.restoreFromBackup(backups[0].filePath);
207
+ * }
208
+ *
209
+ * // Restore specific backup by path
210
+ * await manager.restoreFromBackup('/data/.backups/backup_2024-01-15_10-30-00.jsonl');
211
+ * ```
212
+ */
213
+ async restoreFromBackup(backupPath) {
214
+ try {
215
+ // Verify backup file exists
216
+ await fs.access(backupPath);
217
+ // Read backup content
218
+ const backupContent = await fs.readFile(backupPath, 'utf-8');
219
+ // Write to main graph file
220
+ const mainPath = this.storage.getFilePath();
221
+ await fs.writeFile(mainPath, backupContent);
222
+ // Clear storage cache to force reload
223
+ this.storage.clearCache();
224
+ }
225
+ catch (error) {
226
+ throw new FileOperationError('restore from backup', backupPath, error);
227
+ }
228
+ }
229
+ /**
230
+ * Delete a specific backup file.
231
+ *
232
+ * Also deletes the associated metadata file.
233
+ *
234
+ * @param backupPath - Path to the backup file to delete
235
+ * @returns Promise that resolves when deletion is complete
236
+ * @throws {FileOperationError} If deletion fails
237
+ *
238
+ * @example
239
+ * ```typescript
240
+ * const backups = await manager.listBackups();
241
+ * // Delete oldest backup
242
+ * if (backups.length > 0) {
243
+ * await manager.deleteBackup(backups[backups.length - 1].filePath);
244
+ * }
245
+ * ```
246
+ */
247
+ async deleteBackup(backupPath) {
248
+ try {
249
+ // Delete backup file
250
+ await fs.unlink(backupPath);
251
+ // Delete metadata file (ignore errors if it doesn't exist)
252
+ try {
253
+ await fs.unlink(`${backupPath}.meta.json`);
254
+ }
255
+ catch {
256
+ // Metadata file doesn't exist - that's ok
257
+ }
258
+ }
259
+ catch (error) {
260
+ throw new FileOperationError('delete backup', backupPath, error);
261
+ }
262
+ }
263
+ /**
264
+ * Clean old backups, keeping only the most recent N backups.
265
+ *
266
+ * Backups are sorted by timestamp, and older backups are deleted.
267
+ *
268
+ * @param keepCount - Number of recent backups to keep (default: 10)
269
+ * @returns Promise resolving to number of backups deleted
270
+ * @throws {FileOperationError} If cleanup fails
271
+ *
272
+ * @example
273
+ * ```typescript
274
+ * // Keep only the 5 most recent backups
275
+ * const deleted = await manager.cleanOldBackups(5);
276
+ * console.log(`Deleted ${deleted} old backups`);
277
+ *
278
+ * // Keep default 10 most recent backups
279
+ * await manager.cleanOldBackups();
280
+ * ```
281
+ */
282
+ async cleanOldBackups(keepCount = 10) {
283
+ const backups = await this.listBackups();
284
+ // If we have fewer backups than keepCount, nothing to delete
285
+ if (backups.length <= keepCount) {
286
+ return 0;
287
+ }
288
+ // Delete backups beyond keepCount
289
+ const backupsToDelete = backups.slice(keepCount);
290
+ let deletedCount = 0;
291
+ for (const backup of backupsToDelete) {
292
+ try {
293
+ await this.deleteBackup(backup.filePath);
294
+ deletedCount++;
295
+ }
296
+ catch {
297
+ // Continue deleting other backups even if one fails
298
+ continue;
299
+ }
300
+ }
301
+ return deletedCount;
302
+ }
303
+ /**
304
+ * Get the path to the backup directory.
305
+ *
306
+ * @returns The backup directory path
307
+ */
308
+ getBackupDir() {
309
+ return this.backupDir;
310
+ }
311
+ }