@gresmcp/mcp 1.0.0 → 1.1.0
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/README.md +22 -8
- package/dist/cli.js +683 -116
- package/dist/cli.js.map +1 -1
- package/dist/mcp.js +1 -1
- package/dist/mcp.js.map +1 -1
- package/package.json +32 -1
package/dist/cli.js
CHANGED
|
@@ -37,6 +37,7 @@ __export(db_exports, {
|
|
|
37
37
|
contentHash: () => contentHash,
|
|
38
38
|
countEntries: () => countEntries,
|
|
39
39
|
createKs: () => createKs,
|
|
40
|
+
dbClockTimestamp: () => dbClockTimestamp,
|
|
40
41
|
deleteBySource: () => deleteBySource,
|
|
41
42
|
deleteKs: () => deleteKs,
|
|
42
43
|
ensureEntryTable: () => ensureEntryTable,
|
|
@@ -45,6 +46,7 @@ __export(db_exports, {
|
|
|
45
46
|
getPool: () => getPool,
|
|
46
47
|
initDb: () => initDb,
|
|
47
48
|
insertChunks: () => insertChunks,
|
|
49
|
+
ksStats: () => ksStats,
|
|
48
50
|
listKs: () => listKs,
|
|
49
51
|
sourceChunkHashes: () => sourceChunkHashes,
|
|
50
52
|
updateKs: () => updateKs
|
|
@@ -211,6 +213,41 @@ async function countEntries(ks) {
|
|
|
211
213
|
const res = await getPool().query(`SELECT count(*)::int AS n FROM ${tbl}`);
|
|
212
214
|
return Number(res.rows[0]?.n ?? 0);
|
|
213
215
|
}
|
|
216
|
+
async function dbClockTimestamp() {
|
|
217
|
+
const res = await getPool().query(`SELECT clock_timestamp() AS ts`);
|
|
218
|
+
return res.rows[0].ts;
|
|
219
|
+
}
|
|
220
|
+
async function ksStats(ks, since) {
|
|
221
|
+
const tbl = entryTableName(ks);
|
|
222
|
+
const params = [];
|
|
223
|
+
let where = "";
|
|
224
|
+
if (since) {
|
|
225
|
+
params.push(since);
|
|
226
|
+
where = ` WHERE created_at >= $1`;
|
|
227
|
+
}
|
|
228
|
+
const res = await getPool().query(
|
|
229
|
+
`SELECT count(*)::int AS entries,
|
|
230
|
+
count(DISTINCT source)::int AS sources,
|
|
231
|
+
COALESCE(sum(length(content)), 0)::bigint AS chars,
|
|
232
|
+
round(avg(length(content)))::int AS avg_chunk_chars,
|
|
233
|
+
min(length(content))::int AS min_chunk_chars,
|
|
234
|
+
max(length(content))::int AS max_chunk_chars,
|
|
235
|
+
max(created_at) AS last_fed_at
|
|
236
|
+
FROM ${tbl}${where}`,
|
|
237
|
+
params
|
|
238
|
+
);
|
|
239
|
+
const r = res.rows[0];
|
|
240
|
+
return {
|
|
241
|
+
ks,
|
|
242
|
+
entries: Number(r.entries ?? 0),
|
|
243
|
+
sources: Number(r.sources ?? 0),
|
|
244
|
+
chars: Number(r.chars ?? 0),
|
|
245
|
+
avg_chunk_chars: r.avg_chunk_chars === null ? null : Number(r.avg_chunk_chars),
|
|
246
|
+
min_chunk_chars: r.min_chunk_chars === null ? null : Number(r.min_chunk_chars),
|
|
247
|
+
max_chunk_chars: r.max_chunk_chars === null ? null : Number(r.max_chunk_chars),
|
|
248
|
+
last_fed_at: r.last_fed_at === null ? null : r.last_fed_at
|
|
249
|
+
};
|
|
250
|
+
}
|
|
214
251
|
var pool;
|
|
215
252
|
var init_db = __esm({
|
|
216
253
|
"src/db.ts"() {
|
|
@@ -353,8 +390,8 @@ function chunkMarkdown(text, opts = {}) {
|
|
|
353
390
|
const m = HEADING_RE.exec(line);
|
|
354
391
|
if (m) {
|
|
355
392
|
const level = m[1].length;
|
|
356
|
-
const
|
|
357
|
-
const parent =
|
|
393
|
+
const path3 = sections[sections.length - 1].path;
|
|
394
|
+
const parent = path3.slice(0, level - 1);
|
|
358
395
|
sections.push({ path: [...parent, m[2].trim()], body: [line] });
|
|
359
396
|
} else {
|
|
360
397
|
sections[sections.length - 1].body.push(line);
|
|
@@ -388,9 +425,12 @@ function mdTitle(text) {
|
|
|
388
425
|
// src/html.ts
|
|
389
426
|
import { JSDOM } from "jsdom";
|
|
390
427
|
import TurndownService from "turndown";
|
|
428
|
+
import { Readability, isProbablyReaderable } from "@mozilla/readability";
|
|
391
429
|
import gfmModule from "turndown-plugin-gfm";
|
|
392
430
|
var gfm = typeof gfmModule === "function" ? gfmModule : gfmModule.gfm;
|
|
393
431
|
var REMOVE_SELECTORS = "script, style, noscript, template, iframe, svg, nav, footer, aside, form, button";
|
|
432
|
+
var SHELL_MARKER_RE = /id=["'](root|app|__next|__nuxt|q-app)["']|data-reactroot|ng-version=/i;
|
|
433
|
+
var SHELL_TEXT_THRESHOLD = 250;
|
|
394
434
|
var service;
|
|
395
435
|
function turndown() {
|
|
396
436
|
if (!service) {
|
|
@@ -421,10 +461,207 @@ function htmlToMarkdown(html) {
|
|
|
421
461
|
markdown = markdown.replace(/\n{3,}/g, "\n\n").trim();
|
|
422
462
|
return { markdown, title: title || void 0 };
|
|
423
463
|
}
|
|
464
|
+
function metaContent(doc, selectors) {
|
|
465
|
+
for (const selector of selectors) {
|
|
466
|
+
const el = doc.querySelector(selector);
|
|
467
|
+
const value = (el?.getAttribute("content") ?? el?.textContent ?? "").trim();
|
|
468
|
+
if (value) return value;
|
|
469
|
+
}
|
|
470
|
+
return void 0;
|
|
471
|
+
}
|
|
472
|
+
function collectTags(doc) {
|
|
473
|
+
const tags = [];
|
|
474
|
+
const keywords = metaContent(doc, ['meta[name="keywords"]']);
|
|
475
|
+
if (keywords) {
|
|
476
|
+
for (const part of keywords.split(",")) {
|
|
477
|
+
const tag = part.trim();
|
|
478
|
+
if (tag) tags.push(tag);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
doc.querySelectorAll('meta[property="og:article:tag"]').forEach((el) => {
|
|
482
|
+
const value = (el.getAttribute("content") ?? "").trim();
|
|
483
|
+
if (value) tags.push(value);
|
|
484
|
+
});
|
|
485
|
+
const seen = /* @__PURE__ */ new Set();
|
|
486
|
+
return tags.filter((t) => {
|
|
487
|
+
const key = t.toLowerCase();
|
|
488
|
+
if (seen.has(key)) return false;
|
|
489
|
+
seen.add(key);
|
|
490
|
+
return true;
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
function extractPage(html) {
|
|
494
|
+
const dom = new JSDOM(html);
|
|
495
|
+
const doc = dom.window.document;
|
|
496
|
+
const title = doc.title?.trim() || doc.querySelector('meta[property="og:title"]')?.getAttribute("content")?.trim() || doc.querySelector("h1")?.textContent?.trim() || void 0;
|
|
497
|
+
const description = metaContent(doc, [
|
|
498
|
+
'meta[name="description"]',
|
|
499
|
+
'meta[property="og:description"]'
|
|
500
|
+
]);
|
|
501
|
+
const tags = collectTags(doc);
|
|
502
|
+
doc.querySelectorAll(REMOVE_SELECTORS).forEach((el) => el.remove());
|
|
503
|
+
let markdown = "";
|
|
504
|
+
try {
|
|
505
|
+
if (isProbablyReaderable(doc)) {
|
|
506
|
+
const article = new Readability(doc).parse();
|
|
507
|
+
if (article?.content) markdown = turndown().turndown(article.content);
|
|
508
|
+
}
|
|
509
|
+
} catch {
|
|
510
|
+
markdown = "";
|
|
511
|
+
}
|
|
512
|
+
if (!markdown.trim()) {
|
|
513
|
+
const root = doc.body ?? doc.documentElement;
|
|
514
|
+
try {
|
|
515
|
+
markdown = turndown().turndown(root.innerHTML);
|
|
516
|
+
} catch {
|
|
517
|
+
markdown = root.textContent ?? "";
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
markdown = markdown.replace(/\n{3,}/g, "\n\n").trim();
|
|
521
|
+
const likelyShell = markdown.length < SHELL_TEXT_THRESHOLD && SHELL_MARKER_RE.test(html);
|
|
522
|
+
return {
|
|
523
|
+
markdown,
|
|
524
|
+
title,
|
|
525
|
+
description,
|
|
526
|
+
tags: tags.length > 0 ? tags : void 0,
|
|
527
|
+
likelyShell
|
|
528
|
+
};
|
|
529
|
+
}
|
|
424
530
|
|
|
425
531
|
// src/ingest.ts
|
|
426
532
|
init_db();
|
|
427
|
-
|
|
533
|
+
|
|
534
|
+
// src/progress.ts
|
|
535
|
+
var BAR_WIDTH = 20;
|
|
536
|
+
var PLAIN_STEP = 32;
|
|
537
|
+
function fmt(n) {
|
|
538
|
+
return n.toLocaleString("en-US");
|
|
539
|
+
}
|
|
540
|
+
function bar(pct2) {
|
|
541
|
+
const filled = Math.max(0, Math.min(BAR_WIDTH, Math.round(pct2 * BAR_WIDTH)));
|
|
542
|
+
return "#".repeat(filled) + ".".repeat(BAR_WIDTH - filled);
|
|
543
|
+
}
|
|
544
|
+
function pct(p) {
|
|
545
|
+
return `${Math.round(Math.max(0, Math.min(1, p)) * 100)}%`;
|
|
546
|
+
}
|
|
547
|
+
var FeedProgress = class {
|
|
548
|
+
write;
|
|
549
|
+
interactive;
|
|
550
|
+
total;
|
|
551
|
+
rejected;
|
|
552
|
+
filesDone = 0;
|
|
553
|
+
totalChars = 0;
|
|
554
|
+
current;
|
|
555
|
+
blockLines = 0;
|
|
556
|
+
finished = false;
|
|
557
|
+
constructor(opts) {
|
|
558
|
+
this.total = Math.max(0, Math.floor(opts.total));
|
|
559
|
+
this.rejected = Math.max(0, Math.floor(opts.rejected));
|
|
560
|
+
this.write = opts.out ?? ((s) => process.stderr.write(s));
|
|
561
|
+
this.interactive = opts.interactive ?? (opts.out === void 0 && !!process.stderr.isTTY);
|
|
562
|
+
}
|
|
563
|
+
get isInteractive() {
|
|
564
|
+
return this.interactive;
|
|
565
|
+
}
|
|
566
|
+
startFile(source, totalChunks, totalChars) {
|
|
567
|
+
if (this.finished) return;
|
|
568
|
+
this.current = { source, totalChunks, totalChars, doneChunks: 0, doneChars: 0, printedChunks: 0 };
|
|
569
|
+
this.render();
|
|
570
|
+
}
|
|
571
|
+
handled(chunks, chars) {
|
|
572
|
+
if (this.finished || !this.current || chunks <= 0) return;
|
|
573
|
+
const c = this.current;
|
|
574
|
+
c.doneChunks += chunks;
|
|
575
|
+
c.doneChars += chars;
|
|
576
|
+
this.totalChars += chars;
|
|
577
|
+
if (c.doneChunks >= c.totalChunks) {
|
|
578
|
+
this.filesDone++;
|
|
579
|
+
this.printFileLine();
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (!this.interactive && c.doneChunks - c.printedChunks >= PLAIN_STEP) {
|
|
583
|
+
this.printFileLine();
|
|
584
|
+
} else if (this.interactive) {
|
|
585
|
+
this.render();
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
log(msg) {
|
|
589
|
+
if (this.finished) return;
|
|
590
|
+
this.clearBlock();
|
|
591
|
+
this.write(`${msg}
|
|
592
|
+
`);
|
|
593
|
+
}
|
|
594
|
+
finish() {
|
|
595
|
+
if (this.finished) return;
|
|
596
|
+
this.finished = true;
|
|
597
|
+
if (!this.current && this.total === 0 && this.rejected === 0) return;
|
|
598
|
+
if (this.interactive) {
|
|
599
|
+
this.redraw([this.fileLine(), this.totalLine()]);
|
|
600
|
+
this.write("\n");
|
|
601
|
+
this.blockLines = 0;
|
|
602
|
+
} else {
|
|
603
|
+
this.write(`${this.totalLine()}
|
|
604
|
+
`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
close() {
|
|
608
|
+
if (this.finished) return;
|
|
609
|
+
this.finished = true;
|
|
610
|
+
this.clearBlock();
|
|
611
|
+
}
|
|
612
|
+
render() {
|
|
613
|
+
if (!this.interactive) return;
|
|
614
|
+
this.redraw([this.fileLine(), this.totalLine()]);
|
|
615
|
+
}
|
|
616
|
+
printFileLine() {
|
|
617
|
+
if (!this.current) return;
|
|
618
|
+
this.current.printedChunks = this.current.doneChunks;
|
|
619
|
+
if (this.interactive) {
|
|
620
|
+
this.render();
|
|
621
|
+
} else {
|
|
622
|
+
this.write(`${this.fileLine()}
|
|
623
|
+
`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
fileLine() {
|
|
627
|
+
const c = this.current;
|
|
628
|
+
if (!c) return "";
|
|
629
|
+
const p = c.totalChunks > 0 ? c.doneChunks / c.totalChunks : 1;
|
|
630
|
+
return `[${bar(p)}] ${pct(p).padStart(4)} ${fmt(c.doneChunks)}/${fmt(c.totalChunks)} chunks ${fmt(c.doneChars)}/${fmt(c.totalChars)} chars ${c.source}`;
|
|
631
|
+
}
|
|
632
|
+
totalLine() {
|
|
633
|
+
const p = this.total > 0 ? this.filesDone / this.total : 0;
|
|
634
|
+
const rejected = this.rejected > 0 ? ` | rejected: ${this.rejected} (excluded)` : "";
|
|
635
|
+
return `total: ${this.filesDone}/${this.total} files (${pct(p)}) ${fmt(this.totalChars)} chars${rejected}`;
|
|
636
|
+
}
|
|
637
|
+
redraw(lines) {
|
|
638
|
+
let out = "";
|
|
639
|
+
if (this.blockLines > 1) out += `\x1B[${this.blockLines - 1}A`;
|
|
640
|
+
out += "\r";
|
|
641
|
+
for (let i = 0; i < lines.length; i++) {
|
|
642
|
+
out += `\x1B[2K${lines[i]}${i < lines.length - 1 ? "\n" : ""}`;
|
|
643
|
+
}
|
|
644
|
+
this.write(out);
|
|
645
|
+
this.blockLines = lines.length;
|
|
646
|
+
}
|
|
647
|
+
clearBlock() {
|
|
648
|
+
if (!this.interactive || this.blockLines === 0) {
|
|
649
|
+
this.blockLines = 0;
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
let out = "";
|
|
653
|
+
if (this.blockLines > 1) out += `\x1B[${this.blockLines - 1}A`;
|
|
654
|
+
out += "\r";
|
|
655
|
+
for (let i = 0; i < this.blockLines; i++) {
|
|
656
|
+
out += `\x1B[2K${i < this.blockLines - 1 ? "\n" : ""}`;
|
|
657
|
+
}
|
|
658
|
+
out += "\n";
|
|
659
|
+
this.write(out);
|
|
660
|
+
this.blockLines = 0;
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
// src/ingest.ts
|
|
428
665
|
var MD_EXT = /* @__PURE__ */ new Set([".md", ".markdown", ".mdx"]);
|
|
429
666
|
var HTML_EXT = /* @__PURE__ */ new Set([".html", ".htm", ".xhtml"]);
|
|
430
667
|
var TEXT_EXT = /* @__PURE__ */ new Set([
|
|
@@ -575,115 +812,408 @@ async function loadTarget(target, opts) {
|
|
|
575
812
|
}
|
|
576
813
|
return { docs, fileCount: files.length, skipped };
|
|
577
814
|
}
|
|
578
|
-
async function
|
|
579
|
-
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const byDoc = /* @__PURE__ */ new Map();
|
|
586
|
-
pending.forEach((p, i) => {
|
|
587
|
-
const list = byDoc.get(p.doc) ?? [];
|
|
588
|
-
list.push({
|
|
589
|
-
source: p.doc.source,
|
|
590
|
-
title: p.doc.title,
|
|
591
|
-
chunk_index: p.chunk.index,
|
|
592
|
-
content: p.chunk.content,
|
|
593
|
-
metadata: p.metadata,
|
|
594
|
-
embedding: embeddings[i]
|
|
595
|
-
});
|
|
596
|
-
byDoc.set(p.doc, list);
|
|
597
|
-
});
|
|
598
|
-
let inserted = 0;
|
|
599
|
-
for (const [, rows] of byDoc) {
|
|
600
|
-
inserted += await insertChunks(ks, rows);
|
|
601
|
-
}
|
|
602
|
-
return { inserted };
|
|
603
|
-
}
|
|
604
|
-
async function feedTarget(ks, target, opts = {}) {
|
|
605
|
-
const loadOpts = { maxLen: opts.maxLen ?? 1200, overlap: opts.overlap ?? 180 };
|
|
606
|
-
const { docs, fileCount, skipped } = await loadTarget(target, loadOpts);
|
|
607
|
-
const log = opts.log ?? ((m) => console.log(m));
|
|
608
|
-
for (const doc of docs) {
|
|
609
|
-
log(`${doc.source}: ${doc.chunks.length} chunk(s)`);
|
|
610
|
-
}
|
|
611
|
-
for (const s of skipped) {
|
|
612
|
-
log(`skipped ${s.path}: ${s.reason}`);
|
|
613
|
-
}
|
|
614
|
-
const meta = { ...opts.metadata };
|
|
615
|
-
if (opts.tags && opts.tags.length > 0) meta.tags = opts.tags;
|
|
815
|
+
async function feedDocs(ks, docs, opts = {}) {
|
|
816
|
+
const log2 = opts.log ?? ((m) => console.log(m));
|
|
817
|
+
const progress = opts.progress;
|
|
818
|
+
const batchSize = opts.batchSize ?? 32;
|
|
819
|
+
const baseMeta = { ...opts.metadata ?? {} };
|
|
820
|
+
const cliTags = opts.tags && opts.tags.length > 0 ? opts.tags : void 0;
|
|
821
|
+
const say = (msg) => progress ? progress.log(msg) : log2(msg);
|
|
616
822
|
let deletedForReplace = 0;
|
|
617
|
-
const pending = [];
|
|
618
823
|
let skippedExisting = 0;
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
824
|
+
let inserted = 0;
|
|
825
|
+
let announcedEmbed = false;
|
|
826
|
+
try {
|
|
827
|
+
for (const doc of docs) {
|
|
828
|
+
progress?.startFile(
|
|
829
|
+
doc.source,
|
|
830
|
+
doc.chunks.length,
|
|
831
|
+
doc.chunks.reduce((n, c) => n + c.content.length, 0)
|
|
832
|
+
);
|
|
833
|
+
if (opts.replace) {
|
|
834
|
+
deletedForReplace += await deleteBySource(ks, doc.source);
|
|
835
|
+
}
|
|
836
|
+
const existing = await sourceChunkHashes(ks, doc.source);
|
|
837
|
+
const meta = { ...baseMeta, ...doc.metadata ?? {} };
|
|
838
|
+
if (!("tags" in meta) && cliTags) meta.tags = cliTags;
|
|
839
|
+
const todo = [];
|
|
840
|
+
for (const chunk of doc.chunks) {
|
|
841
|
+
if (!opts.replace && existing.get(chunk.index) === contentHash(chunk.content)) {
|
|
842
|
+
skippedExisting++;
|
|
843
|
+
progress?.handled(1, chunk.content.length);
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
todo.push(chunk);
|
|
847
|
+
}
|
|
848
|
+
if (todo.length === 0) continue;
|
|
849
|
+
if (opts.dryRun) {
|
|
850
|
+
for (const chunk of todo) progress?.handled(1, chunk.content.length);
|
|
627
851
|
continue;
|
|
628
852
|
}
|
|
629
|
-
|
|
853
|
+
if (!announcedEmbed) {
|
|
854
|
+
say(`Embedding with ${ks.embedding_model} at ${ks.ollama_url} ...`);
|
|
855
|
+
announcedEmbed = true;
|
|
856
|
+
}
|
|
857
|
+
const contents = todo.map((c) => c.content);
|
|
858
|
+
const charLens = contents.map((c) => c.length);
|
|
859
|
+
let reported = 0;
|
|
860
|
+
const embeddings = await embedBatch(ks.ollama_url, ks.embedding_model, contents, batchSize, (done) => {
|
|
861
|
+
const delta = done - reported;
|
|
862
|
+
if (delta <= 0) return;
|
|
863
|
+
let chars = 0;
|
|
864
|
+
for (let i = reported; i < done; i++) chars += charLens[i];
|
|
865
|
+
reported = done;
|
|
866
|
+
progress?.handled(delta, chars);
|
|
867
|
+
});
|
|
868
|
+
const rows = todo.map((chunk, i) => ({
|
|
869
|
+
source: doc.source,
|
|
870
|
+
title: doc.title,
|
|
871
|
+
chunk_index: chunk.index,
|
|
872
|
+
content: chunk.content,
|
|
873
|
+
metadata: meta,
|
|
874
|
+
embedding: embeddings[i]
|
|
875
|
+
}));
|
|
876
|
+
inserted += await insertChunks(ks, rows);
|
|
630
877
|
}
|
|
878
|
+
} catch (err) {
|
|
879
|
+
progress?.close();
|
|
880
|
+
throw err;
|
|
631
881
|
}
|
|
632
|
-
|
|
633
|
-
url: ollamaUrl(opts.url),
|
|
634
|
-
dryRun: opts.dryRun ?? false,
|
|
635
|
-
batchSize: opts.batchSize ?? 32,
|
|
636
|
-
log
|
|
637
|
-
});
|
|
882
|
+
progress?.finish();
|
|
638
883
|
return {
|
|
639
884
|
documents: docs.length,
|
|
640
885
|
chunks: docs.reduce((n, d) => n + d.chunks.length, 0),
|
|
641
886
|
inserted,
|
|
642
887
|
skippedExisting,
|
|
643
888
|
deletedForReplace,
|
|
644
|
-
skippedFiles:
|
|
889
|
+
skippedFiles: [],
|
|
645
890
|
dryRun: opts.dryRun ?? false
|
|
646
891
|
};
|
|
647
892
|
}
|
|
893
|
+
async function feedTarget(ks, target, opts = {}) {
|
|
894
|
+
const loadOpts = { maxLen: opts.maxLen ?? 1200, overlap: opts.overlap ?? 180 };
|
|
895
|
+
const { docs, skipped } = await loadTarget(target, loadOpts);
|
|
896
|
+
for (const s of skipped) {
|
|
897
|
+
(opts.log ?? ((m) => console.log(m)))(`skipped ${s.path}: ${s.reason}`);
|
|
898
|
+
}
|
|
899
|
+
const progress = opts.progress ?? new FeedProgress({ total: docs.length, rejected: skipped.length });
|
|
900
|
+
const summary = await feedDocs(ks, docs, { ...opts, progress });
|
|
901
|
+
return { ...summary, skippedFiles: skipped };
|
|
902
|
+
}
|
|
648
903
|
async function feedText(ks, text, opts = {}) {
|
|
649
|
-
const
|
|
904
|
+
const log2 = opts.log ?? ((m) => console.log(m));
|
|
650
905
|
const source = opts.sourceName?.trim() || "manual";
|
|
651
906
|
const title = opts.title?.trim() || source;
|
|
652
907
|
const chunks = chunkMarkdown(text, { maxLen: opts.maxLen ?? 1200, overlap: opts.overlap ?? 180 });
|
|
653
908
|
const doc = { source, title, chunks };
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
909
|
+
const progress = opts.progress ?? new FeedProgress({ total: 1, rejected: 0 });
|
|
910
|
+
return feedDocs(ks, [doc], { ...opts, progress });
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// src/scrape.ts
|
|
914
|
+
import { mkdtempSync } from "fs";
|
|
915
|
+
import { tmpdir } from "os";
|
|
916
|
+
import path2 from "path";
|
|
917
|
+
import { randomUUID } from "crypto";
|
|
918
|
+
import { CheerioCrawler, PlaywrightCrawler, PuppeteerCrawler, RequestQueue, log } from "crawlee";
|
|
919
|
+
var CRAWLER_KINDS = ["auto", "crawlee", "playwright", "puppeteer"];
|
|
920
|
+
var BINARY_LINK_RE = /\.(css|js|mjs|cjs|map|png|jpe?g|gif|svg|webp|avif|ico|bmp|tiff?|woff2?|ttf|otf|eot|pdf|zip|gz|tgz|tar|rar|7z|bz2|xz|zst|mp3|mp4|m4a|webm|mov|avi|wmv|flv|docx?|xlsx?|pptx?|odt|ods|odp|rtf|dmg|exe|msi|apk|iso|bin|wasm|deb|rpm)(\?.*)?$/i;
|
|
921
|
+
process.env.CRAWLEE_STORAGE_DIR ??= mkdtempSync(path2.join(tmpdir(), "gresmcp-crawl-"));
|
|
922
|
+
async function freshQueue() {
|
|
923
|
+
return RequestQueue.open(`gresmcp-${randomUUID()}`);
|
|
924
|
+
}
|
|
925
|
+
function normalizeSeedUrl(seed) {
|
|
926
|
+
let url;
|
|
927
|
+
try {
|
|
928
|
+
url = new URL(seed.trim());
|
|
929
|
+
} catch {
|
|
930
|
+
throw new Error(`invalid URL '${seed}'`);
|
|
662
931
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
932
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
933
|
+
throw new Error(`unsupported URL protocol '${url.protocol.replace(":", "")}' (use http or https)`);
|
|
934
|
+
}
|
|
935
|
+
url.hash = "";
|
|
936
|
+
return url.toString();
|
|
937
|
+
}
|
|
938
|
+
function responseStatus(response) {
|
|
939
|
+
if (!response || typeof response !== "object") return void 0;
|
|
940
|
+
const res = response;
|
|
941
|
+
if (typeof res.status === "number") return res.status;
|
|
942
|
+
if (typeof res.status === "function") {
|
|
943
|
+
const value = res.status();
|
|
944
|
+
return typeof value === "number" ? value : void 0;
|
|
945
|
+
}
|
|
946
|
+
if (typeof res.statusCode === "number") return res.statusCode;
|
|
947
|
+
return void 0;
|
|
948
|
+
}
|
|
949
|
+
function toText(body) {
|
|
950
|
+
return typeof body === "string" ? body : body.toString("utf-8");
|
|
951
|
+
}
|
|
952
|
+
function titleFor(url, fallback) {
|
|
953
|
+
const trimmed = fallback?.trim();
|
|
954
|
+
if (trimmed) return trimmed;
|
|
955
|
+
try {
|
|
956
|
+
const segment = new URL(url).pathname.split("/").filter(Boolean).pop();
|
|
957
|
+
const name = segment ? decodeURIComponent(segment) : "";
|
|
958
|
+
return name || url;
|
|
959
|
+
} catch {
|
|
960
|
+
return url;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
function unionTags(...lists) {
|
|
964
|
+
const out = [];
|
|
965
|
+
const seen = /* @__PURE__ */ new Set();
|
|
966
|
+
for (const list of lists) {
|
|
967
|
+
for (const tag of list ?? []) {
|
|
968
|
+
const value = tag.trim();
|
|
969
|
+
if (!value) continue;
|
|
970
|
+
const key = value.toLowerCase();
|
|
971
|
+
if (seen.has(key)) continue;
|
|
972
|
+
seen.add(key);
|
|
973
|
+
out.push(value);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return out.length > 0 ? out : void 0;
|
|
977
|
+
}
|
|
978
|
+
function buildDoc(url, kind, text, page, deps) {
|
|
979
|
+
const chunkOpts = { maxLen: deps.maxLen, overlap: deps.overlap };
|
|
980
|
+
const chunks = kind === "plain" ? chunkPlain(text, chunkOpts) : chunkMarkdown(text, chunkOpts);
|
|
981
|
+
if (chunks.length === 0) return void 0;
|
|
982
|
+
const title = kind === "html" ? titleFor(url, page?.title) : kind === "md" ? mdTitle(text) ?? titleFor(url) : titleFor(url);
|
|
983
|
+
const metadata = { source_url: url, crawled_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
984
|
+
if (page?.description) metadata.description = page.description;
|
|
985
|
+
const tags = unionTags(page?.tags, deps.cliTags);
|
|
986
|
+
if (tags) metadata.tags = tags;
|
|
987
|
+
return { source: url, title, chunks, metadata };
|
|
988
|
+
}
|
|
989
|
+
function dedupeDocs(docs) {
|
|
990
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
991
|
+
for (const doc of docs) {
|
|
992
|
+
if (!bySource.has(doc.source)) bySource.set(doc.source, doc);
|
|
993
|
+
}
|
|
994
|
+
return [...bySource.values()];
|
|
995
|
+
}
|
|
996
|
+
function shortReason(err) {
|
|
997
|
+
const message = (err?.message ?? String(err)).replace(/\s+/g, " ").trim();
|
|
998
|
+
const contentType = /served Content-Type ([^,\s]+),/.exec(message);
|
|
999
|
+
if (contentType) return `unsupported content-type '${contentType[1]}'`;
|
|
1000
|
+
return message.slice(0, 200);
|
|
1001
|
+
}
|
|
1002
|
+
async function crawlWithCheerio(queue, seedUrls, opts, sink) {
|
|
1003
|
+
const crawler = new CheerioCrawler({
|
|
1004
|
+
requestQueue: queue,
|
|
1005
|
+
maxRequestsPerCrawl: Math.max(1, opts.maxPages),
|
|
1006
|
+
maxCrawlDepth: Math.max(0, opts.depth),
|
|
1007
|
+
additionalMimeTypes: ["text/plain", "text/markdown"],
|
|
1008
|
+
async requestHandler(ctx) {
|
|
1009
|
+
const { request, response, contentType, body } = ctx;
|
|
1010
|
+
const url = request.loadedUrl ?? request.url;
|
|
1011
|
+
const status = responseStatus(response);
|
|
1012
|
+
if (status === void 0 || status < 200 || status >= 300) {
|
|
1013
|
+
sink.skipped.push({ path: url, reason: status === void 0 ? "no HTTP response" : `HTTP ${status}` });
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
const type = contentType?.type ?? "";
|
|
1017
|
+
if (type === "text/html" || type === "application/xhtml+xml") {
|
|
1018
|
+
const html = typeof body === "string" ? body : toText(body);
|
|
1019
|
+
const page = extractPage(html);
|
|
1020
|
+
if (page.likelyShell) {
|
|
1021
|
+
sink.shellUrls?.push(url);
|
|
1022
|
+
sink.skipped.push({ path: url, reason: "likely JS-rendered shell" });
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
if (!page.markdown.trim()) {
|
|
1026
|
+
sink.skipped.push({ path: url, reason: "empty" });
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
const doc = buildDoc(url, "html", page.markdown, page, sinkDeps(opts));
|
|
1030
|
+
if (!doc) {
|
|
1031
|
+
sink.skipped.push({ path: url, reason: "no chunks produced" });
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
sink.docs.push(doc);
|
|
1035
|
+
await ctx.enqueueLinks({ strategy: "same-hostname", exclude: [BINARY_LINK_RE] });
|
|
1036
|
+
} else if (type === "text/markdown") {
|
|
1037
|
+
const doc = buildDoc(url, "md", toText(body), void 0, sinkDeps(opts));
|
|
1038
|
+
if (!doc) {
|
|
1039
|
+
sink.skipped.push({ path: url, reason: "no chunks produced" });
|
|
1040
|
+
return;
|
|
1041
|
+
}
|
|
1042
|
+
sink.docs.push(doc);
|
|
1043
|
+
} else if (type === "text/plain") {
|
|
1044
|
+
const doc = buildDoc(url, "plain", toText(body), void 0, sinkDeps(opts));
|
|
1045
|
+
if (!doc) {
|
|
1046
|
+
sink.skipped.push({ path: url, reason: "no chunks produced" });
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
sink.docs.push(doc);
|
|
1050
|
+
} else {
|
|
1051
|
+
sink.skipped.push({ path: url, reason: `unsupported content-type '${type}'` });
|
|
1052
|
+
}
|
|
1053
|
+
},
|
|
1054
|
+
async failedRequestHandler(ctx, error) {
|
|
1055
|
+
const url = ctx.request.loadedUrl ?? ctx.request.url;
|
|
1056
|
+
sink.skipped.push({ path: url, reason: shortReason(error) });
|
|
668
1057
|
}
|
|
669
|
-
|
|
1058
|
+
});
|
|
1059
|
+
try {
|
|
1060
|
+
await crawler.run(seedUrls);
|
|
1061
|
+
} finally {
|
|
1062
|
+
await crawler.teardown().catch(() => {
|
|
1063
|
+
});
|
|
670
1064
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
1065
|
+
}
|
|
1066
|
+
function sinkDeps(opts) {
|
|
1067
|
+
return { maxLen: opts.maxLen, overlap: opts.overlap, cliTags: opts.tags };
|
|
1068
|
+
}
|
|
1069
|
+
async function importOptional(name) {
|
|
1070
|
+
try {
|
|
1071
|
+
return await import(name);
|
|
1072
|
+
} catch {
|
|
1073
|
+
return void 0;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
async function requireBrowserEngine(kind) {
|
|
1077
|
+
const moduleName = kind === "playwright" ? "playwright" : "puppeteer";
|
|
1078
|
+
const mod = await importOptional(moduleName);
|
|
1079
|
+
if (!mod) {
|
|
1080
|
+
const install = kind === "playwright" ? "npm i -g playwright && npx playwright install chromium" : "npm i -g puppeteer";
|
|
1081
|
+
throw new Error(`--crawler ${kind} requires ${moduleName}, which is not installed (install it with: ${install})`);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
async function handleBrowserPage(ctx, deps) {
|
|
1085
|
+
const request = ctx.request;
|
|
1086
|
+
const url = request.loadedUrl ?? request.url;
|
|
1087
|
+
const status = responseStatus(ctx.response);
|
|
1088
|
+
if (status === void 0 || status < 200 || status >= 300) {
|
|
1089
|
+
deps.sink.skipped.push({ path: url, reason: status === void 0 ? "no HTTP response" : `HTTP ${status}` });
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
const html = await ctx.page.content();
|
|
1093
|
+
const page = extractPage(html);
|
|
1094
|
+
if (!page.markdown.trim()) {
|
|
1095
|
+
deps.sink.skipped.push({ path: url, reason: "empty after render" });
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
const doc = buildDoc(url, "html", page.markdown, page, deps);
|
|
1099
|
+
if (!doc) {
|
|
1100
|
+
deps.sink.skipped.push({ path: url, reason: "no chunks produced" });
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
deps.sink.docs.push(doc);
|
|
1104
|
+
await ctx.enqueueLinks({ strategy: "same-hostname", exclude: [BINARY_LINK_RE] });
|
|
1105
|
+
}
|
|
1106
|
+
async function crawlWithBrowser(kind, queue, seedUrls, opts, sink) {
|
|
1107
|
+
const crawler = kind === "playwright" ? new PlaywrightCrawler({
|
|
1108
|
+
requestQueue: queue,
|
|
1109
|
+
maxRequestsPerCrawl: Math.max(1, opts.maxPages),
|
|
1110
|
+
maxCrawlDepth: Math.max(0, opts.depth),
|
|
1111
|
+
preNavigationHooks: [
|
|
1112
|
+
(_ctx, gotoOptions) => {
|
|
1113
|
+
gotoOptions.waitUntil = "networkidle";
|
|
1114
|
+
gotoOptions.timeout = 3e4;
|
|
1115
|
+
}
|
|
1116
|
+
],
|
|
1117
|
+
requestHandler: (ctx) => handleBrowserPage(ctx, { ...sinkDeps(opts), sink }),
|
|
1118
|
+
failedRequestHandler: (ctx, error) => {
|
|
1119
|
+
const request = ctx.request;
|
|
1120
|
+
sink.skipped.push({ path: request.loadedUrl ?? request.url, reason: shortReason(error) });
|
|
1121
|
+
}
|
|
1122
|
+
}) : new PuppeteerCrawler({
|
|
1123
|
+
requestQueue: queue,
|
|
1124
|
+
maxRequestsPerCrawl: Math.max(1, opts.maxPages),
|
|
1125
|
+
maxCrawlDepth: Math.max(0, opts.depth),
|
|
1126
|
+
preNavigationHooks: [
|
|
1127
|
+
(_ctx, gotoOptions) => {
|
|
1128
|
+
gotoOptions.waitUntil = "networkidle2";
|
|
1129
|
+
gotoOptions.timeout = 3e4;
|
|
1130
|
+
}
|
|
1131
|
+
],
|
|
1132
|
+
requestHandler: (ctx) => handleBrowserPage(ctx, { ...sinkDeps(opts), sink }),
|
|
1133
|
+
failedRequestHandler: (ctx, error) => {
|
|
1134
|
+
const request = ctx.request;
|
|
1135
|
+
sink.skipped.push({ path: request.loadedUrl ?? request.url, reason: shortReason(error) });
|
|
1136
|
+
}
|
|
676
1137
|
});
|
|
1138
|
+
try {
|
|
1139
|
+
await crawler.run(seedUrls);
|
|
1140
|
+
} finally {
|
|
1141
|
+
await crawler.teardown().catch(() => {
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
async function crawlWebsite(seed, opts) {
|
|
1146
|
+
const seedUrl = normalizeSeedUrl(seed);
|
|
1147
|
+
const kind = opts.crawler;
|
|
1148
|
+
if (kind === "playwright" || kind === "puppeteer") {
|
|
1149
|
+
await requireBrowserEngine(kind);
|
|
1150
|
+
const sink2 = { docs: [], skipped: [] };
|
|
1151
|
+
const queue = await freshQueue();
|
|
1152
|
+
await crawlWithBrowser(kind, queue, [seedUrl], opts, sink2);
|
|
1153
|
+
return { docs: dedupeDocs(sink2.docs), skipped: sink2.skipped };
|
|
1154
|
+
}
|
|
1155
|
+
const shellUrls = [];
|
|
1156
|
+
const sink = { docs: [], skipped: [], shellUrls };
|
|
1157
|
+
const staticQueue = await freshQueue();
|
|
1158
|
+
await crawlWithCheerio(staticQueue, [seedUrl], opts, sink);
|
|
1159
|
+
if (kind !== "auto" || shellUrls.length === 0) {
|
|
1160
|
+
return { docs: dedupeDocs(sink.docs), skipped: sink.skipped };
|
|
1161
|
+
}
|
|
1162
|
+
opts.log(
|
|
1163
|
+
`${shellUrls.length} page(s) look JS-rendered; retrying with playwright (install: npm i -g playwright && npx playwright install chromium) ...`
|
|
1164
|
+
);
|
|
1165
|
+
const mod = await importOptional("playwright");
|
|
1166
|
+
if (!mod) {
|
|
1167
|
+
return { docs: dedupeDocs(sink.docs), skipped: sink.skipped };
|
|
1168
|
+
}
|
|
1169
|
+
const browserSink = { docs: [], skipped: [] };
|
|
1170
|
+
const browserQueue = await freshQueue();
|
|
1171
|
+
await crawlWithBrowser("playwright", browserQueue, shellUrls, opts, browserSink);
|
|
677
1172
|
return {
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
inserted,
|
|
681
|
-
skippedExisting,
|
|
682
|
-
deletedForReplace,
|
|
683
|
-
skippedFiles: [],
|
|
684
|
-
dryRun: opts.dryRun ?? false
|
|
1173
|
+
docs: dedupeDocs([...sink.docs, ...browserSink.docs]),
|
|
1174
|
+
skipped: [...sink.skipped, ...browserSink.skipped]
|
|
685
1175
|
};
|
|
686
1176
|
}
|
|
1177
|
+
async function feedUrl(ks, url, opts = {}) {
|
|
1178
|
+
const log2 = opts.log ?? ((m) => console.log(m));
|
|
1179
|
+
const kind = opts.crawler ?? "auto";
|
|
1180
|
+
if (!CRAWLER_KINDS.includes(kind)) {
|
|
1181
|
+
throw new Error(`unknown crawler '${kind}' (use auto, crawlee, playwright or puppeteer)`);
|
|
1182
|
+
}
|
|
1183
|
+
const maxPages = opts.maxPages && opts.maxPages > 0 ? Math.floor(opts.maxPages) : 999;
|
|
1184
|
+
const depth = typeof opts.depth === "number" && Number.isFinite(opts.depth) && opts.depth >= 0 ? Math.floor(opts.depth) : 5;
|
|
1185
|
+
const maxLen = opts.maxLen ?? 1200;
|
|
1186
|
+
const overlap = opts.overlap ?? 180;
|
|
1187
|
+
const { docs, skipped } = await crawlWebsite(url, {
|
|
1188
|
+
crawler: kind,
|
|
1189
|
+
maxPages,
|
|
1190
|
+
depth,
|
|
1191
|
+
maxLen,
|
|
1192
|
+
overlap,
|
|
1193
|
+
tags: opts.tags,
|
|
1194
|
+
log: log2
|
|
1195
|
+
});
|
|
1196
|
+
for (const s of skipped) {
|
|
1197
|
+
log2(`skipped ${s.path}: ${s.reason}`);
|
|
1198
|
+
}
|
|
1199
|
+
const progress = opts.progress ?? new FeedProgress({ total: docs.length, rejected: skipped.length });
|
|
1200
|
+
const summary = await feedDocs(ks, docs, {
|
|
1201
|
+
replace: opts.replace,
|
|
1202
|
+
dryRun: opts.dryRun,
|
|
1203
|
+
batchSize: opts.batchSize,
|
|
1204
|
+
tags: opts.tags,
|
|
1205
|
+
metadata: opts.metadata,
|
|
1206
|
+
log: log2,
|
|
1207
|
+
progress
|
|
1208
|
+
});
|
|
1209
|
+
return { ...summary, skippedFiles: [...summary.skippedFiles, ...skipped] };
|
|
1210
|
+
}
|
|
1211
|
+
try {
|
|
1212
|
+
if (typeof log?.setLevel === "function" && log.LEVELS) {
|
|
1213
|
+
log.setLevel(log.LEVELS.ERROR);
|
|
1214
|
+
}
|
|
1215
|
+
} catch {
|
|
1216
|
+
}
|
|
687
1217
|
|
|
688
1218
|
// src/check.ts
|
|
689
1219
|
init_config();
|
|
@@ -754,7 +1284,7 @@ async function checkOneModel(id, model, baseUrl, names, ks, probe) {
|
|
|
754
1284
|
}
|
|
755
1285
|
return { id, status: "ok", detail: `model '${model}' available at ${baseUrl}${scope}` };
|
|
756
1286
|
}
|
|
757
|
-
async function runChecks(opts
|
|
1287
|
+
async function runChecks(opts) {
|
|
758
1288
|
const results = [];
|
|
759
1289
|
const major = Number(process.versions.node.split(".")[0]);
|
|
760
1290
|
results.push({
|
|
@@ -876,44 +1406,35 @@ async function runChecks(opts = {}) {
|
|
|
876
1406
|
results.push(
|
|
877
1407
|
"error" in defaultTags ? { id: "ollama", status: "fail", detail: `Ollama not reachable at ${defaultUrl} \u2014 ${defaultTags.error}` } : { id: "ollama", status: "ok", detail: `Ollama reachable at ${defaultUrl} (${defaultTags.names.length} model(s))` }
|
|
878
1408
|
);
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
results.push(await checkOneModel("model", opts.model, defaultUrl, tags.names, void 0, opts.probe ?? false));
|
|
889
|
-
}
|
|
1409
|
+
const tags = await getTags(defaultUrl);
|
|
1410
|
+
if ("error" in tags) {
|
|
1411
|
+
results.push({
|
|
1412
|
+
id: "model",
|
|
1413
|
+
status: "skip",
|
|
1414
|
+
detail: `Ollama unreachable at ${defaultUrl} \u2014 cannot verify model '${opts.model}'`
|
|
1415
|
+
});
|
|
1416
|
+
} else {
|
|
1417
|
+
results.push(await checkOneModel("model", opts.model, defaultUrl, tags.names, void 0, opts.probe ?? false));
|
|
890
1418
|
}
|
|
891
1419
|
for (const ks of kss) {
|
|
892
|
-
const
|
|
893
|
-
if ("error" in
|
|
1420
|
+
const tags2 = await getTags(ks.ollama_url);
|
|
1421
|
+
if ("error" in tags2) {
|
|
894
1422
|
results.push({
|
|
895
1423
|
id: `model:${ks.name}`,
|
|
896
1424
|
status: "fail",
|
|
897
|
-
detail: `Ollama not reachable at ${ks.ollama_url} (required by ks '${ks.name}') \u2014 ${
|
|
1425
|
+
detail: `Ollama not reachable at ${ks.ollama_url} (required by ks '${ks.name}') \u2014 ${tags2.error}`
|
|
898
1426
|
});
|
|
899
1427
|
continue;
|
|
900
1428
|
}
|
|
901
1429
|
results.push(
|
|
902
|
-
await checkOneModel(`model:${ks.name}`, ks.embedding_model, ks.ollama_url,
|
|
1430
|
+
await checkOneModel(`model:${ks.name}`, ks.embedding_model, ks.ollama_url, tags2.names, ks, opts.probe ?? false)
|
|
903
1431
|
);
|
|
904
1432
|
}
|
|
905
|
-
if (!opts.model && kss.length === 0) {
|
|
906
|
-
results.push({
|
|
907
|
-
id: "model",
|
|
908
|
-
status: "warn",
|
|
909
|
-
detail: "no embedding model to verify \u2014 pass --model <model> or create a knowledge source"
|
|
910
|
-
});
|
|
911
|
-
}
|
|
912
1433
|
return results;
|
|
913
1434
|
}
|
|
914
1435
|
|
|
915
1436
|
// src/version.ts
|
|
916
|
-
var VERSION = "1.
|
|
1437
|
+
var VERSION = "1.1.0";
|
|
917
1438
|
|
|
918
1439
|
// src/cli.ts
|
|
919
1440
|
async function run(fn) {
|
|
@@ -966,7 +1487,7 @@ program.command("init").description("Initialize the database schema (extension,
|
|
|
966
1487
|
console.log("database ready");
|
|
967
1488
|
})
|
|
968
1489
|
);
|
|
969
|
-
program.command("check").description("Verify environment requirements (Node, Postgres, pgvector, schema, Ollama models)").
|
|
1490
|
+
program.command("check").description("Verify environment requirements (Node, Postgres, pgvector, schema, Ollama models)").requiredOption("--model <model>", "Verify this Ollama model is available (e.g. nomic-embed-text)").option("--url <url>", "Ollama base URL (default http://localhost:11434)").option("--probe", "Embed a test string with each model to verify dimensions (slower)").option("--json", "Output as JSON").action(
|
|
970
1491
|
(opts) => run(async () => {
|
|
971
1492
|
const results = await runChecks({ model: opts.model, url: opts.url, probe: opts.probe });
|
|
972
1493
|
if (opts.json) {
|
|
@@ -1044,6 +1565,34 @@ ksCmd.command("list").description("List knowledge sources").option("--json", "Ou
|
|
|
1044
1565
|
}
|
|
1045
1566
|
})
|
|
1046
1567
|
);
|
|
1568
|
+
ksCmd.command("stats <name>").description("Show statistics for a knowledge source (entries, sources, content volume)").option("--json", "Output as JSON").action(
|
|
1569
|
+
(name, opts) => run(async () => {
|
|
1570
|
+
await initDb();
|
|
1571
|
+
const ks = requireKs(await getKs(name), name);
|
|
1572
|
+
const stats = await ksStats(ks);
|
|
1573
|
+
if (opts.json) {
|
|
1574
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
console.log(
|
|
1578
|
+
`Knowledge source '${stats.ks.name}' (model=${stats.ks.embedding_model}@${stats.ks.ollama_url}, dim=${stats.ks.embedding_dim})`
|
|
1579
|
+
);
|
|
1580
|
+
if (stats.ks.description) console.log(` description: ${stats.ks.description}`);
|
|
1581
|
+
console.log(` created: ${stats.ks.created_at.toISOString()}`);
|
|
1582
|
+
console.log(` last fed: ${stats.last_fed_at ? stats.last_fed_at.toISOString() : "never"}`);
|
|
1583
|
+
console.log("");
|
|
1584
|
+
if (stats.entries === 0) {
|
|
1585
|
+
console.log(` no entries yet; feed it with: gresmcp feed ${stats.ks.name}`);
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
const size = (n) => n === null ? "-" : n.toLocaleString("en-US");
|
|
1589
|
+
console.log(` entries (chunks): ${stats.entries.toLocaleString("en-US")}`);
|
|
1590
|
+
console.log(` distinct sources: ${stats.sources.toLocaleString("en-US")}`);
|
|
1591
|
+
console.log(
|
|
1592
|
+
` content: ${stats.chars.toLocaleString("en-US")} chars (avg ${size(stats.avg_chunk_chars)}, min ${size(stats.min_chunk_chars)}, max ${size(stats.max_chunk_chars)} per chunk)`
|
|
1593
|
+
);
|
|
1594
|
+
})
|
|
1595
|
+
);
|
|
1047
1596
|
ksCmd.command("delete <name>").description("Delete a knowledge source and all of its entries").option("--yes", "Do not ask for confirmation").action(
|
|
1048
1597
|
(name, opts) => run(async () => {
|
|
1049
1598
|
await initDb();
|
|
@@ -1060,15 +1609,13 @@ ksCmd.command("delete <name>").description("Delete a knowledge source and all of
|
|
|
1060
1609
|
console.log(`Deleted knowledge source '${name}'`);
|
|
1061
1610
|
})
|
|
1062
1611
|
);
|
|
1063
|
-
program.command("feed <ks>").description("Feed data into a knowledge source (from a file/folder via --path, or inline via --text/--stdin)").option("--path <path>", "File or folder to ingest (text, markdown, code, html)").option("--text <text>", "Inline text entry").option("--stdin", "Read entry text from stdin").option("--title <title>", "Title for manual entries").option("--source-name <name>", "Source name for manual entries (default 'manual')").option("--tags <tags>", "Comma-separated tags").option("--metadata <key=value...>", "Custom metadata entries", ((v, prev) => [...prev, v]), []).option("--
|
|
1612
|
+
program.command("feed <ks>").description("Feed data into a knowledge source (from a file/folder via --path, a website via --url, or inline via --text/--stdin)").option("--path <path>", "File or folder to ingest (text, markdown, code, html)").option("--url <url>", "Website URL to scrape and ingest (crawls same-host pages linked from the seed)").option("--crawler <crawler>", "Crawler backend for --url: auto, crawlee, playwright, puppeteer (default auto; playwright/puppeteer must be installed globally)", ((v) => v.toLowerCase())).option("--max-pages <n>", "Max pages to fetch when crawling --url (default 999)", parseInt).option("--depth <n>", "Max link depth from the seed URL (default 5; 0 = seed page only)", parseInt).option("--text <text>", "Inline text entry").option("--stdin", "Read entry text from stdin").option("--title <title>", "Title for manual entries").option("--source-name <name>", "Source name for manual entries (default 'manual')").option("--tags <tags>", "Comma-separated tags").option("--metadata <key=value...>", "Custom metadata entries", ((v, prev) => [...prev, v]), []).option("--replace", "Replace existing chunks of the same source instead of deduplicating").option("--dry-run", "Parse and chunk only; do not embed or write").option("--chunk-size <n>", "Max chunk length in characters", parseInt).option("--overlap <n>", "Chunk overlap in characters", parseInt).action(
|
|
1064
1613
|
(name, opts) => run(async () => {
|
|
1065
1614
|
await initDb();
|
|
1066
1615
|
const ks = requireKs(await getKs(name), name);
|
|
1067
|
-
const modes = [opts.path, opts.text, opts.stdin].filter(Boolean).length;
|
|
1068
|
-
if (modes !== 1) throw new Error("provide exactly one of --path, --text or --stdin");
|
|
1616
|
+
const modes = [opts.path, opts.url, opts.text, opts.stdin].filter(Boolean).length;
|
|
1617
|
+
if (modes !== 1) throw new Error("provide exactly one of --path, --url, --text or --stdin");
|
|
1069
1618
|
const common = {
|
|
1070
|
-
title: opts.title,
|
|
1071
|
-
url: opts.url,
|
|
1072
1619
|
replace: opts.replace,
|
|
1073
1620
|
dryRun: opts.dryRun,
|
|
1074
1621
|
batchSize: 32,
|
|
@@ -1078,12 +1625,19 @@ program.command("feed <ks>").description("Feed data into a knowledge source (fro
|
|
|
1078
1625
|
metadata: parseKeyValue(opts.metadata ?? [])
|
|
1079
1626
|
};
|
|
1080
1627
|
let summary;
|
|
1628
|
+
const since = await dbClockTimestamp();
|
|
1081
1629
|
if (opts.path) {
|
|
1082
1630
|
summary = await feedTarget(ks, opts.path, common);
|
|
1631
|
+
} else if (opts.url) {
|
|
1632
|
+
const crawler = opts.crawler;
|
|
1633
|
+
if (crawler && !CRAWLER_KINDS.includes(crawler)) {
|
|
1634
|
+
throw new Error(`unknown crawler '${crawler}' (use auto, crawlee, playwright or puppeteer)`);
|
|
1635
|
+
}
|
|
1636
|
+
summary = await feedUrl(ks, opts.url, { ...common, crawler, maxPages: opts.maxPages, depth: opts.depth });
|
|
1083
1637
|
} else {
|
|
1084
1638
|
const text = opts.stdin ? await readStdin() : opts.text;
|
|
1085
1639
|
if (!text.trim()) throw new Error("empty input text");
|
|
1086
|
-
summary = await feedText(ks, text, { ...common, sourceName: opts.sourceName });
|
|
1640
|
+
summary = await feedText(ks, text, { ...common, title: opts.title, sourceName: opts.sourceName });
|
|
1087
1641
|
}
|
|
1088
1642
|
if (summary.dryRun) {
|
|
1089
1643
|
console.log(`dry run: ${summary.documents} document(s), ${summary.chunks} chunk(s), ${summary.skippedFiles.length} file(s) skipped`);
|
|
@@ -1091,9 +1645,22 @@ program.command("feed <ks>").description("Feed data into a knowledge source (fro
|
|
|
1091
1645
|
console.log(
|
|
1092
1646
|
`fed '${ks.name}': ${summary.inserted} chunk(s) inserted` + (summary.skippedExisting ? `, ${summary.skippedExisting} unchanged chunk(s) skipped` : "") + (summary.deletedForReplace ? `, ${summary.deletedForReplace} old chunk(s) replaced` : "") + `, ${summary.skippedFiles.length} file(s) skipped`
|
|
1093
1647
|
);
|
|
1648
|
+
await printRunStats(ks, since);
|
|
1094
1649
|
}
|
|
1095
1650
|
})
|
|
1096
1651
|
);
|
|
1652
|
+
async function printRunStats(ks, since) {
|
|
1653
|
+
const stats = await ksStats(ks, since);
|
|
1654
|
+
if (stats.entries === 0) return;
|
|
1655
|
+
const size = (n) => n === null ? "-" : n.toLocaleString("en-US");
|
|
1656
|
+
console.log("");
|
|
1657
|
+
console.log(
|
|
1658
|
+
`this run: ${stats.entries.toLocaleString("en-US")} chunk(s) across ${stats.sources.toLocaleString("en-US")} source(s)`
|
|
1659
|
+
);
|
|
1660
|
+
console.log(
|
|
1661
|
+
` content: ${stats.chars.toLocaleString("en-US")} chars (avg ${size(stats.avg_chunk_chars)}, min ${size(stats.min_chunk_chars)}, max ${size(stats.max_chunk_chars)} per chunk)`
|
|
1662
|
+
);
|
|
1663
|
+
}
|
|
1097
1664
|
async function readStdin() {
|
|
1098
1665
|
const chunks = [];
|
|
1099
1666
|
for await (const chunk of stdin) chunks.push(chunk);
|