@aexol/spectral 0.9.42 → 0.9.44
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/dist/extensions/aexol-mcp.d.ts.map +1 -1
- package/dist/extensions/aexol-mcp.js +44 -9
- package/dist/extensions/browser/browser-service.d.ts +4 -1
- package/dist/extensions/browser/browser-service.d.ts.map +1 -1
- package/dist/extensions/browser/browser-service.js +34 -7
- package/dist/extensions/browser/config.d.ts +3 -0
- package/dist/extensions/browser/config.d.ts.map +1 -1
- package/dist/extensions/browser/config.js +1 -0
- package/dist/extensions/browser/index.d.ts.map +1 -1
- package/dist/extensions/browser/index.js +3 -0
- package/dist/extensions/browser/record/record-tool.d.ts.map +1 -1
- package/dist/extensions/browser/record/record-tool.js +27 -4
- package/dist/extensions/browser/scenario/markdown-parser.d.ts +44 -0
- package/dist/extensions/browser/scenario/markdown-parser.d.ts.map +1 -0
- package/dist/extensions/browser/scenario/markdown-parser.js +159 -0
- package/dist/extensions/browser/scenario/scenario-tool.d.ts +4 -0
- package/dist/extensions/browser/scenario/scenario-tool.d.ts.map +1 -0
- package/dist/extensions/browser/scenario/scenario-tool.js +381 -0
- package/dist/extensions/browser/tools/core.js +1 -1
- package/dist/extensions/browser/tools/screenshot.d.ts.map +1 -1
- package/dist/extensions/browser/tools/screenshot.js +2 -1
- package/dist/extensions/seo/analyzers/agent-ux.d.ts +5 -0
- package/dist/extensions/seo/analyzers/agent-ux.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/agent-ux.js +95 -0
- package/dist/extensions/seo/analyzers/content-optimize.d.ts +3 -0
- package/dist/extensions/seo/analyzers/content-optimize.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/content-optimize.js +190 -0
- package/dist/extensions/seo/analyzers/content.d.ts +16 -0
- package/dist/extensions/seo/analyzers/content.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/content.js +174 -0
- package/dist/extensions/seo/analyzers/crawler.d.ts +4 -0
- package/dist/extensions/seo/analyzers/crawler.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/crawler.js +288 -0
- package/dist/extensions/seo/analyzers/drift.d.ts +13 -0
- package/dist/extensions/seo/analyzers/drift.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/drift.js +204 -0
- package/dist/extensions/seo/analyzers/images.d.ts +10 -0
- package/dist/extensions/seo/analyzers/images.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/images.js +49 -0
- package/dist/extensions/seo/analyzers/keywords.d.ts +9 -0
- package/dist/extensions/seo/analyzers/keywords.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/keywords.js +183 -0
- package/dist/extensions/seo/analyzers/parasite.d.ts +18 -0
- package/dist/extensions/seo/analyzers/parasite.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/parasite.js +142 -0
- package/dist/extensions/seo/analyzers/performance.d.ts +6 -0
- package/dist/extensions/seo/analyzers/performance.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/performance.js +257 -0
- package/dist/extensions/seo/analyzers/preload.d.ts +12 -0
- package/dist/extensions/seo/analyzers/preload.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/preload.js +108 -0
- package/dist/extensions/seo/analyzers/readability.d.ts +4 -0
- package/dist/extensions/seo/analyzers/readability.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/readability.js +121 -0
- package/dist/extensions/seo/analyzers/schema.d.ts +21 -0
- package/dist/extensions/seo/analyzers/schema.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/schema.js +324 -0
- package/dist/extensions/seo/analyzers/sitemap.d.ts +20 -0
- package/dist/extensions/seo/analyzers/sitemap.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/sitemap.js +121 -0
- package/dist/extensions/seo/analyzers/technical.d.ts +11 -0
- package/dist/extensions/seo/analyzers/technical.d.ts.map +1 -0
- package/dist/extensions/seo/analyzers/technical.js +268 -0
- package/dist/extensions/seo/config.d.ts +24 -0
- package/dist/extensions/seo/config.d.ts.map +1 -0
- package/dist/extensions/seo/config.js +48 -0
- package/dist/extensions/seo/index.d.ts +16 -0
- package/dist/extensions/seo/index.d.ts.map +1 -0
- package/dist/extensions/seo/index.js +126 -0
- package/dist/extensions/seo/tools/agent-ux.d.ts +7 -0
- package/dist/extensions/seo/tools/agent-ux.d.ts.map +1 -0
- package/dist/extensions/seo/tools/agent-ux.js +63 -0
- package/dist/extensions/seo/tools/batch.d.ts +4 -0
- package/dist/extensions/seo/tools/batch.d.ts.map +1 -0
- package/dist/extensions/seo/tools/batch.js +193 -0
- package/dist/extensions/seo/tools/content.d.ts +7 -0
- package/dist/extensions/seo/tools/content.d.ts.map +1 -0
- package/dist/extensions/seo/tools/content.js +55 -0
- package/dist/extensions/seo/tools/crawl.d.ts +4 -0
- package/dist/extensions/seo/tools/crawl.d.ts.map +1 -0
- package/dist/extensions/seo/tools/crawl.js +166 -0
- package/dist/extensions/seo/tools/drift.d.ts +7 -0
- package/dist/extensions/seo/tools/drift.d.ts.map +1 -0
- package/dist/extensions/seo/tools/drift.js +104 -0
- package/dist/extensions/seo/tools/fetch.d.ts +7 -0
- package/dist/extensions/seo/tools/fetch.d.ts.map +1 -0
- package/dist/extensions/seo/tools/fetch.js +123 -0
- package/dist/extensions/seo/tools/full-audit.d.ts +11 -0
- package/dist/extensions/seo/tools/full-audit.d.ts.map +1 -0
- package/dist/extensions/seo/tools/full-audit.js +191 -0
- package/dist/extensions/seo/tools/helpers.d.ts +13 -0
- package/dist/extensions/seo/tools/helpers.d.ts.map +1 -0
- package/dist/extensions/seo/tools/helpers.js +24 -0
- package/dist/extensions/seo/tools/images.d.ts +7 -0
- package/dist/extensions/seo/tools/images.d.ts.map +1 -0
- package/dist/extensions/seo/tools/images.js +46 -0
- package/dist/extensions/seo/tools/keywords.d.ts +4 -0
- package/dist/extensions/seo/tools/keywords.d.ts.map +1 -0
- package/dist/extensions/seo/tools/keywords.js +234 -0
- package/dist/extensions/seo/tools/parasite.d.ts +7 -0
- package/dist/extensions/seo/tools/parasite.d.ts.map +1 -0
- package/dist/extensions/seo/tools/parasite.js +53 -0
- package/dist/extensions/seo/tools/parse.d.ts +7 -0
- package/dist/extensions/seo/tools/parse.d.ts.map +1 -0
- package/dist/extensions/seo/tools/parse.js +66 -0
- package/dist/extensions/seo/tools/performance.d.ts +4 -0
- package/dist/extensions/seo/tools/performance.d.ts.map +1 -0
- package/dist/extensions/seo/tools/performance.js +201 -0
- package/dist/extensions/seo/tools/preload.d.ts +7 -0
- package/dist/extensions/seo/tools/preload.d.ts.map +1 -0
- package/dist/extensions/seo/tools/preload.js +52 -0
- package/dist/extensions/seo/tools/report.d.ts +4 -0
- package/dist/extensions/seo/tools/report.d.ts.map +1 -0
- package/dist/extensions/seo/tools/report.js +167 -0
- package/dist/extensions/seo/tools/schema.d.ts +7 -0
- package/dist/extensions/seo/tools/schema.d.ts.map +1 -0
- package/dist/extensions/seo/tools/schema.js +105 -0
- package/dist/extensions/seo/tools/sitemap.d.ts +7 -0
- package/dist/extensions/seo/tools/sitemap.d.ts.map +1 -0
- package/dist/extensions/seo/tools/sitemap.js +85 -0
- package/dist/extensions/seo/tools/technical.d.ts +7 -0
- package/dist/extensions/seo/tools/technical.d.ts.map +1 -0
- package/dist/extensions/seo/tools/technical.js +62 -0
- package/dist/extensions/seo/types.d.ts +445 -0
- package/dist/extensions/seo/types.d.ts.map +1 -0
- package/dist/extensions/seo/types.js +7 -0
- package/dist/extensions/seo/utils/fetcher.d.ts +28 -0
- package/dist/extensions/seo/utils/fetcher.d.ts.map +1 -0
- package/dist/extensions/seo/utils/fetcher.js +118 -0
- package/dist/extensions/seo/utils/parser.d.ts +8 -0
- package/dist/extensions/seo/utils/parser.d.ts.map +1 -0
- package/dist/extensions/seo/utils/parser.js +265 -0
- package/dist/extensions/seo/utils/renderer.d.ts +39 -0
- package/dist/extensions/seo/utils/renderer.d.ts.map +1 -0
- package/dist/extensions/seo/utils/renderer.js +190 -0
- package/dist/extensions/seo/utils/url-safety.d.ts +31 -0
- package/dist/extensions/seo/utils/url-safety.d.ts.map +1 -0
- package/dist/extensions/seo/utils/url-safety.js +202 -0
- package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/extensions/native-extensions.js +11 -0
- package/dist/server/handlers/paths-pick-directory.d.ts +0 -27
- package/dist/server/handlers/paths-pick-directory.d.ts.map +1 -1
- package/dist/server/handlers/paths-pick-directory.js +86 -92
- package/package.json +1 -1
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { fetchPage } from "../utils/fetcher.js";
|
|
2
|
+
import { parsePage, extractText } from "../utils/parser.js";
|
|
3
|
+
import { analyzeKeywords, analyzeKeywordDensity } from "../analyzers/keywords.js";
|
|
4
|
+
import { analyzeReadability, interpretReadability } from "../analyzers/readability.js";
|
|
5
|
+
import { optimizeContent } from "../analyzers/content-optimize.js";
|
|
6
|
+
import { errorResult, textResult } from "./helpers.js";
|
|
7
|
+
export function registerKeywordTools(ext, config) {
|
|
8
|
+
// === seo_keywords ===
|
|
9
|
+
const keywordsTool = {
|
|
10
|
+
name: "seo_keywords",
|
|
11
|
+
label: "SEO Keyword Analysis",
|
|
12
|
+
description: "Extract keywords from page content using TF-IDF analysis. " +
|
|
13
|
+
"Returns top keywords with density scores, n-gram phrases (bigrams and trigrams), " +
|
|
14
|
+
"and search intent classification (informational, commercial, transactional). " +
|
|
15
|
+
"Detects keyword stuffing when keyword density exceeds 3%. " +
|
|
16
|
+
"All analysis is offline — no external API keys required.",
|
|
17
|
+
promptSnippet: "`seo_keywords { url? | text? }` — extract keywords, n-grams, and search intent from content",
|
|
18
|
+
parameters: {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
url: { type: "string", description: "URL to fetch and analyze. If provided, text is extracted from the page." },
|
|
22
|
+
text: { type: "string", description: "Raw text to analyze. Used if 'url' is not provided." },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
async execute(_toolCallId, params) {
|
|
26
|
+
const { url, text } = params;
|
|
27
|
+
try {
|
|
28
|
+
let content = text;
|
|
29
|
+
let resolvedUrl = url ?? "";
|
|
30
|
+
if (url) {
|
|
31
|
+
const result = await fetchPage(url, config);
|
|
32
|
+
content = extractText(result.content);
|
|
33
|
+
resolvedUrl = result.finalUrl;
|
|
34
|
+
}
|
|
35
|
+
if (!content) {
|
|
36
|
+
return errorResult("Either 'url' or 'text' must be provided.");
|
|
37
|
+
}
|
|
38
|
+
const analysis = analyzeKeywords(content, resolvedUrl);
|
|
39
|
+
const lines = [
|
|
40
|
+
`✓ Keyword Analysis: ${resolvedUrl || "(text)"}`,
|
|
41
|
+
``,
|
|
42
|
+
`Total words: ${analysis.totalWords}`,
|
|
43
|
+
`Search intent: ${analysis.searchIntent} (confidence: ${(analysis.intentConfidence * 100).toFixed(0)}%)`,
|
|
44
|
+
``,
|
|
45
|
+
`Top Keywords (by TF-IDF):`,
|
|
46
|
+
];
|
|
47
|
+
for (const kw of analysis.keywords.slice(0, 15)) {
|
|
48
|
+
lines.push(` "${kw.keyword}" — ${kw.count}x, density ${kw.density}%, TF-IDF ${kw.tfidf}` +
|
|
49
|
+
(kw.variants.length > 0 ? `, variants: ${kw.variants.join(", ")}` : ""));
|
|
50
|
+
}
|
|
51
|
+
if (analysis.topNgrams.length > 0) {
|
|
52
|
+
lines.push(``, `Top Phrases (n-grams):`);
|
|
53
|
+
for (const ng of analysis.topNgrams.slice(0, 10)) {
|
|
54
|
+
lines.push(` "${ng.ngram}" — ${ng.count}x`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (analysis.keywordStuffingFlags.length > 0) {
|
|
58
|
+
lines.push(``, `⚠ Keyword Stuffing Warnings:`);
|
|
59
|
+
for (const flag of analysis.keywordStuffingFlags) {
|
|
60
|
+
lines.push(` ${flag}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return textResult(lines.join("\n"), { analysis });
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
// === seo_keyword_density ===
|
|
71
|
+
const densityTool = {
|
|
72
|
+
name: "seo_keyword_density",
|
|
73
|
+
label: "SEO Keyword Density Check",
|
|
74
|
+
description: "Check the density of specific target keywords on a page. " +
|
|
75
|
+
"Provides count, density percentage, and a verdict for each keyword " +
|
|
76
|
+
"(too low, optimal, acceptable, or too high/possible stuffing). " +
|
|
77
|
+
"Use this to verify your target keywords are present at appropriate levels.",
|
|
78
|
+
promptSnippet: "`seo_keyword_density { url? | text?, keywords[] }` — check density of target keywords",
|
|
79
|
+
parameters: {
|
|
80
|
+
type: "object",
|
|
81
|
+
properties: {
|
|
82
|
+
url: { type: "string", description: "URL to fetch and analyze." },
|
|
83
|
+
text: { type: "string", description: "Raw text to analyze. Used if 'url' is not provided." },
|
|
84
|
+
keywords: {
|
|
85
|
+
type: "array",
|
|
86
|
+
items: { type: "string" },
|
|
87
|
+
description: "Target keywords to check density for.",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
required: ["keywords"],
|
|
91
|
+
},
|
|
92
|
+
async execute(_toolCallId, params) {
|
|
93
|
+
const { url, text, keywords } = params;
|
|
94
|
+
try {
|
|
95
|
+
let content = text;
|
|
96
|
+
let resolvedUrl = url ?? "";
|
|
97
|
+
if (url) {
|
|
98
|
+
const result = await fetchPage(url, config);
|
|
99
|
+
content = extractText(result.content);
|
|
100
|
+
resolvedUrl = result.finalUrl;
|
|
101
|
+
}
|
|
102
|
+
if (!content)
|
|
103
|
+
return errorResult("Either 'url' or 'text' must be provided.");
|
|
104
|
+
if (!keywords || keywords.length === 0)
|
|
105
|
+
return errorResult("At least one keyword is required.");
|
|
106
|
+
const results = analyzeKeywordDensity(content, keywords);
|
|
107
|
+
const lines = [
|
|
108
|
+
`✓ Keyword Density: ${resolvedUrl || "(text)"}`,
|
|
109
|
+
``,
|
|
110
|
+
];
|
|
111
|
+
for (const r of results) {
|
|
112
|
+
lines.push(` "${r.keyword}": ${r.count}x, ${r.density}% — ${r.verdict}`);
|
|
113
|
+
}
|
|
114
|
+
return textResult(lines.join("\n"), { results });
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
// === seo_readability ===
|
|
122
|
+
const readabilityTool = {
|
|
123
|
+
name: "seo_readability",
|
|
124
|
+
label: "SEO Readability Analysis",
|
|
125
|
+
description: "Analyze text readability using multiple formulas: Flesch-Kincaid Grade Level, " +
|
|
126
|
+
"Flesch Reading Ease, SMOG Index, Automated Readability Index (ARI), and Coleman-Liau Index. " +
|
|
127
|
+
"Returns grade-level averages and SEO-specific readability advice. " +
|
|
128
|
+
"Helps ensure content targets the right audience level.",
|
|
129
|
+
promptSnippet: "`seo_readability { url? | text? }` — analyze content readability",
|
|
130
|
+
parameters: {
|
|
131
|
+
type: "object",
|
|
132
|
+
properties: {
|
|
133
|
+
url: { type: "string", description: "URL to fetch and analyze." },
|
|
134
|
+
text: { type: "string", description: "Raw text to analyze. Used if 'url' is not provided." },
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
async execute(_toolCallId, params) {
|
|
138
|
+
const { url, text } = params;
|
|
139
|
+
try {
|
|
140
|
+
let content = text;
|
|
141
|
+
let resolvedUrl = url ?? "";
|
|
142
|
+
if (url) {
|
|
143
|
+
const result = await fetchPage(url, config);
|
|
144
|
+
content = extractText(result.content);
|
|
145
|
+
resolvedUrl = result.finalUrl;
|
|
146
|
+
}
|
|
147
|
+
if (!content)
|
|
148
|
+
return errorResult("Either 'url' or 'text' must be provided.");
|
|
149
|
+
const result = analyzeReadability(content, resolvedUrl);
|
|
150
|
+
const interpretation = interpretReadability(result);
|
|
151
|
+
const lines = [
|
|
152
|
+
`✓ Readability Analysis: ${resolvedUrl || "(text)"}`,
|
|
153
|
+
``,
|
|
154
|
+
`Words: ${content.split(/\s+/).filter(Boolean).length}`,
|
|
155
|
+
`Sentences: ${result.sentenceCount}`,
|
|
156
|
+
`Syllables: ${result.syllableCount}`,
|
|
157
|
+
`Complex words (3+ syllables): ${result.complexWordCount}`,
|
|
158
|
+
``,
|
|
159
|
+
`Readability Scores:`,
|
|
160
|
+
` Flesch Reading Ease: ${result.fleschReadingEase}`,
|
|
161
|
+
` Flesch-Kincaid Grade: ${result.fleschKincaid}`,
|
|
162
|
+
` SMOG Index: ${result.smog}`,
|
|
163
|
+
` ARI (Automated Readability): ${result.ari}`,
|
|
164
|
+
` Coleman-Liau: ${result.colemanLiau}`,
|
|
165
|
+
` Average Grade Level: ${result.gradeAverage}`,
|
|
166
|
+
``,
|
|
167
|
+
`Interpretation: ${interpretation}`,
|
|
168
|
+
];
|
|
169
|
+
return textResult(lines.join("\n"), { result });
|
|
170
|
+
}
|
|
171
|
+
catch (err) {
|
|
172
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
// === seo_content_optimize ===
|
|
177
|
+
const optimizeTool = {
|
|
178
|
+
name: "seo_content_optimize",
|
|
179
|
+
label: "SEO Content Optimization",
|
|
180
|
+
description: "Analyze on-page content optimization opportunities. Checks title length (ideal 50-60 chars), " +
|
|
181
|
+
"meta description length (ideal 150-160 chars), heading hierarchy (H1→H2→H3 structure), " +
|
|
182
|
+
"content length (minimum 300 words), duplicate title/description, and suspicious headings. " +
|
|
183
|
+
"Returns a score for each category with specific, actionable recommendations.",
|
|
184
|
+
promptSnippet: "`seo_content_optimize { url }` — content optimization audit with actionable fixes",
|
|
185
|
+
parameters: {
|
|
186
|
+
type: "object",
|
|
187
|
+
properties: {
|
|
188
|
+
url: { type: "string", description: "The URL to analyze." },
|
|
189
|
+
},
|
|
190
|
+
required: ["url"],
|
|
191
|
+
},
|
|
192
|
+
async execute(_toolCallId, params) {
|
|
193
|
+
const { url } = params;
|
|
194
|
+
try {
|
|
195
|
+
const fetchResult = await fetchPage(url, config);
|
|
196
|
+
const pageData = parsePage(fetchResult.content, fetchResult.finalUrl);
|
|
197
|
+
const result = optimizeContent(pageData);
|
|
198
|
+
const lines = [
|
|
199
|
+
`✓ Content Optimization: ${result.url}`,
|
|
200
|
+
``,
|
|
201
|
+
`Overall Score: ${result.overallScore}/100`,
|
|
202
|
+
``,
|
|
203
|
+
`Category Scores:`,
|
|
204
|
+
` Title: ${result.titleScore}/100`,
|
|
205
|
+
` Meta Description: ${result.descriptionScore}/100`,
|
|
206
|
+
` Heading Structure: ${result.headingStructureScore}/100`,
|
|
207
|
+
` Content Length: ${result.contentLengthScore}/100`,
|
|
208
|
+
``,
|
|
209
|
+
];
|
|
210
|
+
if (result.issues.length > 0) {
|
|
211
|
+
const sorted = result.issues.sort((a, b) => {
|
|
212
|
+
const rank = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
213
|
+
return (rank[a.severity] ?? 4) - (rank[b.severity] ?? 4);
|
|
214
|
+
});
|
|
215
|
+
lines.push(`Issues (${result.issues.length}):`);
|
|
216
|
+
for (const issue of sorted) {
|
|
217
|
+
lines.push(` [${issue.severity.toUpperCase()}] ${issue.category}: ${issue.issue}`, ` → ${issue.recommendation}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
lines.push(`No issues found — content is well-optimized!`);
|
|
222
|
+
}
|
|
223
|
+
return textResult(lines.join("\n"), { result });
|
|
224
|
+
}
|
|
225
|
+
catch (err) {
|
|
226
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
ext.registerTool(keywordsTool);
|
|
231
|
+
ext.registerTool(densityTool);
|
|
232
|
+
ext.registerTool(readabilityTool);
|
|
233
|
+
ext.registerTool(optimizeTool);
|
|
234
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* seo_parasite_risk tool — parasite-SEO risk scanner.
|
|
3
|
+
*/
|
|
4
|
+
import type { ExtensionAPI } from "../../../sdk/coding-agent/index.js";
|
|
5
|
+
import type { SeoConfig } from "../config.js";
|
|
6
|
+
export declare function registerParasiteTools(ext: ExtensionAPI, config: SeoConfig): void;
|
|
7
|
+
//# sourceMappingURL=parasite.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parasite.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/tools/parasite.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AACvF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAI9C,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI,CA0DhF"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { scanParasiteRisk } from "../analyzers/parasite.js";
|
|
2
|
+
import { errorResult, textResult } from "./helpers.js";
|
|
3
|
+
export function registerParasiteTools(ext, config) {
|
|
4
|
+
const parasiteTool = {
|
|
5
|
+
name: "seo_parasite_risk",
|
|
6
|
+
label: "SEO Parasite Risk",
|
|
7
|
+
description: "Scan a list of URLs from one host for parasite-SEO risk. Fetches each URL and classifies content " +
|
|
8
|
+
"by subfolder, flagging section-level risk via: third-party byline patterns (Partner Content, Sponsored, " +
|
|
9
|
+
"Brand Studio), commercial-intent patterns (Buy Now, Promo Code, Coupon), and affiliate link markers " +
|
|
10
|
+
"(tag=, aff_id=, ref_=). Returns per-subfolder risk classification (high/medium/low). " +
|
|
11
|
+
"Per Google's 2024-11-19 policy, section-level manual actions target this pattern.",
|
|
12
|
+
promptSnippet: "`seo_parasite_risk { urls }` — scan URLs for parasite-SEO risk per subfolder",
|
|
13
|
+
parameters: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
urls: {
|
|
17
|
+
type: "array",
|
|
18
|
+
items: { type: "string" },
|
|
19
|
+
description: "List of URLs from one host to scan. Typically obtained from sitemap sampling.",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
required: ["urls"],
|
|
23
|
+
},
|
|
24
|
+
async execute(_toolCallId, params) {
|
|
25
|
+
const { urls } = params;
|
|
26
|
+
try {
|
|
27
|
+
if (!urls || urls.length === 0) {
|
|
28
|
+
return errorResult("At least one URL is required.");
|
|
29
|
+
}
|
|
30
|
+
const result = await scanParasiteRisk(urls, config);
|
|
31
|
+
const sectionLines = result.sections
|
|
32
|
+
.sort((a, b) => {
|
|
33
|
+
const rank = { high: 0, medium: 1, low: 2 };
|
|
34
|
+
return rank[a.risk] - rank[b.risk];
|
|
35
|
+
})
|
|
36
|
+
.map((s) => ` [${s.risk.toUpperCase()}] ${s.section} (${s.pageCount} pages) — ` +
|
|
37
|
+
`3rd-party: ${s.thirdPartyHitsPerPage}/page, commerce: ${s.commerceHitsPerPage}/page, ` +
|
|
38
|
+
`affiliate: ${s.affiliateLinkHitsPerPage}/page` +
|
|
39
|
+
(s.flags.length > 0 ? ` [${s.flags.join(", ")}]` : ""))
|
|
40
|
+
.join("\n");
|
|
41
|
+
const summary = `✓ Parasite Risk Scan: ${result.host}\n` +
|
|
42
|
+
`Total Pages: ${result.totalPages}\n` +
|
|
43
|
+
`Sections Scanned: ${result.sections.length}\n\n` +
|
|
44
|
+
`Section Risk:\n${sectionLines}`;
|
|
45
|
+
return textResult(summary, { result });
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
ext.registerTool(parasiteTool);
|
|
53
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* seo_parse_page tool — extract on-page SEO data from HTML.
|
|
3
|
+
*/
|
|
4
|
+
import type { ExtensionAPI } from "../../../sdk/coding-agent/index.js";
|
|
5
|
+
import type { SeoConfig } from "../config.js";
|
|
6
|
+
export declare function registerParseTools(ext: ExtensionAPI, config: SeoConfig): void;
|
|
7
|
+
//# sourceMappingURL=parse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/tools/parse.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AACvF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAK9C,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI,CA0E7E"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { fetchPage } from "../utils/fetcher.js";
|
|
2
|
+
import { parsePage } from "../utils/parser.js";
|
|
3
|
+
import { errorResult, textResult, truncate } from "./helpers.js";
|
|
4
|
+
export function registerParseTools(ext, config) {
|
|
5
|
+
const parseTool = {
|
|
6
|
+
name: "seo_parse_page",
|
|
7
|
+
label: "SEO Parse Page",
|
|
8
|
+
description: "Parse HTML and extract all on-page SEO elements: title, meta description, meta robots, canonical, " +
|
|
9
|
+
"headings (H1-H6 with suspicious-heading detection), images (with lazy-loading method), links (internal/external), " +
|
|
10
|
+
"JSON-LD schema blocks, Open Graph tags, Twitter Card tags, hreflang, word count, language, and viewport. " +
|
|
11
|
+
"Accepts either a URL (fetches the page) or raw HTML.",
|
|
12
|
+
promptSnippet: "`seo_parse_page { url? | html?, baseUrl? }` — extract on-page SEO elements from HTML",
|
|
13
|
+
promptGuidelines: [
|
|
14
|
+
"When the user asks about SEO, search optimization, page audits, schema markup, sitemaps, or technical SEO for any website or URL, use the seo_* tools (seo_fetch, seo_parse_page, seo_technical_audit, seo_content_quality, seo_schema_audit, seo_image_audit, seo_preload_audit, seo_agent_ux_audit, seo_audit, etc.). These are offline-first and require no API keys.",
|
|
15
|
+
],
|
|
16
|
+
parameters: {
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
url: { type: "string", description: "URL to fetch and parse. If provided, HTML is fetched automatically." },
|
|
20
|
+
html: { type: "string", description: "Raw HTML to parse. Used if 'url' is not provided." },
|
|
21
|
+
baseUrl: { type: "string", description: "Base URL for resolving relative links. Defaults to 'url' if provided." },
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
async execute(_toolCallId, params) {
|
|
25
|
+
const { url, html, baseUrl } = params;
|
|
26
|
+
try {
|
|
27
|
+
let content = html ?? "";
|
|
28
|
+
let effectiveUrl = url ?? baseUrl ?? "";
|
|
29
|
+
if (url && !html) {
|
|
30
|
+
const result = await fetchPage(url, config);
|
|
31
|
+
if (result.error) {
|
|
32
|
+
return errorResult(`Fetch failed: ${result.error}`);
|
|
33
|
+
}
|
|
34
|
+
content = result.content;
|
|
35
|
+
effectiveUrl = result.finalUrl;
|
|
36
|
+
}
|
|
37
|
+
if (!content) {
|
|
38
|
+
return errorResult("Either 'url' or 'html' must be provided.");
|
|
39
|
+
}
|
|
40
|
+
const pageData = parsePage(content, effectiveUrl, baseUrl);
|
|
41
|
+
const summary = `✓ Parsed: ${effectiveUrl}\n` +
|
|
42
|
+
`Title: ${pageData.title ?? "(none)"} (${pageData.titleLength} chars)\n` +
|
|
43
|
+
`Meta Description: ${pageData.metaDescription ? `${pageData.metaDescriptionLength} chars` : "(none)"}\n` +
|
|
44
|
+
`Canonical: ${pageData.canonical ?? "(none)"}\n` +
|
|
45
|
+
`Headings: H1=${pageData.h1Count}, H2=${pageData.h2Count}, H3=${pageData.h3Count}\n` +
|
|
46
|
+
`Images: ${pageData.images.length} (${pageData.images.filter((i) => !i.alt).length} missing alt)\n` +
|
|
47
|
+
`Links: ${pageData.internalLinkCount} internal, ${pageData.externalLinkCount} external\n` +
|
|
48
|
+
`Schema blocks: ${pageData.schemaBlocks.length}\n` +
|
|
49
|
+
`Hreflang: ${pageData.hreflang.length}\n` +
|
|
50
|
+
`Word count: ${pageData.wordCount}`;
|
|
51
|
+
return textResult(summary, {
|
|
52
|
+
...pageData,
|
|
53
|
+
// Truncate large arrays to avoid token bloat.
|
|
54
|
+
headings: truncate(JSON.stringify(pageData.headings), 10_000),
|
|
55
|
+
links: truncate(JSON.stringify(pageData.links), 10_000),
|
|
56
|
+
images: truncate(JSON.stringify(pageData.images), 10_000),
|
|
57
|
+
schemaBlocks: truncate(JSON.stringify(pageData.schemaBlocks), 10_000),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
ext.registerTool(parseTool);
|
|
66
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"performance.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/tools/performance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AACvF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAW9C,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI,CA4NnF"}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { fetchPage } from "../utils/fetcher.js";
|
|
2
|
+
import { calculatePerformanceFromFetch, analyzePageWeight, analyzeRenderBlocking, analyzeFonts, } from "../analyzers/performance.js";
|
|
3
|
+
import { errorResult, textResult } from "./helpers.js";
|
|
4
|
+
export function registerPerformanceTools(ext, config) {
|
|
5
|
+
// === seo_performance ===
|
|
6
|
+
const perfTool = {
|
|
7
|
+
name: "seo_performance",
|
|
8
|
+
label: "SEO Performance Audit (Core Web Vitals)",
|
|
9
|
+
description: "Estimate Core Web Vitals metrics from page structure analysis. " +
|
|
10
|
+
"Reports LCP (Largest Contentful Paint), CLS (Cumulative Layout Shift), TBT (Total Blocking Time), FCP, and TTFB. " +
|
|
11
|
+
"Each metric is rated: good, needs-improvement, or poor against Google's thresholds. " +
|
|
12
|
+
"Note: These are heuristic estimates based on page complexity. For accurate numbers use Lighthouse or PageSpeed Insights.",
|
|
13
|
+
promptSnippet: "`seo_performance { url }` — estimate Core Web Vitals (LCP, CLS, TBT) with ratings",
|
|
14
|
+
parameters: {
|
|
15
|
+
type: "object",
|
|
16
|
+
properties: {
|
|
17
|
+
url: { type: "string", description: "The URL to analyze." },
|
|
18
|
+
},
|
|
19
|
+
required: ["url"],
|
|
20
|
+
},
|
|
21
|
+
async execute(_toolCallId, params) {
|
|
22
|
+
const { url } = params;
|
|
23
|
+
try {
|
|
24
|
+
const fetchResult = await fetchPage(url, config);
|
|
25
|
+
const perf = calculatePerformanceFromFetch(fetchResult, url);
|
|
26
|
+
const lines = [
|
|
27
|
+
`✓ Performance Estimate: ${perf.url}`,
|
|
28
|
+
``,
|
|
29
|
+
`Overall CWV Score: ${perf.score}/100`,
|
|
30
|
+
``,
|
|
31
|
+
`Core Web Vitals:`,
|
|
32
|
+
` LCP: ${perf.lcp.value}ms — ${perf.lcp.rating.toUpperCase()}`,
|
|
33
|
+
` CLS: ${perf.cls.value} — ${perf.cls.rating.toUpperCase()}`,
|
|
34
|
+
` TBT: ${perf.tbt.value}ms — ${perf.tbt.rating.toUpperCase()}`,
|
|
35
|
+
``,
|
|
36
|
+
`Other Metrics:`,
|
|
37
|
+
` FCP: ${perf.fcp}ms`,
|
|
38
|
+
` TTFB: ${perf.ttfb}ms`,
|
|
39
|
+
``,
|
|
40
|
+
`Google Thresholds:`,
|
|
41
|
+
` LCP: good ≤ 2500ms, poor > 4000ms`,
|
|
42
|
+
` CLS: good ≤ 0.1, poor > 0.25`,
|
|
43
|
+
` TBT: good ≤ 200ms, poor > 600ms`,
|
|
44
|
+
];
|
|
45
|
+
return textResult(lines.join("\n"), { perf });
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
// === seo_page_weight ===
|
|
53
|
+
const weightTool = {
|
|
54
|
+
name: "seo_page_weight",
|
|
55
|
+
label: "SEO Page Weight Analysis",
|
|
56
|
+
description: "Analyze total page weight and resource breakdown (HTML, CSS, JS, images, fonts). " +
|
|
57
|
+
"Identifies heavy resources that slow down page load. " +
|
|
58
|
+
"Helps find optimization opportunities: large images, bloated CSS/JS bundles, heavy fonts.",
|
|
59
|
+
promptSnippet: "`seo_page_weight { url }` — analyze page weight by resource type",
|
|
60
|
+
parameters: {
|
|
61
|
+
type: "object",
|
|
62
|
+
properties: {
|
|
63
|
+
url: { type: "string", description: "The URL to analyze." },
|
|
64
|
+
},
|
|
65
|
+
required: ["url"],
|
|
66
|
+
},
|
|
67
|
+
async execute(_toolCallId, params) {
|
|
68
|
+
const { url } = params;
|
|
69
|
+
try {
|
|
70
|
+
const fetchResult = await fetchPage(url, config);
|
|
71
|
+
const weight = analyzePageWeight(fetchResult, url);
|
|
72
|
+
const lines = [
|
|
73
|
+
`✓ Page Weight: ${weight.url}`,
|
|
74
|
+
``,
|
|
75
|
+
`Total weight: ~${weight.totalKb} KB`,
|
|
76
|
+
` HTML: ${weight.htmlKb} KB`,
|
|
77
|
+
` CSS: ${weight.cssKb} KB`,
|
|
78
|
+
` JS: ${weight.jsKb} KB`,
|
|
79
|
+
` Images: ${weight.imageKb} KB`,
|
|
80
|
+
` Fonts: ${weight.fontKb} KB`,
|
|
81
|
+
``,
|
|
82
|
+
`Resource count: ${weight.resourceCount}`,
|
|
83
|
+
];
|
|
84
|
+
if (weight.topHeavyResources.length > 0) {
|
|
85
|
+
lines.push(``, `Heaviest resources:`);
|
|
86
|
+
for (const r of weight.topHeavyResources) {
|
|
87
|
+
lines.push(` ${r.type.toUpperCase()}: ${r.sizeKb} KB — ${r.url}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (weight.totalKb > 5000) {
|
|
91
|
+
lines.push(``, `⚠ Page exceeds 5MB — may impact mobile performance.`);
|
|
92
|
+
}
|
|
93
|
+
else if (weight.totalKb > 2000) {
|
|
94
|
+
lines.push(``, `⚡ Page weight is above average. Consider optimizing assets.`);
|
|
95
|
+
}
|
|
96
|
+
return textResult(lines.join("\n"), { weight });
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
// === seo_render_blocking ===
|
|
104
|
+
const blockingTool = {
|
|
105
|
+
name: "seo_render_blocking",
|
|
106
|
+
label: "SEO Render-Blocking Resource Audit",
|
|
107
|
+
description: "Detect render-blocking CSS and JavaScript in the <head> that delay First Paint. " +
|
|
108
|
+
"Lists blocking stylesheets and scripts with recommendations: inline critical CSS, " +
|
|
109
|
+
"add async/defer to scripts, code-split bundles.",
|
|
110
|
+
promptSnippet: "`seo_render_blocking { url }` — find render-blocking CSS/JS with fixes",
|
|
111
|
+
parameters: {
|
|
112
|
+
type: "object",
|
|
113
|
+
properties: {
|
|
114
|
+
url: { type: "string", description: "The URL to analyze." },
|
|
115
|
+
},
|
|
116
|
+
required: ["url"],
|
|
117
|
+
},
|
|
118
|
+
async execute(_toolCallId, params) {
|
|
119
|
+
const { url } = params;
|
|
120
|
+
try {
|
|
121
|
+
const fetchResult = await fetchPage(url, config);
|
|
122
|
+
const blocking = analyzeRenderBlocking(fetchResult, url);
|
|
123
|
+
const lines = [
|
|
124
|
+
`✓ Render-Blocking Resources: ${blocking.url}`,
|
|
125
|
+
``,
|
|
126
|
+
`Blocking CSS: ${blocking.blockingCss.length}`,
|
|
127
|
+
`Blocking JS: ${blocking.blockingJs.length}`,
|
|
128
|
+
`Total blocking: ${blocking.totalBlockingUrls}`,
|
|
129
|
+
``,
|
|
130
|
+
];
|
|
131
|
+
if (blocking.blockingCss.length > 0) {
|
|
132
|
+
lines.push(`Blocking Stylesheets:`);
|
|
133
|
+
for (const css of blocking.blockingCss) {
|
|
134
|
+
lines.push(` ${css.href}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (blocking.blockingJs.length > 0) {
|
|
138
|
+
lines.push(`Blocking Scripts:`);
|
|
139
|
+
for (const js of blocking.blockingJs) {
|
|
140
|
+
lines.push(` ${js.src}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
lines.push(``, `Recommendations:`);
|
|
144
|
+
for (const rec of blocking.recommendations) {
|
|
145
|
+
lines.push(` • ${rec}`);
|
|
146
|
+
}
|
|
147
|
+
return textResult(lines.join("\n"), { blocking });
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
// === seo_font_audit ===
|
|
155
|
+
const fontTool = {
|
|
156
|
+
name: "seo_font_audit",
|
|
157
|
+
label: "SEO Font Optimization Audit",
|
|
158
|
+
description: "Audit web fonts for performance best practices. Checks: font-display setting (prevents invisible text), " +
|
|
159
|
+
"format (WOFF2 preferred), subsetting (unicode-range), and counts Google Fonts vs self-hosted fonts. " +
|
|
160
|
+
"Fonts directly impact LCP and CLS scores.",
|
|
161
|
+
promptSnippet: "`seo_font_audit { url }` — audit font loading strategy",
|
|
162
|
+
parameters: {
|
|
163
|
+
type: "object",
|
|
164
|
+
properties: {
|
|
165
|
+
url: { type: "string", description: "The URL to analyze." },
|
|
166
|
+
},
|
|
167
|
+
required: ["url"],
|
|
168
|
+
},
|
|
169
|
+
async execute(_toolCallId, params) {
|
|
170
|
+
const { url } = params;
|
|
171
|
+
try {
|
|
172
|
+
const fetchResult = await fetchPage(url, config);
|
|
173
|
+
const fonts = analyzeFonts(fetchResult, url);
|
|
174
|
+
const lines = [
|
|
175
|
+
`✓ Font Audit: ${fonts.url}`,
|
|
176
|
+
``,
|
|
177
|
+
`Fonts found: ${fonts.fontsFound.length}`,
|
|
178
|
+
`Score: ${fonts.score}/100`,
|
|
179
|
+
``,
|
|
180
|
+
];
|
|
181
|
+
for (const f of fonts.fontsFound) {
|
|
182
|
+
lines.push(` "${f.family}" — ${f.source} (${f.format})` +
|
|
183
|
+
` | display: ${f.hasDisplay ? "✓" : "✗"}` +
|
|
184
|
+
` | subset: ${f.isSubset ? "✓" : "✗"}`);
|
|
185
|
+
}
|
|
186
|
+
lines.push(``, `Recommendations:`);
|
|
187
|
+
for (const rec of fonts.recommendations) {
|
|
188
|
+
lines.push(` • ${rec}`);
|
|
189
|
+
}
|
|
190
|
+
return textResult(lines.join("\n"), { fonts });
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
ext.registerTool(perfTool);
|
|
198
|
+
ext.registerTool(weightTool);
|
|
199
|
+
ext.registerTool(blockingTool);
|
|
200
|
+
ext.registerTool(fontTool);
|
|
201
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* seo_preload_audit tool — Speculation Rules / bfcache / preload audit.
|
|
3
|
+
*/
|
|
4
|
+
import type { ExtensionAPI } from "../../../sdk/coding-agent/index.js";
|
|
5
|
+
import type { SeoConfig } from "../config.js";
|
|
6
|
+
export declare function registerPreloadTools(ext: ExtensionAPI, config: SeoConfig): void;
|
|
7
|
+
//# sourceMappingURL=preload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preload.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/tools/preload.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AACvF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAK9C,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI,CAoD/E"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { auditPreload } from "../analyzers/preload.js";
|
|
2
|
+
import { fetchPage } from "../utils/fetcher.js";
|
|
3
|
+
import { errorResult, textResult } from "./helpers.js";
|
|
4
|
+
export function registerPreloadTools(ext, config) {
|
|
5
|
+
const preloadTool = {
|
|
6
|
+
name: "seo_preload_audit",
|
|
7
|
+
label: "SEO Preload Audit",
|
|
8
|
+
description: "Audit a page's HTML and response headers for preload mechanisms Google rewards in 2025-2026: " +
|
|
9
|
+
"Speculation Rules API (inline blocks + Speculation-Rules header), <link rel=preload> hints, " +
|
|
10
|
+
"deprecated <link rel=prerender>, bfcache signals (Cache-Control: no-store, unload/beforeunload listeners), " +
|
|
11
|
+
"and LCP fetchpriority hints. Returns a 0-100 score and actionable recommendations.",
|
|
12
|
+
promptSnippet: "`seo_preload_audit { url }` — audit speculation rules, preload, bfcache, LCP hints",
|
|
13
|
+
parameters: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
url: { type: "string", description: "The URL to audit for preload/bfcache signals." },
|
|
17
|
+
},
|
|
18
|
+
required: ["url"],
|
|
19
|
+
},
|
|
20
|
+
async execute(_toolCallId, params) {
|
|
21
|
+
const { url } = params;
|
|
22
|
+
try {
|
|
23
|
+
const result = await fetchPage(url, config);
|
|
24
|
+
if (result.error) {
|
|
25
|
+
return errorResult(`Fetch failed: ${result.error}`);
|
|
26
|
+
}
|
|
27
|
+
const audit = auditPreload(result);
|
|
28
|
+
const summary = `✓ Preload Audit: ${result.finalUrl}\n` +
|
|
29
|
+
`Score: ${audit.score}/100\n\n` +
|
|
30
|
+
`Speculation Rules:\n` +
|
|
31
|
+
` Inline blocks: ${audit.speculationRules.inlineBlocks}\n` +
|
|
32
|
+
` Header present: ${audit.speculationRules.headerPresent ? "✓" : "✗"}\n` +
|
|
33
|
+
` Actions: ${audit.speculationRules.actions.join(", ") || "(none)"}\n\n` +
|
|
34
|
+
`Preload hints: ${audit.preloadHints}\n` +
|
|
35
|
+
`Deprecated prerender links: ${audit.prerenderLinks}\n\n` +
|
|
36
|
+
`bfcache Signals:\n` +
|
|
37
|
+
` Cache-Control: no-store: ${audit.bfcacheSignals.cacheControlNoStore ? "⚠️" : "✓"}\n` +
|
|
38
|
+
` unload listener: ${audit.bfcacheSignals.unloadListener ? "⚠️" : "✓"}\n` +
|
|
39
|
+
` beforeunload listener: ${audit.bfcacheSignals.beforeunloadListener ? "⚠️" : "✓"}\n\n` +
|
|
40
|
+
`LCP Hints:\n` +
|
|
41
|
+
` fetchpriority="high" on LCP candidate: ${audit.lcpResourceHints.preloadLcpCandidate ? "✓" : "✗"}\n` +
|
|
42
|
+
` fetchpriority="high" count: ${audit.lcpResourceHints.fetchpriorityHigh}\n\n` +
|
|
43
|
+
`Recommendations:\n${audit.recommendations.map((r) => ` • ${r}`).join("\n") || " (none)"}`;
|
|
44
|
+
return textResult(summary, { audit });
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
ext.registerTool(preloadTool);
|
|
52
|
+
}
|