aeo-ready 1.0.0 → 1.2.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/.claude/settings.local.json +7 -0
- package/bin/cli.js +9 -62
- package/package.json +5 -4
- package/src/benchmark/cloudflare.js +75 -0
- package/src/benchmark/fern.js +51 -0
- package/src/benchmark/index.js +119 -0
- package/src/dashboard/generate.js +22 -48
- package/src/dashboard/sections/benchmark-details.js +79 -0
- package/src/dashboard/sections/history-table.js +10 -7
- package/src/dashboard/sections/overall-score.js +55 -118
- package/src/dashboard/sections/trend-chart.js +31 -46
- package/src/history/index.js +8 -11
- package/src/scan.js +58 -294
- package/.aeo-ready/dashboard.html +0 -339
- package/src/checks/agent-readiness/actionable.js +0 -165
- package/src/checks/agent-readiness/capability.js +0 -209
- package/src/checks/agent-readiness/content-structure.js +0 -242
- package/src/checks/agent-readiness/discovery.js +0 -231
- package/src/checks/ai-visibility/authority.js +0 -195
- package/src/checks/ai-visibility/citation-readiness.js +0 -228
- package/src/checks/ai-visibility/freshness.js +0 -182
- package/src/checks/ai-visibility/structured-data.js +0 -180
- package/src/dashboard/sections/agent-readiness.js +0 -71
- package/src/dashboard/sections/ai-visibility.js +0 -67
- package/src/dashboard/sections/recommendations.js +0 -49
- package/src/fix/generators/agents-json.js +0 -73
- package/src/fix/generators/agents-md.js +0 -85
- package/src/fix/generators/llms-txt.js +0 -166
- package/src/fix/generators/robots-txt.js +0 -64
- package/src/fix/index.js +0 -177
- package/src/track/index.js +0 -167
- package/src/utils/detect-type.js +0 -99
- package/src/utils/tokens.js +0 -18
package/src/scan.js
CHANGED
|
@@ -1,65 +1,38 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import {
|
|
3
|
-
import { detectSiteType } from "./utils/detect-type.js";
|
|
4
|
-
import { runDiscoveryChecks } from "./checks/agent-readiness/discovery.js";
|
|
5
|
-
import { runContentStructureChecks } from "./checks/agent-readiness/content-structure.js";
|
|
6
|
-
import { runCapabilityChecks } from "./checks/agent-readiness/capability.js";
|
|
7
|
-
import { runActionableChecks } from "./checks/agent-readiness/actionable.js";
|
|
8
|
-
import { runStructuredDataChecks } from "./checks/ai-visibility/structured-data.js";
|
|
9
|
-
import { runCitationReadinessChecks } from "./checks/ai-visibility/citation-readiness.js";
|
|
10
|
-
import { runAuthorityChecks } from "./checks/ai-visibility/authority.js";
|
|
11
|
-
import { runFreshnessChecks } from "./checks/ai-visibility/freshness.js";
|
|
12
|
-
import { runBenchmark } from "./benchmark/agentic-seo.js";
|
|
2
|
+
import { runAllBenchmarks, printBenchmarks } from "./benchmark/index.js";
|
|
13
3
|
import { saveResult } from "./history/index.js";
|
|
14
4
|
import { generateDashboard } from "./dashboard/generate.js";
|
|
15
|
-
import { readdirSync, readFileSync, existsSync } from "fs";
|
|
16
|
-
import { join } from "path";
|
|
17
5
|
import { exec } from "child_process";
|
|
18
6
|
|
|
19
7
|
export async function scan(opts) {
|
|
20
|
-
const { url,
|
|
21
|
-
const mode = url ? "url" : "repo";
|
|
8
|
+
const { url, json } = opts;
|
|
22
9
|
|
|
23
10
|
if (!json) {
|
|
24
11
|
console.log(
|
|
25
|
-
chalk.bold("\n aeo-ready") + chalk.dim(" —
|
|
12
|
+
chalk.bold("\n aeo-ready") + chalk.dim(" — AEO benchmark aggregator\n"),
|
|
26
13
|
);
|
|
14
|
+
console.log(chalk.dim(` Scanning ${url}...\n`));
|
|
27
15
|
}
|
|
28
16
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const agentReadiness = await runAgentReadinessChecks(context);
|
|
36
|
-
const aiVisibility = await runAiVisibilityChecks(context);
|
|
37
|
-
|
|
38
|
-
const score = agentReadiness.score + aiVisibility.score;
|
|
39
|
-
const grade = scoreToGrade(score);
|
|
40
|
-
|
|
41
|
-
let benchmarkResult = null;
|
|
42
|
-
if (benchmark) {
|
|
43
|
-
benchmarkResult = await runBenchmark(mode === "url" ? url : dir);
|
|
44
|
-
}
|
|
17
|
+
const benchmarks = await runAllBenchmarks(url);
|
|
18
|
+
const scores = collectScores(benchmarks);
|
|
19
|
+
const averageScore =
|
|
20
|
+
scores.length > 0
|
|
21
|
+
? Math.round(scores.reduce((s, v) => s + v, 0) / scores.length)
|
|
22
|
+
: 0;
|
|
45
23
|
|
|
46
24
|
const result = {
|
|
47
|
-
|
|
48
|
-
grade,
|
|
49
|
-
siteType: context.siteType,
|
|
50
|
-
mode,
|
|
51
|
-
target: url || dir,
|
|
25
|
+
url,
|
|
52
26
|
timestamp: new Date().toISOString(),
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
benchmark: benchmarkResult,
|
|
27
|
+
averageScore,
|
|
28
|
+
benchmarks,
|
|
56
29
|
};
|
|
57
30
|
|
|
58
31
|
if (!json) {
|
|
59
32
|
printReport(result);
|
|
60
33
|
}
|
|
61
34
|
|
|
62
|
-
const baseDir =
|
|
35
|
+
const baseDir = process.cwd();
|
|
63
36
|
await saveResult(result, baseDir);
|
|
64
37
|
|
|
65
38
|
if (!json) {
|
|
@@ -71,280 +44,71 @@ export async function scan(opts) {
|
|
|
71
44
|
return result;
|
|
72
45
|
}
|
|
73
46
|
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const homepage = await fetchUrl(url);
|
|
79
|
-
context.html = homepage.text;
|
|
80
|
-
context.pages.home = homepage;
|
|
81
|
-
|
|
82
|
-
const fetches = [
|
|
83
|
-
fetchUrl(resolveUrl(url, "/llms.txt")),
|
|
84
|
-
fetchUrl(resolveUrl(url, "/robots.txt")),
|
|
85
|
-
fetchUrl(resolveUrl(url, "/sitemap.xml")),
|
|
86
|
-
fetchUrl(resolveUrl(url, "/.well-known/agent.json")),
|
|
87
|
-
fetchUrl(resolveUrl(url, "/openapi.json")),
|
|
88
|
-
];
|
|
89
|
-
|
|
90
|
-
const [llms, robots, sitemap, agentJson, openapi] =
|
|
91
|
-
await Promise.all(fetches);
|
|
92
|
-
context.pages.llmsTxt = llms;
|
|
93
|
-
context.pages.robotsTxt = robots;
|
|
94
|
-
context.pages.sitemap = sitemap;
|
|
95
|
-
context.pages.agentJson = agentJson;
|
|
96
|
-
context.pages.openapi = openapi;
|
|
97
|
-
} else {
|
|
98
|
-
const fileList = listFiles(dir);
|
|
99
|
-
context.files = fileList;
|
|
100
|
-
context.fileContents = {};
|
|
101
|
-
for (const f of fileList) {
|
|
102
|
-
try {
|
|
103
|
-
context.fileContents[f] = readFileSync(join(dir, f), "utf8");
|
|
104
|
-
} catch {
|
|
105
|
-
/* skip binary/unreadable */
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
const indexHtml = [
|
|
109
|
-
"index.html",
|
|
110
|
-
"public/index.html",
|
|
111
|
-
"dist/index.html",
|
|
112
|
-
].find((p) => existsSync(join(dir, p)));
|
|
113
|
-
if (indexHtml) {
|
|
114
|
-
context.html = readFileSync(join(dir, indexHtml), "utf8");
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
context.siteType = detectSiteType({
|
|
119
|
-
html: context.html,
|
|
120
|
-
files: context.files,
|
|
121
|
-
url,
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
return context;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function listFiles(dir) {
|
|
128
|
-
const skip = new Set([
|
|
129
|
-
"node_modules",
|
|
130
|
-
".git",
|
|
131
|
-
"dist",
|
|
132
|
-
"build",
|
|
133
|
-
".next",
|
|
134
|
-
".venv",
|
|
135
|
-
"__pycache__",
|
|
136
|
-
"target",
|
|
137
|
-
".aeo-ready",
|
|
138
|
-
]);
|
|
139
|
-
const results = [];
|
|
140
|
-
|
|
141
|
-
function walk(current, prefix) {
|
|
142
|
-
let entries;
|
|
143
|
-
try {
|
|
144
|
-
entries = readdirSync(join(dir, current), { withFileTypes: true });
|
|
145
|
-
} catch {
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
for (const entry of entries) {
|
|
149
|
-
if (skip.has(entry.name)) continue;
|
|
150
|
-
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
151
|
-
if (entry.isDirectory()) {
|
|
152
|
-
walk(join(current, entry.name), rel);
|
|
153
|
-
} else {
|
|
154
|
-
results.push(rel);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
47
|
+
function collectScores(benchmarks) {
|
|
48
|
+
const scores = [];
|
|
49
|
+
if (benchmarks.agenticSeo?.available) {
|
|
50
|
+
scores.push(benchmarks.agenticSeo.score);
|
|
157
51
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
async function runAgentReadinessChecks(context) {
|
|
164
|
-
const discovery = await runDiscoveryChecks(context);
|
|
165
|
-
const contentStructure = await runContentStructureChecks(context);
|
|
166
|
-
const capability = await runCapabilityChecks(context);
|
|
167
|
-
const actionable = await runActionableChecks(context);
|
|
168
|
-
|
|
169
|
-
const score =
|
|
170
|
-
discovery.score +
|
|
171
|
-
contentStructure.score +
|
|
172
|
-
capability.score +
|
|
173
|
-
actionable.score;
|
|
174
|
-
|
|
175
|
-
return {
|
|
176
|
-
score,
|
|
177
|
-
maxScore: 50,
|
|
178
|
-
categories: { discovery, contentStructure, capability, actionable },
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async function runAiVisibilityChecks(context) {
|
|
183
|
-
const structuredData = await runStructuredDataChecks(context);
|
|
184
|
-
const citationReadiness = await runCitationReadinessChecks(context);
|
|
185
|
-
const authority = await runAuthorityChecks(context);
|
|
186
|
-
const freshness = await runFreshnessChecks(context);
|
|
187
|
-
|
|
188
|
-
const score =
|
|
189
|
-
structuredData.score +
|
|
190
|
-
citationReadiness.score +
|
|
191
|
-
authority.score +
|
|
192
|
-
freshness.score;
|
|
193
|
-
|
|
194
|
-
return {
|
|
195
|
-
score,
|
|
196
|
-
maxScore: 50,
|
|
197
|
-
categories: { structuredData, citationReadiness, authority, freshness },
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function scoreToGrade(score) {
|
|
202
|
-
if (score >= 80) return "A";
|
|
203
|
-
if (score >= 65) return "B";
|
|
204
|
-
if (score >= 50) return "C";
|
|
205
|
-
if (score >= 35) return "D";
|
|
206
|
-
return "F";
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function printReport(result) {
|
|
210
|
-
const { score, grade, agentReadiness, aiVisibility, benchmark } = result;
|
|
211
|
-
|
|
212
|
-
const gradeColor =
|
|
213
|
-
grade === "A"
|
|
214
|
-
? chalk.green
|
|
215
|
-
: grade === "B"
|
|
216
|
-
? chalk.cyan
|
|
217
|
-
: grade === "C"
|
|
218
|
-
? chalk.yellow
|
|
219
|
-
: chalk.red;
|
|
220
|
-
|
|
221
|
-
console.log(
|
|
222
|
-
` ${bar(score, 100, 40)} ${gradeColor.bold(grade)} ${score}/100\n`,
|
|
223
|
-
);
|
|
224
|
-
|
|
225
|
-
console.log(
|
|
226
|
-
chalk.dim(
|
|
227
|
-
" Agent Readiness — can AI agents find, read, and use your site?",
|
|
228
|
-
),
|
|
229
|
-
);
|
|
230
|
-
console.log(
|
|
231
|
-
chalk.dim(
|
|
232
|
-
" AI Visibility — how accurately do AI search engines describe you?",
|
|
233
|
-
),
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
if (agentReadiness.score < 25) {
|
|
237
|
-
console.log(
|
|
238
|
-
chalk.dim(
|
|
239
|
-
"\n Agent Readiness is low — AI engines can't cite what they can't read.",
|
|
52
|
+
if (benchmarks.cloudflare?.available) {
|
|
53
|
+
scores.push(
|
|
54
|
+
Math.round(
|
|
55
|
+
(benchmarks.cloudflare.score / benchmarks.cloudflare.maxScore) * 100,
|
|
240
56
|
),
|
|
241
57
|
);
|
|
242
|
-
console.log(chalk.dim(" Fix the agent side first. Visibility follows."));
|
|
243
58
|
}
|
|
244
|
-
|
|
59
|
+
if (benchmarks.fern?.available) {
|
|
60
|
+
scores.push(benchmarks.fern.score);
|
|
61
|
+
}
|
|
62
|
+
return scores;
|
|
63
|
+
}
|
|
245
64
|
|
|
246
|
-
|
|
247
|
-
|
|
65
|
+
function printReport(result) {
|
|
66
|
+
const { benchmarks, averageScore } = result;
|
|
248
67
|
|
|
249
|
-
|
|
250
|
-
...Object.values(agentReadiness.categories),
|
|
251
|
-
...Object.values(aiVisibility.categories),
|
|
252
|
-
].flatMap((c) => c.checks);
|
|
253
|
-
const passed = allChecks.filter((c) => c.passed).length;
|
|
254
|
-
const failed = allChecks.filter((c) => !c.passed && c.points === 0).length;
|
|
255
|
-
const partial = allChecks.length - passed - failed;
|
|
68
|
+
printBenchmarks(benchmarks);
|
|
256
69
|
|
|
70
|
+
const gc =
|
|
71
|
+
averageScore >= 80
|
|
72
|
+
? chalk.green
|
|
73
|
+
: averageScore >= 50
|
|
74
|
+
? chalk.yellow
|
|
75
|
+
: chalk.red;
|
|
257
76
|
console.log(
|
|
258
|
-
chalk.dim(
|
|
259
|
-
` ${chalk.green(passed + " passed")} · ${partial ? chalk.yellow(partial + " partial") + " · " : ""}${chalk.red(failed + " failed")} · ${allChecks.length} checks total\n`,
|
|
260
|
-
),
|
|
77
|
+
chalk.dim(" ─────────────────────────────────────────────────\n"),
|
|
261
78
|
);
|
|
262
|
-
|
|
263
|
-
if (benchmark && benchmark.available) {
|
|
264
|
-
printBenchmark(benchmark);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function printScorecard(name, scorecard) {
|
|
269
|
-
const pct = Math.round((scorecard.score / scorecard.maxScore) * 100);
|
|
270
79
|
console.log(
|
|
271
|
-
|
|
272
|
-
chalk.dim(` ${scorecard.score}/${scorecard.maxScore} (${pct}%)`),
|
|
80
|
+
` Average across all sources: ${gc.bold(`${averageScore}/100`)}\n`,
|
|
273
81
|
);
|
|
274
82
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const catPct = Math.round((cat.score / cat.maxScore) * 100);
|
|
278
|
-
const icon =
|
|
279
|
-
catPct >= 80
|
|
280
|
-
? chalk.green("✓")
|
|
281
|
-
: catPct >= 40
|
|
282
|
-
? chalk.yellow("◑")
|
|
283
|
-
: chalk.red("✗");
|
|
83
|
+
if (averageScore < 80) {
|
|
84
|
+
console.log(chalk.bold(" Fix it:\n"));
|
|
284
85
|
console.log(
|
|
285
|
-
|
|
86
|
+
chalk.dim(" npx agentic-seo init") +
|
|
87
|
+
" scaffold llms.txt, AGENTS.md, skill.md",
|
|
286
88
|
);
|
|
287
89
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
console.log(
|
|
296
|
-
chalk.red(` -`) +
|
|
297
|
-
` ${check.name} ` +
|
|
298
|
-
chalk.dim(`[${check.points || 0}/${check.maxPoints}]`),
|
|
299
|
-
);
|
|
300
|
-
if (check.fix) {
|
|
301
|
-
console.log(chalk.dim(` ${check.fix}`));
|
|
302
|
-
}
|
|
303
|
-
}
|
|
90
|
+
const cfFails =
|
|
91
|
+
benchmarks.cloudflare?.checks?.filter((c) => c.status === "fail") || [];
|
|
92
|
+
for (const fail of cfFails.slice(0, 2)) {
|
|
93
|
+
console.log(
|
|
94
|
+
chalk.dim(` Cloudflare: ${fail.id}`) +
|
|
95
|
+
chalk.dim(` — see isitagentready.com for fix`),
|
|
96
|
+
);
|
|
304
97
|
}
|
|
305
|
-
}
|
|
306
|
-
console.log("");
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function printBenchmark(benchmark) {
|
|
310
|
-
console.log(
|
|
311
|
-
chalk.dim(" ─── Second opinion: agentic-seo ───────────────────\n"),
|
|
312
|
-
);
|
|
313
|
-
console.log(
|
|
314
|
-
` ${bar(benchmark.score, benchmark.maxScore, 20)} ${chalk.dim(`${benchmark.score}/${benchmark.maxScore}`)}${benchmark.grade ? chalk.dim(` (${benchmark.grade})`) : ""}\n`,
|
|
315
|
-
);
|
|
316
98
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
pct >= 80
|
|
323
|
-
? chalk.green("✓")
|
|
324
|
-
: pct >= 40
|
|
325
|
-
? chalk.yellow("◑")
|
|
326
|
-
: chalk.red("✗");
|
|
99
|
+
const fernFails =
|
|
100
|
+
benchmarks.fern?.checks?.filter(
|
|
101
|
+
(c) => c.status === "fail" || c.status === "warn",
|
|
102
|
+
) || [];
|
|
103
|
+
if (fernFails.length > 0) {
|
|
327
104
|
console.log(
|
|
328
|
-
|
|
105
|
+
chalk.dim(` Fern: ${fernFails.length} issues`) +
|
|
106
|
+
chalk.dim(` — run npx afdocs ${result.url}`),
|
|
329
107
|
);
|
|
330
108
|
}
|
|
331
|
-
console.log("");
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function bar(value, max, width) {
|
|
336
|
-
const filled = max > 0 ? Math.round((value / max) * width) : 0;
|
|
337
|
-
const empty = width - filled;
|
|
338
|
-
const pct = max > 0 ? Math.round((value / max) * 100) : 0;
|
|
339
|
-
const color = pct >= 80 ? chalk.green : pct >= 50 ? chalk.yellow : chalk.red;
|
|
340
|
-
return color("█".repeat(filled)) + chalk.dim("░".repeat(empty));
|
|
341
|
-
}
|
|
342
109
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
.replace(/([A-Z])/g, " $1")
|
|
346
|
-
.replace(/^./, (c) => c.toUpperCase())
|
|
347
|
-
.trim();
|
|
110
|
+
console.log(chalk.dim("\n Fix, then re-scan to track improvement.\n"));
|
|
111
|
+
}
|
|
348
112
|
}
|
|
349
113
|
|
|
350
114
|
function openInBrowser(filePath) {
|