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 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
- vs Stripe 85 · Supabase 78 · Anthropic 72 · Vercel 60
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 agentic-seo init scaffold llms.txt, AGENTS.md
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.3.1",
4
- "description": "AEO benchmark aggregator. One scan, every score. Collects agentic-seo, Cloudflare, and Fern in one report.",
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 { execSync } from "child_process";
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 = execSync(`npx agentic-seo ${scanDir} --json`, {
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
  }
@@ -46,6 +46,7 @@ export async function runFern(url) {
46
46
  available: true,
47
47
  };
48
48
  } catch (err) {
49
+ console.warn(`Warning: Fern benchmark failed for ${url}: ${err.message}`);
49
50
  return { available: false, reason: err.message?.slice(0, 100) };
50
51
  }
51
52
  }
@@ -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 { execSync } from "child_process";
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(chalk.dim(" Checking agentic-seo · Cloudflare · Fern...\n"));
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([`npx afdocs ${result.url}`, `${fernFails.length} Fern issues`]);
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
- execSync(`npx agentic-seo init ${targetDir}`, {
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
- execSync(`npx afdocs ${result.url}`, { stdio: "inherit" });
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
  }
@@ -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
- }