@danielsimonjr/memory-mcp 0.7.1 → 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 -997
- 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,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compression Manager
|
|
3
|
+
*
|
|
4
|
+
* Detects and merges duplicate entities to compress the knowledge graph.
|
|
5
|
+
*
|
|
6
|
+
* @module features/CompressionManager
|
|
7
|
+
*/
|
|
8
|
+
import { levenshteinDistance } from '../utils/levenshtein.js';
|
|
9
|
+
import { EntityNotFoundError, InsufficientEntitiesError } from '../utils/errors.js';
|
|
10
|
+
/**
|
|
11
|
+
* Default threshold for duplicate detection (80% similarity).
|
|
12
|
+
*/
|
|
13
|
+
export const DEFAULT_DUPLICATE_THRESHOLD = 0.8;
|
|
14
|
+
/**
|
|
15
|
+
* Similarity scoring weights for entity comparison.
|
|
16
|
+
* These values determine the relative importance of each factor when calculating
|
|
17
|
+
* entity similarity. Higher weights give more importance to that factor.
|
|
18
|
+
* Total weights must sum to 1.0 (100%).
|
|
19
|
+
*/
|
|
20
|
+
export const SIMILARITY_WEIGHTS = {
|
|
21
|
+
/** Weight for name similarity using Levenshtein distance (40%) */
|
|
22
|
+
NAME: 0.4,
|
|
23
|
+
/** Weight for exact entity type match (20%) */
|
|
24
|
+
TYPE: 0.2,
|
|
25
|
+
/** Weight for observation overlap using Jaccard similarity (30%) */
|
|
26
|
+
OBSERVATIONS: 0.3,
|
|
27
|
+
/** Weight for tag overlap using Jaccard similarity (10%) */
|
|
28
|
+
TAGS: 0.1,
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Manages graph compression through duplicate detection and merging.
|
|
32
|
+
*/
|
|
33
|
+
export class CompressionManager {
|
|
34
|
+
storage;
|
|
35
|
+
constructor(storage) {
|
|
36
|
+
this.storage = storage;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Calculate similarity between two entities using multiple heuristics.
|
|
40
|
+
*
|
|
41
|
+
* Uses configurable weights defined in SIMILARITY_WEIGHTS constant.
|
|
42
|
+
* See SIMILARITY_WEIGHTS for the breakdown of scoring factors.
|
|
43
|
+
*
|
|
44
|
+
* @param e1 - First entity
|
|
45
|
+
* @param e2 - Second entity
|
|
46
|
+
* @returns Similarity score from 0 (completely different) to 1 (identical)
|
|
47
|
+
*/
|
|
48
|
+
calculateEntitySimilarity(e1, e2) {
|
|
49
|
+
let score = 0;
|
|
50
|
+
let factors = 0;
|
|
51
|
+
// Name similarity (Levenshtein-based)
|
|
52
|
+
const nameDistance = levenshteinDistance(e1.name.toLowerCase(), e2.name.toLowerCase());
|
|
53
|
+
const maxNameLength = Math.max(e1.name.length, e2.name.length);
|
|
54
|
+
const nameSimilarity = 1 - nameDistance / maxNameLength;
|
|
55
|
+
score += nameSimilarity * SIMILARITY_WEIGHTS.NAME;
|
|
56
|
+
factors += SIMILARITY_WEIGHTS.NAME;
|
|
57
|
+
// Type similarity (exact match)
|
|
58
|
+
if (e1.entityType.toLowerCase() === e2.entityType.toLowerCase()) {
|
|
59
|
+
score += SIMILARITY_WEIGHTS.TYPE;
|
|
60
|
+
}
|
|
61
|
+
factors += SIMILARITY_WEIGHTS.TYPE;
|
|
62
|
+
// Observation overlap (Jaccard similarity)
|
|
63
|
+
const obs1Set = new Set(e1.observations.map(o => o.toLowerCase()));
|
|
64
|
+
const obs2Set = new Set(e2.observations.map(o => o.toLowerCase()));
|
|
65
|
+
const intersection = new Set([...obs1Set].filter(x => obs2Set.has(x)));
|
|
66
|
+
const union = new Set([...obs1Set, ...obs2Set]);
|
|
67
|
+
const observationSimilarity = union.size > 0 ? intersection.size / union.size : 0;
|
|
68
|
+
score += observationSimilarity * SIMILARITY_WEIGHTS.OBSERVATIONS;
|
|
69
|
+
factors += SIMILARITY_WEIGHTS.OBSERVATIONS;
|
|
70
|
+
// Tag overlap (Jaccard similarity)
|
|
71
|
+
if (e1.tags && e2.tags && (e1.tags.length > 0 || e2.tags.length > 0)) {
|
|
72
|
+
const tags1Set = new Set(e1.tags.map(t => t.toLowerCase()));
|
|
73
|
+
const tags2Set = new Set(e2.tags.map(t => t.toLowerCase()));
|
|
74
|
+
const tagIntersection = new Set([...tags1Set].filter(x => tags2Set.has(x)));
|
|
75
|
+
const tagUnion = new Set([...tags1Set, ...tags2Set]);
|
|
76
|
+
const tagSimilarity = tagUnion.size > 0 ? tagIntersection.size / tagUnion.size : 0;
|
|
77
|
+
score += tagSimilarity * SIMILARITY_WEIGHTS.TAGS;
|
|
78
|
+
factors += SIMILARITY_WEIGHTS.TAGS;
|
|
79
|
+
}
|
|
80
|
+
return factors > 0 ? score / factors : 0;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Find duplicate entities in the graph based on similarity threshold.
|
|
84
|
+
*
|
|
85
|
+
* OPTIMIZED: Uses bucketing strategies to reduce O(n²) comparisons:
|
|
86
|
+
* 1. Buckets entities by entityType (only compare same types)
|
|
87
|
+
* 2. Within each type, buckets by name prefix (first 2 chars normalized)
|
|
88
|
+
* 3. Only compares entities within same or adjacent buckets
|
|
89
|
+
*
|
|
90
|
+
* Complexity: O(n·k) where k is average bucket size (typically << n)
|
|
91
|
+
*
|
|
92
|
+
* @param threshold - Similarity threshold (0.0 to 1.0), default DEFAULT_DUPLICATE_THRESHOLD
|
|
93
|
+
* @returns Array of duplicate groups (each group has similar entities)
|
|
94
|
+
*/
|
|
95
|
+
async findDuplicates(threshold = DEFAULT_DUPLICATE_THRESHOLD) {
|
|
96
|
+
const graph = await this.storage.loadGraph();
|
|
97
|
+
const duplicateGroups = [];
|
|
98
|
+
const processed = new Set();
|
|
99
|
+
// Step 1: Bucket entities by type (reduces comparisons drastically)
|
|
100
|
+
const typeMap = new Map();
|
|
101
|
+
for (const entity of graph.entities) {
|
|
102
|
+
const normalizedType = entity.entityType.toLowerCase();
|
|
103
|
+
if (!typeMap.has(normalizedType)) {
|
|
104
|
+
typeMap.set(normalizedType, []);
|
|
105
|
+
}
|
|
106
|
+
typeMap.get(normalizedType).push(entity);
|
|
107
|
+
}
|
|
108
|
+
// Step 2: For each type bucket, sub-bucket by name prefix
|
|
109
|
+
for (const entities of typeMap.values()) {
|
|
110
|
+
// Skip single-entity types (no duplicates possible)
|
|
111
|
+
if (entities.length < 2)
|
|
112
|
+
continue;
|
|
113
|
+
// Create name prefix buckets (first 2 chars, normalized)
|
|
114
|
+
const prefixMap = new Map();
|
|
115
|
+
for (const entity of entities) {
|
|
116
|
+
const prefix = entity.name.toLowerCase().slice(0, 2);
|
|
117
|
+
if (!prefixMap.has(prefix)) {
|
|
118
|
+
prefixMap.set(prefix, []);
|
|
119
|
+
}
|
|
120
|
+
prefixMap.get(prefix).push(entity);
|
|
121
|
+
}
|
|
122
|
+
// Step 3: Compare only within buckets (or adjacent buckets for fuzzy matching)
|
|
123
|
+
const prefixKeys = Array.from(prefixMap.keys()).sort();
|
|
124
|
+
for (let bucketIdx = 0; bucketIdx < prefixKeys.length; bucketIdx++) {
|
|
125
|
+
const currentPrefix = prefixKeys[bucketIdx];
|
|
126
|
+
const currentBucket = prefixMap.get(currentPrefix);
|
|
127
|
+
// Collect entities to compare: current bucket + adjacent buckets
|
|
128
|
+
const candidateEntities = [...currentBucket];
|
|
129
|
+
// Add next bucket if exists (handles fuzzy prefix matching)
|
|
130
|
+
if (bucketIdx + 1 < prefixKeys.length) {
|
|
131
|
+
candidateEntities.push(...prefixMap.get(prefixKeys[bucketIdx + 1]));
|
|
132
|
+
}
|
|
133
|
+
// Compare entities within candidate pool
|
|
134
|
+
for (let i = 0; i < currentBucket.length; i++) {
|
|
135
|
+
const entity1 = currentBucket[i];
|
|
136
|
+
if (processed.has(entity1.name))
|
|
137
|
+
continue;
|
|
138
|
+
const group = [entity1.name];
|
|
139
|
+
for (let j = 0; j < candidateEntities.length; j++) {
|
|
140
|
+
const entity2 = candidateEntities[j];
|
|
141
|
+
if (entity1.name === entity2.name || processed.has(entity2.name))
|
|
142
|
+
continue;
|
|
143
|
+
const similarity = this.calculateEntitySimilarity(entity1, entity2);
|
|
144
|
+
if (similarity >= threshold) {
|
|
145
|
+
group.push(entity2.name);
|
|
146
|
+
processed.add(entity2.name);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (group.length > 1) {
|
|
150
|
+
duplicateGroups.push(group);
|
|
151
|
+
processed.add(entity1.name);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return duplicateGroups;
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Merge a group of entities into a single entity.
|
|
160
|
+
*
|
|
161
|
+
* Merging strategy:
|
|
162
|
+
* - First entity is kept (or renamed to targetName)
|
|
163
|
+
* - Observations: Union of all observations
|
|
164
|
+
* - Tags: Union of all tags
|
|
165
|
+
* - Importance: Maximum importance value
|
|
166
|
+
* - createdAt: Earliest date
|
|
167
|
+
* - lastModified: Current timestamp
|
|
168
|
+
* - Relations: Redirected to kept entity, duplicates removed
|
|
169
|
+
*
|
|
170
|
+
* @param entityNames - Names of entities to merge (first one is kept)
|
|
171
|
+
* @param targetName - Optional new name for merged entity (default: first entity name)
|
|
172
|
+
* @returns The merged entity
|
|
173
|
+
* @throws {InsufficientEntitiesError} If less than 2 entities provided
|
|
174
|
+
* @throws {EntityNotFoundError} If any entity not found
|
|
175
|
+
*/
|
|
176
|
+
async mergeEntities(entityNames, targetName) {
|
|
177
|
+
if (entityNames.length < 2) {
|
|
178
|
+
throw new InsufficientEntitiesError('merging', 2, entityNames.length);
|
|
179
|
+
}
|
|
180
|
+
const graph = await this.storage.loadGraph();
|
|
181
|
+
const entitiesToMerge = entityNames.map(name => {
|
|
182
|
+
const entity = graph.entities.find(e => e.name === name);
|
|
183
|
+
if (!entity) {
|
|
184
|
+
throw new EntityNotFoundError(name);
|
|
185
|
+
}
|
|
186
|
+
return entity;
|
|
187
|
+
});
|
|
188
|
+
const keepEntity = entitiesToMerge[0];
|
|
189
|
+
const mergeEntities = entitiesToMerge.slice(1);
|
|
190
|
+
// Merge observations (unique)
|
|
191
|
+
const allObservations = new Set();
|
|
192
|
+
for (const entity of entitiesToMerge) {
|
|
193
|
+
entity.observations.forEach(obs => allObservations.add(obs));
|
|
194
|
+
}
|
|
195
|
+
keepEntity.observations = Array.from(allObservations);
|
|
196
|
+
// Merge tags (unique)
|
|
197
|
+
const allTags = new Set();
|
|
198
|
+
for (const entity of entitiesToMerge) {
|
|
199
|
+
if (entity.tags) {
|
|
200
|
+
entity.tags.forEach(tag => allTags.add(tag));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (allTags.size > 0) {
|
|
204
|
+
keepEntity.tags = Array.from(allTags);
|
|
205
|
+
}
|
|
206
|
+
// Use highest importance
|
|
207
|
+
const importances = entitiesToMerge
|
|
208
|
+
.map(e => e.importance)
|
|
209
|
+
.filter(imp => imp !== undefined);
|
|
210
|
+
if (importances.length > 0) {
|
|
211
|
+
keepEntity.importance = Math.max(...importances);
|
|
212
|
+
}
|
|
213
|
+
// Use earliest createdAt
|
|
214
|
+
const createdDates = entitiesToMerge
|
|
215
|
+
.map(e => e.createdAt)
|
|
216
|
+
.filter(date => date !== undefined);
|
|
217
|
+
if (createdDates.length > 0) {
|
|
218
|
+
keepEntity.createdAt = createdDates.sort()[0];
|
|
219
|
+
}
|
|
220
|
+
// Update lastModified
|
|
221
|
+
keepEntity.lastModified = new Date().toISOString();
|
|
222
|
+
// Rename if requested
|
|
223
|
+
if (targetName && targetName !== keepEntity.name) {
|
|
224
|
+
// Update all relations pointing to old name
|
|
225
|
+
graph.relations.forEach(rel => {
|
|
226
|
+
if (rel.from === keepEntity.name)
|
|
227
|
+
rel.from = targetName;
|
|
228
|
+
if (rel.to === keepEntity.name)
|
|
229
|
+
rel.to = targetName;
|
|
230
|
+
});
|
|
231
|
+
keepEntity.name = targetName;
|
|
232
|
+
}
|
|
233
|
+
// Update relations from merged entities to point to kept entity
|
|
234
|
+
for (const mergeEntity of mergeEntities) {
|
|
235
|
+
graph.relations.forEach(rel => {
|
|
236
|
+
if (rel.from === mergeEntity.name)
|
|
237
|
+
rel.from = keepEntity.name;
|
|
238
|
+
if (rel.to === mergeEntity.name)
|
|
239
|
+
rel.to = keepEntity.name;
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
// Remove duplicate relations
|
|
243
|
+
const uniqueRelations = new Map();
|
|
244
|
+
for (const relation of graph.relations) {
|
|
245
|
+
const key = `${relation.from}|${relation.to}|${relation.relationType}`;
|
|
246
|
+
if (!uniqueRelations.has(key)) {
|
|
247
|
+
uniqueRelations.set(key, relation);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
graph.relations = Array.from(uniqueRelations.values());
|
|
251
|
+
// Remove merged entities
|
|
252
|
+
const mergeNames = new Set(mergeEntities.map(e => e.name));
|
|
253
|
+
graph.entities = graph.entities.filter(e => !mergeNames.has(e.name));
|
|
254
|
+
await this.storage.saveGraph(graph);
|
|
255
|
+
return keepEntity;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Compress the knowledge graph by finding and merging duplicates.
|
|
259
|
+
*
|
|
260
|
+
* @param threshold - Similarity threshold for duplicate detection (0.0 to 1.0), default DEFAULT_DUPLICATE_THRESHOLD
|
|
261
|
+
* @param dryRun - If true, only report what would be compressed without applying changes
|
|
262
|
+
* @returns Compression result with statistics
|
|
263
|
+
*/
|
|
264
|
+
async compressGraph(threshold = DEFAULT_DUPLICATE_THRESHOLD, dryRun = false) {
|
|
265
|
+
const initialGraph = await this.storage.loadGraph();
|
|
266
|
+
const initialSize = JSON.stringify(initialGraph).length;
|
|
267
|
+
const duplicateGroups = await this.findDuplicates(threshold);
|
|
268
|
+
const result = {
|
|
269
|
+
duplicatesFound: duplicateGroups.reduce((sum, group) => sum + group.length, 0),
|
|
270
|
+
entitiesMerged: 0,
|
|
271
|
+
observationsCompressed: 0,
|
|
272
|
+
relationsConsolidated: 0,
|
|
273
|
+
spaceFreed: 0,
|
|
274
|
+
mergedEntities: [],
|
|
275
|
+
};
|
|
276
|
+
if (dryRun) {
|
|
277
|
+
// Just report what would happen
|
|
278
|
+
for (const group of duplicateGroups) {
|
|
279
|
+
result.mergedEntities.push({
|
|
280
|
+
kept: group[0],
|
|
281
|
+
merged: group.slice(1),
|
|
282
|
+
});
|
|
283
|
+
result.entitiesMerged += group.length - 1;
|
|
284
|
+
}
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
// Actually merge duplicates
|
|
288
|
+
for (const group of duplicateGroups) {
|
|
289
|
+
try {
|
|
290
|
+
await this.mergeEntities(group);
|
|
291
|
+
result.mergedEntities.push({
|
|
292
|
+
kept: group[0],
|
|
293
|
+
merged: group.slice(1),
|
|
294
|
+
});
|
|
295
|
+
result.entitiesMerged += group.length - 1;
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
// Skip groups that fail to merge
|
|
299
|
+
console.error(`Failed to merge group ${group}:`, error);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// Calculate space saved
|
|
303
|
+
const finalGraph = await this.storage.loadGraph();
|
|
304
|
+
const finalSize = JSON.stringify(finalGraph).length;
|
|
305
|
+
result.spaceFreed = initialSize - finalSize;
|
|
306
|
+
// Count compressed observations (approximation)
|
|
307
|
+
result.observationsCompressed = result.entitiesMerged;
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export Manager
|
|
3
|
+
*
|
|
4
|
+
* Exports knowledge graphs to various formats (JSON, CSV, GraphML, GEXF, DOT, Markdown, Mermaid).
|
|
5
|
+
*
|
|
6
|
+
* @module features/ExportManager
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Manages knowledge graph exports to multiple formats.
|
|
10
|
+
*/
|
|
11
|
+
export class ExportManager {
|
|
12
|
+
/**
|
|
13
|
+
* Export graph to specified format.
|
|
14
|
+
*
|
|
15
|
+
* @param graph - Knowledge graph to export
|
|
16
|
+
* @param format - Export format
|
|
17
|
+
* @returns Formatted export string
|
|
18
|
+
*/
|
|
19
|
+
exportGraph(graph, format) {
|
|
20
|
+
switch (format) {
|
|
21
|
+
case 'json':
|
|
22
|
+
return this.exportAsJson(graph);
|
|
23
|
+
case 'csv':
|
|
24
|
+
return this.exportAsCsv(graph);
|
|
25
|
+
case 'graphml':
|
|
26
|
+
return this.exportAsGraphML(graph);
|
|
27
|
+
case 'gexf':
|
|
28
|
+
return this.exportAsGEXF(graph);
|
|
29
|
+
case 'dot':
|
|
30
|
+
return this.exportAsDOT(graph);
|
|
31
|
+
case 'markdown':
|
|
32
|
+
return this.exportAsMarkdown(graph);
|
|
33
|
+
case 'mermaid':
|
|
34
|
+
return this.exportAsMermaid(graph);
|
|
35
|
+
default:
|
|
36
|
+
throw new Error(`Unsupported export format: ${format}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Export as pretty-printed JSON.
|
|
41
|
+
*/
|
|
42
|
+
exportAsJson(graph) {
|
|
43
|
+
return JSON.stringify(graph, null, 2);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Export as CSV with proper escaping.
|
|
47
|
+
*/
|
|
48
|
+
exportAsCsv(graph) {
|
|
49
|
+
const lines = [];
|
|
50
|
+
const escapeCsvField = (field) => {
|
|
51
|
+
if (field === undefined || field === null)
|
|
52
|
+
return '';
|
|
53
|
+
const str = String(field);
|
|
54
|
+
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
55
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
56
|
+
}
|
|
57
|
+
return str;
|
|
58
|
+
};
|
|
59
|
+
// Entities section
|
|
60
|
+
lines.push('# ENTITIES');
|
|
61
|
+
lines.push('name,entityType,observations,createdAt,lastModified,tags,importance');
|
|
62
|
+
for (const entity of graph.entities) {
|
|
63
|
+
const observationsStr = entity.observations.join('; ');
|
|
64
|
+
const tagsStr = entity.tags ? entity.tags.join('; ') : '';
|
|
65
|
+
const importanceStr = entity.importance !== undefined ? String(entity.importance) : '';
|
|
66
|
+
lines.push([
|
|
67
|
+
escapeCsvField(entity.name),
|
|
68
|
+
escapeCsvField(entity.entityType),
|
|
69
|
+
escapeCsvField(observationsStr),
|
|
70
|
+
escapeCsvField(entity.createdAt),
|
|
71
|
+
escapeCsvField(entity.lastModified),
|
|
72
|
+
escapeCsvField(tagsStr),
|
|
73
|
+
escapeCsvField(importanceStr),
|
|
74
|
+
].join(','));
|
|
75
|
+
}
|
|
76
|
+
// Relations section
|
|
77
|
+
lines.push('');
|
|
78
|
+
lines.push('# RELATIONS');
|
|
79
|
+
lines.push('from,to,relationType,createdAt,lastModified');
|
|
80
|
+
for (const relation of graph.relations) {
|
|
81
|
+
lines.push([
|
|
82
|
+
escapeCsvField(relation.from),
|
|
83
|
+
escapeCsvField(relation.to),
|
|
84
|
+
escapeCsvField(relation.relationType),
|
|
85
|
+
escapeCsvField(relation.createdAt),
|
|
86
|
+
escapeCsvField(relation.lastModified),
|
|
87
|
+
].join(','));
|
|
88
|
+
}
|
|
89
|
+
return lines.join('\n');
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Export as GraphML XML format.
|
|
93
|
+
*/
|
|
94
|
+
exportAsGraphML(graph) {
|
|
95
|
+
const lines = [];
|
|
96
|
+
const escapeXml = (str) => {
|
|
97
|
+
if (str === undefined || str === null)
|
|
98
|
+
return '';
|
|
99
|
+
return String(str)
|
|
100
|
+
.replace(/&/g, '&')
|
|
101
|
+
.replace(/</g, '<')
|
|
102
|
+
.replace(/>/g, '>')
|
|
103
|
+
.replace(/"/g, '"')
|
|
104
|
+
.replace(/'/g, ''');
|
|
105
|
+
};
|
|
106
|
+
lines.push('<?xml version="1.0" encoding="UTF-8"?>');
|
|
107
|
+
lines.push('<graphml xmlns="http://graphml.graphdrawing.org/xmlns">');
|
|
108
|
+
lines.push(' <key id="d0" for="node" attr.name="entityType" attr.type="string"/>');
|
|
109
|
+
lines.push(' <key id="d1" for="node" attr.name="observations" attr.type="string"/>');
|
|
110
|
+
lines.push(' <key id="d2" for="node" attr.name="createdAt" attr.type="string"/>');
|
|
111
|
+
lines.push(' <key id="d3" for="node" attr.name="lastModified" attr.type="string"/>');
|
|
112
|
+
lines.push(' <key id="d4" for="node" attr.name="tags" attr.type="string"/>');
|
|
113
|
+
lines.push(' <key id="d5" for="node" attr.name="importance" attr.type="double"/>');
|
|
114
|
+
lines.push(' <key id="e0" for="edge" attr.name="relationType" attr.type="string"/>');
|
|
115
|
+
lines.push(' <key id="e1" for="edge" attr.name="createdAt" attr.type="string"/>');
|
|
116
|
+
lines.push(' <key id="e2" for="edge" attr.name="lastModified" attr.type="string"/>');
|
|
117
|
+
lines.push(' <graph id="G" edgedefault="directed">');
|
|
118
|
+
// Nodes
|
|
119
|
+
for (const entity of graph.entities) {
|
|
120
|
+
const nodeId = escapeXml(entity.name);
|
|
121
|
+
lines.push(` <node id="${nodeId}">`);
|
|
122
|
+
lines.push(` <data key="d0">${escapeXml(entity.entityType)}</data>`);
|
|
123
|
+
lines.push(` <data key="d1">${escapeXml(entity.observations.join('; '))}</data>`);
|
|
124
|
+
if (entity.createdAt)
|
|
125
|
+
lines.push(` <data key="d2">${escapeXml(entity.createdAt)}</data>`);
|
|
126
|
+
if (entity.lastModified)
|
|
127
|
+
lines.push(` <data key="d3">${escapeXml(entity.lastModified)}</data>`);
|
|
128
|
+
if (entity.tags?.length)
|
|
129
|
+
lines.push(` <data key="d4">${escapeXml(entity.tags.join('; '))}</data>`);
|
|
130
|
+
if (entity.importance !== undefined)
|
|
131
|
+
lines.push(` <data key="d5">${entity.importance}</data>`);
|
|
132
|
+
lines.push(' </node>');
|
|
133
|
+
}
|
|
134
|
+
// Edges
|
|
135
|
+
let edgeId = 0;
|
|
136
|
+
for (const relation of graph.relations) {
|
|
137
|
+
const sourceId = escapeXml(relation.from);
|
|
138
|
+
const targetId = escapeXml(relation.to);
|
|
139
|
+
lines.push(` <edge id="e${edgeId}" source="${sourceId}" target="${targetId}">`);
|
|
140
|
+
lines.push(` <data key="e0">${escapeXml(relation.relationType)}</data>`);
|
|
141
|
+
if (relation.createdAt)
|
|
142
|
+
lines.push(` <data key="e1">${escapeXml(relation.createdAt)}</data>`);
|
|
143
|
+
if (relation.lastModified)
|
|
144
|
+
lines.push(` <data key="e2">${escapeXml(relation.lastModified)}</data>`);
|
|
145
|
+
lines.push(' </edge>');
|
|
146
|
+
edgeId++;
|
|
147
|
+
}
|
|
148
|
+
lines.push(' </graph>');
|
|
149
|
+
lines.push('</graphml>');
|
|
150
|
+
return lines.join('\n');
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Export as GEXF format for Gephi.
|
|
154
|
+
*/
|
|
155
|
+
exportAsGEXF(graph) {
|
|
156
|
+
const lines = [];
|
|
157
|
+
const escapeXml = (str) => {
|
|
158
|
+
if (str === undefined || str === null)
|
|
159
|
+
return '';
|
|
160
|
+
return String(str)
|
|
161
|
+
.replace(/&/g, '&')
|
|
162
|
+
.replace(/</g, '<')
|
|
163
|
+
.replace(/>/g, '>')
|
|
164
|
+
.replace(/"/g, '"')
|
|
165
|
+
.replace(/'/g, ''');
|
|
166
|
+
};
|
|
167
|
+
lines.push('<?xml version="1.0" encoding="UTF-8"?>');
|
|
168
|
+
lines.push('<gexf xmlns="http://www.gexf.net/1.2draft" version="1.2">');
|
|
169
|
+
lines.push(' <meta>');
|
|
170
|
+
lines.push(' <creator>Memory MCP Server</creator>');
|
|
171
|
+
lines.push(' </meta>');
|
|
172
|
+
lines.push(' <graph mode="static" defaultedgetype="directed">');
|
|
173
|
+
lines.push(' <attributes class="node">');
|
|
174
|
+
lines.push(' <attribute id="0" title="entityType" type="string"/>');
|
|
175
|
+
lines.push(' <attribute id="1" title="observations" type="string"/>');
|
|
176
|
+
lines.push(' </attributes>');
|
|
177
|
+
lines.push(' <nodes>');
|
|
178
|
+
for (const entity of graph.entities) {
|
|
179
|
+
const nodeId = escapeXml(entity.name);
|
|
180
|
+
lines.push(` <node id="${nodeId}" label="${nodeId}">`);
|
|
181
|
+
lines.push(' <attvalues>');
|
|
182
|
+
lines.push(` <attvalue for="0" value="${escapeXml(entity.entityType)}"/>`);
|
|
183
|
+
lines.push(` <attvalue for="1" value="${escapeXml(entity.observations.join('; '))}"/>`);
|
|
184
|
+
lines.push(' </attvalues>');
|
|
185
|
+
lines.push(' </node>');
|
|
186
|
+
}
|
|
187
|
+
lines.push(' </nodes>');
|
|
188
|
+
lines.push(' <edges>');
|
|
189
|
+
let edgeId = 0;
|
|
190
|
+
for (const relation of graph.relations) {
|
|
191
|
+
const sourceId = escapeXml(relation.from);
|
|
192
|
+
const targetId = escapeXml(relation.to);
|
|
193
|
+
const label = escapeXml(relation.relationType);
|
|
194
|
+
lines.push(` <edge id="${edgeId}" source="${sourceId}" target="${targetId}" label="${label}"/>`);
|
|
195
|
+
edgeId++;
|
|
196
|
+
}
|
|
197
|
+
lines.push(' </edges>');
|
|
198
|
+
lines.push(' </graph>');
|
|
199
|
+
lines.push('</gexf>');
|
|
200
|
+
return lines.join('\n');
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Export as DOT format for GraphViz.
|
|
204
|
+
*/
|
|
205
|
+
exportAsDOT(graph) {
|
|
206
|
+
const lines = [];
|
|
207
|
+
const escapeDot = (str) => {
|
|
208
|
+
return '"' + str.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n') + '"';
|
|
209
|
+
};
|
|
210
|
+
lines.push('digraph KnowledgeGraph {');
|
|
211
|
+
lines.push(' rankdir=LR;');
|
|
212
|
+
lines.push(' node [shape=box, style=rounded];');
|
|
213
|
+
lines.push('');
|
|
214
|
+
for (const entity of graph.entities) {
|
|
215
|
+
const nodeId = escapeDot(entity.name);
|
|
216
|
+
const label = [`${entity.name}`, `Type: ${entity.entityType}`];
|
|
217
|
+
if (entity.tags?.length)
|
|
218
|
+
label.push(`Tags: ${entity.tags.join(', ')}`);
|
|
219
|
+
const labelStr = escapeDot(label.join('\\n'));
|
|
220
|
+
lines.push(` ${nodeId} [label=${labelStr}];`);
|
|
221
|
+
}
|
|
222
|
+
lines.push('');
|
|
223
|
+
for (const relation of graph.relations) {
|
|
224
|
+
const fromId = escapeDot(relation.from);
|
|
225
|
+
const toId = escapeDot(relation.to);
|
|
226
|
+
const label = escapeDot(relation.relationType);
|
|
227
|
+
lines.push(` ${fromId} -> ${toId} [label=${label}];`);
|
|
228
|
+
}
|
|
229
|
+
lines.push('}');
|
|
230
|
+
return lines.join('\n');
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Export as Markdown documentation.
|
|
234
|
+
*/
|
|
235
|
+
exportAsMarkdown(graph) {
|
|
236
|
+
const lines = [];
|
|
237
|
+
lines.push('# Knowledge Graph Export');
|
|
238
|
+
lines.push('');
|
|
239
|
+
lines.push(`**Exported:** ${new Date().toISOString()}`);
|
|
240
|
+
lines.push(`**Entities:** ${graph.entities.length}`);
|
|
241
|
+
lines.push(`**Relations:** ${graph.relations.length}`);
|
|
242
|
+
lines.push('');
|
|
243
|
+
lines.push('## Entities');
|
|
244
|
+
lines.push('');
|
|
245
|
+
for (const entity of graph.entities) {
|
|
246
|
+
lines.push(`### ${entity.name}`);
|
|
247
|
+
lines.push('');
|
|
248
|
+
lines.push(`- **Type:** ${entity.entityType}`);
|
|
249
|
+
if (entity.tags?.length)
|
|
250
|
+
lines.push(`- **Tags:** ${entity.tags.map(t => `\`${t}\``).join(', ')}`);
|
|
251
|
+
if (entity.importance !== undefined)
|
|
252
|
+
lines.push(`- **Importance:** ${entity.importance}/10`);
|
|
253
|
+
if (entity.observations.length > 0) {
|
|
254
|
+
lines.push('');
|
|
255
|
+
lines.push('**Observations:**');
|
|
256
|
+
for (const obs of entity.observations) {
|
|
257
|
+
lines.push(`- ${obs}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
lines.push('');
|
|
261
|
+
}
|
|
262
|
+
if (graph.relations.length > 0) {
|
|
263
|
+
lines.push('## Relations');
|
|
264
|
+
lines.push('');
|
|
265
|
+
for (const relation of graph.relations) {
|
|
266
|
+
lines.push(`- **${relation.from}** → *${relation.relationType}* → **${relation.to}**`);
|
|
267
|
+
}
|
|
268
|
+
lines.push('');
|
|
269
|
+
}
|
|
270
|
+
return lines.join('\n');
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Export as Mermaid diagram.
|
|
274
|
+
*/
|
|
275
|
+
exportAsMermaid(graph) {
|
|
276
|
+
const lines = [];
|
|
277
|
+
const sanitizeId = (str) => str.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
278
|
+
const escapeLabel = (str) => str.replace(/"/g, '#quot;');
|
|
279
|
+
lines.push('graph LR');
|
|
280
|
+
lines.push(' %% Knowledge Graph');
|
|
281
|
+
lines.push('');
|
|
282
|
+
const nodeIds = new Map();
|
|
283
|
+
for (const entity of graph.entities) {
|
|
284
|
+
nodeIds.set(entity.name, sanitizeId(entity.name));
|
|
285
|
+
}
|
|
286
|
+
for (const entity of graph.entities) {
|
|
287
|
+
const nodeId = nodeIds.get(entity.name);
|
|
288
|
+
const labelParts = [entity.name, `Type: ${entity.entityType}`];
|
|
289
|
+
if (entity.tags?.length)
|
|
290
|
+
labelParts.push(`Tags: ${entity.tags.join(', ')}`);
|
|
291
|
+
const label = escapeLabel(labelParts.join('<br/>'));
|
|
292
|
+
lines.push(` ${nodeId}["${label}"]`);
|
|
293
|
+
}
|
|
294
|
+
lines.push('');
|
|
295
|
+
for (const relation of graph.relations) {
|
|
296
|
+
const fromId = nodeIds.get(relation.from);
|
|
297
|
+
const toId = nodeIds.get(relation.to);
|
|
298
|
+
if (fromId && toId) {
|
|
299
|
+
const label = escapeLabel(relation.relationType);
|
|
300
|
+
lines.push(` ${fromId} -->|"${label}"| ${toId}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return lines.join('\n');
|
|
304
|
+
}
|
|
305
|
+
}
|