aeo-ready 1.1.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.
@@ -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, "&amp;")
68
- .replace(/</g, "&lt;")
69
- .replace(/>/g, "&gt;")
70
- .replace(/"/g, "&quot;");
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, "&amp;")
64
- .replace(/</g, "&lt;")
65
- .replace(/>/g, "&gt;")
66
- .replace(/"/g, "&quot;");
67
- }
@@ -1,196 +0,0 @@
1
- const HOW_TO_FIX = {
2
- "llms.txt": {
3
- time: "1 hour",
4
- steps: [
5
- "Create a file called <code>llms.txt</code> in your site root",
6
- "First line: <code># Your Company Name</code>",
7
- "Second line: one sentence describing what you do and for whom",
8
- "List 3-5 key features, pricing model, and how to get started",
9
- "Keep it under 5,000 tokens — this is NOT your homepage copy",
10
- "Deploy it so it's accessible at <code>yoursite.com/llms.txt</code>",
11
- ],
12
- example: `# Acme Corp\n> API-first billing platform for SaaS companies.\n\n## What it does\n- Usage-based billing with real-time metering\n- Self-serve pricing pages\n- Revenue recognition automation\n\n## Pricing\n- Starter: $0/mo (up to $10K MRR)\n- Growth: $499/mo\n- Enterprise: custom\n\n## Get started\n1. Sign up at acme.com/signup\n2. Install SDK: npm install @acme/billing\n3. Add 3 lines of code to start metering`,
13
- },
14
- "robots.txt AI crawlers": {
15
- time: "5 minutes",
16
- steps: [
17
- "Open your <code>robots.txt</code> file (or create one in your site root)",
18
- "Add explicit Allow rules for each AI crawler",
19
- "Deploy — most hosting platforms serve this automatically from root",
20
- ],
21
- example: `User-agent: GPTBot\nAllow: /\n\nUser-agent: ClaudeBot\nAllow: /\n\nUser-agent: Google-Extended\nAllow: /\n\nUser-agent: PerplexityBot\nAllow: /`,
22
- auto: "Run <code>npx aeo-ready scan --fix</code> to generate this automatically.",
23
- },
24
- "AGENTS.md / CLAUDE.md": {
25
- time: "30 minutes",
26
- steps: [
27
- "Create <code>AGENTS.md</code> in your repo root",
28
- "Describe your project structure — key directories and what's in them",
29
- "List development commands (install, run, test)",
30
- "Note constraints: rate limits, auth requirements, external dependencies",
31
- "Keep it factual and precise — LLM-generated AGENTS.md files hurt agent success rates",
32
- ],
33
- auto: "Run <code>npx aeo-ready scan --fix</code> to get a scaffold, then edit it.",
34
- },
35
- "agents.json manifest": {
36
- time: "20 minutes",
37
- steps: [
38
- "Create <code>.well-known/agent.json</code> in your site root",
39
- "Add name, description, and capabilities array",
40
- "Link to your llms.txt and OpenAPI spec",
41
- "Include contact info and auth method",
42
- ],
43
- example: `{\n "schema_version": "1.0",\n "name": "Your Product",\n "description": "What it does in one sentence",\n "capabilities": ["api-integration", "pricing-lookup"],\n "interfaces": {\n "human": "/",\n "llm": "/llms.txt",\n "api": "/openapi.json"\n }\n}`,
44
- auto: "Run <code>npx aeo-ready scan --fix</code> to generate a draft.",
45
- },
46
- "Question-based headings": {
47
- time: "1-2 hours",
48
- steps: [
49
- "Find your top 10 pages by traffic or importance",
50
- "Rewrite H2/H3 headings as questions users would ask AI",
51
- 'Example: change "Pricing" → "How much does [Product] cost?"',
52
- 'Example: change "Features" → "What can [Product] do?"',
53
- "Aim for 30%+ of headings to be question-based",
54
- ],
55
- },
56
- "Direct answer formatting": {
57
- time: "1-2 hours",
58
- steps: [
59
- "On each key page, add a 40-60 word summary as the first paragraph",
60
- "This paragraph should directly answer the question the page addresses",
61
- "Don't start with background or context — lead with the answer",
62
- "AI systems extract this as the citation snippet",
63
- ],
64
- example: `<!-- Before -->\n<h1>Our Billing Platform</h1>\n<p>Founded in 2020, we set out to solve...</p>\n\n<!-- After -->\n<h1>Our Billing Platform</h1>\n<p>Acme is a usage-based billing platform that helps SaaS companies meter API calls, automate invoicing, and recognize revenue. Plans start at $0/mo for up to $10K MRR.</p>`,
65
- },
66
- "FAQ markup": {
67
- time: "30 minutes",
68
- steps: [
69
- "Identify 5-10 common questions about your product",
70
- "Add them to your page as a FAQ section",
71
- "Wrap them in FAQPage JSON-LD schema",
72
- "Add the script tag to your page's <code>&lt;head&gt;</code>",
73
- ],
74
- example: `<script type="application/ld+json">\n{\n "@context": "https://schema.org",\n "@type": "FAQPage",\n "mainEntity": [{\n "@type": "Question",\n "name": "What does Acme cost?",\n "acceptedAnswer": {\n "@type": "Answer",\n "text": "Acme starts free for up to $10K MRR. Growth is $499/mo."\n }\n }]\n}\n</script>`,
75
- },
76
- "Schema.org markup": {
77
- time: "30 minutes",
78
- steps: [
79
- "Determine your schema type: SoftwareApplication (SaaS), Person (portfolio), Article (blog)",
80
- "Create a JSON-LD block with required fields (name, description, url)",
81
- "Add it to your homepage <code>&lt;head&gt;</code> in a script tag",
82
- "For SaaS: include <code>offers</code> with pricing per plan",
83
- ],
84
- example: `<script type="application/ld+json">\n{\n "@context": "https://schema.org",\n "@type": "SoftwareApplication",\n "name": "Your Product",\n "description": "One sentence",\n "url": "https://yoursite.com",\n "offers": [{\n "@type": "Offer",\n "name": "Starter",\n "price": "0",\n "priceCurrency": "USD"\n }]\n}\n</script>`,
85
- },
86
- "E-E-A-T signals": {
87
- time: "1 hour",
88
- steps: [
89
- "Add author bios with name, role, and credentials to content pages",
90
- "Add a Person JSON-LD schema for each author",
91
- "Include first-hand experience markers: 'We built...', 'In our experience...'",
92
- "Link to author profiles on LinkedIn, GitHub, Twitter",
93
- ],
94
- },
95
- "Content negotiation": {
96
- time: "2-4 hours (requires code)",
97
- steps: [
98
- "Add middleware that checks the <code>Accept</code> header on requests",
99
- "When <code>Accept: text/markdown</code> is present, return markdown instead of HTML",
100
- "Strip navigation, footers, and chrome — return just the content",
101
- "This saves agents thousands of tokens vs parsing your HTML",
102
- ],
103
- },
104
- "Definitive statements": {
105
- time: "1-2 hours",
106
- steps: [
107
- "Search your content for hedging words: 'might', 'perhaps', 'possibly', 'could be'",
108
- "Replace with direct claims: 'is', 'does', 'provides', 'enables'",
109
- "AI cites confident statements over uncertain ones",
110
- "Back claims with data: '50% faster' is better than 'significantly faster'",
111
- ],
112
- },
113
- };
114
-
115
- export function renderRecommendations(result) {
116
- const failed = [];
117
-
118
- for (const scorecard of [result.agentReadiness, result.aiVisibility]) {
119
- for (const [catName, cat] of Object.entries(scorecard.categories)) {
120
- for (const check of cat.checks) {
121
- if (!check.passed && check.fix) {
122
- const impact = check.maxPoints - (check.points || 0);
123
- failed.push({ ...check, category: catName, impact });
124
- }
125
- }
126
- }
127
- }
128
-
129
- failed.sort((a, b) => b.impact - a.impact);
130
- const top = failed.slice(0, 8);
131
-
132
- if (top.length === 0) {
133
- return `<h2 id="recommendations">Recommendations</h2>\n<p style="color:#3fb950;font-size:13px;">All checks passing. Nice.</p>`;
134
- }
135
-
136
- let html = `<h2 id="recommendations">How To Fix It</h2>
137
- <p style="color:#8b949e;font-size:12px;margin-bottom:16px;">Sorted by point impact. Expand each for step-by-step instructions.</p>`;
138
-
139
- for (const rec of top) {
140
- const category = formatName(rec.category);
141
- const howTo = HOW_TO_FIX[rec.name];
142
-
143
- html += `\n<details class="rec" style="cursor:pointer;">
144
- <summary style="display:flex;justify-content:space-between;align-items:center;">
145
- <div>
146
- <div class="rec-title">${esc(rec.name)}</div>
147
- <div class="rec-fix">${esc(rec.fix)}</div>
148
- </div>
149
- <div style="text-align:right;white-space:nowrap;">
150
- <span class="rec-impact">+${rec.impact} pts</span>
151
- ${howTo?.time ? `<div style="font-size:11px;color:#8b949e;">${howTo.time}</div>` : ""}
152
- </div>
153
- </summary>`;
154
-
155
- if (howTo) {
156
- html += `\n <div style="margin-top:12px;padding-top:12px;border-top:1px solid #21262d;">`;
157
-
158
- if (howTo.auto) {
159
- html += `\n <div style="background:#3fb95010;border:1px solid #3fb95033;border-radius:4px;padding:8px 12px;margin-bottom:12px;font-size:12px;color:#3fb950;">${howTo.auto}</div>`;
160
- }
161
-
162
- html += `\n <div style="font-size:12px;color:#c9d1d9;"><strong>Steps:</strong></div>
163
- <ol style="margin:8px 0 0 20px;font-size:12px;color:#8b949e;">`;
164
- for (const step of howTo.steps) {
165
- html += `\n <li style="margin:4px 0;">${step}</li>`;
166
- }
167
- html += `\n </ol>`;
168
-
169
- if (howTo.example) {
170
- html += `\n <div style="margin-top:12px;font-size:12px;color:#c9d1d9;"><strong>Example:</strong></div>
171
- <pre style="background:#0d1117;border:1px solid #21262d;border-radius:4px;padding:12px;margin-top:6px;font-size:11px;color:#79c0ff;overflow-x:auto;white-space:pre-wrap;">${esc(howTo.example)}</pre>`;
172
- }
173
-
174
- html += `\n </div>`;
175
- }
176
-
177
- html += `\n</details>`;
178
- }
179
-
180
- return html;
181
- }
182
-
183
- function formatName(camelCase) {
184
- return camelCase
185
- .replace(/([A-Z])/g, " $1")
186
- .replace(/^./, (c) => c.toUpperCase())
187
- .trim();
188
- }
189
-
190
- function esc(str) {
191
- return str
192
- .replace(/&/g, "&amp;")
193
- .replace(/</g, "&lt;")
194
- .replace(/>/g, "&gt;")
195
- .replace(/"/g, "&quot;");
196
- }
@@ -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
- }