@xbrowser/cli 1.9.8 → 1.9.9

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 (39) hide show
  1. package/dist/{anti-bot-MCVM6IFB.js → anti-bot-DR56Y63V.js} +1 -1
  2. package/dist/{browser-GT4BMX6X.js → browser-FFYPFTBV.js} +1 -1
  3. package/dist/{browser-43YXUIML.js → browser-RHWUAMTB.js} +2 -2
  4. package/dist/{browser-SJWPJFN7.js → browser-YCO4GJMZ.js} +2 -2
  5. package/dist/{cdp-driver-WAFYSIIW.js → cdp-driver-2T4P4ZE2.js} +87 -1
  6. package/dist/{cdp-driver-PUZLDSQU.js → cdp-driver-J3YQ4LJV.js} +1 -1
  7. package/dist/chunk-3OD76SUE.js +20 -0
  8. package/dist/chunk-43TEMG4U.js +9 -0
  9. package/dist/{chunk-C3LIGO4V.js → chunk-4452SPFI.js} +906 -16
  10. package/dist/{chunk-VEWTHYDG.js → chunk-5UGR6MUK.js} +906 -16
  11. package/dist/{chunk-ENCEMM7Z.js → chunk-6OZWKXSW.js} +15 -16
  12. package/dist/{chunk-7ZDWM6T2.js → chunk-BNO7OKO4.js} +5 -6
  13. package/dist/{chunk-24SY6PE5.js → chunk-CG3D3CHY.js} +1 -8
  14. package/dist/{chunk-ZJHQPMCY.js → chunk-DWDXEGVK.js} +5 -6
  15. package/dist/{chunk-A4YHJW3Z.js → chunk-GUTBIYFW.js} +101 -16
  16. package/dist/{chunk-DPJNCMLC.js → chunk-INTQPBYF.js} +7 -0
  17. package/dist/{human-interaction-OU7LZ6OZ.js → chunk-MWFHZUIY.js} +6 -7
  18. package/dist/chunk-Q2DFJQGS.js +128 -0
  19. package/dist/{chunk-GM3HWQ5U.js → chunk-RRBXV7KE.js} +87 -1
  20. package/dist/{chunk-PBDID7SE.js → chunk-RZM5CNIY.js} +15 -16
  21. package/dist/{human-interaction-HJTXFVQS.js → chunk-SQHSZENE.js} +2 -2
  22. package/dist/chunk-TZPKFUBT.js +20 -0
  23. package/dist/{chunk-YAUG3NSI.js → chunk-YMUSHPU4.js} +16 -128
  24. package/dist/cli.js +41 -28
  25. package/dist/{daemon-client-4HJENICO.js → daemon-client-GKEPT4NY.js} +15 -3
  26. package/dist/{daemon-client-CMRAWXTN.js → daemon-client-O6BYVRXV.js} +4 -1
  27. package/dist/daemon-main.js +635 -57
  28. package/dist/human-interaction-5AO42MBA.js +8 -0
  29. package/dist/{human-interaction-2DZK4MW7.js → human-interaction-C5OEGXGO.js} +1 -1
  30. package/dist/human-interaction-ISXZTAYY.js +8 -0
  31. package/dist/index.d.ts +167 -2
  32. package/dist/index.js +173 -63
  33. package/dist/recovery-EF33LKRJ.js +77 -0
  34. package/dist/recovery-J2ISVGUL.js +77 -0
  35. package/dist/recovery-ZJVVHP7N.js +76 -0
  36. package/dist/{session-recorder-K3K63Q33.js → session-recorder-H3KEYU26.js} +1 -1
  37. package/dist/{session-recorder-AG6CH6EI.js → session-recorder-QRZMKFVL.js} +1 -1
  38. package/dist/{session-replayer-V4MP6M6I.js → session-replayer-LJUC4TI7.js} +24 -7
  39. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  launch
3
- } from "./chunk-GM3HWQ5U.js";
3
+ } from "./chunk-RRBXV7KE.js";
4
4
  import {
5
5
  errMsg
6
6
  } from "./chunk-GDKLH7ZY.js";
@@ -1499,7 +1499,7 @@ process.on("exit", () => {
1499
1499
  if (session.isCDP) {
1500
1500
  logSessionEvent("process_exit", `Session "${session.name}": CDP connection (not closing external browser).`);
1501
1501
  } else {
1502
- logSessionEvent("process_exit", `Session "${session.name}": Closing browser.`);
1502
+ logSessionEvent("process_exit", `Session "${session.name}": Closing self-launched browser.`);
1503
1503
  try {
1504
1504
  session.browser?.close();
1505
1505
  } catch {
@@ -1507,7 +1507,7 @@ process.on("exit", () => {
1507
1507
  }
1508
1508
  }
1509
1509
  if (_sharedBrowser) {
1510
- logSessionEvent("process_exit", "Closing shared browser.");
1510
+ logSessionEvent("process_exit", "Closing shared browser (self-launched only, external CDP is safe).");
1511
1511
  try {
1512
1512
  _sharedBrowser.close();
1513
1513
  } catch {
@@ -1993,10 +1993,6 @@ async function closeSessionByName(name) {
1993
1993
  if (session.name === name || session.id === name) {
1994
1994
  logSessionEvent("close_session", `name="${session.name}" id="${session.id}" url="${session.page.url()}"`);
1995
1995
  if (session.isCDP) {
1996
- try {
1997
- await session.page.close();
1998
- } catch {
1999
- }
2000
1996
  if (session.browser) {
2001
1997
  await session.browser.close().catch(() => {
2002
1998
  });
@@ -2021,7 +2017,7 @@ async function closeSessionByName(name) {
2021
2017
  } catch {
2022
2018
  }
2023
2019
  try {
2024
- const { SessionRecorder } = await import("./session-recorder-K3K63Q33.js");
2020
+ const { SessionRecorder } = await import("./session-recorder-H3KEYU26.js");
2025
2021
  SessionRecorder.cleanup(session.name);
2026
2022
  } catch {
2027
2023
  }
@@ -2040,12 +2036,17 @@ async function closeAllSessions() {
2040
2036
  if (names) logSessionEvent("close_all_sessions", `Closing ${sessions.size} sessions: ${names}`);
2041
2037
  for (const session of sessions.list()) {
2042
2038
  try {
2043
- if (!session.isCDP) {
2039
+ if (session.isCDP) {
2040
+ if (session.browser) {
2041
+ await session.browser.close().catch(() => {
2042
+ });
2043
+ }
2044
+ } else {
2044
2045
  await session.context.close();
2045
- }
2046
- if (session.browser) {
2047
- await session.browser.close().catch(() => {
2048
- });
2046
+ if (session.browser) {
2047
+ await session.browser.close().catch(() => {
2048
+ });
2049
+ }
2049
2050
  }
2050
2051
  sessions.removeById(session.id);
2051
2052
  } catch {
@@ -2054,7 +2055,7 @@ async function closeAllSessions() {
2054
2055
  }
2055
2056
  }
2056
2057
  async function destroyBrowser() {
2057
- logSessionEvent("destroy_browser", `Sessions count: ${sessions.size}. Clearing idle timer and closing all browsers.`);
2058
+ logSessionEvent("destroy_browser", `Sessions count: ${sessions.size}. Clearing idle timer and closing all sessions.`);
2058
2059
  if (idleTimer) {
2059
2060
  clearTimeout(idleTimer);
2060
2061
  idleTimer = null;
@@ -2090,8 +2091,6 @@ async function ensureProcessCanExit() {
2090
2091
  for (const session of sessions.list()) {
2091
2092
  if (session.browser) {
2092
2093
  if (session.isCDP) {
2093
- await session.browser.close().catch(() => {
2094
- });
2095
2094
  } else {
2096
2095
  await session.browser.close().catch(() => {
2097
2096
  });
@@ -36,16 +36,15 @@ var ScreencastCapturer = class {
36
36
  const cdp = await page.context().newCDPSession(page);
37
37
  this.cdpSession = cdp;
38
38
  cdp.on("Page.screencastFrame", async (params) => {
39
- if (!this.frameCallback) return;
40
- try {
41
- await cdp.send("Page.screencastFrameAck", { sessionId: params.sessionId });
42
- } catch {
43
- }
39
+ const cb = this.frameCallback;
40
+ if (!cb) return;
41
+ cdp.send("Page.screencastFrameAck", { sessionId: params.sessionId }).catch(() => {
42
+ });
44
43
  const viewport = {
45
44
  width: params.metadata?.deviceWidth || this.maxWidth,
46
45
  height: params.metadata?.deviceHeight || this.maxHeight
47
46
  };
48
- this.frameCallback({
47
+ cb({
49
48
  id: crypto.randomUUID(),
50
49
  sessionId: this.sessionId,
51
50
  timestamp: Date.now(),
@@ -116,16 +116,9 @@ function getDaemonProcessStatus() {
116
116
  };
117
117
  }
118
118
 
119
- // src/version.ts
120
- import { createRequire } from "module";
121
- var require2 = createRequire(import.meta.url);
122
- var pkg = require2("../package.json");
123
- var version = pkg.version;
124
-
125
119
  export {
126
120
  getDaemonConfig,
127
121
  startDaemonProcess,
128
122
  stopDaemonProcess,
129
- getDaemonProcessStatus,
130
- version
123
+ getDaemonProcessStatus
131
124
  };
@@ -39,16 +39,15 @@ var ScreencastCapturer = class {
39
39
  const cdp = await page.context().newCDPSession(page);
40
40
  this.cdpSession = cdp;
41
41
  cdp.on("Page.screencastFrame", async (params) => {
42
- if (!this.frameCallback) return;
43
- try {
44
- await cdp.send("Page.screencastFrameAck", { sessionId: params.sessionId });
45
- } catch {
46
- }
42
+ const cb = this.frameCallback;
43
+ if (!cb) return;
44
+ cdp.send("Page.screencastFrameAck", { sessionId: params.sessionId }).catch(() => {
45
+ });
47
46
  const viewport = {
48
47
  width: params.metadata?.deviceWidth || this.maxWidth,
49
48
  height: params.metadata?.deviceHeight || this.maxHeight
50
49
  };
51
- this.frameCallback({
50
+ cb({
52
51
  id: crypto.randomUUID(),
53
52
  sessionId: this.sessionId,
54
53
  timestamp: Date.now(),
@@ -294,6 +294,36 @@ function queryJS(selector) {
294
294
  const xpath = JSON.stringify(selector.slice(6));
295
295
  return `document.evaluate(${xpath}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue`;
296
296
  }
297
+ if (selector.startsWith("text=")) {
298
+ const raw = selector.slice(5);
299
+ const exact = raw.startsWith('"') && raw.endsWith('"');
300
+ const text = exact ? raw.slice(1, -1) : raw;
301
+ return `(() => {
302
+ const target = ${JSON.stringify(text)};
303
+ const exact = ${exact};
304
+ const els = [...document.querySelectorAll('*')].filter(e => {
305
+ if (e.children.length > 0) return false;
306
+ if (e.offsetParent === null) return false;
307
+ const t = (e.textContent || '').trim();
308
+ if (!t) return false;
309
+ return exact ? t === target : t.toLowerCase().includes(target.toLowerCase());
310
+ });
311
+ return els[0] || null;
312
+ })()`;
313
+ }
314
+ if (selector.startsWith("popup-text=")) {
315
+ const text = selector.slice("popup-text=".length);
316
+ return `(() => {
317
+ const target = ${JSON.stringify(text)};
318
+ const els = [...document.querySelectorAll('*')].filter(e => {
319
+ if (e.children.length > 0) return false;
320
+ if (e.offsetParent === null) return false;
321
+ if ((e.textContent || '').trim() !== target) return false;
322
+ return true;
323
+ });
324
+ return els[0] || null;
325
+ })()`;
326
+ }
297
327
  return `document.querySelector(${JSON.stringify(selector)})`;
298
328
  }
299
329
  function queryAllJS(selector) {
@@ -301,6 +331,9 @@ function queryAllJS(selector) {
301
331
  const xpath = JSON.stringify(selector.slice(6));
302
332
  return `(() => { const it = document.evaluate(${xpath}, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); const r=[]; for(let i=0;i<it.snapshotLength;i++) r.push(it.snapshotItem(i)); return r; })()`;
303
333
  }
334
+ if (selector.startsWith("text=") || selector.startsWith("popup-text=")) {
335
+ return `(() => { const el = ${queryJS(selector)}; return el ? [el] : []; })()`;
336
+ }
304
337
  return `document.querySelectorAll(${JSON.stringify(selector)})`;
305
338
  }
306
339
 
@@ -2763,11 +2796,52 @@ var CDPConnection = class extends EventEmitter4 {
2763
2796
  this.setMaxListeners(0);
2764
2797
  this.defaultSessionId = sessionId;
2765
2798
  if (typeof wsOrUrl === "string") {
2766
- this.ws = new WebSocket(wsOrUrl);
2799
+ const wsOptions = /^wss:\/\/\d+\.\d+\.\d+\.\d+/.test(wsOrUrl) ? { rejectUnauthorized: false } : void 0;
2800
+ this.ws = new WebSocket(wsOrUrl, wsOptions);
2767
2801
  } else {
2768
2802
  this.ws = wsOrUrl;
2769
2803
  }
2770
2804
  this.bindWebSocket();
2805
+ this.startKeepalive();
2806
+ }
2807
+ /** Send periodic WS pings to prevent idle-timeout disconnects (e.g. CF's 100s).
2808
+ * Also detects dead connections: if a pong isn't received within 10s of a
2809
+ * ping, the connection is considered dead and forcibly closed. */
2810
+ keepaliveTimer = null;
2811
+ pongTimer = null;
2812
+ startKeepalive() {
2813
+ this.ws.on("pong", () => {
2814
+ if (this.pongTimer) {
2815
+ clearTimeout(this.pongTimer);
2816
+ this.pongTimer = null;
2817
+ }
2818
+ });
2819
+ this.keepaliveTimer = setInterval(() => {
2820
+ if (this.ws.readyState === WebSocket.OPEN) {
2821
+ if (!this.pongTimer) {
2822
+ this.pongTimer = setTimeout(() => {
2823
+ if (!this.closed) {
2824
+ this.closed = true;
2825
+ this.closeReason = "keepalive timeout (no pong in 10s)";
2826
+ try {
2827
+ this.ws.terminate();
2828
+ } catch {
2829
+ }
2830
+ for (const [, pending] of this.pending) {
2831
+ clearTimeout(pending.timeout);
2832
+ pending.reject(new Error("Connection dead: keepalive timeout"));
2833
+ }
2834
+ this.pending.clear();
2835
+ this.emit("disconnect");
2836
+ }
2837
+ }, 1e4);
2838
+ }
2839
+ this.ws.ping?.();
2840
+ } else if (this.closed) {
2841
+ if (this.keepaliveTimer) clearInterval(this.keepaliveTimer);
2842
+ this.keepaliveTimer = null;
2843
+ }
2844
+ }, 3e4);
2771
2845
  }
2772
2846
  /** Wait for the connection to be fully open */
2773
2847
  async ready() {
@@ -2876,6 +2950,14 @@ var CDPConnection = class extends EventEmitter4 {
2876
2950
  if (this.closed) return;
2877
2951
  this.closed = true;
2878
2952
  this.closeReason = "closed by caller";
2953
+ if (this.keepaliveTimer) {
2954
+ clearInterval(this.keepaliveTimer);
2955
+ this.keepaliveTimer = null;
2956
+ }
2957
+ if (this.pongTimer) {
2958
+ clearTimeout(this.pongTimer);
2959
+ this.pongTimer = null;
2960
+ }
2879
2961
  for (const [id, pending] of this.pending) {
2880
2962
  clearTimeout(pending.timeout);
2881
2963
  pending.reject(new Error(`Connection closed: ${pending.method}`));
@@ -2917,6 +2999,10 @@ var CDPConnection = class extends EventEmitter4 {
2917
2999
  if (this.closed) return;
2918
3000
  this.closed = true;
2919
3001
  this.closeReason = `WebSocket closed: ${code} ${reason?.toString() ?? ""}`.trim();
3002
+ if (this.keepaliveTimer) {
3003
+ clearInterval(this.keepaliveTimer);
3004
+ this.keepaliveTimer = null;
3005
+ }
2920
3006
  for (const [id, pending] of this.pending) {
2921
3007
  clearTimeout(pending.timeout);
2922
3008
  pending.reject(new Error(`Connection closed: ${pending.method}`));
@@ -4458,7 +4544,7 @@ process.on("exit", () => {
4458
4544
  if (session.isCDP) {
4459
4545
  logSessionEvent("process_exit", `Session "${session.name}": CDP connection (not closing external browser).`);
4460
4546
  } else {
4461
- logSessionEvent("process_exit", `Session "${session.name}": Closing browser.`);
4547
+ logSessionEvent("process_exit", `Session "${session.name}": Closing self-launched browser.`);
4462
4548
  try {
4463
4549
  session.browser?.close();
4464
4550
  } catch {
@@ -4466,7 +4552,7 @@ process.on("exit", () => {
4466
4552
  }
4467
4553
  }
4468
4554
  if (_sharedBrowser) {
4469
- logSessionEvent("process_exit", "Closing shared browser.");
4555
+ logSessionEvent("process_exit", "Closing shared browser (self-launched only, external CDP is safe).");
4470
4556
  try {
4471
4557
  _sharedBrowser.close();
4472
4558
  } catch {
@@ -4952,10 +5038,6 @@ async function closeSessionByName(name) {
4952
5038
  if (session.name === name || session.id === name) {
4953
5039
  logSessionEvent("close_session", `name="${session.name}" id="${session.id}" url="${session.page.url()}"`);
4954
5040
  if (session.isCDP) {
4955
- try {
4956
- await session.page.close();
4957
- } catch {
4958
- }
4959
5041
  if (session.browser) {
4960
5042
  await session.browser.close().catch(() => {
4961
5043
  });
@@ -4980,7 +5062,7 @@ async function closeSessionByName(name) {
4980
5062
  } catch {
4981
5063
  }
4982
5064
  try {
4983
- const { SessionRecorder } = await import("./session-recorder-AG6CH6EI.js");
5065
+ const { SessionRecorder } = await import("./session-recorder-QRZMKFVL.js");
4984
5066
  SessionRecorder.cleanup(session.name);
4985
5067
  } catch {
4986
5068
  }
@@ -4999,12 +5081,17 @@ async function closeAllSessions() {
4999
5081
  if (names) logSessionEvent("close_all_sessions", `Closing ${sessions.size} sessions: ${names}`);
5000
5082
  for (const session of sessions.list()) {
5001
5083
  try {
5002
- if (!session.isCDP) {
5084
+ if (session.isCDP) {
5085
+ if (session.browser) {
5086
+ await session.browser.close().catch(() => {
5087
+ });
5088
+ }
5089
+ } else {
5003
5090
  await session.context.close();
5004
- }
5005
- if (session.browser) {
5006
- await session.browser.close().catch(() => {
5007
- });
5091
+ if (session.browser) {
5092
+ await session.browser.close().catch(() => {
5093
+ });
5094
+ }
5008
5095
  }
5009
5096
  sessions.removeById(session.id);
5010
5097
  } catch {
@@ -5013,7 +5100,7 @@ async function closeAllSessions() {
5013
5100
  }
5014
5101
  }
5015
5102
  async function destroyBrowser() {
5016
- logSessionEvent("destroy_browser", `Sessions count: ${sessions.size}. Clearing idle timer and closing all browsers.`);
5103
+ logSessionEvent("destroy_browser", `Sessions count: ${sessions.size}. Clearing idle timer and closing all sessions.`);
5017
5104
  if (idleTimer) {
5018
5105
  clearTimeout(idleTimer);
5019
5106
  idleTimer = null;
@@ -5049,8 +5136,6 @@ async function ensureProcessCanExit() {
5049
5136
  for (const session of sessions.list()) {
5050
5137
  if (session.browser) {
5051
5138
  if (session.isCDP) {
5052
- await session.browser.close().catch(() => {
5053
- });
5054
5139
  } else {
5055
5140
  await session.browser.close().catch(() => {
5056
5141
  });
@@ -98,6 +98,13 @@ async function detectCaptcha(page) {
98
98
  for (const selector of captchaSelectors) {
99
99
  const element = await page.$(selector).catch(() => null);
100
100
  if (element) {
101
+ const visible = await element.isVisible().catch(() => false);
102
+ if (!visible) continue;
103
+ try {
104
+ const box = await element.boundingBox();
105
+ if (box && (box.y < -1e3 || box.x < -1e3)) continue;
106
+ } catch {
107
+ }
101
108
  return {
102
109
  detected: true,
103
110
  type: "captcha",
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  getCaptchaConfig
3
3
  } from "./chunk-IIM5GOD7.js";
4
- import "./chunk-KFQGP6VL.js";
5
4
 
6
5
  // src/human-interaction.ts
7
6
  import { execSync } from "child_process";
@@ -44,16 +43,15 @@ var ScreencastCapturer = class {
44
43
  const cdp = await page.context().newCDPSession(page);
45
44
  this.cdpSession = cdp;
46
45
  cdp.on("Page.screencastFrame", async (params) => {
47
- if (!this.frameCallback) return;
48
- try {
49
- await cdp.send("Page.screencastFrameAck", { sessionId: params.sessionId });
50
- } catch {
51
- }
46
+ const cb = this.frameCallback;
47
+ if (!cb) return;
48
+ cdp.send("Page.screencastFrameAck", { sessionId: params.sessionId }).catch(() => {
49
+ });
52
50
  const viewport = {
53
51
  width: params.metadata?.deviceWidth || this.maxWidth,
54
52
  height: params.metadata?.deviceHeight || this.maxHeight
55
53
  };
56
- this.frameCallback({
54
+ cb({
57
55
  id: crypto.randomUUID(),
58
56
  sessionId: this.sessionId,
59
57
  timestamp: Date.now(),
@@ -429,6 +427,7 @@ var HumanInteractionManager = class {
429
427
  });
430
428
  }
431
429
  };
430
+
432
431
  export {
433
432
  HumanInteractionManager
434
433
  };
@@ -0,0 +1,128 @@
1
+ // src/daemon/daemon.ts
2
+ import { spawn } from "child_process";
3
+ import { join, dirname } from "path";
4
+ import { homedir } from "os";
5
+ import { fileURLToPath } from "url";
6
+ import { writeFileSync, unlinkSync, mkdirSync } from "fs";
7
+ import { stopDaemon as xcliStopDaemon, isDaemonRunning, getDaemonStatus, killAllDaemon } from "@dyyz1993/xcli-core";
8
+ var CONFIG_DIR = join(homedir(), ".xbrowser");
9
+ var __dirname = dirname(fileURLToPath(import.meta.url));
10
+ var WORKER_PATH = join(__dirname, "daemon-main.js");
11
+ function getDaemonConfig() {
12
+ return {
13
+ configDir: CONFIG_DIR,
14
+ workerEntryPath: WORKER_PATH,
15
+ basePort: 9224
16
+ };
17
+ }
18
+ async function startDaemonProcess(port = 9224) {
19
+ const config = getDaemonConfig();
20
+ if (isDaemonRunning(config)) {
21
+ const status = getDaemonStatus(config);
22
+ if (status.port === port && status.pid) {
23
+ return { pid: status.pid, port: status.port, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
24
+ }
25
+ await xcliStopDaemon(config);
26
+ }
27
+ const lockFile = join(CONFIG_DIR, "daemon.lock");
28
+ try {
29
+ mkdirSync(CONFIG_DIR, { recursive: true });
30
+ writeFileSync(lockFile, String(process.pid), { flag: "wx" });
31
+ } catch {
32
+ console.error("Another process is starting the daemon, waiting...");
33
+ for (let i = 0; i < 50; i++) {
34
+ await new Promise((r) => setTimeout(r, 200));
35
+ if (isDaemonRunning(config)) {
36
+ const s = getDaemonStatus(config);
37
+ if (s.port === port && s.pid) {
38
+ return { pid: s.pid, port: s.port, startedAt: (/* @__PURE__ */ new Date()).toISOString() };
39
+ }
40
+ }
41
+ }
42
+ try {
43
+ unlinkSync(lockFile);
44
+ } catch {
45
+ }
46
+ try {
47
+ writeFileSync(lockFile, String(process.pid), { flag: "wx" });
48
+ } catch {
49
+ }
50
+ }
51
+ const child = spawn("node", [WORKER_PATH], {
52
+ detached: true,
53
+ stdio: "ignore",
54
+ env: {
55
+ ...process.env,
56
+ XBROWSER_DAEMON_PORT: String(port)
57
+ }
58
+ });
59
+ child.unref();
60
+ return new Promise((resolve, reject) => {
61
+ let resolved = false;
62
+ const timeout = setTimeout(() => {
63
+ if (!resolved) {
64
+ resolved = true;
65
+ try {
66
+ unlinkSync(lockFile);
67
+ } catch {
68
+ }
69
+ reject(new Error("Daemon start timeout after 15s"));
70
+ }
71
+ }, 15e3);
72
+ const checkInterval = setInterval(() => {
73
+ if (isDaemonRunning(config)) {
74
+ const s = getDaemonStatus(config);
75
+ if (s.port === port && s.pid) {
76
+ resolved = true;
77
+ clearTimeout(timeout);
78
+ clearInterval(checkInterval);
79
+ try {
80
+ unlinkSync(lockFile);
81
+ } catch {
82
+ }
83
+ resolve({ pid: s.pid, port: s.port, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
84
+ }
85
+ }
86
+ }, 200);
87
+ child.on("error", (err) => {
88
+ if (!resolved) {
89
+ resolved = true;
90
+ clearTimeout(timeout);
91
+ clearInterval(checkInterval);
92
+ try {
93
+ unlinkSync(lockFile);
94
+ } catch {
95
+ }
96
+ reject(err);
97
+ }
98
+ });
99
+ });
100
+ }
101
+ async function stopDaemonProcess() {
102
+ await xcliStopDaemon(getDaemonConfig());
103
+ }
104
+ async function killAllDaemonProcesses() {
105
+ await killAllDaemon(getDaemonConfig());
106
+ }
107
+ function getDaemonProcessStatus() {
108
+ const config = getDaemonConfig();
109
+ const running = isDaemonRunning(config);
110
+ if (!running) {
111
+ return { running: false, pid: 0, port: 0, info: null };
112
+ }
113
+ const status = getDaemonStatus(config);
114
+ return {
115
+ running: true,
116
+ pid: status.pid,
117
+ port: status.port,
118
+ info: { pid: status.pid, port: status.port, startedAt: (/* @__PURE__ */ new Date()).toISOString() }
119
+ };
120
+ }
121
+
122
+ export {
123
+ getDaemonConfig,
124
+ startDaemonProcess,
125
+ stopDaemonProcess,
126
+ killAllDaemonProcesses,
127
+ getDaemonProcessStatus
128
+ };
@@ -288,6 +288,36 @@ function queryJS(selector) {
288
288
  const xpath = JSON.stringify(selector.slice(6));
289
289
  return `document.evaluate(${xpath}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue`;
290
290
  }
291
+ if (selector.startsWith("text=")) {
292
+ const raw = selector.slice(5);
293
+ const exact = raw.startsWith('"') && raw.endsWith('"');
294
+ const text = exact ? raw.slice(1, -1) : raw;
295
+ return `(() => {
296
+ const target = ${JSON.stringify(text)};
297
+ const exact = ${exact};
298
+ const els = [...document.querySelectorAll('*')].filter(e => {
299
+ if (e.children.length > 0) return false;
300
+ if (e.offsetParent === null) return false;
301
+ const t = (e.textContent || '').trim();
302
+ if (!t) return false;
303
+ return exact ? t === target : t.toLowerCase().includes(target.toLowerCase());
304
+ });
305
+ return els[0] || null;
306
+ })()`;
307
+ }
308
+ if (selector.startsWith("popup-text=")) {
309
+ const text = selector.slice("popup-text=".length);
310
+ return `(() => {
311
+ const target = ${JSON.stringify(text)};
312
+ const els = [...document.querySelectorAll('*')].filter(e => {
313
+ if (e.children.length > 0) return false;
314
+ if (e.offsetParent === null) return false;
315
+ if ((e.textContent || '').trim() !== target) return false;
316
+ return true;
317
+ });
318
+ return els[0] || null;
319
+ })()`;
320
+ }
291
321
  return `document.querySelector(${JSON.stringify(selector)})`;
292
322
  }
293
323
  function queryAllJS(selector) {
@@ -295,6 +325,9 @@ function queryAllJS(selector) {
295
325
  const xpath = JSON.stringify(selector.slice(6));
296
326
  return `(() => { const it = document.evaluate(${xpath}, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); const r=[]; for(let i=0;i<it.snapshotLength;i++) r.push(it.snapshotItem(i)); return r; })()`;
297
327
  }
328
+ if (selector.startsWith("text=") || selector.startsWith("popup-text=")) {
329
+ return `(() => { const el = ${queryJS(selector)}; return el ? [el] : []; })()`;
330
+ }
298
331
  return `document.querySelectorAll(${JSON.stringify(selector)})`;
299
332
  }
300
333
 
@@ -2757,11 +2790,52 @@ var CDPConnection = class extends EventEmitter4 {
2757
2790
  this.setMaxListeners(0);
2758
2791
  this.defaultSessionId = sessionId;
2759
2792
  if (typeof wsOrUrl === "string") {
2760
- this.ws = new WebSocket(wsOrUrl);
2793
+ const wsOptions = /^wss:\/\/\d+\.\d+\.\d+\.\d+/.test(wsOrUrl) ? { rejectUnauthorized: false } : void 0;
2794
+ this.ws = new WebSocket(wsOrUrl, wsOptions);
2761
2795
  } else {
2762
2796
  this.ws = wsOrUrl;
2763
2797
  }
2764
2798
  this.bindWebSocket();
2799
+ this.startKeepalive();
2800
+ }
2801
+ /** Send periodic WS pings to prevent idle-timeout disconnects (e.g. CF's 100s).
2802
+ * Also detects dead connections: if a pong isn't received within 10s of a
2803
+ * ping, the connection is considered dead and forcibly closed. */
2804
+ keepaliveTimer = null;
2805
+ pongTimer = null;
2806
+ startKeepalive() {
2807
+ this.ws.on("pong", () => {
2808
+ if (this.pongTimer) {
2809
+ clearTimeout(this.pongTimer);
2810
+ this.pongTimer = null;
2811
+ }
2812
+ });
2813
+ this.keepaliveTimer = setInterval(() => {
2814
+ if (this.ws.readyState === WebSocket.OPEN) {
2815
+ if (!this.pongTimer) {
2816
+ this.pongTimer = setTimeout(() => {
2817
+ if (!this.closed) {
2818
+ this.closed = true;
2819
+ this.closeReason = "keepalive timeout (no pong in 10s)";
2820
+ try {
2821
+ this.ws.terminate();
2822
+ } catch {
2823
+ }
2824
+ for (const [, pending] of this.pending) {
2825
+ clearTimeout(pending.timeout);
2826
+ pending.reject(new Error("Connection dead: keepalive timeout"));
2827
+ }
2828
+ this.pending.clear();
2829
+ this.emit("disconnect");
2830
+ }
2831
+ }, 1e4);
2832
+ }
2833
+ this.ws.ping?.();
2834
+ } else if (this.closed) {
2835
+ if (this.keepaliveTimer) clearInterval(this.keepaliveTimer);
2836
+ this.keepaliveTimer = null;
2837
+ }
2838
+ }, 3e4);
2765
2839
  }
2766
2840
  /** Wait for the connection to be fully open */
2767
2841
  async ready() {
@@ -2870,6 +2944,14 @@ var CDPConnection = class extends EventEmitter4 {
2870
2944
  if (this.closed) return;
2871
2945
  this.closed = true;
2872
2946
  this.closeReason = "closed by caller";
2947
+ if (this.keepaliveTimer) {
2948
+ clearInterval(this.keepaliveTimer);
2949
+ this.keepaliveTimer = null;
2950
+ }
2951
+ if (this.pongTimer) {
2952
+ clearTimeout(this.pongTimer);
2953
+ this.pongTimer = null;
2954
+ }
2873
2955
  for (const [id, pending] of this.pending) {
2874
2956
  clearTimeout(pending.timeout);
2875
2957
  pending.reject(new Error(`Connection closed: ${pending.method}`));
@@ -2911,6 +2993,10 @@ var CDPConnection = class extends EventEmitter4 {
2911
2993
  if (this.closed) return;
2912
2994
  this.closed = true;
2913
2995
  this.closeReason = `WebSocket closed: ${code} ${reason?.toString() ?? ""}`.trim();
2996
+ if (this.keepaliveTimer) {
2997
+ clearInterval(this.keepaliveTimer);
2998
+ this.keepaliveTimer = null;
2999
+ }
2914
3000
  for (const [id, pending] of this.pending) {
2915
3001
  clearTimeout(pending.timeout);
2916
3002
  pending.reject(new Error(`Connection closed: ${pending.method}`));