@wordbricks/playwright-mcp 0.1.20 → 0.1.22

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 (87) hide show
  1. package/cli-wrapper.js +15 -14
  2. package/cli.js +1 -1
  3. package/config.d.ts +11 -6
  4. package/index.d.ts +7 -5
  5. package/index.js +1 -1
  6. package/lib/browserContextFactory.js +131 -58
  7. package/lib/browserServerBackend.js +14 -12
  8. package/lib/config.js +60 -46
  9. package/lib/context.js +41 -39
  10. package/lib/extension/cdpRelay.js +67 -61
  11. package/lib/extension/extensionContextFactory.js +10 -10
  12. package/lib/frameworkPatterns.js +21 -21
  13. package/lib/hooks/antiBotDetectionHook.js +59 -52
  14. package/lib/hooks/core.js +11 -10
  15. package/lib/hooks/eventConsumer.js +21 -21
  16. package/lib/hooks/events.js +3 -3
  17. package/lib/hooks/formatToolCallEvent.js +3 -7
  18. package/lib/hooks/frameworkStateHook.js +40 -40
  19. package/lib/hooks/grouping.js +3 -3
  20. package/lib/hooks/jsonLdDetectionHook.js +44 -37
  21. package/lib/hooks/networkFilters.js +17 -17
  22. package/lib/hooks/networkSetup.js +9 -7
  23. package/lib/hooks/networkTrackingHook.js +21 -21
  24. package/lib/hooks/pageHeightHook.js +9 -9
  25. package/lib/hooks/registry.js +15 -16
  26. package/lib/hooks/requireTabHook.js +3 -3
  27. package/lib/hooks/schema.js +38 -38
  28. package/lib/hooks/waitHook.js +7 -7
  29. package/lib/index.js +12 -10
  30. package/lib/mcp/inProcessTransport.js +3 -4
  31. package/lib/mcp/proxyBackend.js +43 -28
  32. package/lib/mcp/server.js +24 -19
  33. package/lib/mcp/tool.js +14 -8
  34. package/lib/mcp/transport.js +60 -53
  35. package/lib/playwrightTransformer.js +129 -106
  36. package/lib/program.js +54 -52
  37. package/lib/response.js +36 -30
  38. package/lib/sessionLog.js +19 -17
  39. package/lib/tab.js +41 -39
  40. package/lib/tools/common.js +19 -19
  41. package/lib/tools/console.js +11 -11
  42. package/lib/tools/dialogs.js +18 -15
  43. package/lib/tools/evaluate.js +26 -17
  44. package/lib/tools/extractFrameworkState.js +48 -37
  45. package/lib/tools/files.js +17 -14
  46. package/lib/tools/form.js +32 -23
  47. package/lib/tools/getSnapshot.js +14 -15
  48. package/lib/tools/getVisibleHtml.js +33 -17
  49. package/lib/tools/install.js +20 -20
  50. package/lib/tools/keyboard.js +29 -24
  51. package/lib/tools/mouse.js +29 -31
  52. package/lib/tools/navigate.js +19 -23
  53. package/lib/tools/network.js +12 -14
  54. package/lib/tools/networkDetail.js +58 -49
  55. package/lib/tools/networkSearch/bodySearch.js +46 -32
  56. package/lib/tools/networkSearch/grouping.js +15 -6
  57. package/lib/tools/networkSearch/helpers.js +4 -4
  58. package/lib/tools/networkSearch/searchHtml.js +25 -16
  59. package/lib/tools/networkSearch/urlSearch.js +56 -14
  60. package/lib/tools/networkSearch.js +46 -36
  61. package/lib/tools/pdf.js +13 -12
  62. package/lib/tools/repl.js +66 -54
  63. package/lib/tools/screenshot.js +57 -33
  64. package/lib/tools/scroll.js +29 -24
  65. package/lib/tools/snapshot.js +66 -49
  66. package/lib/tools/tabs.js +22 -19
  67. package/lib/tools/tool.js +5 -3
  68. package/lib/tools/utils.js +17 -13
  69. package/lib/tools/wait.js +24 -19
  70. package/lib/tools.js +21 -20
  71. package/lib/utils/adBlockFilter.js +29 -26
  72. package/lib/utils/codegen.js +20 -16
  73. package/lib/utils/extensionPath.js +4 -4
  74. package/lib/utils/fileUtils.js +17 -13
  75. package/lib/utils/graphql.js +69 -58
  76. package/lib/utils/guid.js +3 -3
  77. package/lib/utils/httpServer.js +9 -9
  78. package/lib/utils/log.js +3 -3
  79. package/lib/utils/manualPromise.js +7 -7
  80. package/lib/utils/networkFormat.js +7 -5
  81. package/lib/utils/package.js +4 -4
  82. package/lib/utils/sanitizeHtml.js +66 -34
  83. package/lib/utils/truncate.js +25 -25
  84. package/lib/utils/withTimeout.js +1 -1
  85. package/package.json +34 -57
  86. package/src/index.ts +27 -17
  87. package/LICENSE +0 -202
package/lib/tools/pdf.js CHANGED
@@ -13,20 +13,23 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import { z } from 'zod';
17
- import { defineTabTool } from './tool.js';
18
- import * as javascript from '../utils/codegen.js';
16
+ import { z } from "zod";
17
+ import * as javascript from "../utils/codegen.js";
18
+ import { defineTabTool } from "./tool.js";
19
19
  const pdfSchema = z.object({
20
- filename: z.string().optional().describe('File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified.'),
20
+ filename: z
21
+ .string()
22
+ .optional()
23
+ .describe("File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified."),
21
24
  });
22
25
  const pdf = defineTabTool({
23
- capability: 'pdf',
26
+ capability: "pdf",
24
27
  schema: {
25
- name: 'browser_pdf_save',
26
- title: 'Save as PDF',
27
- description: 'Save page as PDF',
28
+ name: "browser_pdf_save",
29
+ title: "Save as PDF",
30
+ description: "Save page as PDF",
28
31
  inputSchema: pdfSchema,
29
- type: 'readOnly',
32
+ type: "readOnly",
30
33
  },
31
34
  handle: async (tab, params, response) => {
32
35
  const fileName = await tab.context.outputFile(params.filename ?? `page-${new Date().toISOString()}.pdf`);
@@ -35,6 +38,4 @@ const pdf = defineTabTool({
35
38
  await tab.page.pdf({ path: fileName });
36
39
  },
37
40
  });
38
- export default [
39
- pdf,
40
- ];
41
+ export default [pdf];
package/lib/tools/repl.js CHANGED
@@ -1,18 +1,18 @@
1
- import { readFileSync } from 'fs';
2
- import { join } from 'path';
3
- import { z } from 'zod';
4
- import debug from 'debug';
5
- import ms from 'ms';
6
- import { defineTabTool } from './tool.js';
7
- import * as javascript from '../utils/codegen.js';
8
- import { truncate } from '../utils/truncate.js';
9
- import { transformScript } from '../playwrightTransformer.js';
10
- const EVALUATE_TIMEOUT_MS = ms('30s');
11
- const debugLog = debug('pw:mcp:repl');
1
+ import debug from "debug";
2
+ import { readFileSync } from "fs";
3
+ import ms from "ms";
4
+ import { join } from "path";
5
+ import { z } from "zod";
6
+ import { transformScript } from "../playwrightTransformer.js";
7
+ import * as javascript from "../utils/codegen.js";
8
+ import { truncate } from "../utils/truncate.js";
9
+ import { defineTabTool } from "./tool.js";
10
+ const EVALUATE_TIMEOUT_MS = ms("30s");
11
+ const debugLog = debug("pw:mcp:repl");
12
12
  const replSchema = z.object({
13
13
  script: z
14
14
  .string()
15
- .describe('JavaScript code to execute in the browser console.'),
15
+ .describe("JavaScript code to execute in the browser console."),
16
16
  });
17
17
  const contextStates = new WeakMap();
18
18
  function getPageState(page) {
@@ -49,17 +49,17 @@ async function setupConsoleCapture(page) {
49
49
  window.__consoleOutputs = [];
50
50
  // Override console methods to capture output
51
51
  const captureConsole = (method) => {
52
- const handler = function (...args) {
52
+ const handler = (...args) => {
53
53
  // Call original method
54
54
  window.__originalConsole?.[method](...args);
55
55
  // Capture output
56
56
  const output = args
57
- .map(arg => {
57
+ .map((arg) => {
58
58
  if (arg === undefined)
59
- return 'undefined';
59
+ return "undefined";
60
60
  if (arg === null)
61
- return 'null';
62
- if (typeof arg === 'object') {
61
+ return "null";
62
+ if (typeof arg === "object") {
63
63
  try {
64
64
  return JSON.stringify(arg);
65
65
  }
@@ -69,26 +69,30 @@ async function setupConsoleCapture(page) {
69
69
  }
70
70
  return String(arg);
71
71
  })
72
- .join(' ');
72
+ .join(" ");
73
73
  window.__consoleOutputs?.push(output);
74
74
  };
75
- Object.defineProperty(console, method, { value: handler, configurable: true, writable: true });
75
+ Object.defineProperty(console, method, {
76
+ value: handler,
77
+ configurable: true,
78
+ writable: true,
79
+ });
76
80
  };
77
- captureConsole('log');
78
- captureConsole('error');
79
- captureConsole('warn');
80
- captureConsole('info');
81
+ captureConsole("log");
82
+ captureConsole("error");
83
+ captureConsole("warn");
84
+ captureConsole("info");
81
85
  });
82
86
  }
83
87
  async function injectLibraries(cdpSession) {
84
88
  try {
85
- const nodeModulesPath = join(process.cwd(), 'node_modules');
89
+ const nodeModulesPath = join(process.cwd(), "node_modules");
86
90
  // Inject utility libraries into the page context
87
- const json5Code = readFileSync(join(nodeModulesPath, 'json5', 'dist', 'index.min.js'), 'utf8');
88
- const jsonpathCode = readFileSync(join(nodeModulesPath, 'jsonpath-plus', 'dist', 'index-browser-umd.min.cjs'), 'utf8');
89
- const lodashCode = readFileSync(join(nodeModulesPath, 'lodash', 'lodash.min.js'), 'utf8');
91
+ const json5Code = readFileSync(join(nodeModulesPath, "json5", "dist", "index.min.js"), "utf8");
92
+ const jsonpathCode = readFileSync(join(nodeModulesPath, "jsonpath-plus", "dist", "index-browser-umd.min.cjs"), "utf8");
93
+ const lodashCode = readFileSync(join(nodeModulesPath, "lodash", "lodash.min.js"), "utf8");
90
94
  // Inject libraries using CDP
91
- await cdpSession.send('Runtime.evaluate', {
95
+ await cdpSession.send("Runtime.evaluate", {
92
96
  expression: `
93
97
  (function(){
94
98
  try {
@@ -209,18 +213,18 @@ async function injectLibraries(cdpSession) {
209
213
  return true;
210
214
  }
211
215
  catch (error) {
212
- debugLog('Failed to inject libraries:', error);
216
+ debugLog("Failed to inject libraries:", error);
213
217
  return false;
214
218
  }
215
219
  }
216
220
  const repl = defineTabTool({
217
- capability: 'core',
221
+ capability: "core",
218
222
  schema: {
219
- name: 'browser_repl',
220
- title: 'Browser REPL',
221
- description: 'DevTools-like browser REPL. Per-tab state persists across calls (const/let/functions); supports top-level await. Survives SPA nav; resets on full reload or when switching tabs. Helpers available via window.mcp: JSON5, JSONPath, _, GraphQLClient, gql, graphqlRequest. No need for wrapping in IIFE.',
223
+ name: "browser_repl",
224
+ title: "Browser REPL",
225
+ description: "DevTools-like browser REPL. Per-tab state persists across calls (const/let/functions); supports top-level await. Survives SPA nav; resets on full reload or when switching tabs. Helpers available via window.mcp: JSON5, JSONPath, _, GraphQLClient, gql, graphqlRequest. No need for wrapping in IIFE.",
222
226
  inputSchema: replSchema,
223
- type: 'action',
227
+ type: "action",
224
228
  },
225
229
  handle: async (tab, params, response) => {
226
230
  const page = tab.page;
@@ -229,7 +233,7 @@ const repl = defineTabTool({
229
233
  async function getOrCreateSession() {
230
234
  if (!state.cdpSession) {
231
235
  state.cdpSession = await page.context().newCDPSession(page);
232
- await state.cdpSession.send('Runtime.enable');
236
+ await state.cdpSession.send("Runtime.enable");
233
237
  }
234
238
  return state.cdpSession;
235
239
  }
@@ -268,7 +272,7 @@ const repl = defineTabTool({
268
272
  // Execute the script with REPL mode for console-like behavior
269
273
  let evaluationResponse;
270
274
  try {
271
- evaluationResponse = await session.send('Runtime.evaluate', {
275
+ evaluationResponse = await session.send("Runtime.evaluate", {
272
276
  expression: transformedScript,
273
277
  replMode: true, // Chrome DevTools console behavior (non-standard, tolerated by CDP)
274
278
  includeCommandLineAPI: true, // Includes console utilities like $, $$, etc.
@@ -280,7 +284,7 @@ const repl = defineTabTool({
280
284
  catch (error) {
281
285
  // If REPL mode fails due to wrapped functions, try without REPL mode
282
286
  if (hasReturnOutsideFunction || hasTopLevelAwait) {
283
- evaluationResponse = await session.send('Runtime.evaluate', {
287
+ evaluationResponse = await session.send("Runtime.evaluate", {
284
288
  expression: transformedScript,
285
289
  replMode: false, // Disable REPL mode for wrapped functions
286
290
  includeCommandLineAPI: true,
@@ -296,7 +300,7 @@ const repl = defineTabTool({
296
300
  if (evaluationResponse.exceptionDetails) {
297
301
  const details = evaluationResponse.exceptionDetails;
298
302
  // Try to get the actual error message from multiple sources
299
- let errorMessage = details.text || 'Evaluation failed';
303
+ let errorMessage = details.text || "Evaluation failed";
300
304
  // If we have exception details, try to extract more info
301
305
  if (details.exception) {
302
306
  if (details.exception.description)
@@ -309,11 +313,11 @@ const repl = defineTabTool({
309
313
  errorMessage += ` (at line ${details.lineNumber}`;
310
314
  if (details.columnNumber !== undefined)
311
315
  errorMessage += `:${details.columnNumber}`;
312
- errorMessage += ')';
316
+ errorMessage += ")";
313
317
  }
314
318
  // Check if it's a timeout error
315
- if (errorMessage.includes('Timeout') ||
316
- errorMessage.includes('timeout'))
319
+ if (errorMessage.includes("Timeout") ||
320
+ errorMessage.includes("timeout"))
317
321
  throw new Error(`Script execution timed out after ${EVALUATE_TIMEOUT_MS / 1000} seconds`);
318
322
  throw new Error(errorMessage);
319
323
  }
@@ -325,14 +329,14 @@ const repl = defineTabTool({
325
329
  const formatOutput = () => {
326
330
  const MAX_OUTPUT_LENGTH = 10_000;
327
331
  const MAX_CONSOLE_OUTPUTS = 100;
328
- let output = '';
332
+ let output = "";
329
333
  // Add console output if any
330
334
  if (consoleOutputs && consoleOutputs.length > 0) {
331
335
  // Limit number of console outputs and truncate each one
332
336
  const truncatedOutputs = consoleOutputs
333
337
  .slice(0, MAX_CONSOLE_OUTPUTS)
334
- .map(o => String(truncate(o, { maxStringLength: 1000 })));
335
- output = truncatedOutputs.join('\n');
338
+ .map((o) => String(truncate(o, { maxStringLength: 1000 })));
339
+ output = truncatedOutputs.join("\n");
336
340
  if (consoleOutputs.length > MAX_CONSOLE_OUTPUTS)
337
341
  output += `\n... (${consoleOutputs.length - MAX_CONSOLE_OUTPUTS} more console outputs omitted)`;
338
342
  }
@@ -344,7 +348,7 @@ const repl = defineTabTool({
344
348
  let resultString;
345
349
  try {
346
350
  resultString =
347
- typeof result === 'string'
351
+ typeof result === "string"
348
352
  ? result
349
353
  : JSON.stringify(result, null, 2);
350
354
  }
@@ -353,29 +357,37 @@ const repl = defineTabTool({
353
357
  resultString = String(result);
354
358
  }
355
359
  // Truncate result if too long (first structure-limit, then clamp string)
356
- const truncatedValue = truncate(result, { maxStringLength: 200, maxItems: 20 });
357
- let limited = '';
360
+ const truncatedValue = truncate(result, {
361
+ maxStringLength: 200,
362
+ maxItems: 20,
363
+ });
364
+ let limited = "";
358
365
  try {
359
- limited = typeof truncatedValue === 'string' ? truncatedValue : JSON.stringify(truncatedValue, null, 2);
366
+ limited =
367
+ typeof truncatedValue === "string"
368
+ ? truncatedValue
369
+ : JSON.stringify(truncatedValue, null, 2);
360
370
  }
361
371
  catch {
362
372
  limited = resultString;
363
373
  }
364
- const truncatedResult = limited.length > 5000 ? (limited.substring(0, 5000) + '\n... (truncated)') : limited;
374
+ const truncatedResult = limited.length > 5000
375
+ ? limited.substring(0, 5000) + "\n... (truncated)"
376
+ : limited;
365
377
  if (output)
366
- output += '\n' + truncatedResult;
378
+ output += "\n" + truncatedResult;
367
379
  else
368
380
  output = truncatedResult;
369
381
  }
370
382
  else if (!consoleOutputs || consoleOutputs.length === 0) {
371
383
  // Only show "undefined" if there's no console output
372
- output = 'undefined';
384
+ output = "undefined";
373
385
  }
374
386
  // Final truncation of the entire output
375
387
  if (output.length > MAX_OUTPUT_LENGTH) {
376
388
  output =
377
389
  output.substring(0, MAX_OUTPUT_LENGTH) +
378
- '\n... (output truncated)';
390
+ "\n... (output truncated)";
379
391
  }
380
392
  return output;
381
393
  };
@@ -385,13 +397,13 @@ const repl = defineTabTool({
385
397
  const errorToMessage = (e) => {
386
398
  if (e instanceof Error)
387
399
  return e.message;
388
- if (typeof e === 'string')
400
+ if (typeof e === "string")
389
401
  return e;
390
402
  try {
391
403
  return JSON.stringify(e);
392
404
  }
393
405
  catch {
394
- return 'Unknown error occurred';
406
+ return "Unknown error occurred";
395
407
  }
396
408
  };
397
409
  response.addError(errorToMessage(error));
@@ -13,67 +13,91 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import { z } from 'zod';
17
- import { defineTabTool } from './tool.js';
18
- import * as javascript from '../utils/codegen.js';
19
- import { generateLocator } from './utils.js';
20
- const screenshotSchema = z.object({
21
- type: z.enum(['png', 'jpeg']).default('png').describe('Image format for the screenshot. Default is png.'),
22
- filename: z.string().optional().describe('File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified.'),
23
- element: z.string().optional().describe('Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too.'),
24
- ref: z.string().optional().describe('Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too.'),
25
- fullPage: z.boolean().optional().describe('When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots.'),
26
- }).refine(data => {
16
+ import { z } from "zod";
17
+ import * as javascript from "../utils/codegen.js";
18
+ import { defineTabTool } from "./tool.js";
19
+ import { generateLocator } from "./utils.js";
20
+ const screenshotSchema = z
21
+ .object({
22
+ type: z
23
+ .enum(["png", "jpeg"])
24
+ .default("png")
25
+ .describe("Image format for the screenshot. Default is png."),
26
+ filename: z
27
+ .string()
28
+ .optional()
29
+ .describe("File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified."),
30
+ element: z
31
+ .string()
32
+ .optional()
33
+ .describe("Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too."),
34
+ ref: z
35
+ .string()
36
+ .optional()
37
+ .describe("Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too."),
38
+ fullPage: z
39
+ .boolean()
40
+ .optional()
41
+ .describe("When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots."),
42
+ })
43
+ .refine((data) => {
27
44
  return !!data.element === !!data.ref;
28
45
  }, {
29
- message: 'Both element and ref must be provided or neither.',
30
- path: ['ref', 'element']
31
- }).refine(data => {
46
+ message: "Both element and ref must be provided or neither.",
47
+ path: ["ref", "element"],
48
+ })
49
+ .refine((data) => {
32
50
  return !(data.fullPage && (data.element || data.ref));
33
51
  }, {
34
- message: 'fullPage cannot be used with element screenshots.',
35
- path: ['fullPage']
52
+ message: "fullPage cannot be used with element screenshots.",
53
+ path: ["fullPage"],
36
54
  });
37
55
  const screenshot = defineTabTool({
38
- capability: 'core',
56
+ capability: "core",
39
57
  schema: {
40
- name: 'browser_take_screenshot',
41
- title: 'Take a screenshot',
58
+ name: "browser_take_screenshot",
59
+ title: "Take a screenshot",
42
60
  description: `Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.`,
43
61
  inputSchema: screenshotSchema,
44
- type: 'readOnly',
62
+ type: "readOnly",
45
63
  },
46
64
  handle: async (tab, params, response) => {
47
- const fileType = params.type || 'png';
65
+ const fileType = params.type || "png";
48
66
  const fileName = await tab.context.outputFile(params.filename ?? `page-${new Date().toISOString()}.${fileType}`);
49
67
  const options = {
50
68
  type: fileType,
51
- quality: fileType === 'png' ? undefined : 90,
52
- scale: 'css',
69
+ quality: fileType === "png" ? undefined : 90,
70
+ scale: "css",
53
71
  path: fileName,
54
- ...(params.fullPage !== undefined && { fullPage: params.fullPage })
72
+ ...(params.fullPage !== undefined && { fullPage: params.fullPage }),
55
73
  };
56
74
  const isElementScreenshot = params.element && params.ref;
57
- const screenshotTarget = isElementScreenshot ? params.element : (params.fullPage ? 'full page' : 'viewport');
75
+ const screenshotTarget = isElementScreenshot
76
+ ? params.element
77
+ : params.fullPage
78
+ ? "full page"
79
+ : "viewport";
58
80
  response.addCode(`// Screenshot ${screenshotTarget} and save it as ${fileName}`);
59
81
  // Only get snapshot when element screenshot is needed
60
- const locator = params.ref ? await tab.refLocator({ element: params.element || '', ref: params.ref }) : null;
82
+ const locator = params.ref
83
+ ? await tab.refLocator({ element: params.element || "", ref: params.ref })
84
+ : null;
61
85
  if (locator)
62
86
  response.addCode(`await page.${await generateLocator(locator)}.screenshot(${javascript.formatObject(options)});`);
63
87
  else
64
88
  response.addCode(`await page.screenshot(${javascript.formatObject(options)});`);
65
- const buffer = locator ? await locator.screenshot(options) : await tab.page.screenshot(options);
89
+ const buffer = locator
90
+ ? await locator.screenshot(options)
91
+ : await tab.page.screenshot(options);
66
92
  response.addResult(`Took the ${screenshotTarget} screenshot and saved it as ${fileName}`);
67
93
  // https://github.com/microsoft/playwright-mcp/issues/817
68
94
  // Never return large images to LLM, saving them to the file system is enough.
69
95
  if (!params.fullPage) {
70
96
  response.addImage({
71
- contentType: fileType === 'png' ? 'image/png' : 'image/jpeg',
72
- data: buffer
97
+ contentType: fileType === "png" ? "image/png" : "image/jpeg",
98
+ data: buffer,
73
99
  });
74
100
  }
75
- }
101
+ },
76
102
  });
77
- export default [
78
- screenshot,
79
- ];
103
+ export default [screenshot];
@@ -1,6 +1,6 @@
1
- import { z } from 'zod';
2
- import ms from 'ms';
3
- import { defineTabTool } from './tool.js';
1
+ import ms from "ms";
2
+ import { z } from "zod";
3
+ import { defineTabTool } from "./tool.js";
4
4
  /**
5
5
  * Generate random number between min and max
6
6
  */
@@ -15,8 +15,10 @@ function generateScrollPattern(totalDelta) {
15
15
  const abs = Math.abs(totalDelta);
16
16
  const direction = totalDelta > 0 ? 1 : -1;
17
17
  // Faster profile: fewer steps overall, still eased
18
- const steps = abs < 250 ? randomBetween(3, 6)
19
- : abs < 1200 ? randomBetween(4, 10)
18
+ const steps = abs < 250
19
+ ? randomBetween(3, 6)
20
+ : abs < 1200
21
+ ? randomBetween(4, 10)
20
22
  : randomBetween(8, 16);
21
23
  const base = abs / steps;
22
24
  const easeInOut = (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t); // 0..1
@@ -28,42 +30,44 @@ function generateScrollPattern(totalDelta) {
28
30
  // Larger per-event wheel to reduce total time: clamp 10..180px
29
31
  const clamped = Math.max(10, Math.min(180, Math.round(scrollAmount)));
30
32
  // Short delays: 15–60ms + small size factor
31
- const delay = randomBetween(ms('15ms'), ms('60ms')) + Math.floor(clamped * 0.08);
33
+ const delay = randomBetween(ms("15ms"), ms("60ms")) + Math.floor(clamped * 0.08);
32
34
  pattern.push({
33
35
  delta: clamped * direction,
34
36
  delay,
35
37
  });
36
38
  // Rare micro-pause (short) to keep a bit of human feel
37
39
  if (Math.random() < 0.05 && i < steps - 1)
38
- pattern.push({ delta: 0, delay: randomBetween(ms('30ms'), ms('80ms')) });
40
+ pattern.push({ delta: 0, delay: randomBetween(ms("30ms"), ms("80ms")) });
39
41
  }
40
42
  // Smaller, rarer overshoot for long moves only
41
43
  if (abs > 600 && Math.random() < 0.1) {
42
44
  pattern.push({
43
45
  delta: -direction * randomBetween(6, 18),
44
- delay: randomBetween(ms('60ms'), ms('120ms')),
46
+ delay: randomBetween(ms("60ms"), ms("120ms")),
45
47
  });
46
48
  }
47
49
  return pattern;
48
50
  }
49
51
  const scrollSchema = z.object({
50
- amount: z.number().describe('Vertical scroll amount in pixels (positive scrolls down, negative up)')
52
+ amount: z
53
+ .number()
54
+ .describe("Vertical scroll amount in pixels (positive scrolls down, negative up)"),
51
55
  });
52
56
  const scrollWheel = defineTabTool({
53
- capability: 'core',
57
+ capability: "core",
54
58
  schema: {
55
- name: 'browser_scroll',
56
- title: 'Scroll page',
57
- description: 'Scroll the page using mouse wheel with human-like behavior',
59
+ name: "browser_scroll",
60
+ title: "Scroll page",
61
+ description: "Scroll the page using mouse wheel with human-like behavior",
58
62
  inputSchema: scrollSchema,
59
- type: 'readOnly',
63
+ type: "readOnly",
60
64
  },
61
65
  handle: async (tab, params, response) => {
62
66
  const requestedDeltaY = params.amount || 0;
63
67
  // Capture initial scroll position
64
68
  const initialScrollPosition = await tab.page.evaluate(() => ({
65
69
  x: window.scrollX,
66
- y: window.scrollY
70
+ y: window.scrollY,
67
71
  }));
68
72
  response.addCode(`// Scroll page by ${requestedDeltaY}px (vertical)`);
69
73
  await tab.waitForCompletion(async () => {
@@ -76,17 +80,20 @@ const scrollWheel = defineTabTool({
76
80
  response.addCode(`await page.mouse.move(${x}, ${y});`);
77
81
  }
78
82
  // Add initial "hover" delay (short)
79
- await tab.page.waitForTimeout(randomBetween(ms('40ms'), ms('120ms')));
83
+ await tab.page.waitForTimeout(randomBetween(ms("40ms"), ms("120ms")));
80
84
  // Generate and execute human-like scroll pattern
81
- const scrollPatternY = requestedDeltaY ? generateScrollPattern(requestedDeltaY) : [];
85
+ const scrollPatternY = requestedDeltaY
86
+ ? generateScrollPattern(requestedDeltaY)
87
+ : [];
82
88
  const maxSteps = scrollPatternY.length;
83
89
  for (let i = 0; i < maxSteps; i++) {
84
90
  let deltaY = scrollPatternY[i]?.delta || 0;
85
91
  const delay = scrollPatternY[i]?.delay || 0;
86
92
  // Clamp vertical delta to avoid scrolling past page edges
87
- const allowedY = await tab.page.evaluate(dy => {
93
+ const allowedY = await tab.page.evaluate((dy) => {
88
94
  const doc = document.scrollingElement || document.documentElement;
89
- const maxY = Math.max(0, (doc.scrollHeight || document.documentElement.scrollHeight) - window.innerHeight);
95
+ const maxY = Math.max(0, (doc.scrollHeight || document.documentElement.scrollHeight) -
96
+ window.innerHeight);
90
97
  const curY = window.scrollY || doc.scrollTop || 0;
91
98
  let a = dy;
92
99
  if (curY + dy < 0)
@@ -108,19 +115,17 @@ const scrollWheel = defineTabTool({
108
115
  }
109
116
  }
110
117
  // Short final pause
111
- const finalDelay = randomBetween(ms('80ms'), ms('200ms'));
118
+ const finalDelay = randomBetween(ms("80ms"), ms("200ms"));
112
119
  await tab.page.waitForTimeout(finalDelay);
113
120
  response.addCode(`await page.waitForTimeout(${finalDelay});`);
114
121
  });
115
122
  // Capture final scroll position
116
123
  const finalScrollPosition = await tab.page.evaluate(() => ({
117
124
  x: window.scrollX,
118
- y: window.scrollY
125
+ y: window.scrollY,
119
126
  }));
120
127
  // Always show scroll position for scroll tool
121
128
  response.addResult(`Scroll position:\nBefore: x=${initialScrollPosition.x}, y=${initialScrollPosition.y}\nAfter: x=${finalScrollPosition.x}, y=${finalScrollPosition.y}`);
122
129
  },
123
130
  });
124
- export default [
125
- scrollWheel,
126
- ];
131
+ export default [scrollWheel];