@lotargo/memory_plugin 1.2.901 → 1.2.902
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/mcp-server/index.js
CHANGED
|
@@ -32,6 +32,18 @@ const server = new McpServer({
|
|
|
32
32
|
version: "1.0.0",
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
+
// Optional string/number that tolerates null (some tool-call layers fill omitted
|
|
36
|
+
// optional args with null). Linking fields must NEVER be mandatory.
|
|
37
|
+
const optStr = () => z.string().optional().nullable();
|
|
38
|
+
const optNum = () => z.number().optional().nullable();
|
|
39
|
+
const defStr = (fallback) =>
|
|
40
|
+
z
|
|
41
|
+
.string()
|
|
42
|
+
.nullish()
|
|
43
|
+
.transform((v) => (v === null || v === undefined || v === "" ? fallback : v));
|
|
44
|
+
const defBool = (fallback) => z.boolean().nullish().transform((v) => (v === null || v === undefined ? fallback : v));
|
|
45
|
+
const defNum = (fallback) => z.number().nullish().transform((v) => (v === null || v === undefined ? fallback : v));
|
|
46
|
+
|
|
35
47
|
// --- Legacy Key-Value Memory Tools ---
|
|
36
48
|
|
|
37
49
|
// --- Legacy Key-Value Memory Tools & Agent Graph Linking ---
|
|
@@ -42,16 +54,17 @@ server.registerTool(
|
|
|
42
54
|
description:
|
|
43
55
|
"Save an important, durable fact to memory. Only use for high-signal information " +
|
|
44
56
|
"(name, goals, constraints, tech preferences, project conventions). " +
|
|
45
|
-
"
|
|
57
|
+
"docId/startLine/endLine/relationType are OPTIONAL and only used to link the fact to a " +
|
|
58
|
+
"Knowledge Base document or line range; omit them when no linking is needed. " +
|
|
46
59
|
"Translate the fact into English and keep it concise. " +
|
|
47
60
|
"scope: 'project' (default) or 'global'",
|
|
48
61
|
inputSchema: z.object({
|
|
49
62
|
fact: z.string().describe("The fact to remember, written in English"),
|
|
50
|
-
scope:
|
|
51
|
-
docId:
|
|
52
|
-
startLine:
|
|
53
|
-
endLine:
|
|
54
|
-
relationType:
|
|
63
|
+
scope: defStr("project").describe("'project' (default) or 'global'"),
|
|
64
|
+
docId: optStr().describe("Optional document ID, title, or path to link this fact to"),
|
|
65
|
+
startLine: optNum().describe("Optional starting line number in target document"),
|
|
66
|
+
endLine: optNum().describe("Optional ending line number in target document"),
|
|
67
|
+
relationType: defStr("LINKS_TO").describe("Relation type (e.g. 'RULES_FOR', 'IMPLEMENTS', 'REFERENCES')"),
|
|
55
68
|
}),
|
|
56
69
|
},
|
|
57
70
|
async ({ fact, scope, docId, startLine, endLine, relationType }) => {
|
|
@@ -97,8 +110,8 @@ server.registerTool(
|
|
|
97
110
|
"scope: 'project', 'global', 'all' (default), or 'list_projects'. " +
|
|
98
111
|
"Use project: '<directory path>' with scope 'project'/'all' to read facts of a specific project from any working directory.",
|
|
99
112
|
inputSchema: z.object({
|
|
100
|
-
scope:
|
|
101
|
-
project:
|
|
113
|
+
scope: defStr("all").describe("'project', 'global', 'all', or 'list_projects'"),
|
|
114
|
+
project: optStr().describe("Directory path of the project to read facts from (e.g. 'F:/projects/plugins/memory')"),
|
|
102
115
|
}),
|
|
103
116
|
},
|
|
104
117
|
async ({ scope, project }) => {
|
|
@@ -169,7 +182,7 @@ server.registerTool(
|
|
|
169
182
|
"Delete a fact by number (from recall), by range (e.g. '3-30', inclusive), or by text search",
|
|
170
183
|
inputSchema: z.object({
|
|
171
184
|
query: z.string().describe("Number, range like '3-30', or text to search for"),
|
|
172
|
-
scope:
|
|
185
|
+
scope: defStr("project").describe("'project' (default) or 'global'"),
|
|
173
186
|
}),
|
|
174
187
|
},
|
|
175
188
|
async ({ query, scope }) => {
|
|
@@ -207,13 +220,13 @@ server.registerTool(
|
|
|
207
220
|
"Explicitly link a Notebook memory fact to a Knowledge Base document, section, or line range. " +
|
|
208
221
|
"Creates Agent-driven Graph Edges connecting memory to RAG documents.",
|
|
209
222
|
inputSchema: z.object({
|
|
210
|
-
action: z.enum(["link", "list_links", "get_doc_links"]).
|
|
211
|
-
factText:
|
|
212
|
-
docId:
|
|
213
|
-
scope:
|
|
214
|
-
startLine:
|
|
215
|
-
endLine:
|
|
216
|
-
relationType:
|
|
223
|
+
action: z.enum(["link", "list_links", "get_doc_links"]).nullish().transform((v) => v || "link").describe("Action type"),
|
|
224
|
+
factText: optStr().describe("Memory fact text or keyword"),
|
|
225
|
+
docId: optStr().describe("Document ID, title, or file path"),
|
|
226
|
+
scope: defStr("project").describe("'project' (default) or 'global'"),
|
|
227
|
+
startLine: optNum().describe("Starting line number in target document"),
|
|
228
|
+
endLine: optNum().describe("Ending line number in target document"),
|
|
229
|
+
relationType: defStr("LINKS_TO").describe("Relation type (e.g. 'RULES_FOR', 'IMPLEMENTS', 'EXPLAINS')"),
|
|
217
230
|
}),
|
|
218
231
|
},
|
|
219
232
|
async ({ action, factText, docId, scope, startLine, endLine, relationType }) => {
|
|
@@ -264,14 +277,15 @@ server.registerTool(
|
|
|
264
277
|
description:
|
|
265
278
|
"Ingest a document into the RAG knowledge base. " +
|
|
266
279
|
"Accepts local file paths, web URLs, or raw Markdown/text content. " +
|
|
280
|
+
"For type='url' the page is fetched and its content is indexed (not just the URL). " +
|
|
267
281
|
"Processes document through 3-tier hierarchy chunking (Big/Medium/Small), " +
|
|
268
282
|
"computes dense vectors, and extracts GraphRAG code symbols.",
|
|
269
283
|
inputSchema: z.object({
|
|
270
284
|
content: z.string().describe("Raw text content, file path, or web URL"),
|
|
271
|
-
type: z.enum(["text", "file", "url"]).
|
|
272
|
-
title:
|
|
273
|
-
path:
|
|
274
|
-
generateEmbeddings:
|
|
285
|
+
type: z.enum(["text", "file", "url"]).nullish().transform((v) => v || "text").describe("Input content type: 'text', 'file', or 'url' (url fetches the page content)"),
|
|
286
|
+
title: optStr().describe("Document title"),
|
|
287
|
+
path: optStr().describe("Original document file path"),
|
|
288
|
+
generateEmbeddings: defBool(true).describe("Compute dense vector embeddings"),
|
|
275
289
|
}),
|
|
276
290
|
},
|
|
277
291
|
async ({ content, type, title, path, generateEmbeddings }) => {
|
|
@@ -313,15 +327,12 @@ server.registerTool(
|
|
|
313
327
|
"Returns top-ranked candidate document sections with breadcrumbs, GraphRAG defined code symbols, and relevance scores.",
|
|
314
328
|
inputSchema: z.object({
|
|
315
329
|
query: z.string().describe("Search query in natural language or symbol name"),
|
|
316
|
-
limit:
|
|
317
|
-
instruction:
|
|
318
|
-
.
|
|
319
|
-
.
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
"Recommended when using E5/BGE models for domain-specific queries."
|
|
323
|
-
),
|
|
324
|
-
generateEmbeddings: z.boolean().default(true).describe("Use vector search alongside BM25"),
|
|
330
|
+
limit: defNum(5).describe("Maximum number of sections to return"),
|
|
331
|
+
instruction: optStr().describe(
|
|
332
|
+
"Optional task-specific retrieval instruction shaping embedding focus (e.g. 'Retrieve code snippets', 'Find user preferences'). " +
|
|
333
|
+
"Recommended when using E5/BGE models for domain-specific queries."
|
|
334
|
+
),
|
|
335
|
+
generateEmbeddings: defBool(true).describe("Use vector search alongside BM25"),
|
|
325
336
|
}),
|
|
326
337
|
},
|
|
327
338
|
async ({ query, limit, instruction, generateEmbeddings }) => {
|
|
@@ -374,8 +385,8 @@ server.registerTool(
|
|
|
374
385
|
"Manage the RAG knowledge base: inspect stats, list documents, read full raw document, delete documents, or export/import snapshots.",
|
|
375
386
|
inputSchema: z.object({
|
|
376
387
|
action: z.enum(["stats", "list", "read_document", "delete", "export_snapshot", "import_snapshot"]).describe("Management action"),
|
|
377
|
-
docId:
|
|
378
|
-
snapshotPath:
|
|
388
|
+
docId: optStr().describe("Document ID, title, or path (required for read_document and delete)"),
|
|
389
|
+
snapshotPath: optStr().describe("File path for snapshot export/import"),
|
|
379
390
|
}),
|
|
380
391
|
},
|
|
381
392
|
async ({ action, docId, snapshotPath }) => {
|
|
@@ -27,6 +27,48 @@ export function cleanHtml(html) {
|
|
|
27
27
|
return cleaned;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// Fetch a web page and convert it to Markdown/text. Used by the 'url' ingestion type
|
|
31
|
+
// so the RAG store gets the page CONTENT, not just the URL string.
|
|
32
|
+
export async function fetchUrlContent(url) {
|
|
33
|
+
if (typeof url !== "string" || !/^https?:\/\//i.test(url.trim())) {
|
|
34
|
+
throw new Error(`Unsupported URL for ingestion: '${url}'. Only http/https URLs are supported.`);
|
|
35
|
+
}
|
|
36
|
+
let res;
|
|
37
|
+
try {
|
|
38
|
+
res = await fetch(url.trim(), {
|
|
39
|
+
headers: {
|
|
40
|
+
"User-Agent": "memory-agent-rag/1.0",
|
|
41
|
+
Accept: "text/html,application/xhtml+xml,application/json,text/plain,*/*",
|
|
42
|
+
},
|
|
43
|
+
redirect: "follow",
|
|
44
|
+
signal: AbortSignal.timeout(15000),
|
|
45
|
+
});
|
|
46
|
+
} catch (err) {
|
|
47
|
+
throw new Error(`Failed to fetch URL '${url}': ${err.message}`);
|
|
48
|
+
}
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
throw new Error(`Failed to fetch URL '${url}': HTTP ${res.status} ${res.statusText}`);
|
|
51
|
+
}
|
|
52
|
+
const raw = await res.text();
|
|
53
|
+
const contentType = (res.headers.get("content-type") || "").toLowerCase();
|
|
54
|
+
const looksLikeHtml = /<html|<body|<div|<article|<main|<!doctype/i.test(raw.slice(0, 4096));
|
|
55
|
+
let markdown;
|
|
56
|
+
if (contentType.includes("html") || looksLikeHtml) {
|
|
57
|
+
markdown = cleanHtml(raw);
|
|
58
|
+
} else if (contentType.includes("json") || /^[\[{]/.test(raw.trim())) {
|
|
59
|
+
try {
|
|
60
|
+
markdown = JSON.stringify(JSON.parse(raw), null, 2);
|
|
61
|
+
} catch {
|
|
62
|
+
markdown = raw.trim();
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
markdown = raw.trim();
|
|
66
|
+
}
|
|
67
|
+
const titleMatch = raw.match(/<title[^>]*>([\s\S]*?)<\/title>/i);
|
|
68
|
+
const title = titleMatch ? titleMatch[1].replace(/\s+/g, " ").trim() : null;
|
|
69
|
+
return { markdown, title: title || null, finalUrl: res.url || url.trim() };
|
|
70
|
+
}
|
|
71
|
+
|
|
30
72
|
export function extractTitle(markdown, fallbackName = "Untitled Document") {
|
|
31
73
|
const h1Match = markdown.match(/^#\s+(.+)$/m);
|
|
32
74
|
if (h1Match && h1Match[1].trim()) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { getDatabase, BLOBS_DIR } from "../db/database.js";
|
|
3
3
|
import { saveBlob, deleteBlob } from "../storage/blob_store.js";
|
|
4
|
-
import { normalizeContent } from "./normalizer.js";
|
|
4
|
+
import { normalizeContent, fetchUrlContent } from "./normalizer.js";
|
|
5
5
|
import { buildTripleHierarchy } from "./chunker.js";
|
|
6
6
|
import { embedText, embedBatch, vectorToBuffer } from "../ml/model_manager.js";
|
|
7
7
|
import { buildGraphEdges, saveGraphEdges } from "../graph/graph_extractor.js";
|
|
@@ -18,13 +18,26 @@ export async function ingestDocument({
|
|
|
18
18
|
}) {
|
|
19
19
|
const db = customDb || getDatabase();
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
let effectiveType = type;
|
|
22
|
+
let effectivePath = path;
|
|
23
|
+
let effectiveTitle = title;
|
|
24
|
+
|
|
25
|
+
if (type === "url") {
|
|
26
|
+
const fetched = await fetchUrlContent(String(content));
|
|
27
|
+
content = fetched.markdown;
|
|
28
|
+
effectiveType = "text";
|
|
29
|
+
effectiveTitle = title || fetched.title;
|
|
30
|
+
effectivePath = path || fetched.finalUrl || content;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { markdown, title: docTitle, metadata } = normalizeContent({ content, type: effectiveType, path: effectivePath, title: effectiveTitle });
|
|
34
|
+
if (type === "url") metadata.source_type = "url";
|
|
22
35
|
|
|
23
36
|
const blobRes = await saveBlob(markdown, customBlobDir);
|
|
24
37
|
const blobHash = blobRes.hash;
|
|
25
38
|
|
|
26
39
|
const docId = `doc_${randomUUID().replace(/-/g, "").substring(0, 12)}`;
|
|
27
|
-
const docPath =
|
|
40
|
+
const docPath = effectivePath || `virtual://${type}/${docId}`;
|
|
28
41
|
const now = Date.now();
|
|
29
42
|
|
|
30
43
|
const hierarchy = buildTripleHierarchy(markdown, docId, docTitle);
|
|
@@ -151,6 +164,14 @@ export async function deleteDocument(docIdOrPath, customDb = null, customBlobDir
|
|
|
151
164
|
|
|
152
165
|
const microChunks = db.prepare("SELECT id FROM micro_chunks WHERE doc_id = ?").all(doc.id);
|
|
153
166
|
|
|
167
|
+
// Collect every id owned by this document so we can purge dangling graph edges
|
|
168
|
+
// (graph_edges has no FK constraints, so section/chunk/doc references would otherwise leak).
|
|
169
|
+
const ownedIds = [doc.id];
|
|
170
|
+
for (const table of ["sections", "medium_chunks", "micro_chunks"]) {
|
|
171
|
+
const rows = db.prepare(`SELECT id FROM ${table} WHERE doc_id = ?`).all(doc.id);
|
|
172
|
+
for (const r of rows) ownedIds.push(r.id);
|
|
173
|
+
}
|
|
174
|
+
|
|
154
175
|
db.exec("BEGIN IMMEDIATE;");
|
|
155
176
|
try {
|
|
156
177
|
for (const mc of microChunks) {
|
|
@@ -159,7 +180,16 @@ export async function deleteDocument(docIdOrPath, customDb = null, customBlobDir
|
|
|
159
180
|
} catch {}
|
|
160
181
|
}
|
|
161
182
|
|
|
162
|
-
|
|
183
|
+
// Auto-clean Agent knowledge graph links pointing at this document.
|
|
184
|
+
db.prepare("DELETE FROM knowledge_links WHERE doc_id = ?").run(doc.id);
|
|
185
|
+
|
|
186
|
+
for (const id of ownedIds) {
|
|
187
|
+
// GLOB: '*' suffix is exact (unlike LIKE, '_' stays literal in ids like doc_xxx).
|
|
188
|
+
db.prepare(
|
|
189
|
+
"DELETE FROM graph_edges WHERE source_id = ? OR target_id = ? OR source_id GLOB ? OR target_id GLOB ?"
|
|
190
|
+
).run(id, id, `${id}*`, `${id}*`);
|
|
191
|
+
}
|
|
192
|
+
|
|
163
193
|
db.prepare("DELETE FROM documents WHERE id = ?").run(doc.id);
|
|
164
194
|
|
|
165
195
|
db.exec("COMMIT;");
|
|
@@ -175,5 +205,5 @@ export async function deleteDocument(docIdOrPath, customDb = null, customBlobDir
|
|
|
175
205
|
}
|
|
176
206
|
}
|
|
177
207
|
|
|
178
|
-
return { deleted: true, docId: doc.id, title: doc.title };
|
|
208
|
+
return { deleted: true, docId: doc.id, title: doc.title, linksCleaned: true };
|
|
179
209
|
}
|
package/opencode-plugin/index.js
CHANGED
|
@@ -240,7 +240,7 @@ const MCP_SERVERS = [
|
|
|
240
240
|
|
|
241
241
|
export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
242
242
|
await ensureDir();
|
|
243
|
-
const
|
|
243
|
+
const activeProjectKey = scopeKey("project", worktree, directory);
|
|
244
244
|
|
|
245
245
|
return {
|
|
246
246
|
"experimental.chat.messages.transform": async (_input, output) => {
|
|
@@ -252,10 +252,10 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
252
252
|
|
|
253
253
|
const [globalFacts, projectFacts] = await Promise.all([
|
|
254
254
|
readMemoryRaw(GLOBAL_KEY),
|
|
255
|
-
readMemoryRaw(
|
|
255
|
+
readMemoryRaw(activeProjectKey),
|
|
256
256
|
]);
|
|
257
257
|
|
|
258
|
-
const context = buildMemoryContext(globalFacts, projectFacts,
|
|
258
|
+
const context = buildMemoryContext(globalFacts, projectFacts, activeProjectKey);
|
|
259
259
|
const ref = firstUser.parts[0];
|
|
260
260
|
firstUser.parts.unshift({ ...ref, type: "text", text: context });
|
|
261
261
|
},
|
|
@@ -288,7 +288,8 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
288
288
|
description:
|
|
289
289
|
"Save an important, durable fact to memory. Only use for high-signal information " +
|
|
290
290
|
"(name, goals, constraints, tech preferences, project conventions). " +
|
|
291
|
-
"
|
|
291
|
+
"docId/startLine/endLine/relationType are OPTIONAL and only used to link the fact to a " +
|
|
292
|
+
"Knowledge Base document or line range; omit them when no linking is needed. " +
|
|
292
293
|
"Translate the fact into English and keep it concise. " +
|
|
293
294
|
"scope: 'project' (default) or 'global'",
|
|
294
295
|
args: {
|
|
@@ -511,11 +512,12 @@ export const MemoryPlugin = async ({ directory, worktree, client }) => {
|
|
|
511
512
|
description:
|
|
512
513
|
"Ingest a document into the RAG knowledge base. " +
|
|
513
514
|
"Accepts local file paths, web URLs, or raw Markdown/text content. " +
|
|
515
|
+
"For type='url' the page is fetched and its content is indexed (not just the URL). " +
|
|
514
516
|
"Processes document through 3-tier hierarchy chunking (Big/Medium/Small), " +
|
|
515
517
|
"computes dense vectors, and extracts GraphRAG code symbols.",
|
|
516
518
|
args: {
|
|
517
519
|
content: { type: "string", description: "Raw text content, file path, or web URL" },
|
|
518
|
-
type: { type: "string", description: "Input content type: 'text', 'file', 'url'", default: "text" },
|
|
520
|
+
type: { type: "string", description: "Input content type: 'text', 'file', 'url' (url fetches the page content)", default: "text" },
|
|
519
521
|
title: { type: "string", description: "Document title" },
|
|
520
522
|
path: { type: "string", description: "Original document file path" },
|
|
521
523
|
generateEmbeddings: { type: "boolean", description: "Compute dense vector embeddings", default: true },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotargo/memory_plugin",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.902",
|
|
4
4
|
"description": "Persistent memory agent for coding AI tools — remembers user preferences and project context across sessions. Works with Antigravity, OpenCode, Claude Code, and Codex.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "opencode-plugin/index.js",
|
|
@@ -59,11 +59,12 @@ Use this tool when adding technical documentation, API specs, architectural docu
|
|
|
59
59
|
- **Hierarchy Chunking**: The engine automatically creates 3-tier chunks (Big Document -> Medium Section -> Small Micro-Chunk) and extracts GraphRAG code symbols.
|
|
60
60
|
- **Auto Vector Embeddings**: Dense ONNX vectors (`multilingual-e5-small`) are automatically computed and indexed in SQLite.
|
|
61
61
|
- **CRITICAL Schema Usage & Parameters**:
|
|
62
|
-
- `content` (required, string):
|
|
63
|
-
- `type` (optional, enum: `"text"`, `"file"`, `"url"`):
|
|
64
|
-
- `path` (optional, string): Provide the absolute file path (e.g. `f:\projects\plugins\memory\README.md`).
|
|
65
|
-
- `title` (optional, string): Provide document title (e.g. `README.md`).
|
|
66
|
-
- **Correct Example**: `ingest_document(content: "
|
|
62
|
+
- `content` (required, string): For `type: "text"`/`"file"` it must be the **actual raw text or markdown content** of the document, NOT just a file path! For `type: "url"` it must be the **page URL** — the page is fetched automatically and its content is indexed (not just the URL).
|
|
63
|
+
- `type` (optional, enum: `"text"`, `"file"`, `"url"`): `"text"` (default), `"file"`, or `"url"` (fetches the web page and indexes its content).
|
|
64
|
+
- `path` (optional, string): Provide the absolute file path (e.g. `f:\projects\plugins\memory\README.md`). For URLs the final URL is used for deduplication.
|
|
65
|
+
- `title` (optional, string): Provide document title (e.g. `README.md`). If omitted for a URL, the page `<title>` is used.
|
|
66
|
+
- **Correct Example (URL)**: `ingest_document(content: "https://docs.example.com/guide", type: "url", title: "Example Guide")`
|
|
67
|
+
- **Correct Example (text)**: `ingest_document(content: "<full text content>", path: "f:/path/to/file.md", title: "file.md", type: "file")`
|
|
67
68
|
- ❌ **Common Error**: `ingest_document(content: "f:/path/to/file.md")` — this causes validation failures because `content` is missing the text content.
|
|
68
69
|
|
|
69
70
|
- **CLI/Script Execution Note**: When writing batch node scripts to call `ingestDocument`, remember that `@lotargo/memory_plugin` uses ES Modules (`"type": "module"`). Use `import` syntax instead of `require()`.
|