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 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
+ [![CI](https://github.com/open-llmo/llmo-checker/actions/workflows/ci.yml/badge.svg)](https://github.com/open-llmo/llmo-checker/actions/workflows/ci.yml)
11
+ [![npm version](https://img.shields.io/npm/v/llmo-checker.svg)](https://www.npmjs.com/package/llmo-checker)
12
+ [![npm downloads](https://img.shields.io/npm/dm/llmo-checker.svg)](https://www.npmjs.com/package/llmo-checker)
13
+ [![node](https://img.shields.io/node/v/llmo-checker.svg)](https://nodejs.org)
14
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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.
@@ -0,0 +1,5 @@
1
+ import type { CheckerReport } from "./types.js";
2
+ export declare const CHECKER_VERSION = "0.1.0";
3
+ export declare const SCORE_VERSION: "0.1";
4
+ export declare function runChecks(rawUrl: string): Promise<CheckerReport>;
5
+ //# sourceMappingURL=check.d.ts.map
@@ -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,3 @@
1
+ import type { CheckContext, CheckResult } from "../types.js";
2
+ export declare function checkCanonical(ctx: CheckContext): CheckResult;
3
+ //# sourceMappingURL=canonical.d.ts.map
@@ -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,3 @@
1
+ import type { CheckContext, CheckResult } from "../types.js";
2
+ export declare function checkJsonLd(ctx: CheckContext): CheckResult;
3
+ //# sourceMappingURL=jsonld.d.ts.map
@@ -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,3 @@
1
+ import type { CheckContext, CheckResult } from "../types.js";
2
+ export declare function checkLlmsTxt(ctx: CheckContext): Promise<CheckResult>;
3
+ //# sourceMappingURL=llms-txt.d.ts.map
@@ -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,3 @@
1
+ import type { CheckContext, CheckResult } from "../types.js";
2
+ export declare function checkMeta(ctx: CheckContext): CheckResult;
3
+ //# sourceMappingURL=meta.d.ts.map
@@ -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,3 @@
1
+ import type { CheckContext, CheckResult } from "../types.js";
2
+ export declare function checkRobotsAi(ctx: CheckContext): Promise<CheckResult>;
3
+ //# sourceMappingURL=robots-ai.d.ts.map
@@ -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,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { CheckResult } from "./types.js";
2
+ export declare function computeScore(checks: CheckResult[]): number;
3
+ //# sourceMappingURL=score.d.ts.map
@@ -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"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -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
+ }