collective-memory-mcp 0.2.0 → 0.3.1

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 +150 -188
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.1",
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,84 @@
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
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
7
8
  import path from "path";
8
9
  import os from "os";
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
+ * Simple file-based storage
16
17
  */
17
18
  export class Storage {
18
19
  constructor(dbPath = DB_PATH) {
19
20
  this.dbPath = dbPath;
20
- this.db = null;
21
+ this.data = null;
22
+ // Initialize synchronously
21
23
  this.init();
22
24
  }
23
25
 
24
26
  /**
25
- * Initialize database and create schema
27
+ * Initialize storage (synchronous)
26
28
  */
27
29
  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
- `);
30
+ try {
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
50
+ this.data = {
51
+ entities: {},
52
+ relations: [],
53
+ version: "1.0",
54
+ };
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Save data synchronously
60
+ */
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);
68
+ }
65
69
  }
66
70
 
67
71
  /**
68
- * Prepare statement helper
72
+ * Save data asynchronously
69
73
  */
70
- prepare(sql) {
71
- return this.db.prepare(sql);
74
+ async save() {
75
+ const dir = path.dirname(this.dbPath);
76
+ try {
77
+ await fs.mkdir(dir, { recursive: true });
78
+ } catch {
79
+ // Ignore
80
+ }
81
+ await fs.writeFile(this.dbPath, JSON.stringify(this.data, null, 2), "utf-8");
72
82
  }
73
83
 
74
84
  // ========== Entity Operations ==========
@@ -76,40 +86,22 @@ export class Storage {
76
86
  /**
77
87
  * Create a new entity. Returns true if created, false if duplicate.
78
88
  */
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
- );
92
- return true;
93
- } catch (err) {
94
- if (err.code === "SQLITE_CONSTRAINT") return false;
95
- throw err;
89
+ async createEntity(entity) {
90
+ if (this.data.entities[entity.name]) {
91
+ return false;
96
92
  }
93
+ this.data.entities[entity.name] = entity.toJSON();
94
+ await this.save();
95
+ return true;
97
96
  }
98
97
 
99
98
  /**
100
99
  * Get an entity by name
101
100
  */
102
101
  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
- });
102
+ const data = this.data.entities[name];
103
+ if (data) {
104
+ return new Entity(data);
113
105
  }
114
106
  return null;
115
107
  }
@@ -118,68 +110,60 @@ export class Storage {
118
110
  * Get all entities
119
111
  */
120
112
  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
- }));
113
+ return Object.values(this.data.entities).map(data => new Entity(data));
130
114
  }
131
115
 
132
116
  /**
133
117
  * Check if an entity exists
134
118
  */
135
119
  entityExists(name) {
136
- const stmt = this.prepare("SELECT 1 FROM entities WHERE name = ?");
137
- return stmt.get(name) !== undefined;
120
+ return name in this.data.entities;
138
121
  }
139
122
 
140
123
  /**
141
- * Update entity observations
124
+ * Update entity
142
125
  */
143
- updateEntity(name, { observations, metadata } = {}) {
144
- const updates = [];
145
- const params = [];
126
+ async updateEntity(name, { observations, metadata } = {}) {
127
+ const entity = this.data.entities[name];
128
+ if (!entity) return false;
146
129
 
147
130
  if (observations !== undefined) {
148
- updates.push("observations = ?");
149
- params.push(JSON.stringify(observations));
131
+ entity.observations = observations;
150
132
  }
151
133
  if (metadata !== undefined) {
152
- updates.push("metadata = ?");
153
- params.push(JSON.stringify(metadata));
134
+ entity.metadata = metadata;
154
135
  }
155
136
 
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;
137
+ await this.save();
138
+ return true;
161
139
  }
162
140
 
163
141
  /**
164
142
  * Delete an entity and its relations
165
143
  */
166
- deleteEntity(name) {
167
- const deleteRelations = this.prepare(
168
- "DELETE FROM relations WHERE from_entity = ? OR to_entity = ?"
144
+ async deleteEntity(name) {
145
+ if (!this.data.entities[name]) {
146
+ return false;
147
+ }
148
+
149
+ delete this.data.entities[name];
150
+
151
+ // Remove relations involving this entity
152
+ this.data.relations = this.data.relations.filter(
153
+ r => r.from !== name && r.to !== name
169
154
  );
170
- deleteRelations.run(name, name);
171
155
 
172
- const deleteEntity = this.prepare("DELETE FROM entities WHERE name = ?");
173
- return deleteEntity.run(name).changes > 0;
156
+ await this.save();
157
+ return true;
174
158
  }
175
159
 
176
160
  /**
177
161
  * Delete multiple entities
178
162
  */
179
- deleteEntities(names) {
163
+ async deleteEntities(names) {
180
164
  let count = 0;
181
165
  for (const name of names) {
182
- if (this.deleteEntity(name)) count++;
166
+ if (await this.deleteEntity(name)) count++;
183
167
  }
184
168
  return count;
185
169
  }
@@ -189,91 +173,82 @@ export class Storage {
189
173
  /**
190
174
  * Create a new relation. Returns true if created, false if duplicate.
191
175
  */
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
- );
205
- return true;
206
- } catch (err) {
207
- if (err.code === "SQLITE_CONSTRAINT") return false;
208
- throw err;
176
+ async createRelation(relation) {
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;
209
180
  }
181
+
182
+ this.data.relations.push(relation.toJSON());
183
+ await this.save();
184
+ return true;
185
+ }
186
+
187
+ /**
188
+ * Generate unique key for relation
189
+ */
190
+ relationKey(from, to, type) {
191
+ return `${from}|${to}|${type}`;
210
192
  }
211
193
 
212
194
  /**
213
195
  * Get relations with optional filters
214
196
  */
215
197
  getRelations({ fromEntity, toEntity, relationType } = {}) {
216
- let sql = "SELECT * FROM relations WHERE 1=1";
217
- const params = [];
198
+ let results = this.data.relations.map(r => new Relation(r));
218
199
 
219
200
  if (fromEntity) {
220
- sql += " AND from_entity = ?";
221
- params.push(fromEntity);
201
+ results = results.filter(r => r.from === fromEntity);
222
202
  }
223
203
  if (toEntity) {
224
- sql += " AND to_entity = ?";
225
- params.push(toEntity);
204
+ results = results.filter(r => r.to === toEntity);
226
205
  }
227
206
  if (relationType) {
228
- sql += " AND relation_type = ?";
229
- params.push(relationType);
207
+ results = results.filter(r => r.relationType === relationType);
230
208
  }
231
209
 
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
- }));
210
+ return results;
241
211
  }
242
212
 
243
213
  /**
244
214
  * Get all relations
245
215
  */
246
216
  getAllRelations() {
247
- return this.getRelations();
217
+ return this.data.relations.map(r => new Relation(r));
248
218
  }
249
219
 
250
220
  /**
251
221
  * Check if a relation exists
252
222
  */
253
223
  relationExists(fromEntity, toEntity, relationType) {
254
- const stmt = this.prepare(
255
- "SELECT 1 FROM relations WHERE from_entity = ? AND to_entity = ? AND relation_type = ?"
224
+ return this.data.relations.some(
225
+ r => r.from === fromEntity && r.to === toEntity && r.relationType === relationType
256
226
  );
257
- return stmt.get(fromEntity, toEntity, relationType) !== undefined;
258
227
  }
259
228
 
260
229
  /**
261
230
  * Delete a specific relation
262
231
  */
263
- deleteRelation(fromEntity, toEntity, relationType) {
264
- const stmt = this.prepare(
265
- "DELETE FROM relations WHERE from_entity = ? AND to_entity = ? AND relation_type = ?"
232
+ async deleteRelation(fromEntity, toEntity, relationType) {
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)
266
236
  );
267
- return stmt.run(fromEntity, toEntity, relationType).changes > 0;
237
+
238
+ if (this.data.relations.length < before) {
239
+ await this.save();
240
+ return true;
241
+ }
242
+ return false;
268
243
  }
269
244
 
270
245
  /**
271
246
  * Delete multiple relations
272
247
  */
273
- deleteRelations(relations) {
248
+ async deleteRelations(relations) {
274
249
  let count = 0;
275
250
  for (const [fromEntity, toEntity, relationType] of relations) {
276
- if (this.deleteRelation(fromEntity, toEntity, relationType)) count++;
251
+ if (await this.deleteRelation(fromEntity, toEntity, relationType)) count++;
277
252
  }
278
253
  return count;
279
254
  }
@@ -284,22 +259,13 @@ export class Storage {
284
259
  * Search entities by name, type, or observations
285
260
  */
286
261
  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
- }));
262
+ const lowerQuery = query.toLowerCase();
263
+ return this.getAllEntities().filter(e => {
264
+ if (e.name.toLowerCase().includes(lowerQuery)) return true;
265
+ if (e.entityType.toLowerCase().includes(lowerQuery)) return true;
266
+ if (e.observations.some(o => o.toLowerCase().includes(lowerQuery))) return true;
267
+ return false;
268
+ });
303
269
  }
304
270
 
305
271
  /**
@@ -308,42 +274,38 @@ export class Storage {
308
274
  getRelatedEntities(entityName) {
309
275
  const result = { connected: [], incoming: [], outgoing: [] };
310
276
 
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
- ]);
277
+ const connectedNames = new Set();
278
+
279
+ for (const rel of this.data.relations) {
280
+ if (rel.from === entityName) {
281
+ connectedNames.add(rel.to);
282
+ result.outgoing.push(rel.to);
283
+ }
284
+ if (rel.to === entityName) {
285
+ connectedNames.add(rel.from);
286
+ result.incoming.push(rel.from);
287
+ }
288
+ }
326
289
 
327
290
  for (const name of connectedNames) {
328
291
  const entity = this.getEntity(name);
329
- if (entity) result.connected.push(entity);
292
+ if (entity) {
293
+ result.connected.push(entity);
294
+ }
330
295
  }
331
296
 
332
297
  return result;
333
298
  }
334
299
 
335
300
  /**
336
- * Close database connection
301
+ * Close storage
337
302
  */
338
- close() {
339
- if (this.db) {
340
- this.db.close();
341
- this.db = null;
342
- }
303
+ async close() {
304
+ await this.save();
343
305
  }
344
306
  }
345
307
 
346
- // Export singleton instance
308
+ // Singleton instance
347
309
  let storageInstance = null;
348
310
 
349
311
  export function getStorage(dbPath) {