@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.
@@ -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
- docId: { type: "string", description: "Optional document ID, title, or path to link this fact to" },
275
- startLine: { type: "number", description: "Optional starting line number in target document" },
276
- endLine: { type: "number", description: "Optional ending line number in target document" },
277
- relationType: {
278
- type: "string",
279
- description: "Relation type (e.g. \x27RULES_FOR\x27, \x27IMPLEMENTS\x27, \x27REFERENCES\x27)",
280
- default: "LINKS_TO",
281
- },
282
- ttl: { type: "string", description: "Optional time-to-live, e.g. \x2790d\x27, \x272w\x27, \x2724h\x27, \x2712m\x27" },
283
- keep: { type: "boolean", description: "Protect the fact from forget deletion unless force=true" },
284
- tags: { type: "string", description: "Optional comma-separated tags, e.g. \x27pref,arch\x27" },
285
- supersedes: { type: "string", description: "Optional number, id, or text of the fact this one replaces" },
286
- },
287
- async execute(args, { worktree, directory }) {
288
- const result = await rememberFact(args, { worktree, directory });
289
- await notify(client, result);
290
- return result;
291
- },
292
- },
293
-
294
- "recall": {
295
- description:
296
- "Show saved facts with any Agent-linked Knowledge Base documents/lines. " +
297
- "scope: \x27project\x27, \x27global\x27, \x27all\x27 (default), or \x27list_projects\x27. " +
298
- "Use project: \x27<directory path>\x27 to read facts of a specific project from any working directory. " +
299
- "query filters by keyword, tags by comma-separated tags, since/until by date (YYYY-MM-DD). " +
300
- "The response includes the store file paths.",
301
- args: {
302
- scope: {
303
- type: "string",
304
- description: "project, global, all (по умолчанию) или list_projects",
305
- default: "all",
306
- },
307
- project: { type: "string", description: "Directory path of the project to read facts from (e.g. \x27F:/projects/plugins/memory\x27)" },
308
- query: { type: "string", description: "Optional keyword filter; all space-separated terms must match" },
309
- tags: { type: "string", description: "Optional comma-separated tag filter (any match)" },
310
- since: { type: "string", description: "Optional start date filter, YYYY-MM-DD (inclusive)" },
311
- until: { type: "string", description: "Optional end date filter, YYYY-MM-DD (inclusive)" },
312
- mode: { type: "string", description: "Result mode: 'full' (with body, default) or 'headers' (title and badges only)", default: "full" },
313
- offset: { type: "number", description: "Pagination offset (optional)" },
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
- async execute(args, { worktree, directory }) {
329
- return await getFactById(args, { worktree, directory });
330
- },
331
- },
332
- "forget": {
333
- description: "Удалить факт по номеру (см. recall), по диапазону (например '3-30', включительно) или тексту. Защищённые факты (remember с keep=true) пропускаются, если не передан force=true",
334
- args: {
335
- query: { type: "string", description: "Номер факта, диапазон вида '3-30' или текст для поиска" },
336
- scope: {
337
- type: "string",
338
- description: "project (по умолчанию) или global",
339
- default: "project",
340
- },
341
- force: { type: "boolean", description: "Удалить также защищённые (keep) факты" },
342
- },
343
- async execute(args, { worktree, directory }) {
344
- const result = await forgetFacts(args, { worktree, directory });
345
- if (result.startsWith("Memory updated")) await notify(client, result);
346
- return result;
347
- },
348
- },
349
- "update_fact": {
350
- description:
351
- "Update the text of an existing fact by number (from recall), id, or text match, " +
352
- "preserving its original date and metadata. Linked Knowledge Base documents are re-pointed to the new text.",
353
- args: {
354
- id: { type: "string", description: "Number (from recall), metadata id, or text of the fact to update" },
355
- newText: { type: "string", description: "New fact text" },
356
- title: { type: "string", description: "Optional new title for the fact" },
357
- scope: { type: "string", description: "\x27project\x27 (default) or \x27global\x27", default: "project" },
358
- },
359
- async execute(args, { worktree, directory }) {
360
- const result = await updateFactText(args, { worktree, directory });
361
- await notify(client, result);
362
- return result;
363
- },
364
- },
365
-
366
- "memory_info": {
367
- description: "Show memory storage paths (store files, MEMORY_DIR, SQLite DB), fact counts, and Knowledge Base stats.",
368
- args: {},
369
- async execute(_args, ctx = {}) {
370
- return await memoryInfo({}, { worktree: ctx.worktree ?? worktree, directory: ctx.directory ?? directory });
371
- },
372
- },
373
- "link_knowledge": {
374
- description:
375
- "Explicitly link a Notebook memory fact to a Knowledge Base document, section, or line range. " +
376
- "Creates Agent-driven Graph Edges connecting memory to RAG documents.",
377
- args: {
378
- action: {
379
- type: "string",
380
- description: "Action type: 'link' (default), 'list_links', 'get_doc_links'",
381
- default: "link",
382
- },
383
- factText: { type: "string", description: "Memory fact text or keyword" },
384
- docId: { type: "string", description: "Document ID, title, or file path" },
385
- scope: { type: "string", description: "'project' (default) or 'global'", default: "project" },
386
- startLine: { type: "number", description: "Starting line number in target document" },
387
- endLine: { type: "number", description: "Ending line number in target document" },
388
- relationType: {
389
- type: "string",
390
- description: "Relation type (e.g. 'RULES_FOR', 'IMPLEMENTS', 'EXPLAINS')",
391
- default: "LINKS_TO",
392
- },
393
- },
394
- async execute({ action, factText, docId, scope, startLine, endLine, relationType }, { worktree, directory }) {
395
- const { linkFactToDocument, getLinksForDoc, listAllLinks } = await import("../mcp-server/graph/knowledge_linker.js");
396
- const key = await scopeKey(scope || "project", worktree, directory);
397
- const act = action || "link";
398
-
399
- if (act === "link" || act === "list_links") {
400
- requireProjectKey(key);
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 projectScope = await resolveRagScopeKey(scope || "project", { worktree, directory });
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 scopeKeys = await resolveRagScopeKeys(scope || "all", { worktree, directory });
502
-
503
- const results = await hybridQuery({
504
- query,
505
- limit: limit || 5,
506
- generateEmbeddings: generateEmbeddings !== false,
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 scopeKeys = await resolveRagScopeKeys(scope || "all", { worktree, directory });
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