@lotargo/memory_plugin 1.2.902 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/mcp-server/cli.js +64 -13
- package/mcp-server/config/config_manager.js +3 -3
- package/mcp-server/fact_format.js +177 -0
- package/mcp-server/index.js +218 -41
- package/mcp-server/ingest/pipeline.js +8 -0
- package/mcp-server/memory.js +203 -188
- package/mcp-server/ml/model_manager.js +2 -2
- package/opencode-plugin/index.js +200 -47
- package/package.json +2 -1
- package/skills/using-memory/SKILL.md +71 -11
package/opencode-plugin/index.js
CHANGED
|
@@ -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
|
|
251
|
+
parts.push("## Global\n" + fmt(globalFacts));
|
|
221
252
|
}
|
|
222
253
|
if (projectFacts.length) {
|
|
223
|
-
parts.push(`## Project: ${projectKey}\n` + projectFacts
|
|
254
|
+
parts.push(`## Project: ${projectKey}\n` + fmt(projectFacts));
|
|
224
255
|
}
|
|
225
256
|
return `<MEMORY>\n${parts.join("\n\n")}\n</MEMORY>`;
|
|
226
257
|
}
|
|
@@ -251,8 +282,8 @@ 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
|
-
|
|
255
|
-
|
|
285
|
+
readMemory(GLOBAL_KEY),
|
|
286
|
+
readMemory(activeProjectKey),
|
|
256
287
|
]);
|
|
257
288
|
|
|
258
289
|
const context = buildMemoryContext(globalFacts, projectFacts, activeProjectKey);
|
|
@@ -290,6 +321,10 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
290
321
|
"(name, goals, constraints, tech preferences, project conventions). " +
|
|
291
322
|
"docId/startLine/endLine/relationType are OPTIONAL and only used to link the fact to a " +
|
|
292
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. " +
|
|
293
328
|
"Translate the fact into English and keep it concise. " +
|
|
294
329
|
"scope: 'project' (default) or 'global'",
|
|
295
330
|
args: {
|
|
@@ -307,16 +342,38 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
307
342
|
description: "Relation type (e.g. 'RULES_FOR', 'IMPLEMENTS', 'REFERENCES')",
|
|
308
343
|
default: "LINKS_TO",
|
|
309
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" },
|
|
310
349
|
},
|
|
311
|
-
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 }) {
|
|
312
351
|
const key = scopeKey(scope || "project", worktree, directory);
|
|
313
352
|
const entries = await readMemory(key);
|
|
314
353
|
const factNormalized = fact.toLowerCase().trim();
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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 }));
|
|
320
377
|
await writeMemory(key, entries);
|
|
321
378
|
}
|
|
322
379
|
|
|
@@ -339,15 +396,18 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
339
396
|
}
|
|
340
397
|
}
|
|
341
398
|
|
|
342
|
-
|
|
343
|
-
|
|
399
|
+
const result = "Memory updated" + supersededInfo + linkInfo;
|
|
400
|
+
await notify(client, result);
|
|
401
|
+
return result;
|
|
344
402
|
},
|
|
345
403
|
},
|
|
346
404
|
"recall": {
|
|
347
405
|
description:
|
|
348
406
|
"Show saved facts with any Agent-linked Knowledge Base documents/lines. " +
|
|
349
407
|
"scope: 'project', 'global', 'all' (default), or 'list_projects'. " +
|
|
350
|
-
"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.",
|
|
351
411
|
args: {
|
|
352
412
|
scope: {
|
|
353
413
|
type: "string",
|
|
@@ -355,8 +415,12 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
355
415
|
default: "all",
|
|
356
416
|
},
|
|
357
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)" },
|
|
358
422
|
},
|
|
359
|
-
async execute({ scope, project }, { worktree, directory }) {
|
|
423
|
+
async execute({ scope, project, query, tags, since, until }, { worktree, directory }) {
|
|
360
424
|
const results = [];
|
|
361
425
|
|
|
362
426
|
let getLinksForFact;
|
|
@@ -365,11 +429,11 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
365
429
|
getLinksForFact = linker.getLinksForFact;
|
|
366
430
|
} catch (e) {}
|
|
367
431
|
|
|
368
|
-
const formatFactWithLinks = (
|
|
369
|
-
let line =
|
|
432
|
+
const formatFactWithLinks = (factLine, key) => {
|
|
433
|
+
let line = displayFact(factLine);
|
|
370
434
|
if (getLinksForFact) {
|
|
371
435
|
try {
|
|
372
|
-
const links = getLinksForFact(key, factText);
|
|
436
|
+
const links = getLinksForFact(key, factText(factLine));
|
|
373
437
|
if (links && links.length > 0) {
|
|
374
438
|
const docStr = links
|
|
375
439
|
.map((l) => {
|
|
@@ -384,39 +448,45 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
384
448
|
return line;
|
|
385
449
|
};
|
|
386
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
|
+
|
|
387
465
|
if (scope === "list_projects") {
|
|
388
466
|
return listProjectStores().then((stores) => {
|
|
389
467
|
if (!stores.length) return "No project memory stores found.";
|
|
390
468
|
const lines = stores.map(
|
|
391
469
|
(s, i) => `${i + 1}. ${s.basename} — ${s.count} fact(s) [${s.file}]${s.path ? ` (bound to ${s.path})` : " (unbound legacy store)"}`
|
|
392
470
|
);
|
|
393
|
-
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}`;
|
|
394
472
|
});
|
|
395
473
|
}
|
|
396
474
|
|
|
397
|
-
const target = project ? canonicalPath(project) : projectKey(worktree, directory);
|
|
398
|
-
const label = project ? target : projectName(worktree, directory);
|
|
399
|
-
|
|
400
475
|
if (scope !== "project") {
|
|
401
|
-
const global = await
|
|
402
|
-
|
|
403
|
-
results.push("--- Global ---");
|
|
404
|
-
global.forEach((e, i) => results.push(`${i + 1}. ${formatFactWithLinks(e, GLOBAL_KEY)}`));
|
|
405
|
-
}
|
|
476
|
+
const global = await readMemory(GLOBAL_KEY);
|
|
477
|
+
collect(global, GLOBAL_KEY);
|
|
406
478
|
}
|
|
407
479
|
if (scope !== "global") {
|
|
408
|
-
const local = await
|
|
409
|
-
|
|
410
|
-
if (results.length) results.push("");
|
|
411
|
-
results.push(`--- Project: ${label} ---`);
|
|
412
|
-
local.forEach((e, i) => results.push(`${i + 1}. ${formatFactWithLinks(e, target)}`));
|
|
413
|
-
}
|
|
480
|
+
const local = await readMemory(target);
|
|
481
|
+
collect(local, target);
|
|
414
482
|
}
|
|
415
|
-
|
|
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}`;
|
|
416
486
|
},
|
|
417
487
|
},
|
|
418
488
|
"forget": {
|
|
419
|
-
description: "Удалить факт по номеру (см. recall), по диапазону (например '3-30', включительно) или
|
|
489
|
+
description: "Удалить факт по номеру (см. recall), по диапазону (например '3-30', включительно) или тексту. Защищённые факты (remember с keep=true) пропускаются, если не передан force=true",
|
|
420
490
|
args: {
|
|
421
491
|
query: { type: "string", description: "Номер факта, диапазон вида '3-30' или текст для поиска" },
|
|
422
492
|
scope: {
|
|
@@ -424,35 +494,118 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
424
494
|
description: "project (по умолчанию) или global",
|
|
425
495
|
default: "project",
|
|
426
496
|
},
|
|
497
|
+
force: { type: "boolean", description: "Удалить также защищённые (keep) факты" },
|
|
427
498
|
},
|
|
428
|
-
async execute({ query, scope }, { worktree, directory }) {
|
|
499
|
+
async execute({ query, scope, force }, { worktree, directory }) {
|
|
429
500
|
const key = scopeKey(scope || "project", worktree, directory);
|
|
430
501
|
const entries = await readMemory(key);
|
|
431
502
|
const rangeMatch = /^\s*(\d+)\s*-\s*(\d+)\s*$/.exec(query);
|
|
432
503
|
const num = parseInt(query, 10);
|
|
433
|
-
let
|
|
504
|
+
let indices = [];
|
|
434
505
|
if (rangeMatch) {
|
|
435
506
|
const from = parseInt(rangeMatch[1], 10);
|
|
436
507
|
const to = parseInt(rangeMatch[2], 10);
|
|
437
508
|
if (from > 0 && to >= from && to <= entries.length) {
|
|
438
|
-
|
|
509
|
+
for (let i = from - 1; i < to; i++) indices.push(i);
|
|
439
510
|
}
|
|
440
511
|
}
|
|
441
|
-
if (!
|
|
442
|
-
|
|
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), []);
|
|
443
518
|
}
|
|
444
|
-
if (!
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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);
|
|
449
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;
|
|
450
551
|
await writeMemory(key, entries);
|
|
451
|
-
|
|
452
|
-
|
|
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);
|
|
453
565
|
return result;
|
|
454
566
|
},
|
|
455
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
|
+
},
|
|
456
609
|
"link_knowledge": {
|
|
457
610
|
description:
|
|
458
611
|
"Explicitly link a Notebook memory fact to a Knowledge Base document, section, or line range. " +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotargo/memory_plugin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.1",
|
|
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,14 +1,15 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: using-memory
|
|
3
|
-
description: Comprehensive guide for using the Memory
|
|
3
|
+
description: Comprehensive guide for using the Memory, Hybrid RAG Knowledge Engine & MCP Helper tools (remember, recall, forget, update_fact, memory_info, link_knowledge, ingest_document, query_knowledge_base, manage_knowledge_base, list-mcp-tools, mcp-reminder). Trigger proactively whenever user preferences, project conventions, technology stack choices, or architecture decisions are introduced, or when querying ingested documentation, indexing files/repos, managing persistent knowledge, or looking up available MCP tool integrations.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Using Memory
|
|
6
|
+
# Using Memory, Hybrid RAG Knowledge Engine & MCP Helper Tools
|
|
7
7
|
|
|
8
|
-
You have access to a persistent dual-layer memory engine supercharged with an **Agent-Driven Knowledge Graph
|
|
8
|
+
You have access to a persistent dual-layer memory engine supercharged with an **Agent-Driven Knowledge Graph** and general MCP integration helpers:
|
|
9
9
|
1. **Layer 1: Notebook Store (Key-Value Facts)**: Stores high-signal personal preferences, project conventions, and durable rules in clean Markdown.
|
|
10
10
|
2. **Layer 2: RAG Knowledge Base**: Indexes documentation, repositories, and technical guides for hybrid semantic retrieval.
|
|
11
11
|
3. **Layer 3: Agent-Driven Knowledge Graph**: Connects Notebook facts (Layer 1) to specific Knowledge Base documents, sections, and **exact line ranges** (Layer 2).
|
|
12
|
+
4. **Integration Layer (General MCP Helpers)**: Quickly discovers connected MCP servers and identifies appropriate tools for specific tasks.
|
|
12
13
|
|
|
13
14
|
---
|
|
14
15
|
|
|
@@ -17,17 +18,24 @@ You have access to a persistent dual-layer memory engine supercharged with an **
|
|
|
17
18
|
| Scenario / Intent | Target Tool | Key Parameters |
|
|
18
19
|
|-------------------|-------------|----------------|
|
|
19
20
|
| 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 | `
|
|
21
|
+
| User asks what you remember about them, the project, or linked docs | `recall` | `scope` ("all", "global", or "project"), optional `query`, `tags`, `since`, `until`, `project` |
|
|
22
|
+
| User corrects/updates an old saved fact | `update_fact` | `id` (number/id/text), `newText`, `scope` |
|
|
23
|
+
| Replace a fact but keep a version trail | `remember` | `fact`, `supersedes` (number/id/text) |
|
|
24
|
+
| Protect a fact from accidental `forget` | `remember` | `keep: true` |
|
|
25
|
+
| Set a time-to-live on a fact | `remember` | `ttl` ("90d", "2w", "24h", "12m") |
|
|
26
|
+
| Filter facts by keyword / tags / date | `recall` | `query`, `tags`, `since`, `until` |
|
|
27
|
+
| Show storage paths, versions, fact & RAG stats | `memory_info` | — |
|
|
22
28
|
| Connect a Notebook fact to a document, section, or line range | `link_knowledge` | `factText`, `docId`, `startLine`, `endLine`, `relationType` |
|
|
23
29
|
| User asks to index a documentation URL, file, or repository | `ingest_document` | `content` or `source_path`, `title`, `metadata` |
|
|
24
30
|
| User asks a complex question about indexed docs or code | `query_knowledge_base` | `query`, `limit`, `generateEmbeddings` |
|
|
25
31
|
| Read full raw content of an ambiguous/abstract document | `manage_knowledge_base` | `action: "read_document"`, `docId` |
|
|
26
|
-
| User asks to view database stats, list indexed docs, or export snapshots | `manage_knowledge_base` | `action` ("stats", "list", "read_document", "delete", "export_snapshot") |
|
|
32
|
+
| User asks to view database stats, list indexed docs, or export snapshots | `manage_knowledge_base` | `action` ("stats", "list", "read_document", "delete", "export_snapshot", "import_snapshot") |
|
|
33
|
+
| Discover available MCP servers and their specific purposes | `list-mcp-tools` | — |
|
|
34
|
+
| Ask which MCP tool / server is suitable for a specific task | `mcp-reminder` | `task` (string, e.g., "db migration") |
|
|
27
35
|
|
|
28
36
|
---
|
|
29
37
|
|
|
30
|
-
## 2. Layer 1 & 3: Notebook Store & Agent-Driven Knowledge Graph (`remember`, `recall`, `link_knowledge`)
|
|
38
|
+
## 2. Layer 1 & 3: Notebook Store & Agent-Driven Knowledge Graph (`remember`, `recall`, `update_fact`, `forget`, `memory_info`, `link_knowledge`)
|
|
31
39
|
|
|
32
40
|
### Agent-Driven Knowledge Graph Architecture
|
|
33
41
|
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 +58,43 @@ When `recall` is invoked, the engine returns saved facts along with their Agent-
|
|
|
50
58
|
2. PostgreSQL 16 is primary database 🔗 [Linked Docs: database_guide.md:L20-35]
|
|
51
59
|
```
|
|
52
60
|
|
|
61
|
+
### Fact Line Format & Metadata
|
|
62
|
+
Each fact is stored as a single Markdown line with an optional invisible HTML comment carrying metadata:
|
|
63
|
+
```
|
|
64
|
+
- [2026-08-02 06:08] user prefers TypeScript <!-- id:8f3a2c, ttl:90d, keep:1, tags:pref,arch -->
|
|
65
|
+
```
|
|
66
|
+
Supported metadata keys (set via `remember`, rendered as badges by `recall`):
|
|
67
|
+
- `id` — auto-generated short id; stable reference for `update_fact` / `forget` / `supersedes`.
|
|
68
|
+
- `ttl` — time-to-live ("90d", "2w", "24h", "12m", bare number = days). Expired facts are marked `[EXPIRED]` but never auto-deleted.
|
|
69
|
+
- `keep` — protection flag; `forget` skips it unless `force: true`.
|
|
70
|
+
- `tags` — comma-separated free-form tags for filtering.
|
|
71
|
+
- `supersedes` / `supersededBy` — versioning: the old fact gets `[SUPERSEDED]` and is excluded from the injected memory block while staying in the store for history.
|
|
72
|
+
|
|
73
|
+
### Remember Options (`remember`)
|
|
74
|
+
- `ttl`: "90d", "2w", "24h", "12m" — mark the fact for expiry; it will show `[EXPIRED]` once past.
|
|
75
|
+
- `keep: true`: protect the fact from `forget` (unless `force: true`).
|
|
76
|
+
- `tags`: comma-separated tags for later filtering, e.g. `"pref,arch"`.
|
|
77
|
+
- `supersedes`: number (as listed by `recall`), metadata `id`, or text of the fact this one replaces.
|
|
78
|
+
|
|
79
|
+
### Filtering Facts (`recall`)
|
|
80
|
+
- `query`: all space-separated terms must match (case-insensitive); searches text, id, tags, and date.
|
|
81
|
+
- `tags`: comma-separated; returns facts with ANY matching tag.
|
|
82
|
+
- `since` / `until`: "YYYY-MM-DD" (inclusive) to filter by fact date.
|
|
83
|
+
- `project`: read a specific project's store from any working directory.
|
|
84
|
+
- Output shows `[EXPIRED]`, `[KEEP]`, `[SUPERSEDED]` badges and the `Store file:` path.
|
|
85
|
+
|
|
86
|
+
### Updating Facts (`update_fact`)
|
|
87
|
+
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.
|
|
88
|
+
- `id`: recall index number, metadata `id`, or text of the fact.
|
|
89
|
+
- `newText`: replacement text.
|
|
90
|
+
- `scope`: "project" (default) or "global".
|
|
91
|
+
|
|
92
|
+
### Protecting Facts (`forget` with `keep`)
|
|
93
|
+
`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.
|
|
94
|
+
|
|
95
|
+
### Storage Diagnostics (`memory_info`)
|
|
96
|
+
`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).
|
|
97
|
+
|
|
53
98
|
---
|
|
54
99
|
|
|
55
100
|
## 3. Layer 2: RAG Knowledge Base (`ingest_document`, `query_knowledge_base`, `manage_knowledge_base`)
|
|
@@ -71,7 +116,7 @@ Use this tool when adding technical documentation, API specs, architectural docu
|
|
|
71
116
|
|
|
72
117
|
### Hybrid Retrieval (`query_knowledge_base`)
|
|
73
118
|
Use this tool BEFORE answering deep architectural or technical questions when indexed documents exist.
|
|
74
|
-
- Performs **Hybrid RRF Fusion** combining SQLite FTS5 BM25 keyword matching with dense ONNX vector semantic search.
|
|
119
|
+
- Performs **Hybrid RRF/RSF Fusion** combining SQLite FTS5 BM25 keyword matching with dense ONNX vector semantic search.
|
|
75
120
|
- Returns candidate sections with breadcrumb paths and defined code symbols (classes, functions, types).
|
|
76
121
|
|
|
77
122
|
#### Query Formulation Rules (CRITICAL for retrieval quality)
|
|
@@ -112,13 +157,28 @@ In such cases, use the **Full Raw Document Reading** mechanism:
|
|
|
112
157
|
- Use `action: "list"` to see all ingested documents.
|
|
113
158
|
- Use `action: "read_document"` with `docId` to read the complete raw text content of any document.
|
|
114
159
|
- Use `action: "delete"` with `docId` to remove an outdated document and purge its CAS blob.
|
|
160
|
+
- Use `action: "export_snapshot"` with `snapshotPath` to export a JSON backup of the RAG base.
|
|
161
|
+
- Use `action: "import_snapshot"` with `snapshotPath` to import and merge a JSON backup into the current database.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## 4. General MCP Helpers (`list-mcp-tools`, `mcp-reminder`)
|
|
166
|
+
|
|
167
|
+
### Discovering Connected MCP Servers (`list-mcp-tools`)
|
|
168
|
+
When working in multi-server environments (e.g., OpenCode, Claude Code), you might have several auxiliary servers installed (for database, UI design, browser automation, etc.).
|
|
169
|
+
- Use `list-mcp-tools` to immediately view all registered servers and their descriptions. This avoids guessing what other capabilities are available in the current workspace.
|
|
170
|
+
|
|
171
|
+
### Contextual Tool Reminders (`mcp-reminder`)
|
|
172
|
+
- If you are unsure which tool/server is best suited for the task at hand (e.g., how to do browser testing, or run a database migration), run `mcp-reminder(task: "your current task definition")`.
|
|
173
|
+
- It analyzes your task and suggests appropriate servers (like `playwright` for testing, `supabase` for DB, or `stitch` for UI design).
|
|
115
174
|
|
|
116
175
|
---
|
|
117
176
|
|
|
118
|
-
##
|
|
177
|
+
## 5. Core Directives for AI Agents
|
|
119
178
|
|
|
120
179
|
1. **Read Memories First (MANDATORY)**: At the very start of any session or conversation, your VERY FIRST STEP MUST BE to execute `recall` to load all saved facts, user context, and project guidelines BEFORE performing any other task or code analysis.
|
|
121
180
|
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.
|
|
122
|
-
3. **Check Knowledge Base First**: If a
|
|
181
|
+
3. **Check Knowledge Base First**: If a query is related to specialized documentation, APIs, or project architectures, call `query_knowledge_base` using concept-dense search phrases.
|
|
123
182
|
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.
|
|
124
|
-
5. **Keep Memory Clean**: If a preference changes, call `
|
|
183
|
+
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]`.
|
|
184
|
+
6. **Leverage MCP Servers**: Proactively list available tools using `list-mcp-tools` and query `mcp-reminder` if unsure of which platform tool can help you automate tasks.
|