@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,381 @@
1
+ import { parseMarkdownScenarioFile, } from "./markdown-parser.js";
2
+ function sleep(ms) {
3
+ return new Promise((r) => setTimeout(r, ms));
4
+ }
5
+ /** Normalise an inline step into the ScenarioStep shape. */
6
+ function normalizeStep(raw, index) {
7
+ return {
8
+ action: (raw.action?.toLowerCase() ?? "note"),
9
+ raw: `Step ${index}: ${raw.action ?? "note"}${raw.target ? " " + raw.target : ""}`,
10
+ target: raw.target,
11
+ value: raw.value,
12
+ fields: raw.fields,
13
+ waitMs: raw.waitMs,
14
+ };
15
+ }
16
+ export function registerScenarioTools(ext, bs) {
17
+ const scenarioTool = {
18
+ name: "browser_run_scenario",
19
+ label: "Run Browser Scenario",
20
+ description: "Use this when the user asks to RUN, EXECUTE, PLAY, or WALK THROUGH a markdown test scenario in the browser. " +
21
+ "Executes the scenario steps in a real (visible) browser with optional video recording of the whole session. " +
22
+ "Accepts either inline `steps` or a `scenarioFile` path to a markdown scenario file. " +
23
+ "Do NOT write `.spec.ts` files or run `npx playwright test` for ad-hoc scenario execution or demo recordings — " +
24
+ "use this tool instead. Reserve `.spec.ts` / Playwright test authoring for when the user EXPLICITLY asks for " +
25
+ "durable e2e test code. Returns per-step pass/fail results and, if recording, the video path plus ffmpeg " +
26
+ "conversion commands.",
27
+ promptSnippet: "`browser_run_scenario { url, steps? | scenarioFile?, record?, filename?, stepDelayMs? }` — execute a markdown scenario in the browser with optional video recording (prefer this over writing .spec.ts for ad-hoc runs)",
28
+ parameters: {
29
+ type: "object",
30
+ properties: {
31
+ url: {
32
+ type: "string",
33
+ description: "The exact reachable URL to navigate to before running the scenario.",
34
+ },
35
+ steps: {
36
+ type: "array",
37
+ description: "Inline scenario steps. Each step: { action, target?, value?, fields?, waitMs? }. " +
38
+ "Actions: navigate, click, type, press, select, hover, wait, screenshot, evaluate, fill_form, note.",
39
+ items: {
40
+ type: "object",
41
+ properties: {
42
+ action: { type: "string", description: "One of: navigate, click, type, press, select, hover, wait, screenshot, evaluate, fill_form, note" },
43
+ target: { type: "string", description: "CSS selector, text, or URL depending on action" },
44
+ value: { type: "string", description: "Text to type, key to press, option to select, or wait duration" },
45
+ fields: {
46
+ type: "array",
47
+ description: "For fill_form: list of { name, value } pairs.",
48
+ items: {
49
+ type: "object",
50
+ properties: {
51
+ name: { type: "string" },
52
+ value: { type: "string" },
53
+ },
54
+ },
55
+ },
56
+ waitMs: { type: "number", description: "Per-step pause in ms after the action (overrides stepDelayMs)" },
57
+ },
58
+ required: ["action"],
59
+ },
60
+ },
61
+ scenarioFile: {
62
+ type: "string",
63
+ description: "Path to a markdown scenario file to parse and execute. Used as an alternative to inline `steps`. " +
64
+ "Numbered list items (1. **Action** ...) are converted to steps.",
65
+ },
66
+ record: {
67
+ type: "boolean",
68
+ description: "If true, record the whole session as a .webm video. Default: false.",
69
+ },
70
+ filename: {
71
+ type: "string",
72
+ description: "Base filename (without extension) for the recording, e.g. 'login-demo'. Ignored unless record=true.",
73
+ },
74
+ stepDelayMs: {
75
+ type: "number",
76
+ description: "Default pause (ms) after each step for readable video. Default: 800. Set 0 to disable.",
77
+ },
78
+ },
79
+ required: ["url"],
80
+ },
81
+ async execute(_toolCallId, params) {
82
+ const p = params;
83
+ if (!p.url) {
84
+ return {
85
+ content: [{ type: "text", text: "Error: url (string) is required." }],
86
+ details: { isError: true },
87
+ };
88
+ }
89
+ // Resolve steps.
90
+ let steps = [];
91
+ try {
92
+ if (p.scenarioFile) {
93
+ steps = await parseMarkdownScenarioFile(p.scenarioFile);
94
+ if (steps.length === 0) {
95
+ return {
96
+ content: [
97
+ {
98
+ type: "text",
99
+ text: `No numbered steps found in scenario file: ${p.scenarioFile}`,
100
+ },
101
+ ],
102
+ details: { isError: true },
103
+ };
104
+ }
105
+ }
106
+ else if (p.steps && p.steps.length > 0) {
107
+ steps = p.steps.map((s, i) => normalizeStep(s, i + 1));
108
+ }
109
+ else {
110
+ return {
111
+ content: [
112
+ {
113
+ type: "text",
114
+ text: "Error: provide either `steps` (inline) or `scenarioFile` (path to markdown).",
115
+ },
116
+ ],
117
+ details: { isError: true },
118
+ };
119
+ }
120
+ }
121
+ catch (err) {
122
+ const msg = err instanceof Error ? err.message : String(err);
123
+ return {
124
+ content: [{ type: "text", text: `Failed to load scenario: ${msg}` }],
125
+ details: { isError: true },
126
+ };
127
+ }
128
+ const record = p.record === true;
129
+ const stepDelay = typeof p.stepDelayMs === "number" ? p.stepDelayMs : 800;
130
+ const results = [];
131
+ // Optional recording start.
132
+ let videoPath;
133
+ let durationSeconds;
134
+ if (record) {
135
+ try {
136
+ const rec = await bs.startRecording({ filename: p.filename });
137
+ videoPath = rec.outputPath;
138
+ }
139
+ catch (err) {
140
+ const msg = err instanceof Error ? err.message : String(err);
141
+ return {
142
+ content: [{ type: "text", text: `Failed to start recording: ${msg}` }],
143
+ details: { isError: true },
144
+ };
145
+ }
146
+ }
147
+ // Navigate to the start URL.
148
+ try {
149
+ await bs.navigate(p.url);
150
+ await sleep(stepDelay);
151
+ }
152
+ catch (err) {
153
+ const msg = err instanceof Error ? err.message : String(err);
154
+ // If we can't even navigate, fail everything.
155
+ for (let i = 0; i < steps.length; i++) {
156
+ results.push({
157
+ step: i + 1,
158
+ action: steps[i].action,
159
+ status: "skipped",
160
+ detail: `Blocked: navigation failed (${msg})`,
161
+ durationMs: 0,
162
+ });
163
+ }
164
+ if (record) {
165
+ try {
166
+ const stop = await bs.stopRecording();
167
+ videoPath = stop.outputPath;
168
+ durationSeconds = stop.durationSeconds;
169
+ }
170
+ catch { /* ignore */ }
171
+ }
172
+ return finalize(results, videoPath, durationSeconds, false);
173
+ }
174
+ // Execute steps.
175
+ for (let i = 0; i < steps.length; i++) {
176
+ const step = steps[i];
177
+ const start = Date.now();
178
+ let status = "pass";
179
+ let detail = "";
180
+ try {
181
+ const page = bs.getActivePage();
182
+ switch (step.action) {
183
+ case "navigate": {
184
+ if (step.target) {
185
+ await bs.navigate(step.target);
186
+ detail = `Navigated to ${step.target}`;
187
+ }
188
+ else {
189
+ status = "skipped";
190
+ detail = "No target URL";
191
+ }
192
+ break;
193
+ }
194
+ case "click": {
195
+ if (step.target) {
196
+ await page.click(step.target, { timeout: bs.getConfig().actionTimeout || 5000 });
197
+ detail = `Clicked ${step.target}`;
198
+ }
199
+ else {
200
+ status = "skipped";
201
+ detail = "No target";
202
+ }
203
+ break;
204
+ }
205
+ case "hover": {
206
+ if (step.target) {
207
+ await page.hover(step.target, { timeout: bs.getConfig().actionTimeout || 5000 });
208
+ detail = `Hovered ${step.target}`;
209
+ }
210
+ else {
211
+ status = "skipped";
212
+ detail = "No target";
213
+ }
214
+ break;
215
+ }
216
+ case "type": {
217
+ if (step.target && step.value !== undefined) {
218
+ await page.fill(step.target, step.value, { timeout: bs.getConfig().actionTimeout || 5000 });
219
+ detail = `Typed into ${step.target}`;
220
+ }
221
+ else {
222
+ status = "skipped";
223
+ detail = "Missing target or value";
224
+ }
225
+ break;
226
+ }
227
+ case "press": {
228
+ const key = step.value ?? step.target;
229
+ if (key) {
230
+ await page.keyboard.press(key);
231
+ detail = `Pressed ${key}`;
232
+ }
233
+ else {
234
+ status = "skipped";
235
+ detail = "No key";
236
+ }
237
+ break;
238
+ }
239
+ case "select": {
240
+ if (step.target && step.value !== undefined) {
241
+ await page.selectOption(step.target, step.value);
242
+ detail = `Selected ${step.value} in ${step.target}`;
243
+ }
244
+ else {
245
+ status = "skipped";
246
+ detail = "Missing target or value";
247
+ }
248
+ break;
249
+ }
250
+ case "wait": {
251
+ const ms = step.waitMs ?? (step.value && /^\d+$/.test(step.value) ? Number(step.value) : null);
252
+ if (ms !== null) {
253
+ await page.waitForTimeout(ms);
254
+ detail = `Waited ${ms}ms`;
255
+ }
256
+ else if (step.value) {
257
+ await page.waitForSelector(`text=${step.value}`, { timeout: 30_000 });
258
+ detail = `Waited for text "${step.value}"`;
259
+ }
260
+ else {
261
+ await page.waitForTimeout(1000);
262
+ detail = "Waited 1000ms (default)";
263
+ }
264
+ break;
265
+ }
266
+ case "screenshot": {
267
+ const config = bs.getConfig();
268
+ const { mkdirSync } = await import("node:fs");
269
+ mkdirSync(config.outputDir, { recursive: true });
270
+ const ts = new Date().toISOString().replace(/[:.]/g, "-");
271
+ const filepath = `${config.outputDir}/scenario-step-${i + 1}-${ts}.png`;
272
+ await page.screenshot({ path: filepath });
273
+ detail = `Screenshot: ${filepath}`;
274
+ break;
275
+ }
276
+ case "evaluate": {
277
+ const expr = step.target ?? step.value;
278
+ if (expr) {
279
+ const res = await page.evaluate(expr);
280
+ detail = `Evaluated: ${expr}${res !== undefined ? ` → ${JSON.stringify(res).slice(0, 120)}` : ""}`;
281
+ }
282
+ else {
283
+ status = "skipped";
284
+ detail = "No expression";
285
+ }
286
+ break;
287
+ }
288
+ case "fill_form": {
289
+ if (step.fields && step.fields.length > 0) {
290
+ for (const f of step.fields) {
291
+ await page.fill(f.name, f.value, { timeout: bs.getConfig().actionTimeout || 5000 });
292
+ }
293
+ detail = `Filled ${step.fields.length} field(s)`;
294
+ }
295
+ else {
296
+ status = "skipped";
297
+ detail = "No fields";
298
+ }
299
+ break;
300
+ }
301
+ case "note":
302
+ default: {
303
+ detail = `Note: ${step.raw}`;
304
+ break;
305
+ }
306
+ }
307
+ }
308
+ catch (err) {
309
+ status = "fail";
310
+ detail = err instanceof Error ? err.message : String(err);
311
+ }
312
+ const durationMs = Date.now() - start;
313
+ results.push({ step: i + 1, action: step.action, status, detail, durationMs });
314
+ // Per-step pause for readable video (skip for wait actions already paused).
315
+ if (step.action !== "wait") {
316
+ await sleep(step.waitMs ?? stepDelay);
317
+ }
318
+ }
319
+ // Optional recording stop.
320
+ if (record) {
321
+ try {
322
+ const stop = await bs.stopRecording();
323
+ videoPath = stop.outputPath;
324
+ durationSeconds = stop.durationSeconds;
325
+ }
326
+ catch (err) {
327
+ const msg = err instanceof Error ? err.message : String(err);
328
+ return finalize(results, videoPath, durationSeconds, true, `Recording stop failed: ${msg}`);
329
+ }
330
+ }
331
+ return finalize(results, videoPath, durationSeconds, true);
332
+ },
333
+ };
334
+ try {
335
+ ext.registerTool(scenarioTool);
336
+ }
337
+ catch (err) {
338
+ const msg = err instanceof Error ? err.message : String(err);
339
+ process.stderr.write(`[browser-scenario] Failed to register browser_run_scenario: ${msg}\n`);
340
+ }
341
+ }
342
+ /** Build the final text response from step results. */
343
+ function finalize(results, videoPath, durationSeconds, ranToCompletion, warning) {
344
+ const passed = results.filter((r) => r.status === "pass").length;
345
+ const failed = results.filter((r) => r.status === "fail").length;
346
+ const skipped = results.filter((r) => r.status === "skipped").length;
347
+ const lines = [];
348
+ lines.push(ranToCompletion ? "✓ Scenario execution complete" : "✗ Scenario execution stopped early");
349
+ if (warning)
350
+ lines.push(`⚠ ${warning}`);
351
+ lines.push("");
352
+ lines.push("### Step Results");
353
+ for (const r of results) {
354
+ const icon = r.status === "pass" ? "✅" : r.status === "fail" ? "❌" : "⏭️";
355
+ lines.push(`${icon} Step ${r.step}: ${r.action} — ${r.detail} (${r.durationMs}ms)`);
356
+ }
357
+ lines.push("");
358
+ lines.push("### Summary");
359
+ lines.push(`- Steps total: ${results.length}`);
360
+ lines.push(`- Passed: ${passed}`);
361
+ lines.push(`- Failed: ${failed}`);
362
+ lines.push(`- Skipped: ${skipped}`);
363
+ if (videoPath) {
364
+ lines.push("");
365
+ lines.push("### Recording");
366
+ lines.push(`- File: ${videoPath}`);
367
+ if (durationSeconds !== undefined) {
368
+ lines.push(`- Duration: ${durationSeconds}s (${Math.floor(durationSeconds / 60)}m ${durationSeconds % 60}s)`);
369
+ }
370
+ const mp4 = videoPath.replace(/\.webm$/i, ".mp4");
371
+ const gif = videoPath.replace(/\.webm$/i, ".gif");
372
+ lines.push("- Convert to MP4:");
373
+ lines.push(` ffmpeg -i "${videoPath}" -c:v libx264 -crf 23 "${mp4}"`);
374
+ lines.push("- Convert to GIF:");
375
+ lines.push(` ffmpeg -i "${videoPath}" -vf "fps=15,scale=1280:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" "${gif}"`);
376
+ }
377
+ return {
378
+ content: [{ type: "text", text: lines.join("\n") }],
379
+ details: { results, videoPath, durationSeconds, passed, failed, skipped },
380
+ };
381
+ }
@@ -5,7 +5,7 @@ export function registerCoreTools(ext, bs) {
5
5
  const navigateTool = {
6
6
  name: "browser_navigate",
7
7
  label: "Browser Navigate",
8
- description: "Navigate to a URL in the browser. Auto-prepends https:// if no protocol is given.",
8
+ description: "Navigate to a URL in the browser. Auto-prepends https:// if no protocol is given. For multi-step scenario walkthroughs with optional recording, prefer browser_run_scenario.",
9
9
  promptSnippet: "`browser_navigate { url }` — navigate the browser to a URL",
10
10
  parameters: {
11
11
  type: "object",
@@ -1 +1 @@
1
- {"version":3,"file":"screenshot.d.ts","sourceRoot":"","sources":["../../../../src/extensions/browser/tools/screenshot.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AACvF,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,YAAY,EAAE,EAAE,EAAE,cAAc,GAAG,IAAI,CAgKnF"}
1
+ {"version":3,"file":"screenshot.d.ts","sourceRoot":"","sources":["../../../../src/extensions/browser/tools/screenshot.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAkB,MAAM,oCAAoC,CAAC;AACvF,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEvD,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,YAAY,EAAE,EAAE,EAAE,cAAc,GAAG,IAAI,CAiKnF"}
@@ -8,7 +8,8 @@ export function registerScreenshotTools(ext, bs) {
8
8
  name: "browser_take_screenshot",
9
9
  label: "Browser Take Screenshot",
10
10
  description: "Take a screenshot of the current page or viewport. " +
11
- "Saves to the output directory. Use browser_snapshot for text-based page analysis.",
11
+ "Saves to the output directory. Use browser_snapshot for text-based page analysis. " +
12
+ "For multi-step scenario walkthroughs with recording, prefer browser_run_scenario.",
12
13
  promptSnippet: "`browser_take_screenshot` — capture a screenshot of the page",
13
14
  parameters: {
14
15
  type: "object",
@@ -0,0 +1,5 @@
1
+ import type { AgentUxAuditResult } from "../types.js";
2
+ import type { RenderResult } from "../utils/renderer.js";
3
+ /** Audit agent UX using a rendered page result (with accessibility tree). */
4
+ export declare function auditAgentUx(renderResult: RenderResult): AgentUxAuditResult;
5
+ //# sourceMappingURL=agent-ux.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-ux.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/agent-ux.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAgEzD,6EAA6E;AAC7E,wBAAgB,YAAY,CAAC,YAAY,EAAE,YAAY,GAAG,kBAAkB,CAyD3E"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Agent UX analyzer — audits pages for AI-agent friendliness.
3
+ *
4
+ * Ported from the Python `agent_ux_check.py` in claude-seo. Uses Playwright's
5
+ * accessibility tree plus semantic HTML analysis to score how easily an AI
6
+ * agent (not just a summarizer) can navigate and interact with the page.
7
+ */
8
+ import * as cheerio from "cheerio";
9
+ const SEMANTIC_LANDMARKS = new Set([
10
+ "header", "nav", "main", "article", "section", "aside", "footer",
11
+ "form", "search", "figure", "figcaption",
12
+ ]);
13
+ function analyzeHtml(html) {
14
+ const $ = cheerio.load(html);
15
+ const realButtons = $("button").length;
16
+ const realAnchors = $("a[href]").length;
17
+ const divOnclick = $('div[onclick]').length;
18
+ const semanticLandmarks = $([...SEMANTIC_LANDMARKS].join(",")).length;
19
+ const inputsTotal = $("input, textarea, select").length;
20
+ let inputsMissingLabel = 0;
21
+ $("input, textarea, select").each((_, el) => {
22
+ const id = $(el).attr("id");
23
+ const ariaLabel = $(el).attr("aria-label");
24
+ const ariaLabelledby = $(el).attr("aria-labelledby");
25
+ const hasLabel = id ? $(`label[for="${id}"]`).length > 0 : false;
26
+ if (!hasLabel && !ariaLabel && !ariaLabelledby) {
27
+ inputsMissingLabel++;
28
+ }
29
+ });
30
+ return {
31
+ realButtons,
32
+ realAnchors,
33
+ divOnclickWidgets: divOnclick,
34
+ semanticLandmarks,
35
+ inputsTotal,
36
+ inputsMissingLabel,
37
+ };
38
+ }
39
+ function analyzeAccessibilityTree(tree) {
40
+ if (!tree || typeof tree !== "object") {
41
+ return { totalNodes: 0, interactiveNodes: 0, unnamedInteractive: 0, roleGenericRatio: 0 };
42
+ }
43
+ const n = tree;
44
+ const totalNodes = n.totalNodes ?? 0;
45
+ const interactiveNodes = n.interactiveNodes ?? 0;
46
+ const unnamedInteractive = n.unnamedInteractive ?? 0;
47
+ const genericNodes = n.genericNodes ?? 0;
48
+ return {
49
+ totalNodes,
50
+ interactiveNodes,
51
+ unnamedInteractive,
52
+ roleGenericRatio: totalNodes > 0 ? Math.round((genericNodes / totalNodes) * 100) / 100 : 0,
53
+ };
54
+ }
55
+ /** Audit agent UX using a rendered page result (with accessibility tree). */
56
+ export function auditAgentUx(renderResult) {
57
+ const htmlAnalysis = analyzeHtml(renderResult.content);
58
+ const a11yAnalysis = analyzeAccessibilityTree(renderResult.accessibilityTree);
59
+ // Score: start at 100, deduct for issues.
60
+ let score = 100;
61
+ const opportunities = [];
62
+ // Penalize div onclick widgets.
63
+ if (htmlAnalysis.divOnclickWidgets > 0) {
64
+ score -= Math.min(30, htmlAnalysis.divOnclickWidgets * 5);
65
+ opportunities.push(`Replace ${htmlAnalysis.divOnclickWidgets} <div onclick> widgets with semantic <button> or <a> elements.`);
66
+ }
67
+ // Penalize missing labels.
68
+ if (htmlAnalysis.inputsMissingLabel > 0) {
69
+ score -= Math.min(20, htmlAnalysis.inputsMissingLabel * 5);
70
+ opportunities.push(`${htmlAnalysis.inputsMissingLabel} of ${htmlAnalysis.inputsTotal} inputs are missing labels. Add <label for> or aria-label.`);
71
+ }
72
+ // Penalize unnamed interactive elements.
73
+ if (a11yAnalysis.unnamedInteractive > 0) {
74
+ score -= Math.min(20, a11yAnalysis.unnamedInteractive * 3);
75
+ opportunities.push(`${a11yAnalysis.unnamedInteractive} interactive elements in the accessibility tree have no accessible name.`);
76
+ }
77
+ // Penalize low semantic landmark count.
78
+ if (htmlAnalysis.semanticLandmarks < 3) {
79
+ score -= 10;
80
+ opportunities.push(`Only ${htmlAnalysis.semanticLandmarks} semantic landmarks found. Use <header>, <nav>, <main>, <footer> for structure.`);
81
+ }
82
+ // Penalize high generic role ratio.
83
+ if (a11yAnalysis.roleGenericRatio > 0.5) {
84
+ score -= 10;
85
+ opportunities.push(`${Math.round(a11yAnalysis.roleGenericRatio * 100)}% of accessibility tree nodes have role="generic". Use semantic HTML to improve structure.`);
86
+ }
87
+ score = Math.max(0, Math.min(100, score));
88
+ return {
89
+ url: renderResult.finalUrl,
90
+ agentUxScore: score,
91
+ html: htmlAnalysis,
92
+ accessibilityTree: a11yAnalysis,
93
+ opportunities,
94
+ };
95
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Content quality analyzer.
3
+ *
4
+ * Ported from the Python `content_quality.py` in claude-seo. Pure heuristic
5
+ * analysis: filler phrase detection, AI-pattern detection, information density,
6
+ * and repetition scoring. No external API calls.
7
+ */
8
+ import type { ContentQualityResult } from "../types.js";
9
+ /**
10
+ * Analyze content quality of a text string.
11
+ *
12
+ * All scores are 0-100 where higher is better (except filler and AI pattern
13
+ * scores, where higher means MORE problematic content).
14
+ */
15
+ export declare function analyzeContentQuality(text: string, url?: string): ContentQualityResult;
16
+ //# sourceMappingURL=content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/content.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AA8HxD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,oBAAoB,CAgDtF"}