@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,193 @@
|
|
|
1
|
+
import { auditTechnical } from "../analyzers/technical.js";
|
|
2
|
+
import { analyzeContentQuality } from "../analyzers/content.js";
|
|
3
|
+
import { fetchPage } from "../utils/fetcher.js";
|
|
4
|
+
import { parsePage, extractText } from "../utils/parser.js";
|
|
5
|
+
import { analyzeKeywords } from "../analyzers/keywords.js";
|
|
6
|
+
import { analyzeReadability } from "../analyzers/readability.js";
|
|
7
|
+
import { errorResult, textResult, truncate } from "./helpers.js";
|
|
8
|
+
export function registerBatchTools(ext, config) {
|
|
9
|
+
// === seo_audit_batch ===
|
|
10
|
+
const batchTool = {
|
|
11
|
+
name: "seo_audit_batch",
|
|
12
|
+
label: "SEO Batch Audit",
|
|
13
|
+
description: "Run SEO audits on multiple URLs, typically from a sitemap. " +
|
|
14
|
+
"Audits each URL for technical SEO, content quality, and schema markup. " +
|
|
15
|
+
"Returns a comparison table with scores for each page. " +
|
|
16
|
+
"Use this after seo_crawl to audit all discovered subpages at once.",
|
|
17
|
+
promptSnippet: "`seo_audit_batch { urls[] }` — audit multiple URLs and compare scores",
|
|
18
|
+
parameters: {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
urls: {
|
|
22
|
+
type: "array",
|
|
23
|
+
items: { type: "string" },
|
|
24
|
+
description: "List of URLs to audit.",
|
|
25
|
+
},
|
|
26
|
+
maxPages: {
|
|
27
|
+
type: "number",
|
|
28
|
+
description: "Max URLs to audit (limits batch size). Default: 20.",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
required: ["urls"],
|
|
32
|
+
},
|
|
33
|
+
async execute(_toolCallId, params) {
|
|
34
|
+
const { urls, maxPages } = params;
|
|
35
|
+
try {
|
|
36
|
+
const toAudit = urls.slice(0, maxPages ?? 20);
|
|
37
|
+
const results = [];
|
|
38
|
+
for (const url of toAudit) {
|
|
39
|
+
try {
|
|
40
|
+
const fetchResult = await fetchPage(url, config);
|
|
41
|
+
const pageData = parsePage(fetchResult.content, fetchResult.finalUrl);
|
|
42
|
+
const technical = await auditTechnical(url, config, pageData, fetchResult);
|
|
43
|
+
const text = extractText(fetchResult.content);
|
|
44
|
+
const content = analyzeContentQuality(text, fetchResult.finalUrl);
|
|
45
|
+
results.push({
|
|
46
|
+
url: fetchResult.finalUrl,
|
|
47
|
+
status: fetchResult.statusCode,
|
|
48
|
+
technicalScore: technical.score,
|
|
49
|
+
contentScore: content.overallQuality,
|
|
50
|
+
title: pageData.title ?? "(no title)",
|
|
51
|
+
wordCount: pageData.wordCount,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
results.push({
|
|
56
|
+
url,
|
|
57
|
+
status: 0,
|
|
58
|
+
technicalScore: 0,
|
|
59
|
+
contentScore: 0,
|
|
60
|
+
title: "(error)",
|
|
61
|
+
wordCount: 0,
|
|
62
|
+
error: err instanceof Error ? err.message : String(err),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const avgTech = Math.round(results.filter((r) => !r.error).reduce((s, r) => s + r.technicalScore, 0) /
|
|
67
|
+
Math.max(1, results.filter((r) => !r.error).length));
|
|
68
|
+
const avgContent = Math.round(results.filter((r) => !r.error).reduce((s, r) => s + r.contentScore, 0) /
|
|
69
|
+
Math.max(1, results.filter((r) => !r.error).length));
|
|
70
|
+
const lines = [
|
|
71
|
+
`✓ Batch Audit: ${results.length} pages`,
|
|
72
|
+
``,
|
|
73
|
+
`Average Technical Score: ${avgTech}/100`,
|
|
74
|
+
`Average Content Score: ${avgContent}/100`,
|
|
75
|
+
``,
|
|
76
|
+
`| # | URL | Status | Tech | Content | Title |`,
|
|
77
|
+
`|---|-----|--------|------|---------|-------|`,
|
|
78
|
+
];
|
|
79
|
+
results.forEach((r, i) => {
|
|
80
|
+
lines.push(`| ${i + 1} | ${truncate(r.url, 60)} | ${r.status || "ERR"} | ${r.technicalScore} | ${r.contentScore} | ${truncate(r.title, 40)} |`);
|
|
81
|
+
});
|
|
82
|
+
return textResult(lines.join("\n"), { results, avgTech, avgContent });
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
// === seo_competitor_gap ===
|
|
90
|
+
const gapTool = {
|
|
91
|
+
name: "seo_competitor_gap",
|
|
92
|
+
label: "SEO Competitor Gap Analysis",
|
|
93
|
+
description: "Compare your page against a competitor's page. Analyzes differences in: " +
|
|
94
|
+
"keyword usage (which keywords do they use that you don't?), content length, " +
|
|
95
|
+
"readability, heading structure, schema markup, and technical signals. " +
|
|
96
|
+
"Helps identify content gaps and optimization opportunities.",
|
|
97
|
+
promptSnippet: "`seo_competitor_gap { yourUrl, competitorUrl }` — compare pages for content/SEO gaps",
|
|
98
|
+
parameters: {
|
|
99
|
+
type: "object",
|
|
100
|
+
properties: {
|
|
101
|
+
yourUrl: { type: "string", description: "Your page URL." },
|
|
102
|
+
competitorUrl: { type: "string", description: "Competitor page URL to compare against." },
|
|
103
|
+
},
|
|
104
|
+
required: ["yourUrl", "competitorUrl"],
|
|
105
|
+
},
|
|
106
|
+
async execute(_toolCallId, params) {
|
|
107
|
+
const { yourUrl, competitorUrl } = params;
|
|
108
|
+
try {
|
|
109
|
+
const [yourFetch, compFetch] = await Promise.all([
|
|
110
|
+
fetchPage(yourUrl, config),
|
|
111
|
+
fetchPage(competitorUrl, config),
|
|
112
|
+
]);
|
|
113
|
+
const yourData = parsePage(yourFetch.content, yourFetch.finalUrl);
|
|
114
|
+
const compData = parsePage(compFetch.content, compFetch.finalUrl);
|
|
115
|
+
const yourText = extractText(yourFetch.content);
|
|
116
|
+
const compText = extractText(compFetch.content);
|
|
117
|
+
const yourKw = analyzeKeywords(yourText, yourData.url);
|
|
118
|
+
const compKw = analyzeKeywords(compText, compData.url);
|
|
119
|
+
const yourReadability = analyzeReadability(yourText, yourData.url);
|
|
120
|
+
const compReadability = analyzeReadability(compText, compData.url);
|
|
121
|
+
// Find keywords competitor uses that you don't
|
|
122
|
+
const yourKeywordSet = new Set(yourKw.keywords.map((k) => k.keyword));
|
|
123
|
+
const compKeywordSet = new Set(compKw.keywords.map((k) => k.keyword));
|
|
124
|
+
const competitorOnlyKeywords = compKw.keywords
|
|
125
|
+
.filter((k) => !yourKeywordSet.has(k.keyword))
|
|
126
|
+
.slice(0, 10);
|
|
127
|
+
const sharedKeywords = compKw.keywords
|
|
128
|
+
.filter((k) => yourKeywordSet.has(k.keyword))
|
|
129
|
+
.slice(0, 10);
|
|
130
|
+
const yourSchemaTypes = new Set(yourData.schemaBlocks.map((s) => s.type));
|
|
131
|
+
const compSchemaTypes = new Set(compData.schemaBlocks.map((s) => s.type));
|
|
132
|
+
const compOnlySchemas = [...compSchemaTypes].filter((t) => !yourSchemaTypes.has(t));
|
|
133
|
+
const lines = [
|
|
134
|
+
`✓ Competitor Gap Analysis`,
|
|
135
|
+
``,
|
|
136
|
+
`Your page: ${yourData.url}`,
|
|
137
|
+
`Competitor: ${compData.url}`,
|
|
138
|
+
``,
|
|
139
|
+
`## Content Comparison`,
|
|
140
|
+
``,
|
|
141
|
+
`| Metric | Yours | Competitor |`,
|
|
142
|
+
`|--------|-------|------------|`,
|
|
143
|
+
`| Word count | ${yourData.wordCount} | ${compData.wordCount} |`,
|
|
144
|
+
`| Headings | H1:${yourData.h1Count} H2:${yourData.h2Count} H3:${yourData.h3Count} | H1:${compData.h1Count} H2:${compData.h2Count} H3:${compData.h3Count} |`,
|
|
145
|
+
`| Internal links | ${yourData.internalLinkCount} | ${compData.internalLinkCount} |`,
|
|
146
|
+
`| Images | ${yourData.images.length} | ${compData.images.length} |`,
|
|
147
|
+
`| Schema types | ${yourData.schemaBlocks.length} | ${compData.schemaBlocks.length} |`,
|
|
148
|
+
`| Readability (grade) | ${yourReadability.gradeAverage} | ${compReadability.gradeAverage} |`,
|
|
149
|
+
`| Readability (ease) | ${yourReadability.fleschReadingEase} | ${compReadability.fleschReadingEase} |`,
|
|
150
|
+
``,
|
|
151
|
+
];
|
|
152
|
+
if (competitorOnlyKeywords.length > 0) {
|
|
153
|
+
lines.push(`## Keywords Competitor Uses That You Don't`, ``, ...competitorOnlyKeywords.map((k) => ` • "${k.keyword}" — ${k.count}x, density ${k.density}%, TF-IDF ${k.tfidf}`), ``);
|
|
154
|
+
}
|
|
155
|
+
if (sharedKeywords.length > 0) {
|
|
156
|
+
lines.push(`## Shared Keywords`, ``, ...sharedKeywords.map((k) => ` • "${k.keyword}" — you: ${yourKw.keywords.find((yk) => yk.keyword === k.keyword)?.count ?? 0}x, competitor: ${k.count}x`), ``);
|
|
157
|
+
}
|
|
158
|
+
if (compOnlySchemas.length > 0) {
|
|
159
|
+
lines.push(`## Schema Types Competitor Has That You Don't`, ``, ...compOnlySchemas.map((t) => ` • ${t}`), ``);
|
|
160
|
+
}
|
|
161
|
+
// Content gap recommendations
|
|
162
|
+
lines.push(`## Recommendations`);
|
|
163
|
+
if (compData.wordCount > yourData.wordCount * 1.5) {
|
|
164
|
+
lines.push(` • Content gap: Competitor has ${compData.wordCount - yourData.wordCount} more words. Consider expanding your content.`);
|
|
165
|
+
}
|
|
166
|
+
if (compData.h2Count > yourData.h2Count) {
|
|
167
|
+
lines.push(` • Structure gap: Competitor uses more H2 subheadings (${compData.h2Count} vs ${yourData.h2Count}). Break your content into more sections.`);
|
|
168
|
+
}
|
|
169
|
+
if (compData.schemaBlocks.length > yourData.schemaBlocks.length) {
|
|
170
|
+
lines.push(` • Schema gap: Competitor has ${compData.schemaBlocks.length - yourData.schemaBlocks.length} more schema types. Consider adding: ${compOnlySchemas.join(", ")}`);
|
|
171
|
+
}
|
|
172
|
+
if (competitorOnlyKeywords.length > 0) {
|
|
173
|
+
lines.push(` • Keyword gap: Competitor ranks for ${competitorOnlyKeywords.length} keywords you don't cover. Consider addressing: ${competitorOnlyKeywords.map((k) => `"${k.keyword}"`).join(", ")}`);
|
|
174
|
+
}
|
|
175
|
+
if (lines[lines.length - 1] === `## Recommendations`) {
|
|
176
|
+
lines.push(` • No significant gaps detected. Your content appears competitive.`);
|
|
177
|
+
}
|
|
178
|
+
return textResult(lines.join("\n"), {
|
|
179
|
+
yourUrl: yourData.url,
|
|
180
|
+
competitorUrl: compData.url,
|
|
181
|
+
competitorOnlyKeywords,
|
|
182
|
+
sharedKeywords,
|
|
183
|
+
compOnlySchemas,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
ext.registerTool(batchTool);
|
|
192
|
+
ext.registerTool(gapTool);
|
|
193
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* seo_content_quality tool — heuristic content quality analysis.
|
|
3
|
+
*/
|
|
4
|
+
import type { ExtensionAPI } from "../../../sdk/coding-agent/index.js";
|
|
5
|
+
import type { SeoConfig } from "../config.js";
|
|
6
|
+
export declare function registerContentTools(ext: ExtensionAPI, config: SeoConfig): void;
|
|
7
|
+
//# sourceMappingURL=content.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/tools/content.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AACvF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAM9C,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI,CAyD/E"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { analyzeContentQuality } from "../analyzers/content.js";
|
|
2
|
+
import { fetchPage } from "../utils/fetcher.js";
|
|
3
|
+
import { extractText } from "../utils/parser.js";
|
|
4
|
+
import { errorResult, textResult } from "./helpers.js";
|
|
5
|
+
export function registerContentTools(ext, config) {
|
|
6
|
+
const contentTool = {
|
|
7
|
+
name: "seo_content_quality",
|
|
8
|
+
label: "SEO Content Quality",
|
|
9
|
+
description: "Analyze content quality using QRG-aligned heuristics: filler phrase detection, AI-pattern detection, " +
|
|
10
|
+
"information density (entities and numbers per token), repetition scoring, and an overall quality composite. " +
|
|
11
|
+
"Accepts either a URL (fetches and extracts text) or raw text. No external API calls — pure heuristics.",
|
|
12
|
+
promptSnippet: "`seo_content_quality { url? | text? }` — analyze content quality (filler, AI patterns, density)",
|
|
13
|
+
parameters: {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
url: { type: "string", description: "URL to fetch and analyze. If provided, text is extracted from the page." },
|
|
17
|
+
text: { type: "string", description: "Raw text to analyze. Used if 'url' is not provided." },
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
async execute(_toolCallId, params) {
|
|
21
|
+
const { url, text } = params;
|
|
22
|
+
try {
|
|
23
|
+
let content = text ?? "";
|
|
24
|
+
let effectiveUrl;
|
|
25
|
+
if (url && !text) {
|
|
26
|
+
const result = await fetchPage(url, config);
|
|
27
|
+
if (result.error) {
|
|
28
|
+
return errorResult(`Fetch failed: ${result.error}`);
|
|
29
|
+
}
|
|
30
|
+
content = extractText(result.content);
|
|
31
|
+
effectiveUrl = result.finalUrl;
|
|
32
|
+
}
|
|
33
|
+
if (!content) {
|
|
34
|
+
return errorResult("Either 'url' or 'text' must be provided.");
|
|
35
|
+
}
|
|
36
|
+
const result = analyzeContentQuality(content, effectiveUrl);
|
|
37
|
+
const summary = `✓ Content Quality Analysis${effectiveUrl ? `: ${effectiveUrl}` : ""}\n` +
|
|
38
|
+
`Word Count: ${result.wordCount}\n` +
|
|
39
|
+
`Overall Quality: ${result.overallQuality}/100\n\n` +
|
|
40
|
+
`Filler Score: ${result.fillerScore}/100 ${result.fillerScore > 30 ? "⚠️" : "✓"}\n` +
|
|
41
|
+
`AI Pattern Score: ${result.aiPatternScore}/100 ${result.aiPatternScore > 30 ? "⚠️" : "✓"}\n` +
|
|
42
|
+
`Information Density: ${result.informationDensity}/100 ${result.informationDensity < 10 ? "⚠️" : "✓"}\n` +
|
|
43
|
+
`Repetition Score: ${result.repetitionScore}/100 ${result.repetitionScore > 40 ? "⚠️" : "✓"}\n\n` +
|
|
44
|
+
`Flags: ${result.flags.length > 0 ? result.flags.join(", ") : "none"}\n` +
|
|
45
|
+
`Filler matches: ${result.fillerMatches.length > 0 ? result.fillerMatches.slice(0, 5).join(", ") : "none"}\n` +
|
|
46
|
+
`AI pattern matches: ${result.aiPatternMatches.length > 0 ? result.aiPatternMatches.slice(0, 5).join(", ") : "none"}`;
|
|
47
|
+
return textResult(summary, { result });
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
ext.registerTool(contentTool);
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"crawl.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/tools/crawl.ts"],"names":[],"mappings":"AAAA,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,CA4F7E"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { crawlSite } from "../analyzers/crawler.js";
|
|
2
|
+
import { errorResult, textResult, truncate } from "./helpers.js";
|
|
3
|
+
export function registerCrawlTools(ext, config) {
|
|
4
|
+
const crawlTool = {
|
|
5
|
+
name: "seo_crawl",
|
|
6
|
+
label: "SEO Site Crawler",
|
|
7
|
+
description: "Crawl an entire website starting from a URL, following internal links up to a configurable depth and page limit. " +
|
|
8
|
+
"Discovers all subpages, builds an internal link graph, detects broken links (4xx/5xx), " +
|
|
9
|
+
"finds duplicate content (same title/description/H1 across pages), and identifies orphan pages. " +
|
|
10
|
+
"Respects robots.txt and rate-limits requests. Returns per-page details and a site-wide summary. " +
|
|
11
|
+
"Use this before seo_audit to discover all pages, then run seo_audit on key subpages.",
|
|
12
|
+
promptSnippet: "`seo_crawl { url, maxPages?, maxDepth?, concurrency? }` — crawl a site and discover all subpages, broken links, and duplicate content",
|
|
13
|
+
promptGuidelines: [
|
|
14
|
+
"Use seo_crawl to discover all pages on a site before running targeted audits on key subpages.",
|
|
15
|
+
"The crawler respects robots.txt and rate-limits requests automatically.",
|
|
16
|
+
"Start with maxPages=50 for a quick scan, increase to 200-500 for a full site audit.",
|
|
17
|
+
],
|
|
18
|
+
parameters: {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
url: { type: "string", description: "The starting URL to crawl." },
|
|
22
|
+
maxPages: { type: "number", description: "Maximum pages to crawl. Default: 200." },
|
|
23
|
+
maxDepth: { type: "number", description: "Maximum crawl depth from start URL. Default: 5." },
|
|
24
|
+
concurrency: { type: "number", description: "Concurrent fetches. Default: 3." },
|
|
25
|
+
},
|
|
26
|
+
required: ["url"],
|
|
27
|
+
},
|
|
28
|
+
async execute(_toolCallId, params) {
|
|
29
|
+
const { url, maxPages, maxDepth, concurrency } = params;
|
|
30
|
+
try {
|
|
31
|
+
const crawlConfig = {};
|
|
32
|
+
if (maxPages !== undefined)
|
|
33
|
+
crawlConfig.maxPages = maxPages;
|
|
34
|
+
if (maxDepth !== undefined)
|
|
35
|
+
crawlConfig.maxDepth = maxDepth;
|
|
36
|
+
if (concurrency !== undefined)
|
|
37
|
+
crawlConfig.concurrency = concurrency;
|
|
38
|
+
const result = await crawlSite(url, config, crawlConfig);
|
|
39
|
+
const summary = formatCrawlSummary(result);
|
|
40
|
+
return textResult(summary, {
|
|
41
|
+
pagesCrawled: result.pagesCrawled,
|
|
42
|
+
brokenLinks: result.brokenLinks.length,
|
|
43
|
+
duplicateGroups: result.duplicateGroups.length,
|
|
44
|
+
orphanPages: result.orphanPages.length,
|
|
45
|
+
urls: result.pages.map((p) => p.url),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
const crawlReportTool = {
|
|
54
|
+
name: "seo_crawl_report",
|
|
55
|
+
label: "SEO Crawl Report",
|
|
56
|
+
description: "Generate a detailed report from a crawl result. " +
|
|
57
|
+
"Shows broken links with source pages, duplicate content groups (title/description/H1), " +
|
|
58
|
+
"orphan pages, link graph statistics, and per-page status codes. " +
|
|
59
|
+
"Use this after seo_crawl to get actionable insights about site structure problems.",
|
|
60
|
+
promptSnippet: "`seo_crawl_report { crawlResult }` — generate detailed crawl analysis report",
|
|
61
|
+
parameters: {
|
|
62
|
+
type: "object",
|
|
63
|
+
properties: {
|
|
64
|
+
crawlResult: {
|
|
65
|
+
type: "object",
|
|
66
|
+
description: "The CrawlResult object from seo_crawl.",
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
required: ["crawlResult"],
|
|
70
|
+
},
|
|
71
|
+
async execute(_toolCallId, params) {
|
|
72
|
+
const { crawlResult } = params;
|
|
73
|
+
try {
|
|
74
|
+
const report = generateCrawlReport(crawlResult);
|
|
75
|
+
return textResult(report, {
|
|
76
|
+
pagesCrawled: crawlResult.pagesCrawled,
|
|
77
|
+
brokenLinks: crawlResult.brokenLinks.length,
|
|
78
|
+
duplicateGroups: crawlResult.duplicateGroups.length,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
ext.registerTool(crawlTool);
|
|
87
|
+
ext.registerTool(crawlReportTool);
|
|
88
|
+
}
|
|
89
|
+
function formatCrawlSummary(result) {
|
|
90
|
+
const lines = [
|
|
91
|
+
`✓ Crawl Complete: ${result.baseUrl}`,
|
|
92
|
+
``,
|
|
93
|
+
`Pages crawled: ${result.pagesCrawled}`,
|
|
94
|
+
`Total pages discovered: ${result.totalPagesFound}`,
|
|
95
|
+
`Max depth reached: ${result.maxDepthReached}`,
|
|
96
|
+
`Broken links: ${result.brokenLinks.length}`,
|
|
97
|
+
`Duplicate content groups: ${result.duplicateGroups.length}`,
|
|
98
|
+
`Orphan pages: ${result.orphanPages.length}`,
|
|
99
|
+
`Errors: ${result.errors.length}`,
|
|
100
|
+
``,
|
|
101
|
+
`Crawled URLs (${result.pages.length}):`,
|
|
102
|
+
];
|
|
103
|
+
for (const page of result.pages) {
|
|
104
|
+
const statusIcon = page.statusCode >= 400 ? "❌" : page.statusCode >= 300 ? "↪" : "✓";
|
|
105
|
+
const title = page.pageData?.title ?? "(no title)";
|
|
106
|
+
lines.push(` ${statusIcon} [${page.statusCode}] depth=${page.depth} ${truncate(page.url, 100)} — ${truncate(title, 60)}`);
|
|
107
|
+
}
|
|
108
|
+
if (result.brokenLinks.length > 0) {
|
|
109
|
+
lines.push(``, `Broken Links (${result.brokenLinks.length}):`);
|
|
110
|
+
for (const bl of result.brokenLinks) {
|
|
111
|
+
lines.push(` ❌ ${bl.targetUrl} → ${bl.statusCode} (from ${bl.sourcePage})`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (result.duplicateGroups.length > 0) {
|
|
115
|
+
lines.push(``, `Duplicate Content (${result.duplicateGroups.length} groups):`);
|
|
116
|
+
for (const dg of result.duplicateGroups) {
|
|
117
|
+
lines.push(` [${dg.type}] "${truncate(dg.value, 80)}" — ${dg.pageUrls.length} pages`);
|
|
118
|
+
for (const url of dg.pageUrls.slice(0, 5)) {
|
|
119
|
+
lines.push(` • ${url}`);
|
|
120
|
+
}
|
|
121
|
+
if (dg.pageUrls.length > 5)
|
|
122
|
+
lines.push(` ... and ${dg.pageUrls.length - 5} more`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (result.errors.length > 0) {
|
|
126
|
+
lines.push(``, `Errors (${result.errors.length}):`);
|
|
127
|
+
for (const err of result.errors.slice(0, 10)) {
|
|
128
|
+
lines.push(` ⚠ ${err}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return lines.join("\n");
|
|
132
|
+
}
|
|
133
|
+
function generateCrawlReport(result) {
|
|
134
|
+
const lines = [
|
|
135
|
+
`# SEO Crawl Report`,
|
|
136
|
+
``,
|
|
137
|
+
`**Base URL:** ${result.baseUrl}`,
|
|
138
|
+
`**Pages crawled:** ${result.pagesCrawled}`,
|
|
139
|
+
`**Max depth:** ${result.maxDepthReached}`,
|
|
140
|
+
`**Errors:** ${result.errors.length}`,
|
|
141
|
+
``,
|
|
142
|
+
`## Page Status Summary`,
|
|
143
|
+
``,
|
|
144
|
+
`| URL | Status | Depth | Title |`,
|
|
145
|
+
`|-----|--------|-------|-------|`,
|
|
146
|
+
...result.pages.map((p) => `| ${p.url} | ${p.statusCode} | ${p.depth} | ${truncate(p.pageData?.title ?? "", 50)} |`),
|
|
147
|
+
``,
|
|
148
|
+
];
|
|
149
|
+
if (result.brokenLinks.length > 0) {
|
|
150
|
+
lines.push(`## Broken Links (${result.brokenLinks.length})`, ``, `| Source | Target | Status |`, `|--------|--------|--------|`, ...result.brokenLinks.map((bl) => `| ${bl.sourcePage} | ${bl.targetUrl} | ${bl.statusCode} |`), ``);
|
|
151
|
+
}
|
|
152
|
+
if (result.duplicateGroups.length > 0) {
|
|
153
|
+
lines.push(`## Duplicate Content (${result.duplicateGroups.length} groups)`, ``);
|
|
154
|
+
for (const dg of result.duplicateGroups) {
|
|
155
|
+
lines.push(`### ${dg.type.toUpperCase()}: "${truncate(dg.value, 100)}"`, ``, `**${dg.pageUrls.length} pages** share this ${dg.type}:`, ...dg.pageUrls.map((u) => `- ${u}`), ``);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (result.linkGraph.length > 0) {
|
|
159
|
+
const avgInlinks = result.linkGraph.reduce((s, n) => s + n.internalInlinks, 0) / result.linkGraph.length;
|
|
160
|
+
lines.push(`## Link Graph`, ``, `**Average internal inlinks per page:** ${avgInlinks.toFixed(1)}`, ``, `### Top Pages by Internal Inlinks`, ``, `| URL | Inlinks | Outlinks | Depth |`, `|-----|---------|----------|-------|`, ...result.linkGraph
|
|
161
|
+
.sort((a, b) => b.internalInlinks - a.internalInlinks)
|
|
162
|
+
.slice(0, 20)
|
|
163
|
+
.map((n) => `| ${n.url} | ${n.internalInlinks} | ${n.internalOutlinks} | ${n.depth} |`), ``);
|
|
164
|
+
}
|
|
165
|
+
return lines.join("\n");
|
|
166
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* seo_drift_baseline, seo_drift_compare, seo_drift_history tools.
|
|
3
|
+
*/
|
|
4
|
+
import type { ExtensionAPI } from "../../../sdk/coding-agent/index.js";
|
|
5
|
+
import type { SeoConfig } from "../config.js";
|
|
6
|
+
export declare function registerDriftTools(ext: ExtensionAPI, config: SeoConfig): void;
|
|
7
|
+
//# sourceMappingURL=drift.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"drift.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/tools/drift.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,kBAAkB,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI,CA2G7E"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { captureBaseline, compareBaseline, getBaselineHistory } from "../analyzers/drift.js";
|
|
2
|
+
import { errorResult, textResult } from "./helpers.js";
|
|
3
|
+
export function registerDriftTools(ext, config) {
|
|
4
|
+
const baselineTool = {
|
|
5
|
+
name: "seo_drift_baseline",
|
|
6
|
+
label: "SEO Drift Baseline",
|
|
7
|
+
description: "Capture an SEO baseline snapshot of a page and store it in a local SQLite database. " +
|
|
8
|
+
"Records: title, meta description, canonical, robots, H1/H2/H3 headings, schema blocks, Open Graph tags, " +
|
|
9
|
+
"HTML hash, schema hash, and status code. Use seo_drift_compare later to detect changes. " +
|
|
10
|
+
"Database path: configured by SEO_DB_PATH (default: ~/.spectral/seo/drift.db).",
|
|
11
|
+
promptSnippet: "`seo_drift_baseline { url }` — capture an SEO baseline snapshot for drift monitoring",
|
|
12
|
+
parameters: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
url: { type: "string", description: "The URL to capture a baseline for." },
|
|
16
|
+
},
|
|
17
|
+
required: ["url"],
|
|
18
|
+
},
|
|
19
|
+
async execute(_toolCallId, params) {
|
|
20
|
+
const { url } = params;
|
|
21
|
+
try {
|
|
22
|
+
const baseline = await captureBaseline(url, config);
|
|
23
|
+
const summary = `✓ Baseline captured: ${baseline.url}\n` +
|
|
24
|
+
`Baseline ID: ${baseline.id}\n` +
|
|
25
|
+
`Timestamp: ${baseline.timestamp}\n` +
|
|
26
|
+
`Title: ${baseline.title ?? "(none)"}\n` +
|
|
27
|
+
`Canonical: ${baseline.canonical ?? "(none)"}\n` +
|
|
28
|
+
`H1: ${baseline.h1 ?? "(none)"}\n` +
|
|
29
|
+
`Status Code: ${baseline.statusCode}\n` +
|
|
30
|
+
`HTML Hash: ${baseline.htmlHash}\n` +
|
|
31
|
+
`Schema Hash: ${baseline.schemaHash}`;
|
|
32
|
+
return textResult(summary, { baseline });
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
const compareTool = {
|
|
40
|
+
name: "seo_drift_compare",
|
|
41
|
+
label: "SEO Drift Compare",
|
|
42
|
+
description: "Compare a page's current state against a previously captured baseline. " +
|
|
43
|
+
"Detects changes in: title, meta description, canonical, robots, H1/H2/H3 headings, schema blocks, " +
|
|
44
|
+
"status code, and HTML hash. Assigns a severity level (none/low/medium/high) based on which fields changed. " +
|
|
45
|
+
"If baselineId is omitted, compares against the latest baseline for the URL.",
|
|
46
|
+
promptSnippet: "`seo_drift_compare { url, baselineId? }` — compare current page vs baseline snapshot",
|
|
47
|
+
parameters: {
|
|
48
|
+
type: "object",
|
|
49
|
+
properties: {
|
|
50
|
+
url: { type: "string", description: "The URL to compare." },
|
|
51
|
+
baselineId: { type: "number", description: "Specific baseline ID to compare against. If omitted, uses the latest." },
|
|
52
|
+
},
|
|
53
|
+
required: ["url"],
|
|
54
|
+
},
|
|
55
|
+
async execute(_toolCallId, params) {
|
|
56
|
+
const { url, baselineId } = params;
|
|
57
|
+
try {
|
|
58
|
+
const result = await compareBaseline(url, config, baselineId);
|
|
59
|
+
const summary = `✓ Drift Comparison: ${result.url}\n` +
|
|
60
|
+
`Baseline ID: ${result.baselineId} (${result.baselineTimestamp})\n` +
|
|
61
|
+
`Compared At: ${result.comparedAt}\n` +
|
|
62
|
+
`Severity: ${result.severity.toUpperCase()}\n` +
|
|
63
|
+
`Changed Fields (${result.changedFields.length}): ${result.changedFields.join(", ") || "(none)"}`;
|
|
64
|
+
return textResult(summary, { result });
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
const historyTool = {
|
|
72
|
+
name: "seo_drift_history",
|
|
73
|
+
label: "SEO Drift History",
|
|
74
|
+
description: "List baseline snapshots for a URL, ordered by most recent. " +
|
|
75
|
+
"Returns baseline IDs, timestamps, titles, and status codes.",
|
|
76
|
+
promptSnippet: "`seo_drift_history { url, limit? }` — list baseline snapshots for a URL",
|
|
77
|
+
parameters: {
|
|
78
|
+
type: "object",
|
|
79
|
+
properties: {
|
|
80
|
+
url: { type: "string", description: "The URL to get baseline history for." },
|
|
81
|
+
limit: { type: "number", description: "Maximum number of baselines to return. Default: 20." },
|
|
82
|
+
},
|
|
83
|
+
required: ["url"],
|
|
84
|
+
},
|
|
85
|
+
async execute(_toolCallId, params) {
|
|
86
|
+
const { url, limit } = params;
|
|
87
|
+
try {
|
|
88
|
+
const history = getBaselineHistory(url, config, limit ?? 20);
|
|
89
|
+
const summary = `✓ Drift History: ${url}\n` +
|
|
90
|
+
`Baselines: ${history.length}\n\n` +
|
|
91
|
+
history
|
|
92
|
+
.map((b) => ` #${b.id} | ${b.timestamp} | ${b.statusCode} | ${b.title ?? "(no title)"}`)
|
|
93
|
+
.join("\n");
|
|
94
|
+
return textResult(summary, { history });
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
return errorResult(err instanceof Error ? err.message : String(err));
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
ext.registerTool(baselineTool);
|
|
102
|
+
ext.registerTool(compareTool);
|
|
103
|
+
ext.registerTool(historyTool);
|
|
104
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* seo_fetch and seo_render tool definitions.
|
|
3
|
+
*/
|
|
4
|
+
import type { ExtensionAPI } from "../../../sdk/coding-agent/index.js";
|
|
5
|
+
import type { SeoConfig } from "../config.js";
|
|
6
|
+
export declare function registerFetchTools(ext: ExtensionAPI, config: SeoConfig): void;
|
|
7
|
+
//# sourceMappingURL=fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/tools/fetch.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,CA0I7E"}
|