@gresmcp/mcp 1.1.0 → 1.2.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 +15 -3
- package/dist/cli.js +392 -125
- package/dist/cli.js.map +1 -1
- package/dist/mcp.js +1 -1
- package/dist/mcp.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -446,17 +446,57 @@ function turndown() {
|
|
|
446
446
|
}
|
|
447
447
|
return service;
|
|
448
448
|
}
|
|
449
|
-
function
|
|
449
|
+
function parseHtmlFilter(spec) {
|
|
450
|
+
const steps = spec.split(/\s*->\s*/).map((s) => s.trim()).filter(Boolean);
|
|
451
|
+
if (steps.length === 0) {
|
|
452
|
+
throw new Error(`invalid --html-filter '${spec.trim()}': expected a CSS selector chain, e.g. '#article -> .content'`);
|
|
453
|
+
}
|
|
454
|
+
return steps;
|
|
455
|
+
}
|
|
456
|
+
function validateCssSelector(selector) {
|
|
457
|
+
const dom = new JSDOM();
|
|
458
|
+
try {
|
|
459
|
+
dom.window.document.querySelector(selector);
|
|
460
|
+
} catch (err) {
|
|
461
|
+
throw new Error(`invalid CSS selector '${selector}': ${err instanceof Error ? err.message : err}`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
function selectChain(scope, steps) {
|
|
465
|
+
for (const step of steps.slice(0, -1)) {
|
|
466
|
+
const next = scope.querySelector(step);
|
|
467
|
+
if (!next) return void 0;
|
|
468
|
+
scope = next;
|
|
469
|
+
}
|
|
470
|
+
const matches = [...scope.querySelectorAll(steps[steps.length - 1])];
|
|
471
|
+
return matches.length > 0 ? matches : void 0;
|
|
472
|
+
}
|
|
473
|
+
function elementsToMarkdown(elements) {
|
|
474
|
+
const parts = [];
|
|
475
|
+
for (const el of elements) {
|
|
476
|
+
try {
|
|
477
|
+
parts.push(turndown().turndown(el.innerHTML));
|
|
478
|
+
} catch {
|
|
479
|
+
parts.push(el.textContent ?? "");
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return parts.join("\n\n");
|
|
483
|
+
}
|
|
484
|
+
function htmlToMarkdown(html, opts = {}) {
|
|
450
485
|
const dom = new JSDOM(html);
|
|
451
486
|
const doc = dom.window.document;
|
|
452
487
|
doc.querySelectorAll(REMOVE_SELECTORS).forEach((el) => el.remove());
|
|
453
488
|
const title = doc.title?.trim() || doc.querySelector("h1")?.textContent?.trim() || void 0;
|
|
454
|
-
const root = doc.body ?? doc.documentElement;
|
|
455
489
|
let markdown = "";
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
490
|
+
if (opts.filter) {
|
|
491
|
+
const elements = selectChain(doc, parseHtmlFilter(opts.filter));
|
|
492
|
+
if (elements) markdown = elementsToMarkdown(elements);
|
|
493
|
+
} else {
|
|
494
|
+
const root = doc.body ?? doc.documentElement;
|
|
495
|
+
try {
|
|
496
|
+
markdown = turndown().turndown(root.innerHTML);
|
|
497
|
+
} catch {
|
|
498
|
+
markdown = root.textContent ?? "";
|
|
499
|
+
}
|
|
460
500
|
}
|
|
461
501
|
markdown = markdown.replace(/\n{3,}/g, "\n\n").trim();
|
|
462
502
|
return { markdown, title: title || void 0 };
|
|
@@ -490,7 +530,7 @@ function collectTags(doc) {
|
|
|
490
530
|
return true;
|
|
491
531
|
});
|
|
492
532
|
}
|
|
493
|
-
function extractPage(html) {
|
|
533
|
+
function extractPage(html, opts = {}) {
|
|
494
534
|
const dom = new JSDOM(html);
|
|
495
535
|
const doc = dom.window.document;
|
|
496
536
|
const title = doc.title?.trim() || doc.querySelector('meta[property="og:title"]')?.getAttribute("content")?.trim() || doc.querySelector("h1")?.textContent?.trim() || void 0;
|
|
@@ -501,20 +541,27 @@ function extractPage(html) {
|
|
|
501
541
|
const tags = collectTags(doc);
|
|
502
542
|
doc.querySelectorAll(REMOVE_SELECTORS).forEach((el) => el.remove());
|
|
503
543
|
let markdown = "";
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
}
|
|
510
|
-
markdown = "";
|
|
511
|
-
}
|
|
512
|
-
if (!markdown.trim()) {
|
|
513
|
-
const root = doc.body ?? doc.documentElement;
|
|
544
|
+
let filterMatched;
|
|
545
|
+
if (opts.filter) {
|
|
546
|
+
const elements = selectChain(doc, parseHtmlFilter(opts.filter));
|
|
547
|
+
filterMatched = elements !== void 0;
|
|
548
|
+
if (elements) markdown = elementsToMarkdown(elements);
|
|
549
|
+
} else {
|
|
514
550
|
try {
|
|
515
|
-
|
|
551
|
+
if (isProbablyReaderable(doc)) {
|
|
552
|
+
const article = new Readability(doc).parse();
|
|
553
|
+
if (article?.content) markdown = turndown().turndown(article.content);
|
|
554
|
+
}
|
|
516
555
|
} catch {
|
|
517
|
-
markdown =
|
|
556
|
+
markdown = "";
|
|
557
|
+
}
|
|
558
|
+
if (!markdown.trim()) {
|
|
559
|
+
const root = doc.body ?? doc.documentElement;
|
|
560
|
+
try {
|
|
561
|
+
markdown = turndown().turndown(root.innerHTML);
|
|
562
|
+
} catch {
|
|
563
|
+
markdown = root.textContent ?? "";
|
|
564
|
+
}
|
|
518
565
|
}
|
|
519
566
|
}
|
|
520
567
|
markdown = markdown.replace(/\n{3,}/g, "\n\n").trim();
|
|
@@ -524,7 +571,8 @@ function extractPage(html) {
|
|
|
524
571
|
title,
|
|
525
572
|
description,
|
|
526
573
|
tags: tags.length > 0 ? tags : void 0,
|
|
527
|
-
likelyShell
|
|
574
|
+
likelyShell,
|
|
575
|
+
filterMatched
|
|
528
576
|
};
|
|
529
577
|
}
|
|
530
578
|
|
|
@@ -544,14 +592,16 @@ function bar(pct2) {
|
|
|
544
592
|
function pct(p) {
|
|
545
593
|
return `${Math.round(Math.max(0, Math.min(1, p)) * 100)}%`;
|
|
546
594
|
}
|
|
595
|
+
var NOOP_HANDLE = { handled: () => {
|
|
596
|
+
} };
|
|
547
597
|
var FeedProgress = class {
|
|
548
598
|
write;
|
|
549
599
|
interactive;
|
|
550
600
|
total;
|
|
551
601
|
rejected;
|
|
602
|
+
slots = [];
|
|
552
603
|
filesDone = 0;
|
|
553
604
|
totalChars = 0;
|
|
554
|
-
current;
|
|
555
605
|
blockLines = 0;
|
|
556
606
|
finished = false;
|
|
557
607
|
constructor(opts) {
|
|
@@ -564,26 +614,13 @@ var FeedProgress = class {
|
|
|
564
614
|
return this.interactive;
|
|
565
615
|
}
|
|
566
616
|
startFile(source, totalChunks, totalChars) {
|
|
567
|
-
if (this.finished) return;
|
|
568
|
-
|
|
617
|
+
if (this.finished) return NOOP_HANDLE;
|
|
618
|
+
const slot = { source, totalChunks, totalChars, doneChunks: 0, doneChars: 0, printedChunks: 0 };
|
|
619
|
+
this.slots.push(slot);
|
|
569
620
|
this.render();
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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
|
-
}
|
|
621
|
+
return {
|
|
622
|
+
handled: (chunks, chars) => this.handleSlot(slot, chunks, chars)
|
|
623
|
+
};
|
|
587
624
|
}
|
|
588
625
|
log(msg) {
|
|
589
626
|
if (this.finished) return;
|
|
@@ -594,9 +631,9 @@ var FeedProgress = class {
|
|
|
594
631
|
finish() {
|
|
595
632
|
if (this.finished) return;
|
|
596
633
|
this.finished = true;
|
|
597
|
-
if (
|
|
634
|
+
if (this.slots.length === 0 && this.total === 0 && this.rejected === 0) return;
|
|
598
635
|
if (this.interactive) {
|
|
599
|
-
this.redraw(
|
|
636
|
+
this.redraw(this.lines());
|
|
600
637
|
this.write("\n");
|
|
601
638
|
this.blockLines = 0;
|
|
602
639
|
} else {
|
|
@@ -609,25 +646,42 @@ var FeedProgress = class {
|
|
|
609
646
|
this.finished = true;
|
|
610
647
|
this.clearBlock();
|
|
611
648
|
}
|
|
649
|
+
lines() {
|
|
650
|
+
return [...this.slots.map((slot) => this.fileLine(slot)), this.totalLine()];
|
|
651
|
+
}
|
|
652
|
+
handleSlot(slot, chunks, chars) {
|
|
653
|
+
if (this.finished || chunks <= 0 || slot.doneChunks >= slot.totalChunks) return;
|
|
654
|
+
slot.doneChunks += chunks;
|
|
655
|
+
slot.doneChars += chars;
|
|
656
|
+
this.totalChars += chars;
|
|
657
|
+
if (slot.doneChunks >= slot.totalChunks) {
|
|
658
|
+
this.filesDone++;
|
|
659
|
+
if (this.interactive) {
|
|
660
|
+
this.render();
|
|
661
|
+
} else {
|
|
662
|
+
this.printFileLine(slot);
|
|
663
|
+
}
|
|
664
|
+
this.slots.splice(this.slots.indexOf(slot), 1);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
if (!this.interactive && slot.doneChunks - slot.printedChunks >= PLAIN_STEP) {
|
|
668
|
+
this.printFileLine(slot);
|
|
669
|
+
} else if (this.interactive) {
|
|
670
|
+
this.render();
|
|
671
|
+
}
|
|
672
|
+
}
|
|
612
673
|
render() {
|
|
613
674
|
if (!this.interactive) return;
|
|
614
|
-
this.redraw(
|
|
675
|
+
this.redraw(this.lines());
|
|
615
676
|
}
|
|
616
|
-
printFileLine() {
|
|
617
|
-
|
|
618
|
-
this.
|
|
619
|
-
if (this.interactive) {
|
|
620
|
-
this.render();
|
|
621
|
-
} else {
|
|
622
|
-
this.write(`${this.fileLine()}
|
|
677
|
+
printFileLine(slot) {
|
|
678
|
+
slot.printedChunks = slot.doneChunks;
|
|
679
|
+
this.write(`${this.fileLine(slot)}
|
|
623
680
|
`);
|
|
624
|
-
}
|
|
625
681
|
}
|
|
626
|
-
fileLine() {
|
|
627
|
-
const
|
|
628
|
-
|
|
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}`;
|
|
682
|
+
fileLine(slot) {
|
|
683
|
+
const p = slot.totalChunks > 0 ? slot.doneChunks / slot.totalChunks : 1;
|
|
684
|
+
return `[${bar(p)}] ${pct(p).padStart(4)} ${fmt(slot.doneChunks)}/${fmt(slot.totalChunks)} chunks ${fmt(slot.doneChars)}/${fmt(slot.totalChars)} chars ${slot.source}`;
|
|
631
685
|
}
|
|
632
686
|
totalLine() {
|
|
633
687
|
const p = this.total > 0 ? this.filesDone / this.total : 0;
|
|
@@ -792,14 +846,17 @@ async function loadTarget(target, opts) {
|
|
|
792
846
|
let text = raw;
|
|
793
847
|
let title = "";
|
|
794
848
|
if (HTML_EXT.has(ext)) {
|
|
795
|
-
const conv = htmlToMarkdown(raw);
|
|
849
|
+
const conv = htmlToMarkdown(raw, { filter: opts.htmlFilter });
|
|
796
850
|
text = conv.markdown;
|
|
797
851
|
title = conv.title ?? "";
|
|
798
852
|
} else if (MD_EXT.has(ext)) {
|
|
799
853
|
title = mdTitle(raw) ?? "";
|
|
800
854
|
}
|
|
801
855
|
if (!text.trim()) {
|
|
802
|
-
skipped.push({
|
|
856
|
+
skipped.push({
|
|
857
|
+
path: file,
|
|
858
|
+
reason: HTML_EXT.has(ext) && opts.htmlFilter ? `html filter '${opts.htmlFilter}' matched nothing` : "empty"
|
|
859
|
+
});
|
|
803
860
|
continue;
|
|
804
861
|
}
|
|
805
862
|
if (!title) title = path.basename(file);
|
|
@@ -812,6 +869,7 @@ async function loadTarget(target, opts) {
|
|
|
812
869
|
}
|
|
813
870
|
return { docs, fileCount: files.length, skipped };
|
|
814
871
|
}
|
|
872
|
+
var DOC_CONCURRENCY = 3;
|
|
815
873
|
async function feedDocs(ks, docs, opts = {}) {
|
|
816
874
|
const log2 = opts.log ?? ((m) => console.log(m));
|
|
817
875
|
const progress = opts.progress;
|
|
@@ -823,61 +881,78 @@ async function feedDocs(ks, docs, opts = {}) {
|
|
|
823
881
|
let skippedExisting = 0;
|
|
824
882
|
let inserted = 0;
|
|
825
883
|
let announcedEmbed = false;
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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);
|
|
884
|
+
const processDoc = async (doc) => {
|
|
885
|
+
const file = progress?.startFile(
|
|
886
|
+
doc.source,
|
|
887
|
+
doc.chunks.length,
|
|
888
|
+
doc.chunks.reduce((n, c) => n + c.content.length, 0)
|
|
889
|
+
);
|
|
890
|
+
if (opts.replace) {
|
|
891
|
+
const deleted = await deleteBySource(ks, doc.source);
|
|
892
|
+
deletedForReplace += deleted;
|
|
893
|
+
}
|
|
894
|
+
const existing = await sourceChunkHashes(ks, doc.source);
|
|
895
|
+
const meta = { ...baseMeta, ...doc.metadata ?? {} };
|
|
896
|
+
if (!("tags" in meta) && cliTags) meta.tags = cliTags;
|
|
897
|
+
const todo = [];
|
|
898
|
+
for (const chunk of doc.chunks) {
|
|
899
|
+
if (!opts.replace && existing.get(chunk.index) === contentHash(chunk.content)) {
|
|
900
|
+
skippedExisting++;
|
|
901
|
+
file?.handled(1, chunk.content.length);
|
|
851
902
|
continue;
|
|
852
903
|
}
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
904
|
+
todo.push(chunk);
|
|
905
|
+
}
|
|
906
|
+
if (todo.length === 0) return;
|
|
907
|
+
if (opts.dryRun) {
|
|
908
|
+
for (const chunk of todo) file?.handled(1, chunk.content.length);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
if (!announcedEmbed) {
|
|
912
|
+
announcedEmbed = true;
|
|
913
|
+
say(`Embedding with ${ks.embedding_model} at ${ks.ollama_url} ...`);
|
|
914
|
+
}
|
|
915
|
+
const contents = todo.map((c) => c.content);
|
|
916
|
+
const charLens = contents.map((c) => c.length);
|
|
917
|
+
let reported = 0;
|
|
918
|
+
const embeddings = await embedBatch(ks.ollama_url, ks.embedding_model, contents, batchSize, (done) => {
|
|
919
|
+
const delta = done - reported;
|
|
920
|
+
if (delta <= 0) return;
|
|
921
|
+
let chars = 0;
|
|
922
|
+
for (let i = reported; i < done; i++) chars += charLens[i];
|
|
923
|
+
reported = done;
|
|
924
|
+
file?.handled(delta, chars);
|
|
925
|
+
});
|
|
926
|
+
const rows = todo.map((chunk, i) => ({
|
|
927
|
+
source: doc.source,
|
|
928
|
+
title: doc.title,
|
|
929
|
+
chunk_index: chunk.index,
|
|
930
|
+
content: chunk.content,
|
|
931
|
+
metadata: meta,
|
|
932
|
+
embedding: embeddings[i]
|
|
933
|
+
}));
|
|
934
|
+
const rowCount = await insertChunks(ks, rows);
|
|
935
|
+
inserted += rowCount;
|
|
936
|
+
};
|
|
937
|
+
let cursor = 0;
|
|
938
|
+
let firstError;
|
|
939
|
+
const runWorker = async () => {
|
|
940
|
+
while (firstError === void 0) {
|
|
941
|
+
const index = cursor++;
|
|
942
|
+
if (index >= docs.length) return;
|
|
943
|
+
try {
|
|
944
|
+
await processDoc(docs[index]);
|
|
945
|
+
} catch (err) {
|
|
946
|
+
firstError ??= err;
|
|
947
|
+
return;
|
|
856
948
|
}
|
|
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);
|
|
877
949
|
}
|
|
878
|
-
}
|
|
950
|
+
};
|
|
951
|
+
const workerCount = Math.min(DOC_CONCURRENCY, docs.length);
|
|
952
|
+
await Promise.all(Array.from({ length: workerCount }, () => runWorker()));
|
|
953
|
+
if (firstError !== void 0) {
|
|
879
954
|
progress?.close();
|
|
880
|
-
throw
|
|
955
|
+
throw firstError;
|
|
881
956
|
}
|
|
882
957
|
progress?.finish();
|
|
883
958
|
return {
|
|
@@ -891,7 +966,7 @@ async function feedDocs(ks, docs, opts = {}) {
|
|
|
891
966
|
};
|
|
892
967
|
}
|
|
893
968
|
async function feedTarget(ks, target, opts = {}) {
|
|
894
|
-
const loadOpts = { maxLen: opts.maxLen ?? 1200, overlap: opts.overlap ?? 180 };
|
|
969
|
+
const loadOpts = { maxLen: opts.maxLen ?? 1200, overlap: opts.overlap ?? 180, htmlFilter: opts.htmlFilter };
|
|
895
970
|
const { docs, skipped } = await loadTarget(target, loadOpts);
|
|
896
971
|
for (const s of skipped) {
|
|
897
972
|
(opts.log ?? ((m) => console.log(m)))(`skipped ${s.path}: ${s.reason}`);
|
|
@@ -915,8 +990,30 @@ import { mkdtempSync } from "fs";
|
|
|
915
990
|
import { tmpdir } from "os";
|
|
916
991
|
import path2 from "path";
|
|
917
992
|
import { randomUUID } from "crypto";
|
|
918
|
-
import {
|
|
993
|
+
import {
|
|
994
|
+
CheerioCrawler,
|
|
995
|
+
PlaywrightCrawler,
|
|
996
|
+
PuppeteerCrawler,
|
|
997
|
+
Request as CrawleeRequest,
|
|
998
|
+
RequestQueue,
|
|
999
|
+
RobotsTxtFile,
|
|
1000
|
+
Sitemap,
|
|
1001
|
+
constructGlobObjectsFromGlobs,
|
|
1002
|
+
filterRequestsByPatterns,
|
|
1003
|
+
log
|
|
1004
|
+
} from "crawlee";
|
|
919
1005
|
var CRAWLER_KINDS = ["auto", "crawlee", "playwright", "puppeteer"];
|
|
1006
|
+
function parseSitemapOption(value) {
|
|
1007
|
+
const trimmed = value.trim();
|
|
1008
|
+
if (!trimmed) {
|
|
1009
|
+
throw new Error("invalid --sitemap '': use auto, sitemap-only, html-only or a CSS selector");
|
|
1010
|
+
}
|
|
1011
|
+
const key = trimmed.toLowerCase();
|
|
1012
|
+
if (key === "auto") return { kind: "auto" };
|
|
1013
|
+
if (key === "sitemap-only") return { kind: "sitemap-only" };
|
|
1014
|
+
if (key === "html-only") return { kind: "html-only" };
|
|
1015
|
+
return { kind: "selector", selector: trimmed };
|
|
1016
|
+
}
|
|
920
1017
|
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
1018
|
process.env.CRAWLEE_STORAGE_DIR ??= mkdtempSync(path2.join(tmpdir(), "gresmcp-crawl-"));
|
|
922
1019
|
async function freshQueue() {
|
|
@@ -1006,7 +1103,7 @@ async function crawlWithCheerio(queue, seedUrls, opts, sink) {
|
|
|
1006
1103
|
maxCrawlDepth: Math.max(0, opts.depth),
|
|
1007
1104
|
additionalMimeTypes: ["text/plain", "text/markdown"],
|
|
1008
1105
|
async requestHandler(ctx) {
|
|
1009
|
-
const { request, response, contentType, body } = ctx;
|
|
1106
|
+
const { request, response, contentType, body, $ } = ctx;
|
|
1010
1107
|
const url = request.loadedUrl ?? request.url;
|
|
1011
1108
|
const status = responseStatus(response);
|
|
1012
1109
|
if (status === void 0 || status < 200 || status >= 300) {
|
|
@@ -1016,12 +1113,16 @@ async function crawlWithCheerio(queue, seedUrls, opts, sink) {
|
|
|
1016
1113
|
const type = contentType?.type ?? "";
|
|
1017
1114
|
if (type === "text/html" || type === "application/xhtml+xml") {
|
|
1018
1115
|
const html = typeof body === "string" ? body : toText(body);
|
|
1019
|
-
const page = extractPage(html);
|
|
1116
|
+
const page = extractPage(html, { filter: opts.htmlFilter });
|
|
1020
1117
|
if (page.likelyShell) {
|
|
1021
1118
|
sink.shellUrls?.push(url);
|
|
1022
1119
|
sink.skipped.push({ path: url, reason: "likely JS-rendered shell" });
|
|
1023
1120
|
return;
|
|
1024
1121
|
}
|
|
1122
|
+
if (opts.htmlFilter && !page.filterMatched) {
|
|
1123
|
+
sink.skipped.push({ path: url, reason: `html filter '${opts.htmlFilter}' matched nothing` });
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1025
1126
|
if (!page.markdown.trim()) {
|
|
1026
1127
|
sink.skipped.push({ path: url, reason: "empty" });
|
|
1027
1128
|
return;
|
|
@@ -1032,7 +1133,28 @@ async function crawlWithCheerio(queue, seedUrls, opts, sink) {
|
|
|
1032
1133
|
return;
|
|
1033
1134
|
}
|
|
1034
1135
|
sink.docs.push(doc);
|
|
1035
|
-
|
|
1136
|
+
if (opts.followLinks !== false) {
|
|
1137
|
+
const selector = opts.linkSelector;
|
|
1138
|
+
if (selector) {
|
|
1139
|
+
if ($(selector).length > 0) {
|
|
1140
|
+
opts.selectorStats.hits += 1;
|
|
1141
|
+
await ctx.enqueueLinks({
|
|
1142
|
+
selector,
|
|
1143
|
+
strategy: "same-hostname",
|
|
1144
|
+
exclude: [BINARY_LINK_RE],
|
|
1145
|
+
globs: opts.urlFilter
|
|
1146
|
+
});
|
|
1147
|
+
} else {
|
|
1148
|
+
opts.selectorStats.misses += 1;
|
|
1149
|
+
}
|
|
1150
|
+
} else {
|
|
1151
|
+
await ctx.enqueueLinks({
|
|
1152
|
+
strategy: "same-hostname",
|
|
1153
|
+
exclude: [BINARY_LINK_RE],
|
|
1154
|
+
globs: opts.urlFilter
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1036
1158
|
} else if (type === "text/markdown") {
|
|
1037
1159
|
const doc = buildDoc(url, "md", toText(body), void 0, sinkDeps(opts));
|
|
1038
1160
|
if (!doc) {
|
|
@@ -1064,7 +1186,16 @@ async function crawlWithCheerio(queue, seedUrls, opts, sink) {
|
|
|
1064
1186
|
}
|
|
1065
1187
|
}
|
|
1066
1188
|
function sinkDeps(opts) {
|
|
1067
|
-
return {
|
|
1189
|
+
return {
|
|
1190
|
+
maxLen: opts.maxLen,
|
|
1191
|
+
overlap: opts.overlap,
|
|
1192
|
+
cliTags: opts.tags,
|
|
1193
|
+
urlFilter: opts.urlFilter,
|
|
1194
|
+
htmlFilter: opts.htmlFilter,
|
|
1195
|
+
followLinks: opts.followLinks,
|
|
1196
|
+
linkSelector: opts.linkSelector,
|
|
1197
|
+
selectorStats: opts.selectorStats
|
|
1198
|
+
};
|
|
1068
1199
|
}
|
|
1069
1200
|
async function importOptional(name) {
|
|
1070
1201
|
try {
|
|
@@ -1090,7 +1221,11 @@ async function handleBrowserPage(ctx, deps) {
|
|
|
1090
1221
|
return;
|
|
1091
1222
|
}
|
|
1092
1223
|
const html = await ctx.page.content();
|
|
1093
|
-
const page = extractPage(html);
|
|
1224
|
+
const page = extractPage(html, { filter: deps.htmlFilter });
|
|
1225
|
+
if (deps.htmlFilter && !page.filterMatched) {
|
|
1226
|
+
deps.sink.skipped.push({ path: url, reason: `html filter '${deps.htmlFilter}' matched nothing` });
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1094
1229
|
if (!page.markdown.trim()) {
|
|
1095
1230
|
deps.sink.skipped.push({ path: url, reason: "empty after render" });
|
|
1096
1231
|
return;
|
|
@@ -1101,7 +1236,29 @@ async function handleBrowserPage(ctx, deps) {
|
|
|
1101
1236
|
return;
|
|
1102
1237
|
}
|
|
1103
1238
|
deps.sink.docs.push(doc);
|
|
1104
|
-
|
|
1239
|
+
if (deps.followLinks !== false) {
|
|
1240
|
+
const selector = deps.linkSelector;
|
|
1241
|
+
if (selector) {
|
|
1242
|
+
const elements = await ctx.page.$$(selector);
|
|
1243
|
+
if (elements.length > 0) {
|
|
1244
|
+
deps.selectorStats.hits += 1;
|
|
1245
|
+
await ctx.enqueueLinks({
|
|
1246
|
+
selector,
|
|
1247
|
+
strategy: "same-hostname",
|
|
1248
|
+
exclude: [BINARY_LINK_RE],
|
|
1249
|
+
globs: deps.urlFilter
|
|
1250
|
+
});
|
|
1251
|
+
} else {
|
|
1252
|
+
deps.selectorStats.misses += 1;
|
|
1253
|
+
}
|
|
1254
|
+
} else {
|
|
1255
|
+
await ctx.enqueueLinks({
|
|
1256
|
+
strategy: "same-hostname",
|
|
1257
|
+
exclude: [BINARY_LINK_RE],
|
|
1258
|
+
globs: deps.urlFilter
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1105
1262
|
}
|
|
1106
1263
|
async function crawlWithBrowser(kind, queue, seedUrls, opts, sink) {
|
|
1107
1264
|
const crawler = kind === "playwright" ? new PlaywrightCrawler({
|
|
@@ -1142,20 +1299,66 @@ async function crawlWithBrowser(kind, queue, seedUrls, opts, sink) {
|
|
|
1142
1299
|
});
|
|
1143
1300
|
}
|
|
1144
1301
|
}
|
|
1145
|
-
|
|
1146
|
-
const
|
|
1147
|
-
const
|
|
1302
|
+
function normalizeSitemapUrls(urls) {
|
|
1303
|
+
const out = [];
|
|
1304
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1305
|
+
for (const raw of urls) {
|
|
1306
|
+
let url;
|
|
1307
|
+
try {
|
|
1308
|
+
url = new URL(raw);
|
|
1309
|
+
} catch {
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") continue;
|
|
1313
|
+
url.hash = "";
|
|
1314
|
+
const key = url.toString();
|
|
1315
|
+
if (seen.has(key)) continue;
|
|
1316
|
+
seen.add(key);
|
|
1317
|
+
out.push(key);
|
|
1318
|
+
}
|
|
1319
|
+
return out;
|
|
1320
|
+
}
|
|
1321
|
+
function applyUrlFilter(urls, globs) {
|
|
1322
|
+
if (!globs || globs.length === 0) return urls;
|
|
1323
|
+
const requests = urls.map((url) => new CrawleeRequest({ url }));
|
|
1324
|
+
const patterns = constructGlobObjectsFromGlobs(globs);
|
|
1325
|
+
return filterRequestsByPatterns(requests, patterns).map((r) => r.url);
|
|
1326
|
+
}
|
|
1327
|
+
async function loadSitemap(urls) {
|
|
1328
|
+
try {
|
|
1329
|
+
const sitemap = await Sitemap.load(urls, void 0, { reportNetworkErrors: false });
|
|
1330
|
+
return normalizeSitemapUrls(sitemap.urls);
|
|
1331
|
+
} catch {
|
|
1332
|
+
return [];
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
async function discoverSitemapUrls(seedUrl) {
|
|
1336
|
+
const origin = new URL(seedUrl).origin;
|
|
1337
|
+
const fromSitemapXml = await loadSitemap([`${origin}/sitemap.xml`]);
|
|
1338
|
+
if (fromSitemapXml.length > 0) return fromSitemapXml;
|
|
1339
|
+
try {
|
|
1340
|
+
const robots = await RobotsTxtFile.find(seedUrl);
|
|
1341
|
+
const referenced = robots.getSitemaps();
|
|
1342
|
+
if (referenced.length > 0) {
|
|
1343
|
+
const fromRobots = await loadSitemap(referenced);
|
|
1344
|
+
if (fromRobots.length > 0) return fromRobots;
|
|
1345
|
+
}
|
|
1346
|
+
} catch {
|
|
1347
|
+
}
|
|
1348
|
+
return void 0;
|
|
1349
|
+
}
|
|
1350
|
+
async function runCrawl(kind, seedUrls, opts) {
|
|
1148
1351
|
if (kind === "playwright" || kind === "puppeteer") {
|
|
1149
1352
|
await requireBrowserEngine(kind);
|
|
1150
1353
|
const sink2 = { docs: [], skipped: [] };
|
|
1151
1354
|
const queue = await freshQueue();
|
|
1152
|
-
await crawlWithBrowser(kind, queue,
|
|
1355
|
+
await crawlWithBrowser(kind, queue, seedUrls, opts, sink2);
|
|
1153
1356
|
return { docs: dedupeDocs(sink2.docs), skipped: sink2.skipped };
|
|
1154
1357
|
}
|
|
1155
1358
|
const shellUrls = [];
|
|
1156
1359
|
const sink = { docs: [], skipped: [], shellUrls };
|
|
1157
1360
|
const staticQueue = await freshQueue();
|
|
1158
|
-
await crawlWithCheerio(staticQueue,
|
|
1361
|
+
await crawlWithCheerio(staticQueue, seedUrls, opts, sink);
|
|
1159
1362
|
if (kind !== "auto" || shellUrls.length === 0) {
|
|
1160
1363
|
return { docs: dedupeDocs(sink.docs), skipped: sink.skipped };
|
|
1161
1364
|
}
|
|
@@ -1174,6 +1377,36 @@ async function crawlWebsite(seed, opts) {
|
|
|
1174
1377
|
skipped: [...sink.skipped, ...browserSink.skipped]
|
|
1175
1378
|
};
|
|
1176
1379
|
}
|
|
1380
|
+
async function crawlWebsite(seed, opts) {
|
|
1381
|
+
const seedUrl = normalizeSeedUrl(seed);
|
|
1382
|
+
const kind = opts.crawler;
|
|
1383
|
+
const mode = opts.sitemap ?? { kind: "auto" };
|
|
1384
|
+
const selectorStats = { hits: 0, misses: 0 };
|
|
1385
|
+
if (mode.kind === "auto" || mode.kind === "sitemap-only") {
|
|
1386
|
+
const sitemapUrls = await discoverSitemapUrls(seedUrl);
|
|
1387
|
+
if (sitemapUrls) {
|
|
1388
|
+
const filtered = applyUrlFilter(sitemapUrls, opts.urlFilter);
|
|
1389
|
+
const detail = filtered.length < sitemapUrls.length ? `${filtered.length} matching --url-filter` : `${filtered.length} URL(s)`;
|
|
1390
|
+
opts.log(`sitemap: ${sitemapUrls.length} URL(s) discovered, ${detail} to crawl`);
|
|
1391
|
+
return runCrawl(kind, filtered, { ...opts, followLinks: false, selectorStats });
|
|
1392
|
+
}
|
|
1393
|
+
if (mode.kind === "sitemap-only") {
|
|
1394
|
+
opts.log(`warning: no sitemap.xml and no sitemaps in robots.txt found for ${seedUrl}; nothing to ingest`);
|
|
1395
|
+
return { docs: [], skipped: [] };
|
|
1396
|
+
}
|
|
1397
|
+
opts.log("no sitemap found; falling back to HTML link crawling");
|
|
1398
|
+
}
|
|
1399
|
+
const result = await runCrawl(kind, [seedUrl], {
|
|
1400
|
+
...opts,
|
|
1401
|
+
followLinks: true,
|
|
1402
|
+
linkSelector: mode.kind === "selector" ? mode.selector : void 0,
|
|
1403
|
+
selectorStats
|
|
1404
|
+
});
|
|
1405
|
+
if (mode.kind === "selector" && selectorStats.hits === 0 && selectorStats.misses > 0) {
|
|
1406
|
+
opts.log(`warning: --sitemap selector '${mode.selector}' matched no links`);
|
|
1407
|
+
}
|
|
1408
|
+
return result;
|
|
1409
|
+
}
|
|
1177
1410
|
async function feedUrl(ks, url, opts = {}) {
|
|
1178
1411
|
const log2 = opts.log ?? ((m) => console.log(m));
|
|
1179
1412
|
const kind = opts.crawler ?? "auto";
|
|
@@ -1191,6 +1424,9 @@ async function feedUrl(ks, url, opts = {}) {
|
|
|
1191
1424
|
maxLen,
|
|
1192
1425
|
overlap,
|
|
1193
1426
|
tags: opts.tags,
|
|
1427
|
+
urlFilter: opts.urlFilter,
|
|
1428
|
+
htmlFilter: opts.htmlFilter,
|
|
1429
|
+
sitemap: opts.sitemap,
|
|
1194
1430
|
log: log2
|
|
1195
1431
|
});
|
|
1196
1432
|
for (const s of skipped) {
|
|
@@ -1434,7 +1670,7 @@ async function runChecks(opts) {
|
|
|
1434
1670
|
}
|
|
1435
1671
|
|
|
1436
1672
|
// src/version.ts
|
|
1437
|
-
var VERSION = "1.
|
|
1673
|
+
var VERSION = "1.2.0";
|
|
1438
1674
|
|
|
1439
1675
|
// src/cli.ts
|
|
1440
1676
|
async function run(fn) {
|
|
@@ -1475,6 +1711,11 @@ function parseTags(tags) {
|
|
|
1475
1711
|
const list = tags.split(",").map((t) => t.trim()).filter(Boolean);
|
|
1476
1712
|
return list.length > 0 ? list : void 0;
|
|
1477
1713
|
}
|
|
1714
|
+
function parseGlobs(globs) {
|
|
1715
|
+
if (!globs || globs.length === 0) return void 0;
|
|
1716
|
+
const list = [...new Set(globs.map((g) => g.trim()).filter(Boolean))];
|
|
1717
|
+
return list.length > 0 ? list : void 0;
|
|
1718
|
+
}
|
|
1478
1719
|
var program = new Command();
|
|
1479
1720
|
program.name("gresmcp").description("Manage Postgres-backed knowledge sources for the gresmcp MCP server").version(VERSION);
|
|
1480
1721
|
program.command("init").description("Initialize the database schema (extension, ks table; repairs entry tables)").action(
|
|
@@ -1609,12 +1850,37 @@ ksCmd.command("delete <name>").description("Delete a knowledge source and all of
|
|
|
1609
1850
|
console.log(`Deleted knowledge source '${name}'`);
|
|
1610
1851
|
})
|
|
1611
1852
|
);
|
|
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(
|
|
1853
|
+
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(
|
|
1854
|
+
"--url-filter <glob>",
|
|
1855
|
+
"Only follow --url links whose URL matches this glob (repeatable; a link is followed if it matches any glob, e.g. '**/docs/**')",
|
|
1856
|
+
((v, prev) => [...prev, v]),
|
|
1857
|
+
[]
|
|
1858
|
+
).option(
|
|
1859
|
+
"--html-filter <selector>",
|
|
1860
|
+
"CSS selector for HTML content \u2014 any DOM selector works (e.g. 'article.post', '#article .content:first-child'); optionally chain steps with ' -> ' (e.g. '#article -> .content'); applies to --path HTML files and --url pages"
|
|
1861
|
+
).option(
|
|
1862
|
+
"--sitemap <mode>",
|
|
1863
|
+
"Link discovery for --url: 'auto' (default; sitemap.xml, then robots.txt sitemaps, else follow links), 'sitemap-only' (only sitemap.xml / robots.txt sitemaps), 'html-only' (follow links), or a CSS selector to only follow links inside it (e.g. 'nav.docs a')"
|
|
1864
|
+
).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(
|
|
1613
1865
|
(name, opts) => run(async () => {
|
|
1614
1866
|
await initDb();
|
|
1615
1867
|
const ks = requireKs(await getKs(name), name);
|
|
1616
1868
|
const modes = [opts.path, opts.url, opts.text, opts.stdin].filter(Boolean).length;
|
|
1617
1869
|
if (modes !== 1) throw new Error("provide exactly one of --path, --url, --text or --stdin");
|
|
1870
|
+
const urlFilter = parseGlobs(opts.urlFilter);
|
|
1871
|
+
if (urlFilter && !opts.url) {
|
|
1872
|
+
console.error("warning: --url-filter has no effect without --url");
|
|
1873
|
+
}
|
|
1874
|
+
if (opts.htmlFilter) parseHtmlFilter(opts.htmlFilter);
|
|
1875
|
+
if (opts.htmlFilter && !opts.path && !opts.url) {
|
|
1876
|
+
console.error("warning: --html-filter has no effect without --path or --url");
|
|
1877
|
+
}
|
|
1878
|
+
let sitemap;
|
|
1879
|
+
if (opts.sitemap !== void 0) {
|
|
1880
|
+
sitemap = parseSitemapOption(opts.sitemap);
|
|
1881
|
+
if (sitemap.kind === "selector") validateCssSelector(sitemap.selector);
|
|
1882
|
+
if (!opts.url) console.error("warning: --sitemap has no effect without --url");
|
|
1883
|
+
}
|
|
1618
1884
|
const common = {
|
|
1619
1885
|
replace: opts.replace,
|
|
1620
1886
|
dryRun: opts.dryRun,
|
|
@@ -1622,7 +1888,8 @@ program.command("feed <ks>").description("Feed data into a knowledge source (fro
|
|
|
1622
1888
|
maxLen: opts.chunkSize && opts.chunkSize > 0 ? opts.chunkSize : void 0,
|
|
1623
1889
|
overlap: opts.overlap,
|
|
1624
1890
|
tags: parseTags(opts.tags),
|
|
1625
|
-
metadata: parseKeyValue(opts.metadata ?? [])
|
|
1891
|
+
metadata: parseKeyValue(opts.metadata ?? []),
|
|
1892
|
+
htmlFilter: opts.htmlFilter
|
|
1626
1893
|
};
|
|
1627
1894
|
let summary;
|
|
1628
1895
|
const since = await dbClockTimestamp();
|
|
@@ -1633,7 +1900,7 @@ program.command("feed <ks>").description("Feed data into a knowledge source (fro
|
|
|
1633
1900
|
if (crawler && !CRAWLER_KINDS.includes(crawler)) {
|
|
1634
1901
|
throw new Error(`unknown crawler '${crawler}' (use auto, crawlee, playwright or puppeteer)`);
|
|
1635
1902
|
}
|
|
1636
|
-
summary = await feedUrl(ks, opts.url, { ...common, crawler, maxPages: opts.maxPages, depth: opts.depth });
|
|
1903
|
+
summary = await feedUrl(ks, opts.url, { ...common, crawler, sitemap, maxPages: opts.maxPages, depth: opts.depth, urlFilter });
|
|
1637
1904
|
} else {
|
|
1638
1905
|
const text = opts.stdin ? await readStdin() : opts.text;
|
|
1639
1906
|
if (!text.trim()) throw new Error("empty input text");
|