@danielsimonjr/memory-mcp 11.1.1 → 12.5.2

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 (161) hide show
  1. package/README.md +302 -107
  2. package/dist/index.d.ts +2 -2
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +6 -0
  5. package/dist/server/toolDefinitions.d.ts +1 -1
  6. package/dist/server/toolDefinitions.d.ts.map +1 -1
  7. package/dist/server/toolDefinitions.js +2323 -6
  8. package/dist/server/toolHandlers.d.ts +2 -10
  9. package/dist/server/toolHandlers.d.ts.map +1 -1
  10. package/dist/server/toolHandlers.js +2097 -7
  11. package/package.json +9 -6
  12. package/dist/core/EntityManager.d.ts +0 -263
  13. package/dist/core/EntityManager.d.ts.map +0 -1
  14. package/dist/core/EntityManager.js +0 -478
  15. package/dist/core/GraphStorage.d.ts +0 -344
  16. package/dist/core/GraphStorage.d.ts.map +0 -1
  17. package/dist/core/GraphStorage.js +0 -708
  18. package/dist/core/GraphTraversal.d.ts +0 -136
  19. package/dist/core/GraphTraversal.d.ts.map +0 -1
  20. package/dist/core/GraphTraversal.js +0 -556
  21. package/dist/core/HierarchyManager.d.ts +0 -111
  22. package/dist/core/HierarchyManager.d.ts.map +0 -1
  23. package/dist/core/HierarchyManager.js +0 -224
  24. package/dist/core/ManagerContext.d.ts +0 -72
  25. package/dist/core/ManagerContext.d.ts.map +0 -1
  26. package/dist/core/ManagerContext.js +0 -118
  27. package/dist/core/ObservationManager.d.ts +0 -85
  28. package/dist/core/ObservationManager.d.ts.map +0 -1
  29. package/dist/core/ObservationManager.js +0 -123
  30. package/dist/core/RelationManager.d.ts +0 -131
  31. package/dist/core/RelationManager.d.ts.map +0 -1
  32. package/dist/core/RelationManager.js +0 -210
  33. package/dist/core/SQLiteStorage.d.ts +0 -354
  34. package/dist/core/SQLiteStorage.d.ts.map +0 -1
  35. package/dist/core/SQLiteStorage.js +0 -917
  36. package/dist/core/StorageFactory.d.ts +0 -45
  37. package/dist/core/StorageFactory.d.ts.map +0 -1
  38. package/dist/core/StorageFactory.js +0 -64
  39. package/dist/core/TransactionManager.d.ts +0 -259
  40. package/dist/core/TransactionManager.d.ts.map +0 -1
  41. package/dist/core/TransactionManager.js +0 -392
  42. package/dist/core/index.d.ts +0 -16
  43. package/dist/core/index.d.ts.map +0 -1
  44. package/dist/core/index.js +0 -17
  45. package/dist/features/AnalyticsManager.d.ts +0 -44
  46. package/dist/features/AnalyticsManager.d.ts.map +0 -1
  47. package/dist/features/AnalyticsManager.js +0 -223
  48. package/dist/features/ArchiveManager.d.ts +0 -121
  49. package/dist/features/ArchiveManager.d.ts.map +0 -1
  50. package/dist/features/ArchiveManager.js +0 -240
  51. package/dist/features/CompressionManager.d.ts +0 -109
  52. package/dist/features/CompressionManager.d.ts.map +0 -1
  53. package/dist/features/CompressionManager.js +0 -404
  54. package/dist/features/IOManager.d.ts +0 -221
  55. package/dist/features/IOManager.d.ts.map +0 -1
  56. package/dist/features/IOManager.js +0 -997
  57. package/dist/features/StreamingExporter.d.ts +0 -105
  58. package/dist/features/StreamingExporter.d.ts.map +0 -1
  59. package/dist/features/StreamingExporter.js +0 -150
  60. package/dist/features/TagManager.d.ts +0 -147
  61. package/dist/features/TagManager.d.ts.map +0 -1
  62. package/dist/features/TagManager.js +0 -210
  63. package/dist/features/index.d.ts +0 -12
  64. package/dist/features/index.d.ts.map +0 -1
  65. package/dist/features/index.js +0 -11
  66. package/dist/search/BasicSearch.d.ts +0 -51
  67. package/dist/search/BasicSearch.d.ts.map +0 -1
  68. package/dist/search/BasicSearch.js +0 -137
  69. package/dist/search/BooleanSearch.d.ts +0 -98
  70. package/dist/search/BooleanSearch.d.ts.map +0 -1
  71. package/dist/search/BooleanSearch.js +0 -430
  72. package/dist/search/EmbeddingService.d.ts +0 -178
  73. package/dist/search/EmbeddingService.d.ts.map +0 -1
  74. package/dist/search/EmbeddingService.js +0 -358
  75. package/dist/search/FuzzySearch.d.ts +0 -118
  76. package/dist/search/FuzzySearch.d.ts.map +0 -1
  77. package/dist/search/FuzzySearch.js +0 -312
  78. package/dist/search/RankedSearch.d.ts +0 -71
  79. package/dist/search/RankedSearch.d.ts.map +0 -1
  80. package/dist/search/RankedSearch.js +0 -238
  81. package/dist/search/SavedSearchManager.d.ts +0 -79
  82. package/dist/search/SavedSearchManager.d.ts.map +0 -1
  83. package/dist/search/SavedSearchManager.js +0 -145
  84. package/dist/search/SearchFilterChain.d.ts +0 -120
  85. package/dist/search/SearchFilterChain.d.ts.map +0 -1
  86. package/dist/search/SearchFilterChain.js +0 -185
  87. package/dist/search/SearchManager.d.ts +0 -285
  88. package/dist/search/SearchManager.d.ts.map +0 -1
  89. package/dist/search/SearchManager.js +0 -338
  90. package/dist/search/SearchSuggestions.d.ts +0 -27
  91. package/dist/search/SearchSuggestions.d.ts.map +0 -1
  92. package/dist/search/SearchSuggestions.js +0 -57
  93. package/dist/search/SemanticSearch.d.ts +0 -146
  94. package/dist/search/SemanticSearch.d.ts.map +0 -1
  95. package/dist/search/SemanticSearch.js +0 -313
  96. package/dist/search/TFIDFIndexManager.d.ts +0 -63
  97. package/dist/search/TFIDFIndexManager.d.ts.map +0 -1
  98. package/dist/search/TFIDFIndexManager.js +0 -215
  99. package/dist/search/VectorStore.d.ts +0 -235
  100. package/dist/search/VectorStore.d.ts.map +0 -1
  101. package/dist/search/VectorStore.js +0 -311
  102. package/dist/search/index.d.ts +0 -18
  103. package/dist/search/index.d.ts.map +0 -1
  104. package/dist/search/index.js +0 -21
  105. package/dist/types/index.d.ts +0 -13
  106. package/dist/types/index.d.ts.map +0 -1
  107. package/dist/types/index.js +0 -12
  108. package/dist/types/types.d.ts +0 -1234
  109. package/dist/types/types.d.ts.map +0 -1
  110. package/dist/types/types.js +0 -9
  111. package/dist/utils/compressedCache.d.ts +0 -192
  112. package/dist/utils/compressedCache.d.ts.map +0 -1
  113. package/dist/utils/compressedCache.js +0 -309
  114. package/dist/utils/compressionUtil.d.ts +0 -214
  115. package/dist/utils/compressionUtil.d.ts.map +0 -1
  116. package/dist/utils/compressionUtil.js +0 -247
  117. package/dist/utils/constants.d.ts +0 -245
  118. package/dist/utils/constants.d.ts.map +0 -1
  119. package/dist/utils/constants.js +0 -252
  120. package/dist/utils/entityUtils.d.ts +0 -321
  121. package/dist/utils/entityUtils.d.ts.map +0 -1
  122. package/dist/utils/entityUtils.js +0 -538
  123. package/dist/utils/errors.d.ts +0 -77
  124. package/dist/utils/errors.d.ts.map +0 -1
  125. package/dist/utils/errors.js +0 -121
  126. package/dist/utils/formatters.d.ts +0 -145
  127. package/dist/utils/formatters.d.ts.map +0 -1
  128. package/dist/utils/formatters.js +0 -132
  129. package/dist/utils/index.d.ts +0 -22
  130. package/dist/utils/index.d.ts.map +0 -1
  131. package/dist/utils/index.js +0 -75
  132. package/dist/utils/indexes.d.ts +0 -270
  133. package/dist/utils/indexes.d.ts.map +0 -1
  134. package/dist/utils/indexes.js +0 -526
  135. package/dist/utils/logger.d.ts +0 -24
  136. package/dist/utils/logger.d.ts.map +0 -1
  137. package/dist/utils/logger.js +0 -33
  138. package/dist/utils/parallelUtils.d.ts +0 -72
  139. package/dist/utils/parallelUtils.d.ts.map +0 -1
  140. package/dist/utils/parallelUtils.js +0 -169
  141. package/dist/utils/schemas.d.ts +0 -374
  142. package/dist/utils/schemas.d.ts.map +0 -1
  143. package/dist/utils/schemas.js +0 -484
  144. package/dist/utils/searchAlgorithms.d.ts +0 -99
  145. package/dist/utils/searchAlgorithms.d.ts.map +0 -1
  146. package/dist/utils/searchAlgorithms.js +0 -167
  147. package/dist/utils/searchCache.d.ts +0 -108
  148. package/dist/utils/searchCache.d.ts.map +0 -1
  149. package/dist/utils/searchCache.js +0 -209
  150. package/dist/utils/taskScheduler.d.ts +0 -290
  151. package/dist/utils/taskScheduler.d.ts.map +0 -1
  152. package/dist/utils/taskScheduler.js +0 -466
  153. package/dist/workers/WorkerPool.d.ts +0 -81
  154. package/dist/workers/WorkerPool.d.ts.map +0 -1
  155. package/dist/workers/WorkerPool.js +0 -121
  156. package/dist/workers/index.d.ts +0 -12
  157. package/dist/workers/index.d.ts.map +0 -1
  158. package/dist/workers/index.js +0 -9
  159. package/dist/workers/levenshteinWorker.d.ts +0 -60
  160. package/dist/workers/levenshteinWorker.d.ts.map +0 -1
  161. package/dist/workers/levenshteinWorker.js +0 -98
@@ -1,997 +0,0 @@
1
- /**
2
- * IO Manager
3
- *
4
- * Unified manager for import, export, and backup operations.
5
- * Consolidates BackupManager, ExportManager, and ImportManager (Sprint 11.4).
6
- *
7
- * @module features/IOManager
8
- */
9
- import { promises as fs } from 'fs';
10
- import { dirname, join } from 'path';
11
- import { FileOperationError } from '../utils/errors.js';
12
- import { compress, decompress, hasBrotliExtension, COMPRESSION_CONFIG, STREAMING_CONFIG, } from '../utils/index.js';
13
- import { StreamingExporter } from './StreamingExporter.js';
14
- // ============================================================
15
- // IO MANAGER CLASS
16
- // ============================================================
17
- /**
18
- * Unified manager for import, export, and backup operations.
19
- *
20
- * Combines functionality from:
21
- * - ExportManager: Graph export to various formats
22
- * - ImportManager: Graph import from various formats
23
- * - BackupManager: Point-in-time backup and restore
24
- */
25
- export class IOManager {
26
- storage;
27
- backupDir;
28
- constructor(storage) {
29
- this.storage = storage;
30
- const filePath = this.storage.getFilePath();
31
- const dir = dirname(filePath);
32
- this.backupDir = join(dir, '.backups');
33
- }
34
- // ============================================================
35
- // EXPORT OPERATIONS
36
- // ============================================================
37
- /**
38
- * Export graph to specified format.
39
- *
40
- * @param graph - Knowledge graph to export
41
- * @param format - Export format
42
- * @returns Formatted export string
43
- */
44
- exportGraph(graph, format) {
45
- switch (format) {
46
- case 'json':
47
- return this.exportAsJson(graph);
48
- case 'csv':
49
- return this.exportAsCsv(graph);
50
- case 'graphml':
51
- return this.exportAsGraphML(graph);
52
- case 'gexf':
53
- return this.exportAsGEXF(graph);
54
- case 'dot':
55
- return this.exportAsDOT(graph);
56
- case 'markdown':
57
- return this.exportAsMarkdown(graph);
58
- case 'mermaid':
59
- return this.exportAsMermaid(graph);
60
- default:
61
- throw new Error(`Unsupported export format: ${format}`);
62
- }
63
- }
64
- /**
65
- * Export graph with optional brotli compression.
66
- *
67
- * Compression is applied when:
68
- * - `options.compress` is explicitly set to `true`
69
- * - The exported content exceeds 100KB (auto-compress threshold)
70
- *
71
- * Compressed content is returned as base64-encoded string.
72
- * Uncompressed content is returned as UTF-8 string.
73
- *
74
- * @param graph - Knowledge graph to export
75
- * @param format - Export format
76
- * @param options - Export options including compression settings
77
- * @returns Export result with content and compression metadata
78
- *
79
- * @example
80
- * ```typescript
81
- * // Export with explicit compression
82
- * const result = await manager.exportGraphWithCompression(graph, 'json', {
83
- * compress: true,
84
- * compressionQuality: 11
85
- * });
86
- *
87
- * // Export with auto-compression for large graphs
88
- * const result = await manager.exportGraphWithCompression(graph, 'json');
89
- * // Compresses automatically if content > 100KB
90
- * ```
91
- */
92
- async exportGraphWithCompression(graph, format, options) {
93
- // Check if streaming should be used
94
- const shouldStream = options?.streaming ||
95
- (options?.outputPath && graph.entities.length >= STREAMING_CONFIG.STREAMING_THRESHOLD);
96
- if (shouldStream && options?.outputPath) {
97
- return this.streamExport(format, graph, options);
98
- }
99
- // Generate export content using existing method
100
- const content = this.exportGraph(graph, format);
101
- const originalSize = Buffer.byteLength(content, 'utf-8');
102
- // Determine if compression should be applied
103
- const shouldCompress = options?.compress === true ||
104
- (options?.compress !== false &&
105
- originalSize > COMPRESSION_CONFIG.AUTO_COMPRESS_EXPORT_SIZE);
106
- if (shouldCompress) {
107
- const quality = options?.compressionQuality ?? COMPRESSION_CONFIG.BROTLI_QUALITY_BATCH;
108
- const compressionResult = await compress(content, {
109
- quality,
110
- mode: 'text',
111
- });
112
- return {
113
- format,
114
- content: compressionResult.compressed.toString('base64'),
115
- entityCount: graph.entities.length,
116
- relationCount: graph.relations.length,
117
- compressed: true,
118
- encoding: 'base64',
119
- originalSize,
120
- compressedSize: compressionResult.compressedSize,
121
- compressionRatio: compressionResult.ratio,
122
- };
123
- }
124
- // Return uncompressed content
125
- return {
126
- format,
127
- content,
128
- entityCount: graph.entities.length,
129
- relationCount: graph.relations.length,
130
- compressed: false,
131
- encoding: 'utf-8',
132
- originalSize,
133
- compressedSize: originalSize,
134
- compressionRatio: 1,
135
- };
136
- }
137
- /**
138
- * Stream export to a file for large graphs.
139
- *
140
- * Uses StreamingExporter to write entities and relations incrementally
141
- * to avoid loading the entire export content into memory.
142
- *
143
- * @param format - Export format
144
- * @param graph - Knowledge graph to export
145
- * @param options - Export options with required outputPath
146
- * @returns Export result with streaming metadata
147
- * @private
148
- */
149
- async streamExport(format, graph, options) {
150
- const exporter = new StreamingExporter(options.outputPath);
151
- let result;
152
- switch (format) {
153
- case 'json':
154
- // Use JSONL format for streaming (line-delimited JSON)
155
- result = await exporter.streamJSONL(graph);
156
- break;
157
- case 'csv':
158
- result = await exporter.streamCSV(graph);
159
- break;
160
- default:
161
- // Fallback to in-memory export for unsupported streaming formats
162
- const content = this.exportGraph(graph, format);
163
- await fs.writeFile(options.outputPath, content);
164
- result = {
165
- bytesWritten: Buffer.byteLength(content, 'utf-8'),
166
- entitiesWritten: graph.entities.length,
167
- relationsWritten: graph.relations.length,
168
- durationMs: 0,
169
- };
170
- }
171
- return {
172
- format,
173
- content: `Streamed to ${options.outputPath}`,
174
- entityCount: result.entitiesWritten,
175
- relationCount: result.relationsWritten,
176
- compressed: false,
177
- encoding: 'utf-8',
178
- originalSize: result.bytesWritten,
179
- compressedSize: result.bytesWritten,
180
- compressionRatio: 1,
181
- streamed: true,
182
- outputPath: options.outputPath,
183
- };
184
- }
185
- exportAsJson(graph) {
186
- return JSON.stringify(graph, null, 2);
187
- }
188
- exportAsCsv(graph) {
189
- const lines = [];
190
- const escapeCsvField = (field) => {
191
- if (field === undefined || field === null)
192
- return '';
193
- const str = String(field);
194
- if (str.includes(',') || str.includes('"') || str.includes('\n')) {
195
- return `"${str.replace(/"/g, '""')}"`;
196
- }
197
- return str;
198
- };
199
- lines.push('# ENTITIES');
200
- lines.push('name,entityType,observations,createdAt,lastModified,tags,importance');
201
- for (const entity of graph.entities) {
202
- const observationsStr = entity.observations.join('; ');
203
- const tagsStr = entity.tags ? entity.tags.join('; ') : '';
204
- const importanceStr = entity.importance !== undefined ? String(entity.importance) : '';
205
- lines.push([
206
- escapeCsvField(entity.name),
207
- escapeCsvField(entity.entityType),
208
- escapeCsvField(observationsStr),
209
- escapeCsvField(entity.createdAt),
210
- escapeCsvField(entity.lastModified),
211
- escapeCsvField(tagsStr),
212
- escapeCsvField(importanceStr),
213
- ].join(','));
214
- }
215
- lines.push('');
216
- lines.push('# RELATIONS');
217
- lines.push('from,to,relationType,createdAt,lastModified');
218
- for (const relation of graph.relations) {
219
- lines.push([
220
- escapeCsvField(relation.from),
221
- escapeCsvField(relation.to),
222
- escapeCsvField(relation.relationType),
223
- escapeCsvField(relation.createdAt),
224
- escapeCsvField(relation.lastModified),
225
- ].join(','));
226
- }
227
- return lines.join('\n');
228
- }
229
- exportAsGraphML(graph) {
230
- const lines = [];
231
- const escapeXml = (str) => {
232
- if (str === undefined || str === null)
233
- return '';
234
- return String(str)
235
- .replace(/&/g, '&')
236
- .replace(/</g, '&lt;')
237
- .replace(/>/g, '&gt;')
238
- .replace(/"/g, '&quot;')
239
- .replace(/'/g, '&apos;');
240
- };
241
- lines.push('<?xml version="1.0" encoding="UTF-8"?>');
242
- lines.push('<graphml xmlns="http://graphml.graphdrawing.org/xmlns">');
243
- lines.push(' <key id="d0" for="node" attr.name="entityType" attr.type="string"/>');
244
- lines.push(' <key id="d1" for="node" attr.name="observations" attr.type="string"/>');
245
- lines.push(' <key id="d2" for="node" attr.name="createdAt" attr.type="string"/>');
246
- lines.push(' <key id="d3" for="node" attr.name="lastModified" attr.type="string"/>');
247
- lines.push(' <key id="d4" for="node" attr.name="tags" attr.type="string"/>');
248
- lines.push(' <key id="d5" for="node" attr.name="importance" attr.type="double"/>');
249
- lines.push(' <key id="e0" for="edge" attr.name="relationType" attr.type="string"/>');
250
- lines.push(' <key id="e1" for="edge" attr.name="createdAt" attr.type="string"/>');
251
- lines.push(' <key id="e2" for="edge" attr.name="lastModified" attr.type="string"/>');
252
- lines.push(' <graph id="G" edgedefault="directed">');
253
- for (const entity of graph.entities) {
254
- const nodeId = escapeXml(entity.name);
255
- lines.push(` <node id="${nodeId}">`);
256
- lines.push(` <data key="d0">${escapeXml(entity.entityType)}</data>`);
257
- lines.push(` <data key="d1">${escapeXml(entity.observations.join('; '))}</data>`);
258
- if (entity.createdAt)
259
- lines.push(` <data key="d2">${escapeXml(entity.createdAt)}</data>`);
260
- if (entity.lastModified)
261
- lines.push(` <data key="d3">${escapeXml(entity.lastModified)}</data>`);
262
- if (entity.tags?.length)
263
- lines.push(` <data key="d4">${escapeXml(entity.tags.join('; '))}</data>`);
264
- if (entity.importance !== undefined)
265
- lines.push(` <data key="d5">${entity.importance}</data>`);
266
- lines.push(' </node>');
267
- }
268
- let edgeId = 0;
269
- for (const relation of graph.relations) {
270
- const sourceId = escapeXml(relation.from);
271
- const targetId = escapeXml(relation.to);
272
- lines.push(` <edge id="e${edgeId}" source="${sourceId}" target="${targetId}">`);
273
- lines.push(` <data key="e0">${escapeXml(relation.relationType)}</data>`);
274
- if (relation.createdAt)
275
- lines.push(` <data key="e1">${escapeXml(relation.createdAt)}</data>`);
276
- if (relation.lastModified)
277
- lines.push(` <data key="e2">${escapeXml(relation.lastModified)}</data>`);
278
- lines.push(' </edge>');
279
- edgeId++;
280
- }
281
- lines.push(' </graph>');
282
- lines.push('</graphml>');
283
- return lines.join('\n');
284
- }
285
- exportAsGEXF(graph) {
286
- const lines = [];
287
- const escapeXml = (str) => {
288
- if (str === undefined || str === null)
289
- return '';
290
- return String(str)
291
- .replace(/&/g, '&amp;')
292
- .replace(/</g, '&lt;')
293
- .replace(/>/g, '&gt;')
294
- .replace(/"/g, '&quot;')
295
- .replace(/'/g, '&apos;');
296
- };
297
- lines.push('<?xml version="1.0" encoding="UTF-8"?>');
298
- lines.push('<gexf xmlns="http://www.gexf.net/1.2draft" version="1.2">');
299
- lines.push(' <meta>');
300
- lines.push(' <creator>Memory MCP Server</creator>');
301
- lines.push(' </meta>');
302
- lines.push(' <graph mode="static" defaultedgetype="directed">');
303
- lines.push(' <attributes class="node">');
304
- lines.push(' <attribute id="0" title="entityType" type="string"/>');
305
- lines.push(' <attribute id="1" title="observations" type="string"/>');
306
- lines.push(' </attributes>');
307
- lines.push(' <nodes>');
308
- for (const entity of graph.entities) {
309
- const nodeId = escapeXml(entity.name);
310
- lines.push(` <node id="${nodeId}" label="${nodeId}">`);
311
- lines.push(' <attvalues>');
312
- lines.push(` <attvalue for="0" value="${escapeXml(entity.entityType)}"/>`);
313
- lines.push(` <attvalue for="1" value="${escapeXml(entity.observations.join('; '))}"/>`);
314
- lines.push(' </attvalues>');
315
- lines.push(' </node>');
316
- }
317
- lines.push(' </nodes>');
318
- lines.push(' <edges>');
319
- let edgeId = 0;
320
- for (const relation of graph.relations) {
321
- const sourceId = escapeXml(relation.from);
322
- const targetId = escapeXml(relation.to);
323
- const label = escapeXml(relation.relationType);
324
- lines.push(` <edge id="${edgeId}" source="${sourceId}" target="${targetId}" label="${label}"/>`);
325
- edgeId++;
326
- }
327
- lines.push(' </edges>');
328
- lines.push(' </graph>');
329
- lines.push('</gexf>');
330
- return lines.join('\n');
331
- }
332
- exportAsDOT(graph) {
333
- const lines = [];
334
- const escapeDot = (str) => {
335
- return '"' + str.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n') + '"';
336
- };
337
- lines.push('digraph KnowledgeGraph {');
338
- lines.push(' rankdir=LR;');
339
- lines.push(' node [shape=box, style=rounded];');
340
- lines.push('');
341
- for (const entity of graph.entities) {
342
- const nodeId = escapeDot(entity.name);
343
- const label = [`${entity.name}`, `Type: ${entity.entityType}`];
344
- if (entity.tags?.length)
345
- label.push(`Tags: ${entity.tags.join(', ')}`);
346
- const labelStr = escapeDot(label.join('\\n'));
347
- lines.push(` ${nodeId} [label=${labelStr}];`);
348
- }
349
- lines.push('');
350
- for (const relation of graph.relations) {
351
- const fromId = escapeDot(relation.from);
352
- const toId = escapeDot(relation.to);
353
- const label = escapeDot(relation.relationType);
354
- lines.push(` ${fromId} -> ${toId} [label=${label}];`);
355
- }
356
- lines.push('}');
357
- return lines.join('\n');
358
- }
359
- exportAsMarkdown(graph) {
360
- const lines = [];
361
- lines.push('# Knowledge Graph Export');
362
- lines.push('');
363
- lines.push(`**Exported:** ${new Date().toISOString()}`);
364
- lines.push(`**Entities:** ${graph.entities.length}`);
365
- lines.push(`**Relations:** ${graph.relations.length}`);
366
- lines.push('');
367
- lines.push('## Entities');
368
- lines.push('');
369
- for (const entity of graph.entities) {
370
- lines.push(`### ${entity.name}`);
371
- lines.push('');
372
- lines.push(`- **Type:** ${entity.entityType}`);
373
- if (entity.tags?.length)
374
- lines.push(`- **Tags:** ${entity.tags.map(t => `\`${t}\``).join(', ')}`);
375
- if (entity.importance !== undefined)
376
- lines.push(`- **Importance:** ${entity.importance}/10`);
377
- if (entity.observations.length > 0) {
378
- lines.push('');
379
- lines.push('**Observations:**');
380
- for (const obs of entity.observations) {
381
- lines.push(`- ${obs}`);
382
- }
383
- }
384
- lines.push('');
385
- }
386
- if (graph.relations.length > 0) {
387
- lines.push('## Relations');
388
- lines.push('');
389
- for (const relation of graph.relations) {
390
- lines.push(`- **${relation.from}** → *${relation.relationType}* → **${relation.to}**`);
391
- }
392
- lines.push('');
393
- }
394
- return lines.join('\n');
395
- }
396
- exportAsMermaid(graph) {
397
- const lines = [];
398
- const sanitizeId = (str) => str.replace(/[^a-zA-Z0-9_]/g, '_');
399
- const escapeLabel = (str) => str.replace(/"/g, '#quot;');
400
- lines.push('graph LR');
401
- lines.push(' %% Knowledge Graph');
402
- lines.push('');
403
- const nodeIds = new Map();
404
- for (const entity of graph.entities) {
405
- nodeIds.set(entity.name, sanitizeId(entity.name));
406
- }
407
- for (const entity of graph.entities) {
408
- const nodeId = nodeIds.get(entity.name);
409
- const labelParts = [entity.name, `Type: ${entity.entityType}`];
410
- if (entity.tags?.length)
411
- labelParts.push(`Tags: ${entity.tags.join(', ')}`);
412
- const label = escapeLabel(labelParts.join('<br/>'));
413
- lines.push(` ${nodeId}["${label}"]`);
414
- }
415
- lines.push('');
416
- for (const relation of graph.relations) {
417
- const fromId = nodeIds.get(relation.from);
418
- const toId = nodeIds.get(relation.to);
419
- if (fromId && toId) {
420
- const label = escapeLabel(relation.relationType);
421
- lines.push(` ${fromId} -->|"${label}"| ${toId}`);
422
- }
423
- }
424
- return lines.join('\n');
425
- }
426
- // ============================================================
427
- // IMPORT OPERATIONS
428
- // ============================================================
429
- /**
430
- * Import graph from formatted data.
431
- *
432
- * @param format - Import format
433
- * @param data - Import data string
434
- * @param mergeStrategy - How to handle conflicts
435
- * @param dryRun - If true, preview changes without applying
436
- * @returns Import result with statistics
437
- */
438
- async importGraph(format, data, mergeStrategy = 'skip', dryRun = false) {
439
- let importedGraph;
440
- try {
441
- switch (format) {
442
- case 'json':
443
- importedGraph = this.parseJsonImport(data);
444
- break;
445
- case 'csv':
446
- importedGraph = this.parseCsvImport(data);
447
- break;
448
- case 'graphml':
449
- importedGraph = this.parseGraphMLImport(data);
450
- break;
451
- default:
452
- throw new Error(`Unsupported import format: ${format}`);
453
- }
454
- }
455
- catch (error) {
456
- return {
457
- entitiesAdded: 0,
458
- entitiesSkipped: 0,
459
- entitiesUpdated: 0,
460
- relationsAdded: 0,
461
- relationsSkipped: 0,
462
- errors: [`Failed to parse ${format} data: ${error instanceof Error ? error.message : String(error)}`],
463
- };
464
- }
465
- return await this.mergeImportedGraph(importedGraph, mergeStrategy, dryRun);
466
- }
467
- parseJsonImport(data) {
468
- const parsed = JSON.parse(data);
469
- if (!parsed.entities || !Array.isArray(parsed.entities)) {
470
- throw new Error('Invalid JSON: missing or invalid entities array');
471
- }
472
- if (!parsed.relations || !Array.isArray(parsed.relations)) {
473
- throw new Error('Invalid JSON: missing or invalid relations array');
474
- }
475
- return {
476
- entities: parsed.entities,
477
- relations: parsed.relations,
478
- };
479
- }
480
- parseCsvImport(data) {
481
- const lines = data
482
- .split('\n')
483
- .map(line => line.trim())
484
- .filter(line => line);
485
- const entities = [];
486
- const relations = [];
487
- let section = null;
488
- let headerParsed = false;
489
- const parseCsvLine = (line) => {
490
- const fields = [];
491
- let current = '';
492
- let inQuotes = false;
493
- for (let i = 0; i < line.length; i++) {
494
- const char = line[i];
495
- if (char === '"') {
496
- if (inQuotes && line[i + 1] === '"') {
497
- current += '"';
498
- i++;
499
- }
500
- else {
501
- inQuotes = !inQuotes;
502
- }
503
- }
504
- else if (char === ',' && !inQuotes) {
505
- fields.push(current);
506
- current = '';
507
- }
508
- else {
509
- current += char;
510
- }
511
- }
512
- fields.push(current);
513
- return fields;
514
- };
515
- for (const line of lines) {
516
- if (line.startsWith('# ENTITIES')) {
517
- section = 'entities';
518
- headerParsed = false;
519
- continue;
520
- }
521
- else if (line.startsWith('# RELATIONS')) {
522
- section = 'relations';
523
- headerParsed = false;
524
- continue;
525
- }
526
- if (line.startsWith('#'))
527
- continue;
528
- if (section === 'entities') {
529
- if (!headerParsed) {
530
- headerParsed = true;
531
- continue;
532
- }
533
- const fields = parseCsvLine(line);
534
- if (fields.length >= 2) {
535
- const entity = {
536
- name: fields[0],
537
- entityType: fields[1],
538
- observations: fields[2]
539
- ? fields[2]
540
- .split(';')
541
- .map(s => s.trim())
542
- .filter(s => s)
543
- : [],
544
- createdAt: fields[3] || undefined,
545
- lastModified: fields[4] || undefined,
546
- tags: fields[5]
547
- ? fields[5]
548
- .split(';')
549
- .map(s => s.trim().toLowerCase())
550
- .filter(s => s)
551
- : undefined,
552
- importance: fields[6] ? parseFloat(fields[6]) : undefined,
553
- };
554
- entities.push(entity);
555
- }
556
- }
557
- else if (section === 'relations') {
558
- if (!headerParsed) {
559
- headerParsed = true;
560
- continue;
561
- }
562
- const fields = parseCsvLine(line);
563
- if (fields.length >= 3) {
564
- const relation = {
565
- from: fields[0],
566
- to: fields[1],
567
- relationType: fields[2],
568
- createdAt: fields[3] || undefined,
569
- lastModified: fields[4] || undefined,
570
- };
571
- relations.push(relation);
572
- }
573
- }
574
- }
575
- return { entities, relations };
576
- }
577
- parseGraphMLImport(data) {
578
- const entities = [];
579
- const relations = [];
580
- const nodeRegex = /<node\s+id="([^"]+)"[^>]*>([\s\S]*?)<\/node>/g;
581
- let nodeMatch;
582
- while ((nodeMatch = nodeRegex.exec(data)) !== null) {
583
- const nodeId = nodeMatch[1];
584
- const nodeContent = nodeMatch[2];
585
- const getDataValue = (key) => {
586
- const dataRegex = new RegExp(`<data\\s+key="${key}">([^<]*)<\/data>`);
587
- const match = dataRegex.exec(nodeContent);
588
- return match ? match[1] : undefined;
589
- };
590
- const entity = {
591
- name: nodeId,
592
- entityType: getDataValue('d0') || getDataValue('entityType') || 'unknown',
593
- observations: (getDataValue('d1') || getDataValue('observations') || '')
594
- .split(';')
595
- .map(s => s.trim())
596
- .filter(s => s),
597
- createdAt: getDataValue('d2') || getDataValue('createdAt'),
598
- lastModified: getDataValue('d3') || getDataValue('lastModified'),
599
- tags: (getDataValue('d4') || getDataValue('tags') || '')
600
- .split(';')
601
- .map(s => s.trim().toLowerCase())
602
- .filter(s => s),
603
- importance: getDataValue('d5') || getDataValue('importance') ? parseFloat(getDataValue('d5') || getDataValue('importance') || '0') : undefined,
604
- };
605
- entities.push(entity);
606
- }
607
- const edgeRegex = /<edge\s+[^>]*source="([^"]+)"\s+target="([^"]+)"[^>]*>([\s\S]*?)<\/edge>/g;
608
- let edgeMatch;
609
- while ((edgeMatch = edgeRegex.exec(data)) !== null) {
610
- const source = edgeMatch[1];
611
- const target = edgeMatch[2];
612
- const edgeContent = edgeMatch[3];
613
- const getDataValue = (key) => {
614
- const dataRegex = new RegExp(`<data\\s+key="${key}">([^<]*)<\/data>`);
615
- const match = dataRegex.exec(edgeContent);
616
- return match ? match[1] : undefined;
617
- };
618
- const relation = {
619
- from: source,
620
- to: target,
621
- relationType: getDataValue('e0') || getDataValue('relationType') || 'related_to',
622
- createdAt: getDataValue('e1') || getDataValue('createdAt'),
623
- lastModified: getDataValue('e2') || getDataValue('lastModified'),
624
- };
625
- relations.push(relation);
626
- }
627
- return { entities, relations };
628
- }
629
- async mergeImportedGraph(importedGraph, mergeStrategy, dryRun) {
630
- const existingGraph = await this.storage.getGraphForMutation();
631
- const result = {
632
- entitiesAdded: 0,
633
- entitiesSkipped: 0,
634
- entitiesUpdated: 0,
635
- relationsAdded: 0,
636
- relationsSkipped: 0,
637
- errors: [],
638
- };
639
- const existingEntitiesMap = new Map();
640
- for (const entity of existingGraph.entities) {
641
- existingEntitiesMap.set(entity.name, entity);
642
- }
643
- const existingRelationsSet = new Set();
644
- for (const relation of existingGraph.relations) {
645
- existingRelationsSet.add(`${relation.from}|${relation.to}|${relation.relationType}`);
646
- }
647
- for (const importedEntity of importedGraph.entities) {
648
- const existing = existingEntitiesMap.get(importedEntity.name);
649
- if (!existing) {
650
- result.entitiesAdded++;
651
- if (!dryRun) {
652
- existingGraph.entities.push(importedEntity);
653
- existingEntitiesMap.set(importedEntity.name, importedEntity);
654
- }
655
- }
656
- else {
657
- switch (mergeStrategy) {
658
- case 'replace':
659
- result.entitiesUpdated++;
660
- if (!dryRun) {
661
- Object.assign(existing, importedEntity);
662
- }
663
- break;
664
- case 'skip':
665
- result.entitiesSkipped++;
666
- break;
667
- case 'merge':
668
- result.entitiesUpdated++;
669
- if (!dryRun) {
670
- existing.observations = [
671
- ...new Set([...existing.observations, ...importedEntity.observations]),
672
- ];
673
- if (importedEntity.tags) {
674
- existing.tags = existing.tags || [];
675
- existing.tags = [...new Set([...existing.tags, ...importedEntity.tags])];
676
- }
677
- if (importedEntity.importance !== undefined) {
678
- existing.importance = importedEntity.importance;
679
- }
680
- existing.lastModified = new Date().toISOString();
681
- }
682
- break;
683
- case 'fail':
684
- result.errors.push(`Entity "${importedEntity.name}" already exists`);
685
- break;
686
- }
687
- }
688
- }
689
- for (const importedRelation of importedGraph.relations) {
690
- const relationKey = `${importedRelation.from}|${importedRelation.to}|${importedRelation.relationType}`;
691
- if (!existingEntitiesMap.has(importedRelation.from)) {
692
- result.errors.push(`Relation source entity "${importedRelation.from}" does not exist`);
693
- continue;
694
- }
695
- if (!existingEntitiesMap.has(importedRelation.to)) {
696
- result.errors.push(`Relation target entity "${importedRelation.to}" does not exist`);
697
- continue;
698
- }
699
- if (!existingRelationsSet.has(relationKey)) {
700
- result.relationsAdded++;
701
- if (!dryRun) {
702
- existingGraph.relations.push(importedRelation);
703
- existingRelationsSet.add(relationKey);
704
- }
705
- }
706
- else {
707
- if (mergeStrategy === 'fail') {
708
- result.errors.push(`Relation "${relationKey}" already exists`);
709
- }
710
- else {
711
- result.relationsSkipped++;
712
- }
713
- }
714
- }
715
- if (!dryRun && (mergeStrategy !== 'fail' || result.errors.length === 0)) {
716
- await this.storage.saveGraph(existingGraph);
717
- }
718
- return result;
719
- }
720
- // ============================================================
721
- // BACKUP OPERATIONS
722
- // ============================================================
723
- /**
724
- * Ensure backup directory exists.
725
- */
726
- async ensureBackupDir() {
727
- try {
728
- await fs.mkdir(this.backupDir, { recursive: true });
729
- }
730
- catch (error) {
731
- throw new FileOperationError('create backup directory', this.backupDir, error);
732
- }
733
- }
734
- /**
735
- * Generate backup file name with timestamp.
736
- * @param compressed - Whether the backup will be compressed (affects extension)
737
- */
738
- generateBackupFileName(compressed = true) {
739
- const now = new Date();
740
- const timestamp = now.toISOString()
741
- .replace(/:/g, '-')
742
- .replace(/\./g, '-')
743
- .replace('T', '_')
744
- .replace('Z', '');
745
- const extension = compressed ? '.jsonl.br' : '.jsonl';
746
- return `backup_${timestamp}${extension}`;
747
- }
748
- /**
749
- * Create a backup of the current knowledge graph.
750
- *
751
- * By default, backups are compressed with brotli for 50-70% space reduction.
752
- * Use `options.compress = false` to create uncompressed backups.
753
- *
754
- * @param options - Backup options (compress, description) or legacy description string
755
- * @returns Promise resolving to BackupResult with compression statistics
756
- *
757
- * @example
758
- * ```typescript
759
- * // Compressed backup (default)
760
- * const result = await manager.createBackup({ description: 'Pre-migration backup' });
761
- * console.log(`Compressed from ${result.originalSize} to ${result.compressedSize} bytes`);
762
- *
763
- * // Uncompressed backup
764
- * const result = await manager.createBackup({ compress: false });
765
- * ```
766
- */
767
- async createBackup(options) {
768
- await this.ensureBackupDir();
769
- // Handle legacy string argument (backward compatibility)
770
- const opts = typeof options === 'string'
771
- ? { description: options, compress: COMPRESSION_CONFIG.AUTO_COMPRESS_BACKUP }
772
- : { compress: COMPRESSION_CONFIG.AUTO_COMPRESS_BACKUP, ...options };
773
- const shouldCompress = opts.compress ?? COMPRESSION_CONFIG.AUTO_COMPRESS_BACKUP;
774
- const graph = await this.storage.loadGraph();
775
- const timestamp = new Date().toISOString();
776
- const fileName = this.generateBackupFileName(shouldCompress);
777
- const backupPath = join(this.backupDir, fileName);
778
- try {
779
- const originalPath = this.storage.getFilePath();
780
- let fileContent;
781
- try {
782
- fileContent = await fs.readFile(originalPath, 'utf-8');
783
- }
784
- catch {
785
- // If file doesn't exist, generate content from graph
786
- const lines = [
787
- ...graph.entities.map(e => JSON.stringify({ type: 'entity', ...e })),
788
- ...graph.relations.map(r => JSON.stringify({ type: 'relation', ...r })),
789
- ];
790
- fileContent = lines.join('\n');
791
- }
792
- const originalSize = Buffer.byteLength(fileContent, 'utf-8');
793
- let compressedSize = originalSize;
794
- let compressionRatio = 1;
795
- if (shouldCompress) {
796
- // Compress with maximum quality for backups (archive quality)
797
- const compressionResult = await compress(fileContent, {
798
- quality: COMPRESSION_CONFIG.BROTLI_QUALITY_ARCHIVE,
799
- mode: 'text',
800
- });
801
- await fs.writeFile(backupPath, compressionResult.compressed);
802
- compressedSize = compressionResult.compressedSize;
803
- compressionRatio = compressionResult.ratio;
804
- }
805
- else {
806
- // Write uncompressed backup
807
- await fs.writeFile(backupPath, fileContent);
808
- }
809
- const stats = await fs.stat(backupPath);
810
- const metadata = {
811
- timestamp,
812
- entityCount: graph.entities.length,
813
- relationCount: graph.relations.length,
814
- fileSize: stats.size,
815
- description: opts.description,
816
- compressed: shouldCompress,
817
- originalSize,
818
- compressionRatio: shouldCompress ? compressionRatio : undefined,
819
- compressionFormat: shouldCompress ? 'brotli' : 'none',
820
- };
821
- const metadataPath = `${backupPath}.meta.json`;
822
- await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
823
- return {
824
- path: backupPath,
825
- timestamp,
826
- entityCount: graph.entities.length,
827
- relationCount: graph.relations.length,
828
- compressed: shouldCompress,
829
- originalSize,
830
- compressedSize,
831
- compressionRatio,
832
- description: opts.description,
833
- };
834
- }
835
- catch (error) {
836
- throw new FileOperationError('create backup', backupPath, error);
837
- }
838
- }
839
- /**
840
- * List all available backups, sorted by timestamp (newest first).
841
- *
842
- * Detects both compressed (.jsonl.br) and uncompressed (.jsonl) backups.
843
- *
844
- * @returns Promise resolving to array of backup information with compression details
845
- */
846
- async listBackups() {
847
- try {
848
- try {
849
- await fs.access(this.backupDir);
850
- }
851
- catch {
852
- return [];
853
- }
854
- const files = await fs.readdir(this.backupDir);
855
- // Match both .jsonl and .jsonl.br backup files, exclude metadata files
856
- const backupFiles = files.filter(f => f.startsWith('backup_') &&
857
- (f.endsWith('.jsonl') || f.endsWith('.jsonl.br')) &&
858
- !f.endsWith('.meta.json'));
859
- const backups = [];
860
- for (const fileName of backupFiles) {
861
- const filePath = join(this.backupDir, fileName);
862
- const isCompressed = hasBrotliExtension(fileName);
863
- // Try to read metadata file (handles both .jsonl.meta.json and .jsonl.br.meta.json)
864
- const metadataPath = `${filePath}.meta.json`;
865
- try {
866
- const [metadataContent, stats] = await Promise.all([
867
- fs.readFile(metadataPath, 'utf-8'),
868
- fs.stat(filePath),
869
- ]);
870
- const metadata = JSON.parse(metadataContent);
871
- // Ensure compression fields are present (backward compatibility)
872
- if (metadata.compressed === undefined) {
873
- metadata.compressed = isCompressed;
874
- }
875
- if (metadata.compressionFormat === undefined) {
876
- metadata.compressionFormat = isCompressed ? 'brotli' : 'none';
877
- }
878
- backups.push({
879
- fileName,
880
- filePath,
881
- metadata,
882
- compressed: isCompressed,
883
- size: stats.size,
884
- });
885
- }
886
- catch {
887
- // Skip backups without valid metadata
888
- continue;
889
- }
890
- }
891
- backups.sort((a, b) => new Date(b.metadata.timestamp).getTime() - new Date(a.metadata.timestamp).getTime());
892
- return backups;
893
- }
894
- catch (error) {
895
- throw new FileOperationError('list backups', this.backupDir, error);
896
- }
897
- }
898
- /**
899
- * Restore the knowledge graph from a backup file.
900
- *
901
- * Automatically detects and decompresses brotli-compressed backups (.br extension).
902
- * Maintains backward compatibility with uncompressed backups.
903
- *
904
- * @param backupPath - Path to the backup file to restore from
905
- * @returns Promise resolving to RestoreResult with restoration details
906
- *
907
- * @example
908
- * ```typescript
909
- * // Restore from compressed backup
910
- * const result = await manager.restoreFromBackup('/path/to/backup.jsonl.br');
911
- * console.log(`Restored ${result.entityCount} entities from compressed backup`);
912
- *
913
- * // Restore from uncompressed backup (legacy)
914
- * const result = await manager.restoreFromBackup('/path/to/backup.jsonl');
915
- * ```
916
- */
917
- async restoreFromBackup(backupPath) {
918
- try {
919
- await fs.access(backupPath);
920
- const isCompressed = hasBrotliExtension(backupPath);
921
- const backupBuffer = await fs.readFile(backupPath);
922
- let backupContent;
923
- if (isCompressed) {
924
- // Decompress the backup
925
- const decompressedBuffer = await decompress(backupBuffer);
926
- backupContent = decompressedBuffer.toString('utf-8');
927
- }
928
- else {
929
- // Read as plain text
930
- backupContent = backupBuffer.toString('utf-8');
931
- }
932
- const mainPath = this.storage.getFilePath();
933
- await fs.writeFile(mainPath, backupContent);
934
- this.storage.clearCache();
935
- // Load the restored graph to get counts
936
- const graph = await this.storage.loadGraph();
937
- return {
938
- entityCount: graph.entities.length,
939
- relationCount: graph.relations.length,
940
- restoredFrom: backupPath,
941
- wasCompressed: isCompressed,
942
- };
943
- }
944
- catch (error) {
945
- throw new FileOperationError('restore from backup', backupPath, error);
946
- }
947
- }
948
- /**
949
- * Delete a specific backup file.
950
- *
951
- * @param backupPath - Path to the backup file to delete
952
- */
953
- async deleteBackup(backupPath) {
954
- try {
955
- await fs.unlink(backupPath);
956
- try {
957
- await fs.unlink(`${backupPath}.meta.json`);
958
- }
959
- catch {
960
- // Metadata file doesn't exist - that's ok
961
- }
962
- }
963
- catch (error) {
964
- throw new FileOperationError('delete backup', backupPath, error);
965
- }
966
- }
967
- /**
968
- * Clean old backups, keeping only the most recent N backups.
969
- *
970
- * @param keepCount - Number of recent backups to keep (default: 10)
971
- * @returns Promise resolving to number of backups deleted
972
- */
973
- async cleanOldBackups(keepCount = 10) {
974
- const backups = await this.listBackups();
975
- if (backups.length <= keepCount) {
976
- return 0;
977
- }
978
- const backupsToDelete = backups.slice(keepCount);
979
- let deletedCount = 0;
980
- for (const backup of backupsToDelete) {
981
- try {
982
- await this.deleteBackup(backup.filePath);
983
- deletedCount++;
984
- }
985
- catch {
986
- continue;
987
- }
988
- }
989
- return deletedCount;
990
- }
991
- /**
992
- * Get the path to the backup directory.
993
- */
994
- getBackupDir() {
995
- return this.backupDir;
996
- }
997
- }