@lotargo/memory_plugin 1.4.620 → 1.5.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/README.md +352 -334
- package/mcp-server/admin/auth.js +293 -42
- package/mcp-server/cli/direct_commands.js +313 -0
- package/mcp-server/cli/handlers/cloud_actions.js +138 -0
- package/mcp-server/cli/handlers/diagnostics_actions.js +107 -0
- package/mcp-server/cli/handlers/engine_actions.js +214 -0
- package/mcp-server/cli/handlers/prompt_actions.js +24 -0
- package/mcp-server/cli/handlers/storage_actions.js +749 -0
- package/mcp-server/cli/quick_stats.js +39 -0
- package/mcp-server/cli/ui.js +565 -0
- package/mcp-server/cli.js +324 -1945
- package/mcp-server/config/auth_store.js +178 -19
- package/mcp-server/config/config_manager.js +1 -0
- package/mcp-server/db/database.js +18 -3
- package/mcp-server/db/migrations.js +28 -0
- package/mcp-server/fact_format.js +244 -177
- package/mcp-server/identity.js +152 -0
- package/mcp-server/index.js +42 -679
- package/mcp-server/memory.js +50 -63
- package/mcp-server/prompt_manager.js +1 -1
- package/mcp-server/setup.js +41 -0
- package/mcp-server/tools/helpers.js +39 -0
- package/mcp-server/tools/identity_tools.js +277 -0
- package/mcp-server/tools/index.js +9 -0
- package/mcp-server/tools/memory_tools.js +506 -0
- package/mcp-server/tools/rag_tools.js +235 -0
- package/opencode-plugin/index.js +460 -48
- package/package.json +7 -3
- package/skills/using-memory/SKILL.md +31 -14
- package/mcp-server/benchmarks/fetch_real_corpus.js +0 -351
- package/mcp-server/benchmarks/gpu_profile_benchmark.js +0 -170
- package/mcp-server/benchmarks/quality_evaluator.js +0 -600
- package/mcp-server/benchmarks/run_benchmarks.js +0 -347
- package/mcp-server/benchmarks/stress_ingestion.js +0 -195
- package/mcp-server/benchmarks/test_dual_layer.js +0 -140
package/opencode-plugin/index.js
CHANGED
|
@@ -16,6 +16,10 @@ const {
|
|
|
16
16
|
matchesQuery,
|
|
17
17
|
matchesTags,
|
|
18
18
|
inDateRange,
|
|
19
|
+
factTitle,
|
|
20
|
+
factBody,
|
|
21
|
+
autoGenerateTitle,
|
|
22
|
+
metaBadges,
|
|
19
23
|
} = await import("../mcp-server/fact_format.js");
|
|
20
24
|
|
|
21
25
|
const {
|
|
@@ -95,6 +99,7 @@ async function notify(client, message, variant = "success") {
|
|
|
95
99
|
const MEMORY_INSTRUCTION =
|
|
96
100
|
"MANDATORY FIRST STEP (READ MEMORIES FIRST):\n" +
|
|
97
101
|
"At the start of every session, you MUST thoroughly review all saved memories provided below BEFORE performing any user task or editing code.\n" +
|
|
102
|
+
"If calling `recall` manually, your very first action MUST BE to request ALL global memories (`scope: \"all\"` without restrictive query filters) to ensure no global facts or preferences are missed.\n" +
|
|
98
103
|
"PROACTIVE MEMORY DIRECTIVE:\n" +
|
|
99
104
|
"You MUST automatically and proactively call `remember` whenever the user shares durable facts, personal preferences, coding guidelines, tech stack choices, architecture decisions, or project conventions.\n" +
|
|
100
105
|
"Do NOT wait for explicit user commands like \"remember this\". Automatically capture high-signal facts in real time.\n" +
|
|
@@ -104,18 +109,84 @@ const MEMORY_INSTRUCTION =
|
|
|
104
109
|
"When saving, translate the fact into clear, concise English.\n" +
|
|
105
110
|
"Use `scope: \"global\"` for personal facts, `scope: \"project\"` for project-specific facts.";
|
|
106
111
|
|
|
107
|
-
function
|
|
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
|
+
function sortNewestFirst(entries) {
|
|
123
|
+
return [...entries].sort((a, b) => {
|
|
124
|
+
const pa = parseFactEntry(a);
|
|
125
|
+
const pb = parseFactEntry(b);
|
|
126
|
+
if (!pa) return 1;
|
|
127
|
+
if (!pb) return -1;
|
|
128
|
+
const timeA = new Date(`${pa.date}T${pa.time}:00`).getTime();
|
|
129
|
+
const timeB = new Date(`${pb.date}T${pb.time}:00`).getTime();
|
|
130
|
+
return timeB - timeA;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function formatInjectedFacts(entries, limit, now = Date.now()) {
|
|
135
|
+
const activeEntries = entries.filter((e) => !isSuperseded(e));
|
|
136
|
+
const sorted = sortNewestFirst(activeEntries);
|
|
137
|
+
|
|
138
|
+
const injectPriority = [];
|
|
139
|
+
const normalPriority = [];
|
|
140
|
+
|
|
141
|
+
for (const entry of sorted) {
|
|
142
|
+
const meta = factMeta(entry);
|
|
143
|
+
if (meta.inject === "1") {
|
|
144
|
+
injectPriority.push(entry);
|
|
145
|
+
} else {
|
|
146
|
+
normalPriority.push(entry);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const combined = [...injectPriority, ...normalPriority];
|
|
151
|
+
const sliced = combined.slice(0, limit);
|
|
152
|
+
|
|
153
|
+
const formattedLines = [];
|
|
154
|
+
for (let i = 0; i < sliced.length; i++) {
|
|
155
|
+
const entry = sliced[i];
|
|
156
|
+
const meta = factMeta(entry);
|
|
157
|
+
const isPriority = meta.inject === "1";
|
|
158
|
+
|
|
159
|
+
let contentStr;
|
|
160
|
+
if (isPriority) {
|
|
161
|
+
contentStr = displayFact(entry, now);
|
|
162
|
+
} else {
|
|
163
|
+
const title = factTitle(entry);
|
|
164
|
+
const badges = metaBadges(entry, now);
|
|
165
|
+
const badgesStr = badges.length ? ` [${badges.join("] [")}]` : "";
|
|
166
|
+
contentStr = `${title}${badgesStr}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
formattedLines.push(`${i + 1}. ${contentStr}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (activeEntries.length > limit) {
|
|
173
|
+
const remaining = activeEntries.length - limit;
|
|
174
|
+
formattedLines.push(`... and ${remaining} more of ${activeEntries.length} memories (use recall tool to fetch all)`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return formattedLines.join("\n");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function buildMemoryContext(globalFacts, projectFacts, projectKey, injectLimit, now = Date.now()) {
|
|
108
181
|
const parts = [MEMORY_INSTRUCTION];
|
|
109
|
-
|
|
110
|
-
entries
|
|
111
|
-
.filter((e) => !isSuperseded(e))
|
|
112
|
-
.map((e, i) => `${i + 1}. ${displayFact(e, now)}`)
|
|
113
|
-
.join("\n");
|
|
182
|
+
|
|
114
183
|
if (globalFacts.length) {
|
|
115
|
-
|
|
184
|
+
const formatted = formatInjectedFacts(globalFacts, injectLimit, now);
|
|
185
|
+
if (formatted) parts.push("## Global\n" + formatted);
|
|
116
186
|
}
|
|
117
187
|
if (projectFacts.length) {
|
|
118
|
-
|
|
188
|
+
const formatted = formatInjectedFacts(projectFacts, injectLimit, now);
|
|
189
|
+
if (formatted) parts.push(`## Project: ${projectKey}\n` + formatted);
|
|
119
190
|
}
|
|
120
191
|
return `<MEMORY>\n${parts.join("\n\n")}\n</MEMORY>`;
|
|
121
192
|
}
|
|
@@ -135,7 +206,22 @@ const MCP_SERVERS = [
|
|
|
135
206
|
|
|
136
207
|
export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
137
208
|
await ensureDir();
|
|
138
|
-
|
|
209
|
+
let activeProjectKey = await scopeKey("project", worktree, directory);
|
|
210
|
+
let identityResolveAt = 0;
|
|
211
|
+
|
|
212
|
+
const currentProjectKey = async () => {
|
|
213
|
+
const now = Date.now();
|
|
214
|
+
if (now < identityResolveAt) return activeProjectKey;
|
|
215
|
+
identityResolveAt = now + 2000;
|
|
216
|
+
try {
|
|
217
|
+
const path = client?.path?.get ? await client.path.get() : null;
|
|
218
|
+
const wt = path?.worktree || worktree;
|
|
219
|
+
const dir = path?.directory || directory;
|
|
220
|
+
const key = await scopeKey("project", wt, dir);
|
|
221
|
+
if (key !== activeProjectKey) activeProjectKey = key;
|
|
222
|
+
} catch (e) {}
|
|
223
|
+
return activeProjectKey;
|
|
224
|
+
};
|
|
139
225
|
|
|
140
226
|
return {
|
|
141
227
|
"experimental.chat.messages.transform": async (_input, output) => {
|
|
@@ -147,10 +233,14 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
147
233
|
|
|
148
234
|
const [globalFacts, projectFacts] = await Promise.all([
|
|
149
235
|
readMemory(GLOBAL_KEY),
|
|
150
|
-
readMemory(
|
|
236
|
+
readMemory(await currentProjectKey()),
|
|
151
237
|
]);
|
|
152
238
|
|
|
153
|
-
const
|
|
239
|
+
const { getConfig } = await import("../mcp-server/config/config_manager.js");
|
|
240
|
+
const config = getConfig();
|
|
241
|
+
const injectLimit = config.injectLimit !== undefined ? config.injectLimit : 100;
|
|
242
|
+
|
|
243
|
+
const context = buildMemoryContext(globalFacts, projectFacts, activeProjectKey, injectLimit);
|
|
154
244
|
const ref = firstUser.parts[0];
|
|
155
245
|
firstUser.parts.unshift({ ...ref, type: "text", text: context });
|
|
156
246
|
},
|
|
@@ -185,17 +275,18 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
185
275
|
"(name, goals, constraints, tech preferences, project conventions). " +
|
|
186
276
|
"docId/startLine/endLine/relationType are OPTIONAL and only used to link the fact to a " +
|
|
187
277
|
"Knowledge Base document or line range; omit them when no linking is needed. " +
|
|
188
|
-
"ttl is OPTIONAL (e.g.
|
|
278
|
+
"ttl is OPTIONAL (e.g. \x2790d\x27, \x272w\x27, \x2724h\x27) — expired facts are shown with [EXPIRED] but not auto-deleted. " +
|
|
189
279
|
"keep=true protects the fact from forget deletion unless force=true. " +
|
|
190
280
|
"tags is OPTIONAL comma-separated text for filtering. " +
|
|
191
281
|
"supersedes is OPTIONAL: a number, id, or text of a fact this one replaces. " +
|
|
192
282
|
"Translate the fact into English and keep it concise. " +
|
|
193
|
-
"scope:
|
|
283
|
+
"scope: \x27project\x27 (default) or \x27global\x27",
|
|
194
284
|
args: {
|
|
195
285
|
fact: { type: "string", description: "The fact to remember, written in English" },
|
|
286
|
+
title: { type: "string", description: "Optional title for the fact" },
|
|
196
287
|
scope: {
|
|
197
288
|
type: "string",
|
|
198
|
-
description: "
|
|
289
|
+
description: "\x27project\x27 (default) or \x27global\x27",
|
|
199
290
|
default: "project",
|
|
200
291
|
},
|
|
201
292
|
docId: { type: "string", description: "Optional document ID, title, or path to link this fact to" },
|
|
@@ -203,19 +294,38 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
203
294
|
endLine: { type: "number", description: "Optional ending line number in target document" },
|
|
204
295
|
relationType: {
|
|
205
296
|
type: "string",
|
|
206
|
-
description: "Relation type (e.g.
|
|
297
|
+
description: "Relation type (e.g. \x27RULES_FOR\x27, \x27IMPLEMENTS\x27, \x27REFERENCES\x27)",
|
|
207
298
|
default: "LINKS_TO",
|
|
208
299
|
},
|
|
209
|
-
ttl: { type: "string", description: "Optional time-to-live, e.g.
|
|
300
|
+
ttl: { type: "string", description: "Optional time-to-live, e.g. \x2790d\x27, \x272w\x27, \x2724h\x27, \x2712m\x27" },
|
|
210
301
|
keep: { type: "boolean", description: "Protect the fact from forget deletion unless force=true" },
|
|
211
|
-
tags: { type: "string", description: "Optional comma-separated tags, e.g.
|
|
302
|
+
tags: { type: "string", description: "Optional comma-separated tags, e.g. \x27pref,arch\x27" },
|
|
212
303
|
supersedes: { type: "string", description: "Optional number, id, or text of the fact this one replaces" },
|
|
213
304
|
},
|
|
214
|
-
async execute({ fact, scope, docId, startLine, endLine, relationType, ttl, keep, tags, supersedes }, { worktree, directory }) {
|
|
215
|
-
const key = scopeKey(scope || "project", worktree, directory);
|
|
305
|
+
async execute({ fact, title, scope, docId, startLine, endLine, relationType, ttl, keep, tags, supersedes }, { worktree, directory }) {
|
|
306
|
+
const key = requireProjectKey(await scopeKey(scope || "project", worktree, directory));
|
|
216
307
|
const entries = await readMemory(key);
|
|
217
|
-
|
|
218
|
-
const
|
|
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);
|
|
219
329
|
|
|
220
330
|
let supersededInfo = "";
|
|
221
331
|
if (!duplicate) {
|
|
@@ -237,7 +347,7 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
237
347
|
}
|
|
238
348
|
}
|
|
239
349
|
if (!meta.id) meta.id = nextFactId(entries);
|
|
240
|
-
entries.push(formatFactEntry({ date, time, text
|
|
350
|
+
entries.push(formatFactEntry({ date, time, text, meta }));
|
|
241
351
|
await writeMemory(key, entries);
|
|
242
352
|
}
|
|
243
353
|
|
|
@@ -247,7 +357,7 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
247
357
|
const { linkFactToDocument } = await import("../mcp-server/graph/knowledge_linker.js");
|
|
248
358
|
const linkRes = linkFactToDocument({
|
|
249
359
|
factKey: key,
|
|
250
|
-
factText:
|
|
360
|
+
factText: finalFact,
|
|
251
361
|
docId,
|
|
252
362
|
startLine,
|
|
253
363
|
endLine,
|
|
@@ -265,11 +375,11 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
265
375
|
return result;
|
|
266
376
|
},
|
|
267
377
|
},
|
|
268
|
-
|
|
378
|
+
"recall": {
|
|
269
379
|
description:
|
|
270
380
|
"Show saved facts with any Agent-linked Knowledge Base documents/lines. " +
|
|
271
|
-
"scope:
|
|
272
|
-
"Use project:
|
|
381
|
+
"scope: \x27project\x27, \x27global\x27, \x27all\x27 (default), or \x27list_projects\x27. " +
|
|
382
|
+
"Use project: \x27<directory path>\x27 to read facts of a specific project from any working directory. " +
|
|
273
383
|
"query filters by keyword, tags by comma-separated tags, since/until by date (YYYY-MM-DD). " +
|
|
274
384
|
"The response includes the store file paths.",
|
|
275
385
|
args: {
|
|
@@ -278,14 +388,20 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
278
388
|
description: "project, global, all (по умолчанию) или list_projects",
|
|
279
389
|
default: "all",
|
|
280
390
|
},
|
|
281
|
-
project: { type: "string", description: "Directory path of the project to read facts from (e.g.
|
|
391
|
+
project: { type: "string", description: "Directory path of the project to read facts from (e.g. \x27F:/projects/plugins/memory\x27)" },
|
|
282
392
|
query: { type: "string", description: "Optional keyword filter; all space-separated terms must match" },
|
|
283
393
|
tags: { type: "string", description: "Optional comma-separated tag filter (any match)" },
|
|
284
394
|
since: { type: "string", description: "Optional start date filter, YYYY-MM-DD (inclusive)" },
|
|
285
395
|
until: { type: "string", description: "Optional end date filter, YYYY-MM-DD (inclusive)" },
|
|
396
|
+
mode: { type: "string", description: "Result mode: 'full' (with body, default) or 'headers' (title and badges only)", default: "full" },
|
|
397
|
+
offset: { type: "number", description: "Pagination offset (optional)" },
|
|
398
|
+
limit: { type: "number", description: "Pagination limit (optional)" },
|
|
286
399
|
},
|
|
287
|
-
async execute({ scope, project, query, tags, since, until }, { worktree, directory }) {
|
|
400
|
+
async execute({ scope, project, query, tags, since, until, mode, offset, limit }, { worktree, directory }) {
|
|
288
401
|
const results = [];
|
|
402
|
+
const now = Date.now();
|
|
403
|
+
const targetMode = mode || "full";
|
|
404
|
+
const targetOffset = offset !== undefined ? offset : 0;
|
|
289
405
|
|
|
290
406
|
let getLinksForFact;
|
|
291
407
|
try {
|
|
@@ -293,11 +409,35 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
293
409
|
getLinksForFact = linker.getLinksForFact;
|
|
294
410
|
} catch (e) {}
|
|
295
411
|
|
|
296
|
-
const formatFactWithLinks = (factLine, key) => {
|
|
297
|
-
|
|
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
|
+
|
|
298
438
|
if (getLinksForFact) {
|
|
299
439
|
try {
|
|
300
|
-
const links = getLinksForFact(key,
|
|
440
|
+
const links = await getLinksForFact(key, p.text);
|
|
301
441
|
if (links && links.length > 0) {
|
|
302
442
|
const docStr = links
|
|
303
443
|
.map((l) => {
|
|
@@ -305,24 +445,34 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
305
445
|
return `${l.doc_title || l.doc_path}${range}`;
|
|
306
446
|
})
|
|
307
447
|
.join(", ");
|
|
308
|
-
|
|
448
|
+
lineText += ` 🔗 [Linked Docs: ${docStr}]`;
|
|
309
449
|
}
|
|
310
450
|
} catch (e) {}
|
|
311
451
|
}
|
|
312
|
-
return
|
|
452
|
+
return `${index}. ${lineText}`;
|
|
313
453
|
};
|
|
314
454
|
|
|
315
|
-
const target = project ? canonicalPath(project) : projectKey(worktree, directory);
|
|
316
|
-
const label = project ? target : projectName(worktree, directory);
|
|
455
|
+
const target = project ? canonicalPath(project) : await projectKey(worktree, directory);
|
|
456
|
+
const label = project ? target : await projectName(worktree, directory);
|
|
317
457
|
|
|
318
|
-
const collect = (entries, key) => {
|
|
458
|
+
const collect = async (entries, key) => {
|
|
319
459
|
const matched = entries.filter(
|
|
320
460
|
(e) => matchesQuery(e, query) && matchesTags(e, tags) && inDateRange(e, since, until)
|
|
321
461
|
);
|
|
322
462
|
if (!matched.length) return;
|
|
323
463
|
if (results.length) results.push("");
|
|
324
464
|
results.push(`--- ${key === GLOBAL_KEY ? "Global" : `Project: ${key === target ? label : key}`} ---`);
|
|
325
|
-
|
|
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
|
+
}
|
|
326
476
|
results.push(`Store file: ${storeFilePath(key)}`);
|
|
327
477
|
};
|
|
328
478
|
|
|
@@ -338,17 +488,65 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
338
488
|
|
|
339
489
|
if (scope !== "project") {
|
|
340
490
|
const global = await readMemory(GLOBAL_KEY);
|
|
341
|
-
collect(global, GLOBAL_KEY);
|
|
491
|
+
await collect(global, GLOBAL_KEY);
|
|
342
492
|
}
|
|
343
493
|
if (scope !== "global") {
|
|
344
494
|
const local = await readMemory(target);
|
|
345
|
-
collect(local, target);
|
|
495
|
+
await collect(local, target);
|
|
346
496
|
}
|
|
347
497
|
const filtered = Boolean(query || tags || since || until);
|
|
348
498
|
if (!results.length) return filtered ? "No facts match the search." : "Memory is empty.";
|
|
349
499
|
return results.join("\n") + `\n\nMemory dir: ${MEMORY_DIR}`;
|
|
350
500
|
},
|
|
351
501
|
},
|
|
502
|
+
"get_fact": {
|
|
503
|
+
description: "Get the full text and metadata of a single fact by its metadata id.",
|
|
504
|
+
args: {
|
|
505
|
+
id: { type: "string", description: "The unique metadata id of the fact (e.g. \x278f3a2c\x27)" },
|
|
506
|
+
scope: { type: "string", description: "\x27project\x27, \x27global\x27, or \x27all\x27 (default)", default: "all" },
|
|
507
|
+
},
|
|
508
|
+
async execute({ id, scope }, { worktree, directory }) {
|
|
509
|
+
const results = [];
|
|
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");
|
|
548
|
+
},
|
|
549
|
+
},
|
|
352
550
|
"forget": {
|
|
353
551
|
description: "Удалить факт по номеру (см. recall), по диапазону (например '3-30', включительно) или тексту. Защищённые факты (remember с keep=true) пропускаются, если не передан force=true",
|
|
354
552
|
args: {
|
|
@@ -361,7 +559,7 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
361
559
|
force: { type: "boolean", description: "Удалить также защищённые (keep) факты" },
|
|
362
560
|
},
|
|
363
561
|
async execute({ query, scope, force }, { worktree, directory }) {
|
|
364
|
-
const key = scopeKey(scope || "project", worktree, directory);
|
|
562
|
+
const key = requireProjectKey(await scopeKey(scope || "project", worktree, directory));
|
|
365
563
|
const entries = await readMemory(key);
|
|
366
564
|
const rangeMatch = /^\s*(\d+)\s*-\s*(\d+)\s*$/.exec(query);
|
|
367
565
|
const num = parseInt(query, 10);
|
|
@@ -401,26 +599,48 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
401
599
|
args: {
|
|
402
600
|
id: { type: "string", description: "Number (from recall), metadata id, or text of the fact to update" },
|
|
403
601
|
newText: { type: "string", description: "New fact text" },
|
|
404
|
-
|
|
602
|
+
title: { type: "string", description: "Optional new title for the fact" },
|
|
603
|
+
scope: { type: "string", description: "\x27project\x27 (default) or \x27global\x27", default: "project" },
|
|
405
604
|
},
|
|
406
|
-
async execute({ id, newText, scope }, { worktree, directory }) {
|
|
407
|
-
const key = scopeKey(scope || "project", worktree, directory);
|
|
605
|
+
async execute({ id, newText, title, scope }, { worktree, directory }) {
|
|
606
|
+
const key = requireProjectKey(await scopeKey(scope || "project", worktree, directory));
|
|
408
607
|
const entries = await readMemory(key);
|
|
409
608
|
const idx = resolveFactIndex(entries, id);
|
|
410
609
|
if (idx === -1) throw new Error(`Fact not found: ${id}`);
|
|
411
610
|
const p = parseFactEntry(entries[idx]);
|
|
412
611
|
const oldText = p ? p.text : entries[idx];
|
|
413
|
-
const
|
|
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 });
|
|
414
634
|
entries[idx] = newLine;
|
|
415
635
|
await writeMemory(key, entries);
|
|
416
636
|
|
|
417
637
|
let linksUpdated = 0;
|
|
418
638
|
try {
|
|
419
639
|
const { getDatabase } = await import("../mcp-server/db/database.js");
|
|
420
|
-
const db = getDatabase();
|
|
640
|
+
const db = await getDatabase();
|
|
421
641
|
const res = db
|
|
422
642
|
.prepare("UPDATE knowledge_links SET fact_text = ? WHERE fact_key = ? AND fact_text = ?")
|
|
423
|
-
.run(
|
|
643
|
+
.run(finalFact, key, oldBody);
|
|
424
644
|
linksUpdated = res.changes;
|
|
425
645
|
} catch (e) {}
|
|
426
646
|
|
|
@@ -429,7 +649,7 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
429
649
|
return result;
|
|
430
650
|
},
|
|
431
651
|
},
|
|
432
|
-
|
|
652
|
+
"memory_info": {
|
|
433
653
|
description: "Show memory storage paths (store files, MEMORY_DIR, SQLite DB), fact counts, and Knowledge Base stats.",
|
|
434
654
|
args: {},
|
|
435
655
|
async execute() {
|
|
@@ -455,12 +675,29 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
455
675
|
rag.error = e.message;
|
|
456
676
|
}
|
|
457
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
|
+
|
|
458
694
|
const lines = [
|
|
459
695
|
`Version: ${version}`,
|
|
460
696
|
`MEMORY_DIR: ${MEMORY_DIR}`,
|
|
461
697
|
`SQLite DB: ${dbPath}`,
|
|
462
698
|
`Global store: ${storeFilePath(GLOBAL_KEY)}`,
|
|
463
699
|
`Project store: ${storeFilePath(activeProjectKey)}`,
|
|
700
|
+
...identityLines,
|
|
464
701
|
];
|
|
465
702
|
if (rag.error) lines.push(`RAG: unavailable (${rag.error})`);
|
|
466
703
|
else
|
|
@@ -493,9 +730,13 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
493
730
|
},
|
|
494
731
|
async execute({ action, factText, docId, scope, startLine, endLine, relationType }, { worktree, directory }) {
|
|
495
732
|
const { linkFactToDocument, getLinksForDoc, listAllLinks } = await import("../mcp-server/graph/knowledge_linker.js");
|
|
496
|
-
const key = scopeKey(scope || "project", worktree, directory);
|
|
733
|
+
const key = await scopeKey(scope || "project", worktree, directory);
|
|
497
734
|
const act = action || "link";
|
|
498
735
|
|
|
736
|
+
if (act === "link" || act === "list_links") {
|
|
737
|
+
requireProjectKey(key);
|
|
738
|
+
}
|
|
739
|
+
|
|
499
740
|
if (act === "link") {
|
|
500
741
|
if (!factText || !docId) {
|
|
501
742
|
throw new Error("factText and docId are required parameters for link action");
|
|
@@ -697,6 +938,177 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
697
938
|
throw new Error(`Unknown action: ${action}`);
|
|
698
939
|
},
|
|
699
940
|
},
|
|
941
|
+
"link_project_memory": {
|
|
942
|
+
description: "Link the current directory to a Git-based project identity, register aliases, and optionally migrate legacy/path stores.",
|
|
943
|
+
args: {
|
|
944
|
+
directory: { type: "string", description: "Directory path to link (default: current directory)" },
|
|
945
|
+
remote: { type: "string", description: "Optional explicit remote URL to use as primary identity key" },
|
|
946
|
+
},
|
|
947
|
+
async execute({ directory, remote }, { worktree, directory: contextDir }) {
|
|
948
|
+
const { getDatabase } = await import("../mcp-server/db/database.js");
|
|
949
|
+
const { resolveProjectIdentity, upsertIdentity, registerAlias, normalizeRemoteUrl } = await import("../mcp-server/identity.js");
|
|
950
|
+
const db = await getDatabase();
|
|
951
|
+
|
|
952
|
+
const dir = directory || contextDir || process.cwd();
|
|
953
|
+
const identity = await resolveProjectIdentity(dir);
|
|
954
|
+
if (!identity && !remote) {
|
|
955
|
+
throw new Error("No Git repository detected and no remote URL specified.");
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
let key = identity ? identity.key : `git:${normalizeRemoteUrl(remote)}`;
|
|
959
|
+
let name = identity ? identity.name : basename(dir) || "unbound";
|
|
960
|
+
let primaryRemote = remote ? normalizeRemoteUrl(remote) : (identity ? identity.primaryRemote : null);
|
|
961
|
+
|
|
962
|
+
await upsertIdentity(db, { key, name, primaryRemote });
|
|
963
|
+
|
|
964
|
+
const aliases = [];
|
|
965
|
+
if (primaryRemote) {
|
|
966
|
+
aliases.push({ alias: `remote:${primaryRemote}`, kind: "remote" });
|
|
967
|
+
}
|
|
968
|
+
aliases.push({ alias: `path:${canonicalPath(dir)}`, kind: "path" });
|
|
969
|
+
aliases.push({ alias: `basename:${name}`, kind: "basename" });
|
|
970
|
+
|
|
971
|
+
for (const a of aliases) {
|
|
972
|
+
await registerAlias(db, { alias: a.alias, identityKey: key, kind: a.kind });
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
let migrated = false;
|
|
976
|
+
const legacyPathKey = canonicalPath(dir);
|
|
977
|
+
const legacyEntries = await readMemory(legacyPathKey);
|
|
978
|
+
if (legacyEntries && legacyEntries.length > 0) {
|
|
979
|
+
const gitEntries = await readMemory(key);
|
|
980
|
+
const seen = new Set(gitEntries.map((e) => factBody(e).toLowerCase().trim()));
|
|
981
|
+
let mergedCount = 0;
|
|
982
|
+
for (const entry of legacyEntries) {
|
|
983
|
+
const body = factBody(entry).toLowerCase().trim();
|
|
984
|
+
if (!seen.has(body)) {
|
|
985
|
+
seen.add(body);
|
|
986
|
+
gitEntries.push(entry);
|
|
987
|
+
mergedCount++;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
if (mergedCount > 0) {
|
|
991
|
+
await writeMemory(key, gitEntries);
|
|
992
|
+
migrated = true;
|
|
993
|
+
}
|
|
994
|
+
try {
|
|
995
|
+
const legacyFp = storeFilePath(legacyPathKey);
|
|
996
|
+
const { existsSync } = await import("node:fs");
|
|
997
|
+
if (existsSync(legacyFp)) {
|
|
998
|
+
const { unlink } = await import("fs/promises");
|
|
999
|
+
await unlink(legacyFp);
|
|
1000
|
+
}
|
|
1001
|
+
} catch (e) {}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const res = {
|
|
1005
|
+
status: "success",
|
|
1006
|
+
key,
|
|
1007
|
+
name,
|
|
1008
|
+
primaryRemote,
|
|
1009
|
+
aliases: aliases.map((a) => a.alias),
|
|
1010
|
+
migrated
|
|
1011
|
+
};
|
|
1012
|
+
await notify(client, "Project memory linked successfully");
|
|
1013
|
+
return JSON.stringify(res, null, 2);
|
|
1014
|
+
},
|
|
1015
|
+
},
|
|
1016
|
+
"unlink_project_memory": {
|
|
1017
|
+
description: "Remove the path alias link for the specified project directory.",
|
|
1018
|
+
args: {
|
|
1019
|
+
directory: { type: "string", description: "Directory path to unlink (default: current directory)" },
|
|
1020
|
+
purge: { type: "boolean", description: "If true, completely purge the project identity from the SQLite store" },
|
|
1021
|
+
},
|
|
1022
|
+
async execute({ directory, purge }, { worktree, directory: contextDir }) {
|
|
1023
|
+
const { getDatabase } = await import("../mcp-server/db/database.js");
|
|
1024
|
+
const { unregisterAlias, removeIdentity, resolveProjectIdentity } = await import("../mcp-server/identity.js");
|
|
1025
|
+
const db = await getDatabase();
|
|
1026
|
+
|
|
1027
|
+
const dir = directory || contextDir || process.cwd();
|
|
1028
|
+
const alias = `path:${canonicalPath(dir)}`;
|
|
1029
|
+
await unregisterAlias(db, alias);
|
|
1030
|
+
|
|
1031
|
+
let key = null;
|
|
1032
|
+
if (purge) {
|
|
1033
|
+
const identity = await resolveProjectIdentity(dir);
|
|
1034
|
+
if (identity) {
|
|
1035
|
+
key = identity.key;
|
|
1036
|
+
await removeIdentity(db, key);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
const res = {
|
|
1041
|
+
status: "success",
|
|
1042
|
+
alias,
|
|
1043
|
+
purgedIdentityKey: key
|
|
1044
|
+
};
|
|
1045
|
+
await notify(client, "Project memory unlinked");
|
|
1046
|
+
return JSON.stringify(res, null, 2);
|
|
1047
|
+
},
|
|
1048
|
+
},
|
|
1049
|
+
"relink_project_memory": {
|
|
1050
|
+
description: "Move or merge project memories from the current identity to a new target identity.",
|
|
1051
|
+
args: {
|
|
1052
|
+
directory: { type: "string", description: "Directory path to relink (default: current directory)" },
|
|
1053
|
+
remote: { type: "string", description: "New target remote URL / identity key to move memories to" },
|
|
1054
|
+
},
|
|
1055
|
+
async execute({ directory, remote }, { worktree, directory: contextDir }) {
|
|
1056
|
+
const { getDatabase } = await import("../mcp-server/db/database.js");
|
|
1057
|
+
const { resolveProjectIdentity, upsertIdentity, removeIdentity, normalizeRemoteUrl } = await import("../mcp-server/identity.js");
|
|
1058
|
+
const db = await getDatabase();
|
|
1059
|
+
|
|
1060
|
+
const dir = directory || contextDir || process.cwd();
|
|
1061
|
+
const sourceIdentity = await resolveProjectIdentity(dir);
|
|
1062
|
+
if (!sourceIdentity) {
|
|
1063
|
+
throw new Error("Source project identity not detected.");
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
const targetKey = `git:${normalizeRemoteUrl(remote)}`;
|
|
1067
|
+
const sourceKey = sourceIdentity.key;
|
|
1068
|
+
|
|
1069
|
+
if (sourceKey === targetKey) {
|
|
1070
|
+
return "Source and target identities are already identical.";
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const sourceFacts = await readMemory(sourceKey);
|
|
1074
|
+
const targetFacts = await readMemory(targetKey);
|
|
1075
|
+
const seen = new Set(targetFacts.map((e) => factBody(e).toLowerCase().trim()));
|
|
1076
|
+
|
|
1077
|
+
let mergedCount = 0;
|
|
1078
|
+
for (const f of sourceFacts) {
|
|
1079
|
+
const body = factBody(f).toLowerCase().trim();
|
|
1080
|
+
if (!seen.has(body)) {
|
|
1081
|
+
seen.add(body);
|
|
1082
|
+
targetFacts.push(f);
|
|
1083
|
+
mergedCount++;
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
await writeMemory(targetKey, targetFacts);
|
|
1088
|
+
|
|
1089
|
+
await db.prepare("UPDATE project_aliases SET identity_key = ? WHERE identity_key = ?;").run(targetKey, sourceKey);
|
|
1090
|
+
await upsertIdentity(db, { key: targetKey, name: sourceIdentity.name, primaryRemote: normalizeRemoteUrl(remote) });
|
|
1091
|
+
await removeIdentity(db, sourceKey);
|
|
1092
|
+
|
|
1093
|
+
try {
|
|
1094
|
+
const sourceFp = storeFilePath(sourceKey);
|
|
1095
|
+
const { existsSync } = await import("node:fs");
|
|
1096
|
+
if (existsSync(sourceFp)) {
|
|
1097
|
+
const { unlink } = await import("fs/promises");
|
|
1098
|
+
await unlink(sourceFp);
|
|
1099
|
+
}
|
|
1100
|
+
} catch (e) {}
|
|
1101
|
+
|
|
1102
|
+
const res = {
|
|
1103
|
+
status: "success",
|
|
1104
|
+
sourceKey,
|
|
1105
|
+
targetKey,
|
|
1106
|
+
mergedFacts: mergedCount
|
|
1107
|
+
};
|
|
1108
|
+
await notify(client, "Project memory relinked");
|
|
1109
|
+
return JSON.stringify(res, null, 2);
|
|
1110
|
+
},
|
|
1111
|
+
},
|
|
700
1112
|
},
|
|
701
1113
|
};
|
|
702
1114
|
};
|