aeo-ready 1.5.0 → 1.7.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/package.json +1 -1
- package/src/benchmark/agentic-seo.js +88 -30
- package/src/benchmark/index.js +12 -13
- package/src/fix.js +0 -15
- package/src/scan.js +2 -64
package/package.json
CHANGED
|
@@ -12,21 +12,31 @@ const KNOWN_FILES = [
|
|
|
12
12
|
"skill.md",
|
|
13
13
|
"agent-permissions.json",
|
|
14
14
|
"agents.json",
|
|
15
|
+
"agents.txt",
|
|
16
|
+
"openapi.json",
|
|
15
17
|
"sitemap.xml",
|
|
16
18
|
".well-known/ai-plugin.json",
|
|
19
|
+
".well-known/agent-card.json",
|
|
20
|
+
".well-known/mcp/server-card.json",
|
|
21
|
+
".well-known/agent-skills/index.json",
|
|
17
22
|
];
|
|
18
23
|
|
|
19
|
-
async function fetchText(url) {
|
|
24
|
+
async function fetchText(url, headers) {
|
|
20
25
|
try {
|
|
21
|
-
const res = await fetch(url, { redirect: "follow" });
|
|
26
|
+
const res = await fetch(url, { redirect: "follow", headers });
|
|
22
27
|
if (!res.ok) return null;
|
|
23
28
|
return await res.text();
|
|
24
|
-
} catch
|
|
25
|
-
console.warn(`Warning: failed to fetch ${url}: ${err.message}`);
|
|
29
|
+
} catch {
|
|
26
30
|
return null;
|
|
27
31
|
}
|
|
28
32
|
}
|
|
29
33
|
|
|
34
|
+
function saveToDisk(tempDir, relativePath, content) {
|
|
35
|
+
const filePath = join(tempDir, relativePath);
|
|
36
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
37
|
+
writeFileSync(filePath, content);
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
function parseSitemapUrls(xml, baseUrl) {
|
|
31
41
|
const urls = [];
|
|
32
42
|
const matches = xml.matchAll(/<loc>([^<]+)<\/loc>/g);
|
|
@@ -37,25 +47,71 @@ function parseSitemapUrls(xml, baseUrl) {
|
|
|
37
47
|
return urls;
|
|
38
48
|
}
|
|
39
49
|
|
|
40
|
-
function urlToFilePath(url
|
|
50
|
+
function urlToFilePath(url) {
|
|
41
51
|
let path = new URL(url).pathname;
|
|
42
52
|
if (path.endsWith("/")) path += "index.html";
|
|
43
53
|
else if (!path.includes(".")) path += ".html";
|
|
44
54
|
return path.replace(/^\//, "");
|
|
45
55
|
}
|
|
46
56
|
|
|
47
|
-
async function
|
|
48
|
-
const
|
|
57
|
+
async function discoverSkillFiles(baseUrl, tempDir) {
|
|
58
|
+
const indexContent = await fetchText(
|
|
59
|
+
`${baseUrl}/.well-known/agent-skills/index.json`,
|
|
60
|
+
);
|
|
61
|
+
if (!indexContent) return;
|
|
49
62
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
let index;
|
|
64
|
+
try {
|
|
65
|
+
index = JSON.parse(indexContent);
|
|
66
|
+
} catch {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const skills = index.skills || index;
|
|
71
|
+
if (!Array.isArray(skills)) return;
|
|
72
|
+
|
|
73
|
+
const fetches = [];
|
|
74
|
+
for (const skill of skills) {
|
|
75
|
+
const dir = skill.path || skill.id;
|
|
76
|
+
if (!dir) continue;
|
|
77
|
+
|
|
78
|
+
const base = `.well-known/agent-skills/${dir}`;
|
|
79
|
+
fetches.push(
|
|
80
|
+
fetchText(`${baseUrl}/${base}/SKILL.md`).then((content) => {
|
|
81
|
+
if (content) saveToDisk(tempDir, `${base}/SKILL.md`, content);
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const refs = skill.references || [];
|
|
86
|
+
for (const ref of refs) {
|
|
87
|
+
const refPath = typeof ref === "string" ? ref : ref.path || ref.file;
|
|
88
|
+
if (!refPath) continue;
|
|
89
|
+
fetches.push(
|
|
90
|
+
fetchText(`${baseUrl}/${base}/references/${refPath}`).then(
|
|
91
|
+
(content) => {
|
|
92
|
+
if (content)
|
|
93
|
+
saveToDisk(tempDir, `${base}/references/${refPath}`, content);
|
|
94
|
+
},
|
|
95
|
+
),
|
|
96
|
+
);
|
|
56
97
|
}
|
|
57
98
|
}
|
|
58
99
|
|
|
100
|
+
await Promise.all(fetches);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function fetchSiteToDir(baseUrl) {
|
|
104
|
+
const tempDir = mkdtempSync(join(tmpdir(), "aeo-"));
|
|
105
|
+
|
|
106
|
+
await Promise.all(
|
|
107
|
+
KNOWN_FILES.map(async (file) => {
|
|
108
|
+
const content = await fetchText(`${baseUrl}/${file}`);
|
|
109
|
+
if (content) saveToDisk(tempDir, file, content);
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
await discoverSkillFiles(baseUrl, tempDir);
|
|
114
|
+
|
|
59
115
|
const sitemap = await fetchText(`${baseUrl}/sitemap.xml`);
|
|
60
116
|
if (!sitemap) return tempDir;
|
|
61
117
|
|
|
@@ -63,25 +119,27 @@ async function fetchSiteToDir(baseUrl) {
|
|
|
63
119
|
|
|
64
120
|
await Promise.all(
|
|
65
121
|
urls.map(async (url) => {
|
|
66
|
-
const path = urlToFilePath(url
|
|
122
|
+
const path = urlToFilePath(url);
|
|
67
123
|
if (KNOWN_FILES.includes(path)) return;
|
|
68
124
|
|
|
69
|
-
const html = await
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
.replace(/\.html$/, ".md")
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
125
|
+
const [html, md] = await Promise.all([
|
|
126
|
+
fetchText(url),
|
|
127
|
+
fetchText(url, { Accept: "text/markdown" }),
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
if (html) saveToDisk(tempDir, path, html);
|
|
131
|
+
|
|
132
|
+
if (md && md !== html) {
|
|
133
|
+
saveToDisk(tempDir, path.replace(/\.html$/, ".md"), md);
|
|
134
|
+
} else {
|
|
135
|
+
const mdUrl = url
|
|
136
|
+
.replace(/\.html$/, ".md")
|
|
137
|
+
.replace(/\/?$/, (m) => (m === "/" ? "/index.md" : ".md"));
|
|
138
|
+
if (mdUrl !== url) {
|
|
139
|
+
const mdFallback = await fetchText(mdUrl);
|
|
140
|
+
if (mdFallback) {
|
|
141
|
+
saveToDisk(tempDir, path.replace(/\.html$/, ".md"), mdFallback);
|
|
142
|
+
}
|
|
85
143
|
}
|
|
86
144
|
}
|
|
87
145
|
}),
|
package/src/benchmark/index.js
CHANGED
|
@@ -8,24 +8,23 @@ import { runAgentgrade } from "./agentgrade.js";
|
|
|
8
8
|
const REFERENCE_SCORES = {
|
|
9
9
|
agenticSeo: {
|
|
10
10
|
Stripe: 17,
|
|
11
|
-
|
|
12
|
-
Supabase:
|
|
13
|
-
|
|
14
|
-
Average: 25,
|
|
11
|
+
Cloudflare: 20,
|
|
12
|
+
Supabase: 20,
|
|
13
|
+
Average: 19,
|
|
15
14
|
},
|
|
16
15
|
cloudflare: {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
Supabase: 4,
|
|
17
|
+
Cloudflare: 3,
|
|
18
|
+
Vercel: 2,
|
|
19
|
+
Stripe: 1,
|
|
21
20
|
Average: 2,
|
|
22
21
|
},
|
|
23
22
|
fern: {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
Supabase:
|
|
27
|
-
|
|
28
|
-
Average:
|
|
23
|
+
Cloudflare: 85,
|
|
24
|
+
Stripe: 84,
|
|
25
|
+
Supabase: 82,
|
|
26
|
+
Vercel: 75,
|
|
27
|
+
Average: 55,
|
|
29
28
|
},
|
|
30
29
|
};
|
|
31
30
|
|
package/src/fix.js
CHANGED
|
@@ -162,7 +162,6 @@ export async function runFixes(result, dir) {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
const fixed = [];
|
|
165
|
-
const skipped = [];
|
|
166
165
|
const manual = [];
|
|
167
166
|
|
|
168
167
|
for (const [, action] of triggered) {
|
|
@@ -170,10 +169,6 @@ export async function runFixes(result, dir) {
|
|
|
170
169
|
manual.push(action.label);
|
|
171
170
|
continue;
|
|
172
171
|
}
|
|
173
|
-
if (!dir) {
|
|
174
|
-
skipped.push(action.label);
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
172
|
const r = await action.apply(dir, result);
|
|
178
173
|
if (r) fixed.push(r);
|
|
179
174
|
}
|
|
@@ -191,16 +186,6 @@ export async function runFixes(result, dir) {
|
|
|
191
186
|
console.log("");
|
|
192
187
|
}
|
|
193
188
|
|
|
194
|
-
if (skipped.length > 0) {
|
|
195
|
-
console.log(
|
|
196
|
-
chalk.bold(" Skipped") + chalk.dim(" (run with --dir to auto-fix):\n"),
|
|
197
|
-
);
|
|
198
|
-
for (const s of skipped) {
|
|
199
|
-
console.log(` ${chalk.yellow("-")} ${s}`);
|
|
200
|
-
}
|
|
201
|
-
console.log("");
|
|
202
|
-
}
|
|
203
|
-
|
|
204
189
|
if (manual.length > 0) {
|
|
205
190
|
console.log(chalk.bold(" Manual fixes needed:\n"));
|
|
206
191
|
manual.forEach((m, i) => {
|
package/src/scan.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import { createInterface } from "readline";
|
|
3
2
|
import { runAllBenchmarks, printBenchmarks } from "./benchmark/index.js";
|
|
4
3
|
import { saveResult } from "./history/index.js";
|
|
5
|
-
import {
|
|
4
|
+
import { showRecommendations } from "./recommendations.js";
|
|
6
5
|
|
|
7
6
|
export async function scan(opts) {
|
|
8
7
|
const { url, dir, json } = opts;
|
|
@@ -38,7 +37,7 @@ export async function scan(opts) {
|
|
|
38
37
|
await saveResult(result, baseDir);
|
|
39
38
|
|
|
40
39
|
if (!json && averageScore < 100 && process.stdin.isTTY) {
|
|
41
|
-
await
|
|
40
|
+
await showRecommendations(result, dir);
|
|
42
41
|
}
|
|
43
42
|
|
|
44
43
|
return result;
|
|
@@ -84,65 +83,4 @@ function printReport(result) {
|
|
|
84
83
|
console.log(
|
|
85
84
|
` ${chalk.bold("Overall")}${" ".repeat(37)}${gc.bold(`${averageScore}/100`)}\n`,
|
|
86
85
|
);
|
|
87
|
-
|
|
88
|
-
printNextSteps(result);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function printNextSteps(result) {
|
|
92
|
-
const { benchmarks, averageScore } = result;
|
|
93
|
-
const steps = [];
|
|
94
|
-
|
|
95
|
-
if (averageScore < 80) {
|
|
96
|
-
steps.push(["npx agentic-seo init", "scaffold llms.txt, AGENTS.md"]);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const cfFails =
|
|
100
|
-
benchmarks.cloudflare?.checks?.filter((c) => c.status === "fail") || [];
|
|
101
|
-
if (cfFails.length > 0) {
|
|
102
|
-
steps.push([
|
|
103
|
-
`Cloudflare: ${cfFails.length} failing`,
|
|
104
|
-
"see isitagentready.com",
|
|
105
|
-
]);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const fernFails =
|
|
109
|
-
benchmarks.fern?.checks?.filter(
|
|
110
|
-
(c) => c.status === "fail" || c.status === "warn",
|
|
111
|
-
) || [];
|
|
112
|
-
if (fernFails.length > 0) {
|
|
113
|
-
steps.push([
|
|
114
|
-
`npx afdocs check ${result.url}`,
|
|
115
|
-
`${fernFails.length} Fern issues`,
|
|
116
|
-
]);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
steps.push([
|
|
120
|
-
"npx skills add katrinalaszlo/agent-serve",
|
|
121
|
-
"make your product agent-ready",
|
|
122
|
-
]);
|
|
123
|
-
|
|
124
|
-
if (steps.length > 0) {
|
|
125
|
-
console.log(chalk.bold(" Next steps\n"));
|
|
126
|
-
const maxCmd = Math.max(...steps.map(([cmd]) => cmd.length));
|
|
127
|
-
for (const [cmd, desc] of steps) {
|
|
128
|
-
console.log(` ${cmd.padEnd(maxCmd + 4)}${chalk.dim(desc)}`);
|
|
129
|
-
}
|
|
130
|
-
console.log("");
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function ask(question) {
|
|
135
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
136
|
-
return new Promise((resolve) => {
|
|
137
|
-
rl.question(question, (answer) => {
|
|
138
|
-
rl.close();
|
|
139
|
-
resolve(answer.trim().toLowerCase());
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
async function promptFix(result, dir) {
|
|
145
|
-
const answer = await ask(chalk.bold(" Fix now? ") + chalk.dim("[y/N] "));
|
|
146
|
-
if (answer !== "y" && answer !== "yes") return;
|
|
147
|
-
await runFixes(result, dir);
|
|
148
86
|
}
|