@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,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"}
@@ -0,0 +1,123 @@
1
+ import { fetchPage } from "../utils/fetcher.js";
2
+ import { renderPage, isRendererAvailable } from "../utils/renderer.js";
3
+ import { errorResult, textResult, truncate } from "./helpers.js";
4
+ export function registerFetchTools(ext, config) {
5
+ const fetchTool = {
6
+ name: "seo_fetch",
7
+ label: "SEO Fetch Page",
8
+ description: "Fetch a web page's HTML content with full redirect-chain tracking, status code, and response headers. " +
9
+ "Uses SSRF protection to block private/internal IPs. Does NOT execute JavaScript — use seo_render for SPA pages.",
10
+ promptSnippet: "`seo_fetch { url, userAgent?, timeout? }` — fetch a page's HTML with redirect tracking",
11
+ promptGuidelines: [
12
+ "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.",
13
+ ],
14
+ parameters: {
15
+ type: "object",
16
+ properties: {
17
+ url: { type: "string", description: "The URL to fetch. https:// is prepended if missing." },
18
+ userAgent: {
19
+ type: "string",
20
+ description: "User agent: 'default', 'googlebot', or a custom UA string. Default: 'default'.",
21
+ },
22
+ timeout: { type: "number", description: "Request timeout in milliseconds. Default: 30000." },
23
+ followRedirects: { type: "boolean", description: "Whether to follow HTTP redirects. Default: true." },
24
+ },
25
+ required: ["url"],
26
+ },
27
+ async execute(_toolCallId, params) {
28
+ const { url, userAgent, timeout, followRedirects } = params;
29
+ try {
30
+ const result = await fetchPage(url, config, {
31
+ userAgent: userAgent ?? "default",
32
+ timeout,
33
+ followRedirects,
34
+ });
35
+ if (result.error) {
36
+ return errorResult(`Fetch failed: ${result.error}`, { result });
37
+ }
38
+ const summary = `✓ Fetched: ${result.finalUrl}\n` +
39
+ `Status: ${result.statusCode} ${result.statusText}\n` +
40
+ `Redirects: ${result.redirectChain.length}\n` +
41
+ `Content-Length: ${result.contentLengthBytes} bytes\n` +
42
+ `Headers: ${Object.keys(result.headers).join(", ")}`;
43
+ return textResult(summary, {
44
+ url: result.url,
45
+ finalUrl: result.finalUrl,
46
+ statusCode: result.statusCode,
47
+ content: truncate(result.content, 50_000),
48
+ headers: result.headers,
49
+ redirectChain: result.redirectChain,
50
+ });
51
+ }
52
+ catch (err) {
53
+ return errorResult(err instanceof Error ? err.message : String(err));
54
+ }
55
+ },
56
+ };
57
+ const renderTool = {
58
+ name: "seo_render",
59
+ label: "SEO Render Page",
60
+ description: "Render a web page with Playwright (Chromium) to get the full DOM after JavaScript execution. " +
61
+ "Use this for SPA pages where critical content is injected via JS. " +
62
+ "Slower than seo_fetch but necessary for JavaScript-heavy sites. " +
63
+ "Optionally captures the accessibility tree (for agent UX audits) and console errors.",
64
+ promptSnippet: "`seo_render { url, viewport?, captureA11y? }` — render a page with Playwright (SPA-aware)",
65
+ parameters: {
66
+ type: "object",
67
+ properties: {
68
+ url: { type: "string", description: "The URL to render." },
69
+ viewport: {
70
+ type: "string",
71
+ enum: ["desktop", "mobile"],
72
+ description: "Viewport size. Default: 'desktop'.",
73
+ },
74
+ captureA11y: {
75
+ type: "boolean",
76
+ description: "Whether to capture the accessibility tree. Default: false.",
77
+ },
78
+ captureConsole: {
79
+ type: "boolean",
80
+ description: "Whether to capture console errors. Default: true.",
81
+ },
82
+ timeout: { type: "number", description: "Render timeout in milliseconds. Default: 30000." },
83
+ },
84
+ required: ["url"],
85
+ },
86
+ async execute(_toolCallId, params) {
87
+ const { url, viewport, captureA11y, captureConsole, timeout } = params;
88
+ const available = await isRendererAvailable();
89
+ if (!available) {
90
+ return errorResult("Playwright Chromium is not available. Install it with: npx playwright install chromium");
91
+ }
92
+ try {
93
+ const result = await renderPage(url, config, {
94
+ viewport,
95
+ captureA11y,
96
+ captureConsole,
97
+ timeout,
98
+ });
99
+ const summary = `✓ Rendered: ${result.finalUrl}\n` +
100
+ `Status: ${result.statusCode}\n` +
101
+ `SPA: ${result.isSpa}\n` +
102
+ `Render time: ${result.renderMs}ms\n` +
103
+ `Console errors: ${result.consoleErrors.length}`;
104
+ return textResult(summary, {
105
+ url: result.url,
106
+ finalUrl: result.finalUrl,
107
+ statusCode: result.statusCode,
108
+ content: truncate(result.content, 50_000),
109
+ rawContent: truncate(result.rawContent, 20_000),
110
+ isSpa: result.isSpa,
111
+ consoleErrors: result.consoleErrors,
112
+ renderMs: result.renderMs,
113
+ accessibilityTree: result.accessibilityTree,
114
+ });
115
+ }
116
+ catch (err) {
117
+ return errorResult(err instanceof Error ? err.message : String(err));
118
+ }
119
+ },
120
+ };
121
+ ext.registerTool(fetchTool);
122
+ ext.registerTool(renderTool);
123
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * seo_audit tool — full audit orchestrator.
3
+ *
4
+ * Runs multiple sub-audits in a single call and aggregates results into a
5
+ * combined report with an overall score and prioritized issue list.
6
+ */
7
+ import type { ExtensionAPI } from "../../../sdk/coding-agent/index.js";
8
+ import type { SeoConfig } from "../config.js";
9
+ export declare function registerFullAuditTools(ext: ExtensionAPI, config: SeoConfig): void;
10
+ //# sourceMappingURL=full-audit.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"full-audit.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/tools/full-audit.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AACvF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAiB9C,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,GAAG,IAAI,CA+DjF"}
@@ -0,0 +1,191 @@
1
+ import { auditTechnical } from "../analyzers/technical.js";
2
+ import { analyzeContentQuality } from "../analyzers/content.js";
3
+ import { auditSchema } from "../analyzers/schema.js";
4
+ import { auditImages } from "../analyzers/images.js";
5
+ import { auditPreload } from "../analyzers/preload.js";
6
+ import { auditAgentUx } from "../analyzers/agent-ux.js";
7
+ import { fetchPage } from "../utils/fetcher.js";
8
+ import { renderPage, isRendererAvailable } from "../utils/renderer.js";
9
+ import { parsePage, extractText } from "../utils/parser.js";
10
+ import { errorResult, textResult } from "./helpers.js";
11
+ const ALL_CATEGORIES = ["technical", "content", "schema", "images", "preload", "agentUx"];
12
+ export function registerFullAuditTools(ext, config) {
13
+ const fullAuditTool = {
14
+ name: "seo_audit",
15
+ label: "SEO Full Audit",
16
+ description: "Run a comprehensive SEO audit in a single call. Combines: technical audit (crawlability, security, mobile, AI crawlers), " +
17
+ "content quality (filler, AI patterns, density), schema audit (detection, validation), image audit (alt, dimensions, formats), " +
18
+ "preload audit (speculation rules, bfcache), and agent UX audit (accessibility, semantic HTML). " +
19
+ "Returns an overall score and all issues prioritized by severity. " +
20
+ "Use 'include' to run only specific sub-audits.",
21
+ promptSnippet: "`seo_audit { url, include? }` — full SEO audit (technical + content + schema + images + preload + agentUx)",
22
+ promptGuidelines: [
23
+ "When the user asks about SEO, search optimization, page audits, schema markup, sitemaps, Core Web Vitals, or technical SEO for any website or URL, use the seo_* tools. Start with seo_audit for comprehensive analysis — it runs all sub-audits in one call. For specific concerns use the targeted tools (seo_technical_audit, seo_content_quality, seo_schema_audit, seo_image_audit, seo_preload_audit, seo_agent_ux_audit). These tools are offline-first and require no API keys.",
24
+ ],
25
+ parameters: {
26
+ type: "object",
27
+ properties: {
28
+ url: { type: "string", description: "The URL to audit." },
29
+ include: {
30
+ type: "array",
31
+ items: {
32
+ type: "string",
33
+ enum: ["technical", "content", "schema", "images", "preload", "agentUx"],
34
+ },
35
+ description: "Sub-audits to include. If omitted, runs all sub-audits.",
36
+ },
37
+ },
38
+ required: ["url"],
39
+ },
40
+ async execute(_toolCallId, params) {
41
+ const { url, include } = params;
42
+ try {
43
+ const categories = include ?? ALL_CATEGORIES;
44
+ const result = await runFullAudit(url, config, categories);
45
+ const issueLines = result.allIssues
46
+ .sort((a, b) => severityRank(a.severity) - severityRank(b.severity))
47
+ .slice(0, 30)
48
+ .map((i) => ` [${i.severity.toUpperCase()}] ${i.category}: ${i.title}`)
49
+ .join("\n");
50
+ const summary = `✓ Full SEO Audit: ${result.url}\n` +
51
+ `Audited At: ${result.auditedAt}\n` +
52
+ `Overall Score: ${result.overallScore}/100\n\n` +
53
+ `Sub-audits run:\n` +
54
+ ` Technical: ${result.technical ? `${result.technical.score}/100` : "skipped"}\n` +
55
+ ` Content: ${result.content ? `${result.content.overallQuality}/100` : "skipped"}\n` +
56
+ ` Schema: ${result.schema ? `${result.schema.detectedSchemas.length} blocks` : "skipped"}\n` +
57
+ ` Images: ${result.images ? `${result.images.missingAlt} missing alt` : "skipped"}\n` +
58
+ ` Preload: ${result.preload ? `${result.preload.score}/100` : "skipped"}\n` +
59
+ ` Agent UX: ${result.agentUx ? `${result.agentUx.agentUxScore}/100` : "skipped"}\n\n` +
60
+ `Top Issues (${result.allIssues.length} total):\n${issueLines || " (none)"}`;
61
+ return textResult(summary, { result });
62
+ }
63
+ catch (err) {
64
+ return errorResult(err instanceof Error ? err.message : String(err));
65
+ }
66
+ },
67
+ };
68
+ ext.registerTool(fullAuditTool);
69
+ }
70
+ async function runFullAudit(url, config, categories) {
71
+ // Fetch the page once and reuse for all sub-audits.
72
+ const fetchResult = await fetchPage(url, config);
73
+ const pageData = parsePage(fetchResult.content, fetchResult.finalUrl);
74
+ const allIssues = [];
75
+ let scores = [];
76
+ // Technical audit.
77
+ let technical;
78
+ if (categories.includes("technical")) {
79
+ technical = await auditTechnical(url, config, pageData, fetchResult);
80
+ allIssues.push(...technical.issues);
81
+ scores.push(technical.score);
82
+ }
83
+ // Content quality.
84
+ let content;
85
+ if (categories.includes("content")) {
86
+ const text = extractText(fetchResult.content);
87
+ content = analyzeContentQuality(text, fetchResult.finalUrl);
88
+ scores.push(content.overallQuality);
89
+ }
90
+ // Schema audit.
91
+ let schema;
92
+ if (categories.includes("schema")) {
93
+ schema = auditSchema(fetchResult.content, fetchResult.finalUrl);
94
+ // Convert schema recommendations to issues.
95
+ for (const rec of schema.recommendations) {
96
+ allIssues.push({
97
+ category: "Schema",
98
+ severity: "medium",
99
+ title: rec,
100
+ description: rec,
101
+ recommendation: rec,
102
+ });
103
+ }
104
+ }
105
+ // Image audit.
106
+ let images;
107
+ if (categories.includes("images")) {
108
+ images = auditImages(fetchResult.content, fetchResult.finalUrl);
109
+ if (images.missingAlt > 0) {
110
+ allIssues.push({
111
+ category: "Images",
112
+ severity: "medium",
113
+ title: `${images.missingAlt} images missing alt text`,
114
+ description: `${images.missingAlt} of ${images.totalImages} images have no alt attribute.`,
115
+ recommendation: "Add descriptive alt text to all informative images.",
116
+ });
117
+ }
118
+ if (images.missingDimensions > 0) {
119
+ allIssues.push({
120
+ category: "Images",
121
+ severity: "low",
122
+ title: `${images.missingDimensions} images missing dimensions`,
123
+ description: `${images.missingDimensions} images lack width/height attributes, causing layout shift.`,
124
+ recommendation: "Add width and height attributes to prevent CLS.",
125
+ });
126
+ }
127
+ }
128
+ // Preload audit.
129
+ let preload;
130
+ if (categories.includes("preload")) {
131
+ preload = auditPreload(fetchResult);
132
+ scores.push(preload.score);
133
+ for (const rec of preload.recommendations) {
134
+ allIssues.push({
135
+ category: "Preload",
136
+ severity: "medium",
137
+ title: rec,
138
+ description: rec,
139
+ recommendation: rec,
140
+ });
141
+ }
142
+ }
143
+ // Agent UX audit (requires rendering).
144
+ let agentUx;
145
+ if (categories.includes("agentUx")) {
146
+ const available = await isRendererAvailable();
147
+ if (available) {
148
+ const renderResult = await renderPage(url, config, {
149
+ captureA11y: true,
150
+ captureConsole: false,
151
+ });
152
+ agentUx = auditAgentUx(renderResult);
153
+ scores.push(agentUx.agentUxScore);
154
+ for (const opp of agentUx.opportunities) {
155
+ allIssues.push({
156
+ category: "Agent UX",
157
+ severity: "medium",
158
+ title: opp,
159
+ description: opp,
160
+ recommendation: opp,
161
+ });
162
+ }
163
+ }
164
+ }
165
+ const overallScore = scores.length > 0
166
+ ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length)
167
+ : 0;
168
+ const summary = `SEO audit for ${fetchResult.finalUrl} — Overall score: ${overallScore}/100, ${allIssues.length} issues found.`;
169
+ return {
170
+ url: fetchResult.finalUrl,
171
+ auditedAt: new Date().toISOString(),
172
+ overallScore,
173
+ technical,
174
+ content,
175
+ schema,
176
+ images,
177
+ preload,
178
+ agentUx,
179
+ allIssues,
180
+ summary,
181
+ };
182
+ }
183
+ function severityRank(s) {
184
+ switch (s) {
185
+ case "critical": return 0;
186
+ case "high": return 1;
187
+ case "medium": return 2;
188
+ case "low": return 3;
189
+ default: return 4;
190
+ }
191
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Helper utilities for SEO tool implementations.
3
+ */
4
+ import type { AgentToolResult } from "../../../sdk/coding-agent/index.js";
5
+ /** Create a text-only tool result. */
6
+ export declare function textResult(text: string, details?: Record<string, unknown>): AgentToolResult<Record<string, unknown>>;
7
+ /** Create an error tool result. */
8
+ export declare function errorResult(message: string, details?: Record<string, unknown>): AgentToolResult<Record<string, unknown>>;
9
+ /** Format a JSON object as pretty-printed text for tool output. */
10
+ export declare function jsonText(obj: unknown): string;
11
+ /** Truncate text to a maximum length. */
12
+ export declare function truncate(text: string, maxLen: number): string;
13
+ //# sourceMappingURL=helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/tools/helpers.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAE1E,sCAAsC;AACtC,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,eAAe,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAKxH;AAED,mCAAmC;AACnC,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GAAG,eAAe,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAK5H;AAED,mEAAmE;AACnE,wBAAgB,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAE7C;AAED,yCAAyC;AACzC,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAG7D"}
@@ -0,0 +1,24 @@
1
+ /** Create a text-only tool result. */
2
+ export function textResult(text, details = {}) {
3
+ return {
4
+ content: [{ type: "text", text }],
5
+ details,
6
+ };
7
+ }
8
+ /** Create an error tool result. */
9
+ export function errorResult(message, details = {}) {
10
+ return {
11
+ content: [{ type: "text", text: `❌ ${message}` }],
12
+ details: { isError: true, ...details },
13
+ };
14
+ }
15
+ /** Format a JSON object as pretty-printed text for tool output. */
16
+ export function jsonText(obj) {
17
+ return JSON.stringify(obj, null, 2);
18
+ }
19
+ /** Truncate text to a maximum length. */
20
+ export function truncate(text, maxLen) {
21
+ if (text.length <= maxLen)
22
+ return text;
23
+ return text.slice(0, maxLen) + "... (truncated)";
24
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * seo_image_audit tool.
3
+ */
4
+ import type { ExtensionAPI } from "../../../sdk/coding-agent/index.js";
5
+ import type { SeoConfig } from "../config.js";
6
+ export declare function registerImageTools(ext: ExtensionAPI, config: SeoConfig): void;
7
+ //# sourceMappingURL=images.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"images.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/tools/images.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,CA8C7E"}
@@ -0,0 +1,46 @@
1
+ import { auditImages } from "../analyzers/images.js";
2
+ import { fetchPage } from "../utils/fetcher.js";
3
+ import { errorResult, textResult } from "./helpers.js";
4
+ export function registerImageTools(ext, config) {
5
+ const imageTool = {
6
+ name: "seo_image_audit",
7
+ label: "SEO Image Audit",
8
+ description: "Audit all images on a page for SEO issues: missing alt text, missing width/height dimensions, " +
9
+ "lazy-loading method (native, data-src, data-lazy, none), modern format usage (WebP, AVIF, JXL), " +
10
+ "and oversized images (>2000px). Returns per-image details and aggregate counts.",
11
+ promptSnippet: "`seo_image_audit { url }` — audit images for alt, dimensions, lazy loading, modern formats",
12
+ parameters: {
13
+ type: "object",
14
+ properties: {
15
+ url: { type: "string", description: "The URL to audit for image SEO." },
16
+ },
17
+ required: ["url"],
18
+ },
19
+ async execute(_toolCallId, params) {
20
+ const { url } = params;
21
+ try {
22
+ const result = await fetchPage(url, config);
23
+ if (result.error) {
24
+ return errorResult(`Fetch failed: ${result.error}`);
25
+ }
26
+ const audit = auditImages(result.content, result.finalUrl);
27
+ const summary = `✓ Image Audit: ${result.finalUrl}\n` +
28
+ `Total Images: ${audit.totalImages}\n` +
29
+ `Missing Alt: ${audit.missingAlt} ${audit.missingAlt > 0 ? "⚠️" : "✓"}\n` +
30
+ `Missing Dimensions: ${audit.missingDimensions} ${audit.missingDimensions > 0 ? "⚠️" : "✓"}\n\n` +
31
+ `Lazy Loading:\n` +
32
+ ` Native (loading="lazy"): ${audit.lazyLoading.native}\n` +
33
+ ` data-src: ${audit.lazyLoading.dataSrc}\n` +
34
+ ` data-lazy: ${audit.lazyLoading.dataLazy}\n` +
35
+ ` None: ${audit.lazyLoading.none}\n\n` +
36
+ `Modern Formats: ${audit.modernFormats} (WebP/AVIF/JXL)\n` +
37
+ `Oversized (>2000px): ${audit.oversized.length}`;
38
+ return textResult(summary, { audit });
39
+ }
40
+ catch (err) {
41
+ return errorResult(err instanceof Error ? err.message : String(err));
42
+ }
43
+ },
44
+ };
45
+ ext.registerTool(imageTool);
46
+ }
@@ -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"}