@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.
@@ -1,9 +1,13 @@
1
1
  import {
2
- AI_BOTS
3
- } from "./chunk-CKXHTGGT.js";
2
+ AI_BOTS,
3
+ scanHtml
4
+ } from "./chunk-CGIIB6AF.js";
4
5
  import {
5
- AGENT_MAP_VERSION
6
- } from "./chunk-LCVI2PRP.js";
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-AZG5MLNG.js";
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
- const n = html.length;
290
- let i = 0;
291
- while (i < n) {
292
- const lt = html.indexOf("<", i);
293
- if (lt === -1) {
294
- if (excluded === 0) chunks.push(decode2(html.slice(i)));
295
- break;
296
- }
297
- if (lt > i && excluded === 0) chunks.push(decode2(html.slice(i, lt)));
298
- const c1 = lt + 1 < n ? html.charCodeAt(lt + 1) : 0;
299
- if (c1 === 33 || c1 === 63) {
300
- if (html.startsWith("<!--", lt)) {
301
- const end = html.indexOf("-->", lt + 4);
302
- i = end === -1 ? n : end + 3;
303
- } else {
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 = c1 === 47;
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
- let selfClosing = false;
325
- while (p < n) {
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, path) {
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(path);
522
- if (!byPattern) byPath.set(path, byPattern = /* @__PURE__ */ new Map());
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, path));
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, path) {
699
- if (!isAllowed(parsed, "*", path)) {
618
+ function robotsVerdict(parsed, path2) {
619
+ if (!isAllowed(parsed, "*", path2)) {
700
620
  return fail(
701
- `${ROBOTS_PATH} blocks ${path} for all crawlers`,
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, path));
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, path));
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 = (path) => new URL(path, page.finalUrl).href;
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
- Audit how readable a page is for AI agents and crawlers.
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 clean = collapseWhitespace(s.replace(CONTROL, " ").replace(BIDI, ""));
1030
- const chars = Array.from(clean);
1031
- return chars.length > max ? chars.slice(0, max - 1).join("") + "\u2026" : clean;
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 Error(readyTimeoutMessage(timeoutMs));
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 Error(message)), remaining());
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
+ };