cbrowser 19.5.0 → 19.6.0

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 (48) hide show
  1. package/THIRD_PARTY_LICENSES.md +39 -0
  2. package/dist/browser.d.ts.map +1 -1
  3. package/dist/browser.js +34 -7
  4. package/dist/browser.js.map +1 -1
  5. package/dist/cli.js +97 -95
  6. package/dist/cli.js.map +1 -1
  7. package/dist/cognitive/index.d.ts +5 -0
  8. package/dist/cognitive/index.d.ts.map +1 -1
  9. package/dist/cognitive/index.js +5 -0
  10. package/dist/cognitive/index.js.map +1 -1
  11. package/dist/config.d.ts +9 -0
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js.map +1 -1
  14. package/dist/daemon.d.ts +4 -1
  15. package/dist/daemon.d.ts.map +1 -1
  16. package/dist/daemon.js +15 -7
  17. package/dist/daemon.js.map +1 -1
  18. package/dist/device-names.d.ts +10 -0
  19. package/dist/device-names.d.ts.map +1 -0
  20. package/dist/device-names.js +16 -0
  21. package/dist/device-names.js.map +1 -0
  22. package/dist/device-presets-devtools.d.ts +3 -0
  23. package/dist/device-presets-devtools.d.ts.map +1 -0
  24. package/dist/device-presets-devtools.js +823 -0
  25. package/dist/device-presets-devtools.js.map +1 -0
  26. package/dist/devices.d.ts +15 -0
  27. package/dist/devices.d.ts.map +1 -0
  28. package/dist/devices.js +196 -0
  29. package/dist/devices.js.map +1 -0
  30. package/dist/eval-script.d.ts +29 -0
  31. package/dist/eval-script.d.ts.map +1 -0
  32. package/dist/eval-script.js +46 -0
  33. package/dist/eval-script.js.map +1 -0
  34. package/dist/index.d.ts +1 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1 -0
  37. package/dist/index.js.map +1 -1
  38. package/dist/mcp-tools/base/audit-tools.js +1 -1
  39. package/dist/mcp-tools/base/audit-tools.js.map +1 -1
  40. package/dist/testing/nl-test-suite.d.ts +7 -0
  41. package/dist/testing/nl-test-suite.d.ts.map +1 -1
  42. package/dist/testing/nl-test-suite.js +3 -1
  43. package/dist/testing/nl-test-suite.js.map +1 -1
  44. package/dist/types.d.ts +0 -1
  45. package/dist/types.d.ts.map +1 -1
  46. package/dist/types.js +0 -99
  47. package/dist/types.js.map +1 -1
  48. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -20,7 +20,8 @@ import { capturePerformanceBaseline, listPerformanceBaselines, loadPerformanceBa
20
20
  // Visual module imports
21
21
  import { captureVisualBaseline, listVisualBaselines, getVisualBaseline, deleteVisualBaseline, runVisualRegression, runVisualRegressionSuite, formatVisualRegressionReport, generateVisualRegressionHtmlReport, runCrossBrowserTest, runCrossBrowserSuite, formatCrossBrowserReport, generateCrossBrowserHtmlReport, runResponsiveTest, runResponsiveSuite, formatResponsiveReport, generateResponsiveHtmlReport, listViewportPresets, runABComparison, runABSuite, formatABReport, generateABHtmlReport, crossBrowserDiff } from "./visual/index.js";
22
22
  import { BUILTIN_PERSONAS, loadCustomPersonas, saveCustomPersona, deleteCustomPersona, isBuiltinPersona, generatePersonaFromDescription, listAccessibilityPersonas, } from "./personas.js";
23
- import { DEVICE_PRESETS, LOCATION_PRESETS } from "./types.js";
23
+ import { LOCATION_PRESETS } from "./types.js";
24
+ import { CBROWSER_DEVICE_KEYS, DEVICE_PRESETS, resolveDevice } from "./devices.js";
24
25
  import { startMcpServer } from "./mcp-server.js";
25
26
  import { startRemoteMcpServer } from "./mcp-server-remote.js";
26
27
  import { startDaemon, stopDaemon, getDaemonStatus, isDaemonRunning, sendToDaemon, runDaemonServer } from "./daemon.js";
@@ -30,6 +31,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync,
30
31
  import { resolveCookieDomain, hostFromUrl } from "./utils.js";
31
32
  import { dirname, join, resolve } from "node:path";
32
33
  import { guardConsole, guardTerminalOutput } from "./terminal-safe.js";
34
+ import { evaluateScript } from "./eval-script.js";
33
35
  import { VideoCaptureSession } from "./recording/engine.js";
34
36
  import { changeSignal, parseManifest } from "./recording/types.js";
35
37
  import { printEnterpriseStatus } from "./stealth/index.js";
@@ -299,7 +301,7 @@ COOKIE MANAGEMENT
299
301
  cookie clear Clear all cookies
300
302
 
301
303
  DEVICE EMULATION
302
- device list List available device presets
304
+ device list [filter] List device presets, one per line (filter by name)
303
305
  device set <name> Set device emulation for session
304
306
  Example: cbrowser device set iphone-15
305
307
 
@@ -745,6 +747,8 @@ CLAUDE SKILL INSTALLATION & SYNC
745
747
  DAEMON MODE (v6.4.0)
746
748
  daemon start Start background daemon (keeps browser running)
747
749
  --port <port> Daemon port (default: 9222)
750
+ --device <name> Device the daemon's browser emulates
751
+ --viewport <WxH> Page size of the daemon's browser
748
752
  --timeout <min> Idle timeout in minutes (default: 30)
749
753
  daemon stop Stop the running daemon
750
754
  daemon status Check if daemon is running
@@ -796,7 +800,9 @@ STORAGE & CLEANUP
796
800
 
797
801
  OPTIONS
798
802
  --browser <type> Browser: chromium, firefox, webkit (default: chromium)
799
- --device <name> Device preset: iphone-15, pixel-8, ipad-pro-12, etc.
803
+ --device <name> Device preset: iphone-16, pixel-9, ipad-pro-13, ... ('device list')
804
+ --viewport <WxH> Page size, e.g. 390x844; with --device it sets the size and
805
+ the device keeps its pixel ratio, touch and user agent
800
806
  --geo <location> Location preset or lat,lon coordinates
801
807
  --locale <locale> Browser locale (e.g., en-US, fr-FR)
802
808
  --timezone <tz> Timezone (e.g., America/New_York)
@@ -975,10 +981,17 @@ const DRIVES_THE_PAGE = [
975
981
  "keyboard", "press", "type", "evaluate", "eval", "drag", "upload",
976
982
  "smart-click", "dismiss-overlay", "assert",
977
983
  ];
978
- const GLOBAL_FLAGS = [
979
- "url", "browser", "device", "headless", "persistent", "restore",
980
- "locale", "timezone", "geo", "verbose", "help", "no-color", "quiet", "json",
984
+ /**
985
+ * Flags the shared CBrowser construction consumes, for every command that uses
986
+ * it. One list, spread into each command's set below: when this list was copied
987
+ * into each set, --viewport (2026-10-21) reached some commands and was refused
988
+ * by evaluate and keyboard.
989
+ */
990
+ const SHARED_BROWSER_FLAGS = [
991
+ "url", "browser", "device", "viewport", "headless", "persistent", "restore",
992
+ "locale", "timezone", "geo", "verbose",
981
993
  ];
994
+ const GLOBAL_FLAGS = [...SHARED_BROWSER_FLAGS, "help", "no-color", "quiet", "json"];
982
995
  /**
983
996
  * Declared options per command.
984
997
  *
@@ -1146,29 +1159,24 @@ const CAPTURE_FLAGS = new Set([
1146
1159
  "contact-sheet", "attention-overlay", "overlay-persona", "overlay-goal",
1147
1160
  "after", "after-element", "after-delay",
1148
1161
  "until-element", "until-element-gone", "until-idle", "timeout",
1149
- // globals honoured by the shared CBrowser construction
1150
- "url", "browser", "device", "headless", "persistent", "restore",
1151
- "locale", "timezone", "geo", "verbose",
1162
+ ...SHARED_BROWSER_FLAGS,
1152
1163
  ]);
1153
1164
  /**
1154
1165
  * Resolve `--device` to a real preset key, or exit naming the bad value.
1155
1166
  *
1156
- * CBrowser silently ignores an unknown device name (browser.ts: the preset
1157
- * lookup is inside the `if`), which would hand back a 1280x800 desktop capture
1158
- * for `--device "iPhone 14"` and call it success. Spaces and case are
1159
- * normalised so "iPhone 15" finds "iphone-15".
1167
+ * `capture start` checks here, before it builds anything. Every other command
1168
+ * gets the same check from CBrowser.launch() (src/devices.ts resolveDevice);
1169
+ * before 2026-10-21 the browser skipped names it did not know and rendered
1170
+ * 1280x800 desktop. Case and spaces do not matter: "iPhone 15" is "iphone-15".
1160
1171
  */
1161
1172
  function resolveDevicePreset(value) {
1162
- const normalised = value.trim().toLowerCase().replace(/\s+/g, "-");
1163
- if (DEVICE_PRESETS[normalised])
1164
- return normalised;
1165
- const known = Object.keys(DEVICE_PRESETS);
1166
- const stem = normalised.replace(/-?\d+.*$/, "");
1167
- const close = known.filter((k) => k.startsWith(stem) && stem.length > 0);
1173
+ const resolved = resolveDevice(value);
1174
+ if ("key" in resolved)
1175
+ return resolved.key;
1168
1176
  console.error(`Error: unknown --device "${value}"`);
1169
- if (close.length > 0)
1170
- console.error(` Did you mean: ${close.join(", ")}?`);
1171
- console.error(` Available presets: ${known.join(", ")}`);
1177
+ if (resolved.suggestions.length > 0)
1178
+ console.error(` Did you mean: ${resolved.suggestions.join(", ")}?`);
1179
+ console.error(` 'cbrowser device list' shows all ${Object.keys(DEVICE_PRESETS).length} presets; --viewport WIDTHxHEIGHT sets any size.`);
1172
1180
  process.exit(1);
1173
1181
  }
1174
1182
  /** Reject any flag `capture start` cannot honour, naming it. */
@@ -1752,8 +1760,7 @@ function reportAutoCapture(capture) {
1752
1760
  /** Flags `evaluate` understands; anything else is rejected rather than ignored. */
1753
1761
  const EVALUATE_FLAGS = new Set([
1754
1762
  "file", "arg", "json", "raw", "wait-for", "timeout", "expect-truthy", "force",
1755
- "url", "browser", "device", "headless", "persistent", "restore",
1756
- "locale", "timezone", "geo", "verbose",
1763
+ ...SHARED_BROWSER_FLAGS,
1757
1764
  ]);
1758
1765
  /**
1759
1766
  * `--force` is a bare flag, but the parser gives any flag the next non-flag
@@ -1786,53 +1793,6 @@ function repeatedFlag(name) {
1786
1793
  }
1787
1794
  return values;
1788
1795
  }
1789
- /**
1790
- * Evaluate a script in the page, accepting all three shapes people write.
1791
- *
1792
- * `--file` exists because multi-line scripts die in shell quoting, so it has to
1793
- * accept real scripts - `const`, `let`, an explicit `return` - not only a
1794
- * single expression. The source is compiled inside the page as a function
1795
- * body: a bare expression gets an implicit return, a statement body keeps its
1796
- * own, and a function/arrow literal is called with the arguments.
1797
- */
1798
- async function evaluateScript(page, script, args) {
1799
- const source = script.trim();
1800
- // A function or arrow literal is called with the arguments directly.
1801
- const isCallable = /^(async\s+)?function\b/.test(source) || /=>/.test(source);
1802
- return page.evaluate((payload) => {
1803
- if (payload.callable) {
1804
- const fn = (0, eval)(`(${payload.src})`);
1805
- return fn(...payload.a);
1806
- }
1807
- const compile = (src) => {
1808
- // 1. A bare expression: implicit return.
1809
- try {
1810
- return new Function("args", `return (${src});`);
1811
- }
1812
- catch { /* not an expression */ }
1813
- // 2. Statements with no explicit return: return the final expression,
1814
- // which is what a REPL and the old one-arg form both did.
1815
- if (!/\breturn\b/.test(src)) {
1816
- const trimmed = src.replace(/;\s*$/, "");
1817
- const split = Math.max(trimmed.lastIndexOf(";"), trimmed.lastIndexOf("\n"));
1818
- if (split > -1) {
1819
- const head = trimmed.slice(0, split + 1);
1820
- const tail = trimmed.slice(split + 1).trim();
1821
- if (tail) {
1822
- try {
1823
- return new Function("args", `${head}\nreturn (${tail});`);
1824
- }
1825
- catch { /* the tail is not an expression either */ }
1826
- }
1827
- }
1828
- }
1829
- // 3. Statements as written. Any syntax error surfaces from here, which
1830
- // is the error the user should actually see.
1831
- return new Function("args", src);
1832
- };
1833
- return compile(payload.src)(payload.a);
1834
- }, { src: source, a: args, callable: isCallable });
1835
- }
1836
1796
  /**
1837
1797
  * Run JavaScript in the page.
1838
1798
  *
@@ -2072,8 +2032,7 @@ function parseKeyboardToken(token, literal = false) {
2072
2032
  /** Flags `keyboard` understands. */
2073
2033
  const KEYBOARD_FLAGS = new Set([
2074
2034
  "delay", "selector", "hold", "repeat", "text", "force",
2075
- "url", "browser", "device", "headless", "persistent", "restore",
2076
- "locale", "timezone", "geo", "verbose",
2035
+ ...SHARED_BROWSER_FLAGS,
2077
2036
  ]);
2078
2037
  /**
2079
2038
  * Drive a keyboard sequence.
@@ -2868,9 +2827,28 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
2868
2827
  const port = parseInt(options.port) || 9222;
2869
2828
  switch (subCommand) {
2870
2829
  case "start": {
2830
+ // The daemon's browser takes its device and size from here. Both are
2831
+ // checked now: a bad value inside the detached daemon would only show
2832
+ // up as a daemon that failed to start.
2833
+ const launchArgs = [];
2834
+ if (typeof options.device === "string")
2835
+ launchArgs.push("--device", resolveDevicePreset(options.device));
2836
+ if (typeof options.viewport === "string") {
2837
+ try {
2838
+ parseViewportList(options.viewport);
2839
+ }
2840
+ catch (e) {
2841
+ console.error(`Error: ${e.message}`);
2842
+ process.exit(1);
2843
+ }
2844
+ launchArgs.push("--viewport", options.viewport);
2845
+ }
2871
2846
  console.log("šŸš€ Starting CBrowser daemon...");
2872
- const result = await startDaemon(port);
2847
+ const result = await startDaemon(port, launchArgs);
2873
2848
  console.log(result.success ? `āœ“ ${result.message}` : `āœ— ${result.message}`);
2849
+ if (result.alreadyRunning && launchArgs.length > 0) {
2850
+ console.error("⚠ --device and --viewport were not applied: the daemon was already running. Run cbrowser daemon stop first.");
2851
+ }
2874
2852
  process.exit(result.success ? 0 : 1);
2875
2853
  break;
2876
2854
  }
@@ -2894,9 +2872,12 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
2894
2872
  : options.browser === "webkit" ? "webkit"
2895
2873
  : "chromium";
2896
2874
  // runDaemonServer will merge with defaults internally
2875
+ const runViewport = typeof options.viewport === "string" ? parseViewportList(options.viewport)[0] : undefined;
2897
2876
  await runDaemonServer({
2898
2877
  browser: browserType,
2899
2878
  headless: options.headless !== false && options.headless !== "false",
2879
+ ...(typeof options.device === "string" ? { device: options.device } : {}),
2880
+ ...(runViewport ? { viewportWidth: runViewport.width, viewportHeight: runViewport.height, viewportOverride: runViewport } : {}),
2900
2881
  }, port);
2901
2882
  return;
2902
2883
  }
@@ -2919,6 +2900,12 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
2919
2900
  ];
2920
2901
  if (daemonRunning && INTERACTION_COMMANDS.includes(command)) {
2921
2902
  console.log("šŸ”Œ Connected to running daemon");
2903
+ // The daemon's browser keeps the device and size it started with; a flag on
2904
+ // a command it runs cannot change them, so say so instead of ignoring it.
2905
+ if (typeof options.device === "string" || typeof options.viewport === "string") {
2906
+ console.error("⚠ --device and --viewport do not apply to commands the daemon runs: its browser keeps the device and size it started with.");
2907
+ console.error(" To change them: cbrowser daemon stop, then cbrowser daemon start --device <name> --viewport <WxH>");
2908
+ }
2922
2909
  const daemonCommand = command;
2923
2910
  let daemonArgs = {};
2924
2911
  switch (command) {
@@ -2989,7 +2976,11 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
2989
2976
  console.error(" Re-run with --force to run it anyway.");
2990
2977
  process.exit(1);
2991
2978
  }
2992
- daemonArgs = { body: `return (function(){ ${/\breturn\b/.test(script) ? script : `return (${script});`} })();`, args: [], force: true };
2979
+ // The raw script: the daemon compiles it with the same evaluateScript as the
2980
+ // local path. This hand-built body treated a "return" anywhere - even inside
2981
+ // a nested function - as statements, and an IIFE's value was dropped.
2982
+ // (2026-10-13)
2983
+ daemonArgs = { script, args: [], force: true };
2993
2984
  break;
2994
2985
  }
2995
2986
  case "screenshot": {
@@ -3030,7 +3021,9 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
3030
3021
  const result = await sendToDaemon(daemonCommand, daemonArgs);
3031
3022
  if (result.success) {
3032
3023
  console.log("āœ“ Command executed via daemon");
3033
- if (result.result) {
3024
+ // evaluate's value is the answer, so 0, false and "" print too; for other
3025
+ // commands an empty result means nothing to show. (2026-10-13)
3026
+ if (daemonCommand === "evaluate" ? result.result !== undefined : result.result) {
3034
3027
  console.log(JSON.stringify(result.result, null, 2));
3035
3028
  }
3036
3029
  }
@@ -3096,7 +3089,7 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
3096
3089
  browser: browserType,
3097
3090
  headless,
3098
3091
  device: options.device,
3099
- ...(viewportOverride ? { viewportWidth: viewportOverride.width, viewportHeight: viewportOverride.height } : {}),
3092
+ ...(viewportOverride ? { viewportWidth: viewportOverride.width, viewportHeight: viewportOverride.height, viewportOverride } : {}),
3100
3093
  geolocation,
3101
3094
  locale: options.locale,
3102
3095
  timezone: options.timezone,
@@ -4241,14 +4234,18 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
4241
4234
  const subcommand = args[0];
4242
4235
  switch (subcommand) {
4243
4236
  case "list": {
4244
- console.log("\nšŸ“± Available Device Presets:\n");
4245
- for (const [name, device] of Object.entries(DEVICE_PRESETS)) {
4246
- console.log(` ${name}`);
4247
- console.log(` ${device.name}`);
4248
- console.log(` ${device.viewport.width}x${device.viewport.height} @${device.deviceScaleFactor}x`);
4249
- console.log(` Mobile: ${device.isMobile} | Touch: ${device.hasTouch}`);
4250
- console.log("");
4251
- }
4237
+ // One line per preset: there are a few hundred now. An optional
4238
+ // filter matches the key or the display name.
4239
+ const filter = (args[1] ?? "").toLowerCase();
4240
+ const rows = Object.entries(DEVICE_PRESETS).filter(([key, d]) => !filter || key.includes(filter) || d.name.toLowerCase().includes(filter));
4241
+ const width = Math.max(...rows.map(([key]) => key.length), 4);
4242
+ console.log(`\nšŸ“± Device presets${filter ? ` matching "${filter}"` : ""}: ${rows.length}\n`);
4243
+ for (const [key, d] of rows) {
4244
+ const kind = d.isMobile ? "mobile" : "desktop";
4245
+ const touch = d.hasTouch ? ", touch" : "";
4246
+ console.log(` ${key.padEnd(width)} ${d.viewport.width}x${d.viewport.height} @${d.deviceScaleFactor}x ${kind}${touch} (${d.name})`);
4247
+ }
4248
+ console.log("\n Case and spaces do not matter: --device \"iPhone 16\" is iphone-16.");
4252
4249
  break;
4253
4250
  }
4254
4251
  case "set": {
@@ -4257,15 +4254,11 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
4257
4254
  console.error("Usage: cbrowser device set <name>");
4258
4255
  process.exit(1);
4259
4256
  }
4260
- if (!DEVICE_PRESETS[deviceName]) {
4261
- console.error(`Unknown device: ${deviceName}`);
4262
- console.error("Run 'cbrowser device list' to see available devices");
4263
- process.exit(1);
4264
- }
4257
+ const presetKey = resolveDevicePreset(deviceName);
4265
4258
  // Save device setting to persistent session state
4266
4259
  const deviceBrowser = new CBrowser({ persistent: true, headless: true, verbose: false });
4267
- deviceBrowser.saveDeviceSetting(deviceName);
4268
- console.log(`āœ“ Device set: ${deviceName}`);
4260
+ deviceBrowser.saveDeviceSetting(presetKey);
4261
+ console.log(`āœ“ Device set: ${presetKey}`);
4269
4262
  console.log(" Device emulation will apply to all subsequent commands");
4270
4263
  break;
4271
4264
  }
@@ -5448,9 +5441,10 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
5448
5441
  console.error("Usage: cbrowser parallel devices <url> [--devices <list>] [--concurrency <n>]");
5449
5442
  process.exit(1);
5450
5443
  }
5444
+ // Without --devices, cbrowser's own presets, not all few hundred.
5451
5445
  const deviceList = options.devices
5452
- ? options.devices.split(",")
5453
- : Object.keys(DEVICE_PRESETS);
5446
+ ? options.devices.split(",").map((d) => resolveDevicePreset(d))
5447
+ : [...CBROWSER_DEVICE_KEYS];
5454
5448
  const concurrency = options.concurrency ? parseInt(options.concurrency) : 3;
5455
5449
  console.log(`\nšŸš€ Running parallel device tests...`);
5456
5450
  console.log(` URL: ${url}`);
@@ -5912,7 +5906,11 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
5912
5906
  });
5913
5907
  console.log(`\nšŸ” Step-through mode enabled. Press Enter to execute each step.\n`);
5914
5908
  const { CBrowser } = await import("./browser.js");
5915
- const stepBrowser = new CBrowser({ headless });
5909
+ const stepBrowser = new CBrowser({
5910
+ headless,
5911
+ ...(typeof options.device === "string" ? { device: options.device } : {}),
5912
+ ...(viewportOverride ? { viewportWidth: viewportOverride.width, viewportHeight: viewportOverride.height, viewportOverride } : {}),
5913
+ });
5916
5914
  await stepBrowser.launch();
5917
5915
  try {
5918
5916
  const totalSteps = suite.tests.reduce((sum, t) => sum + t.steps.length, 0);
@@ -5993,6 +5991,8 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
5993
5991
  continueOnFailure: options["continue-on-failure"] === true,
5994
5992
  screenshotOnFailure: options["screenshot-on-failure"] !== false,
5995
5993
  headless,
5994
+ device: typeof options.device === "string" ? options.device : undefined,
5995
+ viewport: viewportOverride,
5996
5996
  fuzzyMatch: options["fuzzy-match"] === true,
5997
5997
  capture: buildAutoCaptureSetting(options, `test-suite-${suite.name.replace(/[^a-zA-Z0-9-_]+/g, "-").toLowerCase()}-${Date.now()}`),
5998
5998
  };
@@ -6567,6 +6567,8 @@ Documentation: https://github.com/alexandriashai/cbrowser/wiki
6567
6567
  maxTime,
6568
6568
  verbose,
6569
6569
  headless,
6570
+ device: typeof options.device === "string" ? options.device : undefined,
6571
+ viewport: viewportOverride,
6570
6572
  vision,
6571
6573
  customTraits, // Pass custom traits from env var
6572
6574
  location: locationOverride, // Pass location override