@gresmcp/mcp 1.0.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 +36 -10
- package/dist/cli.js +953 -119
- 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) {
|
|
@@ -406,25 +446,276 @@ function turndown() {
|
|
|
406
446
|
}
|
|
407
447
|
return service;
|
|
408
448
|
}
|
|
409
|
-
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 = {}) {
|
|
410
485
|
const dom = new JSDOM(html);
|
|
411
486
|
const doc = dom.window.document;
|
|
412
487
|
doc.querySelectorAll(REMOVE_SELECTORS).forEach((el) => el.remove());
|
|
413
488
|
const title = doc.title?.trim() || doc.querySelector("h1")?.textContent?.trim() || void 0;
|
|
414
|
-
const root = doc.body ?? doc.documentElement;
|
|
415
489
|
let markdown = "";
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
+
}
|
|
420
500
|
}
|
|
421
501
|
markdown = markdown.replace(/\n{3,}/g, "\n\n").trim();
|
|
422
502
|
return { markdown, title: title || void 0 };
|
|
423
503
|
}
|
|
504
|
+
function metaContent(doc, selectors) {
|
|
505
|
+
for (const selector of selectors) {
|
|
506
|
+
const el = doc.querySelector(selector);
|
|
507
|
+
const value = (el?.getAttribute("content") ?? el?.textContent ?? "").trim();
|
|
508
|
+
if (value) return value;
|
|
509
|
+
}
|
|
510
|
+
return void 0;
|
|
511
|
+
}
|
|
512
|
+
function collectTags(doc) {
|
|
513
|
+
const tags = [];
|
|
514
|
+
const keywords = metaContent(doc, ['meta[name="keywords"]']);
|
|
515
|
+
if (keywords) {
|
|
516
|
+
for (const part of keywords.split(",")) {
|
|
517
|
+
const tag = part.trim();
|
|
518
|
+
if (tag) tags.push(tag);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
doc.querySelectorAll('meta[property="og:article:tag"]').forEach((el) => {
|
|
522
|
+
const value = (el.getAttribute("content") ?? "").trim();
|
|
523
|
+
if (value) tags.push(value);
|
|
524
|
+
});
|
|
525
|
+
const seen = /* @__PURE__ */ new Set();
|
|
526
|
+
return tags.filter((t) => {
|
|
527
|
+
const key = t.toLowerCase();
|
|
528
|
+
if (seen.has(key)) return false;
|
|
529
|
+
seen.add(key);
|
|
530
|
+
return true;
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
function extractPage(html, opts = {}) {
|
|
534
|
+
const dom = new JSDOM(html);
|
|
535
|
+
const doc = dom.window.document;
|
|
536
|
+
const title = doc.title?.trim() || doc.querySelector('meta[property="og:title"]')?.getAttribute("content")?.trim() || doc.querySelector("h1")?.textContent?.trim() || void 0;
|
|
537
|
+
const description = metaContent(doc, [
|
|
538
|
+
'meta[name="description"]',
|
|
539
|
+
'meta[property="og:description"]'
|
|
540
|
+
]);
|
|
541
|
+
const tags = collectTags(doc);
|
|
542
|
+
doc.querySelectorAll(REMOVE_SELECTORS).forEach((el) => el.remove());
|
|
543
|
+
let markdown = "";
|
|
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 {
|
|
550
|
+
try {
|
|
551
|
+
if (isProbablyReaderable(doc)) {
|
|
552
|
+
const article = new Readability(doc).parse();
|
|
553
|
+
if (article?.content) markdown = turndown().turndown(article.content);
|
|
554
|
+
}
|
|
555
|
+
} catch {
|
|
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
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
markdown = markdown.replace(/\n{3,}/g, "\n\n").trim();
|
|
568
|
+
const likelyShell = markdown.length < SHELL_TEXT_THRESHOLD && SHELL_MARKER_RE.test(html);
|
|
569
|
+
return {
|
|
570
|
+
markdown,
|
|
571
|
+
title,
|
|
572
|
+
description,
|
|
573
|
+
tags: tags.length > 0 ? tags : void 0,
|
|
574
|
+
likelyShell,
|
|
575
|
+
filterMatched
|
|
576
|
+
};
|
|
577
|
+
}
|
|
424
578
|
|
|
425
579
|
// src/ingest.ts
|
|
426
580
|
init_db();
|
|
427
|
-
|
|
581
|
+
|
|
582
|
+
// src/progress.ts
|
|
583
|
+
var BAR_WIDTH = 20;
|
|
584
|
+
var PLAIN_STEP = 32;
|
|
585
|
+
function fmt(n) {
|
|
586
|
+
return n.toLocaleString("en-US");
|
|
587
|
+
}
|
|
588
|
+
function bar(pct2) {
|
|
589
|
+
const filled = Math.max(0, Math.min(BAR_WIDTH, Math.round(pct2 * BAR_WIDTH)));
|
|
590
|
+
return "#".repeat(filled) + ".".repeat(BAR_WIDTH - filled);
|
|
591
|
+
}
|
|
592
|
+
function pct(p) {
|
|
593
|
+
return `${Math.round(Math.max(0, Math.min(1, p)) * 100)}%`;
|
|
594
|
+
}
|
|
595
|
+
var NOOP_HANDLE = { handled: () => {
|
|
596
|
+
} };
|
|
597
|
+
var FeedProgress = class {
|
|
598
|
+
write;
|
|
599
|
+
interactive;
|
|
600
|
+
total;
|
|
601
|
+
rejected;
|
|
602
|
+
slots = [];
|
|
603
|
+
filesDone = 0;
|
|
604
|
+
totalChars = 0;
|
|
605
|
+
blockLines = 0;
|
|
606
|
+
finished = false;
|
|
607
|
+
constructor(opts) {
|
|
608
|
+
this.total = Math.max(0, Math.floor(opts.total));
|
|
609
|
+
this.rejected = Math.max(0, Math.floor(opts.rejected));
|
|
610
|
+
this.write = opts.out ?? ((s) => process.stderr.write(s));
|
|
611
|
+
this.interactive = opts.interactive ?? (opts.out === void 0 && !!process.stderr.isTTY);
|
|
612
|
+
}
|
|
613
|
+
get isInteractive() {
|
|
614
|
+
return this.interactive;
|
|
615
|
+
}
|
|
616
|
+
startFile(source, totalChunks, totalChars) {
|
|
617
|
+
if (this.finished) return NOOP_HANDLE;
|
|
618
|
+
const slot = { source, totalChunks, totalChars, doneChunks: 0, doneChars: 0, printedChunks: 0 };
|
|
619
|
+
this.slots.push(slot);
|
|
620
|
+
this.render();
|
|
621
|
+
return {
|
|
622
|
+
handled: (chunks, chars) => this.handleSlot(slot, chunks, chars)
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
log(msg) {
|
|
626
|
+
if (this.finished) return;
|
|
627
|
+
this.clearBlock();
|
|
628
|
+
this.write(`${msg}
|
|
629
|
+
`);
|
|
630
|
+
}
|
|
631
|
+
finish() {
|
|
632
|
+
if (this.finished) return;
|
|
633
|
+
this.finished = true;
|
|
634
|
+
if (this.slots.length === 0 && this.total === 0 && this.rejected === 0) return;
|
|
635
|
+
if (this.interactive) {
|
|
636
|
+
this.redraw(this.lines());
|
|
637
|
+
this.write("\n");
|
|
638
|
+
this.blockLines = 0;
|
|
639
|
+
} else {
|
|
640
|
+
this.write(`${this.totalLine()}
|
|
641
|
+
`);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
close() {
|
|
645
|
+
if (this.finished) return;
|
|
646
|
+
this.finished = true;
|
|
647
|
+
this.clearBlock();
|
|
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
|
+
}
|
|
673
|
+
render() {
|
|
674
|
+
if (!this.interactive) return;
|
|
675
|
+
this.redraw(this.lines());
|
|
676
|
+
}
|
|
677
|
+
printFileLine(slot) {
|
|
678
|
+
slot.printedChunks = slot.doneChunks;
|
|
679
|
+
this.write(`${this.fileLine(slot)}
|
|
680
|
+
`);
|
|
681
|
+
}
|
|
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}`;
|
|
685
|
+
}
|
|
686
|
+
totalLine() {
|
|
687
|
+
const p = this.total > 0 ? this.filesDone / this.total : 0;
|
|
688
|
+
const rejected = this.rejected > 0 ? ` | rejected: ${this.rejected} (excluded)` : "";
|
|
689
|
+
return `total: ${this.filesDone}/${this.total} files (${pct(p)}) ${fmt(this.totalChars)} chars${rejected}`;
|
|
690
|
+
}
|
|
691
|
+
redraw(lines) {
|
|
692
|
+
let out = "";
|
|
693
|
+
if (this.blockLines > 1) out += `\x1B[${this.blockLines - 1}A`;
|
|
694
|
+
out += "\r";
|
|
695
|
+
for (let i = 0; i < lines.length; i++) {
|
|
696
|
+
out += `\x1B[2K${lines[i]}${i < lines.length - 1 ? "\n" : ""}`;
|
|
697
|
+
}
|
|
698
|
+
this.write(out);
|
|
699
|
+
this.blockLines = lines.length;
|
|
700
|
+
}
|
|
701
|
+
clearBlock() {
|
|
702
|
+
if (!this.interactive || this.blockLines === 0) {
|
|
703
|
+
this.blockLines = 0;
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
let out = "";
|
|
707
|
+
if (this.blockLines > 1) out += `\x1B[${this.blockLines - 1}A`;
|
|
708
|
+
out += "\r";
|
|
709
|
+
for (let i = 0; i < this.blockLines; i++) {
|
|
710
|
+
out += `\x1B[2K${i < this.blockLines - 1 ? "\n" : ""}`;
|
|
711
|
+
}
|
|
712
|
+
out += "\n";
|
|
713
|
+
this.write(out);
|
|
714
|
+
this.blockLines = 0;
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
// src/ingest.ts
|
|
428
719
|
var MD_EXT = /* @__PURE__ */ new Set([".md", ".markdown", ".mdx"]);
|
|
429
720
|
var HTML_EXT = /* @__PURE__ */ new Set([".html", ".htm", ".xhtml"]);
|
|
430
721
|
var TEXT_EXT = /* @__PURE__ */ new Set([
|
|
@@ -555,14 +846,17 @@ async function loadTarget(target, opts) {
|
|
|
555
846
|
let text = raw;
|
|
556
847
|
let title = "";
|
|
557
848
|
if (HTML_EXT.has(ext)) {
|
|
558
|
-
const conv = htmlToMarkdown(raw);
|
|
849
|
+
const conv = htmlToMarkdown(raw, { filter: opts.htmlFilter });
|
|
559
850
|
text = conv.markdown;
|
|
560
851
|
title = conv.title ?? "";
|
|
561
852
|
} else if (MD_EXT.has(ext)) {
|
|
562
853
|
title = mdTitle(raw) ?? "";
|
|
563
854
|
}
|
|
564
855
|
if (!text.trim()) {
|
|
565
|
-
skipped.push({
|
|
856
|
+
skipped.push({
|
|
857
|
+
path: file,
|
|
858
|
+
reason: HTML_EXT.has(ext) && opts.htmlFilter ? `html filter '${opts.htmlFilter}' matched nothing` : "empty"
|
|
859
|
+
});
|
|
566
860
|
continue;
|
|
567
861
|
}
|
|
568
862
|
if (!title) title = path.basename(file);
|
|
@@ -575,115 +869,587 @@ async function loadTarget(target, opts) {
|
|
|
575
869
|
}
|
|
576
870
|
return { docs, fileCount: files.length, skipped };
|
|
577
871
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const
|
|
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;
|
|
872
|
+
var DOC_CONCURRENCY = 3;
|
|
873
|
+
async function feedDocs(ks, docs, opts = {}) {
|
|
874
|
+
const log2 = opts.log ?? ((m) => console.log(m));
|
|
875
|
+
const progress = opts.progress;
|
|
876
|
+
const batchSize = opts.batchSize ?? 32;
|
|
877
|
+
const baseMeta = { ...opts.metadata ?? {} };
|
|
878
|
+
const cliTags = opts.tags && opts.tags.length > 0 ? opts.tags : void 0;
|
|
879
|
+
const say = (msg) => progress ? progress.log(msg) : log2(msg);
|
|
616
880
|
let deletedForReplace = 0;
|
|
617
|
-
const pending = [];
|
|
618
881
|
let skippedExisting = 0;
|
|
619
|
-
|
|
882
|
+
let inserted = 0;
|
|
883
|
+
let announcedEmbed = false;
|
|
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
|
+
);
|
|
620
890
|
if (opts.replace) {
|
|
621
|
-
|
|
891
|
+
const deleted = await deleteBySource(ks, doc.source);
|
|
892
|
+
deletedForReplace += deleted;
|
|
622
893
|
}
|
|
623
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 = [];
|
|
624
898
|
for (const chunk of doc.chunks) {
|
|
625
899
|
if (!opts.replace && existing.get(chunk.index) === contentHash(chunk.content)) {
|
|
626
900
|
skippedExisting++;
|
|
901
|
+
file?.handled(1, chunk.content.length);
|
|
627
902
|
continue;
|
|
628
903
|
}
|
|
629
|
-
|
|
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;
|
|
630
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;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
const workerCount = Math.min(DOC_CONCURRENCY, docs.length);
|
|
952
|
+
await Promise.all(Array.from({ length: workerCount }, () => runWorker()));
|
|
953
|
+
if (firstError !== void 0) {
|
|
954
|
+
progress?.close();
|
|
955
|
+
throw firstError;
|
|
631
956
|
}
|
|
632
|
-
|
|
633
|
-
url: ollamaUrl(opts.url),
|
|
634
|
-
dryRun: opts.dryRun ?? false,
|
|
635
|
-
batchSize: opts.batchSize ?? 32,
|
|
636
|
-
log
|
|
637
|
-
});
|
|
957
|
+
progress?.finish();
|
|
638
958
|
return {
|
|
639
959
|
documents: docs.length,
|
|
640
960
|
chunks: docs.reduce((n, d) => n + d.chunks.length, 0),
|
|
641
961
|
inserted,
|
|
642
962
|
skippedExisting,
|
|
643
963
|
deletedForReplace,
|
|
644
|
-
skippedFiles:
|
|
964
|
+
skippedFiles: [],
|
|
645
965
|
dryRun: opts.dryRun ?? false
|
|
646
966
|
};
|
|
647
967
|
}
|
|
968
|
+
async function feedTarget(ks, target, opts = {}) {
|
|
969
|
+
const loadOpts = { maxLen: opts.maxLen ?? 1200, overlap: opts.overlap ?? 180, htmlFilter: opts.htmlFilter };
|
|
970
|
+
const { docs, skipped } = await loadTarget(target, loadOpts);
|
|
971
|
+
for (const s of skipped) {
|
|
972
|
+
(opts.log ?? ((m) => console.log(m)))(`skipped ${s.path}: ${s.reason}`);
|
|
973
|
+
}
|
|
974
|
+
const progress = opts.progress ?? new FeedProgress({ total: docs.length, rejected: skipped.length });
|
|
975
|
+
const summary = await feedDocs(ks, docs, { ...opts, progress });
|
|
976
|
+
return { ...summary, skippedFiles: skipped };
|
|
977
|
+
}
|
|
648
978
|
async function feedText(ks, text, opts = {}) {
|
|
649
|
-
const
|
|
979
|
+
const log2 = opts.log ?? ((m) => console.log(m));
|
|
650
980
|
const source = opts.sourceName?.trim() || "manual";
|
|
651
981
|
const title = opts.title?.trim() || source;
|
|
652
982
|
const chunks = chunkMarkdown(text, { maxLen: opts.maxLen ?? 1200, overlap: opts.overlap ?? 180 });
|
|
653
983
|
const doc = { source, title, chunks };
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
984
|
+
const progress = opts.progress ?? new FeedProgress({ total: 1, rejected: 0 });
|
|
985
|
+
return feedDocs(ks, [doc], { ...opts, progress });
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// src/scrape.ts
|
|
989
|
+
import { mkdtempSync } from "fs";
|
|
990
|
+
import { tmpdir } from "os";
|
|
991
|
+
import path2 from "path";
|
|
992
|
+
import { randomUUID } from "crypto";
|
|
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";
|
|
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");
|
|
662
1010
|
}
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
+
}
|
|
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;
|
|
1018
|
+
process.env.CRAWLEE_STORAGE_DIR ??= mkdtempSync(path2.join(tmpdir(), "gresmcp-crawl-"));
|
|
1019
|
+
async function freshQueue() {
|
|
1020
|
+
return RequestQueue.open(`gresmcp-${randomUUID()}`);
|
|
1021
|
+
}
|
|
1022
|
+
function normalizeSeedUrl(seed) {
|
|
1023
|
+
let url;
|
|
1024
|
+
try {
|
|
1025
|
+
url = new URL(seed.trim());
|
|
1026
|
+
} catch {
|
|
1027
|
+
throw new Error(`invalid URL '${seed}'`);
|
|
1028
|
+
}
|
|
1029
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
1030
|
+
throw new Error(`unsupported URL protocol '${url.protocol.replace(":", "")}' (use http or https)`);
|
|
1031
|
+
}
|
|
1032
|
+
url.hash = "";
|
|
1033
|
+
return url.toString();
|
|
1034
|
+
}
|
|
1035
|
+
function responseStatus(response) {
|
|
1036
|
+
if (!response || typeof response !== "object") return void 0;
|
|
1037
|
+
const res = response;
|
|
1038
|
+
if (typeof res.status === "number") return res.status;
|
|
1039
|
+
if (typeof res.status === "function") {
|
|
1040
|
+
const value = res.status();
|
|
1041
|
+
return typeof value === "number" ? value : void 0;
|
|
1042
|
+
}
|
|
1043
|
+
if (typeof res.statusCode === "number") return res.statusCode;
|
|
1044
|
+
return void 0;
|
|
1045
|
+
}
|
|
1046
|
+
function toText(body) {
|
|
1047
|
+
return typeof body === "string" ? body : body.toString("utf-8");
|
|
1048
|
+
}
|
|
1049
|
+
function titleFor(url, fallback) {
|
|
1050
|
+
const trimmed = fallback?.trim();
|
|
1051
|
+
if (trimmed) return trimmed;
|
|
1052
|
+
try {
|
|
1053
|
+
const segment = new URL(url).pathname.split("/").filter(Boolean).pop();
|
|
1054
|
+
const name = segment ? decodeURIComponent(segment) : "";
|
|
1055
|
+
return name || url;
|
|
1056
|
+
} catch {
|
|
1057
|
+
return url;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
function unionTags(...lists) {
|
|
1061
|
+
const out = [];
|
|
1062
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1063
|
+
for (const list of lists) {
|
|
1064
|
+
for (const tag of list ?? []) {
|
|
1065
|
+
const value = tag.trim();
|
|
1066
|
+
if (!value) continue;
|
|
1067
|
+
const key = value.toLowerCase();
|
|
1068
|
+
if (seen.has(key)) continue;
|
|
1069
|
+
seen.add(key);
|
|
1070
|
+
out.push(value);
|
|
668
1071
|
}
|
|
669
|
-
pending.push({ doc, chunk, metadata: meta });
|
|
670
1072
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
1073
|
+
return out.length > 0 ? out : void 0;
|
|
1074
|
+
}
|
|
1075
|
+
function buildDoc(url, kind, text, page, deps) {
|
|
1076
|
+
const chunkOpts = { maxLen: deps.maxLen, overlap: deps.overlap };
|
|
1077
|
+
const chunks = kind === "plain" ? chunkPlain(text, chunkOpts) : chunkMarkdown(text, chunkOpts);
|
|
1078
|
+
if (chunks.length === 0) return void 0;
|
|
1079
|
+
const title = kind === "html" ? titleFor(url, page?.title) : kind === "md" ? mdTitle(text) ?? titleFor(url) : titleFor(url);
|
|
1080
|
+
const metadata = { source_url: url, crawled_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1081
|
+
if (page?.description) metadata.description = page.description;
|
|
1082
|
+
const tags = unionTags(page?.tags, deps.cliTags);
|
|
1083
|
+
if (tags) metadata.tags = tags;
|
|
1084
|
+
return { source: url, title, chunks, metadata };
|
|
1085
|
+
}
|
|
1086
|
+
function dedupeDocs(docs) {
|
|
1087
|
+
const bySource = /* @__PURE__ */ new Map();
|
|
1088
|
+
for (const doc of docs) {
|
|
1089
|
+
if (!bySource.has(doc.source)) bySource.set(doc.source, doc);
|
|
1090
|
+
}
|
|
1091
|
+
return [...bySource.values()];
|
|
1092
|
+
}
|
|
1093
|
+
function shortReason(err) {
|
|
1094
|
+
const message = (err?.message ?? String(err)).replace(/\s+/g, " ").trim();
|
|
1095
|
+
const contentType = /served Content-Type ([^,\s]+),/.exec(message);
|
|
1096
|
+
if (contentType) return `unsupported content-type '${contentType[1]}'`;
|
|
1097
|
+
return message.slice(0, 200);
|
|
1098
|
+
}
|
|
1099
|
+
async function crawlWithCheerio(queue, seedUrls, opts, sink) {
|
|
1100
|
+
const crawler = new CheerioCrawler({
|
|
1101
|
+
requestQueue: queue,
|
|
1102
|
+
maxRequestsPerCrawl: Math.max(1, opts.maxPages),
|
|
1103
|
+
maxCrawlDepth: Math.max(0, opts.depth),
|
|
1104
|
+
additionalMimeTypes: ["text/plain", "text/markdown"],
|
|
1105
|
+
async requestHandler(ctx) {
|
|
1106
|
+
const { request, response, contentType, body, $ } = ctx;
|
|
1107
|
+
const url = request.loadedUrl ?? request.url;
|
|
1108
|
+
const status = responseStatus(response);
|
|
1109
|
+
if (status === void 0 || status < 200 || status >= 300) {
|
|
1110
|
+
sink.skipped.push({ path: url, reason: status === void 0 ? "no HTTP response" : `HTTP ${status}` });
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
const type = contentType?.type ?? "";
|
|
1114
|
+
if (type === "text/html" || type === "application/xhtml+xml") {
|
|
1115
|
+
const html = typeof body === "string" ? body : toText(body);
|
|
1116
|
+
const page = extractPage(html, { filter: opts.htmlFilter });
|
|
1117
|
+
if (page.likelyShell) {
|
|
1118
|
+
sink.shellUrls?.push(url);
|
|
1119
|
+
sink.skipped.push({ path: url, reason: "likely JS-rendered shell" });
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
if (opts.htmlFilter && !page.filterMatched) {
|
|
1123
|
+
sink.skipped.push({ path: url, reason: `html filter '${opts.htmlFilter}' matched nothing` });
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
if (!page.markdown.trim()) {
|
|
1127
|
+
sink.skipped.push({ path: url, reason: "empty" });
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
const doc = buildDoc(url, "html", page.markdown, page, sinkDeps(opts));
|
|
1131
|
+
if (!doc) {
|
|
1132
|
+
sink.skipped.push({ path: url, reason: "no chunks produced" });
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
sink.docs.push(doc);
|
|
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
|
+
}
|
|
1158
|
+
} else if (type === "text/markdown") {
|
|
1159
|
+
const doc = buildDoc(url, "md", toText(body), void 0, sinkDeps(opts));
|
|
1160
|
+
if (!doc) {
|
|
1161
|
+
sink.skipped.push({ path: url, reason: "no chunks produced" });
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
sink.docs.push(doc);
|
|
1165
|
+
} else if (type === "text/plain") {
|
|
1166
|
+
const doc = buildDoc(url, "plain", toText(body), void 0, sinkDeps(opts));
|
|
1167
|
+
if (!doc) {
|
|
1168
|
+
sink.skipped.push({ path: url, reason: "no chunks produced" });
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
sink.docs.push(doc);
|
|
1172
|
+
} else {
|
|
1173
|
+
sink.skipped.push({ path: url, reason: `unsupported content-type '${type}'` });
|
|
1174
|
+
}
|
|
1175
|
+
},
|
|
1176
|
+
async failedRequestHandler(ctx, error) {
|
|
1177
|
+
const url = ctx.request.loadedUrl ?? ctx.request.url;
|
|
1178
|
+
sink.skipped.push({ path: url, reason: shortReason(error) });
|
|
1179
|
+
}
|
|
676
1180
|
});
|
|
1181
|
+
try {
|
|
1182
|
+
await crawler.run(seedUrls);
|
|
1183
|
+
} finally {
|
|
1184
|
+
await crawler.teardown().catch(() => {
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
function sinkDeps(opts) {
|
|
677
1189
|
return {
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
async function importOptional(name) {
|
|
1201
|
+
try {
|
|
1202
|
+
return await import(name);
|
|
1203
|
+
} catch {
|
|
1204
|
+
return void 0;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
async function requireBrowserEngine(kind) {
|
|
1208
|
+
const moduleName = kind === "playwright" ? "playwright" : "puppeteer";
|
|
1209
|
+
const mod = await importOptional(moduleName);
|
|
1210
|
+
if (!mod) {
|
|
1211
|
+
const install = kind === "playwright" ? "npm i -g playwright && npx playwright install chromium" : "npm i -g puppeteer";
|
|
1212
|
+
throw new Error(`--crawler ${kind} requires ${moduleName}, which is not installed (install it with: ${install})`);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
async function handleBrowserPage(ctx, deps) {
|
|
1216
|
+
const request = ctx.request;
|
|
1217
|
+
const url = request.loadedUrl ?? request.url;
|
|
1218
|
+
const status = responseStatus(ctx.response);
|
|
1219
|
+
if (status === void 0 || status < 200 || status >= 300) {
|
|
1220
|
+
deps.sink.skipped.push({ path: url, reason: status === void 0 ? "no HTTP response" : `HTTP ${status}` });
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
const html = await ctx.page.content();
|
|
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
|
+
}
|
|
1229
|
+
if (!page.markdown.trim()) {
|
|
1230
|
+
deps.sink.skipped.push({ path: url, reason: "empty after render" });
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
const doc = buildDoc(url, "html", page.markdown, page, deps);
|
|
1234
|
+
if (!doc) {
|
|
1235
|
+
deps.sink.skipped.push({ path: url, reason: "no chunks produced" });
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
deps.sink.docs.push(doc);
|
|
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
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
async function crawlWithBrowser(kind, queue, seedUrls, opts, sink) {
|
|
1264
|
+
const crawler = kind === "playwright" ? new PlaywrightCrawler({
|
|
1265
|
+
requestQueue: queue,
|
|
1266
|
+
maxRequestsPerCrawl: Math.max(1, opts.maxPages),
|
|
1267
|
+
maxCrawlDepth: Math.max(0, opts.depth),
|
|
1268
|
+
preNavigationHooks: [
|
|
1269
|
+
(_ctx, gotoOptions) => {
|
|
1270
|
+
gotoOptions.waitUntil = "networkidle";
|
|
1271
|
+
gotoOptions.timeout = 3e4;
|
|
1272
|
+
}
|
|
1273
|
+
],
|
|
1274
|
+
requestHandler: (ctx) => handleBrowserPage(ctx, { ...sinkDeps(opts), sink }),
|
|
1275
|
+
failedRequestHandler: (ctx, error) => {
|
|
1276
|
+
const request = ctx.request;
|
|
1277
|
+
sink.skipped.push({ path: request.loadedUrl ?? request.url, reason: shortReason(error) });
|
|
1278
|
+
}
|
|
1279
|
+
}) : new PuppeteerCrawler({
|
|
1280
|
+
requestQueue: queue,
|
|
1281
|
+
maxRequestsPerCrawl: Math.max(1, opts.maxPages),
|
|
1282
|
+
maxCrawlDepth: Math.max(0, opts.depth),
|
|
1283
|
+
preNavigationHooks: [
|
|
1284
|
+
(_ctx, gotoOptions) => {
|
|
1285
|
+
gotoOptions.waitUntil = "networkidle2";
|
|
1286
|
+
gotoOptions.timeout = 3e4;
|
|
1287
|
+
}
|
|
1288
|
+
],
|
|
1289
|
+
requestHandler: (ctx) => handleBrowserPage(ctx, { ...sinkDeps(opts), sink }),
|
|
1290
|
+
failedRequestHandler: (ctx, error) => {
|
|
1291
|
+
const request = ctx.request;
|
|
1292
|
+
sink.skipped.push({ path: request.loadedUrl ?? request.url, reason: shortReason(error) });
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
1295
|
+
try {
|
|
1296
|
+
await crawler.run(seedUrls);
|
|
1297
|
+
} finally {
|
|
1298
|
+
await crawler.teardown().catch(() => {
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
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) {
|
|
1351
|
+
if (kind === "playwright" || kind === "puppeteer") {
|
|
1352
|
+
await requireBrowserEngine(kind);
|
|
1353
|
+
const sink2 = { docs: [], skipped: [] };
|
|
1354
|
+
const queue = await freshQueue();
|
|
1355
|
+
await crawlWithBrowser(kind, queue, seedUrls, opts, sink2);
|
|
1356
|
+
return { docs: dedupeDocs(sink2.docs), skipped: sink2.skipped };
|
|
1357
|
+
}
|
|
1358
|
+
const shellUrls = [];
|
|
1359
|
+
const sink = { docs: [], skipped: [], shellUrls };
|
|
1360
|
+
const staticQueue = await freshQueue();
|
|
1361
|
+
await crawlWithCheerio(staticQueue, seedUrls, opts, sink);
|
|
1362
|
+
if (kind !== "auto" || shellUrls.length === 0) {
|
|
1363
|
+
return { docs: dedupeDocs(sink.docs), skipped: sink.skipped };
|
|
1364
|
+
}
|
|
1365
|
+
opts.log(
|
|
1366
|
+
`${shellUrls.length} page(s) look JS-rendered; retrying with playwright (install: npm i -g playwright && npx playwright install chromium) ...`
|
|
1367
|
+
);
|
|
1368
|
+
const mod = await importOptional("playwright");
|
|
1369
|
+
if (!mod) {
|
|
1370
|
+
return { docs: dedupeDocs(sink.docs), skipped: sink.skipped };
|
|
1371
|
+
}
|
|
1372
|
+
const browserSink = { docs: [], skipped: [] };
|
|
1373
|
+
const browserQueue = await freshQueue();
|
|
1374
|
+
await crawlWithBrowser("playwright", browserQueue, shellUrls, opts, browserSink);
|
|
1375
|
+
return {
|
|
1376
|
+
docs: dedupeDocs([...sink.docs, ...browserSink.docs]),
|
|
1377
|
+
skipped: [...sink.skipped, ...browserSink.skipped]
|
|
685
1378
|
};
|
|
686
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
|
+
}
|
|
1410
|
+
async function feedUrl(ks, url, opts = {}) {
|
|
1411
|
+
const log2 = opts.log ?? ((m) => console.log(m));
|
|
1412
|
+
const kind = opts.crawler ?? "auto";
|
|
1413
|
+
if (!CRAWLER_KINDS.includes(kind)) {
|
|
1414
|
+
throw new Error(`unknown crawler '${kind}' (use auto, crawlee, playwright or puppeteer)`);
|
|
1415
|
+
}
|
|
1416
|
+
const maxPages = opts.maxPages && opts.maxPages > 0 ? Math.floor(opts.maxPages) : 999;
|
|
1417
|
+
const depth = typeof opts.depth === "number" && Number.isFinite(opts.depth) && opts.depth >= 0 ? Math.floor(opts.depth) : 5;
|
|
1418
|
+
const maxLen = opts.maxLen ?? 1200;
|
|
1419
|
+
const overlap = opts.overlap ?? 180;
|
|
1420
|
+
const { docs, skipped } = await crawlWebsite(url, {
|
|
1421
|
+
crawler: kind,
|
|
1422
|
+
maxPages,
|
|
1423
|
+
depth,
|
|
1424
|
+
maxLen,
|
|
1425
|
+
overlap,
|
|
1426
|
+
tags: opts.tags,
|
|
1427
|
+
urlFilter: opts.urlFilter,
|
|
1428
|
+
htmlFilter: opts.htmlFilter,
|
|
1429
|
+
sitemap: opts.sitemap,
|
|
1430
|
+
log: log2
|
|
1431
|
+
});
|
|
1432
|
+
for (const s of skipped) {
|
|
1433
|
+
log2(`skipped ${s.path}: ${s.reason}`);
|
|
1434
|
+
}
|
|
1435
|
+
const progress = opts.progress ?? new FeedProgress({ total: docs.length, rejected: skipped.length });
|
|
1436
|
+
const summary = await feedDocs(ks, docs, {
|
|
1437
|
+
replace: opts.replace,
|
|
1438
|
+
dryRun: opts.dryRun,
|
|
1439
|
+
batchSize: opts.batchSize,
|
|
1440
|
+
tags: opts.tags,
|
|
1441
|
+
metadata: opts.metadata,
|
|
1442
|
+
log: log2,
|
|
1443
|
+
progress
|
|
1444
|
+
});
|
|
1445
|
+
return { ...summary, skippedFiles: [...summary.skippedFiles, ...skipped] };
|
|
1446
|
+
}
|
|
1447
|
+
try {
|
|
1448
|
+
if (typeof log?.setLevel === "function" && log.LEVELS) {
|
|
1449
|
+
log.setLevel(log.LEVELS.ERROR);
|
|
1450
|
+
}
|
|
1451
|
+
} catch {
|
|
1452
|
+
}
|
|
687
1453
|
|
|
688
1454
|
// src/check.ts
|
|
689
1455
|
init_config();
|
|
@@ -754,7 +1520,7 @@ async function checkOneModel(id, model, baseUrl, names, ks, probe) {
|
|
|
754
1520
|
}
|
|
755
1521
|
return { id, status: "ok", detail: `model '${model}' available at ${baseUrl}${scope}` };
|
|
756
1522
|
}
|
|
757
|
-
async function runChecks(opts
|
|
1523
|
+
async function runChecks(opts) {
|
|
758
1524
|
const results = [];
|
|
759
1525
|
const major = Number(process.versions.node.split(".")[0]);
|
|
760
1526
|
results.push({
|
|
@@ -876,44 +1642,35 @@ async function runChecks(opts = {}) {
|
|
|
876
1642
|
results.push(
|
|
877
1643
|
"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
1644
|
);
|
|
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
|
-
}
|
|
1645
|
+
const tags = await getTags(defaultUrl);
|
|
1646
|
+
if ("error" in tags) {
|
|
1647
|
+
results.push({
|
|
1648
|
+
id: "model",
|
|
1649
|
+
status: "skip",
|
|
1650
|
+
detail: `Ollama unreachable at ${defaultUrl} \u2014 cannot verify model '${opts.model}'`
|
|
1651
|
+
});
|
|
1652
|
+
} else {
|
|
1653
|
+
results.push(await checkOneModel("model", opts.model, defaultUrl, tags.names, void 0, opts.probe ?? false));
|
|
890
1654
|
}
|
|
891
1655
|
for (const ks of kss) {
|
|
892
|
-
const
|
|
893
|
-
if ("error" in
|
|
1656
|
+
const tags2 = await getTags(ks.ollama_url);
|
|
1657
|
+
if ("error" in tags2) {
|
|
894
1658
|
results.push({
|
|
895
1659
|
id: `model:${ks.name}`,
|
|
896
1660
|
status: "fail",
|
|
897
|
-
detail: `Ollama not reachable at ${ks.ollama_url} (required by ks '${ks.name}') \u2014 ${
|
|
1661
|
+
detail: `Ollama not reachable at ${ks.ollama_url} (required by ks '${ks.name}') \u2014 ${tags2.error}`
|
|
898
1662
|
});
|
|
899
1663
|
continue;
|
|
900
1664
|
}
|
|
901
1665
|
results.push(
|
|
902
|
-
await checkOneModel(`model:${ks.name}`, ks.embedding_model, ks.ollama_url,
|
|
1666
|
+
await checkOneModel(`model:${ks.name}`, ks.embedding_model, ks.ollama_url, tags2.names, ks, opts.probe ?? false)
|
|
903
1667
|
);
|
|
904
1668
|
}
|
|
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
1669
|
return results;
|
|
913
1670
|
}
|
|
914
1671
|
|
|
915
1672
|
// src/version.ts
|
|
916
|
-
var VERSION = "1.
|
|
1673
|
+
var VERSION = "1.2.0";
|
|
917
1674
|
|
|
918
1675
|
// src/cli.ts
|
|
919
1676
|
async function run(fn) {
|
|
@@ -954,6 +1711,11 @@ function parseTags(tags) {
|
|
|
954
1711
|
const list = tags.split(",").map((t) => t.trim()).filter(Boolean);
|
|
955
1712
|
return list.length > 0 ? list : void 0;
|
|
956
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
|
+
}
|
|
957
1719
|
var program = new Command();
|
|
958
1720
|
program.name("gresmcp").description("Manage Postgres-backed knowledge sources for the gresmcp MCP server").version(VERSION);
|
|
959
1721
|
program.command("init").description("Initialize the database schema (extension, ks table; repairs entry tables)").action(
|
|
@@ -966,7 +1728,7 @@ program.command("init").description("Initialize the database schema (extension,
|
|
|
966
1728
|
console.log("database ready");
|
|
967
1729
|
})
|
|
968
1730
|
);
|
|
969
|
-
program.command("check").description("Verify environment requirements (Node, Postgres, pgvector, schema, Ollama models)").
|
|
1731
|
+
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
1732
|
(opts) => run(async () => {
|
|
971
1733
|
const results = await runChecks({ model: opts.model, url: opts.url, probe: opts.probe });
|
|
972
1734
|
if (opts.json) {
|
|
@@ -1044,6 +1806,34 @@ ksCmd.command("list").description("List knowledge sources").option("--json", "Ou
|
|
|
1044
1806
|
}
|
|
1045
1807
|
})
|
|
1046
1808
|
);
|
|
1809
|
+
ksCmd.command("stats <name>").description("Show statistics for a knowledge source (entries, sources, content volume)").option("--json", "Output as JSON").action(
|
|
1810
|
+
(name, opts) => run(async () => {
|
|
1811
|
+
await initDb();
|
|
1812
|
+
const ks = requireKs(await getKs(name), name);
|
|
1813
|
+
const stats = await ksStats(ks);
|
|
1814
|
+
if (opts.json) {
|
|
1815
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
1816
|
+
return;
|
|
1817
|
+
}
|
|
1818
|
+
console.log(
|
|
1819
|
+
`Knowledge source '${stats.ks.name}' (model=${stats.ks.embedding_model}@${stats.ks.ollama_url}, dim=${stats.ks.embedding_dim})`
|
|
1820
|
+
);
|
|
1821
|
+
if (stats.ks.description) console.log(` description: ${stats.ks.description}`);
|
|
1822
|
+
console.log(` created: ${stats.ks.created_at.toISOString()}`);
|
|
1823
|
+
console.log(` last fed: ${stats.last_fed_at ? stats.last_fed_at.toISOString() : "never"}`);
|
|
1824
|
+
console.log("");
|
|
1825
|
+
if (stats.entries === 0) {
|
|
1826
|
+
console.log(` no entries yet; feed it with: gresmcp feed ${stats.ks.name}`);
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
const size = (n) => n === null ? "-" : n.toLocaleString("en-US");
|
|
1830
|
+
console.log(` entries (chunks): ${stats.entries.toLocaleString("en-US")}`);
|
|
1831
|
+
console.log(` distinct sources: ${stats.sources.toLocaleString("en-US")}`);
|
|
1832
|
+
console.log(
|
|
1833
|
+
` 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)`
|
|
1834
|
+
);
|
|
1835
|
+
})
|
|
1836
|
+
);
|
|
1047
1837
|
ksCmd.command("delete <name>").description("Delete a knowledge source and all of its entries").option("--yes", "Do not ask for confirmation").action(
|
|
1048
1838
|
(name, opts) => run(async () => {
|
|
1049
1839
|
await initDb();
|
|
@@ -1060,30 +1850,61 @@ ksCmd.command("delete <name>").description("Delete a knowledge source and all of
|
|
|
1060
1850
|
console.log(`Deleted knowledge source '${name}'`);
|
|
1061
1851
|
})
|
|
1062
1852
|
);
|
|
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("--
|
|
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(
|
|
1064
1865
|
(name, opts) => run(async () => {
|
|
1065
1866
|
await initDb();
|
|
1066
1867
|
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");
|
|
1868
|
+
const modes = [opts.path, opts.url, opts.text, opts.stdin].filter(Boolean).length;
|
|
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
|
+
}
|
|
1069
1884
|
const common = {
|
|
1070
|
-
title: opts.title,
|
|
1071
|
-
url: opts.url,
|
|
1072
1885
|
replace: opts.replace,
|
|
1073
1886
|
dryRun: opts.dryRun,
|
|
1074
1887
|
batchSize: 32,
|
|
1075
1888
|
maxLen: opts.chunkSize && opts.chunkSize > 0 ? opts.chunkSize : void 0,
|
|
1076
1889
|
overlap: opts.overlap,
|
|
1077
1890
|
tags: parseTags(opts.tags),
|
|
1078
|
-
metadata: parseKeyValue(opts.metadata ?? [])
|
|
1891
|
+
metadata: parseKeyValue(opts.metadata ?? []),
|
|
1892
|
+
htmlFilter: opts.htmlFilter
|
|
1079
1893
|
};
|
|
1080
1894
|
let summary;
|
|
1895
|
+
const since = await dbClockTimestamp();
|
|
1081
1896
|
if (opts.path) {
|
|
1082
1897
|
summary = await feedTarget(ks, opts.path, common);
|
|
1898
|
+
} else if (opts.url) {
|
|
1899
|
+
const crawler = opts.crawler;
|
|
1900
|
+
if (crawler && !CRAWLER_KINDS.includes(crawler)) {
|
|
1901
|
+
throw new Error(`unknown crawler '${crawler}' (use auto, crawlee, playwright or puppeteer)`);
|
|
1902
|
+
}
|
|
1903
|
+
summary = await feedUrl(ks, opts.url, { ...common, crawler, sitemap, maxPages: opts.maxPages, depth: opts.depth, urlFilter });
|
|
1083
1904
|
} else {
|
|
1084
1905
|
const text = opts.stdin ? await readStdin() : opts.text;
|
|
1085
1906
|
if (!text.trim()) throw new Error("empty input text");
|
|
1086
|
-
summary = await feedText(ks, text, { ...common, sourceName: opts.sourceName });
|
|
1907
|
+
summary = await feedText(ks, text, { ...common, title: opts.title, sourceName: opts.sourceName });
|
|
1087
1908
|
}
|
|
1088
1909
|
if (summary.dryRun) {
|
|
1089
1910
|
console.log(`dry run: ${summary.documents} document(s), ${summary.chunks} chunk(s), ${summary.skippedFiles.length} file(s) skipped`);
|
|
@@ -1091,9 +1912,22 @@ program.command("feed <ks>").description("Feed data into a knowledge source (fro
|
|
|
1091
1912
|
console.log(
|
|
1092
1913
|
`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
1914
|
);
|
|
1915
|
+
await printRunStats(ks, since);
|
|
1094
1916
|
}
|
|
1095
1917
|
})
|
|
1096
1918
|
);
|
|
1919
|
+
async function printRunStats(ks, since) {
|
|
1920
|
+
const stats = await ksStats(ks, since);
|
|
1921
|
+
if (stats.entries === 0) return;
|
|
1922
|
+
const size = (n) => n === null ? "-" : n.toLocaleString("en-US");
|
|
1923
|
+
console.log("");
|
|
1924
|
+
console.log(
|
|
1925
|
+
`this run: ${stats.entries.toLocaleString("en-US")} chunk(s) across ${stats.sources.toLocaleString("en-US")} source(s)`
|
|
1926
|
+
);
|
|
1927
|
+
console.log(
|
|
1928
|
+
` 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)`
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1097
1931
|
async function readStdin() {
|
|
1098
1932
|
const chunks = [];
|
|
1099
1933
|
for await (const chunk of stdin) chunks.push(chunk);
|