@danielsimonjr/memory-mcp 0.7.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/README.md +283 -0
- package/dist/__tests__/file-path.test.js +119 -0
- package/dist/__tests__/knowledge-graph.test.js +318 -0
- package/dist/index.js +1041 -0
- package/dist/vitest.config.js +13 -0
- package/package.json +48 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { KnowledgeGraphManager } from '../index.js';
|
|
6
|
+
describe('KnowledgeGraphManager', () => {
|
|
7
|
+
let manager;
|
|
8
|
+
let testFilePath;
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
// Create a temporary test file path
|
|
11
|
+
testFilePath = path.join(path.dirname(fileURLToPath(import.meta.url)), `test-memory-${Date.now()}.jsonl`);
|
|
12
|
+
manager = new KnowledgeGraphManager(testFilePath);
|
|
13
|
+
});
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
// Clean up test file
|
|
16
|
+
try {
|
|
17
|
+
await fs.unlink(testFilePath);
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
// Ignore errors if file doesn't exist
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
describe('createEntities', () => {
|
|
24
|
+
it('should create new entities', async () => {
|
|
25
|
+
const entities = [
|
|
26
|
+
{ name: 'Alice', entityType: 'person', observations: ['works at Acme Corp'] },
|
|
27
|
+
{ name: 'Bob', entityType: 'person', observations: ['likes programming'] },
|
|
28
|
+
];
|
|
29
|
+
const newEntities = await manager.createEntities(entities);
|
|
30
|
+
expect(newEntities).toHaveLength(2);
|
|
31
|
+
expect(newEntities).toEqual(entities);
|
|
32
|
+
const graph = await manager.readGraph();
|
|
33
|
+
expect(graph.entities).toHaveLength(2);
|
|
34
|
+
});
|
|
35
|
+
it('should not create duplicate entities', async () => {
|
|
36
|
+
const entities = [
|
|
37
|
+
{ name: 'Alice', entityType: 'person', observations: ['works at Acme Corp'] },
|
|
38
|
+
];
|
|
39
|
+
await manager.createEntities(entities);
|
|
40
|
+
const newEntities = await manager.createEntities(entities);
|
|
41
|
+
expect(newEntities).toHaveLength(0);
|
|
42
|
+
const graph = await manager.readGraph();
|
|
43
|
+
expect(graph.entities).toHaveLength(1);
|
|
44
|
+
});
|
|
45
|
+
it('should handle empty entity arrays', async () => {
|
|
46
|
+
const newEntities = await manager.createEntities([]);
|
|
47
|
+
expect(newEntities).toHaveLength(0);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe('createRelations', () => {
|
|
51
|
+
it('should create new relations', async () => {
|
|
52
|
+
await manager.createEntities([
|
|
53
|
+
{ name: 'Alice', entityType: 'person', observations: [] },
|
|
54
|
+
{ name: 'Bob', entityType: 'person', observations: [] },
|
|
55
|
+
]);
|
|
56
|
+
const relations = [
|
|
57
|
+
{ from: 'Alice', to: 'Bob', relationType: 'knows' },
|
|
58
|
+
];
|
|
59
|
+
const newRelations = await manager.createRelations(relations);
|
|
60
|
+
expect(newRelations).toHaveLength(1);
|
|
61
|
+
expect(newRelations).toEqual(relations);
|
|
62
|
+
const graph = await manager.readGraph();
|
|
63
|
+
expect(graph.relations).toHaveLength(1);
|
|
64
|
+
});
|
|
65
|
+
it('should not create duplicate relations', async () => {
|
|
66
|
+
await manager.createEntities([
|
|
67
|
+
{ name: 'Alice', entityType: 'person', observations: [] },
|
|
68
|
+
{ name: 'Bob', entityType: 'person', observations: [] },
|
|
69
|
+
]);
|
|
70
|
+
const relations = [
|
|
71
|
+
{ from: 'Alice', to: 'Bob', relationType: 'knows' },
|
|
72
|
+
];
|
|
73
|
+
await manager.createRelations(relations);
|
|
74
|
+
const newRelations = await manager.createRelations(relations);
|
|
75
|
+
expect(newRelations).toHaveLength(0);
|
|
76
|
+
const graph = await manager.readGraph();
|
|
77
|
+
expect(graph.relations).toHaveLength(1);
|
|
78
|
+
});
|
|
79
|
+
it('should handle empty relation arrays', async () => {
|
|
80
|
+
const newRelations = await manager.createRelations([]);
|
|
81
|
+
expect(newRelations).toHaveLength(0);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
describe('addObservations', () => {
|
|
85
|
+
it('should add observations to existing entities', async () => {
|
|
86
|
+
await manager.createEntities([
|
|
87
|
+
{ name: 'Alice', entityType: 'person', observations: ['works at Acme Corp'] },
|
|
88
|
+
]);
|
|
89
|
+
const results = await manager.addObservations([
|
|
90
|
+
{ entityName: 'Alice', contents: ['likes coffee', 'has a dog'] },
|
|
91
|
+
]);
|
|
92
|
+
expect(results).toHaveLength(1);
|
|
93
|
+
expect(results[0].entityName).toBe('Alice');
|
|
94
|
+
expect(results[0].addedObservations).toHaveLength(2);
|
|
95
|
+
const graph = await manager.readGraph();
|
|
96
|
+
const alice = graph.entities.find(e => e.name === 'Alice');
|
|
97
|
+
expect(alice?.observations).toHaveLength(3);
|
|
98
|
+
});
|
|
99
|
+
it('should not add duplicate observations', async () => {
|
|
100
|
+
await manager.createEntities([
|
|
101
|
+
{ name: 'Alice', entityType: 'person', observations: ['works at Acme Corp'] },
|
|
102
|
+
]);
|
|
103
|
+
await manager.addObservations([
|
|
104
|
+
{ entityName: 'Alice', contents: ['likes coffee'] },
|
|
105
|
+
]);
|
|
106
|
+
const results = await manager.addObservations([
|
|
107
|
+
{ entityName: 'Alice', contents: ['likes coffee', 'has a dog'] },
|
|
108
|
+
]);
|
|
109
|
+
expect(results[0].addedObservations).toHaveLength(1);
|
|
110
|
+
expect(results[0].addedObservations).toContain('has a dog');
|
|
111
|
+
const graph = await manager.readGraph();
|
|
112
|
+
const alice = graph.entities.find(e => e.name === 'Alice');
|
|
113
|
+
expect(alice?.observations).toHaveLength(3);
|
|
114
|
+
});
|
|
115
|
+
it('should throw error for non-existent entity', async () => {
|
|
116
|
+
await expect(manager.addObservations([
|
|
117
|
+
{ entityName: 'NonExistent', contents: ['some observation'] },
|
|
118
|
+
])).rejects.toThrow('Entity with name NonExistent not found');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
describe('deleteEntities', () => {
|
|
122
|
+
it('should delete entities', async () => {
|
|
123
|
+
await manager.createEntities([
|
|
124
|
+
{ name: 'Alice', entityType: 'person', observations: [] },
|
|
125
|
+
{ name: 'Bob', entityType: 'person', observations: [] },
|
|
126
|
+
]);
|
|
127
|
+
await manager.deleteEntities(['Alice']);
|
|
128
|
+
const graph = await manager.readGraph();
|
|
129
|
+
expect(graph.entities).toHaveLength(1);
|
|
130
|
+
expect(graph.entities[0].name).toBe('Bob');
|
|
131
|
+
});
|
|
132
|
+
it('should cascade delete relations when deleting entities', async () => {
|
|
133
|
+
await manager.createEntities([
|
|
134
|
+
{ name: 'Alice', entityType: 'person', observations: [] },
|
|
135
|
+
{ name: 'Bob', entityType: 'person', observations: [] },
|
|
136
|
+
{ name: 'Charlie', entityType: 'person', observations: [] },
|
|
137
|
+
]);
|
|
138
|
+
await manager.createRelations([
|
|
139
|
+
{ from: 'Alice', to: 'Bob', relationType: 'knows' },
|
|
140
|
+
{ from: 'Bob', to: 'Charlie', relationType: 'knows' },
|
|
141
|
+
]);
|
|
142
|
+
await manager.deleteEntities(['Bob']);
|
|
143
|
+
const graph = await manager.readGraph();
|
|
144
|
+
expect(graph.entities).toHaveLength(2);
|
|
145
|
+
expect(graph.relations).toHaveLength(0);
|
|
146
|
+
});
|
|
147
|
+
it('should handle deleting non-existent entities', async () => {
|
|
148
|
+
await manager.deleteEntities(['NonExistent']);
|
|
149
|
+
const graph = await manager.readGraph();
|
|
150
|
+
expect(graph.entities).toHaveLength(0);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
describe('deleteObservations', () => {
|
|
154
|
+
it('should delete observations from entities', async () => {
|
|
155
|
+
await manager.createEntities([
|
|
156
|
+
{ name: 'Alice', entityType: 'person', observations: ['works at Acme Corp', 'likes coffee'] },
|
|
157
|
+
]);
|
|
158
|
+
await manager.deleteObservations([
|
|
159
|
+
{ entityName: 'Alice', observations: ['likes coffee'] },
|
|
160
|
+
]);
|
|
161
|
+
const graph = await manager.readGraph();
|
|
162
|
+
const alice = graph.entities.find(e => e.name === 'Alice');
|
|
163
|
+
expect(alice?.observations).toHaveLength(1);
|
|
164
|
+
expect(alice?.observations).toContain('works at Acme Corp');
|
|
165
|
+
});
|
|
166
|
+
it('should handle deleting from non-existent entities', async () => {
|
|
167
|
+
await manager.deleteObservations([
|
|
168
|
+
{ entityName: 'NonExistent', observations: ['some observation'] },
|
|
169
|
+
]);
|
|
170
|
+
// Should not throw error
|
|
171
|
+
const graph = await manager.readGraph();
|
|
172
|
+
expect(graph.entities).toHaveLength(0);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
describe('deleteRelations', () => {
|
|
176
|
+
it('should delete specific relations', async () => {
|
|
177
|
+
await manager.createEntities([
|
|
178
|
+
{ name: 'Alice', entityType: 'person', observations: [] },
|
|
179
|
+
{ name: 'Bob', entityType: 'person', observations: [] },
|
|
180
|
+
]);
|
|
181
|
+
await manager.createRelations([
|
|
182
|
+
{ from: 'Alice', to: 'Bob', relationType: 'knows' },
|
|
183
|
+
{ from: 'Alice', to: 'Bob', relationType: 'works_with' },
|
|
184
|
+
]);
|
|
185
|
+
await manager.deleteRelations([
|
|
186
|
+
{ from: 'Alice', to: 'Bob', relationType: 'knows' },
|
|
187
|
+
]);
|
|
188
|
+
const graph = await manager.readGraph();
|
|
189
|
+
expect(graph.relations).toHaveLength(1);
|
|
190
|
+
expect(graph.relations[0].relationType).toBe('works_with');
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
describe('readGraph', () => {
|
|
194
|
+
it('should return empty graph when file does not exist', async () => {
|
|
195
|
+
const graph = await manager.readGraph();
|
|
196
|
+
expect(graph.entities).toHaveLength(0);
|
|
197
|
+
expect(graph.relations).toHaveLength(0);
|
|
198
|
+
});
|
|
199
|
+
it('should return complete graph with entities and relations', async () => {
|
|
200
|
+
await manager.createEntities([
|
|
201
|
+
{ name: 'Alice', entityType: 'person', observations: ['works at Acme Corp'] },
|
|
202
|
+
]);
|
|
203
|
+
await manager.createRelations([
|
|
204
|
+
{ from: 'Alice', to: 'Alice', relationType: 'self' },
|
|
205
|
+
]);
|
|
206
|
+
const graph = await manager.readGraph();
|
|
207
|
+
expect(graph.entities).toHaveLength(1);
|
|
208
|
+
expect(graph.relations).toHaveLength(1);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
describe('searchNodes', () => {
|
|
212
|
+
beforeEach(async () => {
|
|
213
|
+
await manager.createEntities([
|
|
214
|
+
{ name: 'Alice', entityType: 'person', observations: ['works at Acme Corp', 'likes programming'] },
|
|
215
|
+
{ name: 'Bob', entityType: 'person', observations: ['works at TechCo'] },
|
|
216
|
+
{ name: 'Acme Corp', entityType: 'company', observations: ['tech company'] },
|
|
217
|
+
]);
|
|
218
|
+
await manager.createRelations([
|
|
219
|
+
{ from: 'Alice', to: 'Acme Corp', relationType: 'works_at' },
|
|
220
|
+
{ from: 'Bob', to: 'Acme Corp', relationType: 'competitor' },
|
|
221
|
+
]);
|
|
222
|
+
});
|
|
223
|
+
it('should search by entity name', async () => {
|
|
224
|
+
const result = await manager.searchNodes('Alice');
|
|
225
|
+
expect(result.entities).toHaveLength(1);
|
|
226
|
+
expect(result.entities[0].name).toBe('Alice');
|
|
227
|
+
});
|
|
228
|
+
it('should search by entity type', async () => {
|
|
229
|
+
const result = await manager.searchNodes('company');
|
|
230
|
+
expect(result.entities).toHaveLength(1);
|
|
231
|
+
expect(result.entities[0].name).toBe('Acme Corp');
|
|
232
|
+
});
|
|
233
|
+
it('should search by observation content', async () => {
|
|
234
|
+
const result = await manager.searchNodes('programming');
|
|
235
|
+
expect(result.entities).toHaveLength(1);
|
|
236
|
+
expect(result.entities[0].name).toBe('Alice');
|
|
237
|
+
});
|
|
238
|
+
it('should be case insensitive', async () => {
|
|
239
|
+
const result = await manager.searchNodes('ALICE');
|
|
240
|
+
expect(result.entities).toHaveLength(1);
|
|
241
|
+
expect(result.entities[0].name).toBe('Alice');
|
|
242
|
+
});
|
|
243
|
+
it('should include relations between matched entities', async () => {
|
|
244
|
+
const result = await manager.searchNodes('Acme');
|
|
245
|
+
expect(result.entities).toHaveLength(2); // Alice and Acme Corp
|
|
246
|
+
expect(result.relations).toHaveLength(1); // Only Alice -> Acme Corp relation
|
|
247
|
+
});
|
|
248
|
+
it('should return empty graph for no matches', async () => {
|
|
249
|
+
const result = await manager.searchNodes('NonExistent');
|
|
250
|
+
expect(result.entities).toHaveLength(0);
|
|
251
|
+
expect(result.relations).toHaveLength(0);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
describe('openNodes', () => {
|
|
255
|
+
beforeEach(async () => {
|
|
256
|
+
await manager.createEntities([
|
|
257
|
+
{ name: 'Alice', entityType: 'person', observations: [] },
|
|
258
|
+
{ name: 'Bob', entityType: 'person', observations: [] },
|
|
259
|
+
{ name: 'Charlie', entityType: 'person', observations: [] },
|
|
260
|
+
]);
|
|
261
|
+
await manager.createRelations([
|
|
262
|
+
{ from: 'Alice', to: 'Bob', relationType: 'knows' },
|
|
263
|
+
{ from: 'Bob', to: 'Charlie', relationType: 'knows' },
|
|
264
|
+
]);
|
|
265
|
+
});
|
|
266
|
+
it('should open specific nodes by name', async () => {
|
|
267
|
+
const result = await manager.openNodes(['Alice', 'Bob']);
|
|
268
|
+
expect(result.entities).toHaveLength(2);
|
|
269
|
+
expect(result.entities.map(e => e.name)).toContain('Alice');
|
|
270
|
+
expect(result.entities.map(e => e.name)).toContain('Bob');
|
|
271
|
+
});
|
|
272
|
+
it('should include relations between opened nodes', async () => {
|
|
273
|
+
const result = await manager.openNodes(['Alice', 'Bob']);
|
|
274
|
+
expect(result.relations).toHaveLength(1);
|
|
275
|
+
expect(result.relations[0].from).toBe('Alice');
|
|
276
|
+
expect(result.relations[0].to).toBe('Bob');
|
|
277
|
+
});
|
|
278
|
+
it('should exclude relations to unopened nodes', async () => {
|
|
279
|
+
const result = await manager.openNodes(['Bob']);
|
|
280
|
+
expect(result.relations).toHaveLength(0);
|
|
281
|
+
});
|
|
282
|
+
it('should handle opening non-existent nodes', async () => {
|
|
283
|
+
const result = await manager.openNodes(['NonExistent']);
|
|
284
|
+
expect(result.entities).toHaveLength(0);
|
|
285
|
+
});
|
|
286
|
+
it('should handle empty node list', async () => {
|
|
287
|
+
const result = await manager.openNodes([]);
|
|
288
|
+
expect(result.entities).toHaveLength(0);
|
|
289
|
+
expect(result.relations).toHaveLength(0);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
describe('file persistence', () => {
|
|
293
|
+
it('should persist data across manager instances', async () => {
|
|
294
|
+
await manager.createEntities([
|
|
295
|
+
{ name: 'Alice', entityType: 'person', observations: ['persistent data'] },
|
|
296
|
+
]);
|
|
297
|
+
// Create new manager instance with same file path
|
|
298
|
+
const manager2 = new KnowledgeGraphManager(testFilePath);
|
|
299
|
+
const graph = await manager2.readGraph();
|
|
300
|
+
expect(graph.entities).toHaveLength(1);
|
|
301
|
+
expect(graph.entities[0].name).toBe('Alice');
|
|
302
|
+
});
|
|
303
|
+
it('should handle JSONL format correctly', async () => {
|
|
304
|
+
await manager.createEntities([
|
|
305
|
+
{ name: 'Alice', entityType: 'person', observations: [] },
|
|
306
|
+
]);
|
|
307
|
+
await manager.createRelations([
|
|
308
|
+
{ from: 'Alice', to: 'Alice', relationType: 'self' },
|
|
309
|
+
]);
|
|
310
|
+
// Read file directly
|
|
311
|
+
const fileContent = await fs.readFile(testFilePath, 'utf-8');
|
|
312
|
+
const lines = fileContent.split('\n').filter(line => line.trim());
|
|
313
|
+
expect(lines).toHaveLength(2);
|
|
314
|
+
expect(JSON.parse(lines[0])).toHaveProperty('type', 'entity');
|
|
315
|
+
expect(JSON.parse(lines[1])).toHaveProperty('type', 'relation');
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
});
|