@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.
- package/dist/server.js +88 -58
- 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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
existingRelation.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
r.
|
|
121
|
-
|
|
122
|
-
|
|
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.
|
|
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",
|