@levalicious/server-memory 0.0.6 → 0.0.7

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/dist/server.js +88 -58
  2. package/package.json +4 -2
package/dist/server.js CHANGED
@@ -4,6 +4,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextpro
4
4
  import { promises as fs } from 'fs';
5
5
  import path from 'path';
6
6
  import { fileURLToPath } from 'url';
7
+ import lockfile from 'proper-lockfile';
7
8
  // Define memory file path using environment variable with fallback
8
9
  const defaultMemoryPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'memory.json');
9
10
  // If MEMORY_FILE_PATH is just a filename, put it in the same directory as the script
@@ -20,6 +21,22 @@ export class KnowledgeGraphManager {
20
21
  constructor(memoryFilePath = DEFAULT_MEMORY_FILE_PATH) {
21
22
  this.memoryFilePath = memoryFilePath;
22
23
  }
24
+ async withLock(fn) {
25
+ // Ensure file exists for locking
26
+ try {
27
+ await fs.access(this.memoryFilePath);
28
+ }
29
+ catch {
30
+ await fs.writeFile(this.memoryFilePath, "");
31
+ }
32
+ const release = await lockfile.lock(this.memoryFilePath, { retries: { retries: 5, minTimeout: 100 } });
33
+ try {
34
+ return await fn();
35
+ }
36
+ finally {
37
+ await release();
38
+ }
39
+ }
23
40
  async loadGraph() {
24
41
  try {
25
42
  const data = await fs.readFile(this.memoryFilePath, "utf-8");
@@ -45,81 +62,94 @@ export class KnowledgeGraphManager {
45
62
  ...graph.entities.map(e => JSON.stringify({ type: "entity", ...e })),
46
63
  ...graph.relations.map(r => JSON.stringify({ type: "relation", ...r })),
47
64
  ];
48
- await fs.writeFile(this.memoryFilePath, lines.join("\n"));
65
+ const content = lines.join("\n") + (lines.length > 0 ? "\n" : "");
66
+ await fs.writeFile(this.memoryFilePath, content);
49
67
  }
50
68
  async createEntities(entities) {
51
- const graph = await this.loadGraph();
52
- // Validate observation limits
53
- for (const entity of entities) {
54
- if (entity.observations.length > 2) {
55
- throw new Error(`Entity "${entity.name}" has ${entity.observations.length} observations. Maximum allowed is 2.`);
56
- }
57
- for (const obs of entity.observations) {
58
- if (obs.length > 140) {
59
- throw new Error(`Observation in entity "${entity.name}" exceeds 140 characters (${obs.length} chars): "${obs.substring(0, 50)}..."`);
69
+ return this.withLock(async () => {
70
+ const graph = await this.loadGraph();
71
+ // Validate observation limits
72
+ for (const entity of entities) {
73
+ if (entity.observations.length > 2) {
74
+ throw new Error(`Entity "${entity.name}" has ${entity.observations.length} observations. Maximum allowed is 2.`);
75
+ }
76
+ for (const obs of entity.observations) {
77
+ if (obs.length > 140) {
78
+ throw new Error(`Observation in entity "${entity.name}" exceeds 140 characters (${obs.length} chars): "${obs.substring(0, 50)}..."`);
79
+ }
60
80
  }
61
81
  }
62
- }
63
- const newEntities = entities.filter(e => !graph.entities.some(existingEntity => existingEntity.name === e.name));
64
- graph.entities.push(...newEntities);
65
- await this.saveGraph(graph);
66
- return newEntities;
82
+ const newEntities = entities.filter(e => !graph.entities.some(existingEntity => existingEntity.name === e.name));
83
+ graph.entities.push(...newEntities);
84
+ await this.saveGraph(graph);
85
+ return newEntities;
86
+ });
67
87
  }
68
88
  async createRelations(relations) {
69
- const graph = await this.loadGraph();
70
- const newRelations = relations.filter(r => !graph.relations.some(existingRelation => existingRelation.from === r.from &&
71
- existingRelation.to === r.to &&
72
- existingRelation.relationType === r.relationType));
73
- graph.relations.push(...newRelations);
74
- await this.saveGraph(graph);
75
- return newRelations;
89
+ return this.withLock(async () => {
90
+ const graph = await this.loadGraph();
91
+ const newRelations = relations.filter(r => !graph.relations.some(existingRelation => existingRelation.from === r.from &&
92
+ existingRelation.to === r.to &&
93
+ existingRelation.relationType === r.relationType));
94
+ graph.relations.push(...newRelations);
95
+ await this.saveGraph(graph);
96
+ return newRelations;
97
+ });
76
98
  }
77
99
  async addObservations(observations) {
78
- const graph = await this.loadGraph();
79
- const results = observations.map(o => {
80
- const entity = graph.entities.find(e => e.name === o.entityName);
81
- if (!entity) {
82
- throw new Error(`Entity with name ${o.entityName} not found`);
83
- }
84
- // Validate observation character limits
85
- for (const obs of o.contents) {
86
- if (obs.length > 140) {
87
- throw new Error(`Observation for "${o.entityName}" exceeds 140 characters (${obs.length} chars): "${obs.substring(0, 50)}..."`);
100
+ return this.withLock(async () => {
101
+ const graph = await this.loadGraph();
102
+ const results = observations.map(o => {
103
+ const entity = graph.entities.find(e => e.name === o.entityName);
104
+ if (!entity) {
105
+ throw new Error(`Entity with name ${o.entityName} not found`);
88
106
  }
89
- }
90
- const newObservations = o.contents.filter(content => !entity.observations.includes(content));
91
- // Validate total observation count
92
- if (entity.observations.length + newObservations.length > 2) {
93
- throw new Error(`Adding ${newObservations.length} observations to "${o.entityName}" would exceed limit of 2 (currently has ${entity.observations.length}).`);
94
- }
95
- entity.observations.push(...newObservations);
96
- return { entityName: o.entityName, addedObservations: newObservations };
107
+ // Validate observation character limits
108
+ for (const obs of o.contents) {
109
+ if (obs.length > 140) {
110
+ throw new Error(`Observation for "${o.entityName}" exceeds 140 characters (${obs.length} chars): "${obs.substring(0, 50)}..."`);
111
+ }
112
+ }
113
+ const newObservations = o.contents.filter(content => !entity.observations.includes(content));
114
+ // Validate total observation count
115
+ if (entity.observations.length + newObservations.length > 2) {
116
+ throw new Error(`Adding ${newObservations.length} observations to "${o.entityName}" would exceed limit of 2 (currently has ${entity.observations.length}).`);
117
+ }
118
+ entity.observations.push(...newObservations);
119
+ return { entityName: o.entityName, addedObservations: newObservations };
120
+ });
121
+ await this.saveGraph(graph);
122
+ return results;
97
123
  });
98
- await this.saveGraph(graph);
99
- return results;
100
124
  }
101
125
  async deleteEntities(entityNames) {
102
- const graph = await this.loadGraph();
103
- graph.entities = graph.entities.filter(e => !entityNames.includes(e.name));
104
- graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to));
105
- await this.saveGraph(graph);
126
+ return this.withLock(async () => {
127
+ const graph = await this.loadGraph();
128
+ graph.entities = graph.entities.filter(e => !entityNames.includes(e.name));
129
+ graph.relations = graph.relations.filter(r => !entityNames.includes(r.from) && !entityNames.includes(r.to));
130
+ await this.saveGraph(graph);
131
+ });
106
132
  }
107
133
  async deleteObservations(deletions) {
108
- const graph = await this.loadGraph();
109
- deletions.forEach(d => {
110
- const entity = graph.entities.find(e => e.name === d.entityName);
111
- if (entity) {
112
- entity.observations = entity.observations.filter(o => !d.observations.includes(o));
113
- }
134
+ return this.withLock(async () => {
135
+ const graph = await this.loadGraph();
136
+ deletions.forEach(d => {
137
+ const entity = graph.entities.find(e => e.name === d.entityName);
138
+ if (entity) {
139
+ entity.observations = entity.observations.filter(o => !d.observations.includes(o));
140
+ }
141
+ });
142
+ await this.saveGraph(graph);
114
143
  });
115
- await this.saveGraph(graph);
116
144
  }
117
145
  async deleteRelations(relations) {
118
- const graph = await this.loadGraph();
119
- graph.relations = graph.relations.filter(r => !relations.some(delRelation => r.from === delRelation.from &&
120
- r.to === delRelation.to &&
121
- r.relationType === delRelation.relationType));
122
- await this.saveGraph(graph);
146
+ return this.withLock(async () => {
147
+ const graph = await this.loadGraph();
148
+ graph.relations = graph.relations.filter(r => !relations.some(delRelation => r.from === delRelation.from &&
149
+ r.to === delRelation.to &&
150
+ r.relationType === delRelation.relationType));
151
+ await this.saveGraph(graph);
152
+ });
123
153
  }
124
154
  // Regex-based search function
125
155
  async searchNodes(query) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@levalicious/server-memory",
3
- "version": "0.0.6",
3
+ "version": "0.0.7",
4
4
  "description": "MCP server for enabling memory for Claude through a knowledge graph",
5
5
  "license": "MIT",
6
6
  "author": "Levalicious",
@@ -20,11 +20,13 @@
20
20
  "test": "NODE_OPTIONS='--experimental-vm-modules' jest"
21
21
  },
22
22
  "dependencies": {
23
- "@modelcontextprotocol/sdk": "1.22.0"
23
+ "@modelcontextprotocol/sdk": "1.22.0",
24
+ "proper-lockfile": "^4.1.2"
24
25
  },
25
26
  "devDependencies": {
26
27
  "@types/jest": "^30.0.0",
27
28
  "@types/node": "^24",
29
+ "@types/proper-lockfile": "^4.1.4",
28
30
  "jest": "^30.2.0",
29
31
  "shx": "^0.4.0",
30
32
  "ts-jest": "^29.4.5",