@lotargo/memory_plugin 1.5.2 → 1.6.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/CHANGELOG.md +108 -0
- package/README.md +383 -352
- package/mcp-server/admin/auth.js +13 -4
- package/mcp-server/admin/snapshot.js +24 -7
- package/mcp-server/cli/direct_commands.js +334 -313
- package/mcp-server/cli/handlers/engine_actions.js +41 -0
- package/mcp-server/cli/handlers/storage_actions.js +58 -0
- package/mcp-server/cli/secret_input.js +44 -0
- package/mcp-server/cli/ui.js +564 -565
- package/mcp-server/cli.js +356 -324
- package/mcp-server/config/auth_store.js +74 -16
- package/mcp-server/config/config_manager.js +4 -0
- package/mcp-server/db/database.js +33 -14
- package/mcp-server/db/sync_queue.js +9 -19
- package/mcp-server/index.js +112 -42
- package/mcp-server/ingest/normalizer.js +116 -29
- package/mcp-server/ingest/pipeline.js +94 -6
- package/mcp-server/logger.js +49 -0
- package/mcp-server/memory.js +6 -9
- package/mcp-server/ml/gpu_monitor.js +169 -166
- package/mcp-server/ml/model_manager.js +17 -4
- package/mcp-server/retrieval/retriever.js +35 -15
- package/mcp-server/security/path_guard.js +67 -0
- package/mcp-server/setup.js +10 -2
- package/mcp-server/storage/blob_store.js +15 -2
- package/mcp-server/tools/core/memory_core.js +393 -0
- package/mcp-server/tools/helpers.js +59 -39
- package/mcp-server/tools/memory_tools.js +123 -506
- package/mcp-server/tools/rag_tools.js +49 -1
- package/opencode-plugin/index.js +95 -387
- package/package.json +7 -1
- 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
|
{
|
package/opencode-plugin/index.js
CHANGED
|
@@ -1,51 +1,56 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
}
|
|
17
|
+
} from "../mcp-server/fact_format.js";
|
|
24
18
|
|
|
25
|
-
|
|
19
|
+
import {
|
|
26
20
|
MEMORY_DIR,
|
|
27
21
|
GLOBAL_KEY,
|
|
28
22
|
canonicalPath,
|
|
29
|
-
projectName,
|
|
30
23
|
projectKey,
|
|
31
24
|
scopeKey,
|
|
32
25
|
readMemory,
|
|
33
26
|
writeMemory,
|
|
34
|
-
listProjectStores,
|
|
35
27
|
storeFilePath,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
+
});
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
const CONFIG_DIR = process.env.OPENCODE_CONFIG_DIR || join(homedir(), ".config", "opencode");
|
|
@@ -109,16 +114,6 @@ const MEMORY_INSTRUCTION =
|
|
|
109
114
|
"When saving, translate the fact into clear, concise English.\n" +
|
|
110
115
|
"Use `scope: \"global\"` for personal facts, `scope: \"project\"` for project-specific facts.";
|
|
111
116
|
|
|
112
|
-
function requireProjectKey(key) {
|
|
113
|
-
if (!key) {
|
|
114
|
-
throw new Error(
|
|
115
|
-
"No project memory available: this directory is not inside a git repository. " +
|
|
116
|
-
"Project memory is tied to a git repo; use scope: 'global' or open a git repository."
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
return key;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
117
|
function sortNewestFirst(entries) {
|
|
123
118
|
return [...entries].sort((a, b) => {
|
|
124
119
|
const pa = parseFactEntry(a);
|
|
@@ -205,6 +200,7 @@ const MCP_SERVERS = [
|
|
|
205
200
|
];
|
|
206
201
|
|
|
207
202
|
export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
203
|
+
installExitHook();
|
|
208
204
|
await ensureDir();
|
|
209
205
|
let activeProjectKey = await scopeKey("project", worktree, directory);
|
|
210
206
|
let identityResolveAt = 0;
|
|
@@ -302,79 +298,13 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
302
298
|
tags: { type: "string", description: "Optional comma-separated tags, e.g. \x27pref,arch\x27" },
|
|
303
299
|
supersedes: { type: "string", description: "Optional number, id, or text of the fact this one replaces" },
|
|
304
300
|
},
|
|
305
|
-
async execute(
|
|
306
|
-
const
|
|
307
|
-
const entries = await readMemory(key);
|
|
308
|
-
|
|
309
|
-
const explicitTitle = title ? title.trim() : null;
|
|
310
|
-
let finalTitle = explicitTitle;
|
|
311
|
-
let finalFact = fact.trim();
|
|
312
|
-
|
|
313
|
-
// If fact already contains a title pattern, extract it
|
|
314
|
-
const titleMatch = /^\\*\\*([^\x2a]+)\\*\\*\\s*(?:—|--|-|:)?\\s*(.*)$/.exec(finalFact);
|
|
315
|
-
if (titleMatch) {
|
|
316
|
-
if (!finalTitle) {
|
|
317
|
-
finalTitle = titleMatch[1].trim();
|
|
318
|
-
}
|
|
319
|
-
finalFact = titleMatch[2].trim();
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if (!finalTitle) {
|
|
323
|
-
finalTitle = autoGenerateTitle(finalFact);
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const text = `**${finalTitle}** — ${finalFact}`;
|
|
327
|
-
const factBodyNormalized = finalFact.toLowerCase();
|
|
328
|
-
const duplicate = entries.some((e) => factBody(e).toLowerCase().trim() === factBodyNormalized);
|
|
329
|
-
|
|
330
|
-
let supersededInfo = "";
|
|
331
|
-
if (!duplicate) {
|
|
332
|
-
const [date, time] = today().split(" ");
|
|
333
|
-
const meta = { ttl, tags };
|
|
334
|
-
if (keep) meta.keep = "1";
|
|
335
|
-
if (supersedes) {
|
|
336
|
-
const targetIdx = resolveFactIndex(entries, supersedes);
|
|
337
|
-
if (targetIdx !== -1) {
|
|
338
|
-
const newId = nextFactId(entries);
|
|
339
|
-
const targetMeta = factMeta(entries[targetIdx]);
|
|
340
|
-
const targetId = targetMeta.id || nextFactId(entries);
|
|
341
|
-
entries[targetIdx] = withMeta(entries[targetIdx], { id: targetId, supersededBy: newId });
|
|
342
|
-
meta.id = newId;
|
|
343
|
-
meta.supersedes = targetId;
|
|
344
|
-
supersededInfo = ` [superseded: "${factText(entries[targetIdx]).slice(0, 60)}"]`;
|
|
345
|
-
} else {
|
|
346
|
-
supersededInfo = " (note: supersedes target not found)";
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
if (!meta.id) meta.id = nextFactId(entries);
|
|
350
|
-
entries.push(formatFactEntry({ date, time, text, meta }));
|
|
351
|
-
await writeMemory(key, entries);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
let linkInfo = "";
|
|
355
|
-
if (docId) {
|
|
356
|
-
try {
|
|
357
|
-
const { linkFactToDocument } = await import("../mcp-server/graph/knowledge_linker.js");
|
|
358
|
-
const linkRes = linkFactToDocument({
|
|
359
|
-
factKey: key,
|
|
360
|
-
factText: finalFact,
|
|
361
|
-
docId,
|
|
362
|
-
startLine,
|
|
363
|
-
endLine,
|
|
364
|
-
relationType: relationType || "LINKS_TO",
|
|
365
|
-
});
|
|
366
|
-
const linesStr = startLine ? `:L${startLine}${endLine ? `-${endLine}` : ""}` : "";
|
|
367
|
-
linkInfo = ` [Linked to Doc: "${linkRes.docTitle}"${linesStr}]`;
|
|
368
|
-
} catch (err) {
|
|
369
|
-
linkInfo = ` (Note: Fact saved, but document link failed: ${err.message})`;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const result = "Memory updated" + supersededInfo + linkInfo;
|
|
301
|
+
async execute(args, { worktree, directory }) {
|
|
302
|
+
const result = await rememberFact(args, { worktree, directory });
|
|
374
303
|
await notify(client, result);
|
|
375
304
|
return result;
|
|
376
305
|
},
|
|
377
306
|
},
|
|
307
|
+
|
|
378
308
|
"recall": {
|
|
379
309
|
description:
|
|
380
310
|
"Show saved facts with any Agent-linked Knowledge Base documents/lines. " +
|
|
@@ -397,154 +327,19 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
397
327
|
offset: { type: "number", description: "Pagination offset (optional)" },
|
|
398
328
|
limit: { type: "number", description: "Pagination limit (optional)" },
|
|
399
329
|
},
|
|
400
|
-
async execute(
|
|
401
|
-
|
|
402
|
-
const now = Date.now();
|
|
403
|
-
const targetMode = mode || "full";
|
|
404
|
-
const targetOffset = offset !== undefined ? offset : 0;
|
|
405
|
-
|
|
406
|
-
let getLinksForFact;
|
|
407
|
-
try {
|
|
408
|
-
const linker = await import("../mcp-server/graph/knowledge_linker.js");
|
|
409
|
-
getLinksForFact = linker.getLinksForFact;
|
|
410
|
-
} catch (e) {}
|
|
411
|
-
|
|
412
|
-
const formatFactWithLinks = async (factLine, index, key) => {
|
|
413
|
-
const p = parseFactEntry(factLine);
|
|
414
|
-
if (!p) return factLine;
|
|
415
|
-
|
|
416
|
-
const title = factTitle(factLine);
|
|
417
|
-
const body = factBody(factLine);
|
|
418
|
-
const meta = p.meta;
|
|
419
|
-
|
|
420
|
-
const badges = [];
|
|
421
|
-
if (isExpiredLine(factLine, now)) badges.push("EXPIRED");
|
|
422
|
-
if (isKeepFact(factLine)) badges.push("KEEP");
|
|
423
|
-
if (isSuperseded(factLine)) badges.push("SUPERSEDED");
|
|
424
|
-
if (meta.inject === "1") badges.push("INJECT");
|
|
425
|
-
if (meta.id) badges.push(`id:${meta.id}`);
|
|
426
|
-
if (meta.tags) badges.push(`tags:${meta.tags}`);
|
|
427
|
-
badges.push(`${p.date} ${p.time}`);
|
|
428
|
-
|
|
429
|
-
const badgesStr = badges.length ? ` [${badges.join("] [")}]` : "";
|
|
430
|
-
|
|
431
|
-
let lineText;
|
|
432
|
-
if (targetMode === "headers") {
|
|
433
|
-
lineText = `**${title}**${badgesStr}`;
|
|
434
|
-
} else {
|
|
435
|
-
lineText = p.text;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
if (getLinksForFact) {
|
|
439
|
-
try {
|
|
440
|
-
const links = await getLinksForFact(key, p.text);
|
|
441
|
-
if (links && links.length > 0) {
|
|
442
|
-
const docStr = links
|
|
443
|
-
.map((l) => {
|
|
444
|
-
const range = l.start_line ? `:L${l.start_line}${l.end_line ? `-${l.end_line}` : ""}` : "";
|
|
445
|
-
return `${l.doc_title || l.doc_path}${range}`;
|
|
446
|
-
})
|
|
447
|
-
.join(", ");
|
|
448
|
-
lineText += ` 🔗 [Linked Docs: ${docStr}]`;
|
|
449
|
-
}
|
|
450
|
-
} catch (e) {}
|
|
451
|
-
}
|
|
452
|
-
return `${index}. ${lineText}`;
|
|
453
|
-
};
|
|
454
|
-
|
|
455
|
-
const target = project ? canonicalPath(project) : await projectKey(worktree, directory);
|
|
456
|
-
const label = project ? target : await projectName(worktree, directory);
|
|
457
|
-
|
|
458
|
-
const collect = async (entries, key) => {
|
|
459
|
-
const matched = entries.filter(
|
|
460
|
-
(e) => matchesQuery(e, query) && matchesTags(e, tags) && inDateRange(e, since, until)
|
|
461
|
-
);
|
|
462
|
-
if (!matched.length) return;
|
|
463
|
-
if (results.length) results.push("");
|
|
464
|
-
results.push(`--- ${key === GLOBAL_KEY ? "Global" : `Project: ${key === target ? label : key}`} ---`);
|
|
465
|
-
|
|
466
|
-
const targetLimit = limit !== undefined ? limit : matched.length;
|
|
467
|
-
|
|
468
|
-
const paginated = matched.slice(targetOffset, targetOffset + targetLimit);
|
|
469
|
-
for (let i = 0; i < paginated.length; i++) {
|
|
470
|
-
results.push(await formatFactWithLinks(paginated[i], targetOffset + i + 1, key));
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
if (limit !== undefined && matched.length > targetLimit) {
|
|
474
|
-
results.push(`Showing entries ${targetOffset + 1}-${Math.min(targetOffset + targetLimit, matched.length)} of ${matched.length}`);
|
|
475
|
-
}
|
|
476
|
-
results.push(`Store file: ${storeFilePath(key)}`);
|
|
477
|
-
};
|
|
478
|
-
|
|
479
|
-
if (scope === "list_projects") {
|
|
480
|
-
return listProjectStores().then((stores) => {
|
|
481
|
-
if (!stores.length) return "No project memory stores found.";
|
|
482
|
-
const lines = stores.map(
|
|
483
|
-
(s, i) => `${i + 1}. ${s.basename} — ${s.count} fact(s) [${s.file}]${s.path ? ` (bound to ${s.path})` : " (unbound legacy store)"}`
|
|
484
|
-
);
|
|
485
|
-
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}`;
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
if (scope !== "project") {
|
|
490
|
-
const global = await readMemory(GLOBAL_KEY);
|
|
491
|
-
await collect(global, GLOBAL_KEY);
|
|
492
|
-
}
|
|
493
|
-
if (scope !== "global") {
|
|
494
|
-
const local = await readMemory(target);
|
|
495
|
-
await collect(local, target);
|
|
496
|
-
}
|
|
497
|
-
const filtered = Boolean(query || tags || since || until);
|
|
498
|
-
if (!results.length) return filtered ? "No facts match the search." : "Memory is empty.";
|
|
499
|
-
return results.join("\n") + `\n\nMemory dir: ${MEMORY_DIR}`;
|
|
330
|
+
async execute(args, { worktree, directory }) {
|
|
331
|
+
return await recallFacts(args, { worktree, directory });
|
|
500
332
|
},
|
|
501
333
|
},
|
|
334
|
+
|
|
502
335
|
"get_fact": {
|
|
503
336
|
description: "Get the full text and metadata of a single fact by its metadata id.",
|
|
504
337
|
args: {
|
|
505
338
|
id: { type: "string", description: "The unique metadata id of the fact (e.g. \x278f3a2c\x27)" },
|
|
506
339
|
scope: { type: "string", description: "\x27project\x27, \x27global\x27, or \x27all\x27 (default)", default: "all" },
|
|
507
340
|
},
|
|
508
|
-
async execute(
|
|
509
|
-
|
|
510
|
-
const targetId = String(id || "").trim();
|
|
511
|
-
if (!targetId) throw new Error("ID parameter is required.");
|
|
512
|
-
|
|
513
|
-
const check = async (key) => {
|
|
514
|
-
const entries = await readMemory(key);
|
|
515
|
-
const match = entries.find((e) => factMeta(e).id === targetId);
|
|
516
|
-
if (match) {
|
|
517
|
-
const title = factTitle(match);
|
|
518
|
-
const body = factBody(match);
|
|
519
|
-
const meta = factMeta(match);
|
|
520
|
-
results.push({
|
|
521
|
-
key,
|
|
522
|
-
title,
|
|
523
|
-
body,
|
|
524
|
-
meta,
|
|
525
|
-
line: match
|
|
526
|
-
});
|
|
527
|
-
}
|
|
528
|
-
};
|
|
529
|
-
|
|
530
|
-
if (scope !== "project") {
|
|
531
|
-
await check(GLOBAL_KEY);
|
|
532
|
-
}
|
|
533
|
-
if (scope !== "global") {
|
|
534
|
-
const target = await projectKey(worktree, directory);
|
|
535
|
-
await check(target);
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
if (!results.length) {
|
|
539
|
-
return `Fact with ID "${targetId}" not found.`;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const lines = results.map((r) => {
|
|
543
|
-
const metaStr = Object.entries(r.meta).map(([k, v]) => `${k}:${v}`).join(", ");
|
|
544
|
-
return `[Store: ${r.key === GLOBAL_KEY ? "Global" : "Project"}]\nTitle: ${r.title}\nBody: ${r.body}\nMetadata: ${metaStr ? `<!-- ${metaStr} -->` : "none"}`;
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
return lines.join("\n\n");
|
|
341
|
+
async execute(args, { worktree, directory }) {
|
|
342
|
+
return await getFactById(args, { worktree, directory });
|
|
548
343
|
},
|
|
549
344
|
},
|
|
550
345
|
"forget": {
|
|
@@ -558,37 +353,9 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
558
353
|
},
|
|
559
354
|
force: { type: "boolean", description: "Удалить также защищённые (keep) факты" },
|
|
560
355
|
},
|
|
561
|
-
async execute(
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
const rangeMatch = /^\s*(\d+)\s*-\s*(\d+)\s*$/.exec(query);
|
|
565
|
-
const num = parseInt(query, 10);
|
|
566
|
-
let indices = [];
|
|
567
|
-
if (rangeMatch) {
|
|
568
|
-
const from = parseInt(rangeMatch[1], 10);
|
|
569
|
-
const to = parseInt(rangeMatch[2], 10);
|
|
570
|
-
if (from > 0 && to >= from && to <= entries.length) {
|
|
571
|
-
for (let i = from - 1; i < to; i++) indices.push(i);
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
if (!indices.length && !isNaN(num) && num > 0 && num <= entries.length) {
|
|
575
|
-
indices.push(num - 1);
|
|
576
|
-
}
|
|
577
|
-
if (!indices.length) {
|
|
578
|
-
const q = query.toLowerCase();
|
|
579
|
-
indices = entries.reduce((acc, e, i) => (e.toLowerCase().includes(q) ? acc.concat(i) : acc), []);
|
|
580
|
-
}
|
|
581
|
-
if (!indices.length) return "Not found.";
|
|
582
|
-
|
|
583
|
-
const removable = indices.filter((i) => force || !isKeepFact(entries[i]));
|
|
584
|
-
const protectedCount = indices.length - removable.length;
|
|
585
|
-
if (removable.length) {
|
|
586
|
-
for (const i of removable.sort((a, b) => b - a)) entries.splice(i, 1);
|
|
587
|
-
await writeMemory(key, entries);
|
|
588
|
-
}
|
|
589
|
-
let result = removable.length ? "Memory updated" : "Nothing removed.";
|
|
590
|
-
if (protectedCount) result += ` (${protectedCount} protected fact(s) skipped; use force=true to override)`;
|
|
591
|
-
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);
|
|
592
359
|
return result;
|
|
593
360
|
},
|
|
594
361
|
},
|
|
@@ -602,109 +369,18 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
602
369
|
title: { type: "string", description: "Optional new title for the fact" },
|
|
603
370
|
scope: { type: "string", description: "\x27project\x27 (default) or \x27global\x27", default: "project" },
|
|
604
371
|
},
|
|
605
|
-
async execute(
|
|
606
|
-
const
|
|
607
|
-
const entries = await readMemory(key);
|
|
608
|
-
const idx = resolveFactIndex(entries, id);
|
|
609
|
-
if (idx === -1) throw new Error(`Fact not found: ${id}`);
|
|
610
|
-
const p = parseFactEntry(entries[idx]);
|
|
611
|
-
const oldText = p ? p.text : entries[idx];
|
|
612
|
-
const oldBody = factBody(entries[idx]) || oldText;
|
|
613
|
-
|
|
614
|
-
const explicitTitle = title ? title.trim() : null;
|
|
615
|
-
let finalTitle = explicitTitle;
|
|
616
|
-
let finalFact = newText.trim();
|
|
617
|
-
|
|
618
|
-
// Check if newText has a title
|
|
619
|
-
const titleMatch = /^\\*\\*([^\x2a]+)\\*\\*\\s*(?:—|--|-|:)?\\s*(.*)$/.exec(finalFact);
|
|
620
|
-
if (titleMatch) {
|
|
621
|
-
if (!finalTitle) {
|
|
622
|
-
finalTitle = titleMatch[1].trim();
|
|
623
|
-
}
|
|
624
|
-
finalFact = titleMatch[2].trim();
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// If no new title is specified, preserve the old title
|
|
628
|
-
if (!finalTitle) {
|
|
629
|
-
finalTitle = factTitle(entries[idx]) || autoGenerateTitle(finalFact);
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
const newTextFormatted = `**${finalTitle}** — ${finalFact}`;
|
|
633
|
-
const newLine = formatFactEntry({ date: p.date, time: p.time, text: newTextFormatted, meta: p.meta });
|
|
634
|
-
entries[idx] = newLine;
|
|
635
|
-
await writeMemory(key, entries);
|
|
636
|
-
|
|
637
|
-
let linksUpdated = 0;
|
|
638
|
-
try {
|
|
639
|
-
const { getDatabase } = await import("../mcp-server/db/database.js");
|
|
640
|
-
const db = await getDatabase();
|
|
641
|
-
const res = db
|
|
642
|
-
.prepare("UPDATE knowledge_links SET fact_text = ? WHERE fact_key = ? AND fact_text = ?")
|
|
643
|
-
.run(finalFact, key, oldBody);
|
|
644
|
-
linksUpdated = res.changes;
|
|
645
|
-
} catch (e) {}
|
|
646
|
-
|
|
647
|
-
const result = `Fact updated${linksUpdated ? `, ${linksUpdated} doc link(s) updated` : ""}`;
|
|
372
|
+
async execute(args, { worktree, directory }) {
|
|
373
|
+
const result = await updateFactText(args, { worktree, directory });
|
|
648
374
|
await notify(client, result);
|
|
649
375
|
return result;
|
|
650
376
|
},
|
|
651
377
|
},
|
|
378
|
+
|
|
652
379
|
"memory_info": {
|
|
653
380
|
description: "Show memory storage paths (store files, MEMORY_DIR, SQLite DB), fact counts, and Knowledge Base stats.",
|
|
654
381
|
args: {},
|
|
655
|
-
async execute() {
|
|
656
|
-
|
|
657
|
-
let version = "unknown";
|
|
658
|
-
try {
|
|
659
|
-
const { readFile } = await import("fs/promises");
|
|
660
|
-
version = JSON.parse(
|
|
661
|
-
await readFile(new URL("../package.json", import.meta.url), "utf-8")
|
|
662
|
-
).version;
|
|
663
|
-
} catch (e) {}
|
|
664
|
-
|
|
665
|
-
let rag = {};
|
|
666
|
-
try {
|
|
667
|
-
const { getDatabase } = await import("../mcp-server/db/database.js");
|
|
668
|
-
const db = getDatabase();
|
|
669
|
-
rag.documents = db.prepare("SELECT COUNT(*) AS c FROM documents").get().c;
|
|
670
|
-
rag.sections = db.prepare("SELECT COUNT(*) AS c FROM sections").get().c;
|
|
671
|
-
rag.chunks = db.prepare("SELECT COUNT(*) AS c FROM micro_chunks").get().c;
|
|
672
|
-
rag.edges = db.prepare("SELECT COUNT(*) AS c FROM graph_edges").get().c;
|
|
673
|
-
rag.links = db.prepare("SELECT COUNT(*) AS c FROM knowledge_links").get().c;
|
|
674
|
-
} catch (e) {
|
|
675
|
-
rag.error = e.message;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
let identityLines = [];
|
|
679
|
-
try {
|
|
680
|
-
const { getDatabase } = await import("../mcp-server/db/database.js");
|
|
681
|
-
const { resolveProjectIdentity, listIdentities } = await import("../mcp-server/identity.js");
|
|
682
|
-
const db = getDatabase();
|
|
683
|
-
const identity = await resolveProjectIdentity(directory || process.cwd());
|
|
684
|
-
const all = await listIdentities(db);
|
|
685
|
-
identityLines.push(
|
|
686
|
-
`Identity: ${identity ? "git" : "no-git"}` +
|
|
687
|
-
(identity ? ` | key: ${identity.key} | name: ${identity.name}${identity.primaryRemote ? ` | remote: ${identity.primaryRemote}` : ""}` : ""),
|
|
688
|
-
`Known identities: ${all.length}`
|
|
689
|
-
);
|
|
690
|
-
} catch (e) {
|
|
691
|
-
identityLines.push(`Identity: unavailable (${e.message})`);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
const lines = [
|
|
695
|
-
`Version: ${version}`,
|
|
696
|
-
`MEMORY_DIR: ${MEMORY_DIR}`,
|
|
697
|
-
`SQLite DB: ${dbPath}`,
|
|
698
|
-
`Global store: ${storeFilePath(GLOBAL_KEY)}`,
|
|
699
|
-
`Project store: ${storeFilePath(activeProjectKey)}`,
|
|
700
|
-
...identityLines,
|
|
701
|
-
];
|
|
702
|
-
if (rag.error) lines.push(`RAG: unavailable (${rag.error})`);
|
|
703
|
-
else
|
|
704
|
-
lines.push(
|
|
705
|
-
`RAG: ${rag.documents} doc(s), ${rag.sections} section(s), ${rag.chunks} chunk(s), ${rag.edges} edge(s), ${rag.links} memory link(s)`
|
|
706
|
-
);
|
|
707
|
-
return lines.join("\n");
|
|
382
|
+
async execute(_args, ctx = {}) {
|
|
383
|
+
return await memoryInfo({}, { worktree: ctx.worktree ?? worktree, directory: ctx.directory ?? directory });
|
|
708
384
|
},
|
|
709
385
|
},
|
|
710
386
|
"link_knowledge": {
|
|
@@ -741,7 +417,7 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
741
417
|
if (!factText || !docId) {
|
|
742
418
|
throw new Error("factText and docId are required parameters for link action");
|
|
743
419
|
}
|
|
744
|
-
const res = linkFactToDocument({
|
|
420
|
+
const res = await linkFactToDocument({
|
|
745
421
|
factKey: key,
|
|
746
422
|
factText,
|
|
747
423
|
docId,
|
|
@@ -754,12 +430,12 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
754
430
|
|
|
755
431
|
if (act === "get_doc_links") {
|
|
756
432
|
if (!docId) throw new Error("docId parameter is required for get_doc_links action");
|
|
757
|
-
const links = getLinksForDoc(docId);
|
|
433
|
+
const links = await getLinksForDoc(docId);
|
|
758
434
|
return JSON.stringify(links, null, 2);
|
|
759
435
|
}
|
|
760
436
|
|
|
761
437
|
if (act === "list_links") {
|
|
762
|
-
const links = listAllLinks(key);
|
|
438
|
+
const links = await listAllLinks(key);
|
|
763
439
|
return JSON.stringify(links, null, 2);
|
|
764
440
|
}
|
|
765
441
|
|
|
@@ -864,13 +540,17 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
864
540
|
},
|
|
865
541
|
async execute({ action, docId, snapshotPath }) {
|
|
866
542
|
const { getDatabase } = await import("../mcp-server/db/database.js");
|
|
867
|
-
const db = getDatabase();
|
|
543
|
+
const db = await getDatabase();
|
|
868
544
|
|
|
869
545
|
if (action === "stats") {
|
|
870
|
-
const
|
|
871
|
-
const
|
|
872
|
-
const
|
|
873
|
-
const
|
|
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;
|
|
874
554
|
return JSON.stringify(
|
|
875
555
|
{
|
|
876
556
|
documents: docCount,
|
|
@@ -884,7 +564,7 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
884
564
|
}
|
|
885
565
|
|
|
886
566
|
if (action === "list") {
|
|
887
|
-
const docs = db
|
|
567
|
+
const docs = await db
|
|
888
568
|
.prepare("SELECT id, title, path, blob_hash, created_at FROM documents ORDER BY created_at DESC")
|
|
889
569
|
.all();
|
|
890
570
|
return JSON.stringify(docs, null, 2);
|
|
@@ -892,7 +572,7 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
892
572
|
|
|
893
573
|
if (action === "read_document") {
|
|
894
574
|
if (!docId) throw new Error("docId parameter is required for read_document action");
|
|
895
|
-
const doc = db
|
|
575
|
+
const doc = await db
|
|
896
576
|
.prepare("SELECT id, title, path, blob_hash, created_at FROM documents WHERE id = ? OR path = ? OR title = ?")
|
|
897
577
|
.get(docId, docId, docId);
|
|
898
578
|
if (!doc) {
|
|
@@ -938,6 +618,34 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
938
618
|
throw new Error(`Unknown action: ${action}`);
|
|
939
619
|
},
|
|
940
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
|
+
},
|
|
941
649
|
"link_project_memory": {
|
|
942
650
|
description: "Link the current directory to a Git-based project identity, register aliases, and optionally migrate legacy/path stores.",
|
|
943
651
|
args: {
|