collective-memory-mcp 0.1.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.
package/src/storage.js ADDED
@@ -0,0 +1,442 @@
1
+ /**
2
+ * Storage layer for the Collective Memory System using JSON file.
3
+ * Pure JavaScript - no native dependencies required.
4
+ */
5
+
6
+ import { promises as fs } from "fs";
7
+ import path from "path";
8
+ import os from "os";
9
+ import { createHash } from "crypto";
10
+ import { Entity, Relation } from "./models.js";
11
+
12
+ const DB_DIR = path.join(os.homedir(), ".collective-memory");
13
+ const DB_PATH = path.join(DB_DIR, "memory.json");
14
+
15
+ /**
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
22
+ */
23
+ export class Storage {
24
+ constructor(dbPath = DB_PATH) {
25
+ this.dbPath = dbPath;
26
+ this.lockPath = LOCK_FILE;
27
+ this.data = null;
28
+ this.lockRetries = 10;
29
+ this.lockDelay = 50;
30
+ this.init();
31
+ }
32
+
33
+ /**
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
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
83
+ */
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
+ }
121
+ }
122
+
123
+ // ========== Entity Operations ==========
124
+
125
+ /**
126
+ * Create a new entity. Returns true if created, false if duplicate.
127
+ */
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();
135
+ return true;
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Get an entity by name
141
+ */
142
+ getEntity(name) {
143
+ const data = this.data.entities[name];
144
+ if (data) {
145
+ return new Entity(data);
146
+ }
147
+ return null;
148
+ }
149
+
150
+ /**
151
+ * Get all entities
152
+ */
153
+ getAllEntities() {
154
+ return Object.values(this.data.entities).map(data => new Entity(data));
155
+ }
156
+
157
+ /**
158
+ * Check if an entity exists
159
+ */
160
+ entityExists(name) {
161
+ return name in this.data.entities;
162
+ }
163
+
164
+ /**
165
+ * Update entity
166
+ */
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
+ });
182
+ }
183
+
184
+ /**
185
+ * Delete an entity and its relations
186
+ */
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];
194
+
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
+ });
203
+ }
204
+
205
+ /**
206
+ * Delete multiple entities
207
+ */
208
+ async deleteEntities(names) {
209
+ let count = 0;
210
+ for (const name of names) {
211
+ if (await this.deleteEntity(name)) count++;
212
+ }
213
+ return count;
214
+ }
215
+
216
+ // ========== Relation Operations ==========
217
+
218
+ /**
219
+ * Create a new relation. Returns true if created, false if duplicate.
220
+ */
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();
230
+ return true;
231
+ });
232
+ }
233
+
234
+ /**
235
+ * Generate unique key for relation
236
+ */
237
+ relationKey(from, to, type) {
238
+ return `${from}|${to}|${type}`;
239
+ }
240
+
241
+ /**
242
+ * Get relations with optional filters
243
+ */
244
+ getRelations({ fromEntity, toEntity, relationType } = {}) {
245
+ let results = this.data.relations.map(r => new Relation(r));
246
+
247
+ if (fromEntity) {
248
+ results = results.filter(r => r.from === fromEntity);
249
+ }
250
+ if (toEntity) {
251
+ results = results.filter(r => r.to === toEntity);
252
+ }
253
+ if (relationType) {
254
+ results = results.filter(r => r.relationType === relationType);
255
+ }
256
+
257
+ return results;
258
+ }
259
+
260
+ /**
261
+ * Get all relations
262
+ */
263
+ getAllRelations() {
264
+ return this.data.relations.map(r => new Relation(r));
265
+ }
266
+
267
+ /**
268
+ * Check if a relation exists
269
+ */
270
+ relationExists(fromEntity, toEntity, relationType) {
271
+ return this.data.relations.some(
272
+ r => r.from === fromEntity && r.to === toEntity && r.relationType === relationType
273
+ );
274
+ }
275
+
276
+ /**
277
+ * Delete a specific relation
278
+ */
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
+ });
292
+ }
293
+
294
+ /**
295
+ * Delete multiple relations
296
+ */
297
+ async deleteRelations(relations) {
298
+ let count = 0;
299
+ for (const [fromEntity, toEntity, relationType] of relations) {
300
+ if (await this.deleteRelation(fromEntity, toEntity, relationType)) count++;
301
+ }
302
+ return count;
303
+ }
304
+
305
+ // ========== Search ==========
306
+
307
+ /**
308
+ * Search entities by name, type, or observations
309
+ */
310
+ searchEntities(query) {
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
+ });
318
+ }
319
+
320
+ /**
321
+ * Get entities related to a given entity
322
+ */
323
+ getRelatedEntities(entityName) {
324
+ const result = { connected: [], incoming: [], outgoing: [] };
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
+ }
338
+
339
+ for (const name of connectedNames) {
340
+ const entity = this.getEntity(name);
341
+ if (entity) {
342
+ result.connected.push(entity);
343
+ }
344
+ }
345
+
346
+ return result;
347
+ }
348
+
349
+ /**
350
+ * Close storage
351
+ */
352
+ async close() {
353
+ await this.save();
354
+ }
355
+ }
356
+
357
+ // Sync wrapper for async operations
358
+ let storageInstance = null;
359
+
360
+ export function getStorage(dbPath) {
361
+ if (!storageInstance) {
362
+ storageInstance = new Storage(dbPath);
363
+ // Initialize asynchronously but don't wait
364
+ storageInstance.init().catch(console.error);
365
+ }
366
+ return storageInstance;
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
+ }
package/index.js DELETED
@@ -1,127 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * Collective Memory MCP Server - npx Wrapper
5
- *
6
- * This wrapper spawns the Python MCP server, making it available via npx:
7
- * npx collective-memory-mcp
8
- *
9
- * The Python package will be automatically installed on first run.
10
- */
11
-
12
- import { spawn, spawnSync } from 'child_process';
13
- import { fileURLToPath } from 'url';
14
- import { dirname, join } from 'path';
15
- import { existsSync, readFileSync } from 'fs';
16
- import { homedir } from 'os';
17
-
18
- const __filename = fileURLToPath(import.meta.url);
19
- const __dirname = dirname(__filename);
20
-
21
- // ANSI color codes for better output
22
- const colors = {
23
- reset: '\x1b[0m',
24
- red: '\x1b[31m',
25
- green: '\x1b[32m',
26
- yellow: '\x1b[33m',
27
- blue: '\x1b[34m',
28
- cyan: '\x1b[36m',
29
- };
30
-
31
- function log(message, color = 'reset') {
32
- console.error(`${colors[color]}${message}${colors.reset}`);
33
- }
34
-
35
- function findPython() {
36
- // Try different Python commands
37
- const pythonCommands = ['python3', 'python', 'python3.12', 'python3.11', 'python3.10'];
38
-
39
- for (const cmd of pythonCommands) {
40
- const result = spawnSync(cmd, ['--version'], { stdio: 'pipe' });
41
- if (result.status === 0) {
42
- return cmd;
43
- }
44
- }
45
-
46
- return null;
47
- }
48
-
49
- function getDbPath() {
50
- // Allow override via environment variable
51
- if (process.env.COLLECTIVE_MEMORY_DB_PATH) {
52
- return process.env.COLLECTIVE_MEMORY_DB_PATH;
53
- }
54
-
55
- // Default to ~/.collective-memory/memory.db
56
- return join(homedir(), '.collective-memory', 'memory.db');
57
- }
58
-
59
- async function main() {
60
- const pythonCmd = findPython();
61
-
62
- if (!pythonCmd) {
63
- log('Error: Python 3.10+ is required but not found.', 'red');
64
- log('Please install Python from https://python.org', 'yellow');
65
- process.exit(1);
66
- }
67
-
68
- // Check if collective_memory module is available
69
- const checkModule = spawnSync(pythonCmd, ['-c', 'import collective_memory'], {
70
- stdio: 'pipe'
71
- });
72
-
73
- if (checkModule.status !== 0) {
74
- log('Collective Memory MCP Server not found. Installing...', 'yellow');
75
- log('', 'reset');
76
-
77
- const installArgs = ['-m', 'pip', 'install', '-U', 'collective-memory-mcp'];
78
- const install = spawn(pythonCmd, installArgs, {
79
- stdio: 'inherit'
80
- });
81
-
82
- install.on('close', (code) => {
83
- if (code !== 0) {
84
- log('', 'reset');
85
- log('Failed to install collective-memory-mcp Python package.', 'red');
86
- log('You can install it manually:', 'yellow');
87
- log(` ${pythonCmd} -m pip install collective-memory-mcp`, 'cyan');
88
- process.exit(1);
89
- }
90
-
91
- log('', 'reset');
92
- log('Installation complete. Starting server...', 'green');
93
- startServer(pythonCmd);
94
- });
95
- } else {
96
- startServer(pythonCmd);
97
- }
98
- }
99
-
100
- function startServer(pythonCmd) {
101
- const dbPath = getDbPath();
102
-
103
- // Spawn the Python MCP server
104
- const server = spawn(pythonCmd, ['-m', 'collective_memory'], {
105
- stdio: 'inherit',
106
- env: {
107
- ...process.env,
108
- COLLECTIVE_MEMORY_DB_PATH: dbPath,
109
- PYTHONUNBUFFERED: '1',
110
- }
111
- });
112
-
113
- server.on('error', (err) => {
114
- log(`Error starting server: ${err.message}`, 'red');
115
- process.exit(1);
116
- });
117
-
118
- server.on('exit', (code) => {
119
- process.exit(code ?? 0);
120
- });
121
- }
122
-
123
- // Handle signals
124
- process.on('SIGINT', () => process.exit(0));
125
- process.on('SIGTERM', () => process.exit(0));
126
-
127
- main();