@gresmcp/mcp 1.2.0 → 1.3.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 +55 -13
- package/dist/cli.js +615 -42
- package/dist/cli.js.map +1 -1
- package/dist/mcp.js +1 -1
- package/dist/mcp.js.map +1 -1
- package/package.json +9 -1
package/dist/cli.js
CHANGED
|
@@ -325,8 +325,8 @@ async function probeModel(baseUrl, model) {
|
|
|
325
325
|
init_config();
|
|
326
326
|
|
|
327
327
|
// src/ingest.ts
|
|
328
|
-
import { promises as
|
|
329
|
-
import
|
|
328
|
+
import { promises as fs2 } from "fs";
|
|
329
|
+
import path2 from "path";
|
|
330
330
|
|
|
331
331
|
// src/chunk.ts
|
|
332
332
|
var HEADING_RE = /^(#{1,6})\s+(.+)$/;
|
|
@@ -390,8 +390,8 @@ function chunkMarkdown(text, opts = {}) {
|
|
|
390
390
|
const m = HEADING_RE.exec(line);
|
|
391
391
|
if (m) {
|
|
392
392
|
const level = m[1].length;
|
|
393
|
-
const
|
|
394
|
-
const parent =
|
|
393
|
+
const path4 = sections[sections.length - 1].path;
|
|
394
|
+
const parent = path4.slice(0, level - 1);
|
|
395
395
|
sections.push({ path: [...parent, m[2].trim()], body: [line] });
|
|
396
396
|
} else {
|
|
397
397
|
sections[sections.length - 1].body.push(line);
|
|
@@ -576,9 +576,337 @@ function extractPage(html, opts = {}) {
|
|
|
576
576
|
};
|
|
577
577
|
}
|
|
578
578
|
|
|
579
|
+
// src/doc.ts
|
|
580
|
+
import mammoth from "mammoth";
|
|
581
|
+
import {
|
|
582
|
+
Doc97File,
|
|
583
|
+
InvalidKeyError,
|
|
584
|
+
OfficeFile,
|
|
585
|
+
OleFileIO,
|
|
586
|
+
isEncrypted
|
|
587
|
+
} from "office-crypto";
|
|
588
|
+
import WordExtractor from "word-extractor";
|
|
589
|
+
var DocError = class extends Error {
|
|
590
|
+
constructor(message, kind) {
|
|
591
|
+
super(message);
|
|
592
|
+
this.kind = kind;
|
|
593
|
+
this.name = "DocError";
|
|
594
|
+
}
|
|
595
|
+
kind;
|
|
596
|
+
};
|
|
597
|
+
var OLE_MAGIC = [208, 207, 17, 224, 161, 177, 26, 225];
|
|
598
|
+
function hasMagic(data, magic) {
|
|
599
|
+
if (data.length < magic.length) return false;
|
|
600
|
+
return magic.every((byte, i) => data[i] === byte);
|
|
601
|
+
}
|
|
602
|
+
function isOle(data) {
|
|
603
|
+
return hasMagic(data, OLE_MAGIC);
|
|
604
|
+
}
|
|
605
|
+
function isZip(data) {
|
|
606
|
+
return data.length >= 4 && data[0] === 80 && data[1] === 75 && data[2] === 3 && data[3] === 4;
|
|
607
|
+
}
|
|
608
|
+
function toDocError(err, prefix) {
|
|
609
|
+
if (err instanceof DocError) return err;
|
|
610
|
+
if (err instanceof InvalidKeyError) {
|
|
611
|
+
return new DocError("wrong password for encrypted Word document", "password");
|
|
612
|
+
}
|
|
613
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
614
|
+
return new DocError(`${prefix}: ${message.slice(0, 200)}`, "corrupt");
|
|
615
|
+
}
|
|
616
|
+
function requirePassword() {
|
|
617
|
+
return new DocError("password-protected Word document (use --file-password)", "password");
|
|
618
|
+
}
|
|
619
|
+
function oleHasStream(data, name) {
|
|
620
|
+
try {
|
|
621
|
+
return new OleFileIO(data).exists(name);
|
|
622
|
+
} catch {
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
function decryptOoxml(data, password) {
|
|
627
|
+
try {
|
|
628
|
+
if (!password) throw requirePassword();
|
|
629
|
+
const file = OfficeFile(data);
|
|
630
|
+
file.loadKey({ password, verifyPassword: true });
|
|
631
|
+
return file.decrypt();
|
|
632
|
+
} catch (err) {
|
|
633
|
+
throw toDocError(err, "cannot decrypt Word document");
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function decryptDoc97(data, password) {
|
|
637
|
+
try {
|
|
638
|
+
if (!password) throw requirePassword();
|
|
639
|
+
const file = new Doc97File(new OleFileIO(data));
|
|
640
|
+
file.loadKey({ password });
|
|
641
|
+
return file.decrypt();
|
|
642
|
+
} catch (err) {
|
|
643
|
+
throw toDocError(err, "cannot decrypt Word document");
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
async function docxToMarkdown(data, encrypted) {
|
|
647
|
+
let html;
|
|
648
|
+
try {
|
|
649
|
+
const result = await mammoth.convertToHtml({ buffer: Buffer.from(data) });
|
|
650
|
+
html = result.value;
|
|
651
|
+
} catch (err) {
|
|
652
|
+
throw toDocError(err, "cannot parse docx");
|
|
653
|
+
}
|
|
654
|
+
html = html.replace(/<img\b[^>]*>/gi, "").replace(
|
|
655
|
+
/<t([dh])\b([^>]*)>([\s\S]*?)<\/t\1>/gi,
|
|
656
|
+
(_m, letter, attrs, inner) => `<t${letter}${attrs}>${inner.replace(/<\/?p(?:\s[^>]*)?>/gi, "").replace(/<br(?:\s[^>]*)?>/gi, " ")}</t${letter}>`
|
|
657
|
+
).replace(
|
|
658
|
+
/<table\b[^>]*>([\s\S]*?)<\/table>/gi,
|
|
659
|
+
(m, inner) => m.replace(
|
|
660
|
+
/(<tr\b[^>]*>)([\s\S]*?)(<\/tr>)/i,
|
|
661
|
+
(row, open, cells) => open + cells.replace(/<td\b/gi, "<th").replace(/<\/td>/gi, "</th>") + "</tr>"
|
|
662
|
+
)
|
|
663
|
+
);
|
|
664
|
+
const { markdown, title } = htmlToMarkdown(html);
|
|
665
|
+
return {
|
|
666
|
+
markdown: markdown.replace(/\n{3,}/g, "\n\n").trim(),
|
|
667
|
+
title: title || void 0,
|
|
668
|
+
format: "docx",
|
|
669
|
+
...encrypted ? { encrypted: true } : {}
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
async function docToText(data, encrypted) {
|
|
673
|
+
try {
|
|
674
|
+
const doc = await new WordExtractor().extract(Buffer.from(data));
|
|
675
|
+
const normalize = (s) => s.replace(/\r\n?/g, "\n").replace(/\f/g, "\n\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
676
|
+
const parts = [doc.getBody(), doc.getTextboxes(), doc.getFootnotes(), doc.getEndnotes()].map(normalize).filter(Boolean);
|
|
677
|
+
return { markdown: parts.join("\n\n"), format: "doc", ...encrypted ? { encrypted: true } : {} };
|
|
678
|
+
} catch (err) {
|
|
679
|
+
throw toDocError(err, "cannot parse doc");
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
async function docToDoc(data, opts = {}) {
|
|
683
|
+
if (isZip(data)) return docxToMarkdown(data, false);
|
|
684
|
+
if (isOle(data)) {
|
|
685
|
+
if (oleHasStream(data, "EncryptionInfo")) {
|
|
686
|
+
if (oleHasStream(data, "Workbook") || oleHasStream(data, "PowerPoint Document")) {
|
|
687
|
+
throw new DocError("not a Word document (encrypted Excel/PowerPoint package)", "corrupt");
|
|
688
|
+
}
|
|
689
|
+
const plain = decryptOoxml(data, opts.password);
|
|
690
|
+
return isZip(plain) ? docxToMarkdown(plain, true) : docToText(plain, true);
|
|
691
|
+
}
|
|
692
|
+
if (oleHasStream(data, "WordDocument") || oleHasStream(data, "wordDocument")) {
|
|
693
|
+
if (isEncrypted(data)) {
|
|
694
|
+
const plain = decryptDoc97(data, opts.password);
|
|
695
|
+
return docToText(plain, true);
|
|
696
|
+
}
|
|
697
|
+
return docToText(data, false);
|
|
698
|
+
}
|
|
699
|
+
throw new DocError("not a Word document (unsupported OLE compound file)", "corrupt");
|
|
700
|
+
}
|
|
701
|
+
throw new DocError("not a Word document (missing docx/doc signature)", "corrupt");
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// src/epub.ts
|
|
705
|
+
import { promises as fs } from "fs";
|
|
706
|
+
import { mkdtempSync } from "fs";
|
|
707
|
+
import { tmpdir } from "os";
|
|
708
|
+
import path from "path";
|
|
709
|
+
import { EPub } from "epub2";
|
|
710
|
+
var EpubError = class extends Error {
|
|
711
|
+
constructor(message, kind) {
|
|
712
|
+
super(message);
|
|
713
|
+
this.kind = kind;
|
|
714
|
+
this.name = "EpubError";
|
|
715
|
+
}
|
|
716
|
+
kind;
|
|
717
|
+
};
|
|
718
|
+
var REWRITE_ROOT_RE = /^\/(?:links|images)\//i;
|
|
719
|
+
var ENCRYPTION_ENTRY = "meta-inf/encryption.xml";
|
|
720
|
+
function toEpubError(err) {
|
|
721
|
+
if (err instanceof EpubError) return err;
|
|
722
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
723
|
+
return new EpubError(`cannot parse EPUB: ${message.slice(0, 200)}`, "corrupt");
|
|
724
|
+
}
|
|
725
|
+
function hasEncryptionEntry(names) {
|
|
726
|
+
return (names ?? []).some((n) => n.toLowerCase() === ENCRYPTION_ENTRY);
|
|
727
|
+
}
|
|
728
|
+
function normalizeHref(href) {
|
|
729
|
+
const raw = href.split("#")[0].trim();
|
|
730
|
+
if (!raw) return "";
|
|
731
|
+
let decoded = raw;
|
|
732
|
+
try {
|
|
733
|
+
decoded = decodeURIComponent(raw);
|
|
734
|
+
} catch {
|
|
735
|
+
}
|
|
736
|
+
return path.posix.normalize(decoded).toLowerCase();
|
|
737
|
+
}
|
|
738
|
+
function cleanChapterHtml(html) {
|
|
739
|
+
return html.replace(/<img\b[^>]*>/gi, "").replace(/<a\b([^>]*)>([\s\S]*?)<\/a>/gi, (m, attrs, inner) => {
|
|
740
|
+
const href = /\shref\s*=\s*(?:"([^"]*)"|'([^']*)')/i.exec(attrs);
|
|
741
|
+
const url = (href?.[1] ?? href?.[2] ?? "").trim();
|
|
742
|
+
return REWRITE_ROOT_RE.test(url) ? inner : m;
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
function tocTitleMap(epub) {
|
|
746
|
+
const titles = /* @__PURE__ */ new Map();
|
|
747
|
+
for (const entry of epub.toc ?? []) {
|
|
748
|
+
const title = entry?.title?.trim();
|
|
749
|
+
if (!title) continue;
|
|
750
|
+
if (entry.id && epub.manifest[entry.id]) {
|
|
751
|
+
const idKey = `id:${entry.id}`;
|
|
752
|
+
if (!titles.has(idKey)) titles.set(idKey, title);
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
if (entry.href) {
|
|
756
|
+
const hrefKey = normalizeHref(entry.href);
|
|
757
|
+
if (hrefKey && !titles.has(hrefKey)) titles.set(hrefKey, title);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return titles;
|
|
761
|
+
}
|
|
762
|
+
async function epubToDoc(filePath) {
|
|
763
|
+
let epub;
|
|
764
|
+
try {
|
|
765
|
+
epub = await EPub.createAsync(filePath);
|
|
766
|
+
} catch (err) {
|
|
767
|
+
throw toEpubError(err);
|
|
768
|
+
}
|
|
769
|
+
const encrypted = hasEncryptionEntry(epub.zip?.names);
|
|
770
|
+
const titles = tocTitleMap(epub);
|
|
771
|
+
const parts = [];
|
|
772
|
+
for (const item of epub.flow ?? []) {
|
|
773
|
+
const id = item?.id;
|
|
774
|
+
if (!id) continue;
|
|
775
|
+
let html;
|
|
776
|
+
try {
|
|
777
|
+
html = await epub.getChapterAsync(id);
|
|
778
|
+
} catch {
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
const { markdown: markdown2 } = htmlToMarkdown(cleanChapterHtml(html));
|
|
782
|
+
if (!markdown2.trim()) continue;
|
|
783
|
+
const heading = titles.get(`id:${id}`) ?? (item.href ? titles.get(normalizeHref(item.href)) : void 0);
|
|
784
|
+
parts.push(heading ? `## ${heading}
|
|
785
|
+
|
|
786
|
+
${markdown2}` : markdown2);
|
|
787
|
+
}
|
|
788
|
+
const markdown = parts.join("\n\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
789
|
+
if (!markdown) {
|
|
790
|
+
throw new EpubError(
|
|
791
|
+
encrypted ? "DRM-protected EPUB (encryption.xml; no readable text)" : "no extractable text from EPUB",
|
|
792
|
+
encrypted ? "drm" : "corrupt"
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
return {
|
|
796
|
+
markdown,
|
|
797
|
+
title: epub.metadata?.title?.trim() || void 0,
|
|
798
|
+
author: epub.metadata?.creator?.trim() || void 0,
|
|
799
|
+
language: epub.metadata?.language?.trim() || void 0,
|
|
800
|
+
chapters: parts.length,
|
|
801
|
+
...encrypted ? { encrypted: true } : {}
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
async function epubFromBuffer(data) {
|
|
805
|
+
const dir = mkdtempSync(path.join(tmpdir(), "gresmcp-epub-"));
|
|
806
|
+
try {
|
|
807
|
+
const file = path.join(dir, "book.epub");
|
|
808
|
+
await fs.writeFile(file, data);
|
|
809
|
+
return await epubToDoc(file);
|
|
810
|
+
} finally {
|
|
811
|
+
await fs.rm(dir, { recursive: true, force: true }).catch(() => {
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
579
816
|
// src/ingest.ts
|
|
580
817
|
init_db();
|
|
581
818
|
|
|
819
|
+
// src/pdf.ts
|
|
820
|
+
import pdf2md from "@opendocsg/pdf2md";
|
|
821
|
+
import { extractText, getMeta } from "unpdf";
|
|
822
|
+
var PAGE_BREAK_MARK = "<!-- PAGE_BREAK -->";
|
|
823
|
+
var PdfError = class extends Error {
|
|
824
|
+
constructor(message, kind) {
|
|
825
|
+
super(message);
|
|
826
|
+
this.kind = kind;
|
|
827
|
+
this.name = "PdfError";
|
|
828
|
+
}
|
|
829
|
+
kind;
|
|
830
|
+
};
|
|
831
|
+
function isPasswordException(err) {
|
|
832
|
+
return err instanceof Error && err.name === "PasswordException";
|
|
833
|
+
}
|
|
834
|
+
function toPdfError(err) {
|
|
835
|
+
if (err instanceof PdfError) return err;
|
|
836
|
+
if (isPasswordException(err)) {
|
|
837
|
+
return new PdfError("wrong password for encrypted PDF", "password");
|
|
838
|
+
}
|
|
839
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
840
|
+
return new PdfError(`cannot parse PDF: ${message.slice(0, 200)}`, "corrupt");
|
|
841
|
+
}
|
|
842
|
+
function titleFromInfo(info) {
|
|
843
|
+
const raw = info?.Title;
|
|
844
|
+
const value = typeof raw === "string" ? raw.trim() : "";
|
|
845
|
+
return value || void 0;
|
|
846
|
+
}
|
|
847
|
+
function imageOnlyFromPages(pages) {
|
|
848
|
+
const out = [];
|
|
849
|
+
for (const page of pages) {
|
|
850
|
+
const hasText = page.items.some((it) => String(it.text ?? "").trim().length > 0);
|
|
851
|
+
if (!hasText) out.push(page.index + 1);
|
|
852
|
+
}
|
|
853
|
+
return out;
|
|
854
|
+
}
|
|
855
|
+
async function extractEncrypted(data, password) {
|
|
856
|
+
try {
|
|
857
|
+
const extractOpts = { mergePages: false, password };
|
|
858
|
+
const metaOpts = { password };
|
|
859
|
+
const [result, meta] = await Promise.all([
|
|
860
|
+
extractText(data, extractOpts),
|
|
861
|
+
getMeta(data, metaOpts).catch(() => void 0)
|
|
862
|
+
]);
|
|
863
|
+
const pages = result.text;
|
|
864
|
+
const imageOnlyPages = [];
|
|
865
|
+
pages.forEach((text, i) => {
|
|
866
|
+
if (!text.trim()) imageOnlyPages.push(i + 1);
|
|
867
|
+
});
|
|
868
|
+
const markdown = pages.map((p) => p.trim()).filter(Boolean).join("\n\n").trim();
|
|
869
|
+
return {
|
|
870
|
+
markdown,
|
|
871
|
+
title: titleFromInfo(meta?.info),
|
|
872
|
+
pageCount: result.totalPages,
|
|
873
|
+
imageOnlyPages
|
|
874
|
+
};
|
|
875
|
+
} catch (err) {
|
|
876
|
+
throw toPdfError(err);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
async function pdfToDoc(data, opts = {}) {
|
|
880
|
+
let title;
|
|
881
|
+
let pages = [];
|
|
882
|
+
let markdown;
|
|
883
|
+
try {
|
|
884
|
+
markdown = await pdf2md(data, {
|
|
885
|
+
metadataParsed: (meta) => {
|
|
886
|
+
title = titleFromInfo(meta.info);
|
|
887
|
+
},
|
|
888
|
+
pageParsed: (parsed) => {
|
|
889
|
+
pages = parsed;
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
} catch (err) {
|
|
893
|
+
if (isPasswordException(err)) {
|
|
894
|
+
if (opts.password) {
|
|
895
|
+
return extractEncrypted(data, opts.password);
|
|
896
|
+
}
|
|
897
|
+
throw new PdfError("password-protected PDF (use --file-password)", "password");
|
|
898
|
+
}
|
|
899
|
+
throw toPdfError(err);
|
|
900
|
+
}
|
|
901
|
+
markdown = markdown.split(PAGE_BREAK_MARK).map((part) => part.replace(/\s+$/, "")).join("\n\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
902
|
+
return {
|
|
903
|
+
markdown,
|
|
904
|
+
title,
|
|
905
|
+
pageCount: pages.length,
|
|
906
|
+
imageOnlyPages: imageOnlyFromPages(pages)
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
|
|
582
910
|
// src/progress.ts
|
|
583
911
|
var BAR_WIDTH = 20;
|
|
584
912
|
var PLAIN_STEP = 32;
|
|
@@ -718,6 +1046,9 @@ var FeedProgress = class {
|
|
|
718
1046
|
// src/ingest.ts
|
|
719
1047
|
var MD_EXT = /* @__PURE__ */ new Set([".md", ".markdown", ".mdx"]);
|
|
720
1048
|
var HTML_EXT = /* @__PURE__ */ new Set([".html", ".htm", ".xhtml"]);
|
|
1049
|
+
var PDF_EXT = /* @__PURE__ */ new Set([".pdf"]);
|
|
1050
|
+
var DOC_EXT = /* @__PURE__ */ new Set([".doc", ".docx"]);
|
|
1051
|
+
var EPUB_EXT = /* @__PURE__ */ new Set([".epub"]);
|
|
721
1052
|
var TEXT_EXT = /* @__PURE__ */ new Set([
|
|
722
1053
|
".txt",
|
|
723
1054
|
".text",
|
|
@@ -805,13 +1136,13 @@ var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
805
1136
|
".cache"
|
|
806
1137
|
]);
|
|
807
1138
|
function isAllowed(ext) {
|
|
808
|
-
return MD_EXT.has(ext) || HTML_EXT.has(ext) || TEXT_EXT.has(ext) || CODE_EXT.has(ext);
|
|
1139
|
+
return MD_EXT.has(ext) || HTML_EXT.has(ext) || TEXT_EXT.has(ext) || CODE_EXT.has(ext) || PDF_EXT.has(ext) || DOC_EXT.has(ext) || EPUB_EXT.has(ext);
|
|
809
1140
|
}
|
|
810
1141
|
async function walk(root) {
|
|
811
1142
|
const out = [];
|
|
812
|
-
const entries = await
|
|
1143
|
+
const entries = await fs2.readdir(root, { withFileTypes: true });
|
|
813
1144
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
814
|
-
const full =
|
|
1145
|
+
const full = path2.join(root, entry.name);
|
|
815
1146
|
if (entry.isDirectory()) {
|
|
816
1147
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
817
1148
|
out.push(...await walk(full));
|
|
@@ -822,50 +1153,110 @@ async function walk(root) {
|
|
|
822
1153
|
return out;
|
|
823
1154
|
}
|
|
824
1155
|
async function loadTarget(target, opts) {
|
|
825
|
-
const stat = await
|
|
1156
|
+
const stat = await fs2.stat(target);
|
|
826
1157
|
const files = stat.isDirectory() ? await walk(target) : [target];
|
|
827
1158
|
const docs = [];
|
|
828
1159
|
const skipped = [];
|
|
829
1160
|
for (const file of files) {
|
|
830
|
-
const ext =
|
|
1161
|
+
const ext = path2.extname(file).toLowerCase();
|
|
831
1162
|
if (!isAllowed(ext)) {
|
|
832
1163
|
skipped.push({ path: file, reason: `unsupported extension '${ext || "(none)"}'` });
|
|
833
1164
|
continue;
|
|
834
1165
|
}
|
|
835
|
-
let
|
|
1166
|
+
let buf;
|
|
836
1167
|
try {
|
|
837
|
-
|
|
1168
|
+
buf = await fs2.readFile(file);
|
|
838
1169
|
} catch {
|
|
839
1170
|
skipped.push({ path: file, reason: "unreadable" });
|
|
840
1171
|
continue;
|
|
841
1172
|
}
|
|
842
|
-
|
|
843
|
-
skipped.push({ path: file, reason: "binary content" });
|
|
844
|
-
continue;
|
|
845
|
-
}
|
|
846
|
-
let text = raw;
|
|
1173
|
+
let text = "";
|
|
847
1174
|
let title = "";
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1175
|
+
let docMeta;
|
|
1176
|
+
let chunks = [];
|
|
1177
|
+
if (PDF_EXT.has(ext)) {
|
|
1178
|
+
let pdf;
|
|
1179
|
+
try {
|
|
1180
|
+
pdf = await pdfToDoc(new Uint8Array(buf), { password: opts.filePassword });
|
|
1181
|
+
} catch (err) {
|
|
1182
|
+
skipped.push({ path: file, reason: err instanceof Error ? err.message : "PDF parse failed" });
|
|
1183
|
+
continue;
|
|
1184
|
+
}
|
|
1185
|
+
text = pdf.markdown;
|
|
1186
|
+
title = pdf.title ?? "";
|
|
1187
|
+
if (pdf.pageCount > 0) docMeta = { pdf_pages: pdf.pageCount };
|
|
1188
|
+
if (pdf.imageOnlyPages.length > 0) {
|
|
1189
|
+
docMeta = { ...docMeta, pdf_image_only_pages: pdf.imageOnlyPages.length };
|
|
1190
|
+
}
|
|
1191
|
+
if (!text.trim()) {
|
|
1192
|
+
skipped.push({
|
|
1193
|
+
path: file,
|
|
1194
|
+
reason: pdf.pageCount > 0 && pdf.imageOnlyPages.length === pdf.pageCount ? "image-only PDF (no text layer; likely scanned)" : "no extractable text from PDF"
|
|
1195
|
+
});
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
chunks = chunkMarkdown(text, opts);
|
|
1199
|
+
} else if (DOC_EXT.has(ext)) {
|
|
1200
|
+
let word;
|
|
1201
|
+
try {
|
|
1202
|
+
word = await docToDoc(new Uint8Array(buf), { password: opts.filePassword });
|
|
1203
|
+
} catch (err) {
|
|
1204
|
+
skipped.push({ path: file, reason: err instanceof Error ? err.message : "Word document parse failed" });
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
text = word.markdown;
|
|
1208
|
+
title = word.title ?? "";
|
|
1209
|
+
docMeta = { doc_format: word.format };
|
|
1210
|
+
if (word.encrypted) docMeta.doc_encrypted = true;
|
|
1211
|
+
if (!text.trim()) {
|
|
1212
|
+
skipped.push({ path: file, reason: "no extractable text from Word document" });
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
chunks = word.format === "docx" ? chunkMarkdown(text, opts) : chunkPlain(text, opts);
|
|
1216
|
+
} else if (EPUB_EXT.has(ext)) {
|
|
1217
|
+
let epub;
|
|
1218
|
+
try {
|
|
1219
|
+
epub = await epubToDoc(file);
|
|
1220
|
+
} catch (err) {
|
|
1221
|
+
skipped.push({ path: file, reason: err instanceof Error ? err.message : "EPUB parse failed" });
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
text = epub.markdown;
|
|
1225
|
+
title = epub.title ?? "";
|
|
1226
|
+
docMeta = { epub_chapters: epub.chapters };
|
|
1227
|
+
if (epub.author) docMeta.epub_author = epub.author;
|
|
1228
|
+
if (epub.language) docMeta.epub_language = epub.language;
|
|
1229
|
+
if (epub.encrypted) docMeta.epub_encrypted = true;
|
|
1230
|
+
chunks = chunkMarkdown(text, opts);
|
|
1231
|
+
} else {
|
|
1232
|
+
const raw = buf.toString("utf8");
|
|
1233
|
+
if (raw.includes("\0")) {
|
|
1234
|
+
skipped.push({ path: file, reason: "binary content" });
|
|
1235
|
+
continue;
|
|
1236
|
+
}
|
|
1237
|
+
text = raw;
|
|
1238
|
+
if (HTML_EXT.has(ext)) {
|
|
1239
|
+
const conv = htmlToMarkdown(raw, { filter: opts.htmlFilter });
|
|
1240
|
+
text = conv.markdown;
|
|
1241
|
+
title = conv.title ?? "";
|
|
1242
|
+
} else if (MD_EXT.has(ext)) {
|
|
1243
|
+
title = mdTitle(raw) ?? "";
|
|
1244
|
+
}
|
|
1245
|
+
if (!text.trim()) {
|
|
1246
|
+
skipped.push({
|
|
1247
|
+
path: file,
|
|
1248
|
+
reason: HTML_EXT.has(ext) && opts.htmlFilter ? `html filter '${opts.htmlFilter}' matched nothing` : "empty"
|
|
1249
|
+
});
|
|
1250
|
+
continue;
|
|
1251
|
+
}
|
|
1252
|
+
chunks = HTML_EXT.has(ext) || MD_EXT.has(ext) ? chunkMarkdown(text, opts) : chunkPlain(text, opts);
|
|
861
1253
|
}
|
|
862
|
-
if (!title) title = path.basename(file);
|
|
863
|
-
const chunks = HTML_EXT.has(ext) || MD_EXT.has(ext) ? chunkMarkdown(text, opts) : chunkPlain(text, opts);
|
|
864
1254
|
if (chunks.length === 0) {
|
|
865
1255
|
skipped.push({ path: file, reason: "no chunks produced" });
|
|
866
1256
|
continue;
|
|
867
1257
|
}
|
|
868
|
-
|
|
1258
|
+
if (!title) title = path2.basename(file);
|
|
1259
|
+
docs.push({ source: path2.resolve(file), title, chunks, metadata: docMeta });
|
|
869
1260
|
}
|
|
870
1261
|
return { docs, fileCount: files.length, skipped };
|
|
871
1262
|
}
|
|
@@ -966,7 +1357,7 @@ async function feedDocs(ks, docs, opts = {}) {
|
|
|
966
1357
|
};
|
|
967
1358
|
}
|
|
968
1359
|
async function feedTarget(ks, target, opts = {}) {
|
|
969
|
-
const loadOpts = { maxLen: opts.maxLen ?? 1200, overlap: opts.overlap ?? 180, htmlFilter: opts.htmlFilter };
|
|
1360
|
+
const loadOpts = { maxLen: opts.maxLen ?? 1200, overlap: opts.overlap ?? 180, htmlFilter: opts.htmlFilter, filePassword: opts.filePassword };
|
|
970
1361
|
const { docs, skipped } = await loadTarget(target, loadOpts);
|
|
971
1362
|
for (const s of skipped) {
|
|
972
1363
|
(opts.log ?? ((m) => console.log(m)))(`skipped ${s.path}: ${s.reason}`);
|
|
@@ -986,9 +1377,9 @@ async function feedText(ks, text, opts = {}) {
|
|
|
986
1377
|
}
|
|
987
1378
|
|
|
988
1379
|
// src/scrape.ts
|
|
989
|
-
import { mkdtempSync } from "fs";
|
|
990
|
-
import { tmpdir } from "os";
|
|
991
|
-
import
|
|
1380
|
+
import { mkdtempSync as mkdtempSync2 } from "fs";
|
|
1381
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
1382
|
+
import path3 from "path";
|
|
992
1383
|
import { randomUUID } from "crypto";
|
|
993
1384
|
import {
|
|
994
1385
|
CheerioCrawler,
|
|
@@ -1014,8 +1405,11 @@ function parseSitemapOption(value) {
|
|
|
1014
1405
|
if (key === "html-only") return { kind: "html-only" };
|
|
1015
1406
|
return { kind: "selector", selector: trimmed };
|
|
1016
1407
|
}
|
|
1017
|
-
var BINARY_LINK_RE = /\.(css|js|mjs|cjs|map|png|jpe?g|gif|svg|webp|avif|ico|bmp|tiff?|woff2?|ttf|otf|eot|
|
|
1018
|
-
|
|
1408
|
+
var BINARY_LINK_RE = /\.(css|js|mjs|cjs|map|png|jpe?g|gif|svg|webp|avif|ico|bmp|tiff?|woff2?|ttf|otf|eot|zip|gz|tgz|tar|rar|7z|bz2|xz|zst|mp3|mp4|m4a|webm|mov|avi|wmv|flv|xlsx?|pptx?|odt|ods|odp|rtf|dmg|exe|msi|apk|iso|bin|wasm|deb|rpm)(\?.*)?$/i;
|
|
1409
|
+
var PDF_URL_RE = /\.pdf(\?|#|$)/i;
|
|
1410
|
+
var DOC_URL_RE = /\.docx?(\?|#|$)/i;
|
|
1411
|
+
var EPUB_URL_RE = /\.epub(\?|#|$)/i;
|
|
1412
|
+
process.env.CRAWLEE_STORAGE_DIR ??= mkdtempSync2(path3.join(tmpdir2(), "gresmcp-crawl-"));
|
|
1019
1413
|
async function freshQueue() {
|
|
1020
1414
|
return RequestQueue.open(`gresmcp-${randomUUID()}`);
|
|
1021
1415
|
}
|
|
@@ -1090,18 +1484,84 @@ function dedupeDocs(docs) {
|
|
|
1090
1484
|
}
|
|
1091
1485
|
return [...bySource.values()];
|
|
1092
1486
|
}
|
|
1487
|
+
async function buildPdfDoc(url, data, deps) {
|
|
1488
|
+
try {
|
|
1489
|
+
const pdf = await pdfToDoc(data, { password: deps.filePassword });
|
|
1490
|
+
if (!pdf.markdown.trim()) {
|
|
1491
|
+
const reason = pdf.pageCount > 0 && pdf.imageOnlyPages.length === pdf.pageCount ? "image-only PDF (no text layer; likely scanned)" : "no extractable text from PDF";
|
|
1492
|
+
return { reason };
|
|
1493
|
+
}
|
|
1494
|
+
const chunks = chunkMarkdown(pdf.markdown, { maxLen: deps.maxLen, overlap: deps.overlap });
|
|
1495
|
+
if (chunks.length === 0) return { reason: "no chunks produced" };
|
|
1496
|
+
const metadata = { source_url: url, crawled_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1497
|
+
if (pdf.pageCount > 0) metadata.pdf_pages = pdf.pageCount;
|
|
1498
|
+
if (pdf.imageOnlyPages.length > 0) metadata.pdf_image_only_pages = pdf.imageOnlyPages.length;
|
|
1499
|
+
const tags = unionTags(void 0, deps.cliTags);
|
|
1500
|
+
if (tags) metadata.tags = tags;
|
|
1501
|
+
return { doc: { source: url, title: pdf.title?.trim() || titleFor(url), chunks, metadata } };
|
|
1502
|
+
} catch (err) {
|
|
1503
|
+
return { reason: err instanceof Error ? err.message : "PDF parse failed" };
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
async function buildEpubDoc(url, data, deps) {
|
|
1507
|
+
try {
|
|
1508
|
+
const epub = await epubFromBuffer(data);
|
|
1509
|
+
if (!epub.markdown.trim()) {
|
|
1510
|
+
return {
|
|
1511
|
+
reason: epub.encrypted ? "DRM-protected EPUB (encryption.xml; no readable text)" : "no extractable text from EPUB"
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
const chunks = chunkMarkdown(epub.markdown, { maxLen: deps.maxLen, overlap: deps.overlap });
|
|
1515
|
+
if (chunks.length === 0) return { reason: "no chunks produced" };
|
|
1516
|
+
const metadata = {
|
|
1517
|
+
source_url: url,
|
|
1518
|
+
crawled_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1519
|
+
epub_chapters: epub.chapters
|
|
1520
|
+
};
|
|
1521
|
+
if (epub.author) metadata.epub_author = epub.author;
|
|
1522
|
+
if (epub.language) metadata.epub_language = epub.language;
|
|
1523
|
+
if (epub.encrypted) metadata.epub_encrypted = true;
|
|
1524
|
+
const tags = unionTags(void 0, deps.cliTags);
|
|
1525
|
+
if (tags) metadata.tags = tags;
|
|
1526
|
+
return { doc: { source: url, title: epub.title?.trim() || titleFor(url), chunks, metadata } };
|
|
1527
|
+
} catch (err) {
|
|
1528
|
+
return { reason: err instanceof Error ? err.message : "EPUB parse failed" };
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1093
1531
|
function shortReason(err) {
|
|
1094
1532
|
const message = (err?.message ?? String(err)).replace(/\s+/g, " ").trim();
|
|
1095
1533
|
const contentType = /served Content-Type ([^,\s]+),/.exec(message);
|
|
1096
1534
|
if (contentType) return `unsupported content-type '${contentType[1]}'`;
|
|
1097
1535
|
return message.slice(0, 200);
|
|
1098
1536
|
}
|
|
1537
|
+
async function buildDocDoc(url, data, deps) {
|
|
1538
|
+
try {
|
|
1539
|
+
const word = await docToDoc(data, { password: deps.filePassword });
|
|
1540
|
+
if (!word.markdown.trim()) {
|
|
1541
|
+
return { reason: "no extractable text from Word document" };
|
|
1542
|
+
}
|
|
1543
|
+
const chunkOpts = { maxLen: deps.maxLen, overlap: deps.overlap };
|
|
1544
|
+
const chunks = word.format === "docx" ? chunkMarkdown(word.markdown, chunkOpts) : chunkPlain(word.markdown, chunkOpts);
|
|
1545
|
+
if (chunks.length === 0) return { reason: "no chunks produced" };
|
|
1546
|
+
const metadata = {
|
|
1547
|
+
source_url: url,
|
|
1548
|
+
crawled_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1549
|
+
doc_format: word.format
|
|
1550
|
+
};
|
|
1551
|
+
if (word.encrypted) metadata.doc_encrypted = true;
|
|
1552
|
+
const tags = unionTags(void 0, deps.cliTags);
|
|
1553
|
+
if (tags) metadata.tags = tags;
|
|
1554
|
+
return { doc: { source: url, title: word.title?.trim() || titleFor(url), chunks, metadata } };
|
|
1555
|
+
} catch (err) {
|
|
1556
|
+
return { reason: err instanceof Error ? err.message : "Word document parse failed" };
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1099
1559
|
async function crawlWithCheerio(queue, seedUrls, opts, sink) {
|
|
1100
1560
|
const crawler = new CheerioCrawler({
|
|
1101
1561
|
requestQueue: queue,
|
|
1102
1562
|
maxRequestsPerCrawl: Math.max(1, opts.maxPages),
|
|
1103
1563
|
maxCrawlDepth: Math.max(0, opts.depth),
|
|
1104
|
-
additionalMimeTypes: ["text/plain", "text/markdown"],
|
|
1564
|
+
additionalMimeTypes: ["text/plain", "text/markdown", "application/pdf", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/epub+zip", "application/zip", "application/octet-stream"],
|
|
1105
1565
|
async requestHandler(ctx) {
|
|
1106
1566
|
const { request, response, contentType, body, $ } = ctx;
|
|
1107
1567
|
const url = request.loadedUrl ?? request.url;
|
|
@@ -1169,6 +1629,47 @@ async function crawlWithCheerio(queue, seedUrls, opts, sink) {
|
|
|
1169
1629
|
return;
|
|
1170
1630
|
}
|
|
1171
1631
|
sink.docs.push(doc);
|
|
1632
|
+
} else if (type === "application/pdf") {
|
|
1633
|
+
const data = typeof body === "string" ? new TextEncoder().encode(body) : new Uint8Array(body);
|
|
1634
|
+
const { doc, reason } = await buildPdfDoc(url, data, sinkDeps(opts));
|
|
1635
|
+
if (reason || !doc) {
|
|
1636
|
+
sink.skipped.push({ path: url, reason: reason ?? "PDF produced no document" });
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
sink.docs.push(doc);
|
|
1640
|
+
} else if (type === "application/msword" || type === "application/vnd.openxmlformats-officedocument.wordprocessingml.document") {
|
|
1641
|
+
const data = typeof body === "string" ? new TextEncoder().encode(body) : new Uint8Array(body);
|
|
1642
|
+
const { doc, reason } = await buildDocDoc(url, data, sinkDeps(opts));
|
|
1643
|
+
if (reason || !doc) {
|
|
1644
|
+
sink.skipped.push({ path: url, reason: reason ?? "Word document produced no document" });
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
sink.docs.push(doc);
|
|
1648
|
+
} else if (type === "application/epub+zip" || type === "application/zip") {
|
|
1649
|
+
const data = typeof body === "string" ? new TextEncoder().encode(body) : new Uint8Array(body);
|
|
1650
|
+
const { doc, reason } = await buildEpubDoc(url, data, sinkDeps(opts));
|
|
1651
|
+
if (reason || !doc) {
|
|
1652
|
+
sink.skipped.push({ path: url, reason: reason ?? "EPUB produced no document" });
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
sink.docs.push(doc);
|
|
1656
|
+
} else if (type === "application/octet-stream") {
|
|
1657
|
+
const data = typeof body === "string" ? new TextEncoder().encode(body) : new Uint8Array(body);
|
|
1658
|
+
if (EPUB_URL_RE.test(url)) {
|
|
1659
|
+
const { doc: doc2, reason: reason2 } = await buildEpubDoc(url, data, sinkDeps(opts));
|
|
1660
|
+
if (reason2 || !doc2) {
|
|
1661
|
+
sink.skipped.push({ path: url, reason: reason2 ?? "EPUB produced no document" });
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
sink.docs.push(doc2);
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
const { doc, reason } = await buildDocDoc(url, data, sinkDeps(opts));
|
|
1668
|
+
if (reason || !doc) {
|
|
1669
|
+
sink.skipped.push({ path: url, reason: reason ?? "Word document produced no document" });
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
sink.docs.push(doc);
|
|
1172
1673
|
} else {
|
|
1173
1674
|
sink.skipped.push({ path: url, reason: `unsupported content-type '${type}'` });
|
|
1174
1675
|
}
|
|
@@ -1192,6 +1693,7 @@ function sinkDeps(opts) {
|
|
|
1192
1693
|
cliTags: opts.tags,
|
|
1193
1694
|
urlFilter: opts.urlFilter,
|
|
1194
1695
|
htmlFilter: opts.htmlFilter,
|
|
1696
|
+
filePassword: opts.filePassword,
|
|
1195
1697
|
followLinks: opts.followLinks,
|
|
1196
1698
|
linkSelector: opts.linkSelector,
|
|
1197
1699
|
selectorStats: opts.selectorStats
|
|
@@ -1212,9 +1714,75 @@ async function requireBrowserEngine(kind) {
|
|
|
1212
1714
|
throw new Error(`--crawler ${kind} requires ${moduleName}, which is not installed (install it with: ${install})`);
|
|
1213
1715
|
}
|
|
1214
1716
|
}
|
|
1717
|
+
async function handleBrowserPdf(url, deps) {
|
|
1718
|
+
try {
|
|
1719
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
1720
|
+
if (!res.ok) {
|
|
1721
|
+
deps.sink.skipped.push({ path: url, reason: `HTTP ${res.status}` });
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
const data = new Uint8Array(await res.arrayBuffer());
|
|
1725
|
+
const { doc, reason } = await buildPdfDoc(url, data, deps);
|
|
1726
|
+
if (reason || !doc) {
|
|
1727
|
+
deps.sink.skipped.push({ path: url, reason: reason ?? "PDF produced no document" });
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
deps.sink.docs.push(doc);
|
|
1731
|
+
} catch (err) {
|
|
1732
|
+
deps.sink.skipped.push({ path: url, reason: err instanceof Error ? err.message : "PDF fetch failed" });
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
async function handleBrowserDoc(url, deps) {
|
|
1736
|
+
try {
|
|
1737
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
1738
|
+
if (!res.ok) {
|
|
1739
|
+
deps.sink.skipped.push({ path: url, reason: `HTTP ${res.status}` });
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
const data = new Uint8Array(await res.arrayBuffer());
|
|
1743
|
+
const { doc, reason } = await buildDocDoc(url, data, deps);
|
|
1744
|
+
if (reason || !doc) {
|
|
1745
|
+
deps.sink.skipped.push({ path: url, reason: reason ?? "Word document produced no document" });
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
deps.sink.docs.push(doc);
|
|
1749
|
+
} catch (err) {
|
|
1750
|
+
deps.sink.skipped.push({ path: url, reason: err instanceof Error ? err.message : "Word document fetch failed" });
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
async function handleBrowserEpub(url, deps) {
|
|
1754
|
+
try {
|
|
1755
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
1756
|
+
if (!res.ok) {
|
|
1757
|
+
deps.sink.skipped.push({ path: url, reason: `HTTP ${res.status}` });
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
const data = new Uint8Array(await res.arrayBuffer());
|
|
1761
|
+
const { doc, reason } = await buildEpubDoc(url, data, deps);
|
|
1762
|
+
if (reason || !doc) {
|
|
1763
|
+
deps.sink.skipped.push({ path: url, reason: reason ?? "EPUB produced no document" });
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
deps.sink.docs.push(doc);
|
|
1767
|
+
} catch (err) {
|
|
1768
|
+
deps.sink.skipped.push({ path: url, reason: err instanceof Error ? err.message : "EPUB fetch failed" });
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1215
1771
|
async function handleBrowserPage(ctx, deps) {
|
|
1216
1772
|
const request = ctx.request;
|
|
1217
1773
|
const url = request.loadedUrl ?? request.url;
|
|
1774
|
+
if (PDF_URL_RE.test(url)) {
|
|
1775
|
+
await handleBrowserPdf(url, deps);
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
if (DOC_URL_RE.test(url)) {
|
|
1779
|
+
await handleBrowserDoc(url, deps);
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
if (EPUB_URL_RE.test(url)) {
|
|
1783
|
+
await handleBrowserEpub(url, deps);
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1218
1786
|
const status = responseStatus(ctx.response);
|
|
1219
1787
|
if (status === void 0 || status < 200 || status >= 300) {
|
|
1220
1788
|
deps.sink.skipped.push({ path: url, reason: status === void 0 ? "no HTTP response" : `HTTP ${status}` });
|
|
@@ -1426,6 +1994,7 @@ async function feedUrl(ks, url, opts = {}) {
|
|
|
1426
1994
|
tags: opts.tags,
|
|
1427
1995
|
urlFilter: opts.urlFilter,
|
|
1428
1996
|
htmlFilter: opts.htmlFilter,
|
|
1997
|
+
filePassword: opts.filePassword,
|
|
1429
1998
|
sitemap: opts.sitemap,
|
|
1430
1999
|
log: log2
|
|
1431
2000
|
});
|
|
@@ -1670,7 +2239,7 @@ async function runChecks(opts) {
|
|
|
1670
2239
|
}
|
|
1671
2240
|
|
|
1672
2241
|
// src/version.ts
|
|
1673
|
-
var VERSION = "1.
|
|
2242
|
+
var VERSION = "1.3.0";
|
|
1674
2243
|
|
|
1675
2244
|
// src/cli.ts
|
|
1676
2245
|
async function run(fn) {
|
|
@@ -1850,7 +2419,7 @@ ksCmd.command("delete <name>").description("Delete a knowledge source and all of
|
|
|
1850
2419
|
console.log(`Deleted knowledge source '${name}'`);
|
|
1851
2420
|
})
|
|
1852
2421
|
);
|
|
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(
|
|
2422
|
+
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, pdf, doc, docx, epub)").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
2423
|
"--url-filter <glob>",
|
|
1855
2424
|
"Only follow --url links whose URL matches this glob (repeatable; a link is followed if it matches any glob, e.g. '**/docs/**')",
|
|
1856
2425
|
((v, prev) => [...prev, v]),
|
|
@@ -1861,6 +2430,9 @@ program.command("feed <ks>").description("Feed data into a knowledge source (fro
|
|
|
1861
2430
|
).option(
|
|
1862
2431
|
"--sitemap <mode>",
|
|
1863
2432
|
"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')"
|
|
2433
|
+
).option(
|
|
2434
|
+
"--file-password <password>",
|
|
2435
|
+
"Password for encrypted files (PDF, DOC/DOCX); applies to --path and --url ingestion. Owner-restricted PDFs without a user password are read without it"
|
|
1864
2436
|
).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(
|
|
1865
2437
|
(name, opts) => run(async () => {
|
|
1866
2438
|
await initDb();
|
|
@@ -1889,7 +2461,8 @@ program.command("feed <ks>").description("Feed data into a knowledge source (fro
|
|
|
1889
2461
|
overlap: opts.overlap,
|
|
1890
2462
|
tags: parseTags(opts.tags),
|
|
1891
2463
|
metadata: parseKeyValue(opts.metadata ?? []),
|
|
1892
|
-
htmlFilter: opts.htmlFilter
|
|
2464
|
+
htmlFilter: opts.htmlFilter,
|
|
2465
|
+
filePassword: opts.filePassword
|
|
1893
2466
|
};
|
|
1894
2467
|
let summary;
|
|
1895
2468
|
const since = await dbClockTimestamp();
|