@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.
- package/dist/__tests__/edge-cases/edge-cases.test.js +406 -0
- package/dist/__tests__/file-path.test.js +5 -5
- package/dist/__tests__/integration/workflows.test.js +449 -0
- package/dist/__tests__/knowledge-graph.test.js +8 -3
- package/dist/__tests__/performance/benchmarks.test.js +411 -0
- package/dist/__tests__/unit/core/EntityManager.test.js +334 -0
- package/dist/__tests__/unit/core/GraphStorage.test.js +205 -0
- package/dist/__tests__/unit/core/RelationManager.test.js +274 -0
- package/dist/__tests__/unit/features/CompressionManager.test.js +350 -0
- package/dist/__tests__/unit/search/BasicSearch.test.js +311 -0
- package/dist/__tests__/unit/search/BooleanSearch.test.js +432 -0
- package/dist/__tests__/unit/search/FuzzySearch.test.js +448 -0
- package/dist/__tests__/unit/search/RankedSearch.test.js +379 -0
- package/dist/__tests__/unit/utils/levenshtein.test.js +77 -0
- package/dist/core/EntityManager.js +554 -0
- package/dist/core/GraphStorage.js +172 -0
- package/dist/core/KnowledgeGraphManager.js +400 -0
- package/dist/core/ObservationManager.js +129 -0
- package/dist/core/RelationManager.js +186 -0
- package/dist/core/TransactionManager.js +389 -0
- package/dist/core/index.js +9 -0
- package/dist/features/AnalyticsManager.js +222 -0
- package/dist/features/ArchiveManager.js +74 -0
- package/dist/features/BackupManager.js +311 -0
- package/dist/features/CompressionManager.js +310 -0
- package/dist/features/ExportManager.js +305 -0
- package/dist/features/HierarchyManager.js +219 -0
- package/dist/features/ImportExportManager.js +50 -0
- package/dist/features/ImportManager.js +328 -0
- package/dist/features/TagManager.js +210 -0
- package/dist/features/index.js +12 -0
- package/dist/index.js +13 -996
- package/dist/memory.jsonl +225 -0
- package/dist/search/BasicSearch.js +161 -0
- package/dist/search/BooleanSearch.js +304 -0
- package/dist/search/FuzzySearch.js +115 -0
- package/dist/search/RankedSearch.js +206 -0
- package/dist/search/SavedSearchManager.js +145 -0
- package/dist/search/SearchManager.js +305 -0
- package/dist/search/SearchSuggestions.js +57 -0
- package/dist/search/TFIDFIndexManager.js +217 -0
- package/dist/search/index.js +10 -0
- package/dist/server/MCPServer.js +889 -0
- package/dist/types/analytics.types.js +6 -0
- package/dist/types/entity.types.js +7 -0
- package/dist/types/import-export.types.js +7 -0
- package/dist/types/index.js +12 -0
- package/dist/types/search.types.js +7 -0
- package/dist/types/tag.types.js +6 -0
- package/dist/utils/constants.js +127 -0
- package/dist/utils/dateUtils.js +89 -0
- package/dist/utils/errors.js +121 -0
- package/dist/utils/index.js +13 -0
- package/dist/utils/levenshtein.js +62 -0
- package/dist/utils/logger.js +33 -0
- package/dist/utils/pathUtils.js +115 -0
- package/dist/utils/schemas.js +184 -0
- package/dist/utils/searchCache.js +209 -0
- package/dist/utils/tfidf.js +90 -0
- package/dist/utils/validationUtils.js +109 -0
- 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
|
+
}
|