@aexol/spectral 0.9.42 → 0.9.43

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.
Files changed (114) hide show
  1. package/dist/extensions/aexol-mcp.d.ts.map +1 -1
  2. package/dist/extensions/aexol-mcp.js +44 -9
  3. package/dist/extensions/browser/browser-service.d.ts +4 -1
  4. package/dist/extensions/browser/browser-service.d.ts.map +1 -1
  5. package/dist/extensions/browser/browser-service.js +34 -7
  6. package/dist/extensions/browser/config.d.ts +3 -0
  7. package/dist/extensions/browser/config.d.ts.map +1 -1
  8. package/dist/extensions/browser/config.js +1 -0
  9. package/dist/extensions/browser/index.d.ts.map +1 -1
  10. package/dist/extensions/browser/index.js +3 -0
  11. package/dist/extensions/browser/record/record-tool.d.ts.map +1 -1
  12. package/dist/extensions/browser/record/record-tool.js +27 -4
  13. package/dist/extensions/browser/scenario/markdown-parser.d.ts +44 -0
  14. package/dist/extensions/browser/scenario/markdown-parser.d.ts.map +1 -0
  15. package/dist/extensions/browser/scenario/markdown-parser.js +159 -0
  16. package/dist/extensions/browser/scenario/scenario-tool.d.ts +4 -0
  17. package/dist/extensions/browser/scenario/scenario-tool.d.ts.map +1 -0
  18. package/dist/extensions/browser/scenario/scenario-tool.js +381 -0
  19. package/dist/extensions/browser/tools/core.js +1 -1
  20. package/dist/extensions/browser/tools/screenshot.d.ts.map +1 -1
  21. package/dist/extensions/browser/tools/screenshot.js +2 -1
  22. package/dist/extensions/seo/analyzers/agent-ux.d.ts +5 -0
  23. package/dist/extensions/seo/analyzers/agent-ux.d.ts.map +1 -0
  24. package/dist/extensions/seo/analyzers/agent-ux.js +95 -0
  25. package/dist/extensions/seo/analyzers/content.d.ts +16 -0
  26. package/dist/extensions/seo/analyzers/content.d.ts.map +1 -0
  27. package/dist/extensions/seo/analyzers/content.js +174 -0
  28. package/dist/extensions/seo/analyzers/drift.d.ts +13 -0
  29. package/dist/extensions/seo/analyzers/drift.d.ts.map +1 -0
  30. package/dist/extensions/seo/analyzers/drift.js +204 -0
  31. package/dist/extensions/seo/analyzers/images.d.ts +10 -0
  32. package/dist/extensions/seo/analyzers/images.d.ts.map +1 -0
  33. package/dist/extensions/seo/analyzers/images.js +49 -0
  34. package/dist/extensions/seo/analyzers/parasite.d.ts +18 -0
  35. package/dist/extensions/seo/analyzers/parasite.d.ts.map +1 -0
  36. package/dist/extensions/seo/analyzers/parasite.js +142 -0
  37. package/dist/extensions/seo/analyzers/preload.d.ts +12 -0
  38. package/dist/extensions/seo/analyzers/preload.d.ts.map +1 -0
  39. package/dist/extensions/seo/analyzers/preload.js +108 -0
  40. package/dist/extensions/seo/analyzers/schema.d.ts +21 -0
  41. package/dist/extensions/seo/analyzers/schema.d.ts.map +1 -0
  42. package/dist/extensions/seo/analyzers/schema.js +324 -0
  43. package/dist/extensions/seo/analyzers/sitemap.d.ts +20 -0
  44. package/dist/extensions/seo/analyzers/sitemap.d.ts.map +1 -0
  45. package/dist/extensions/seo/analyzers/sitemap.js +121 -0
  46. package/dist/extensions/seo/analyzers/technical.d.ts +11 -0
  47. package/dist/extensions/seo/analyzers/technical.d.ts.map +1 -0
  48. package/dist/extensions/seo/analyzers/technical.js +268 -0
  49. package/dist/extensions/seo/config.d.ts +24 -0
  50. package/dist/extensions/seo/config.d.ts.map +1 -0
  51. package/dist/extensions/seo/config.js +48 -0
  52. package/dist/extensions/seo/index.d.ts +16 -0
  53. package/dist/extensions/seo/index.d.ts.map +1 -0
  54. package/dist/extensions/seo/index.js +88 -0
  55. package/dist/extensions/seo/tools/agent-ux.d.ts +7 -0
  56. package/dist/extensions/seo/tools/agent-ux.d.ts.map +1 -0
  57. package/dist/extensions/seo/tools/agent-ux.js +63 -0
  58. package/dist/extensions/seo/tools/content.d.ts +7 -0
  59. package/dist/extensions/seo/tools/content.d.ts.map +1 -0
  60. package/dist/extensions/seo/tools/content.js +55 -0
  61. package/dist/extensions/seo/tools/drift.d.ts +7 -0
  62. package/dist/extensions/seo/tools/drift.d.ts.map +1 -0
  63. package/dist/extensions/seo/tools/drift.js +104 -0
  64. package/dist/extensions/seo/tools/fetch.d.ts +7 -0
  65. package/dist/extensions/seo/tools/fetch.d.ts.map +1 -0
  66. package/dist/extensions/seo/tools/fetch.js +123 -0
  67. package/dist/extensions/seo/tools/full-audit.d.ts +10 -0
  68. package/dist/extensions/seo/tools/full-audit.d.ts.map +1 -0
  69. package/dist/extensions/seo/tools/full-audit.js +191 -0
  70. package/dist/extensions/seo/tools/helpers.d.ts +13 -0
  71. package/dist/extensions/seo/tools/helpers.d.ts.map +1 -0
  72. package/dist/extensions/seo/tools/helpers.js +24 -0
  73. package/dist/extensions/seo/tools/images.d.ts +7 -0
  74. package/dist/extensions/seo/tools/images.d.ts.map +1 -0
  75. package/dist/extensions/seo/tools/images.js +46 -0
  76. package/dist/extensions/seo/tools/parasite.d.ts +7 -0
  77. package/dist/extensions/seo/tools/parasite.d.ts.map +1 -0
  78. package/dist/extensions/seo/tools/parasite.js +53 -0
  79. package/dist/extensions/seo/tools/parse.d.ts +7 -0
  80. package/dist/extensions/seo/tools/parse.d.ts.map +1 -0
  81. package/dist/extensions/seo/tools/parse.js +66 -0
  82. package/dist/extensions/seo/tools/preload.d.ts +7 -0
  83. package/dist/extensions/seo/tools/preload.d.ts.map +1 -0
  84. package/dist/extensions/seo/tools/preload.js +52 -0
  85. package/dist/extensions/seo/tools/report.d.ts +4 -0
  86. package/dist/extensions/seo/tools/report.d.ts.map +1 -0
  87. package/dist/extensions/seo/tools/report.js +167 -0
  88. package/dist/extensions/seo/tools/schema.d.ts +7 -0
  89. package/dist/extensions/seo/tools/schema.d.ts.map +1 -0
  90. package/dist/extensions/seo/tools/schema.js +105 -0
  91. package/dist/extensions/seo/tools/sitemap.d.ts +7 -0
  92. package/dist/extensions/seo/tools/sitemap.d.ts.map +1 -0
  93. package/dist/extensions/seo/tools/sitemap.js +85 -0
  94. package/dist/extensions/seo/tools/technical.d.ts +7 -0
  95. package/dist/extensions/seo/tools/technical.d.ts.map +1 -0
  96. package/dist/extensions/seo/tools/technical.js +62 -0
  97. package/dist/extensions/seo/types.d.ts +289 -0
  98. package/dist/extensions/seo/types.d.ts.map +1 -0
  99. package/dist/extensions/seo/types.js +7 -0
  100. package/dist/extensions/seo/utils/fetcher.d.ts +28 -0
  101. package/dist/extensions/seo/utils/fetcher.d.ts.map +1 -0
  102. package/dist/extensions/seo/utils/fetcher.js +118 -0
  103. package/dist/extensions/seo/utils/parser.d.ts +8 -0
  104. package/dist/extensions/seo/utils/parser.d.ts.map +1 -0
  105. package/dist/extensions/seo/utils/parser.js +265 -0
  106. package/dist/extensions/seo/utils/renderer.d.ts +39 -0
  107. package/dist/extensions/seo/utils/renderer.d.ts.map +1 -0
  108. package/dist/extensions/seo/utils/renderer.js +190 -0
  109. package/dist/extensions/seo/utils/url-safety.d.ts +31 -0
  110. package/dist/extensions/seo/utils/url-safety.d.ts.map +1 -0
  111. package/dist/extensions/seo/utils/url-safety.js +200 -0
  112. package/dist/sdk/coding-agent/core/extensions/native-extensions.d.ts.map +1 -1
  113. package/dist/sdk/coding-agent/core/extensions/native-extensions.js +11 -0
  114. package/package.json +1 -1
@@ -0,0 +1,268 @@
1
+ import { fetchPage, fetchRobotsTxt } from "../utils/fetcher.js";
2
+ import { parsePage } from "../utils/parser.js";
3
+ const AI_CRAWLER_TOKENS = [
4
+ "GPTBot",
5
+ "ChatGPT-User",
6
+ "ClaudeBot",
7
+ "PerplexityBot",
8
+ "Bytespider",
9
+ "Google-Extended",
10
+ "CCBot",
11
+ ];
12
+ function parseRobotsTxt(content) {
13
+ const lines = content.split("\n").map((l) => l.trim());
14
+ let sitemapDeclared = false;
15
+ const aiCrawlerRules = [];
16
+ let currentUserAgents = [];
17
+ for (const line of lines) {
18
+ if (!line || line.startsWith("#"))
19
+ continue;
20
+ const colonIdx = line.indexOf(":");
21
+ if (colonIdx === -1)
22
+ continue;
23
+ const key = line.slice(0, colonIdx).trim().toLowerCase();
24
+ const value = line.slice(colonIdx + 1).trim();
25
+ if (key === "user-agent") {
26
+ currentUserAgents.push(value);
27
+ }
28
+ else if (key === "disallow" || key === "allow") {
29
+ for (const ua of currentUserAgents) {
30
+ if (AI_CRAWLER_TOKENS.includes(ua)) {
31
+ aiCrawlerRules.push(`${ua}: ${key} ${value}`);
32
+ }
33
+ }
34
+ }
35
+ else if (key === "sitemap") {
36
+ sitemapDeclared = true;
37
+ }
38
+ // Reset user-agents when we hit a non-user-agent line after a group.
39
+ if (key !== "user-agent") {
40
+ currentUserAgents = [];
41
+ }
42
+ }
43
+ return { sitemapDeclared, aiCrawlerRules };
44
+ }
45
+ const SECURITY_HEADERS = [
46
+ "content-security-policy",
47
+ "strict-transport-security",
48
+ "x-frame-options",
49
+ "x-content-type-options",
50
+ "referrer-policy",
51
+ "permissions-policy",
52
+ ];
53
+ export async function auditTechnical(url, config, pageData, fetchResult) {
54
+ const issues = [];
55
+ const categoryScores = {};
56
+ // Fetch the page if not provided.
57
+ if (!fetchResult) {
58
+ fetchResult = await fetchPage(url, config);
59
+ }
60
+ if (!pageData) {
61
+ pageData = parsePage(fetchResult.content, fetchResult.finalUrl);
62
+ }
63
+ // Fetch robots.txt.
64
+ let robotsResult = null;
65
+ try {
66
+ robotsResult = await fetchRobotsTxt(url, config);
67
+ }
68
+ catch {
69
+ // ignore
70
+ }
71
+ const robotsContent = robotsResult?.statusCode === 200 ? robotsResult.content : "";
72
+ const robotsParsed = robotsContent ? parseRobotsTxt(robotsContent) : { sitemapDeclared: false, aiCrawlerRules: [] };
73
+ // --- Crawlability ---
74
+ let crawlScore = 100;
75
+ if (robotsResult?.statusCode !== 200) {
76
+ crawlScore -= 30;
77
+ issues.push({
78
+ category: "Crawlability",
79
+ severity: "high",
80
+ title: "robots.txt not found or inaccessible",
81
+ description: `robots.txt returned status ${robotsResult?.statusCode ?? "N/A"}.`,
82
+ recommendation: "Create a robots.txt file at the site root with appropriate directives.",
83
+ });
84
+ }
85
+ if (!robotsParsed.sitemapDeclared) {
86
+ crawlScore -= 15;
87
+ issues.push({
88
+ category: "Crawlability",
89
+ severity: "medium",
90
+ title: "Sitemap not declared in robots.txt",
91
+ description: "No Sitemap directive found in robots.txt.",
92
+ recommendation: "Add 'Sitemap: https://example.com/sitemap.xml' to robots.txt.",
93
+ });
94
+ }
95
+ categoryScores["Crawlability"] = Math.max(0, crawlScore);
96
+ // --- Indexability ---
97
+ let indexScore = 100;
98
+ if (pageData.metaRobots?.toLowerCase().includes("noindex")) {
99
+ indexScore -= 40;
100
+ issues.push({
101
+ category: "Indexability",
102
+ severity: "critical",
103
+ title: "Page is blocked from indexing (noindex)",
104
+ description: `meta robots contains 'noindex': ${pageData.metaRobots}`,
105
+ recommendation: "Remove the noindex directive if this page should be indexed.",
106
+ });
107
+ }
108
+ if (pageData.h1Count === 0) {
109
+ indexScore -= 15;
110
+ issues.push({
111
+ category: "Indexability",
112
+ severity: "medium",
113
+ title: "Missing H1 heading",
114
+ description: "The page has no H1 element.",
115
+ recommendation: "Add a single, descriptive H1 heading to the page.",
116
+ });
117
+ }
118
+ if (pageData.h1Count > 1) {
119
+ indexScore -= 10;
120
+ issues.push({
121
+ category: "Indexability",
122
+ severity: "low",
123
+ title: "Multiple H1 headings",
124
+ description: `Found ${pageData.h1Count} H1 elements. Best practice is one H1 per page.`,
125
+ recommendation: "Use a single H1 per page; demote additional H1s to H2.",
126
+ });
127
+ }
128
+ if (pageData.canonical) {
129
+ const canonicalUrl = new URL(pageData.canonical, fetchResult.finalUrl).href;
130
+ if (canonicalUrl !== fetchResult.finalUrl) {
131
+ indexScore -= 10;
132
+ issues.push({
133
+ category: "Indexability",
134
+ severity: "low",
135
+ title: "Canonical URL differs from page URL",
136
+ description: `canonical=${canonicalUrl}, url=${fetchResult.finalUrl}`,
137
+ recommendation: "Verify the canonical tag is correct — it may point to a different preferred URL intentionally.",
138
+ });
139
+ }
140
+ }
141
+ else {
142
+ indexScore -= 10;
143
+ issues.push({
144
+ category: "Indexability",
145
+ severity: "medium",
146
+ title: "Missing canonical tag",
147
+ description: "No canonical link tag found.",
148
+ recommendation: "Add a self-referencing canonical tag to prevent duplicate content issues.",
149
+ });
150
+ }
151
+ categoryScores["Indexability"] = Math.max(0, indexScore);
152
+ // --- Security ---
153
+ let securityScore = 100;
154
+ const isHttps = fetchResult.finalUrl.startsWith("https://");
155
+ if (!isHttps) {
156
+ securityScore -= 50;
157
+ issues.push({
158
+ category: "Security",
159
+ severity: "critical",
160
+ title: "Site is not served over HTTPS",
161
+ description: "The page is served over HTTP, not HTTPS.",
162
+ recommendation: "Install an SSL certificate and redirect all HTTP traffic to HTTPS.",
163
+ });
164
+ }
165
+ const securityHeaders = {};
166
+ for (const header of SECURITY_HEADERS) {
167
+ securityHeaders[header] = fetchResult.headers[header] ?? null;
168
+ if (!securityHeaders[header]) {
169
+ securityScore -= 10;
170
+ issues.push({
171
+ category: "Security",
172
+ severity: "medium",
173
+ title: `Missing security header: ${header}`,
174
+ description: `The ${header} response header is not set.`,
175
+ recommendation: `Configure the ${header} header on the web server.`,
176
+ });
177
+ }
178
+ }
179
+ categoryScores["Security"] = Math.max(0, securityScore);
180
+ // --- URL Structure ---
181
+ let urlScore = 100;
182
+ if (fetchResult.finalUrl.length > 100) {
183
+ urlScore -= 10;
184
+ issues.push({
185
+ category: "URL Structure",
186
+ severity: "low",
187
+ title: "URL is longer than 100 characters",
188
+ description: `URL length: ${fetchResult.finalUrl.length}`,
189
+ recommendation: "Shorten the URL where possible for better readability and SEO.",
190
+ });
191
+ }
192
+ if (fetchResult.redirectChain.length > 1) {
193
+ urlScore -= 15;
194
+ issues.push({
195
+ category: "URL Structure",
196
+ severity: "medium",
197
+ title: "Redirect chain detected",
198
+ description: `${fetchResult.redirectChain.length} redirects before reaching the final URL.`,
199
+ recommendation: "Reduce redirect chains to a single hop (max 1 redirect).",
200
+ });
201
+ }
202
+ // Check for uppercase in path.
203
+ try {
204
+ const path = new URL(fetchResult.finalUrl).pathname;
205
+ if (path !== path.toLowerCase() && path.match(/[A-Z]/)) {
206
+ urlScore -= 5;
207
+ issues.push({
208
+ category: "URL Structure",
209
+ severity: "low",
210
+ title: "URL contains uppercase characters",
211
+ description: `Path: ${path}`,
212
+ recommendation: "Use lowercase URLs for consistency.",
213
+ });
214
+ }
215
+ }
216
+ catch {
217
+ // ignore
218
+ }
219
+ categoryScores["URL Structure"] = Math.max(0, urlScore);
220
+ // --- Mobile Optimization ---
221
+ let mobileScore = 100;
222
+ const mobileOptimized = !!pageData.viewport;
223
+ if (!mobileOptimized) {
224
+ mobileScore -= 50;
225
+ issues.push({
226
+ category: "Mobile",
227
+ severity: "critical",
228
+ title: "Missing viewport meta tag",
229
+ description: "No viewport meta tag found. Mobile-first indexing requires responsive design.",
230
+ recommendation: 'Add <meta name="viewport" content="width=device-width, initial-scale=1"> to the <head>.',
231
+ });
232
+ }
233
+ categoryScores["Mobile"] = Math.max(0, mobileScore);
234
+ // --- AI Crawler Management ---
235
+ let aiScore = 100;
236
+ if (robotsParsed.aiCrawlerRules.length === 0) {
237
+ // Not an error, but an opportunity.
238
+ aiScore -= 0;
239
+ issues.push({
240
+ category: "AI Crawlers",
241
+ severity: "low",
242
+ title: "No AI crawler directives in robots.txt",
243
+ description: "No rules found for GPTBot, ClaudeBot, Google-Extended, or other AI crawlers.",
244
+ recommendation: "Consider your AI visibility strategy — block or allow AI training crawlers explicitly.",
245
+ });
246
+ }
247
+ categoryScores["AI Crawlers"] = aiScore;
248
+ // --- Overall score ---
249
+ const scores = Object.values(categoryScores);
250
+ const overallScore = scores.length > 0
251
+ ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length)
252
+ : 0;
253
+ return {
254
+ url: fetchResult.finalUrl,
255
+ score: overallScore,
256
+ issues,
257
+ categoryScores,
258
+ robotsTxt: {
259
+ exists: robotsResult?.statusCode === 200,
260
+ sitemapDeclared: robotsParsed.sitemapDeclared,
261
+ aiCrawlerRules: robotsParsed.aiCrawlerRules,
262
+ content: robotsContent || undefined,
263
+ },
264
+ securityHeaders,
265
+ isHttps,
266
+ mobileOptimized,
267
+ };
268
+ }
@@ -0,0 +1,24 @@
1
+ export interface SeoConfig {
2
+ /** Default request timeout in milliseconds. */
3
+ timeout: number;
4
+ /** Default User-Agent string for HTTP requests. */
5
+ userAgent: string;
6
+ /** User-Agent used when impersonating Googlebot. */
7
+ googlebotUserAgent: string;
8
+ /** Whether to follow HTTP redirects by default. */
9
+ followRedirects: boolean;
10
+ /** Maximum number of redirects to follow. */
11
+ maxRedirects: number;
12
+ /** Directory for SEO report output. */
13
+ outputDir: string;
14
+ /** Path to the drift-monitoring SQLite database. */
15
+ dbPath: string;
16
+ /** Default viewport for rendering. */
17
+ defaultViewport: "desktop" | "mobile";
18
+ /** Block resource types during rendering to speed up fetches. */
19
+ blockRenderResources: string[];
20
+ /** Whether SSRF protection is enabled (blocks private/internal IPs). */
21
+ ssrfProtection: boolean;
22
+ }
23
+ export declare function loadSeoConfig(): SeoConfig;
24
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/extensions/seo/config.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,SAAS;IACxB,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,SAAS,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mDAAmD;IACnD,eAAe,EAAE,OAAO,CAAC;IACzB,6CAA6C;IAC7C,YAAY,EAAE,MAAM,CAAC;IACrB,uCAAuC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,MAAM,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,eAAe,EAAE,SAAS,GAAG,QAAQ,CAAC;IACtC,iEAAiE;IACjE,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B,wEAAwE;IACxE,cAAc,EAAE,OAAO,CAAC;CACzB;AA8BD,wBAAgB,aAAa,IAAI,SAAS,CAezC"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Configuration for the SEO extension.
3
+ *
4
+ * All values are environment-variable driven with sensible defaults so the
5
+ * extension works out-of-the-box without any setup.
6
+ */
7
+ import { homedir } from "node:os";
8
+ import { join } from "node:path";
9
+ function envBool(key, def) {
10
+ const v = process.env[key];
11
+ if (v === undefined)
12
+ return def;
13
+ return v === "1" || v.toLowerCase() === "true" || v.toLowerCase() === "yes";
14
+ }
15
+ function envInt(key, def) {
16
+ const v = process.env[key];
17
+ if (v === undefined)
18
+ return def;
19
+ const n = Number.parseInt(v, 10);
20
+ return Number.isNaN(n) ? def : n;
21
+ }
22
+ function envList(key, def) {
23
+ const v = process.env[key];
24
+ if (!v)
25
+ return def;
26
+ return v
27
+ .split(",")
28
+ .map((s) => s.trim())
29
+ .filter(Boolean);
30
+ }
31
+ const DEFAULT_USER_AGENT = "Mozilla/5.0 (compatible; SpectralSEOBot/1.0; +https://aexol.com)";
32
+ const GOOGLEBOT_UA = "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)";
33
+ export function loadSeoConfig() {
34
+ const home = homedir();
35
+ const spectralDir = process.env.SPECTRAL_CONFIG_DIR ?? join(home, ".spectral");
36
+ return {
37
+ timeout: envInt("SEO_TIMEOUT", 30_000),
38
+ userAgent: process.env.SEO_USER_AGENT ?? DEFAULT_USER_AGENT,
39
+ googlebotUserAgent: process.env.SEO_GOOGLEBOT_UA ?? GOOGLEBOT_UA,
40
+ followRedirects: envBool("SEO_FOLLOW_REDIRECTS", true),
41
+ maxRedirects: envInt("SEO_MAX_REDIRECTS", 10),
42
+ outputDir: process.env.SEO_OUTPUT_DIR ?? join(spectralDir, "seo-reports"),
43
+ dbPath: process.env.SEO_DB_PATH ?? join(spectralDir, "seo", "drift.db"),
44
+ defaultViewport: process.env.SEO_VIEWPORT ?? "desktop",
45
+ blockRenderResources: envList("SEO_BLOCK_RESOURCES", ["image", "font", "stylesheet"]),
46
+ ssrfProtection: envBool("SEO_SSRF_PROTECTION", true),
47
+ };
48
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * SEO Analysis extension — native TypeScript SEO audit tools.
3
+ *
4
+ * Provides offline-first SEO analysis with no external API keys required.
5
+ * Covers: on-page parsing, technical audits, content quality, Schema.org
6
+ * detection/generation, sitemap analysis/generation, image audits, preload/
7
+ * bfcache audits, agent UX audits, parasite-SEO risk scanning, drift
8
+ * monitoring, and full audit orchestration with report generation.
9
+ *
10
+ * Ported from AgriciDaniel/claude-seo — only components that don't require
11
+ * external service integrations (PageSpeed API, GSC, GA4, DataForSEO, etc.)
12
+ * are included.
13
+ */
14
+ import type { ExtensionAPI } from "../../sdk/coding-agent/index.js";
15
+ export default function seoExtension(ext: ExtensionAPI): Promise<void>;
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/extensions/seo/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AAkBpE,wBAA8B,YAAY,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA+C3E"}
@@ -0,0 +1,88 @@
1
+ import { loadSeoConfig } from "./config.js";
2
+ import { closeRenderer } from "./utils/renderer.js";
3
+ import { closeDriftDb } from "./analyzers/drift.js";
4
+ import { registerFetchTools } from "./tools/fetch.js";
5
+ import { registerParseTools } from "./tools/parse.js";
6
+ import { registerTechnicalTools } from "./tools/technical.js";
7
+ import { registerContentTools } from "./tools/content.js";
8
+ import { registerSchemaTools } from "./tools/schema.js";
9
+ import { registerSitemapTools } from "./tools/sitemap.js";
10
+ import { registerImageTools } from "./tools/images.js";
11
+ import { registerPreloadTools } from "./tools/preload.js";
12
+ import { registerAgentUxTools } from "./tools/agent-ux.js";
13
+ import { registerParasiteTools } from "./tools/parasite.js";
14
+ import { registerDriftTools } from "./tools/drift.js";
15
+ import { registerFullAuditTools } from "./tools/full-audit.js";
16
+ import { registerReportTools } from "./tools/report.js";
17
+ export default async function seoExtension(ext) {
18
+ const config = loadSeoConfig();
19
+ // Clean up resources on session shutdown.
20
+ ext.on("session_shutdown", async () => {
21
+ try {
22
+ await closeRenderer();
23
+ closeDriftDb();
24
+ }
25
+ catch (err) {
26
+ const msg = err instanceof Error ? err.message : String(err);
27
+ process.stderr.write(`[seo] Error during cleanup: ${msg}\n`);
28
+ }
29
+ });
30
+ const registrars = [
31
+ { name: "fetch", fn: () => registerFetchTools(ext, config) },
32
+ { name: "parse", fn: () => registerParseTools(ext, config) },
33
+ { name: "technical", fn: () => registerTechnicalTools(ext, config) },
34
+ { name: "content", fn: () => registerContentTools(ext, config) },
35
+ { name: "schema", fn: () => registerSchemaTools(ext, config) },
36
+ { name: "sitemap", fn: () => registerSitemapTools(ext, config) },
37
+ { name: "images", fn: () => registerImageTools(ext, config) },
38
+ { name: "preload", fn: () => registerPreloadTools(ext, config) },
39
+ { name: "agent-ux", fn: () => registerAgentUxTools(ext, config) },
40
+ { name: "parasite", fn: () => registerParasiteTools(ext, config) },
41
+ { name: "drift", fn: () => registerDriftTools(ext, config) },
42
+ { name: "full-audit", fn: () => registerFullAuditTools(ext, config) },
43
+ { name: "report", fn: () => registerReportTools(ext, config) },
44
+ ];
45
+ let totalRegistered = 0;
46
+ for (const registrar of registrars) {
47
+ try {
48
+ const before = countSeoTools(ext);
49
+ registrar.fn();
50
+ const after = countSeoTools(ext);
51
+ totalRegistered += after - before;
52
+ }
53
+ catch (err) {
54
+ const msg = err instanceof Error ? err.message : String(err);
55
+ process.stderr.write(`[seo] Failed to register ${registrar.name} tools: ${msg}\n`);
56
+ }
57
+ }
58
+ process.stderr.write(`[seo] Registered ${totalRegistered} SEO analysis tool(s). ` +
59
+ `Output dir: ${config.outputDir}, Drift DB: ${config.dbPath}.\n`);
60
+ }
61
+ const SEO_TOOL_NAMES = [
62
+ "seo_fetch",
63
+ "seo_render",
64
+ "seo_parse_page",
65
+ "seo_technical_audit",
66
+ "seo_content_quality",
67
+ "seo_schema_audit",
68
+ "seo_schema_generate",
69
+ "seo_sitemap_analyze",
70
+ "seo_sitemap_generate",
71
+ "seo_image_audit",
72
+ "seo_preload_audit",
73
+ "seo_agent_ux_audit",
74
+ "seo_parasite_risk",
75
+ "seo_drift_baseline",
76
+ "seo_drift_compare",
77
+ "seo_drift_history",
78
+ "seo_audit",
79
+ "seo_report",
80
+ ];
81
+ function countSeoTools(ext) {
82
+ let count = 0;
83
+ for (const name of SEO_TOOL_NAMES) {
84
+ if (ext.getToolDefinition(name))
85
+ count++;
86
+ }
87
+ return count;
88
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * seo_agent_ux_audit tool — AI-agent friendliness audit.
3
+ */
4
+ import type { ExtensionAPI } from "../../../sdk/coding-agent/index.js";
5
+ import type { SeoConfig } from "../config.js";
6
+ export declare function registerAgentUxTools(ext: ExtensionAPI, config: SeoConfig): void;
7
+ //# sourceMappingURL=agent-ux.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-ux.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/tools/agent-ux.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,CAmE/E"}
@@ -0,0 +1,63 @@
1
+ import { auditAgentUx } from "../analyzers/agent-ux.js";
2
+ import { renderPage, isRendererAvailable } from "../utils/renderer.js";
3
+ import { errorResult, textResult } from "./helpers.js";
4
+ export function registerAgentUxTools(ext, config) {
5
+ const agentUxTool = {
6
+ name: "seo_agent_ux_audit",
7
+ label: "SEO Agent UX Audit",
8
+ description: "Audit a page for AI-agent friendliness using Playwright's accessibility tree and semantic HTML analysis. " +
9
+ "Checks: real <button>/<a> vs <div onclick> widgets, semantic landmarks (header/nav/main/footer), " +
10
+ "inputs without labels, unnamed interactive elements in the accessibility tree, and role='generic' ratio. " +
11
+ "Returns a 0-100 Agent-UX score and improvement opportunities. Requires Playwright Chromium.",
12
+ promptSnippet: "`seo_agent_ux_audit { url, viewport? }` — audit AI-agent friendliness (a11y tree + semantic HTML)",
13
+ parameters: {
14
+ type: "object",
15
+ properties: {
16
+ url: { type: "string", description: "The URL to audit." },
17
+ viewport: {
18
+ type: "string",
19
+ enum: ["desktop", "mobile"],
20
+ description: "Viewport size. Default: 'desktop'.",
21
+ },
22
+ },
23
+ required: ["url"],
24
+ },
25
+ async execute(_toolCallId, params) {
26
+ const { url, viewport } = params;
27
+ const available = await isRendererAvailable();
28
+ if (!available) {
29
+ return errorResult("Playwright Chromium is not available. Install it with: npx playwright install chromium");
30
+ }
31
+ try {
32
+ const renderResult = await renderPage(url, config, {
33
+ viewport,
34
+ captureA11y: true,
35
+ captureConsole: false,
36
+ });
37
+ if (renderResult.error) {
38
+ return errorResult(`Render failed: ${renderResult.error}`);
39
+ }
40
+ const audit = auditAgentUx(renderResult);
41
+ const summary = `✓ Agent UX Audit: ${renderResult.finalUrl}\n` +
42
+ `Agent-UX Score: ${audit.agentUxScore}/100\n\n` +
43
+ `HTML Analysis:\n` +
44
+ ` Real <button>: ${audit.html.realButtons}\n` +
45
+ ` Real <a href>: ${audit.html.realAnchors}\n` +
46
+ ` <div onclick> widgets: ${audit.html.divOnclickWidgets} ${audit.html.divOnclickWidgets > 0 ? "⚠️" : "✓"}\n` +
47
+ ` Semantic landmarks: ${audit.html.semanticLandmarks}\n` +
48
+ ` Inputs missing labels: ${audit.html.inputsMissingLabel}/${audit.html.inputsTotal}\n\n` +
49
+ `Accessibility Tree:\n` +
50
+ ` Total nodes: ${audit.accessibilityTree.totalNodes}\n` +
51
+ ` Interactive nodes: ${audit.accessibilityTree.interactiveNodes}\n` +
52
+ ` Unnamed interactive: ${audit.accessibilityTree.unnamedInteractive} ${audit.accessibilityTree.unnamedInteractive > 0 ? "⚠️" : "✓"}\n` +
53
+ ` role="generic" ratio: ${(audit.accessibilityTree.roleGenericRatio * 100).toFixed(0)}%\n\n` +
54
+ `Opportunities:\n${audit.opportunities.map((o) => ` • ${o}`).join("\n") || " (none)"}`;
55
+ return textResult(summary, { audit });
56
+ }
57
+ catch (err) {
58
+ return errorResult(err instanceof Error ? err.message : String(err));
59
+ }
60
+ },
61
+ };
62
+ ext.registerTool(agentUxTool);
63
+ }
@@ -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,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"}