@levalicious/server-memory 0.0.10 → 0.0.12
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/README.md +41 -27
- package/dist/index.js +4 -0
- package/dist/scripts/migrate-jsonl.js +169 -0
- package/dist/scripts/verify-migration.js +39 -0
- package/dist/server.js +763 -536
- package/dist/src/graphfile.js +560 -0
- package/dist/src/memoryfile.js +121 -0
- package/dist/src/pagerank.js +78 -0
- package/dist/src/stringtable.js +373 -0
- package/dist/tests/concurrency.test.js +189 -0
- package/dist/tests/memory-server.test.js +225 -53
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -108,11 +108,12 @@ Example:
|
|
|
108
108
|
|
|
109
109
|
- **search_nodes**
|
|
110
110
|
- Search for nodes using a regex pattern
|
|
111
|
-
- Input:
|
|
112
|
-
|
|
113
|
-
-
|
|
114
|
-
-
|
|
115
|
-
-
|
|
111
|
+
- Input:
|
|
112
|
+
- `query` (string): Regex pattern to search
|
|
113
|
+
- `sortBy` (string, optional): Sort field ("mtime", "obsMtime", or "name")
|
|
114
|
+
- `sortDir` (string, optional): Sort direction ("asc" or "desc")
|
|
115
|
+
- `entityCursor` (number, optional), `relationCursor` (number, optional)
|
|
116
|
+
- Searches across entity names, types, and observation content
|
|
116
117
|
- Returns matching entities and their relations (paginated)
|
|
117
118
|
|
|
118
119
|
- **open_nodes_filtered**
|
|
@@ -132,9 +133,15 @@ Example:
|
|
|
132
133
|
- Silently skips non-existent nodes (paginated)
|
|
133
134
|
|
|
134
135
|
- **get_neighbors**
|
|
135
|
-
- Get neighboring entities connected to a specific entity within a given depth
|
|
136
|
-
- Input:
|
|
137
|
-
|
|
136
|
+
- Get names of neighboring entities connected to a specific entity within a given depth
|
|
137
|
+
- Input:
|
|
138
|
+
- `entityName` (string): The entity to find neighbors for
|
|
139
|
+
- `depth` (number, default: 1): Maximum traversal depth
|
|
140
|
+
- `sortBy` (string, optional): Sort field ("mtime", "obsMtime", or "name")
|
|
141
|
+
- `sortDir` (string, optional): Sort direction ("asc" or "desc")
|
|
142
|
+
- `cursor` (number, optional): Pagination cursor
|
|
143
|
+
- Returns neighbor names with timestamps (paginated)
|
|
144
|
+
- Use `open_nodes` to get full entity data for neighbors
|
|
138
145
|
|
|
139
146
|
- **find_path**
|
|
140
147
|
- Find a path between two entities in the knowledge graph
|
|
@@ -143,7 +150,11 @@ Example:
|
|
|
143
150
|
|
|
144
151
|
- **get_entities_by_type**
|
|
145
152
|
- Get all entities of a specific type
|
|
146
|
-
- Input:
|
|
153
|
+
- Input:
|
|
154
|
+
- `entityType` (string): Type to filter by
|
|
155
|
+
- `sortBy` (string, optional): Sort field ("mtime", "obsMtime", or "name")
|
|
156
|
+
- `sortDir` (string, optional): Sort direction ("asc" or "desc")
|
|
157
|
+
- `cursor` (number, optional)
|
|
147
158
|
- Returns all entities matching the specified type (paginated)
|
|
148
159
|
|
|
149
160
|
- **get_entity_types**
|
|
@@ -163,8 +174,11 @@ Example:
|
|
|
163
174
|
|
|
164
175
|
- **get_orphaned_entities**
|
|
165
176
|
- Get entities that have no relations (orphaned entities)
|
|
166
|
-
- Input:
|
|
167
|
-
|
|
177
|
+
- Input:
|
|
178
|
+
- `strict` (boolean, default: false): If true, returns entities not connected to 'Self' entity
|
|
179
|
+
- `sortBy` (string, optional): Sort field ("mtime", "obsMtime", or "name")
|
|
180
|
+
- `sortDir` (string, optional): Sort direction ("asc" or "desc")
|
|
181
|
+
- `cursor` (number, optional)
|
|
168
182
|
- Returns entities with no connections (paginated)
|
|
169
183
|
|
|
170
184
|
- **validate_graph**
|
|
@@ -172,22 +186,22 @@ Example:
|
|
|
172
186
|
- No input required
|
|
173
187
|
- Returns missing entities referenced in relations and observation limit violations
|
|
174
188
|
|
|
175
|
-
- **
|
|
176
|
-
-
|
|
177
|
-
- Input:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
-
|
|
185
|
-
-
|
|
186
|
-
|
|
187
|
-
-
|
|
188
|
-
|
|
189
|
-
-
|
|
190
|
-
-
|
|
189
|
+
- **decode_timestamp**
|
|
190
|
+
- Decode a millisecond timestamp to human-readable UTC format
|
|
191
|
+
- Input:
|
|
192
|
+
- `timestamp` (number, optional): Millisecond timestamp to decode. If omitted, returns current time
|
|
193
|
+
- `relative` (boolean, optional): If true, include relative time (e.g., "3 days ago")
|
|
194
|
+
- Returns timestamp, ISO 8601 string, formatted UTC string, and optional relative time
|
|
195
|
+
- Useful for interpreting `mtime`/`obsMtime` values from entities
|
|
196
|
+
|
|
197
|
+
- **random_walk**
|
|
198
|
+
- Perform a random walk from a starting entity, following random relations
|
|
199
|
+
- Input:
|
|
200
|
+
- `start` (string): Name of the entity to start the walk from
|
|
201
|
+
- `depth` (number, default: 3): Number of hops to take
|
|
202
|
+
- `seed` (string, optional): Seed for reproducible walks
|
|
203
|
+
- Returns the terminal entity name and the path taken
|
|
204
|
+
- Useful for serendipitous exploration of the knowledge graph
|
|
191
205
|
|
|
192
206
|
- **sequentialthinking**
|
|
193
207
|
- Record a thought in the knowledge graph
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import { createServer } from "./server.js";
|
|
4
|
+
// Workaround: Node 24 segfaults on exit when any N-API addon is loaded,
|
|
5
|
+
// even a bare no-op module. This is a confirmed Node bug, not ours.
|
|
6
|
+
// Force a clean exit to avoid the cosmetic segfault.
|
|
7
|
+
// process.on('exit', () => { process._exit(0); });
|
|
4
8
|
const server = createServer();
|
|
5
9
|
const transport = new StdioServerTransport();
|
|
6
10
|
server.connect(transport).catch((error) => {
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* migrate-jsonl.ts — Convert a JSONL knowledge graph to binary (GraphFile + StringTable).
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx tsx scripts/migrate-jsonl.ts [path/to/memory.json]
|
|
7
|
+
*
|
|
8
|
+
* If no path given, defaults to ~/.local/share/memory/vscode.json
|
|
9
|
+
*
|
|
10
|
+
* Creates:
|
|
11
|
+
* <base>.graph — binary graph store
|
|
12
|
+
* <base>.strings — binary string table
|
|
13
|
+
*
|
|
14
|
+
* The original .json file is NOT modified or deleted.
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
import * as readline from 'readline';
|
|
19
|
+
import { StringTable } from '../src/stringtable.js';
|
|
20
|
+
import { GraphFile, DIR_FORWARD, DIR_BACKWARD } from '../src/graphfile.js';
|
|
21
|
+
async function migrate(jsonlPath) {
|
|
22
|
+
const dir = path.dirname(jsonlPath);
|
|
23
|
+
const base = path.basename(jsonlPath, path.extname(jsonlPath));
|
|
24
|
+
const graphPath = path.join(dir, `${base}.graph`);
|
|
25
|
+
const strPath = path.join(dir, `${base}.strings`);
|
|
26
|
+
// Safety: don't clobber existing binary files
|
|
27
|
+
if (fs.existsSync(graphPath) || fs.existsSync(strPath)) {
|
|
28
|
+
console.error(`ERROR: Binary files already exist:\n ${graphPath}\n ${strPath}`);
|
|
29
|
+
console.error('Delete them first if you want to re-migrate.');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
console.log(`Source: ${jsonlPath}`);
|
|
33
|
+
console.log(`Target: ${graphPath}`);
|
|
34
|
+
console.log(` ${strPath}`);
|
|
35
|
+
console.log();
|
|
36
|
+
// --- Pass 1: Parse JSONL, collect entities and relations ---
|
|
37
|
+
const entities = [];
|
|
38
|
+
const relations = [];
|
|
39
|
+
let lineNum = 0;
|
|
40
|
+
let parseErrors = 0;
|
|
41
|
+
const fileStream = fs.createReadStream(jsonlPath, { encoding: 'utf-8' });
|
|
42
|
+
const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
43
|
+
for await (const line of rl) {
|
|
44
|
+
lineNum++;
|
|
45
|
+
const trimmed = line.trim();
|
|
46
|
+
if (!trimmed)
|
|
47
|
+
continue;
|
|
48
|
+
try {
|
|
49
|
+
const obj = JSON.parse(trimmed);
|
|
50
|
+
if (obj.type === 'entity') {
|
|
51
|
+
entities.push(obj);
|
|
52
|
+
}
|
|
53
|
+
else if (obj.type === 'relation') {
|
|
54
|
+
relations.push(obj);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
parseErrors++;
|
|
59
|
+
if (parseErrors <= 5) {
|
|
60
|
+
console.warn(` WARN: parse error on line ${lineNum}: ${e.message}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
console.log(`Parsed: ${entities.length} entities, ${relations.length} relations`);
|
|
65
|
+
if (parseErrors > 0) {
|
|
66
|
+
console.warn(` (${parseErrors} lines had parse errors — skipped)`);
|
|
67
|
+
}
|
|
68
|
+
// --- Pass 2: Build binary store ---
|
|
69
|
+
// Start with a generous initial size to reduce remaps.
|
|
70
|
+
// Rough estimate: 64B per entity + ~200B string overhead per entity + 24B per adj entry
|
|
71
|
+
const estimatedSize = Math.max(65536, entities.length * 300 + relations.length * 100);
|
|
72
|
+
const st = new StringTable(strPath, estimatedSize);
|
|
73
|
+
const gf = new GraphFile(graphPath, st, estimatedSize);
|
|
74
|
+
// Create all entities first, build name→offset map
|
|
75
|
+
const nameToOffset = new Map();
|
|
76
|
+
let created = 0;
|
|
77
|
+
let skippedDuplicates = 0;
|
|
78
|
+
for (const e of entities) {
|
|
79
|
+
if (nameToOffset.has(e.name)) {
|
|
80
|
+
skippedDuplicates++;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const mtime = BigInt(e.mtime ?? 0);
|
|
84
|
+
const obsMtime = BigInt(e.obsMtime ?? 0);
|
|
85
|
+
const rec = gf.createEntity(e.name, e.entityType, mtime, obsMtime);
|
|
86
|
+
// Add observations (max 2)
|
|
87
|
+
const obs = e.observations.slice(0, 2);
|
|
88
|
+
for (const o of obs) {
|
|
89
|
+
// Truncate to 140 chars if needed
|
|
90
|
+
const truncated = o.length > 140 ? o.substring(0, 140) : o;
|
|
91
|
+
gf.addObservation(rec.offset, truncated, obsMtime);
|
|
92
|
+
}
|
|
93
|
+
// Fix timestamps (addObservation clobbers mtime)
|
|
94
|
+
if (obs.length > 0) {
|
|
95
|
+
const updated = gf.readEntity(rec.offset);
|
|
96
|
+
updated.mtime = mtime;
|
|
97
|
+
updated.obsMtime = obsMtime;
|
|
98
|
+
gf.updateEntity(updated);
|
|
99
|
+
}
|
|
100
|
+
nameToOffset.set(e.name, rec.offset);
|
|
101
|
+
created++;
|
|
102
|
+
if (created % 1000 === 0) {
|
|
103
|
+
process.stdout.write(` Entities: ${created}/${entities.length}\r`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
console.log(` Entities: ${created} created, ${skippedDuplicates} duplicates skipped`);
|
|
107
|
+
// Create all relations
|
|
108
|
+
let relCreated = 0;
|
|
109
|
+
let relSkipped = 0;
|
|
110
|
+
for (const r of relations) {
|
|
111
|
+
const fromOffset = nameToOffset.get(r.from);
|
|
112
|
+
const toOffset = nameToOffset.get(r.to);
|
|
113
|
+
if (fromOffset === undefined || toOffset === undefined) {
|
|
114
|
+
relSkipped++;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const mtime = BigInt(r.mtime ?? 0);
|
|
118
|
+
const relTypeId = Number(st.intern(r.relationType));
|
|
119
|
+
// Forward edge on 'from'
|
|
120
|
+
const forwardEntry = {
|
|
121
|
+
targetOffset: toOffset,
|
|
122
|
+
direction: DIR_FORWARD,
|
|
123
|
+
relTypeId,
|
|
124
|
+
mtime,
|
|
125
|
+
};
|
|
126
|
+
gf.addEdge(fromOffset, forwardEntry);
|
|
127
|
+
// Backward edge on 'to' (intern again to bump refcount)
|
|
128
|
+
const relTypeId2 = Number(st.intern(r.relationType));
|
|
129
|
+
const backwardEntry = {
|
|
130
|
+
targetOffset: fromOffset,
|
|
131
|
+
direction: DIR_BACKWARD,
|
|
132
|
+
relTypeId: relTypeId2,
|
|
133
|
+
mtime,
|
|
134
|
+
};
|
|
135
|
+
gf.addEdge(toOffset, backwardEntry);
|
|
136
|
+
relCreated++;
|
|
137
|
+
if (relCreated % 1000 === 0) {
|
|
138
|
+
process.stdout.write(` Relations: ${relCreated}/${relations.length}\r`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
console.log(` Relations: ${relCreated} created, ${relSkipped} skipped (missing endpoints)`);
|
|
142
|
+
// Sync and close
|
|
143
|
+
gf.sync();
|
|
144
|
+
st.sync();
|
|
145
|
+
// Report sizes
|
|
146
|
+
const graphSize = fs.statSync(graphPath).size;
|
|
147
|
+
const strSize = fs.statSync(strPath).size;
|
|
148
|
+
const jsonlSize = fs.statSync(jsonlPath).size;
|
|
149
|
+
console.log();
|
|
150
|
+
console.log(`File sizes:`);
|
|
151
|
+
console.log(` JSONL: ${(jsonlSize / 1024 / 1024).toFixed(2)} MB`);
|
|
152
|
+
console.log(` Graph: ${(graphSize / 1024 / 1024).toFixed(2)} MB`);
|
|
153
|
+
console.log(` Strings: ${(strSize / 1024 / 1024).toFixed(2)} MB`);
|
|
154
|
+
console.log(` Binary total: ${((graphSize + strSize) / 1024 / 1024).toFixed(2)} MB`);
|
|
155
|
+
gf.close();
|
|
156
|
+
st.close();
|
|
157
|
+
console.log();
|
|
158
|
+
console.log('Migration complete. Original JSONL file preserved.');
|
|
159
|
+
}
|
|
160
|
+
// --- Main ---
|
|
161
|
+
const inputPath = process.argv[2] || path.join(process.env.HOME || process.env.USERPROFILE || '.', '.local', 'share', 'memory', 'vscode.json');
|
|
162
|
+
if (!fs.existsSync(inputPath)) {
|
|
163
|
+
console.error(`ERROR: File not found: ${inputPath}`);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
migrate(inputPath).catch(err => {
|
|
167
|
+
console.error('Migration failed:', err);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Quick verification that binary files are readable after migration.
|
|
4
|
+
*/
|
|
5
|
+
import { StringTable } from '../src/stringtable.js';
|
|
6
|
+
import { GraphFile, DIR_FORWARD } from '../src/graphfile.js';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
const inputPath = process.argv[2] || path.join(process.env.HOME || '.', '.local', 'share', 'memory', 'vscode.json');
|
|
9
|
+
const dir = path.dirname(inputPath);
|
|
10
|
+
const base = path.basename(inputPath, path.extname(inputPath));
|
|
11
|
+
const graphPath = path.join(dir, `${base}.graph`);
|
|
12
|
+
const strPath = path.join(dir, `${base}.strings`);
|
|
13
|
+
const st = new StringTable(strPath);
|
|
14
|
+
const gf = new GraphFile(graphPath, st);
|
|
15
|
+
const offsets = gf.getAllEntityOffsets();
|
|
16
|
+
console.log(`Entity count: ${offsets.length}`);
|
|
17
|
+
// Sample first 5 entities
|
|
18
|
+
console.log('\nSample entities:');
|
|
19
|
+
for (const off of offsets.slice(0, 5)) {
|
|
20
|
+
const rec = gf.readEntity(off);
|
|
21
|
+
const name = st.get(BigInt(rec.nameId));
|
|
22
|
+
const type = st.get(BigInt(rec.typeId));
|
|
23
|
+
const obs = [];
|
|
24
|
+
if (rec.obs0Id)
|
|
25
|
+
obs.push(st.get(BigInt(rec.obs0Id)));
|
|
26
|
+
if (rec.obs1Id)
|
|
27
|
+
obs.push(st.get(BigInt(rec.obs1Id)));
|
|
28
|
+
console.log(` ${name} [${type}] obs=${obs.length} mtime=${Number(rec.mtime)}`);
|
|
29
|
+
}
|
|
30
|
+
// Count total relations
|
|
31
|
+
let relCount = 0;
|
|
32
|
+
for (const off of offsets) {
|
|
33
|
+
const edges = gf.getEdges(off);
|
|
34
|
+
relCount += edges.filter(e => e.direction === DIR_FORWARD).length;
|
|
35
|
+
}
|
|
36
|
+
console.log(`\nRelation count (forward edges): ${relCount}`);
|
|
37
|
+
gf.close();
|
|
38
|
+
st.close();
|
|
39
|
+
console.log('\nVerification passed.');
|