@xbrowser/cli 1.9.9 → 1.11.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 (27) hide show
  1. package/dist/{anti-bot-DR56Y63V.js → anti-bot-GTTYNEFB.js} +1 -1
  2. package/dist/{browser-FFYPFTBV.js → browser-Q5APBNF6.js} +1 -1
  3. package/dist/{browser-YCO4GJMZ.js → browser-V3JIWSTR.js} +2 -2
  4. package/dist/{browser-RHWUAMTB.js → browser-XNU7JQYC.js} +3 -2
  5. package/dist/{cdp-driver-J3YQ4LJV.js → cdp-driver-GD6YBBGE.js} +1 -1
  6. package/dist/cdp-driver-VEK6BNN6.js +49 -0
  7. package/dist/{cdp-driver-2T4P4ZE2.js → cdp-driver-XFTTZI5O.js} +436 -56
  8. package/dist/chunk-3FWLW7FS.js +106 -0
  9. package/dist/chunk-7OFM755Z.js +3509 -0
  10. package/dist/{chunk-5UGR6MUK.js → chunk-HBHOKZPN.js} +51 -16
  11. package/dist/{chunk-6OZWKXSW.js → chunk-HVPUVVSA.js} +72 -15
  12. package/dist/{chunk-GUTBIYFW.js → chunk-JEDP4PJW.js} +507 -70
  13. package/dist/{chunk-INTQPBYF.js → chunk-JKVUFP3G.js} +6 -2
  14. package/dist/{chunk-4452SPFI.js → chunk-NODRQGOK.js} +51 -16
  15. package/dist/{chunk-YMUSHPU4.js → chunk-WHPBNUKB.js} +35 -1
  16. package/dist/{chunk-RZM5CNIY.js → chunk-XKQVUFUS.js} +72 -15
  17. package/dist/{chunk-RRBXV7KE.js → chunk-XQ4HRPDJ.js} +384 -102
  18. package/dist/cli.js +79 -29
  19. package/dist/{daemon-client-GKEPT4NY.js → daemon-client-MMDRYCF5.js} +35 -1
  20. package/dist/{daemon-client-O6BYVRXV.js → daemon-client-Y2YSZCGK.js} +1 -1
  21. package/dist/daemon-main.js +103 -32
  22. package/dist/index.d.ts +8 -0
  23. package/dist/index.js +94 -31
  24. package/dist/{session-recorder-H3KEYU26.js → session-recorder-3BEVWHOK.js} +1 -1
  25. package/dist/{session-recorder-QRZMKFVL.js → session-recorder-SLDBENVF.js} +1 -1
  26. package/dist/{session-replayer-LJUC4TI7.js → session-replayer-WIUYVN5J.js} +90 -2
  27. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  detectAntiBot,
3
3
  formatDetectionMessage
4
- } from "./chunk-INTQPBYF.js";
4
+ } from "./chunk-JKVUFP3G.js";
5
5
  import {
6
6
  forwardCommandLog,
7
7
  forwardNetworkAnalyze,
@@ -25,7 +25,7 @@ import {
25
25
  forwardSessionList,
26
26
  forwardViewerCheckSelector,
27
27
  version
28
- } from "./chunk-YMUSHPU4.js";
28
+ } from "./chunk-WHPBNUKB.js";
29
29
  import {
30
30
  buildViewerUrl
31
31
  } from "./chunk-3OD76SUE.js";
@@ -66,7 +66,7 @@ import {
66
66
  } from "./chunk-ANVL2ID2.js";
67
67
  import {
68
68
  SessionRecorder
69
- } from "./chunk-4452SPFI.js";
69
+ } from "./chunk-NODRQGOK.js";
70
70
  import {
71
71
  addKnownIssue,
72
72
  getKnowledgePath,
@@ -91,8 +91,8 @@ import {
91
91
  resolveLaunchOpts,
92
92
  saveSessionDiskMeta,
93
93
  setActivePage
94
- } from "./chunk-RZM5CNIY.js";
95
- import "./chunk-RRBXV7KE.js";
94
+ } from "./chunk-XKQVUFUS.js";
95
+ import "./chunk-7OFM755Z.js";
96
96
  import "./chunk-TNEN6VQ2.js";
97
97
  import {
98
98
  errMsg
@@ -491,14 +491,23 @@ var CAPTCHA_SELECTORS = {
491
491
  generic: '#captcha, .captcha-container, [class*="captcha"]'
492
492
  };
493
493
  async function detectCaptcha(page) {
494
- for (const [type, selector] of Object.entries(CAPTCHA_SELECTORS)) {
495
- const el = await page.$(selector);
496
- if (el) {
497
- const iframeUrl = await el.getAttribute("src").catch(() => void 0);
498
- return { type, selector, iframeUrl: iframeUrl || void 0 };
499
- }
500
- }
501
- return null;
494
+ const hit = await page.evaluate(`
495
+ (function() {
496
+ var S = ${JSON.stringify(CAPTCHA_SELECTORS)};
497
+ for (var type in S) {
498
+ var sel = S[type];
499
+ var el;
500
+ try { el = document.querySelector(sel); } catch (e) { continue; }
501
+ if (el) {
502
+ var url = el.getAttribute && el.getAttribute('src');
503
+ return { type: type, selector: sel, iframeUrl: url || undefined };
504
+ }
505
+ }
506
+ return null;
507
+ })()
508
+ `).catch(() => null);
509
+ if (!hit) return null;
510
+ return { type: hit.type, selector: hit.selector, iframeUrl: hit.iframeUrl || void 0 };
502
511
  }
503
512
  async function waitForCaptchaSolved(page, captchaInfo, timeoutMs = 18e4) {
504
513
  const startTime = Date.now();
@@ -549,12 +558,24 @@ var clickCommand = registerCommand({
549
558
  let detectedNewPage;
550
559
  let cleanup;
551
560
  if (ctx.browserContext?.on) {
561
+ const targetIdOf = (p2) => p2._targetId;
562
+ let targetIdsBefore;
563
+ try {
564
+ targetIdsBefore = new Set(ctx.browserContext.pages().map(targetIdOf));
565
+ } catch {
566
+ targetIdsBefore = /* @__PURE__ */ new Set();
567
+ }
552
568
  const pagePromise = new Promise((resolve10) => {
553
569
  const timer = setTimeout(() => {
554
570
  ctx.browserContext.off("page", handler);
555
571
  resolve10(void 0);
556
572
  }, 3e3);
557
573
  const handler = (page2) => {
574
+ if (targetIdsBefore.has(targetIdOf(page2))) return;
575
+ try {
576
+ if ((page2.url?.() ?? "") === "about:blank") return;
577
+ } catch {
578
+ }
558
579
  clearTimeout(timer);
559
580
  ctx.browserContext.off("page", handler);
560
581
  resolve10(page2);
@@ -955,9 +976,11 @@ var mouseCommand = registerCommand({
955
976
  await ctx.page.mouse.move(p.x, p.y, { steps: p.steps || 1 });
956
977
  break;
957
978
  case "down":
979
+ await ctx.page.mouse.move(p.x, p.y);
958
980
  await ctx.page.mouse.down({ button });
959
981
  break;
960
982
  case "up":
983
+ await ctx.page.mouse.move(p.x, p.y);
961
984
  await ctx.page.mouse.up({ button });
962
985
  break;
963
986
  case "click":
@@ -976,10 +999,11 @@ import { z as z7 } from "zod";
976
999
  import { ok as ok7, normalizeTips as normalizeTips2 } from "@dyyz1993/xcli-core";
977
1000
  var evaluateCommand = registerCommand({
978
1001
  name: "eval",
979
- description: "Evaluate JavaScript expression in the browser",
1002
+ description: "Evaluate JavaScript expression in the browser (--frame <url-substring> \u53EF\u5B9A\u5411\u5230 iframe \u4E0A\u4E0B\u6587\uFF0C\u8DE8\u57DF iframe \u4EA6\u53EF)",
980
1003
  scope: "page",
981
1004
  parameters: z7.object({
982
- expression: z7.string()
1005
+ expression: z7.string(),
1006
+ frame: z7.string().optional().describe("\u5728 URL \u542B\u6B64\u5B50\u4E32\u7684 iframe \u91CC\u6267\u884C\uFF08\u540C\u8FDB\u7A0B\u7528 contextId\uFF0C\u8DE8\u57DF OOPIF \u7528 Target auto-attach\uFF09")
983
1007
  }),
984
1008
  handler: async (p, ctx) => {
985
1009
  const engine = createRuleEngine();
@@ -991,7 +1015,7 @@ var evaluateCommand = registerCommand({
991
1015
  direction: "client\u2192browser"
992
1016
  });
993
1017
  engine.stop();
994
- const result = await ctx.page.evaluate(p.expression);
1018
+ const result = p.frame ? await ctx.page.evaluateInFrame(p.frame, p.expression) : await ctx.page.evaluate(p.expression);
995
1019
  const response = ok7({ result });
996
1020
  if (decision && decision.severity === "danger") {
997
1021
  response.tips = normalizeTips2([`${decision.suggestion}`]);
@@ -5756,6 +5780,11 @@ async function handleSwitch(p, pages, ctx) {
5756
5780
  const targetPage = pages[p.index];
5757
5781
  await targetPage.bringToFront().catch(() => {
5758
5782
  });
5783
+ for (let i = 0; i < 5; i++) {
5784
+ const okProbe = await targetPage.evaluate("1").then(() => true, () => false);
5785
+ if (okProbe) break;
5786
+ await new Promise((r) => setTimeout(r, 300));
5787
+ }
5759
5788
  const session = ctx.sessionId ? getSessionById(ctx.sessionId) : void 0;
5760
5789
  if (session) {
5761
5790
  setActivePage(session, targetPage);
@@ -6210,7 +6239,7 @@ function attachWaitForHuman(ctx, getOrCreateWSServer) {
6210
6239
  }
6211
6240
  function attachDetectAntiBot(ctx) {
6212
6241
  ctx.detectAntiBot = async (page, config) => {
6213
- const { detectAntiBot: detectAntiBot2 } = await import("./anti-bot-DR56Y63V.js");
6242
+ const { detectAntiBot: detectAntiBot2 } = await import("./anti-bot-GTTYNEFB.js");
6214
6243
  return detectAntiBot2(page, config);
6215
6244
  };
6216
6245
  }
@@ -7399,7 +7428,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
7399
7428
  }
7400
7429
  let targetPageOverride = null;
7401
7430
  if (_target && extraOpts?.cdpEndpoint) {
7402
- const { findTargetPage } = await import("./browser-YCO4GJMZ.js");
7431
+ const { findTargetPage } = await import("./browser-V3JIWSTR.js");
7403
7432
  targetPageOverride = await findTargetPage(extraOpts.cdpEndpoint, _target);
7404
7433
  if (!targetPageOverride) {
7405
7434
  return errorResult(`Target "${_target}" not found. Use 'xbrowser targets --cdp ${extraOpts.cdpEndpoint}' to list available pages.`);
@@ -7416,7 +7445,7 @@ async function executeCommand(commandName, params, sessionName = "default", extr
7416
7445
  params = result.data;
7417
7446
  }
7418
7447
  if (command.scope !== "cli" && !process.env.XBROWSER_DAEMON_WORKER) {
7419
- const { forwardExec } = await import("./daemon-client-O6BYVRXV.js");
7448
+ const { forwardExec } = await import("./daemon-client-Y2YSZCGK.js");
7420
7449
  const result = await forwardExec(commandName, params, sessionName, extraOpts?.cdpEndpoint);
7421
7450
  if (result) return result;
7422
7451
  }
@@ -10065,10 +10094,12 @@ var HTML_TAGS = /* @__PURE__ */ new Set([
10065
10094
  "map",
10066
10095
  "area"
10067
10096
  ]);
10097
+ var SELECTOR_DSL_PREFIXES = /^(text=|popup-text=|xpath=|role=|css=|chain=)/;
10068
10098
  function normalizeSelector(input) {
10069
10099
  if (!input) return input;
10070
10100
  if (/^[#\.\[\:\/]/.test(input)) return input;
10071
10101
  if (input === "*") return input;
10102
+ if (SELECTOR_DSL_PREFIXES.test(input)) return input;
10072
10103
  if (/[>\s+,~]/.test(input)) return input;
10073
10104
  const tagName = input.match(/^[a-zA-Z][a-zA-Z0-9]*/)?.[0];
10074
10105
  if (tagName && HTML_TAGS.has(tagName.toLowerCase())) return input;
@@ -10243,11 +10274,23 @@ async function handleBrowserCommand(command, args, options, sessionName, mode, c
10243
10274
  output: options.output || options.o || args[0]
10244
10275
  };
10245
10276
  break;
10246
- case "eval":
10277
+ case "eval": {
10247
10278
  cmdName = "eval";
10248
- params = { expression: args.join(" ") || options.expression || "" };
10249
- if (!params.expression) outputError("Usage: xbrowser eval <expression>");
10279
+ const frameVal = options.frame || void 0;
10280
+ const exprParts = [];
10281
+ for (let i = 0; i < args.length; i++) {
10282
+ if (args[i] === "--frame") {
10283
+ i++;
10284
+ continue;
10285
+ }
10286
+ if (args[i].startsWith("--frame=")) continue;
10287
+ exprParts.push(args[i]);
10288
+ }
10289
+ params = { expression: exprParts.join(" ") || options.expression || "" };
10290
+ if (frameVal) params.frame = frameVal;
10291
+ if (!params.expression) outputError("Usage: xbrowser eval <expression> [--frame <url-substring>]");
10250
10292
  break;
10293
+ }
10251
10294
  case "scroll": {
10252
10295
  let direction = "down";
10253
10296
  let distance;
@@ -10950,7 +10993,7 @@ async function handlePlugin(args, options, mode) {
10950
10993
  } catch {
10951
10994
  }
10952
10995
  try {
10953
- const { daemonPing } = await import("./daemon-client-O6BYVRXV.js");
10996
+ const { daemonPing } = await import("./daemon-client-Y2YSZCGK.js");
10954
10997
  if (await daemonPing()) {
10955
10998
  await fetch("http://localhost:9224/rpc", {
10956
10999
  method: "POST",
@@ -11516,7 +11559,7 @@ async function handleFilter(args, _mode, options) {
11516
11559
  }
11517
11560
  }
11518
11561
  async function handleGeneratePlugin(sessionName, pluginName, outputDir) {
11519
- const { SessionRecorder: SessionRecorder2 } = await import("./session-recorder-QRZMKFVL.js");
11562
+ const { SessionRecorder: SessionRecorder2 } = await import("./session-recorder-SLDBENVF.js");
11520
11563
  const { readSiteKnowledge: readSiteKnowledge2, toMarkdown } = await import("./site-knowledge-SYC6VCDB.js");
11521
11564
  const { mkdirSync: mkdirSync11, writeFileSync: writeFileSync13 } = await import("fs");
11522
11565
  const { join: join14 } = await import("path");
@@ -12822,6 +12865,9 @@ var HTTPServer = class {
12822
12865
  this.server = null;
12823
12866
  reject(err);
12824
12867
  });
12868
+ server.headersTimeout = 20 * 6e4;
12869
+ server.requestTimeout = 20 * 6e4;
12870
+ server.keepAliveTimeout = 20 * 6e4;
12825
12871
  server.listen(this.port, this.host, () => {
12826
12872
  const addr = server.address();
12827
12873
  if (addr && typeof addr === "object") {
@@ -13084,12 +13130,16 @@ async function routeCommand(argvIn, stdinCommands) {
13084
13130
  }
13085
13131
  const globalFlags = /* @__PURE__ */ new Set(["--session", "--cdp", "--json", "--yaml", "--output", "--timeout", "--help", "-h", "--version"]);
13086
13132
  let chainArgIdx = -1;
13133
+ let seenCommandWord = false;
13087
13134
  for (let i = 0; i < argv.length; i++) {
13088
13135
  if (globalFlags.has(argv[i])) continue;
13089
13136
  if (globalFlags.has(argv[i]) || i > 0 && globalFlags.has(argv[i - 1]) && !argv[i].startsWith("-")) continue;
13090
- if (argv[i].includes(" ") && /^[a-zA-Z]/.test(argv[i])) {
13091
- chainArgIdx = i;
13092
- break;
13137
+ if (!seenCommandWord) {
13138
+ if (argv[i].includes(" ") && /^[a-zA-Z]/.test(argv[i])) {
13139
+ chainArgIdx = i;
13140
+ break;
13141
+ }
13142
+ seenCommandWord = true;
13093
13143
  }
13094
13144
  }
13095
13145
  if (chainArgIdx >= 0) {
@@ -13453,7 +13503,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
13453
13503
  }
13454
13504
  const needsBrowser = cmdEntry.scope === "page" || cmdEntry.scope === "browser";
13455
13505
  if (needsBrowser && !process.env.XBROWSER_DAEMON_WORKER) {
13456
- const { forwardPluginExec } = await import("./daemon-client-O6BYVRXV.js");
13506
+ const { forwardPluginExec } = await import("./daemon-client-Y2YSZCGK.js");
13457
13507
  const userTimeout = typeof params.timeout === "number" && params.timeout > 0 ? params.timeout * 1e3 + 3e4 : void 0;
13458
13508
  const result = await forwardPluginExec(`${command}.${subCommand}`, params, sessionName, cdpEndpoint, userTimeout);
13459
13509
  const resultData = result && typeof result === "object" && "data" in result ? result.data : void 0;
@@ -13493,7 +13543,7 @@ Run "xbrowser ${command} ${subCommand} --help" to see available parameters.`
13493
13543
  const targetPage = pages[cmdTabIndex];
13494
13544
  await targetPage.bringToFront().catch(() => {
13495
13545
  });
13496
- const { setActivePage: setActivePage2 } = await import("./browser-YCO4GJMZ.js");
13546
+ const { setActivePage: setActivePage2 } = await import("./browser-V3JIWSTR.js");
13497
13547
  setActivePage2(session, targetPage);
13498
13548
  }
13499
13549
  }
@@ -14096,7 +14146,19 @@ var PlaybackEngine = class _PlaybackEngine {
14096
14146
  import { EventEmitter as EventEmitter2 } from "events";
14097
14147
 
14098
14148
  // src/stream/frame-processor.ts
14099
- import sharp from "sharp";
14149
+ var _sharp = null;
14150
+ async function loadSharp() {
14151
+ if (!_sharp) {
14152
+ try {
14153
+ _sharp = (await import("sharp")).default;
14154
+ } catch (err) {
14155
+ throw new Error(
14156
+ "\u76F4\u64AD\u6D41\u5E27\u5904\u7406\u9700\u8981 sharp \u539F\u751F\u6A21\u5757\uFF0C\u5F53\u524D\u73AF\u5883\u52A0\u8F7D\u5931\u8D25\uFF1A" + (err instanceof Error ? err.message : String(err)) + "\u3002\u4E0D\u7528\u76F4\u64AD\u6D41\u529F\u80FD\u53EF\u5FFD\u7565\uFF1B\u9700\u8981\u76F4\u64AD\u8BF7\u5728\u517C\u5BB9\u73AF\u5883\u8FD0\u884C\u6216\u5B89\u88C5\u5BF9\u5E94\u5E73\u53F0\u4F9D\u8D56\u3002"
14157
+ );
14158
+ }
14159
+ }
14160
+ return _sharp;
14161
+ }
14100
14162
  var STATE_CONFIGS = {
14101
14163
  // Full-screen: aggressive compression for speed
14102
14164
  // user_interacting: low quality + downscale — prioritize FPS over clarity
@@ -14248,6 +14310,7 @@ var FrameProcessor = class {
14248
14310
  if (!needsResize && !needsCrop && !needsReencode) {
14249
14311
  return buffer;
14250
14312
  }
14313
+ const sharp = await loadSharp();
14251
14314
  let processed = sharp(buffer);
14252
14315
  if (cropConfig) {
14253
14316
  const meta = await processed.metadata();
@@ -16653,7 +16716,7 @@ var DataCollector = class {
16653
16716
  return results;
16654
16717
  }
16655
16718
  async createBrowserContext() {
16656
- const { launch } = await import("./cdp-driver-J3YQ4LJV.js");
16719
+ const { launch } = await import("./cdp-driver-GD6YBBGE.js");
16657
16720
  const { browser } = await launch({
16658
16721
  headless: true,
16659
16722
  args: ["--no-sandbox", "--disable-setuid-sandbox"]
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  SessionRecorder
3
- } from "./chunk-5UGR6MUK.js";
3
+ } from "./chunk-HBHOKZPN.js";
4
4
  import "./chunk-KFQGP6VL.js";
5
5
  export {
6
6
  SessionRecorder
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  SessionRecorder
3
- } from "./chunk-4452SPFI.js";
3
+ } from "./chunk-NODRQGOK.js";
4
4
  import "./chunk-OZKD3W4X.js";
5
5
  import "./chunk-KFQGP6VL.js";
6
6
  export {
@@ -1,3 +1,6 @@
1
+ import {
2
+ queryJS
3
+ } from "./chunk-3FWLW7FS.js";
1
4
  import "./chunk-KFQGP6VL.js";
2
5
 
3
6
  // src/recorder/session-replayer.ts
@@ -31,7 +34,7 @@ var SessionReplayer = class {
31
34
  if (this.opts.page) {
32
35
  this.page = this.opts.page;
33
36
  } else if (this.opts.cdpUrl) {
34
- const { launch } = await import("./cdp-driver-J3YQ4LJV.js");
37
+ const { launch } = await import("./cdp-driver-VEK6BNN6.js");
35
38
  const { browser } = await launch({ cdpEndpoint: this.opts.cdpUrl });
36
39
  let contexts = browser.contexts();
37
40
  for (let i = 0; i < 10 && contexts.length === 0; i++) {
@@ -44,13 +47,14 @@ var SessionReplayer = class {
44
47
  this.page = pages[0];
45
48
  }
46
49
  if (!this.page) throw new Error("No page available. Provide cdpUrl or page.");
47
- const actions = this.recording.actions;
50
+ const actions = this.dedupAdjacentActions(this.recording.actions);
48
51
  let success = 0;
49
52
  let failed = 0;
50
53
  let skipped = 0;
51
54
  for (let i = 0; i < actions.length; i++) {
52
55
  const action = actions[i];
53
56
  this.opts.onStep?.(action, i, actions.length);
57
+ const pagesBefore = this.listContextPages();
54
58
  try {
55
59
  if (action.trajectory) {
56
60
  await this.replayTrajectory(action.trajectory);
@@ -63,6 +67,15 @@ var SessionReplayer = class {
63
67
  }
64
68
  }
65
69
  success++;
70
+ if (action.type === "click" || action.type === "cdp-click") {
71
+ const pagesAfter = this.listContextPages();
72
+ if (pagesAfter && pagesBefore && pagesAfter.length > pagesBefore.length) {
73
+ const newest = pagesAfter[pagesAfter.length - 1];
74
+ await newest.bringToFront().catch(() => {
75
+ });
76
+ this.page = newest;
77
+ }
78
+ }
66
79
  } catch (e) {
67
80
  const err = e instanceof Error ? e : new Error(String(e));
68
81
  this.opts.onError?.(action, err, i);
@@ -75,6 +88,47 @@ var SessionReplayer = class {
75
88
  return { success, failed, skipped };
76
89
  }
77
90
  /** Replay a single action */
91
+ /** Pages of the replayer page's context (best-effort; null if unavailable). */
92
+ listContextPages() {
93
+ try {
94
+ const ctx = this.page.context?.();
95
+ const pages = ctx?.pages?.();
96
+ return Array.isArray(pages) ? pages : null;
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+ /**
102
+ * Replay-time adjacent dedup (rec-duel d02/d03/d05).
103
+ *
104
+ * The recorder can emit both the real action signal AND the injected cdp
105
+ * command action for a single interaction when the signal flush lags behind
106
+ * the recorder-side dedup window (heavy snapshot capture slows polling).
107
+ * Replaying both executes the interaction twice. Filter here: an action is
108
+ * skipped when a nearby (≤15s apart) earlier action matches on
109
+ * type-normalized key (selector + text) AND coordinates agree — either side
110
+ * lacks coords (cdp actions carry none) or both are within 30px. Two real
111
+ * clicks on the same element at different spots (canvas buttons) survive.
112
+ */
113
+ dedupAdjacentActions(actions) {
114
+ const normType = (t) => t === "cdp-click" ? "click" : t === "cdp-fill" ? "input" : t;
115
+ const generic = (a) => (a.type === "click" || a.type === "dblclick" || a.type === "contextmenu") && (a.element?.selector === "html" || a.element?.selector === "body");
116
+ const keyOf = (a) => `${normType(a.type)}|${a.element?.selector || ""}|${a.element?.text || ""}`;
117
+ const coordsOf = (a) => typeof a.x === "number" && typeof a.y === "number" ? { x: a.x, y: a.y } : null;
118
+ const near = (p, q) => Math.abs(p.x - q.x) <= 30 && Math.abs(p.y - q.y) <= 30;
119
+ const lastKept = /* @__PURE__ */ new Map();
120
+ return actions.filter((a) => {
121
+ if (generic(a)) return false;
122
+ const key = keyOf(a);
123
+ const c = coordsOf(a);
124
+ const recent = lastKept.get(key) || [];
125
+ const dup = recent.some((e) => Math.abs(a.timestamp - e.ts) <= 15e3 && (!c || !e.c || near(c, e.c)));
126
+ if (dup) return false;
127
+ recent.push({ ts: a.timestamp, c });
128
+ lastKept.set(key, recent);
129
+ return true;
130
+ });
131
+ }
78
132
  async replayAction(action) {
79
133
  const page = this.page;
80
134
  const timeout = this.opts.stepTimeout;
@@ -93,9 +147,43 @@ var SessionReplayer = class {
93
147
  case "click":
94
148
  case "cdp-click": {
95
149
  const selector = await this.resolveAndWait(action);
150
+ if (typeof action.x === "number" && typeof action.y === "number") {
151
+ const hit = await page.evaluate(`
152
+ (function() {
153
+ const el = ${queryJS(selector)};
154
+ if (!el) return false;
155
+ const r = el.getBoundingClientRect();
156
+ return ${action.x} >= r.x - 2 && ${action.x} <= r.x + r.width + 2
157
+ && ${action.y} >= r.y - 2 && ${action.y} <= r.y + r.height + 2;
158
+ })()
159
+ `).catch(() => false);
160
+ if (hit) {
161
+ await page.mouse.click(action.x, action.y, { stealth: true });
162
+ break;
163
+ }
164
+ }
96
165
  await page.click(selector, { timeout });
97
166
  break;
98
167
  }
168
+ case "scroll": {
169
+ const [dir, distStr] = (action.value || "down:300").split(":");
170
+ const dist = Number(distStr) || 300;
171
+ const sign = dir === "up" ? -1 : dir === "left" ? 0 : 1;
172
+ const selector = await this.resolveAndWait(action).catch(() => void 0);
173
+ if (selector) {
174
+ await page.evaluate(`
175
+ (function() {
176
+ const el = ${queryJS(selector)};
177
+ if (el) el.scrollTop += ${sign * dist};
178
+ })()
179
+ `).catch(() => {
180
+ });
181
+ } else {
182
+ await page.evaluate(`window.scrollBy(0, ${sign * dist})`).catch(() => {
183
+ });
184
+ }
185
+ break;
186
+ }
99
187
  case "input": {
100
188
  const selector = await this.resolveAndWait(action);
101
189
  await page.fill(selector, action.value ?? "", { timeout });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xbrowser/cli",
3
- "version": "1.9.9",
3
+ "version": "1.11.0",
4
4
  "description": "Browser automation CLI for web scraping, headless browsing, SEO analysis, and AI agent workflows. A command-line alternative to Playwright, Puppeteer, and Selenium.",
5
5
  "type": "module",
6
6
  "bin": {