aeo-ready 1.3.1 → 1.4.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 +13 -0
- package/README.md +20 -6
- package/package.json +2 -3
- package/src/benchmark/agentgrade.js +55 -0
- package/src/benchmark/agentic-seo.js +9 -4
- package/src/benchmark/cloudflare.js +3 -0
- package/src/benchmark/fern.js +1 -0
- package/src/benchmark/index.js +12 -0
- package/src/benchmark/vercel.js +57 -0
- package/src/scan.js +21 -6
- package/src/utils/fetch.js +0 -42
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.4.0
|
|
4
|
+
|
|
5
|
+
- Add Vercel Agent Readability benchmark (`@vercel/agent-readability`)
|
|
6
|
+
- Add AgentGrade benchmark (`agentgrade-cli`)
|
|
7
|
+
- Scan now runs 5 benchmarks in parallel: agentic-seo, Cloudflare, Fern, Vercel, AgentGrade
|
|
8
|
+
|
|
9
|
+
## 1.3.2
|
|
10
|
+
|
|
11
|
+
- Fix broken `afdocs` fix command — was calling `npx afdocs <url>` instead of `npx afdocs check <url>`
|
|
12
|
+
- Fix shell injection in `promptFix` — switched `execSync` to `execFileSync`
|
|
13
|
+
- Add test suite (`node --test`)
|
|
14
|
+
- Add GitHub Actions CI
|
|
15
|
+
|
|
3
16
|
## 1.3.1
|
|
4
17
|
|
|
5
18
|
- Add progress indicator during scan
|
package/README.md
CHANGED
|
@@ -17,6 +17,8 @@ Runs every major AEO (Agentic Engine Optimization) benchmark against your site i
|
|
|
17
17
|
| **agentic-seo** (Addy Osmani) | Discovery, content structure, token economics, capability signaling, UX bridge | 10 |
|
|
18
18
|
| **Cloudflare** (isitagentready.com) | Discoverability, content accessibility, bot access, API/MCP/A2A discovery, commerce | 19 |
|
|
19
19
|
| **Fern** (afdocs) | llms.txt quality, markdown availability, page size, content structure, URL stability, auth | 23 |
|
|
20
|
+
| **Vercel** (Agent Readability Spec) | Agent reachability, discoverability, markdown serving, HTML agent-friendliness | 25 |
|
|
21
|
+
| **AgentGrade** (agentgrade.com) | MCP, payment protocols, identity standards, content negotiation, OpenAPI, infrastructure | 70+ |
|
|
20
22
|
|
|
21
23
|
## Usage
|
|
22
24
|
|
|
@@ -41,19 +43,19 @@ With --dir: agentic-seo 92/100 (A)
|
|
|
41
43
|
```
|
|
42
44
|
aeo-ready — yoursite.com
|
|
43
45
|
|
|
46
|
+
Checking agentic-seo · Cloudflare · Fern · Vercel · AgentGrade...
|
|
47
|
+
|
|
44
48
|
agentic-seo ·································· 91/100 A
|
|
45
49
|
✓ Discovery 25/25
|
|
46
50
|
◑ Content Structure 18/25
|
|
47
51
|
✓ Token Economics 25/25
|
|
48
52
|
✓ Capability Signaling 15/15
|
|
49
53
|
✓ UX Bridge 8/10
|
|
50
|
-
vs Cloudflare 55 · Supabase 52 · Vercel 48 · Stripe 17
|
|
51
54
|
|
|
52
55
|
Cloudflare ···································· 4/5 B
|
|
53
56
|
10 passed 2 failed
|
|
54
57
|
✗ robotsTxtAiRules No rules for AI bots found
|
|
55
58
|
✗ contentSignals No content signals in robots.txt
|
|
56
|
-
vs Cloudflare 5 · Vercel 4 · Supabase 3 · Stripe 2
|
|
57
59
|
|
|
58
60
|
Fern ········································ 83/100 B
|
|
59
61
|
9 passed 4 failed
|
|
@@ -61,14 +63,26 @@ With --dir: agentic-seo 92/100 (A)
|
|
|
61
63
|
✗ content-start-position 2 pages have content past 50%
|
|
62
64
|
✗ llms-txt-coverage Covers 67% of sitemap
|
|
63
65
|
✗ markdown-content-parity 4 pages have content differences
|
|
64
|
-
|
|
66
|
+
|
|
67
|
+
Vercel ····································· 75/100 B
|
|
68
|
+
15 passed 5 failed
|
|
69
|
+
✗ robots.txt blocked: ccbot
|
|
70
|
+
✗ Agent UA → markdown returned HTML
|
|
71
|
+
✗ .md URL → markdown status 404
|
|
72
|
+
✗ Frontmatter no frontmatter found
|
|
73
|
+
✗ Missing page → markdown returned 404
|
|
74
|
+
|
|
75
|
+
AgentGrade ································ 81/100 B+
|
|
76
|
+
30 passed 10 failed
|
|
77
|
+
✗ llms.txt linked from HTML
|
|
78
|
+
✗ Accept: JSON returns JSON
|
|
79
|
+
✗ Accept: text returns text
|
|
65
80
|
|
|
66
81
|
──────────────────────────────────────────────────
|
|
67
82
|
Overall 85/100
|
|
68
83
|
|
|
69
84
|
Next steps
|
|
70
|
-
npx
|
|
71
|
-
npx afdocs https://yoursite.com 4 Fern issues
|
|
85
|
+
npx afdocs check https://yoursite.com 4 Fern issues
|
|
72
86
|
npx skills add katrinalaszlo/agent-serve make your product agent-ready
|
|
73
87
|
|
|
74
88
|
Fix now? [y/N]
|
|
@@ -96,7 +110,7 @@ npx aeo-ready history # show last 10 scans
|
|
|
96
110
|
import { scan, getHistory } from "aeo-ready";
|
|
97
111
|
|
|
98
112
|
const result = await scan({ url: "https://yoursite.com", dir: "./public", json: true });
|
|
99
|
-
// result.averageScore, result.benchmarks.agenticSeo, .cloudflare, .fern
|
|
113
|
+
// result.averageScore, result.benchmarks.agenticSeo, .cloudflare, .fern, .vercel, .agentgrade
|
|
100
114
|
|
|
101
115
|
const history = getHistory(process.cwd());
|
|
102
116
|
// history.scans — array of past scan results
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aeo-ready",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "AEO benchmark aggregator. One scan, every score. Collects agentic-seo, Cloudflare, and
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "AEO benchmark aggregator. One scan, every score. Collects agentic-seo, Cloudflare, Fern, Vercel, and AgentGrade in one report.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"aeo-ready": "./bin/cli.js"
|
|
@@ -37,7 +37,6 @@
|
|
|
37
37
|
"bin/",
|
|
38
38
|
"src/benchmark/",
|
|
39
39
|
"src/history/",
|
|
40
|
-
"src/utils/",
|
|
41
40
|
"src/index.js",
|
|
42
41
|
"src/scan.js",
|
|
43
42
|
"skills/",
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { execFileSync } from "child_process";
|
|
2
|
+
|
|
3
|
+
export async function runAgentgrade(url) {
|
|
4
|
+
try {
|
|
5
|
+
const raw = execFileSync("npx", ["agentgrade-cli", url, "--json"], {
|
|
6
|
+
timeout: 60000,
|
|
7
|
+
encoding: "utf8",
|
|
8
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const jsonStart = raw.indexOf("{");
|
|
12
|
+
if (jsonStart === -1) {
|
|
13
|
+
return { available: false, reason: "no JSON in output" };
|
|
14
|
+
}
|
|
15
|
+
const result = JSON.parse(raw.slice(jsonStart));
|
|
16
|
+
const scoreObj = result.score || {};
|
|
17
|
+
|
|
18
|
+
const categories = {};
|
|
19
|
+
for (const group of scoreObj.groups || []) {
|
|
20
|
+
if (!group.applicable) continue;
|
|
21
|
+
categories[group.key] = {
|
|
22
|
+
name: group.label,
|
|
23
|
+
score: group.passed,
|
|
24
|
+
maxScore: group.total,
|
|
25
|
+
percentage: group.pct ?? 0,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const checks = [];
|
|
30
|
+
for (const group of scoreObj.groups || []) {
|
|
31
|
+
if (!group.applicable) continue;
|
|
32
|
+
for (const check of group.checks || []) {
|
|
33
|
+
checks.push({
|
|
34
|
+
id: check.label,
|
|
35
|
+
status: check.passed ? "pass" : "fail",
|
|
36
|
+
message: check.hint || "",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
score: scoreObj.pct ?? 0,
|
|
43
|
+
maxScore: 100,
|
|
44
|
+
grade: scoreObj.grade || null,
|
|
45
|
+
categories,
|
|
46
|
+
checks,
|
|
47
|
+
available: true,
|
|
48
|
+
};
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.warn(
|
|
51
|
+
`Warning: AgentGrade benchmark failed for ${url}: ${err.message}`,
|
|
52
|
+
);
|
|
53
|
+
return { available: false, reason: err.message?.slice(0, 100) };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFileSync } from "child_process";
|
|
2
2
|
import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "fs";
|
|
3
3
|
import { tmpdir } from "os";
|
|
4
4
|
import { join, dirname } from "path";
|
|
@@ -21,7 +21,8 @@ async function fetchText(url) {
|
|
|
21
21
|
const res = await fetch(url, { redirect: "follow" });
|
|
22
22
|
if (!res.ok) return null;
|
|
23
23
|
return await res.text();
|
|
24
|
-
} catch {
|
|
24
|
+
} catch (err) {
|
|
25
|
+
console.warn(`Warning: failed to fetch ${url}: ${err.message}`);
|
|
25
26
|
return null;
|
|
26
27
|
}
|
|
27
28
|
}
|
|
@@ -102,7 +103,7 @@ export async function runBenchmark(target) {
|
|
|
102
103
|
scanDir = target || ".";
|
|
103
104
|
}
|
|
104
105
|
|
|
105
|
-
const output =
|
|
106
|
+
const output = execFileSync("npx", ["agentic-seo", scanDir, "--json"], {
|
|
106
107
|
timeout: 60000,
|
|
107
108
|
encoding: "utf8",
|
|
108
109
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -135,7 +136,11 @@ export async function runBenchmark(target) {
|
|
|
135
136
|
if (tempDir) {
|
|
136
137
|
try {
|
|
137
138
|
rmSync(tempDir, { recursive: true, force: true });
|
|
138
|
-
} catch {
|
|
139
|
+
} catch (cleanupErr) {
|
|
140
|
+
console.warn(
|
|
141
|
+
`Warning: failed to clean up temp dir ${tempDir}: ${cleanupErr.message}`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
139
144
|
}
|
|
140
145
|
}
|
|
141
146
|
}
|
|
@@ -62,6 +62,9 @@ export async function runCloudflare(url) {
|
|
|
62
62
|
available: true,
|
|
63
63
|
};
|
|
64
64
|
} catch (err) {
|
|
65
|
+
console.warn(
|
|
66
|
+
`Warning: Cloudflare benchmark failed for ${url}: ${err.message}`,
|
|
67
|
+
);
|
|
65
68
|
return { available: false, reason: err.message?.slice(0, 100) };
|
|
66
69
|
}
|
|
67
70
|
}
|
package/src/benchmark/fern.js
CHANGED
package/src/benchmark/index.js
CHANGED
|
@@ -2,6 +2,8 @@ import chalk from "chalk";
|
|
|
2
2
|
import { runBenchmark as runAgenticSeo } from "./agentic-seo.js";
|
|
3
3
|
import { runCloudflare } from "./cloudflare.js";
|
|
4
4
|
import { runFern } from "./fern.js";
|
|
5
|
+
import { runVercel } from "./vercel.js";
|
|
6
|
+
import { runAgentgrade } from "./agentgrade.js";
|
|
5
7
|
|
|
6
8
|
const REFERENCE_SCORES = {
|
|
7
9
|
agenticSeo: {
|
|
@@ -36,12 +38,16 @@ export async function runAllBenchmarks(target, dir) {
|
|
|
36
38
|
runAgenticSeo(dir || target),
|
|
37
39
|
isUrl ? runCloudflare(target) : Promise.resolve(null),
|
|
38
40
|
isUrl ? runFern(target) : Promise.resolve(null),
|
|
41
|
+
isUrl ? runVercel(target) : Promise.resolve(null),
|
|
42
|
+
isUrl ? runAgentgrade(target) : Promise.resolve(null),
|
|
39
43
|
]);
|
|
40
44
|
|
|
41
45
|
return {
|
|
42
46
|
agenticSeo: results[0].status === "fulfilled" ? results[0].value : null,
|
|
43
47
|
cloudflare: results[1].status === "fulfilled" ? results[1].value : null,
|
|
44
48
|
fern: results[2].status === "fulfilled" ? results[2].value : null,
|
|
49
|
+
vercel: results[3].status === "fulfilled" ? results[3].value : null,
|
|
50
|
+
agentgrade: results[4].status === "fulfilled" ? results[4].value : null,
|
|
45
51
|
};
|
|
46
52
|
}
|
|
47
53
|
|
|
@@ -60,6 +66,12 @@ export function printBenchmarks(benchmarks) {
|
|
|
60
66
|
if (benchmarks.fern?.available) {
|
|
61
67
|
printBenchmarkBlock("Fern", "fern", benchmarks.fern);
|
|
62
68
|
}
|
|
69
|
+
if (benchmarks.vercel?.available) {
|
|
70
|
+
printBenchmarkBlock("Vercel", "vercel", benchmarks.vercel);
|
|
71
|
+
}
|
|
72
|
+
if (benchmarks.agentgrade?.available) {
|
|
73
|
+
printBenchmarkBlock("AgentGrade", "agentgrade", benchmarks.agentgrade);
|
|
74
|
+
}
|
|
63
75
|
}
|
|
64
76
|
|
|
65
77
|
function scoreColor(pct) {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { execFileSync } from "child_process";
|
|
2
|
+
|
|
3
|
+
export async function runVercel(url) {
|
|
4
|
+
try {
|
|
5
|
+
const output = execFileSync(
|
|
6
|
+
"npx",
|
|
7
|
+
["@vercel/agent-readability", "audit", url, "--json"],
|
|
8
|
+
{ timeout: 60000, encoding: "utf8", stdio: ["pipe", "pipe", "pipe"] },
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
const result = JSON.parse(output);
|
|
12
|
+
const score = result.score ?? 0;
|
|
13
|
+
const maxScore = 100;
|
|
14
|
+
|
|
15
|
+
const categories = {};
|
|
16
|
+
for (const [key, cat] of Object.entries(result.categories || {})) {
|
|
17
|
+
categories[key] = {
|
|
18
|
+
name: key.replace(/_/g, " "),
|
|
19
|
+
score: cat.passed,
|
|
20
|
+
maxScore: cat.total,
|
|
21
|
+
percentage:
|
|
22
|
+
cat.total > 0 ? Math.round((cat.passed / cat.total) * 100) : 0,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const checks = [];
|
|
27
|
+
for (const cat of Object.values(result.categories || {})) {
|
|
28
|
+
for (const check of cat.checks || []) {
|
|
29
|
+
checks.push({
|
|
30
|
+
id: check.name,
|
|
31
|
+
status: check.skipped ? "skip" : check.passed ? "pass" : "fail",
|
|
32
|
+
message: check.detail || check.hint || "",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
score,
|
|
39
|
+
maxScore,
|
|
40
|
+
grade: ratingToGrade(result.rating),
|
|
41
|
+
categories,
|
|
42
|
+
checks,
|
|
43
|
+
available: true,
|
|
44
|
+
};
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.warn(`Warning: Vercel benchmark failed for ${url}: ${err.message}`);
|
|
47
|
+
return { available: false, reason: err.message?.slice(0, 100) };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function ratingToGrade(rating) {
|
|
52
|
+
if (rating === "Excellent") return "A";
|
|
53
|
+
if (rating === "Good") return "B";
|
|
54
|
+
if (rating === "Fair") return "C";
|
|
55
|
+
if (rating === "Poor") return "D";
|
|
56
|
+
return "F";
|
|
57
|
+
}
|
package/src/scan.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { createInterface } from "readline";
|
|
3
|
-
import {
|
|
3
|
+
import { execFileSync } from "child_process";
|
|
4
4
|
import { runAllBenchmarks, printBenchmarks } from "./benchmark/index.js";
|
|
5
5
|
import { saveResult } from "./history/index.js";
|
|
6
6
|
|
|
@@ -9,7 +9,11 @@ export async function scan(opts) {
|
|
|
9
9
|
|
|
10
10
|
if (!json) {
|
|
11
11
|
console.log(chalk.bold("\n aeo-ready") + chalk.dim(` — ${url}\n`));
|
|
12
|
-
console.log(
|
|
12
|
+
console.log(
|
|
13
|
+
chalk.dim(
|
|
14
|
+
" Checking agentic-seo · Cloudflare · Fern · Vercel · AgentGrade...\n",
|
|
15
|
+
),
|
|
16
|
+
);
|
|
13
17
|
}
|
|
14
18
|
|
|
15
19
|
const benchmarks = await runAllBenchmarks(url, dir);
|
|
@@ -55,6 +59,12 @@ function collectScores(benchmarks) {
|
|
|
55
59
|
if (benchmarks.fern?.available) {
|
|
56
60
|
scores.push(benchmarks.fern.score);
|
|
57
61
|
}
|
|
62
|
+
if (benchmarks.vercel?.available) {
|
|
63
|
+
scores.push(benchmarks.vercel.score);
|
|
64
|
+
}
|
|
65
|
+
if (benchmarks.agentgrade?.available) {
|
|
66
|
+
scores.push(benchmarks.agentgrade.score);
|
|
67
|
+
}
|
|
58
68
|
return scores;
|
|
59
69
|
}
|
|
60
70
|
|
|
@@ -100,7 +110,10 @@ function printNextSteps(result) {
|
|
|
100
110
|
(c) => c.status === "fail" || c.status === "warn",
|
|
101
111
|
) || [];
|
|
102
112
|
if (fernFails.length > 0) {
|
|
103
|
-
steps.push([
|
|
113
|
+
steps.push([
|
|
114
|
+
`npx afdocs check ${result.url}`,
|
|
115
|
+
`${fernFails.length} Fern issues`,
|
|
116
|
+
]);
|
|
104
117
|
}
|
|
105
118
|
|
|
106
119
|
steps.push([
|
|
@@ -138,7 +151,7 @@ async function promptFix(result, dir) {
|
|
|
138
151
|
|
|
139
152
|
console.log(chalk.dim(` Running: npx agentic-seo init ${targetDir}\n`));
|
|
140
153
|
try {
|
|
141
|
-
|
|
154
|
+
execFileSync("npx", ["agentic-seo", "init", targetDir], {
|
|
142
155
|
stdio: "inherit",
|
|
143
156
|
});
|
|
144
157
|
} catch (err) {
|
|
@@ -150,9 +163,11 @@ async function promptFix(result, dir) {
|
|
|
150
163
|
(c) => c.status === "fail" || c.status === "warn",
|
|
151
164
|
) || [];
|
|
152
165
|
if (fernFails.length > 0) {
|
|
153
|
-
console.log(chalk.dim(`\n Running: npx afdocs ${result.url}\n`));
|
|
166
|
+
console.log(chalk.dim(`\n Running: npx afdocs check ${result.url}\n`));
|
|
154
167
|
try {
|
|
155
|
-
|
|
168
|
+
execFileSync("npx", ["afdocs", "check", result.url], {
|
|
169
|
+
stdio: "inherit",
|
|
170
|
+
});
|
|
156
171
|
} catch (err) {
|
|
157
172
|
console.log(chalk.red(`\n afdocs failed: ${err.message}\n`));
|
|
158
173
|
}
|
package/src/utils/fetch.js
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
const DEFAULT_TIMEOUT = 10000;
|
|
2
|
-
const USER_AGENT = "aeo-ready/1.3 (AEO benchmark aggregator)";
|
|
3
|
-
|
|
4
|
-
export async function fetchUrl(url, opts = {}) {
|
|
5
|
-
const controller = new AbortController();
|
|
6
|
-
const timeout = setTimeout(
|
|
7
|
-
() => controller.abort(),
|
|
8
|
-
opts.timeout || DEFAULT_TIMEOUT,
|
|
9
|
-
);
|
|
10
|
-
|
|
11
|
-
try {
|
|
12
|
-
const res = await fetch(url, {
|
|
13
|
-
signal: controller.signal,
|
|
14
|
-
headers: {
|
|
15
|
-
"User-Agent": USER_AGENT,
|
|
16
|
-
...opts.headers,
|
|
17
|
-
},
|
|
18
|
-
redirect: "follow",
|
|
19
|
-
});
|
|
20
|
-
const text = await res.text();
|
|
21
|
-
return {
|
|
22
|
-
status: res.status,
|
|
23
|
-
text,
|
|
24
|
-
headers: Object.fromEntries(res.headers),
|
|
25
|
-
};
|
|
26
|
-
} catch (err) {
|
|
27
|
-
if (err.name === "AbortError") {
|
|
28
|
-
return { status: 0, text: "", headers: {}, error: "timeout" };
|
|
29
|
-
}
|
|
30
|
-
return { status: 0, text: "", headers: {}, error: err.message };
|
|
31
|
-
} finally {
|
|
32
|
-
clearTimeout(timeout);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function resolveUrl(base, path) {
|
|
37
|
-
try {
|
|
38
|
-
return new URL(path, base).href;
|
|
39
|
-
} catch {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
}
|