@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.
- package/mcp-server/cli.js +64 -13
- package/mcp-server/fact_format.js +177 -0
- package/mcp-server/index.js +257 -70
- package/mcp-server/ingest/normalizer.js +42 -0
- package/mcp-server/ingest/pipeline.js +35 -5
- package/mcp-server/memory.js +4 -0
- package/opencode-plugin/index.js +206 -51
- package/package.json +2 -1
- package/skills/using-memory/SKILL.md +53 -10
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
|
}
|
|
@@ -240,7 +271,7 @@ const MCP_SERVERS = [
|
|
|
240
271
|
|
|
241
272
|
export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
242
273
|
await ensureDir();
|
|
243
|
-
const
|
|
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
|
-
|
|
255
|
-
|
|
285
|
+
readMemory(GLOBAL_KEY),
|
|
286
|
+
readMemory(activeProjectKey),
|
|
256
287
|
]);
|
|
257
288
|
|
|
258
|
-
const context = buildMemoryContext(globalFacts, projectFacts,
|
|
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
|
-
"
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
342
|
-
|
|
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 = (
|
|
368
|
-
let line =
|
|
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
|
|
401
|
-
|
|
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
|
|
408
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
509
|
+
for (let i = from - 1; i < to; i++) indices.push(i);
|
|
438
510
|
}
|
|
439
511
|
}
|
|
440
|
-
if (!
|
|
441
|
-
|
|
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 (!
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
|
|
451
|
-
|
|
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.
|
|
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 | `
|
|
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):
|
|
63
|
-
- `type` (optional, enum: `"text"`, `"file"`, `"url"`):
|
|
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: "
|
|
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 `
|
|
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]`.
|