@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 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`, `-d` | `.vector-memory/memories.db` | Database location |
109
- | `--port`, `-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 |
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aeriondyseti/vector-memory-mcp",
3
- "version": "2.0.0-rc.4",
3
+ "version": "2.1.0",
4
4
  "description": "A zero-configuration RAG memory server for MCP clients",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -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(overrides.dbPath ?? DEFAULT_DB_PATH),
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: overrides.httpPort ?? DEFAULT_HTTP_PORT,
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 = this.db
185
- .prepare(
186
- "SELECT id FROM memories_fts WHERE memories_fts MATCH ? LIMIT ?",
187
- )
188
- .all(ftsQuery, candidateLimit) as Array<{ id: string }>;
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);
@@ -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, sorted descending.
32
+ * Returns a map of id -> combined RRF score.
33
33
  */
34
34
  export function hybridRRF(
35
35
  vectorResults: Array<{ id: string }>,
@@ -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.map((m) => ({ id: m.id, content: m.content }));
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
- const since = body.since ? new Date(body.since as string) : undefined;
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(
@@ -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 as string;
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: parseHistoryFilters(args),
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
- ? new Date(args.history_after as string)
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)) return Array.from(new Float32Array((vec as DataView).buffer));
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<{ indexed: number; skipped: number; errors: string[] }> {
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
- errors.push(
219
- `${file.sessionId}: ${err instanceof Error ? err.message : String(err)}`
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<void> {
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
- indexState.set(file.sessionId, {
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
- return;
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
- indexState.set(file.sessionId, {
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?: string | null;
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;