@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
package/opencode-plugin/index.js
CHANGED
|
@@ -8,13 +8,11 @@ import { fileURLToPath } from "node:url";
|
|
|
8
8
|
import {
|
|
9
9
|
parseFactEntry,
|
|
10
10
|
factText,
|
|
11
|
-
factMeta,
|
|
12
|
-
isSuperseded,
|
|
13
|
-
displayFact,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
metaBadges,
|
|
17
|
-
} from "../mcp-server/fact_format.js";
|
|
11
|
+
factMeta,
|
|
12
|
+
isSuperseded,
|
|
13
|
+
displayFact,
|
|
14
|
+
factBody,
|
|
15
|
+
} from "../mcp-server/fact_format.js";
|
|
18
16
|
|
|
19
17
|
import {
|
|
20
18
|
MEMORY_DIR,
|
|
@@ -28,7 +26,8 @@ import {
|
|
|
28
26
|
} from "../mcp-server/memory.js";
|
|
29
27
|
|
|
30
28
|
import { closeDatabase } from "../mcp-server/db/database.js";
|
|
31
|
-
import { requireProjectKey } from "../mcp-server/tools/helpers.js";
|
|
29
|
+
import { requireProjectKey } from "../mcp-server/tools/helpers.js";
|
|
30
|
+
import { resolveRagScopeKey, resolveRagScopeKeys, resolveManageRagScopeKeys, removeDocumentScopes } from "../mcp-server/rag_scope.js";
|
|
32
31
|
// Shared Notebook tool implementations — the same code the MCP server runs, so
|
|
33
32
|
// a fix in one surface can no longer miss the other.
|
|
34
33
|
import {
|
|
@@ -103,16 +102,20 @@ async function notify(client, message, variant = "success") {
|
|
|
103
102
|
|
|
104
103
|
const MEMORY_INSTRUCTION =
|
|
105
104
|
"MANDATORY FIRST STEP (READ MEMORIES FIRST):\n" +
|
|
106
|
-
"At the start of every session, you MUST thoroughly review all saved memories provided below BEFORE performing any user task or editing code.\n" +
|
|
107
|
-
"If calling `recall` manually, your very first action MUST BE to request ALL global memories (`scope: \"all\"` without restrictive query filters) to ensure no global facts or preferences are missed.\n" +
|
|
108
|
-
"
|
|
105
|
+
"At the start of every session, you MUST thoroughly review all saved memories provided below BEFORE performing any user task or editing code.\n" +
|
|
106
|
+
"If calling `recall` manually, your very first action MUST BE to request ALL global memories (`scope: \"all\"` without restrictive query filters) to ensure no global facts or preferences are missed.\n" +
|
|
107
|
+
"PROJECT IDENTITY DIRECTIVE:\n" +
|
|
108
|
+
"After reviewing the injected memories, call `memory_info`. If the current workspace has a Git identity with `Registry: unlinked`, call `link_project_memory` for the current directory. Re-read memories only when linking migrated legacy facts. Outside Git, use global memory only.\n" +
|
|
109
|
+
"PROACTIVE MEMORY DIRECTIVE:\n" +
|
|
109
110
|
"You MUST automatically and proactively call `remember` whenever the user shares durable facts, personal preferences, coding guidelines, tech stack choices, architecture decisions, or project conventions.\n" +
|
|
110
111
|
"Do NOT wait for explicit user commands like \"remember this\". Automatically capture high-signal facts in real time.\n" +
|
|
111
112
|
"Use `remember` only for important, durable facts about the user and project.\n" +
|
|
112
113
|
"Save high-signal items: user role, goals, constraints, tech stack preferences, architecture decisions, project conventions.\n" +
|
|
113
114
|
"DO NOT save: transient details, one-off statements, full conversation turns, or anything unlikely to be useful in future sessions.\n" +
|
|
114
|
-
"When saving, translate the fact into clear, concise English.\n" +
|
|
115
|
-
"Use `scope: \"global\"` for personal facts, `scope: \"project\"` for project-specific facts
|
|
115
|
+
"When saving, translate the fact into clear, concise English.\n" +
|
|
116
|
+
"Use `scope: \"global\"` for personal facts, `scope: \"project\"` for project-specific facts.\n" +
|
|
117
|
+
"SELECTIVE RAG DIRECTIVE:\n" +
|
|
118
|
+
"When web research or current technical documentation yields reliable project knowledge likely to be reused, ingest only the relevant source or excerpt with project scope and link it to the project Notebook fact it supports. Use global RAG only for intentionally cross-project sources. Prefer authoritative and newer-than-training documentation; do not dump everything encountered into RAG.";
|
|
116
119
|
|
|
117
120
|
function sortNewestFirst(entries) {
|
|
118
121
|
return [...entries].sort((a, b) => {
|
|
@@ -126,7 +129,7 @@ function sortNewestFirst(entries) {
|
|
|
126
129
|
});
|
|
127
130
|
}
|
|
128
131
|
|
|
129
|
-
function formatInjectedFacts(entries, limit, now = Date.now()) {
|
|
132
|
+
export function formatInjectedFacts(entries, limit, now = Date.now()) {
|
|
130
133
|
const activeEntries = entries.filter((e) => !isSuperseded(e));
|
|
131
134
|
const sorted = sortNewestFirst(activeEntries);
|
|
132
135
|
|
|
@@ -143,36 +146,23 @@ function formatInjectedFacts(entries, limit, now = Date.now()) {
|
|
|
143
146
|
}
|
|
144
147
|
|
|
145
148
|
const combined = [...injectPriority, ...normalPriority];
|
|
146
|
-
const
|
|
149
|
+
const hasLimit = Number.isFinite(Number(limit)) && Number(limit) > 0;
|
|
150
|
+
const sliced = hasLimit ? combined.slice(0, Number(limit)) : combined;
|
|
147
151
|
|
|
148
|
-
const formattedLines = [];
|
|
149
|
-
for (let i = 0; i < sliced.length; i++) {
|
|
150
|
-
|
|
151
|
-
const meta = factMeta(entry);
|
|
152
|
-
const isPriority = meta.inject === "1";
|
|
153
|
-
|
|
154
|
-
let contentStr;
|
|
155
|
-
if (isPriority) {
|
|
156
|
-
contentStr = displayFact(entry, now);
|
|
157
|
-
} else {
|
|
158
|
-
const title = factTitle(entry);
|
|
159
|
-
const badges = metaBadges(entry, now);
|
|
160
|
-
const badgesStr = badges.length ? ` [${badges.join("] [")}]` : "";
|
|
161
|
-
contentStr = `${title}${badgesStr}`;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
formattedLines.push(`${i + 1}. ${contentStr}`);
|
|
152
|
+
const formattedLines = [];
|
|
153
|
+
for (let i = 0; i < sliced.length; i++) {
|
|
154
|
+
formattedLines.push(`${i + 1}. ${displayFact(sliced[i], now)}`);
|
|
165
155
|
}
|
|
166
156
|
|
|
167
|
-
if (activeEntries.length > limit) {
|
|
168
|
-
const remaining = activeEntries.length - limit;
|
|
157
|
+
if (hasLimit && activeEntries.length > Number(limit)) {
|
|
158
|
+
const remaining = activeEntries.length - Number(limit);
|
|
169
159
|
formattedLines.push(`... and ${remaining} more of ${activeEntries.length} memories (use recall tool to fetch all)`);
|
|
170
160
|
}
|
|
171
161
|
|
|
172
162
|
return formattedLines.join("\n");
|
|
173
163
|
}
|
|
174
164
|
|
|
175
|
-
function buildMemoryContext(globalFacts, projectFacts, projectKey, injectLimit, now = Date.now()) {
|
|
165
|
+
export function buildMemoryContext(globalFacts, projectFacts, projectKey, injectLimit, now = Date.now()) {
|
|
176
166
|
const parts = [MEMORY_INSTRUCTION];
|
|
177
167
|
|
|
178
168
|
if (globalFacts.length) {
|
|
@@ -232,11 +222,7 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
232
222
|
readMemory(await currentProjectKey()),
|
|
233
223
|
]);
|
|
234
224
|
|
|
235
|
-
const
|
|
236
|
-
const config = getConfig();
|
|
237
|
-
const injectLimit = config.injectLimit !== undefined ? config.injectLimit : 100;
|
|
238
|
-
|
|
239
|
-
const context = buildMemoryContext(globalFacts, projectFacts, activeProjectKey, injectLimit);
|
|
225
|
+
const context = buildMemoryContext(globalFacts, projectFacts, activeProjectKey, null);
|
|
240
226
|
const ref = firstUser.parts[0];
|
|
241
227
|
firstUser.parts.unshift({ ...ref, type: "text", text: context });
|
|
242
228
|
},
|
|
@@ -325,7 +311,8 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
325
311
|
until: { type: "string", description: "Optional end date filter, YYYY-MM-DD (inclusive)" },
|
|
326
312
|
mode: { type: "string", description: "Result mode: 'full' (with body, default) or 'headers' (title and badges only)", default: "full" },
|
|
327
313
|
offset: { type: "number", description: "Pagination offset (optional)" },
|
|
328
|
-
limit: { type: "number", description: "Pagination limit (optional)" },
|
|
314
|
+
limit: { type: "number", description: "Pagination limit (optional)" },
|
|
315
|
+
includeSuperseded: { type: "boolean", description: "Include superseded historical facts (excluded by default)", default: false },
|
|
329
316
|
},
|
|
330
317
|
async execute(args, { worktree, directory }) {
|
|
331
318
|
return await recallFacts(args, { worktree, directory });
|
|
@@ -413,13 +400,22 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
413
400
|
requireProjectKey(key);
|
|
414
401
|
}
|
|
415
402
|
|
|
416
|
-
if (act === "link") {
|
|
417
|
-
if (!factText || !docId) {
|
|
418
|
-
throw new Error("factText and docId are required parameters for link action");
|
|
419
|
-
}
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
403
|
+
if (act === "link") {
|
|
404
|
+
if (!factText || !docId) {
|
|
405
|
+
throw new Error("factText and docId are required parameters for link action");
|
|
406
|
+
}
|
|
407
|
+
const facts = await readMemory(key);
|
|
408
|
+
const needle = factText.toLowerCase().trim();
|
|
409
|
+
const matches = facts.filter((entry) => {
|
|
410
|
+
const body = factBody(entry).toLowerCase();
|
|
411
|
+
return body === needle || body.includes(needle) || entry.toLowerCase().includes(needle);
|
|
412
|
+
});
|
|
413
|
+
if (matches.length === 0) throw new Error(`Notebook fact not found for link: ${factText}`);
|
|
414
|
+
if (matches.length > 1) throw new Error(`Notebook fact match is ambiguous; use a more specific factText: ${factText}`);
|
|
415
|
+
const resolvedFactText = factBody(matches[0]);
|
|
416
|
+
const res = await linkFactToDocument({
|
|
417
|
+
factKey: key,
|
|
418
|
+
factText: resolvedFactText,
|
|
423
419
|
docId,
|
|
424
420
|
startLine,
|
|
425
421
|
endLine,
|
|
@@ -428,9 +424,10 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
428
424
|
return JSON.stringify(res, null, 2);
|
|
429
425
|
}
|
|
430
426
|
|
|
431
|
-
if (act === "get_doc_links") {
|
|
432
|
-
if (!docId) throw new Error("docId parameter is required for get_doc_links action");
|
|
433
|
-
const
|
|
427
|
+
if (act === "get_doc_links") {
|
|
428
|
+
if (!docId) throw new Error("docId parameter is required for get_doc_links action");
|
|
429
|
+
const allowedScopes = key === GLOBAL_KEY ? [GLOBAL_KEY] : [GLOBAL_KEY, key];
|
|
430
|
+
const links = await getLinksForDoc(docId, allowedScopes);
|
|
434
431
|
return JSON.stringify(links, null, 2);
|
|
435
432
|
}
|
|
436
433
|
|
|
@@ -443,8 +440,8 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
443
440
|
},
|
|
444
441
|
},
|
|
445
442
|
"ingest_document": {
|
|
446
|
-
description:
|
|
447
|
-
"
|
|
443
|
+
description:
|
|
444
|
+
"Selectively preserve a reliable, reusable source in the RAG knowledge base; do not ingest everything encountered. " +
|
|
448
445
|
"Accepts local file paths, web URLs, or raw Markdown/text content. " +
|
|
449
446
|
"For type='url' the page is fetched and its content is indexed (not just the URL). " +
|
|
450
447
|
"Processes document through 3-tier hierarchy chunking (Big/Medium/Small), " +
|
|
@@ -453,17 +450,20 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
453
450
|
content: { type: "string", description: "Raw text content, file path, or web URL" },
|
|
454
451
|
type: { type: "string", description: "Input content type: 'text', 'file', 'url' (url fetches the page content)", default: "text" },
|
|
455
452
|
title: { type: "string", description: "Document title" },
|
|
456
|
-
path: { type: "string", description: "Original document file path" },
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const
|
|
453
|
+
path: { type: "string", description: "Original document file path" },
|
|
454
|
+
scope: { type: "string", description: "RAG visibility: current Git project (default) or global", default: "project" },
|
|
455
|
+
generateEmbeddings: { type: "boolean", description: "Compute dense vector embeddings", default: true },
|
|
456
|
+
},
|
|
457
|
+
async execute({ content, type, title, path, scope, generateEmbeddings }, { worktree, directory }) {
|
|
458
|
+
const { ingestDocument } = await import("../mcp-server/ingest/pipeline.js");
|
|
459
|
+
const projectScope = await resolveRagScopeKey(scope || "project", { worktree, directory });
|
|
460
|
+
const result = await ingestDocument({
|
|
462
461
|
content,
|
|
463
462
|
type: type || "text",
|
|
464
463
|
title: title || null,
|
|
465
464
|
path: path || null,
|
|
466
|
-
generateEmbeddings: generateEmbeddings !== false,
|
|
465
|
+
generateEmbeddings: generateEmbeddings !== false,
|
|
466
|
+
projectScope,
|
|
467
467
|
});
|
|
468
468
|
return JSON.stringify(
|
|
469
469
|
{
|
|
@@ -472,16 +472,17 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
472
472
|
title: result.title,
|
|
473
473
|
sectionsCount: result.sectionsCount,
|
|
474
474
|
microChunksCount: result.microChunksCount,
|
|
475
|
-
deduplicated: result.deduplicated,
|
|
475
|
+
deduplicated: result.deduplicated,
|
|
476
|
+
scope: result.projectScope,
|
|
476
477
|
},
|
|
477
478
|
null,
|
|
478
479
|
2
|
|
479
480
|
);
|
|
480
481
|
},
|
|
481
482
|
},
|
|
482
|
-
"query_knowledge_base": {
|
|
483
|
-
description:
|
|
484
|
-
"Perform hybrid search (RSF/RRF BM25 full-text + dense vector similarity) across the RAG knowledge base. " +
|
|
483
|
+
"query_knowledge_base": {
|
|
484
|
+
description:
|
|
485
|
+
"Perform project-isolated hybrid search (RSF/RRF BM25 full-text + dense vector similarity) across the RAG knowledge base. " +
|
|
485
486
|
"Returns top-ranked candidate document sections with breadcrumbs, GraphRAG defined code symbols, and relevance scores.",
|
|
486
487
|
args: {
|
|
487
488
|
query: { type: "string", description: "Search query in natural language or symbol name" },
|
|
@@ -490,18 +491,21 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
490
491
|
type: "string",
|
|
491
492
|
description: "Optional task-specific retrieval instruction shaping embedding focus",
|
|
492
493
|
},
|
|
493
|
-
generateEmbeddings: { type: "boolean", description: "Use vector search alongside BM25", default: true },
|
|
494
|
-
|
|
495
|
-
|
|
494
|
+
generateEmbeddings: { type: "boolean", description: "Use vector search alongside BM25", default: true },
|
|
495
|
+
scope: { type: "string", description: "Search global + current project (default), project only, or global only", default: "all" },
|
|
496
|
+
},
|
|
497
|
+
async execute({ query, limit, instruction, generateEmbeddings, scope }, { worktree, directory }) {
|
|
496
498
|
const { hybridQuery } = await import("../mcp-server/retrieval/retriever.js");
|
|
497
499
|
const { getConfig } = await import("../mcp-server/config/config_manager.js");
|
|
498
|
-
const activeConfig = getConfig();
|
|
500
|
+
const activeConfig = getConfig();
|
|
501
|
+
const scopeKeys = await resolveRagScopeKeys(scope || "all", { worktree, directory });
|
|
499
502
|
|
|
500
503
|
const results = await hybridQuery({
|
|
501
504
|
query,
|
|
502
505
|
limit: limit || 5,
|
|
503
506
|
generateEmbeddings: generateEmbeddings !== false,
|
|
504
|
-
instruction: instruction || null,
|
|
507
|
+
instruction: instruction || null,
|
|
508
|
+
scopeKeys,
|
|
505
509
|
});
|
|
506
510
|
|
|
507
511
|
if (!results || results.length === 0) {
|
|
@@ -524,33 +528,99 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
524
528
|
})
|
|
525
529
|
.join("\n\n---\n\n");
|
|
526
530
|
|
|
527
|
-
return headerNote + formatted;
|
|
528
|
-
},
|
|
529
|
-
},
|
|
530
|
-
"
|
|
531
|
-
description:
|
|
532
|
-
"
|
|
531
|
+
return headerNote + formatted;
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
"batch_query_knowledge_base": {
|
|
535
|
+
description:
|
|
536
|
+
"Execute multiple project-isolated hybrid searches in one call. " +
|
|
537
|
+
"All query embeddings are computed in one ONNX pass and results are returned in input order.",
|
|
538
|
+
args: {
|
|
539
|
+
queries: { type: "array", items: { type: "string" }, description: "Search queries to execute in one batch" },
|
|
540
|
+
limit: { type: "number", description: "Maximum sections per query", default: 5 },
|
|
541
|
+
instruction: { type: "string", description: "Optional retrieval instruction applied to every query" },
|
|
542
|
+
generateEmbeddings: { type: "boolean", description: "Use vector search alongside BM25", default: true },
|
|
543
|
+
scope: { type: "string", description: "Search global + current project (default), project only, or global only", default: "all" },
|
|
544
|
+
},
|
|
545
|
+
async execute({ queries, limit, instruction, generateEmbeddings, scope }, { worktree, directory }) {
|
|
546
|
+
const { batchHybridQuery } = await import("../mcp-server/retrieval/retriever.js");
|
|
547
|
+
const { getConfig } = await import("../mcp-server/config/config_manager.js");
|
|
548
|
+
const activeConfig = getConfig();
|
|
549
|
+
const scopeKeys = await resolveRagScopeKeys(scope || "all", { worktree, directory });
|
|
550
|
+
const allResults = await batchHybridQuery(queries, {
|
|
551
|
+
limit: limit || 5,
|
|
552
|
+
generateEmbeddings: generateEmbeddings !== false,
|
|
553
|
+
instruction: instruction || null,
|
|
554
|
+
scopeKeys,
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
const formatted = allResults.map((results, queryIndex) => {
|
|
558
|
+
const header = `## Query ${queryIndex + 1}: "${queries[queryIndex]}"\n`;
|
|
559
|
+
if (!results || results.length === 0) return `${header}_No results found._`;
|
|
560
|
+
return header + results.map((result, resultIndex) => {
|
|
561
|
+
let itemHeader = `### [${resultIndex + 1}] ${result.doc_title || "Untitled"}`;
|
|
562
|
+
if (result.heading) itemHeader += ` > ${result.heading}`;
|
|
563
|
+
if (result.breadcrumbs) itemHeader += ` (${result.breadcrumbs})`;
|
|
564
|
+
let body = `Score: ${(result.score || 0).toFixed(4)}`;
|
|
565
|
+
if (result.retrieval_policy && result.retrieval_policy !== "micro_chunk") {
|
|
566
|
+
body += ` [${result.retrieval_policy}]`;
|
|
567
|
+
}
|
|
568
|
+
if (result.defined_symbols && result.defined_symbols.length > 0) {
|
|
569
|
+
body += `\nDefined Symbols: ${result.defined_symbols.join(", ")}`;
|
|
570
|
+
}
|
|
571
|
+
body += `\n\n${result.snippet || result.full_section_content || ""}`;
|
|
572
|
+
return `${itemHeader}\n${body}`;
|
|
573
|
+
}).join("\n\n---\n\n");
|
|
574
|
+
}).join("\n\n===\n\n");
|
|
575
|
+
|
|
576
|
+
return `[Active Model: ${activeConfig.embeddingModel} | Fusion: ${activeConfig.fusionAlgorithm.toUpperCase()} | ${queries.length} queries]\n\n${formatted}`;
|
|
577
|
+
},
|
|
578
|
+
},
|
|
579
|
+
"manage_knowledge_base": {
|
|
580
|
+
description:
|
|
581
|
+
"Manage the project-isolated RAG knowledge base: inspect stats, list documents, read full raw document, unlink/delete documents, or export/import complete snapshots.",
|
|
533
582
|
args: {
|
|
534
583
|
action: {
|
|
535
584
|
type: "string",
|
|
536
585
|
description: "Management action: 'stats', 'list', 'read_document', 'delete', 'export_snapshot', 'import_snapshot'",
|
|
537
586
|
},
|
|
538
587
|
docId: { type: "string", description: "Document ID, title, or path (required for read_document and delete)" },
|
|
539
|
-
snapshotPath: { type: "string", description: "File path for snapshot export/import" },
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
const
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
588
|
+
snapshotPath: { type: "string", description: "File path for snapshot export/import" },
|
|
589
|
+
scope: { type: "string", description: "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" },
|
|
590
|
+
},
|
|
591
|
+
async execute({ action, docId, snapshotPath, scope }, { worktree, directory }) {
|
|
592
|
+
const { getDatabase } = await import("../mcp-server/db/database.js");
|
|
593
|
+
const db = await getDatabase();
|
|
594
|
+
const scopeKeys = ["stats", "list", "read_document", "delete"].includes(action)
|
|
595
|
+
? await resolveManageRagScopeKeys(action, scope, { worktree, directory })
|
|
596
|
+
: null;
|
|
597
|
+
const placeholders = scopeKeys ? scopeKeys.map(() => "?").join(",") : "";
|
|
598
|
+
const visibleDocWhere = scopeKeys
|
|
599
|
+
? `EXISTS (SELECT 1 FROM document_scopes ds WHERE ds.doc_id = d.id AND ds.scope_key IN (${placeholders}))`
|
|
600
|
+
: "1=1";
|
|
601
|
+
|
|
602
|
+
if (action === "stats") {
|
|
603
|
+
const docCountRow = await db.prepare(`SELECT COUNT(*) as cnt FROM documents d WHERE ${visibleDocWhere}`).get(...scopeKeys);
|
|
604
|
+
const docCount = docCountRow ? docCountRow.cnt : 0;
|
|
605
|
+
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);
|
|
606
|
+
const secCount = secCountRow ? secCountRow.cnt : 0;
|
|
607
|
+
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);
|
|
608
|
+
const chunkCount = chunkCountRow ? chunkCountRow.cnt : 0;
|
|
609
|
+
const visibleDocIds = await db.prepare(`SELECT d.id FROM documents d WHERE ${visibleDocWhere}`).all(...scopeKeys);
|
|
610
|
+
let edgeCount = 0;
|
|
611
|
+
if (visibleDocIds.length > 0) {
|
|
612
|
+
const docIds = visibleDocIds.map((row) => row.id);
|
|
613
|
+
const docPlaceholders = docIds.map(() => "?").join(",");
|
|
614
|
+
const ownedRows = await db.prepare(`
|
|
615
|
+
SELECT id FROM sections WHERE doc_id IN (${docPlaceholders})
|
|
616
|
+
UNION SELECT id FROM medium_chunks WHERE doc_id IN (${docPlaceholders})
|
|
617
|
+
UNION SELECT id FROM micro_chunks WHERE doc_id IN (${docPlaceholders})
|
|
618
|
+
`).all(...docIds, ...docIds, ...docIds);
|
|
619
|
+
const ownedIds = [...docIds, ...ownedRows.map((row) => row.id)];
|
|
620
|
+
const edgePlaceholders = ownedIds.map(() => "?").join(",");
|
|
621
|
+
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);
|
|
622
|
+
edgeCount = edgeCountRow ? edgeCountRow.cnt : 0;
|
|
623
|
+
}
|
|
554
624
|
return JSON.stringify(
|
|
555
625
|
{
|
|
556
626
|
documents: docCount,
|
|
@@ -563,18 +633,18 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
563
633
|
);
|
|
564
634
|
}
|
|
565
635
|
|
|
566
|
-
if (action === "list") {
|
|
567
|
-
const docs = await db
|
|
568
|
-
.prepare(
|
|
569
|
-
.all();
|
|
636
|
+
if (action === "list") {
|
|
637
|
+
const docs = await db
|
|
638
|
+
.prepare(`SELECT d.id, d.title, d.path, d.blob_hash, d.created_at FROM documents d WHERE ${visibleDocWhere} ORDER BY d.created_at DESC`)
|
|
639
|
+
.all(...scopeKeys);
|
|
570
640
|
return JSON.stringify(docs, null, 2);
|
|
571
641
|
}
|
|
572
642
|
|
|
573
643
|
if (action === "read_document") {
|
|
574
644
|
if (!docId) throw new Error("docId parameter is required for read_document action");
|
|
575
|
-
const doc = await db
|
|
576
|
-
.prepare(
|
|
577
|
-
.get(docId, docId, docId);
|
|
645
|
+
const doc = await db
|
|
646
|
+
.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}`)
|
|
647
|
+
.get(docId, docId, docId, ...scopeKeys);
|
|
578
648
|
if (!doc) {
|
|
579
649
|
throw new Error(`Document not found in knowledge base for docId: ${docId}`);
|
|
580
650
|
}
|
|
@@ -593,10 +663,24 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
593
663
|
);
|
|
594
664
|
}
|
|
595
665
|
|
|
596
|
-
if (action === "delete") {
|
|
597
|
-
if (!docId) throw new Error("docId parameter is required for delete action");
|
|
598
|
-
const
|
|
599
|
-
|
|
666
|
+
if (action === "delete") {
|
|
667
|
+
if (!docId) throw new Error("docId parameter is required for delete action");
|
|
668
|
+
const visible = await db
|
|
669
|
+
.prepare(`SELECT d.id FROM documents d WHERE (d.id = ? OR d.path = ? OR d.title = ?) AND ${visibleDocWhere}`)
|
|
670
|
+
.get(docId, docId, docId, ...scopeKeys);
|
|
671
|
+
if (!visible) throw new Error(`Document not found in the selected RAG scope for docId: ${docId}`);
|
|
672
|
+
const scopeRemoval = await removeDocumentScopes(db, visible.id, scopeKeys);
|
|
673
|
+
if (scopeRemoval.remainingScopes > 0) {
|
|
674
|
+
return JSON.stringify({
|
|
675
|
+
deleted: false,
|
|
676
|
+
unlinked: true,
|
|
677
|
+
docId: visible.id,
|
|
678
|
+
removedScopes: scopeRemoval.removedScopes,
|
|
679
|
+
remainingScopes: scopeRemoval.remainingScopes,
|
|
680
|
+
}, null, 2);
|
|
681
|
+
}
|
|
682
|
+
const { deleteDocument } = await import("../mcp-server/ingest/pipeline.js");
|
|
683
|
+
const result = await deleteDocument(visible.id, db);
|
|
600
684
|
return JSON.stringify(result, null, 2);
|
|
601
685
|
}
|
|
602
686
|
|
|
@@ -683,7 +767,7 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
683
767
|
let migrated = false;
|
|
684
768
|
const legacyPathKey = canonicalPath(dir);
|
|
685
769
|
const legacyEntries = await readMemory(legacyPathKey);
|
|
686
|
-
if (legacyEntries && legacyEntries.length > 0) {
|
|
770
|
+
if (legacyEntries && legacyEntries.length > 0) {
|
|
687
771
|
const gitEntries = await readMemory(key);
|
|
688
772
|
const seen = new Set(gitEntries.map((e) => factBody(e).toLowerCase().trim()));
|
|
689
773
|
let mergedCount = 0;
|
|
@@ -706,8 +790,11 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
706
790
|
const { unlink } = await import("fs/promises");
|
|
707
791
|
await unlink(legacyFp);
|
|
708
792
|
}
|
|
709
|
-
} catch (e) {}
|
|
710
|
-
}
|
|
793
|
+
} catch (e) {}
|
|
794
|
+
}
|
|
795
|
+
const { moveKnowledgeScope } = await import("../mcp-server/graph/knowledge_linker.js");
|
|
796
|
+
const migratedKnowledge = await moveKnowledgeScope(db, legacyPathKey, key);
|
|
797
|
+
if (migratedKnowledge.movedLinks > 0 || migratedKnowledge.movedDocuments > 0) migrated = true;
|
|
711
798
|
|
|
712
799
|
const res = {
|
|
713
800
|
status: "success",
|
|
@@ -794,9 +881,11 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
794
881
|
|
|
795
882
|
await writeMemory(targetKey, targetFacts);
|
|
796
883
|
|
|
797
|
-
await db
|
|
798
|
-
await
|
|
799
|
-
await
|
|
884
|
+
await upsertIdentity(db, { key: targetKey, name: sourceIdentity.name, primaryRemote: normalizeRemoteUrl(remote) });
|
|
885
|
+
await db.prepare("UPDATE project_aliases SET identity_key = ? WHERE identity_key = ?;").run(targetKey, sourceKey);
|
|
886
|
+
const { moveKnowledgeScope } = await import("../mcp-server/graph/knowledge_linker.js");
|
|
887
|
+
const movedKnowledge = await moveKnowledgeScope(db, sourceKey, targetKey);
|
|
888
|
+
await removeIdentity(db, sourceKey);
|
|
800
889
|
|
|
801
890
|
try {
|
|
802
891
|
const sourceFp = storeFilePath(sourceKey);
|
|
@@ -811,7 +900,9 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
811
900
|
status: "success",
|
|
812
901
|
sourceKey,
|
|
813
902
|
targetKey,
|
|
814
|
-
mergedFacts: mergedCount
|
|
903
|
+
mergedFacts: mergedCount,
|
|
904
|
+
movedKnowledgeLinks: movedKnowledge.movedLinks,
|
|
905
|
+
movedRagDocuments: movedKnowledge.movedDocuments
|
|
815
906
|
};
|
|
816
907
|
await notify(client, "Project memory relinked");
|
|
817
908
|
return JSON.stringify(res, null, 2);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotargo/memory_plugin",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.4",
|
|
4
4
|
"description": "100% local hybrid RAG memory for AI coding agents (OpenCode, Claude Code, Codex, Antigravity). MCP server + plugin: persistent user facts, document ingestion, vector + SQLite FTS5 retrieval across sessions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "opencode-plugin/index.js",
|
|
@@ -33,8 +33,10 @@
|
|
|
33
33
|
"mcp-server/tools",
|
|
34
34
|
"mcp-server/boot.js",
|
|
35
35
|
"mcp-server/cli_boot.js",
|
|
36
|
-
"mcp-server/cli.js",
|
|
37
|
-
"mcp-server/
|
|
36
|
+
"mcp-server/cli.js",
|
|
37
|
+
"mcp-server/codex_config.js",
|
|
38
|
+
"mcp-server/codex_diagnostics.js",
|
|
39
|
+
"mcp-server/index.js",
|
|
38
40
|
"mcp-server/fact_format.js",
|
|
39
41
|
"mcp-server/identity.js",
|
|
40
42
|
"mcp-server/logger.js",
|