@lotargo/memory_plugin 1.6.6 → 1.6.7

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.
Files changed (38) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +576 -443
  3. package/mcp-server/cli/direct_commands.js +39 -0
  4. package/mcp-server/cli.js +16 -5
  5. package/mcp-server/cli_boot.js +4 -1
  6. package/mcp-server/client_cli.js +73 -0
  7. package/mcp-server/client_paths.js +44 -0
  8. package/mcp-server/client_registration.js +38 -0
  9. package/mcp-server/codex_config.js +86 -8
  10. package/mcp-server/db/database.js +14 -21
  11. package/mcp-server/db/migrations.js +66 -77
  12. package/mcp-server/db/rag_blob_transport.js +143 -0
  13. package/mcp-server/db/rag_sync.js +284 -0
  14. package/mcp-server/db/sync_queue.js +219 -307
  15. package/mcp-server/dev_link.js +142 -0
  16. package/mcp-server/fact_format.js +44 -12
  17. package/mcp-server/index.js +17 -7
  18. package/mcp-server/ingest/exporter.js +44 -38
  19. package/mcp-server/ingest/pipeline.js +260 -248
  20. package/mcp-server/persona_migration.js +39 -0
  21. package/mcp-server/prompt_manager.js +162 -55
  22. package/mcp-server/retrieval/retriever.js +99 -64
  23. package/mcp-server/setup.js +150 -100
  24. package/mcp-server/storage/blob_store.js +53 -1
  25. package/mcp-server/tools/core/knowledge_read_core.js +163 -0
  26. package/mcp-server/tools/core/memory_core.js +24 -4
  27. package/mcp-server/tools/core/memory_routing.js +10 -0
  28. package/mcp-server/tools/core/note_core.js +53 -0
  29. package/mcp-server/tools/core/rag_query_core.js +169 -0
  30. package/mcp-server/tools/index.js +11 -9
  31. package/mcp-server/tools/memory_tools.js +4 -1
  32. package/mcp-server/tools/note_tools.js +35 -0
  33. package/mcp-server/tools/rag_tools.js +211 -364
  34. package/mcp-server/uninstall.js +627 -0
  35. package/opencode-plugin/index.js +80 -12
  36. package/opencode-plugin/main.js +136 -0
  37. package/package.json +16 -12
  38. package/skills/using-memory/SKILL.md +28 -19
@@ -2,394 +2,243 @@ import * as z from "zod/v4";
2
2
  import { optStr, defBool, defNum, optNum } from "./helpers.js";
3
3
  import { MEMORY_DIR } from "../memory.js";
4
4
  import { registerSnapshotDir } from "../admin/snapshot.js";
5
- import { ensureExportsDir } from "../ingest/exporter.js";
6
- import { resolveRagScopeKey, resolveRagScopeKeys, resolveManageRagScopeKeys, removeDocumentScopes } from "../rag_scope.js";
5
+ import { ensureExportsDir } from "../ingest/exporter.js";
6
+ import { resolveRagScopeKey, resolveManageRagScopeKeys, removeDocumentScopes } from "../rag_scope.js";
7
+ import { runSingleRagQuery, runBatchRagQuery } from "./core/rag_query_core.js";
8
+ import { readKnowledgeDocument, listKnowledgeDocuments } from "./core/knowledge_read_core.js";
7
9
 
8
10
  export function registerRagTools(server) {
9
- // Restrict snapshot export/import paths to the plugin's own data directories.
10
11
  registerSnapshotDir(ensureExportsDir());
11
12
  registerSnapshotDir(MEMORY_DIR);
12
13
 
13
14
  server.registerTool(
14
15
  "ingest_document",
15
16
  {
16
- description:
17
- "Selectively preserve a reliable, reusable source in the RAG knowledge base; do not ingest everything encountered. " +
18
- "Accepts local file paths, web URLs, or raw Markdown/text content. " +
19
- "For type='file' the file is read from disk and indexed with a code-block wrapper. " +
20
- "For type='url' the page is fetched and its content is indexed (not just the URL). " +
21
- "Processes document through 3-tier hierarchy chunking (Big/Medium/Small), " +
22
- "computes dense vectors, and extracts GraphRAG code symbols.",
23
- inputSchema: z.object({
24
- content: z
25
- .string()
26
- .describe(
27
- "Raw text content, file path, or web URL. For type='file' this can be the file path (reads from disk) or the file content directly"
28
- ),
29
- type: z
30
- .enum(["text", "file", "url"])
31
- .nullish()
32
- .transform((v) => v || "text")
33
- .describe("Input content type: 'text' (raw content), 'file' (reads from disk, wraps in code block), or 'url' (fetches page content)"),
34
- title: optStr().describe("Document title"),
35
- path: optStr().describe("Original document file path"),
36
- scope: z.enum(["project", "global"]).nullish().transform((v) => v || "project").describe("RAG visibility: current Git project (default) or global"),
37
- directory: optStr().describe("Optional workspace/project directory path to target"),
38
- project: optStr().describe("Alias for directory"),
39
- generateEmbeddings: defBool(true).describe("Compute dense vector embeddings"),
40
- }),
41
- },
42
- async ({ content, type, title, path, scope, directory, project, generateEmbeddings }) => {
43
- const { ingestDocument } = await import("../ingest/pipeline.js");
44
- const projectScope = await resolveRagScopeKey(scope, { directory, project });
45
- const result = await ingestDocument({
46
- content,
47
- type,
48
- title: title || null,
49
- path: path || null,
50
- generateEmbeddings,
51
- projectScope,
52
- });
53
- return {
54
- content: [
55
- {
56
- type: "text",
57
- text: JSON.stringify(
58
- {
59
- status: "success",
60
- docId: result.docId,
61
- title: result.title,
62
- sectionsCount: result.sectionsCount,
63
- microChunksCount: result.microChunksCount,
64
- deduplicated: result.deduplicated,
65
- scope: result.projectScope,
66
- },
67
- null,
68
- 2
69
- ),
70
- },
71
- ],
72
- };
73
- }
74
- );
75
-
76
- server.registerTool(
77
- "query_knowledge_base",
78
- {
79
- description:
80
- "Perform project-isolated hybrid search (RSF/RRF BM25 full-text + dense vector similarity) across the RAG knowledge base. " +
81
- "Returns top-ranked candidate document sections with breadcrumbs, GraphRAG defined code symbols, and relevance scores.",
82
- inputSchema: z.object({
83
- query: z.string().describe("Search query in natural language or symbol name"),
84
- limit: defNum(5).describe("Maximum number of sections to return"),
85
- instruction: optStr().describe(
86
- "Optional task-specific retrieval instruction shaping embedding focus (e.g. 'Retrieve code snippets', 'Find user preferences'). " +
87
- "Recommended when using E5/BGE models for domain-specific queries."
88
- ),
89
- generateEmbeddings: defBool(true).describe("Use vector search alongside BM25"),
90
- scope: z.enum(["all", "project", "global"]).nullish().transform((v) => v || "all").describe("Search global + current project (default), project only, or global only"),
91
- directory: optStr().describe("Optional workspace/project directory path to target"),
92
- project: optStr().describe("Alias for directory"),
93
- }),
94
- },
95
- async ({ query, limit, instruction, generateEmbeddings, scope, directory, project }) => {
96
- const { hybridQuery } = await import("../retrieval/retriever.js");
97
- const { getConfig } = await import("../config/config_manager.js");
98
- const activeConfig = getConfig();
99
- const scopeKeys = await resolveRagScopeKeys(scope, { directory, project });
100
-
101
- const results = await hybridQuery({
102
- query,
103
- limit,
104
- generateEmbeddings,
105
- instruction: instruction || null,
106
- scopeKeys,
107
- });
108
-
109
- if (!results || results.length === 0) {
110
- return {
111
- content: [
112
- {
113
- type: "text",
114
- text: `[Active Model: ${activeConfig.embeddingModel}]\nNo matching knowledge found for query.`,
115
- },
116
- ],
117
- };
118
- }
119
-
120
- const headerNote = `[Active Model: ${activeConfig.embeddingModel} | Fusion: ${activeConfig.fusionAlgorithm.toUpperCase()}]\n\n`;
121
-
122
- const formatted = results
123
- .map((r, i) => {
124
- let header = `### [${i + 1}] ${r.doc_title || "Untitled"}`;
125
- if (r.heading) header += ` > ${r.heading}`;
126
- if (r.breadcrumbs) header += ` (${r.breadcrumbs})`;
127
- let body = `Score: ${(r.score || 0).toFixed(4)}\n`;
128
- if (r.defined_symbols && r.defined_symbols.length > 0) {
129
- body += `Defined Symbols: ${r.defined_symbols.join(", ")}\n`;
130
- }
131
- body += `\n${r.snippet || r.full_section_content || ""}`;
132
- return `${header}\n${body}`;
133
- })
134
- .join("\n\n---\n\n");
135
-
136
- return { content: [{ type: "text", text: headerNote + formatted }] };
137
- }
138
- );
139
-
140
- server.registerTool(
141
- "batch_query_knowledge_base",
142
- {
143
- description:
144
- "Execute multiple hybrid search queries in a single batch call. " +
145
- "Search is isolated to global plus the current project unless another scope is requested. " +
146
- "More efficient than separate query_knowledge_base calls: all query embeddings computed in one ONNX pass. " +
147
- "Returns one result set per query, in the same order as input.",
148
- inputSchema: z.object({
149
- queries: z
150
- .array(z.string())
151
- .describe("Array of search queries to execute in batch"),
152
- limit: defNum(5).describe("Maximum number of sections to return per query"),
153
- instruction: optStr().describe(
154
- "Optional task-specific retrieval instruction shaping embedding focus. Applied to all queries."
155
- ),
156
- generateEmbeddings: defBool(true).describe("Use vector search alongside BM25"),
157
- scope: z.enum(["all", "project", "global"]).nullish().transform((v) => v || "all").describe("Search global + current project (default), project only, or global only"),
158
- directory: optStr().describe("Optional workspace/project directory path to target"),
159
- project: optStr().describe("Alias for directory"),
160
- }),
161
- },
162
- async ({ queries, limit, instruction, generateEmbeddings, scope, directory, project }) => {
163
- const { batchHybridQuery } = await import("../retrieval/retriever.js");
164
- const { getConfig } = await import("../config/config_manager.js");
165
- const activeConfig = getConfig();
166
- const scopeKeys = await resolveRagScopeKeys(scope, { directory, project });
167
-
168
- const allResults = await batchHybridQuery(queries, {
169
- limit,
170
- generateEmbeddings,
171
- instruction: instruction || null,
172
- scopeKeys,
173
- });
174
-
175
- const formatted = allResults
176
- .map((results, qi) => {
177
- const header = `### Query [${qi + 1}]: "${queries[qi]}"\n\n`;
178
- if (!results || results.length === 0) {
179
- return header + "No matching knowledge found for this query.";
180
- }
181
- const items = results
182
- .map((item, j) => {
183
- let title = `#### [${j + 1}] ${item.doc_title || "Untitled"}`;
184
- if (item.heading) title += ` > ${item.heading}`;
185
- if (item.breadcrumbs) title += ` (${item.breadcrumbs})`;
186
- let body = `Score: ${(item.score || 0).toFixed(4)}\n`;
187
- if (item.defined_symbols && item.defined_symbols.length > 0) {
188
- body += `Defined Symbols: ${item.defined_symbols.join(", ")}\n`;
189
- }
190
- body += `\n${item.snippet || item.full_section_content || ""}`;
191
- return `${title}\n${body}`;
192
- })
193
- .join("\n\n---\n\n");
194
- return header + items;
195
- })
196
- .join("\n\n===\n\n");
197
-
198
- const headerNote = `[Active Model: ${activeConfig.embeddingModel} | Fusion: ${activeConfig.fusionAlgorithm.toUpperCase()} | ${queries.length} queries]\n\n`;
199
-
200
- return { content: [{ type: "text", text: headerNote + formatted }] };
201
- }
202
- );
203
-
204
- server.registerTool(
205
- "reindex_knowledge_base",
206
- {
207
- description:
208
- "Re-embed all existing documents in the RAG knowledge base with the active (or specified) embedding model and vector dimension. " +
209
- "Use after switching the embedding model or vector dimension so previously stored vectors match the new configuration. " +
210
- "Preserves documents, sections, FTS index, graph edges, and fact links.",
211
- inputSchema: z.object({
212
- model: optStr().describe("Embedding model to use (defaults to active config.embeddingModel)"),
213
- dimension: optNum().describe(
214
- "Fixed vector dimension (defaults to active config.vectorDimension; auto-detect if unset)"
215
- ),
216
- }),
217
- },
218
- async ({ model, dimension }) => {
219
- const { reindexEmbeddings } = await import("../ingest/pipeline.js");
220
- const result = await reindexEmbeddings({
221
- model: model || null,
222
- dimension: dimension !== undefined && dimension !== null ? dimension : null,
223
- });
224
- return {
225
- content: [
226
- {
227
- type: "text",
228
- text: JSON.stringify(
229
- {
230
- status: "success",
231
- reindexed: result.reindexed,
232
- documentsAffected: result.documentsAffected,
233
- model: result.model,
234
- dimension: result.dimension || "auto",
235
- },
236
- null,
237
- 2
238
- ),
239
- },
240
- ],
241
- };
242
- }
243
- );
244
-
245
- server.registerTool(
246
- "manage_knowledge_base",
247
- {
248
- description:
249
- "Manage the project-isolated RAG knowledge base: inspect stats, list documents, read full raw document, unlink/delete documents, or export/import complete snapshots.",
250
- inputSchema: z.object({
251
- action: z.enum(["stats", "list", "read_document", "delete", "export_snapshot", "import_snapshot"]).describe("Management action"),
252
- docId: optStr().describe("Document ID, title, or path (required for read_document and delete)"),
253
- snapshotPath: optStr().describe("File path for snapshot export/import"),
254
- scope: z.enum(["all", "project", "global"]).nullish().describe("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"),
255
- directory: optStr().describe("Optional workspace/project directory path to target"),
256
- project: optStr().describe("Alias for directory"),
257
- }),
258
- },
259
- async ({ action, docId, snapshotPath, scope, directory, project }) => {
260
- const { getDatabase } = await import("../db/database.js");
261
- const db = await getDatabase();
262
- const scopeKeys = ["stats", "list", "read_document", "delete"].includes(action)
263
- ? await resolveManageRagScopeKeys(action, scope, { directory, project })
264
- : null;
265
- const placeholders = scopeKeys ? scopeKeys.map(() => "?").join(",") : "";
266
- const visibleDocWhere = scopeKeys
267
- ? `EXISTS (SELECT 1 FROM document_scopes ds WHERE ds.doc_id = d.id AND ds.scope_key IN (${placeholders}))`
268
- : "1=1";
269
-
270
- if (action === "stats") {
271
- const docCountRow = await db.prepare(`SELECT COUNT(*) as cnt FROM documents d WHERE ${visibleDocWhere}`).get(...scopeKeys);
272
- const docCount = docCountRow ? docCountRow.cnt : 0;
273
- 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);
274
- const secCount = secCountRow ? secCountRow.cnt : 0;
275
- 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);
276
- const chunkCount = chunkCountRow ? chunkCountRow.cnt : 0;
277
- const visibleDocIds = await db.prepare(`SELECT d.id FROM documents d WHERE ${visibleDocWhere}`).all(...scopeKeys);
278
- let edgeCount = 0;
279
- if (visibleDocIds.length > 0) {
280
- const docIds = visibleDocIds.map((row) => row.id);
281
- const docPlaceholders = docIds.map(() => "?").join(",");
282
- const ownedRows = await db.prepare(`
283
- SELECT id FROM sections WHERE doc_id IN (${docPlaceholders})
284
- UNION SELECT id FROM medium_chunks WHERE doc_id IN (${docPlaceholders})
285
- UNION SELECT id FROM micro_chunks WHERE doc_id IN (${docPlaceholders})
286
- `).all(...docIds, ...docIds, ...docIds);
287
- const ownedIds = [...docIds, ...ownedRows.map((row) => row.id)];
288
- const edgePlaceholders = ownedIds.map(() => "?").join(",");
289
- 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);
290
- edgeCount = edgeCountRow ? edgeCountRow.cnt : 0;
291
- }
17
+ description:
18
+ "Selectively preserve a reliable, reusable source in the RAG knowledge base; do not ingest everything encountered. " +
19
+ "Accepts local file paths, web URLs, or raw Markdown/text content. " +
20
+ "For type='file' the file is read from disk and indexed with a code-block wrapper. " +
21
+ "For type='url' the page is fetched and its content is indexed (not just the URL). " +
22
+ "Processes document through 3-tier hierarchy chunking (Big/Medium/Small), " +
23
+ "computes dense vectors, and extracts GraphRAG code symbols.",
24
+ inputSchema: z.object({
25
+ content: z.string().describe("Raw text content, file path, or web URL. For type='file' this can be the file path (reads from disk) or the file content directly"),
26
+ type: z.enum(["text", "file", "url"]).nullish().transform((v) => v || "text").describe("Input content type: 'text' (raw content), 'file' (reads from disk, wraps in code block), or 'url' (fetches page content)"),
27
+ title: optStr().describe("Document title"),
28
+ path: optStr().describe("Original document file path"),
29
+ scope: z.enum(["project", "global"]).nullish().transform((v) => v || "project").describe("RAG visibility: current Git project (default) or global"),
30
+ directory: optStr().describe("Optional workspace/project directory path to target"),
31
+ project: optStr().describe("Alias for directory"),
32
+ generateEmbeddings: defBool(true).describe("Compute dense vector embeddings"),
33
+ }),
34
+ },
35
+ async ({ content, type, title, path, scope, directory, project, generateEmbeddings }) => {
36
+ const { ingestDocument } = await import("../ingest/pipeline.js");
37
+ const projectScope = await resolveRagScopeKey(scope, { directory, project });
38
+ const result = await ingestDocument({
39
+ content,
40
+ type,
41
+ title: title || null,
42
+ path: path || null,
43
+ generateEmbeddings,
44
+ projectScope,
45
+ });
46
+ return {
47
+ content: [{
48
+ type: "text",
49
+ text: JSON.stringify({
50
+ status: "success",
51
+ docId: result.docId,
52
+ title: result.title,
53
+ sectionsCount: result.sectionsCount,
54
+ microChunksCount: result.microChunksCount,
55
+ deduplicated: result.deduplicated,
56
+ scope: result.projectScope,
57
+ }, null, 2),
58
+ }],
59
+ };
60
+ }
61
+ );
62
+
63
+ server.registerTool(
64
+ "query_knowledge_base",
65
+ {
66
+ description:
67
+ "Perform project-isolated hybrid search (RSF/RRF BM25 full-text + dense vector similarity) across the RAG knowledge base. " +
68
+ "Returns ranked candidates with stable parent document IDs and source metadata. Use resultMode='index' for a compact semantic table of contents without retrieved bodies.",
69
+ inputSchema: z.object({
70
+ query: z.string().describe("Search query in natural language or symbol name"),
71
+ limit: defNum(5).describe("Maximum number of sections to return"),
72
+ instruction: optStr().describe("Optional task-specific retrieval instruction shaping embedding focus (e.g. 'Retrieve code snippets', 'Find user preferences'). Recommended when using E5/BGE models for domain-specific queries."),
73
+ generateEmbeddings: defBool(true).describe("Use vector search alongside BM25"),
74
+ resultMode: z.enum(["snippet", "index"]).nullish().transform((v) => v || "snippet").describe("Result presentation: snippet (default) or compact metadata-only semantic TOC index"),
75
+ scope: z.enum(["all", "project", "global"]).nullish().transform((v) => v || "all").describe("Search global + current project (default), project only, or global only"),
76
+ directory: optStr().describe("Optional workspace/project directory path to target"),
77
+ project: optStr().describe("Alias for directory"),
78
+ }),
79
+ },
80
+ async (args) => ({
81
+ content: [{ type: "text", text: await runSingleRagQuery(args) }],
82
+ })
83
+ );
84
+
85
+ server.registerTool(
86
+ "batch_query_knowledge_base",
87
+ {
88
+ description:
89
+ "Execute multiple hybrid search queries in a single batch call. " +
90
+ "Search is isolated to global plus the current project unless another scope is requested. " +
91
+ "All query embeddings are computed in one ONNX pass. Use resultMode='index' to return compact candidate metadata without retrieved bodies.",
92
+ inputSchema: z.object({
93
+ queries: z.array(z.string()).describe("Array of search queries to execute in batch"),
94
+ limit: defNum(5).describe("Maximum number of sections to return per query"),
95
+ instruction: optStr().describe("Optional task-specific retrieval instruction shaping embedding focus. Applied to all queries."),
96
+ generateEmbeddings: defBool(true).describe("Use vector search alongside BM25"),
97
+ resultMode: z.enum(["snippet", "index"]).nullish().transform((v) => v || "snippet").describe("Result presentation for every query: snippet (default) or compact metadata-only semantic TOC index"),
98
+ scope: z.enum(["all", "project", "global"]).nullish().transform((v) => v || "all").describe("Search global + current project (default), project only, or global only"),
99
+ directory: optStr().describe("Optional workspace/project directory path to target"),
100
+ project: optStr().describe("Alias for directory"),
101
+ }),
102
+ },
103
+ async (args) => ({
104
+ content: [{ type: "text", text: await runBatchRagQuery(args) }],
105
+ })
106
+ );
107
+
108
+ server.registerTool(
109
+ "reindex_knowledge_base",
110
+ {
111
+ description:
112
+ "Re-embed all existing documents in the RAG knowledge base with the active (or specified) embedding model and vector dimension. " +
113
+ "Use after switching the embedding model or vector dimension so previously stored vectors match the new configuration. " +
114
+ "Preserves documents, sections, FTS index, graph edges, and fact links.",
115
+ inputSchema: z.object({
116
+ model: optStr().describe("Embedding model to use (defaults to active config.embeddingModel)"),
117
+ dimension: optNum().describe("Fixed vector dimension (defaults to active config.vectorDimension; auto-detect if unset)"),
118
+ }),
119
+ },
120
+ async ({ model, dimension }) => {
121
+ const { reindexEmbeddings } = await import("../ingest/pipeline.js");
122
+ const result = await reindexEmbeddings({
123
+ model: model || null,
124
+ dimension: dimension !== undefined && dimension !== null ? dimension : null,
125
+ });
126
+ return {
127
+ content: [{
128
+ type: "text",
129
+ text: JSON.stringify({
130
+ status: "success",
131
+ reindexed: result.reindexed,
132
+ documentsAffected: result.documentsAffected,
133
+ model: result.model,
134
+ dimension: result.dimension || "auto",
135
+ }, null, 2),
136
+ }],
137
+ };
138
+ }
139
+ );
140
+
141
+ server.registerTool(
142
+ "manage_knowledge_base",
143
+ {
144
+ description:
145
+ "Manage the project-isolated RAG knowledge base: inspect stats, list documents/notes with source metadata, read full raw document/note, unlink/delete documents, or export/import complete snapshots.",
146
+ inputSchema: z.object({
147
+ action: z.enum(["stats", "list", "read_document", "delete", "export_snapshot", "import_snapshot"]).describe("Management action"),
148
+ docId: optStr().describe("Document ID, title, or path (required for read_document and delete)"),
149
+ snapshotPath: optStr().describe("File path for snapshot export/import"),
150
+ scope: z.enum(["all", "project", "global"]).nullish().describe("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"),
151
+ directory: optStr().describe("Optional workspace/project directory path to target"),
152
+ project: optStr().describe("Alias for directory"),
153
+ }),
154
+ },
155
+ async ({ action, docId, snapshotPath, scope, directory, project }) => {
156
+ if (action === "read_document") {
292
157
  return {
293
- content: [
294
- {
295
- type: "text",
296
- text: JSON.stringify(
297
- {
298
- documents: docCount,
299
- sections: secCount,
300
- micro_chunks: chunkCount,
301
- graph_edges: edgeCount,
302
- },
303
- null,
304
- 2
305
- ),
306
- },
307
- ],
158
+ content: [{
159
+ type: "text",
160
+ text: JSON.stringify(await readKnowledgeDocument({ docId, scope, directory, project }), null, 2),
161
+ }],
308
162
  };
309
163
  }
310
164
 
311
- if (action === "list") {
312
- const docs = await db.prepare(`
313
- SELECT d.id, d.title, d.path, d.blob_hash, d.created_at,
314
- GROUP_CONCAT(ds.scope_key) AS scopes
315
- FROM documents d
316
- JOIN document_scopes ds ON ds.doc_id = d.id AND ds.scope_key IN (${placeholders})
317
- GROUP BY d.id, d.title, d.path, d.blob_hash, d.created_at
318
- ORDER BY d.created_at DESC
319
- `).all(...scopeKeys);
165
+ if (action === "list") {
320
166
  return {
321
- content: [{ type: "text", text: JSON.stringify(docs, null, 2) }],
167
+ content: [{
168
+ type: "text",
169
+ text: JSON.stringify(await listKnowledgeDocuments({ scope, directory, project }), null, 2),
170
+ }],
322
171
  };
323
172
  }
324
173
 
325
- if (action === "read_document") {
326
- if (!docId) throw new Error("docId parameter is required for read_document action");
327
- const doc = await db
328
- .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}`)
329
- .get(docId, docId, docId, ...scopeKeys);
330
- if (!doc) {
331
- throw new Error(`Document not found in knowledge base for docId: ${docId}`);
174
+ const { getDatabase } = await import("../db/database.js");
175
+ const db = await getDatabase();
176
+ const scopeKeys = ["stats", "delete"].includes(action)
177
+ ? await resolveManageRagScopeKeys(action, scope, { directory, project })
178
+ : null;
179
+ const placeholders = scopeKeys ? scopeKeys.map(() => "?").join(",") : "";
180
+ const visibleDocWhere = scopeKeys
181
+ ? `EXISTS (SELECT 1 FROM document_scopes ds WHERE ds.doc_id = d.id AND ds.scope_key IN (${placeholders}))`
182
+ : "1=1";
183
+
184
+ if (action === "stats") {
185
+ const docCountRow = await db.prepare(`SELECT COUNT(*) as cnt FROM documents d WHERE ${visibleDocWhere}`).get(...scopeKeys);
186
+ const docCount = docCountRow ? docCountRow.cnt : 0;
187
+ 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);
188
+ const secCount = secCountRow ? secCountRow.cnt : 0;
189
+ 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);
190
+ const chunkCount = chunkCountRow ? chunkCountRow.cnt : 0;
191
+ const visibleDocIds = await db.prepare(`SELECT d.id FROM documents d WHERE ${visibleDocWhere}`).all(...scopeKeys);
192
+ let edgeCount = 0;
193
+ if (visibleDocIds.length > 0) {
194
+ const docIds = visibleDocIds.map((row) => row.id);
195
+ const docPlaceholders = docIds.map(() => "?").join(",");
196
+ const ownedRows = await db.prepare(`
197
+ SELECT id FROM sections WHERE doc_id IN (${docPlaceholders})
198
+ UNION SELECT id FROM medium_chunks WHERE doc_id IN (${docPlaceholders})
199
+ UNION SELECT id FROM micro_chunks WHERE doc_id IN (${docPlaceholders})
200
+ `).all(...docIds, ...docIds, ...docIds);
201
+ const ownedIds = [...docIds, ...ownedRows.map((row) => row.id)];
202
+ const edgePlaceholders = ownedIds.map(() => "?").join(",");
203
+ 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);
204
+ edgeCount = edgeCountRow ? edgeCountRow.cnt : 0;
332
205
  }
333
- const { readBlob } = await import("../storage/blob_store.js");
334
- const rawContent = await readBlob(doc.blob_hash);
335
206
  return {
336
- content: [
337
- {
338
- type: "text",
339
- text: JSON.stringify(
340
- {
341
- id: doc.id,
342
- title: doc.title,
343
- path: doc.path,
344
- created_at: doc.created_at,
345
- content: rawContent,
346
- },
347
- null,
348
- 2
349
- ),
350
- },
351
- ],
207
+ content: [{ type: "text", text: JSON.stringify({ documents: docCount, sections: secCount, micro_chunks: chunkCount, graph_edges: edgeCount }, null, 2) }],
352
208
  };
353
209
  }
354
210
 
355
- if (action === "delete") {
356
- if (!docId) throw new Error("docId parameter is required for delete action");
357
- const visible = await db
358
- .prepare(`SELECT d.id FROM documents d WHERE (d.id = ? OR d.path = ? OR d.title = ?) AND ${visibleDocWhere}`)
359
- .get(docId, docId, docId, ...scopeKeys);
360
- if (!visible) throw new Error(`Document not found in the selected RAG scope for docId: ${docId}`);
361
- const scopeRemoval = await removeDocumentScopes(db, visible.id, scopeKeys);
362
- if (scopeRemoval.remainingScopes > 0) {
363
- return {
364
- content: [{
365
- type: "text",
366
- text: JSON.stringify({
367
- deleted: false,
368
- unlinked: true,
369
- docId: visible.id,
370
- removedScopes: scopeRemoval.removedScopes,
371
- remainingScopes: scopeRemoval.remainingScopes,
372
- }, null, 2),
373
- }],
374
- };
375
- }
376
- const { deleteDocument } = await import("../ingest/pipeline.js");
377
- const result = await deleteDocument(visible.id, db);
378
- return {
379
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
380
- };
211
+ if (action === "delete") {
212
+ if (!docId) throw new Error("docId parameter is required for delete action");
213
+ const visible = await db
214
+ .prepare(`SELECT d.id FROM documents d WHERE (d.id = ? OR d.path = ? OR d.title = ?) AND ${visibleDocWhere}`)
215
+ .get(docId, docId, docId, ...scopeKeys);
216
+ if (!visible) throw new Error(`Document not found in the selected RAG scope for docId: ${docId}`);
217
+ const scopeRemoval = await removeDocumentScopes(db, visible.id, scopeKeys);
218
+ if (scopeRemoval.remainingScopes > 0) {
219
+ return {
220
+ content: [{
221
+ type: "text",
222
+ text: JSON.stringify({
223
+ deleted: false,
224
+ unlinked: true,
225
+ docId: visible.id,
226
+ removedScopes: scopeRemoval.removedScopes,
227
+ remainingScopes: scopeRemoval.remainingScopes,
228
+ }, null, 2),
229
+ }],
230
+ };
231
+ }
232
+ const { deleteDocument } = await import("../ingest/pipeline.js");
233
+ const result = await deleteDocument(visible.id, db);
234
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
381
235
  }
382
236
 
383
237
  if (action === "export_snapshot") {
384
238
  const { exportSnapshot } = await import("../admin/snapshot.js");
385
239
  const result = await exportSnapshot({ customDb: db, outputPath: snapshotPath || null });
386
240
  return {
387
- content: [
388
- {
389
- type: "text",
390
- text: snapshotPath ? `Snapshot exported successfully to ${snapshotPath}` : JSON.stringify(result, null, 2),
391
- },
392
- ],
241
+ content: [{ type: "text", text: snapshotPath ? `Snapshot exported successfully to ${snapshotPath}` : JSON.stringify(result, null, 2) }],
393
242
  };
394
243
  }
395
244
 
@@ -397,9 +246,7 @@ export function registerRagTools(server) {
397
246
  if (!snapshotPath) throw new Error("snapshotPath parameter is required for import_snapshot action");
398
247
  const { importSnapshot } = await import("../admin/snapshot.js");
399
248
  const result = await importSnapshot({ customDb: db, snapshotPathOrData: snapshotPath });
400
- return {
401
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
402
- };
249
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
403
250
  }
404
251
 
405
252
  throw new Error(`Unknown action: ${action}`);