@aia-matrix/llms-txt-validator 0.0.0-stage → 1.0.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/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +152 -2
- package/THIRD_PARTY_LICENSES.md +204 -0
- package/dist/action.cjs +14153 -0
- package/dist/action.d.ts +1 -0
- package/dist/action.js +35 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +52 -0
- package/dist/discovery.d.ts +7 -0
- package/dist/discovery.js +55 -0
- package/dist/format.d.ts +6 -0
- package/dist/format.js +117 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/markdown-versions.d.ts +2 -0
- package/dist/markdown-versions.js +32 -0
- package/dist/network.d.ts +11 -0
- package/dist/network.js +107 -0
- package/dist/report.d.ts +3 -0
- package/dist/report.js +17 -0
- package/dist/scope.d.ts +2 -0
- package/dist/scope.js +20 -0
- package/dist/spec.d.ts +6 -0
- package/dist/spec.js +6 -0
- package/dist/types.d.ts +27 -0
- package/dist/types.js +1 -0
- package/dist/validator.d.ts +2 -0
- package/dist/validator.js +236 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +2 -0
- package/package.json +54 -4
package/dist/action.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/action.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { appendFile } from "node:fs/promises";
|
|
2
|
+
import { validate } from "./validator.js";
|
|
3
|
+
import { human, exitCode } from "./report.js";
|
|
4
|
+
async function main() {
|
|
5
|
+
const url = process.env.INPUT_URL;
|
|
6
|
+
if (!url)
|
|
7
|
+
throw Error("url input is required");
|
|
8
|
+
const failOn = process.env["INPUT_FAIL-ON"] ?? "error";
|
|
9
|
+
if (failOn !== "warn" && failOn !== "error")
|
|
10
|
+
throw Error("fail-on must be warn or error");
|
|
11
|
+
const numeric = (name, fallback, minimum) => {
|
|
12
|
+
const raw = process.env[`INPUT_${name.toUpperCase()}`] ?? fallback;
|
|
13
|
+
const value = Number(raw);
|
|
14
|
+
if (!/^\d+$/.test(raw) || !Number.isSafeInteger(value) || value < minimum)
|
|
15
|
+
throw Error(`${name} must be ${minimum === 0 ? "a nonnegative" : "a positive"} integer`);
|
|
16
|
+
return value;
|
|
17
|
+
};
|
|
18
|
+
const maxLinks = numeric("max-links", "50", 0);
|
|
19
|
+
const timeout = numeric("timeout", "10000", 1);
|
|
20
|
+
const report = await validate(url, {
|
|
21
|
+
failOn,
|
|
22
|
+
maxLinks,
|
|
23
|
+
timeout,
|
|
24
|
+
});
|
|
25
|
+
console.log(human(report));
|
|
26
|
+
if (process.env.GITHUB_STEP_SUMMARY) {
|
|
27
|
+
const escape = (s) => s.replace(/[&<>]/g, (c) => ({ "&": "&", "<": "<", ">": ">" })[c]);
|
|
28
|
+
await appendFile(process.env.GITHUB_STEP_SUMMARY, `## llms.txt validation\n\n${report.summary.pass} pass · ${report.summary.info} info · ${report.summary.warn} warn · ${report.summary.fail} fail\n\n<pre>${escape(human(report))}</pre>\n`);
|
|
29
|
+
}
|
|
30
|
+
process.exitCode = exitCode(report, failOn);
|
|
31
|
+
}
|
|
32
|
+
main().catch((e) => {
|
|
33
|
+
console.error(`Tool error: ${e instanceof Error ? e.message : String(e)}`);
|
|
34
|
+
process.exitCode = 2;
|
|
35
|
+
});
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
import { validate } from "./validator.js";
|
|
4
|
+
import { human, exitCode } from "./report.js";
|
|
5
|
+
import { VERSION } from "./spec.js";
|
|
6
|
+
let json = process.argv.slice(2).includes("--json");
|
|
7
|
+
try {
|
|
8
|
+
const { values, positionals } = parseArgs({
|
|
9
|
+
allowPositionals: true,
|
|
10
|
+
options: {
|
|
11
|
+
json: { type: "boolean" },
|
|
12
|
+
"max-links": { type: "string" },
|
|
13
|
+
timeout: { type: "string" },
|
|
14
|
+
"fail-on": { type: "string" },
|
|
15
|
+
"no-network": { type: "boolean" },
|
|
16
|
+
help: { type: "boolean", short: "h" },
|
|
17
|
+
version: { type: "boolean", short: "v" },
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
json = values.json ?? false;
|
|
21
|
+
if (values.help) {
|
|
22
|
+
console.log("Usage: llms-txt-validator <url|file> [--json] [--max-links 50] [--timeout 10000] [--fail-on warn|error] [--no-network]");
|
|
23
|
+
}
|
|
24
|
+
else if (values.version) {
|
|
25
|
+
console.log(VERSION);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
if (positionals.length !== 1)
|
|
29
|
+
throw Error("Provide exactly one website URL or local llms.txt file");
|
|
30
|
+
const failOn = values["fail-on"] ?? "error";
|
|
31
|
+
if (failOn !== "warn" && failOn !== "error")
|
|
32
|
+
throw Error("--fail-on must be warn or error");
|
|
33
|
+
const report = await validate(positionals[0], {
|
|
34
|
+
maxLinks: values["max-links"] === undefined ? 50 : Number(values["max-links"]),
|
|
35
|
+
timeout: values.timeout === undefined ? 10000 : Number(values.timeout),
|
|
36
|
+
failOn,
|
|
37
|
+
network: !values["no-network"],
|
|
38
|
+
});
|
|
39
|
+
console.log(json ? JSON.stringify(report, null, 2) : human(report));
|
|
40
|
+
process.exitCode = exitCode(report, failOn);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
if (json)
|
|
45
|
+
console.log(JSON.stringify({
|
|
46
|
+
version: "1.0",
|
|
47
|
+
error: { message: e instanceof Error ? e.message : String(e) },
|
|
48
|
+
}));
|
|
49
|
+
else
|
|
50
|
+
console.error(`Tool error: ${e instanceof Error ? e.message : String(e)}`);
|
|
51
|
+
process.exitCode = 2;
|
|
52
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { parse } from "parse5";
|
|
2
|
+
export function relations(body, header, base) {
|
|
3
|
+
const out = [];
|
|
4
|
+
const add = (href, rel, type) => {
|
|
5
|
+
try {
|
|
6
|
+
out.push({ href: new URL(href, base).href, rel, type });
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
/* Invalid relation is not usable. */
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
for (const match of (header ?? "").matchAll(/<([^>]+)>((?:\s*;\s*[\w-]+\s*=\s*(?:"[^"]*"|[^;,]+))*)/g)) {
|
|
13
|
+
const attrs = new Map();
|
|
14
|
+
for (const a of match[2].matchAll(/;\s*([\w-]+)\s*=\s*(?:"([^"]*)"|([^;,]+))/g))
|
|
15
|
+
attrs.set(a[1].toLowerCase(), (a[2] ?? a[3]).trim());
|
|
16
|
+
add(match[1], attrs.get("rel") ?? "", attrs.get("type"));
|
|
17
|
+
}
|
|
18
|
+
const document = parse(body);
|
|
19
|
+
let effective = base;
|
|
20
|
+
const walk = (node, visit) => {
|
|
21
|
+
const n = node;
|
|
22
|
+
if (n.tagName)
|
|
23
|
+
visit(n.tagName, new Map((n.attrs ?? []).map((a) => [a.name, a.value])));
|
|
24
|
+
for (const child of n.childNodes ?? [])
|
|
25
|
+
walk(child, visit);
|
|
26
|
+
};
|
|
27
|
+
let foundBase = false;
|
|
28
|
+
walk(document, (tag, a) => {
|
|
29
|
+
if (tag === "base" && a.has("href") && !foundBase) {
|
|
30
|
+
try {
|
|
31
|
+
effective = new URL(a.get("href"), base).href;
|
|
32
|
+
foundBase = true;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
/* Ignore invalid base. */
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
walk(document, (tag, a) => {
|
|
40
|
+
if (tag === "link" && a.has("href")) {
|
|
41
|
+
try {
|
|
42
|
+
out.push({
|
|
43
|
+
href: new URL(a.get("href"), effective).href,
|
|
44
|
+
rel: a.get("rel") ?? "",
|
|
45
|
+
type: a.get("type"),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
/* Ignore invalid link. */
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
export const hasRel = (r, name) => r.rel.toLowerCase().split(/\s+/).includes(name);
|
package/dist/format.d.ts
ADDED
package/dist/format.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import MarkdownIt from "markdown-it";
|
|
2
|
+
import { SPEC } from "./spec.js";
|
|
3
|
+
const parser = new MarkdownIt();
|
|
4
|
+
export function checkFormat(input, url) {
|
|
5
|
+
const text = input.replace(/^\uFEFF/, "");
|
|
6
|
+
const results = [];
|
|
7
|
+
const links = [];
|
|
8
|
+
const add = (id, ok, message) => results.push({
|
|
9
|
+
id,
|
|
10
|
+
status: ok ? "pass" : "fail",
|
|
11
|
+
message,
|
|
12
|
+
specRef: SPEC.format,
|
|
13
|
+
url,
|
|
14
|
+
});
|
|
15
|
+
const tokens = parser.parse(text, {});
|
|
16
|
+
const heads = tokens.filter((t) => t.type === "heading_open");
|
|
17
|
+
add("format.h1", heads.filter((t) => t.tag === "h1").length === 1 &&
|
|
18
|
+
tokens[0]?.type === "heading_open" &&
|
|
19
|
+
tokens[0]?.tag === "h1" &&
|
|
20
|
+
!!tokens[1]?.content.trim(), "File must begin with exactly one nonempty H1.");
|
|
21
|
+
add("format.heading-levels", heads.every((t) => t.tag === "h1" || t.tag === "h2"), "Only H1 and H2 headings are allowed.");
|
|
22
|
+
let section = false;
|
|
23
|
+
let count = 0;
|
|
24
|
+
let sectionNumber = 0;
|
|
25
|
+
let sectionBad = false;
|
|
26
|
+
let preambleStarted = false;
|
|
27
|
+
let summarySeen = false;
|
|
28
|
+
const warn = (id, message) => results.push({ id, status: "warn", message, specRef: SPEC.format, url });
|
|
29
|
+
const close = () => {
|
|
30
|
+
if (section)
|
|
31
|
+
add(`format.section.${sectionNumber}`, count > 0 && !sectionBad, "Each H2 section must contain nonempty link-list items.");
|
|
32
|
+
};
|
|
33
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
34
|
+
const t = tokens[i];
|
|
35
|
+
if (t.level !== 0)
|
|
36
|
+
continue;
|
|
37
|
+
if (t.type === "heading_open") {
|
|
38
|
+
if (t.tag === "h2") {
|
|
39
|
+
close();
|
|
40
|
+
section = true;
|
|
41
|
+
count = 0;
|
|
42
|
+
sectionBad = !tokens[i + 1]?.content.trim();
|
|
43
|
+
sectionNumber++;
|
|
44
|
+
}
|
|
45
|
+
i += 2;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (!section) {
|
|
49
|
+
if (t.type === "blockquote_open") {
|
|
50
|
+
if (preambleStarted || summarySeen)
|
|
51
|
+
warn("format.summary-order", "An optional summary should precede the non-heading preamble.");
|
|
52
|
+
else
|
|
53
|
+
add("format.summary-order", true, "Summary precedes the non-heading preamble.");
|
|
54
|
+
summarySeen = true;
|
|
55
|
+
}
|
|
56
|
+
else if (t.type.endsWith("_open") ||
|
|
57
|
+
t.type === "fence" ||
|
|
58
|
+
t.type === "html_block")
|
|
59
|
+
preambleStarted = true;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (t.type === "bullet_list_open" || t.type === "ordered_list_open") {
|
|
63
|
+
const end = tokens.findIndex((x, j) => j > i &&
|
|
64
|
+
x.type === t.type.replace("_open", "_close") &&
|
|
65
|
+
x.level === 0);
|
|
66
|
+
for (let j = i + 1; j < end; j++) {
|
|
67
|
+
if (tokens[j].type !== "list_item_open")
|
|
68
|
+
continue;
|
|
69
|
+
count++;
|
|
70
|
+
const level = tokens[j].level;
|
|
71
|
+
const itemEnd = tokens.findIndex((x, k) => k > j && x.type === "list_item_close" && x.level === level);
|
|
72
|
+
const inlines = tokens
|
|
73
|
+
.slice(j + 1, itemEnd)
|
|
74
|
+
.filter((x) => x.type === "inline");
|
|
75
|
+
const children = inlines.flatMap((x) => x.children ?? []);
|
|
76
|
+
const firstLink = children.find((x) => x.type === "link_open");
|
|
77
|
+
// Preserve links even when the item's arrangement needs a warning.
|
|
78
|
+
for (const child of children.filter((x) => x.type === "link_open")) {
|
|
79
|
+
const href = child.attrGet("href");
|
|
80
|
+
if (href)
|
|
81
|
+
links.push(href);
|
|
82
|
+
}
|
|
83
|
+
if (!firstLink)
|
|
84
|
+
sectionBad = true;
|
|
85
|
+
else if (children[0]?.type !== "link_open")
|
|
86
|
+
warn("format.item-prefix", "A file-list item should start with its link.");
|
|
87
|
+
else {
|
|
88
|
+
const linkEnd = children.findIndex((x) => x.type === "link_close");
|
|
89
|
+
const suffix = children
|
|
90
|
+
.slice(linkEnd + 1)
|
|
91
|
+
.map((x) => x.content)
|
|
92
|
+
.join("")
|
|
93
|
+
.trim();
|
|
94
|
+
if (suffix && !suffix.startsWith(":"))
|
|
95
|
+
warn("format.item-notes", "Notes after a file-list link should start with a colon.");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
i = end;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (!t.type.endsWith("_close"))
|
|
102
|
+
warn("format.section-prose", "H2 sections should contain file lists; additional prose is present.");
|
|
103
|
+
}
|
|
104
|
+
close();
|
|
105
|
+
const bytes = Buffer.byteLength(input);
|
|
106
|
+
const size = bytes >= 1024 ? `${(bytes / 1024).toFixed(1)} KiB` : `${bytes} B`;
|
|
107
|
+
results.push({
|
|
108
|
+
id: "file.size",
|
|
109
|
+
status: bytes > 50 * 1024 ? "warn" : "info",
|
|
110
|
+
message: bytes > 50 * 1024
|
|
111
|
+
? `llms.txt is ${size}, above the 50 KiB guidance; agents may truncate or skip it (not a spec requirement).`
|
|
112
|
+
: `llms.txt is ${size} (guidance: 50 KiB or less; not a spec requirement).`,
|
|
113
|
+
specRef: SPEC.proposal,
|
|
114
|
+
url,
|
|
115
|
+
});
|
|
116
|
+
return { results, links };
|
|
117
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function markdownCandidates(page) {
|
|
2
|
+
const u = new URL(page);
|
|
3
|
+
const path = u.pathname;
|
|
4
|
+
const paths = path.endsWith("/")
|
|
5
|
+
? [path + "index.md", path + "index.html.md"]
|
|
6
|
+
: [path + ".md", path.replace(/\.[^/.]+$/, ".md")];
|
|
7
|
+
return [
|
|
8
|
+
...new Set(paths.map((p) => {
|
|
9
|
+
const v = new URL(u);
|
|
10
|
+
v.pathname = p;
|
|
11
|
+
return v.href;
|
|
12
|
+
})),
|
|
13
|
+
];
|
|
14
|
+
}
|
|
15
|
+
export function pageCandidates(markdown) {
|
|
16
|
+
const u = new URL(markdown);
|
|
17
|
+
if (!u.pathname.endsWith(".md"))
|
|
18
|
+
return [];
|
|
19
|
+
const path = u.pathname.slice(0, -3);
|
|
20
|
+
const paths = path.endsWith("/index")
|
|
21
|
+
? [path.slice(0, -5), path + ".html"]
|
|
22
|
+
: path.endsWith("/index.html")
|
|
23
|
+
? [path.slice(0, -10), path]
|
|
24
|
+
: [path, ...(!/\.[^/]+$/.test(path) ? [path + ".html"] : [])];
|
|
25
|
+
return [
|
|
26
|
+
...new Set(paths.map((p) => {
|
|
27
|
+
const v = new URL(u);
|
|
28
|
+
v.pathname = p;
|
|
29
|
+
return v.href;
|
|
30
|
+
})),
|
|
31
|
+
];
|
|
32
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Resource } from "./types.js";
|
|
2
|
+
export declare function networkMessage(error: unknown, timeout?: number): string;
|
|
3
|
+
export declare class Client {
|
|
4
|
+
private timeout;
|
|
5
|
+
private cache;
|
|
6
|
+
constructor(timeout: number);
|
|
7
|
+
request(url: string, method?: string): Promise<Resource>;
|
|
8
|
+
private fetch;
|
|
9
|
+
resolve(url: string): Promise<Resource>;
|
|
10
|
+
}
|
|
11
|
+
export declare function pooled<T>(items: T[], work: (item: T) => Promise<void>): Promise<void>;
|
package/dist/network.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { VERSION } from "./spec.js";
|
|
2
|
+
export function networkMessage(error, timeout) {
|
|
3
|
+
const e = error;
|
|
4
|
+
if (e?.name === "AbortError" || e?.name === "TimeoutError")
|
|
5
|
+
return timeout ? `timed out after ${timeout} ms` : "timed out";
|
|
6
|
+
const message = e?.message ?? String(error);
|
|
7
|
+
const cause = [e?.cause?.code, e?.cause?.message].filter(Boolean).join(" ");
|
|
8
|
+
return cause ? `${message}: ${cause}` : message;
|
|
9
|
+
}
|
|
10
|
+
export class Client {
|
|
11
|
+
timeout;
|
|
12
|
+
cache = new Map();
|
|
13
|
+
constructor(timeout) {
|
|
14
|
+
this.timeout = timeout;
|
|
15
|
+
}
|
|
16
|
+
async request(url, method = "GET") {
|
|
17
|
+
const key = method + " " + url;
|
|
18
|
+
const existing = this.cache.get(key);
|
|
19
|
+
if (existing)
|
|
20
|
+
return existing;
|
|
21
|
+
const pending = this.fetch(url, method);
|
|
22
|
+
this.cache.set(key, pending);
|
|
23
|
+
return pending;
|
|
24
|
+
}
|
|
25
|
+
async fetch(url, method) {
|
|
26
|
+
const controller = new AbortController();
|
|
27
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
28
|
+
try {
|
|
29
|
+
let current = url;
|
|
30
|
+
const seen = new Set();
|
|
31
|
+
for (let n = 0; n <= 10; n++) {
|
|
32
|
+
if (seen.has(current))
|
|
33
|
+
throw Error("Redirect loop");
|
|
34
|
+
seen.add(current);
|
|
35
|
+
const parsed = new URL(current);
|
|
36
|
+
if (!["http:", "https:"].includes(parsed.protocol))
|
|
37
|
+
throw Error("Only HTTP(S) URLs are supported");
|
|
38
|
+
const response = await fetch(current, {
|
|
39
|
+
method,
|
|
40
|
+
redirect: "manual",
|
|
41
|
+
signal: controller.signal,
|
|
42
|
+
headers: {
|
|
43
|
+
"User-Agent": `aiamatrix-llms-txt-validator/${VERSION} (+https://github.com/aiamatrix/llms-txt-validator)`,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
47
|
+
const location = response.headers.get("location");
|
|
48
|
+
await response.body?.cancel();
|
|
49
|
+
if (!location)
|
|
50
|
+
throw Error("Redirect missing Location");
|
|
51
|
+
current = new URL(location, current).href;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const reader = response.body?.getReader();
|
|
55
|
+
const chunks = [];
|
|
56
|
+
let bytes = 0;
|
|
57
|
+
if (reader)
|
|
58
|
+
while (true) {
|
|
59
|
+
const { done, value } = await reader.read();
|
|
60
|
+
if (done)
|
|
61
|
+
break;
|
|
62
|
+
bytes += value.length;
|
|
63
|
+
if (bytes > 2 * 1024 * 1024) {
|
|
64
|
+
await reader.cancel();
|
|
65
|
+
throw Error("Response exceeds 2 MiB safety limit");
|
|
66
|
+
}
|
|
67
|
+
chunks.push(value);
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
url: current,
|
|
71
|
+
status: response.status,
|
|
72
|
+
headers: response.headers,
|
|
73
|
+
body: Buffer.concat(chunks).toString("utf8"),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
throw Error("More than 10 redirects");
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
throw new Error(controller.signal.aborted
|
|
80
|
+
? `timed out after ${this.timeout} ms`
|
|
81
|
+
: networkMessage(e, this.timeout));
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
clearTimeout(timer);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async resolve(url) {
|
|
88
|
+
try {
|
|
89
|
+
const head = await this.request(url, "HEAD");
|
|
90
|
+
if (head.status === 200)
|
|
91
|
+
return head;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
/* Retry with GET. */
|
|
95
|
+
}
|
|
96
|
+
return this.request(url);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export async function pooled(items, work) {
|
|
100
|
+
let next = 0;
|
|
101
|
+
await Promise.all(Array.from({ length: Math.min(5, items.length) }, async () => {
|
|
102
|
+
while (next < items.length) {
|
|
103
|
+
const item = items[next++];
|
|
104
|
+
await work(item);
|
|
105
|
+
}
|
|
106
|
+
}));
|
|
107
|
+
}
|
package/dist/report.d.ts
ADDED
package/dist/report.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function exitCode(report, failOn = "error") {
|
|
2
|
+
return report.summary.fail > 0 ||
|
|
3
|
+
(failOn === "warn" && report.summary.warn > 0)
|
|
4
|
+
? 1
|
|
5
|
+
: 0;
|
|
6
|
+
}
|
|
7
|
+
export function human(report) {
|
|
8
|
+
const groups = new Map();
|
|
9
|
+
for (const r of report.results) {
|
|
10
|
+
const key = r.id.split(".")[0];
|
|
11
|
+
const group = groups.get(key) ?? [];
|
|
12
|
+
group.push(` ${r.status.toUpperCase()} ${r.id}: ${r.message}\n ${r.url}\n ${r.specRef}`);
|
|
13
|
+
groups.set(key, group);
|
|
14
|
+
}
|
|
15
|
+
return ([...groups].map(([k, v]) => `${k}\n${v.join("\n")}`).join("\n\n") +
|
|
16
|
+
`\n\nSummary: ${report.summary.pass} pass, ${report.summary.info} info, ${report.summary.warn} warn, ${report.summary.fail} fail\nGet a full AI-readiness report: https://aiamatrix.com`);
|
|
17
|
+
}
|
package/dist/scope.d.ts
ADDED
package/dist/scope.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function scopedCandidates(page) {
|
|
2
|
+
const u = new URL(page);
|
|
3
|
+
const dirs = u.pathname
|
|
4
|
+
.slice(0, u.pathname.lastIndexOf("/") + 1)
|
|
5
|
+
.split("/")
|
|
6
|
+
.filter(Boolean);
|
|
7
|
+
const result = [];
|
|
8
|
+
for (let i = dirs.length; i >= 0; i--)
|
|
9
|
+
result.push(new URL("/" + dirs.slice(0, i).join("/") + (i ? "/" : "") + "llms.txt", u.origin).href);
|
|
10
|
+
return result;
|
|
11
|
+
}
|
|
12
|
+
export function applicable(page, indexes) {
|
|
13
|
+
const p = new URL(page);
|
|
14
|
+
return indexes
|
|
15
|
+
.filter((s) => {
|
|
16
|
+
const u = new URL(s);
|
|
17
|
+
return (u.origin === p.origin && p.pathname.startsWith(u.pathname.slice(0, -8)));
|
|
18
|
+
})
|
|
19
|
+
.sort((a, b) => b.length - a.length)[0];
|
|
20
|
+
}
|
package/dist/spec.d.ts
ADDED
package/dist/spec.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type Status = "pass" | "info" | "warn" | "fail";
|
|
2
|
+
export interface Result {
|
|
3
|
+
id: string;
|
|
4
|
+
status: Status;
|
|
5
|
+
message: string;
|
|
6
|
+
specRef: string;
|
|
7
|
+
url: string;
|
|
8
|
+
}
|
|
9
|
+
export interface Report {
|
|
10
|
+
version: "1.0";
|
|
11
|
+
url: string;
|
|
12
|
+
checkedAt: string;
|
|
13
|
+
summary: Record<Status, number>;
|
|
14
|
+
results: Result[];
|
|
15
|
+
}
|
|
16
|
+
export interface Options {
|
|
17
|
+
maxLinks?: number;
|
|
18
|
+
timeout?: number;
|
|
19
|
+
failOn?: "warn" | "error";
|
|
20
|
+
network?: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface Resource {
|
|
23
|
+
url: string;
|
|
24
|
+
status: number;
|
|
25
|
+
headers: Headers;
|
|
26
|
+
body: string;
|
|
27
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|