@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.
- package/CHANGELOG.md +138 -0
- package/README.md +406 -352
- package/mcp-server/admin/auth.js +13 -4
- package/mcp-server/admin/snapshot.js +24 -7
- package/mcp-server/boot.js +43 -0
- 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/cli_boot.js +37 -0
- 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/preinstall.js +23 -2
- 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 -516
- package/mcp-server/tools/rag_tools.js +49 -1
- package/opencode-plugin/index.js +94 -397
- package/package.json +13 -4
- 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,52 +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
|
-
|
|
24
|
-
} = await import("../mcp-server/fact_format.js");
|
|
17
|
+
} from "../mcp-server/fact_format.js";
|
|
25
18
|
|
|
26
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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(
|
|
307
|
-
const
|
|
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(
|
|
402
|
-
|
|
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(
|
|
520
|
-
|
|
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(
|
|
573
|
-
const
|
|
574
|
-
|
|
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(
|
|
617
|
-
const
|
|
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
|
-
|
|
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
|
|
882
|
-
const
|
|
883
|
-
const
|
|
884
|
-
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;
|
|
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: {
|