@aeriondyseti/vector-memory-mcp 2.4.4 → 2.5.0-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 +42 -1
- package/package.json +3 -1
- package/scripts/lancedb-extract.ts +181 -0
- package/scripts/warmup.ts +63 -0
- package/server/config/index.ts +11 -2
- package/server/core/connection.ts +160 -4
- package/server/core/consolidation.service.ts +815 -0
- package/server/core/conversation.repository.ts +137 -30
- package/server/core/conversation.service.ts +51 -51
- package/server/core/conversation.ts +17 -0
- package/server/core/memory.repository.ts +80 -22
- package/server/core/memory.service.ts +171 -49
- package/server/core/memory.ts +43 -1
- package/server/core/migrations.ts +197 -16
- package/server/core/parsers/claude-code.parser.ts +18 -4
- package/server/core/project.ts +25 -0
- package/server/core/sqlite-utils.ts +56 -5
- package/server/core/time-expr.ts +77 -0
- package/server/index.ts +92 -2
- package/server/transports/http/server.ts +82 -32
- package/server/transports/mcp/handlers.ts +71 -26
- package/server/transports/mcp/tools.ts +40 -4
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile, readdir, stat } from "fs/promises";
|
|
2
2
|
import { basename, dirname, join } from "path";
|
|
3
3
|
import type { ParsedMessage, SessionFileInfo } from "../conversation";
|
|
4
|
+
import { normalizeProject } from "../project";
|
|
4
5
|
import type { SessionLogParser } from "./types";
|
|
5
6
|
|
|
6
7
|
// UUID pattern for session IDs
|
|
@@ -18,16 +19,19 @@ function extractAssistantText(
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
|
-
*
|
|
22
|
+
* Fallback project derivation from a path-encoded directory name.
|
|
22
23
|
* Claude Code encodes paths by replacing `/` with `-`, e.g. `/home/user/project` → `-home-user-project`.
|
|
23
24
|
* This is a lossy encoding: directory names containing literal dashes (e.g. `my-project`)
|
|
24
25
|
* cannot be distinguished from path separators, so `my-project` decodes as `my/project`.
|
|
25
|
-
*
|
|
26
|
+
*
|
|
27
|
+
* Only used when a session file carries no `cwd` field — the authoritative
|
|
28
|
+
* project source is the `cwd` recorded on each message (see parse()).
|
|
26
29
|
*/
|
|
27
30
|
function extractProjectFromDir(dirName: string): string {
|
|
28
|
-
|
|
31
|
+
const decoded = dirName.startsWith("-")
|
|
29
32
|
? dirName.slice(1).replace(/-/g, "/")
|
|
30
33
|
: dirName;
|
|
34
|
+
return normalizeProject(decoded);
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
export class ClaudeCodeSessionParser implements SessionLogParser {
|
|
@@ -53,7 +57,10 @@ export class ClaudeCodeSessionParser implements SessionLogParser {
|
|
|
53
57
|
? basename(dirname(dirname(dirname(filePath))))
|
|
54
58
|
: parentDir;
|
|
55
59
|
|
|
56
|
-
|
|
60
|
+
// Project identity: the `cwd` recorded on session entries is the true
|
|
61
|
+
// absolute path. The dash-encoded directory name is a lossy fallback.
|
|
62
|
+
let project = extractProjectFromDir(projectDir);
|
|
63
|
+
let projectFromCwd = false;
|
|
57
64
|
|
|
58
65
|
for (const line of lines) {
|
|
59
66
|
let entry: Record<string, unknown>;
|
|
@@ -64,6 +71,13 @@ export class ClaudeCodeSessionParser implements SessionLogParser {
|
|
|
64
71
|
continue;
|
|
65
72
|
}
|
|
66
73
|
|
|
74
|
+
if (!projectFromCwd && typeof entry.cwd === "string" && entry.cwd.length > 0) {
|
|
75
|
+
project = normalizeProject(entry.cwd as string);
|
|
76
|
+
projectFromCwd = true;
|
|
77
|
+
// Retroactively fix messages parsed before the first cwd appeared
|
|
78
|
+
for (const m of messages) m.project = project;
|
|
79
|
+
}
|
|
80
|
+
|
|
67
81
|
const type = entry.type as string;
|
|
68
82
|
|
|
69
83
|
// Skip non-message entries
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { basename } from "path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Canonical project identifier: the normalized absolute path of the project
|
|
5
|
+
* root (e.g. `/home/user/Development/my-repo`).
|
|
6
|
+
*
|
|
7
|
+
* This exact function must be used everywhere a project value is produced or
|
|
8
|
+
* compared — memory stamping, waypoint ID hashing, search filters, the
|
|
9
|
+
* consolidation re-key, and the hooks' `?project=` param — so values join
|
|
10
|
+
* byte-for-byte across subsystems.
|
|
11
|
+
*/
|
|
12
|
+
export function normalizeProject(value: string): string {
|
|
13
|
+
let p = value.trim();
|
|
14
|
+
if (p.length === 0) return "";
|
|
15
|
+
// Collapse trailing slashes (but keep bare root "/")
|
|
16
|
+
p = p.replace(/\/+$/, "");
|
|
17
|
+
if (p.length === 0) return "/";
|
|
18
|
+
if (!p.startsWith("/")) p = `/${p}`;
|
|
19
|
+
return p;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Short human-readable name for a project path. */
|
|
23
|
+
export function projectDisplayName(project: string): string {
|
|
24
|
+
return basename(project) || project;
|
|
25
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite";
|
|
2
2
|
|
|
3
|
-
/** RRF constant
|
|
4
|
-
export const RRF_K =
|
|
3
|
+
/** RRF constant — lower K gives sharper top-rank discrimination in the 1/(K+rank) formula */
|
|
4
|
+
export const RRF_K = 10;
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Maximum parameters per SQLite query to stay within SQLITE_MAX_VARIABLE_NUMBER.
|
|
@@ -53,6 +53,11 @@ export function cosineSimilarity(a: Float32Array, b: Float32Array): number {
|
|
|
53
53
|
* Brute-force KNN search over a vector blob table.
|
|
54
54
|
* Loads all vectors, computes cosine similarity, returns top-K results
|
|
55
55
|
* sorted by descending similarity (ascending distance).
|
|
56
|
+
*
|
|
57
|
+
* `candidates` overrides the candidate query — used to pre-filter the scan
|
|
58
|
+
* (e.g. by project) so filtered searches rank within the filtered set instead
|
|
59
|
+
* of post-filtering a global top-K (which can return false-empty results).
|
|
60
|
+
* The SQL must select `id` and `vector` columns.
|
|
56
61
|
*/
|
|
57
62
|
type VecTable = "memories_vec" | "conversation_history_vec";
|
|
58
63
|
|
|
@@ -61,10 +66,13 @@ export function knnSearch(
|
|
|
61
66
|
table: VecTable,
|
|
62
67
|
queryVec: number[],
|
|
63
68
|
k: number,
|
|
69
|
+
candidates?: { sql: string; params: Array<string | number> },
|
|
64
70
|
): Array<{ id: string; distance: number }> {
|
|
65
|
-
const rows =
|
|
66
|
-
|
|
67
|
-
|
|
71
|
+
const rows = (
|
|
72
|
+
candidates
|
|
73
|
+
? db.prepare(candidates.sql).all(...candidates.params)
|
|
74
|
+
: db.prepare(`SELECT id, vector FROM ${table}`).all()
|
|
75
|
+
) as Array<{ id: string; vector: Buffer }>;
|
|
68
76
|
|
|
69
77
|
const qv = new Float32Array(queryVec);
|
|
70
78
|
const scored = rows.map((r) => {
|
|
@@ -116,6 +124,49 @@ export function hybridRRF(
|
|
|
116
124
|
return scores;
|
|
117
125
|
}
|
|
118
126
|
|
|
127
|
+
import type { SearchSignals } from "./memory";
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Compute hybrid RRF scores while preserving per-result search signals
|
|
131
|
+
* (cosine similarity, FTS match, rank positions) for confidence scoring.
|
|
132
|
+
*/
|
|
133
|
+
export function hybridRRFWithSignals(
|
|
134
|
+
vectorResults: Array<{ id: string; distance: number }>,
|
|
135
|
+
ftsResults: Array<{ id: string }>,
|
|
136
|
+
k: number = RRF_K
|
|
137
|
+
): Map<string, SearchSignals & { rrfScore: number }> {
|
|
138
|
+
const knnMap = new Map<string, { similarity: number; rank: number }>();
|
|
139
|
+
vectorResults.forEach((r, i) => {
|
|
140
|
+
knnMap.set(r.id, { similarity: 1 - r.distance, rank: i + 1 });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const ftsMap = new Map<string, number>();
|
|
144
|
+
ftsResults.forEach((r, i) => {
|
|
145
|
+
ftsMap.set(r.id, i + 1);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const allIds = new Set([...knnMap.keys(), ...ftsMap.keys()]);
|
|
149
|
+
const results = new Map<string, SearchSignals & { rrfScore: number }>();
|
|
150
|
+
|
|
151
|
+
for (const id of allIds) {
|
|
152
|
+
const knn = knnMap.get(id);
|
|
153
|
+
const ftsRank = ftsMap.get(id) ?? null;
|
|
154
|
+
let rrfScore = 0;
|
|
155
|
+
if (knn) rrfScore += 1 / (k + knn.rank);
|
|
156
|
+
if (ftsRank !== null) rrfScore += 1 / (k + ftsRank);
|
|
157
|
+
|
|
158
|
+
results.set(id, {
|
|
159
|
+
rrfScore,
|
|
160
|
+
cosineSimilarity: knn?.similarity ?? null,
|
|
161
|
+
ftsMatch: ftsRank !== null,
|
|
162
|
+
knnRank: knn?.rank ?? null,
|
|
163
|
+
ftsRank,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return results;
|
|
168
|
+
}
|
|
169
|
+
|
|
119
170
|
/**
|
|
120
171
|
* Sort ids by RRF score descending and return top N.
|
|
121
172
|
*/
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const PATTERN =
|
|
2
|
+
/^(?:past|last)\s+(\d+)\s+(minute|hour|day|week|month|year)s?$/i;
|
|
3
|
+
|
|
4
|
+
export function parseTimeExpr(
|
|
5
|
+
expr: string,
|
|
6
|
+
now: Date = new Date(),
|
|
7
|
+
): Date | null {
|
|
8
|
+
const match = expr.trim().match(PATTERN);
|
|
9
|
+
if (!match) return null;
|
|
10
|
+
|
|
11
|
+
const n = parseInt(match[1], 10);
|
|
12
|
+
const unit = match[2].toLowerCase();
|
|
13
|
+
const result = new Date(now);
|
|
14
|
+
|
|
15
|
+
switch (unit) {
|
|
16
|
+
case "minute":
|
|
17
|
+
result.setMinutes(result.getMinutes() - n);
|
|
18
|
+
break;
|
|
19
|
+
case "hour":
|
|
20
|
+
result.setHours(result.getHours() - n);
|
|
21
|
+
break;
|
|
22
|
+
case "day":
|
|
23
|
+
result.setDate(result.getDate() - n);
|
|
24
|
+
break;
|
|
25
|
+
case "week":
|
|
26
|
+
result.setDate(result.getDate() - n * 7);
|
|
27
|
+
break;
|
|
28
|
+
case "month":
|
|
29
|
+
result.setMonth(result.getMonth() - n);
|
|
30
|
+
break;
|
|
31
|
+
case "year":
|
|
32
|
+
result.setFullYear(result.getFullYear() - n);
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface DateFilters {
|
|
40
|
+
after?: Date;
|
|
41
|
+
before?: Date;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function resolveDateFilters(raw: {
|
|
45
|
+
after?: unknown;
|
|
46
|
+
before?: unknown;
|
|
47
|
+
time_expr?: unknown;
|
|
48
|
+
}): DateFilters {
|
|
49
|
+
let after: Date | undefined;
|
|
50
|
+
let before: Date | undefined;
|
|
51
|
+
|
|
52
|
+
if (raw.after !== undefined) {
|
|
53
|
+
after = new Date(raw.after as string);
|
|
54
|
+
if (isNaN(after.getTime())) throw new Error("'after' is not a valid date");
|
|
55
|
+
}
|
|
56
|
+
if (raw.before !== undefined) {
|
|
57
|
+
before = new Date(raw.before as string);
|
|
58
|
+
if (isNaN(before.getTime())) throw new Error("'before' is not a valid date");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!after && typeof raw.time_expr === "string") {
|
|
62
|
+
const resolved = parseTimeExpr(raw.time_expr);
|
|
63
|
+
if (!resolved) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Unsupported time_expr format: "${raw.time_expr}". ` +
|
|
66
|
+
`Use "past N unit" or "last N unit" (e.g. "past 7 days", "last 2 weeks").`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
after = resolved;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (after && before && after >= before) {
|
|
73
|
+
throw new Error("'after' date must be before 'before' date");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { after, before };
|
|
77
|
+
}
|
package/server/index.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import arg from "arg";
|
|
3
6
|
import { loadConfig, parseCliArgs } from "./config/index";
|
|
4
7
|
import { connectToDatabase } from "./core/connection";
|
|
5
|
-
import { backfillVectors } from "./core/migrations";
|
|
8
|
+
import { backfillVectors, repairConversationProjects } from "./core/migrations";
|
|
6
9
|
import { MemoryRepository } from "./core/memory.repository";
|
|
7
10
|
import { ConversationRepository } from "./core/conversation.repository";
|
|
8
11
|
import { EmbeddingsService } from "./core/embeddings.service";
|
|
@@ -21,18 +24,39 @@ async function main(): Promise<void> {
|
|
|
21
24
|
return;
|
|
22
25
|
}
|
|
23
26
|
|
|
27
|
+
// Consolidate repo-local databases into the global store
|
|
28
|
+
if (args[0] === "consolidate") {
|
|
29
|
+
await runConsolidate(args.slice(1));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
24
33
|
// Parse CLI args and load config
|
|
25
34
|
const overrides = parseCliArgs(args);
|
|
26
35
|
const config = loadConfig(overrides);
|
|
27
36
|
|
|
37
|
+
// Nudge: a repo-local database exists but the global store is active —
|
|
38
|
+
// suggest consolidating it. Skipped when the user explicitly chose a db.
|
|
39
|
+
if (
|
|
40
|
+
!overrides.dbPath &&
|
|
41
|
+
!process.env.VECTOR_MEMORY_DB_PATH &&
|
|
42
|
+
existsSync(join(process.cwd(), ".vector-memory", "memories.db"))
|
|
43
|
+
) {
|
|
44
|
+
console.error(
|
|
45
|
+
"[vector-memory-mcp] Found a repo-local .vector-memory/memories.db — " +
|
|
46
|
+
"memories now live in a global store (~/.vector-memory). " +
|
|
47
|
+
"Run `bunx @aeriondyseti/vector-memory-mcp consolidate` to import it."
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
28
51
|
// Initialize database and backfill any missing vectors before services start
|
|
29
52
|
const db = connectToDatabase(config.dbPath);
|
|
30
53
|
const embeddings = new EmbeddingsService(config.embeddingModel, config.embeddingDimension);
|
|
31
54
|
await backfillVectors(db, embeddings);
|
|
55
|
+
await repairConversationProjects(db, config.dbPath);
|
|
32
56
|
|
|
33
57
|
// Initialize layers
|
|
34
58
|
const repository = new MemoryRepository(db);
|
|
35
|
-
const memoryService = new MemoryService(repository, embeddings);
|
|
59
|
+
const memoryService = new MemoryService(repository, embeddings, config.project);
|
|
36
60
|
|
|
37
61
|
if (config.pluginMode) {
|
|
38
62
|
console.error("[vector-memory-mcp] Running in plugin mode");
|
|
@@ -88,4 +112,70 @@ async function main(): Promise<void> {
|
|
|
88
112
|
}
|
|
89
113
|
}
|
|
90
114
|
|
|
115
|
+
async function runConsolidate(argv: string[]): Promise<void> {
|
|
116
|
+
const flags = arg(
|
|
117
|
+
{
|
|
118
|
+
"--recursive": Boolean,
|
|
119
|
+
"--dry-run": Boolean,
|
|
120
|
+
"--archive": Boolean,
|
|
121
|
+
"--force": Boolean,
|
|
122
|
+
"--db-file": String,
|
|
123
|
+
"-d": "--db-file",
|
|
124
|
+
"-r": "--recursive",
|
|
125
|
+
},
|
|
126
|
+
{ argv, permissive: true }
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const root = flags._.find((a) => !a.startsWith("-")) ?? process.cwd();
|
|
130
|
+
const config = loadConfig({ dbPath: flags["--db-file"] });
|
|
131
|
+
|
|
132
|
+
const { ConsolidationService } = await import("./core/consolidation.service");
|
|
133
|
+
const db = connectToDatabase(config.dbPath);
|
|
134
|
+
const embeddings = new EmbeddingsService(
|
|
135
|
+
config.embeddingModel,
|
|
136
|
+
config.embeddingDimension
|
|
137
|
+
);
|
|
138
|
+
const service = new ConsolidationService(db, config.dbPath, embeddings);
|
|
139
|
+
|
|
140
|
+
const summary = await service.consolidate({
|
|
141
|
+
root,
|
|
142
|
+
recursive: flags["--recursive"] ?? false,
|
|
143
|
+
dryRun: flags["--dry-run"] ?? false,
|
|
144
|
+
archive: flags["--archive"] ?? false,
|
|
145
|
+
force: flags["--force"] ?? false,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const log = console.error;
|
|
149
|
+
log(`\nConsolidation ${summary.dryRun ? "(dry run) " : ""}-> ${summary.targetDb}`);
|
|
150
|
+
if (summary.backupPath) log(`Backup: ${summary.backupPath}`);
|
|
151
|
+
log(`Import batch: ${summary.importBatch}`);
|
|
152
|
+
|
|
153
|
+
if (summary.sources.length === 0) {
|
|
154
|
+
log(`No repo-local .vector-memory/memories.db found under ${root}.`);
|
|
155
|
+
db.close();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const s of summary.sources) {
|
|
160
|
+
log(`\n${s.sourceDb}`);
|
|
161
|
+
log(` project: ${s.project}`);
|
|
162
|
+
log(` memories: ${s.memoriesImported} imported, ${s.memoriesSkipped} skipped, ${s.memoriesRekeyed} re-keyed`);
|
|
163
|
+
log(` conversations: ${s.conversationsImported} imported, ${s.conversationsSkipped} skipped`);
|
|
164
|
+
log(` index state: ${s.indexStateImported} sessions`);
|
|
165
|
+
for (const [oldId, newId] of Object.entries(s.rekeyMap)) {
|
|
166
|
+
log(` re-key: ${oldId} -> ${newId}`);
|
|
167
|
+
}
|
|
168
|
+
for (const ref of s.unresolvedReferences) {
|
|
169
|
+
log(` unresolved reference: ${ref}`);
|
|
170
|
+
}
|
|
171
|
+
for (const err of s.errors) {
|
|
172
|
+
log(` ERROR: ${err}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const failed = summary.sources.some((s) => s.errors.length > 0);
|
|
177
|
+
db.close();
|
|
178
|
+
if (failed) process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
|
|
91
181
|
main().catch(console.error);
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { Hono } from "hono";
|
|
2
2
|
import { cors } from "hono/cors";
|
|
3
|
+
import { createHash } from "crypto";
|
|
3
4
|
import { createServer } from "net";
|
|
4
|
-
import { writeFileSync, mkdirSync, unlinkSync } from "fs";
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs";
|
|
6
|
+
import { homedir } from "os";
|
|
5
7
|
import { join } from "path";
|
|
6
8
|
import type { MemoryService } from "../../core/memory.service";
|
|
7
9
|
import type { Config } from "../../config/index";
|
|
8
10
|
import { isDeleted } from "../../core/memory";
|
|
9
11
|
import { createMcpRoutes } from "./mcp-transport";
|
|
10
12
|
import type { Memory, SearchIntent } from "../../core/memory";
|
|
13
|
+
import { resolveDateFilters } from "../../core/time-expr";
|
|
14
|
+
|
|
15
|
+
const VALID_INTENTS = new Set(["continuity", "fact_check", "frequent", "associative", "explore"]);
|
|
11
16
|
|
|
12
17
|
|
|
13
18
|
/**
|
|
@@ -56,27 +61,54 @@ async function findAvailablePort(
|
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
64
|
+
* Per-project lock path under the global data directory. Keyed by a hash of
|
|
65
|
+
* the canonical project path so hooks (which know their cwd) can compute the
|
|
66
|
+
* same path without any per-repo files. Must stay in sync with the copy in
|
|
67
|
+
* plugin/hooks/scripts/hooks-lib.ts.
|
|
61
68
|
*/
|
|
62
|
-
function
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
export function globalLockPath(project: string): string {
|
|
70
|
+
const hash = createHash("sha256").update(project).digest("hex").slice(0, 16);
|
|
71
|
+
return join(homedir(), ".vector-memory", "locks", `${hash}.lock`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function legacyLockPath(): string {
|
|
75
|
+
return join(process.cwd(), ".vector-memory", "server.lock");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Write lockfiles so hooks can discover which port this server bound to.
|
|
80
|
+
* Written after the HTTP server successfully binds.
|
|
81
|
+
*
|
|
82
|
+
* Writes the global per-project lock, plus the legacy per-repo lock when a
|
|
83
|
+
* `.vector-memory/` directory already exists in the repo (so pre-2.5 plugins
|
|
84
|
+
* keep working without us creating new per-repo litter). Legacy dual-write
|
|
85
|
+
* is temporary — remove after one stable release cycle.
|
|
86
|
+
*/
|
|
87
|
+
function writeLockfiles(port: number, project: string): void {
|
|
88
|
+
const payload = JSON.stringify({ port, pid: process.pid, project });
|
|
89
|
+
|
|
90
|
+
const globalPath = globalLockPath(project);
|
|
91
|
+
mkdirSync(join(homedir(), ".vector-memory", "locks"), { recursive: true });
|
|
92
|
+
writeFileSync(globalPath, payload, "utf8");
|
|
93
|
+
|
|
94
|
+
if (existsSync(join(process.cwd(), ".vector-memory"))) {
|
|
95
|
+
writeFileSync(legacyLockPath(), payload, "utf8");
|
|
96
|
+
}
|
|
70
97
|
}
|
|
71
98
|
|
|
72
99
|
/**
|
|
73
|
-
* Remove
|
|
100
|
+
* Remove this process's lockfiles on clean shutdown. Only deletes a lock
|
|
101
|
+
* whose recorded pid is ours — another session in the same project may have
|
|
102
|
+
* written its own lock since.
|
|
74
103
|
*/
|
|
75
|
-
export function
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
104
|
+
export function removeLockfiles(project: string): void {
|
|
105
|
+
for (const path of [globalLockPath(project), legacyLockPath()]) {
|
|
106
|
+
try {
|
|
107
|
+
const { pid } = JSON.parse(readFileSync(path, "utf8"));
|
|
108
|
+
if (pid === process.pid) unlinkSync(path);
|
|
109
|
+
} catch {
|
|
110
|
+
// missing or unreadable — fine
|
|
111
|
+
}
|
|
80
112
|
}
|
|
81
113
|
}
|
|
82
114
|
|
|
@@ -132,14 +164,27 @@ export function createHttpApp(memoryService: MemoryService, config: Config): Hon
|
|
|
132
164
|
try {
|
|
133
165
|
const body = await c.req.json();
|
|
134
166
|
const query = body.query;
|
|
135
|
-
const intent = (body.intent as SearchIntent) ?? "fact_check";
|
|
136
|
-
const limit = body.limit ?? 10;
|
|
137
|
-
|
|
138
167
|
if (!query || typeof query !== "string") {
|
|
139
168
|
return c.json({ error: "Missing or invalid 'query' field" }, 400);
|
|
140
169
|
}
|
|
170
|
+
if (body.intent && !VALID_INTENTS.has(body.intent)) {
|
|
171
|
+
return c.json({ error: "Invalid 'intent' value" }, 400);
|
|
172
|
+
}
|
|
173
|
+
const intent = (body.intent as SearchIntent) ?? "fact_check";
|
|
174
|
+
const limit = Math.max(1, Math.min(1000, Math.floor(typeof body.limit === "number" ? body.limit : 10)));
|
|
141
175
|
|
|
142
|
-
|
|
176
|
+
let dateFilters: { after?: Date; before?: Date };
|
|
177
|
+
try {
|
|
178
|
+
dateFilters = resolveDateFilters({ after: body.after, before: body.before, time_expr: body.time_expr });
|
|
179
|
+
} catch (e) {
|
|
180
|
+
return c.json({ error: e instanceof Error ? e.message : String(e) }, 400);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const results = await memoryService.search(query, intent, {
|
|
184
|
+
limit,
|
|
185
|
+
scope: typeof body.scope === "string" ? body.scope : undefined,
|
|
186
|
+
...dateFilters,
|
|
187
|
+
});
|
|
143
188
|
|
|
144
189
|
return c.json({
|
|
145
190
|
results: results.map((r) => ({
|
|
@@ -147,6 +192,8 @@ export function createHttpApp(memoryService: MemoryService, config: Config): Hon
|
|
|
147
192
|
content: r.content,
|
|
148
193
|
metadata: r.metadata,
|
|
149
194
|
source: r.source,
|
|
195
|
+
confidence: r.confidence,
|
|
196
|
+
project: r.project,
|
|
150
197
|
createdAt: r.createdAt.toISOString(),
|
|
151
198
|
})),
|
|
152
199
|
count: results.length,
|
|
@@ -161,16 +208,21 @@ export function createHttpApp(memoryService: MemoryService, config: Config): Hon
|
|
|
161
208
|
app.post("/store", async (c) => {
|
|
162
209
|
try {
|
|
163
210
|
const body = await c.req.json();
|
|
164
|
-
const { content,
|
|
211
|
+
const { content, embeddingText } = body;
|
|
165
212
|
|
|
166
213
|
if (!content || typeof content !== "string") {
|
|
167
214
|
return c.json({ error: "Missing or invalid 'content' field" }, 400);
|
|
168
215
|
}
|
|
169
216
|
|
|
217
|
+
const metadata = typeof body.metadata === "object" && body.metadata !== null && !Array.isArray(body.metadata)
|
|
218
|
+
? body.metadata as Record<string, unknown>
|
|
219
|
+
: {};
|
|
220
|
+
|
|
170
221
|
const memory = await memoryService.store(
|
|
171
222
|
content,
|
|
172
|
-
metadata
|
|
173
|
-
embeddingText
|
|
223
|
+
metadata,
|
|
224
|
+
typeof embeddingText === "string" ? embeddingText : undefined,
|
|
225
|
+
typeof body.project === "string" ? body.project : undefined
|
|
174
226
|
);
|
|
175
227
|
|
|
176
228
|
return c.json({
|
|
@@ -239,16 +291,14 @@ export function createHttpApp(memoryService: MemoryService, config: Config): Hon
|
|
|
239
291
|
|
|
240
292
|
const body = await c.req.json().catch(() => ({}));
|
|
241
293
|
let since: Date | undefined;
|
|
242
|
-
if (body.since) {
|
|
243
|
-
since = new Date(body.since
|
|
294
|
+
if (typeof body.since === "string") {
|
|
295
|
+
since = new Date(body.since);
|
|
244
296
|
if (isNaN(since.getTime())) {
|
|
245
297
|
return c.json({ error: "Invalid 'since' date format" }, 400);
|
|
246
298
|
}
|
|
247
299
|
}
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
since
|
|
251
|
-
);
|
|
300
|
+
const path = typeof body.path === "string" ? body.path : undefined;
|
|
301
|
+
const result = await conversationService.indexConversations(path, since);
|
|
252
302
|
|
|
253
303
|
return c.json(result);
|
|
254
304
|
} catch (error) {
|
|
@@ -298,13 +348,13 @@ export async function startHttpServer(
|
|
|
298
348
|
fetch: app.fetch,
|
|
299
349
|
});
|
|
300
350
|
|
|
301
|
-
|
|
351
|
+
writeLockfiles(actualPort, config.project);
|
|
302
352
|
console.error(
|
|
303
353
|
`[vector-memory-mcp] HTTP server listening on http://${config.httpHost}:${actualPort}`
|
|
304
354
|
);
|
|
305
355
|
|
|
306
356
|
return {
|
|
307
|
-
stop: () => {
|
|
357
|
+
stop: () => { removeLockfiles(config.project); server.stop(); },
|
|
308
358
|
port: actualPort,
|
|
309
359
|
};
|
|
310
360
|
}
|