@anubhav-choubey/web-for-agents 0.1.0 → 0.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 +74 -515
- package/dist/bin.js +4 -4
- package/dist/chunk-CGIIB6AF.js +127 -0
- package/dist/{chunk-AZG5MLNG.js → chunk-F7PN5R25.js} +2 -0
- package/dist/chunk-LCVPXUFN.js +245 -0
- package/dist/{chunk-IF32TPGR.js → chunk-P6JSPAA2.js} +169 -124
- package/dist/{chunk-5TNG4QMP.js → chunk-PGZH33B5.js} +20 -2
- package/dist/chunk-PHTV5NAY.js +44 -0
- package/dist/{chunk-ZYMNO5AO.js → chunk-PSNLS4GL.js} +93 -57
- package/dist/{chunk-HONWT6GK.js → chunk-SMFRAZAA.js} +66 -34
- package/dist/cli.js +4 -4
- package/dist/{happyDom-XVDHYJDM.js → happyDom-WTYQ5UAC.js} +3 -3
- package/dist/index.js +41 -235
- package/dist/overlay.js +7 -32
- package/dist/{renderChild-Y2RFSYY4.js → renderChild-H6TR4SGV.js} +27 -4
- package/dist/vite.d.ts +88 -2
- package/dist/vite.js +628 -146
- package/package.json +1 -1
- package/dist/chunk-CKXHTGGT.js +0 -25
- package/dist/chunk-LCVI2PRP.js +0 -13
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
-
AI_BOTS
|
|
3
|
-
|
|
2
|
+
AI_BOTS,
|
|
3
|
+
scanHtml
|
|
4
|
+
} from "./chunk-CGIIB6AF.js";
|
|
4
5
|
import {
|
|
5
|
-
AGENT_MAP_VERSION
|
|
6
|
-
|
|
6
|
+
AGENT_MAP_VERSION,
|
|
7
|
+
DESCRIPTION_MAX_CHARS,
|
|
8
|
+
DESCRIPTION_MIN_CHARS,
|
|
9
|
+
TITLE_MAX_CHARS
|
|
10
|
+
} from "./chunk-PHTV5NAY.js";
|
|
7
11
|
import {
|
|
8
12
|
jsonLdTypes
|
|
9
13
|
} from "./chunk-LCLDQ3A3.js";
|
|
@@ -22,7 +26,7 @@ import {
|
|
|
22
26
|
markdownPathFor,
|
|
23
27
|
trimTrailingSlash,
|
|
24
28
|
tryResolveUrl
|
|
25
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-F7PN5R25.js";
|
|
26
30
|
import {
|
|
27
31
|
errorMessage,
|
|
28
32
|
withPrefix
|
|
@@ -266,16 +270,6 @@ function decode2(s) {
|
|
|
266
270
|
return String.fromCodePoint(cp);
|
|
267
271
|
});
|
|
268
272
|
}
|
|
269
|
-
var isSpace = (c) => c === 32 || c === 9 || c === 10 || c === 12 || c === 13;
|
|
270
|
-
var isAlpha = (c) => c >= 65 && c <= 90 || c >= 97 && c <= 122;
|
|
271
|
-
function findRawEnd(html, name, from) {
|
|
272
|
-
let i = html.indexOf("</", from);
|
|
273
|
-
while (i !== -1) {
|
|
274
|
-
if (html.slice(i + 2, i + 2 + name.length).toLowerCase() === name) return i;
|
|
275
|
-
i = html.indexOf("</", i + 2);
|
|
276
|
-
}
|
|
277
|
-
return -1;
|
|
278
|
-
}
|
|
279
273
|
function extractHtmlFacts(html) {
|
|
280
274
|
const metas = [];
|
|
281
275
|
const links = [];
|
|
@@ -286,88 +280,29 @@ function extractHtmlFacts(html) {
|
|
|
286
280
|
let prerendered = false;
|
|
287
281
|
const depth = {};
|
|
288
282
|
let excluded = 0;
|
|
289
|
-
|
|
290
|
-
let
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const end = html.indexOf(">", lt + 2);
|
|
305
|
-
i = end === -1 ? n : end + 1;
|
|
283
|
+
let pos = 0;
|
|
284
|
+
let lastAttrs = {};
|
|
285
|
+
for (const t of scanHtml(html, RAW_TEXT)) {
|
|
286
|
+
if (t.start > pos && excluded === 0) chunks.push(decode2(html.slice(pos, t.start)));
|
|
287
|
+
pos = t.end;
|
|
288
|
+
if (t.type === "comment") continue;
|
|
289
|
+
if (t.type === "raw") {
|
|
290
|
+
const raw = html.slice(t.start, t.end);
|
|
291
|
+
if (t.name === "title") {
|
|
292
|
+
if (title === null && (depth.svg ?? 0) === 0) {
|
|
293
|
+
title = decode2(raw).replace(/\s+/g, " ").trim();
|
|
294
|
+
}
|
|
295
|
+
} else if (t.name === "script") {
|
|
296
|
+
const type = (lastAttrs.type ?? "").split(";")[0].trim().toLowerCase();
|
|
297
|
+
if (type === "application/ld+json") jsonLd.push(raw);
|
|
306
298
|
}
|
|
307
299
|
continue;
|
|
308
300
|
}
|
|
309
|
-
const closing
|
|
310
|
-
const nameStart = lt + (closing ? 2 : 1);
|
|
311
|
-
if (!isAlpha(nameStart < n ? html.charCodeAt(nameStart) : 0)) {
|
|
312
|
-
if (excluded === 0) chunks.push("<");
|
|
313
|
-
i = lt + 1;
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
let p = nameStart;
|
|
317
|
-
while (p < n) {
|
|
318
|
-
const c = html.charCodeAt(p);
|
|
319
|
-
if (isSpace(c) || c === 62 || c === 47) break;
|
|
320
|
-
p++;
|
|
321
|
-
}
|
|
322
|
-
const name = html.slice(nameStart, p).toLowerCase();
|
|
301
|
+
const { name, closing, selfClosing } = t;
|
|
323
302
|
const attrs = {};
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const c = html.charCodeAt(p);
|
|
327
|
-
if (isSpace(c)) {
|
|
328
|
-
p++;
|
|
329
|
-
continue;
|
|
330
|
-
}
|
|
331
|
-
if (c === 62) {
|
|
332
|
-
p++;
|
|
333
|
-
break;
|
|
334
|
-
}
|
|
335
|
-
if (c === 47) {
|
|
336
|
-
selfClosing = p + 1 < n && html.charCodeAt(p + 1) === 62;
|
|
337
|
-
p++;
|
|
338
|
-
continue;
|
|
339
|
-
}
|
|
340
|
-
const as = p;
|
|
341
|
-
while (p < n) {
|
|
342
|
-
const d = html.charCodeAt(p);
|
|
343
|
-
if (isSpace(d) || d === 62 || d === 47 || d === 61) break;
|
|
344
|
-
p++;
|
|
345
|
-
}
|
|
346
|
-
if (p === as) {
|
|
347
|
-
p++;
|
|
348
|
-
continue;
|
|
349
|
-
}
|
|
350
|
-
const aname = html.slice(as, p).toLowerCase();
|
|
351
|
-
while (p < n && isSpace(html.charCodeAt(p))) p++;
|
|
352
|
-
let value = "";
|
|
353
|
-
if (p < n && html.charCodeAt(p) === 61) {
|
|
354
|
-
p++;
|
|
355
|
-
while (p < n && isSpace(html.charCodeAt(p))) p++;
|
|
356
|
-
const q = p < n ? html[p] : "";
|
|
357
|
-
if (q === '"' || q === "'") {
|
|
358
|
-
const end = html.indexOf(q, p + 1);
|
|
359
|
-
const stop = end === -1 ? n : end;
|
|
360
|
-
value = html.slice(p + 1, stop);
|
|
361
|
-
p = end === -1 ? n : end + 1;
|
|
362
|
-
} else {
|
|
363
|
-
const vs = p;
|
|
364
|
-
while (p < n && !isSpace(html.charCodeAt(p)) && html.charCodeAt(p) !== 62) p++;
|
|
365
|
-
value = html.slice(vs, p);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
if (!(aname in attrs) && !closing) attrs[aname] = decode2(value);
|
|
303
|
+
if (!closing) {
|
|
304
|
+
for (const a of t.attrs) if (!(a.name in attrs)) attrs[a.name] = decode2(a.value);
|
|
369
305
|
}
|
|
370
|
-
i = p;
|
|
371
306
|
if (closing) {
|
|
372
307
|
if (EXCLUDED.has(name) && (depth[name] ?? 0) > 0) {
|
|
373
308
|
depth[name]--;
|
|
@@ -376,6 +311,7 @@ function extractHtmlFacts(html) {
|
|
|
376
311
|
if (excluded === 0 && !INLINE.has(name)) chunks.push(" ");
|
|
377
312
|
continue;
|
|
378
313
|
}
|
|
314
|
+
lastAttrs = attrs;
|
|
379
315
|
if ((depth.head ?? 0) > 0 && excluded === depth.head && !HEAD_OK.has(name)) {
|
|
380
316
|
excluded -= depth.head;
|
|
381
317
|
depth.head = 0;
|
|
@@ -387,27 +323,14 @@ function extractHtmlFacts(html) {
|
|
|
387
323
|
} else if (name === "link") {
|
|
388
324
|
links.push(attrs);
|
|
389
325
|
}
|
|
390
|
-
if (RAW_TEXT.has(name) && !selfClosing)
|
|
391
|
-
const end = findRawEnd(html, name, i);
|
|
392
|
-
const raw = html.slice(i, end === -1 ? n : end);
|
|
393
|
-
i = end === -1 ? n : end;
|
|
394
|
-
if (name === "title") {
|
|
395
|
-
if (title === null && (depth.svg ?? 0) === 0) {
|
|
396
|
-
title = decode2(raw).replace(/\s+/g, " ").trim();
|
|
397
|
-
}
|
|
398
|
-
} else if (name === "script") {
|
|
399
|
-
const type = (attrs.type ?? "").split(";")[0].trim().toLowerCase();
|
|
400
|
-
if (type === "application/ld+json") jsonLd.push(raw);
|
|
401
|
-
}
|
|
402
|
-
if (EXCLUDED.has(name) && end === -1) break;
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
326
|
+
if (RAW_TEXT.has(name) && !selfClosing) continue;
|
|
405
327
|
if (EXCLUDED.has(name) && !selfClosing) {
|
|
406
328
|
depth[name] = (depth[name] ?? 0) + 1;
|
|
407
329
|
excluded++;
|
|
408
330
|
}
|
|
409
331
|
if (excluded === 0 && !INLINE.has(name)) chunks.push(" ");
|
|
410
332
|
}
|
|
333
|
+
if (pos < html.length && excluded === 0) chunks.push(decode2(html.slice(pos)));
|
|
411
334
|
return {
|
|
412
335
|
title,
|
|
413
336
|
metas,
|
|
@@ -515,13 +438,13 @@ function matches(pattern, text) {
|
|
|
515
438
|
return last === "" || text.indexOf(last, at) >= 0;
|
|
516
439
|
}
|
|
517
440
|
var matchCache = /* @__PURE__ */ new WeakMap();
|
|
518
|
-
function cachedMatch(robots, pattern,
|
|
441
|
+
function cachedMatch(robots, pattern, path2) {
|
|
519
442
|
let byPath = matchCache.get(robots);
|
|
520
443
|
if (!byPath) matchCache.set(robots, byPath = /* @__PURE__ */ new Map());
|
|
521
|
-
let byPattern = byPath.get(
|
|
522
|
-
if (!byPattern) byPath.set(
|
|
444
|
+
let byPattern = byPath.get(path2);
|
|
445
|
+
if (!byPattern) byPath.set(path2, byPattern = /* @__PURE__ */ new Map());
|
|
523
446
|
let hit = byPattern.get(pattern);
|
|
524
|
-
if (hit === void 0) byPattern.set(pattern, hit = matches(pattern,
|
|
447
|
+
if (hit === void 0) byPattern.set(pattern, hit = matches(pattern, path2));
|
|
525
448
|
return hit;
|
|
526
449
|
}
|
|
527
450
|
function isAllowed(robots, userAgent, pathAndQuery2) {
|
|
@@ -556,9 +479,6 @@ var CHECK_WEIGHTS = {
|
|
|
556
479
|
};
|
|
557
480
|
var CONTENT_MIN_CHARS = 200;
|
|
558
481
|
var CONTENT_WARN_CHARS = 50;
|
|
559
|
-
var TITLE_MAX_CHARS = 70;
|
|
560
|
-
var DESCRIPTION_MIN_CHARS = 50;
|
|
561
|
-
var DESCRIPTION_MAX_CHARS = 160;
|
|
562
482
|
var MAX_BODY_LABEL = `${MAX_BODY_BYTES / (1024 * 1024)} MiB`;
|
|
563
483
|
var TITLES = {
|
|
564
484
|
content: "Content without JavaScript",
|
|
@@ -695,21 +615,21 @@ function checkRobots({ page, robots }) {
|
|
|
695
615
|
const out = robotsVerdict(parsed, pathAndQuery(page.finalUrl));
|
|
696
616
|
return parsed.truncated ? { ...out, message: `${out.message} (${parsed.truncated})` } : out;
|
|
697
617
|
}
|
|
698
|
-
function robotsVerdict(parsed,
|
|
699
|
-
if (!isAllowed(parsed, "*",
|
|
618
|
+
function robotsVerdict(parsed, path2) {
|
|
619
|
+
if (!isAllowed(parsed, "*", path2)) {
|
|
700
620
|
return fail(
|
|
701
|
-
`${ROBOTS_PATH} blocks ${
|
|
621
|
+
`${ROBOTS_PATH} blocks ${path2} for all crawlers`,
|
|
702
622
|
"Remove the Disallow rule matching this path under User-agent: *."
|
|
703
623
|
);
|
|
704
624
|
}
|
|
705
|
-
const searchBlocked = AI_BOTS.search.filter((b) => !isAllowed(parsed, b,
|
|
625
|
+
const searchBlocked = AI_BOTS.search.filter((b) => !isAllowed(parsed, b, path2));
|
|
706
626
|
if (searchBlocked.length > 0) {
|
|
707
627
|
return warn(
|
|
708
628
|
`AI search bots blocked: ${searchBlocked.join(", ")}`,
|
|
709
629
|
'Allow AI search bots (robots.ai.search: "allow") so answer engines can cite this page.'
|
|
710
630
|
);
|
|
711
631
|
}
|
|
712
|
-
const trainingBlocked = AI_BOTS.training.filter((b) => !isAllowed(parsed, b,
|
|
632
|
+
const trainingBlocked = AI_BOTS.training.filter((b) => !isAllowed(parsed, b, path2));
|
|
713
633
|
if (trainingBlocked.length > 0) {
|
|
714
634
|
return pass(`Training crawlers blocked (${trainingBlocked.join(", ")}); search bots allowed`);
|
|
715
635
|
}
|
|
@@ -875,7 +795,7 @@ async function auditWithHooks(url, opts = {}, hooks = {}) {
|
|
|
875
795
|
return report;
|
|
876
796
|
}
|
|
877
797
|
const html = extractHtmlFacts(page.body);
|
|
878
|
-
const at = (
|
|
798
|
+
const at = (path2) => new URL(path2, page.finalUrl).href;
|
|
879
799
|
const getSameOrigin = (u, accept) => sameOrigin(u, page.finalUrl) ? get(u, accept) : Promise.resolve(notFetched(u));
|
|
880
800
|
const alt = markdownAlternate(html);
|
|
881
801
|
const altUrl = alt === null ? null : tryResolveUrl(alt, page.finalUrl);
|
|
@@ -945,8 +865,9 @@ function normalizeInputUrl(input) {
|
|
|
945
865
|
return u.href;
|
|
946
866
|
}
|
|
947
867
|
var USAGE = `Usage: web-for-agents audit <url> [options]
|
|
868
|
+
web-for-agents generate <outDir> --site-url <url> [options]
|
|
948
869
|
|
|
949
|
-
|
|
870
|
+
audit: check how readable a page is for AI agents and crawlers.
|
|
950
871
|
|
|
951
872
|
Options:
|
|
952
873
|
--json print the report as JSON (page data is not sanitized; control
|
|
@@ -959,6 +880,22 @@ Options:
|
|
|
959
880
|
link-local address (default: follow them and print a warning)
|
|
960
881
|
-h, --help show this help
|
|
961
882
|
|
|
883
|
+
generate: write llms.txt, llms-full.txt, .md pages, robots.txt, sitemap.xml and
|
|
884
|
+
agents.json for HTML you prerendered yourself.
|
|
885
|
+
|
|
886
|
+
Options:
|
|
887
|
+
--site-url <url> absolute URL of the deployed site (or siteUrl in --config)
|
|
888
|
+
--ai-training <allow|block>
|
|
889
|
+
--ai-search <allow|block>
|
|
890
|
+
robots.txt policy for AI crawlers; both are required here or
|
|
891
|
+
as robots.ai in --config
|
|
892
|
+
--config <file> .json, .js or .mjs file whose default export holds options
|
|
893
|
+
(exclude, markdown, llms, agentMap, sitemap, robots, ...)
|
|
894
|
+
--no-link-markdown do not add the markdown <link> to each page's <head>
|
|
895
|
+
--overwrite replace an existing robots.txt, sitemap.xml, llms.txt,
|
|
896
|
+
llms-full.txt or agents.json that differs
|
|
897
|
+
--json print the result as JSON
|
|
898
|
+
|
|
962
899
|
Exit codes: 0 ok, 1 score below --min-score, 2 usage error or fatal error
|
|
963
900
|
`;
|
|
964
901
|
function parseCliArgs(argv) {
|
|
@@ -973,6 +910,12 @@ function parseCliArgs(argv) {
|
|
|
973
910
|
timeout: { type: "string" },
|
|
974
911
|
"agent-map": { type: "string" },
|
|
975
912
|
"no-private-redirects": { type: "boolean" },
|
|
913
|
+
"site-url": { type: "string" },
|
|
914
|
+
"ai-training": { type: "string" },
|
|
915
|
+
"ai-search": { type: "string" },
|
|
916
|
+
config: { type: "string" },
|
|
917
|
+
"no-link-markdown": { type: "boolean" },
|
|
918
|
+
overwrite: { type: "boolean" },
|
|
976
919
|
help: { type: "boolean", short: "h" }
|
|
977
920
|
}
|
|
978
921
|
});
|
|
@@ -982,7 +925,34 @@ function parseCliArgs(argv) {
|
|
|
982
925
|
const { values, positionals } = parsed;
|
|
983
926
|
if (values.help || positionals.length === 0) return { command: "help", json: false, timeoutMs: DEFAULT_TIMEOUT_MS };
|
|
984
927
|
const [command, input, ...rest] = positionals;
|
|
928
|
+
const GENERATE_FLAGS = ["site-url", "ai-training", "ai-search", "config", "no-link-markdown", "overwrite"];
|
|
929
|
+
const AUDIT_FLAGS = ["min-score", "timeout", "agent-map", "no-private-redirects"];
|
|
930
|
+
if (command === "generate") {
|
|
931
|
+
if (input === void 0) return fail2("generate requires an output directory");
|
|
932
|
+
if (rest.length > 0) return fail2(`unexpected argument "${rest[0]}"`);
|
|
933
|
+
const wrong2 = AUDIT_FLAGS.find((f) => values[f] !== void 0);
|
|
934
|
+
if (wrong2) return fail2(`--${wrong2} is not an option of generate`);
|
|
935
|
+
const gen = {
|
|
936
|
+
outDir: input,
|
|
937
|
+
linkMarkdown: values["no-link-markdown"] !== true,
|
|
938
|
+
overwrite: values.overwrite === true
|
|
939
|
+
};
|
|
940
|
+
for (const [flag, key] of [["ai-training", "aiTraining"], ["ai-search", "aiSearch"]]) {
|
|
941
|
+
const v = values[flag];
|
|
942
|
+
if (v === void 0) continue;
|
|
943
|
+
if (v !== "allow" && v !== "block") return fail2(`--${flag} must be "allow" or "block", got "${v}"`);
|
|
944
|
+
gen[key] = v;
|
|
945
|
+
}
|
|
946
|
+
if (values["site-url"] !== void 0) gen.siteUrl = values["site-url"];
|
|
947
|
+
if (values.config !== void 0) {
|
|
948
|
+
if (values.config.trim() === "") return fail2("--config must be a file path");
|
|
949
|
+
gen.config = values.config;
|
|
950
|
+
}
|
|
951
|
+
return { command: "generate", generate: gen, json: values.json === true, timeoutMs: DEFAULT_TIMEOUT_MS };
|
|
952
|
+
}
|
|
985
953
|
if (command !== "audit") return fail2(`unknown command "${command}"`);
|
|
954
|
+
const wrong = GENERATE_FLAGS.find((f) => values[f] !== void 0);
|
|
955
|
+
if (wrong) return fail2(`--${wrong} is an option of generate, not audit`);
|
|
986
956
|
if (input === void 0) return fail2("audit requires a URL");
|
|
987
957
|
if (rest.length > 0) return fail2(`unexpected argument "${rest[0]}"`);
|
|
988
958
|
const args = {
|
|
@@ -1020,15 +990,20 @@ function parseCliArgs(argv) {
|
|
|
1020
990
|
return args;
|
|
1021
991
|
}
|
|
1022
992
|
|
|
993
|
+
// src/cli/generate.ts
|
|
994
|
+
import { existsSync, readFileSync } from "fs";
|
|
995
|
+
import path from "path";
|
|
996
|
+
import { pathToFileURL } from "url";
|
|
997
|
+
|
|
1023
998
|
// src/cli/terminal.ts
|
|
1024
999
|
var MAX_TEXT_CHARS = 500;
|
|
1025
1000
|
var CONTROL = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
1026
1001
|
var BIDI_CHARS = "\\u061c\\u200e\\u200f\\u202a-\\u202e\\u2066-\\u2069";
|
|
1027
1002
|
var BIDI = new RegExp(`[${BIDI_CHARS}]`, "g");
|
|
1028
1003
|
function safeText(s, max = MAX_TEXT_CHARS) {
|
|
1029
|
-
const
|
|
1030
|
-
const chars = Array.from(
|
|
1031
|
-
return chars.length > max ? chars.slice(0, max - 1).join("") + "\u2026" :
|
|
1004
|
+
const clean2 = collapseWhitespace(s.replace(CONTROL, " ").replace(BIDI, ""));
|
|
1005
|
+
const chars = Array.from(clean2);
|
|
1006
|
+
return chars.length > max ? chars.slice(0, max - 1).join("") + "\u2026" : clean2;
|
|
1032
1007
|
}
|
|
1033
1008
|
var JSON_UNSAFE = new RegExp(`[\\u007f-\\u009f\\u2028\\u2029${BIDI_CHARS}]`, "g");
|
|
1034
1009
|
function safeJson(value, space) {
|
|
@@ -1038,6 +1013,75 @@ function safeJson(value, space) {
|
|
|
1038
1013
|
);
|
|
1039
1014
|
}
|
|
1040
1015
|
|
|
1016
|
+
// src/cli/generate.ts
|
|
1017
|
+
var VITE_ENTRY = "@anubhav-choubey/web-for-agents/vite";
|
|
1018
|
+
var clean = (s) => s.split("\n").map((l) => safeText(l, Infinity)).join("\n");
|
|
1019
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1020
|
+
async function loadConfig(file) {
|
|
1021
|
+
const abs = path.resolve(file);
|
|
1022
|
+
if (!existsSync(abs)) throw new Error(withPrefix(`config file not found: ${abs}`));
|
|
1023
|
+
let config;
|
|
1024
|
+
try {
|
|
1025
|
+
if (abs.endsWith(".json")) config = JSON.parse(readFileSync(abs, "utf8"));
|
|
1026
|
+
else if (/\.m?js$/.test(abs)) config = (await import(pathToFileURL(abs).href)).default;
|
|
1027
|
+
else throw new Error("expected a .json, .js or .mjs file");
|
|
1028
|
+
} catch (e) {
|
|
1029
|
+
throw new Error(withPrefix(`could not load config ${abs}: ${errorMessage(e)}`), { cause: e });
|
|
1030
|
+
}
|
|
1031
|
+
if (!isObject(config)) throw new Error(withPrefix(`config ${abs} must export an options object`));
|
|
1032
|
+
return config;
|
|
1033
|
+
}
|
|
1034
|
+
async function generateOptions(args) {
|
|
1035
|
+
const config = args.config ? await loadConfig(args.config) : {};
|
|
1036
|
+
const robots = isObject(config.robots) ? config.robots : {};
|
|
1037
|
+
const ai = { ...isObject(robots.ai) ? robots.ai : {} };
|
|
1038
|
+
if (args.aiTraining) ai.training = args.aiTraining;
|
|
1039
|
+
if (args.aiSearch) ai.search = args.aiSearch;
|
|
1040
|
+
if (ai.training === void 0 || ai.search === void 0) {
|
|
1041
|
+
throw new Error(withPrefix("--ai-training and --ai-search are required (or robots.ai in --config)"));
|
|
1042
|
+
}
|
|
1043
|
+
const siteUrl = args.siteUrl ?? config.siteUrl;
|
|
1044
|
+
if (siteUrl === void 0) throw new Error(withPrefix("--site-url is required (or siteUrl in --config)"));
|
|
1045
|
+
return {
|
|
1046
|
+
...config,
|
|
1047
|
+
outDir: args.outDir,
|
|
1048
|
+
siteUrl,
|
|
1049
|
+
robots: { ...robots, ai },
|
|
1050
|
+
...args.linkMarkdown ? {} : { linkMarkdown: false },
|
|
1051
|
+
...args.overwrite ? { overwrite: true } : {}
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
async function runGenerate(args, json, io) {
|
|
1055
|
+
let result;
|
|
1056
|
+
try {
|
|
1057
|
+
const options = await generateOptions(args);
|
|
1058
|
+
const { generateAgentFiles } = await import(VITE_ENTRY);
|
|
1059
|
+
result = await generateAgentFiles({
|
|
1060
|
+
...options,
|
|
1061
|
+
logger: {
|
|
1062
|
+
warn: (m) => io.stderr(clean(m) + "\n"),
|
|
1063
|
+
info: (m) => (json ? io.stderr : io.stdout)(clean(m) + "\n")
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
} catch (e) {
|
|
1067
|
+
io.stderr(withPrefix(`generate failed: ${clean(errorMessage(e).replace(/^\[web-for-agents\] /, ""))}
|
|
1068
|
+
`));
|
|
1069
|
+
return 2;
|
|
1070
|
+
}
|
|
1071
|
+
if (json) {
|
|
1072
|
+
io.stdout(safeJson(result, 2) + "\n");
|
|
1073
|
+
} else {
|
|
1074
|
+
const lines = [
|
|
1075
|
+
`Layout: ${result.fileLayout}`,
|
|
1076
|
+
`Pages: ${result.pages.map((p) => p.route + (p.listed ? "" : " (noindex)")).join(", ")}`,
|
|
1077
|
+
`Wrote ${result.written.length} files: ${result.written.join(", ")}`
|
|
1078
|
+
];
|
|
1079
|
+
if (result.kept.length > 0) lines.push(`Kept existing: ${result.kept.join(", ")} (use --overwrite to replace)`);
|
|
1080
|
+
io.stdout(clean(lines.join("\n")) + "\n");
|
|
1081
|
+
}
|
|
1082
|
+
return 0;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1041
1085
|
// src/cli/report.ts
|
|
1042
1086
|
function formatText(report) {
|
|
1043
1087
|
const lines = [
|
|
@@ -1077,6 +1121,7 @@ ${USAGE}`);
|
|
|
1077
1121
|
io.stdout(USAGE);
|
|
1078
1122
|
return 0;
|
|
1079
1123
|
}
|
|
1124
|
+
if (args.command === "generate") return runGenerate(args.generate, args.json, io);
|
|
1080
1125
|
const report = await audit(args.url, {
|
|
1081
1126
|
timeoutMs: args.timeoutMs,
|
|
1082
1127
|
...args.agentMap === void 0 ? {} : { agentMapPath: args.agentMap },
|
|
@@ -52,15 +52,29 @@ function withoutBlockedLoads(errors, blocked) {
|
|
|
52
52
|
function readyTimeoutMessage(timeoutMs) {
|
|
53
53
|
return `did not become ready within ${timeoutMs}ms`;
|
|
54
54
|
}
|
|
55
|
+
var TimeoutError = class extends Error {
|
|
56
|
+
constructor(message) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.name = "TimeoutError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
var SNAPSHOT_GRACE_MS = 5e3;
|
|
62
|
+
function snapshotWarning(timeoutMs) {
|
|
63
|
+
return `${readyTimeoutMessage(timeoutMs)}; saved the page as it was at the deadline (prerender.onTimeout: "snapshot"). If it is still loading or animating, set prerender.readySelector to an element that marks it ready`;
|
|
64
|
+
}
|
|
55
65
|
var POLL_MS = 50;
|
|
56
66
|
var SETTLED_POLL_MS = 20;
|
|
67
|
+
async function assertNoPageErrors(readErrors) {
|
|
68
|
+
const errors = await readErrors();
|
|
69
|
+
if (errors.length) throw new Error(errors[0]);
|
|
70
|
+
}
|
|
57
71
|
async function waitForReady(check, timeoutMs, pollMs = POLL_MS) {
|
|
58
72
|
const deadline = Date.now() + timeoutMs;
|
|
59
73
|
for (; ; ) {
|
|
60
74
|
const { ready, errors } = await check();
|
|
61
75
|
if (errors.length) throw new Error(errors[0]);
|
|
62
76
|
if (ready) return;
|
|
63
|
-
if (Date.now() >= deadline) throw new
|
|
77
|
+
if (Date.now() >= deadline) throw new TimeoutError(readyTimeoutMessage(timeoutMs));
|
|
64
78
|
await new Promise((r) => setTimeout(r, pollMs));
|
|
65
79
|
}
|
|
66
80
|
}
|
|
@@ -72,7 +86,7 @@ function createDeadline(timeoutMs) {
|
|
|
72
86
|
race(work, message) {
|
|
73
87
|
let timer;
|
|
74
88
|
const expired = new Promise((_, reject) => {
|
|
75
|
-
timer = setTimeout(() => reject(new
|
|
89
|
+
timer = setTimeout(() => reject(new TimeoutError(message)), remaining());
|
|
76
90
|
});
|
|
77
91
|
work.catch(() => {
|
|
78
92
|
});
|
|
@@ -103,8 +117,12 @@ export {
|
|
|
103
117
|
blockedHint,
|
|
104
118
|
withoutBlockedLoads,
|
|
105
119
|
readyTimeoutMessage,
|
|
120
|
+
TimeoutError,
|
|
121
|
+
SNAPSHOT_GRACE_MS,
|
|
122
|
+
snapshotWarning,
|
|
106
123
|
POLL_MS,
|
|
107
124
|
SETTLED_POLL_MS,
|
|
125
|
+
assertNoPageErrors,
|
|
108
126
|
waitForReady,
|
|
109
127
|
createDeadline,
|
|
110
128
|
runInPage
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// src/shared/metaLength.ts
|
|
2
|
+
var TITLE_MAX_CHARS = 60;
|
|
3
|
+
var DESCRIPTION_MIN_CHARS = 50;
|
|
4
|
+
var DESCRIPTION_MAX_CHARS = 160;
|
|
5
|
+
function metaLengthWarnings(pages) {
|
|
6
|
+
const longTitle = [];
|
|
7
|
+
const shortDesc = [];
|
|
8
|
+
const longDesc = [];
|
|
9
|
+
for (const { route, title, description } of pages) {
|
|
10
|
+
if (title && title.length > TITLE_MAX_CHARS) longTitle.push(`${route} (${title.length})`);
|
|
11
|
+
if (!description) continue;
|
|
12
|
+
if (description.length < DESCRIPTION_MIN_CHARS) shortDesc.push(`${route} (${description.length})`);
|
|
13
|
+
else if (description.length > DESCRIPTION_MAX_CHARS) longDesc.push(`${route} (${description.length})`);
|
|
14
|
+
}
|
|
15
|
+
const out = [];
|
|
16
|
+
if (longTitle.length > 0) {
|
|
17
|
+
out.push(`[web-for-agents] Title longer than ${TITLE_MAX_CHARS} characters on ${longTitle.join(", ")}`);
|
|
18
|
+
}
|
|
19
|
+
if (shortDesc.length > 0) {
|
|
20
|
+
out.push(`[web-for-agents] Meta description shorter than ${DESCRIPTION_MIN_CHARS} characters on ${shortDesc.join(", ")}`);
|
|
21
|
+
}
|
|
22
|
+
if (longDesc.length > 0) {
|
|
23
|
+
out.push(`[web-for-agents] Meta description longer than ${DESCRIPTION_MAX_CHARS} characters on ${longDesc.join(", ")}`);
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/shared/agent.ts
|
|
29
|
+
var ACTION_NAME_RE = /^[A-Za-z0-9_.-]{1,128}$/;
|
|
30
|
+
function isValidActionName(name) {
|
|
31
|
+
return typeof name === "string" && ACTION_NAME_RE.test(name);
|
|
32
|
+
}
|
|
33
|
+
var AGENT_GLOBAL_VERSION = 1;
|
|
34
|
+
var AGENT_MAP_VERSION = 1;
|
|
35
|
+
|
|
36
|
+
export {
|
|
37
|
+
TITLE_MAX_CHARS,
|
|
38
|
+
DESCRIPTION_MIN_CHARS,
|
|
39
|
+
DESCRIPTION_MAX_CHARS,
|
|
40
|
+
metaLengthWarnings,
|
|
41
|
+
isValidActionName,
|
|
42
|
+
AGENT_GLOBAL_VERSION,
|
|
43
|
+
AGENT_MAP_VERSION
|
|
44
|
+
};
|