aeo-ready 1.2.0 → 1.3.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/README.md CHANGED
@@ -1,115 +1,91 @@
1
- # agent-web
1
+ # aeo-ready
2
2
 
3
- Is your site AI-ready? One scan, two scorecards, one score.
3
+ AEO benchmark aggregator. One scan, every score.
4
4
 
5
5
  ```bash
6
- npx agent-web scan --url https://your-site.com
6
+ npx aeo-ready scan yoursite.com
7
7
  ```
8
8
 
9
- ## What it measures
9
+ ## What it does
10
10
 
11
- **Agent Readiness** (0-50) Can AI agents discover, parse, and act on your site?
12
- - Discovery: llms.txt, robots.txt AI crawlers, sitemap, meta tags
13
- - Content Structure: markdown availability, heading hierarchy, token budgets, front-loading
14
- - Capability Signaling: AGENTS.md, agents.json, OpenAPI, content negotiation
15
- - Actionable: machine-readable contact, pricing, API endpoints, SDK manifest
11
+ Runs every major AEO (Agentic Engine Optimization) benchmark against your site in one command. Shows per-check pass/fail, company comparisons, and tracks scores over time.
16
12
 
17
- **AI Visibility** (0-50) — Does your content get cited in AI-generated responses?
18
- - Structured Data: schema.org, FAQ markup, rich schemas, Open Graph
19
- - Citation Readiness: direct answer formatting, question headings, citable structure
20
- - Authority: E-E-A-T signals, entity optimization, external validation
21
- - Freshness: modification dates, publication cadence, content recency
13
+ ## Sources
22
14
 
23
- **Overall: X/100** with letter grade (A-F).
15
+ | Benchmark | What it checks | Checks |
16
+ |-----------|---------------|--------|
17
+ | **agentic-seo** (Addy Osmani) | Discovery, content structure, token economics, capability signaling, UX bridge | 10 |
18
+ | **Cloudflare** (isitagentready.com) | Discoverability, content accessibility, bot access, API/auth/MCP/A2A discovery, commerce | 19 |
19
+ | **Fern** (afdocs) | llms.txt quality, markdown availability, page size, content structure, URL stability, auth | 23 |
24
20
 
25
21
  ## Usage
26
22
 
27
23
  ```bash
28
- # Audit a live URL
29
- npx agent-web scan --url https://example.com
24
+ npx aeo-ready scan yoursite.com # scan a URL (remote checks)
25
+ npx aeo-ready scan yoursite.com --dir ./public # full scan (local + remote)
26
+ npx aeo-ready scan yoursite.com --json # JSON output for CI
27
+ npx aeo-ready scan yoursite.com --threshold 60 # exit 1 if below
28
+ ```
30
29
 
31
- # Audit current directory (repo mode)
32
- npx agent-web scan
30
+ ### Why `--dir`?
33
31
 
34
- # JSON output for CI pipelines
35
- npx agent-web scan --json --threshold 60
32
+ agentic-seo scores ~23/100 in URL-only mode because most checks (content structure, token economics, capability signaling, UX bridge) need filesystem access. Pass `--dir` to your build output or public directory to get the real score.
36
33
 
37
- # Skip benchmark comparison
38
- npx agent-web scan --no-benchmark
34
+ ```
35
+ URL-only: agentic-seo 23/100 (F)
36
+ With --dir: agentic-seo 92/100 (A)
39
37
  ```
40
38
 
41
39
  ## Output
42
40
 
43
41
  ```
44
- agent-webAI readiness audit
45
-
46
- Mode: url | Type: saas
42
+ aeo-readyAEO benchmark aggregator
47
43
 
48
- Overall: C 50/100
44
+ ─── Benchmarks ────────────────────────────────────
49
45
 
50
- Agent Readiness: 22/50
51
- discovery: 3/12
52
- + AI-friendly meta tags [3]
53
- - llms.txt [0/4]
54
- fix: No llms.txt found.
55
- - robots.txt AI crawlers [0/3]
56
- fix: robots.txt exists but doesn't mention AI crawlers.
46
+ ███████████████░ agentic-seo 92/100 (A)
47
+ Discovery 25/25
48
+ Content Structure 19/25
49
+ Token Economics 25/25
50
+ Capability Signaling 15/15
51
+ UX Bridge 8/10
52
+ compare: Cloudflare 55 · Supabase 52 · Vercel 48
57
53
 
58
- AI Visibility: 28/50
59
- structured Data: 6/12
60
- + FAQ markup [3]
61
- + Rich schemas [3]
62
- - Schema.org markup [0/4]
63
- fix: type doesn't match site category.
64
-
65
- Second opinion: agentic-seo scored you 45/100
66
- ```
54
+ █████████████░░░ Cloudflare 4/5 (B)
55
+ + robotsTxt, + sitemap, + linkHeaders, + agentSkills...
56
+ compare: Cloudflare 5 · Vercel 4 · Supabase 3
67
57
 
68
- ## CI Mode
58
+ █████████████░░░ Fern 83/100 (B)
59
+ + llms-txt-exists, + rendering-strategy, - content-negotiation...
60
+ compare: Stripe 85 · Supabase 78 · Anthropic 72
69
61
 
70
- Exit with code 1 if below threshold:
62
+ Average across all sources: 85/100
71
63
 
72
- ```bash
73
- npx agent-web scan --json --threshold 60
64
+ Fix it:
65
+ npx agentic-seo init scaffold llms.txt, AGENTS.md
66
+ Fern: 6 issues — run npx afdocs https://yoursite.com
74
67
  ```
75
68
 
76
- Add to GitHub Actions:
69
+ ## CI Mode
77
70
 
78
71
  ```yaml
79
- - run: npx agent-web scan --threshold 50
72
+ - run: npx aeo-ready scan yoursite.com --dir ./public --threshold 50
80
73
  ```
81
74
 
82
- ## Benchmark
75
+ ## Dashboard
83
76
 
84
- Runs `npx agentic-seo --json` as an independent second opinion. Shows their score alongside yours. If agentic-seo isn't installed, the benchmark line is skipped gracefully.
85
-
86
- ## History
77
+ Each scan generates a self-contained HTML dashboard at `.aeo-ready/dashboard.html` with:
78
+ - Score cards for each benchmark
79
+ - Per-check detail (expandable)
80
+ - Company comparisons
81
+ - Score trends over time (inline SVG)
82
+ - Scan history with deltas
87
83
 
88
- Each scan saves to `.agent-web/history.json`. Gitignored by default.
84
+ Auto-opens in browser after each scan.
89
85
 
90
- ## Site Type Detection
91
-
92
- Automatically infers site type from signals:
93
- - **SaaS** — pricing + auth pages detected
94
- - **API/Developer Tool** — /docs, /api, SDK references
95
- - **Content/Blog** — articles, blog posts, publication cadence
96
- - **Personal/Portfolio** — portfolio, about me, single person
97
-
98
- Scoring adjusts by type (e.g., OpenAPI is required for SaaS, N/A for personal sites).
99
-
100
- ## Coming Soon
101
-
102
- - `--fix` mode: generate missing files (robots.txt, meta tags, structured data)
103
- - HTML dashboard with score trends over time
104
- - Citation correlation: query AI models and track whether fixes improve citations
105
-
106
- ## Also available as a Claude Code skill
107
-
108
- ```bash
109
- npx skills add katrinalaszlo/agent-web
110
- ```
86
+ ## History
111
87
 
112
- Then use `/agent-web` or `/agent-web https://example.com` inside Claude Code for an interactive audit that also generates missing files.
88
+ Scores persist in `.aeo-ready/history.json`. Re-scan to track improvement over time.
113
89
 
114
90
  ## Author
115
91
 
package/bin/cli.js CHANGED
@@ -20,7 +20,13 @@ program
20
20
 
21
21
  program
22
22
  .command("scan [url]")
23
- .description("Run all AEO benchmarks against a URL")
23
+ .description(
24
+ "Run all AEO benchmarks against a URL (add --dir for local scanning)",
25
+ )
26
+ .option(
27
+ "-d, --dir <path>",
28
+ "Local directory to scan (gives agentic-seo full access)",
29
+ )
24
30
  .option("--json", "Output results as JSON")
25
31
  .option(
26
32
  "--threshold <number>",
@@ -32,10 +38,15 @@ program
32
38
  if (url && !url.startsWith("http")) url = `https://${url}`;
33
39
  if (!url) {
34
40
  console.error(" Usage: npx aeo-ready scan <url>");
41
+ console.error(" npx aeo-ready scan <url> --dir ./public");
35
42
  process.exit(1);
36
43
  }
37
44
 
38
- const result = await scan({ url, json: opts.json || false });
45
+ const result = await scan({
46
+ url,
47
+ dir: opts.dir || null,
48
+ json: opts.json || false,
49
+ });
39
50
 
40
51
  if (opts.json) {
41
52
  process.stdout.write(JSON.stringify(result, null, 2) + "\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aeo-ready",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "AEO benchmark aggregator. One scan, every score. Collects agentic-seo, Cloudflare, and Fern in one report.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,14 +1,109 @@
1
1
  import { execSync } from "child_process";
2
+ import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "fs";
3
+ import { tmpdir } from "os";
4
+ import { join, dirname } from "path";
5
+
6
+ const KNOWN_FILES = [
7
+ "robots.txt",
8
+ "llms.txt",
9
+ "llms-full.txt",
10
+ "AGENTS.md",
11
+ "CLAUDE.md",
12
+ "skill.md",
13
+ "agent-permissions.json",
14
+ "agents.json",
15
+ "sitemap.xml",
16
+ ".well-known/ai-plugin.json",
17
+ ];
18
+
19
+ async function fetchText(url) {
20
+ try {
21
+ const res = await fetch(url, { redirect: "follow" });
22
+ if (!res.ok) return null;
23
+ return await res.text();
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ function parseSitemapUrls(xml, baseUrl) {
30
+ const urls = [];
31
+ const matches = xml.matchAll(/<loc>([^<]+)<\/loc>/g);
32
+ for (const m of matches) {
33
+ const loc = m[1].trim();
34
+ if (loc.startsWith(baseUrl)) urls.push(loc);
35
+ }
36
+ return urls;
37
+ }
38
+
39
+ function urlToFilePath(url, baseUrl) {
40
+ let path = new URL(url).pathname;
41
+ if (path.endsWith("/")) path += "index.html";
42
+ else if (!path.includes(".")) path += ".html";
43
+ return path.replace(/^\//, "");
44
+ }
45
+
46
+ async function fetchSiteToDir(baseUrl) {
47
+ const tempDir = mkdtempSync(join(tmpdir(), "aeo-"));
48
+
49
+ for (const file of KNOWN_FILES) {
50
+ const content = await fetchText(`${baseUrl}/${file}`);
51
+ if (content) {
52
+ const filePath = join(tempDir, file);
53
+ mkdirSync(dirname(filePath), { recursive: true });
54
+ writeFileSync(filePath, content);
55
+ }
56
+ }
57
+
58
+ const sitemap = await fetchText(`${baseUrl}/sitemap.xml`);
59
+ if (!sitemap) return tempDir;
60
+
61
+ const urls = parseSitemapUrls(sitemap, baseUrl);
62
+
63
+ await Promise.all(
64
+ urls.map(async (url) => {
65
+ const path = urlToFilePath(url, baseUrl);
66
+ if (KNOWN_FILES.includes(path)) return;
67
+
68
+ const html = await fetchText(url);
69
+ if (html) {
70
+ const htmlPath = join(tempDir, path);
71
+ mkdirSync(dirname(htmlPath), { recursive: true });
72
+ writeFileSync(htmlPath, html);
73
+ }
74
+
75
+ const mdUrl = url
76
+ .replace(/\.html$/, ".md")
77
+ .replace(/\/?$/, (m) => (m === "/" ? "/index.md" : ".md"));
78
+ if (mdUrl !== url) {
79
+ const md = await fetchText(mdUrl);
80
+ if (md) {
81
+ const mdPath = join(tempDir, path.replace(/\.html$/, ".md"));
82
+ mkdirSync(dirname(mdPath), { recursive: true });
83
+ writeFileSync(mdPath, md);
84
+ }
85
+ }
86
+ }),
87
+ );
88
+
89
+ return tempDir;
90
+ }
2
91
 
3
92
  export async function runBenchmark(target) {
93
+ let tempDir = null;
94
+
4
95
  try {
5
- const args =
6
- target && target.startsWith("http")
7
- ? `--url ${target} --json`
8
- : `${target || "."} --json`;
96
+ let scanDir;
9
97
 
10
- const output = execSync(`npx agentic-seo ${args}`, {
11
- timeout: 30000,
98
+ if (target && target.startsWith("http")) {
99
+ tempDir = await fetchSiteToDir(target);
100
+ scanDir = tempDir;
101
+ } else {
102
+ scanDir = target || ".";
103
+ }
104
+
105
+ const output = execSync(`npx agentic-seo ${scanDir} --json`, {
106
+ timeout: 60000,
12
107
  encoding: "utf8",
13
108
  stdio: ["pipe", "pipe", "pipe"],
14
109
  });
@@ -36,5 +131,11 @@ export async function runBenchmark(target) {
36
131
  available: false,
37
132
  reason: err.message?.slice(0, 100),
38
133
  };
134
+ } finally {
135
+ if (tempDir) {
136
+ try {
137
+ rmSync(tempDir, { recursive: true, force: true });
138
+ } catch {}
139
+ }
39
140
  }
40
141
  }
@@ -27,11 +27,11 @@ const REFERENCE_SCORES = {
27
27
  },
28
28
  };
29
29
 
30
- export async function runAllBenchmarks(target) {
30
+ export async function runAllBenchmarks(target, dir) {
31
31
  const isUrl = target && target.startsWith("http");
32
32
 
33
33
  const results = await Promise.allSettled([
34
- runAgenticSeo(target),
34
+ runAgenticSeo(dir || target),
35
35
  isUrl ? runCloudflare(target) : Promise.resolve(null),
36
36
  isUrl ? runFern(target) : Promise.resolve(null),
37
37
  ]);
package/src/scan.js CHANGED
@@ -5,7 +5,7 @@ import { generateDashboard } from "./dashboard/generate.js";
5
5
  import { exec } from "child_process";
6
6
 
7
7
  export async function scan(opts) {
8
- const { url, json } = opts;
8
+ const { url, dir, json } = opts;
9
9
 
10
10
  if (!json) {
11
11
  console.log(
@@ -14,7 +14,7 @@ export async function scan(opts) {
14
14
  console.log(chalk.dim(` Scanning ${url}...\n`));
15
15
  }
16
16
 
17
- const benchmarks = await runAllBenchmarks(url);
17
+ const benchmarks = await runAllBenchmarks(url, dir);
18
18
  const scores = collectScores(benchmarks);
19
19
  const averageScore =
20
20
  scores.length > 0