@lotargo/memory_plugin 1.5.3 → 1.6.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +138 -0
  2. package/README.md +406 -352
  3. package/mcp-server/admin/auth.js +13 -4
  4. package/mcp-server/admin/snapshot.js +24 -7
  5. package/mcp-server/boot.js +43 -0
  6. package/mcp-server/cli/direct_commands.js +334 -313
  7. package/mcp-server/cli/handlers/engine_actions.js +41 -0
  8. package/mcp-server/cli/handlers/storage_actions.js +58 -0
  9. package/mcp-server/cli/secret_input.js +44 -0
  10. package/mcp-server/cli/ui.js +564 -565
  11. package/mcp-server/cli.js +356 -324
  12. package/mcp-server/cli_boot.js +37 -0
  13. package/mcp-server/config/auth_store.js +74 -16
  14. package/mcp-server/config/config_manager.js +4 -0
  15. package/mcp-server/db/database.js +33 -14
  16. package/mcp-server/db/sync_queue.js +9 -19
  17. package/mcp-server/index.js +112 -42
  18. package/mcp-server/ingest/normalizer.js +116 -29
  19. package/mcp-server/ingest/pipeline.js +94 -6
  20. package/mcp-server/logger.js +49 -0
  21. package/mcp-server/memory.js +6 -9
  22. package/mcp-server/ml/gpu_monitor.js +169 -166
  23. package/mcp-server/ml/model_manager.js +17 -4
  24. package/mcp-server/preinstall.js +23 -2
  25. package/mcp-server/retrieval/retriever.js +35 -15
  26. package/mcp-server/security/path_guard.js +67 -0
  27. package/mcp-server/setup.js +10 -2
  28. package/mcp-server/storage/blob_store.js +15 -2
  29. package/mcp-server/tools/core/memory_core.js +393 -0
  30. package/mcp-server/tools/helpers.js +59 -39
  31. package/mcp-server/tools/memory_tools.js +123 -516
  32. package/mcp-server/tools/rag_tools.js +49 -1
  33. package/opencode-plugin/index.js +94 -397
  34. package/package.json +13 -4
  35. package/skills/using-memory/SKILL.md +7 -2
@@ -1,7 +1,14 @@
1
1
  import * as z from "zod/v4";
2
- import { optStr, defBool, defNum } from "./helpers.js";
2
+ import { optStr, defBool, defNum, optNum } from "./helpers.js";
3
+ import { MEMORY_DIR } from "../memory.js";
4
+ import { registerSnapshotDir } from "../admin/snapshot.js";
5
+ import { ensureExportsDir } from "../ingest/exporter.js";
3
6
 
4
7
  export function registerRagTools(server) {
8
+ // Restrict snapshot export/import paths to the plugin's own data directories.
9
+ registerSnapshotDir(ensureExportsDir());
10
+ registerSnapshotDir(MEMORY_DIR);
11
+
5
12
  server.registerTool(
6
13
  "ingest_document",
7
14
  {
@@ -118,6 +125,47 @@ export function registerRagTools(server) {
118
125
  }
119
126
  );
120
127
 
128
+ server.registerTool(
129
+ "reindex_knowledge_base",
130
+ {
131
+ description:
132
+ "Re-embed all existing documents in the RAG knowledge base with the active (or specified) embedding model and vector dimension. " +
133
+ "Use after switching the embedding model or vector dimension so previously stored vectors match the new configuration. " +
134
+ "Preserves documents, sections, FTS index, graph edges, and fact links.",
135
+ inputSchema: z.object({
136
+ model: optStr().describe("Embedding model to use (defaults to active config.embeddingModel)"),
137
+ dimension: optNum().describe(
138
+ "Fixed vector dimension (defaults to active config.vectorDimension; auto-detect if unset)"
139
+ ),
140
+ }),
141
+ },
142
+ async ({ model, dimension }) => {
143
+ const { reindexEmbeddings } = await import("../ingest/pipeline.js");
144
+ const result = await reindexEmbeddings({
145
+ model: model || null,
146
+ dimension: dimension !== undefined && dimension !== null ? dimension : null,
147
+ });
148
+ return {
149
+ content: [
150
+ {
151
+ type: "text",
152
+ text: JSON.stringify(
153
+ {
154
+ status: "success",
155
+ reindexed: result.reindexed,
156
+ documentsAffected: result.documentsAffected,
157
+ model: result.model,
158
+ dimension: result.dimension || "auto",
159
+ },
160
+ null,
161
+ 2
162
+ ),
163
+ },
164
+ ],
165
+ };
166
+ }
167
+ );
168
+
121
169
  server.registerTool(
122
170
  "manage_knowledge_base",
123
171
  {
@@ -1,52 +1,56 @@
1
- const { mkdir, cp, readdir } = await import("fs/promises");
2
- const { existsSync } = await import("fs");
3
- const { join, dirname } = await import("path");
4
- const { homedir } = await import("os");
5
- const { fileURLToPath } = await import("url");
6
- const {
1
+ // Static ESM imports: top-level `await import(...)` blocked module evaluation
2
+ // and made this file an async module for every consumer.
3
+ import { mkdir, cp, readdir } from "node:fs/promises";
4
+ import { existsSync } from "node:fs";
5
+ import { join, dirname } from "node:path";
6
+ import { homedir } from "node:os";
7
+ import { fileURLToPath } from "node:url";
8
+ import {
7
9
  parseFactEntry,
8
10
  factText,
9
11
  factMeta,
10
- withMeta,
11
- nextFactId,
12
- isKeepFact,
13
12
  isSuperseded,
14
13
  displayFact,
15
- formatFactEntry,
16
- matchesQuery,
17
- matchesTags,
18
- inDateRange,
19
14
  factTitle,
20
15
  factBody,
21
- autoGenerateTitle,
22
16
  metaBadges,
23
- isExpiredLine,
24
- } = await import("../mcp-server/fact_format.js");
17
+ } from "../mcp-server/fact_format.js";
25
18
 
26
- const {
19
+ import {
27
20
  MEMORY_DIR,
28
21
  GLOBAL_KEY,
29
22
  canonicalPath,
30
- projectName,
31
23
  projectKey,
32
24
  scopeKey,
33
25
  readMemory,
34
26
  writeMemory,
35
- listProjectStores,
36
27
  storeFilePath,
37
- today,
38
- } = await import("../mcp-server/memory.js");
39
-
40
- // Resolve a fact reference (1-based number, metadata id, or text) to an index.
41
- function resolveFactIndex(entries, ref) {
42
- const trimmed = String(ref || "").trim();
43
- if (!trimmed) return -1;
44
- const num = parseInt(trimmed, 10);
45
- if (/^\d+$/.test(trimmed) && num >= 1 && num <= entries.length) return num - 1;
46
- const idIdx = entries.findIndex((e) => factMeta(e).id === trimmed);
47
- if (idIdx !== -1) return idIdx;
48
- const textIdx = entries.findIndex((e) => factText(e).toLowerCase().includes(trimmed.toLowerCase()));
49
- return textIdx;
28
+ } from "../mcp-server/memory.js";
29
+
30
+ import { closeDatabase } from "../mcp-server/db/database.js";
31
+ import { requireProjectKey } from "../mcp-server/tools/helpers.js";
32
+ // Shared Notebook tool implementations — the same code the MCP server runs, so
33
+ // a fix in one surface can no longer miss the other.
34
+ import {
35
+ rememberFact,
36
+ recallFacts,
37
+ getFactById,
38
+ forgetFacts,
39
+ updateFactText,
40
+ memoryInfo,
41
+ } from "../mcp-server/tools/core/memory_core.js";
42
+
43
+ // Registered once when the plugin is instantiated, never at import time:
44
+ // importing this module repeatedly used to stack duplicate "exit" listeners.
45
+ let exitHookInstalled = false;
46
+ function installExitHook() {
47
+ if (exitHookInstalled) return;
48
+ exitHookInstalled = true;
49
+ process.on("exit", () => {
50
+ try {
51
+ closeDatabase();
52
+ } catch {}
53
+ });
50
54
  }
51
55
 
52
56
  const CONFIG_DIR = process.env.OPENCODE_CONFIG_DIR || join(homedir(), ".config", "opencode");
@@ -110,16 +114,6 @@ const MEMORY_INSTRUCTION =
110
114
  "When saving, translate the fact into clear, concise English.\n" +
111
115
  "Use `scope: \"global\"` for personal facts, `scope: \"project\"` for project-specific facts.";
112
116
 
113
- function requireProjectKey(key) {
114
- if (!key) {
115
- throw new Error(
116
- "No project memory available: this directory is not inside a git repository. " +
117
- "Project memory is tied to a git repo; use scope: 'global' or open a git repository."
118
- );
119
- }
120
- return key;
121
- }
122
-
123
117
  function sortNewestFirst(entries) {
124
118
  return [...entries].sort((a, b) => {
125
119
  const pa = parseFactEntry(a);
@@ -206,6 +200,7 @@ const MCP_SERVERS = [
206
200
  ];
207
201
 
208
202
  export const MemoryPlugin = async ({ directory, worktree, client }) => {
203
+ installExitHook();
209
204
  await ensureDir();
210
205
  let activeProjectKey = await scopeKey("project", worktree, directory);
211
206
  let identityResolveAt = 0;
@@ -303,79 +298,13 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
303
298
  tags: { type: "string", description: "Optional comma-separated tags, e.g. \x27pref,arch\x27" },
304
299
  supersedes: { type: "string", description: "Optional number, id, or text of the fact this one replaces" },
305
300
  },
306
- async execute({ fact, title, scope, docId, startLine, endLine, relationType, ttl, keep, tags, supersedes }, { worktree, directory }) {
307
- const key = requireProjectKey(await scopeKey(scope || "project", worktree, directory));
308
- const entries = await readMemory(key);
309
-
310
- const explicitTitle = title ? title.trim() : null;
311
- let finalTitle = explicitTitle;
312
- let finalFact = fact.trim();
313
-
314
- // If fact already contains a title pattern, extract it
315
- const titleMatch = /^\\*\\*([^\x2a]+)\\*\\*\\s*(?:—|--|-|:)?\\s*(.*)$/.exec(finalFact);
316
- if (titleMatch) {
317
- if (!finalTitle) {
318
- finalTitle = titleMatch[1].trim();
319
- }
320
- finalFact = titleMatch[2].trim();
321
- }
322
-
323
- if (!finalTitle) {
324
- finalTitle = autoGenerateTitle(finalFact);
325
- }
326
-
327
- const text = `**${finalTitle}** — ${finalFact}`;
328
- const factBodyNormalized = finalFact.toLowerCase();
329
- const duplicate = entries.some((e) => factBody(e).toLowerCase().trim() === factBodyNormalized);
330
-
331
- let supersededInfo = "";
332
- if (!duplicate) {
333
- const [date, time] = today().split(" ");
334
- const meta = { ttl, tags };
335
- if (keep) meta.keep = "1";
336
- if (supersedes) {
337
- const targetIdx = resolveFactIndex(entries, supersedes);
338
- if (targetIdx !== -1) {
339
- const newId = nextFactId(entries);
340
- const targetMeta = factMeta(entries[targetIdx]);
341
- const targetId = targetMeta.id || nextFactId(entries);
342
- entries[targetIdx] = withMeta(entries[targetIdx], { id: targetId, supersededBy: newId });
343
- meta.id = newId;
344
- meta.supersedes = targetId;
345
- supersededInfo = ` [superseded: "${factText(entries[targetIdx]).slice(0, 60)}"]`;
346
- } else {
347
- supersededInfo = " (note: supersedes target not found)";
348
- }
349
- }
350
- if (!meta.id) meta.id = nextFactId(entries);
351
- entries.push(formatFactEntry({ date, time, text, meta }));
352
- await writeMemory(key, entries);
353
- }
354
-
355
- let linkInfo = "";
356
- if (docId) {
357
- try {
358
- const { linkFactToDocument } = await import("../mcp-server/graph/knowledge_linker.js");
359
- const linkRes = linkFactToDocument({
360
- factKey: key,
361
- factText: finalFact,
362
- docId,
363
- startLine,
364
- endLine,
365
- relationType: relationType || "LINKS_TO",
366
- });
367
- const linesStr = startLine ? `:L${startLine}${endLine ? `-${endLine}` : ""}` : "";
368
- linkInfo = ` [Linked to Doc: "${linkRes.docTitle}"${linesStr}]`;
369
- } catch (err) {
370
- linkInfo = ` (Note: Fact saved, but document link failed: ${err.message})`;
371
- }
372
- }
373
-
374
- const result = "Memory updated" + supersededInfo + linkInfo;
301
+ async execute(args, { worktree, directory }) {
302
+ const result = await rememberFact(args, { worktree, directory });
375
303
  await notify(client, result);
376
304
  return result;
377
305
  },
378
306
  },
307
+
379
308
  "recall": {
380
309
  description:
381
310
  "Show saved facts with any Agent-linked Knowledge Base documents/lines. " +
@@ -398,164 +327,19 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
398
327
  offset: { type: "number", description: "Pagination offset (optional)" },
399
328
  limit: { type: "number", description: "Pagination limit (optional)" },
400
329
  },
401
- async execute({ scope, project, query, tags, since, until, mode, offset, limit }, { worktree, directory }) {
402
- const results = [];
403
- const now = Date.now();
404
- const targetMode = mode || "full";
405
- const targetOffset = offset !== undefined ? offset : 0;
406
-
407
- let getLinksForFact;
408
- try {
409
- const linker = await import("../mcp-server/graph/knowledge_linker.js");
410
- getLinksForFact = linker.getLinksForFact;
411
- } catch (e) {}
412
-
413
- const formatFactWithLinks = async (factLine, index, key) => {
414
- const p = parseFactEntry(factLine);
415
- if (!p) return factLine;
416
-
417
- const title = factTitle(factLine);
418
- const body = factBody(factLine);
419
- const meta = p.meta;
420
-
421
- const badges = [];
422
- if (isExpiredLine(factLine, now)) badges.push("EXPIRED");
423
- if (isKeepFact(factLine)) badges.push("KEEP");
424
- if (isSuperseded(factLine)) badges.push("SUPERSEDED");
425
- if (meta.inject === "1") badges.push("INJECT");
426
- if (meta.id) badges.push(`id:${meta.id}`);
427
- if (meta.tags) badges.push(`tags:${meta.tags}`);
428
- badges.push(`${p.date} ${p.time}`);
429
-
430
- const badgesStr = badges.length ? ` [${badges.join("] [")}]` : "";
431
-
432
- let lineText;
433
- if (targetMode === "headers") {
434
- lineText = `**${title}**${badgesStr}`;
435
- } else {
436
- lineText = p.text;
437
- }
438
-
439
- if (getLinksForFact) {
440
- try {
441
- const links = await getLinksForFact(key, p.text);
442
- if (links && links.length > 0) {
443
- const docStr = links
444
- .map((l) => {
445
- const range = l.start_line ? `:L${l.start_line}${l.end_line ? `-${l.end_line}` : ""}` : "";
446
- return `${l.doc_title || l.doc_path}${range}`;
447
- })
448
- .join(", ");
449
- lineText += ` 🔗 [Linked Docs: ${docStr}]`;
450
- }
451
- } catch (e) {}
452
- }
453
- return `${index}. ${lineText}`;
454
- };
455
-
456
- const resolveTargetKey = async (projectPath) => {
457
- if (!projectPath) return null;
458
- try {
459
- const { resolveProjectIdentity } = await import("../mcp-server/identity.js");
460
- const identity = await resolveProjectIdentity(projectPath);
461
- if (identity) return identity.key;
462
- } catch (e) {}
463
- return canonicalPath(projectPath);
464
- };
465
-
466
- const target = await resolveTargetKey(project) ?? await projectKey(worktree, directory);
467
- const label = project ? target : await projectName(worktree, directory);
468
-
469
- const collect = async (entries, key) => {
470
- const matched = entries.filter(
471
- (e) => matchesQuery(e, query) && matchesTags(e, tags) && inDateRange(e, since, until)
472
- );
473
- if (!matched.length) return;
474
- if (results.length) results.push("");
475
- results.push(`--- ${key === GLOBAL_KEY ? "Global" : `Project: ${key === target ? label : key}`} ---`);
476
-
477
- const targetLimit = limit !== undefined ? limit : matched.length;
478
-
479
- const paginated = matched.slice(targetOffset, targetOffset + targetLimit);
480
- for (let i = 0; i < paginated.length; i++) {
481
- results.push(await formatFactWithLinks(paginated[i], targetOffset + i + 1, key));
482
- }
483
-
484
- if (limit !== undefined && matched.length > targetLimit) {
485
- results.push(`Showing entries ${targetOffset + 1}-${Math.min(targetOffset + targetLimit, matched.length)} of ${matched.length}`);
486
- }
487
- results.push(`Store file: ${storeFilePath(key)}`);
488
- };
489
-
490
- if (scope === "list_projects") {
491
- return listProjectStores().then((stores) => {
492
- if (!stores.length) return "No project memory stores found.";
493
- const lines = stores.map(
494
- (s, i) => `${i + 1}. ${s.basename} — ${s.count} fact(s) [${s.file}]${s.path ? ` (bound to ${s.path})` : " (unbound legacy store)"}`
495
- );
496
- 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}`;
497
- });
498
- }
499
-
500
- if (scope !== "project") {
501
- const global = await readMemory(GLOBAL_KEY);
502
- await collect(global, GLOBAL_KEY);
503
- }
504
- if (scope !== "global") {
505
- const local = await readMemory(target);
506
- await collect(local, target);
507
- }
508
- const filtered = Boolean(query || tags || since || until);
509
- if (!results.length) return filtered ? "No facts match the search." : "Memory is empty.";
510
- return results.join("\n") + `\n\nMemory dir: ${MEMORY_DIR}`;
330
+ async execute(args, { worktree, directory }) {
331
+ return await recallFacts(args, { worktree, directory });
511
332
  },
512
333
  },
334
+
513
335
  "get_fact": {
514
336
  description: "Get the full text and metadata of a single fact by its metadata id.",
515
337
  args: {
516
338
  id: { type: "string", description: "The unique metadata id of the fact (e.g. \x278f3a2c\x27)" },
517
339
  scope: { type: "string", description: "\x27project\x27, \x27global\x27, or \x27all\x27 (default)", default: "all" },
518
340
  },
519
- async execute({ id, scope }, { worktree, directory }) {
520
- const results = [];
521
- const targetId = String(id || "").trim();
522
- if (!targetId) throw new Error("ID parameter is required.");
523
-
524
- const check = async (key) => {
525
- const entries = await readMemory(key);
526
- const match = entries.find((e) => factMeta(e).id === targetId);
527
- if (match) {
528
- const title = factTitle(match);
529
- const body = factBody(match);
530
- const meta = factMeta(match);
531
- results.push({
532
- key,
533
- title,
534
- body,
535
- meta,
536
- line: match
537
- });
538
- }
539
- };
540
-
541
- if (scope !== "project") {
542
- await check(GLOBAL_KEY);
543
- }
544
- if (scope !== "global") {
545
- const target = await projectKey(worktree, directory);
546
- await check(target);
547
- }
548
-
549
- if (!results.length) {
550
- return `Fact with ID "${targetId}" not found.`;
551
- }
552
-
553
- const lines = results.map((r) => {
554
- const metaStr = Object.entries(r.meta).map(([k, v]) => `${k}:${v}`).join(", ");
555
- return `[Store: ${r.key === GLOBAL_KEY ? "Global" : "Project"}]\nTitle: ${r.title}\nBody: ${r.body}\nMetadata: ${metaStr ? `<!-- ${metaStr} -->` : "none"}`;
556
- });
557
-
558
- return lines.join("\n\n");
341
+ async execute(args, { worktree, directory }) {
342
+ return await getFactById(args, { worktree, directory });
559
343
  },
560
344
  },
561
345
  "forget": {
@@ -569,37 +353,9 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
569
353
  },
570
354
  force: { type: "boolean", description: "Удалить также защищённые (keep) факты" },
571
355
  },
572
- async execute({ query, scope, force }, { worktree, directory }) {
573
- const key = requireProjectKey(await scopeKey(scope || "project", worktree, directory));
574
- const entries = await readMemory(key);
575
- const rangeMatch = /^\s*(\d+)\s*-\s*(\d+)\s*$/.exec(query);
576
- const num = parseInt(query, 10);
577
- let indices = [];
578
- if (rangeMatch) {
579
- const from = parseInt(rangeMatch[1], 10);
580
- const to = parseInt(rangeMatch[2], 10);
581
- if (from > 0 && to >= from && to <= entries.length) {
582
- for (let i = from - 1; i < to; i++) indices.push(i);
583
- }
584
- }
585
- if (!indices.length && !isNaN(num) && num > 0 && num <= entries.length) {
586
- indices.push(num - 1);
587
- }
588
- if (!indices.length) {
589
- const q = query.toLowerCase();
590
- indices = entries.reduce((acc, e, i) => (e.toLowerCase().includes(q) ? acc.concat(i) : acc), []);
591
- }
592
- if (!indices.length) return "Not found.";
593
-
594
- const removable = indices.filter((i) => force || !isKeepFact(entries[i]));
595
- const protectedCount = indices.length - removable.length;
596
- if (removable.length) {
597
- for (const i of removable.sort((a, b) => b - a)) entries.splice(i, 1);
598
- await writeMemory(key, entries);
599
- }
600
- let result = removable.length ? "Memory updated" : "Nothing removed.";
601
- if (protectedCount) result += ` (${protectedCount} protected fact(s) skipped; use force=true to override)`;
602
- if (removable.length) await notify(client, result);
356
+ async execute(args, { worktree, directory }) {
357
+ const result = await forgetFacts(args, { worktree, directory });
358
+ if (result.startsWith("Memory updated")) await notify(client, result);
603
359
  return result;
604
360
  },
605
361
  },
@@ -613,109 +369,18 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
613
369
  title: { type: "string", description: "Optional new title for the fact" },
614
370
  scope: { type: "string", description: "\x27project\x27 (default) or \x27global\x27", default: "project" },
615
371
  },
616
- async execute({ id, newText, title, scope }, { worktree, directory }) {
617
- const key = requireProjectKey(await scopeKey(scope || "project", worktree, directory));
618
- const entries = await readMemory(key);
619
- const idx = resolveFactIndex(entries, id);
620
- if (idx === -1) throw new Error(`Fact not found: ${id}`);
621
- const p = parseFactEntry(entries[idx]);
622
- const oldText = p ? p.text : entries[idx];
623
- const oldBody = factBody(entries[idx]) || oldText;
624
-
625
- const explicitTitle = title ? title.trim() : null;
626
- let finalTitle = explicitTitle;
627
- let finalFact = newText.trim();
628
-
629
- // Check if newText has a title
630
- const titleMatch = /^\\*\\*([^\x2a]+)\\*\\*\\s*(?:—|--|-|:)?\\s*(.*)$/.exec(finalFact);
631
- if (titleMatch) {
632
- if (!finalTitle) {
633
- finalTitle = titleMatch[1].trim();
634
- }
635
- finalFact = titleMatch[2].trim();
636
- }
637
-
638
- // If no new title is specified, preserve the old title
639
- if (!finalTitle) {
640
- finalTitle = factTitle(entries[idx]) || autoGenerateTitle(finalFact);
641
- }
642
-
643
- const newTextFormatted = `**${finalTitle}** — ${finalFact}`;
644
- const newLine = formatFactEntry({ date: p.date, time: p.time, text: newTextFormatted, meta: p.meta });
645
- entries[idx] = newLine;
646
- await writeMemory(key, entries);
647
-
648
- let linksUpdated = 0;
649
- try {
650
- const { getDatabase } = await import("../mcp-server/db/database.js");
651
- const db = await getDatabase();
652
- const res = db
653
- .prepare("UPDATE knowledge_links SET fact_text = ? WHERE fact_key = ? AND fact_text = ?")
654
- .run(finalFact, key, oldBody);
655
- linksUpdated = res.changes;
656
- } catch (e) {}
657
-
658
- const result = `Fact updated${linksUpdated ? `, ${linksUpdated} doc link(s) updated` : ""}`;
372
+ async execute(args, { worktree, directory }) {
373
+ const result = await updateFactText(args, { worktree, directory });
659
374
  await notify(client, result);
660
375
  return result;
661
376
  },
662
377
  },
378
+
663
379
  "memory_info": {
664
380
  description: "Show memory storage paths (store files, MEMORY_DIR, SQLite DB), fact counts, and Knowledge Base stats.",
665
381
  args: {},
666
- async execute() {
667
- const dbPath = join(MEMORY_DIR, "storage", "memory.sqlite");
668
- let version = "unknown";
669
- try {
670
- const { readFile } = await import("fs/promises");
671
- version = JSON.parse(
672
- await readFile(new URL("../package.json", import.meta.url), "utf-8")
673
- ).version;
674
- } catch (e) {}
675
-
676
- let rag = {};
677
- try {
678
- const { getDatabase } = await import("../mcp-server/db/database.js");
679
- const db = await getDatabase();
680
- rag.documents = db.prepare("SELECT COUNT(*) AS c FROM documents").get().c;
681
- rag.sections = db.prepare("SELECT COUNT(*) AS c FROM sections").get().c;
682
- rag.chunks = db.prepare("SELECT COUNT(*) AS c FROM micro_chunks").get().c;
683
- rag.edges = db.prepare("SELECT COUNT(*) AS c FROM graph_edges").get().c;
684
- rag.links = db.prepare("SELECT COUNT(*) AS c FROM knowledge_links").get().c;
685
- } catch (e) {
686
- rag.error = e.message;
687
- }
688
-
689
- let identityLines = [];
690
- try {
691
- const { getDatabase } = await import("../mcp-server/db/database.js");
692
- const { resolveProjectIdentity, listIdentities } = await import("../mcp-server/identity.js");
693
- const db = await getDatabase();
694
- const identity = await resolveProjectIdentity(directory || process.cwd());
695
- const all = await listIdentities(db);
696
- identityLines.push(
697
- `Identity: ${identity ? "git" : "no-git"}` +
698
- (identity ? ` | key: ${identity.key} | name: ${identity.name}${identity.primaryRemote ? ` | remote: ${identity.primaryRemote}` : ""}` : ""),
699
- `Known identities: ${all.length}`
700
- );
701
- } catch (e) {
702
- identityLines.push(`Identity: unavailable (${e.message})`);
703
- }
704
-
705
- const lines = [
706
- `Version: ${version}`,
707
- `MEMORY_DIR: ${MEMORY_DIR}`,
708
- `SQLite DB: ${dbPath}`,
709
- `Global store: ${storeFilePath(GLOBAL_KEY)}`,
710
- `Project store: ${storeFilePath(activeProjectKey)}`,
711
- ...identityLines,
712
- ];
713
- if (rag.error) lines.push(`RAG: unavailable (${rag.error})`);
714
- else
715
- lines.push(
716
- `RAG: ${rag.documents} doc(s), ${rag.sections} section(s), ${rag.chunks} chunk(s), ${rag.edges} edge(s), ${rag.links} memory link(s)`
717
- );
718
- return lines.join("\n");
382
+ async execute(_args, ctx = {}) {
383
+ return await memoryInfo({}, { worktree: ctx.worktree ?? worktree, directory: ctx.directory ?? directory });
719
384
  },
720
385
  },
721
386
  "link_knowledge": {
@@ -752,7 +417,7 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
752
417
  if (!factText || !docId) {
753
418
  throw new Error("factText and docId are required parameters for link action");
754
419
  }
755
- const res = linkFactToDocument({
420
+ const res = await linkFactToDocument({
756
421
  factKey: key,
757
422
  factText,
758
423
  docId,
@@ -765,12 +430,12 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
765
430
 
766
431
  if (act === "get_doc_links") {
767
432
  if (!docId) throw new Error("docId parameter is required for get_doc_links action");
768
- const links = getLinksForDoc(docId);
433
+ const links = await getLinksForDoc(docId);
769
434
  return JSON.stringify(links, null, 2);
770
435
  }
771
436
 
772
437
  if (act === "list_links") {
773
- const links = listAllLinks(key);
438
+ const links = await listAllLinks(key);
774
439
  return JSON.stringify(links, null, 2);
775
440
  }
776
441
 
@@ -878,10 +543,14 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
878
543
  const db = await getDatabase();
879
544
 
880
545
  if (action === "stats") {
881
- const docCount = db.prepare("SELECT COUNT(*) as cnt FROM documents").get().cnt;
882
- const secCount = db.prepare("SELECT COUNT(*) as cnt FROM sections").get().cnt;
883
- const chunkCount = db.prepare("SELECT COUNT(*) as cnt FROM micro_chunks").get().cnt;
884
- const edgeCount = db.prepare("SELECT COUNT(*) as cnt FROM graph_edges").get().cnt;
546
+ const docCountRow = await db.prepare("SELECT COUNT(*) as cnt FROM documents").get();
547
+ const docCount = docCountRow ? docCountRow.cnt : 0;
548
+ const secCountRow = await db.prepare("SELECT COUNT(*) as cnt FROM sections").get();
549
+ const secCount = secCountRow ? secCountRow.cnt : 0;
550
+ const chunkCountRow = await db.prepare("SELECT COUNT(*) as cnt FROM micro_chunks").get();
551
+ const chunkCount = chunkCountRow ? chunkCountRow.cnt : 0;
552
+ const edgeCountRow = await db.prepare("SELECT COUNT(*) as cnt FROM graph_edges").get();
553
+ const edgeCount = edgeCountRow ? edgeCountRow.cnt : 0;
885
554
  return JSON.stringify(
886
555
  {
887
556
  documents: docCount,
@@ -895,7 +564,7 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
895
564
  }
896
565
 
897
566
  if (action === "list") {
898
- const docs = db
567
+ const docs = await db
899
568
  .prepare("SELECT id, title, path, blob_hash, created_at FROM documents ORDER BY created_at DESC")
900
569
  .all();
901
570
  return JSON.stringify(docs, null, 2);
@@ -903,7 +572,7 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
903
572
 
904
573
  if (action === "read_document") {
905
574
  if (!docId) throw new Error("docId parameter is required for read_document action");
906
- const doc = db
575
+ const doc = await db
907
576
  .prepare("SELECT id, title, path, blob_hash, created_at FROM documents WHERE id = ? OR path = ? OR title = ?")
908
577
  .get(docId, docId, docId);
909
578
  if (!doc) {
@@ -949,6 +618,34 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
949
618
  throw new Error(`Unknown action: ${action}`);
950
619
  },
951
620
  },
621
+ "reindex_knowledge_base": {
622
+ description:
623
+ "Re-embed all existing documents in the RAG knowledge base with the active (or specified) embedding model and vector dimension. " +
624
+ "Use after switching the embedding model or vector dimension so previously stored vectors match the new configuration. " +
625
+ "Preserves documents, sections, FTS index, graph edges, and fact links.",
626
+ args: {
627
+ model: { type: "string", description: "Embedding model to use (defaults to active config.embeddingModel)" },
628
+ dimension: { type: "number", description: "Fixed vector dimension (defaults to active config.vectorDimension; auto-detect if unset)" },
629
+ },
630
+ async execute({ model, dimension }) {
631
+ const { reindexEmbeddings } = await import("../mcp-server/ingest/pipeline.js");
632
+ const result = await reindexEmbeddings({
633
+ model: model || null,
634
+ dimension: dimension !== undefined && dimension !== null ? dimension : null,
635
+ });
636
+ return JSON.stringify(
637
+ {
638
+ status: "success",
639
+ reindexed: result.reindexed,
640
+ documentsAffected: result.documentsAffected,
641
+ model: result.model,
642
+ dimension: result.dimension || "auto",
643
+ },
644
+ null,
645
+ 2
646
+ );
647
+ },
648
+ },
952
649
  "link_project_memory": {
953
650
  description: "Link the current directory to a Git-based project identity, register aliases, and optionally migrate legacy/path stores.",
954
651
  args: {