@rivi-software/agentic-seo-check 0.1.1 → 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.
Files changed (60) hide show
  1. package/README.md +61 -5
  2. package/dist/cli.js +69 -26
  3. package/dist/cli.js.map +1 -1
  4. package/dist/core/aeo/audit.d.ts +44 -0
  5. package/dist/core/aeo/audit.js +310 -0
  6. package/dist/core/aeo/audit.js.map +1 -0
  7. package/dist/core/aeo/bots.d.ts +30 -0
  8. package/dist/core/aeo/bots.js +299 -0
  9. package/dist/core/aeo/bots.js.map +1 -0
  10. package/dist/core/aeo/llms-txt.d.ts +8 -0
  11. package/dist/core/aeo/llms-txt.js +88 -0
  12. package/dist/core/aeo/llms-txt.js.map +1 -0
  13. package/dist/core/aeo/page.d.ts +53 -0
  14. package/dist/core/aeo/page.js +312 -0
  15. package/dist/core/aeo/page.js.map +1 -0
  16. package/dist/core/aeo/probe.d.ts +10 -0
  17. package/dist/core/aeo/probe.js +53 -0
  18. package/dist/core/aeo/probe.js.map +1 -0
  19. package/dist/core/aeo/site-rules.d.ts +13 -0
  20. package/dist/core/aeo/site-rules.js +178 -0
  21. package/dist/core/aeo/site-rules.js.map +1 -0
  22. package/dist/core/audit.d.ts +23 -2
  23. package/dist/core/audit.js +47 -22
  24. package/dist/core/audit.js.map +1 -1
  25. package/dist/core/crawl.d.ts +5 -1
  26. package/dist/core/crawl.js +26 -7
  27. package/dist/core/crawl.js.map +1 -1
  28. package/dist/core/html/answer.d.ts +4 -0
  29. package/dist/core/html/answer.js +198 -0
  30. package/dist/core/html/answer.js.map +1 -0
  31. package/dist/core/html/dom.d.ts +4 -0
  32. package/dist/core/html/dom.js +67 -1
  33. package/dist/core/html/dom.js.map +1 -1
  34. package/dist/core/html/extract.js +5 -71
  35. package/dist/core/html/extract.js.map +1 -1
  36. package/dist/core/report/markdown.d.ts +3 -1
  37. package/dist/core/report/markdown.js +102 -0
  38. package/dist/core/report/markdown.js.map +1 -1
  39. package/dist/core/robots.d.ts +7 -0
  40. package/dist/core/robots.js +12 -1
  41. package/dist/core/robots.js.map +1 -1
  42. package/dist/core/rules/aeo.d.ts +11 -0
  43. package/dist/core/rules/aeo.js +423 -0
  44. package/dist/core/rules/aeo.js.map +1 -0
  45. package/dist/core/rules/index.d.ts +4 -0
  46. package/dist/core/rules/index.js +7 -1
  47. package/dist/core/rules/index.js.map +1 -1
  48. package/dist/core/score.d.ts +4 -2
  49. package/dist/core/score.js.map +1 -1
  50. package/dist/core/static.js +1 -0
  51. package/dist/core/static.js.map +1 -1
  52. package/dist/core/types.d.ts +177 -2
  53. package/dist/core/types.js.map +1 -1
  54. package/dist/index.d.ts +6 -2
  55. package/dist/index.js +4 -2
  56. package/dist/index.js.map +1 -1
  57. package/dist/mcp.js +129 -3
  58. package/dist/mcp.js.map +1 -1
  59. package/package.json +6 -2
  60. package/skills/seo-audit/SKILL.md +8 -2
@@ -0,0 +1,310 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { DEFAULT_CRAWL, readStdin, resolveTarget, rollupIssues, trimFinding, } from "../audit.js";
4
+ import { crawlSite } from "../crawl.js";
5
+ import { DEFAULT_USER_AGENT, fetchPage, toResponseInfo } from "../fetch.js";
6
+ import { extractPage } from "../html/extract.js";
7
+ import { loadRobotsTxt } from "../robots.js";
8
+ import { AEO_PAGE_RULES, runPageRules, sortFindings } from "../rules/index.js";
9
+ import { pageScore, siteScore } from "../score.js";
10
+ import { scanDirectory } from "../static.js";
11
+ import { countFindings, DEFAULT_THRESHOLDS } from "../types.js";
12
+ import { AI_BOTS, crawlerAccess } from "./bots.js";
13
+ import { loadLlmsTxt, readLlmsTxt } from "./llms-txt.js";
14
+ import { aeoPageSummary } from "./page.js";
15
+ import { probeBots } from "./probe.js";
16
+ import { aeoSiteFindings } from "./site-rules.js";
17
+ const userAgentOf = (options) => options.fetch?.userAgent ?? DEFAULT_USER_AGENT;
18
+ /** robots.txt and llms.txt of the origin a page is served from. */
19
+ const loadOrigin = async (pageUrl, options) => {
20
+ const { origin } = new URL(pageUrl);
21
+ options.onProgress?.({
22
+ phase: "robots",
23
+ message: `Fetching ${origin}/robots.txt and ${origin}/llms.txt`,
24
+ });
25
+ const [robots, llmsTxt] = await Promise.all([
26
+ loadRobotsTxt(origin, userAgentOf(options), options.fetch),
27
+ loadLlmsTxt(origin, options.fetch),
28
+ ]);
29
+ return { robots: robots.info, robotsText: robots.text, llmsTxt };
30
+ };
31
+ const collectUrl = async (url, options) => {
32
+ const res = await fetchPage(url, options.fetch ?? {});
33
+ const page = extractPage(res.body ?? "", {
34
+ url: res.finalUrl,
35
+ includeSubdomains: options.includeSubdomains,
36
+ });
37
+ // A page that never answered says nothing about the site; its robots.txt would fail the same way.
38
+ const site = res.status === null ? null : await loadOrigin(res.finalUrl, options);
39
+ return {
40
+ target: url,
41
+ url: res.finalUrl,
42
+ source: "url",
43
+ pageSource: "url",
44
+ records: [
45
+ {
46
+ url: res.finalUrl,
47
+ requestedUrl: url,
48
+ page,
49
+ response: toResponseInfo(res),
50
+ depth: 0,
51
+ },
52
+ ],
53
+ robots: site?.robots ?? null,
54
+ robotsText: site?.robotsText ?? null,
55
+ llmsTxt: site?.llmsTxt ?? null,
56
+ live: true,
57
+ };
58
+ };
59
+ const collectSite = async (url, options) => {
60
+ const result = await crawlSite(url, {
61
+ maxPages: options.maxPages ?? DEFAULT_CRAWL.maxPages,
62
+ maxDepth: options.maxDepth ?? DEFAULT_CRAWL.maxDepth,
63
+ concurrency: options.concurrency ?? DEFAULT_CRAWL.concurrency,
64
+ delayMs: options.delayMs ?? DEFAULT_CRAWL.delayMs,
65
+ respectRobots: options.respectRobots ?? true,
66
+ includeSubdomains: options.includeSubdomains ?? false,
67
+ checkExternalLinks: false,
68
+ // The AEO audit reads pages; broken links, sitemaps and redirect probes are the SEO crawl's job.
69
+ maxLinkChecks: 0,
70
+ siteChecks: false,
71
+ // oxlint-disable-next-line eslint/require-unicode-regexp -- the user's own patterns, in JavaScript's default syntax; the u flag would reject some of them
72
+ includePatterns: (options.includePatterns ?? []).map((p) => new RegExp(p)),
73
+ // oxlint-disable-next-line eslint/require-unicode-regexp -- the user's own patterns, in JavaScript's default syntax; the u flag would reject some of them
74
+ excludePatterns: (options.excludePatterns ?? []).map((p) => new RegExp(p)),
75
+ fetch: { ...options.fetch, userAgent: userAgentOf(options) },
76
+ onProgress: options.onProgress,
77
+ });
78
+ options.onProgress?.({ phase: "llms-txt", message: "Fetching llms.txt" });
79
+ return {
80
+ target: url,
81
+ url: result.startUrl,
82
+ source: "crawl",
83
+ pageSource: "url",
84
+ records: result.records,
85
+ robots: result.robots,
86
+ robotsText: result.robotsText ?? null,
87
+ llmsTxt: await loadLlmsTxt(new URL(result.startUrl).origin, options.fetch),
88
+ live: true,
89
+ };
90
+ };
91
+ const collectDirectory = async (dir, options) => {
92
+ const baseUrl = options.baseUrl ?? "http://localhost/";
93
+ const result = await scanDirectory(dir, {
94
+ baseUrl,
95
+ includeSubdomains: options.includeSubdomains ?? false,
96
+ maxPages: options.maxPages ?? 500,
97
+ userAgent: options.fetch?.userAgent,
98
+ });
99
+ return {
100
+ target: dir,
101
+ url: dir,
102
+ source: "directory",
103
+ pageSource: "file",
104
+ records: result.records,
105
+ robots: result.robots,
106
+ robotsText: result.robotsText ?? null,
107
+ llmsTxt: await readLlmsTxt(dir, baseUrl),
108
+ live: false,
109
+ };
110
+ };
111
+ /** One page from HTML in hand: no site around it, so no robots.txt or llms.txt. */
112
+ const collectHtml = (html, url, target, source, options) => ({
113
+ target,
114
+ url,
115
+ source,
116
+ pageSource: source,
117
+ records: [
118
+ {
119
+ url,
120
+ requestedUrl: url,
121
+ page: extractPage(html, {
122
+ url,
123
+ includeSubdomains: options.includeSubdomains,
124
+ }),
125
+ depth: 0,
126
+ },
127
+ ],
128
+ robots: null,
129
+ robotsText: null,
130
+ llmsTxt: null,
131
+ live: false,
132
+ });
133
+ const evaluatePage = (record, source, options) => {
134
+ let findings = runPageRules({
135
+ page: record.page,
136
+ source,
137
+ response: record.response,
138
+ thresholds: DEFAULT_THRESHOLDS,
139
+ depth: record.depth,
140
+ }, { disable: new Set(options.disableRules), rules: AEO_PAGE_RULES });
141
+ if (options.includeInfo === false) {
142
+ findings = findings.filter((f) => f.severity !== "info");
143
+ }
144
+ const counts = countFindings(findings);
145
+ return { record, findings, counts, score: pageScore(counts) };
146
+ };
147
+ /**
148
+ * Requests the start page as each AI bot and records the answers on `crawlers`. Returns why the
149
+ * probe could not run, when it could not.
150
+ */
151
+ const runProbe = async (c, start, crawlers, options) => {
152
+ const response = start?.record.response;
153
+ if (!c.live || !response) {
154
+ return "it needs a live URL, and this target is HTML, a file or a directory";
155
+ }
156
+ if (response.error || response.status === null || response.status >= 400) {
157
+ return `the audit's own request got ${response.status === null ? "no answer" : `HTTP ${response.status}`}, so a refusal could not be told apart from a page that fails for everyone`;
158
+ }
159
+ if (!crawlers) {
160
+ return "robots.txt could not be read";
161
+ }
162
+ options.onProgress?.({
163
+ phase: "ai-bots",
164
+ message: `Requesting ${response.finalUrl} with AI bot User-Agents`,
165
+ });
166
+ const probes = await probeBots(response.finalUrl, AI_BOTS, options.fetch);
167
+ crawlers.probeBaseline = {
168
+ userAgent: userAgentOf(options),
169
+ status: response.status,
170
+ };
171
+ for (const bot of crawlers.bots) {
172
+ const probe = probes.get(bot.name);
173
+ if (probe) {
174
+ bot.probe = probe;
175
+ }
176
+ }
177
+ return undefined;
178
+ };
179
+ const blockedBots = (crawlers, purpose) => crawlers
180
+ ? crawlers.bots
181
+ .filter((b) => b.purpose === purpose && (!b.allowed || b.blockedPages > 0))
182
+ .map((b) => b.name)
183
+ : [];
184
+ const pageEntry = (p) => {
185
+ // url and depth lead the entry, the way a reader scans a list of pages.
186
+ const head = {
187
+ url: p.record.file ?? p.record.url,
188
+ };
189
+ if (p.record.depth >= 0) {
190
+ head.depth = p.record.depth;
191
+ }
192
+ return {
193
+ ...head,
194
+ score: p.score,
195
+ counts: p.counts,
196
+ summary: aeoPageSummary(p.record.page, p.record.response),
197
+ };
198
+ };
199
+ const buildAudit = async (c, options) => {
200
+ const isSite = c.source === "crawl" || c.source === "directory";
201
+ const pages = c.records.map((r) => evaluatePage(r, c.pageSource, options));
202
+ const start = pages.find((p) => p.record.depth === 0) ?? pages[0];
203
+ const crawlers = c.robots
204
+ ? crawlerAccess(c.robots, c.robotsText, start?.record.url ?? c.url, c.records.map((r) => r.url))
205
+ : null;
206
+ const probeSkipped = options.aiBots
207
+ ? await runProbe(c, start, crawlers, options)
208
+ : undefined;
209
+ const disabled = new Set(options.disableRules);
210
+ let siteFindings = aeoSiteFindings({
211
+ crawlers,
212
+ llmsTxt: c.llmsTxt,
213
+ pageCount: pages.length,
214
+ probeSkipped,
215
+ }).filter((f) => !disabled.has(f.rule));
216
+ if (options.includeInfo === false) {
217
+ siteFindings = siteFindings.filter((f) => f.severity !== "info");
218
+ }
219
+ const pageFindings = pages.flatMap((p) => p.findings);
220
+ const audit = {
221
+ schemaVersion: 1,
222
+ kind: "aeo",
223
+ target: c.target,
224
+ url: c.url,
225
+ source: c.source,
226
+ score: siteScore(pages, siteFindings),
227
+ counts: countFindings([...siteFindings, ...pageFindings]),
228
+ auditedAt: new Date().toISOString(),
229
+ summary: {
230
+ pages: pages.length,
231
+ robotsTxt: crawlers?.status ?? null,
232
+ searchCrawlersBlocked: blockedBots(crawlers, "search"),
233
+ answerBotsBlocked: blockedBots(crawlers, "answer"),
234
+ trainingBotsBlocked: blockedBots(crawlers, "training"),
235
+ trainingPreference: crawlers?.trainingPreference ?? null,
236
+ blockedByServer: crawlers?.probeBaseline
237
+ ? crawlers.bots.filter((b) => b.probe?.blocked).map((b) => b.name)
238
+ : null,
239
+ llmsTxt: c.llmsTxt?.status ?? null,
240
+ page: start
241
+ ? aeoPageSummary(start.record.page, start.record.response)
242
+ : null,
243
+ },
244
+ findings: sortFindings(isSite && options.pageDetail === "summary"
245
+ ? siteFindings
246
+ : [...siteFindings, ...pageFindings]),
247
+ crawlers,
248
+ llmsTxt: c.llmsTxt,
249
+ };
250
+ if (isSite) {
251
+ audit.pages = pages
252
+ .toSorted((a, b) => a.record.depth - b.record.depth || a.score - b.score)
253
+ .map(pageEntry);
254
+ audit.issues = rollupIssues(pages.map((p) => ({
255
+ url: p.record.file ?? p.record.url,
256
+ findings: p.findings,
257
+ })));
258
+ }
259
+ return audit;
260
+ };
261
+ /** AEO audit of an HTML string. `url` is where the HTML would be served, for links and display. */
262
+ // oxlint-disable-next-line eslint/require-await -- public API: it returns a promise like the other audit functions, so a throw reaches callers as a rejection
263
+ export const auditAeoHtml = async (html, input = {}, options = {}) => {
264
+ const url = input.url ?? options.baseUrl ?? "http://localhost/";
265
+ return buildAudit(collectHtml(html, url, input.target ?? url, "html", options), options);
266
+ };
267
+ /** AEO audit of one live page, with its site's robots.txt and llms.txt. */
268
+ export const auditAeoUrl = async (url, options = {}) => buildAudit(await collectUrl(url, options), options);
269
+ /** AEO audit of a site, crawled from `startUrl`. */
270
+ export const auditAeoSite = async (startUrl, options = {}) => buildAudit(await collectSite(startUrl, options), options);
271
+ /** AEO audit of a directory of built HTML, with the robots.txt and llms.txt in it. */
272
+ export const auditAeoDirectory = async (dir, options = {}) => buildAudit(await collectDirectory(dir, options), options);
273
+ /** AEO audit of a URL (a crawl with `crawl`), a directory, an .html file, or "-" for stdin. */
274
+ export const auditAeoTarget = async (target, options = {}) => {
275
+ if (target === "-" || options.stdinHtml !== undefined) {
276
+ const html = options.stdinHtml ?? (await readStdin());
277
+ return auditAeoHtml(html, { url: options.baseUrl, target: "stdin" }, options);
278
+ }
279
+ const resolved = await resolveTarget(target);
280
+ switch (resolved.kind) {
281
+ case "url": {
282
+ return options.crawl
283
+ ? auditAeoSite(resolved.url, options)
284
+ : auditAeoUrl(resolved.url, options);
285
+ }
286
+ case "directory": {
287
+ return auditAeoDirectory(resolved.path, options);
288
+ }
289
+ default: {
290
+ const base = options.baseUrl ?? "http://localhost/";
291
+ const url = new URL(`/${path.basename(target)}`, base.endsWith("/") ? base : `${base}/`).href;
292
+ return buildAudit(collectHtml(await fs.readFile(target, "utf-8"), url, target, "file", options), options);
293
+ }
294
+ }
295
+ };
296
+ /** Drops bulky detail to save tokens when an LLM reads the result. */
297
+ export const compactAeoAudit = (audit) => ({
298
+ ...audit,
299
+ findings: audit.findings.map(trimFinding),
300
+ crawlers: audit.crawlers
301
+ ? {
302
+ ...audit.crawlers,
303
+ contentSignals: audit.crawlers.contentSignals.slice(0, 3),
304
+ }
305
+ : null,
306
+ llmsTxt: audit.llmsTxt
307
+ ? { ...audit.llmsTxt, sections: audit.llmsTxt.sections.slice(0, 5) }
308
+ : null,
309
+ });
310
+ //# sourceMappingURL=audit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit.js","sourceRoot":"","sources":["../../../src/core/aeo/audit.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ import type { AiBotPurpose, AiCrawlerReport, ContentSignal, RobotsTxtInfo } from "../types.js";
2
+ export interface AiBot {
3
+ /** The robots.txt product token. */
4
+ name: string;
5
+ operator: string;
6
+ purpose: AiBotPurpose;
7
+ usedFor: string;
8
+ docs: string;
9
+ /**
10
+ * The User-Agent the probe sends. Absent for robots.txt-only tokens (Google-Extended), and for
11
+ * search crawlers, which CDNs often block when the request does not come from the crawler's IPs.
12
+ */
13
+ userAgent?: string;
14
+ }
15
+ /**
16
+ * The crawlers that decide whether a site can show up in AI answers, from the operators' own
17
+ * documentation. Order: search crawlers, answer bots, training bots.
18
+ */
19
+ export declare const AI_BOTS: AiBot[];
20
+ /** The policy token of the shared AI training preference (draft-kelley-aipref-training-preference-signal). */
21
+ export declare const TRAINING_PREFERENCE_TOKEN = "__usage_ai-training";
22
+ /**
23
+ * Content-Signal lines: `Content-Signal: search=yes, ai-input=no` or, for one path,
24
+ * `Content-Signal: /blog/ ai-train=no`.
25
+ */
26
+ export declare const contentSignals: (text: string) => ContentSignal[];
27
+ /** Signals in a Content-Signal line that the policy does not define, or values other than yes and no. */
28
+ export declare const invalidSignals: (signal: ContentSignal) => string[];
29
+ /** What robots.txt lets each AI bot do on the audited pages; `startUrl` is the page the summary describes. */
30
+ export declare const crawlerAccess: (robots: RobotsTxtInfo, text: string | null, startUrl: string, pageUrls: string[]) => AiCrawlerReport;
@@ -0,0 +1,299 @@
1
+ import { agentMatcher } from "../robots.js";
2
+ const OPENAI_DOCS = "https://developers.openai.com/api/docs/bots";
3
+ const ANTHROPIC_DOCS = "https://support.claude.com/en/articles/8896518-does-anthropic-crawl-data-from-the-web-and-how-can-site-owners-block-the-crawler";
4
+ const PERPLEXITY_DOCS = "https://docs.perplexity.ai/guides/bots";
5
+ const GOOGLE_CRAWLERS_DOCS = "https://developers.google.com/crawling/docs/crawlers-fetchers/google-common-crawlers";
6
+ /**
7
+ * The crawlers that decide whether a site can show up in AI answers, from the operators' own
8
+ * documentation. Order: search crawlers, answer bots, training bots.
9
+ */
10
+ export const AI_BOTS = [
11
+ {
12
+ name: "Googlebot",
13
+ operator: "Google",
14
+ purpose: "search",
15
+ usedFor: "Google Search, AI Overviews and AI Mode",
16
+ docs: "https://developers.google.com/search/docs/appearance/ai-features",
17
+ },
18
+ {
19
+ name: "Bingbot",
20
+ operator: "Microsoft",
21
+ purpose: "search",
22
+ usedFor: "Bing search and Microsoft Copilot",
23
+ docs: "https://www.bing.com/webmasters/help/webmaster-guidelines-30fba23a",
24
+ },
25
+ {
26
+ name: "Applebot",
27
+ operator: "Apple",
28
+ purpose: "search",
29
+ usedFor: "Siri, Spotlight and Safari search",
30
+ docs: "https://support.apple.com/en-us/119829",
31
+ },
32
+ {
33
+ name: "OAI-SearchBot",
34
+ operator: "OpenAI",
35
+ purpose: "answer",
36
+ usedFor: "ChatGPT search results",
37
+ docs: OPENAI_DOCS,
38
+ userAgent: "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; OAI-SearchBot/1.4; +https://openai.com/searchbot",
39
+ },
40
+ {
41
+ name: "ChatGPT-User",
42
+ operator: "OpenAI",
43
+ purpose: "answer",
44
+ usedFor: "pages ChatGPT fetches for a user; robots.txt may not apply",
45
+ docs: OPENAI_DOCS,
46
+ userAgent: "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; ChatGPT-User/1.0; +https://openai.com/bot",
47
+ },
48
+ {
49
+ name: "Claude-SearchBot",
50
+ operator: "Anthropic",
51
+ purpose: "answer",
52
+ usedFor: "Claude's search results",
53
+ docs: ANTHROPIC_DOCS,
54
+ userAgent: "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Claude-SearchBot/1.0; +https://www.anthropic.com)",
55
+ },
56
+ {
57
+ name: "Claude-User",
58
+ operator: "Anthropic",
59
+ purpose: "answer",
60
+ usedFor: "pages Claude fetches for a user's question",
61
+ docs: ANTHROPIC_DOCS,
62
+ userAgent: "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Claude-User/1.0; +https://www.anthropic.com)",
63
+ },
64
+ {
65
+ name: "PerplexityBot",
66
+ operator: "Perplexity",
67
+ purpose: "answer",
68
+ usedFor: "Perplexity search results",
69
+ docs: PERPLEXITY_DOCS,
70
+ userAgent: "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; PerplexityBot/1.0; +https://perplexity.ai/perplexitybot)",
71
+ },
72
+ {
73
+ name: "Perplexity-User",
74
+ operator: "Perplexity",
75
+ purpose: "answer",
76
+ usedFor: "pages Perplexity fetches for a user",
77
+ docs: PERPLEXITY_DOCS,
78
+ userAgent: "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; Perplexity-User/1.0; +https://perplexity.ai/perplexity-user)",
79
+ },
80
+ {
81
+ name: "Google-Extended",
82
+ operator: "Google",
83
+ purpose: "answer",
84
+ usedFor: "Gemini Apps grounding and Gemini training, as a robots.txt token; not AI Overviews",
85
+ docs: GOOGLE_CRAWLERS_DOCS,
86
+ },
87
+ {
88
+ name: "DuckAssistBot",
89
+ operator: "DuckDuckGo",
90
+ purpose: "answer",
91
+ usedFor: "DuckDuckGo's AI-assisted answers",
92
+ docs: "https://duckduckgo.com/duckduckgo-help-pages/results/duckassistbot",
93
+ userAgent: "DuckAssistBot/1.2; (+http://duckduckgo.com/duckassistbot.html)",
94
+ },
95
+ {
96
+ name: "MistralAI-User",
97
+ operator: "Mistral AI",
98
+ purpose: "answer",
99
+ usedFor: "pages Le Chat fetches for a user",
100
+ docs: "https://docs.mistral.ai/robots",
101
+ userAgent: "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; MistralAI-User/1.0; +https://docs.mistral.ai/robots)",
102
+ },
103
+ {
104
+ name: "GPTBot",
105
+ operator: "OpenAI",
106
+ purpose: "training",
107
+ usedFor: "training OpenAI's models",
108
+ docs: OPENAI_DOCS,
109
+ userAgent: "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko); compatible; GPTBot/1.4; +https://openai.com/gptbot",
110
+ },
111
+ {
112
+ name: "ClaudeBot",
113
+ operator: "Anthropic",
114
+ purpose: "training",
115
+ usedFor: "training Anthropic's models",
116
+ docs: ANTHROPIC_DOCS,
117
+ userAgent: "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; ClaudeBot/1.0; +claudebot@anthropic.com)",
118
+ },
119
+ {
120
+ name: "Applebot-Extended",
121
+ operator: "Apple",
122
+ purpose: "training",
123
+ usedFor: "training Apple's models, as a robots.txt token",
124
+ docs: "https://support.apple.com/en-us/119829",
125
+ },
126
+ {
127
+ name: "CCBot",
128
+ operator: "Common Crawl",
129
+ purpose: "training",
130
+ usedFor: "Common Crawl's archive, which many models train on",
131
+ docs: "https://commoncrawl.org/ccbot",
132
+ userAgent: "CCBot/2.0 (https://commoncrawl.org/faq/)",
133
+ },
134
+ {
135
+ name: "meta-externalagent",
136
+ operator: "Meta",
137
+ purpose: "training",
138
+ usedFor: "training Meta's AI models and indexing for Meta products",
139
+ docs: "https://developers.facebook.com/docs/sharing/webmasters/web-crawlers",
140
+ userAgent: "meta-externalagent/1.1 (+https://developers.facebook.com/docs/sharing/webmasters/crawler)",
141
+ },
142
+ ];
143
+ /** The policy token of the shared AI training preference (draft-kelley-aipref-training-preference-signal). */
144
+ export const TRAINING_PREFERENCE_TOKEN = "__usage_ai-training";
145
+ /** Signals the Content Signals Policy defines, plus Cloudflare's experimental `use`. */
146
+ const KNOWN_SIGNALS = new Set(["search", "ai-input", "ai-train", "use"]);
147
+ /** How robots.txt groups match product tokens: lower-cased, without a version. */
148
+ const productToken = (agent) => agent.split("/")[0]?.trim().toLowerCase() ?? "";
149
+ /** The groups of a robots.txt file: every user agent it names, and each rule line with its group. */
150
+ const readGroups = (text) => {
151
+ const named = new Set();
152
+ const lines = [];
153
+ let agents = [];
154
+ let inAgentRun = false;
155
+ for (const raw of text.split(/\r\n|\r|\n/u)) {
156
+ const hash = raw.indexOf("#");
157
+ const line = (hash === -1 ? raw : raw.slice(0, hash)).trim();
158
+ const colon = line.indexOf(":");
159
+ if (colon === -1) {
160
+ continue;
161
+ }
162
+ const key = line.slice(0, colon).trim().toLowerCase();
163
+ const value = line.slice(colon + 1).trim();
164
+ if (key === "user-agent") {
165
+ // Consecutive user-agent lines share one group; a rule line in between starts a new one.
166
+ if (!inAgentRun) {
167
+ agents = [];
168
+ }
169
+ if (value) {
170
+ agents.push(value);
171
+ named.add(productToken(value));
172
+ }
173
+ inAgentRun = true;
174
+ continue;
175
+ }
176
+ inAgentRun = false;
177
+ lines.push({ agents, key, value });
178
+ }
179
+ return { named, lines };
180
+ };
181
+ /**
182
+ * Content-Signal lines: `Content-Signal: search=yes, ai-input=no` or, for one path,
183
+ * `Content-Signal: /blog/ ai-train=no`.
184
+ */
185
+ export const contentSignals = (text) => readGroups(text)
186
+ .lines.filter((l) => l.key === "content-signal" && l.value)
187
+ .slice(0, 50)
188
+ .map((l) => {
189
+ let rest = l.value;
190
+ let path = null;
191
+ if (rest.startsWith("/")) {
192
+ const space = rest.search(/\s/u);
193
+ path = space === -1 ? rest : rest.slice(0, space);
194
+ rest = space === -1 ? "" : rest.slice(space + 1);
195
+ }
196
+ const signals = {};
197
+ for (const pair of rest.split(",")) {
198
+ const eq = pair.indexOf("=");
199
+ const name = (eq === -1 ? pair : pair.slice(0, eq))
200
+ .trim()
201
+ .toLowerCase();
202
+ if (name) {
203
+ signals[name] =
204
+ eq === -1
205
+ ? ""
206
+ : pair
207
+ .slice(eq + 1)
208
+ .trim()
209
+ .toLowerCase();
210
+ }
211
+ }
212
+ return { userAgents: l.agents, path, signals };
213
+ });
214
+ /** Signals in a Content-Signal line that the policy does not define, or values other than yes and no. */
215
+ export const invalidSignals = (signal) => Object.entries(signal.signals).flatMap(([name, value]) => {
216
+ if (!KNOWN_SIGNALS.has(name)) {
217
+ return [`${name}=${value}`];
218
+ }
219
+ return name !== "use" && value !== "yes" && value !== "no"
220
+ ? [`${name}=${value}`]
221
+ : [];
222
+ });
223
+ /**
224
+ * robots.txt as RFC 9309 has crawlers read it: a file is "found" with status 200, "missing" with a
225
+ * 4xx (crawl everything), "unreachable" with a 5xx or no answer (crawl nothing).
226
+ */
227
+ const robotsStatus = (robots) => {
228
+ if (robots.found) {
229
+ return "found";
230
+ }
231
+ if (robots.status === null || robots.status >= 500) {
232
+ return "unreachable";
233
+ }
234
+ return "missing";
235
+ };
236
+ /** What robots.txt lets each AI bot do on the audited pages; `startUrl` is the page the summary describes. */
237
+ export const crawlerAccess = (robots, text, startUrl, pageUrls) => {
238
+ const status = robotsStatus(robots);
239
+ const found = status === "found" && text !== null;
240
+ const groups = found ? readGroups(text) : null;
241
+ const allows = found ? agentMatcher(robots.url, text) : null;
242
+ const urls = pageUrls.length ? pageUrls : [startUrl];
243
+ const bots = AI_BOTS.map((bot) => {
244
+ const token = productToken(bot.name);
245
+ let robotsGroup = "none";
246
+ let agent = bot.name;
247
+ if (groups?.named.has(token)) {
248
+ robotsGroup = "named";
249
+ }
250
+ else if (token === "applebot" && groups?.named.has("googlebot")) {
251
+ // Apple: without an Applebot group, Applebot follows the Googlebot group.
252
+ robotsGroup = "googlebot";
253
+ agent = "Googlebot";
254
+ }
255
+ else if (groups?.named.has("*")) {
256
+ robotsGroup = "wildcard";
257
+ }
258
+ if (status === "unreachable") {
259
+ return {
260
+ name: bot.name,
261
+ operator: bot.operator,
262
+ purpose: bot.purpose,
263
+ usedFor: bot.usedFor,
264
+ robotsGroup,
265
+ allowed: false,
266
+ blockedPages: urls.length,
267
+ };
268
+ }
269
+ return {
270
+ name: bot.name,
271
+ operator: bot.operator,
272
+ purpose: bot.purpose,
273
+ usedFor: bot.usedFor,
274
+ robotsGroup,
275
+ allowed: allows ? allows(startUrl, agent) : true,
276
+ // A path `User-agent: *` keeps every crawler from is a site-wide choice, which the SEO
277
+ // audit reports; only a page this bot may not fetch while other crawlers may counts here.
278
+ blockedPages: allows
279
+ ? urls.filter((u) => !allows(u, agent) && allows(u, "*")).length
280
+ : 0,
281
+ };
282
+ });
283
+ let trainingPreference = "none";
284
+ // `User-agent: *` never speaks for the policy token, so only a group that names it counts.
285
+ if (allows && groups?.named.has(TRAINING_PREFERENCE_TOKEN)) {
286
+ trainingPreference = allows(startUrl, TRAINING_PREFERENCE_TOKEN)
287
+ ? "allowed"
288
+ : "disallowed";
289
+ }
290
+ return {
291
+ robotsTxt: robots.url,
292
+ status,
293
+ httpStatus: robots.status,
294
+ trainingPreference,
295
+ contentSignals: found ? contentSignals(text) : [],
296
+ bots,
297
+ };
298
+ };
299
+ //# sourceMappingURL=bots.js.map