@aeriondyseti/vector-memory-mcp 2.0.0-rc.4 → 2.1.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/README.md +9 -9
- package/package.json +1 -1
- package/src/config/index.ts +11 -2
- package/src/db/memory.repository.ts +7 -5
- package/src/db/sqlite-utils.ts +1 -1
- package/src/http/server.ts +10 -2
- package/src/index.ts +1 -1
- package/src/mcp/handlers.ts +26 -8
- package/src/migration.ts +8 -3
- package/src/services/conversation.service.ts +32 -12
- package/src/services/memory.service.ts +1 -0
- package/src/services/parsers/claude-code.parser.ts +1 -0
- package/src/types/conversation.ts +14 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ A local-first MCP server that provides vector-based memory storage. Uses local e
|
|
|
11
11
|
|
|
12
12
|
## Features
|
|
13
13
|
|
|
14
|
-
- **Local & Private** - All embeddings generated locally, data stored in a single SQLite file
|
|
14
|
+
- **Local & Private** - All embeddings generated locally ([all-MiniLM-L6-v2](https://huggingface.co/Xenova/all-MiniLM-L6-v2), 384-dim), data stored in a single SQLite file
|
|
15
15
|
- **Semantic Search** - Hybrid vector + full-text search with intent-based ranking
|
|
16
16
|
- **Batch Operations** - Store, update, delete, and retrieve multiple memories at once
|
|
17
17
|
- **Session Waypoints** - Save and restore project context between sessions
|
|
@@ -103,14 +103,14 @@ Assistant: [calls search_memories with history_only: true, history_before/after
|
|
|
103
103
|
|
|
104
104
|
CLI flags:
|
|
105
105
|
|
|
106
|
-
| Flag | Default | Description |
|
|
107
|
-
|
|
108
|
-
| `--db-file
|
|
109
|
-
| `--port
|
|
110
|
-
| `--no-http` | *(HTTP enabled)* | Disable HTTP/SSE transport |
|
|
111
|
-
| `--enable-history` | *(disabled)* | Enable conversation history indexing |
|
|
112
|
-
| `--history-path` | *(auto-detect)* | Path to session log directory |
|
|
113
|
-
| `--history-weight` | `0.75` | Weight for history results in unified search |
|
|
106
|
+
| Flag | Alias | Default | Description |
|
|
107
|
+
|------|-------|---------|-------------|
|
|
108
|
+
| `--db-file <path>` | `-d` | `.vector-memory/memories.db` | Database location (relative to cwd) |
|
|
109
|
+
| `--port <number>` | `-p` | `3271` | HTTP server port |
|
|
110
|
+
| `--no-http` | | *(HTTP enabled)* | Disable HTTP/SSE transport |
|
|
111
|
+
| `--enable-history` | | *(disabled)* | Enable conversation history indexing |
|
|
112
|
+
| `--history-path` | | *(auto-detect)* | Path to session log directory |
|
|
113
|
+
| `--history-weight` | | `0.75` | Weight for history results in unified search |
|
|
114
114
|
|
|
115
115
|
---
|
|
116
116
|
|
package/package.json
CHANGED
package/src/config/index.ts
CHANGED
|
@@ -59,10 +59,19 @@ export function loadConfig(overrides: ConfigOverrides = {}): Config {
|
|
|
59
59
|
const enableHttp = overrides.enableHttp ?? true;
|
|
60
60
|
|
|
61
61
|
return {
|
|
62
|
-
dbPath: resolvePath(
|
|
62
|
+
dbPath: resolvePath(
|
|
63
|
+
overrides.dbPath
|
|
64
|
+
?? process.env.VECTOR_MEMORY_DB_PATH
|
|
65
|
+
?? DEFAULT_DB_PATH
|
|
66
|
+
),
|
|
63
67
|
embeddingModel: DEFAULT_EMBEDDING_MODEL,
|
|
64
68
|
embeddingDimension: DEFAULT_EMBEDDING_DIMENSION,
|
|
65
|
-
httpPort:
|
|
69
|
+
httpPort:
|
|
70
|
+
overrides.httpPort
|
|
71
|
+
?? (process.env.VECTOR_MEMORY_HTTP_PORT
|
|
72
|
+
? parseInt(process.env.VECTOR_MEMORY_HTTP_PORT, 10)
|
|
73
|
+
: undefined)
|
|
74
|
+
?? DEFAULT_HTTP_PORT,
|
|
66
75
|
httpHost: DEFAULT_HTTP_HOST,
|
|
67
76
|
enableHttp,
|
|
68
77
|
transportMode,
|
|
@@ -181,11 +181,13 @@ export class MemoryRepository {
|
|
|
181
181
|
|
|
182
182
|
// Full-text search
|
|
183
183
|
const ftsQuery = sanitizeFtsQuery(query);
|
|
184
|
-
const ftsResults =
|
|
185
|
-
.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
184
|
+
const ftsResults: Array<{ id: string }> = ftsQuery
|
|
185
|
+
? (this.db
|
|
186
|
+
.prepare(
|
|
187
|
+
"SELECT id FROM memories_fts WHERE memories_fts MATCH ? LIMIT ?",
|
|
188
|
+
)
|
|
189
|
+
.all(ftsQuery, candidateLimit) as Array<{ id: string }>)
|
|
190
|
+
: [];
|
|
189
191
|
|
|
190
192
|
// Compute RRF scores and pick top ids
|
|
191
193
|
const rrfScores = hybridRRF(vectorResults, ftsResults);
|
package/src/db/sqlite-utils.ts
CHANGED
|
@@ -29,7 +29,7 @@ export function sanitizeFtsQuery(query: string): string {
|
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Compute hybrid RRF scores from two ranked result lists.
|
|
32
|
-
* Returns a map of id -> combined RRF score
|
|
32
|
+
* Returns a map of id -> combined RRF score.
|
|
33
33
|
*/
|
|
34
34
|
export function hybridRRF(
|
|
35
35
|
vectorResults: Array<{ id: string }>,
|
package/src/http/server.ts
CHANGED
|
@@ -198,7 +198,9 @@ export function createHttpApp(memoryService: MemoryService, config: Config): Hon
|
|
|
198
198
|
// Fetch referenced memories in a single query
|
|
199
199
|
const memoryIds = (waypoint.metadata.memory_ids as string[] | undefined) ?? [];
|
|
200
200
|
const memories = await memoryService.getMultiple(memoryIds);
|
|
201
|
-
const referencedMemories = memories
|
|
201
|
+
const referencedMemories = memories
|
|
202
|
+
.filter((m) => !isDeleted(m))
|
|
203
|
+
.map((m) => ({ id: m.id, content: m.content }));
|
|
202
204
|
|
|
203
205
|
return c.json({
|
|
204
206
|
content: waypoint.content,
|
|
@@ -221,7 +223,13 @@ export function createHttpApp(memoryService: MemoryService, config: Config): Hon
|
|
|
221
223
|
}
|
|
222
224
|
|
|
223
225
|
const body = await c.req.json().catch(() => ({}));
|
|
224
|
-
|
|
226
|
+
let since: Date | undefined;
|
|
227
|
+
if (body.since) {
|
|
228
|
+
since = new Date(body.since as string);
|
|
229
|
+
if (isNaN(since.getTime())) {
|
|
230
|
+
return c.json({ error: "Invalid 'since' date format" }, 400);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
225
233
|
const result = await conversationService.indexConversations(
|
|
226
234
|
body.path as string | undefined,
|
|
227
235
|
since
|
package/src/index.ts
CHANGED
|
@@ -16,7 +16,7 @@ async function runMigrate(args: string[]): Promise<void> {
|
|
|
16
16
|
const config = loadConfig(overrides);
|
|
17
17
|
|
|
18
18
|
const source = config.dbPath;
|
|
19
|
-
const target = source + ".sqlite";
|
|
19
|
+
const target = source.endsWith(".sqlite") ? source.replace(/\.sqlite$/, "-migrated.sqlite") : source + ".sqlite";
|
|
20
20
|
|
|
21
21
|
if (!isLanceDbDirectory(source)) {
|
|
22
22
|
console.error(
|
package/src/mcp/handlers.ts
CHANGED
|
@@ -38,6 +38,15 @@ function errorText(e: unknown): string {
|
|
|
38
38
|
return e instanceof Error ? e.message : String(e);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
function parseDate(value: unknown, fieldName: string): Date | undefined {
|
|
42
|
+
if (value === undefined) return undefined;
|
|
43
|
+
const date = new Date(value as string);
|
|
44
|
+
if (isNaN(date.getTime())) {
|
|
45
|
+
throw new Error(`${fieldName} is not a valid date`);
|
|
46
|
+
}
|
|
47
|
+
return date;
|
|
48
|
+
}
|
|
49
|
+
|
|
41
50
|
export async function handleStoreMemories(
|
|
42
51
|
args: Record<string, unknown> | undefined,
|
|
43
52
|
service: MemoryService
|
|
@@ -157,7 +166,10 @@ export async function handleSearchMemories(
|
|
|
157
166
|
args: Record<string, unknown> | undefined,
|
|
158
167
|
service: MemoryService
|
|
159
168
|
): Promise<CallToolResult> {
|
|
160
|
-
const query = args?.query
|
|
169
|
+
const query = args?.query;
|
|
170
|
+
if (typeof query !== "string" || query.trim() === "") {
|
|
171
|
+
return { isError: true, content: [{ type: "text", text: "query is required and must be a non-empty string" }] };
|
|
172
|
+
}
|
|
161
173
|
const intent = (args?.intent as SearchIntent) ?? "fact_check";
|
|
162
174
|
const limit = (args?.limit as number) ?? 10;
|
|
163
175
|
const includeDeleted = (args?.include_deleted as boolean) ?? false;
|
|
@@ -165,10 +177,17 @@ export async function handleSearchMemories(
|
|
|
165
177
|
// history_only implies include_history
|
|
166
178
|
const includeHistory = historyOnly ? true : (args?.include_history as boolean | undefined);
|
|
167
179
|
|
|
180
|
+
let historyFilters: HistoryFilters;
|
|
181
|
+
try {
|
|
182
|
+
historyFilters = parseHistoryFilters(args);
|
|
183
|
+
} catch (e) {
|
|
184
|
+
return { isError: true, content: [{ type: "text", text: errorText(e) }] };
|
|
185
|
+
}
|
|
186
|
+
|
|
168
187
|
const results = await service.search(query, intent, limit, includeDeleted, {
|
|
169
188
|
includeHistory,
|
|
170
189
|
historyOnly,
|
|
171
|
-
historyFilters
|
|
190
|
+
historyFilters,
|
|
172
191
|
});
|
|
173
192
|
|
|
174
193
|
if (results.length === 0) {
|
|
@@ -320,12 +339,8 @@ function parseHistoryFilters(
|
|
|
320
339
|
return {
|
|
321
340
|
sessionId: args?.session_id as string | undefined,
|
|
322
341
|
role: args?.role_filter as string | undefined,
|
|
323
|
-
after: args?.history_after
|
|
324
|
-
|
|
325
|
-
: undefined,
|
|
326
|
-
before: args?.history_before
|
|
327
|
-
? new Date(args.history_before as string)
|
|
328
|
-
: undefined,
|
|
342
|
+
after: parseDate(args?.history_after, "history_after"),
|
|
343
|
+
before: parseDate(args?.history_before, "history_before"),
|
|
329
344
|
};
|
|
330
345
|
}
|
|
331
346
|
|
|
@@ -360,6 +375,9 @@ export async function handleIndexConversations(
|
|
|
360
375
|
const path = args?.path as string | undefined;
|
|
361
376
|
const sinceStr = args?.since as string | undefined;
|
|
362
377
|
const since = sinceStr ? new Date(sinceStr) : undefined;
|
|
378
|
+
if (since && isNaN(since.getTime())) {
|
|
379
|
+
return { isError: true, content: [{ type: "text", text: "Invalid 'since' date format" }] };
|
|
380
|
+
}
|
|
363
381
|
|
|
364
382
|
const result = await conversationService.indexConversations(path, since);
|
|
365
383
|
|
package/src/migration.ts
CHANGED
|
@@ -18,6 +18,7 @@ function toEpochMs(value: unknown): number {
|
|
|
18
18
|
if (typeof value === "number") return value;
|
|
19
19
|
if (value instanceof Date) return value.getTime();
|
|
20
20
|
if (typeof value === "bigint") return Number(value);
|
|
21
|
+
console.warn(`⚠️ Unexpected timestamp type: ${typeof value} (value: ${value}), using current time`);
|
|
21
22
|
return Date.now();
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -28,7 +29,10 @@ function toFloatArray(vec: unknown): number[] {
|
|
|
28
29
|
if (vec && typeof (vec as any).toArray === "function") {
|
|
29
30
|
return Array.from((vec as any).toArray());
|
|
30
31
|
}
|
|
31
|
-
if (ArrayBuffer.isView(vec))
|
|
32
|
+
if (ArrayBuffer.isView(vec)) {
|
|
33
|
+
const view = vec as DataView;
|
|
34
|
+
return Array.from(new Float32Array(view.buffer, view.byteOffset, view.byteLength / 4));
|
|
35
|
+
}
|
|
32
36
|
return [];
|
|
33
37
|
}
|
|
34
38
|
|
|
@@ -226,6 +230,7 @@ export async function migrate(opts: MigrateOptions): Promise<MigrateResult> {
|
|
|
226
230
|
}
|
|
227
231
|
|
|
228
232
|
// ── Finalize ────────────────────────────────────────────────────
|
|
233
|
+
await lanceDb.close?.();
|
|
229
234
|
sqliteDb.close();
|
|
230
235
|
|
|
231
236
|
const { size } = statSync(target);
|
|
@@ -247,8 +252,8 @@ export function formatMigrationSummary(
|
|
|
247
252
|
${result.memoriesMigrated} memories, ${result.conversationChunksMigrated} conversation chunks
|
|
248
253
|
|
|
249
254
|
Next steps:
|
|
250
|
-
1. Backup: mv ${source} ${source}.lance-backup
|
|
251
|
-
2. Activate: mv ${target} ${source}
|
|
255
|
+
1. Backup: mv "${source}" "${source}.lance-backup"
|
|
256
|
+
2. Activate: mv "${target}" "${source}"
|
|
252
257
|
3. Restart your MCP server
|
|
253
258
|
`;
|
|
254
259
|
}
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
IndexedSession,
|
|
10
10
|
ParsedMessage,
|
|
11
11
|
SessionFileInfo,
|
|
12
|
+
SessionIndexDetail,
|
|
12
13
|
} from "../types/conversation.js";
|
|
13
14
|
import type { ConversationHistoryConfig } from "../config/index.js";
|
|
14
15
|
import { resolveSessionLogPath } from "../config/index.js";
|
|
@@ -175,12 +176,18 @@ export class ConversationHistoryService {
|
|
|
175
176
|
async indexConversations(
|
|
176
177
|
path?: string,
|
|
177
178
|
since?: Date
|
|
178
|
-
): Promise<{
|
|
179
|
+
): Promise<{
|
|
180
|
+
indexed: number;
|
|
181
|
+
skipped: number;
|
|
182
|
+
errors: string[];
|
|
183
|
+
details: SessionIndexDetail[];
|
|
184
|
+
}> {
|
|
179
185
|
if (!this.config.enabled) {
|
|
180
186
|
return {
|
|
181
187
|
indexed: 0,
|
|
182
188
|
skipped: 0,
|
|
183
189
|
errors: ["Conversation history indexing is not enabled"],
|
|
190
|
+
details: [],
|
|
184
191
|
};
|
|
185
192
|
}
|
|
186
193
|
|
|
@@ -190,6 +197,7 @@ export class ConversationHistoryService {
|
|
|
190
197
|
indexed: 0,
|
|
191
198
|
skipped: 0,
|
|
192
199
|
errors: ["No session log path configured or detected"],
|
|
200
|
+
details: [],
|
|
193
201
|
};
|
|
194
202
|
}
|
|
195
203
|
|
|
@@ -203,39 +211,48 @@ export class ConversationHistoryService {
|
|
|
203
211
|
let indexed = 0;
|
|
204
212
|
let skipped = 0;
|
|
205
213
|
const errors: string[] = [];
|
|
214
|
+
const details: SessionIndexDetail[] = [];
|
|
206
215
|
|
|
207
216
|
for (const file of sessionFiles) {
|
|
208
217
|
const existing = indexState.get(file.sessionId);
|
|
209
218
|
if (existing && existing.lastModified >= file.lastModified.getTime()) {
|
|
210
219
|
skipped++;
|
|
220
|
+
details.push({ sessionId: file.sessionId, project: file.project, status: "skipped" });
|
|
211
221
|
continue;
|
|
212
222
|
}
|
|
213
223
|
|
|
214
224
|
try {
|
|
215
|
-
await this.indexSession(file, indexState);
|
|
225
|
+
const state = await this.indexSession(file, indexState);
|
|
216
226
|
indexed++;
|
|
227
|
+
details.push({
|
|
228
|
+
sessionId: file.sessionId,
|
|
229
|
+
project: file.project,
|
|
230
|
+
status: "indexed",
|
|
231
|
+
chunks: state.chunkCount,
|
|
232
|
+
messages: state.messageCount,
|
|
233
|
+
});
|
|
217
234
|
} catch (err) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
);
|
|
235
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
236
|
+
errors.push(`${file.sessionId}: ${message}`);
|
|
237
|
+
details.push({ sessionId: file.sessionId, project: file.project, status: "error", error: message });
|
|
221
238
|
}
|
|
222
239
|
}
|
|
223
240
|
|
|
224
241
|
await this.saveIndexState(indexState);
|
|
225
|
-
return { indexed, skipped, errors };
|
|
242
|
+
return { indexed, skipped, errors, details };
|
|
226
243
|
}
|
|
227
244
|
|
|
228
245
|
private async indexSession(
|
|
229
246
|
file: SessionFileInfo,
|
|
230
247
|
indexState: Map<string, IndexedSession>
|
|
231
|
-
): Promise<
|
|
248
|
+
): Promise<IndexedSession> {
|
|
232
249
|
const messages = await this.parser.parse(
|
|
233
250
|
file.filePath,
|
|
234
251
|
this.config.indexSubagents
|
|
235
252
|
);
|
|
236
253
|
if (messages.length === 0) {
|
|
237
254
|
// Still track it so we don't re-attempt
|
|
238
|
-
|
|
255
|
+
const session: IndexedSession = {
|
|
239
256
|
sessionId: file.sessionId,
|
|
240
257
|
filePath: file.filePath,
|
|
241
258
|
project: file.project,
|
|
@@ -245,8 +262,9 @@ export class ConversationHistoryService {
|
|
|
245
262
|
indexedAt: new Date(),
|
|
246
263
|
firstMessageAt: file.lastModified,
|
|
247
264
|
lastMessageAt: file.lastModified,
|
|
248
|
-
}
|
|
249
|
-
|
|
265
|
+
};
|
|
266
|
+
indexState.set(file.sessionId, session);
|
|
267
|
+
return session;
|
|
250
268
|
}
|
|
251
269
|
|
|
252
270
|
const chunks = chunkMessages(
|
|
@@ -280,7 +298,7 @@ export class ConversationHistoryService {
|
|
|
280
298
|
await this.repository.insertBatch(rows);
|
|
281
299
|
|
|
282
300
|
// Update index state
|
|
283
|
-
|
|
301
|
+
const session: IndexedSession = {
|
|
284
302
|
sessionId: file.sessionId,
|
|
285
303
|
filePath: file.filePath,
|
|
286
304
|
project: file.project,
|
|
@@ -290,7 +308,9 @@ export class ConversationHistoryService {
|
|
|
290
308
|
indexedAt: new Date(),
|
|
291
309
|
firstMessageAt: messages[0].timestamp,
|
|
292
310
|
lastMessageAt: messages[messages.length - 1].timestamp,
|
|
293
|
-
}
|
|
311
|
+
};
|
|
312
|
+
indexState.set(file.sessionId, session);
|
|
313
|
+
return session;
|
|
294
314
|
}
|
|
295
315
|
|
|
296
316
|
async reindexSession(
|
|
@@ -237,6 +237,7 @@ export class MemoryService {
|
|
|
237
237
|
updatedAt: row.createdAt,
|
|
238
238
|
source: "conversation_history" as const,
|
|
239
239
|
score: row.rrfScore * historyWeight,
|
|
240
|
+
supersededBy: null,
|
|
240
241
|
sessionId: (row.metadata?.session_id as string) ?? "",
|
|
241
242
|
role: (row.metadata?.role as string) ?? "unknown",
|
|
242
243
|
messageIndexStart: (row.metadata?.message_index_start as number) ?? 0,
|
|
@@ -227,6 +227,7 @@ export class ClaudeCodeSessionParser implements SessionLogParser {
|
|
|
227
227
|
if (since && lastModified <= since) continue;
|
|
228
228
|
|
|
229
229
|
const sessionId = basename(entry, ".jsonl");
|
|
230
|
+
if (!UUID_PATTERN.test(sessionId)) continue;
|
|
230
231
|
const project = extractProjectFromDir(projectDir);
|
|
231
232
|
|
|
232
233
|
files.push({
|
|
@@ -71,7 +71,7 @@ export interface SearchResult {
|
|
|
71
71
|
source: "memory" | "conversation_history";
|
|
72
72
|
score: number;
|
|
73
73
|
// Memory-specific fields
|
|
74
|
-
supersededBy
|
|
74
|
+
supersededBy: string | null;
|
|
75
75
|
usefulness?: number;
|
|
76
76
|
accessCount?: number;
|
|
77
77
|
lastAccessed?: Date | null;
|
|
@@ -90,6 +90,19 @@ export interface SessionFileInfo {
|
|
|
90
90
|
lastModified: Date;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/** Outcome status for a single session during indexing */
|
|
94
|
+
export type IndexStatus = "indexed" | "skipped" | "error";
|
|
95
|
+
|
|
96
|
+
/** Per-session detail returned from indexConversations */
|
|
97
|
+
export interface SessionIndexDetail {
|
|
98
|
+
sessionId: string;
|
|
99
|
+
project: string;
|
|
100
|
+
status: IndexStatus;
|
|
101
|
+
chunks?: number;
|
|
102
|
+
messages?: number;
|
|
103
|
+
error?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
93
106
|
/** Search filter options for conversation history */
|
|
94
107
|
export interface HistoryFilters {
|
|
95
108
|
sessionId?: string;
|