@lotargo/memory_plugin 1.6.4 → 1.6.6
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 +17 -0
- package/mcp-server/benchmarks/fetch_real_corpus.js +351 -0
- package/mcp-server/benchmarks/gpu_profile_benchmark.js +170 -0
- package/mcp-server/benchmarks/policy_dominance_test.js +221 -0
- package/mcp-server/benchmarks/quality_evaluator.js +598 -0
- package/mcp-server/benchmarks/raw_corpus_data.js +613 -0
- package/mcp-server/benchmarks/run_benchmarks.js +366 -0
- package/mcp-server/benchmarks/stress_ingestion.js +195 -0
- package/mcp-server/benchmarks/table_code_retrieval.js +453 -0
- package/mcp-server/benchmarks/test_dual_layer.js +141 -0
- package/mcp-server/memory.js +13 -3
- package/mcp-server/rag_scope.js +83 -0
- package/mcp-server/tools/core/memory_core.js +376 -356
- package/mcp-server/tools/identity_tools.js +4 -2
- package/mcp-server/tools/memory_tools.js +134 -121
- package/mcp-server/tools/rag_tools.js +207 -202
- package/opencode-plugin/index.js +366 -339
- package/package.json +4 -25
- package/skills/using-memory/SKILL.md +93 -89
package/opencode-plugin/index.js
CHANGED
|
@@ -152,254 +152,269 @@ export function formatInjectedFacts(entries, limit, now = Date.now()) {
|
|
|
152
152
|
const formattedLines = [];
|
|
153
153
|
for (let i = 0; i < sliced.length; i++) {
|
|
154
154
|
formattedLines.push(`${i + 1}. ${displayFact(sliced[i], now)}`);
|
|
155
|
-
}
|
|
156
|
-
|
|
155
|
+
}
|
|
156
|
+
|
|
157
157
|
if (hasLimit && activeEntries.length > Number(limit)) {
|
|
158
158
|
const remaining = activeEntries.length - Number(limit);
|
|
159
|
-
formattedLines.push(`... and ${remaining} more of ${activeEntries.length} memories (use recall tool to fetch all)`);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return formattedLines.join("\n");
|
|
163
|
-
}
|
|
164
|
-
|
|
159
|
+
formattedLines.push(`... and ${remaining} more of ${activeEntries.length} memories (use recall tool to fetch all)`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return formattedLines.join("\n");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
165
|
export function buildMemoryContext(globalFacts, projectFacts, projectKey, injectLimit, now = Date.now()) {
|
|
166
|
-
const parts = [MEMORY_INSTRUCTION];
|
|
167
|
-
|
|
168
|
-
if (globalFacts.length) {
|
|
169
|
-
const formatted = formatInjectedFacts(globalFacts, injectLimit, now);
|
|
170
|
-
if (formatted) parts.push("## Global\n" + formatted);
|
|
171
|
-
}
|
|
172
|
-
if (projectFacts.length) {
|
|
173
|
-
const formatted = formatInjectedFacts(projectFacts, injectLimit, now);
|
|
174
|
-
if (formatted) parts.push(`## Project: ${projectKey}\n` + formatted);
|
|
175
|
-
}
|
|
176
|
-
return `<MEMORY>\n${parts.join("\n\n")}\n</MEMORY>`;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const MCP_SERVERS = [
|
|
180
|
-
{ id: "context7", desc: "Документация библиотек и фреймворков (Context7)" },
|
|
181
|
-
{ id: "supabase", desc: "БД Supabase — SQL, миграции, edge functions" },
|
|
182
|
-
{ id: "stitch", desc: "UI дизайн — генерация и редактирование экранов" },
|
|
183
|
-
{ id: "neon", desc: "БД Neon — PostgreSQL, схемы, миграции" },
|
|
184
|
-
{ id: "linear", desc: "Linear — задачи, проекты, документы" },
|
|
185
|
-
{ id: "grep", desc: "Поиск примеров кода на GitHub" },
|
|
186
|
-
{ id: "skills-anthropic", desc: "Скиллы Anthropic — дизайн, доки, MCP, PDF/PPTX/XLSX" },
|
|
187
|
-
{ id: "skills-vercel", desc: "Скиллы mattpocock — engineering workflow (grill, tdd, triage, architecture)" },
|
|
188
|
-
{ id: "playwright", desc: "Браузерные тесты — навигация, скриншоты, клики" },
|
|
189
|
-
{ id: "github", desc: "GitHub API — PRs, issues, репозитории" },
|
|
190
|
-
];
|
|
191
|
-
|
|
192
|
-
export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
193
|
-
installExitHook();
|
|
194
|
-
await ensureDir();
|
|
195
|
-
let activeProjectKey = await scopeKey("project", worktree, directory);
|
|
196
|
-
let identityResolveAt = 0;
|
|
197
|
-
|
|
198
|
-
const currentProjectKey = async () => {
|
|
199
|
-
const now = Date.now();
|
|
200
|
-
if (now < identityResolveAt) return activeProjectKey;
|
|
201
|
-
identityResolveAt = now + 2000;
|
|
202
|
-
try {
|
|
203
|
-
const path = client?.path?.get ? await client.path.get() : null;
|
|
204
|
-
const wt = path?.worktree || worktree;
|
|
205
|
-
const dir = path?.directory || directory;
|
|
206
|
-
const key = await scopeKey("project", wt, dir);
|
|
207
|
-
if (key !== activeProjectKey) activeProjectKey = key;
|
|
208
|
-
} catch (e) {}
|
|
209
|
-
return activeProjectKey;
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
return {
|
|
213
|
-
"experimental.chat.messages.transform": async (_input, output) => {
|
|
214
|
-
if (!output.messages?.length) return;
|
|
215
|
-
const firstUser = output.messages.find((m) => m?.info?.role === "user");
|
|
216
|
-
if (!firstUser?.parts?.length) return;
|
|
217
|
-
|
|
218
|
-
if (firstUser.parts.some((p) => p.type === "text" && p.text.includes("<MEMORY>"))) return;
|
|
219
|
-
|
|
220
|
-
const [globalFacts, projectFacts] = await Promise.all([
|
|
221
|
-
readMemory(GLOBAL_KEY),
|
|
222
|
-
readMemory(await currentProjectKey()),
|
|
223
|
-
]);
|
|
224
|
-
|
|
166
|
+
const parts = [MEMORY_INSTRUCTION];
|
|
167
|
+
|
|
168
|
+
if (globalFacts.length) {
|
|
169
|
+
const formatted = formatInjectedFacts(globalFacts, injectLimit, now);
|
|
170
|
+
if (formatted) parts.push("## Global\n" + formatted);
|
|
171
|
+
}
|
|
172
|
+
if (projectFacts.length) {
|
|
173
|
+
const formatted = formatInjectedFacts(projectFacts, injectLimit, now);
|
|
174
|
+
if (formatted) parts.push(`## Project: ${projectKey}\n` + formatted);
|
|
175
|
+
}
|
|
176
|
+
return `<MEMORY>\n${parts.join("\n\n")}\n</MEMORY>`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const MCP_SERVERS = [
|
|
180
|
+
{ id: "context7", desc: "Документация библиотек и фреймворков (Context7)" },
|
|
181
|
+
{ id: "supabase", desc: "БД Supabase — SQL, миграции, edge functions" },
|
|
182
|
+
{ id: "stitch", desc: "UI дизайн — генерация и редактирование экранов" },
|
|
183
|
+
{ id: "neon", desc: "БД Neon — PostgreSQL, схемы, миграции" },
|
|
184
|
+
{ id: "linear", desc: "Linear — задачи, проекты, документы" },
|
|
185
|
+
{ id: "grep", desc: "Поиск примеров кода на GitHub" },
|
|
186
|
+
{ id: "skills-anthropic", desc: "Скиллы Anthropic — дизайн, доки, MCP, PDF/PPTX/XLSX" },
|
|
187
|
+
{ id: "skills-vercel", desc: "Скиллы mattpocock — engineering workflow (grill, tdd, triage, architecture)" },
|
|
188
|
+
{ id: "playwright", desc: "Браузерные тесты — навигация, скриншоты, клики" },
|
|
189
|
+
{ id: "github", desc: "GitHub API — PRs, issues, репозитории" },
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
193
|
+
installExitHook();
|
|
194
|
+
await ensureDir();
|
|
195
|
+
let activeProjectKey = await scopeKey("project", worktree, directory);
|
|
196
|
+
let identityResolveAt = 0;
|
|
197
|
+
|
|
198
|
+
const currentProjectKey = async () => {
|
|
199
|
+
const now = Date.now();
|
|
200
|
+
if (now < identityResolveAt) return activeProjectKey;
|
|
201
|
+
identityResolveAt = now + 2000;
|
|
202
|
+
try {
|
|
203
|
+
const path = client?.path?.get ? await client.path.get() : null;
|
|
204
|
+
const wt = path?.worktree || worktree;
|
|
205
|
+
const dir = path?.directory || directory;
|
|
206
|
+
const key = await scopeKey("project", wt, dir);
|
|
207
|
+
if (key !== activeProjectKey) activeProjectKey = key;
|
|
208
|
+
} catch (e) {}
|
|
209
|
+
return activeProjectKey;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
"experimental.chat.messages.transform": async (_input, output) => {
|
|
214
|
+
if (!output.messages?.length) return;
|
|
215
|
+
const firstUser = output.messages.find((m) => m?.info?.role === "user");
|
|
216
|
+
if (!firstUser?.parts?.length) return;
|
|
217
|
+
|
|
218
|
+
if (firstUser.parts.some((p) => p.type === "text" && p.text.includes("<MEMORY>"))) return;
|
|
219
|
+
|
|
220
|
+
const [globalFacts, projectFacts] = await Promise.all([
|
|
221
|
+
readMemory(GLOBAL_KEY),
|
|
222
|
+
readMemory(await currentProjectKey()),
|
|
223
|
+
]);
|
|
224
|
+
|
|
225
225
|
const context = buildMemoryContext(globalFacts, projectFacts, activeProjectKey, null);
|
|
226
|
-
const ref = firstUser.parts[0];
|
|
227
|
-
firstUser.parts.unshift({ ...ref, type: "text", text: context });
|
|
228
|
-
},
|
|
229
|
-
|
|
230
|
-
tool: {
|
|
231
|
-
"list-mcp-tools": {
|
|
232
|
-
description: "Показать список всех подключённых MCP серверов и их назначение",
|
|
233
|
-
args: {},
|
|
234
|
-
async execute() {
|
|
235
|
-
const lines = MCP_SERVERS.map((s) => ` ${s.id.padEnd(20)} ${s.desc}`);
|
|
236
|
-
return "Доступные MCP серверы:\n" + lines.join("\n");
|
|
237
|
-
},
|
|
238
|
-
},
|
|
239
|
-
"mcp-reminder": {
|
|
240
|
-
description: "Напомнить какие MCP инструменты подходят для текущей задачи. Вызови когда сомневаешься что выбрать.",
|
|
241
|
-
args: {
|
|
242
|
-
task: {
|
|
243
|
-
type: "string",
|
|
244
|
-
description: "Описание того что собираешься делать (опционально)",
|
|
245
|
-
},
|
|
246
|
-
},
|
|
247
|
-
async execute({ task }) {
|
|
248
|
-
if (task) {
|
|
249
|
-
return `Для задачи "${task}" рекомендую посмотреть список через list-mcp-tools. Основные сценарии:\n- Работа с кодом → skills-vercel (grill, tdd, review), github\n- UI/дизайн → stitch, skills-anthropic (frontend-design, webapp-testing)\n- База данных → supabase, neon\n- Документы → skills-anthropic (docx, pdf, pptx, xlsx)\n- Поиск примеров → grep`;
|
|
250
|
-
}
|
|
251
|
-
return "Вызови list-mcp-tools чтобы увидеть все доступные MCP серверы";
|
|
252
|
-
},
|
|
253
|
-
},
|
|
254
|
-
"remember": {
|
|
255
|
-
description:
|
|
256
|
-
"Save an important, durable fact to memory. Only use for high-signal information " +
|
|
257
|
-
"(name, goals, constraints, tech preferences, project conventions). " +
|
|
258
|
-
"docId/startLine/endLine/relationType are OPTIONAL and only used to link the fact to a " +
|
|
259
|
-
"Knowledge Base document or line range; omit them when no linking is needed. " +
|
|
260
|
-
"ttl is OPTIONAL (e.g. \x2790d\x27, \x272w\x27, \x2724h\x27) — expired facts are shown with [EXPIRED] but not auto-deleted. " +
|
|
261
|
-
"keep=true protects the fact from forget deletion unless force=true. " +
|
|
262
|
-
"tags is OPTIONAL comma-separated text for filtering. " +
|
|
263
|
-
"supersedes is OPTIONAL: a number, id, or text of a fact this one replaces. " +
|
|
264
|
-
"Translate the fact into English and keep it concise. " +
|
|
265
|
-
"scope: \x27project\x27 (default) or \x27global\x27",
|
|
266
|
-
args: {
|
|
267
|
-
fact: { type: "string", description: "The fact to remember, written in English" },
|
|
268
|
-
title: { type: "string", description: "Optional title for the fact" },
|
|
269
|
-
scope: {
|
|
270
|
-
type: "string",
|
|
271
|
-
description: "\x27project\x27 (default) or \x27global\x27",
|
|
272
|
-
default: "project",
|
|
273
|
-
},
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
"
|
|
299
|
-
"
|
|
300
|
-
"
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
226
|
+
const ref = firstUser.parts[0];
|
|
227
|
+
firstUser.parts.unshift({ ...ref, type: "text", text: context });
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
tool: {
|
|
231
|
+
"list-mcp-tools": {
|
|
232
|
+
description: "Показать список всех подключённых MCP серверов и их назначение",
|
|
233
|
+
args: {},
|
|
234
|
+
async execute() {
|
|
235
|
+
const lines = MCP_SERVERS.map((s) => ` ${s.id.padEnd(20)} ${s.desc}`);
|
|
236
|
+
return "Доступные MCP серверы:\n" + lines.join("\n");
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
"mcp-reminder": {
|
|
240
|
+
description: "Напомнить какие MCP инструменты подходят для текущей задачи. Вызови когда сомневаешься что выбрать.",
|
|
241
|
+
args: {
|
|
242
|
+
task: {
|
|
243
|
+
type: "string",
|
|
244
|
+
description: "Описание того что собираешься делать (опционально)",
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
async execute({ task }) {
|
|
248
|
+
if (task) {
|
|
249
|
+
return `Для задачи "${task}" рекомендую посмотреть список через list-mcp-tools. Основные сценарии:\n- Работа с кодом → skills-vercel (grill, tdd, review), github\n- UI/дизайн → stitch, skills-anthropic (frontend-design, webapp-testing)\n- База данных → supabase, neon\n- Документы → skills-anthropic (docx, pdf, pptx, xlsx)\n- Поиск примеров → grep`;
|
|
250
|
+
}
|
|
251
|
+
return "Вызови list-mcp-tools чтобы увидеть все доступные MCP серверы";
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
"remember": {
|
|
255
|
+
description:
|
|
256
|
+
"Save an important, durable fact to memory. Only use for high-signal information " +
|
|
257
|
+
"(name, goals, constraints, tech preferences, project conventions). " +
|
|
258
|
+
"docId/startLine/endLine/relationType are OPTIONAL and only used to link the fact to a " +
|
|
259
|
+
"Knowledge Base document or line range; omit them when no linking is needed. " +
|
|
260
|
+
"ttl is OPTIONAL (e.g. \x2790d\x27, \x272w\x27, \x2724h\x27) — expired facts are shown with [EXPIRED] but not auto-deleted. " +
|
|
261
|
+
"keep=true protects the fact from forget deletion unless force=true. " +
|
|
262
|
+
"tags is OPTIONAL comma-separated text for filtering. " +
|
|
263
|
+
"supersedes is OPTIONAL: a number, id, or text of a fact this one replaces. " +
|
|
264
|
+
"Translate the fact into English and keep it concise. " +
|
|
265
|
+
"scope: \x27project\x27 (default) or \x27global\x27",
|
|
266
|
+
args: {
|
|
267
|
+
fact: { type: "string", description: "The fact to remember, written in English" },
|
|
268
|
+
title: { type: "string", description: "Optional title for the fact" },
|
|
269
|
+
scope: {
|
|
270
|
+
type: "string",
|
|
271
|
+
description: "\x27project\x27 (default) or \x27global\x27",
|
|
272
|
+
default: "project",
|
|
273
|
+
},
|
|
274
|
+
directory: { type: "string", description: "Optional workspace/project directory path to target when scope='project' (e.g. 'F:/projects/my-app')" },
|
|
275
|
+
project: { type: "string", description: "Alias for directory" },
|
|
276
|
+
docId: { type: "string", description: "Optional document ID, title, or path to link this fact to" },
|
|
277
|
+
startLine: { type: "number", description: "Optional starting line number in target document" },
|
|
278
|
+
endLine: { type: "number", description: "Optional ending line number in target document" },
|
|
279
|
+
relationType: {
|
|
280
|
+
type: "string",
|
|
281
|
+
description: "Relation type (e.g. \x27RULES_FOR\x27, \x27IMPLEMENTS\x27, \x27REFERENCES\x27)",
|
|
282
|
+
default: "LINKS_TO",
|
|
283
|
+
},
|
|
284
|
+
ttl: { type: "string", description: "Optional time-to-live, e.g. \x2790d\x27, \x272w\x27, \x2724h\x27, \x2712m\x27" },
|
|
285
|
+
keep: { type: "boolean", description: "Protect the fact from forget deletion unless force=true" },
|
|
286
|
+
tags: { type: "string", description: "Optional comma-separated tags, e.g. \x27pref,arch\x27" },
|
|
287
|
+
supersedes: { type: "string", description: "Optional number, id, or text of the fact this one replaces" },
|
|
288
|
+
},
|
|
289
|
+
async execute(args, { worktree, directory }) {
|
|
290
|
+
const result = await rememberFact(args, { worktree, directory });
|
|
291
|
+
await notify(client, result);
|
|
292
|
+
return result;
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
"recall": {
|
|
297
|
+
description:
|
|
298
|
+
"Show saved facts with any Agent-linked Knowledge Base documents/lines. " +
|
|
299
|
+
"scope: \x27project\x27, \x27global\x27, \x27all\x27 (default), or \x27list_projects\x27. " +
|
|
300
|
+
"Use directory: \x27<directory path>\x27 to read facts of a specific project from any working directory. " +
|
|
301
|
+
"query filters by keyword, tags by comma-separated tags, since/until by date (YYYY-MM-DD). " +
|
|
302
|
+
"The response includes the store file paths.",
|
|
303
|
+
args: {
|
|
304
|
+
scope: {
|
|
305
|
+
type: "string",
|
|
306
|
+
description: "project, global, all (по умолчанию) или list_projects",
|
|
307
|
+
default: "all",
|
|
308
|
+
},
|
|
309
|
+
directory: { type: "string", description: "Directory path of the project to read facts from (e.g. \x27F:/projects/plugins/memory\x27)" },
|
|
310
|
+
project: { type: "string", description: "Alias for directory" },
|
|
311
|
+
query: { type: "string", description: "Optional keyword filter; all space-separated terms must match" },
|
|
312
|
+
tags: { type: "string", description: "Optional comma-separated tag filter (any match)" },
|
|
313
|
+
since: { type: "string", description: "Optional start date filter, YYYY-MM-DD (inclusive)" },
|
|
314
|
+
until: { type: "string", description: "Optional end date filter, YYYY-MM-DD (inclusive)" },
|
|
315
|
+
mode: { type: "string", description: "Result mode: 'full' (with body, default) or 'headers' (title and badges only)", default: "full" },
|
|
316
|
+
offset: { type: "number", description: "Pagination offset (optional)" },
|
|
314
317
|
limit: { type: "number", description: "Pagination limit (optional)" },
|
|
315
318
|
includeSuperseded: { type: "boolean", description: "Include superseded historical facts (excluded by default)", default: false },
|
|
316
|
-
},
|
|
317
|
-
async execute(args, { worktree, directory }) {
|
|
318
|
-
return await recallFacts(args, { worktree, directory });
|
|
319
|
-
},
|
|
320
|
-
},
|
|
321
|
-
|
|
322
|
-
"get_fact": {
|
|
323
|
-
description: "Get the full text and metadata of a single fact by its metadata id.",
|
|
324
|
-
args: {
|
|
325
|
-
id: { type: "string", description: "The unique metadata id of the fact (e.g. \x278f3a2c\x27)" },
|
|
326
|
-
scope: { type: "string", description: "\x27project\x27, \x27global\x27, or \x27all\x27 (default)", default: "all" },
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
type: "
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
},
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
319
|
+
},
|
|
320
|
+
async execute(args, { worktree, directory }) {
|
|
321
|
+
return await recallFacts(args, { worktree, directory });
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
"get_fact": {
|
|
326
|
+
description: "Get the full text and metadata of a single fact by its metadata id.",
|
|
327
|
+
args: {
|
|
328
|
+
id: { type: "string", description: "The unique metadata id of the fact (e.g. \x278f3a2c\x27)" },
|
|
329
|
+
scope: { type: "string", description: "\x27project\x27, \x27global\x27, or \x27all\x27 (default)", default: "all" },
|
|
330
|
+
directory: { type: "string", description: "Optional workspace/project directory path" },
|
|
331
|
+
project: { type: "string", description: "Alias for directory" },
|
|
332
|
+
},
|
|
333
|
+
async execute(args, { worktree, directory }) {
|
|
334
|
+
return await getFactById(args, { worktree, directory });
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
"forget": {
|
|
338
|
+
description: "Удалить факт по номеру (см. recall), по диапазону (например '3-30', включительно) или тексту. Защищённые факты (remember с keep=true) пропускаются, если не передан force=true",
|
|
339
|
+
args: {
|
|
340
|
+
query: { type: "string", description: "Номер факта, диапазон вида '3-30' или текст для поиска" },
|
|
341
|
+
scope: {
|
|
342
|
+
type: "string",
|
|
343
|
+
description: "project (по умолчанию) или global",
|
|
344
|
+
default: "project",
|
|
345
|
+
},
|
|
346
|
+
directory: { type: "string", description: "Optional workspace/project directory path" },
|
|
347
|
+
project: { type: "string", description: "Alias for directory" },
|
|
348
|
+
force: { type: "boolean", description: "Удалить также защищённые (keep) факты" },
|
|
349
|
+
},
|
|
350
|
+
async execute(args, { worktree, directory }) {
|
|
351
|
+
const result = await forgetFacts(args, { worktree, directory });
|
|
352
|
+
if (result.startsWith("Memory updated")) await notify(client, result);
|
|
353
|
+
return result;
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
"update_fact": {
|
|
357
|
+
description:
|
|
358
|
+
"Update the text of an existing fact by number (from recall), id, or text match, " +
|
|
359
|
+
"preserving its original date and metadata. Linked Knowledge Base documents are re-pointed to the new text.",
|
|
360
|
+
args: {
|
|
361
|
+
id: { type: "string", description: "Number (from recall), metadata id, or text of the fact to update" },
|
|
362
|
+
newText: { type: "string", description: "New fact text" },
|
|
363
|
+
title: { type: "string", description: "Optional new title for the fact" },
|
|
364
|
+
scope: { type: "string", description: "\x27project\x27 (default) or \x27global\x27", default: "project" },
|
|
365
|
+
directory: { type: "string", description: "Optional workspace/project directory path" },
|
|
366
|
+
project: { type: "string", description: "Alias for directory" },
|
|
367
|
+
},
|
|
368
|
+
async execute(args, { worktree, directory }) {
|
|
369
|
+
const result = await updateFactText(args, { worktree, directory });
|
|
370
|
+
await notify(client, result);
|
|
371
|
+
return result;
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
"memory_info": {
|
|
376
|
+
description: "Show memory storage paths (store files, MEMORY_DIR, SQLite DB), fact counts, and Knowledge Base stats.",
|
|
377
|
+
args: {
|
|
378
|
+
directory: { type: "string", description: "Optional workspace/project directory path to inspect (default: current directory)" },
|
|
379
|
+
project: { type: "string", description: "Alias for directory" },
|
|
380
|
+
},
|
|
381
|
+
async execute(args, ctx = {}) {
|
|
382
|
+
return await memoryInfo(args, { worktree: ctx.worktree ?? worktree, directory: ctx.directory ?? directory });
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
"link_knowledge": {
|
|
386
|
+
description:
|
|
387
|
+
"Explicitly link a Notebook memory fact to a Knowledge Base document, section, or line range. " +
|
|
388
|
+
"Creates Agent-driven Graph Edges connecting memory to RAG documents.",
|
|
389
|
+
args: {
|
|
390
|
+
action: {
|
|
391
|
+
type: "string",
|
|
392
|
+
description: "Action type: 'link' (default), 'list_links', 'get_doc_links'",
|
|
393
|
+
default: "link",
|
|
394
|
+
},
|
|
395
|
+
factText: { type: "string", description: "Memory fact text or keyword" },
|
|
396
|
+
docId: { type: "string", description: "Document ID, title, or file path" },
|
|
397
|
+
scope: { type: "string", description: "'project' (default) or 'global'", default: "project" },
|
|
398
|
+
directory: { type: "string", description: "Optional workspace/project directory path" },
|
|
399
|
+
project: { type: "string", description: "Alias for directory" },
|
|
400
|
+
startLine: { type: "number", description: "Starting line number in target document" },
|
|
401
|
+
endLine: { type: "number", description: "Ending line number in target document" },
|
|
402
|
+
relationType: {
|
|
403
|
+
type: "string",
|
|
404
|
+
description: "Relation type (e.g. 'RULES_FOR', 'IMPLEMENTS', 'EXPLAINS')",
|
|
405
|
+
default: "LINKS_TO",
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
async execute({ action, factText, docId, scope, directory, project, startLine, endLine, relationType }, { worktree, directory: ctxDir }) {
|
|
409
|
+
const { linkFactToDocument, getLinksForDoc, listAllLinks } = await import("../mcp-server/graph/knowledge_linker.js");
|
|
410
|
+
const effectiveDir = directory || project || ctxDir;
|
|
411
|
+
const key = await scopeKey(scope || "project", worktree, effectiveDir);
|
|
412
|
+
const act = action || "link";
|
|
413
|
+
|
|
414
|
+
if (act === "link" || act === "list_links") {
|
|
415
|
+
requireProjectKey(key);
|
|
416
|
+
}
|
|
417
|
+
|
|
403
418
|
if (act === "link") {
|
|
404
419
|
if (!factText || !docId) {
|
|
405
420
|
throw new Error("factText and docId are required parameters for link action");
|
|
@@ -416,118 +431,124 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
416
431
|
const res = await linkFactToDocument({
|
|
417
432
|
factKey: key,
|
|
418
433
|
factText: resolvedFactText,
|
|
419
|
-
docId,
|
|
420
|
-
startLine,
|
|
421
|
-
endLine,
|
|
422
|
-
relationType: relationType || "LINKS_TO",
|
|
423
|
-
});
|
|
424
|
-
return JSON.stringify(res, null, 2);
|
|
425
|
-
}
|
|
426
|
-
|
|
434
|
+
docId,
|
|
435
|
+
startLine,
|
|
436
|
+
endLine,
|
|
437
|
+
relationType: relationType || "LINKS_TO",
|
|
438
|
+
});
|
|
439
|
+
return JSON.stringify(res, null, 2);
|
|
440
|
+
}
|
|
441
|
+
|
|
427
442
|
if (act === "get_doc_links") {
|
|
428
443
|
if (!docId) throw new Error("docId parameter is required for get_doc_links action");
|
|
429
444
|
const allowedScopes = key === GLOBAL_KEY ? [GLOBAL_KEY] : [GLOBAL_KEY, key];
|
|
430
445
|
const links = await getLinksForDoc(docId, allowedScopes);
|
|
431
|
-
return JSON.stringify(links, null, 2);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
if (act === "list_links") {
|
|
435
|
-
const links = await listAllLinks(key);
|
|
436
|
-
return JSON.stringify(links, null, 2);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
throw new Error(`Unknown action: ${act}`);
|
|
440
|
-
},
|
|
441
|
-
},
|
|
442
|
-
"ingest_document": {
|
|
446
|
+
return JSON.stringify(links, null, 2);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (act === "list_links") {
|
|
450
|
+
const links = await listAllLinks(key);
|
|
451
|
+
return JSON.stringify(links, null, 2);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
throw new Error(`Unknown action: ${act}`);
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
"ingest_document": {
|
|
443
458
|
description:
|
|
444
459
|
"Selectively preserve a reliable, reusable source in the RAG knowledge base; do not ingest everything encountered. " +
|
|
445
|
-
"Accepts local file paths, web URLs, or raw Markdown/text content. " +
|
|
446
|
-
"For type='url' the page is fetched and its content is indexed (not just the URL). " +
|
|
447
|
-
"Processes document through 3-tier hierarchy chunking (Big/Medium/Small), " +
|
|
448
|
-
"computes dense vectors, and extracts GraphRAG code symbols.",
|
|
449
|
-
args: {
|
|
450
|
-
content: { type: "string", description: "Raw text content, file path, or web URL" },
|
|
451
|
-
type: { type: "string", description: "Input content type: 'text', 'file', 'url' (url fetches the page content)", default: "text" },
|
|
452
|
-
title: { type: "string", description: "Document title" },
|
|
460
|
+
"Accepts local file paths, web URLs, or raw Markdown/text content. " +
|
|
461
|
+
"For type='url' the page is fetched and its content is indexed (not just the URL). " +
|
|
462
|
+
"Processes document through 3-tier hierarchy chunking (Big/Medium/Small), " +
|
|
463
|
+
"computes dense vectors, and extracts GraphRAG code symbols.",
|
|
464
|
+
args: {
|
|
465
|
+
content: { type: "string", description: "Raw text content, file path, or web URL" },
|
|
466
|
+
type: { type: "string", description: "Input content type: 'text', 'file', 'url' (url fetches the page content)", default: "text" },
|
|
467
|
+
title: { type: "string", description: "Document title" },
|
|
453
468
|
path: { type: "string", description: "Original document file path" },
|
|
454
469
|
scope: { type: "string", description: "RAG visibility: current Git project (default) or global", default: "project" },
|
|
470
|
+
directory: { type: "string", description: "Optional workspace/project directory path to target" },
|
|
471
|
+
project: { type: "string", description: "Alias for directory" },
|
|
455
472
|
generateEmbeddings: { type: "boolean", description: "Compute dense vector embeddings", default: true },
|
|
456
473
|
},
|
|
457
|
-
async execute({ content, type, title, path, scope, generateEmbeddings }, { worktree, directory }) {
|
|
474
|
+
async execute({ content, type, title, path, scope, directory, project, generateEmbeddings }, { worktree, directory: ctxDir }) {
|
|
458
475
|
const { ingestDocument } = await import("../mcp-server/ingest/pipeline.js");
|
|
459
|
-
const
|
|
476
|
+
const effectiveDir = directory || project || ctxDir;
|
|
477
|
+
const projectScope = await resolveRagScopeKey(scope || "project", { worktree, directory: effectiveDir });
|
|
460
478
|
const result = await ingestDocument({
|
|
461
|
-
content,
|
|
462
|
-
type: type || "text",
|
|
463
|
-
title: title || null,
|
|
464
|
-
path: path || null,
|
|
479
|
+
content,
|
|
480
|
+
type: type || "text",
|
|
481
|
+
title: title || null,
|
|
482
|
+
path: path || null,
|
|
465
483
|
generateEmbeddings: generateEmbeddings !== false,
|
|
466
484
|
projectScope,
|
|
467
|
-
});
|
|
468
|
-
return JSON.stringify(
|
|
469
|
-
{
|
|
470
|
-
status: "success",
|
|
471
|
-
docId: result.docId,
|
|
472
|
-
title: result.title,
|
|
473
|
-
sectionsCount: result.sectionsCount,
|
|
474
|
-
microChunksCount: result.microChunksCount,
|
|
485
|
+
});
|
|
486
|
+
return JSON.stringify(
|
|
487
|
+
{
|
|
488
|
+
status: "success",
|
|
489
|
+
docId: result.docId,
|
|
490
|
+
title: result.title,
|
|
491
|
+
sectionsCount: result.sectionsCount,
|
|
492
|
+
microChunksCount: result.microChunksCount,
|
|
475
493
|
deduplicated: result.deduplicated,
|
|
476
494
|
scope: result.projectScope,
|
|
477
|
-
},
|
|
478
|
-
null,
|
|
479
|
-
2
|
|
480
|
-
);
|
|
481
|
-
},
|
|
482
|
-
},
|
|
495
|
+
},
|
|
496
|
+
null,
|
|
497
|
+
2
|
|
498
|
+
);
|
|
499
|
+
},
|
|
500
|
+
},
|
|
483
501
|
"query_knowledge_base": {
|
|
484
502
|
description:
|
|
485
503
|
"Perform project-isolated hybrid search (RSF/RRF BM25 full-text + dense vector similarity) across the RAG knowledge base. " +
|
|
486
|
-
"Returns top-ranked candidate document sections with breadcrumbs, GraphRAG defined code symbols, and relevance scores.",
|
|
487
|
-
args: {
|
|
488
|
-
query: { type: "string", description: "Search query in natural language or symbol name" },
|
|
489
|
-
limit: { type: "number", description: "Maximum number of sections to return", default: 5 },
|
|
490
|
-
instruction: {
|
|
491
|
-
type: "string",
|
|
492
|
-
description: "Optional task-specific retrieval instruction shaping embedding focus",
|
|
493
|
-
},
|
|
504
|
+
"Returns top-ranked candidate document sections with breadcrumbs, GraphRAG defined code symbols, and relevance scores.",
|
|
505
|
+
args: {
|
|
506
|
+
query: { type: "string", description: "Search query in natural language or symbol name" },
|
|
507
|
+
limit: { type: "number", description: "Maximum number of sections to return", default: 5 },
|
|
508
|
+
instruction: {
|
|
509
|
+
type: "string",
|
|
510
|
+
description: "Optional task-specific retrieval instruction shaping embedding focus",
|
|
511
|
+
},
|
|
494
512
|
generateEmbeddings: { type: "boolean", description: "Use vector search alongside BM25", default: true },
|
|
495
513
|
scope: { type: "string", description: "Search global + current project (default), project only, or global only", default: "all" },
|
|
514
|
+
directory: { type: "string", description: "Optional workspace/project directory path to target" },
|
|
515
|
+
project: { type: "string", description: "Alias for directory" },
|
|
496
516
|
},
|
|
497
|
-
async execute({ query, limit, instruction, generateEmbeddings, scope }, { worktree, directory }) {
|
|
498
|
-
const { hybridQuery } = await import("../mcp-server/retrieval/retriever.js");
|
|
499
|
-
const { getConfig } = await import("../mcp-server/config/config_manager.js");
|
|
517
|
+
async execute({ query, limit, instruction, generateEmbeddings, scope, directory, project }, { worktree, directory: ctxDir }) {
|
|
518
|
+
const { hybridQuery } = await import("../mcp-server/retrieval/retriever.js");
|
|
519
|
+
const { getConfig } = await import("../mcp-server/config/config_manager.js");
|
|
500
520
|
const activeConfig = getConfig();
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
521
|
+
const effectiveDir = directory || project || ctxDir;
|
|
522
|
+
const scopeKeys = await resolveRagScopeKeys(scope || "all", { worktree, directory: effectiveDir });
|
|
523
|
+
|
|
524
|
+
const results = await hybridQuery({
|
|
525
|
+
query,
|
|
526
|
+
limit: limit || 5,
|
|
527
|
+
generateEmbeddings: generateEmbeddings !== false,
|
|
507
528
|
instruction: instruction || null,
|
|
508
529
|
scopeKeys,
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
if (!results || results.length === 0) {
|
|
512
|
-
return `[Active Model: ${activeConfig.embeddingModel}]\nNo matching knowledge found for query.`;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
const headerNote = `[Active Model: ${activeConfig.embeddingModel} | Fusion: ${activeConfig.fusionAlgorithm.toUpperCase()}]\n\n`;
|
|
516
|
-
|
|
517
|
-
const formatted = results
|
|
518
|
-
.map((r, i) => {
|
|
519
|
-
let header = `### [${i + 1}] ${r.doc_title || "Untitled"}`;
|
|
520
|
-
if (r.heading) header += ` > ${r.heading}`;
|
|
521
|
-
if (r.breadcrumbs) header += ` (${r.breadcrumbs})`;
|
|
522
|
-
let body = `Score: ${(r.score || 0).toFixed(4)}\n`;
|
|
523
|
-
if (r.defined_symbols && r.defined_symbols.length > 0) {
|
|
524
|
-
body += `Defined Symbols: ${r.defined_symbols.join(", ")}\n`;
|
|
525
|
-
}
|
|
526
|
-
body += `\n${r.snippet || r.full_section_content || ""}`;
|
|
527
|
-
return `${header}\n${body}`;
|
|
528
|
-
})
|
|
529
|
-
.join("\n\n---\n\n");
|
|
530
|
-
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
if (!results || results.length === 0) {
|
|
533
|
+
return `[Active Model: ${activeConfig.embeddingModel}]\nNo matching knowledge found for query.`;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
const headerNote = `[Active Model: ${activeConfig.embeddingModel} | Fusion: ${activeConfig.fusionAlgorithm.toUpperCase()}]\n\n`;
|
|
537
|
+
|
|
538
|
+
const formatted = results
|
|
539
|
+
.map((r, i) => {
|
|
540
|
+
let header = `### [${i + 1}] ${r.doc_title || "Untitled"}`;
|
|
541
|
+
if (r.heading) header += ` > ${r.heading}`;
|
|
542
|
+
if (r.breadcrumbs) header += ` (${r.breadcrumbs})`;
|
|
543
|
+
let body = `Score: ${(r.score || 0).toFixed(4)}\n`;
|
|
544
|
+
if (r.defined_symbols && r.defined_symbols.length > 0) {
|
|
545
|
+
body += `Defined Symbols: ${r.defined_symbols.join(", ")}\n`;
|
|
546
|
+
}
|
|
547
|
+
body += `\n${r.snippet || r.full_section_content || ""}`;
|
|
548
|
+
return `${header}\n${body}`;
|
|
549
|
+
})
|
|
550
|
+
.join("\n\n---\n\n");
|
|
551
|
+
|
|
531
552
|
return headerNote + formatted;
|
|
532
553
|
},
|
|
533
554
|
},
|
|
@@ -541,12 +562,15 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
541
562
|
instruction: { type: "string", description: "Optional retrieval instruction applied to every query" },
|
|
542
563
|
generateEmbeddings: { type: "boolean", description: "Use vector search alongside BM25", default: true },
|
|
543
564
|
scope: { type: "string", description: "Search global + current project (default), project only, or global only", default: "all" },
|
|
565
|
+
directory: { type: "string", description: "Optional workspace/project directory path to target" },
|
|
566
|
+
project: { type: "string", description: "Alias for directory" },
|
|
544
567
|
},
|
|
545
|
-
async execute({ queries, limit, instruction, generateEmbeddings, scope }, { worktree, directory }) {
|
|
568
|
+
async execute({ queries, limit, instruction, generateEmbeddings, scope, directory, project }, { worktree, directory: ctxDir }) {
|
|
546
569
|
const { batchHybridQuery } = await import("../mcp-server/retrieval/retriever.js");
|
|
547
570
|
const { getConfig } = await import("../mcp-server/config/config_manager.js");
|
|
548
571
|
const activeConfig = getConfig();
|
|
549
|
-
const
|
|
572
|
+
const effectiveDir = directory || project || ctxDir;
|
|
573
|
+
const scopeKeys = await resolveRagScopeKeys(scope || "all", { worktree, directory: effectiveDir });
|
|
550
574
|
const allResults = await batchHybridQuery(queries, {
|
|
551
575
|
limit: limit || 5,
|
|
552
576
|
generateEmbeddings: generateEmbeddings !== false,
|
|
@@ -579,20 +603,23 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
579
603
|
"manage_knowledge_base": {
|
|
580
604
|
description:
|
|
581
605
|
"Manage the project-isolated RAG knowledge base: inspect stats, list documents, read full raw document, unlink/delete documents, or export/import complete snapshots.",
|
|
582
|
-
args: {
|
|
583
|
-
action: {
|
|
584
|
-
type: "string",
|
|
585
|
-
description: "Management action: 'stats', 'list', 'read_document', 'delete', 'export_snapshot', 'import_snapshot'",
|
|
586
|
-
},
|
|
587
|
-
docId: { type: "string", description: "Document ID, title, or path (required for read_document and delete)" },
|
|
606
|
+
args: {
|
|
607
|
+
action: {
|
|
608
|
+
type: "string",
|
|
609
|
+
description: "Management action: 'stats', 'list', 'read_document', 'delete', 'export_snapshot', 'import_snapshot'",
|
|
610
|
+
},
|
|
611
|
+
docId: { type: "string", description: "Document ID, title, or path (required for read_document and delete)" },
|
|
588
612
|
snapshotPath: { type: "string", description: "File path for snapshot export/import" },
|
|
589
613
|
scope: { type: "string", description: "For stats/list/read: global + current project by default. Delete defaults to the current project (or global outside Git); pass all/global explicitly for broader removal" },
|
|
614
|
+
directory: { type: "string", description: "Optional workspace/project directory path to target" },
|
|
615
|
+
project: { type: "string", description: "Alias for directory" },
|
|
590
616
|
},
|
|
591
|
-
async execute({ action, docId, snapshotPath, scope }, { worktree, directory }) {
|
|
617
|
+
async execute({ action, docId, snapshotPath, scope, directory, project }, { worktree, directory: ctxDir }) {
|
|
592
618
|
const { getDatabase } = await import("../mcp-server/db/database.js");
|
|
593
619
|
const db = await getDatabase();
|
|
620
|
+
const effectiveDir = directory || project || ctxDir;
|
|
594
621
|
const scopeKeys = ["stats", "list", "read_document", "delete"].includes(action)
|
|
595
|
-
? await resolveManageRagScopeKeys(action, scope, { worktree, directory })
|
|
622
|
+
? await resolveManageRagScopeKeys(action, scope, { worktree, directory: effectiveDir })
|
|
596
623
|
: null;
|
|
597
624
|
const placeholders = scopeKeys ? scopeKeys.map(() => "?").join(",") : "";
|
|
598
625
|
const visibleDocWhere = scopeKeys
|