llmo-checker 0.1.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/LICENSE +21 -0
- package/README.md +142 -0
- package/dist/check.d.ts +5 -0
- package/dist/check.d.ts.map +1 -0
- package/dist/check.js +42 -0
- package/dist/check.js.map +1 -0
- package/dist/checks/canonical.d.ts +3 -0
- package/dist/checks/canonical.d.ts.map +1 -0
- package/dist/checks/canonical.js +50 -0
- package/dist/checks/canonical.js.map +1 -0
- package/dist/checks/jsonld.d.ts +3 -0
- package/dist/checks/jsonld.d.ts.map +1 -0
- package/dist/checks/jsonld.js +96 -0
- package/dist/checks/jsonld.js.map +1 -0
- package/dist/checks/llms-txt.d.ts +3 -0
- package/dist/checks/llms-txt.d.ts.map +1 -0
- package/dist/checks/llms-txt.js +69 -0
- package/dist/checks/llms-txt.js.map +1 -0
- package/dist/checks/meta.d.ts +3 -0
- package/dist/checks/meta.d.ts.map +1 -0
- package/dist/checks/meta.js +72 -0
- package/dist/checks/meta.js.map +1 -0
- package/dist/checks/robots-ai.d.ts +3 -0
- package/dist/checks/robots-ai.d.ts.map +1 -0
- package/dist/checks/robots-ai.js +105 -0
- package/dist/checks/robots-ai.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +66 -0
- package/dist/cli.js.map +1 -0
- package/dist/score.d.ts +3 -0
- package/dist/score.d.ts.map +1 -0
- package/dist/score.js +8 -0
- package/dist/score.js.map +1 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ken Imoto and contributors to the Open LLMO Research Initiative
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/open-llmo/llmo-checker/main/assets/banner.svg" alt="llmo-checker — Lighthouse-style CLI for AI-retrievability scoring. Part of the Open LLMO Research Initiative." width="100%">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# llmo-checker
|
|
6
|
+
|
|
7
|
+
> LLMO Score checker — measures AI-retrieval readiness of a URL.
|
|
8
|
+
> Part of the [Open LLMO Research Initiative](https://llmoframework.com).
|
|
9
|
+
|
|
10
|
+
[](https://github.com/open-llmo/llmo-checker/actions/workflows/ci.yml)
|
|
11
|
+
[](https://www.npmjs.com/package/llmo-checker)
|
|
12
|
+
[](https://www.npmjs.com/package/llmo-checker)
|
|
13
|
+
[](https://nodejs.org)
|
|
14
|
+
[](LICENSE)
|
|
15
|
+
|
|
16
|
+
`llmo-checker` is a Lighthouse-style CLI that scores how "AI-retrievable" a given URL is. It fetches the page, runs a small set of static checks, and returns a JSON report with a single LLMO Score (0-100) plus per-check scores and notes.
|
|
17
|
+
|
|
18
|
+
It is intentionally **not** a full AI-citation simulator. It measures the *substrate* — the structured signals an AI crawler can extract without running an LLM — so that page authors get a fast, reproducible signal before paying for citation simulations.
|
|
19
|
+
|
|
20
|
+
## Status
|
|
21
|
+
|
|
22
|
+
**v0.1 — Draft.** Score weights, check list, and JSON schema may change in v0.2. Pin a specific version if you depend on the JSON shape.
|
|
23
|
+
|
|
24
|
+
## Install / Run
|
|
25
|
+
|
|
26
|
+
> **Note:** The npm publish is pending account verification. Until the npm release lands, install directly from GitHub.
|
|
27
|
+
|
|
28
|
+
Run from GitHub with `npx` (no clone needed):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx github:open-llmo/llmo-checker https://example.com
|
|
32
|
+
npx github:open-llmo/llmo-checker https://example.com --json
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Once published, the same commands work without the `github:` prefix:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx llmo-checker https://example.com # after npm publish
|
|
39
|
+
npx llmo-checker https://example.com --json
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Or clone and run locally:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
git clone https://github.com/open-llmo/llmo-checker.git
|
|
46
|
+
cd llmo-checker
|
|
47
|
+
npm install
|
|
48
|
+
npm run dev https://example.com
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Requires Node.js 20+.
|
|
52
|
+
|
|
53
|
+
## What it checks (v0.1)
|
|
54
|
+
|
|
55
|
+
| Check | Weight | What it measures |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `llms-txt` | 20 | Presence and structure of `/llms.txt` per [llmstxt.org](https://llmstxt.org/) |
|
|
58
|
+
| `robots-ai` | 15 | Explicit posture toward known AI crawlers in `/robots.txt` (GPTBot, ClaudeBot, CCBot, Google-Extended, PerplexityBot, etc.) |
|
|
59
|
+
| `canonical` | 15 | `<link rel="canonical">` correctness and hreflang alternates |
|
|
60
|
+
| `jsonld` | 20 | JSON-LD structured data presence, parseability, and recognized schema.org `@type`s |
|
|
61
|
+
| `meta` | 15 | `<title>` / `<meta name="description">` / OpenGraph / `<h1>` / `<html lang>` |
|
|
62
|
+
|
|
63
|
+
Total weight in v0.1 is **85** (scores normalize to 0-100). Citation Visibility and Chunk Readability are planned for v0.2.
|
|
64
|
+
|
|
65
|
+
## Score bands
|
|
66
|
+
|
|
67
|
+
| Band | Score | Meaning |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| Green | 85-100 | Well-grounded for AI retrieval |
|
|
70
|
+
| Yellow | 65-84 | Needs work — several signals missing or weak |
|
|
71
|
+
| Yellow | 40-64 | Poor — significant grounding gaps |
|
|
72
|
+
| Red | 0-39 | Critical — page is largely invisible to AI crawlers |
|
|
73
|
+
|
|
74
|
+
## JSON output
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npx llmo-checker https://example.com --json
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"url": "https://example.com/",
|
|
83
|
+
"origin": "https://example.com",
|
|
84
|
+
"timestamp": "2026-05-24T10:00:00.000Z",
|
|
85
|
+
"checkerVersion": "0.1.0",
|
|
86
|
+
"scoreVersion": "0.1",
|
|
87
|
+
"score": 72,
|
|
88
|
+
"checks": [
|
|
89
|
+
{
|
|
90
|
+
"id": "llms-txt",
|
|
91
|
+
"name": "llms.txt presence and structure",
|
|
92
|
+
"status": "pass",
|
|
93
|
+
"score": 100,
|
|
94
|
+
"weight": 20,
|
|
95
|
+
"details": { "...": "..." },
|
|
96
|
+
"notes": []
|
|
97
|
+
}
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Each check exits with one of `pass` / `warn` / `fail` / `skip`. The CLI exits with status `0` if the overall score is ≥ 50, `1` otherwise, and `2` on fetch errors.
|
|
103
|
+
|
|
104
|
+
## Score v0.1 indicator set (Draft)
|
|
105
|
+
|
|
106
|
+
These are the indicator categories the v0.1 score covers. Inclusion does not imply causation with downstream AI citation — they are *necessary substrate signals* that have a clear definition.
|
|
107
|
+
|
|
108
|
+
- Citation Visibility — whether AI assistants cite the URL (planned v0.2, requires probing)
|
|
109
|
+
- Chunk Readability — heuristic readability of extracted chunks (planned v0.2)
|
|
110
|
+
- Semantic Structure — JSON-LD, OpenGraph, heading hierarchy (covered by `jsonld` and `meta`)
|
|
111
|
+
- AI Crawlability — robots.txt posture toward known AI bots (covered by `robots-ai`)
|
|
112
|
+
- llms.txt — covered by `llms-txt`
|
|
113
|
+
- Markdown Quality — applies only when the page has a Markdown source (planned v0.2)
|
|
114
|
+
- Entity Clarity — JSON-LD `@type` Organization / Person / Book recognition (partially covered by `jsonld`)
|
|
115
|
+
|
|
116
|
+
The full draft spec lives at <https://llmoframework.com/en/experimental-projects/>.
|
|
117
|
+
|
|
118
|
+
## Development
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
git clone https://github.com/open-llmo/llmo-checker.git
|
|
122
|
+
cd llmo-checker
|
|
123
|
+
npm install
|
|
124
|
+
npm run dev https://example.com
|
|
125
|
+
npm test
|
|
126
|
+
npm run build
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Contributing
|
|
130
|
+
|
|
131
|
+
This is an early Draft. Issues and PRs welcome at <https://github.com/open-llmo/llmo-checker/issues>.
|
|
132
|
+
|
|
133
|
+
If you want to propose a new check, open an issue with:
|
|
134
|
+
- the signal name and what it measures
|
|
135
|
+
- why it predicts AI-retrieval readiness (a paper, a public experiment, or a Lighthouse-style argument)
|
|
136
|
+
- proposed weight and scoring rule
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT — see [LICENSE](LICENSE).
|
|
141
|
+
|
|
142
|
+
Founded and maintained by [Ken Imoto](https://kenimoto.dev) as part of the Open LLMO Research Initiative.
|
package/dist/check.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAgB,aAAa,EAAE,MAAM,YAAY,CAAC;AAE9D,eAAO,MAAM,eAAe,UAAU,CAAC;AACvC,eAAO,MAAM,aAAa,EAAG,KAAc,CAAC;AAE5C,wBAAsB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAqCtE"}
|
package/dist/check.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { checkLlmsTxt } from "./checks/llms-txt.js";
|
|
2
|
+
import { checkRobotsAi } from "./checks/robots-ai.js";
|
|
3
|
+
import { checkCanonical } from "./checks/canonical.js";
|
|
4
|
+
import { checkJsonLd } from "./checks/jsonld.js";
|
|
5
|
+
import { checkMeta } from "./checks/meta.js";
|
|
6
|
+
import { computeScore } from "./score.js";
|
|
7
|
+
export const CHECKER_VERSION = "0.1.0";
|
|
8
|
+
export const SCORE_VERSION = "0.1";
|
|
9
|
+
export async function runChecks(rawUrl) {
|
|
10
|
+
const url = new URL(rawUrl).toString();
|
|
11
|
+
const origin = new URL(rawUrl).origin;
|
|
12
|
+
const res = await fetch(url, {
|
|
13
|
+
headers: {
|
|
14
|
+
"User-Agent": "llmo-checker/0.1.0 (+https://llmoframework.com)",
|
|
15
|
+
Accept: "text/html,application/xhtml+xml",
|
|
16
|
+
},
|
|
17
|
+
redirect: "follow",
|
|
18
|
+
});
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`);
|
|
21
|
+
}
|
|
22
|
+
const html = await res.text();
|
|
23
|
+
const ctx = { url, origin, html, fetch };
|
|
24
|
+
const [llms, robots, canonical, jsonld, meta] = await Promise.all([
|
|
25
|
+
checkLlmsTxt(ctx),
|
|
26
|
+
checkRobotsAi(ctx),
|
|
27
|
+
Promise.resolve(checkCanonical(ctx)),
|
|
28
|
+
Promise.resolve(checkJsonLd(ctx)),
|
|
29
|
+
Promise.resolve(checkMeta(ctx)),
|
|
30
|
+
]);
|
|
31
|
+
const checks = [llms, robots, canonical, jsonld, meta];
|
|
32
|
+
return {
|
|
33
|
+
url,
|
|
34
|
+
origin,
|
|
35
|
+
timestamp: new Date().toISOString(),
|
|
36
|
+
checkerVersion: CHECKER_VERSION,
|
|
37
|
+
scoreVersion: SCORE_VERSION,
|
|
38
|
+
score: computeScore(checks),
|
|
39
|
+
checks,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=check.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"check.js","sourceRoot":"","sources":["../src/check.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG1C,MAAM,CAAC,MAAM,eAAe,GAAG,OAAO,CAAC;AACvC,MAAM,CAAC,MAAM,aAAa,GAAG,KAAc,CAAC;AAE5C,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,MAAc;IAC5C,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;IACvC,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC;IAEtC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;QAC3B,OAAO,EAAE;YACP,YAAY,EAAE,iDAAiD;YAC/D,MAAM,EAAE,iCAAiC;SAC1C;QACD,QAAQ,EAAE,QAAQ;KACnB,CAAC,CAAC;IACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,mBAAmB,GAAG,UAAU,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;IAChE,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;IAE9B,MAAM,GAAG,GAAiB,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IAEvD,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAChE,YAAY,CAAC,GAAG,CAAC;QACjB,aAAa,CAAC,GAAG,CAAC;QAClB,OAAO,CAAC,OAAO,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC;QACpC,OAAO,CAAC,OAAO,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QACjC,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;KAChC,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,CAAC;IAEvD,OAAO;QACL,GAAG;QACH,MAAM;QACN,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;QACnC,cAAc,EAAE,eAAe;QAC/B,YAAY,EAAE,aAAa;QAC3B,KAAK,EAAE,YAAY,CAAC,MAAM,CAAC;QAC3B,MAAM;KACP,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canonical.d.ts","sourceRoot":"","sources":["../../src/checks/canonical.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE7D,wBAAgB,cAAc,CAAC,GAAG,EAAE,YAAY,GAAG,WAAW,CA2C7D"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as cheerio from "cheerio";
|
|
2
|
+
export function checkCanonical(ctx) {
|
|
3
|
+
const $ = cheerio.load(ctx.html);
|
|
4
|
+
const notes = [];
|
|
5
|
+
const details = {};
|
|
6
|
+
const canonical = $('link[rel="canonical"]').attr("href")?.trim();
|
|
7
|
+
details.canonical = canonical ?? null;
|
|
8
|
+
if (!canonical) {
|
|
9
|
+
notes.push("Missing <link rel=\"canonical\">. AI crawlers may dedupe wrong URL.");
|
|
10
|
+
return result("fail", 0, details, notes);
|
|
11
|
+
}
|
|
12
|
+
let absolute;
|
|
13
|
+
try {
|
|
14
|
+
absolute = new URL(canonical, ctx.url);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
notes.push(`Canonical href is not a valid URL: ${canonical}`);
|
|
18
|
+
return result("fail", 20, details, notes);
|
|
19
|
+
}
|
|
20
|
+
details.canonicalAbsolute = absolute.toString();
|
|
21
|
+
const here = new URL(ctx.url);
|
|
22
|
+
const matchesOrigin = absolute.origin === here.origin;
|
|
23
|
+
details.matchesOrigin = matchesOrigin;
|
|
24
|
+
if (!matchesOrigin) {
|
|
25
|
+
notes.push(`Canonical points to a different origin (${absolute.origin}). Intentional only if this page is a republished mirror.`);
|
|
26
|
+
return result("warn", 60, details, notes);
|
|
27
|
+
}
|
|
28
|
+
const hreflang = $('link[rel="alternate"][hreflang]')
|
|
29
|
+
.map((_, el) => $(el).attr("hreflang"))
|
|
30
|
+
.get();
|
|
31
|
+
details.hreflangCount = hreflang.length;
|
|
32
|
+
let score = 90;
|
|
33
|
+
if (hreflang.length > 0)
|
|
34
|
+
score += 10;
|
|
35
|
+
else
|
|
36
|
+
notes.push("No hreflang alternates — fine for single-language sites.");
|
|
37
|
+
return result("pass", Math.min(score, 100), details, notes);
|
|
38
|
+
}
|
|
39
|
+
function result(status, score, details, notes) {
|
|
40
|
+
return {
|
|
41
|
+
id: "canonical",
|
|
42
|
+
name: "Canonical URL and hreflang",
|
|
43
|
+
status,
|
|
44
|
+
score,
|
|
45
|
+
weight: 15,
|
|
46
|
+
details,
|
|
47
|
+
notes,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=canonical.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"canonical.js","sourceRoot":"","sources":["../../src/checks/canonical.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,OAAO,MAAM,SAAS,CAAC;AAGnC,MAAM,UAAU,cAAc,CAAC,GAAiB;IAC9C,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACjC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,OAAO,GAA4B,EAAE,CAAC;IAE5C,MAAM,SAAS,GAAG,CAAC,CAAC,uBAAuB,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC;IAClE,OAAO,CAAC,SAAS,GAAG,SAAS,IAAI,IAAI,CAAC;IAEtC,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,qEAAqE,CAAC,CAAC;QAClF,OAAO,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;IAC3C,CAAC;IAED,IAAI,QAAa,CAAC;IAClB,IAAI,CAAC;QACH,QAAQ,GAAG,IAAI,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,KAAK,CAAC,IAAI,CAAC,sCAAsC,SAAS,EAAE,CAAC,CAAC;QAC9D,OAAO,MAAM,CAAC,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;IAC5C,CAAC;IACD,OAAO,CAAC,iBAAiB,GAAG,QAAQ,CAAC,QAAQ,EAAE,CAAC;IAEhD,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC9B,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,CAAC;IACtD,OAAO,CAAC,aAAa,GAAG,aAAa,CAAC;IAEtC,IAAI,CAAC,aAAa,EAAE,CAAC;QACnB,KAAK,CAAC,IAAI,CACR,2CAA2C,QAAQ,CAAC,MAAM,2DAA2D,CACtH,CAAC;QACF,OAAO,MAAM,CAAC,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;IAC5C,CAAC;IAED,MAAM,QAAQ,GAAG,CAAC,CAAC,iCAAiC,CAAC;SAClD,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;SACtC,GAAG,EAAE,CAAC;IACT,OAAO,CAAC,aAAa,GAAG,QAAQ,CAAC,MAAM,CAAC;IAExC,IAAI,KAAK,GAAG,EAAE,CAAC;IACf,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,KAAK,IAAI,EAAE,CAAC;;QAChC,KAAK,CAAC,IAAI,CAAC,0DAA0D,CAAC,CAAC;IAE5E,OAAO,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;AAC9D,CAAC;AAED,SAAS,MAAM,CACb,MAAgC,EAChC,KAAa,EACb,OAAgC,EAChC,KAAe;IAEf,OAAO;QACL,EAAE,EAAE,WAAW;QACf,IAAI,EAAE,4BAA4B;QAClC,MAAM;QACN,KAAK;QACL,MAAM,EAAE,EAAE;QACV,OAAO;QACP,KAAK;KACN,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsonld.d.ts","sourceRoot":"","sources":["../../src/checks/jsonld.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAkB7D,wBAAgB,WAAW,CAAC,GAAG,EAAE,YAAY,GAAG,WAAW,CAgD1D"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as cheerio from "cheerio";
|
|
2
|
+
const ENTITY_TYPES = [
|
|
3
|
+
"Organization",
|
|
4
|
+
"Person",
|
|
5
|
+
"Article",
|
|
6
|
+
"BlogPosting",
|
|
7
|
+
"TechArticle",
|
|
8
|
+
"Book",
|
|
9
|
+
"WebSite",
|
|
10
|
+
"WebPage",
|
|
11
|
+
"BreadcrumbList",
|
|
12
|
+
"FAQPage",
|
|
13
|
+
"HowTo",
|
|
14
|
+
"Product",
|
|
15
|
+
"SoftwareApplication",
|
|
16
|
+
];
|
|
17
|
+
export function checkJsonLd(ctx) {
|
|
18
|
+
const $ = cheerio.load(ctx.html);
|
|
19
|
+
const notes = [];
|
|
20
|
+
const blocks = [];
|
|
21
|
+
const types = new Set();
|
|
22
|
+
let parseErrors = 0;
|
|
23
|
+
$('script[type="application/ld+json"]').each((_, el) => {
|
|
24
|
+
const raw = $(el).contents().text();
|
|
25
|
+
if (!raw.trim())
|
|
26
|
+
return;
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(raw);
|
|
29
|
+
blocks.push(parsed);
|
|
30
|
+
collectTypes(parsed, types);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
parseErrors += 1;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
const details = {
|
|
37
|
+
blockCount: blocks.length,
|
|
38
|
+
typesFound: [...types],
|
|
39
|
+
parseErrors,
|
|
40
|
+
};
|
|
41
|
+
if (blocks.length === 0) {
|
|
42
|
+
notes.push("No JSON-LD found. Entity grounding will rely on text alone.");
|
|
43
|
+
return result("fail", 0, details, notes);
|
|
44
|
+
}
|
|
45
|
+
if (parseErrors > 0) {
|
|
46
|
+
notes.push(`${parseErrors} JSON-LD block(s) failed to parse.`);
|
|
47
|
+
}
|
|
48
|
+
const recognized = [...types].filter((t) => ENTITY_TYPES.includes(t));
|
|
49
|
+
details.recognizedTypes = recognized;
|
|
50
|
+
let score = 50;
|
|
51
|
+
score += Math.min(recognized.length * 12, 36);
|
|
52
|
+
if (types.has("Organization") || types.has("Person"))
|
|
53
|
+
score += 8;
|
|
54
|
+
if (parseErrors > 0)
|
|
55
|
+
score -= 20;
|
|
56
|
+
score = Math.max(0, Math.min(100, score));
|
|
57
|
+
const status = score >= 85 ? "pass" : score >= 50 ? "warn" : "fail";
|
|
58
|
+
if (recognized.length === 0) {
|
|
59
|
+
notes.push("JSON-LD present but no recognized schema.org entity types.");
|
|
60
|
+
}
|
|
61
|
+
return result(status, score, details, notes);
|
|
62
|
+
}
|
|
63
|
+
function collectTypes(node, out) {
|
|
64
|
+
if (!node)
|
|
65
|
+
return;
|
|
66
|
+
if (Array.isArray(node)) {
|
|
67
|
+
for (const item of node)
|
|
68
|
+
collectTypes(item, out);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (typeof node !== "object")
|
|
72
|
+
return;
|
|
73
|
+
const obj = node;
|
|
74
|
+
const t = obj["@type"];
|
|
75
|
+
if (typeof t === "string")
|
|
76
|
+
out.add(t);
|
|
77
|
+
else if (Array.isArray(t)) {
|
|
78
|
+
for (const sub of t)
|
|
79
|
+
if (typeof sub === "string")
|
|
80
|
+
out.add(sub);
|
|
81
|
+
}
|
|
82
|
+
if ("@graph" in obj)
|
|
83
|
+
collectTypes(obj["@graph"], out);
|
|
84
|
+
}
|
|
85
|
+
function result(status, score, details, notes) {
|
|
86
|
+
return {
|
|
87
|
+
id: "jsonld",
|
|
88
|
+
name: "JSON-LD structured data",
|
|
89
|
+
status,
|
|
90
|
+
score,
|
|
91
|
+
weight: 20,
|
|
92
|
+
details,
|
|
93
|
+
notes,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=jsonld.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsonld.js","sourceRoot":"","sources":["../../src/checks/jsonld.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"llms-txt.d.ts","sourceRoot":"","sources":["../../src/checks/llms-txt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE7D,wBAAsB,YAAY,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CA4D1E"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export async function checkLlmsTxt(ctx) {
|
|
2
|
+
const url = `${ctx.origin}/llms.txt`;
|
|
3
|
+
const notes = [];
|
|
4
|
+
const details = { url };
|
|
5
|
+
try {
|
|
6
|
+
const res = await ctx.fetch(url, {
|
|
7
|
+
headers: { "User-Agent": "llmo-checker/0.1.0 (+https://llmoframework.com)" },
|
|
8
|
+
});
|
|
9
|
+
details.httpStatus = res.status;
|
|
10
|
+
if (res.status === 404) {
|
|
11
|
+
notes.push("No /llms.txt found. See https://llmstxt.org/ for the spec.");
|
|
12
|
+
return result("fail", 0, details, notes);
|
|
13
|
+
}
|
|
14
|
+
if (!res.ok) {
|
|
15
|
+
notes.push(`/llms.txt returned HTTP ${res.status}`);
|
|
16
|
+
return result("fail", 0, details, notes);
|
|
17
|
+
}
|
|
18
|
+
const body = await res.text();
|
|
19
|
+
const trimmed = body.trim();
|
|
20
|
+
details.byteLength = body.length;
|
|
21
|
+
details.lineCount = trimmed.split("\n").length;
|
|
22
|
+
if (trimmed.length === 0) {
|
|
23
|
+
notes.push("/llms.txt exists but is empty.");
|
|
24
|
+
return result("fail", 10, details, notes);
|
|
25
|
+
}
|
|
26
|
+
const lines = trimmed.split("\n");
|
|
27
|
+
const h1 = lines.find((l) => l.startsWith("# "));
|
|
28
|
+
const linkCount = (body.match(/^- \[/gm) ?? []).length;
|
|
29
|
+
const sectionCount = (body.match(/^## /gm) ?? []).length;
|
|
30
|
+
details.hasH1Title = Boolean(h1);
|
|
31
|
+
details.linkCount = linkCount;
|
|
32
|
+
details.sectionCount = sectionCount;
|
|
33
|
+
let score = 60;
|
|
34
|
+
if (h1)
|
|
35
|
+
score += 15;
|
|
36
|
+
else
|
|
37
|
+
notes.push("Missing top-level `# Title` line.");
|
|
38
|
+
if (sectionCount > 0)
|
|
39
|
+
score += 10;
|
|
40
|
+
else
|
|
41
|
+
notes.push("No `## Section` headings found — links should be grouped.");
|
|
42
|
+
if (linkCount >= 3)
|
|
43
|
+
score += 15;
|
|
44
|
+
else if (linkCount > 0)
|
|
45
|
+
score += 8;
|
|
46
|
+
else {
|
|
47
|
+
score += 5;
|
|
48
|
+
notes.push("No `- [Title](url)` link entries. Optional per spec, but link lists help retrieval.");
|
|
49
|
+
}
|
|
50
|
+
const status = score >= 85 ? "pass" : score >= 60 ? "warn" : "fail";
|
|
51
|
+
return result(status, score, details, notes);
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
notes.push(`Fetch error: ${err.message}`);
|
|
55
|
+
return result("fail", 0, details, notes);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function result(status, score, details, notes) {
|
|
59
|
+
return {
|
|
60
|
+
id: "llms-txt",
|
|
61
|
+
name: "llms.txt presence and structure",
|
|
62
|
+
status,
|
|
63
|
+
score,
|
|
64
|
+
weight: 20,
|
|
65
|
+
details,
|
|
66
|
+
notes,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=llms-txt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"llms-txt.js","sourceRoot":"","sources":["../../src/checks/llms-txt.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta.d.ts","sourceRoot":"","sources":["../../src/checks/meta.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE7D,wBAAgB,SAAS,CAAC,GAAG,EAAE,YAAY,GAAG,WAAW,CAsExD"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as cheerio from "cheerio";
|
|
2
|
+
export function checkMeta(ctx) {
|
|
3
|
+
const $ = cheerio.load(ctx.html);
|
|
4
|
+
const notes = [];
|
|
5
|
+
const title = $("head > title").first().text().trim();
|
|
6
|
+
const description = $('meta[name="description"]').attr("content")?.trim() ?? "";
|
|
7
|
+
const ogTitle = $('meta[property="og:title"]').attr("content")?.trim() ?? "";
|
|
8
|
+
const ogDescription = $('meta[property="og:description"]').attr("content")?.trim() ?? "";
|
|
9
|
+
const ogType = $('meta[property="og:type"]').attr("content")?.trim() ?? "";
|
|
10
|
+
const h1Count = $("h1").length;
|
|
11
|
+
const lang = $("html").attr("lang")?.trim() ?? "";
|
|
12
|
+
const details = {
|
|
13
|
+
title,
|
|
14
|
+
titleLength: title.length,
|
|
15
|
+
description,
|
|
16
|
+
descriptionLength: description.length,
|
|
17
|
+
ogTitle,
|
|
18
|
+
ogDescription,
|
|
19
|
+
ogType,
|
|
20
|
+
h1Count,
|
|
21
|
+
htmlLang: lang,
|
|
22
|
+
};
|
|
23
|
+
let score = 0;
|
|
24
|
+
if (title) {
|
|
25
|
+
if (title.length >= 20 && title.length <= 70)
|
|
26
|
+
score += 20;
|
|
27
|
+
else {
|
|
28
|
+
score += 10;
|
|
29
|
+
notes.push(`Title length ${title.length} is outside the 20-70 sweet spot.`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
notes.push("Missing <title>.");
|
|
34
|
+
}
|
|
35
|
+
if (description) {
|
|
36
|
+
if (description.length >= 80 && description.length <= 200)
|
|
37
|
+
score += 20;
|
|
38
|
+
else {
|
|
39
|
+
score += 10;
|
|
40
|
+
notes.push(`Description length ${description.length} is outside the 80-200 sweet spot.`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
notes.push("Missing <meta name=\"description\">.");
|
|
45
|
+
}
|
|
46
|
+
if (ogTitle && ogDescription)
|
|
47
|
+
score += 20;
|
|
48
|
+
else
|
|
49
|
+
notes.push("Missing OpenGraph title/description.");
|
|
50
|
+
if (ogType)
|
|
51
|
+
score += 10;
|
|
52
|
+
if (h1Count === 1)
|
|
53
|
+
score += 20;
|
|
54
|
+
else
|
|
55
|
+
notes.push(`Found ${h1Count} <h1> elements (recommended: exactly 1).`);
|
|
56
|
+
if (lang)
|
|
57
|
+
score += 10;
|
|
58
|
+
else
|
|
59
|
+
notes.push("Missing <html lang=\"...\"> attribute.");
|
|
60
|
+
score = Math.min(score, 100);
|
|
61
|
+
const status = score >= 85 ? "pass" : score >= 60 ? "warn" : "fail";
|
|
62
|
+
return {
|
|
63
|
+
id: "meta",
|
|
64
|
+
name: "Semantic metadata and headings",
|
|
65
|
+
status,
|
|
66
|
+
score,
|
|
67
|
+
weight: 15,
|
|
68
|
+
details,
|
|
69
|
+
notes,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=meta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta.js","sourceRoot":"","sources":["../../src/checks/meta.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"robots-ai.d.ts","sourceRoot":"","sources":["../../src/checks/robots-ai.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAgB7D,wBAAsB,aAAa,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC,CAwD3E"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const AI_BOTS = [
|
|
2
|
+
"GPTBot",
|
|
3
|
+
"ChatGPT-User",
|
|
4
|
+
"OAI-SearchBot",
|
|
5
|
+
"ClaudeBot",
|
|
6
|
+
"Claude-Web",
|
|
7
|
+
"anthropic-ai",
|
|
8
|
+
"CCBot",
|
|
9
|
+
"Google-Extended",
|
|
10
|
+
"PerplexityBot",
|
|
11
|
+
"Applebot-Extended",
|
|
12
|
+
"cohere-ai",
|
|
13
|
+
];
|
|
14
|
+
export async function checkRobotsAi(ctx) {
|
|
15
|
+
const url = `${ctx.origin}/robots.txt`;
|
|
16
|
+
const notes = [];
|
|
17
|
+
const details = { url, knownAiBots: AI_BOTS };
|
|
18
|
+
try {
|
|
19
|
+
const res = await ctx.fetch(url, {
|
|
20
|
+
headers: { "User-Agent": "llmo-checker/0.1.0 (+https://llmoframework.com)" },
|
|
21
|
+
});
|
|
22
|
+
details.httpStatus = res.status;
|
|
23
|
+
if (res.status === 404) {
|
|
24
|
+
notes.push("No /robots.txt found. AI crawlers will use default allow, but explicit posture is recommended.");
|
|
25
|
+
return result("warn", 60, details, notes);
|
|
26
|
+
}
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
notes.push(`/robots.txt returned HTTP ${res.status}`);
|
|
29
|
+
return result("fail", 0, details, notes);
|
|
30
|
+
}
|
|
31
|
+
const body = await res.text();
|
|
32
|
+
details.byteLength = body.length;
|
|
33
|
+
const mentioned = [];
|
|
34
|
+
const disallowed = [];
|
|
35
|
+
const groups = parseRobotsGroups(body);
|
|
36
|
+
for (const bot of AI_BOTS) {
|
|
37
|
+
const lower = bot.toLowerCase();
|
|
38
|
+
const group = groups.find((g) => g.userAgents.some((u) => u.toLowerCase() === lower));
|
|
39
|
+
if (!group)
|
|
40
|
+
continue;
|
|
41
|
+
mentioned.push(bot);
|
|
42
|
+
if (group.disallows.some((d) => d.trim() === "/")) {
|
|
43
|
+
disallowed.push(bot);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
details.mentionedBots = mentioned;
|
|
47
|
+
details.disallowedBots = disallowed;
|
|
48
|
+
details.hasWildcardUserAgent = groups.some((g) => g.userAgents.includes("*"));
|
|
49
|
+
let score = 70;
|
|
50
|
+
if (mentioned.length >= 3)
|
|
51
|
+
score += 20;
|
|
52
|
+
else if (mentioned.length > 0)
|
|
53
|
+
score += 10;
|
|
54
|
+
else
|
|
55
|
+
notes.push("No explicit AI-bot rules in robots.txt.");
|
|
56
|
+
if (groups.length > 0 && details.hasWildcardUserAgent)
|
|
57
|
+
score += 10;
|
|
58
|
+
const status = score >= 85 ? "pass" : score >= 60 ? "warn" : "fail";
|
|
59
|
+
return result(status, Math.min(score, 100), details, notes);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
notes.push(`Fetch error: ${err.message}`);
|
|
63
|
+
return result("fail", 0, details, notes);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function parseRobotsGroups(body) {
|
|
67
|
+
const groups = [];
|
|
68
|
+
let current = null;
|
|
69
|
+
for (const rawLine of body.split("\n")) {
|
|
70
|
+
const line = rawLine.replace(/#.*$/, "").trim();
|
|
71
|
+
if (!line)
|
|
72
|
+
continue;
|
|
73
|
+
const idx = line.indexOf(":");
|
|
74
|
+
if (idx < 0)
|
|
75
|
+
continue;
|
|
76
|
+
const key = line.slice(0, idx).trim().toLowerCase();
|
|
77
|
+
const value = line.slice(idx + 1).trim();
|
|
78
|
+
if (key === "user-agent") {
|
|
79
|
+
if (!current || current.disallows.length > 0 || current.allows.length > 0) {
|
|
80
|
+
current = { userAgents: [], disallows: [], allows: [] };
|
|
81
|
+
groups.push(current);
|
|
82
|
+
}
|
|
83
|
+
current.userAgents.push(value);
|
|
84
|
+
}
|
|
85
|
+
else if (key === "disallow" && current) {
|
|
86
|
+
current.disallows.push(value);
|
|
87
|
+
}
|
|
88
|
+
else if (key === "allow" && current) {
|
|
89
|
+
current.allows.push(value);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return groups;
|
|
93
|
+
}
|
|
94
|
+
function result(status, score, details, notes) {
|
|
95
|
+
return {
|
|
96
|
+
id: "robots-ai",
|
|
97
|
+
name: "AI crawler posture (robots.txt)",
|
|
98
|
+
status,
|
|
99
|
+
score,
|
|
100
|
+
weight: 15,
|
|
101
|
+
details,
|
|
102
|
+
notes,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=robots-ai.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"robots-ai.js","sourceRoot":"","sources":["../../src/checks/robots-ai.ts"],"names":[],"mappings":"AAEA,MAAM,OAAO,GAAG;IACd,QAAQ;IACR,cAAc;IACd,eAAe;IACf,WAAW;IACX,YAAY;IACZ,cAAc;IACd,OAAO;IACP,iBAAiB;IACjB,eAAe;IACf,mBAAmB;IACnB,WAAW;CACH,CAAC;AAEX,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,GAAiB;IACnD,MAAM,GAAG,GAAG,GAAG,GAAG,CAAC,MAAM,aAAa,CAAC;IACvC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,OAAO,GAA4B,EAAE,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC;IAEvE,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,KAAK,CAAC,GAAG,EAAE;YAC/B,OAAO,EAAE,EAAE,YAAY,EAAE,iDAAiD,EAAE;SAC7E,CAAC,CAAC;QACH,OAAO,CAAC,UAAU,GAAG,GAAG,CAAC,MAAM,CAAC;QAEhC,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YACvB,KAAK,CAAC,IAAI,CACR,gGAAgG,CACjG,CAAC;YACF,OAAO,MAAM,CAAC,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;QAC5C,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,KAAK,CAAC,IAAI,CAAC,6BAA6B,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;YACtD,OAAO,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;QAC3C,CAAC;QAED,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QAC9B,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC;QAEjC,MAAM,SAAS,GAAa,EAAE,CAAC;QAC/B,MAAM,UAAU,GAAa,EAAE,CAAC;QAEhC,MAAM,MAAM,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;QACvC,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;YAC1B,MAAM,KAAK,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC;YAChC,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,KAAK,KAAK,CAAC,CAAC,CAAC;YACtF,IAAI,CAAC,KAAK;gBAAE,SAAS;YACrB,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACpB,IAAI,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,GAAG,CAAC,EAAE,CAAC;gBAClD,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACvB,CAAC;QACH,CAAC;QAED,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC;QAClC,OAAO,CAAC,cAAc,GAAG,UAAU,CAAC;QACpC,OAAO,CAAC,oBAAoB,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;QAE9E,IAAI,KAAK,GAAG,EAAE,CAAC;QACf,IAAI,SAAS,CAAC,MAAM,IAAI,CAAC;YAAE,KAAK,IAAI,EAAE,CAAC;aAClC,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC;YAAE,KAAK,IAAI,EAAE,CAAC;;YACtC,KAAK,CAAC,IAAI,CAAC,yCAAyC,CAAC,CAAC;QAE3D,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,CAAC,oBAAoB;YAAE,KAAK,IAAI,EAAE,CAAC;QAEnE,MAAM,MAAM,GAAG,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;QACpE,OAAO,MAAM,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;IAC9D,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,KAAK,CAAC,IAAI,CAAC,gBAAiB,GAAa,CAAC,OAAO,EAAE,CAAC,CAAC;QACrD,OAAO,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;IAC3C,CAAC;AACH,CAAC;AAQD,SAAS,iBAAiB,CAAC,IAAY;IACrC,MAAM,MAAM,GAAkB,EAAE,CAAC;IACjC,IAAI,OAAO,GAAuB,IAAI,CAAC;IAEvC,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAChD,IAAI,CAAC,IAAI;YAAE,SAAS;QACpB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC9B,IAAI,GAAG,GAAG,CAAC;YAAE,SAAS;QACtB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACpD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAEzC,IAAI,GAAG,KAAK,YAAY,EAAE,CAAC;YACzB,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC1E,OAAO,GAAG,EAAE,UAAU,EAAE,EAAE,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;gBACxD,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACvB,CAAC;YACD,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACjC,CAAC;aAAM,IAAI,GAAG,KAAK,UAAU,IAAI,OAAO,EAAE,CAAC;YACzC,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAChC,CAAC;aAAM,IAAI,GAAG,KAAK,OAAO,IAAI,OAAO,EAAE,CAAC;YACtC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,MAAM,CACb,MAAgC,EAChC,KAAa,EACb,OAAgC,EAChC,KAAe;IAEf,OAAO;QACL,EAAE,EAAE,WAAW;QACf,IAAI,EAAE,iCAAiC;QACvC,MAAM;QACN,KAAK;QACL,MAAM,EAAE,EAAE;QACV,OAAO;QACP,KAAK;KACN,CAAC;AACJ,CAAC"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { CHECKER_VERSION, runChecks } from "./check.js";
|
|
5
|
+
const program = new Command();
|
|
6
|
+
program
|
|
7
|
+
.name("llmo-checker")
|
|
8
|
+
.description("LLMO Score checker — measures AI-retrieval readiness of a URL.\n" +
|
|
9
|
+
"Part of the Open LLMO Research Initiative (https://llmoframework.com).")
|
|
10
|
+
.version(CHECKER_VERSION)
|
|
11
|
+
.argument("<url>", "URL to check (must include https://)")
|
|
12
|
+
.option("--json", "output JSON report only (no pretty print)")
|
|
13
|
+
.action(async (rawUrl, options) => {
|
|
14
|
+
try {
|
|
15
|
+
const report = await runChecks(rawUrl);
|
|
16
|
+
if (options.json) {
|
|
17
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
printPretty(report);
|
|
21
|
+
}
|
|
22
|
+
process.exit(report.score >= 50 ? 0 : 1);
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
console.error(pc.red(`Error: ${err.message}`));
|
|
26
|
+
process.exit(2);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
program.parseAsync(process.argv);
|
|
30
|
+
function printPretty(report) {
|
|
31
|
+
const out = process.stdout;
|
|
32
|
+
const band = scoreBand(report.score);
|
|
33
|
+
out.write(`\n${pc.bold("LLMO Score")} ${band.color(`${report.score}/100`)} ${pc.dim(band.label)}\n`);
|
|
34
|
+
out.write(`${pc.dim(`url: ${report.url}`)}\n`);
|
|
35
|
+
out.write(`${pc.dim(`checker: v${report.checkerVersion} score: v${report.scoreVersion}`)}\n\n`);
|
|
36
|
+
for (const c of report.checks) {
|
|
37
|
+
out.write(`${statusBadge(c)} ${pc.bold(c.name)} ${pc.dim(`${c.score}/100 · weight ${c.weight}`)}\n`);
|
|
38
|
+
for (const note of c.notes) {
|
|
39
|
+
out.write(` ${pc.dim("·")} ${note}\n`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
out.write("\n");
|
|
43
|
+
out.write(`${pc.dim("Full audit (measured AI-citation rates, GA4/GSC analysis, prioritized roadmap): https://propel-lab.co.jp/llmo-audit")}\n\n`);
|
|
44
|
+
}
|
|
45
|
+
function statusBadge(c) {
|
|
46
|
+
switch (c.status) {
|
|
47
|
+
case "pass":
|
|
48
|
+
return pc.green("PASS");
|
|
49
|
+
case "warn":
|
|
50
|
+
return pc.yellow("WARN");
|
|
51
|
+
case "fail":
|
|
52
|
+
return pc.red("FAIL");
|
|
53
|
+
default:
|
|
54
|
+
return pc.dim("SKIP");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function scoreBand(score) {
|
|
58
|
+
if (score >= 85)
|
|
59
|
+
return { color: pc.green, label: "well-grounded" };
|
|
60
|
+
if (score >= 65)
|
|
61
|
+
return { color: pc.yellow, label: "needs work" };
|
|
62
|
+
if (score >= 40)
|
|
63
|
+
return { color: pc.yellow, label: "poor" };
|
|
64
|
+
return { color: pc.red, label: "critical" };
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/score.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"score.d.ts","sourceRoot":"","sources":["../src/score.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,MAAM,CAK1D"}
|
package/dist/score.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function computeScore(checks) {
|
|
2
|
+
const totalWeight = checks.reduce((sum, c) => sum + c.weight, 0);
|
|
3
|
+
if (totalWeight === 0)
|
|
4
|
+
return 0;
|
|
5
|
+
const weighted = checks.reduce((sum, c) => sum + c.score * c.weight, 0);
|
|
6
|
+
return Math.round(weighted / totalWeight);
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=score.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"score.js","sourceRoot":"","sources":["../src/score.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,YAAY,CAAC,MAAqB;IAChD,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IACjE,IAAI,WAAW,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IAChC,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IACxE,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,GAAG,WAAW,CAAC,CAAC;AAC5C,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type CheckStatus = "pass" | "warn" | "fail" | "skip";
|
|
2
|
+
export interface CheckResult {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
status: CheckStatus;
|
|
6
|
+
score: number;
|
|
7
|
+
weight: number;
|
|
8
|
+
details: Record<string, unknown>;
|
|
9
|
+
notes: string[];
|
|
10
|
+
}
|
|
11
|
+
export interface CheckerReport {
|
|
12
|
+
url: string;
|
|
13
|
+
origin: string;
|
|
14
|
+
timestamp: string;
|
|
15
|
+
checkerVersion: string;
|
|
16
|
+
score: number;
|
|
17
|
+
scoreVersion: "0.1";
|
|
18
|
+
checks: CheckResult[];
|
|
19
|
+
}
|
|
20
|
+
export interface CheckContext {
|
|
21
|
+
url: string;
|
|
22
|
+
origin: string;
|
|
23
|
+
html: string;
|
|
24
|
+
fetch: typeof fetch;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAE5D,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,KAAK,CAAC;IACpB,MAAM,EAAE,WAAW,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,KAAK,CAAC;CACrB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "llmo-checker",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "LLMO Score checker — measures AI-retrieval readiness of a URL. Part of the Open LLMO Research Initiative.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"llmo-checker": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/check.js",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./dist/check.js",
|
|
12
|
+
"./cli": "./dist/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"dev": "tsx src/cli.ts",
|
|
22
|
+
"test": "vitest run",
|
|
23
|
+
"prepublishOnly": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"llmo",
|
|
27
|
+
"ai-seo",
|
|
28
|
+
"llms.txt",
|
|
29
|
+
"ai-crawler",
|
|
30
|
+
"grounding",
|
|
31
|
+
"citation",
|
|
32
|
+
"lighthouse"
|
|
33
|
+
],
|
|
34
|
+
"author": "Ken Imoto <ken@propel-lab.com> (https://kenimoto.dev)",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"homepage": "https://llmoframework.com",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/open-llmo/llmo-checker.git"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/open-llmo/llmo-checker/issues"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=20"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"cheerio": "^1.0.0",
|
|
49
|
+
"commander": "^12.1.0",
|
|
50
|
+
"picocolors": "^1.1.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^22.0.0",
|
|
54
|
+
"tsx": "^4.19.0",
|
|
55
|
+
"typescript": "^5.6.0",
|
|
56
|
+
"vitest": "^2.1.0"
|
|
57
|
+
}
|
|
58
|
+
}
|