@aeriondyseti/vector-memory-mcp 2.2.3 → 2.2.6-dev.2
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 +33 -13
- package/package.json +12 -12
- package/{src → server}/config/index.ts +10 -2
- package/{src/db → server/core}/connection.ts +10 -2
- package/{src/db → server/core}/conversation.repository.ts +1 -1
- package/{src/services → server/core}/conversation.service.ts +2 -2
- package/server/core/embeddings.service.ts +125 -0
- package/{src/db → server/core}/memory.repository.ts +5 -1
- package/{src/services → server/core}/memory.service.ts +20 -4
- package/server/core/migration.service.ts +882 -0
- package/server/core/migrations.ts +263 -0
- package/{src/services → server/core}/parsers/claude-code.parser.ts +1 -1
- package/{src/services → server/core}/parsers/types.ts +1 -1
- package/{src → server}/index.ts +16 -48
- package/{src → server/transports}/http/mcp-transport.ts +2 -2
- package/{src → server/transports}/http/server.ts +6 -4
- package/{src → server/transports}/mcp/handlers.ts +5 -5
- package/server/transports/mcp/resources.ts +20 -0
- package/{src → server/transports}/mcp/server.ts +14 -3
- package/server/utils/formatting.ts +143 -0
- package/scripts/lancedb-extract.ts +0 -181
- package/scripts/migrate-from-lancedb.ts +0 -56
- package/scripts/smoke-test.ts +0 -699
- package/scripts/test-runner.ts +0 -76
- package/scripts/warmup.ts +0 -72
- package/src/db/migrations.ts +0 -108
- package/src/migration.ts +0 -203
- package/src/services/embeddings.service.ts +0 -48
- /package/{src/types → server/core}/conversation.ts +0 -0
- /package/{src/types → server/core}/memory.ts +0 -0
- /package/{src/db → server/core}/sqlite-utils.ts +0 -0
- /package/{src → server/transports}/mcp/tools.ts +0 -0
|
@@ -0,0 +1,882 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
import { existsSync, statSync, readdirSync } from "fs";
|
|
4
|
+
import { resolve, dirname } from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import { serializeVector } from "./sqlite-utils.js";
|
|
7
|
+
import type { MemoryRepository } from "./memory.repository.js";
|
|
8
|
+
import type { EmbeddingsService } from "./embeddings.service.js";
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
|
|
12
|
+
const BATCH_SIZE = 100;
|
|
13
|
+
|
|
14
|
+
// ── Types ────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export interface NormalizedMemory {
|
|
17
|
+
id: string;
|
|
18
|
+
content: string;
|
|
19
|
+
metadata: Record<string, unknown>;
|
|
20
|
+
createdAt: number;
|
|
21
|
+
updatedAt: number;
|
|
22
|
+
supersededBy: string | null;
|
|
23
|
+
usefulness: number;
|
|
24
|
+
accessCount: number;
|
|
25
|
+
lastAccessed: number | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface NormalizedConversation {
|
|
29
|
+
id: string;
|
|
30
|
+
content: string;
|
|
31
|
+
metadata: string;
|
|
32
|
+
createdAt: number;
|
|
33
|
+
sessionId: string;
|
|
34
|
+
role: string;
|
|
35
|
+
messageIndexStart: number;
|
|
36
|
+
messageIndexEnd: number;
|
|
37
|
+
project: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface MigrationSummary {
|
|
41
|
+
source: string;
|
|
42
|
+
format: string;
|
|
43
|
+
memoriesImported: number;
|
|
44
|
+
memoriesSkipped: number;
|
|
45
|
+
conversationsImported: number;
|
|
46
|
+
conversationsSkipped: number;
|
|
47
|
+
errors: string[];
|
|
48
|
+
durationMs: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type SourceFormat =
|
|
52
|
+
| { type: "lancedb"; path: string }
|
|
53
|
+
| { type: "own-sqlite"; path: string }
|
|
54
|
+
| { type: "cccmemory"; path: string }
|
|
55
|
+
| { type: "mcp-memory-service"; path: string }
|
|
56
|
+
| { type: "mif-json"; path: string };
|
|
57
|
+
|
|
58
|
+
// ── Service ──────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
export class MigrationService {
|
|
61
|
+
constructor(
|
|
62
|
+
private repository: MemoryRepository,
|
|
63
|
+
private embeddings: EmbeddingsService,
|
|
64
|
+
private db: Database,
|
|
65
|
+
) {}
|
|
66
|
+
|
|
67
|
+
async migrate(sourcePath: string): Promise<MigrationSummary> {
|
|
68
|
+
const start = Date.now();
|
|
69
|
+
const summary: MigrationSummary = {
|
|
70
|
+
source: sourcePath,
|
|
71
|
+
format: "unknown",
|
|
72
|
+
memoriesImported: 0,
|
|
73
|
+
memoriesSkipped: 0,
|
|
74
|
+
conversationsImported: 0,
|
|
75
|
+
conversationsSkipped: 0,
|
|
76
|
+
errors: [],
|
|
77
|
+
durationMs: 0,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const format = this.detectFormat(sourcePath);
|
|
81
|
+
summary.format = format.type;
|
|
82
|
+
|
|
83
|
+
let memories: NormalizedMemory[] = [];
|
|
84
|
+
let conversations: NormalizedConversation[] = [];
|
|
85
|
+
|
|
86
|
+
switch (format.type) {
|
|
87
|
+
case "lancedb": {
|
|
88
|
+
const data = await this.extractFromLanceDb(format.path);
|
|
89
|
+
memories = data.memories;
|
|
90
|
+
conversations = data.conversations;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
case "own-sqlite": {
|
|
94
|
+
const data = this.extractFromOwnSqlite(format.path);
|
|
95
|
+
memories = data.memories;
|
|
96
|
+
conversations = data.conversations;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
case "cccmemory": {
|
|
100
|
+
memories = this.extractFromCccMemory(format.path);
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case "mcp-memory-service": {
|
|
104
|
+
memories = this.extractFromMcpMemoryService(format.path);
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
case "mif-json": {
|
|
108
|
+
memories = await this.extractFromMif(format.path);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await this.importMemories(memories, summary);
|
|
114
|
+
await this.importConversations(conversations, summary);
|
|
115
|
+
|
|
116
|
+
summary.durationMs = Date.now() - start;
|
|
117
|
+
return summary;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── Format Detection ─────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
private detectFormat(sourcePath: string): SourceFormat {
|
|
123
|
+
if (!existsSync(sourcePath)) {
|
|
124
|
+
throw new Error(`Source not found: ${sourcePath}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const stat = statSync(sourcePath);
|
|
128
|
+
|
|
129
|
+
if (stat.isDirectory()) {
|
|
130
|
+
const entries = readdirSync(sourcePath);
|
|
131
|
+
const hasLance = entries.some(
|
|
132
|
+
(e) => e.endsWith(".lance") || e === "_versions" || e === "_indices",
|
|
133
|
+
);
|
|
134
|
+
if (hasLance) {
|
|
135
|
+
return { type: "lancedb", path: sourcePath };
|
|
136
|
+
}
|
|
137
|
+
throw new Error(
|
|
138
|
+
`Unrecognized directory format at ${sourcePath}. Expected a LanceDB directory.`,
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (sourcePath.endsWith(".json")) {
|
|
143
|
+
return { type: "mif-json", path: sourcePath };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Assume SQLite file — probe schema
|
|
147
|
+
return this.detectSqliteFormat(sourcePath);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private detectSqliteFormat(sourcePath: string): SourceFormat {
|
|
151
|
+
let sourceDb: Database | null = null;
|
|
152
|
+
try {
|
|
153
|
+
sourceDb = new Database(sourcePath, { readonly: true });
|
|
154
|
+
const tables = this.getTableNames(sourceDb);
|
|
155
|
+
|
|
156
|
+
if (tables.includes("memories")) {
|
|
157
|
+
const columns = this.getColumnNames(sourceDb, "memories");
|
|
158
|
+
if (columns.includes("usefulness")) {
|
|
159
|
+
return { type: "own-sqlite", path: sourcePath };
|
|
160
|
+
}
|
|
161
|
+
if (columns.includes("content_hash")) {
|
|
162
|
+
return { type: "mcp-memory-service", path: sourcePath };
|
|
163
|
+
}
|
|
164
|
+
// Fallback: any memories table we'll try as own-sqlite
|
|
165
|
+
return { type: "own-sqlite", path: sourcePath };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (
|
|
169
|
+
tables.includes("decisions") &&
|
|
170
|
+
tables.includes("mistakes") &&
|
|
171
|
+
tables.includes("working_memory")
|
|
172
|
+
) {
|
|
173
|
+
return { type: "cccmemory", path: sourcePath };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
throw new Error(
|
|
177
|
+
`Unrecognized SQLite schema at ${sourcePath}. Found tables: ${tables.join(", ")}`,
|
|
178
|
+
);
|
|
179
|
+
} finally {
|
|
180
|
+
sourceDb?.close();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private getTableNames(db: Database): string[] {
|
|
185
|
+
const rows = db
|
|
186
|
+
.prepare(
|
|
187
|
+
`SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'`,
|
|
188
|
+
)
|
|
189
|
+
.all() as Array<{ name: string }>;
|
|
190
|
+
return rows.map((r) => r.name);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private getColumnNames(db: Database, table: string): string[] {
|
|
194
|
+
const rows = db.prepare(`PRAGMA table_info(${table})`).all() as Array<{
|
|
195
|
+
name: string;
|
|
196
|
+
}>;
|
|
197
|
+
return rows.map((r) => r.name);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── LanceDB Extraction ───────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
private async extractFromLanceDb(
|
|
203
|
+
path: string,
|
|
204
|
+
): Promise<{
|
|
205
|
+
memories: NormalizedMemory[];
|
|
206
|
+
conversations: NormalizedConversation[];
|
|
207
|
+
}> {
|
|
208
|
+
const extractScript = resolve(
|
|
209
|
+
__dirname,
|
|
210
|
+
"..",
|
|
211
|
+
"..",
|
|
212
|
+
"scripts",
|
|
213
|
+
"lancedb-extract.ts",
|
|
214
|
+
);
|
|
215
|
+
const proc = Bun.spawn(["bun", extractScript, path], {
|
|
216
|
+
stdout: "pipe",
|
|
217
|
+
stderr: "inherit",
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const output = await new Response(proc.stdout).text();
|
|
221
|
+
const exitCode = await proc.exited;
|
|
222
|
+
|
|
223
|
+
if (exitCode !== 0) {
|
|
224
|
+
throw new Error(`LanceDB extraction failed (exit code ${exitCode})`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const data = JSON.parse(output) as {
|
|
228
|
+
memories: Array<{
|
|
229
|
+
id: string;
|
|
230
|
+
content: string;
|
|
231
|
+
metadata: string;
|
|
232
|
+
created_at: number;
|
|
233
|
+
updated_at: number;
|
|
234
|
+
last_accessed: number | null;
|
|
235
|
+
superseded_by: string | null;
|
|
236
|
+
usefulness: number;
|
|
237
|
+
access_count: number;
|
|
238
|
+
}>;
|
|
239
|
+
conversations: Array<{
|
|
240
|
+
id: string;
|
|
241
|
+
content: string;
|
|
242
|
+
metadata: string;
|
|
243
|
+
created_at: number;
|
|
244
|
+
session_id: string;
|
|
245
|
+
role: string;
|
|
246
|
+
message_index_start: number;
|
|
247
|
+
message_index_end: number;
|
|
248
|
+
project: string;
|
|
249
|
+
}>;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const memories: NormalizedMemory[] = data.memories.map((row) => ({
|
|
253
|
+
id: row.id,
|
|
254
|
+
content: row.content,
|
|
255
|
+
metadata: this.safeParseJson(row.metadata),
|
|
256
|
+
createdAt: row.created_at,
|
|
257
|
+
updatedAt: row.updated_at,
|
|
258
|
+
supersededBy: row.superseded_by,
|
|
259
|
+
usefulness: row.usefulness ?? 0,
|
|
260
|
+
accessCount: row.access_count ?? 0,
|
|
261
|
+
lastAccessed: row.last_accessed,
|
|
262
|
+
}));
|
|
263
|
+
|
|
264
|
+
const conversations: NormalizedConversation[] = data.conversations.map(
|
|
265
|
+
(row) => ({
|
|
266
|
+
id: row.id,
|
|
267
|
+
content: row.content,
|
|
268
|
+
metadata: row.metadata ?? "{}",
|
|
269
|
+
createdAt: row.created_at,
|
|
270
|
+
sessionId: row.session_id,
|
|
271
|
+
role: row.role,
|
|
272
|
+
messageIndexStart: row.message_index_start ?? 0,
|
|
273
|
+
messageIndexEnd: row.message_index_end ?? 0,
|
|
274
|
+
project: row.project ?? "",
|
|
275
|
+
}),
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
return { memories, conversations };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ── Own SQLite Extraction ────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
private extractFromOwnSqlite(path: string): {
|
|
284
|
+
memories: NormalizedMemory[];
|
|
285
|
+
conversations: NormalizedConversation[];
|
|
286
|
+
} {
|
|
287
|
+
const sourceDb = new Database(path, { readonly: true });
|
|
288
|
+
try {
|
|
289
|
+
const memories = this.extractOwnMemories(sourceDb);
|
|
290
|
+
const conversations = this.extractOwnConversations(sourceDb);
|
|
291
|
+
return { memories, conversations };
|
|
292
|
+
} finally {
|
|
293
|
+
sourceDb.close();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private extractOwnMemories(sourceDb: Database): NormalizedMemory[] {
|
|
298
|
+
const columns = this.getColumnNames(sourceDb, "memories");
|
|
299
|
+
const hasUsefulness = columns.includes("usefulness");
|
|
300
|
+
const hasAccessCount = columns.includes("access_count");
|
|
301
|
+
const hasLastAccessed = columns.includes("last_accessed");
|
|
302
|
+
|
|
303
|
+
const rows = sourceDb
|
|
304
|
+
.prepare("SELECT * FROM memories")
|
|
305
|
+
.all() as Array<Record<string, unknown>>;
|
|
306
|
+
|
|
307
|
+
return rows.map((row) => ({
|
|
308
|
+
id: row.id as string,
|
|
309
|
+
content: row.content as string,
|
|
310
|
+
metadata: this.safeParseJson(row.metadata as string),
|
|
311
|
+
createdAt: row.created_at as number,
|
|
312
|
+
updatedAt: row.updated_at as number,
|
|
313
|
+
supersededBy: (row.superseded_by as string) ?? null,
|
|
314
|
+
usefulness: hasUsefulness ? ((row.usefulness as number) ?? 0) : 0,
|
|
315
|
+
accessCount: hasAccessCount ? ((row.access_count as number) ?? 0) : 0,
|
|
316
|
+
lastAccessed: hasLastAccessed
|
|
317
|
+
? ((row.last_accessed as number) ?? null)
|
|
318
|
+
: null,
|
|
319
|
+
}));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private extractOwnConversations(
|
|
323
|
+
sourceDb: Database,
|
|
324
|
+
): NormalizedConversation[] {
|
|
325
|
+
const tables = this.getTableNames(sourceDb);
|
|
326
|
+
if (!tables.includes("conversation_history")) return [];
|
|
327
|
+
|
|
328
|
+
const rows = sourceDb
|
|
329
|
+
.prepare("SELECT * FROM conversation_history")
|
|
330
|
+
.all() as Array<Record<string, unknown>>;
|
|
331
|
+
|
|
332
|
+
return rows.map((row) => ({
|
|
333
|
+
id: row.id as string,
|
|
334
|
+
content: row.content as string,
|
|
335
|
+
metadata: (row.metadata as string) ?? "{}",
|
|
336
|
+
createdAt: row.created_at as number,
|
|
337
|
+
sessionId: (row.session_id as string) ?? "",
|
|
338
|
+
role: (row.role as string) ?? "unknown",
|
|
339
|
+
messageIndexStart: (row.message_index_start as number) ?? 0,
|
|
340
|
+
messageIndexEnd: (row.message_index_end as number) ?? 0,
|
|
341
|
+
project: (row.project as string) ?? "",
|
|
342
|
+
}));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ── CCCMemory Extraction ─────────────────────────────────────────
|
|
346
|
+
|
|
347
|
+
private extractFromCccMemory(path: string): NormalizedMemory[] {
|
|
348
|
+
const sourceDb = new Database(path, { readonly: true });
|
|
349
|
+
try {
|
|
350
|
+
const tables = this.getTableNames(sourceDb);
|
|
351
|
+
const memories: NormalizedMemory[] = [];
|
|
352
|
+
|
|
353
|
+
if (tables.includes("decisions")) {
|
|
354
|
+
memories.push(...this.extractCccDecisions(sourceDb));
|
|
355
|
+
}
|
|
356
|
+
if (tables.includes("mistakes")) {
|
|
357
|
+
memories.push(...this.extractCccMistakes(sourceDb));
|
|
358
|
+
}
|
|
359
|
+
if (tables.includes("methodologies")) {
|
|
360
|
+
memories.push(...this.extractCccMethodologies(sourceDb));
|
|
361
|
+
}
|
|
362
|
+
if (tables.includes("research_findings")) {
|
|
363
|
+
memories.push(...this.extractCccResearch(sourceDb));
|
|
364
|
+
}
|
|
365
|
+
if (tables.includes("solution_patterns")) {
|
|
366
|
+
memories.push(...this.extractCccPatterns(sourceDb));
|
|
367
|
+
}
|
|
368
|
+
if (tables.includes("working_memory")) {
|
|
369
|
+
memories.push(...this.extractCccWorkingMemory(sourceDb));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return memories;
|
|
373
|
+
} finally {
|
|
374
|
+
sourceDb.close();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
private cccId(table: string, id: string | number): string {
|
|
379
|
+
const hex = createHash("sha256")
|
|
380
|
+
.update(`cccmemory:${table}:${id}`)
|
|
381
|
+
.digest("hex");
|
|
382
|
+
return [
|
|
383
|
+
hex.slice(0, 8),
|
|
384
|
+
hex.slice(8, 12),
|
|
385
|
+
hex.slice(12, 16),
|
|
386
|
+
hex.slice(16, 20),
|
|
387
|
+
hex.slice(20, 32),
|
|
388
|
+
].join("-");
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private extractCccDecisions(sourceDb: Database): NormalizedMemory[] {
|
|
392
|
+
const rows = sourceDb.prepare("SELECT * FROM decisions").all() as Array<
|
|
393
|
+
Record<string, unknown>
|
|
394
|
+
>;
|
|
395
|
+
return rows.map((row) => {
|
|
396
|
+
const ts = (row.timestamp as number) ?? Date.now();
|
|
397
|
+
return {
|
|
398
|
+
id: this.cccId("decisions", row.id as number),
|
|
399
|
+
content: `Decision: ${row.decision_text as string}${row.rationale ? `\nRationale: ${row.rationale}` : ""}`,
|
|
400
|
+
metadata: {
|
|
401
|
+
source_type: "cccmemory",
|
|
402
|
+
memory_type: "decision",
|
|
403
|
+
rationale: row.rationale ?? null,
|
|
404
|
+
alternatives_considered: row.alternatives_considered ?? null,
|
|
405
|
+
rejected_reasons: row.rejected_reasons ?? null,
|
|
406
|
+
context: row.context ?? null,
|
|
407
|
+
related_files: row.related_files ?? null,
|
|
408
|
+
},
|
|
409
|
+
createdAt: ts,
|
|
410
|
+
updatedAt: ts,
|
|
411
|
+
supersededBy: null,
|
|
412
|
+
usefulness: 0,
|
|
413
|
+
accessCount: 0,
|
|
414
|
+
lastAccessed: null,
|
|
415
|
+
};
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
private extractCccMistakes(sourceDb: Database): NormalizedMemory[] {
|
|
420
|
+
const rows = sourceDb.prepare("SELECT * FROM mistakes").all() as Array<
|
|
421
|
+
Record<string, unknown>
|
|
422
|
+
>;
|
|
423
|
+
return rows.map((row) => {
|
|
424
|
+
const ts = (row.timestamp as number) ?? Date.now();
|
|
425
|
+
return {
|
|
426
|
+
id: this.cccId("mistakes", row.id as number),
|
|
427
|
+
content: `Mistake (${row.mistake_type}): ${row.what_went_wrong as string}${row.correction ? `\nCorrection: ${row.correction}` : ""}`,
|
|
428
|
+
metadata: {
|
|
429
|
+
source_type: "cccmemory",
|
|
430
|
+
memory_type: "mistake",
|
|
431
|
+
mistake_type: row.mistake_type ?? null,
|
|
432
|
+
correction: row.correction ?? null,
|
|
433
|
+
user_correction_message: row.user_correction_message ?? null,
|
|
434
|
+
files_affected: row.files_affected ?? null,
|
|
435
|
+
},
|
|
436
|
+
createdAt: ts,
|
|
437
|
+
updatedAt: ts,
|
|
438
|
+
supersededBy: null,
|
|
439
|
+
usefulness: 0,
|
|
440
|
+
accessCount: 0,
|
|
441
|
+
lastAccessed: null,
|
|
442
|
+
};
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private extractCccMethodologies(sourceDb: Database): NormalizedMemory[] {
|
|
447
|
+
const rows = sourceDb
|
|
448
|
+
.prepare("SELECT * FROM methodologies")
|
|
449
|
+
.all() as Array<Record<string, unknown>>;
|
|
450
|
+
return rows.map((row) => {
|
|
451
|
+
const ts = (row.created_at as number) ?? Date.now();
|
|
452
|
+
return {
|
|
453
|
+
id: this.cccId("methodologies", row.id as string),
|
|
454
|
+
content: `Problem: ${row.problem_statement}\nApproach: ${row.approach}\nOutcome: ${row.outcome}${row.what_worked ? `\nWhat worked: ${row.what_worked}` : ""}${row.what_didnt_work ? `\nWhat didn't work: ${row.what_didnt_work}` : ""}`,
|
|
455
|
+
metadata: {
|
|
456
|
+
source_type: "cccmemory",
|
|
457
|
+
memory_type: "methodology",
|
|
458
|
+
steps_taken: row.steps_taken ?? null,
|
|
459
|
+
tools_used: row.tools_used ?? null,
|
|
460
|
+
files_involved: row.files_involved ?? null,
|
|
461
|
+
},
|
|
462
|
+
createdAt: ts,
|
|
463
|
+
updatedAt: ts,
|
|
464
|
+
supersededBy: null,
|
|
465
|
+
usefulness: 0,
|
|
466
|
+
accessCount: 0,
|
|
467
|
+
lastAccessed: null,
|
|
468
|
+
};
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
private extractCccResearch(sourceDb: Database): NormalizedMemory[] {
|
|
473
|
+
const rows = sourceDb
|
|
474
|
+
.prepare("SELECT * FROM research_findings")
|
|
475
|
+
.all() as Array<Record<string, unknown>>;
|
|
476
|
+
return rows.map((row) => {
|
|
477
|
+
const ts = (row.created_at as number) ?? Date.now();
|
|
478
|
+
return {
|
|
479
|
+
id: this.cccId("research_findings", row.id as string),
|
|
480
|
+
content: `Research - ${row.topic}: ${row.discovery}`,
|
|
481
|
+
metadata: {
|
|
482
|
+
source_type: "cccmemory",
|
|
483
|
+
memory_type: "research",
|
|
484
|
+
source_type_detail: row.source_type ?? null,
|
|
485
|
+
source_reference: row.source_reference ?? null,
|
|
486
|
+
relevance: row.relevance ?? null,
|
|
487
|
+
confidence: row.confidence ?? null,
|
|
488
|
+
related_to: row.related_to ?? null,
|
|
489
|
+
},
|
|
490
|
+
createdAt: ts,
|
|
491
|
+
updatedAt: ts,
|
|
492
|
+
supersededBy: null,
|
|
493
|
+
usefulness: 0,
|
|
494
|
+
accessCount: 0,
|
|
495
|
+
lastAccessed: null,
|
|
496
|
+
};
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
private extractCccPatterns(sourceDb: Database): NormalizedMemory[] {
|
|
501
|
+
const rows = sourceDb
|
|
502
|
+
.prepare("SELECT * FROM solution_patterns")
|
|
503
|
+
.all() as Array<Record<string, unknown>>;
|
|
504
|
+
return rows.map((row) => {
|
|
505
|
+
const ts = (row.created_at as number) ?? Date.now();
|
|
506
|
+
return {
|
|
507
|
+
id: this.cccId("solution_patterns", row.id as string),
|
|
508
|
+
content: `Problem: ${row.problem_description}\nSolution: ${row.solution_summary}\nApplies when: ${row.applies_when}${row.avoid_when ? `\nAvoid when: ${row.avoid_when}` : ""}`,
|
|
509
|
+
metadata: {
|
|
510
|
+
source_type: "cccmemory",
|
|
511
|
+
memory_type: "solution_pattern",
|
|
512
|
+
problem_category: row.problem_category ?? null,
|
|
513
|
+
solution_steps: row.solution_steps ?? null,
|
|
514
|
+
code_pattern: row.code_pattern ?? null,
|
|
515
|
+
technology: row.technology ?? null,
|
|
516
|
+
prerequisites: row.prerequisites ?? null,
|
|
517
|
+
effectiveness: row.effectiveness ?? null,
|
|
518
|
+
},
|
|
519
|
+
createdAt: ts,
|
|
520
|
+
updatedAt: ts,
|
|
521
|
+
supersededBy: null,
|
|
522
|
+
usefulness: 0,
|
|
523
|
+
accessCount: 0,
|
|
524
|
+
lastAccessed: null,
|
|
525
|
+
};
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
private extractCccWorkingMemory(sourceDb: Database): NormalizedMemory[] {
|
|
530
|
+
const rows = sourceDb
|
|
531
|
+
.prepare("SELECT * FROM working_memory")
|
|
532
|
+
.all() as Array<Record<string, unknown>>;
|
|
533
|
+
return rows.map((row) => {
|
|
534
|
+
const ts = (row.created_at as number) ?? Date.now();
|
|
535
|
+
return {
|
|
536
|
+
id: this.cccId("working_memory", row.id as string),
|
|
537
|
+
content: `${row.key}: ${row.value}${row.context ? `\nContext: ${row.context}` : ""}`,
|
|
538
|
+
metadata: {
|
|
539
|
+
source_type: "cccmemory",
|
|
540
|
+
memory_type: "working_memory",
|
|
541
|
+
key: row.key ?? null,
|
|
542
|
+
tags: row.tags ?? null,
|
|
543
|
+
session_id: row.session_id ?? null,
|
|
544
|
+
project_path: row.project_path ?? null,
|
|
545
|
+
},
|
|
546
|
+
createdAt: ts,
|
|
547
|
+
updatedAt: (row.updated_at as number) ?? ts,
|
|
548
|
+
supersededBy: null,
|
|
549
|
+
usefulness: 0,
|
|
550
|
+
accessCount: 0,
|
|
551
|
+
lastAccessed: null,
|
|
552
|
+
};
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// ── MCP Memory Service Extraction ────────────────────────────────
|
|
557
|
+
|
|
558
|
+
private extractFromMcpMemoryService(path: string): NormalizedMemory[] {
|
|
559
|
+
const sourceDb = new Database(path, { readonly: true });
|
|
560
|
+
try {
|
|
561
|
+
const rows = sourceDb
|
|
562
|
+
.prepare("SELECT * FROM memories WHERE deleted_at IS NULL")
|
|
563
|
+
.all() as Array<Record<string, unknown>>;
|
|
564
|
+
|
|
565
|
+
return rows.map((row) => {
|
|
566
|
+
// created_at/updated_at are REAL (unix timestamp as float, seconds)
|
|
567
|
+
const createdAt = row.created_at
|
|
568
|
+
? Math.floor((row.created_at as number) * 1000)
|
|
569
|
+
: Date.now();
|
|
570
|
+
const updatedAt = row.updated_at
|
|
571
|
+
? Math.floor((row.updated_at as number) * 1000)
|
|
572
|
+
: createdAt;
|
|
573
|
+
|
|
574
|
+
let tags: string[] = [];
|
|
575
|
+
if (row.tags) {
|
|
576
|
+
try {
|
|
577
|
+
tags = JSON.parse(row.tags as string);
|
|
578
|
+
} catch {
|
|
579
|
+
tags = (row.tags as string)
|
|
580
|
+
.split(",")
|
|
581
|
+
.map((t) => t.trim())
|
|
582
|
+
.filter(Boolean);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return {
|
|
587
|
+
id: this.cccId("mcp-memory-service", row.content_hash as string),
|
|
588
|
+
content: row.content as string,
|
|
589
|
+
metadata: {
|
|
590
|
+
source_type: "mcp-memory-service",
|
|
591
|
+
memory_type: row.memory_type ?? null,
|
|
592
|
+
tags,
|
|
593
|
+
content_hash: row.content_hash ?? null,
|
|
594
|
+
...this.safeParseJson((row.metadata as string) ?? "{}"),
|
|
595
|
+
},
|
|
596
|
+
createdAt,
|
|
597
|
+
updatedAt,
|
|
598
|
+
supersededBy: null,
|
|
599
|
+
usefulness: 0,
|
|
600
|
+
accessCount: 0,
|
|
601
|
+
lastAccessed: null,
|
|
602
|
+
};
|
|
603
|
+
});
|
|
604
|
+
} finally {
|
|
605
|
+
sourceDb.close();
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// ── MIF JSON Extraction ──────────────────────────────────────────
|
|
610
|
+
|
|
611
|
+
private async extractFromMif(path: string): Promise<NormalizedMemory[]> {
|
|
612
|
+
const file = Bun.file(path);
|
|
613
|
+
const data = (await file.json()) as {
|
|
614
|
+
memories?: Array<{
|
|
615
|
+
id?: string;
|
|
616
|
+
content?: string;
|
|
617
|
+
memory_type?: string;
|
|
618
|
+
created_at?: string;
|
|
619
|
+
tags?: string[];
|
|
620
|
+
entities?: Array<{
|
|
621
|
+
name: string;
|
|
622
|
+
entity_type: string;
|
|
623
|
+
confidence: number;
|
|
624
|
+
}>;
|
|
625
|
+
metadata?: Record<string, string>;
|
|
626
|
+
source?: {
|
|
627
|
+
source_type?: string;
|
|
628
|
+
session_id?: string;
|
|
629
|
+
agent?: string;
|
|
630
|
+
};
|
|
631
|
+
parent_id?: string;
|
|
632
|
+
related_memory_ids?: string[];
|
|
633
|
+
}>;
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
if (!data.memories || !Array.isArray(data.memories)) {
|
|
637
|
+
throw new Error("MIF JSON file has no 'memories' array");
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return data.memories
|
|
641
|
+
.filter((m) => m.content)
|
|
642
|
+
.map((m) => {
|
|
643
|
+
const createdAt = m.created_at
|
|
644
|
+
? new Date(m.created_at).getTime()
|
|
645
|
+
: Date.now();
|
|
646
|
+
|
|
647
|
+
return {
|
|
648
|
+
id: m.id ?? this.cccId("mif", m.content!),
|
|
649
|
+
content: m.content!,
|
|
650
|
+
metadata: {
|
|
651
|
+
source_type: "mif",
|
|
652
|
+
memory_type: m.memory_type ?? null,
|
|
653
|
+
tags: m.tags ?? [],
|
|
654
|
+
entities: m.entities ?? [],
|
|
655
|
+
source: m.source ?? null,
|
|
656
|
+
parent_id: m.parent_id ?? null,
|
|
657
|
+
related_memory_ids: m.related_memory_ids ?? [],
|
|
658
|
+
...(m.metadata ?? {}),
|
|
659
|
+
},
|
|
660
|
+
createdAt,
|
|
661
|
+
updatedAt: createdAt,
|
|
662
|
+
supersededBy: null,
|
|
663
|
+
usefulness: 0,
|
|
664
|
+
accessCount: 0,
|
|
665
|
+
lastAccessed: null,
|
|
666
|
+
};
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// ── Import Logic ─────────────────────────────────────────────────
|
|
671
|
+
|
|
672
|
+
private async importMemories(
|
|
673
|
+
memories: NormalizedMemory[],
|
|
674
|
+
summary: MigrationSummary,
|
|
675
|
+
): Promise<void> {
|
|
676
|
+
if (memories.length === 0) return;
|
|
677
|
+
|
|
678
|
+
const insertMain = this.db.prepare(
|
|
679
|
+
`INSERT OR REPLACE INTO memories
|
|
680
|
+
(id, content, metadata, created_at, updated_at, superseded_by, usefulness, access_count, last_accessed)
|
|
681
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
682
|
+
);
|
|
683
|
+
const deleteVec = this.db.prepare(
|
|
684
|
+
"DELETE FROM memories_vec WHERE id = ?",
|
|
685
|
+
);
|
|
686
|
+
const insertVec = this.db.prepare(
|
|
687
|
+
"INSERT INTO memories_vec (id, vector) VALUES (?, ?)",
|
|
688
|
+
);
|
|
689
|
+
const insertFts = this.db.prepare(
|
|
690
|
+
"INSERT OR REPLACE INTO memories_fts (id, content) VALUES (?, ?)",
|
|
691
|
+
);
|
|
692
|
+
|
|
693
|
+
for (let i = 0; i < memories.length; i += BATCH_SIZE) {
|
|
694
|
+
const batch = memories.slice(i, i + BATCH_SIZE);
|
|
695
|
+
|
|
696
|
+
// Check for existing IDs
|
|
697
|
+
const ids = batch.map((m) => m.id);
|
|
698
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
699
|
+
const existing = new Set(
|
|
700
|
+
(
|
|
701
|
+
this.db
|
|
702
|
+
.prepare(`SELECT id FROM memories WHERE id IN (${placeholders})`)
|
|
703
|
+
.all(...ids) as Array<{ id: string }>
|
|
704
|
+
).map((r) => r.id),
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
const toImport = batch.filter((m) => {
|
|
708
|
+
if (existing.has(m.id)) {
|
|
709
|
+
summary.memoriesSkipped++;
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
return true;
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
if (toImport.length === 0) continue;
|
|
716
|
+
|
|
717
|
+
// Generate embeddings for the batch
|
|
718
|
+
const embeddings: number[][] = [];
|
|
719
|
+
for (const mem of toImport) {
|
|
720
|
+
try {
|
|
721
|
+
embeddings.push(await this.embeddings.embed(mem.content));
|
|
722
|
+
} catch (err) {
|
|
723
|
+
const msg =
|
|
724
|
+
err instanceof Error ? err.message : "Unknown embed error";
|
|
725
|
+
summary.errors.push(`Failed to embed memory ${mem.id}: ${msg}`);
|
|
726
|
+
embeddings.push([]);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Insert batch in a transaction
|
|
731
|
+
const tx = this.db.transaction(() => {
|
|
732
|
+
for (let j = 0; j < toImport.length; j++) {
|
|
733
|
+
const mem = toImport[j];
|
|
734
|
+
const embedding = embeddings[j];
|
|
735
|
+
|
|
736
|
+
if (embedding.length === 0) {
|
|
737
|
+
summary.errors.push(
|
|
738
|
+
`Skipping memory ${mem.id}: no embedding generated`,
|
|
739
|
+
);
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
try {
|
|
744
|
+
insertMain.run(
|
|
745
|
+
mem.id,
|
|
746
|
+
mem.content,
|
|
747
|
+
JSON.stringify(mem.metadata),
|
|
748
|
+
mem.createdAt,
|
|
749
|
+
mem.updatedAt,
|
|
750
|
+
mem.supersededBy,
|
|
751
|
+
mem.usefulness,
|
|
752
|
+
mem.accessCount,
|
|
753
|
+
mem.lastAccessed,
|
|
754
|
+
);
|
|
755
|
+
deleteVec.run(mem.id);
|
|
756
|
+
insertVec.run(mem.id, serializeVector(embedding));
|
|
757
|
+
insertFts.run(mem.id, mem.content);
|
|
758
|
+
summary.memoriesImported++;
|
|
759
|
+
} catch (err) {
|
|
760
|
+
const msg =
|
|
761
|
+
err instanceof Error ? err.message : "Unknown insert error";
|
|
762
|
+
summary.errors.push(`Failed to insert memory ${mem.id}: ${msg}`);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
});
|
|
766
|
+
tx();
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
private async importConversations(
|
|
771
|
+
conversations: NormalizedConversation[],
|
|
772
|
+
summary: MigrationSummary,
|
|
773
|
+
): Promise<void> {
|
|
774
|
+
if (conversations.length === 0) return;
|
|
775
|
+
|
|
776
|
+
const insertMain = this.db.prepare(
|
|
777
|
+
`INSERT OR REPLACE INTO conversation_history
|
|
778
|
+
(id, content, metadata, created_at, session_id, role, message_index_start, message_index_end, project)
|
|
779
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
780
|
+
);
|
|
781
|
+
const deleteVec = this.db.prepare(
|
|
782
|
+
"DELETE FROM conversation_history_vec WHERE id = ?",
|
|
783
|
+
);
|
|
784
|
+
const insertVec = this.db.prepare(
|
|
785
|
+
"INSERT INTO conversation_history_vec (id, vector) VALUES (?, ?)",
|
|
786
|
+
);
|
|
787
|
+
const insertFts = this.db.prepare(
|
|
788
|
+
"INSERT OR REPLACE INTO conversation_history_fts (id, content) VALUES (?, ?)",
|
|
789
|
+
);
|
|
790
|
+
|
|
791
|
+
for (let i = 0; i < conversations.length; i += BATCH_SIZE) {
|
|
792
|
+
const batch = conversations.slice(i, i + BATCH_SIZE);
|
|
793
|
+
|
|
794
|
+
const ids = batch.map((c) => c.id);
|
|
795
|
+
const placeholders = ids.map(() => "?").join(",");
|
|
796
|
+
const existing = new Set(
|
|
797
|
+
(
|
|
798
|
+
this.db
|
|
799
|
+
.prepare(
|
|
800
|
+
`SELECT id FROM conversation_history WHERE id IN (${placeholders})`,
|
|
801
|
+
)
|
|
802
|
+
.all(...ids) as Array<{ id: string }>
|
|
803
|
+
).map((r) => r.id),
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
const toImport = batch.filter((c) => {
|
|
807
|
+
if (existing.has(c.id)) {
|
|
808
|
+
summary.conversationsSkipped++;
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
return true;
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
if (toImport.length === 0) continue;
|
|
815
|
+
|
|
816
|
+
const embeddings: number[][] = [];
|
|
817
|
+
for (const conv of toImport) {
|
|
818
|
+
try {
|
|
819
|
+
embeddings.push(await this.embeddings.embed(conv.content));
|
|
820
|
+
} catch (err) {
|
|
821
|
+
const msg =
|
|
822
|
+
err instanceof Error ? err.message : "Unknown embed error";
|
|
823
|
+
summary.errors.push(
|
|
824
|
+
`Failed to embed conversation ${conv.id}: ${msg}`,
|
|
825
|
+
);
|
|
826
|
+
embeddings.push([]);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const tx = this.db.transaction(() => {
|
|
831
|
+
for (let j = 0; j < toImport.length; j++) {
|
|
832
|
+
const conv = toImport[j];
|
|
833
|
+
const embedding = embeddings[j];
|
|
834
|
+
|
|
835
|
+
if (embedding.length === 0) {
|
|
836
|
+
summary.errors.push(
|
|
837
|
+
`Skipping conversation ${conv.id}: no embedding generated`,
|
|
838
|
+
);
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
try {
|
|
843
|
+
insertMain.run(
|
|
844
|
+
conv.id,
|
|
845
|
+
conv.content,
|
|
846
|
+
conv.metadata,
|
|
847
|
+
conv.createdAt,
|
|
848
|
+
conv.sessionId,
|
|
849
|
+
conv.role,
|
|
850
|
+
conv.messageIndexStart,
|
|
851
|
+
conv.messageIndexEnd,
|
|
852
|
+
conv.project,
|
|
853
|
+
);
|
|
854
|
+
deleteVec.run(conv.id);
|
|
855
|
+
insertVec.run(conv.id, serializeVector(embedding));
|
|
856
|
+
insertFts.run(conv.id, conv.content);
|
|
857
|
+
summary.conversationsImported++;
|
|
858
|
+
} catch (err) {
|
|
859
|
+
const msg =
|
|
860
|
+
err instanceof Error ? err.message : "Unknown insert error";
|
|
861
|
+
summary.errors.push(
|
|
862
|
+
`Failed to insert conversation ${conv.id}: ${msg}`,
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
tx();
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// ── Helpers ──────────────────────────────────────────────────────
|
|
872
|
+
|
|
873
|
+
private safeParseJson(value: unknown): Record<string, unknown> {
|
|
874
|
+
if (typeof value !== "string") return {};
|
|
875
|
+
try {
|
|
876
|
+
const parsed = JSON.parse(value);
|
|
877
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
878
|
+
} catch {
|
|
879
|
+
return {};
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|