collective-memory-mcp 0.2.0 → 0.3.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.
Files changed (2) hide show
  1. package/package.json +2 -6
  2. package/src/storage.js +285 -197
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "collective-memory-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "A persistent, graph-based memory system for AI agents (MCP Server)",
5
5
  "type": "module",
6
6
  "main": "src/server.js",
@@ -37,10 +37,6 @@
37
37
  "win32"
38
38
  ],
39
39
  "dependencies": {
40
- "@modelcontextprotocol/sdk": "^0.6.0",
41
- "better-sqlite3": "^11.0.0"
42
- },
43
- "devDependencies": {
44
- "@types/better-sqlite3": "^7.6.0"
40
+ "@modelcontextprotocol/sdk": "^0.6.0"
45
41
  }
46
42
  }
package/src/storage.js CHANGED
@@ -1,74 +1,123 @@
1
1
  /**
2
- * Storage layer for the Collective Memory System using SQLite.
2
+ * Storage layer for the Collective Memory System using JSON file.
3
+ * Pure JavaScript - no native dependencies required.
3
4
  */
4
5
 
5
- import Database from "better-sqlite3";
6
6
  import { promises as fs } from "fs";
7
7
  import path from "path";
8
8
  import os from "os";
9
+ import { createHash } from "crypto";
9
10
  import { Entity, Relation } from "./models.js";
10
11
 
11
12
  const DB_DIR = path.join(os.homedir(), ".collective-memory");
12
- const DB_PATH = path.join(DB_DIR, "memory.db");
13
+ const DB_PATH = path.join(DB_DIR, "memory.json");
13
14
 
14
15
  /**
15
- * SQLite-based storage for the knowledge graph
16
+ * Lock file to prevent concurrent writes
17
+ */
18
+ const LOCK_FILE = path.join(DB_DIR, "memory.lock");
19
+
20
+ /**
21
+ * Simple file-based storage with locking
16
22
  */
17
23
  export class Storage {
18
24
  constructor(dbPath = DB_PATH) {
19
25
  this.dbPath = dbPath;
20
- this.db = null;
26
+ this.lockPath = LOCK_FILE;
27
+ this.data = null;
28
+ this.lockRetries = 10;
29
+ this.lockDelay = 50;
21
30
  this.init();
22
31
  }
23
32
 
24
33
  /**
25
- * Initialize database and create schema
34
+ * Initialize storage
35
+ */
36
+ async init() {
37
+ await this.ensureDir();
38
+ await this.load();
39
+ }
40
+
41
+ /**
42
+ * Ensure data directory exists
43
+ */
44
+ async ensureDir() {
45
+ try {
46
+ await fs.mkdir(path.dirname(this.dbPath), { recursive: true });
47
+ } catch {
48
+ // Ignore if exists
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Load data from file
54
+ */
55
+ async load() {
56
+ try {
57
+ const content = await fs.readFile(this.dbPath, "utf-8");
58
+ this.data = JSON.parse(content);
59
+ } catch {
60
+ // File doesn't exist or is invalid - create new
61
+ this.data = {
62
+ entities: {},
63
+ relations: [],
64
+ version: "1.0",
65
+ };
66
+ await this.save();
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Save data to file
26
72
  */
27
- init() {
28
- // Ensure directory exists
29
- fs.mkdir(path.dirname(this.dbPath), { recursive: true }).catch(() => {});
30
-
31
- this.db = new Database(this.dbPath);
32
- this.db.pragma("journal_mode = WAL");
33
-
34
- // Create entities table
35
- this.db.exec(`
36
- CREATE TABLE IF NOT EXISTS entities (
37
- name TEXT PRIMARY KEY,
38
- entity_type TEXT NOT NULL,
39
- observations TEXT NOT NULL DEFAULT '[]',
40
- created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
41
- metadata TEXT NOT NULL DEFAULT '{}'
42
- )
43
- `);
44
-
45
- // Create relations table
46
- this.db.exec(`
47
- CREATE TABLE IF NOT EXISTS relations (
48
- id INTEGER PRIMARY KEY AUTOINCREMENT,
49
- from_entity TEXT NOT NULL,
50
- to_entity TEXT NOT NULL,
51
- relation_type TEXT NOT NULL,
52
- created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
53
- metadata TEXT NOT NULL DEFAULT '{}',
54
- UNIQUE(from_entity, to_entity, relation_type)
55
- )
56
- `);
57
-
58
- // Create indexes
59
- this.db.exec(`
60
- CREATE INDEX IF NOT EXISTS idx_entities_type ON entities(entity_type);
61
- CREATE INDEX IF NOT EXISTS idx_relations_from ON relations(from_entity);
62
- CREATE INDEX IF NOT EXISTS idx_relations_to ON relations(to_entity);
63
- CREATE INDEX IF NOT EXISTS idx_relations_type ON relations(relation_type);
64
- `);
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);
65
79
  }
66
80
 
67
81
  /**
68
- * Prepare statement helper
82
+ * Acquire lock
69
83
  */
70
- prepare(sql) {
71
- return this.db.prepare(sql);
84
+ async acquireLock() {
85
+ for (let i = 0; i < this.lockRetries; i++) {
86
+ try {
87
+ await fs.writeFile(
88
+ this.lockPath,
89
+ process.pid.toString(),
90
+ { flag: "wx" }
91
+ );
92
+ return;
93
+ } catch {
94
+ await new Promise(r => setTimeout(r, this.lockDelay));
95
+ }
96
+ }
97
+ // If we can't get lock, just proceed (it's a simple file lock)
98
+ }
99
+
100
+ /**
101
+ * Release lock
102
+ */
103
+ async releaseLock() {
104
+ try {
105
+ await fs.unlink(this.lockPath);
106
+ } catch {
107
+ // Ignore
108
+ }
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
+ }
72
121
  }
73
122
 
74
123
  // ========== Entity Operations ==========
@@ -76,40 +125,24 @@ export class Storage {
76
125
  /**
77
126
  * Create a new entity. Returns true if created, false if duplicate.
78
127
  */
79
- createEntity(entity) {
80
- try {
81
- const stmt = this.prepare(`
82
- INSERT INTO entities (name, entity_type, observations, created_at, metadata)
83
- VALUES (?, ?, ?, ?, ?)
84
- `);
85
- stmt.run(
86
- entity.name,
87
- entity.entityType,
88
- JSON.stringify(entity.observations),
89
- entity.createdAt,
90
- JSON.stringify(entity.metadata)
91
- );
128
+ async createEntity(entity) {
129
+ return this.withLock(async () => {
130
+ if (this.data.entities[entity.name]) {
131
+ return false;
132
+ }
133
+ this.data.entities[entity.name] = entity.toJSON();
134
+ await this.save();
92
135
  return true;
93
- } catch (err) {
94
- if (err.code === "SQLITE_CONSTRAINT") return false;
95
- throw err;
96
- }
136
+ });
97
137
  }
98
138
 
99
139
  /**
100
140
  * Get an entity by name
101
141
  */
102
142
  getEntity(name) {
103
- const stmt = this.prepare("SELECT * FROM entities WHERE name = ?");
104
- const row = stmt.get(name);
105
- if (row) {
106
- return new Entity({
107
- name: row.name,
108
- entityType: row.entity_type,
109
- observations: JSON.parse(row.observations),
110
- createdAt: row.created_at,
111
- metadata: JSON.parse(row.metadata),
112
- });
143
+ const data = this.data.entities[name];
144
+ if (data) {
145
+ return new Entity(data);
113
146
  }
114
147
  return null;
115
148
  }
@@ -118,68 +151,64 @@ export class Storage {
118
151
  * Get all entities
119
152
  */
120
153
  getAllEntities() {
121
- const stmt = this.prepare("SELECT * FROM entities");
122
- const rows = stmt.all();
123
- return rows.map(row => new Entity({
124
- name: row.name,
125
- entityType: row.entity_type,
126
- observations: JSON.parse(row.observations),
127
- createdAt: row.created_at,
128
- metadata: JSON.parse(row.metadata),
129
- }));
154
+ return Object.values(this.data.entities).map(data => new Entity(data));
130
155
  }
131
156
 
132
157
  /**
133
158
  * Check if an entity exists
134
159
  */
135
160
  entityExists(name) {
136
- const stmt = this.prepare("SELECT 1 FROM entities WHERE name = ?");
137
- return stmt.get(name) !== undefined;
161
+ return name in this.data.entities;
138
162
  }
139
163
 
140
164
  /**
141
- * Update entity observations
165
+ * Update entity
142
166
  */
143
- updateEntity(name, { observations, metadata } = {}) {
144
- const updates = [];
145
- const params = [];
146
-
147
- if (observations !== undefined) {
148
- updates.push("observations = ?");
149
- params.push(JSON.stringify(observations));
150
- }
151
- if (metadata !== undefined) {
152
- updates.push("metadata = ?");
153
- params.push(JSON.stringify(metadata));
154
- }
155
-
156
- if (updates.length === 0) return true;
157
-
158
- params.push(name);
159
- const stmt = this.prepare(`UPDATE entities SET ${updates.join(", ")} WHERE name = ?`);
160
- return stmt.run(...params).changes > 0;
167
+ async updateEntity(name, { observations, metadata } = {}) {
168
+ return this.withLock(async () => {
169
+ const entity = this.data.entities[name];
170
+ if (!entity) return false;
171
+
172
+ if (observations !== undefined) {
173
+ entity.observations = observations;
174
+ }
175
+ if (metadata !== undefined) {
176
+ entity.metadata = metadata;
177
+ }
178
+
179
+ await this.save();
180
+ return true;
181
+ });
161
182
  }
162
183
 
163
184
  /**
164
185
  * Delete an entity and its relations
165
186
  */
166
- deleteEntity(name) {
167
- const deleteRelations = this.prepare(
168
- "DELETE FROM relations WHERE from_entity = ? OR to_entity = ?"
169
- );
170
- deleteRelations.run(name, name);
187
+ async deleteEntity(name) {
188
+ return this.withLock(async () => {
189
+ if (!this.data.entities[name]) {
190
+ return false;
191
+ }
192
+
193
+ delete this.data.entities[name];
171
194
 
172
- const deleteEntity = this.prepare("DELETE FROM entities WHERE name = ?");
173
- return deleteEntity.run(name).changes > 0;
195
+ // Remove relations involving this entity
196
+ this.data.relations = this.data.relations.filter(
197
+ r => r.from !== name && r.to !== name
198
+ );
199
+
200
+ await this.save();
201
+ return true;
202
+ });
174
203
  }
175
204
 
176
205
  /**
177
206
  * Delete multiple entities
178
207
  */
179
- deleteEntities(names) {
208
+ async deleteEntities(names) {
180
209
  let count = 0;
181
210
  for (const name of names) {
182
- if (this.deleteEntity(name)) count++;
211
+ if (await this.deleteEntity(name)) count++;
183
212
  }
184
213
  return count;
185
214
  }
@@ -189,91 +218,86 @@ export class Storage {
189
218
  /**
190
219
  * Create a new relation. Returns true if created, false if duplicate.
191
220
  */
192
- createRelation(relation) {
193
- try {
194
- const stmt = this.prepare(`
195
- INSERT INTO relations (from_entity, to_entity, relation_type, created_at, metadata)
196
- VALUES (?, ?, ?, ?, ?)
197
- `);
198
- stmt.run(
199
- relation.from,
200
- relation.to,
201
- relation.relationType,
202
- relation.createdAt,
203
- JSON.stringify(relation.metadata)
204
- );
221
+ async createRelation(relation) {
222
+ return this.withLock(async () => {
223
+ const key = this.relationKey(relation.from, relation.to, relation.relationType);
224
+ if (this.data.relations.some(r => this.relationKey(r.from, r.to, r.relationType) === key)) {
225
+ return false;
226
+ }
227
+
228
+ this.data.relations.push(relation.toJSON());
229
+ await this.save();
205
230
  return true;
206
- } catch (err) {
207
- if (err.code === "SQLITE_CONSTRAINT") return false;
208
- throw err;
209
- }
231
+ });
232
+ }
233
+
234
+ /**
235
+ * Generate unique key for relation
236
+ */
237
+ relationKey(from, to, type) {
238
+ return `${from}|${to}|${type}`;
210
239
  }
211
240
 
212
241
  /**
213
242
  * Get relations with optional filters
214
243
  */
215
244
  getRelations({ fromEntity, toEntity, relationType } = {}) {
216
- let sql = "SELECT * FROM relations WHERE 1=1";
217
- const params = [];
245
+ let results = this.data.relations.map(r => new Relation(r));
218
246
 
219
247
  if (fromEntity) {
220
- sql += " AND from_entity = ?";
221
- params.push(fromEntity);
248
+ results = results.filter(r => r.from === fromEntity);
222
249
  }
223
250
  if (toEntity) {
224
- sql += " AND to_entity = ?";
225
- params.push(toEntity);
251
+ results = results.filter(r => r.to === toEntity);
226
252
  }
227
253
  if (relationType) {
228
- sql += " AND relation_type = ?";
229
- params.push(relationType);
254
+ results = results.filter(r => r.relationType === relationType);
230
255
  }
231
256
 
232
- const stmt = this.prepare(sql);
233
- const rows = stmt.all(...params);
234
- return rows.map(row => new Relation({
235
- from: row.from_entity,
236
- to: row.to_entity,
237
- relationType: row.relation_type,
238
- createdAt: row.created_at,
239
- metadata: JSON.parse(row.metadata),
240
- }));
257
+ return results;
241
258
  }
242
259
 
243
260
  /**
244
261
  * Get all relations
245
262
  */
246
263
  getAllRelations() {
247
- return this.getRelations();
264
+ return this.data.relations.map(r => new Relation(r));
248
265
  }
249
266
 
250
267
  /**
251
268
  * Check if a relation exists
252
269
  */
253
270
  relationExists(fromEntity, toEntity, relationType) {
254
- const stmt = this.prepare(
255
- "SELECT 1 FROM relations WHERE from_entity = ? AND to_entity = ? AND relation_type = ?"
271
+ return this.data.relations.some(
272
+ r => r.from === fromEntity && r.to === toEntity && r.relationType === relationType
256
273
  );
257
- return stmt.get(fromEntity, toEntity, relationType) !== undefined;
258
274
  }
259
275
 
260
276
  /**
261
277
  * Delete a specific relation
262
278
  */
263
- deleteRelation(fromEntity, toEntity, relationType) {
264
- const stmt = this.prepare(
265
- "DELETE FROM relations WHERE from_entity = ? AND to_entity = ? AND relation_type = ?"
266
- );
267
- return stmt.run(fromEntity, toEntity, relationType).changes > 0;
279
+ async deleteRelation(fromEntity, toEntity, relationType) {
280
+ return this.withLock(async () => {
281
+ const before = this.data.relations.length;
282
+ this.data.relations = this.data.relations.filter(
283
+ r => !(r.from === fromEntity && r.to === toEntity && r.relationType === relationType)
284
+ );
285
+
286
+ if (this.data.relations.length < before) {
287
+ await this.save();
288
+ return true;
289
+ }
290
+ return false;
291
+ });
268
292
  }
269
293
 
270
294
  /**
271
295
  * Delete multiple relations
272
296
  */
273
- deleteRelations(relations) {
297
+ async deleteRelations(relations) {
274
298
  let count = 0;
275
299
  for (const [fromEntity, toEntity, relationType] of relations) {
276
- if (this.deleteRelation(fromEntity, toEntity, relationType)) count++;
300
+ if (await this.deleteRelation(fromEntity, toEntity, relationType)) count++;
277
301
  }
278
302
  return count;
279
303
  }
@@ -284,22 +308,13 @@ export class Storage {
284
308
  * Search entities by name, type, or observations
285
309
  */
286
310
  searchEntities(query) {
287
- const pattern = `%${query}%`;
288
- const stmt = this.prepare(`
289
- SELECT DISTINCT e.* FROM entities e
290
- WHERE e.name LIKE ?
291
- OR e.entity_type LIKE ?
292
- OR e.observations LIKE ?
293
- ORDER BY e.created_at DESC
294
- `);
295
- const rows = stmt.all(pattern, pattern, pattern);
296
- return rows.map(row => new Entity({
297
- name: row.name,
298
- entityType: row.entity_type,
299
- observations: JSON.parse(row.observations),
300
- createdAt: row.created_at,
301
- metadata: JSON.parse(row.metadata),
302
- }));
311
+ const lowerQuery = query.toLowerCase();
312
+ return this.getAllEntities().filter(e => {
313
+ if (e.name.toLowerCase().includes(lowerQuery)) return true;
314
+ if (e.entityType.toLowerCase().includes(lowerQuery)) return true;
315
+ if (e.observations.some(o => o.toLowerCase().includes(lowerQuery))) return true;
316
+ return false;
317
+ });
303
318
  }
304
319
 
305
320
  /**
@@ -308,47 +323,120 @@ export class Storage {
308
323
  getRelatedEntities(entityName) {
309
324
  const result = { connected: [], incoming: [], outgoing: [] };
310
325
 
311
- // Get connected entity names
312
- const outgoingStmt = this.prepare(
313
- "SELECT DISTINCT to_entity FROM relations WHERE from_entity = ?"
314
- );
315
- const incomingStmt = this.prepare(
316
- "SELECT DISTINCT from_entity FROM relations WHERE to_entity = ?"
317
- );
318
-
319
- const outgoing = outgoingStmt.all(entityName);
320
- const incoming = incomingStmt.all(entityName);
321
-
322
- const connectedNames = new Set([
323
- ...outgoing.map(r => r.to_entity),
324
- ...incoming.map(r => r.from_entity)
325
- ]);
326
+ const connectedNames = new Set();
327
+
328
+ for (const rel of this.data.relations) {
329
+ if (rel.from === entityName) {
330
+ connectedNames.add(rel.to);
331
+ result.outgoing.push(rel.to);
332
+ }
333
+ if (rel.to === entityName) {
334
+ connectedNames.add(rel.from);
335
+ result.incoming.push(rel.from);
336
+ }
337
+ }
326
338
 
327
339
  for (const name of connectedNames) {
328
340
  const entity = this.getEntity(name);
329
- if (entity) result.connected.push(entity);
341
+ if (entity) {
342
+ result.connected.push(entity);
343
+ }
330
344
  }
331
345
 
332
346
  return result;
333
347
  }
334
348
 
335
349
  /**
336
- * Close database connection
350
+ * Close storage
337
351
  */
338
- close() {
339
- if (this.db) {
340
- this.db.close();
341
- this.db = null;
342
- }
352
+ async close() {
353
+ await this.save();
343
354
  }
344
355
  }
345
356
 
346
- // Export singleton instance
357
+ // Sync wrapper for async operations
347
358
  let storageInstance = null;
348
359
 
349
360
  export function getStorage(dbPath) {
350
361
  if (!storageInstance) {
351
362
  storageInstance = new Storage(dbPath);
363
+ // Initialize asynchronously but don't wait
364
+ storageInstance.init().catch(console.error);
352
365
  }
353
366
  return storageInstance;
354
367
  }
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
+ }