@xbrowser/cli 1.20.0 → 1.22.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 (35) hide show
  1. package/README.md +1 -0
  2. package/dist/{browser-Y6B3NS4Q.js → browser-BVRFSUXU.js} +3 -2
  3. package/dist/{browser-NVC7MTML.js → browser-DSYUAE7H.js} +5 -5
  4. package/dist/{browser-PBBC7RAV.js → browser-Q67BJTI2.js} +6 -4
  5. package/dist/{cdp-driver-QYFA4QTP.js → cdp-driver-DC7EIUG5.js} +4 -3
  6. package/dist/{cdp-driver-Z2PLAO42.js → cdp-driver-T7D6H2RW.js} +368 -510
  7. package/dist/{cdp-driver-73F5CVH2.js → cdp-driver-WWO2UZDR.js} +4 -3
  8. package/dist/{chunk-76EKW7YJ.js → chunk-2Q3GQRUP.js} +3 -3
  9. package/dist/{chunk-UYEJ66VX.js → chunk-4OU37CXP.js} +387 -540
  10. package/dist/{chunk-H2A5JUK5.js → chunk-75DNUI6M.js} +9 -0
  11. package/dist/{chunk-FDU5BCPV.js → chunk-7OP2ISEY.js} +378 -540
  12. package/dist/{chunk-3FWLW7FS.js → chunk-A7NEA4PA.js} +39 -1
  13. package/dist/chunk-GYB5BOSP.js +602 -0
  14. package/dist/{chunk-T2KUVO4H.js → chunk-IFKJO2UR.js} +393 -546
  15. package/dist/{chunk-NODRQGOK.js → chunk-IR6C24P7.js} +97 -0
  16. package/dist/{chunk-3KZ34AUC.js → chunk-MYH6FIBZ.js} +2 -1
  17. package/dist/{chunk-75TLPVVZ.js → chunk-T4WTKBWI.js} +2 -2
  18. package/dist/{chunk-HBHOKZPN.js → chunk-V6KGBU5J.js} +97 -0
  19. package/dist/{chunk-IWVG4XYZ.js → chunk-VCVDILH6.js} +2 -1
  20. package/dist/{chunk-A6LPGFAL.js → chunk-WYETQ5C7.js} +1 -1
  21. package/dist/cli.js +25 -13
  22. package/dist/daemon-main.js +20 -14
  23. package/dist/index.d.ts +136 -1
  24. package/dist/index.js +29 -18
  25. package/dist/keep-awake-M4KJS4B4.js +29 -0
  26. package/dist/{launcher-44STYRJC.js → launcher-IK2FKW6T.js} +1 -1
  27. package/dist/{launcher-EXJHHAUL.js → launcher-LUOMMGZC.js} +1 -1
  28. package/dist/{proxy-LUR4U5YF.js → proxy-NKCYE76M.js} +2 -2
  29. package/dist/{session-recorder-SLDBENVF.js → session-recorder-563ISSJK.js} +1 -1
  30. package/dist/{session-recorder-3BEVWHOK.js → session-recorder-GEZVQBKW.js} +1 -1
  31. package/dist/session-replayer-XM5L7LFD.js +927 -0
  32. package/dist/stealth-2AA2FSNN.js +29 -0
  33. package/dist/stealth-JOXGV34D.js +29 -0
  34. package/package.json +2 -1
  35. package/dist/session-replayer-SBYJXQZZ.js +0 -413
@@ -0,0 +1,29 @@
1
+ import {
2
+ DEFAULT_STEALTH_CONFIG,
3
+ StealthInjector,
4
+ bezierTrajectory,
5
+ buildStealthInitScript,
6
+ cosineEase,
7
+ deriveUaChProfileForBinary,
8
+ landingOffset,
9
+ lookupKey,
10
+ rand,
11
+ sleep,
12
+ typingDelay,
13
+ wheelDelta
14
+ } from "./chunk-GYB5BOSP.js";
15
+ import "./chunk-3RG5ZIWI.js";
16
+ export {
17
+ DEFAULT_STEALTH_CONFIG,
18
+ StealthInjector,
19
+ bezierTrajectory,
20
+ buildStealthInitScript,
21
+ cosineEase,
22
+ deriveUaChProfileForBinary,
23
+ landingOffset,
24
+ lookupKey,
25
+ rand,
26
+ sleep,
27
+ typingDelay,
28
+ wheelDelta
29
+ };
@@ -0,0 +1,29 @@
1
+ import {
2
+ DEFAULT_STEALTH_CONFIG,
3
+ StealthInjector,
4
+ bezierTrajectory,
5
+ buildStealthInitScript,
6
+ cosineEase,
7
+ deriveUaChProfileForBinary,
8
+ landingOffset,
9
+ lookupKey,
10
+ rand,
11
+ sleep,
12
+ typingDelay,
13
+ wheelDelta
14
+ } from "./chunk-GYB5BOSP.js";
15
+ import "./chunk-KFQGP6VL.js";
16
+ export {
17
+ DEFAULT_STEALTH_CONFIG,
18
+ StealthInjector,
19
+ bezierTrajectory,
20
+ buildStealthInitScript,
21
+ cosineEase,
22
+ deriveUaChProfileForBinary,
23
+ landingOffset,
24
+ lookupKey,
25
+ rand,
26
+ sleep,
27
+ typingDelay,
28
+ wheelDelta
29
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xbrowser/cli",
3
- "version": "1.20.0",
3
+ "version": "1.22.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": {
@@ -75,6 +75,7 @@
75
75
  "lint": "eslint src/ bin/ --ext .ts",
76
76
  "lint:plugin-contract": "node lint-scripts/check-plugin-contract.mjs && node lint-scripts/check-plugin-status.mjs",
77
77
  "lint:plugin-status": "node lint-scripts/check-plugin-status.mjs --update",
78
+ "stealth:check": "vitest run tests/cdp-driver/stealth-health.test.ts tests/cdp-driver/stealth-audit.test.ts tests/cdp-driver/stealth-probe.test.ts",
78
79
  "lint:docs": "node lint-scripts/check-docs-consistency.mjs",
79
80
  "typecheck": "tsc --noEmit",
80
81
  "start": "node dist/cli.js",
@@ -1,413 +0,0 @@
1
- import {
2
- queryJS
3
- } from "./chunk-3FWLW7FS.js";
4
- import "./chunk-KFQGP6VL.js";
5
-
6
- // src/recorder/session-replayer.ts
7
- var SessionReplayer = class {
8
- opts;
9
- recording = null;
10
- page = null;
11
- constructor(opts) {
12
- this.opts = {
13
- cdpUrl: opts.cdpUrl,
14
- page: opts.page,
15
- stepDelay: opts.stepDelay ?? 500,
16
- stepTimeout: opts.stepTimeout ?? 1e4,
17
- onStep: opts.onStep,
18
- onError: opts.onError
19
- };
20
- }
21
- /** Load a recording from a file path or parsed JSON */
22
- async load(source) {
23
- if (typeof source === "string") {
24
- const fs = await import("fs");
25
- const raw = fs.readFileSync(source, "utf8");
26
- this.recording = JSON.parse(raw);
27
- } else {
28
- this.recording = source;
29
- }
30
- }
31
- /** Run the full replay */
32
- async run() {
33
- if (!this.recording) throw new Error("No recording loaded. Call load() first.");
34
- if (this.opts.page) {
35
- this.page = this.opts.page;
36
- } else if (this.opts.cdpUrl) {
37
- const { launch } = await import("./cdp-driver-73F5CVH2.js");
38
- const { browser } = await launch({ cdpEndpoint: this.opts.cdpUrl });
39
- let contexts = browser.contexts();
40
- for (let i = 0; i < 10 && contexts.length === 0; i++) {
41
- await new Promise((r) => setTimeout(r, 500));
42
- contexts = browser.contexts();
43
- }
44
- const context = contexts[0];
45
- if (!context) throw new Error("No browser context available");
46
- const pages = context.pages();
47
- this.page = pages[0];
48
- }
49
- if (!this.page) throw new Error("No page available. Provide cdpUrl or page.");
50
- const actions = this.dedupAdjacentActions(this.recording.actions);
51
- let success = 0;
52
- let failed = 0;
53
- let skipped = 0;
54
- for (let i = 0; i < actions.length; i++) {
55
- const action = actions[i];
56
- this.opts.onStep?.(action, i, actions.length);
57
- const pagesBefore = this.listContextPages();
58
- try {
59
- if (action.trajectory) {
60
- await this.replayTrajectory(action.trajectory);
61
- }
62
- await this.replayAction(action);
63
- if (action.type !== "resize" && action.type !== "clipboard" && action.type !== "visibility") {
64
- try {
65
- await this.page.waitForLoadState("domcontentloaded", this.opts.stepTimeout);
66
- } catch {
67
- }
68
- }
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
- }
79
- } catch (e) {
80
- const err = e instanceof Error ? e : new Error(String(e));
81
- this.opts.onError?.(action, err, i);
82
- failed++;
83
- }
84
- if (i < actions.length - 1) {
85
- await new Promise((r) => setTimeout(r, this.opts.stepDelay));
86
- }
87
- }
88
- return { success, failed, skipped };
89
- }
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
- }
132
- async replayAction(action) {
133
- const page = this.page;
134
- const timeout = this.opts.stepTimeout;
135
- switch (action.type) {
136
- // Proactive sensing actions are observations, not user actions — skip
137
- // them during replay (they don't represent anything the user did).
138
- case "popup_appear":
139
- case "discovered_filters":
140
- return;
141
- case "navigation":
142
- await page.goto(action.url, { waitUntil: "load", timeout });
143
- break;
144
- case "goto":
145
- await page.goto(action.url, { waitUntil: "load", timeout });
146
- break;
147
- case "click":
148
- case "cdp-click": {
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
- }
165
- await page.click(selector, { timeout });
166
- break;
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
- }
187
- case "input": {
188
- const selector = await this.resolveAndWait(action);
189
- await page.fill(selector, action.value ?? "", { timeout });
190
- break;
191
- }
192
- case "cdp-fill": {
193
- const selector = await this.resolveAndWait(action);
194
- await page.fill(selector, action.value ?? "", { timeout });
195
- break;
196
- }
197
- case "change": {
198
- const selector = await this.resolveAndWait(action);
199
- if (action.value) {
200
- await page.selectOption(selector, action.value);
201
- }
202
- break;
203
- }
204
- case "filechooser": {
205
- const selector = await this.resolveAndWait(action);
206
- const files = this.resolveFiles(action);
207
- if (files.length > 0) {
208
- await page.setInputFiles(selector, files);
209
- }
210
- break;
211
- }
212
- case "keydown": {
213
- const key = action.key ?? "";
214
- if (key === "Enter" || key === "Tab" || key === "Escape") {
215
- await page.keyboard.press(key);
216
- } else if (key === "Backspace") {
217
- await page.keyboard.press("Backspace");
218
- } else if (key === "Delete") {
219
- await page.keyboard.press("Delete");
220
- } else if (key.startsWith("Arrow")) {
221
- await page.keyboard.press(key);
222
- } else if (key.includes("+")) {
223
- await page.keyboard.press(key.replace("Meta", "Meta").replace("Ctrl", "Control"));
224
- }
225
- break;
226
- }
227
- case "dblclick": {
228
- const selector = this.resolveSelector(action);
229
- if (selector) {
230
- await page.waitForSelector(selector, { state: "visible", timeout });
231
- await page.dblclick(selector, { timeout });
232
- } else if (action.x !== void 0 && action.y !== void 0) {
233
- await page.mouse.dblclick(action.x, action.y);
234
- }
235
- break;
236
- }
237
- case "contextmenu": {
238
- const selector = this.resolveSelector(action);
239
- if (selector) {
240
- await page.waitForSelector(selector, { state: "visible", timeout });
241
- await page.click(selector, { button: "right", timeout });
242
- } else if (action.x !== void 0 && action.y !== void 0) {
243
- await page.mouse.click(action.x, action.y, { button: "right" });
244
- }
245
- break;
246
- }
247
- case "hover": {
248
- const selector = await this.resolveAndWait(action);
249
- if (selector) {
250
- await page.hover(selector);
251
- }
252
- const firstPopup = action.hoverContext?.appeared?.[0];
253
- if (firstPopup?.selector) {
254
- try {
255
- await page.waitForSelector(firstPopup.selector, {
256
- state: "visible",
257
- timeout: 1e3
258
- });
259
- } catch {
260
- }
261
- }
262
- break;
263
- }
264
- case "drag": {
265
- if (action.drag) {
266
- const { fromX, fromY, toX, toY } = action.drag;
267
- await page.mouse.move(fromX, fromY);
268
- await page.mouse.down();
269
- const steps = 5;
270
- for (let i = 1; i <= steps; i++) {
271
- await page.mouse.move(
272
- fromX + (toX - fromX) * i / steps,
273
- fromY + (toY - fromY) * i / steps
274
- );
275
- await new Promise((r) => setTimeout(r, 30));
276
- }
277
- await page.mouse.up();
278
- }
279
- break;
280
- }
281
- case "resize": {
282
- break;
283
- }
284
- case "clipboard": {
285
- break;
286
- }
287
- case "touch": {
288
- if (action.touch) {
289
- const { touchType, touches } = action.touch;
290
- if (touchType === "start" && touches.length > 0) {
291
- await page.mouse.move(touches[0].x, touches[0].y);
292
- await page.mouse.down();
293
- } else if (touchType === "end" && touches.length > 0) {
294
- await page.mouse.move(touches[0].x, touches[0].y);
295
- await page.mouse.up();
296
- }
297
- }
298
- break;
299
- }
300
- case "focus": {
301
- const selector = await this.resolveAndWait(action);
302
- if (action.focus?.focusType === "focus") {
303
- await page.locator(selector).focus();
304
- }
305
- break;
306
- }
307
- case "visibility": {
308
- break;
309
- }
310
- case "submit": {
311
- const selector = await this.resolveAndWait(action);
312
- await page.evaluate((sel) => {
313
- const form = document.querySelector(sel);
314
- if (!form) return;
315
- if (typeof form.requestSubmit === "function") {
316
- form.requestSubmit();
317
- } else {
318
- form.dispatchEvent(new Event("submit", { bubbles: true, cancelable: true }));
319
- }
320
- }, selector);
321
- break;
322
- }
323
- case "scroll": {
324
- await page.evaluate(() => {
325
- window.scrollBy(action.scrollX ?? 0, action.scrollY ?? 0);
326
- });
327
- break;
328
- }
329
- default:
330
- break;
331
- }
332
- }
333
- /** Replay a mouse trajectory (smooth movement between actions) */
334
- async replayTrajectory(trajectory) {
335
- const page = this.page;
336
- const { points } = trajectory;
337
- if (!points || points.length < 2) return;
338
- for (let i = 0; i < points.length; i++) {
339
- const { x, y, dt } = points[i];
340
- if (dt > 0) {
341
- await new Promise((r) => setTimeout(r, Math.min(dt, 200)));
342
- }
343
- await page.mouse.move(x, y);
344
- }
345
- }
346
- /** Resolve the best selector for an action (primary selector only) */
347
- resolveSelector(action) {
348
- const el = action.element;
349
- if (!el) return "";
350
- if (el.selector) {
351
- return el.selector;
352
- }
353
- if (el.tag) {
354
- return el.tag;
355
- }
356
- return "";
357
- }
358
- /**
359
- * X2: Wait for an element using the best available selector, with
360
- * confidence-based fallback support.
361
- *
362
- * Returns the first matching selector, or throws if none match.
363
- * Fallback order:
364
- * 1. Primary CSS selector (always tried first)
365
- * 2. textFallback selector (used when primary fails — not just for low
366
- * confidence, since high-confidence selectors from dynamic attributes
367
- * like data-spm-anchor-id can also fail on replay)
368
- * 3. Tag-based fallback (last resort)
369
- */
370
- async resolveAndWait(action) {
371
- const el = action.element;
372
- if (!el) throw new Error("No element metadata");
373
- const page = this.page;
374
- const timeout = this.opts.stepTimeout;
375
- const candidates = [];
376
- if (el.selector) candidates.push(el.selector);
377
- if (el.textFallback?.selector && !candidates.includes(el.textFallback.selector)) {
378
- candidates.push(el.textFallback.selector);
379
- }
380
- if (el.tag && !candidates.includes(el.tag)) {
381
- candidates.push(el.tag);
382
- }
383
- if (candidates.length === 0) throw new Error("No selector available for element");
384
- for (const sel of candidates) {
385
- try {
386
- await page.waitForSelector(sel, { state: "visible", timeout });
387
- return sel;
388
- } catch {
389
- }
390
- }
391
- throw new Error(`Element not found, tried: ${candidates.join(", ")}`);
392
- }
393
- /** Resolve file payloads from a filechooser action */
394
- resolveFiles(action) {
395
- if (!action.files?.fileData) return [];
396
- return action.files.fileData.filter((f) => f.dataUrl).map((f) => {
397
- const match = f.dataUrl.match(/^data:[^;]+;base64,(.+)$/);
398
- if (!match) return null;
399
- return {
400
- name: f.name,
401
- mimeType: f.type || "application/octet-stream",
402
- buffer: Buffer.from(match[1], "base64")
403
- };
404
- }).filter((f) => f !== null);
405
- }
406
- /** Clean up */
407
- async close() {
408
- this.page = null;
409
- }
410
- };
411
- export {
412
- SessionReplayer
413
- };