@intentic/webq 1.317.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/LICENSE +21 -0
- package/README.md +46 -0
- package/dist/app.d.ts +2 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +38 -0
- package/dist/app.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +4 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/cache.command.d.ts +3 -0
- package/dist/commands/cache.command.d.ts.map +1 -0
- package/dist/commands/cache.command.js +54 -0
- package/dist/commands/cache.command.js.map +1 -0
- package/dist/commands/crawl.command.d.ts +3 -0
- package/dist/commands/crawl.command.d.ts.map +1 -0
- package/dist/commands/crawl.command.js +77 -0
- package/dist/commands/crawl.command.js.map +1 -0
- package/dist/commands/fetch.command.d.ts +3 -0
- package/dist/commands/fetch.command.d.ts.map +1 -0
- package/dist/commands/fetch.command.js +55 -0
- package/dist/commands/fetch.command.js.map +1 -0
- package/dist/lib/bm25.d.ts +7 -0
- package/dist/lib/bm25.d.ts.map +1 -0
- package/dist/lib/bm25.js +51 -0
- package/dist/lib/bm25.js.map +1 -0
- package/dist/lib/browser.d.ts +8 -0
- package/dist/lib/browser.d.ts.map +1 -0
- package/dist/lib/browser.js +29 -0
- package/dist/lib/browser.js.map +1 -0
- package/dist/lib/cache.d.ts +15 -0
- package/dist/lib/cache.d.ts.map +1 -0
- package/dist/lib/cache.js +28 -0
- package/dist/lib/cache.js.map +1 -0
- package/dist/lib/crawl.d.ts +31 -0
- package/dist/lib/crawl.d.ts.map +1 -0
- package/dist/lib/crawl.js +188 -0
- package/dist/lib/crawl.js.map +1 -0
- package/dist/lib/dom.d.ts +23 -0
- package/dist/lib/dom.d.ts.map +1 -0
- package/dist/lib/dom.js +74 -0
- package/dist/lib/dom.js.map +1 -0
- package/dist/lib/env.d.ts +2 -0
- package/dist/lib/env.d.ts.map +1 -0
- package/dist/lib/env.js +4 -0
- package/dist/lib/env.js.map +1 -0
- package/dist/lib/flags.d.ts +69 -0
- package/dist/lib/flags.d.ts.map +1 -0
- package/dist/lib/flags.js +23 -0
- package/dist/lib/flags.js.map +1 -0
- package/dist/lib/http.d.ts +16 -0
- package/dist/lib/http.d.ts.map +1 -0
- package/dist/lib/http.js +75 -0
- package/dist/lib/http.js.map +1 -0
- package/dist/lib/links.d.ts +9 -0
- package/dist/lib/links.d.ts.map +1 -0
- package/dist/lib/links.js +41 -0
- package/dist/lib/links.js.map +1 -0
- package/dist/lib/markdown.d.ts +6 -0
- package/dist/lib/markdown.d.ts.map +1 -0
- package/dist/lib/markdown.js +292 -0
- package/dist/lib/markdown.js.map +1 -0
- package/dist/lib/output.d.ts +20 -0
- package/dist/lib/output.d.ts.map +1 -0
- package/dist/lib/output.js +64 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/page.d.ts +26 -0
- package/dist/lib/page.d.ts.map +1 -0
- package/dist/lib/page.js +153 -0
- package/dist/lib/page.js.map +1 -0
- package/dist/lib/prune.d.ts +7 -0
- package/dist/lib/prune.d.ts.map +1 -0
- package/dist/lib/prune.js +100 -0
- package/dist/lib/prune.js.map +1 -0
- package/dist/lib/robots.d.ts +10 -0
- package/dist/lib/robots.d.ts.map +1 -0
- package/dist/lib/robots.js +84 -0
- package/dist/lib/robots.js.map +1 -0
- package/dist/lib/sitemap.d.ts +9 -0
- package/dist/lib/sitemap.d.ts.map +1 -0
- package/dist/lib/sitemap.js +61 -0
- package/dist/lib/sitemap.js.map +1 -0
- package/dist/lib/version.d.ts +2 -0
- package/dist/lib/version.d.ts.map +1 -0
- package/dist/lib/version.js +3 -0
- package/dist/lib/version.js.map +1 -0
- package/package.json +69 -0
- package/src/app.ts +43 -0
- package/src/cli.integration.test.ts +191 -0
- package/src/cli.ts +6 -0
- package/src/commands/cache.command.ts +54 -0
- package/src/commands/crawl.command.ts +94 -0
- package/src/commands/fetch.command.ts +61 -0
- package/src/lib/bm25.test.ts +33 -0
- package/src/lib/bm25.ts +70 -0
- package/src/lib/browser.ts +42 -0
- package/src/lib/cache.ts +44 -0
- package/src/lib/crawl.ts +247 -0
- package/src/lib/dom.ts +107 -0
- package/src/lib/env.ts +6 -0
- package/src/lib/flags.ts +41 -0
- package/src/lib/http.ts +101 -0
- package/src/lib/links.test.ts +37 -0
- package/src/lib/links.ts +47 -0
- package/src/lib/markdown.test.ts +76 -0
- package/src/lib/markdown.ts +337 -0
- package/src/lib/output.test.ts +16 -0
- package/src/lib/output.ts +86 -0
- package/src/lib/page.ts +213 -0
- package/src/lib/prune.test.ts +78 -0
- package/src/lib/prune.ts +133 -0
- package/src/lib/robots.test.ts +52 -0
- package/src/lib/robots.ts +102 -0
- package/src/lib/sitemap.test.ts +37 -0
- package/src/lib/sitemap.ts +74 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { attr, childElements, isElement, isText, remove, textOf } from "./dom.js";
|
|
2
|
+
const EXCLUDED = new Set(["nav", "footer", "header", "aside", "script", "style", "form", "iframe", "noscript", "template", "svg", "canvas"]);
|
|
3
|
+
const NEGATIVE = /nav|footer|header|sidebar|ads|comment|promo|advert|social|share/i;
|
|
4
|
+
const TAG_WEIGHTS = {
|
|
5
|
+
div: 0.5,
|
|
6
|
+
p: 1.0,
|
|
7
|
+
article: 1.5,
|
|
8
|
+
section: 1.0,
|
|
9
|
+
span: 0.3,
|
|
10
|
+
li: 0.5,
|
|
11
|
+
ul: 0.5,
|
|
12
|
+
ol: 0.5,
|
|
13
|
+
h1: 1.2,
|
|
14
|
+
h2: 1.1,
|
|
15
|
+
h3: 1.0,
|
|
16
|
+
h4: 0.9,
|
|
17
|
+
h5: 0.8,
|
|
18
|
+
h6: 0.7,
|
|
19
|
+
};
|
|
20
|
+
const WEIGHTS = { textDensity: 0.4, linkDensity: 0.2, tagWeight: 0.2, classIdWeight: 0.1, textLength: 0.1 };
|
|
21
|
+
export const pruneTree = (body, options = {}) => {
|
|
22
|
+
const threshold = options.threshold ?? 0.48;
|
|
23
|
+
const before = textOf(body).length;
|
|
24
|
+
stripExcluded(body);
|
|
25
|
+
const masses = new Map();
|
|
26
|
+
weigh(body, masses);
|
|
27
|
+
for (const child of childElements(body)) {
|
|
28
|
+
pruneNode(child, threshold, options.minWords, masses);
|
|
29
|
+
}
|
|
30
|
+
const after = textOf(body).length;
|
|
31
|
+
return before === 0 ? 0 : (before - after) / before;
|
|
32
|
+
};
|
|
33
|
+
const stripExcluded = (el) => {
|
|
34
|
+
for (const child of childElements(el)) {
|
|
35
|
+
if (EXCLUDED.has(child.tagName)) {
|
|
36
|
+
remove(child);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
stripExcluded(child);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const weigh = (el, memo) => {
|
|
44
|
+
let textLen = 0;
|
|
45
|
+
let markupLen = 0;
|
|
46
|
+
let linkTextLen = 0;
|
|
47
|
+
for (const child of el.childNodes) {
|
|
48
|
+
if (isText(child)) {
|
|
49
|
+
const trimmed = child.value.trim();
|
|
50
|
+
textLen += trimmed.length;
|
|
51
|
+
markupLen += trimmed.length;
|
|
52
|
+
}
|
|
53
|
+
else if (isElement(child)) {
|
|
54
|
+
const mass = weigh(child, memo);
|
|
55
|
+
textLen += mass.textLen;
|
|
56
|
+
markupLen += mass.markupLen;
|
|
57
|
+
if (child.tagName === "a") {
|
|
58
|
+
linkTextLen += textOf(child).length;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (el.tagName === "pre") {
|
|
63
|
+
markupLen = textLen;
|
|
64
|
+
}
|
|
65
|
+
markupLen += 2 * (el.tagName.length + 2) + el.attrs.reduce((sum, a) => sum + a.name.length + a.value.length + 4, 0);
|
|
66
|
+
const mass = { textLen, markupLen, linkTextLen };
|
|
67
|
+
memo.set(el, mass);
|
|
68
|
+
return mass;
|
|
69
|
+
};
|
|
70
|
+
const pruneNode = (el, threshold, minWords, masses) => {
|
|
71
|
+
if (compositeScore(el, minWords, masses) < threshold) {
|
|
72
|
+
remove(el);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (el.tagName === "pre") {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
for (const child of childElements(el)) {
|
|
79
|
+
pruneNode(child, threshold, minWords, masses);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const compositeScore = (el, minWords, masses) => {
|
|
83
|
+
const mass = masses.get(el) ?? { textLen: 0, markupLen: 0, linkTextLen: 0 };
|
|
84
|
+
if (minWords !== undefined && textOf(el).split(" ").filter(Boolean).length < minWords) {
|
|
85
|
+
return -1;
|
|
86
|
+
}
|
|
87
|
+
const textDensity = mass.markupLen > 0 ? mass.textLen / mass.markupLen : 0;
|
|
88
|
+
const linkDensity = 1 - (mass.textLen > 0 ? mass.linkTextLen / mass.textLen : 0);
|
|
89
|
+
const tagWeight = TAG_WEIGHTS[el.tagName] ?? 0.5;
|
|
90
|
+
const classId = attr(el, "class") ?? "";
|
|
91
|
+
const id = attr(el, "id") ?? "";
|
|
92
|
+
const classIdScore = (NEGATIVE.test(classId) ? -0.5 : 0) + (NEGATIVE.test(id) ? -0.5 : 0);
|
|
93
|
+
const score = WEIGHTS.textDensity * textDensity +
|
|
94
|
+
WEIGHTS.linkDensity * linkDensity +
|
|
95
|
+
WEIGHTS.tagWeight * tagWeight +
|
|
96
|
+
WEIGHTS.classIdWeight * Math.max(0, classIdScore) +
|
|
97
|
+
WEIGHTS.textLength * Math.log(mass.textLen + 1);
|
|
98
|
+
return score / (WEIGHTS.textDensity + WEIGHTS.linkDensity + WEIGHTS.tagWeight + WEIGHTS.classIdWeight + WEIGHTS.textLength);
|
|
99
|
+
};
|
|
100
|
+
//# sourceMappingURL=prune.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prune.js","sourceRoot":"","sources":["../../src/lib/prune.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface RobotsRules {
|
|
2
|
+
readonly allows: string[];
|
|
3
|
+
readonly disallows: string[];
|
|
4
|
+
readonly crawlDelayS: number | undefined;
|
|
5
|
+
readonly sitemaps: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare const EVERYTHING_ALLOWED: RobotsRules;
|
|
8
|
+
export declare const parseRobots: (text: string) => RobotsRules;
|
|
9
|
+
export declare const isAllowed: (rules: RobotsRules, path: string) => boolean;
|
|
10
|
+
//# sourceMappingURL=robots.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"robots.d.ts","sourceRoot":"","sources":["../../src/lib/robots.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,WAAW;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IACzC,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;CAC/B;AAED,eAAO,MAAM,kBAAkB,EAAE,WAAiF,CAAC;AAInH,eAAO,MAAM,WAAW,SAAU,MAAM,KAAG,WAsD1C,CAAC;AAEF,eAAO,MAAM,SAAS,UAAW,WAAW,QAAQ,MAAM,KAAG,OAQ5D,CAAC"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export const EVERYTHING_ALLOWED = { allows: [], disallows: [], crawlDelayS: undefined, sitemaps: [] };
|
|
2
|
+
const OUR_NAME = "webq";
|
|
3
|
+
export const parseRobots = (text) => {
|
|
4
|
+
const groups = [];
|
|
5
|
+
const sitemaps = [];
|
|
6
|
+
let current;
|
|
7
|
+
let openForAgents = false;
|
|
8
|
+
for (const rawLine of text.split("\n")) {
|
|
9
|
+
const line = (rawLine.split("#")[0] ?? "").trim();
|
|
10
|
+
const colon = line.indexOf(":");
|
|
11
|
+
if (colon < 0) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
const field = line.slice(0, colon).trim().toLowerCase();
|
|
15
|
+
const value = line.slice(colon + 1).trim();
|
|
16
|
+
if (field === "sitemap") {
|
|
17
|
+
sitemaps.push(value);
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (field === "user-agent") {
|
|
21
|
+
if (!openForAgents || current === undefined) {
|
|
22
|
+
current = { agents: [], allows: [], disallows: [], crawlDelayS: undefined };
|
|
23
|
+
groups.push(current);
|
|
24
|
+
openForAgents = true;
|
|
25
|
+
}
|
|
26
|
+
current.agents.push(value.toLowerCase());
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
openForAgents = false;
|
|
30
|
+
if (current === undefined) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (field === "allow") {
|
|
34
|
+
current.allows.push(value);
|
|
35
|
+
}
|
|
36
|
+
else if (field === "disallow") {
|
|
37
|
+
current.disallows.push(value);
|
|
38
|
+
}
|
|
39
|
+
else if (field === "crawl-delay") {
|
|
40
|
+
const delay = Number(value);
|
|
41
|
+
current.crawlDelayS = Number.isFinite(delay) ? delay : undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const named = groups.find((group) => group.agents.some((agent) => OUR_NAME.includes(agent) || agent.includes(OUR_NAME)));
|
|
45
|
+
const wildcard = groups.find((group) => group.agents.includes("*"));
|
|
46
|
+
const chosen = named ?? wildcard;
|
|
47
|
+
return {
|
|
48
|
+
allows: chosen?.allows ?? [],
|
|
49
|
+
disallows: chosen?.disallows ?? [],
|
|
50
|
+
crawlDelayS: chosen?.crawlDelayS,
|
|
51
|
+
sitemaps,
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
export const isAllowed = (rules, path) => {
|
|
55
|
+
const disallow = longestMatch(rules.disallows, path);
|
|
56
|
+
if (disallow === undefined) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
const allow = longestMatch(rules.allows, path);
|
|
60
|
+
return allow !== undefined && allow >= disallow;
|
|
61
|
+
};
|
|
62
|
+
const longestMatch = (patterns, path) => {
|
|
63
|
+
let longest;
|
|
64
|
+
for (const pattern of patterns) {
|
|
65
|
+
if (pattern === "") {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (matches(pattern, path) && (longest === undefined || pattern.length > longest)) {
|
|
69
|
+
longest = pattern.length;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return longest;
|
|
73
|
+
};
|
|
74
|
+
const matches = (pattern, path) => {
|
|
75
|
+
const escaped = pattern.replaceAll(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*");
|
|
76
|
+
const anchored = escaped.endsWith("$") ? `^${escaped.slice(0, -2)}$` : `^${escaped}`;
|
|
77
|
+
try {
|
|
78
|
+
return new RegExp(anchored).test(path);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
//# sourceMappingURL=robots.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"robots.js","sourceRoot":"","sources":["../../src/lib/robots.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface SitemapSeed {
|
|
2
|
+
readonly urls: string[];
|
|
3
|
+
readonly capped: boolean;
|
|
4
|
+
}
|
|
5
|
+
export declare const fetchSitemapUrls: (startUrl: string, robotsSitemaps: string[], fetchText: (url: string) => Promise<{
|
|
6
|
+
body: string;
|
|
7
|
+
bytes?: Uint8Array;
|
|
8
|
+
} | undefined>) => Promise<SitemapSeed>;
|
|
9
|
+
//# sourceMappingURL=sitemap.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sitemap.d.ts","sourceRoot":"","sources":["../../src/lib/sitemap.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,WAAW;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;IAExB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC5B;AAED,eAAO,MAAM,gBAAgB,aACf,MAAM,kBACA,MAAM,EAAE,aACb,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,UAAU,CAAA;CAAE,GAAG,SAAS,CAAC,KACtF,OAAO,CAAC,WAAW,CA0CrB,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { gunzipSync } from "node:zlib";
|
|
2
|
+
import { normalizeUrl } from "./links.js";
|
|
3
|
+
const URL_CAP = 2_000;
|
|
4
|
+
const CHILD_SITEMAP_CAP = 10;
|
|
5
|
+
export const fetchSitemapUrls = async (startUrl, robotsSitemaps, fetchText) => {
|
|
6
|
+
const origin = new URL(startUrl).origin;
|
|
7
|
+
const roots = robotsSitemaps.length > 0 ? robotsSitemaps : [`${origin}/sitemap.xml`];
|
|
8
|
+
const urls = new Set();
|
|
9
|
+
let capped = false;
|
|
10
|
+
let childBudget = CHILD_SITEMAP_CAP;
|
|
11
|
+
const queue = [...roots];
|
|
12
|
+
while (queue.length > 0 && urls.size < URL_CAP) {
|
|
13
|
+
const sitemapUrl = queue.shift();
|
|
14
|
+
if (sitemapUrl === undefined) {
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
const fetched = await fetchText(sitemapUrl);
|
|
18
|
+
if (fetched === undefined) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const xml = maybeGunzip(sitemapUrl, fetched);
|
|
22
|
+
const locs = [...xml.matchAll(/<loc>\s*([^<]+?)\s*<\/loc>/g)].map((m) => m[1] ?? "");
|
|
23
|
+
if (/<sitemapindex/i.test(xml)) {
|
|
24
|
+
for (const loc of locs) {
|
|
25
|
+
if (childBudget > 0) {
|
|
26
|
+
queue.push(loc);
|
|
27
|
+
childBudget -= 1;
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
capped = true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
for (const loc of locs) {
|
|
36
|
+
const url = normalizeUrl(decodeXmlEntities(loc));
|
|
37
|
+
if (url === undefined) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (urls.size >= URL_CAP) {
|
|
41
|
+
capped = true;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
urls.add(url);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { urls: [...urls], capped };
|
|
48
|
+
};
|
|
49
|
+
const maybeGunzip = (url, fetched) => {
|
|
50
|
+
if (!url.endsWith(".gz") || fetched.bytes === undefined) {
|
|
51
|
+
return fetched.body;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
return gunzipSync(fetched.bytes).toString("utf8");
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return fetched.body;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const decodeXmlEntities = (text) => text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll(""", '"').replaceAll("'", "'");
|
|
61
|
+
//# sourceMappingURL=sitemap.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sitemap.js","sourceRoot":"","sources":["../../src/lib/sitemap.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../../src/lib/version.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,OAAO,EAAE,MAA8F,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../../src/lib/version.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAI5C,MAAM,CAAC,MAAM,OAAO,GAAY,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,oBAAoB,CAAyB,CAAC,OAAO,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@intentic/webq",
|
|
3
|
+
"version": "1.317.0",
|
|
4
|
+
"description": "webq, agent-native web fetch CLI: any URL as clean budgeted markdown, and bounded same-site crawls, over one shared cache",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/intentic/intentic.git",
|
|
10
|
+
"directory": "_sandbox/webq"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"src"
|
|
15
|
+
],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public",
|
|
18
|
+
"registry": "https://registry.npmjs.org/"
|
|
19
|
+
},
|
|
20
|
+
"bin": {
|
|
21
|
+
"webq": "./dist/cli.js"
|
|
22
|
+
},
|
|
23
|
+
"exports": {
|
|
24
|
+
"./markdown": {
|
|
25
|
+
"types": "./dist/lib/markdown.d.ts",
|
|
26
|
+
"import": {
|
|
27
|
+
"@intentic/src": "./src/lib/markdown.ts",
|
|
28
|
+
"default": "./dist/lib/markdown.js"
|
|
29
|
+
},
|
|
30
|
+
"require": {
|
|
31
|
+
"@intentic/src": "./src/lib/markdown.ts",
|
|
32
|
+
"default": "./dist/lib/markdown.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"./dom": {
|
|
36
|
+
"types": "./dist/lib/dom.d.ts",
|
|
37
|
+
"import": {
|
|
38
|
+
"@intentic/src": "./src/lib/dom.ts",
|
|
39
|
+
"default": "./dist/lib/dom.js"
|
|
40
|
+
},
|
|
41
|
+
"require": {
|
|
42
|
+
"@intentic/src": "./src/lib/dom.ts",
|
|
43
|
+
"default": "./dist/lib/dom.js"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@intentic/agent-cli": "1.317.0",
|
|
49
|
+
"@intentic/base": "1.317.0",
|
|
50
|
+
"@stricli/core": "1.3.0",
|
|
51
|
+
"parse5": "8.0.1",
|
|
52
|
+
"playwright": "1.63.0",
|
|
53
|
+
"tslib": "2.8.1"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@intentic/testing": "0.0.0",
|
|
57
|
+
"@intentic/tsconfig": "0.0.0",
|
|
58
|
+
"@types/bun": "1.4.2",
|
|
59
|
+
"@types/node": "24.13.6",
|
|
60
|
+
"@typescript/native-preview": "7.0.0-dev.20260707.2"
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"build": "tsgo",
|
|
64
|
+
"typecheck": "tsgo --noEmit -p tsconfig.test.json",
|
|
65
|
+
"watch": "tsgo --build --watch",
|
|
66
|
+
"test": "suites",
|
|
67
|
+
"test:watch": "suites --watch"
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/app.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { packageVersion } from "@intentic/agent-cli/version";
|
|
2
|
+
import { buildApplication, buildRouteMap } from "@stricli/core";
|
|
3
|
+
import { cacheCommand } from "./commands/cache.command.js";
|
|
4
|
+
import { crawlCommand } from "./commands/crawl.command.js";
|
|
5
|
+
import { fetchCommand } from "./commands/fetch.command.js";
|
|
6
|
+
|
|
7
|
+
// The agent-facing contract, kept small — this is what `webq --help` prints.
|
|
8
|
+
const HELP = `Web pages as clean markdown, built for an agent's context window.
|
|
9
|
+
|
|
10
|
+
webq fetch <url> one page: capsule + budgeted markdown on stdout, whole page saved to a file
|
|
11
|
+
webq fetch <url> --query "rate limits" keep only the blocks relevant to a question
|
|
12
|
+
webq fetch <url> --raw whole page, no chrome-pruning
|
|
13
|
+
webq crawl <url> --max-pages 30 bounded same-site crawl: a file per page + index.md
|
|
14
|
+
webq crawl <url> --query "webhooks" best-first: links about the query are visited first
|
|
15
|
+
webq cache [--clear] the shared fetch cache
|
|
16
|
+
|
|
17
|
+
Every fetch is cached for 15 minutes (--fresh bypasses, --max-age tunes), so parallel agents and repeated
|
|
18
|
+
runs stop paying the network. JS-heavy pages render in the image's Chromium automatically when the static
|
|
19
|
+
HTML is an empty app shell (--browser never|force overrides); without the browser pack webq says so and
|
|
20
|
+
serves the static HTML. Crawls obey robots.txt (--ignore-robots is on you), stay on the start origin
|
|
21
|
+
(--external follows out), and report every skipped-URL count — a capped crawl never poses as a complete one.
|
|
22
|
+
|
|
23
|
+
Read the capsule first: title · final URL · token cost · fit/raw · cache|network|browser. The saved file
|
|
24
|
+
carries front matter (url, title, fetched_at) so it stays self-describing when found later.
|
|
25
|
+
Exit codes: 0 content, 1 none (HTTP error / empty crawl), 2 broken invocation.`;
|
|
26
|
+
|
|
27
|
+
export const app = buildApplication(
|
|
28
|
+
buildRouteMap({
|
|
29
|
+
routes: {
|
|
30
|
+
fetch: fetchCommand,
|
|
31
|
+
crawl: crawlCommand,
|
|
32
|
+
cache: cacheCommand,
|
|
33
|
+
},
|
|
34
|
+
defaultCommand: "fetch",
|
|
35
|
+
docs: { brief: "webq, agent-native web fetch: URLs as clean budgeted markdown", fullDescription: HELP },
|
|
36
|
+
}),
|
|
37
|
+
{
|
|
38
|
+
name: "webq",
|
|
39
|
+
versionInfo: { currentVersion: packageVersion(import.meta.url) },
|
|
40
|
+
scanner: { caseStyle: "allow-kebab-for-camel" },
|
|
41
|
+
determineExitCode: () => 2,
|
|
42
|
+
},
|
|
43
|
+
);
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { requires } from "@intentic/testing/requires";
|
|
2
|
+
/* The CLI end to end against a loopback fixture site: fetch, cache, query filtering, crawl with robots and caps, budget clipping. */
|
|
3
|
+
import { mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
4
|
+
import { createServer, type Server } from "node:http";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import type { AddressInfo } from "node:net";
|
|
8
|
+
import { captureCli, type CliOutcome } from "@intentic/agent-cli/testing";
|
|
9
|
+
import { app } from "./app.js";
|
|
10
|
+
import { chromiumAvailable } from "./lib/browser.js";
|
|
11
|
+
|
|
12
|
+
const page = (title: string, body: string): string =>
|
|
13
|
+
`<html><head><title>${title}</title></head><body>
|
|
14
|
+
<nav><a href="/">Home</a><a href="/a">A</a></nav>
|
|
15
|
+
<main><article>${body}</article></main>
|
|
16
|
+
<footer><a href="/tos">Terms</a> Copyright</footer></body></html>`;
|
|
17
|
+
|
|
18
|
+
const paragraphs = (label: string): string =>
|
|
19
|
+
Array.from(
|
|
20
|
+
{ length: 12 },
|
|
21
|
+
(_, i) => `<p>${label} paragraph ${i} with plenty of dense readable words in a complete sentence about topics agents research.</p>`,
|
|
22
|
+
).join("");
|
|
23
|
+
|
|
24
|
+
let server: Server;
|
|
25
|
+
let base = "";
|
|
26
|
+
let home = "";
|
|
27
|
+
|
|
28
|
+
const routes = (): Record<string, { body: string; type?: string }> => ({
|
|
29
|
+
"/": {
|
|
30
|
+
body: page(
|
|
31
|
+
"Home",
|
|
32
|
+
`<h1>Docs home</h1>${paragraphs("home")}<p><a href="/a">Alpha guide</a> <a href="/sub/c">Webhooks retry reference</a> <a href="/blocked">Secret</a> <a href="https://offsite.example/x">Offsite</a> <a href="/gone">Gone</a></p>`,
|
|
33
|
+
),
|
|
34
|
+
},
|
|
35
|
+
"/a": { body: page("Alpha", `<h1>Alpha</h1>${paragraphs("alpha")}`) },
|
|
36
|
+
"/sub/c": { body: page("Webhooks", `<h1>Webhook retries</h1><p>webhook retry policy with backoff.</p>${paragraphs("webhook retry")}`) },
|
|
37
|
+
"/blocked": { body: page("Blocked", "<h1>Never fetched</h1>") },
|
|
38
|
+
"/robots.txt": { body: "User-agent: *\nDisallow: /blocked\n", type: "text/plain" },
|
|
39
|
+
"/plain.json": { body: JSON.stringify({ hello: "world" }), type: "application/json" },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
beforeAll(async () => {
|
|
43
|
+
server = createServer((req, res) => {
|
|
44
|
+
// A connection cut before any answer: the one failure a crawl can only name, never count as a status.
|
|
45
|
+
if (req.url === "/cut") {
|
|
46
|
+
req.socket.destroy();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const route = routes()[(req.url ?? "/").split("?")[0] ?? "/"];
|
|
50
|
+
if (route === undefined) {
|
|
51
|
+
res.writeHead(404, { "content-type": "text/html" });
|
|
52
|
+
res.end("<html><body>nope</body></html>");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
res.writeHead(200, { "content-type": route.type ?? "text/html; charset=utf-8" });
|
|
56
|
+
res.end(route.body);
|
|
57
|
+
});
|
|
58
|
+
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
59
|
+
base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
|
|
60
|
+
home = mkdtempSync(join(tmpdir(), "webq-test-"));
|
|
61
|
+
process.env["WEBQ_HOME"] = home;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
afterAll(async () => {
|
|
65
|
+
await new Promise((resolve) => server.close(resolve));
|
|
66
|
+
delete process.env["WEBQ_HOME"];
|
|
67
|
+
rmSync(home, { recursive: true, force: true });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const webq = (...args: string[]): Promise<CliOutcome> => captureCli(app, args);
|
|
71
|
+
|
|
72
|
+
describe("webq fetch", () => {
|
|
73
|
+
it("prints a capsule, clips to the budget, and saves the whole page", async () => {
|
|
74
|
+
const { out, exitCode } = await webq("fetch", `${base}/a`, "--budget", "60");
|
|
75
|
+
expect(exitCode).toBe(0);
|
|
76
|
+
expect(out).toContain("webq: Alpha");
|
|
77
|
+
expect(out).toContain("· network");
|
|
78
|
+
expect(out).toContain("[cut at 60 of");
|
|
79
|
+
const path = /saved: (\S+)/.exec(out)?.[1] ?? "";
|
|
80
|
+
const file = readFileSync(path, "utf8");
|
|
81
|
+
expect(file).toContain(`url: ${base}/a`);
|
|
82
|
+
expect(file).toContain("alpha paragraph 11");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("serves the second fetch from cache", async () => {
|
|
86
|
+
const { out } = await webq("fetch", `${base}/a`, "--budget", "0");
|
|
87
|
+
expect(out).toContain("· cache");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("bypasses the cache under --fresh", async () => {
|
|
91
|
+
const { out } = await webq("fetch", `${base}/a`, "--budget", "0", "--fresh");
|
|
92
|
+
expect(out).toContain("· network");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("keeps only query-relevant blocks and says what it kept", async () => {
|
|
96
|
+
const { out } = await webq("fetch", `${base}/sub/c`, "--query", "backoff policy", "--budget", "200", "--fresh");
|
|
97
|
+
expect(out).toContain("webhook retry policy with backoff");
|
|
98
|
+
expect(out).toMatch(/query (kept|matched)/);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("fences non-HTML text content instead of parsing it", async () => {
|
|
102
|
+
const { out } = await webq("fetch", `${base}/plain.json`, "--budget", "100");
|
|
103
|
+
expect(out).toContain("```json");
|
|
104
|
+
expect(out).toContain('"hello"');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("exits 1 on an HTTP error page", async () => {
|
|
108
|
+
const { exitCode } = await webq("fetch", `${base}/gone`, "--budget", "0");
|
|
109
|
+
expect(exitCode).toBe(1);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("emits machine-readable output under --json", async () => {
|
|
113
|
+
const { out } = await webq("fetch", `${base}/a`, "--json");
|
|
114
|
+
const parsed = JSON.parse(out) as { title: string; path: string; source: string };
|
|
115
|
+
expect(parsed.title).toBe("Alpha");
|
|
116
|
+
expect(parsed.path).toContain(home);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("webq crawl", () => {
|
|
121
|
+
it("crawls the site under its caps, obeying robots and staying on-origin", async () => {
|
|
122
|
+
const outDir = join(home, "crawl-basic");
|
|
123
|
+
const { out, exitCode } = await webq("crawl", `${base}/`, "--max-pages", "10", "--out", outDir, "--fresh");
|
|
124
|
+
expect(exitCode).toBe(0);
|
|
125
|
+
expect(out).toContain("3 pages");
|
|
126
|
+
expect(out).toContain("robots 1");
|
|
127
|
+
expect(out).toContain("offsite 1");
|
|
128
|
+
// Two links lied: /gone from the home page and /tos from every footer.
|
|
129
|
+
expect(out).toContain("http-errors 2");
|
|
130
|
+
const files = readdirSync(outDir);
|
|
131
|
+
expect(files).toContain("index.md");
|
|
132
|
+
expect(files).toContain("index.json");
|
|
133
|
+
expect(files.join(" ")).not.toContain("blocked");
|
|
134
|
+
expect(readFileSync(join(outDir, "index.md"), "utf8")).toContain("| Alpha |");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("visits query-relevant links first under a tight cap", async () => {
|
|
138
|
+
const outDir = join(home, "crawl-query");
|
|
139
|
+
const { out } = await webq("crawl", `${base}/`, "--query", "webhook retries", "--max-pages", "2", "--out", outDir, "--fresh");
|
|
140
|
+
expect(out).toContain("2 pages");
|
|
141
|
+
expect(out).toContain("/sub/c");
|
|
142
|
+
expect(out).not.toContain(`${base}/a →`);
|
|
143
|
+
expect(out).toContain("beyond-cap");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("exits 1 when nothing could be crawled", async () => {
|
|
147
|
+
const { exitCode } = await webq("crawl", `${base}/gone`, "--max-pages", "2", "--out", join(home, "crawl-empty"), "--fresh");
|
|
148
|
+
expect(exitCode).toBe(1);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("names each page that failed, and why, beside the count", async () => {
|
|
152
|
+
const { out, exitCode } = await webq("crawl", `${base}/cut`, "--max-pages", "1", "--out", join(home, "crawl-cut"), "--fresh");
|
|
153
|
+
expect(out).toContain("skipped: errors 1");
|
|
154
|
+
expect(out).toMatch(new RegExp(`failed: ${base}/cut: \\S`));
|
|
155
|
+
expect(exitCode).toBe(1);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("webq cache", () => {
|
|
160
|
+
it("reports entries and clears them", async () => {
|
|
161
|
+
const stats = await webq("cache");
|
|
162
|
+
expect(stats.out).toMatch(/cache: \d+ entries/);
|
|
163
|
+
const cleared = await webq("cache", "--clear");
|
|
164
|
+
expect(cleared.out).toContain("cache cleared");
|
|
165
|
+
const after = await webq("cache");
|
|
166
|
+
expect(after.out).toContain("cache: 0 entries");
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// CI's verify-machine job installs it (ci.yml); ci-base carries no browser, so a stray one cannot wake other suites.
|
|
171
|
+
const chromium = requires(await chromiumAvailable(), "Playwright's Chromium (playwright install chromium)", { lane: "machine" });
|
|
172
|
+
|
|
173
|
+
describe("browser fallback", () => {
|
|
174
|
+
it.skipIf(!chromium.runs)(chromium.title("renders an app-shell page through Chromium"), async () => {
|
|
175
|
+
const shellServer = createServer((_req, res) => {
|
|
176
|
+
res.writeHead(200, { "content-type": "text/html" });
|
|
177
|
+
res.end(
|
|
178
|
+
`<html><head><title>Shell</title></head><body><div id="root"></div><script>document.getElementById("root").innerHTML = "<h1>Hydrated</h1><p>Content that only exists after JavaScript ran in a real browser engine, long enough to clear the app-shell text threshold when rendered, with plenty of words to be sure the heuristic sees a real page.</p>";</script></body></html>`,
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
await new Promise<void>((resolve) => shellServer.listen(0, "127.0.0.1", resolve));
|
|
182
|
+
const shellBase = `http://127.0.0.1:${(shellServer.address() as AddressInfo).port}`;
|
|
183
|
+
try {
|
|
184
|
+
const { out } = await webq("fetch", `${shellBase}/`, "--budget", "100", "--fresh");
|
|
185
|
+
expect(out).toContain("# Hydrated");
|
|
186
|
+
expect(out).toContain("· browser");
|
|
187
|
+
} finally {
|
|
188
|
+
await new Promise((resolve) => shellServer.close(resolve));
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
});
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The shared agent-CLI shell (@intentic/agent-cli/run) is the ONE thing this file may import at runtime: it
|
|
3
|
+
// pulls nothing in itself, so webq's own module graph still loads inside a catch that can report a bad install.
|
|
4
|
+
import { runAgentCli } from "@intentic/agent-cli/run";
|
|
5
|
+
|
|
6
|
+
await runAgentCli({ name: "webq", noun: "page", load: async () => ({ app: (await import("./app.js")).app }) });
|