@lotargo/memory_plugin 1.2.902 → 1.3.1
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/cli.js +64 -13
- package/mcp-server/config/config_manager.js +3 -3
- package/mcp-server/fact_format.js +177 -0
- package/mcp-server/index.js +218 -41
- package/mcp-server/ingest/pipeline.js +8 -0
- package/mcp-server/memory.js +203 -188
- package/mcp-server/ml/model_manager.js +2 -2
- package/opencode-plugin/index.js +200 -47
- package/package.json +2 -1
- package/skills/using-memory/SKILL.md +71 -11
package/mcp-server/index.js
CHANGED
|
@@ -2,7 +2,34 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import * as z from "zod/v4";
|
|
5
|
-
import { ensureDir, readMemory, readMemoryRaw, writeMemory, today, MEMORY_DIR, GLOBAL_KEY, scopeKey, projectKey, projectName, canonicalPath, listProjectStores } from "./memory.js";
|
|
5
|
+
import { ensureDir, readMemory, readMemoryRaw, writeMemory, today, MEMORY_DIR, GLOBAL_KEY, scopeKey, projectKey, projectName, canonicalPath, listProjectStores, storeFilePath } from "./memory.js";
|
|
6
|
+
import {
|
|
7
|
+
parseFactEntry,
|
|
8
|
+
factText,
|
|
9
|
+
factMeta,
|
|
10
|
+
withMeta,
|
|
11
|
+
nextFactId,
|
|
12
|
+
isKeepFact,
|
|
13
|
+
displayFact,
|
|
14
|
+
formatFactEntry,
|
|
15
|
+
matchesQuery,
|
|
16
|
+
matchesTags,
|
|
17
|
+
inDateRange,
|
|
18
|
+
} from "./fact_format.js";
|
|
19
|
+
import { readFile } from "node:fs/promises";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
|
|
22
|
+
// Resolve a fact reference (1-based number, metadata id, or text) to an index.
|
|
23
|
+
function resolveFactIndex(entries, ref) {
|
|
24
|
+
const trimmed = String(ref || "").trim();
|
|
25
|
+
if (!trimmed) return -1;
|
|
26
|
+
const num = parseInt(trimmed, 10);
|
|
27
|
+
if (/^\d+$/.test(trimmed) && num >= 1 && num <= entries.length) return num - 1;
|
|
28
|
+
const idIdx = entries.findIndex((e) => factMeta(e).id === trimmed);
|
|
29
|
+
if (idIdx !== -1) return idIdx;
|
|
30
|
+
const textIdx = entries.findIndex((e) => factText(e).toLowerCase().includes(trimmed.toLowerCase()));
|
|
31
|
+
return textIdx;
|
|
32
|
+
}
|
|
6
33
|
|
|
7
34
|
const cliArgs = process.argv.slice(2);
|
|
8
35
|
|
|
@@ -56,6 +83,11 @@ server.registerTool(
|
|
|
56
83
|
"(name, goals, constraints, tech preferences, project conventions). " +
|
|
57
84
|
"docId/startLine/endLine/relationType are OPTIONAL and only used to link the fact to a " +
|
|
58
85
|
"Knowledge Base document or line range; omit them when no linking is needed. " +
|
|
86
|
+
"ttl is OPTIONAL (e.g. '90d', '2w', '24h') — expired facts are shown with [EXPIRED] but not auto-deleted. " +
|
|
87
|
+
"keep=true protects the fact from forget deletion unless force=true. " +
|
|
88
|
+
"tags is OPTIONAL comma-separated text for filtering. " +
|
|
89
|
+
"supersedes is OPTIONAL: a number (from recall), id, or text of a fact this one replaces; " +
|
|
90
|
+
"the target is then marked [SUPERSEDED]. " +
|
|
59
91
|
"Translate the fact into English and keep it concise. " +
|
|
60
92
|
"scope: 'project' (default) or 'global'",
|
|
61
93
|
inputSchema: z.object({
|
|
@@ -65,17 +97,42 @@ server.registerTool(
|
|
|
65
97
|
startLine: optNum().describe("Optional starting line number in target document"),
|
|
66
98
|
endLine: optNum().describe("Optional ending line number in target document"),
|
|
67
99
|
relationType: defStr("LINKS_TO").describe("Relation type (e.g. 'RULES_FOR', 'IMPLEMENTS', 'REFERENCES')"),
|
|
100
|
+
ttl: optStr().describe("Optional time-to-live, e.g. '90d', '2w', '24h', '12m'"),
|
|
101
|
+
keep: defBool(false).describe("Protect the fact from forget deletion unless force=true"),
|
|
102
|
+
tags: optStr().describe("Optional comma-separated tags, e.g. 'pref,arch'"),
|
|
103
|
+
supersedes: optStr().describe("Optional number, id, or text of the fact this one replaces"),
|
|
68
104
|
}),
|
|
69
105
|
},
|
|
70
|
-
async ({ fact, scope, docId, startLine, endLine, relationType }) => {
|
|
106
|
+
async ({ fact, scope, docId, startLine, endLine, relationType, ttl, keep, tags, supersedes }) => {
|
|
71
107
|
const key = scopeKey(scope, null, null);
|
|
72
108
|
const entries = await readMemory(key);
|
|
73
109
|
const factNormalized = fact.toLowerCase().trim();
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
110
|
+
let duplicate = false;
|
|
111
|
+
if (entries.some((e) => factText(e).toLowerCase().trim() === factNormalized)) {
|
|
112
|
+
duplicate = true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let supersededInfo = "";
|
|
116
|
+
if (!duplicate) {
|
|
117
|
+
const [date, time] = today().split(" ");
|
|
118
|
+
const meta = { ttl, tags };
|
|
119
|
+
if (keep) meta.keep = "1";
|
|
120
|
+
if (supersedes) {
|
|
121
|
+
const targetIdx = resolveFactIndex(entries, supersedes);
|
|
122
|
+
if (targetIdx !== -1) {
|
|
123
|
+
const newId = nextFactId(entries);
|
|
124
|
+
const targetMeta = factMeta(entries[targetIdx]);
|
|
125
|
+
const targetId = targetMeta.id || nextFactId(entries);
|
|
126
|
+
entries[targetIdx] = withMeta(entries[targetIdx], { id: targetId, supersededBy: newId });
|
|
127
|
+
meta.id = newId;
|
|
128
|
+
meta.supersedes = targetId;
|
|
129
|
+
supersededInfo = ` [superseded: "${factText(entries[targetIdx]).slice(0, 60)}"]`;
|
|
130
|
+
} else {
|
|
131
|
+
supersededInfo = " (note: supersedes target not found)";
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (!meta.id) meta.id = nextFactId(entries);
|
|
135
|
+
entries.push(formatFactEntry({ date, time, text: fact, meta }));
|
|
79
136
|
await writeMemory(key, entries);
|
|
80
137
|
}
|
|
81
138
|
|
|
@@ -98,7 +155,7 @@ server.registerTool(
|
|
|
98
155
|
}
|
|
99
156
|
}
|
|
100
157
|
|
|
101
|
-
return { content: [{ type: "text", text: `Memory updated${linkInfo}` }] };
|
|
158
|
+
return { content: [{ type: "text", text: `Memory updated${supersededInfo}${linkInfo}` }] };
|
|
102
159
|
}
|
|
103
160
|
);
|
|
104
161
|
|
|
@@ -108,20 +165,27 @@ server.registerTool(
|
|
|
108
165
|
description:
|
|
109
166
|
"Show saved facts with any Agent-linked Knowledge Base documents/lines. " +
|
|
110
167
|
"scope: 'project', 'global', 'all' (default), or 'list_projects'. " +
|
|
111
|
-
"Use project: '<directory path>' with scope 'project'/'all' to read facts of a specific project from any working directory."
|
|
168
|
+
"Use project: '<directory path>' with scope 'project'/'all' to read facts of a specific project from any working directory. " +
|
|
169
|
+
"query filters by keyword (all space-separated terms must match). " +
|
|
170
|
+
"tags filters by comma-separated tags. since/until filter by date (YYYY-MM-DD, inclusive). " +
|
|
171
|
+
"Expired facts are shown with [EXPIRED], protected ones with [KEEP]. The response includes the store file paths.",
|
|
112
172
|
inputSchema: z.object({
|
|
113
173
|
scope: defStr("all").describe("'project', 'global', 'all', or 'list_projects'"),
|
|
114
174
|
project: optStr().describe("Directory path of the project to read facts from (e.g. 'F:/projects/plugins/memory')"),
|
|
175
|
+
query: optStr().describe("Optional keyword filter; all space-separated terms must match"),
|
|
176
|
+
tags: optStr().describe("Optional comma-separated tag filter (any match)"),
|
|
177
|
+
since: optStr().describe("Optional start date filter, YYYY-MM-DD (inclusive)"),
|
|
178
|
+
until: optStr().describe("Optional end date filter, YYYY-MM-DD (inclusive)"),
|
|
115
179
|
}),
|
|
116
180
|
},
|
|
117
|
-
async ({ scope, project }) => {
|
|
181
|
+
async ({ scope, project, query, tags, since, until }) => {
|
|
118
182
|
const { getLinksForFact } = await import("./graph/knowledge_linker.js");
|
|
119
183
|
const results = [];
|
|
120
184
|
|
|
121
|
-
const formatFactWithLinks = (
|
|
122
|
-
let line =
|
|
185
|
+
const formatFactWithLinks = (factLine, key) => {
|
|
186
|
+
let line = displayFact(factLine);
|
|
123
187
|
try {
|
|
124
|
-
const links = getLinksForFact(key, factText);
|
|
188
|
+
const links = getLinksForFact(key, factText(factLine));
|
|
125
189
|
if (links && links.length > 0) {
|
|
126
190
|
const docStr = links
|
|
127
191
|
.map((l) => {
|
|
@@ -135,6 +199,17 @@ server.registerTool(
|
|
|
135
199
|
return line;
|
|
136
200
|
};
|
|
137
201
|
|
|
202
|
+
const collect = (entries, key) => {
|
|
203
|
+
const matched = entries.filter(
|
|
204
|
+
(e) => matchesQuery(e, query) && matchesTags(e, tags) && inDateRange(e, since, until)
|
|
205
|
+
);
|
|
206
|
+
if (!matched.length) return;
|
|
207
|
+
if (results.length) results.push("");
|
|
208
|
+
results.push(`--- ${key === GLOBAL_KEY ? "Global" : `Project: ${key === target ? label : key}`} ---`);
|
|
209
|
+
matched.forEach((e, i) => results.push(`${i + 1}. ${formatFactWithLinks(e, key)}`));
|
|
210
|
+
results.push(`Store file: ${storeFilePath(key)}`);
|
|
211
|
+
};
|
|
212
|
+
|
|
138
213
|
if (scope === "list_projects") {
|
|
139
214
|
const stores = await listProjectStores();
|
|
140
215
|
if (!stores.length) {
|
|
@@ -147,7 +222,7 @@ server.registerTool(
|
|
|
147
222
|
content: [
|
|
148
223
|
{
|
|
149
224
|
type: "text",
|
|
150
|
-
text: `Project Memory Stores:\n${lines.join("\n")}\n\nUse recall(scope: "project", project: "<path>") to read a specific store
|
|
225
|
+
text: `Project Memory Stores:\n${lines.join("\n")}\n\nUse recall(scope: "project", project: "<path>") to read a specific store.\n\nMemory dir: ${MEMORY_DIR}`,
|
|
151
226
|
},
|
|
152
227
|
],
|
|
153
228
|
};
|
|
@@ -156,21 +231,19 @@ server.registerTool(
|
|
|
156
231
|
const target = project ? canonicalPath(project) : projectKey(null, null);
|
|
157
232
|
const label = project ? target : projectName();
|
|
158
233
|
if (scope !== "project") {
|
|
159
|
-
const global = await
|
|
160
|
-
|
|
161
|
-
results.push("--- Global ---");
|
|
162
|
-
global.forEach((e, i) => results.push(`${i + 1}. ${formatFactWithLinks(e, GLOBAL_KEY)}`));
|
|
163
|
-
}
|
|
234
|
+
const global = await readMemory(GLOBAL_KEY);
|
|
235
|
+
collect(global, GLOBAL_KEY);
|
|
164
236
|
}
|
|
165
237
|
if (scope !== "global") {
|
|
166
|
-
const local = await
|
|
167
|
-
|
|
168
|
-
if (results.length) results.push("");
|
|
169
|
-
results.push(`--- Project: ${label} ---`);
|
|
170
|
-
local.forEach((e, i) => results.push(`${i + 1}. ${formatFactWithLinks(e, target)}`));
|
|
171
|
-
}
|
|
238
|
+
const local = await readMemory(target);
|
|
239
|
+
collect(local, target);
|
|
172
240
|
}
|
|
173
|
-
const
|
|
241
|
+
const filtered = Boolean(query || tags || since || until);
|
|
242
|
+
const text = results.length
|
|
243
|
+
? `${results.join("\n")}\n\nMemory dir: ${MEMORY_DIR}`
|
|
244
|
+
: filtered
|
|
245
|
+
? "No facts match the search."
|
|
246
|
+
: "Memory is empty.";
|
|
174
247
|
return { content: [{ type: "text", text }] };
|
|
175
248
|
}
|
|
176
249
|
);
|
|
@@ -179,40 +252,143 @@ server.registerTool(
|
|
|
179
252
|
"forget",
|
|
180
253
|
{
|
|
181
254
|
description:
|
|
182
|
-
"Delete a fact by number (from recall), by range (e.g. '3-30', inclusive), or by text search"
|
|
255
|
+
"Delete a fact by number (from recall), by range (e.g. '3-30', inclusive), or by text search. " +
|
|
256
|
+
"Protected facts (remember with keep=true) are skipped unless force=true.",
|
|
183
257
|
inputSchema: z.object({
|
|
184
258
|
query: z.string().describe("Number, range like '3-30', or text to search for"),
|
|
185
259
|
scope: defStr("project").describe("'project' (default) or 'global'"),
|
|
260
|
+
force: defBool(false).describe("Also delete protected (keep) facts"),
|
|
186
261
|
}),
|
|
187
262
|
},
|
|
188
|
-
async ({ query, scope }) => {
|
|
263
|
+
async ({ query, scope, force }) => {
|
|
189
264
|
const key = scopeKey(scope, null, null);
|
|
190
265
|
const entries = await readMemory(key);
|
|
191
266
|
const rangeMatch = /^\s*(\d+)\s*-\s*(\d+)\s*$/.exec(query);
|
|
192
267
|
const num = parseInt(query, 10);
|
|
193
|
-
let
|
|
268
|
+
let indices = [];
|
|
194
269
|
if (rangeMatch) {
|
|
195
270
|
const from = parseInt(rangeMatch[1], 10);
|
|
196
271
|
const to = parseInt(rangeMatch[2], 10);
|
|
197
272
|
if (from > 0 && to >= from && to <= entries.length) {
|
|
198
|
-
|
|
273
|
+
for (let i = from - 1; i < to; i++) indices.push(i);
|
|
199
274
|
}
|
|
200
275
|
}
|
|
201
|
-
if (!
|
|
202
|
-
|
|
276
|
+
if (!indices.length && !isNaN(num) && num > 0 && num <= entries.length) {
|
|
277
|
+
indices.push(num - 1);
|
|
203
278
|
}
|
|
204
|
-
if (!
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
entries.length = 0;
|
|
208
|
-
entries.push(...filtered);
|
|
279
|
+
if (!indices.length) {
|
|
280
|
+
const q = query.toLowerCase();
|
|
281
|
+
indices = entries.reduce((acc, e, i) => (e.toLowerCase().includes(q) ? acc.concat(i) : acc), []);
|
|
209
282
|
}
|
|
210
|
-
|
|
211
|
-
|
|
283
|
+
if (!indices.length) {
|
|
284
|
+
return { content: [{ type: "text", text: "Not found." }] };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const removable = indices.filter((i) => force || !isKeepFact(entries[i]));
|
|
288
|
+
const protectedCount = indices.length - removable.length;
|
|
289
|
+
if (removable.length) {
|
|
290
|
+
for (const i of removable.sort((a, b) => b - a)) entries.splice(i, 1);
|
|
291
|
+
await writeMemory(key, entries);
|
|
292
|
+
}
|
|
293
|
+
let text = removable.length ? "Memory updated" : "Nothing removed.";
|
|
294
|
+
if (protectedCount) text += ` (${protectedCount} protected fact(s) skipped; use force=true to override)`;
|
|
212
295
|
return { content: [{ type: "text", text }] };
|
|
213
296
|
}
|
|
214
297
|
);
|
|
215
298
|
|
|
299
|
+
server.registerTool(
|
|
300
|
+
"update_fact",
|
|
301
|
+
{
|
|
302
|
+
description:
|
|
303
|
+
"Update the text of an existing fact by number (from recall), id, or text match, " +
|
|
304
|
+
"preserving its original date and metadata. Linked Knowledge Base documents are re-pointed to the new text.",
|
|
305
|
+
inputSchema: z.object({
|
|
306
|
+
id: z.string().describe("Number (from recall), metadata id, or text of the fact to update"),
|
|
307
|
+
newText: z.string().describe("New fact text"),
|
|
308
|
+
scope: defStr("project").describe("'project' (default) or 'global'"),
|
|
309
|
+
}),
|
|
310
|
+
},
|
|
311
|
+
async ({ id, newText, scope }) => {
|
|
312
|
+
const key = scopeKey(scope, null, null);
|
|
313
|
+
const entries = await readMemory(key);
|
|
314
|
+
const idx = resolveFactIndex(entries, id);
|
|
315
|
+
if (idx === -1) throw new Error(`Fact not found: ${id}`);
|
|
316
|
+
const p = parseFactEntry(entries[idx]);
|
|
317
|
+
const oldText = p ? p.text : entries[idx];
|
|
318
|
+
const newLine = formatFactEntry({ date: p.date, time: p.time, text: newText, meta: p.meta });
|
|
319
|
+
entries[idx] = newLine;
|
|
320
|
+
await writeMemory(key, entries);
|
|
321
|
+
|
|
322
|
+
let linksUpdated = 0;
|
|
323
|
+
try {
|
|
324
|
+
const { getDatabase } = await import("./db/database.js");
|
|
325
|
+
const db = getDatabase();
|
|
326
|
+
const res = db
|
|
327
|
+
.prepare(
|
|
328
|
+
"UPDATE knowledge_links SET fact_text = ? WHERE fact_key = ? AND fact_text = ?"
|
|
329
|
+
)
|
|
330
|
+
.run(newText, key, oldText);
|
|
331
|
+
linksUpdated = res.changes;
|
|
332
|
+
} catch (e) {}
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
content: [
|
|
336
|
+
{ type: "text", text: `Fact updated${linksUpdated ? `, ${linksUpdated} doc link(s) updated` : ""}` },
|
|
337
|
+
],
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
server.registerTool(
|
|
343
|
+
"memory_info",
|
|
344
|
+
{
|
|
345
|
+
description:
|
|
346
|
+
"Show memory storage paths (store file locations, MEMORY_DIR, SQLite DB), fact counts, " +
|
|
347
|
+
"Knowledge Base stats, and the installed package version.",
|
|
348
|
+
inputSchema: z.object({}),
|
|
349
|
+
},
|
|
350
|
+
async () => {
|
|
351
|
+
const dbPath = join(MEMORY_DIR, "storage", "memory.sqlite");
|
|
352
|
+
const globalFile = storeFilePath(GLOBAL_KEY);
|
|
353
|
+
const projectFile = storeFilePath(projectKey(null, null));
|
|
354
|
+
|
|
355
|
+
let version = "unknown";
|
|
356
|
+
try {
|
|
357
|
+
version = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf-8")).version;
|
|
358
|
+
} catch (e) {}
|
|
359
|
+
|
|
360
|
+
let rag = {};
|
|
361
|
+
try {
|
|
362
|
+
const { getDatabase } = await import("./db/database.js");
|
|
363
|
+
const db = getDatabase();
|
|
364
|
+
rag.documents = db.prepare("SELECT COUNT(*) AS c FROM documents").get().c;
|
|
365
|
+
rag.sections = db.prepare("SELECT COUNT(*) AS c FROM sections").get().c;
|
|
366
|
+
rag.chunks = db.prepare("SELECT COUNT(*) AS c FROM micro_chunks").get().c;
|
|
367
|
+
rag.edges = db.prepare("SELECT COUNT(*) AS c FROM graph_edges").get().c;
|
|
368
|
+
rag.links = db.prepare("SELECT COUNT(*) AS c FROM knowledge_links").get().c;
|
|
369
|
+
} catch (e) {
|
|
370
|
+
rag.error = e.message;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const stores = await listProjectStores();
|
|
374
|
+
const lines = [
|
|
375
|
+
`Version: ${version}`,
|
|
376
|
+
`MEMORY_DIR: ${MEMORY_DIR}`,
|
|
377
|
+
`SQLite DB: ${dbPath}`,
|
|
378
|
+
`Global store: ${globalFile}`,
|
|
379
|
+
`Project store: ${projectFile}`,
|
|
380
|
+
`Project stores: ${stores.length}`,
|
|
381
|
+
`Facts (global): ${(await readMemoryRaw(GLOBAL_KEY)).length}`,
|
|
382
|
+
`Facts (project): ${(await readMemoryRaw(projectKey(null, null))).length}`,
|
|
383
|
+
];
|
|
384
|
+
if (rag.error) lines.push(`RAG: unavailable (${rag.error})`);
|
|
385
|
+
else lines.push(
|
|
386
|
+
`RAG: ${rag.documents} doc(s), ${rag.sections} section(s), ${rag.chunks} chunk(s), ${rag.edges} edge(s), ${rag.links} memory link(s)`
|
|
387
|
+
);
|
|
388
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
389
|
+
}
|
|
390
|
+
);
|
|
391
|
+
|
|
216
392
|
server.registerTool(
|
|
217
393
|
"link_knowledge",
|
|
218
394
|
{
|
|
@@ -277,12 +453,13 @@ server.registerTool(
|
|
|
277
453
|
description:
|
|
278
454
|
"Ingest a document into the RAG knowledge base. " +
|
|
279
455
|
"Accepts local file paths, web URLs, or raw Markdown/text content. " +
|
|
456
|
+
"For type='file' the file is read from disk and indexed with a code-block wrapper. " +
|
|
280
457
|
"For type='url' the page is fetched and its content is indexed (not just the URL). " +
|
|
281
458
|
"Processes document through 3-tier hierarchy chunking (Big/Medium/Small), " +
|
|
282
459
|
"computes dense vectors, and extracts GraphRAG code symbols.",
|
|
283
460
|
inputSchema: z.object({
|
|
284
|
-
content: z.string().describe("Raw text content, file path, or web URL"),
|
|
285
|
-
type: z.enum(["text", "file", "url"]).nullish().transform((v) => v || "text").describe("Input content type: 'text', 'file', or 'url' (
|
|
461
|
+
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"),
|
|
462
|
+
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)"),
|
|
286
463
|
title: optStr().describe("Document title"),
|
|
287
464
|
path: optStr().describe("Original document file path"),
|
|
288
465
|
generateEmbeddings: defBool(true).describe("Compute dense vector embeddings"),
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
2
3
|
import { getDatabase, BLOBS_DIR } from "../db/database.js";
|
|
3
4
|
import { saveBlob, deleteBlob } from "../storage/blob_store.js";
|
|
4
5
|
import { normalizeContent, fetchUrlContent } from "./normalizer.js";
|
|
@@ -28,6 +29,13 @@ export async function ingestDocument({
|
|
|
28
29
|
effectiveType = "text";
|
|
29
30
|
effectiveTitle = title || fetched.title;
|
|
30
31
|
effectivePath = path || fetched.finalUrl || content;
|
|
32
|
+
} else if (type === "file") {
|
|
33
|
+
const filePath = effectivePath || content;
|
|
34
|
+
const needsRead = !content || content === filePath;
|
|
35
|
+
if (needsRead && filePath) {
|
|
36
|
+
content = await readFile(filePath, "utf-8");
|
|
37
|
+
effectivePath = filePath;
|
|
38
|
+
}
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
const { markdown, title: docTitle, metadata } = normalizeContent({ content, type: effectiveType, path: effectivePath, title: effectiveTitle });
|