@aeriondyseti/vector-memory-mcp 2.3.0-rc.1 → 2.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/package.json +1 -1
- package/server/core/migrations.ts +90 -0
- package/server/index.ts +4 -37
- package/server/transports/http/server.ts +1 -29
- package/server/transports/mcp/resources.ts +8 -149
- package/scripts/migrate-from-lancedb.ts +0 -56
- package/server/migration.ts +0 -203
package/package.json
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite";
|
|
2
|
+
import type { EmbeddingsService } from "./embeddings.service.js";
|
|
3
|
+
import { serializeVector } from "./sqlite-utils.js";
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Pre-migration step: remove vec0 virtual table entries from sqlite_master
|
|
@@ -113,3 +115,91 @@ export function runMigrations(db: Database): void {
|
|
|
113
115
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_conversation_role ON conversation_history(role)`);
|
|
114
116
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_conversation_created_at ON conversation_history(created_at)`);
|
|
115
117
|
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Backfill missing vectors in memories_vec and conversation_history_vec.
|
|
121
|
+
*
|
|
122
|
+
* After the vec0-to-BLOB migration, existing rows may lack vector embeddings.
|
|
123
|
+
* This re-embeds their content and inserts into the _vec tables.
|
|
124
|
+
* Idempotent: skips rows that already have vectors. Fast no-op when fully backfilled.
|
|
125
|
+
*/
|
|
126
|
+
export async function backfillVectors(
|
|
127
|
+
db: Database,
|
|
128
|
+
embeddings: EmbeddingsService,
|
|
129
|
+
): Promise<void> {
|
|
130
|
+
// ── Memories ──────────────────────────────────────────────────────
|
|
131
|
+
// Catch both missing rows (v.id IS NULL) and corrupt 0-byte BLOBs
|
|
132
|
+
const missingMemories = db
|
|
133
|
+
.prepare(
|
|
134
|
+
`SELECT m.id, m.content, json_extract(m.metadata, '$.type') AS type
|
|
135
|
+
FROM memories m
|
|
136
|
+
LEFT JOIN memories_vec v ON m.id = v.id
|
|
137
|
+
WHERE v.id IS NULL OR length(v.vector) = 0`,
|
|
138
|
+
)
|
|
139
|
+
.all() as Array<{ id: string; content: string; type: string | null }>;
|
|
140
|
+
|
|
141
|
+
if (missingMemories.length > 0) {
|
|
142
|
+
console.error(
|
|
143
|
+
`[vector-memory-mcp] Backfilling vectors for ${missingMemories.length} memories...`,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const insertVec = db.prepare(
|
|
147
|
+
"INSERT OR REPLACE INTO memories_vec (id, vector) VALUES (?, ?)",
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const zeroVector = serializeVector(
|
|
151
|
+
new Array(embeddings.dimension).fill(0),
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
for (const row of missingMemories) {
|
|
155
|
+
// Waypoints use a zero vector (not semantically searched)
|
|
156
|
+
const blob =
|
|
157
|
+
row.type === "waypoint"
|
|
158
|
+
? zeroVector
|
|
159
|
+
: serializeVector(await embeddings.embed(row.content));
|
|
160
|
+
|
|
161
|
+
insertVec.run(row.id, blob);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
console.error(
|
|
165
|
+
`[vector-memory-mcp] Backfilled ${missingMemories.length} memory vectors`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Conversation history ──────────────────────────────────────────
|
|
170
|
+
const missingConvos = db
|
|
171
|
+
.prepare(
|
|
172
|
+
`SELECT c.id, c.content
|
|
173
|
+
FROM conversation_history c
|
|
174
|
+
LEFT JOIN conversation_history_vec v ON c.id = v.id
|
|
175
|
+
WHERE v.id IS NULL OR length(v.vector) = 0`,
|
|
176
|
+
)
|
|
177
|
+
.all() as Array<{ id: string; content: string }>;
|
|
178
|
+
|
|
179
|
+
if (missingConvos.length > 0) {
|
|
180
|
+
console.error(
|
|
181
|
+
`[vector-memory-mcp] Backfilling vectors for ${missingConvos.length} conversation chunks...`,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const insertConvoVec = db.prepare(
|
|
185
|
+
"INSERT OR REPLACE INTO conversation_history_vec (id, vector) VALUES (?, ?)",
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
for (let i = 0; i < missingConvos.length; i++) {
|
|
189
|
+
const row = missingConvos[i];
|
|
190
|
+
const vec = serializeVector(await embeddings.embed(row.content));
|
|
191
|
+
insertConvoVec.run(row.id, vec);
|
|
192
|
+
|
|
193
|
+
// Log progress every 100 chunks
|
|
194
|
+
if ((i + 1) % 100 === 0) {
|
|
195
|
+
console.error(
|
|
196
|
+
`[vector-memory-mcp] ...${i + 1}/${missingConvos.length} conversation chunks`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.error(
|
|
202
|
+
`[vector-memory-mcp] Backfilled ${missingConvos.length} conversation vectors`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
package/server/index.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { loadConfig, parseCliArgs } from "./config/index.js";
|
|
4
4
|
import { connectToDatabase } from "./core/connection.js";
|
|
5
|
+
import { backfillVectors } from "./core/migrations.js";
|
|
5
6
|
import { MemoryRepository } from "./core/memory.repository.js";
|
|
6
7
|
import { ConversationRepository } from "./core/conversation.repository.js";
|
|
7
8
|
import { EmbeddingsService } from "./core/embeddings.service.js";
|
|
@@ -9,26 +10,6 @@ import { MemoryService } from "./core/memory.service.js";
|
|
|
9
10
|
import { ConversationHistoryService } from "./core/conversation.service.js";
|
|
10
11
|
import { startServer } from "./transports/mcp/server.js";
|
|
11
12
|
import { startHttpServer } from "./transports/http/server.js";
|
|
12
|
-
import { isLanceDbDirectory, migrate, formatMigrationSummary } from "./migration.js";
|
|
13
|
-
|
|
14
|
-
async function runMigrate(args: string[]): Promise<void> {
|
|
15
|
-
const overrides = parseCliArgs(args.slice(1)); // skip "migrate"
|
|
16
|
-
const config = loadConfig(overrides);
|
|
17
|
-
|
|
18
|
-
const source = config.dbPath;
|
|
19
|
-
const target = source.endsWith(".sqlite") ? source.replace(/\.sqlite$/, "-migrated.sqlite") : source + ".sqlite";
|
|
20
|
-
|
|
21
|
-
if (!isLanceDbDirectory(source)) {
|
|
22
|
-
console.error(
|
|
23
|
-
`[vector-memory-mcp] No LanceDB data found at ${source}\n` +
|
|
24
|
-
` Nothing to migrate. The server will create a fresh SQLite database on startup.`
|
|
25
|
-
);
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const result = await migrate({ source, target });
|
|
30
|
-
console.error(formatMigrationSummary(source, target, result));
|
|
31
|
-
}
|
|
32
13
|
|
|
33
14
|
async function main(): Promise<void> {
|
|
34
15
|
const args = process.argv.slice(2);
|
|
@@ -40,27 +21,10 @@ async function main(): Promise<void> {
|
|
|
40
21
|
return;
|
|
41
22
|
}
|
|
42
23
|
|
|
43
|
-
// Check for migrate command
|
|
44
|
-
if (args[0] === "migrate") {
|
|
45
|
-
await runMigrate(args);
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
24
|
// Parse CLI args and load config
|
|
50
25
|
const overrides = parseCliArgs(args);
|
|
51
26
|
const config = loadConfig(overrides);
|
|
52
27
|
|
|
53
|
-
// Detect legacy LanceDB data and warn
|
|
54
|
-
if (isLanceDbDirectory(config.dbPath)) {
|
|
55
|
-
console.error(
|
|
56
|
-
`[vector-memory-mcp] ⚠️ Legacy LanceDB data detected at ${config.dbPath}\n` +
|
|
57
|
-
` Your data must be migrated to the new SQLite format.\n` +
|
|
58
|
-
` Run: vector-memory-mcp migrate\n` +
|
|
59
|
-
` Or: bun run server/index.ts migrate\n`
|
|
60
|
-
);
|
|
61
|
-
process.exit(1);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
28
|
// Initialize database
|
|
65
29
|
const db = connectToDatabase(config.dbPath);
|
|
66
30
|
|
|
@@ -69,6 +33,9 @@ async function main(): Promise<void> {
|
|
|
69
33
|
const embeddings = new EmbeddingsService(config.embeddingModel, config.embeddingDimension);
|
|
70
34
|
const memoryService = new MemoryService(repository, embeddings);
|
|
71
35
|
|
|
36
|
+
// Backfill any missing vectors (e.g. after vec0-to-BLOB migration)
|
|
37
|
+
await backfillVectors(db, embeddings);
|
|
38
|
+
|
|
72
39
|
if (config.pluginMode) {
|
|
73
40
|
console.error("[vector-memory-mcp] Running in plugin mode");
|
|
74
41
|
}
|
|
@@ -8,7 +8,7 @@ import type { Config } from "../../config/index.js";
|
|
|
8
8
|
import { isDeleted } from "../../core/memory.js";
|
|
9
9
|
import { createMcpRoutes } from "./mcp-transport.js";
|
|
10
10
|
import type { Memory, SearchIntent } from "../../core/memory.js";
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Check if a port is available by attempting to bind to it
|
|
@@ -245,34 +245,6 @@ export function createHttpApp(memoryService: MemoryService, config: Config): Hon
|
|
|
245
245
|
}
|
|
246
246
|
});
|
|
247
247
|
|
|
248
|
-
// Migrate from external memory database
|
|
249
|
-
app.post("/migrate", async (c) => {
|
|
250
|
-
try {
|
|
251
|
-
const body = await c.req.json().catch(() => null);
|
|
252
|
-
if (!body || typeof body !== "object") {
|
|
253
|
-
return c.json({ error: "Invalid or missing JSON body" }, 400);
|
|
254
|
-
}
|
|
255
|
-
const source = body.source;
|
|
256
|
-
|
|
257
|
-
if (!source || typeof source !== "string") {
|
|
258
|
-
return c.json({ error: "Missing or invalid 'source' field" }, 400);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const repository = memoryService.getRepository();
|
|
262
|
-
const migrationService = new MigrationService(
|
|
263
|
-
repository,
|
|
264
|
-
memoryService.getEmbeddings(),
|
|
265
|
-
repository.getDb(),
|
|
266
|
-
);
|
|
267
|
-
|
|
268
|
-
const result = await migrationService.migrate(source);
|
|
269
|
-
return c.json(result);
|
|
270
|
-
} catch (error) {
|
|
271
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
272
|
-
return c.json({ error: message }, 500);
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
|
|
276
248
|
// Get single memory
|
|
277
249
|
app.get("/memories/:id", async (c) => {
|
|
278
250
|
try {
|
|
@@ -1,152 +1,11 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
\`\`\`
|
|
11
|
-
POST http://<host>:<port>/migrate
|
|
12
|
-
Content-Type: application/json
|
|
13
|
-
|
|
14
|
-
{ "source": "/absolute/path/to/source/database" }
|
|
15
|
-
\`\`\`
|
|
16
|
-
|
|
17
|
-
## Discovering the Server Port
|
|
18
|
-
|
|
19
|
-
The HTTP server writes a lockfile at \`.vector-memory/server.lock\` in the
|
|
20
|
-
project's working directory. Read it to discover the current port:
|
|
21
|
-
|
|
22
|
-
\`\`\`json
|
|
23
|
-
{ "port": 3271, "pid": 12345 }
|
|
24
|
-
\`\`\`
|
|
25
|
-
|
|
26
|
-
## Supported Source Formats
|
|
27
|
-
|
|
28
|
-
The endpoint auto-detects the source format from the path provided.
|
|
29
|
-
|
|
30
|
-
### 1. LanceDB Directory
|
|
31
|
-
Provide the path to a LanceDB data directory (contains \`.lance\` files or
|
|
32
|
-
\`_versions\`/\`_indices\` subdirectories). Both memories and conversation
|
|
33
|
-
history are imported.
|
|
34
|
-
|
|
35
|
-
\`\`\`json
|
|
36
|
-
{ "source": "/path/to/project/.vector-memory" }
|
|
37
|
-
\`\`\`
|
|
38
|
-
|
|
39
|
-
### 2. Own SQLite (Current or Older Schema)
|
|
40
|
-
Provide the path to a \`.db\` file that was created by any version of
|
|
41
|
-
vector-memory-mcp. The migrator handles missing columns (e.g. \`usefulness\`,
|
|
42
|
-
\`access_count\`) by using sensible defaults. Both memories and conversation
|
|
43
|
-
history are imported.
|
|
44
|
-
|
|
45
|
-
\`\`\`json
|
|
46
|
-
{ "source": "/path/to/old-project/.vector-memory/memories.db" }
|
|
47
|
-
\`\`\`
|
|
48
|
-
|
|
49
|
-
### 3. CCCMemory SQLite
|
|
50
|
-
Provide the path to a CCCMemory database. The migrator extracts from the
|
|
51
|
-
\`decisions\`, \`mistakes\`, \`methodologies\`, \`research_findings\`,
|
|
52
|
-
\`solution_patterns\`, and \`working_memory\` tables. Each record is tagged
|
|
53
|
-
with \`source_type: "cccmemory"\` and the appropriate \`memory_type\` in
|
|
54
|
-
metadata.
|
|
55
|
-
|
|
56
|
-
\`\`\`json
|
|
57
|
-
{ "source": "/path/to/cccmemory.db" }
|
|
58
|
-
\`\`\`
|
|
59
|
-
|
|
60
|
-
### 4. MCP Memory Service SQLite
|
|
61
|
-
Provide the path to an mcp-memory-service database. Memories with
|
|
62
|
-
\`deleted_at IS NULL\` are imported. Tags and memory type are preserved in
|
|
63
|
-
metadata.
|
|
64
|
-
|
|
65
|
-
\`\`\`json
|
|
66
|
-
{ "source": "/path/to/mcp-memory-service.db" }
|
|
67
|
-
\`\`\`
|
|
68
|
-
|
|
69
|
-
### 5. MIF JSON (Shodh Memory Interchange Format)
|
|
70
|
-
Provide the path to a \`.json\` file exported from Shodh Memory. The file must
|
|
71
|
-
contain a top-level \`memories\` array. Memory type, tags, entities, and source
|
|
72
|
-
metadata are preserved.
|
|
73
|
-
|
|
74
|
-
\`\`\`json
|
|
75
|
-
{ "source": "/path/to/export.mif.json" }
|
|
76
|
-
\`\`\`
|
|
77
|
-
|
|
78
|
-
## Response
|
|
79
|
-
|
|
80
|
-
The endpoint returns a JSON summary upon completion:
|
|
81
|
-
|
|
82
|
-
\`\`\`json
|
|
83
|
-
{
|
|
84
|
-
"source": "/path/to/source",
|
|
85
|
-
"format": "own-sqlite",
|
|
86
|
-
"memoriesImported": 142,
|
|
87
|
-
"memoriesSkipped": 3,
|
|
88
|
-
"conversationsImported": 0,
|
|
89
|
-
"conversationsSkipped": 0,
|
|
90
|
-
"errors": [],
|
|
91
|
-
"durationMs": 8320
|
|
92
|
-
}
|
|
93
|
-
\`\`\`
|
|
94
|
-
|
|
95
|
-
- **memoriesImported**: Number of new memories written to the database.
|
|
96
|
-
- **memoriesSkipped**: Records skipped because a memory with the same ID
|
|
97
|
-
already exists (safe for idempotent re-runs).
|
|
98
|
-
- **conversationsImported / conversationsSkipped**: Same, for conversation
|
|
99
|
-
history chunks (LanceDB and own-sqlite formats only).
|
|
100
|
-
- **errors**: Per-record errors that did not abort the migration.
|
|
101
|
-
- **durationMs**: Wall-clock time for the entire operation.
|
|
102
|
-
|
|
103
|
-
## Important Notes
|
|
104
|
-
|
|
105
|
-
- **Re-embedding**: All content is re-embedded regardless of the source format.
|
|
106
|
-
This ensures vector consistency with the server's current model but means the
|
|
107
|
-
operation can take time for large databases (~50ms per record).
|
|
108
|
-
- **Idempotent**: Running the same migration twice is safe. Duplicate IDs are
|
|
109
|
-
skipped.
|
|
110
|
-
- **Non-destructive**: The source database is opened read-only and is never
|
|
111
|
-
modified.
|
|
112
|
-
- **Batched writes**: Records are inserted in batches of 100 within
|
|
113
|
-
transactions. If the process is interrupted, already-committed batches are
|
|
114
|
-
durable.
|
|
115
|
-
- **Error isolation**: A single bad record does not abort the migration. Check
|
|
116
|
-
the \`errors\` array in the response for any per-record failures.
|
|
117
|
-
|
|
118
|
-
## Workflow Example
|
|
119
|
-
|
|
120
|
-
1. Locate the source database file or directory.
|
|
121
|
-
2. Read \`.vector-memory/server.lock\` to get the port.
|
|
122
|
-
3. Send the migrate request:
|
|
123
|
-
\`\`\`bash
|
|
124
|
-
curl -X POST http://127.0.0.1:3271/migrate \\
|
|
125
|
-
-H "Content-Type: application/json" \\
|
|
126
|
-
-d '{"source": "/path/to/old/memories.db"}'
|
|
127
|
-
\`\`\`
|
|
128
|
-
4. Inspect the response summary.
|
|
129
|
-
5. Verify imported memories with a search:
|
|
130
|
-
\`\`\`bash
|
|
131
|
-
curl -X POST http://127.0.0.1:3271/search \\
|
|
132
|
-
-H "Content-Type: application/json" \\
|
|
133
|
-
-d '{"query": "test query", "limit": 5}'
|
|
134
|
-
\`\`\`
|
|
135
|
-
`;
|
|
136
|
-
|
|
137
|
-
export const resources = [
|
|
138
|
-
{
|
|
139
|
-
uri: "vector-memory://guides/migrate",
|
|
140
|
-
name: "Migration Guide",
|
|
141
|
-
description:
|
|
142
|
-
"How to use the POST /migrate HTTP endpoint to import memories from external database formats (LanceDB, older SQLite, CCCMemory, MCP Memory Service, MIF JSON) into the running vector-memory instance.",
|
|
143
|
-
mimeType: "text/markdown",
|
|
144
|
-
},
|
|
145
|
-
];
|
|
146
|
-
|
|
147
|
-
const RESOURCE_CONTENT: Record<string, string> = {
|
|
148
|
-
"vector-memory://guides/migrate": MIGRATE_GUIDE,
|
|
149
|
-
};
|
|
1
|
+
export const resources: Array<{
|
|
2
|
+
uri: string;
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
mimeType: string;
|
|
6
|
+
}> = [];
|
|
7
|
+
|
|
8
|
+
const RESOURCE_CONTENT: Record<string, string> = {};
|
|
150
9
|
|
|
151
10
|
export function readResource(uri: string): {
|
|
152
11
|
contents: Array<{ uri: string; mimeType: string; text: string }>;
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* Standalone migration script: LanceDB → SQLite (sqlite-vec)
|
|
4
|
-
*
|
|
5
|
-
* This is a thin wrapper around server/migration.ts for direct invocation.
|
|
6
|
-
* The preferred way to migrate is `vector-memory-mcp migrate`.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* bun scripts/migrate-from-lancedb.ts [--source <lancedb-dir>] [--target <sqlite-file>]
|
|
10
|
-
*
|
|
11
|
-
* Defaults:
|
|
12
|
-
* --source .vector-memory/memories.db (the old LanceDB directory)
|
|
13
|
-
* --target .vector-memory/memories.db.sqlite (new SQLite file)
|
|
14
|
-
*
|
|
15
|
-
* @deprecated Use `vector-memory-mcp migrate` instead. This script will be
|
|
16
|
-
* removed in the next major version.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { migrate, formatMigrationSummary } from "../server/migration.js";
|
|
20
|
-
|
|
21
|
-
function parseArgs(): { source: string; target: string } {
|
|
22
|
-
const args = process.argv.slice(2);
|
|
23
|
-
let source = ".vector-memory/memories.db";
|
|
24
|
-
let target = ".vector-memory/memories.db.sqlite";
|
|
25
|
-
|
|
26
|
-
for (let i = 0; i < args.length; i++) {
|
|
27
|
-
if (args[i] === "--source" && args[i + 1]) source = args[++i];
|
|
28
|
-
else if (args[i] === "--target" && args[i + 1]) target = args[++i];
|
|
29
|
-
else if (args[i] === "--help" || args[i] === "-h") {
|
|
30
|
-
console.log(`
|
|
31
|
-
Usage: bun scripts/migrate-from-lancedb.ts [options]
|
|
32
|
-
|
|
33
|
-
Prefer: vector-memory-mcp migrate
|
|
34
|
-
|
|
35
|
-
Options:
|
|
36
|
-
--source <path> LanceDB directory (default: .vector-memory/memories.db)
|
|
37
|
-
--target <path> SQLite output file (default: .vector-memory/memories.db.sqlite)
|
|
38
|
-
--help Show this help
|
|
39
|
-
`);
|
|
40
|
-
process.exit(0);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return { source, target };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function main() {
|
|
48
|
-
const { source, target } = parseArgs();
|
|
49
|
-
const result = await migrate({ source, target });
|
|
50
|
-
console.error(formatMigrationSummary(source, target, result));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
main().catch((err) => {
|
|
54
|
-
console.error("❌ Migration failed:", err.message ?? err);
|
|
55
|
-
process.exit(1);
|
|
56
|
-
});
|
package/server/migration.ts
DELETED
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LanceDB -> SQLite migration logic.
|
|
3
|
-
*
|
|
4
|
-
* Reads LanceDB data in a child process (scripts/lancedb-extract.ts) to avoid
|
|
5
|
-
* a native symbol collision between @lancedb/lancedb and bun:sqlite.
|
|
6
|
-
* The extracted JSON is then written to SQLite in-process.
|
|
7
|
-
*
|
|
8
|
-
* @deprecated Will be removed in the next major version once LanceDB
|
|
9
|
-
* support is dropped.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { existsSync, statSync } from "fs";
|
|
13
|
-
import { resolve, dirname } from "path";
|
|
14
|
-
import { fileURLToPath } from "url";
|
|
15
|
-
import { connectToDatabase } from "./core/connection.js";
|
|
16
|
-
import { serializeVector } from "./core/sqlite-utils.js";
|
|
17
|
-
|
|
18
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
-
|
|
20
|
-
// ── Detection ───────────────────────────────────────────────────────
|
|
21
|
-
|
|
22
|
-
export function isLanceDbDirectory(dbPath: string): boolean {
|
|
23
|
-
return existsSync(dbPath) && statSync(dbPath).isDirectory();
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// ── Types ───────────────────────────────────────────────────────────
|
|
27
|
-
|
|
28
|
-
export interface MigrateOptions {
|
|
29
|
-
source: string;
|
|
30
|
-
target: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface MigrateResult {
|
|
34
|
-
memoriesMigrated: number;
|
|
35
|
-
conversationChunksMigrated: number;
|
|
36
|
-
outputSizeMB: string;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
interface ExtractedData {
|
|
40
|
-
memories: Array<{
|
|
41
|
-
id: string;
|
|
42
|
-
content: string;
|
|
43
|
-
metadata: string;
|
|
44
|
-
vector: number[];
|
|
45
|
-
created_at: number;
|
|
46
|
-
updated_at: number;
|
|
47
|
-
last_accessed: number | null;
|
|
48
|
-
superseded_by: string | null;
|
|
49
|
-
usefulness: number;
|
|
50
|
-
access_count: number;
|
|
51
|
-
}>;
|
|
52
|
-
conversations: Array<{
|
|
53
|
-
id: string;
|
|
54
|
-
content: string;
|
|
55
|
-
metadata: string;
|
|
56
|
-
vector: number[];
|
|
57
|
-
created_at: number;
|
|
58
|
-
session_id: string;
|
|
59
|
-
role: string;
|
|
60
|
-
message_index_start: number;
|
|
61
|
-
message_index_end: number;
|
|
62
|
-
project: string;
|
|
63
|
-
}>;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// ── Migration ───────────────────────────────────────────────────────
|
|
67
|
-
|
|
68
|
-
export async function migrate(opts: MigrateOptions): Promise<MigrateResult> {
|
|
69
|
-
const { source, target } = opts;
|
|
70
|
-
|
|
71
|
-
if (!existsSync(source)) {
|
|
72
|
-
throw new Error(`Source not found: ${source}`);
|
|
73
|
-
}
|
|
74
|
-
if (!statSync(source).isDirectory()) {
|
|
75
|
-
throw new Error(`Source is not a directory (expected LanceDB): ${source}`);
|
|
76
|
-
}
|
|
77
|
-
if (existsSync(target)) {
|
|
78
|
-
throw new Error(
|
|
79
|
-
`Target already exists: ${target}\n Delete it first or choose a different target path.`
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
console.error(`📂 Source (LanceDB): ${source}`);
|
|
84
|
-
console.error(`📄 Target (SQLite): ${target}`);
|
|
85
|
-
console.error();
|
|
86
|
-
|
|
87
|
-
// Phase 1: Extract data from LanceDB in a subprocess.
|
|
88
|
-
// This avoids a native symbol collision between @lancedb/lancedb and bun:sqlite.
|
|
89
|
-
const extractScript = resolve(__dirname, "..", "scripts", "lancedb-extract.ts");
|
|
90
|
-
const proc = Bun.spawn(["bun", extractScript, source], {
|
|
91
|
-
stdout: "pipe",
|
|
92
|
-
stderr: "inherit",
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
const output = await new Response(proc.stdout).text();
|
|
96
|
-
const exitCode = await proc.exited;
|
|
97
|
-
|
|
98
|
-
if (exitCode !== 0) {
|
|
99
|
-
throw new Error(`LanceDB extraction failed (exit code ${exitCode})`);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const data: ExtractedData = JSON.parse(output);
|
|
103
|
-
|
|
104
|
-
// Phase 2: Write to SQLite (no LanceDB in this process).
|
|
105
|
-
const sqliteDb = connectToDatabase(target);
|
|
106
|
-
|
|
107
|
-
let memoriesMigrated = 0;
|
|
108
|
-
let conversationChunksMigrated = 0;
|
|
109
|
-
|
|
110
|
-
if (data.memories.length > 0) {
|
|
111
|
-
console.error(`\n🧠 Writing ${data.memories.length} memories to SQLite...`);
|
|
112
|
-
|
|
113
|
-
const insertMain = sqliteDb.prepare(
|
|
114
|
-
`INSERT OR REPLACE INTO memories
|
|
115
|
-
(id, content, metadata, created_at, updated_at, superseded_by, usefulness, access_count, last_accessed)
|
|
116
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
117
|
-
);
|
|
118
|
-
const deleteVec = sqliteDb.prepare(`DELETE FROM memories_vec WHERE id = ?`);
|
|
119
|
-
const insertVec = sqliteDb.prepare(
|
|
120
|
-
`INSERT INTO memories_vec (id, vector) VALUES (?, ?)`
|
|
121
|
-
);
|
|
122
|
-
const insertFts = sqliteDb.prepare(
|
|
123
|
-
`INSERT OR REPLACE INTO memories_fts (id, content) VALUES (?, ?)`
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
const tx = sqliteDb.transaction(() => {
|
|
127
|
-
for (const row of data.memories) {
|
|
128
|
-
insertMain.run(
|
|
129
|
-
row.id, row.content, row.metadata,
|
|
130
|
-
row.created_at, row.updated_at,
|
|
131
|
-
row.superseded_by, row.usefulness,
|
|
132
|
-
row.access_count, row.last_accessed,
|
|
133
|
-
);
|
|
134
|
-
if (row.vector.length > 0) {
|
|
135
|
-
deleteVec.run(row.id);
|
|
136
|
-
insertVec.run(row.id, serializeVector(row.vector));
|
|
137
|
-
}
|
|
138
|
-
insertFts.run(row.id, row.content);
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
tx();
|
|
142
|
-
memoriesMigrated = data.memories.length;
|
|
143
|
-
console.error(` ✅ ${memoriesMigrated} memories migrated`);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (data.conversations.length > 0) {
|
|
147
|
-
console.error(`\n💬 Writing ${data.conversations.length} conversation chunks to SQLite...`);
|
|
148
|
-
|
|
149
|
-
const insertMain = sqliteDb.prepare(
|
|
150
|
-
`INSERT OR REPLACE INTO conversation_history
|
|
151
|
-
(id, content, metadata, created_at, session_id, role, message_index_start, message_index_end, project)
|
|
152
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
153
|
-
);
|
|
154
|
-
const deleteVec = sqliteDb.prepare(`DELETE FROM conversation_history_vec WHERE id = ?`);
|
|
155
|
-
const insertVec = sqliteDb.prepare(
|
|
156
|
-
`INSERT INTO conversation_history_vec (id, vector) VALUES (?, ?)`
|
|
157
|
-
);
|
|
158
|
-
const insertFts = sqliteDb.prepare(
|
|
159
|
-
`INSERT OR REPLACE INTO conversation_history_fts (id, content) VALUES (?, ?)`
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
const tx = sqliteDb.transaction(() => {
|
|
163
|
-
for (const row of data.conversations) {
|
|
164
|
-
insertMain.run(
|
|
165
|
-
row.id, row.content, row.metadata,
|
|
166
|
-
row.created_at, row.session_id, row.role,
|
|
167
|
-
row.message_index_start, row.message_index_end, row.project,
|
|
168
|
-
);
|
|
169
|
-
if (row.vector.length > 0) {
|
|
170
|
-
deleteVec.run(row.id);
|
|
171
|
-
insertVec.run(row.id, serializeVector(row.vector));
|
|
172
|
-
}
|
|
173
|
-
insertFts.run(row.id, row.content);
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
tx();
|
|
177
|
-
conversationChunksMigrated = data.conversations.length;
|
|
178
|
-
console.error(` ✅ ${conversationChunksMigrated} conversation chunks migrated`);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
sqliteDb.close();
|
|
182
|
-
|
|
183
|
-
const { size } = statSync(target);
|
|
184
|
-
const outputSizeMB = (size / 1024 / 1024).toFixed(2);
|
|
185
|
-
|
|
186
|
-
return { memoriesMigrated, conversationChunksMigrated, outputSizeMB };
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
export function formatMigrationSummary(
|
|
190
|
-
source: string,
|
|
191
|
-
target: string,
|
|
192
|
-
result: MigrateResult,
|
|
193
|
-
): string {
|
|
194
|
-
return `
|
|
195
|
-
✅ Migration complete! (${result.outputSizeMB} MB)
|
|
196
|
-
${result.memoriesMigrated} memories, ${result.conversationChunksMigrated} conversation chunks
|
|
197
|
-
|
|
198
|
-
Next steps:
|
|
199
|
-
1. Backup: mv "${source}" "${source}.lance-backup"
|
|
200
|
-
2. Activate: mv "${target}" "${source}"
|
|
201
|
-
3. Restart your MCP server
|
|
202
|
-
`;
|
|
203
|
-
}
|