@lotargo/memory_plugin 1.6.2 → 1.6.4
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/CHANGELOG.md +82 -3
- package/README.md +78 -33
- package/mcp-server/admin/snapshot.js +93 -26
- package/mcp-server/cli/direct_commands.js +15 -4
- package/mcp-server/cli/handlers/storage_actions.js +3 -1
- package/mcp-server/cli.js +2 -1
- package/mcp-server/codex_config.js +171 -0
- package/mcp-server/codex_diagnostics.js +263 -0
- package/mcp-server/config/config_manager.js +0 -1
- package/mcp-server/db/migrations.js +26 -5
- package/mcp-server/db/sync_queue.js +118 -44
- package/mcp-server/graph/knowledge_linker.js +126 -19
- package/mcp-server/index.js +4 -2
- package/mcp-server/ingest/exporter.js +38 -9
- package/mcp-server/ingest/pipeline.js +146 -48
- package/mcp-server/prompt_manager.js +30 -25
- package/mcp-server/retrieval/retriever.js +62 -38
- package/mcp-server/setup.js +54 -32
- package/mcp-server/tools/core/memory_core.js +91 -39
- package/mcp-server/tools/identity_tools.js +21 -4
- package/mcp-server/tools/memory_tools.js +4 -2
- package/mcp-server/tools/rag_tools.js +114 -55
- package/opencode-plugin/index.js +199 -108
- package/package.json +5 -3
- package/skills/using-memory/SKILL.md +62 -31
|
@@ -76,15 +76,19 @@ export async function rememberFact(
|
|
|
76
76
|
let supersededInfo = "";
|
|
77
77
|
if (!duplicate) {
|
|
78
78
|
const [date, time] = today().split(" ");
|
|
79
|
-
const meta = { ttl, tags };
|
|
80
|
-
if (keep) meta.keep = "1";
|
|
81
|
-
if (supersedes) {
|
|
82
|
-
const targetIdx = resolveFactIndex(entries, supersedes);
|
|
83
|
-
if (targetIdx !== -1) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
79
|
+
const meta = { ttl, tags };
|
|
80
|
+
if (keep) meta.keep = "1";
|
|
81
|
+
if (supersedes) {
|
|
82
|
+
const targetIdx = resolveFactIndex(entries, supersedes);
|
|
83
|
+
if (targetIdx !== -1) {
|
|
84
|
+
let targetId = factMeta(entries[targetIdx]).id;
|
|
85
|
+
if (!targetId) {
|
|
86
|
+
targetId = nextFactId(entries);
|
|
87
|
+
entries[targetIdx] = withMeta(entries[targetIdx], { id: targetId });
|
|
88
|
+
}
|
|
89
|
+
const newId = nextFactId(entries);
|
|
90
|
+
entries[targetIdx] = withMeta(entries[targetIdx], { id: targetId, supersededBy: newId });
|
|
91
|
+
meta.id = newId;
|
|
88
92
|
meta.supersedes = targetId;
|
|
89
93
|
supersededInfo = ` [superseded: "${factText(entries[targetIdx]).slice(0, 60)}"]`;
|
|
90
94
|
} else {
|
|
@@ -129,7 +133,7 @@ async function resolveTargetKey(projectPath) {
|
|
|
129
133
|
}
|
|
130
134
|
|
|
131
135
|
export async function recallFacts(
|
|
132
|
-
{ scope, project, query, tags, since, until, mode, offset, limit },
|
|
136
|
+
{ scope, project, query, tags, since, until, mode, offset, limit, includeSuperseded = false },
|
|
133
137
|
ctx = {}
|
|
134
138
|
) {
|
|
135
139
|
const results = [];
|
|
@@ -196,18 +200,26 @@ export async function recallFacts(
|
|
|
196
200
|
};
|
|
197
201
|
|
|
198
202
|
const collect = async (entries, key) => {
|
|
199
|
-
const matched = entries
|
|
200
|
-
(
|
|
201
|
-
|
|
203
|
+
const matched = entries
|
|
204
|
+
.map((entry, storageIndex) => ({ entry, storageIndex }))
|
|
205
|
+
.filter(
|
|
206
|
+
({ entry }) =>
|
|
207
|
+
(includeSuperseded || !isSuperseded(entry)) &&
|
|
208
|
+
matchesQuery(entry, query) &&
|
|
209
|
+
matchesTags(entry, tags) &&
|
|
210
|
+
inDateRange(entry, since, until)
|
|
211
|
+
);
|
|
202
212
|
if (!matched.length) return;
|
|
203
213
|
if (results.length) results.push("");
|
|
204
214
|
results.push(`--- ${key === GLOBAL_KEY ? "Global" : `Project: ${key === target ? label : key}`} ---`);
|
|
205
215
|
|
|
206
216
|
const hasLimit = limit !== undefined && limit !== null;
|
|
207
|
-
const targetLimit = hasLimit ? limit : matched.length;
|
|
208
|
-
const paginated = matched.slice(targetOffset, targetOffset + targetLimit);
|
|
209
|
-
for (let i = 0; i < paginated.length; i++) {
|
|
210
|
-
results.push(
|
|
217
|
+
const targetLimit = hasLimit ? limit : matched.length;
|
|
218
|
+
const paginated = matched.slice(targetOffset, targetOffset + targetLimit);
|
|
219
|
+
for (let i = 0; i < paginated.length; i++) {
|
|
220
|
+
results.push(
|
|
221
|
+
await formatFactWithLinks(paginated[i].entry, paginated[i].storageIndex + 1, key)
|
|
222
|
+
);
|
|
211
223
|
}
|
|
212
224
|
if (hasLimit && matched.length > targetLimit) {
|
|
213
225
|
results.push(
|
|
@@ -279,11 +291,17 @@ export async function forgetFacts({ query, scope, force }, ctx = {}) {
|
|
|
279
291
|
if (!indices.length) return "Not found.";
|
|
280
292
|
|
|
281
293
|
const removable = indices.filter((i) => force || !isKeepFact(entries[i]));
|
|
282
|
-
const protectedCount = indices.length - removable.length;
|
|
283
|
-
if (removable.length) {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
294
|
+
const protectedCount = indices.length - removable.length;
|
|
295
|
+
if (removable.length) {
|
|
296
|
+
const removedBodies = removable.map((i) => factBody(entries[i]) || factText(entries[i]));
|
|
297
|
+
for (const i of removable.sort((a, b) => b - a)) entries.splice(i, 1);
|
|
298
|
+
await writeMemory(key, entries);
|
|
299
|
+
try {
|
|
300
|
+
const { getDatabase } = await import("../../db/database.js");
|
|
301
|
+
const { deleteLinksForFacts } = await import("../../graph/knowledge_linker.js");
|
|
302
|
+
await deleteLinksForFacts(await getDatabase(), key, removedBodies);
|
|
303
|
+
} catch {}
|
|
304
|
+
}
|
|
287
305
|
let text = removable.length ? "Memory updated" : "Nothing removed.";
|
|
288
306
|
if (protectedCount) text += ` (${protectedCount} protected fact(s) skipped; use force=true to override)`;
|
|
289
307
|
return text;
|
|
@@ -312,13 +330,44 @@ export async function updateFactText({ id, newText, title, scope }, ctx = {}) {
|
|
|
312
330
|
|
|
313
331
|
let linksUpdated = 0;
|
|
314
332
|
try {
|
|
315
|
-
const { getDatabase } = await import("../../db/database.js");
|
|
316
|
-
const db = await getDatabase();
|
|
317
|
-
const
|
|
318
|
-
.prepare("
|
|
319
|
-
.
|
|
320
|
-
|
|
321
|
-
|
|
333
|
+
const { getDatabase } = await import("../../db/database.js");
|
|
334
|
+
const db = await getDatabase();
|
|
335
|
+
const linkedRows = await db
|
|
336
|
+
.prepare("SELECT * FROM knowledge_links WHERE fact_key = ? AND fact_text = ?")
|
|
337
|
+
.all(key, oldBody);
|
|
338
|
+
const res = await db
|
|
339
|
+
.prepare("UPDATE knowledge_links SET fact_text = ? WHERE fact_key = ? AND fact_text = ?")
|
|
340
|
+
.run(finalFact, key, oldBody);
|
|
341
|
+
linksUpdated = res.changes;
|
|
342
|
+
if (linksUpdated) {
|
|
343
|
+
const { queueDocumentSyncIfNeeded } = await import("../../graph/knowledge_linker.js");
|
|
344
|
+
const docIds = new Set();
|
|
345
|
+
for (const link of linkedRows) {
|
|
346
|
+
const targetSpec = link.start_line
|
|
347
|
+
? `${link.doc_id}:L${link.start_line}-${link.end_line || link.start_line}`
|
|
348
|
+
: link.doc_id;
|
|
349
|
+
await db.prepare(
|
|
350
|
+
"DELETE FROM graph_edges WHERE source_id = ? AND target_id = ? AND relation_type = ?"
|
|
351
|
+
).run(
|
|
352
|
+
`fact:${key}:${oldBody.substring(0, 30)}`,
|
|
353
|
+
targetSpec,
|
|
354
|
+
link.relation_type || "LINKS_TO"
|
|
355
|
+
);
|
|
356
|
+
await db.prepare(`
|
|
357
|
+
INSERT OR IGNORE INTO graph_edges (source_id, target_id, relation_type, metadata_json, created_at)
|
|
358
|
+
VALUES (?, ?, ?, ?, ?)
|
|
359
|
+
`).run(
|
|
360
|
+
`fact:${key}:${finalFact.substring(0, 30)}`,
|
|
361
|
+
targetSpec,
|
|
362
|
+
link.relation_type || "LINKS_TO",
|
|
363
|
+
link.metadata_json || JSON.stringify({ linkId: link.id }),
|
|
364
|
+
link.created_at || Date.now()
|
|
365
|
+
);
|
|
366
|
+
docIds.add(link.doc_id);
|
|
367
|
+
}
|
|
368
|
+
for (const docId of docIds) await queueDocumentSyncIfNeeded(db, docId);
|
|
369
|
+
}
|
|
370
|
+
} catch {}
|
|
322
371
|
|
|
323
372
|
return `Fact updated${linksUpdated ? `, ${linksUpdated} doc link(s) updated` : ""}`;
|
|
324
373
|
}
|
|
@@ -327,7 +376,7 @@ export async function memoryInfo(_args = {}, ctx = {}) {
|
|
|
327
376
|
const dbPath = join(MEMORY_DIR, "storage", "memory.sqlite");
|
|
328
377
|
const activeKey = await projectKey(ctx.worktree ?? null, ctx.directory ?? null);
|
|
329
378
|
const globalFile = storeFilePath(GLOBAL_KEY);
|
|
330
|
-
const projectFile = storeFilePath(activeKey);
|
|
379
|
+
const projectFile = activeKey ? storeFilePath(activeKey) : null;
|
|
331
380
|
|
|
332
381
|
let version = "unknown";
|
|
333
382
|
try {
|
|
@@ -358,17 +407,20 @@ export async function memoryInfo(_args = {}, ctx = {}) {
|
|
|
358
407
|
const { getDatabase } = await import("../../db/database.js");
|
|
359
408
|
const { resolveProjectIdentity, listIdentities } = await import("../../identity.js");
|
|
360
409
|
const db = await getDatabase();
|
|
361
|
-
const identity = await resolveProjectIdentity(ctx.worktree || ctx.directory || process.cwd());
|
|
362
|
-
const all = await listIdentities(db);
|
|
363
|
-
|
|
364
|
-
|
|
410
|
+
const identity = await resolveProjectIdentity(ctx.worktree || ctx.directory || process.cwd());
|
|
411
|
+
const all = await listIdentities(db);
|
|
412
|
+
const registered = identity ? all.find((item) => item.key === identity.key) : null;
|
|
413
|
+
identityLines.push(
|
|
414
|
+
`Identity: ${identity ? "git" : "no-git"}` +
|
|
365
415
|
(identity
|
|
366
416
|
? ` | key: ${identity.key} | name: ${identity.name}${
|
|
367
417
|
identity.primaryRemote ? ` | remote: ${identity.primaryRemote}` : ""
|
|
368
|
-
}`
|
|
369
|
-
: ""),
|
|
370
|
-
`
|
|
371
|
-
|
|
418
|
+
}`
|
|
419
|
+
: ""),
|
|
420
|
+
`Registry: ${identity ? (registered ? "linked" : "unlinked") : "not-applicable"}` +
|
|
421
|
+
(registered ? ` | aliases: ${registered.aliases.length}` : ""),
|
|
422
|
+
`Known identities: ${all.length}`
|
|
423
|
+
);
|
|
372
424
|
} catch (e) {
|
|
373
425
|
identityLines.push(`Identity: unavailable (${e.message})`);
|
|
374
426
|
}
|
|
@@ -378,7 +430,7 @@ export async function memoryInfo(_args = {}, ctx = {}) {
|
|
|
378
430
|
`MEMORY_DIR: ${MEMORY_DIR}`,
|
|
379
431
|
`SQLite DB: ${dbPath}`,
|
|
380
432
|
`Global store: ${globalFile}`,
|
|
381
|
-
`Project store: ${projectFile}`,
|
|
433
|
+
`Project store: ${projectFile || "not applicable (outside Git)"}`,
|
|
382
434
|
`Project stores: ${stores.length}`,
|
|
383
435
|
`Facts (global): ${(await readMemoryRaw(GLOBAL_KEY)).length}`,
|
|
384
436
|
`Facts (project): ${(await readMemoryRaw(activeKey)).length}`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as z from "zod/v4";
|
|
2
2
|
import { basename } from "node:path";
|
|
3
|
-
import { scopeKey, canonicalPath, readMemory, writeMemory, storeFilePath } from "../memory.js";
|
|
3
|
+
import { GLOBAL_KEY, scopeKey, canonicalPath, readMemory, writeMemory, storeFilePath } from "../memory.js";
|
|
4
4
|
import { factBody } from "../fact_format.js";
|
|
5
5
|
import { optStr, optNum, defStr, defBool, requireProjectKey } from "./helpers.js";
|
|
6
6
|
|
|
@@ -33,9 +33,18 @@ export function registerIdentityTools(server) {
|
|
|
33
33
|
if (!factText || !docId) {
|
|
34
34
|
throw new Error("factText and docId are required parameters for link action");
|
|
35
35
|
}
|
|
36
|
+
const facts = await readMemory(key);
|
|
37
|
+
const needle = factText.toLowerCase().trim();
|
|
38
|
+
const matches = facts.filter((entry) => {
|
|
39
|
+
const body = factBody(entry).toLowerCase();
|
|
40
|
+
return body === needle || body.includes(needle) || entry.toLowerCase().includes(needle);
|
|
41
|
+
});
|
|
42
|
+
if (matches.length === 0) throw new Error(`Notebook fact not found for link: ${factText}`);
|
|
43
|
+
if (matches.length > 1) throw new Error(`Notebook fact match is ambiguous; use a more specific factText: ${factText}`);
|
|
44
|
+
const resolvedFactText = factBody(matches[0]);
|
|
36
45
|
const res = await linkFactToDocument({
|
|
37
46
|
factKey: key,
|
|
38
|
-
factText,
|
|
47
|
+
factText: resolvedFactText,
|
|
39
48
|
docId,
|
|
40
49
|
startLine,
|
|
41
50
|
endLine,
|
|
@@ -48,7 +57,8 @@ export function registerIdentityTools(server) {
|
|
|
48
57
|
|
|
49
58
|
if (action === "get_doc_links") {
|
|
50
59
|
if (!docId) throw new Error("docId parameter is required for get_doc_links action");
|
|
51
|
-
const
|
|
60
|
+
const allowedScopes = key === GLOBAL_KEY ? [GLOBAL_KEY] : [GLOBAL_KEY, key];
|
|
61
|
+
const links = await getLinksForDoc(docId, allowedScopes);
|
|
52
62
|
return {
|
|
53
63
|
content: [{ type: "text", text: JSON.stringify(links, null, 2) }],
|
|
54
64
|
};
|
|
@@ -130,6 +140,9 @@ export function registerIdentityTools(server) {
|
|
|
130
140
|
}
|
|
131
141
|
} catch (e) {}
|
|
132
142
|
}
|
|
143
|
+
const { moveKnowledgeScope } = await import("../graph/knowledge_linker.js");
|
|
144
|
+
const migratedKnowledge = await moveKnowledgeScope(db, legacyPathKey, key);
|
|
145
|
+
if (migratedKnowledge.movedLinks > 0 || migratedKnowledge.movedDocuments > 0) migrated = true;
|
|
133
146
|
|
|
134
147
|
return {
|
|
135
148
|
content: [
|
|
@@ -242,8 +255,10 @@ export function registerIdentityTools(server) {
|
|
|
242
255
|
|
|
243
256
|
await writeMemory(targetKey, targetFacts);
|
|
244
257
|
|
|
245
|
-
await db.prepare("UPDATE project_aliases SET identity_key = ? WHERE identity_key = ?;").run(targetKey, sourceKey);
|
|
246
258
|
await upsertIdentity(db, { key: targetKey, name: sourceIdentity.name, primaryRemote: normalizeRemoteUrl(remote) });
|
|
259
|
+
await db.prepare("UPDATE project_aliases SET identity_key = ? WHERE identity_key = ?;").run(targetKey, sourceKey);
|
|
260
|
+
const { moveKnowledgeScope } = await import("../graph/knowledge_linker.js");
|
|
261
|
+
const movedKnowledge = await moveKnowledgeScope(db, sourceKey, targetKey);
|
|
247
262
|
await removeIdentity(db, sourceKey);
|
|
248
263
|
|
|
249
264
|
try {
|
|
@@ -265,6 +280,8 @@ export function registerIdentityTools(server) {
|
|
|
265
280
|
sourceKey,
|
|
266
281
|
targetKey,
|
|
267
282
|
mergedFacts: mergedCount,
|
|
283
|
+
movedKnowledgeLinks: movedKnowledge.movedLinks,
|
|
284
|
+
movedRagDocuments: movedKnowledge.movedDocuments,
|
|
268
285
|
},
|
|
269
286
|
null,
|
|
270
287
|
2
|
|
@@ -51,7 +51,8 @@ export function registerMemoryTools(server) {
|
|
|
51
51
|
"Use project: '<directory path>' with scope 'project'/'all' to read facts of a specific project from any working directory. " +
|
|
52
52
|
"query filters by keyword (all space-separated terms must match). " +
|
|
53
53
|
"tags filters by comma-separated tags. since/until filter by date (YYYY-MM-DD, inclusive). " +
|
|
54
|
-
"
|
|
54
|
+
"Superseded facts are excluded by default; pass includeSuperseded=true to inspect history. " +
|
|
55
|
+
"Expired facts are shown with [EXPIRED], protected ones with [KEEP]. The response includes the store file paths.",
|
|
55
56
|
inputSchema: z.object({
|
|
56
57
|
scope: defStr("all").describe("'project', 'global', 'all', or 'list_projects'"),
|
|
57
58
|
project: optStr().describe("Directory path of the project to read facts from (e.g. 'F:/projects/plugins/memory')"),
|
|
@@ -61,7 +62,8 @@ export function registerMemoryTools(server) {
|
|
|
61
62
|
until: optStr().describe("Optional end date filter, YYYY-MM-DD (inclusive)"),
|
|
62
63
|
mode: z.enum(["headers", "full"]).nullish().transform((v) => v || "full").describe("Result mode: 'full' (with body, default) or 'headers' (title and badges only)"),
|
|
63
64
|
offset: optNum().describe("Pagination offset (optional)"),
|
|
64
|
-
limit: optNum().describe("Pagination limit (optional)"),
|
|
65
|
+
limit: optNum().describe("Pagination limit (optional)"),
|
|
66
|
+
includeSuperseded: defBool(false).describe("Include superseded historical facts (excluded by default)"),
|
|
65
67
|
}),
|
|
66
68
|
},
|
|
67
69
|
async (args) => ({ content: [{ type: "text", text: await recallFacts(args) }] })
|
|
@@ -2,7 +2,8 @@ import * as z from "zod/v4";
|
|
|
2
2
|
import { optStr, defBool, defNum, optNum } from "./helpers.js";
|
|
3
3
|
import { MEMORY_DIR } from "../memory.js";
|
|
4
4
|
import { registerSnapshotDir } from "../admin/snapshot.js";
|
|
5
|
-
import { ensureExportsDir } from "../ingest/exporter.js";
|
|
5
|
+
import { ensureExportsDir } from "../ingest/exporter.js";
|
|
6
|
+
import { resolveRagScopeKey, resolveRagScopeKeys, resolveManageRagScopeKeys, removeDocumentScopes } from "../rag_scope.js";
|
|
6
7
|
|
|
7
8
|
export function registerRagTools(server) {
|
|
8
9
|
// Restrict snapshot export/import paths to the plugin's own data directories.
|
|
@@ -12,8 +13,8 @@ export function registerRagTools(server) {
|
|
|
12
13
|
server.registerTool(
|
|
13
14
|
"ingest_document",
|
|
14
15
|
{
|
|
15
|
-
description:
|
|
16
|
-
"
|
|
16
|
+
description:
|
|
17
|
+
"Selectively preserve a reliable, reusable source in the RAG knowledge base; do not ingest everything encountered. " +
|
|
17
18
|
"Accepts local file paths, web URLs, or raw Markdown/text content. " +
|
|
18
19
|
"For type='file' the file is read from disk and indexed with a code-block wrapper. " +
|
|
19
20
|
"For type='url' the page is fetched and its content is indexed (not just the URL). " +
|
|
@@ -31,18 +32,21 @@ export function registerRagTools(server) {
|
|
|
31
32
|
.transform((v) => v || "text")
|
|
32
33
|
.describe("Input content type: 'text' (raw content), 'file' (reads from disk, wraps in code block), or 'url' (fetches page content)"),
|
|
33
34
|
title: optStr().describe("Document title"),
|
|
34
|
-
path: optStr().describe("Original document file path"),
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
35
|
+
path: optStr().describe("Original document file path"),
|
|
36
|
+
scope: z.enum(["project", "global"]).nullish().transform((v) => v || "project").describe("RAG visibility: current Git project (default) or global"),
|
|
37
|
+
generateEmbeddings: defBool(true).describe("Compute dense vector embeddings"),
|
|
38
|
+
}),
|
|
39
|
+
},
|
|
40
|
+
async ({ content, type, title, path, scope, generateEmbeddings }) => {
|
|
41
|
+
const { ingestDocument } = await import("../ingest/pipeline.js");
|
|
42
|
+
const projectScope = await resolveRagScopeKey(scope);
|
|
43
|
+
const result = await ingestDocument({
|
|
41
44
|
content,
|
|
42
45
|
type,
|
|
43
46
|
title: title || null,
|
|
44
47
|
path: path || null,
|
|
45
|
-
generateEmbeddings,
|
|
48
|
+
generateEmbeddings,
|
|
49
|
+
projectScope,
|
|
46
50
|
});
|
|
47
51
|
return {
|
|
48
52
|
content: [
|
|
@@ -55,7 +59,8 @@ export function registerRagTools(server) {
|
|
|
55
59
|
title: result.title,
|
|
56
60
|
sectionsCount: result.sectionsCount,
|
|
57
61
|
microChunksCount: result.microChunksCount,
|
|
58
|
-
deduplicated: result.deduplicated,
|
|
62
|
+
deduplicated: result.deduplicated,
|
|
63
|
+
scope: result.projectScope,
|
|
59
64
|
},
|
|
60
65
|
null,
|
|
61
66
|
2
|
|
@@ -69,8 +74,8 @@ export function registerRagTools(server) {
|
|
|
69
74
|
server.registerTool(
|
|
70
75
|
"query_knowledge_base",
|
|
71
76
|
{
|
|
72
|
-
description:
|
|
73
|
-
"Perform hybrid search (RSF/RRF BM25 full-text + dense vector similarity) across the RAG knowledge base. " +
|
|
77
|
+
description:
|
|
78
|
+
"Perform project-isolated hybrid search (RSF/RRF BM25 full-text + dense vector similarity) across the RAG knowledge base. " +
|
|
74
79
|
"Returns top-ranked candidate document sections with breadcrumbs, GraphRAG defined code symbols, and relevance scores.",
|
|
75
80
|
inputSchema: z.object({
|
|
76
81
|
query: z.string().describe("Search query in natural language or symbol name"),
|
|
@@ -79,19 +84,22 @@ export function registerRagTools(server) {
|
|
|
79
84
|
"Optional task-specific retrieval instruction shaping embedding focus (e.g. 'Retrieve code snippets', 'Find user preferences'). " +
|
|
80
85
|
"Recommended when using E5/BGE models for domain-specific queries."
|
|
81
86
|
),
|
|
82
|
-
generateEmbeddings: defBool(true).describe("Use vector search alongside BM25"),
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
generateEmbeddings: defBool(true).describe("Use vector search alongside BM25"),
|
|
88
|
+
scope: z.enum(["all", "project", "global"]).nullish().transform((v) => v || "all").describe("Search global + current project (default), project only, or global only"),
|
|
89
|
+
}),
|
|
90
|
+
},
|
|
91
|
+
async ({ query, limit, instruction, generateEmbeddings, scope }) => {
|
|
86
92
|
const { hybridQuery } = await import("../retrieval/retriever.js");
|
|
87
93
|
const { getConfig } = await import("../config/config_manager.js");
|
|
88
|
-
const activeConfig = getConfig();
|
|
94
|
+
const activeConfig = getConfig();
|
|
95
|
+
const scopeKeys = await resolveRagScopeKeys(scope);
|
|
89
96
|
|
|
90
97
|
const results = await hybridQuery({
|
|
91
98
|
query,
|
|
92
99
|
limit,
|
|
93
100
|
generateEmbeddings,
|
|
94
|
-
instruction: instruction || null,
|
|
101
|
+
instruction: instruction || null,
|
|
102
|
+
scopeKeys,
|
|
95
103
|
});
|
|
96
104
|
|
|
97
105
|
if (!results || results.length === 0) {
|
|
@@ -128,8 +136,9 @@ export function registerRagTools(server) {
|
|
|
128
136
|
server.registerTool(
|
|
129
137
|
"batch_query_knowledge_base",
|
|
130
138
|
{
|
|
131
|
-
description:
|
|
132
|
-
"Execute multiple hybrid search queries in a single batch call. " +
|
|
139
|
+
description:
|
|
140
|
+
"Execute multiple hybrid search queries in a single batch call. " +
|
|
141
|
+
"Search is isolated to global plus the current project unless another scope is requested. " +
|
|
133
142
|
"More efficient than separate query_knowledge_base calls: all query embeddings computed in one ONNX pass. " +
|
|
134
143
|
"Returns one result set per query, in the same order as input.",
|
|
135
144
|
inputSchema: z.object({
|
|
@@ -140,18 +149,21 @@ export function registerRagTools(server) {
|
|
|
140
149
|
instruction: optStr().describe(
|
|
141
150
|
"Optional task-specific retrieval instruction shaping embedding focus. Applied to all queries."
|
|
142
151
|
),
|
|
143
|
-
generateEmbeddings: defBool(true).describe("Use vector search alongside BM25"),
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
152
|
+
generateEmbeddings: defBool(true).describe("Use vector search alongside BM25"),
|
|
153
|
+
scope: z.enum(["all", "project", "global"]).nullish().transform((v) => v || "all").describe("Search global + current project (default), project only, or global only"),
|
|
154
|
+
}),
|
|
155
|
+
},
|
|
156
|
+
async ({ queries, limit, instruction, generateEmbeddings, scope }) => {
|
|
147
157
|
const { batchHybridQuery } = await import("../retrieval/retriever.js");
|
|
148
158
|
const { getConfig } = await import("../config/config_manager.js");
|
|
149
|
-
const activeConfig = getConfig();
|
|
159
|
+
const activeConfig = getConfig();
|
|
160
|
+
const scopeKeys = await resolveRagScopeKeys(scope);
|
|
150
161
|
|
|
151
162
|
const allResults = await batchHybridQuery(queries, {
|
|
152
163
|
limit,
|
|
153
164
|
generateEmbeddings,
|
|
154
|
-
instruction: instruction || null,
|
|
165
|
+
instruction: instruction || null,
|
|
166
|
+
scopeKeys,
|
|
155
167
|
});
|
|
156
168
|
|
|
157
169
|
const formatted = allResults
|
|
@@ -230,27 +242,48 @@ export function registerRagTools(server) {
|
|
|
230
242
|
server.registerTool(
|
|
231
243
|
"manage_knowledge_base",
|
|
232
244
|
{
|
|
233
|
-
description:
|
|
234
|
-
"Manage the RAG knowledge base: inspect stats, list documents, read full raw document, delete documents, or export/import snapshots.",
|
|
245
|
+
description:
|
|
246
|
+
"Manage the project-isolated RAG knowledge base: inspect stats, list documents, read full raw document, unlink/delete documents, or export/import complete snapshots.",
|
|
235
247
|
inputSchema: z.object({
|
|
236
248
|
action: z.enum(["stats", "list", "read_document", "delete", "export_snapshot", "import_snapshot"]).describe("Management action"),
|
|
237
249
|
docId: optStr().describe("Document ID, title, or path (required for read_document and delete)"),
|
|
238
|
-
snapshotPath: optStr().describe("File path for snapshot export/import"),
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
250
|
+
snapshotPath: optStr().describe("File path for snapshot export/import"),
|
|
251
|
+
scope: z.enum(["all", "project", "global"]).nullish().describe("For stats/list/read: global + current project by default. Delete defaults to the current project (or global outside Git); pass all/global explicitly for broader removal"),
|
|
252
|
+
}),
|
|
253
|
+
},
|
|
254
|
+
async ({ action, docId, snapshotPath, scope }) => {
|
|
255
|
+
const { getDatabase } = await import("../db/database.js");
|
|
256
|
+
const db = await getDatabase();
|
|
257
|
+
const scopeKeys = ["stats", "list", "read_document", "delete"].includes(action)
|
|
258
|
+
? await resolveManageRagScopeKeys(action, scope)
|
|
259
|
+
: null;
|
|
260
|
+
const placeholders = scopeKeys ? scopeKeys.map(() => "?").join(",") : "";
|
|
261
|
+
const visibleDocWhere = scopeKeys
|
|
262
|
+
? `EXISTS (SELECT 1 FROM document_scopes ds WHERE ds.doc_id = d.id AND ds.scope_key IN (${placeholders}))`
|
|
263
|
+
: "1=1";
|
|
264
|
+
|
|
265
|
+
if (action === "stats") {
|
|
266
|
+
const docCountRow = await db.prepare(`SELECT COUNT(*) as cnt FROM documents d WHERE ${visibleDocWhere}`).get(...scopeKeys);
|
|
267
|
+
const docCount = docCountRow ? docCountRow.cnt : 0;
|
|
268
|
+
const secCountRow = await db.prepare(`SELECT COUNT(*) as cnt FROM sections s JOIN documents d ON d.id = s.doc_id WHERE ${visibleDocWhere}`).get(...scopeKeys);
|
|
269
|
+
const secCount = secCountRow ? secCountRow.cnt : 0;
|
|
270
|
+
const chunkCountRow = await db.prepare(`SELECT COUNT(*) as cnt FROM micro_chunks m JOIN documents d ON d.id = m.doc_id WHERE ${visibleDocWhere}`).get(...scopeKeys);
|
|
271
|
+
const chunkCount = chunkCountRow ? chunkCountRow.cnt : 0;
|
|
272
|
+
const visibleDocIds = await db.prepare(`SELECT d.id FROM documents d WHERE ${visibleDocWhere}`).all(...scopeKeys);
|
|
273
|
+
let edgeCount = 0;
|
|
274
|
+
if (visibleDocIds.length > 0) {
|
|
275
|
+
const docIds = visibleDocIds.map((row) => row.id);
|
|
276
|
+
const docPlaceholders = docIds.map(() => "?").join(",");
|
|
277
|
+
const ownedRows = await db.prepare(`
|
|
278
|
+
SELECT id FROM sections WHERE doc_id IN (${docPlaceholders})
|
|
279
|
+
UNION SELECT id FROM medium_chunks WHERE doc_id IN (${docPlaceholders})
|
|
280
|
+
UNION SELECT id FROM micro_chunks WHERE doc_id IN (${docPlaceholders})
|
|
281
|
+
`).all(...docIds, ...docIds, ...docIds);
|
|
282
|
+
const ownedIds = [...docIds, ...ownedRows.map((row) => row.id)];
|
|
283
|
+
const edgePlaceholders = ownedIds.map(() => "?").join(",");
|
|
284
|
+
const edgeCountRow = await db.prepare(`SELECT COUNT(*) as cnt FROM graph_edges WHERE source_id IN (${edgePlaceholders}) OR target_id IN (${edgePlaceholders})`).get(...ownedIds, ...ownedIds);
|
|
285
|
+
edgeCount = edgeCountRow ? edgeCountRow.cnt : 0;
|
|
286
|
+
}
|
|
254
287
|
return {
|
|
255
288
|
content: [
|
|
256
289
|
{
|
|
@@ -270,8 +303,15 @@ export function registerRagTools(server) {
|
|
|
270
303
|
};
|
|
271
304
|
}
|
|
272
305
|
|
|
273
|
-
if (action === "list") {
|
|
274
|
-
const docs = await db.prepare(
|
|
306
|
+
if (action === "list") {
|
|
307
|
+
const docs = await db.prepare(`
|
|
308
|
+
SELECT d.id, d.title, d.path, d.blob_hash, d.created_at,
|
|
309
|
+
GROUP_CONCAT(ds.scope_key) AS scopes
|
|
310
|
+
FROM documents d
|
|
311
|
+
JOIN document_scopes ds ON ds.doc_id = d.id AND ds.scope_key IN (${placeholders})
|
|
312
|
+
GROUP BY d.id, d.title, d.path, d.blob_hash, d.created_at
|
|
313
|
+
ORDER BY d.created_at DESC
|
|
314
|
+
`).all(...scopeKeys);
|
|
275
315
|
return {
|
|
276
316
|
content: [{ type: "text", text: JSON.stringify(docs, null, 2) }],
|
|
277
317
|
};
|
|
@@ -279,9 +319,9 @@ export function registerRagTools(server) {
|
|
|
279
319
|
|
|
280
320
|
if (action === "read_document") {
|
|
281
321
|
if (!docId) throw new Error("docId parameter is required for read_document action");
|
|
282
|
-
const doc = await db
|
|
283
|
-
.prepare(
|
|
284
|
-
.get(docId, docId, docId);
|
|
322
|
+
const doc = await db
|
|
323
|
+
.prepare(`SELECT d.id, d.title, d.path, d.blob_hash, d.created_at FROM documents d WHERE (d.id = ? OR d.path = ? OR d.title = ?) AND ${visibleDocWhere}`)
|
|
324
|
+
.get(docId, docId, docId, ...scopeKeys);
|
|
285
325
|
if (!doc) {
|
|
286
326
|
throw new Error(`Document not found in knowledge base for docId: ${docId}`);
|
|
287
327
|
}
|
|
@@ -307,10 +347,29 @@ export function registerRagTools(server) {
|
|
|
307
347
|
};
|
|
308
348
|
}
|
|
309
349
|
|
|
310
|
-
if (action === "delete") {
|
|
311
|
-
if (!docId) throw new Error("docId parameter is required for delete action");
|
|
312
|
-
const
|
|
313
|
-
|
|
350
|
+
if (action === "delete") {
|
|
351
|
+
if (!docId) throw new Error("docId parameter is required for delete action");
|
|
352
|
+
const visible = await db
|
|
353
|
+
.prepare(`SELECT d.id FROM documents d WHERE (d.id = ? OR d.path = ? OR d.title = ?) AND ${visibleDocWhere}`)
|
|
354
|
+
.get(docId, docId, docId, ...scopeKeys);
|
|
355
|
+
if (!visible) throw new Error(`Document not found in the selected RAG scope for docId: ${docId}`);
|
|
356
|
+
const scopeRemoval = await removeDocumentScopes(db, visible.id, scopeKeys);
|
|
357
|
+
if (scopeRemoval.remainingScopes > 0) {
|
|
358
|
+
return {
|
|
359
|
+
content: [{
|
|
360
|
+
type: "text",
|
|
361
|
+
text: JSON.stringify({
|
|
362
|
+
deleted: false,
|
|
363
|
+
unlinked: true,
|
|
364
|
+
docId: visible.id,
|
|
365
|
+
removedScopes: scopeRemoval.removedScopes,
|
|
366
|
+
remainingScopes: scopeRemoval.remainingScopes,
|
|
367
|
+
}, null, 2),
|
|
368
|
+
}],
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
const { deleteDocument } = await import("../ingest/pipeline.js");
|
|
372
|
+
const result = await deleteDocument(visible.id, db);
|
|
314
373
|
return {
|
|
315
374
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
316
375
|
};
|