@aeriondyseti/vector-memory-mcp 2.2.6 → 2.3.0-dev.1
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 +5 -6
- package/server/core/embeddings.service.ts +95 -18
- package/server/core/migrations.ts +148 -0
- package/server/index.ts +4 -39
- package/server/transports/http/server.ts +1 -29
- package/server/transports/mcp/resources.ts +8 -149
- package/scripts/lancedb-extract.ts +0 -181
- package/scripts/migrate-from-lancedb.ts +0 -56
- package/scripts/smoke-test.ts +0 -699
- package/scripts/sync-version.ts +0 -35
- package/scripts/test-runner.ts +0 -76
- package/scripts/warmup.ts +0 -72
- package/server/migration.ts +0 -203
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aeriondyseti/vector-memory-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0-dev.1",
|
|
4
4
|
"description": "A zero-configuration RAG memory server for MCP clients",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "server/index.ts",
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"server",
|
|
12
|
-
"scripts",
|
|
13
12
|
"README.md",
|
|
14
13
|
"LICENSE"
|
|
15
14
|
],
|
|
@@ -47,18 +46,18 @@
|
|
|
47
46
|
],
|
|
48
47
|
"license": "MIT",
|
|
49
48
|
"dependencies": {
|
|
50
|
-
"@huggingface/
|
|
49
|
+
"@huggingface/tokenizers": "^0.1.3",
|
|
51
50
|
"@lancedb/lancedb": "^0.26.2",
|
|
52
51
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
53
52
|
"arg": "^5.0.2",
|
|
54
|
-
"hono": "^4.11.3"
|
|
53
|
+
"hono": "^4.11.3",
|
|
54
|
+
"onnxruntime-node": "^1.21.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/bun": "latest",
|
|
58
58
|
"typescript": "^5.0.0"
|
|
59
59
|
},
|
|
60
60
|
"trustedDependencies": [
|
|
61
|
-
"protobufjs"
|
|
62
|
-
"sharp"
|
|
61
|
+
"protobufjs"
|
|
63
62
|
]
|
|
64
63
|
}
|
|
@@ -1,9 +1,17 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as ort from "onnxruntime-node";
|
|
2
|
+
import { Tokenizer } from "@huggingface/tokenizers";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import { mkdir } from "fs/promises";
|
|
5
|
+
import { existsSync } from "fs";
|
|
6
|
+
|
|
7
|
+
const HF_CDN = "https://huggingface.co";
|
|
8
|
+
const MAX_SEQ_LENGTH = 512;
|
|
2
9
|
|
|
3
10
|
export class EmbeddingsService {
|
|
4
11
|
private modelName: string;
|
|
5
|
-
private
|
|
6
|
-
private
|
|
12
|
+
private session: ort.InferenceSession | null = null;
|
|
13
|
+
private tokenizer: Tokenizer | null = null;
|
|
14
|
+
private initPromise: Promise<void> | null = null;
|
|
7
15
|
private _dimension: number;
|
|
8
16
|
|
|
9
17
|
constructor(modelName: string, dimension: number) {
|
|
@@ -15,27 +23,71 @@ export class EmbeddingsService {
|
|
|
15
23
|
return this._dimension;
|
|
16
24
|
}
|
|
17
25
|
|
|
18
|
-
private async
|
|
19
|
-
if (this.
|
|
20
|
-
return this.extractor;
|
|
21
|
-
}
|
|
22
|
-
|
|
26
|
+
private async initialize(): Promise<void> {
|
|
27
|
+
if (this.session) return;
|
|
23
28
|
if (!this.initPromise) {
|
|
24
|
-
this.initPromise =
|
|
25
|
-
"feature-extraction",
|
|
26
|
-
this.modelName,
|
|
27
|
-
{ dtype: "fp32" } as any
|
|
28
|
-
) as Promise<FeatureExtractionPipeline>;
|
|
29
|
+
this.initPromise = this._init();
|
|
29
30
|
}
|
|
31
|
+
await this.initPromise;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private get cacheDir(): string {
|
|
35
|
+
const packageRoot = join(dirname(Bun.main), "..");
|
|
36
|
+
return join(packageRoot, ".cache", "models", this.modelName);
|
|
37
|
+
}
|
|
30
38
|
|
|
31
|
-
|
|
32
|
-
|
|
39
|
+
private async downloadIfMissing(fileName: string): Promise<string> {
|
|
40
|
+
const filePath = join(this.cacheDir, fileName);
|
|
41
|
+
if (existsSync(filePath)) return filePath;
|
|
42
|
+
|
|
43
|
+
const url = `${HF_CDN}/${this.modelName}/resolve/main/${fileName}`;
|
|
44
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
45
|
+
const response = await fetch(url);
|
|
46
|
+
if (!response.ok) throw new Error(`Failed to download ${url}: ${response.status}`);
|
|
47
|
+
const buffer = await response.arrayBuffer();
|
|
48
|
+
await Bun.write(filePath, buffer);
|
|
49
|
+
return filePath;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private async _init(): Promise<void> {
|
|
53
|
+
const modelPath = await this.downloadIfMissing("onnx/model.onnx");
|
|
54
|
+
const tokenizerJsonPath = await this.downloadIfMissing("tokenizer.json");
|
|
55
|
+
const tokenizerConfigPath = await this.downloadIfMissing("tokenizer_config.json");
|
|
56
|
+
|
|
57
|
+
this.session = await ort.InferenceSession.create(modelPath, {
|
|
58
|
+
executionProviders: ["cpu"],
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const tokenizerJson = await Bun.file(tokenizerJsonPath).json();
|
|
62
|
+
const tokenizerConfig = await Bun.file(tokenizerConfigPath).json();
|
|
63
|
+
this.tokenizer = new Tokenizer(tokenizerJson, tokenizerConfig);
|
|
33
64
|
}
|
|
34
65
|
|
|
35
66
|
async embed(text: string): Promise<number[]> {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
67
|
+
await this.initialize();
|
|
68
|
+
|
|
69
|
+
const encoded = this.tokenizer!.encode(text);
|
|
70
|
+
|
|
71
|
+
// Truncate to model's max sequence length
|
|
72
|
+
const seqLen = Math.min(encoded.ids.length, MAX_SEQ_LENGTH);
|
|
73
|
+
const ids = encoded.ids.slice(0, seqLen);
|
|
74
|
+
const mask = encoded.attention_mask.slice(0, seqLen);
|
|
75
|
+
|
|
76
|
+
const inputIds = BigInt64Array.from(ids.map(BigInt));
|
|
77
|
+
const attentionMask = BigInt64Array.from(mask.map(BigInt));
|
|
78
|
+
const tokenTypeIds = new BigInt64Array(seqLen); // zeros for single-sequence input
|
|
79
|
+
|
|
80
|
+
const feeds: Record<string, ort.Tensor> = {
|
|
81
|
+
input_ids: new ort.Tensor("int64", inputIds, [1, seqLen]),
|
|
82
|
+
attention_mask: new ort.Tensor("int64", attentionMask, [1, seqLen]),
|
|
83
|
+
token_type_ids: new ort.Tensor("int64", tokenTypeIds, [1, seqLen]),
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const output = await this.session!.run(feeds);
|
|
87
|
+
const lastHidden = output["last_hidden_state"];
|
|
88
|
+
|
|
89
|
+
const pooled = this.meanPool(lastHidden.data as Float32Array, mask, seqLen);
|
|
90
|
+
return this.normalize(pooled);
|
|
39
91
|
}
|
|
40
92
|
|
|
41
93
|
async embedBatch(texts: string[]): Promise<number[][]> {
|
|
@@ -45,4 +97,29 @@ export class EmbeddingsService {
|
|
|
45
97
|
}
|
|
46
98
|
return results;
|
|
47
99
|
}
|
|
100
|
+
|
|
101
|
+
private meanPool(data: Float32Array, mask: number[], seqLen: number): number[] {
|
|
102
|
+
const dim = this._dimension;
|
|
103
|
+
const pooled = new Array(dim).fill(0);
|
|
104
|
+
let maskSum = 0;
|
|
105
|
+
for (let t = 0; t < seqLen; t++) {
|
|
106
|
+
if (mask[t]) {
|
|
107
|
+
maskSum += 1;
|
|
108
|
+
for (let d = 0; d < dim; d++) {
|
|
109
|
+
pooled[d] += data[t * dim + d];
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
for (let d = 0; d < dim; d++) {
|
|
114
|
+
pooled[d] /= maskSum;
|
|
115
|
+
}
|
|
116
|
+
return pooled;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private normalize(vec: number[]): number[] {
|
|
120
|
+
let norm = 0;
|
|
121
|
+
for (const v of vec) norm += v * v;
|
|
122
|
+
norm = Math.sqrt(norm);
|
|
123
|
+
return vec.map(v => v / norm);
|
|
124
|
+
}
|
|
48
125
|
}
|
|
@@ -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,149 @@ 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
|
+
// Fast sentinel check: skip the LEFT JOIN queries entirely when backfill is done
|
|
131
|
+
const sentinel = db
|
|
132
|
+
.prepare("SELECT 1 FROM memories_vec LIMIT 1")
|
|
133
|
+
.get();
|
|
134
|
+
const memoriesExist = db.prepare("SELECT 1 FROM memories LIMIT 1").get();
|
|
135
|
+
const convosExist = db.prepare("SELECT 1 FROM conversation_history LIMIT 1").get();
|
|
136
|
+
|
|
137
|
+
// If vec tables have data and source tables have data, backfill is likely complete.
|
|
138
|
+
// Only run the expensive LEFT JOIN when there's reason to suspect gaps.
|
|
139
|
+
const convoSentinel = db
|
|
140
|
+
.prepare("SELECT 1 FROM conversation_history_vec LIMIT 1")
|
|
141
|
+
.get();
|
|
142
|
+
const mayNeedMemoryBackfill = memoriesExist && !sentinel;
|
|
143
|
+
const mayNeedConvoBackfill = convosExist && !convoSentinel;
|
|
144
|
+
|
|
145
|
+
// If both vec tables are populated, do a quick count check to confirm
|
|
146
|
+
if (!mayNeedMemoryBackfill && !mayNeedConvoBackfill) {
|
|
147
|
+
if (memoriesExist) {
|
|
148
|
+
const gap = db.prepare(
|
|
149
|
+
`SELECT 1 FROM memories m LEFT JOIN memories_vec v ON m.id = v.id
|
|
150
|
+
WHERE v.id IS NULL OR length(v.vector) = 0 LIMIT 1`,
|
|
151
|
+
).get();
|
|
152
|
+
if (!gap && convosExist) {
|
|
153
|
+
const convoGap = db.prepare(
|
|
154
|
+
`SELECT 1 FROM conversation_history c LEFT JOIN conversation_history_vec v ON c.id = v.id
|
|
155
|
+
WHERE v.id IS NULL OR length(v.vector) = 0 LIMIT 1`,
|
|
156
|
+
).get();
|
|
157
|
+
if (!convoGap) return;
|
|
158
|
+
} else if (!gap && !convosExist) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
return; // No data at all
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── Memories ──────────────────────────────────────────────────────
|
|
167
|
+
const missingMemories = db
|
|
168
|
+
.prepare(
|
|
169
|
+
`SELECT m.id, m.content, json_extract(m.metadata, '$.type') AS type
|
|
170
|
+
FROM memories m
|
|
171
|
+
LEFT JOIN memories_vec v ON m.id = v.id
|
|
172
|
+
WHERE v.id IS NULL OR length(v.vector) = 0`,
|
|
173
|
+
)
|
|
174
|
+
.all() as Array<{ id: string; content: string; type: string | null }>;
|
|
175
|
+
|
|
176
|
+
if (missingMemories.length > 0) {
|
|
177
|
+
console.error(
|
|
178
|
+
`[vector-memory-mcp] Backfilling vectors for ${missingMemories.length} memories...`,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const insertVec = db.prepare(
|
|
182
|
+
"INSERT OR REPLACE INTO memories_vec (id, vector) VALUES (?, ?)",
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const zeroVector = serializeVector(
|
|
186
|
+
new Array(embeddings.dimension).fill(0),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// Separate waypoints from content that needs embedding
|
|
190
|
+
const toEmbed = missingMemories.filter((r) => r.type !== "waypoint");
|
|
191
|
+
const waypoints = missingMemories.filter((r) => r.type === "waypoint");
|
|
192
|
+
|
|
193
|
+
// Batch embed all non-waypoint content
|
|
194
|
+
const vectors = toEmbed.length > 0
|
|
195
|
+
? await embeddings.embedBatch(toEmbed.map((r) => r.content))
|
|
196
|
+
: [];
|
|
197
|
+
|
|
198
|
+
db.exec("BEGIN");
|
|
199
|
+
try {
|
|
200
|
+
for (const row of waypoints) {
|
|
201
|
+
insertVec.run(row.id, zeroVector);
|
|
202
|
+
}
|
|
203
|
+
for (let i = 0; i < toEmbed.length; i++) {
|
|
204
|
+
insertVec.run(toEmbed[i].id, serializeVector(vectors[i]));
|
|
205
|
+
}
|
|
206
|
+
db.exec("COMMIT");
|
|
207
|
+
} catch (e) {
|
|
208
|
+
db.exec("ROLLBACK");
|
|
209
|
+
throw e;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
console.error(
|
|
213
|
+
`[vector-memory-mcp] Backfilled ${missingMemories.length} memory vectors`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── Conversation history ──────────────────────────────────────────
|
|
218
|
+
const missingConvos = db
|
|
219
|
+
.prepare(
|
|
220
|
+
`SELECT c.id, c.content
|
|
221
|
+
FROM conversation_history c
|
|
222
|
+
LEFT JOIN conversation_history_vec v ON c.id = v.id
|
|
223
|
+
WHERE v.id IS NULL OR length(v.vector) = 0`,
|
|
224
|
+
)
|
|
225
|
+
.all() as Array<{ id: string; content: string }>;
|
|
226
|
+
|
|
227
|
+
if (missingConvos.length > 0) {
|
|
228
|
+
console.error(
|
|
229
|
+
`[vector-memory-mcp] Backfilling vectors for ${missingConvos.length} conversation chunks...`,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const insertConvoVec = db.prepare(
|
|
233
|
+
"INSERT OR REPLACE INTO conversation_history_vec (id, vector) VALUES (?, ?)",
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
// Batch embed in chunks of 32
|
|
237
|
+
const BATCH_SIZE = 32;
|
|
238
|
+
db.exec("BEGIN");
|
|
239
|
+
try {
|
|
240
|
+
for (let i = 0; i < missingConvos.length; i += BATCH_SIZE) {
|
|
241
|
+
const batch = missingConvos.slice(i, i + BATCH_SIZE);
|
|
242
|
+
const vecs = await embeddings.embedBatch(batch.map((r) => r.content));
|
|
243
|
+
for (let j = 0; j < batch.length; j++) {
|
|
244
|
+
insertConvoVec.run(batch[j].id, serializeVector(vecs[j]));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if ((i + BATCH_SIZE) % 100 < BATCH_SIZE) {
|
|
248
|
+
console.error(
|
|
249
|
+
`[vector-memory-mcp] ...${Math.min(i + BATCH_SIZE, missingConvos.length)}/${missingConvos.length} conversation chunks`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
db.exec("COMMIT");
|
|
254
|
+
} catch (e) {
|
|
255
|
+
db.exec("ROLLBACK");
|
|
256
|
+
throw e;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
console.error(
|
|
260
|
+
`[vector-memory-mcp] Backfilled ${missingConvos.length} conversation vectors`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
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,33 +21,17 @@ 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
|
-
//
|
|
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
|
-
// Initialize database
|
|
28
|
+
// Initialize database and backfill any missing vectors before services start
|
|
65
29
|
const db = connectToDatabase(config.dbPath);
|
|
30
|
+
const embeddings = new EmbeddingsService(config.embeddingModel, config.embeddingDimension);
|
|
31
|
+
await backfillVectors(db, embeddings);
|
|
66
32
|
|
|
67
33
|
// Initialize layers
|
|
68
34
|
const repository = new MemoryRepository(db);
|
|
69
|
-
const embeddings = new EmbeddingsService(config.embeddingModel, config.embeddingDimension);
|
|
70
35
|
const memoryService = new MemoryService(repository, embeddings);
|
|
71
36
|
|
|
72
37
|
if (config.pluginMode) {
|
|
@@ -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 }>;
|