@levalicious/server-memory 0.0.11 → 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/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 +711 -404
- 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 +135 -24
- package/package.json +4 -3
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.');
|