@lotargo/memory_plugin 1.2.901 → 1.3.0

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.
@@ -3,6 +3,32 @@ const { existsSync } = await import("fs");
3
3
  const { join, basename, dirname, resolve } = await import("path");
4
4
  const { homedir } = await import("os");
5
5
  const { fileURLToPath } = await import("url");
6
+ const {
7
+ parseFactEntry,
8
+ factText,
9
+ factMeta,
10
+ withMeta,
11
+ nextFactId,
12
+ isKeepFact,
13
+ isSuperseded,
14
+ displayFact,
15
+ formatFactEntry,
16
+ matchesQuery,
17
+ matchesTags,
18
+ inDateRange,
19
+ } = await import("../mcp-server/fact_format.js");
20
+
21
+ // Resolve a fact reference (1-based number, metadata id, or text) to an index.
22
+ function resolveFactIndex(entries, ref) {
23
+ const trimmed = String(ref || "").trim();
24
+ if (!trimmed) return -1;
25
+ const num = parseInt(trimmed, 10);
26
+ if (/^\d+$/.test(trimmed) && num >= 1 && num <= entries.length) return num - 1;
27
+ const idIdx = entries.findIndex((e) => factMeta(e).id === trimmed);
28
+ if (idIdx !== -1) return idIdx;
29
+ const textIdx = entries.findIndex((e) => factText(e).toLowerCase().includes(trimmed.toLowerCase()));
30
+ return textIdx;
31
+ }
6
32
 
7
33
  const CONFIG_DIR = process.env.OPENCODE_CONFIG_DIR || join(homedir(), ".config", "opencode");
8
34
  const MEMORY_DIR = join(CONFIG_DIR, "memory");
@@ -214,13 +240,18 @@ const MEMORY_INSTRUCTION =
214
240
  "When saving, translate the fact into clear, concise English.\n" +
215
241
  "Use `scope: \"global\"` for personal facts, `scope: \"project\"` for project-specific facts.";
216
242
 
217
- function buildMemoryContext(globalFacts, projectFacts, projectKey) {
243
+ function buildMemoryContext(globalFacts, projectFacts, projectKey, now = Date.now()) {
218
244
  const parts = [MEMORY_INSTRUCTION];
245
+ const fmt = (entries) =>
246
+ entries
247
+ .filter((e) => !isSuperseded(e))
248
+ .map((e, i) => `${i + 1}. ${displayFact(e, now)}`)
249
+ .join("\n");
219
250
  if (globalFacts.length) {
220
- parts.push("## Global\n" + globalFacts.map((f, i) => `${i + 1}. ${f}`).join("\n"));
251
+ parts.push("## Global\n" + fmt(globalFacts));
221
252
  }
222
253
  if (projectFacts.length) {
223
- parts.push(`## Project: ${projectKey}\n` + projectFacts.map((f, i) => `${i + 1}. ${f}`).join("\n"));
254
+ parts.push(`## Project: ${projectKey}\n` + fmt(projectFacts));
224
255
  }
225
256
  return `<MEMORY>\n${parts.join("\n\n")}\n</MEMORY>`;
226
257
  }
@@ -240,7 +271,7 @@ const MCP_SERVERS = [
240
271
 
241
272
  export const MemoryPlugin = async ({ directory, worktree, client }) => {
242
273
  await ensureDir();
243
- const projectKey = scopeKey("project", worktree, directory);
274
+ const activeProjectKey = scopeKey("project", worktree, directory);
244
275
 
245
276
  return {
246
277
  "experimental.chat.messages.transform": async (_input, output) => {
@@ -251,11 +282,11 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
251
282
  if (firstUser.parts.some((p) => p.type === "text" && p.text.includes("<MEMORY>"))) return;
252
283
 
253
284
  const [globalFacts, projectFacts] = await Promise.all([
254
- readMemoryRaw(GLOBAL_KEY),
255
- readMemoryRaw(projectKey),
285
+ readMemory(GLOBAL_KEY),
286
+ readMemory(activeProjectKey),
256
287
  ]);
257
288
 
258
- const context = buildMemoryContext(globalFacts, projectFacts, projectKey);
289
+ const context = buildMemoryContext(globalFacts, projectFacts, activeProjectKey);
259
290
  const ref = firstUser.parts[0];
260
291
  firstUser.parts.unshift({ ...ref, type: "text", text: context });
261
292
  },
@@ -288,7 +319,12 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
288
319
  description:
289
320
  "Save an important, durable fact to memory. Only use for high-signal information " +
290
321
  "(name, goals, constraints, tech preferences, project conventions). " +
291
- "Optionally link the fact to a Knowledge Base document or exact line range (docId, startLine, endLine). " +
322
+ "docId/startLine/endLine/relationType are OPTIONAL and only used to link the fact to a " +
323
+ "Knowledge Base document or line range; omit them when no linking is needed. " +
324
+ "ttl is OPTIONAL (e.g. '90d', '2w', '24h') — expired facts are shown with [EXPIRED] but not auto-deleted. " +
325
+ "keep=true protects the fact from forget deletion unless force=true. " +
326
+ "tags is OPTIONAL comma-separated text for filtering. " +
327
+ "supersedes is OPTIONAL: a number, id, or text of a fact this one replaces. " +
292
328
  "Translate the fact into English and keep it concise. " +
293
329
  "scope: 'project' (default) or 'global'",
294
330
  args: {
@@ -306,16 +342,38 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
306
342
  description: "Relation type (e.g. 'RULES_FOR', 'IMPLEMENTS', 'REFERENCES')",
307
343
  default: "LINKS_TO",
308
344
  },
345
+ ttl: { type: "string", description: "Optional time-to-live, e.g. '90d', '2w', '24h', '12m'" },
346
+ keep: { type: "boolean", description: "Protect the fact from forget deletion unless force=true" },
347
+ tags: { type: "string", description: "Optional comma-separated tags, e.g. 'pref,arch'" },
348
+ supersedes: { type: "string", description: "Optional number, id, or text of the fact this one replaces" },
309
349
  },
310
- async execute({ fact, scope, docId, startLine, endLine, relationType }, { worktree, directory }) {
350
+ async execute({ fact, scope, docId, startLine, endLine, relationType, ttl, keep, tags, supersedes }, { worktree, directory }) {
311
351
  const key = scopeKey(scope || "project", worktree, directory);
312
352
  const entries = await readMemory(key);
313
353
  const factNormalized = fact.toLowerCase().trim();
314
- if (!entries.some((e) => {
315
- const idx = e.indexOf("] ");
316
- return idx !== -1 && e.slice(idx + 2).toLowerCase().trim() === factNormalized;
317
- })) {
318
- entries.push(`- [${today()}] ${fact}`);
354
+ const duplicate = entries.some((e) => factText(e).toLowerCase().trim() === factNormalized);
355
+
356
+ let supersededInfo = "";
357
+ if (!duplicate) {
358
+ const [date, time] = today().split(" ");
359
+ const meta = { ttl, tags };
360
+ if (keep) meta.keep = "1";
361
+ if (supersedes) {
362
+ const targetIdx = resolveFactIndex(entries, supersedes);
363
+ if (targetIdx !== -1) {
364
+ const newId = nextFactId(entries);
365
+ const targetMeta = factMeta(entries[targetIdx]);
366
+ const targetId = targetMeta.id || nextFactId(entries);
367
+ entries[targetIdx] = withMeta(entries[targetIdx], { id: targetId, supersededBy: newId });
368
+ meta.id = newId;
369
+ meta.supersedes = targetId;
370
+ supersededInfo = ` [superseded: "${factText(entries[targetIdx]).slice(0, 60)}"]`;
371
+ } else {
372
+ supersededInfo = " (note: supersedes target not found)";
373
+ }
374
+ }
375
+ if (!meta.id) meta.id = nextFactId(entries);
376
+ entries.push(formatFactEntry({ date, time, text: fact, meta }));
319
377
  await writeMemory(key, entries);
320
378
  }
321
379
 
@@ -338,15 +396,18 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
338
396
  }
339
397
  }
340
398
 
341
- await notify(client, "Memory updated" + linkInfo);
342
- return "Memory updated" + linkInfo;
399
+ const result = "Memory updated" + supersededInfo + linkInfo;
400
+ await notify(client, result);
401
+ return result;
343
402
  },
344
403
  },
345
404
  "recall": {
346
405
  description:
347
406
  "Show saved facts with any Agent-linked Knowledge Base documents/lines. " +
348
407
  "scope: 'project', 'global', 'all' (default), or 'list_projects'. " +
349
- "Use project: '<directory path>' to read facts of a specific project from any working directory.",
408
+ "Use project: '<directory path>' to read facts of a specific project from any working directory. " +
409
+ "query filters by keyword, tags by comma-separated tags, since/until by date (YYYY-MM-DD). " +
410
+ "The response includes the store file paths.",
350
411
  args: {
351
412
  scope: {
352
413
  type: "string",
@@ -354,8 +415,12 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
354
415
  default: "all",
355
416
  },
356
417
  project: { type: "string", description: "Directory path of the project to read facts from (e.g. 'F:/projects/plugins/memory')" },
418
+ query: { type: "string", description: "Optional keyword filter; all space-separated terms must match" },
419
+ tags: { type: "string", description: "Optional comma-separated tag filter (any match)" },
420
+ since: { type: "string", description: "Optional start date filter, YYYY-MM-DD (inclusive)" },
421
+ until: { type: "string", description: "Optional end date filter, YYYY-MM-DD (inclusive)" },
357
422
  },
358
- async execute({ scope, project }, { worktree, directory }) {
423
+ async execute({ scope, project, query, tags, since, until }, { worktree, directory }) {
359
424
  const results = [];
360
425
 
361
426
  let getLinksForFact;
@@ -364,11 +429,11 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
364
429
  getLinksForFact = linker.getLinksForFact;
365
430
  } catch (e) {}
366
431
 
367
- const formatFactWithLinks = (factText, key) => {
368
- let line = factText;
432
+ const formatFactWithLinks = (factLine, key) => {
433
+ let line = displayFact(factLine);
369
434
  if (getLinksForFact) {
370
435
  try {
371
- const links = getLinksForFact(key, factText);
436
+ const links = getLinksForFact(key, factText(factLine));
372
437
  if (links && links.length > 0) {
373
438
  const docStr = links
374
439
  .map((l) => {
@@ -383,39 +448,45 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
383
448
  return line;
384
449
  };
385
450
 
451
+ const target = project ? canonicalPath(project) : projectKey(worktree, directory);
452
+ const label = project ? target : projectName(worktree, directory);
453
+
454
+ const collect = (entries, key) => {
455
+ const matched = entries.filter(
456
+ (e) => matchesQuery(e, query) && matchesTags(e, tags) && inDateRange(e, since, until)
457
+ );
458
+ if (!matched.length) return;
459
+ if (results.length) results.push("");
460
+ results.push(`--- ${key === GLOBAL_KEY ? "Global" : `Project: ${key === target ? label : key}`} ---`);
461
+ matched.forEach((e, i) => results.push(`${i + 1}. ${formatFactWithLinks(e, key)}`));
462
+ results.push(`Store file: ${memoryPath(key)}`);
463
+ };
464
+
386
465
  if (scope === "list_projects") {
387
466
  return listProjectStores().then((stores) => {
388
467
  if (!stores.length) return "No project memory stores found.";
389
468
  const lines = stores.map(
390
469
  (s, i) => `${i + 1}. ${s.basename} — ${s.count} fact(s) [${s.file}]${s.path ? ` (bound to ${s.path})` : " (unbound legacy store)"}`
391
470
  );
392
- return `Project Memory Stores:\n${lines.join("\n")}\n\nUse recall(scope: "project", project: "<path>") to read a specific store.`;
471
+ return `Project Memory Stores:\n${lines.join("\n")}\n\nUse recall(scope: "project", project: "<path>") to read a specific store.\n\nMemory dir: ${MEMORY_DIR}`;
393
472
  });
394
473
  }
395
474
 
396
- const target = project ? canonicalPath(project) : projectKey(worktree, directory);
397
- const label = project ? target : projectName(worktree, directory);
398
-
399
475
  if (scope !== "project") {
400
- const global = await readMemoryRaw(GLOBAL_KEY);
401
- if (global.length) {
402
- results.push("--- Global ---");
403
- global.forEach((e, i) => results.push(`${i + 1}. ${formatFactWithLinks(e, GLOBAL_KEY)}`));
404
- }
476
+ const global = await readMemory(GLOBAL_KEY);
477
+ collect(global, GLOBAL_KEY);
405
478
  }
406
479
  if (scope !== "global") {
407
- const local = await readMemoryRaw(target);
408
- if (local.length) {
409
- if (results.length) results.push("");
410
- results.push(`--- Project: ${label} ---`);
411
- local.forEach((e, i) => results.push(`${i + 1}. ${formatFactWithLinks(e, target)}`));
412
- }
480
+ const local = await readMemory(target);
481
+ collect(local, target);
413
482
  }
414
- return results.length ? results.join("\n") : "Memory is empty.";
483
+ const filtered = Boolean(query || tags || since || until);
484
+ if (!results.length) return filtered ? "No facts match the search." : "Memory is empty.";
485
+ return results.join("\n") + `\n\nMemory dir: ${MEMORY_DIR}`;
415
486
  },
416
487
  },
417
488
  "forget": {
418
- description: "Удалить факт по номеру (см. recall), по диапазону (например '3-30', включительно) или тексту",
489
+ description: "Удалить факт по номеру (см. recall), по диапазону (например '3-30', включительно) или тексту. Защищённые факты (remember с keep=true) пропускаются, если не передан force=true",
419
490
  args: {
420
491
  query: { type: "string", description: "Номер факта, диапазон вида '3-30' или текст для поиска" },
421
492
  scope: {
@@ -423,35 +494,118 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
423
494
  description: "project (по умолчанию) или global",
424
495
  default: "project",
425
496
  },
497
+ force: { type: "boolean", description: "Удалить также защищённые (keep) факты" },
426
498
  },
427
- async execute({ query, scope }, { worktree, directory }) {
499
+ async execute({ query, scope, force }, { worktree, directory }) {
428
500
  const key = scopeKey(scope || "project", worktree, directory);
429
501
  const entries = await readMemory(key);
430
502
  const rangeMatch = /^\s*(\d+)\s*-\s*(\d+)\s*$/.exec(query);
431
503
  const num = parseInt(query, 10);
432
- let removed;
504
+ let indices = [];
433
505
  if (rangeMatch) {
434
506
  const from = parseInt(rangeMatch[1], 10);
435
507
  const to = parseInt(rangeMatch[2], 10);
436
508
  if (from > 0 && to >= from && to <= entries.length) {
437
- removed = entries.splice(from - 1, to - from + 1);
509
+ for (let i = from - 1; i < to; i++) indices.push(i);
438
510
  }
439
511
  }
440
- if (!removed && !isNaN(num) && num > 0 && num <= entries.length) {
441
- removed = entries.splice(num - 1, 1);
512
+ if (!indices.length && !isNaN(num) && num > 0 && num <= entries.length) {
513
+ indices.push(num - 1);
514
+ }
515
+ if (!indices.length) {
516
+ const q = query.toLowerCase();
517
+ indices = entries.reduce((acc, e, i) => (e.toLowerCase().includes(q) ? acc.concat(i) : acc), []);
442
518
  }
443
- if (!removed) {
444
- const filtered = entries.filter((e) => !e.toLowerCase().includes(query.toLowerCase()));
445
- removed = entries.filter((e) => e.toLowerCase().includes(query.toLowerCase()));
446
- entries.length = 0;
447
- entries.push(...filtered);
519
+ if (!indices.length) return "Not found.";
520
+
521
+ const removable = indices.filter((i) => force || !isKeepFact(entries[i]));
522
+ const protectedCount = indices.length - removable.length;
523
+ if (removable.length) {
524
+ for (const i of removable.sort((a, b) => b - a)) entries.splice(i, 1);
525
+ await writeMemory(key, entries);
448
526
  }
527
+ let result = removable.length ? "Memory updated" : "Nothing removed.";
528
+ if (protectedCount) result += ` (${protectedCount} protected fact(s) skipped; use force=true to override)`;
529
+ if (removable.length) await notify(client, result);
530
+ return result;
531
+ },
532
+ },
533
+ "update_fact": {
534
+ description:
535
+ "Update the text of an existing fact by number (from recall), id, or text match, " +
536
+ "preserving its original date and metadata. Linked Knowledge Base documents are re-pointed to the new text.",
537
+ args: {
538
+ id: { type: "string", description: "Number (from recall), metadata id, or text of the fact to update" },
539
+ newText: { type: "string", description: "New fact text" },
540
+ scope: { type: "string", description: "'project' (default) or 'global'", default: "project" },
541
+ },
542
+ async execute({ id, newText, scope }, { worktree, directory }) {
543
+ const key = scopeKey(scope || "project", worktree, directory);
544
+ const entries = await readMemory(key);
545
+ const idx = resolveFactIndex(entries, id);
546
+ if (idx === -1) throw new Error(`Fact not found: ${id}`);
547
+ const p = parseFactEntry(entries[idx]);
548
+ const oldText = p ? p.text : entries[idx];
549
+ const newLine = formatFactEntry({ date: p.date, time: p.time, text: newText, meta: p.meta });
550
+ entries[idx] = newLine;
449
551
  await writeMemory(key, entries);
450
- const result = removed.length ? "Memory updated" : "Not found.";
451
- if (removed.length) await notify(client, "Memory updated");
552
+
553
+ let linksUpdated = 0;
554
+ try {
555
+ const { getDatabase } = await import("../mcp-server/db/database.js");
556
+ const db = getDatabase();
557
+ const res = db
558
+ .prepare("UPDATE knowledge_links SET fact_text = ? WHERE fact_key = ? AND fact_text = ?")
559
+ .run(newText, key, oldText);
560
+ linksUpdated = res.changes;
561
+ } catch (e) {}
562
+
563
+ const result = `Fact updated${linksUpdated ? `, ${linksUpdated} doc link(s) updated` : ""}`;
564
+ await notify(client, result);
452
565
  return result;
453
566
  },
454
567
  },
568
+ "memory_info": {
569
+ description: "Show memory storage paths (store files, MEMORY_DIR, SQLite DB), fact counts, and Knowledge Base stats.",
570
+ args: {},
571
+ async execute() {
572
+ const dbPath = join(MEMORY_DIR, "storage", "memory.sqlite");
573
+ let version = "unknown";
574
+ try {
575
+ const { readFile } = await import("fs/promises");
576
+ version = JSON.parse(
577
+ await readFile(new URL("../package.json", import.meta.url), "utf-8")
578
+ ).version;
579
+ } catch (e) {}
580
+
581
+ let rag = {};
582
+ try {
583
+ const { getDatabase } = await import("../mcp-server/db/database.js");
584
+ const db = getDatabase();
585
+ rag.documents = db.prepare("SELECT COUNT(*) AS c FROM documents").get().c;
586
+ rag.sections = db.prepare("SELECT COUNT(*) AS c FROM sections").get().c;
587
+ rag.chunks = db.prepare("SELECT COUNT(*) AS c FROM micro_chunks").get().c;
588
+ rag.edges = db.prepare("SELECT COUNT(*) AS c FROM graph_edges").get().c;
589
+ rag.links = db.prepare("SELECT COUNT(*) AS c FROM knowledge_links").get().c;
590
+ } catch (e) {
591
+ rag.error = e.message;
592
+ }
593
+
594
+ const lines = [
595
+ `Version: ${version}`,
596
+ `MEMORY_DIR: ${MEMORY_DIR}`,
597
+ `SQLite DB: ${dbPath}`,
598
+ `Global store: ${memoryPath(GLOBAL_KEY)}`,
599
+ `Project store: ${memoryPath(activeProjectKey)}`,
600
+ ];
601
+ if (rag.error) lines.push(`RAG: unavailable (${rag.error})`);
602
+ else
603
+ lines.push(
604
+ `RAG: ${rag.documents} doc(s), ${rag.sections} section(s), ${rag.chunks} chunk(s), ${rag.edges} edge(s), ${rag.links} memory link(s)`
605
+ );
606
+ return lines.join("\n");
607
+ },
608
+ },
455
609
  "link_knowledge": {
456
610
  description:
457
611
  "Explicitly link a Notebook memory fact to a Knowledge Base document, section, or line range. " +
@@ -511,11 +665,12 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
511
665
  description:
512
666
  "Ingest a document into the RAG knowledge base. " +
513
667
  "Accepts local file paths, web URLs, or raw Markdown/text content. " +
668
+ "For type='url' the page is fetched and its content is indexed (not just the URL). " +
514
669
  "Processes document through 3-tier hierarchy chunking (Big/Medium/Small), " +
515
670
  "computes dense vectors, and extracts GraphRAG code symbols.",
516
671
  args: {
517
672
  content: { type: "string", description: "Raw text content, file path, or web URL" },
518
- type: { type: "string", description: "Input content type: 'text', 'file', 'url'", default: "text" },
673
+ type: { type: "string", description: "Input content type: 'text', 'file', 'url' (url fetches the page content)", default: "text" },
519
674
  title: { type: "string", description: "Document title" },
520
675
  path: { type: "string", description: "Original document file path" },
521
676
  generateEmbeddings: { type: "boolean", description: "Compute dense vector embeddings", default: true },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotargo/memory_plugin",
3
- "version": "1.2.901",
3
+ "version": "1.3.0",
4
4
  "description": "Persistent memory agent for coding AI tools — remembers user preferences and project context across sessions. Works with Antigravity, OpenCode, Claude Code, and Codex.",
5
5
  "type": "module",
6
6
  "main": "opencode-plugin/index.js",
@@ -25,6 +25,7 @@
25
25
  "mcp-server/storage",
26
26
  "mcp-server/cli.js",
27
27
  "mcp-server/index.js",
28
+ "mcp-server/fact_format.js",
28
29
  "mcp-server/memory.js",
29
30
  "mcp-server/setup.js",
30
31
  "mcp-server/preinstall.js",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: using-memory
3
- description: Comprehensive guide for using the Memory & Hybrid RAG Knowledge Engine tools (remember, recall, forget, link_knowledge, ingest_document, query_knowledge_base, manage_knowledge_base). Trigger proactively whenever user preferences, project conventions, technology stack choices, or architecture decisions are introduced, or when querying ingested documentation, indexing files/repos, or managing persistent knowledge.
3
+ description: Comprehensive guide for using the Memory & Hybrid RAG Knowledge Engine tools (remember, recall, forget, update_fact, memory_info, link_knowledge, ingest_document, query_knowledge_base, manage_knowledge_base). Trigger proactively whenever user preferences, project conventions, technology stack choices, or architecture decisions are introduced, or when querying ingested documentation, indexing files/repos, or managing persistent knowledge.
4
4
  ---
5
5
 
6
6
  # Using Memory & Hybrid RAG Knowledge Engine
@@ -17,8 +17,13 @@ You have access to a persistent dual-layer memory engine supercharged with an **
17
17
  | Scenario / Intent | Target Tool | Key Parameters |
18
18
  |-------------------|-------------|----------------|
19
19
  | User shares identity, tech stack preference, or workflow rule | `remember` | `fact` (English), `scope`, optional `docId`, `startLine`, `endLine` |
20
- | User asks what you remember about them, the project, or linked docs | `recall` | `scope` ("all", "global", or "project") |
21
- | User corrects/updates an old saved fact | `forget` then `remember` | `query` (text or index number) |
20
+ | User asks what you remember about them, the project, or linked docs | `recall` | `scope` ("all", "global", or "project"), optional `query`, `tags`, `since`, `until`, `project` |
21
+ | User corrects/updates an old saved fact | `update_fact` | `id` (number/id/text), `newText`, `scope` |
22
+ | Replace a fact but keep a version trail | `remember` | `fact`, `supersedes` (number/id/text) |
23
+ | Protect a fact from accidental `forget` | `remember` | `keep: true` |
24
+ | Set a time-to-live on a fact | `remember` | `ttl` ("90d", "2w", "24h", "12m") |
25
+ | Filter facts by keyword / tags / date | `recall` | `query`, `tags`, `since`, `until` |
26
+ | Show storage paths, versions, fact & RAG stats | `memory_info` | — |
22
27
  | Connect a Notebook fact to a document, section, or line range | `link_knowledge` | `factText`, `docId`, `startLine`, `endLine`, `relationType` |
23
28
  | User asks to index a documentation URL, file, or repository | `ingest_document` | `content` or `source_path`, `title`, `metadata` |
24
29
  | User asks a complex question about indexed docs or code | `query_knowledge_base` | `query`, `limit`, `generateEmbeddings` |
@@ -27,7 +32,7 @@ You have access to a persistent dual-layer memory engine supercharged with an **
27
32
 
28
33
  ---
29
34
 
30
- ## 2. Layer 1 & 3: Notebook Store & Agent-Driven Knowledge Graph (`remember`, `recall`, `link_knowledge`)
35
+ ## 2. Layer 1 & 3: Notebook Store & Agent-Driven Knowledge Graph (`remember`, `recall`, `update_fact`, `forget`, `memory_info`, `link_knowledge`)
31
36
 
32
37
  ### Agent-Driven Knowledge Graph Architecture
33
38
  Automatic regex/heuristic algorithms alone CANNOT infer high-level semantic intent or cross-document relationships. **You (the AI Agent) are the primary architect of the Knowledge Graph.**
@@ -50,6 +55,43 @@ When `recall` is invoked, the engine returns saved facts along with their Agent-
50
55
  2. PostgreSQL 16 is primary database 🔗 [Linked Docs: database_guide.md:L20-35]
51
56
  ```
52
57
 
58
+ ### Fact Line Format & Metadata
59
+ Each fact is stored as a single Markdown line with an optional invisible HTML comment carrying metadata:
60
+ ```
61
+ - [2026-08-02 06:08] user prefers TypeScript <!-- id:8f3a2c, ttl:90d, keep:1, tags:pref,arch -->
62
+ ```
63
+ Supported metadata keys (set via `remember`, rendered as badges by `recall`):
64
+ - `id` — auto-generated short id; stable reference for `update_fact` / `forget` / `supersedes`.
65
+ - `ttl` — time-to-live ("90d", "2w", "24h", "12m", bare number = days). Expired facts are marked `[EXPIRED]` but never auto-deleted.
66
+ - `keep` — protection flag; `forget` skips it unless `force: true`.
67
+ - `tags` — comma-separated free-form tags for filtering.
68
+ - `supersedes` / `supersededBy` — versioning: the old fact gets `[SUPERSEDED]` and is excluded from the injected memory block while staying in the store for history.
69
+
70
+ ### Remember Options (`remember`)
71
+ - `ttl`: "90d", "2w", "24h", "12m" — mark the fact for expiry; it will show `[EXPIRED]` once past.
72
+ - `keep: true`: protect the fact from `forget` (unless `force: true`).
73
+ - `tags`: comma-separated tags for later filtering, e.g. `"pref,arch"`.
74
+ - `supersedes`: number (as listed by `recall`), metadata `id`, or text of the fact this one replaces.
75
+
76
+ ### Filtering Facts (`recall`)
77
+ - `query`: all space-separated terms must match (case-insensitive); searches text, id, tags, and date.
78
+ - `tags`: comma-separated; returns facts with ANY matching tag.
79
+ - `since` / `until`: "YYYY-MM-DD" (inclusive) to filter by fact date.
80
+ - `project`: read a specific project's store from any working directory.
81
+ - Output shows `[EXPIRED]`, `[KEEP]`, `[SUPERSEDED]` badges and the `Store file:` path.
82
+
83
+ ### Updating Facts (`update_fact`)
84
+ When the user corrects an old fact, prefer `update_fact` over `forget`+`remember` — it rewrites the text while preserving the original date and all metadata (`ttl`, `keep`, `tags`, `supersedes`), and re-points any linked Knowledge Base documents.
85
+ - `id`: recall index number, metadata `id`, or text of the fact.
86
+ - `newText`: replacement text.
87
+ - `scope`: "project" (default) or "global".
88
+
89
+ ### Protecting Facts (`forget` with `keep`)
90
+ `forget` refuses to delete facts saved with `keep: true`; pass `force: true` to override. It still supports deleting by index number, range ("3-30"), or text.
91
+
92
+ ### Storage Diagnostics (`memory_info`)
93
+ `memory_info` returns the package version, `MEMORY_DIR`, SQLite DB path, store-file locations, fact counts per store, and RAG stats (documents, sections, chunks, graph edges, links).
94
+
53
95
  ---
54
96
 
55
97
  ## 3. Layer 2: RAG Knowledge Base (`ingest_document`, `query_knowledge_base`, `manage_knowledge_base`)
@@ -59,11 +101,12 @@ Use this tool when adding technical documentation, API specs, architectural docu
59
101
  - **Hierarchy Chunking**: The engine automatically creates 3-tier chunks (Big Document -> Medium Section -> Small Micro-Chunk) and extracts GraphRAG code symbols.
60
102
  - **Auto Vector Embeddings**: Dense ONNX vectors (`multilingual-e5-small`) are automatically computed and indexed in SQLite.
61
103
  - **CRITICAL Schema Usage & Parameters**:
62
- - `content` (required, string): Must be the **actual raw text or markdown content** of the document, NOT just a file path!
63
- - `type` (optional, enum: `"text"`, `"file"`, `"url"`): Set to `"text"` (default) or `"file"`.
64
- - `path` (optional, string): Provide the absolute file path (e.g. `f:\projects\plugins\memory\README.md`).
65
- - `title` (optional, string): Provide document title (e.g. `README.md`).
66
- - **Correct Example**: `ingest_document(content: "<full text content>", path: "f:/path/to/file.md", title: "file.md", type: "file")`
104
+ - `content` (required, string): For `type: "text"`/`"file"` it must be the **actual raw text or markdown content** of the document, NOT just a file path! For `type: "url"` it must be the **page URL** — the page is fetched automatically and its content is indexed (not just the URL).
105
+ - `type` (optional, enum: `"text"`, `"file"`, `"url"`): `"text"` (default), `"file"`, or `"url"` (fetches the web page and indexes its content).
106
+ - `path` (optional, string): Provide the absolute file path (e.g. `f:\projects\plugins\memory\README.md`). For URLs the final URL is used for deduplication.
107
+ - `title` (optional, string): Provide document title (e.g. `README.md`). If omitted for a URL, the page `<title>` is used.
108
+ - **Correct Example (URL)**: `ingest_document(content: "https://docs.example.com/guide", type: "url", title: "Example Guide")`
109
+ - **Correct Example (text)**: `ingest_document(content: "<full text content>", path: "f:/path/to/file.md", title: "file.md", type: "file")`
67
110
  - ❌ **Common Error**: `ingest_document(content: "f:/path/to/file.md")` — this causes validation failures because `content` is missing the text content.
68
111
 
69
112
  - **CLI/Script Execution Note**: When writing batch node scripts to call `ingestDocument`, remember that `@lotargo/memory_plugin` uses ES Modules (`"type": "module"`). Use `import` syntax instead of `require()`.
@@ -120,4 +163,4 @@ In such cases, use the **Full Raw Document Reading** mechanism:
120
163
  2. **Be Proactive**: When the user mentions a durable preference, personal fact, or constraint, save it immediately using `remember`. Do not wait for explicit user commands.
121
164
  3. **Check Knowledge Base First**: If a user asks how a specific module, API, or project architecture works, call `query_knowledge_base` using concept-dense search phrases.
122
165
  4. **Inspect Ambiguous Docs Directly**: If querying produces low relevance scores on abstractly-named documents, call `manage_knowledge_base(action: "read_document")` to inspect the full text directly.
123
- 5. **Keep Memory Clean**: If a preference changes, call `forget` on the outdated entry before saving the new one.
166
+ 5. **Keep Memory Clean**: If a preference changes, call `update_fact` to edit it in place, or `remember` with `supersedes` to keep a version trail. Use `keep: true` for facts that must survive an accidental `forget`, and give ephemeral facts a `ttl` so stale ones surface as `[EXPIRED]`.