@aeriondyseti/vector-memory-mcp 2.4.4-dev.1 → 2.5.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -1
- package/package.json +1 -1
- package/server/config/index.ts +11 -2
- package/server/core/connection.ts +110 -4
- package/server/core/consolidation.service.ts +652 -0
- package/server/core/conversation.repository.ts +115 -16
- package/server/core/conversation.service.ts +51 -51
- package/server/core/conversation.ts +12 -0
- package/server/core/memory.repository.ts +65 -16
- package/server/core/memory.service.ts +162 -44
- package/server/core/memory.ts +3 -0
- 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 +11 -3
- package/server/core/time-expr.ts +77 -0
- package/server/index.ts +92 -2
- package/server/transports/http/server.ts +81 -32
- package/server/transports/mcp/handlers.ts +71 -26
- package/server/transports/mcp/tools.ts +40 -4
|
@@ -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) => {
|
|
@@ -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) => ({
|
|
@@ -148,6 +193,7 @@ export function createHttpApp(memoryService: MemoryService, config: Config): Hon
|
|
|
148
193
|
metadata: r.metadata,
|
|
149
194
|
source: r.source,
|
|
150
195
|
confidence: r.confidence,
|
|
196
|
+
project: r.project,
|
|
151
197
|
createdAt: r.createdAt.toISOString(),
|
|
152
198
|
})),
|
|
153
199
|
count: results.length,
|
|
@@ -162,16 +208,21 @@ export function createHttpApp(memoryService: MemoryService, config: Config): Hon
|
|
|
162
208
|
app.post("/store", async (c) => {
|
|
163
209
|
try {
|
|
164
210
|
const body = await c.req.json();
|
|
165
|
-
const { content,
|
|
211
|
+
const { content, embeddingText } = body;
|
|
166
212
|
|
|
167
213
|
if (!content || typeof content !== "string") {
|
|
168
214
|
return c.json({ error: "Missing or invalid 'content' field" }, 400);
|
|
169
215
|
}
|
|
170
216
|
|
|
217
|
+
const metadata = typeof body.metadata === "object" && body.metadata !== null && !Array.isArray(body.metadata)
|
|
218
|
+
? body.metadata as Record<string, unknown>
|
|
219
|
+
: {};
|
|
220
|
+
|
|
171
221
|
const memory = await memoryService.store(
|
|
172
222
|
content,
|
|
173
|
-
metadata
|
|
174
|
-
embeddingText
|
|
223
|
+
metadata,
|
|
224
|
+
typeof embeddingText === "string" ? embeddingText : undefined,
|
|
225
|
+
typeof body.project === "string" ? body.project : undefined
|
|
175
226
|
);
|
|
176
227
|
|
|
177
228
|
return c.json({
|
|
@@ -240,16 +291,14 @@ export function createHttpApp(memoryService: MemoryService, config: Config): Hon
|
|
|
240
291
|
|
|
241
292
|
const body = await c.req.json().catch(() => ({}));
|
|
242
293
|
let since: Date | undefined;
|
|
243
|
-
if (body.since) {
|
|
244
|
-
since = new Date(body.since
|
|
294
|
+
if (typeof body.since === "string") {
|
|
295
|
+
since = new Date(body.since);
|
|
245
296
|
if (isNaN(since.getTime())) {
|
|
246
297
|
return c.json({ error: "Invalid 'since' date format" }, 400);
|
|
247
298
|
}
|
|
248
299
|
}
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
since
|
|
252
|
-
);
|
|
300
|
+
const path = typeof body.path === "string" ? body.path : undefined;
|
|
301
|
+
const result = await conversationService.indexConversations(path, since);
|
|
253
302
|
|
|
254
303
|
return c.json(result);
|
|
255
304
|
} catch (error) {
|
|
@@ -299,13 +348,13 @@ export async function startHttpServer(
|
|
|
299
348
|
fetch: app.fetch,
|
|
300
349
|
});
|
|
301
350
|
|
|
302
|
-
|
|
351
|
+
writeLockfiles(actualPort, config.project);
|
|
303
352
|
console.error(
|
|
304
353
|
`[vector-memory-mcp] HTTP server listening on http://${config.httpHost}:${actualPort}`
|
|
305
354
|
);
|
|
306
355
|
|
|
307
356
|
return {
|
|
308
|
-
stop: () => {
|
|
357
|
+
stop: () => { removeLockfiles(config.project); server.stop(); },
|
|
309
358
|
port: actualPort,
|
|
310
359
|
};
|
|
311
360
|
}
|
|
@@ -3,6 +3,7 @@ import type { MemoryService } from "../../core/memory.service";
|
|
|
3
3
|
import type { ConversationHistoryService } from "../../core/conversation.service";
|
|
4
4
|
import type { SearchIntent } from "../../core/memory";
|
|
5
5
|
import type { HistoryFilters, SearchResult } from "../../core/conversation";
|
|
6
|
+
import { resolveDateFilters } from "../../core/time-expr";
|
|
6
7
|
import { DEBUG } from "../../config/index";
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -59,6 +60,32 @@ function requireString(args: Record<string, unknown> | undefined, field: string)
|
|
|
59
60
|
return value;
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
const VALID_INTENTS = new Set(["continuity", "fact_check", "frequent", "associative", "explore"]);
|
|
64
|
+
|
|
65
|
+
function asIntent(value: unknown): SearchIntent {
|
|
66
|
+
if (typeof value === "string" && VALID_INTENTS.has(value)) return value as SearchIntent;
|
|
67
|
+
return "fact_check";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function asInt(value: unknown, fallback: number, min: number, max: number): number {
|
|
71
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
72
|
+
return Math.max(min, Math.min(max, Math.floor(value)));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function asBool(value: unknown, fallback: boolean): boolean {
|
|
76
|
+
return typeof value === "boolean" ? value : fallback;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function asOptionalString(value: unknown): string | undefined {
|
|
80
|
+
return typeof value === "string" ? value : undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function asObject(value: unknown): Record<string, unknown> {
|
|
84
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
85
|
+
? value as Record<string, unknown>
|
|
86
|
+
: {};
|
|
87
|
+
}
|
|
88
|
+
|
|
62
89
|
export async function handleStoreMemories(
|
|
63
90
|
args: Record<string, unknown> | undefined,
|
|
64
91
|
service: MemoryService
|
|
@@ -67,6 +94,7 @@ export async function handleStoreMemories(
|
|
|
67
94
|
content: string;
|
|
68
95
|
embedding_text?: string;
|
|
69
96
|
metadata?: Record<string, unknown>;
|
|
97
|
+
project?: string;
|
|
70
98
|
}>;
|
|
71
99
|
try {
|
|
72
100
|
memories = asArray(args?.memories, "memories");
|
|
@@ -79,7 +107,8 @@ export async function handleStoreMemories(
|
|
|
79
107
|
const memory = await service.store(
|
|
80
108
|
item.content,
|
|
81
109
|
item.metadata ?? {},
|
|
82
|
-
item.embedding_text
|
|
110
|
+
item.embedding_text,
|
|
111
|
+
typeof item.project === "string" ? item.project : undefined
|
|
83
112
|
);
|
|
84
113
|
ids.push(memory.id);
|
|
85
114
|
}
|
|
@@ -182,13 +211,13 @@ export async function handleSearchMemories(
|
|
|
182
211
|
if (typeof query !== "string" || query.trim() === "") {
|
|
183
212
|
return errorResult("query is required and must be a non-empty string");
|
|
184
213
|
}
|
|
185
|
-
const intent = (args?.intent
|
|
186
|
-
const limit = (args?.limit
|
|
187
|
-
const offset = (args?.offset
|
|
188
|
-
const includeDeleted = (args?.include_deleted
|
|
189
|
-
const historyOnly = (args?.history_only
|
|
214
|
+
const intent = asIntent(args?.intent);
|
|
215
|
+
const limit = asInt(args?.limit, 10, 1, 1000);
|
|
216
|
+
const offset = asInt(args?.offset, 0, 0, 10000);
|
|
217
|
+
const includeDeleted = asBool(args?.include_deleted, false);
|
|
218
|
+
const historyOnly = asBool(args?.history_only, false);
|
|
190
219
|
// history_only implies include_history
|
|
191
|
-
const includeHistory = historyOnly ? true : (args?.include_history
|
|
220
|
+
const includeHistory = historyOnly ? true : (typeof args?.include_history === "boolean" ? args.include_history : undefined);
|
|
192
221
|
|
|
193
222
|
let historyFilters: HistoryFilters;
|
|
194
223
|
try {
|
|
@@ -197,13 +226,27 @@ export async function handleSearchMemories(
|
|
|
197
226
|
return errorResult(errorText(e));
|
|
198
227
|
}
|
|
199
228
|
|
|
229
|
+
let dateFilters: { after?: Date; before?: Date };
|
|
230
|
+
try {
|
|
231
|
+
dateFilters = resolveDateFilters({
|
|
232
|
+
after: args?.after,
|
|
233
|
+
before: args?.before,
|
|
234
|
+
time_expr: args?.time_expr,
|
|
235
|
+
});
|
|
236
|
+
} catch (e) {
|
|
237
|
+
return errorResult(errorText(e));
|
|
238
|
+
}
|
|
239
|
+
|
|
200
240
|
const results = await service.search(query, intent, {
|
|
201
241
|
limit,
|
|
242
|
+
scope: asOptionalString(args?.scope),
|
|
202
243
|
includeDeleted,
|
|
203
244
|
includeHistory,
|
|
204
245
|
historyOnly,
|
|
205
246
|
historyFilters,
|
|
206
247
|
offset,
|
|
248
|
+
after: dateFilters.after,
|
|
249
|
+
before: dateFilters.before,
|
|
207
250
|
});
|
|
208
251
|
|
|
209
252
|
if (results.length === 0) {
|
|
@@ -240,7 +283,11 @@ function formatMemoryDetail(
|
|
|
240
283
|
}
|
|
241
284
|
|
|
242
285
|
function formatSearchResult(r: SearchResult, includeDeleted: boolean): string {
|
|
243
|
-
let result = `[${r.source}] ID: ${r.id}\nConfidence: ${r.confidence.toFixed(2)}
|
|
286
|
+
let result = `[${r.source}] ID: ${r.id}\nConfidence: ${r.confidence.toFixed(2)}`;
|
|
287
|
+
if (r.project) {
|
|
288
|
+
result += `\nProject: ${r.project}`;
|
|
289
|
+
}
|
|
290
|
+
result += `\nContent: ${r.content}`;
|
|
244
291
|
if (r.metadata && Object.keys(r.metadata).length > 0) {
|
|
245
292
|
result += `\nMetadata: ${JSON.stringify(r.metadata)}`;
|
|
246
293
|
}
|
|
@@ -305,25 +352,23 @@ export async function handleSetWaypoint(
|
|
|
305
352
|
args: Record<string, unknown> | undefined,
|
|
306
353
|
service: MemoryService
|
|
307
354
|
): Promise<CallToolResult> {
|
|
308
|
-
let project: string;
|
|
309
355
|
let summary: string;
|
|
310
356
|
try {
|
|
311
|
-
project = requireString(args, "project");
|
|
312
357
|
summary = requireString(args, "summary");
|
|
313
358
|
} catch (e) {
|
|
314
359
|
return errorResult(errorText(e));
|
|
315
360
|
}
|
|
316
361
|
|
|
317
362
|
const memory = await service.setWaypoint({
|
|
318
|
-
project,
|
|
319
|
-
branch: args?.branch
|
|
363
|
+
project: asOptionalString(args?.project),
|
|
364
|
+
branch: asOptionalString(args?.branch),
|
|
320
365
|
summary,
|
|
321
|
-
completed:
|
|
322
|
-
in_progress_blocked:
|
|
323
|
-
key_decisions:
|
|
324
|
-
next_steps:
|
|
325
|
-
memory_ids:
|
|
326
|
-
metadata: (args?.metadata
|
|
366
|
+
completed: args?.completed ? asArray(args.completed, "completed") : [],
|
|
367
|
+
in_progress_blocked: args?.in_progress_blocked ? asArray(args.in_progress_blocked, "in_progress_blocked") : [],
|
|
368
|
+
key_decisions: args?.key_decisions ? asArray(args.key_decisions, "key_decisions") : [],
|
|
369
|
+
next_steps: args?.next_steps ? asArray(args.next_steps, "next_steps") : [],
|
|
370
|
+
memory_ids: args?.memory_ids ? asArray(args.memory_ids, "memory_ids") : [],
|
|
371
|
+
metadata: asObject(args?.metadata),
|
|
327
372
|
});
|
|
328
373
|
|
|
329
374
|
return {
|
|
@@ -335,7 +380,7 @@ export async function handleGetWaypoint(
|
|
|
335
380
|
args: Record<string, unknown> | undefined,
|
|
336
381
|
service: MemoryService
|
|
337
382
|
): Promise<CallToolResult> {
|
|
338
|
-
const project = args?.project
|
|
383
|
+
const project = asOptionalString(args?.project);
|
|
339
384
|
const waypoint = await service.getLatestWaypoint(project);
|
|
340
385
|
|
|
341
386
|
if (!waypoint) {
|
|
@@ -365,8 +410,8 @@ function parseHistoryFilters(
|
|
|
365
410
|
args: Record<string, unknown> | undefined
|
|
366
411
|
): HistoryFilters {
|
|
367
412
|
return {
|
|
368
|
-
sessionId: args?.session_id
|
|
369
|
-
role: args?.role_filter
|
|
413
|
+
sessionId: asOptionalString(args?.session_id),
|
|
414
|
+
role: asOptionalString(args?.role_filter),
|
|
370
415
|
after: parseDate(args?.history_after, "history_after"),
|
|
371
416
|
before: parseDate(args?.history_before, "history_before"),
|
|
372
417
|
};
|
|
@@ -394,8 +439,8 @@ export async function handleIndexConversations(
|
|
|
394
439
|
if ("error" in conv) return conv.error;
|
|
395
440
|
const conversationService = conv.service;
|
|
396
441
|
|
|
397
|
-
const path = args?.path
|
|
398
|
-
const sinceStr = args?.since
|
|
442
|
+
const path = asOptionalString(args?.path);
|
|
443
|
+
const sinceStr = asOptionalString(args?.since);
|
|
399
444
|
const since = sinceStr ? new Date(sinceStr) : undefined;
|
|
400
445
|
if (since && isNaN(since.getTime())) {
|
|
401
446
|
return errorResult("Invalid 'since' date format");
|
|
@@ -425,8 +470,8 @@ export async function handleListIndexedSessions(
|
|
|
425
470
|
if ("error" in conv) return conv.error;
|
|
426
471
|
const conversationService = conv.service;
|
|
427
472
|
|
|
428
|
-
const limit = (args?.limit
|
|
429
|
-
const offset = (args?.offset
|
|
473
|
+
const limit = asInt(args?.limit, 20, 1, 1000);
|
|
474
|
+
const offset = asInt(args?.offset, 0, 0, 10000);
|
|
430
475
|
const { sessions, total } =
|
|
431
476
|
await conversationService.listIndexedSessions(limit, offset);
|
|
432
477
|
|
|
@@ -464,7 +509,7 @@ export async function handleReindexSession(
|
|
|
464
509
|
if ("error" in conv) return conv.error;
|
|
465
510
|
const conversationService = conv.service;
|
|
466
511
|
|
|
467
|
-
const sessionId = args?.session_id
|
|
512
|
+
const sessionId = asOptionalString(args?.session_id);
|
|
468
513
|
if (!sessionId) {
|
|
469
514
|
return errorResult("session_id is required");
|
|
470
515
|
}
|