@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.
@@ -15,247 +15,252 @@ export function registerRagTools(server) {
15
15
  {
16
16
  description:
17
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"),
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
35
  path: optStr().describe("Original document file path"),
36
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"),
37
39
  generateEmbeddings: defBool(true).describe("Compute dense vector embeddings"),
38
40
  }),
39
41
  },
40
- async ({ content, type, title, path, scope, generateEmbeddings }) => {
42
+ async ({ content, type, title, path, scope, directory, project, generateEmbeddings }) => {
41
43
  const { ingestDocument } = await import("../ingest/pipeline.js");
42
- const projectScope = await resolveRagScopeKey(scope);
44
+ const projectScope = await resolveRagScopeKey(scope, { directory, project });
43
45
  const result = await ingestDocument({
44
- content,
45
- type,
46
- title: title || null,
47
- path: path || null,
46
+ content,
47
+ type,
48
+ title: title || null,
49
+ path: path || null,
48
50
  generateEmbeddings,
49
51
  projectScope,
50
- });
51
- return {
52
- content: [
53
- {
54
- type: "text",
55
- text: JSON.stringify(
56
- {
57
- status: "success",
58
- docId: result.docId,
59
- title: result.title,
60
- sectionsCount: result.sectionsCount,
61
- microChunksCount: result.microChunksCount,
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,
62
64
  deduplicated: result.deduplicated,
63
65
  scope: result.projectScope,
64
- },
65
- null,
66
- 2
67
- ),
68
- },
69
- ],
70
- };
71
- }
72
- );
73
-
74
- server.registerTool(
75
- "query_knowledge_base",
76
- {
66
+ },
67
+ null,
68
+ 2
69
+ ),
70
+ },
71
+ ],
72
+ };
73
+ }
74
+ );
75
+
76
+ server.registerTool(
77
+ "query_knowledge_base",
78
+ {
77
79
  description:
78
80
  "Perform project-isolated hybrid search (RSF/RRF BM25 full-text + dense vector similarity) across the RAG knowledge base. " +
79
- "Returns top-ranked candidate document sections with breadcrumbs, GraphRAG defined code symbols, and relevance scores.",
80
- inputSchema: z.object({
81
- query: z.string().describe("Search query in natural language or symbol name"),
82
- limit: defNum(5).describe("Maximum number of sections to return"),
83
- instruction: optStr().describe(
84
- "Optional task-specific retrieval instruction shaping embedding focus (e.g. 'Retrieve code snippets', 'Find user preferences'). " +
85
- "Recommended when using E5/BGE models for domain-specific queries."
86
- ),
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
+ ),
87
89
  generateEmbeddings: defBool(true).describe("Use vector search alongside BM25"),
88
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"),
89
93
  }),
90
94
  },
91
- async ({ query, limit, instruction, generateEmbeddings, scope }) => {
92
- const { hybridQuery } = await import("../retrieval/retriever.js");
93
- const { getConfig } = await import("../config/config_manager.js");
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");
94
98
  const activeConfig = getConfig();
95
- const scopeKeys = await resolveRagScopeKeys(scope);
96
-
97
- const results = await hybridQuery({
98
- query,
99
- limit,
100
- generateEmbeddings,
99
+ const scopeKeys = await resolveRagScopeKeys(scope, { directory, project });
100
+
101
+ const results = await hybridQuery({
102
+ query,
103
+ limit,
104
+ generateEmbeddings,
101
105
  instruction: instruction || null,
102
106
  scopeKeys,
103
- });
104
-
105
- if (!results || results.length === 0) {
106
- return {
107
- content: [
108
- {
109
- type: "text",
110
- text: `[Active Model: ${activeConfig.embeddingModel}]\nNo matching knowledge found for query.`,
111
- },
112
- ],
113
- };
114
- }
115
-
116
- const headerNote = `[Active Model: ${activeConfig.embeddingModel} | Fusion: ${activeConfig.fusionAlgorithm.toUpperCase()}]\n\n`;
117
-
118
- const formatted = results
119
- .map((r, i) => {
120
- let header = `### [${i + 1}] ${r.doc_title || "Untitled"}`;
121
- if (r.heading) header += ` > ${r.heading}`;
122
- if (r.breadcrumbs) header += ` (${r.breadcrumbs})`;
123
- let body = `Score: ${(r.score || 0).toFixed(4)}\n`;
124
- if (r.defined_symbols && r.defined_symbols.length > 0) {
125
- body += `Defined Symbols: ${r.defined_symbols.join(", ")}\n`;
126
- }
127
- body += `\n${r.snippet || r.full_section_content || ""}`;
128
- return `${header}\n${body}`;
129
- })
130
- .join("\n\n---\n\n");
131
-
132
- return { content: [{ type: "text", text: headerNote + formatted }] };
133
- }
134
- );
135
-
136
- server.registerTool(
137
- "batch_query_knowledge_base",
138
- {
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
+ {
139
143
  description:
140
144
  "Execute multiple hybrid search queries in a single batch call. " +
141
145
  "Search is isolated to global plus the current project unless another scope is requested. " +
142
- "More efficient than separate query_knowledge_base calls: all query embeddings computed in one ONNX pass. " +
143
- "Returns one result set per query, in the same order as input.",
144
- inputSchema: z.object({
145
- queries: z
146
- .array(z.string())
147
- .describe("Array of search queries to execute in batch"),
148
- limit: defNum(5).describe("Maximum number of sections to return per query"),
149
- instruction: optStr().describe(
150
- "Optional task-specific retrieval instruction shaping embedding focus. Applied to all queries."
151
- ),
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
+ ),
152
156
  generateEmbeddings: defBool(true).describe("Use vector search alongside BM25"),
153
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"),
154
160
  }),
155
161
  },
156
- async ({ queries, limit, instruction, generateEmbeddings, scope }) => {
157
- const { batchHybridQuery } = await import("../retrieval/retriever.js");
158
- const { getConfig } = await import("../config/config_manager.js");
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");
159
165
  const activeConfig = getConfig();
160
- const scopeKeys = await resolveRagScopeKeys(scope);
161
-
162
- const allResults = await batchHybridQuery(queries, {
163
- limit,
164
- generateEmbeddings,
166
+ const scopeKeys = await resolveRagScopeKeys(scope, { directory, project });
167
+
168
+ const allResults = await batchHybridQuery(queries, {
169
+ limit,
170
+ generateEmbeddings,
165
171
  instruction: instruction || null,
166
172
  scopeKeys,
167
- });
168
-
169
- const formatted = allResults
170
- .map((results, qi) => {
171
- const header = `## Query ${qi + 1}: "${queries[qi]}"\n`;
172
- if (!results || results.length === 0) {
173
- return header + "_No results found._";
174
- }
175
- const items = results
176
- .map((r, i) => {
177
- let h = `### [${i + 1}] ${r.doc_title || "Untitled"}`;
178
- if (r.heading) h += ` > ${r.heading}`;
179
- if (r.breadcrumbs) h += ` (${r.breadcrumbs})`;
180
- let body = `Score: ${(r.score || 0).toFixed(4)}`;
181
- if (r.retrieval_policy && r.retrieval_policy !== "micro_chunk") {
182
- body += ` [${r.retrieval_policy}]`;
183
- }
184
- if (r.defined_symbols && r.defined_symbols.length > 0) {
185
- body += `\nDefined Symbols: ${r.defined_symbols.join(", ")}`;
186
- }
187
- body += `\n\n${r.snippet || r.full_section_content || ""}`;
188
- return `${h}\n${body}`;
189
- })
190
- .join("\n\n---\n\n");
191
- return header + items;
192
- })
193
- .join("\n\n===\n\n");
194
-
195
- const headerNote = `[Active Model: ${activeConfig.embeddingModel} | Fusion: ${activeConfig.fusionAlgorithm.toUpperCase()} | ${queries.length} queries]\n\n`;
196
-
197
- return { content: [{ type: "text", text: headerNote + formatted }] };
198
- }
199
- );
200
-
201
- server.registerTool(
202
- "reindex_knowledge_base",
203
- {
204
- description:
205
- "Re-embed all existing documents in the RAG knowledge base with the active (or specified) embedding model and vector dimension. " +
206
- "Use after switching the embedding model or vector dimension so previously stored vectors match the new configuration. " +
207
- "Preserves documents, sections, FTS index, graph edges, and fact links.",
208
- inputSchema: z.object({
209
- model: optStr().describe("Embedding model to use (defaults to active config.embeddingModel)"),
210
- dimension: optNum().describe(
211
- "Fixed vector dimension (defaults to active config.vectorDimension; auto-detect if unset)"
212
- ),
213
- }),
214
- },
215
- async ({ model, dimension }) => {
216
- const { reindexEmbeddings } = await import("../ingest/pipeline.js");
217
- const result = await reindexEmbeddings({
218
- model: model || null,
219
- dimension: dimension !== undefined && dimension !== null ? dimension : null,
220
- });
221
- return {
222
- content: [
223
- {
224
- type: "text",
225
- text: JSON.stringify(
226
- {
227
- status: "success",
228
- reindexed: result.reindexed,
229
- documentsAffected: result.documentsAffected,
230
- model: result.model,
231
- dimension: result.dimension || "auto",
232
- },
233
- null,
234
- 2
235
- ),
236
- },
237
- ],
238
- };
239
- }
240
- );
241
-
242
- server.registerTool(
243
- "manage_knowledge_base",
244
- {
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
+ {
245
248
  description:
246
249
  "Manage the project-isolated RAG knowledge base: inspect stats, list documents, read full raw document, unlink/delete documents, or export/import complete snapshots.",
247
- inputSchema: z.object({
248
- action: z.enum(["stats", "list", "read_document", "delete", "export_snapshot", "import_snapshot"]).describe("Management action"),
249
- docId: optStr().describe("Document ID, title, or path (required for read_document and delete)"),
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)"),
250
253
  snapshotPath: optStr().describe("File path for snapshot export/import"),
251
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"),
252
257
  }),
253
258
  },
254
- async ({ action, docId, snapshotPath, scope }) => {
259
+ async ({ action, docId, snapshotPath, scope, directory, project }) => {
255
260
  const { getDatabase } = await import("../db/database.js");
256
261
  const db = await getDatabase();
257
262
  const scopeKeys = ["stats", "list", "read_document", "delete"].includes(action)
258
- ? await resolveManageRagScopeKeys(action, scope)
263
+ ? await resolveManageRagScopeKeys(action, scope, { directory, project })
259
264
  : null;
260
265
  const placeholders = scopeKeys ? scopeKeys.map(() => "?").join(",") : "";
261
266
  const visibleDocWhere = scopeKeys