collective-memory-mcp 0.3.0 → 0.3.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.
- package/package.json +1 -1
- package/src/server.js +1 -0
- package/src/storage.js +81 -207
package/package.json
CHANGED
package/src/server.js
CHANGED
package/src/storage.js
CHANGED
|
@@ -4,120 +4,81 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { promises as fs } from "fs";
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
7
8
|
import path from "path";
|
|
8
9
|
import os from "os";
|
|
9
|
-
import { createHash } from "crypto";
|
|
10
10
|
import { Entity, Relation } from "./models.js";
|
|
11
11
|
|
|
12
12
|
const DB_DIR = path.join(os.homedir(), ".collective-memory");
|
|
13
13
|
const DB_PATH = path.join(DB_DIR, "memory.json");
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
*/
|
|
18
|
-
const LOCK_FILE = path.join(DB_DIR, "memory.lock");
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Simple file-based storage with locking
|
|
16
|
+
* Simple file-based storage
|
|
22
17
|
*/
|
|
23
18
|
export class Storage {
|
|
24
19
|
constructor(dbPath = DB_PATH) {
|
|
25
20
|
this.dbPath = dbPath;
|
|
26
|
-
this.lockPath = LOCK_FILE;
|
|
27
21
|
this.data = null;
|
|
28
|
-
|
|
29
|
-
this.lockDelay = 50;
|
|
22
|
+
// Initialize synchronously
|
|
30
23
|
this.init();
|
|
31
24
|
}
|
|
32
25
|
|
|
33
26
|
/**
|
|
34
|
-
* Initialize storage
|
|
35
|
-
*/
|
|
36
|
-
async init() {
|
|
37
|
-
await this.ensureDir();
|
|
38
|
-
await this.load();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Ensure data directory exists
|
|
27
|
+
* Initialize storage (synchronous)
|
|
43
28
|
*/
|
|
44
|
-
|
|
29
|
+
init() {
|
|
45
30
|
try {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
31
|
+
// Ensure directory exists
|
|
32
|
+
const dir = path.dirname(this.dbPath);
|
|
33
|
+
mkdirSync(dir, { recursive: true });
|
|
34
|
+
|
|
35
|
+
// Try to load existing data
|
|
36
|
+
if (existsSync(this.dbPath)) {
|
|
37
|
+
const content = readFileSync(this.dbPath, "utf-8");
|
|
38
|
+
this.data = JSON.parse(content);
|
|
39
|
+
} else {
|
|
40
|
+
// Create new empty data
|
|
41
|
+
this.data = {
|
|
42
|
+
entities: {},
|
|
43
|
+
relations: [],
|
|
44
|
+
version: "1.0",
|
|
45
|
+
};
|
|
46
|
+
this.saveSync();
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
// If anything fails, start with empty data
|
|
61
50
|
this.data = {
|
|
62
51
|
entities: {},
|
|
63
52
|
relations: [],
|
|
64
53
|
version: "1.0",
|
|
65
54
|
};
|
|
66
|
-
await this.save();
|
|
67
55
|
}
|
|
68
56
|
}
|
|
69
57
|
|
|
70
58
|
/**
|
|
71
|
-
* Save data
|
|
72
|
-
*/
|
|
73
|
-
async save() {
|
|
74
|
-
const dir = path.dirname(this.dbPath);
|
|
75
|
-
await fs.mkdir(dir, { recursive: true });
|
|
76
|
-
const tempPath = path.join(dir, ".memory.tmp");
|
|
77
|
-
await fs.writeFile(tempPath, JSON.stringify(this.data, null, 2), "utf-8");
|
|
78
|
-
await fs.rename(tempPath, this.dbPath);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Acquire lock
|
|
59
|
+
* Save data synchronously
|
|
83
60
|
*/
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
);
|
|
92
|
-
return;
|
|
93
|
-
} catch {
|
|
94
|
-
await new Promise(r => setTimeout(r, this.lockDelay));
|
|
95
|
-
}
|
|
61
|
+
saveSync() {
|
|
62
|
+
try {
|
|
63
|
+
const dir = path.dirname(this.dbPath);
|
|
64
|
+
mkdirSync(dir, { recursive: true });
|
|
65
|
+
writeFileSync(this.dbPath, JSON.stringify(this.data, null, 2), "utf-8");
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error("Failed to save:", error.message);
|
|
96
68
|
}
|
|
97
|
-
// If we can't get lock, just proceed (it's a simple file lock)
|
|
98
69
|
}
|
|
99
70
|
|
|
100
71
|
/**
|
|
101
|
-
*
|
|
72
|
+
* Save data asynchronously
|
|
102
73
|
*/
|
|
103
|
-
async
|
|
74
|
+
async save() {
|
|
75
|
+
const dir = path.dirname(this.dbPath);
|
|
104
76
|
try {
|
|
105
|
-
await fs.
|
|
77
|
+
await fs.mkdir(dir, { recursive: true });
|
|
106
78
|
} catch {
|
|
107
79
|
// Ignore
|
|
108
80
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Execute operation with lock
|
|
113
|
-
*/
|
|
114
|
-
async withLock(fn) {
|
|
115
|
-
await this.acquireLock();
|
|
116
|
-
try {
|
|
117
|
-
return await fn();
|
|
118
|
-
} finally {
|
|
119
|
-
await this.releaseLock();
|
|
120
|
-
}
|
|
81
|
+
await fs.writeFile(this.dbPath, JSON.stringify(this.data, null, 2), "utf-8");
|
|
121
82
|
}
|
|
122
83
|
|
|
123
84
|
// ========== Entity Operations ==========
|
|
@@ -126,14 +87,12 @@ export class Storage {
|
|
|
126
87
|
* Create a new entity. Returns true if created, false if duplicate.
|
|
127
88
|
*/
|
|
128
89
|
async createEntity(entity) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return true;
|
|
136
|
-
});
|
|
90
|
+
if (this.data.entities[entity.name]) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
this.data.entities[entity.name] = entity.toJSON();
|
|
94
|
+
await this.save();
|
|
95
|
+
return true;
|
|
137
96
|
}
|
|
138
97
|
|
|
139
98
|
/**
|
|
@@ -165,41 +124,37 @@ export class Storage {
|
|
|
165
124
|
* Update entity
|
|
166
125
|
*/
|
|
167
126
|
async updateEntity(name, { observations, metadata } = {}) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (!entity) return false;
|
|
127
|
+
const entity = this.data.entities[name];
|
|
128
|
+
if (!entity) return false;
|
|
171
129
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
130
|
+
if (observations !== undefined) {
|
|
131
|
+
entity.observations = observations;
|
|
132
|
+
}
|
|
133
|
+
if (metadata !== undefined) {
|
|
134
|
+
entity.metadata = metadata;
|
|
135
|
+
}
|
|
178
136
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
});
|
|
137
|
+
await this.save();
|
|
138
|
+
return true;
|
|
182
139
|
}
|
|
183
140
|
|
|
184
141
|
/**
|
|
185
142
|
* Delete an entity and its relations
|
|
186
143
|
*/
|
|
187
144
|
async deleteEntity(name) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
145
|
+
if (!this.data.entities[name]) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
192
148
|
|
|
193
|
-
|
|
149
|
+
delete this.data.entities[name];
|
|
194
150
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
151
|
+
// Remove relations involving this entity
|
|
152
|
+
this.data.relations = this.data.relations.filter(
|
|
153
|
+
r => r.from !== name && r.to !== name
|
|
154
|
+
);
|
|
199
155
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
});
|
|
156
|
+
await this.save();
|
|
157
|
+
return true;
|
|
203
158
|
}
|
|
204
159
|
|
|
205
160
|
/**
|
|
@@ -219,16 +174,14 @@ export class Storage {
|
|
|
219
174
|
* Create a new relation. Returns true if created, false if duplicate.
|
|
220
175
|
*/
|
|
221
176
|
async createRelation(relation) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
177
|
+
const key = this.relationKey(relation.from, relation.to, relation.relationType);
|
|
178
|
+
if (this.data.relations.some(r => this.relationKey(r.from, r.to, r.relationType) === key)) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
227
181
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
});
|
|
182
|
+
this.data.relations.push(relation.toJSON());
|
|
183
|
+
await this.save();
|
|
184
|
+
return true;
|
|
232
185
|
}
|
|
233
186
|
|
|
234
187
|
/**
|
|
@@ -277,18 +230,16 @@ export class Storage {
|
|
|
277
230
|
* Delete a specific relation
|
|
278
231
|
*/
|
|
279
232
|
async deleteRelation(fromEntity, toEntity, relationType) {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
return false;
|
|
291
|
-
});
|
|
233
|
+
const before = this.data.relations.length;
|
|
234
|
+
this.data.relations = this.data.relations.filter(
|
|
235
|
+
r => !(r.from === fromEntity && r.to === toEntity && r.relationType === relationType)
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
if (this.data.relations.length < before) {
|
|
239
|
+
await this.save();
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
return false;
|
|
292
243
|
}
|
|
293
244
|
|
|
294
245
|
/**
|
|
@@ -354,89 +305,12 @@ export class Storage {
|
|
|
354
305
|
}
|
|
355
306
|
}
|
|
356
307
|
|
|
357
|
-
//
|
|
308
|
+
// Singleton instance
|
|
358
309
|
let storageInstance = null;
|
|
359
310
|
|
|
360
311
|
export function getStorage(dbPath) {
|
|
361
312
|
if (!storageInstance) {
|
|
362
313
|
storageInstance = new Storage(dbPath);
|
|
363
|
-
// Initialize asynchronously but don't wait
|
|
364
|
-
storageInstance.init().catch(console.error);
|
|
365
314
|
}
|
|
366
315
|
return storageInstance;
|
|
367
316
|
}
|
|
368
|
-
|
|
369
|
-
// Synchronous methods for compatibility
|
|
370
|
-
export class SyncStorage {
|
|
371
|
-
constructor(dbPath = DB_PATH) {
|
|
372
|
-
this.storage = new Storage(dbPath);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
async init() {
|
|
376
|
-
await this.storage.init();
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
createEntity(entity) {
|
|
380
|
-
return this.storage.createEntity(entity);
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
getEntity(name) {
|
|
384
|
-
return this.storage.getEntity(name);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
getAllEntities() {
|
|
388
|
-
return this.storage.getAllEntities();
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
entityExists(name) {
|
|
392
|
-
return this.storage.entityExists(name);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
updateEntity(name, data) {
|
|
396
|
-
return this.storage.updateEntity(name, data);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
deleteEntity(name) {
|
|
400
|
-
return this.storage.deleteEntity(name);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
deleteEntities(names) {
|
|
404
|
-
return this.storage.deleteEntities(names);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
createRelation(relation) {
|
|
408
|
-
return this.storage.createRelation(relation);
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
getRelations(filters) {
|
|
412
|
-
return this.storage.getRelations(filters);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
getAllRelations() {
|
|
416
|
-
return this.storage.getAllRelations();
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
relationExists(from, to, type) {
|
|
420
|
-
return this.storage.relationExists(from, to, type);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
deleteRelation(from, to, type) {
|
|
424
|
-
return this.storage.deleteRelation(from, to, type);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
deleteRelations(relations) {
|
|
428
|
-
return this.storage.deleteRelations(relations);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
searchEntities(query) {
|
|
432
|
-
return this.storage.searchEntities(query);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
getRelatedEntities(name) {
|
|
436
|
-
return this.storage.getRelatedEntities(name);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
close() {
|
|
440
|
-
return this.storage.close();
|
|
441
|
-
}
|
|
442
|
-
}
|