@lotargo/memory_plugin 1.6.3 → 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.
@@ -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
- factTitle,
15
- factBody,
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
- "PROACTIVE MEMORY DIRECTIVE:\n" +
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 sliced = combined.slice(0, limit);
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
- const entry = sliced[i];
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 { getConfig } = await import("../mcp-server/config/config_manager.js");
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 res = await linkFactToDocument({
421
- factKey: key,
422
- factText,
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 links = await getLinksForDoc(docId);
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
- "Ingest a document into the RAG knowledge base. " +
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
- generateEmbeddings: { type: "boolean", description: "Compute dense vector embeddings", default: true },
458
- },
459
- async execute({ content, type, title, path, generateEmbeddings }) {
460
- const { ingestDocument } = await import("../mcp-server/ingest/pipeline.js");
461
- const result = await ingestDocument({
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
- async execute({ query, limit, instruction, generateEmbeddings }) {
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
- "manage_knowledge_base": {
531
- description:
532
- "Manage the RAG knowledge base: inspect stats, list documents, read full raw document, delete documents, or export/import snapshots.",
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
- async execute({ action, docId, snapshotPath }) {
542
- const { getDatabase } = await import("../mcp-server/db/database.js");
543
- const db = await getDatabase();
544
-
545
- if (action === "stats") {
546
- const docCountRow = await db.prepare("SELECT COUNT(*) as cnt FROM documents").get();
547
- const docCount = docCountRow ? docCountRow.cnt : 0;
548
- const secCountRow = await db.prepare("SELECT COUNT(*) as cnt FROM sections").get();
549
- const secCount = secCountRow ? secCountRow.cnt : 0;
550
- const chunkCountRow = await db.prepare("SELECT COUNT(*) as cnt FROM micro_chunks").get();
551
- const chunkCount = chunkCountRow ? chunkCountRow.cnt : 0;
552
- const edgeCountRow = await db.prepare("SELECT COUNT(*) as cnt FROM graph_edges").get();
553
- const edgeCount = edgeCountRow ? edgeCountRow.cnt : 0;
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("SELECT id, title, path, blob_hash, created_at FROM documents ORDER BY created_at DESC")
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("SELECT id, title, path, blob_hash, created_at FROM documents WHERE id = ? OR path = ? OR title = ?")
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 { deleteDocument } = await import("../mcp-server/ingest/pipeline.js");
599
- const result = await deleteDocument(docId, db);
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.prepare("UPDATE project_aliases SET identity_key = ? WHERE identity_key = ?;").run(targetKey, sourceKey);
798
- await upsertIdentity(db, { key: targetKey, name: sourceIdentity.name, primaryRemote: normalizeRemoteUrl(remote) });
799
- await removeIdentity(db, sourceKey);
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",
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",