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
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
export async function runStructuredDataChecks(context) {
|
|
2
|
-
const checks = [
|
|
3
|
-
checkSchemaOrg(context),
|
|
4
|
-
checkFaqMarkup(context),
|
|
5
|
-
checkRichSchemas(context),
|
|
6
|
-
checkOpenGraph(context),
|
|
7
|
-
];
|
|
8
|
-
|
|
9
|
-
const score = checks.reduce((sum, c) => sum + (c.passed ? c.points : 0), 0);
|
|
10
|
-
return { score, maxScore: 12, checks };
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function checkSchemaOrg(context) {
|
|
14
|
-
const allContent = getAllContent(context);
|
|
15
|
-
const html = context.html || "";
|
|
16
|
-
|
|
17
|
-
const jsonLdBlocks = extractJsonLd(html);
|
|
18
|
-
if (jsonLdBlocks.length === 0 && !allContent.includes("schema.org")) {
|
|
19
|
-
return fail(
|
|
20
|
-
"Schema.org markup",
|
|
21
|
-
4,
|
|
22
|
-
"No schema.org structured data found. Add JSON-LD with appropriate type (SoftwareApplication, Person, Article, Organization).",
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
let points = 2;
|
|
27
|
-
const types = jsonLdBlocks.map((b) => b["@type"]).filter(Boolean);
|
|
28
|
-
const hasCorrectType = hasAppropriateType(types, context.siteType);
|
|
29
|
-
if (hasCorrectType) points += 1;
|
|
30
|
-
const hasRequiredFields = jsonLdBlocks.some((b) => hasMinimumFields(b));
|
|
31
|
-
if (hasRequiredFields) points += 1;
|
|
32
|
-
|
|
33
|
-
if (points === 4) return pass("Schema.org markup", 4);
|
|
34
|
-
const issues = [];
|
|
35
|
-
if (!hasCorrectType) issues.push("type doesn't match site category");
|
|
36
|
-
if (!hasRequiredFields) issues.push("missing required fields");
|
|
37
|
-
return partial(
|
|
38
|
-
"Schema.org markup",
|
|
39
|
-
points,
|
|
40
|
-
4,
|
|
41
|
-
`Schema.org present but: ${issues.join(", ")}.`,
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function checkFaqMarkup(context) {
|
|
46
|
-
const html = context.html || "";
|
|
47
|
-
const allContent = getAllContent(context);
|
|
48
|
-
|
|
49
|
-
const hasFaqSchema =
|
|
50
|
-
allContent.includes("FAQPage") || allContent.includes("Question");
|
|
51
|
-
const hasFaqContent = /faq|frequently asked|common questions/i.test(
|
|
52
|
-
allContent,
|
|
53
|
-
);
|
|
54
|
-
|
|
55
|
-
if (hasFaqSchema) return pass("FAQ markup", 3);
|
|
56
|
-
if (hasFaqContent) {
|
|
57
|
-
return partial(
|
|
58
|
-
"FAQ markup",
|
|
59
|
-
1,
|
|
60
|
-
3,
|
|
61
|
-
"FAQ content exists but not marked up with FAQPage schema. Add JSON-LD FAQPage.",
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
return fail(
|
|
65
|
-
"FAQ markup",
|
|
66
|
-
3,
|
|
67
|
-
"No FAQ markup. Add FAQPage schema for common questions — these surface directly in AI answers.",
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function checkRichSchemas(context) {
|
|
72
|
-
const allContent = getAllContent(context);
|
|
73
|
-
|
|
74
|
-
const schemas = [];
|
|
75
|
-
if (allContent.includes("BreadcrumbList")) schemas.push("Breadcrumb");
|
|
76
|
-
if (allContent.includes("HowTo")) schemas.push("HowTo");
|
|
77
|
-
if (
|
|
78
|
-
allContent.includes("Product") ||
|
|
79
|
-
allContent.includes("SoftwareApplication")
|
|
80
|
-
)
|
|
81
|
-
schemas.push("Product");
|
|
82
|
-
if (allContent.includes("VideoObject")) schemas.push("Video");
|
|
83
|
-
if (allContent.includes("Review") || allContent.includes("AggregateRating"))
|
|
84
|
-
schemas.push("Review");
|
|
85
|
-
|
|
86
|
-
if (schemas.length >= 2)
|
|
87
|
-
return pass("Rich schemas (Breadcrumb/HowTo/Product)", 3);
|
|
88
|
-
if (schemas.length === 1)
|
|
89
|
-
return partial(
|
|
90
|
-
"Rich schemas (Breadcrumb/HowTo/Product)",
|
|
91
|
-
2,
|
|
92
|
-
3,
|
|
93
|
-
`Only ${schemas[0]} schema found. Add more rich schemas for better AI comprehension.`,
|
|
94
|
-
);
|
|
95
|
-
return fail(
|
|
96
|
-
"Rich schemas (Breadcrumb/HowTo/Product)",
|
|
97
|
-
3,
|
|
98
|
-
"No rich schemas (BreadcrumbList, HowTo, Product). These help AI understand page context and relationships.",
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function checkOpenGraph(context) {
|
|
103
|
-
const html = context.html || "";
|
|
104
|
-
|
|
105
|
-
const hasOgTitle = /og:title/i.test(html);
|
|
106
|
-
const hasOgDesc = /og:description/i.test(html);
|
|
107
|
-
const hasMetaDesc = /name=["']description["']/i.test(html);
|
|
108
|
-
|
|
109
|
-
let points = 0;
|
|
110
|
-
if (hasOgTitle) points += 1;
|
|
111
|
-
if (hasOgDesc || hasMetaDesc) points += 1;
|
|
112
|
-
|
|
113
|
-
if (points === 2) return pass("Open Graph + meta description", 2);
|
|
114
|
-
if (points === 1)
|
|
115
|
-
return partial(
|
|
116
|
-
"Open Graph + meta description",
|
|
117
|
-
1,
|
|
118
|
-
2,
|
|
119
|
-
"Partial OG/meta tags. Need both og:title and description.",
|
|
120
|
-
);
|
|
121
|
-
return fail(
|
|
122
|
-
"Open Graph + meta description",
|
|
123
|
-
2,
|
|
124
|
-
"No Open Graph or meta description tags. AI systems use these for page summaries.",
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function extractJsonLd(html) {
|
|
129
|
-
const blocks = [];
|
|
130
|
-
const regex =
|
|
131
|
-
/<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
|
|
132
|
-
let match;
|
|
133
|
-
while ((match = regex.exec(html)) !== null) {
|
|
134
|
-
try {
|
|
135
|
-
const parsed = JSON.parse(match[1]);
|
|
136
|
-
if (Array.isArray(parsed)) blocks.push(...parsed);
|
|
137
|
-
else blocks.push(parsed);
|
|
138
|
-
} catch {
|
|
139
|
-
/* skip invalid */
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return blocks;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function hasAppropriateType(types, siteType) {
|
|
146
|
-
const typeMap = {
|
|
147
|
-
saas: ["SoftwareApplication", "WebApplication", "Product", "Organization"],
|
|
148
|
-
api: ["SoftwareApplication", "WebAPI", "TechArticle"],
|
|
149
|
-
content: ["Article", "BlogPosting", "WebSite", "Organization"],
|
|
150
|
-
personal: ["Person", "ProfilePage", "WebSite"],
|
|
151
|
-
};
|
|
152
|
-
const expected = typeMap[siteType] || [];
|
|
153
|
-
return types.some((t) => expected.includes(t));
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function hasMinimumFields(block) {
|
|
157
|
-
const required = ["name", "@type"];
|
|
158
|
-
return required.every((f) => block[f]);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function getAllContent(context) {
|
|
162
|
-
if (context.mode === "url") {
|
|
163
|
-
return Object.values(context.pages || {})
|
|
164
|
-
.map((p) => p?.text || "")
|
|
165
|
-
.join("\n");
|
|
166
|
-
}
|
|
167
|
-
return Object.values(context.fileContents || {}).join("\n");
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function pass(name, points) {
|
|
171
|
-
return { name, passed: true, points, maxPoints: points };
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function partial(name, points, maxPoints, fix) {
|
|
175
|
-
return { name, passed: false, points, maxPoints, fix };
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function fail(name, maxPoints, fix) {
|
|
179
|
-
return { name, passed: false, points: 0, maxPoints, fix };
|
|
180
|
-
}
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
export function renderAgentReadiness(result) {
|
|
2
|
-
return renderScorecard(
|
|
3
|
-
"Agent Readiness",
|
|
4
|
-
result.agentReadiness,
|
|
5
|
-
"agent-readiness",
|
|
6
|
-
);
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function renderScorecard(title, scorecard, id) {
|
|
10
|
-
const pct = Math.round((scorecard.score / scorecard.maxScore) * 100);
|
|
11
|
-
const gradeClass =
|
|
12
|
-
pct >= 80
|
|
13
|
-
? "grade-a"
|
|
14
|
-
: pct >= 60
|
|
15
|
-
? "grade-b"
|
|
16
|
-
: pct >= 40
|
|
17
|
-
? "grade-c"
|
|
18
|
-
: "grade-d";
|
|
19
|
-
|
|
20
|
-
let html = `<div class="scorecard" id="${id}">
|
|
21
|
-
<div class="scorecard-header">
|
|
22
|
-
<h3>${title}</h3>
|
|
23
|
-
<span class="pct ${gradeClass}">${scorecard.score}/${scorecard.maxScore} (${pct}%)</span>
|
|
24
|
-
</div>`;
|
|
25
|
-
|
|
26
|
-
for (const [catName, cat] of Object.entries(scorecard.categories)) {
|
|
27
|
-
const label = formatName(catName);
|
|
28
|
-
const catPct = Math.round((cat.score / cat.maxScore) * 100);
|
|
29
|
-
const barColor =
|
|
30
|
-
catPct >= 80 ? "#3fb950" : catPct >= 50 ? "#d29922" : "#f85149";
|
|
31
|
-
|
|
32
|
-
html += `\n <div class="category">
|
|
33
|
-
<div class="cat-header">
|
|
34
|
-
<span class="cat-name">${label}</span>
|
|
35
|
-
<span class="cat-score">${cat.score}/${cat.maxScore}</span>
|
|
36
|
-
<div class="cat-bar"><div class="cat-bar-fill" style="width:${catPct}%;background:${barColor}"></div></div>
|
|
37
|
-
</div>`;
|
|
38
|
-
|
|
39
|
-
for (const check of cat.checks) {
|
|
40
|
-
if (check.passed) {
|
|
41
|
-
html += `\n <div class="check pass">+ ${check.name} [${check.points}]</div>`;
|
|
42
|
-
} else {
|
|
43
|
-
html += `\n <div class="check fail">- ${check.name} [${check.points || 0}/${check.maxPoints}]`;
|
|
44
|
-
if (check.fix) {
|
|
45
|
-
html += `<span class="fix">${escapeHtml(check.fix)}</span>`;
|
|
46
|
-
}
|
|
47
|
-
html += `</div>`;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
html += `\n </div>`;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
html += `\n</div>`;
|
|
55
|
-
return html;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function formatName(camelCase) {
|
|
59
|
-
return camelCase
|
|
60
|
-
.replace(/([A-Z])/g, " $1")
|
|
61
|
-
.replace(/^./, (c) => c.toUpperCase())
|
|
62
|
-
.trim();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function escapeHtml(str) {
|
|
66
|
-
return str
|
|
67
|
-
.replace(/&/g, "&")
|
|
68
|
-
.replace(/</g, "<")
|
|
69
|
-
.replace(/>/g, ">")
|
|
70
|
-
.replace(/"/g, """);
|
|
71
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
export function renderAiVisibility(result) {
|
|
2
|
-
return renderScorecard("AI Visibility", result.aiVisibility, "ai-visibility");
|
|
3
|
-
}
|
|
4
|
-
|
|
5
|
-
function renderScorecard(title, scorecard, id) {
|
|
6
|
-
const pct = Math.round((scorecard.score / scorecard.maxScore) * 100);
|
|
7
|
-
const gradeClass =
|
|
8
|
-
pct >= 80
|
|
9
|
-
? "grade-a"
|
|
10
|
-
: pct >= 60
|
|
11
|
-
? "grade-b"
|
|
12
|
-
: pct >= 40
|
|
13
|
-
? "grade-c"
|
|
14
|
-
: "grade-d";
|
|
15
|
-
|
|
16
|
-
let html = `<div class="scorecard" id="${id}">
|
|
17
|
-
<div class="scorecard-header">
|
|
18
|
-
<h3>${title}</h3>
|
|
19
|
-
<span class="pct ${gradeClass}">${scorecard.score}/${scorecard.maxScore} (${pct}%)</span>
|
|
20
|
-
</div>`;
|
|
21
|
-
|
|
22
|
-
for (const [catName, cat] of Object.entries(scorecard.categories)) {
|
|
23
|
-
const label = formatName(catName);
|
|
24
|
-
const catPct = Math.round((cat.score / cat.maxScore) * 100);
|
|
25
|
-
const barColor =
|
|
26
|
-
catPct >= 80 ? "#3fb950" : catPct >= 50 ? "#d29922" : "#f85149";
|
|
27
|
-
|
|
28
|
-
html += `\n <div class="category">
|
|
29
|
-
<div class="cat-header">
|
|
30
|
-
<span class="cat-name">${label}</span>
|
|
31
|
-
<span class="cat-score">${cat.score}/${cat.maxScore}</span>
|
|
32
|
-
<div class="cat-bar"><div class="cat-bar-fill" style="width:${catPct}%;background:${barColor}"></div></div>
|
|
33
|
-
</div>`;
|
|
34
|
-
|
|
35
|
-
for (const check of cat.checks) {
|
|
36
|
-
if (check.passed) {
|
|
37
|
-
html += `\n <div class="check pass">+ ${check.name} [${check.points}]</div>`;
|
|
38
|
-
} else {
|
|
39
|
-
html += `\n <div class="check fail">- ${check.name} [${check.points || 0}/${check.maxPoints}]`;
|
|
40
|
-
if (check.fix) {
|
|
41
|
-
html += `<span class="fix">${escapeHtml(check.fix)}</span>`;
|
|
42
|
-
}
|
|
43
|
-
html += `</div>`;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
html += `\n </div>`;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
html += `\n</div>`;
|
|
51
|
-
return html;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function formatName(camelCase) {
|
|
55
|
-
return camelCase
|
|
56
|
-
.replace(/([A-Z])/g, " $1")
|
|
57
|
-
.replace(/^./, (c) => c.toUpperCase())
|
|
58
|
-
.trim();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function escapeHtml(str) {
|
|
62
|
-
return str
|
|
63
|
-
.replace(/&/g, "&")
|
|
64
|
-
.replace(/</g, "<")
|
|
65
|
-
.replace(/>/g, ">")
|
|
66
|
-
.replace(/"/g, """);
|
|
67
|
-
}
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
export function renderRecommendations(result) {
|
|
2
|
-
const failed = [];
|
|
3
|
-
|
|
4
|
-
for (const scorecard of [result.agentReadiness, result.aiVisibility]) {
|
|
5
|
-
for (const [catName, cat] of Object.entries(scorecard.categories)) {
|
|
6
|
-
for (const check of cat.checks) {
|
|
7
|
-
if (!check.passed && check.fix) {
|
|
8
|
-
const impact = check.maxPoints - (check.points || 0);
|
|
9
|
-
failed.push({ ...check, category: catName, impact });
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
failed.sort((a, b) => b.impact - a.impact);
|
|
16
|
-
const top = failed.slice(0, 8);
|
|
17
|
-
|
|
18
|
-
if (top.length === 0) {
|
|
19
|
-
return `<h2 id="recommendations">Recommendations</h2>\n<p style="color:#3fb950;font-size:13px;">All checks passing. Nice.</p>`;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
let html = `<h2 id="recommendations">Top Recommendations</h2>\n<p style="color:#8b949e;font-size:12px;margin-bottom:12px;">Sorted by point impact — fix these first.</p>`;
|
|
23
|
-
|
|
24
|
-
for (const rec of top) {
|
|
25
|
-
const category = formatName(rec.category);
|
|
26
|
-
html += `\n<div class="rec">
|
|
27
|
-
<div class="rec-title">${escapeHtml(rec.name)}</div>
|
|
28
|
-
<div class="rec-fix">${escapeHtml(rec.fix)}</div>
|
|
29
|
-
<div class="rec-impact">+${rec.impact} points · ${category}</div>
|
|
30
|
-
</div>`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return html;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function formatName(camelCase) {
|
|
37
|
-
return camelCase
|
|
38
|
-
.replace(/([A-Z])/g, " $1")
|
|
39
|
-
.replace(/^./, (c) => c.toUpperCase())
|
|
40
|
-
.trim();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function escapeHtml(str) {
|
|
44
|
-
return str
|
|
45
|
-
.replace(/&/g, "&")
|
|
46
|
-
.replace(/</g, "<")
|
|
47
|
-
.replace(/>/g, ">")
|
|
48
|
-
.replace(/"/g, """);
|
|
49
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
|
|
4
|
-
export function generateAgentsJson(check, scanResult, dir) {
|
|
5
|
-
const { siteType, target } = scanResult;
|
|
6
|
-
|
|
7
|
-
const hostname = extractHostname(target);
|
|
8
|
-
const name = inferName(dir, hostname);
|
|
9
|
-
|
|
10
|
-
const manifest = {
|
|
11
|
-
schema_version: "1.0",
|
|
12
|
-
name,
|
|
13
|
-
description: "",
|
|
14
|
-
url: target || "",
|
|
15
|
-
site_type: siteType,
|
|
16
|
-
interfaces: {
|
|
17
|
-
human: "/",
|
|
18
|
-
llm: "/llms.txt",
|
|
19
|
-
structured: "/.well-known/agent.json",
|
|
20
|
-
},
|
|
21
|
-
capabilities: getCapabilities(siteType),
|
|
22
|
-
contact: {},
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
if (siteType === "saas" || siteType === "api") {
|
|
26
|
-
manifest.api_base = "";
|
|
27
|
-
manifest.openapi = "/openapi.json";
|
|
28
|
-
manifest.protocols = ["rest"];
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return {
|
|
32
|
-
file: ".well-known/agent.json",
|
|
33
|
-
description: `Agent manifest for ${siteType} site — fill in description and contact`,
|
|
34
|
-
draft: true,
|
|
35
|
-
content: JSON.stringify(manifest, null, 2) + "\n",
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function getCapabilities(siteType) {
|
|
40
|
-
switch (siteType) {
|
|
41
|
-
case "saas":
|
|
42
|
-
return ["pricing-lookup", "api-integration", "trial-signup"];
|
|
43
|
-
case "api":
|
|
44
|
-
return ["api-integration", "sdk-install", "docs-lookup"];
|
|
45
|
-
case "content":
|
|
46
|
-
return ["content-search", "topic-lookup", "citation"];
|
|
47
|
-
case "personal":
|
|
48
|
-
return ["contact", "expertise-lookup", "project-listing"];
|
|
49
|
-
default:
|
|
50
|
-
return [];
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function extractHostname(target) {
|
|
55
|
-
if (!target) return null;
|
|
56
|
-
try {
|
|
57
|
-
return new URL(target).hostname.replace("www.", "");
|
|
58
|
-
} catch {
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function inferName(dir, hostname) {
|
|
64
|
-
const pkgPath = join(dir, "package.json");
|
|
65
|
-
if (existsSync(pkgPath)) {
|
|
66
|
-
try {
|
|
67
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
68
|
-
if (pkg.name) return pkg.name;
|
|
69
|
-
} catch {}
|
|
70
|
-
}
|
|
71
|
-
if (hostname) return hostname.split(".")[0];
|
|
72
|
-
return "";
|
|
73
|
-
}
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
|
|
4
|
-
export function generateAgentsMd(check, scanResult, dir) {
|
|
5
|
-
const { siteType, target } = scanResult;
|
|
6
|
-
const name = inferName(dir);
|
|
7
|
-
const structure = inferStructure(dir);
|
|
8
|
-
|
|
9
|
-
const content = `# ${name || "[Project Name]"}
|
|
10
|
-
|
|
11
|
-
> [One sentence: what this project does]
|
|
12
|
-
|
|
13
|
-
## Project structure
|
|
14
|
-
|
|
15
|
-
${structure || "```\n[Add key directories and their purpose]\n```"}
|
|
16
|
-
|
|
17
|
-
## Key files
|
|
18
|
-
|
|
19
|
-
- Entry point: [path]
|
|
20
|
-
- Config: [path]
|
|
21
|
-
- API routes: [path]
|
|
22
|
-
|
|
23
|
-
## Development
|
|
24
|
-
|
|
25
|
-
\`\`\`bash
|
|
26
|
-
[install command]
|
|
27
|
-
[run command]
|
|
28
|
-
\`\`\`
|
|
29
|
-
|
|
30
|
-
## Conventions
|
|
31
|
-
|
|
32
|
-
- [Language/framework convention 1]
|
|
33
|
-
- [Language/framework convention 2]
|
|
34
|
-
|
|
35
|
-
## Constraints
|
|
36
|
-
|
|
37
|
-
- [Rate limits, auth requirements, etc]
|
|
38
|
-
- [External dependencies]
|
|
39
|
-
`;
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
file: "AGENTS.md",
|
|
43
|
-
description: "Draft AGENTS.md — REVIEW AND EDIT with real project details",
|
|
44
|
-
draft: true,
|
|
45
|
-
content,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function inferName(dir) {
|
|
50
|
-
const pkgPath = join(dir, "package.json");
|
|
51
|
-
if (existsSync(pkgPath)) {
|
|
52
|
-
try {
|
|
53
|
-
return JSON.parse(readFileSync(pkgPath, "utf8")).name || "";
|
|
54
|
-
} catch {}
|
|
55
|
-
}
|
|
56
|
-
const pomPath = join(dir, "pom.xml");
|
|
57
|
-
if (existsSync(pomPath)) {
|
|
58
|
-
const pom = readFileSync(pomPath, "utf8");
|
|
59
|
-
const match = pom.match(/<artifactId>([^<]+)<\/artifactId>/);
|
|
60
|
-
if (match) return match[1];
|
|
61
|
-
}
|
|
62
|
-
return "";
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function inferStructure(dir) {
|
|
66
|
-
try {
|
|
67
|
-
const entries = readdirSync(dir, { withFileTypes: true });
|
|
68
|
-
const dirs = entries
|
|
69
|
-
.filter(
|
|
70
|
-
(e) =>
|
|
71
|
-
e.isDirectory() &&
|
|
72
|
-
!e.name.startsWith(".") &&
|
|
73
|
-
e.name !== "node_modules",
|
|
74
|
-
)
|
|
75
|
-
.map((e) => e.name)
|
|
76
|
-
.slice(0, 10);
|
|
77
|
-
|
|
78
|
-
if (dirs.length === 0) return null;
|
|
79
|
-
|
|
80
|
-
const lines = dirs.map((d) => `${d}/`);
|
|
81
|
-
return "```\n" + lines.join("\n") + "\n```";
|
|
82
|
-
} catch {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "fs";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
|
|
4
|
-
export function generateLlmsTxt(check, scanResult, dir) {
|
|
5
|
-
const { siteType, target } = scanResult;
|
|
6
|
-
const hostname = extractHostname(target);
|
|
7
|
-
const name = inferName(dir, hostname);
|
|
8
|
-
|
|
9
|
-
const content = generateByType(siteType, name, target);
|
|
10
|
-
|
|
11
|
-
return {
|
|
12
|
-
file: "llms.txt",
|
|
13
|
-
description: `Draft llms.txt for ${siteType} site — REVIEW AND EDIT before publishing`,
|
|
14
|
-
draft: true,
|
|
15
|
-
content,
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function generateByType(siteType, name, target) {
|
|
20
|
-
const base = target || "https://example.com";
|
|
21
|
-
|
|
22
|
-
switch (siteType) {
|
|
23
|
-
case "saas":
|
|
24
|
-
return `# ${name || "[Company Name]"}
|
|
25
|
-
|
|
26
|
-
> [One sentence: what problem you solve and for whom]
|
|
27
|
-
|
|
28
|
-
## What it does
|
|
29
|
-
- [Core capability 1]
|
|
30
|
-
- [Core capability 2]
|
|
31
|
-
- [Core capability 3]
|
|
32
|
-
|
|
33
|
-
## Pricing
|
|
34
|
-
- [Plan 1]: $X/mo — [what's included]
|
|
35
|
-
- [Plan 2]: $X/mo — [what's included]
|
|
36
|
-
- Details: ${base}/pricing
|
|
37
|
-
|
|
38
|
-
## Integration
|
|
39
|
-
- API docs: ${base}/docs
|
|
40
|
-
- OpenAPI spec: ${base}/openapi.json
|
|
41
|
-
- Auth: [API key / OAuth / etc]
|
|
42
|
-
|
|
43
|
-
## Get started
|
|
44
|
-
1. [Signup step]
|
|
45
|
-
2. [First API call or setup]
|
|
46
|
-
3. [See results]
|
|
47
|
-
|
|
48
|
-
## Links
|
|
49
|
-
- Documentation: ${base}/docs
|
|
50
|
-
- Changelog: ${base}/changelog
|
|
51
|
-
- Status: ${base}/status
|
|
52
|
-
`;
|
|
53
|
-
|
|
54
|
-
case "api":
|
|
55
|
-
return `# ${name || "[Tool Name]"}
|
|
56
|
-
|
|
57
|
-
> [One sentence: what this tool does]
|
|
58
|
-
|
|
59
|
-
## Quick start
|
|
60
|
-
\`\`\`bash
|
|
61
|
-
[install command]
|
|
62
|
-
\`\`\`
|
|
63
|
-
|
|
64
|
-
## Key concepts
|
|
65
|
-
- [Concept 1]: [one-line explanation]
|
|
66
|
-
- [Concept 2]: [one-line explanation]
|
|
67
|
-
|
|
68
|
-
## API reference
|
|
69
|
-
- Base URL: ${base}/api/v1
|
|
70
|
-
- Auth: [method]
|
|
71
|
-
- OpenAPI: ${base}/openapi.json
|
|
72
|
-
|
|
73
|
-
## SDKs
|
|
74
|
-
- Node.js: \`npm install [package]\`
|
|
75
|
-
- Python: \`pip install [package]\`
|
|
76
|
-
|
|
77
|
-
## Links
|
|
78
|
-
- Full docs: ${base}/docs
|
|
79
|
-
- Examples: ${base}/examples
|
|
80
|
-
- Changelog: ${base}/changelog
|
|
81
|
-
`;
|
|
82
|
-
|
|
83
|
-
case "personal":
|
|
84
|
-
return `# ${name || "[Your Name]"}
|
|
85
|
-
|
|
86
|
-
> [Role/title] — [what you're known for]
|
|
87
|
-
|
|
88
|
-
## Expertise
|
|
89
|
-
- [Area 1]
|
|
90
|
-
- [Area 2]
|
|
91
|
-
- [Area 3]
|
|
92
|
-
|
|
93
|
-
## Notable work
|
|
94
|
-
- [Project 1]: [one-line description] — [url]
|
|
95
|
-
- [Project 2]: [one-line description] — [url]
|
|
96
|
-
|
|
97
|
-
## Writing & speaking
|
|
98
|
-
- [Topic you cover]
|
|
99
|
-
- [Where to find your content]
|
|
100
|
-
|
|
101
|
-
## Contact
|
|
102
|
-
- Email: [email]
|
|
103
|
-
- LinkedIn: [url]
|
|
104
|
-
- Available for: [consulting / speaking / hiring / etc]
|
|
105
|
-
`;
|
|
106
|
-
|
|
107
|
-
case "content":
|
|
108
|
-
return `# ${name || "[Publication Name]"}
|
|
109
|
-
|
|
110
|
-
> [What topics you cover and your angle/expertise]
|
|
111
|
-
|
|
112
|
-
## Topics
|
|
113
|
-
- [Topic 1]: [what you cover, how many pieces]
|
|
114
|
-
- [Topic 2]: [what you cover, how many pieces]
|
|
115
|
-
|
|
116
|
-
## Best starting points
|
|
117
|
-
- [Most important article]: ${base}/[path]
|
|
118
|
-
- [Second article]: ${base}/[path]
|
|
119
|
-
- [Third article]: ${base}/[path]
|
|
120
|
-
|
|
121
|
-
## About
|
|
122
|
-
- Author(s): [who writes]
|
|
123
|
-
- Cadence: [how often you publish]
|
|
124
|
-
- Angle: [what makes your coverage unique]
|
|
125
|
-
|
|
126
|
-
## Links
|
|
127
|
-
- All articles: ${base}/blog
|
|
128
|
-
- RSS: ${base}/feed.xml
|
|
129
|
-
- Newsletter: ${base}/subscribe
|
|
130
|
-
`;
|
|
131
|
-
|
|
132
|
-
default:
|
|
133
|
-
return `# ${name || "[Site Name]"}
|
|
134
|
-
|
|
135
|
-
> [One sentence: what this site is and who it's for]
|
|
136
|
-
|
|
137
|
-
## What you'll find here
|
|
138
|
-
- [Section 1]: [description]
|
|
139
|
-
- [Section 2]: [description]
|
|
140
|
-
|
|
141
|
-
## Links
|
|
142
|
-
- Homepage: ${base}
|
|
143
|
-
`;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function extractHostname(target) {
|
|
148
|
-
if (!target) return null;
|
|
149
|
-
try {
|
|
150
|
-
return new URL(target).hostname.replace("www.", "");
|
|
151
|
-
} catch {
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function inferName(dir, hostname) {
|
|
157
|
-
const pkgPath = join(dir, "package.json");
|
|
158
|
-
if (existsSync(pkgPath)) {
|
|
159
|
-
try {
|
|
160
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
161
|
-
if (pkg.name) return pkg.name;
|
|
162
|
-
} catch {}
|
|
163
|
-
}
|
|
164
|
-
if (hostname) return hostname.split(".")[0];
|
|
165
|
-
return "";
|
|
166
|
-
}
|