@anubhav-choubey/web-for-agents 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,16 +1,23 @@
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
+ DESCRIPTION_MAX_CHARS,
7
+ DESCRIPTION_MIN_CHARS,
8
+ TITLE_MAX_CHARS
9
+ } from "./chunk-L3AYWCSX.js";
7
10
  import {
8
- jsonLdTypes
9
- } from "./chunk-LCLDQ3A3.js";
11
+ MAX_JSON_LD_NODES,
12
+ jsonLdNodes,
13
+ jsonLdTypes,
14
+ nodeTypes
15
+ } from "./chunk-MIVGE5TM.js";
10
16
  import {
17
+ AGENT_MAP_VERSION,
11
18
  collapseWhitespace,
12
19
  stripBom
13
- } from "./chunk-FEAKVNMG.js";
20
+ } from "./chunk-4STTPCBR.js";
14
21
  import {
15
22
  AGENT_MAP_PATH,
16
23
  LLMS_TXT_PATH,
@@ -22,12 +29,177 @@ import {
22
29
  markdownPathFor,
23
30
  trimTrailingSlash,
24
31
  tryResolveUrl
25
- } from "./chunk-AZG5MLNG.js";
32
+ } from "./chunk-2Y3LLYEC.js";
26
33
  import {
27
34
  errorMessage,
28
35
  withPrefix
29
36
  } from "./chunk-JJN6FNJD.js";
30
37
 
38
+ // src/shared/schemaRules.ts
39
+ var DOCS = "https://developers.google.com/search/docs/appearance/structured-data/";
40
+ var req = (...anyOf) => ({ anyOf });
41
+ var ARTICLE = {
42
+ docs: `${DOCS}article`,
43
+ required: [],
44
+ recommended: ["author", "author.name", "author.url", "dateModified", "datePublished", "headline", "image"]
45
+ };
46
+ var SOFTWARE_APPLICATION = {
47
+ docs: `${DOCS}software-app`,
48
+ required: [req("name"), req("offers"), req("aggregateRating", "review")],
49
+ recommended: ["applicationCategory", "operatingSystem"],
50
+ nested: [{ prop: "offers", whenType: ["Offer"], orUntyped: true, required: [req("price")] }]
51
+ };
52
+ var SCHEMA_RULES = {
53
+ Article: ARTICLE,
54
+ NewsArticle: ARTICLE,
55
+ BlogPosting: ARTICLE,
56
+ BreadcrumbList: {
57
+ docs: `${DOCS}breadcrumb`,
58
+ required: [req("itemListElement")],
59
+ recommended: [],
60
+ nested: [
61
+ {
62
+ prop: "itemListElement",
63
+ required: [req("position"), req("name", "item.name"), { anyOf: ["item"], exceptLast: true }]
64
+ }
65
+ ]
66
+ },
67
+ Event: {
68
+ docs: `${DOCS}event`,
69
+ required: [req("name"), req("startDate"), req("location")],
70
+ recommended: ["description", "endDate", "eventStatus", "image", "location.name", "offers", "organizer", "performer"],
71
+ nested: [{ prop: "location", whenType: ["Place"], required: [req("address")] }]
72
+ },
73
+ FAQPage: {
74
+ docs: `${DOCS}faqpage`,
75
+ required: [req("mainEntity")],
76
+ recommended: [],
77
+ nested: [
78
+ {
79
+ prop: "mainEntity",
80
+ required: [req("name"), req("acceptedAnswer")],
81
+ nested: [{ prop: "acceptedAnswer", required: [req("text")] }]
82
+ }
83
+ ],
84
+ note: "Google no longer shows FAQ rich results; answer engines may still use it",
85
+ retired: true
86
+ },
87
+ Organization: {
88
+ docs: `${DOCS}organization`,
89
+ required: [],
90
+ recommended: ["name", "url", "logo", "description", "sameAs", "address", "contactPoint", "email", "telephone"]
91
+ },
92
+ Person: {
93
+ docs: `${DOCS}profile-page`,
94
+ required: [],
95
+ recommended: ["name", "alternateName", "description", "identifier", "image", "sameAs"]
96
+ },
97
+ ProfilePage: {
98
+ docs: `${DOCS}profile-page`,
99
+ required: [req("mainEntity")],
100
+ recommended: ["dateCreated", "dateModified"],
101
+ nested: [{ prop: "mainEntity", required: [req("name", "alternateName")] }]
102
+ },
103
+ Product: {
104
+ docs: `${DOCS}product-snippet`,
105
+ required: [req("name"), req("offers", "aggregateRating", "review")],
106
+ recommended: ["aggregateRating", "offers", "review", "offers.priceCurrency", "offers.availability"],
107
+ nested: [
108
+ { prop: "offers", whenType: ["Offer"], orUntyped: true, required: [req("price", "priceSpecification.price")] },
109
+ { prop: "offers", whenType: ["AggregateOffer"], required: [req("lowPrice"), req("priceCurrency")] }
110
+ ]
111
+ },
112
+ Recipe: {
113
+ docs: `${DOCS}recipe`,
114
+ required: [req("name"), req("image")],
115
+ recommended: ["author", "datePublished", "description", "recipeIngredient", "recipeInstructions", "totalTime"]
116
+ },
117
+ SoftwareApplication: SOFTWARE_APPLICATION,
118
+ MobileApplication: SOFTWARE_APPLICATION,
119
+ WebApplication: SOFTWARE_APPLICATION,
120
+ WebSite: {
121
+ docs: "https://developers.google.com/search/docs/appearance/site-names",
122
+ required: [req("name"), req("url")],
123
+ recommended: ["alternateName"]
124
+ }
125
+ };
126
+ function present(v) {
127
+ if (v === void 0 || v === null) return false;
128
+ if (typeof v === "string") return v.trim() !== "";
129
+ if (Array.isArray(v)) return v.length > 0;
130
+ return true;
131
+ }
132
+ var isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
133
+ function hasPath(node, path2) {
134
+ let level = [node];
135
+ for (const key of path2.split(".")) {
136
+ const next = [];
137
+ for (const v of level) {
138
+ for (const o of Array.isArray(v) ? v : [v]) if (isObject(o) && present(o[key])) next.push(o[key]);
139
+ }
140
+ if (next.length === 0) return false;
141
+ level = next;
142
+ }
143
+ return true;
144
+ }
145
+ function resolveRef(o, byId) {
146
+ const keys = Object.keys(o);
147
+ if (keys.length !== 1 || keys[0] !== "@id") return o;
148
+ return typeof o["@id"] === "string" ? byId.get(o["@id"]) ?? null : null;
149
+ }
150
+ var label = (prefix, anyOf) => prefix + anyOf[0] + (anyOf.length > 1 ? ` (or ${anyOf.slice(1).join(", ")})` : "");
151
+ function checkSchemaNodes(nodes, limit = Infinity) {
152
+ const byId = /* @__PURE__ */ new Map();
153
+ for (const n of nodes) if (typeof n["@id"] === "string" && !byId.has(n["@id"])) byId.set(n["@id"], n);
154
+ const scoredIssues = [];
155
+ const retiredIssues = [];
156
+ let total = 0;
157
+ let scored = 0;
158
+ let budget = MAX_JSON_LD_NODES;
159
+ const report = (type, path2) => {
160
+ total++;
161
+ const retired = SCHEMA_RULES[type].retired === true;
162
+ if (!retired) scored++;
163
+ const list = retired ? retiredIssues : scoredIssues;
164
+ if (list.length < limit) list.push({ type, path: path2 });
165
+ };
166
+ const apply = (type, node, prefix, required, nested, isLast) => {
167
+ for (const r of required) {
168
+ if (r.exceptLast && isLast) continue;
169
+ if (!r.anyOf.some((p) => hasPath(node, p))) report(type, label(prefix, r.anyOf));
170
+ }
171
+ for (const n of nested ?? []) {
172
+ const v = node[n.prop];
173
+ const items = Array.isArray(v) ? v : [v];
174
+ for (let i = 0; i < items.length; i++) {
175
+ if (--budget < 0) return;
176
+ const raw = items[i];
177
+ if (!isObject(raw)) continue;
178
+ const child = resolveRef(raw, byId);
179
+ if (child === null) continue;
180
+ if (n.whenType !== void 0) {
181
+ const types = nodeTypes(child);
182
+ const matches2 = types.length === 0 ? n.orUntyped === true : types.some((t) => n.whenType.includes(t));
183
+ if (!matches2) continue;
184
+ }
185
+ const at = `${prefix}${n.prop}${Array.isArray(v) ? `[${i}]` : ""}.`;
186
+ apply(type, child, at, n.required, n.nested, i === items.length - 1);
187
+ }
188
+ }
189
+ };
190
+ for (const node of nodes) {
191
+ if (--budget < 0) break;
192
+ const seen = /* @__PURE__ */ new Set();
193
+ for (const type of nodeTypes(node)) {
194
+ const rule = Object.hasOwn(SCHEMA_RULES, type) ? SCHEMA_RULES[type] : void 0;
195
+ if (rule === void 0 || seen.has(rule)) continue;
196
+ seen.add(rule);
197
+ apply(type, node, "", rule.required, rule.nested, true);
198
+ }
199
+ }
200
+ return { issues: [...scoredIssues, ...retiredIssues].slice(0, limit), total, scored };
201
+ }
202
+
31
203
  // src/cli/privateAddress.ts
32
204
  import { lookup } from "dns/promises";
33
205
  import { BlockList, isIP } from "net";
@@ -79,10 +251,10 @@ function abortable(p, signal) {
79
251
  });
80
252
  }
81
253
  function decode(bytes, contentType) {
82
- const label = /charset\s*=\s*"?([^;"\s]+)/i.exec(contentType)?.[1] ?? "utf-8";
254
+ const label2 = /charset\s*=\s*"?([^;"\s]+)/i.exec(contentType)?.[1] ?? "utf-8";
83
255
  let decoder;
84
256
  try {
85
- decoder = new TextDecoder(label);
257
+ decoder = new TextDecoder(label2);
86
258
  } catch (e) {
87
259
  if (!(e instanceof RangeError)) throw e;
88
260
  decoder = new TextDecoder("utf-8");
@@ -266,16 +438,6 @@ function decode2(s) {
266
438
  return String.fromCodePoint(cp);
267
439
  });
268
440
  }
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
441
  function extractHtmlFacts(html) {
280
442
  const metas = [];
281
443
  const links = [];
@@ -286,88 +448,29 @@ function extractHtmlFacts(html) {
286
448
  let prerendered = false;
287
449
  const depth = {};
288
450
  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;
451
+ let pos = 0;
452
+ let lastAttrs = {};
453
+ for (const t of scanHtml(html, RAW_TEXT)) {
454
+ if (t.start > pos && excluded === 0) chunks.push(decode2(html.slice(pos, t.start)));
455
+ pos = t.end;
456
+ if (t.type === "comment") continue;
457
+ if (t.type === "raw") {
458
+ const raw = html.slice(t.start, t.end);
459
+ if (t.name === "title") {
460
+ if (title === null && (depth.svg ?? 0) === 0) {
461
+ title = decode2(raw).replace(/\s+/g, " ").trim();
462
+ }
463
+ } else if (t.name === "script") {
464
+ const type = (lastAttrs.type ?? "").split(";")[0].trim().toLowerCase();
465
+ if (type === "application/ld+json") jsonLd.push(raw);
306
466
  }
307
467
  continue;
308
468
  }
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();
469
+ const { name, closing, selfClosing } = t;
323
470
  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);
471
+ if (!closing) {
472
+ for (const a of t.attrs) if (!(a.name in attrs)) attrs[a.name] = decode2(a.value);
369
473
  }
370
- i = p;
371
474
  if (closing) {
372
475
  if (EXCLUDED.has(name) && (depth[name] ?? 0) > 0) {
373
476
  depth[name]--;
@@ -376,6 +479,7 @@ function extractHtmlFacts(html) {
376
479
  if (excluded === 0 && !INLINE.has(name)) chunks.push(" ");
377
480
  continue;
378
481
  }
482
+ lastAttrs = attrs;
379
483
  if ((depth.head ?? 0) > 0 && excluded === depth.head && !HEAD_OK.has(name)) {
380
484
  excluded -= depth.head;
381
485
  depth.head = 0;
@@ -387,27 +491,14 @@ function extractHtmlFacts(html) {
387
491
  } else if (name === "link") {
388
492
  links.push(attrs);
389
493
  }
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
- }
494
+ if (RAW_TEXT.has(name) && !selfClosing) continue;
405
495
  if (EXCLUDED.has(name) && !selfClosing) {
406
496
  depth[name] = (depth[name] ?? 0) + 1;
407
497
  excluded++;
408
498
  }
409
499
  if (excluded === 0 && !INLINE.has(name)) chunks.push(" ");
410
500
  }
501
+ if (pos < html.length && excluded === 0) chunks.push(decode2(html.slice(pos)));
411
502
  return {
412
503
  title,
413
504
  metas,
@@ -515,13 +606,13 @@ function matches(pattern, text) {
515
606
  return last === "" || text.indexOf(last, at) >= 0;
516
607
  }
517
608
  var matchCache = /* @__PURE__ */ new WeakMap();
518
- function cachedMatch(robots, pattern, path) {
609
+ function cachedMatch(robots, pattern, path2) {
519
610
  let byPath = matchCache.get(robots);
520
611
  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());
612
+ let byPattern = byPath.get(path2);
613
+ if (!byPattern) byPath.set(path2, byPattern = /* @__PURE__ */ new Map());
523
614
  let hit = byPattern.get(pattern);
524
- if (hit === void 0) byPattern.set(pattern, hit = matches(pattern, path));
615
+ if (hit === void 0) byPattern.set(pattern, hit = matches(pattern, path2));
525
616
  return hit;
526
617
  }
527
618
  function isAllowed(robots, userAgent, pathAndQuery2) {
@@ -556,9 +647,7 @@ var CHECK_WEIGHTS = {
556
647
  };
557
648
  var CONTENT_MIN_CHARS = 200;
558
649
  var CONTENT_WARN_CHARS = 50;
559
- var TITLE_MAX_CHARS = 70;
560
- var DESCRIPTION_MIN_CHARS = 50;
561
- var DESCRIPTION_MAX_CHARS = 160;
650
+ var SCHEMA_ISSUES_SHOWN = 3;
562
651
  var MAX_BODY_LABEL = `${MAX_BODY_BYTES / (1024 * 1024)} MiB`;
563
652
  var TITLES = {
564
653
  content: "Content without JavaScript",
@@ -681,7 +770,32 @@ function checkJsonLd({ html }) {
681
770
  if (!parsed.some((p) => jsonLdTypes(p).length > 0)) {
682
771
  return warn("JSON-LD parses but has no @type", "Add an @type (for example Organization) to your JSON-LD.");
683
772
  }
684
- return pass(`${parsed.length} JSON-LD block${parsed.length === 1 ? " parses" : "s parse"} with an @type`);
773
+ const { issues, total, scored } = checkSchemaNodes(parsed.flatMap(jsonLdNodes), SCHEMA_ISSUES_SHOWN);
774
+ const base = `${parsed.length} JSON-LD block${parsed.length === 1 ? " parses" : "s parse"} with an @type`;
775
+ if (total === 0) return pass(base);
776
+ const isRetired = (i) => SCHEMA_RULES[i.type].retired === true;
777
+ const shownScored = issues.filter((i) => !isRetired(i));
778
+ const shownRetired = issues.filter(isRetired);
779
+ const retiredNote = notScored(shownRetired, total - scored - shownRetired.length);
780
+ if (scored === 0) return pass(`${base}; ${retiredNote}`);
781
+ const moreScored = scored - shownScored.length;
782
+ const rules = [...new Set(shownScored.map((i) => SCHEMA_RULES[i.type]))];
783
+ return warn(
784
+ `Missing required structured data fields: ${shownScored.map((i) => `${i.type} ${i.path}`).join("; ")}` + (moreScored > 0 ? `; +${moreScored} more` : "") + (total > scored ? `. ${retiredNote}` : ""),
785
+ `Add the missing properties; Google requires them for rich results (${rules.map((r) => r.docs).join(", ")}).`
786
+ );
787
+ }
788
+ var RETIRED_RULES = [...new Set(Object.values(SCHEMA_RULES))].filter((r) => r.retired === true);
789
+ var retiredNotes = (rules) => rules.flatMap((r) => r.note !== void 0 ? [r.note] : []).join("; ");
790
+ function notScored(shown, more) {
791
+ if (shown.length === 0) {
792
+ return `+${more} more not scored (${retiredNotes(RETIRED_RULES)})`;
793
+ }
794
+ const byType = /* @__PURE__ */ new Map();
795
+ for (const i of shown) byType.set(i.type, [...byType.get(i.type) ?? [], i.path]);
796
+ const listed = [...byType].map(([type, paths]) => `${type}: ${paths.join(", ")}`).join("; ");
797
+ const notes = retiredNotes([...new Set(shown.map((i) => SCHEMA_RULES[i.type]))]);
798
+ return `${listed}${more > 0 ? `, +${more} more` : ""} missing${notes ? ` (${notes})` : ""}`;
685
799
  }
686
800
  function checkRobots({ page, robots }) {
687
801
  const missingFix = `Add ${ROBOTS_PATH}; webForAgents() generates one from robots.ai.training / robots.ai.search.`;
@@ -695,21 +809,21 @@ function checkRobots({ page, robots }) {
695
809
  const out = robotsVerdict(parsed, pathAndQuery(page.finalUrl));
696
810
  return parsed.truncated ? { ...out, message: `${out.message} (${parsed.truncated})` } : out;
697
811
  }
698
- function robotsVerdict(parsed, path) {
699
- if (!isAllowed(parsed, "*", path)) {
812
+ function robotsVerdict(parsed, path2) {
813
+ if (!isAllowed(parsed, "*", path2)) {
700
814
  return fail(
701
- `${ROBOTS_PATH} blocks ${path} for all crawlers`,
815
+ `${ROBOTS_PATH} blocks ${path2} for all crawlers`,
702
816
  "Remove the Disallow rule matching this path under User-agent: *."
703
817
  );
704
818
  }
705
- const searchBlocked = AI_BOTS.search.filter((b) => !isAllowed(parsed, b, path));
819
+ const searchBlocked = AI_BOTS.search.filter((b) => !isAllowed(parsed, b, path2));
706
820
  if (searchBlocked.length > 0) {
707
821
  return warn(
708
822
  `AI search bots blocked: ${searchBlocked.join(", ")}`,
709
823
  'Allow AI search bots (robots.ai.search: "allow") so answer engines can cite this page.'
710
824
  );
711
825
  }
712
- const trainingBlocked = AI_BOTS.training.filter((b) => !isAllowed(parsed, b, path));
826
+ const trainingBlocked = AI_BOTS.training.filter((b) => !isAllowed(parsed, b, path2));
713
827
  if (trainingBlocked.length > 0) {
714
828
  return pass(`Training crawlers blocked (${trainingBlocked.join(", ")}); search bots allowed`);
715
829
  }
@@ -875,7 +989,7 @@ async function auditWithHooks(url, opts = {}, hooks = {}) {
875
989
  return report;
876
990
  }
877
991
  const html = extractHtmlFacts(page.body);
878
- const at = (path) => new URL(path, page.finalUrl).href;
992
+ const at = (path2) => new URL(path2, page.finalUrl).href;
879
993
  const getSameOrigin = (u, accept) => sameOrigin(u, page.finalUrl) ? get(u, accept) : Promise.resolve(notFetched(u));
880
994
  const alt = markdownAlternate(html);
881
995
  const altUrl = alt === null ? null : tryResolveUrl(alt, page.finalUrl);
@@ -945,8 +1059,9 @@ function normalizeInputUrl(input) {
945
1059
  return u.href;
946
1060
  }
947
1061
  var USAGE = `Usage: web-for-agents audit <url> [options]
1062
+ web-for-agents generate <outDir> --site-url <url> [options]
948
1063
 
949
- Audit how readable a page is for AI agents and crawlers.
1064
+ audit: check how readable a page is for AI agents and crawlers.
950
1065
 
951
1066
  Options:
952
1067
  --json print the report as JSON (page data is not sanitized; control
@@ -959,6 +1074,22 @@ Options:
959
1074
  link-local address (default: follow them and print a warning)
960
1075
  -h, --help show this help
961
1076
 
1077
+ generate: write llms.txt, llms-full.txt, .md pages, robots.txt, sitemap.xml and
1078
+ agents.json for HTML you prerendered yourself.
1079
+
1080
+ Options:
1081
+ --site-url <url> absolute URL of the deployed site (or siteUrl in --config)
1082
+ --ai-training <allow|block>
1083
+ --ai-search <allow|block>
1084
+ robots.txt policy for AI crawlers; both are required here or
1085
+ as robots.ai in --config
1086
+ --config <file> .json, .js or .mjs file whose default export holds options
1087
+ (exclude, markdown, llms, agentMap, sitemap, robots, ...)
1088
+ --no-link-markdown do not add the markdown <link> to each page's <head>
1089
+ --overwrite replace an existing robots.txt, sitemap.xml, llms.txt,
1090
+ llms-full.txt or agents.json that differs
1091
+ --json print the result as JSON
1092
+
962
1093
  Exit codes: 0 ok, 1 score below --min-score, 2 usage error or fatal error
963
1094
  `;
964
1095
  function parseCliArgs(argv) {
@@ -973,6 +1104,12 @@ function parseCliArgs(argv) {
973
1104
  timeout: { type: "string" },
974
1105
  "agent-map": { type: "string" },
975
1106
  "no-private-redirects": { type: "boolean" },
1107
+ "site-url": { type: "string" },
1108
+ "ai-training": { type: "string" },
1109
+ "ai-search": { type: "string" },
1110
+ config: { type: "string" },
1111
+ "no-link-markdown": { type: "boolean" },
1112
+ overwrite: { type: "boolean" },
976
1113
  help: { type: "boolean", short: "h" }
977
1114
  }
978
1115
  });
@@ -982,7 +1119,34 @@ function parseCliArgs(argv) {
982
1119
  const { values, positionals } = parsed;
983
1120
  if (values.help || positionals.length === 0) return { command: "help", json: false, timeoutMs: DEFAULT_TIMEOUT_MS };
984
1121
  const [command, input, ...rest] = positionals;
1122
+ const GENERATE_FLAGS = ["site-url", "ai-training", "ai-search", "config", "no-link-markdown", "overwrite"];
1123
+ const AUDIT_FLAGS = ["min-score", "timeout", "agent-map", "no-private-redirects"];
1124
+ if (command === "generate") {
1125
+ if (input === void 0) return fail2("generate requires an output directory");
1126
+ if (rest.length > 0) return fail2(`unexpected argument "${rest[0]}"`);
1127
+ const wrong2 = AUDIT_FLAGS.find((f) => values[f] !== void 0);
1128
+ if (wrong2) return fail2(`--${wrong2} is not an option of generate`);
1129
+ const gen = {
1130
+ outDir: input,
1131
+ linkMarkdown: values["no-link-markdown"] !== true,
1132
+ overwrite: values.overwrite === true
1133
+ };
1134
+ for (const [flag, key] of [["ai-training", "aiTraining"], ["ai-search", "aiSearch"]]) {
1135
+ const v = values[flag];
1136
+ if (v === void 0) continue;
1137
+ if (v !== "allow" && v !== "block") return fail2(`--${flag} must be "allow" or "block", got "${v}"`);
1138
+ gen[key] = v;
1139
+ }
1140
+ if (values["site-url"] !== void 0) gen.siteUrl = values["site-url"];
1141
+ if (values.config !== void 0) {
1142
+ if (values.config.trim() === "") return fail2("--config must be a file path");
1143
+ gen.config = values.config;
1144
+ }
1145
+ return { command: "generate", generate: gen, json: values.json === true, timeoutMs: DEFAULT_TIMEOUT_MS };
1146
+ }
985
1147
  if (command !== "audit") return fail2(`unknown command "${command}"`);
1148
+ const wrong = GENERATE_FLAGS.find((f) => values[f] !== void 0);
1149
+ if (wrong) return fail2(`--${wrong} is an option of generate, not audit`);
986
1150
  if (input === void 0) return fail2("audit requires a URL");
987
1151
  if (rest.length > 0) return fail2(`unexpected argument "${rest[0]}"`);
988
1152
  const args = {
@@ -1020,15 +1184,20 @@ function parseCliArgs(argv) {
1020
1184
  return args;
1021
1185
  }
1022
1186
 
1187
+ // src/cli/generate.ts
1188
+ import { existsSync, readFileSync } from "fs";
1189
+ import path from "path";
1190
+ import { pathToFileURL } from "url";
1191
+
1023
1192
  // src/cli/terminal.ts
1024
1193
  var MAX_TEXT_CHARS = 500;
1025
1194
  var CONTROL = /[\u0000-\u001f\u007f-\u009f]/g;
1026
1195
  var BIDI_CHARS = "\\u061c\\u200e\\u200f\\u202a-\\u202e\\u2066-\\u2069";
1027
1196
  var BIDI = new RegExp(`[${BIDI_CHARS}]`, "g");
1028
1197
  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;
1198
+ const clean2 = collapseWhitespace(s.replace(CONTROL, " ").replace(BIDI, ""));
1199
+ const chars = Array.from(clean2);
1200
+ return chars.length > max ? chars.slice(0, max - 1).join("") + "\u2026" : clean2;
1032
1201
  }
1033
1202
  var JSON_UNSAFE = new RegExp(`[\\u007f-\\u009f\\u2028\\u2029${BIDI_CHARS}]`, "g");
1034
1203
  function safeJson(value, space) {
@@ -1038,6 +1207,75 @@ function safeJson(value, space) {
1038
1207
  );
1039
1208
  }
1040
1209
 
1210
+ // src/cli/generate.ts
1211
+ var VITE_ENTRY = "@anubhav-choubey/web-for-agents/vite";
1212
+ var clean = (s) => s.split("\n").map((l) => safeText(l, Infinity)).join("\n");
1213
+ var isObject2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
1214
+ async function loadConfig(file) {
1215
+ const abs = path.resolve(file);
1216
+ if (!existsSync(abs)) throw new Error(withPrefix(`config file not found: ${abs}`));
1217
+ let config;
1218
+ try {
1219
+ if (abs.endsWith(".json")) config = JSON.parse(readFileSync(abs, "utf8"));
1220
+ else if (/\.m?js$/.test(abs)) config = (await import(pathToFileURL(abs).href)).default;
1221
+ else throw new Error("expected a .json, .js or .mjs file");
1222
+ } catch (e) {
1223
+ throw new Error(withPrefix(`could not load config ${abs}: ${errorMessage(e)}`), { cause: e });
1224
+ }
1225
+ if (!isObject2(config)) throw new Error(withPrefix(`config ${abs} must export an options object`));
1226
+ return config;
1227
+ }
1228
+ async function generateOptions(args) {
1229
+ const config = args.config ? await loadConfig(args.config) : {};
1230
+ const robots = isObject2(config.robots) ? config.robots : {};
1231
+ const ai = { ...isObject2(robots.ai) ? robots.ai : {} };
1232
+ if (args.aiTraining) ai.training = args.aiTraining;
1233
+ if (args.aiSearch) ai.search = args.aiSearch;
1234
+ if (ai.training === void 0 || ai.search === void 0) {
1235
+ throw new Error(withPrefix("--ai-training and --ai-search are required (or robots.ai in --config)"));
1236
+ }
1237
+ const siteUrl = args.siteUrl ?? config.siteUrl;
1238
+ if (siteUrl === void 0) throw new Error(withPrefix("--site-url is required (or siteUrl in --config)"));
1239
+ return {
1240
+ ...config,
1241
+ outDir: args.outDir,
1242
+ siteUrl,
1243
+ robots: { ...robots, ai },
1244
+ ...args.linkMarkdown ? {} : { linkMarkdown: false },
1245
+ ...args.overwrite ? { overwrite: true } : {}
1246
+ };
1247
+ }
1248
+ async function runGenerate(args, json, io) {
1249
+ let result;
1250
+ try {
1251
+ const options = await generateOptions(args);
1252
+ const { generateAgentFiles } = await import(VITE_ENTRY);
1253
+ result = await generateAgentFiles({
1254
+ ...options,
1255
+ logger: {
1256
+ warn: (m) => io.stderr(clean(m) + "\n"),
1257
+ info: (m) => (json ? io.stderr : io.stdout)(clean(m) + "\n")
1258
+ }
1259
+ });
1260
+ } catch (e) {
1261
+ io.stderr(withPrefix(`generate failed: ${clean(errorMessage(e).replace(/^\[web-for-agents\] /, ""))}
1262
+ `));
1263
+ return 2;
1264
+ }
1265
+ if (json) {
1266
+ io.stdout(safeJson(result, 2) + "\n");
1267
+ } else {
1268
+ const lines = [
1269
+ `Layout: ${result.fileLayout}`,
1270
+ `Pages: ${result.pages.map((p) => p.route + (p.listed ? "" : " (noindex)")).join(", ")}`,
1271
+ `Wrote ${result.written.length} files: ${result.written.join(", ")}`
1272
+ ];
1273
+ if (result.kept.length > 0) lines.push(`Kept existing: ${result.kept.join(", ")} (use --overwrite to replace)`);
1274
+ io.stdout(clean(lines.join("\n")) + "\n");
1275
+ }
1276
+ return 0;
1277
+ }
1278
+
1041
1279
  // src/cli/report.ts
1042
1280
  function formatText(report) {
1043
1281
  const lines = [
@@ -1077,6 +1315,7 @@ ${USAGE}`);
1077
1315
  io.stdout(USAGE);
1078
1316
  return 0;
1079
1317
  }
1318
+ if (args.command === "generate") return runGenerate(args.generate, args.json, io);
1080
1319
  const report = await audit(args.url, {
1081
1320
  timeoutMs: args.timeoutMs,
1082
1321
  ...args.agentMap === void 0 ? {} : { agentMapPath: args.agentMap },