@lotargo/memory_plugin 1.6.3 → 1.6.5
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 +64 -1
- package/README.md +28 -20
- package/mcp-server/admin/snapshot.js +93 -26
- package/mcp-server/cli/direct_commands.js +5 -3
- package/mcp-server/cli/handlers/storage_actions.js +3 -1
- package/mcp-server/config/config_manager.js +0 -1
- package/mcp-server/db/migrations.js +26 -5
- package/mcp-server/db/sync_queue.js +118 -44
- package/mcp-server/graph/knowledge_linker.js +126 -19
- package/mcp-server/ingest/exporter.js +38 -9
- package/mcp-server/ingest/pipeline.js +146 -48
- package/mcp-server/memory.js +13 -3
- package/mcp-server/prompt_manager.js +10 -7
- package/mcp-server/retrieval/retriever.js +62 -38
- package/mcp-server/setup.js +18 -12
- package/mcp-server/tools/core/memory_core.js +465 -393
- package/mcp-server/tools/identity_tools.js +25 -6
- package/mcp-server/tools/memory_tools.js +138 -123
- package/mcp-server/tools/rag_tools.js +313 -249
- package/opencode-plugin/index.js +558 -440
- package/package.json +5 -5
- package/skills/using-memory/SKILL.md +152 -117
package/opencode-plugin/index.js
CHANGED
|
@@ -8,13 +8,11 @@ import { fileURLToPath } from "node:url";
|
|
|
8
8
|
import {
|
|
9
9
|
parseFactEntry,
|
|
10
10
|
factText,
|
|
11
|
-
factMeta,
|
|
12
|
-
isSuperseded,
|
|
13
|
-
displayFact,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
metaBadges,
|
|
17
|
-
} from "../mcp-server/fact_format.js";
|
|
11
|
+
factMeta,
|
|
12
|
+
isSuperseded,
|
|
13
|
+
displayFact,
|
|
14
|
+
factBody,
|
|
15
|
+
} from "../mcp-server/fact_format.js";
|
|
18
16
|
|
|
19
17
|
import {
|
|
20
18
|
MEMORY_DIR,
|
|
@@ -28,7 +26,8 @@ import {
|
|
|
28
26
|
} from "../mcp-server/memory.js";
|
|
29
27
|
|
|
30
28
|
import { closeDatabase } from "../mcp-server/db/database.js";
|
|
31
|
-
import { requireProjectKey } from "../mcp-server/tools/helpers.js";
|
|
29
|
+
import { requireProjectKey } from "../mcp-server/tools/helpers.js";
|
|
30
|
+
import { resolveRagScopeKey, resolveRagScopeKeys, resolveManageRagScopeKeys, removeDocumentScopes } from "../mcp-server/rag_scope.js";
|
|
32
31
|
// Shared Notebook tool implementations — the same code the MCP server runs, so
|
|
33
32
|
// a fix in one surface can no longer miss the other.
|
|
34
33
|
import {
|
|
@@ -103,16 +102,20 @@ async function notify(client, message, variant = "success") {
|
|
|
103
102
|
|
|
104
103
|
const MEMORY_INSTRUCTION =
|
|
105
104
|
"MANDATORY FIRST STEP (READ MEMORIES FIRST):\n" +
|
|
106
|
-
"At the start of every session, you MUST thoroughly review all saved memories provided below BEFORE performing any user task or editing code.\n" +
|
|
107
|
-
"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" +
|
|
108
|
-
"
|
|
105
|
+
"At the start of every session, you MUST thoroughly review all saved memories provided below BEFORE performing any user task or editing code.\n" +
|
|
106
|
+
"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" +
|
|
107
|
+
"PROJECT IDENTITY DIRECTIVE:\n" +
|
|
108
|
+
"After reviewing the injected memories, call `memory_info`. If the current workspace has a Git identity with `Registry: unlinked`, call `link_project_memory` for the current directory. Re-read memories only when linking migrated legacy facts. Outside Git, use global memory only.\n" +
|
|
109
|
+
"PROACTIVE MEMORY DIRECTIVE:\n" +
|
|
109
110
|
"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" +
|
|
110
111
|
"Do NOT wait for explicit user commands like \"remember this\". Automatically capture high-signal facts in real time.\n" +
|
|
111
112
|
"Use `remember` only for important, durable facts about the user and project.\n" +
|
|
112
113
|
"Save high-signal items: user role, goals, constraints, tech stack preferences, architecture decisions, project conventions.\n" +
|
|
113
114
|
"DO NOT save: transient details, one-off statements, full conversation turns, or anything unlikely to be useful in future sessions.\n" +
|
|
114
|
-
"When saving, translate the fact into clear, concise English.\n" +
|
|
115
|
-
"Use `scope: \"global\"` for personal facts, `scope: \"project\"` for project-specific facts
|
|
115
|
+
"When saving, translate the fact into clear, concise English.\n" +
|
|
116
|
+
"Use `scope: \"global\"` for personal facts, `scope: \"project\"` for project-specific facts.\n" +
|
|
117
|
+
"SELECTIVE RAG DIRECTIVE:\n" +
|
|
118
|
+
"When web research or current technical documentation yields reliable project knowledge likely to be reused, ingest only the relevant source or excerpt with project scope and link it to the project Notebook fact it supports. Use global RAG only for intentionally cross-project sources. Prefer authoritative and newer-than-training documentation; do not dump everything encountered into RAG.";
|
|
116
119
|
|
|
117
120
|
function sortNewestFirst(entries) {
|
|
118
121
|
return [...entries].sort((a, b) => {
|
|
@@ -126,7 +129,7 @@ function sortNewestFirst(entries) {
|
|
|
126
129
|
});
|
|
127
130
|
}
|
|
128
131
|
|
|
129
|
-
function formatInjectedFacts(entries, limit, now = Date.now()) {
|
|
132
|
+
export function formatInjectedFacts(entries, limit, now = Date.now()) {
|
|
130
133
|
const activeEntries = entries.filter((e) => !isSuperseded(e));
|
|
131
134
|
const sorted = sortNewestFirst(activeEntries);
|
|
132
135
|
|
|
@@ -143,414 +146,508 @@ function formatInjectedFacts(entries, limit, now = Date.now()) {
|
|
|
143
146
|
}
|
|
144
147
|
|
|
145
148
|
const combined = [...injectPriority, ...normalPriority];
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
"
|
|
272
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
},
|
|
330
|
-
async execute(args, { worktree, directory }) {
|
|
331
|
-
return await
|
|
332
|
-
},
|
|
333
|
-
},
|
|
334
|
-
|
|
335
|
-
"
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
args
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
},
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
const
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
149
|
+
const hasLimit = Number.isFinite(Number(limit)) && Number(limit) > 0;
|
|
150
|
+
const sliced = hasLimit ? combined.slice(0, Number(limit)) : combined;
|
|
151
|
+
|
|
152
|
+
const formattedLines = [];
|
|
153
|
+
for (let i = 0; i < sliced.length; i++) {
|
|
154
|
+
formattedLines.push(`${i + 1}. ${displayFact(sliced[i], now)}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (hasLimit && activeEntries.length > Number(limit)) {
|
|
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
|
+
|
|
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
|
+
|
|
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
|
+
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)" },
|
|
317
|
+
limit: { type: "number", description: "Pagination limit (optional)" },
|
|
318
|
+
includeSuperseded: { type: "boolean", description: "Include superseded historical facts (excluded by default)", default: false },
|
|
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
|
+
|
|
418
|
+
if (act === "link") {
|
|
419
|
+
if (!factText || !docId) {
|
|
420
|
+
throw new Error("factText and docId are required parameters for link action");
|
|
421
|
+
}
|
|
422
|
+
const facts = await readMemory(key);
|
|
423
|
+
const needle = factText.toLowerCase().trim();
|
|
424
|
+
const matches = facts.filter((entry) => {
|
|
425
|
+
const body = factBody(entry).toLowerCase();
|
|
426
|
+
return body === needle || body.includes(needle) || entry.toLowerCase().includes(needle);
|
|
427
|
+
});
|
|
428
|
+
if (matches.length === 0) throw new Error(`Notebook fact not found for link: ${factText}`);
|
|
429
|
+
if (matches.length > 1) throw new Error(`Notebook fact match is ambiguous; use a more specific factText: ${factText}`);
|
|
430
|
+
const resolvedFactText = factBody(matches[0]);
|
|
431
|
+
const res = await linkFactToDocument({
|
|
432
|
+
factKey: key,
|
|
433
|
+
factText: resolvedFactText,
|
|
434
|
+
docId,
|
|
435
|
+
startLine,
|
|
436
|
+
endLine,
|
|
437
|
+
relationType: relationType || "LINKS_TO",
|
|
438
|
+
});
|
|
439
|
+
return JSON.stringify(res, null, 2);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (act === "get_doc_links") {
|
|
443
|
+
if (!docId) throw new Error("docId parameter is required for get_doc_links action");
|
|
444
|
+
const allowedScopes = key === GLOBAL_KEY ? [GLOBAL_KEY] : [GLOBAL_KEY, key];
|
|
445
|
+
const links = await getLinksForDoc(docId, allowedScopes);
|
|
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": {
|
|
458
|
+
description:
|
|
459
|
+
"Selectively preserve a reliable, reusable source in the RAG knowledge base; do not ingest everything encountered. " +
|
|
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" },
|
|
468
|
+
path: { type: "string", description: "Original document file path" },
|
|
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" },
|
|
472
|
+
generateEmbeddings: { type: "boolean", description: "Compute dense vector embeddings", default: true },
|
|
473
|
+
},
|
|
474
|
+
async execute({ content, type, title, path, scope, directory, project, generateEmbeddings }, { worktree, directory: ctxDir }) {
|
|
475
|
+
const { ingestDocument } = await import("../mcp-server/ingest/pipeline.js");
|
|
476
|
+
const effectiveDir = directory || project || ctxDir;
|
|
477
|
+
const projectScope = await resolveRagScopeKey(scope || "project", { worktree, directory: effectiveDir });
|
|
478
|
+
const result = await ingestDocument({
|
|
479
|
+
content,
|
|
480
|
+
type: type || "text",
|
|
481
|
+
title: title || null,
|
|
482
|
+
path: path || null,
|
|
483
|
+
generateEmbeddings: generateEmbeddings !== false,
|
|
484
|
+
projectScope,
|
|
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,
|
|
493
|
+
deduplicated: result.deduplicated,
|
|
494
|
+
scope: result.projectScope,
|
|
495
|
+
},
|
|
496
|
+
null,
|
|
497
|
+
2
|
|
498
|
+
);
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
"query_knowledge_base": {
|
|
502
|
+
description:
|
|
503
|
+
"Perform project-isolated hybrid search (RSF/RRF BM25 full-text + dense vector similarity) across the RAG knowledge base. " +
|
|
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
|
+
},
|
|
512
|
+
generateEmbeddings: { type: "boolean", description: "Use vector search alongside BM25", default: true },
|
|
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" },
|
|
516
|
+
},
|
|
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");
|
|
520
|
+
const activeConfig = getConfig();
|
|
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,
|
|
528
|
+
instruction: instruction || null,
|
|
529
|
+
scopeKeys,
|
|
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
|
+
|
|
552
|
+
return headerNote + formatted;
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
"batch_query_knowledge_base": {
|
|
556
|
+
description:
|
|
557
|
+
"Execute multiple project-isolated hybrid searches in one call. " +
|
|
558
|
+
"All query embeddings are computed in one ONNX pass and results are returned in input order.",
|
|
559
|
+
args: {
|
|
560
|
+
queries: { type: "array", items: { type: "string" }, description: "Search queries to execute in one batch" },
|
|
561
|
+
limit: { type: "number", description: "Maximum sections per query", default: 5 },
|
|
562
|
+
instruction: { type: "string", description: "Optional retrieval instruction applied to every query" },
|
|
563
|
+
generateEmbeddings: { type: "boolean", description: "Use vector search alongside BM25", default: true },
|
|
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" },
|
|
567
|
+
},
|
|
568
|
+
async execute({ queries, limit, instruction, generateEmbeddings, scope, directory, project }, { worktree, directory: ctxDir }) {
|
|
569
|
+
const { batchHybridQuery } = await import("../mcp-server/retrieval/retriever.js");
|
|
570
|
+
const { getConfig } = await import("../mcp-server/config/config_manager.js");
|
|
571
|
+
const activeConfig = getConfig();
|
|
572
|
+
const effectiveDir = directory || project || ctxDir;
|
|
573
|
+
const scopeKeys = await resolveRagScopeKeys(scope || "all", { worktree, directory: effectiveDir });
|
|
574
|
+
const allResults = await batchHybridQuery(queries, {
|
|
575
|
+
limit: limit || 5,
|
|
576
|
+
generateEmbeddings: generateEmbeddings !== false,
|
|
577
|
+
instruction: instruction || null,
|
|
578
|
+
scopeKeys,
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
const formatted = allResults.map((results, queryIndex) => {
|
|
582
|
+
const header = `## Query ${queryIndex + 1}: "${queries[queryIndex]}"\n`;
|
|
583
|
+
if (!results || results.length === 0) return `${header}_No results found._`;
|
|
584
|
+
return header + results.map((result, resultIndex) => {
|
|
585
|
+
let itemHeader = `### [${resultIndex + 1}] ${result.doc_title || "Untitled"}`;
|
|
586
|
+
if (result.heading) itemHeader += ` > ${result.heading}`;
|
|
587
|
+
if (result.breadcrumbs) itemHeader += ` (${result.breadcrumbs})`;
|
|
588
|
+
let body = `Score: ${(result.score || 0).toFixed(4)}`;
|
|
589
|
+
if (result.retrieval_policy && result.retrieval_policy !== "micro_chunk") {
|
|
590
|
+
body += ` [${result.retrieval_policy}]`;
|
|
591
|
+
}
|
|
592
|
+
if (result.defined_symbols && result.defined_symbols.length > 0) {
|
|
593
|
+
body += `\nDefined Symbols: ${result.defined_symbols.join(", ")}`;
|
|
594
|
+
}
|
|
595
|
+
body += `\n\n${result.snippet || result.full_section_content || ""}`;
|
|
596
|
+
return `${itemHeader}\n${body}`;
|
|
597
|
+
}).join("\n\n---\n\n");
|
|
598
|
+
}).join("\n\n===\n\n");
|
|
599
|
+
|
|
600
|
+
return `[Active Model: ${activeConfig.embeddingModel} | Fusion: ${activeConfig.fusionAlgorithm.toUpperCase()} | ${queries.length} queries]\n\n${formatted}`;
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
"manage_knowledge_base": {
|
|
604
|
+
description:
|
|
605
|
+
"Manage the project-isolated RAG knowledge base: inspect stats, list documents, read full raw document, unlink/delete documents, or export/import complete snapshots.",
|
|
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)" },
|
|
612
|
+
snapshotPath: { type: "string", description: "File path for snapshot export/import" },
|
|
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" },
|
|
616
|
+
},
|
|
617
|
+
async execute({ action, docId, snapshotPath, scope, directory, project }, { worktree, directory: ctxDir }) {
|
|
618
|
+
const { getDatabase } = await import("../mcp-server/db/database.js");
|
|
619
|
+
const db = await getDatabase();
|
|
620
|
+
const effectiveDir = directory || project || ctxDir;
|
|
621
|
+
const scopeKeys = ["stats", "list", "read_document", "delete"].includes(action)
|
|
622
|
+
? await resolveManageRagScopeKeys(action, scope, { worktree, directory: effectiveDir })
|
|
623
|
+
: null;
|
|
624
|
+
const placeholders = scopeKeys ? scopeKeys.map(() => "?").join(",") : "";
|
|
625
|
+
const visibleDocWhere = scopeKeys
|
|
626
|
+
? `EXISTS (SELECT 1 FROM document_scopes ds WHERE ds.doc_id = d.id AND ds.scope_key IN (${placeholders}))`
|
|
627
|
+
: "1=1";
|
|
628
|
+
|
|
629
|
+
if (action === "stats") {
|
|
630
|
+
const docCountRow = await db.prepare(`SELECT COUNT(*) as cnt FROM documents d WHERE ${visibleDocWhere}`).get(...scopeKeys);
|
|
631
|
+
const docCount = docCountRow ? docCountRow.cnt : 0;
|
|
632
|
+
const secCountRow = await db.prepare(`SELECT COUNT(*) as cnt FROM sections s JOIN documents d ON d.id = s.doc_id WHERE ${visibleDocWhere}`).get(...scopeKeys);
|
|
633
|
+
const secCount = secCountRow ? secCountRow.cnt : 0;
|
|
634
|
+
const chunkCountRow = await db.prepare(`SELECT COUNT(*) as cnt FROM micro_chunks m JOIN documents d ON d.id = m.doc_id WHERE ${visibleDocWhere}`).get(...scopeKeys);
|
|
635
|
+
const chunkCount = chunkCountRow ? chunkCountRow.cnt : 0;
|
|
636
|
+
const visibleDocIds = await db.prepare(`SELECT d.id FROM documents d WHERE ${visibleDocWhere}`).all(...scopeKeys);
|
|
637
|
+
let edgeCount = 0;
|
|
638
|
+
if (visibleDocIds.length > 0) {
|
|
639
|
+
const docIds = visibleDocIds.map((row) => row.id);
|
|
640
|
+
const docPlaceholders = docIds.map(() => "?").join(",");
|
|
641
|
+
const ownedRows = await db.prepare(`
|
|
642
|
+
SELECT id FROM sections WHERE doc_id IN (${docPlaceholders})
|
|
643
|
+
UNION SELECT id FROM medium_chunks WHERE doc_id IN (${docPlaceholders})
|
|
644
|
+
UNION SELECT id FROM micro_chunks WHERE doc_id IN (${docPlaceholders})
|
|
645
|
+
`).all(...docIds, ...docIds, ...docIds);
|
|
646
|
+
const ownedIds = [...docIds, ...ownedRows.map((row) => row.id)];
|
|
647
|
+
const edgePlaceholders = ownedIds.map(() => "?").join(",");
|
|
648
|
+
const edgeCountRow = await db.prepare(`SELECT COUNT(*) as cnt FROM graph_edges WHERE source_id IN (${edgePlaceholders}) OR target_id IN (${edgePlaceholders})`).get(...ownedIds, ...ownedIds);
|
|
649
|
+
edgeCount = edgeCountRow ? edgeCountRow.cnt : 0;
|
|
650
|
+
}
|
|
554
651
|
return JSON.stringify(
|
|
555
652
|
{
|
|
556
653
|
documents: docCount,
|
|
@@ -563,18 +660,18 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
563
660
|
);
|
|
564
661
|
}
|
|
565
662
|
|
|
566
|
-
if (action === "list") {
|
|
567
|
-
const docs = await db
|
|
568
|
-
.prepare(
|
|
569
|
-
.all();
|
|
663
|
+
if (action === "list") {
|
|
664
|
+
const docs = await db
|
|
665
|
+
.prepare(`SELECT d.id, d.title, d.path, d.blob_hash, d.created_at FROM documents d WHERE ${visibleDocWhere} ORDER BY d.created_at DESC`)
|
|
666
|
+
.all(...scopeKeys);
|
|
570
667
|
return JSON.stringify(docs, null, 2);
|
|
571
668
|
}
|
|
572
669
|
|
|
573
670
|
if (action === "read_document") {
|
|
574
671
|
if (!docId) throw new Error("docId parameter is required for read_document action");
|
|
575
|
-
const doc = await db
|
|
576
|
-
.prepare(
|
|
577
|
-
.get(docId, docId, docId);
|
|
672
|
+
const doc = await db
|
|
673
|
+
.prepare(`SELECT d.id, d.title, d.path, d.blob_hash, d.created_at FROM documents d WHERE (d.id = ? OR d.path = ? OR d.title = ?) AND ${visibleDocWhere}`)
|
|
674
|
+
.get(docId, docId, docId, ...scopeKeys);
|
|
578
675
|
if (!doc) {
|
|
579
676
|
throw new Error(`Document not found in knowledge base for docId: ${docId}`);
|
|
580
677
|
}
|
|
@@ -593,10 +690,24 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
593
690
|
);
|
|
594
691
|
}
|
|
595
692
|
|
|
596
|
-
if (action === "delete") {
|
|
597
|
-
if (!docId) throw new Error("docId parameter is required for delete action");
|
|
598
|
-
const
|
|
599
|
-
|
|
693
|
+
if (action === "delete") {
|
|
694
|
+
if (!docId) throw new Error("docId parameter is required for delete action");
|
|
695
|
+
const visible = await db
|
|
696
|
+
.prepare(`SELECT d.id FROM documents d WHERE (d.id = ? OR d.path = ? OR d.title = ?) AND ${visibleDocWhere}`)
|
|
697
|
+
.get(docId, docId, docId, ...scopeKeys);
|
|
698
|
+
if (!visible) throw new Error(`Document not found in the selected RAG scope for docId: ${docId}`);
|
|
699
|
+
const scopeRemoval = await removeDocumentScopes(db, visible.id, scopeKeys);
|
|
700
|
+
if (scopeRemoval.remainingScopes > 0) {
|
|
701
|
+
return JSON.stringify({
|
|
702
|
+
deleted: false,
|
|
703
|
+
unlinked: true,
|
|
704
|
+
docId: visible.id,
|
|
705
|
+
removedScopes: scopeRemoval.removedScopes,
|
|
706
|
+
remainingScopes: scopeRemoval.remainingScopes,
|
|
707
|
+
}, null, 2);
|
|
708
|
+
}
|
|
709
|
+
const { deleteDocument } = await import("../mcp-server/ingest/pipeline.js");
|
|
710
|
+
const result = await deleteDocument(visible.id, db);
|
|
600
711
|
return JSON.stringify(result, null, 2);
|
|
601
712
|
}
|
|
602
713
|
|
|
@@ -683,7 +794,7 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
683
794
|
let migrated = false;
|
|
684
795
|
const legacyPathKey = canonicalPath(dir);
|
|
685
796
|
const legacyEntries = await readMemory(legacyPathKey);
|
|
686
|
-
if (legacyEntries && legacyEntries.length > 0) {
|
|
797
|
+
if (legacyEntries && legacyEntries.length > 0) {
|
|
687
798
|
const gitEntries = await readMemory(key);
|
|
688
799
|
const seen = new Set(gitEntries.map((e) => factBody(e).toLowerCase().trim()));
|
|
689
800
|
let mergedCount = 0;
|
|
@@ -706,8 +817,11 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
706
817
|
const { unlink } = await import("fs/promises");
|
|
707
818
|
await unlink(legacyFp);
|
|
708
819
|
}
|
|
709
|
-
} catch (e) {}
|
|
710
|
-
}
|
|
820
|
+
} catch (e) {}
|
|
821
|
+
}
|
|
822
|
+
const { moveKnowledgeScope } = await import("../mcp-server/graph/knowledge_linker.js");
|
|
823
|
+
const migratedKnowledge = await moveKnowledgeScope(db, legacyPathKey, key);
|
|
824
|
+
if (migratedKnowledge.movedLinks > 0 || migratedKnowledge.movedDocuments > 0) migrated = true;
|
|
711
825
|
|
|
712
826
|
const res = {
|
|
713
827
|
status: "success",
|
|
@@ -794,9 +908,11 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
794
908
|
|
|
795
909
|
await writeMemory(targetKey, targetFacts);
|
|
796
910
|
|
|
797
|
-
await db
|
|
798
|
-
await
|
|
799
|
-
await
|
|
911
|
+
await upsertIdentity(db, { key: targetKey, name: sourceIdentity.name, primaryRemote: normalizeRemoteUrl(remote) });
|
|
912
|
+
await db.prepare("UPDATE project_aliases SET identity_key = ? WHERE identity_key = ?;").run(targetKey, sourceKey);
|
|
913
|
+
const { moveKnowledgeScope } = await import("../mcp-server/graph/knowledge_linker.js");
|
|
914
|
+
const movedKnowledge = await moveKnowledgeScope(db, sourceKey, targetKey);
|
|
915
|
+
await removeIdentity(db, sourceKey);
|
|
800
916
|
|
|
801
917
|
try {
|
|
802
918
|
const sourceFp = storeFilePath(sourceKey);
|
|
@@ -811,7 +927,9 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
811
927
|
status: "success",
|
|
812
928
|
sourceKey,
|
|
813
929
|
targetKey,
|
|
814
|
-
mergedFacts: mergedCount
|
|
930
|
+
mergedFacts: mergedCount,
|
|
931
|
+
movedKnowledgeLinks: movedKnowledge.movedLinks,
|
|
932
|
+
movedRagDocuments: movedKnowledge.movedDocuments
|
|
815
933
|
};
|
|
816
934
|
await notify(client, "Project memory relinked");
|
|
817
935
|
return JSON.stringify(res, null, 2);
|