@xbrowser/cli 1.0.0 → 1.0.3

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/README.md +17 -26
  2. package/dist/{browser-DSVV4GHS.js → browser-5CTOA2WS.js} +4 -3
  3. package/dist/{browser-53KUFEEM.js → browser-ITLZZDHJ.js} +5 -5
  4. package/dist/{browser-GURRY444.js → browser-IUJXXNBT.js} +6 -3
  5. package/dist/{cdp-driver-MNPR3HZH.js → cdp-driver-4X3DK6PS.js} +339 -59
  6. package/dist/{cdp-driver-SSXUGXP6.js → cdp-driver-D6WMSMWX.js} +4 -3
  7. package/dist/chunk-2SVQTI2O.js +2794 -0
  8. package/dist/{chunk-IDVD44ED.js → chunk-6WOSXSCQ.js} +23 -7
  9. package/dist/{chunk-ZZ2TFWIV.js → chunk-ABXMBNQ6.js} +1 -1
  10. package/dist/{chunk-2MFXKN32.js → chunk-ACFE6PKF.js} +1013 -119
  11. package/dist/chunk-AMI64BSD.js +268 -0
  12. package/dist/{chunk-E4O5ZU3H.js → chunk-DKWR54XQ.js} +412 -98
  13. package/dist/{chunk-DTJRVA76.js → chunk-ETCO4SNK.js} +2 -2
  14. package/dist/chunk-GDKLH7ZY.js +8 -0
  15. package/dist/chunk-KFQGP6VL.js +33 -0
  16. package/dist/{chunk-2BQZIT3S.js → chunk-LRBSUKUZ.js} +85 -2497
  17. package/dist/{chunk-ITKPSIP7.js → chunk-MDAPTB7C.js} +6 -25
  18. package/dist/{chunk-42RPMJ76.js → chunk-N2JFPWMI.js} +342 -60
  19. package/dist/chunk-OZKD3W4X.js +417 -0
  20. package/dist/{chunk-T4J4C2NZ.js → chunk-TNEN6VQ2.js} +17 -4
  21. package/dist/{chunk-YKOHDEFV.js → chunk-TWWOIJM7.js} +74 -38
  22. package/dist/chunk-WJRE55TN.js +83 -0
  23. package/dist/cli.js +1558 -1122
  24. package/dist/{convert-EGFYNICZ.js → convert-LB3GJTLR.js} +3 -3
  25. package/dist/{convert-EKQVHKB4.js → convert-R3XXYKC6.js} +2 -2
  26. package/dist/{daemon-client-YAVQ343A.js → daemon-client-3JOKX2L2.js} +3 -2
  27. package/dist/{daemon-client-3VM7VU7O.js → daemon-client-DIEHGP5B.js} +28 -74
  28. package/dist/daemon-main.js +2296 -1722
  29. package/dist/{extract-JUOQQX4V.js → extract-2ZFW2MX7.js} +1 -1
  30. package/dist/{extract-L2IW3IUB.js → extract-BSYBM4MR.js} +1 -1
  31. package/dist/{filter-HC4RA7JY.js → filter-KCFO4RSV.js} +1 -1
  32. package/dist/{filter-VID2GGZ7.js → filter-T7DSZ2X7.js} +1 -1
  33. package/dist/{human-interaction-W753RVJB.js → human-interaction-UKAS5ZXV.js} +2 -2
  34. package/dist/index.d.ts +166 -109
  35. package/dist/index.js +2668 -1742
  36. package/dist/launcher-L2JNDB2H.js +20 -0
  37. package/dist/{launcher-KA7J32K5.js → launcher-OZXJQPNG.js} +1 -1
  38. package/dist/{network-store-66A2RATI.js → network-store-XGZ25FFC.js} +1 -1
  39. package/dist/{network-store-BN6QEZ7R.js → network-store-YVDNUREI.js} +1 -1
  40. package/dist/{parse-action-dsl-T3DYC33D.js → parse-action-dsl-UM333TL2.js} +1 -1
  41. package/dist/{proxy-WKGUCH2C.js → proxy-C6CK3UH5.js} +2 -2
  42. package/dist/session-recorder-RTDGURIJ.js +8 -0
  43. package/dist/session-recorder-YI7YYM36.js +7 -0
  44. package/dist/session-replayer-MY27H4DX.js +276 -0
  45. package/dist/site-knowledge-SYC6VCDB.js +23 -0
  46. package/package.json +5 -4
  47. package/dist/screenshot-CWAWMXVA.js +0 -28
  48. package/dist/session-recorder-MA75PKTQ.js +0 -7
@@ -1,10 +1,13 @@
1
1
  import {
2
2
  connectToCDP,
3
3
  launchChrome
4
- } from "./chunk-T4J4C2NZ.js";
4
+ } from "./chunk-TNEN6VQ2.js";
5
+ import {
6
+ errMsg
7
+ } from "./chunk-GDKLH7ZY.js";
5
8
  import {
6
9
  __require
7
- } from "./chunk-3RG5ZIWI.js";
10
+ } from "./chunk-KFQGP6VL.js";
8
11
 
9
12
  // src/browser.ts
10
13
  import { randomUUID } from "crypto";
@@ -250,36 +253,36 @@ function resolveKeyMapping(key) {
250
253
  return { key, code: key };
251
254
  }
252
255
  var KEY_MAP = {
253
- Enter: { key: "Enter", code: "Enter", text: "\r" },
254
- Tab: { key: "Tab", code: "Tab", text: " " },
255
- Escape: { key: "Escape", code: "Escape" },
256
- Backspace: { key: "Backspace", code: "Backspace" },
257
- Delete: { key: "Delete", code: "Delete" },
258
- Space: { key: " ", code: "Space", text: " " },
259
- ArrowUp: { key: "ArrowUp", code: "ArrowUp" },
260
- ArrowDown: { key: "ArrowDown", code: "ArrowDown" },
261
- ArrowLeft: { key: "ArrowLeft", code: "ArrowLeft" },
262
- ArrowRight: { key: "ArrowRight", code: "ArrowRight" },
263
- Home: { key: "Home", code: "Home" },
264
- End: { key: "End", code: "End" },
265
- PageUp: { key: "PageUp", code: "PageUp" },
266
- PageDown: { key: "PageDown", code: "PageDown" },
267
- Control: { key: "Control", code: "ControlLeft" },
268
- Shift: { key: "Shift", code: "ShiftLeft" },
269
- Alt: { key: "Alt", code: "AltLeft" },
270
- Meta: { key: "Meta", code: "MetaLeft" },
271
- F1: { key: "F1", code: "F1" },
272
- F2: { key: "F2", code: "F2" },
273
- F3: { key: "F3", code: "F3" },
274
- F4: { key: "F4", code: "F4" },
275
- F5: { key: "F5", code: "F5" },
276
- F6: { key: "F6", code: "F6" },
277
- F7: { key: "F7", code: "F7" },
278
- F8: { key: "F8", code: "F8" },
279
- F9: { key: "F9", code: "F9" },
280
- F10: { key: "F10", code: "F10" },
281
- F11: { key: "F11", code: "F11" },
282
- F12: { key: "F12", code: "F12" }
256
+ Enter: { key: "Enter", code: "Enter", text: "\r", keyCode: 13 },
257
+ Tab: { key: "Tab", code: "Tab", text: " ", keyCode: 9 },
258
+ Escape: { key: "Escape", code: "Escape", keyCode: 27 },
259
+ Backspace: { key: "Backspace", code: "Backspace", keyCode: 8 },
260
+ Delete: { key: "Delete", code: "Delete", keyCode: 46 },
261
+ Space: { key: " ", code: "Space", text: " ", keyCode: 32 },
262
+ ArrowUp: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
263
+ ArrowDown: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
264
+ ArrowLeft: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
265
+ ArrowRight: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 },
266
+ Home: { key: "Home", code: "Home", keyCode: 36 },
267
+ End: { key: "End", code: "End", keyCode: 35 },
268
+ PageUp: { key: "PageUp", code: "PageUp", keyCode: 33 },
269
+ PageDown: { key: "PageDown", code: "PageDown", keyCode: 34 },
270
+ Control: { key: "Control", code: "ControlLeft", keyCode: 17 },
271
+ Shift: { key: "Shift", code: "ShiftLeft", keyCode: 16 },
272
+ Alt: { key: "Alt", code: "AltLeft", keyCode: 18 },
273
+ Meta: { key: "Meta", code: "MetaLeft", keyCode: 91 },
274
+ F1: { key: "F1", code: "F1", keyCode: 112 },
275
+ F2: { key: "F2", code: "F2", keyCode: 113 },
276
+ F3: { key: "F3", code: "F3", keyCode: 114 },
277
+ F4: { key: "F4", code: "F4", keyCode: 115 },
278
+ F5: { key: "F5", code: "F5", keyCode: 116 },
279
+ F6: { key: "F6", code: "F6", keyCode: 117 },
280
+ F7: { key: "F7", code: "F7", keyCode: 118 },
281
+ F8: { key: "F8", code: "F8", keyCode: 119 },
282
+ F9: { key: "F9", code: "F9", keyCode: 120 },
283
+ F10: { key: "F10", code: "F10", keyCode: 121 },
284
+ F11: { key: "F11", code: "F11", keyCode: 122 },
285
+ F12: { key: "F12", code: "F12", keyCode: 123 }
283
286
  };
284
287
  function sleep2(ms) {
285
288
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -974,7 +977,7 @@ function createXBRequest(page, data) {
974
977
  }
975
978
  };
976
979
  }
977
- function createXBRouteFetch(conn, sessionId, params) {
980
+ function createXBRouteFetch(conn, sessionId, params, emitter) {
978
981
  const request = createXBRequest(null, {
979
982
  requestId: params.requestId,
980
983
  url: params.request.url,
@@ -1012,6 +1015,16 @@ function createXBRouteFetch(conn, sessionId, params) {
1012
1015
  responseHeaders: Object.entries(headers).map(([k, v]) => ({ name: k, value: v })),
1013
1016
  body: bodyBytes.toString("base64")
1014
1017
  }, sessionId);
1018
+ if (emitter) {
1019
+ const responseData = {
1020
+ requestId: params.requestId,
1021
+ status: opts.status ?? 200,
1022
+ url: params.request.url,
1023
+ headers
1024
+ };
1025
+ const response = createXBResponse(responseData, conn, sessionId);
1026
+ emitter.emit("response", response);
1027
+ }
1015
1028
  }
1016
1029
  };
1017
1030
  }
@@ -1193,7 +1206,7 @@ var XBPageImpl = class _XBPageImpl {
1193
1206
  );
1194
1207
  if (result) return result;
1195
1208
  } catch (err) {
1196
- lastError = err;
1209
+ lastError = err instanceof Error ? err : new Error(errMsg(err));
1197
1210
  }
1198
1211
  const pollMs = typeof polling === "number" ? polling : 16;
1199
1212
  await this.waitForTimeout(pollMs);
@@ -1233,6 +1246,25 @@ Last error: ${lastError.message}` : "";
1233
1246
  }
1234
1247
  return result.result?.value;
1235
1248
  }
1249
+ /** evaluateHandle — evaluates fn and returns a handle for element bounding box */
1250
+ async evaluateHandle(fn, ...args) {
1251
+ let expression;
1252
+ if (typeof fn === "string") {
1253
+ expression = fn;
1254
+ } else {
1255
+ const argStr = args.length > 0 ? `...${JSON.stringify(args)}` : "";
1256
+ expression = `(()=>{const __fn=(${fn.toString()});const __el=__fn(${argStr});if(__el&&typeof __el.getBoundingClientRect==='function'){const r=__el.getBoundingClientRect();return JSON.parse(JSON.stringify({x:r.x,y:r.y,w:r.width,h:r.height}));}return null;})()`;
1257
+ }
1258
+ const result = await this.conn.send("Runtime.evaluate", { expression, returnByValue: true }).catch(() => ({ result: { value: null } }));
1259
+ let box = null;
1260
+ try {
1261
+ box = JSON.parse(result.result?.value);
1262
+ } catch {
1263
+ }
1264
+ return {
1265
+ asElement: () => box ? { boundingBox: async () => box } : null
1266
+ };
1267
+ }
1236
1268
  async $eval(selector, fn, ...args) {
1237
1269
  const fnBody = typeof fn === "function" ? fn.toString() : fn;
1238
1270
  return this.evaluate(
@@ -1448,6 +1480,26 @@ Last error: ${lastError.message}` : "";
1448
1480
  off(event, handler) {
1449
1481
  this._emitter.off(event, handler);
1450
1482
  }
1483
+ /**
1484
+ * Wait for a one-shot event (Playwright-compatible subset).
1485
+ * Used to listen for 'filechooser', 'dialog', 'popup', 'framenavigated', etc.
1486
+ */
1487
+ async waitForEvent(event, opts = {}) {
1488
+ const timeout = opts.timeout ?? 3e4;
1489
+ return new Promise((resolve, reject) => {
1490
+ const timer = setTimeout(() => {
1491
+ this._emitter.off(event, handler);
1492
+ reject(new Error(`waitForEvent('${event}') timeout after ${timeout}ms`));
1493
+ }, timeout);
1494
+ const handler = (...args) => {
1495
+ if (opts.predicate && !opts.predicate(...args)) return;
1496
+ clearTimeout(timer);
1497
+ this._emitter.off(event, handler);
1498
+ resolve(args.length === 1 ? args[0] : args);
1499
+ };
1500
+ this._emitter.on(event, handler);
1501
+ });
1502
+ }
1451
1503
  // ── Lifecycle ───────────────────────────────────────────────
1452
1504
  async close() {
1453
1505
  if (this._closed) return;
@@ -1564,6 +1616,19 @@ Last error: ${lastError.message}` : "";
1564
1616
  async _cdpSend(method, params) {
1565
1617
  return this.conn.send(method, params, this.sessionId);
1566
1618
  }
1619
+ /**
1620
+ * 开启/关闭原生文件选择框拦截(CDP stateful 开关)。
1621
+ *
1622
+ * - enabled=true(执行期默认):点击上传按钮时不弹系统文件框,改发 Page.fileChooserOpened 事件,
1623
+ * page 的 'filechooser' 监听器拿到 chooser 后用 setFiles/setInputFiles 注入文件。
1624
+ * - enabled=false(录制期默认):真实文件选择框正常弹出,用户手动选文件;
1625
+ * 前端 input[type=file] 的 change 事件由 action signal 脚本捕获记录。
1626
+ *
1627
+ * 可多次调用切换状态(CDP 协议是 stateful 的)。
1628
+ */
1629
+ async setFileDialogInterception(enabled) {
1630
+ await this.conn.send("Page.setInterceptFileChooserDialog", { enabled }, this.sessionId).catch((e) => console.error("[XBPage] setInterceptFileChooserDialog failed:", errMsg(e)));
1631
+ }
1567
1632
  /** Subscribe to a CDP event on this page's session. Returns unsubscribe function. */
1568
1633
  _subscribe(event, handler) {
1569
1634
  return this.conn.subscribe(event, this.sessionId, handler);
@@ -1611,6 +1676,40 @@ Last error: ${lastError.message}` : "";
1611
1676
  this._emit("dialog", dialog);
1612
1677
  })
1613
1678
  );
1679
+ this._subscriptions.push(
1680
+ this.conn.subscribe("Page.fileChooserOpened", this.sessionId, async (params) => {
1681
+ const p = params;
1682
+ let selector = "";
1683
+ try {
1684
+ const result = await this.conn.send("DOM.describeNode", { backendNodeId: p.backendNodeId }, this.sessionId);
1685
+ const attrs = result.node?.attributes || [];
1686
+ const idIdx = attrs.indexOf("id");
1687
+ if (idIdx >= 0) selector = "#" + attrs[idIdx + 1];
1688
+ } catch {
1689
+ }
1690
+ if (!selector) {
1691
+ try {
1692
+ const result = await this.conn.send("DOM.resolveNode", { backendNodeId: p.backendNodeId }, this.sessionId);
1693
+ const evalResult = await this.conn.send("Runtime.callFunctionOn", {
1694
+ objectId: result.objectId,
1695
+ functionDeclaration: 'function() { return this.id || this.name || "" }',
1696
+ returnByValue: true
1697
+ }, this.sessionId);
1698
+ if (evalResult.result?.value) selector = "#" + evalResult.result.value;
1699
+ } catch {
1700
+ }
1701
+ }
1702
+ const fileChooser = {
1703
+ selector,
1704
+ isMultiple: p.mode === "selectMultiple",
1705
+ setFiles: async (files) => {
1706
+ const fileArray = Array.isArray(files) ? files : [files];
1707
+ await this.setInputFiles(selector || 'input[type="file"]', fileArray);
1708
+ }
1709
+ };
1710
+ this._emit("filechooser", fileChooser);
1711
+ })
1712
+ );
1614
1713
  }
1615
1714
  setupNetworkEvents() {
1616
1715
  this._subscriptions.push(
@@ -1624,7 +1723,17 @@ Last error: ${lastError.message}` : "";
1624
1723
  postData: p.request.postData ?? null,
1625
1724
  resourceType: p.type
1626
1725
  });
1627
- this._emit("request", p);
1726
+ this._emit("request", createXBRequest(
1727
+ null,
1728
+ {
1729
+ requestId: p.requestId,
1730
+ url: p.request.url,
1731
+ method: p.request.method,
1732
+ headers: p.request.headers,
1733
+ postData: p.request.postData ?? null,
1734
+ resourceType: p.type
1735
+ }
1736
+ ));
1628
1737
  this.checkNetworkIdle();
1629
1738
  })
1630
1739
  );
@@ -1636,7 +1745,11 @@ Last error: ${lastError.message}` : "";
1636
1745
  url: p.response.url,
1637
1746
  headers: p.response.headers
1638
1747
  });
1639
- this._emit("response", p);
1748
+ this._emit("response", createXBResponse(
1749
+ { requestId: p.requestId, status: p.response.status, url: p.response.url, headers: p.response.headers },
1750
+ this.conn,
1751
+ this.sessionId
1752
+ ));
1640
1753
  })
1641
1754
  );
1642
1755
  this._subscriptions.push(
@@ -1722,14 +1835,21 @@ Last error: ${lastError.message}` : "";
1722
1835
  reject(new Error(`waitForResponse timed out after ${timeout}ms`));
1723
1836
  }, timeout);
1724
1837
  const handler = (params) => {
1725
- const p = params;
1726
- const data = {
1727
- requestId: p.requestId,
1728
- status: p.response.status,
1729
- url: p.response.url,
1730
- headers: p.response.headers
1731
- };
1732
- const response = createXBResponse(data, this.conn, this.sessionId);
1838
+ let response;
1839
+ const respObj = params;
1840
+ if (respObj.response) {
1841
+ const data = {
1842
+ requestId: respObj.requestId || "",
1843
+ status: respObj.response.status || 0,
1844
+ url: respObj.response.url || "",
1845
+ headers: respObj.response.headers || {}
1846
+ };
1847
+ response = createXBResponse(data, this.conn, this.sessionId);
1848
+ } else if (typeof params.status === "function") {
1849
+ response = params;
1850
+ } else {
1851
+ return;
1852
+ }
1733
1853
  if (predicate(response)) {
1734
1854
  clearTimeout(timer);
1735
1855
  this._emitter.removeListener("response", handler);
@@ -1753,16 +1873,22 @@ Last error: ${lastError.message}` : "";
1753
1873
  reject(new Error(`waitForRequest timed out after ${timeout}ms`));
1754
1874
  }, timeout);
1755
1875
  const handler = (params) => {
1756
- const p = params;
1757
- const data = {
1758
- requestId: p.requestId,
1759
- url: p.request.url,
1760
- method: p.request.method,
1761
- headers: p.request.headers,
1762
- postData: p.request.postData ?? null,
1763
- resourceType: p.type
1764
- };
1765
- const request = createXBRequest(this, data);
1876
+ let request;
1877
+ const reqObj = params;
1878
+ if (reqObj.request) {
1879
+ request = createXBRequest(this, {
1880
+ requestId: reqObj.requestId || "",
1881
+ url: reqObj.request.url || "",
1882
+ method: reqObj.request.method || "",
1883
+ headers: reqObj.request.headers || {},
1884
+ postData: reqObj.request.postData ?? null,
1885
+ resourceType: reqObj.type || ""
1886
+ });
1887
+ } else if (typeof params.url === "function") {
1888
+ request = params;
1889
+ } else {
1890
+ return;
1891
+ }
1766
1892
  if (predicate(request)) {
1767
1893
  clearTimeout(timer);
1768
1894
  this._emitter.removeListener("request", handler);
@@ -1823,7 +1949,18 @@ Last error: ${lastError.message}` : "";
1823
1949
  const requestUrl = params.request.url;
1824
1950
  for (const { regex, handler } of this._routeHandlers) {
1825
1951
  if (regex.test(requestUrl)) {
1826
- const route = createXBRouteFetch(this.conn, this.sessionId, params);
1952
+ this._emit("request", createXBRequest(
1953
+ null,
1954
+ {
1955
+ requestId: params.requestId,
1956
+ url: params.request.url,
1957
+ method: params.request.method,
1958
+ headers: params.request.headers,
1959
+ postData: params.request.postData ?? null,
1960
+ resourceType: params.resourceType
1961
+ }
1962
+ ));
1963
+ const route = createXBRouteFetch(this.conn, this.sessionId, params, this._emitter);
1827
1964
  try {
1828
1965
  await handler(route);
1829
1966
  } catch {
@@ -1990,6 +2127,7 @@ var XBContextImpl = class {
1990
2127
  const page = new XBPageImpl(this.conn, sessionId, targetId, this, this._browser);
1991
2128
  await page._init();
1992
2129
  this._pages.push(page);
2130
+ this.forwardPageEvents(page);
1993
2131
  if (this.options.viewport) {
1994
2132
  await page.setViewportSize(this.options.viewport).catch(() => {
1995
2133
  });
@@ -2031,7 +2169,10 @@ var XBContextImpl = class {
2031
2169
  }
2032
2170
  this._browser._removeContext(this.contextId);
2033
2171
  }
2034
- async newCDPSession(_page) {
2172
+ async newCDPSession(page) {
2173
+ if (page instanceof XBPageImpl) {
2174
+ return new XBCDPSessionImpl(this.conn, page.sessionId);
2175
+ }
2035
2176
  return new XBCDPSessionImpl(this.conn);
2036
2177
  }
2037
2178
  async addInitScript(script) {
@@ -2068,7 +2209,31 @@ var XBContextImpl = class {
2068
2209
  off(event, handler) {
2069
2210
  this._emitter.off(event, handler);
2070
2211
  }
2212
+ /**
2213
+ * Register a page that was attached to an existing target (discovered via
2214
+ * Target.getTargets). Used by XBBrowserImpl.discoverContexts() to wire up
2215
+ * pages from the user's existing browser session into the context wrapper
2216
+ * so they appear in `context.pages()` and can be reused by plugins.
2217
+ */
2218
+ _addDiscoveredPage(page) {
2219
+ const exists = this._pages.some((p) => p._targetId === page._targetId);
2220
+ if (exists) return;
2221
+ this._pages.push(page);
2222
+ this.forwardPageEvents(page);
2223
+ }
2071
2224
  // ── Private ─────────────────────────────────────────────────
2225
+ /** Forward page-level events (request, response, etc.) to context listeners */
2226
+ forwardPageEvents(page) {
2227
+ const forward = (event) => {
2228
+ page.on(event, (...args) => {
2229
+ this._emitter.emit(event, ...args);
2230
+ });
2231
+ };
2232
+ forward("request");
2233
+ forward("response");
2234
+ forward("requestfailed");
2235
+ forward("requestfinished");
2236
+ }
2072
2237
  setupAutoAttach() {
2073
2238
  this.targetAttachedHandler = (paramsRaw) => {
2074
2239
  const params = paramsRaw;
@@ -2093,6 +2258,7 @@ var XBContextImpl = class {
2093
2258
  });
2094
2259
  }
2095
2260
  this._pages.push(page);
2261
+ this.forwardPageEvents(page);
2096
2262
  this._emitter.emit("page", page);
2097
2263
  });
2098
2264
  };
@@ -2109,10 +2275,17 @@ var XBBrowserImpl = class {
2109
2275
  childProcess = null;
2110
2276
  tmpDir;
2111
2277
  _exitHandler = null;
2112
- constructor(conn, childProcess, tmpDir) {
2278
+ /**
2279
+ * Original CDP endpoint (HTTP or ws URL) used to construct this browser.
2280
+ * Used by discoverContexts() as a fallback to HTTP /json/list when
2281
+ * Target.getTargets doesn't return page-type targets (e.g. cdp-tunnel proxy).
2282
+ */
2283
+ cdpEndpoint;
2284
+ constructor(conn, childProcess, tmpDir, cdpEndpoint) {
2113
2285
  this.conn = conn;
2114
2286
  this.childProcess = childProcess ?? null;
2115
2287
  this.tmpDir = tmpDir;
2288
+ this.cdpEndpoint = cdpEndpoint;
2116
2289
  conn.on("disconnect", () => {
2117
2290
  this._disconnected = true;
2118
2291
  this._emitter.emit("disconnected");
@@ -2156,7 +2329,7 @@ var XBBrowserImpl = class {
2156
2329
  this._exitHandler = null;
2157
2330
  }
2158
2331
  if (this.childProcess) {
2159
- const { killChrome: killChrome2 } = await import("./launcher-KA7J32K5.js");
2332
+ const { killChrome: killChrome2 } = await import("./launcher-L2JNDB2H.js");
2160
2333
  await killChrome2(this.childProcess, this.tmpDir);
2161
2334
  }
2162
2335
  await this.conn.close();
@@ -2186,6 +2359,10 @@ var XBBrowserImpl = class {
2186
2359
  contextId,
2187
2360
  context
2188
2361
  });
2362
+ if (this.childProcess) {
2363
+ this._enableAutoAttach().catch(() => {
2364
+ });
2365
+ }
2189
2366
  return context;
2190
2367
  }
2191
2368
  contexts() {
@@ -2214,6 +2391,23 @@ var XBBrowserImpl = class {
2214
2391
  async _detachFromTarget(sessionId) {
2215
2392
  await this.conn.send("Target.detachFromTarget", { sessionId });
2216
2393
  }
2394
+ /**
2395
+ * Derive the HTTP /json base URL from the original cdpEndpoint for use
2396
+ * as a fallback when Target.getTargets doesn't return page targets.
2397
+ * Supports both http:// and ws:// input formats.
2398
+ */
2399
+ _httpFallbackURL() {
2400
+ if (!this.cdpEndpoint) return void 0;
2401
+ if (this.cdpEndpoint.startsWith("http://") || this.cdpEndpoint.startsWith("https://")) {
2402
+ return this.cdpEndpoint;
2403
+ }
2404
+ if (this.cdpEndpoint.startsWith("ws://") || this.cdpEndpoint.startsWith("wss://")) {
2405
+ const url = this.cdpEndpoint.replace(/^ws/, "http");
2406
+ const slashIdx = url.indexOf("/", url.indexOf("//") + 2);
2407
+ return slashIdx >= 0 ? url.substring(0, slashIdx) : url;
2408
+ }
2409
+ return void 0;
2410
+ }
2217
2411
  /** Create a new page target within a browser context */
2218
2412
  async _createTarget(contextId, url = "about:blank") {
2219
2413
  const params = { url };
@@ -2230,10 +2424,97 @@ var XBBrowserImpl = class {
2230
2424
  async _enableAutoAttach() {
2231
2425
  await this.conn.send("Target.setAutoAttach", {
2232
2426
  autoAttach: true,
2233
- waitForDebuggerOnStart: true,
2427
+ waitForDebuggerOnStart: false,
2234
2428
  flatten: true
2235
2429
  });
2236
2430
  }
2431
+ /**
2432
+ * Discover existing browser contexts and pages via Target.getTargets.
2433
+ *
2434
+ * For CDP tunnel connections (cdp-tunnel, attach scenarios), the
2435
+ * Target.attachedToTarget auto-attach flow is unreliable. Without this
2436
+ * call, `b.contexts()` would return [] and callers would fall back to
2437
+ * `b.newContext()` — which creates an isolated context with NO cookies
2438
+ * shared with the user's existing browser session (causing login failures).
2439
+ *
2440
+ * This method:
2441
+ * 1. Queries Target.getTargets to enumerate all page targets
2442
+ * 2. Groups them by browserContextId
2443
+ * 3. Attaches to each existing page via Target.attachToTarget
2444
+ * 4. Wraps the discovered pages in a XBContextImpl and registers it in
2445
+ * this._contexts so `contexts()` returns the user's actual contexts
2446
+ * 5. Enables Target.setAutoAttach for future pages
2447
+ *
2448
+ * No-op for self-launched browsers (they already populated contexts via
2449
+ * newContext() + childProcess-gated auto-attach).
2450
+ */
2451
+ async discoverContexts() {
2452
+ if (this._disconnected) return;
2453
+ let targetInfos = [];
2454
+ try {
2455
+ const result = await this.conn.send(
2456
+ "Target.getTargets"
2457
+ );
2458
+ targetInfos = result.targetInfos ?? [];
2459
+ } catch {
2460
+ return;
2461
+ }
2462
+ const pageTargets = targetInfos.filter((t) => t.type === "page");
2463
+ const httpFallbackUrl = this._httpFallbackURL();
2464
+ if (pageTargets.length === 0 && httpFallbackUrl) {
2465
+ console.log(`[discoverContexts] Target.getTargets returned ${targetInfos.length} targets (0 page type). Falling back to HTTP /json/list at ${httpFallbackUrl}`);
2466
+ try {
2467
+ const { getCDPTargets: getCDPTargets3 } = await import("./launcher-L2JNDB2H.js");
2468
+ const httpPages = await getCDPTargets3(httpFallbackUrl);
2469
+ console.log(`[discoverContexts] HTTP /json/list returned ${httpPages.length} pages`);
2470
+ for (const p of httpPages) {
2471
+ if (p.type !== "page") continue;
2472
+ if (!p.url || p.url.startsWith("chrome://") || p.url.startsWith("devtools://")) continue;
2473
+ targetInfos.push({
2474
+ targetId: p.id,
2475
+ type: "page",
2476
+ url: p.url,
2477
+ title: p.title
2478
+ });
2479
+ }
2480
+ console.log(`[discoverContexts] After HTTP fallback: ${targetInfos.length} total targets, ${targetInfos.filter((t) => t.type === "page").length} pages`);
2481
+ } catch (err) {
2482
+ console.log(`[discoverContexts] HTTP fallback failed: ${errMsg(err)}`);
2483
+ }
2484
+ }
2485
+ const pagesByContext = /* @__PURE__ */ new Map();
2486
+ for (const t of targetInfos) {
2487
+ if (t.type !== "page") continue;
2488
+ if (!t.url || t.url.startsWith("chrome://") || t.url.startsWith("devtools://")) {
2489
+ continue;
2490
+ }
2491
+ const ctxId = t.browserContextId || "default";
2492
+ if (!pagesByContext.has(ctxId)) pagesByContext.set(ctxId, []);
2493
+ pagesByContext.get(ctxId).push(t);
2494
+ }
2495
+ for (const [ctxId, pages] of pagesByContext) {
2496
+ if (this._contexts.has(ctxId)) continue;
2497
+ const context = new XBContextImpl(this.conn, ctxId, this, {});
2498
+ for (const p of pages) {
2499
+ try {
2500
+ const sessionId = await this._attachToTarget(p.targetId);
2501
+ const page = new XBPageImpl(this.conn, sessionId, p.targetId, context, this);
2502
+ await page._init();
2503
+ context._addDiscoveredPage(page);
2504
+ } catch {
2505
+ }
2506
+ }
2507
+ this._contexts.set(ctxId, { contextId: ctxId, context });
2508
+ }
2509
+ try {
2510
+ await this.conn.send("Target.setAutoAttach", {
2511
+ autoAttach: true,
2512
+ waitForDebuggerOnStart: false,
2513
+ flatten: true
2514
+ });
2515
+ } catch {
2516
+ }
2517
+ }
2237
2518
  };
2238
2519
 
2239
2520
  // src/cdp-driver/connection.ts
@@ -2453,7 +2734,8 @@ async function launch(options = {}) {
2453
2734
  }
2454
2735
  const conn = new CDPConnection(wsEndpoint);
2455
2736
  await conn.ready();
2456
- const browser = new XBBrowserImpl(conn, childProcess, tmpDir);
2737
+ const httpEndpoint = options.cdpEndpoint && !options.cdpEndpoint.startsWith("ws") ? options.cdpEndpoint : void 0;
2738
+ const browser = new XBBrowserImpl(conn, childProcess, tmpDir, httpEndpoint);
2457
2739
  return { browser, wsEndpoint };
2458
2740
  }
2459
2741
 
@@ -3749,7 +4031,7 @@ var CDPInterceptorProxy = class {
3749
4031
  const ctx = {
3750
4032
  method: request.method,
3751
4033
  params: request.params ?? {},
3752
- sessionId: makeCompoundId(browserWs._cdpSession, request.sessionId),
4034
+ sessionId: makeCompoundId("_cdpSession" in browserWs ? browserWs._cdpSession : void 0, request.sessionId),
3753
4035
  direction: "client\u2192browser"
3754
4036
  };
3755
4037
  const decision = this.engine.evaluate(ctx);
@@ -3879,6 +4161,7 @@ async function resolveCDPEndpoint(raw) {
3879
4161
  }
3880
4162
 
3881
4163
  // src/browser.ts
4164
+ import { SessionStore } from "@dyyz1993/xcli-core";
3882
4165
  function logSessionEvent(event, details) {
3883
4166
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").substring(0, 19);
3884
4167
  const pid = process.pid;
@@ -3891,7 +4174,7 @@ function sessionFile(name) {
3891
4174
  function ensureSessionDir() {
3892
4175
  mkdirSync(SESSION_DIR, { recursive: true });
3893
4176
  }
3894
- var sessions = /* @__PURE__ */ new Map();
4177
+ var sessions = new SessionStore();
3895
4178
  var _sharedBrowser = null;
3896
4179
  var _sharedCdpProxy = null;
3897
4180
  var IDLE_TIMEOUT_MS = (process.env.XBROWSER_IDLE_TIMEOUT ? parseInt(process.env.XBROWSER_IDLE_TIMEOUT, 10) : 30) * 60 * 1e3;
@@ -3902,7 +4185,7 @@ function resetIdleTimer() {
3902
4185
  const now = Date.now();
3903
4186
  let allIdle = true;
3904
4187
  const idleSessions = [];
3905
- for (const [, s] of sessions) {
4188
+ for (const s of sessions) {
3906
4189
  if (now - s.lastActivityAt < IDLE_TIMEOUT_MS) {
3907
4190
  allIdle = false;
3908
4191
  } else {
@@ -3925,7 +4208,7 @@ function touchSession(id) {
3925
4208
  resetIdleTimer();
3926
4209
  }
3927
4210
  process.on("exit", () => {
3928
- for (const session of sessions.values()) {
4211
+ for (const session of sessions.list()) {
3929
4212
  if (session.isCDP) {
3930
4213
  logSessionEvent("process_exit", `Session "${session.name}": CDP connection (not closing external browser).`);
3931
4214
  } else {
@@ -4015,6 +4298,9 @@ async function createBrowser(options) {
4015
4298
  return browser3;
4016
4299
  }
4017
4300
  const { browser: browser2 } = await launch({ cdpEndpoint: realEndpoint });
4301
+ await browser2.discoverContexts().catch((err) => {
4302
+ console.error(`[browser] discoverContexts failed: ${errMsg(err)}`);
4303
+ });
4018
4304
  return browser2;
4019
4305
  }
4020
4306
  const executablePath = options?.executablePath || process.env.XBROWSER_CHROMIUM_PATH || discoverChromiumPath();
@@ -4029,10 +4315,7 @@ async function getBrowser(options) {
4029
4315
  return _sharedBrowser;
4030
4316
  }
4031
4317
  function findSession(name) {
4032
- for (const [, session] of sessions) {
4033
- if (session.name === name) return session;
4034
- }
4035
- return void 0;
4318
+ return sessions.find(name);
4036
4319
  }
4037
4320
  function getSessionById(id) {
4038
4321
  return sessions.get(id);
@@ -4136,9 +4419,14 @@ async function findOrRestoreSession(name, cdpEndpoint) {
4136
4419
  return void 0;
4137
4420
  }
4138
4421
  const targetUrl = meta.conversationUrl || meta.url;
4139
- if (targetUrl && page.url() !== targetUrl && !page.url().includes(new URL(targetUrl).hostname)) {
4140
- await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 3e4 }).catch(() => {
4141
- });
4422
+ if (targetUrl && page.url() !== targetUrl) {
4423
+ try {
4424
+ if (!page.url().includes(new URL(targetUrl).hostname)) {
4425
+ await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: 3e4 }).catch(() => {
4426
+ });
4427
+ }
4428
+ } catch {
4429
+ }
4142
4430
  }
4143
4431
  const session = {
4144
4432
  id: meta.id || randomUUID(),
@@ -4151,18 +4439,18 @@ async function findOrRestoreSession(name, cdpEndpoint) {
4151
4439
  isCDP: true,
4152
4440
  cdpEndpoint: ep
4153
4441
  };
4154
- for (const [existingId, existingSession] of sessions) {
4442
+ for (const existingSession of sessions.list()) {
4155
4443
  if (existingSession.name === name) {
4156
- logSessionEvent("remove_stale", `Removing stale session name="${name}" id="${existingId}" during restore`);
4157
- sessions.delete(existingId);
4444
+ logSessionEvent("remove_stale", `Removing stale session name="${name}" id="${existingSession.id}" during restore`);
4445
+ sessions.removeById(existingSession.id);
4158
4446
  }
4159
4447
  }
4160
- sessions.set(session.id, session);
4448
+ sessions.set(session);
4161
4449
  resetIdleTimer();
4162
4450
  await installNetworkCapture(page, name);
4163
4451
  return session;
4164
4452
  } catch (e) {
4165
- console.error(`[Session Restore] Failed for "${name}":`, e.message);
4453
+ console.error(`[Session Restore] Failed for "${name}":`, errMsg(e));
4166
4454
  deleteSessionDiskMeta(name);
4167
4455
  return void 0;
4168
4456
  }
@@ -4173,7 +4461,12 @@ async function createEphemeralContext(options) {
4173
4461
  const { browser: b2 } = await launch({ cdpEndpoint: endpoint });
4174
4462
  const contexts = b2.contexts();
4175
4463
  const ctx = contexts[0] || await b2.newContext();
4176
- const page2 = await ctx.newPage();
4464
+ const allPages = ctx.pages();
4465
+ const existingPages = allPages.filter((p) => {
4466
+ const url = p.url();
4467
+ return url !== "about:blank" && !url.startsWith("chrome://");
4468
+ });
4469
+ const page2 = existingPages.length > 0 ? existingPages[0] : allPages.length > 0 ? allPages[0] : await ctx.newPage();
4177
4470
  resetIdleTimer();
4178
4471
  ephemeralConnections.set(page2, b2);
4179
4472
  return { context: ctx, page: page2 };
@@ -4205,11 +4498,11 @@ async function closeEphemeralContext(context) {
4205
4498
  }
4206
4499
  }
4207
4500
  function getAllSessions() {
4208
- return Array.from(sessions.values());
4501
+ return sessions.list();
4209
4502
  }
4210
4503
  async function installNetworkCapture(page, sessionName) {
4211
4504
  if (process.env.XBROWSER_DAEMON_WORKER !== "1") return;
4212
- const { networkStore } = await import("./network-store-66A2RATI.js");
4505
+ const { networkStore } = await import("./network-store-YVDNUREI.js");
4213
4506
  const requestData = /* @__PURE__ */ new Map();
4214
4507
  const responseMeta = /* @__PURE__ */ new Map();
4215
4508
  const xbPage = page;
@@ -4330,16 +4623,38 @@ async function createSession(name, url, options) {
4330
4623
  }
4331
4624
  context = contexts[0] || await b.newContext();
4332
4625
  let targetPage = null;
4333
- for (const ctx of contexts) {
4334
- const pages = ctx.pages();
4335
- for (const p of pages) {
4336
- const pUrl = p.url();
4337
- if (pUrl && pUrl !== "about:blank" && !pUrl.startsWith("chrome://")) {
4338
- targetPage = p;
4339
- break;
4626
+ const targetHostname = url ? (() => {
4627
+ try {
4628
+ return new URL(url).hostname;
4629
+ } catch {
4630
+ return "";
4631
+ }
4632
+ })() : "";
4633
+ if (targetHostname) {
4634
+ for (const ctx of contexts) {
4635
+ const pages = ctx.pages();
4636
+ for (const p of pages) {
4637
+ const pUrl = p.url();
4638
+ if (pUrl && pUrl !== "about:blank" && !pUrl.startsWith("chrome://") && pUrl.includes(targetHostname)) {
4639
+ targetPage = p;
4640
+ break;
4641
+ }
4642
+ }
4643
+ if (targetPage) break;
4644
+ }
4645
+ }
4646
+ if (!targetPage) {
4647
+ for (const ctx of contexts) {
4648
+ const pages = ctx.pages();
4649
+ for (const p of pages) {
4650
+ const pUrl = p.url();
4651
+ if (pUrl && pUrl !== "about:blank" && !pUrl.startsWith("chrome://")) {
4652
+ targetPage = p;
4653
+ break;
4654
+ }
4340
4655
  }
4656
+ if (targetPage) break;
4341
4657
  }
4342
- if (targetPage) break;
4343
4658
  }
4344
4659
  if (!targetPage && options?.cdpEndpoint) {
4345
4660
  const targets = await getCDPTargets2(options.cdpEndpoint);
@@ -4380,14 +4695,14 @@ async function createSession(name, url, options) {
4380
4695
  isCDP,
4381
4696
  cdpEndpoint: options?.cdpEndpoint
4382
4697
  };
4383
- sessions.set(session.id, session);
4698
+ sessions.set(session);
4384
4699
  logSessionEvent("create_session", `name="${name}" id="${session.id}" url="${url || "(no url)"}" isCDP=${isCDP} cdpEndpoint=${options?.cdpEndpoint || "(none)"}`);
4385
4700
  resetIdleTimer();
4386
4701
  await installNetworkCapture(page, name);
4387
4702
  return session;
4388
4703
  }
4389
4704
  async function closeSessionByName(name) {
4390
- for (const [id, session] of sessions) {
4705
+ for (const session of sessions) {
4391
4706
  if (session.name === name || session.id === name) {
4392
4707
  logSessionEvent("close_session", `name="${session.name}" id="${session.id}" url="${session.page.url()}"`);
4393
4708
  if (session.isCDP) {
@@ -4406,20 +4721,20 @@ async function closeSessionByName(name) {
4406
4721
  });
4407
4722
  }
4408
4723
  }
4409
- sessions.delete(id);
4724
+ sessions.removeById(session.id);
4410
4725
  const file2 = sessionFile(session.name);
4411
4726
  try {
4412
4727
  unlinkSync(file2);
4413
4728
  } catch {
4414
4729
  }
4415
4730
  try {
4416
- const { networkStore, commandLogStore } = await import("./network-store-66A2RATI.js");
4731
+ const { networkStore, commandLogStore } = await import("./network-store-YVDNUREI.js");
4417
4732
  networkStore.clear(session.name);
4418
4733
  commandLogStore.clear(session.name);
4419
4734
  } catch {
4420
4735
  }
4421
4736
  try {
4422
- const { SessionRecorder } = await import("./session-recorder-MA75PKTQ.js");
4737
+ const { SessionRecorder } = await import("./session-recorder-RTDGURIJ.js");
4423
4738
  SessionRecorder.cleanup(session.name);
4424
4739
  } catch {
4425
4740
  }
@@ -4434,9 +4749,9 @@ async function closeSessionByName(name) {
4434
4749
  return false;
4435
4750
  }
4436
4751
  async function closeAllSessions() {
4437
- const names = [...sessions.values()].map((s) => `${s.name}(${s.page.url()})`).join(", ");
4752
+ const names = sessions.list().map((s) => `${s.name}(${s.page.url()})`).join(", ");
4438
4753
  if (names) logSessionEvent("close_all_sessions", `Closing ${sessions.size} sessions: ${names}`);
4439
- for (const [id, session] of sessions) {
4754
+ for (const session of sessions.list()) {
4440
4755
  try {
4441
4756
  if (!session.isCDP) {
4442
4757
  await session.context.close();
@@ -4445,9 +4760,9 @@ async function closeAllSessions() {
4445
4760
  await session.browser.close().catch(() => {
4446
4761
  });
4447
4762
  }
4448
- sessions.delete(id);
4763
+ sessions.removeById(session.id);
4449
4764
  } catch {
4450
- sessions.delete(id);
4765
+ sessions.removeById(session.id);
4451
4766
  }
4452
4767
  }
4453
4768
  }
@@ -4485,7 +4800,7 @@ async function ensureProcessCanExit() {
4485
4800
  clearTimeout(idleTimer);
4486
4801
  idleTimer = null;
4487
4802
  }
4488
- for (const session of sessions.values()) {
4803
+ for (const session of sessions.list()) {
4489
4804
  if (session.browser) {
4490
4805
  if (session.isCDP) {
4491
4806
  await session.browser.close().catch(() => {
@@ -4511,7 +4826,6 @@ async function ensureProcessCanExit() {
4511
4826
 
4512
4827
  export {
4513
4828
  createRuleEngine,
4514
- resolveCDPEndpoint,
4515
4829
  touchSession,
4516
4830
  findTargetPage,
4517
4831
  resolveLaunchOpts,