@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.
- package/README.md +17 -26
- package/dist/{browser-DSVV4GHS.js → browser-5CTOA2WS.js} +4 -3
- package/dist/{browser-53KUFEEM.js → browser-ITLZZDHJ.js} +5 -5
- package/dist/{browser-GURRY444.js → browser-IUJXXNBT.js} +6 -3
- package/dist/{cdp-driver-MNPR3HZH.js → cdp-driver-4X3DK6PS.js} +339 -59
- package/dist/{cdp-driver-SSXUGXP6.js → cdp-driver-D6WMSMWX.js} +4 -3
- package/dist/chunk-2SVQTI2O.js +2794 -0
- package/dist/{chunk-IDVD44ED.js → chunk-6WOSXSCQ.js} +23 -7
- package/dist/{chunk-ZZ2TFWIV.js → chunk-ABXMBNQ6.js} +1 -1
- package/dist/{chunk-2MFXKN32.js → chunk-ACFE6PKF.js} +1013 -119
- package/dist/chunk-AMI64BSD.js +268 -0
- package/dist/{chunk-E4O5ZU3H.js → chunk-DKWR54XQ.js} +412 -98
- package/dist/{chunk-DTJRVA76.js → chunk-ETCO4SNK.js} +2 -2
- package/dist/chunk-GDKLH7ZY.js +8 -0
- package/dist/chunk-KFQGP6VL.js +33 -0
- package/dist/{chunk-2BQZIT3S.js → chunk-LRBSUKUZ.js} +85 -2497
- package/dist/{chunk-ITKPSIP7.js → chunk-MDAPTB7C.js} +6 -25
- package/dist/{chunk-42RPMJ76.js → chunk-N2JFPWMI.js} +342 -60
- package/dist/chunk-OZKD3W4X.js +417 -0
- package/dist/{chunk-T4J4C2NZ.js → chunk-TNEN6VQ2.js} +17 -4
- package/dist/{chunk-YKOHDEFV.js → chunk-TWWOIJM7.js} +74 -38
- package/dist/chunk-WJRE55TN.js +83 -0
- package/dist/cli.js +1558 -1122
- package/dist/{convert-EGFYNICZ.js → convert-LB3GJTLR.js} +3 -3
- package/dist/{convert-EKQVHKB4.js → convert-R3XXYKC6.js} +2 -2
- package/dist/{daemon-client-YAVQ343A.js → daemon-client-3JOKX2L2.js} +3 -2
- package/dist/{daemon-client-3VM7VU7O.js → daemon-client-DIEHGP5B.js} +28 -74
- package/dist/daemon-main.js +2296 -1722
- package/dist/{extract-JUOQQX4V.js → extract-2ZFW2MX7.js} +1 -1
- package/dist/{extract-L2IW3IUB.js → extract-BSYBM4MR.js} +1 -1
- package/dist/{filter-HC4RA7JY.js → filter-KCFO4RSV.js} +1 -1
- package/dist/{filter-VID2GGZ7.js → filter-T7DSZ2X7.js} +1 -1
- package/dist/{human-interaction-W753RVJB.js → human-interaction-UKAS5ZXV.js} +2 -2
- package/dist/index.d.ts +166 -109
- package/dist/index.js +2668 -1742
- package/dist/launcher-L2JNDB2H.js +20 -0
- package/dist/{launcher-KA7J32K5.js → launcher-OZXJQPNG.js} +1 -1
- package/dist/{network-store-66A2RATI.js → network-store-XGZ25FFC.js} +1 -1
- package/dist/{network-store-BN6QEZ7R.js → network-store-YVDNUREI.js} +1 -1
- package/dist/{parse-action-dsl-T3DYC33D.js → parse-action-dsl-UM333TL2.js} +1 -1
- package/dist/{proxy-WKGUCH2C.js → proxy-C6CK3UH5.js} +2 -2
- package/dist/session-recorder-RTDGURIJ.js +8 -0
- package/dist/session-recorder-YI7YYM36.js +7 -0
- package/dist/session-replayer-MY27H4DX.js +276 -0
- package/dist/site-knowledge-SYC6VCDB.js +23 -0
- package/package.json +5 -4
- package/dist/screenshot-CWAWMXVA.js +0 -28
- package/dist/session-recorder-MA75PKTQ.js +0 -7
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ANTI_DETECT_ARGS,
|
|
3
|
+
DEFAULT_ARGS,
|
|
4
|
+
connectToCDP,
|
|
5
|
+
findChrome,
|
|
6
|
+
getCDPTargets,
|
|
7
|
+
killChrome,
|
|
8
|
+
launchChrome
|
|
9
|
+
} from "./chunk-TNEN6VQ2.js";
|
|
10
|
+
import "./chunk-GDKLH7ZY.js";
|
|
11
|
+
import "./chunk-KFQGP6VL.js";
|
|
12
|
+
export {
|
|
13
|
+
ANTI_DETECT_ARGS,
|
|
14
|
+
DEFAULT_ARGS,
|
|
15
|
+
connectToCDP,
|
|
16
|
+
findChrome,
|
|
17
|
+
getCDPTargets,
|
|
18
|
+
killChrome,
|
|
19
|
+
launchChrome
|
|
20
|
+
};
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import "./chunk-KFQGP6VL.js";
|
|
2
|
+
|
|
3
|
+
// src/recorder/session-replayer.ts
|
|
4
|
+
var SessionReplayer = class {
|
|
5
|
+
opts;
|
|
6
|
+
recording = null;
|
|
7
|
+
page = null;
|
|
8
|
+
constructor(opts) {
|
|
9
|
+
this.opts = {
|
|
10
|
+
cdpUrl: opts.cdpUrl,
|
|
11
|
+
page: opts.page,
|
|
12
|
+
stepDelay: opts.stepDelay ?? 500,
|
|
13
|
+
stepTimeout: opts.stepTimeout ?? 1e4,
|
|
14
|
+
onStep: opts.onStep,
|
|
15
|
+
onError: opts.onError
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
/** Load a recording from a file path or parsed JSON */
|
|
19
|
+
async load(source) {
|
|
20
|
+
if (typeof source === "string") {
|
|
21
|
+
const fs = await import("fs");
|
|
22
|
+
const raw = fs.readFileSync(source, "utf8");
|
|
23
|
+
this.recording = JSON.parse(raw);
|
|
24
|
+
} else {
|
|
25
|
+
this.recording = source;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Run the full replay */
|
|
29
|
+
async run() {
|
|
30
|
+
if (!this.recording) throw new Error("No recording loaded. Call load() first.");
|
|
31
|
+
if (this.opts.page) {
|
|
32
|
+
this.page = this.opts.page;
|
|
33
|
+
} else if (this.opts.cdpUrl) {
|
|
34
|
+
const { launch } = await import("./cdp-driver-D6WMSMWX.js");
|
|
35
|
+
const { browser } = await launch({ cdpEndpoint: this.opts.cdpUrl });
|
|
36
|
+
let contexts = browser.contexts();
|
|
37
|
+
for (let i = 0; i < 10 && contexts.length === 0; i++) {
|
|
38
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
39
|
+
contexts = browser.contexts();
|
|
40
|
+
}
|
|
41
|
+
const context = contexts[0];
|
|
42
|
+
if (!context) throw new Error("No browser context available");
|
|
43
|
+
const pages = context.pages();
|
|
44
|
+
this.page = pages[0];
|
|
45
|
+
}
|
|
46
|
+
if (!this.page) throw new Error("No page available. Provide cdpUrl or page.");
|
|
47
|
+
const actions = this.recording.actions;
|
|
48
|
+
let success = 0;
|
|
49
|
+
let failed = 0;
|
|
50
|
+
let skipped = 0;
|
|
51
|
+
for (let i = 0; i < actions.length; i++) {
|
|
52
|
+
const action = actions[i];
|
|
53
|
+
this.opts.onStep?.(action, i, actions.length);
|
|
54
|
+
try {
|
|
55
|
+
if (action.trajectory) {
|
|
56
|
+
await this.replayTrajectory(action.trajectory);
|
|
57
|
+
}
|
|
58
|
+
await this.replayAction(action);
|
|
59
|
+
success++;
|
|
60
|
+
} catch (e) {
|
|
61
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
62
|
+
this.opts.onError?.(action, err);
|
|
63
|
+
if (action.type === "navigation") {
|
|
64
|
+
skipped++;
|
|
65
|
+
} else {
|
|
66
|
+
failed++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (i < actions.length - 1) {
|
|
70
|
+
await new Promise((r) => setTimeout(r, this.opts.stepDelay));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { success, failed, skipped };
|
|
74
|
+
}
|
|
75
|
+
/** Replay a single action */
|
|
76
|
+
async replayAction(action) {
|
|
77
|
+
const page = this.page;
|
|
78
|
+
const timeout = this.opts.stepTimeout;
|
|
79
|
+
switch (action.type) {
|
|
80
|
+
case "navigation":
|
|
81
|
+
await page.goto(action.url, { waitUntil: "domcontentloaded", timeout });
|
|
82
|
+
break;
|
|
83
|
+
case "goto":
|
|
84
|
+
await page.goto(action.url, { waitUntil: "domcontentloaded", timeout });
|
|
85
|
+
break;
|
|
86
|
+
case "click":
|
|
87
|
+
case "cdp-click": {
|
|
88
|
+
const selector = this.resolveSelector(action);
|
|
89
|
+
await page.waitForSelector(selector, { state: "visible", timeout });
|
|
90
|
+
await page.click(selector, { timeout });
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
case "input": {
|
|
94
|
+
const selector = this.resolveSelector(action);
|
|
95
|
+
await page.waitForSelector(selector, { state: "visible", timeout });
|
|
96
|
+
await page.fill(selector, action.value ?? "", { timeout });
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
case "cdp-fill": {
|
|
100
|
+
const selector = this.resolveSelector(action);
|
|
101
|
+
await page.waitForSelector(selector, { state: "visible", timeout });
|
|
102
|
+
await page.fill(selector, action.value ?? "", { timeout });
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
case "change": {
|
|
106
|
+
const selector = this.resolveSelector(action);
|
|
107
|
+
await page.waitForSelector(selector, { state: "visible", timeout });
|
|
108
|
+
if (action.value) {
|
|
109
|
+
await page.selectOption(selector, action.value);
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
case "filechooser": {
|
|
114
|
+
const selector = this.resolveSelector(action);
|
|
115
|
+
await page.waitForSelector(selector, { state: "visible", timeout });
|
|
116
|
+
const files = this.resolveFiles(action);
|
|
117
|
+
if (files.length > 0) {
|
|
118
|
+
await page.setInputFiles(selector, files);
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
case "keydown": {
|
|
123
|
+
const key = action.key ?? "";
|
|
124
|
+
if (key === "Enter" || key === "Tab" || key === "Escape") {
|
|
125
|
+
await page.keyboard.press(key);
|
|
126
|
+
} else if (key === "Backspace") {
|
|
127
|
+
await page.keyboard.press("Backspace");
|
|
128
|
+
} else if (key === "Delete") {
|
|
129
|
+
await page.keyboard.press("Delete");
|
|
130
|
+
} else if (key.startsWith("Arrow")) {
|
|
131
|
+
await page.keyboard.press(key);
|
|
132
|
+
} else if (key.includes("+")) {
|
|
133
|
+
await page.keyboard.press(key.replace("Meta", "Meta").replace("Ctrl", "Control"));
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case "dblclick": {
|
|
138
|
+
const selector = this.resolveSelector(action);
|
|
139
|
+
if (selector) {
|
|
140
|
+
await page.waitForSelector(selector, { state: "visible", timeout });
|
|
141
|
+
await page.dblclick(selector, { timeout });
|
|
142
|
+
} else if (action.x !== void 0 && action.y !== void 0) {
|
|
143
|
+
await page.mouse.dblclick(action.x, action.y);
|
|
144
|
+
}
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case "contextmenu": {
|
|
148
|
+
const selector = this.resolveSelector(action);
|
|
149
|
+
if (selector) {
|
|
150
|
+
await page.waitForSelector(selector, { state: "visible", timeout });
|
|
151
|
+
await page.click(selector, { button: "right", timeout });
|
|
152
|
+
} else if (action.x !== void 0 && action.y !== void 0) {
|
|
153
|
+
await page.mouse.click(action.x, action.y, { button: "right" });
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
case "hover": {
|
|
158
|
+
const selector = this.resolveSelector(action);
|
|
159
|
+
if (selector) {
|
|
160
|
+
await page.waitForSelector(selector, { state: "visible", timeout });
|
|
161
|
+
await page.hover(selector);
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
case "drag": {
|
|
166
|
+
if (action.drag) {
|
|
167
|
+
const { fromX, fromY, toX, toY } = action.drag;
|
|
168
|
+
await page.mouse.move(fromX, fromY);
|
|
169
|
+
await page.mouse.down();
|
|
170
|
+
const steps = 5;
|
|
171
|
+
for (let i = 1; i <= steps; i++) {
|
|
172
|
+
await page.mouse.move(
|
|
173
|
+
fromX + (toX - fromX) * i / steps,
|
|
174
|
+
fromY + (toY - fromY) * i / steps
|
|
175
|
+
);
|
|
176
|
+
await new Promise((r) => setTimeout(r, 30));
|
|
177
|
+
}
|
|
178
|
+
await page.mouse.up();
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
case "resize": {
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
case "clipboard": {
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
case "touch": {
|
|
189
|
+
if (action.touch) {
|
|
190
|
+
const { touchType, touches } = action.touch;
|
|
191
|
+
if (touchType === "start" && touches.length > 0) {
|
|
192
|
+
await page.mouse.move(touches[0].x, touches[0].y);
|
|
193
|
+
await page.mouse.down();
|
|
194
|
+
} else if (touchType === "end" && touches.length > 0) {
|
|
195
|
+
await page.mouse.move(touches[0].x, touches[0].y);
|
|
196
|
+
await page.mouse.up();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
case "focus": {
|
|
202
|
+
const selector = this.resolveSelector(action);
|
|
203
|
+
if (selector && action.focus?.focusType === "focus") {
|
|
204
|
+
await page.waitForSelector(selector, { state: "visible", timeout });
|
|
205
|
+
await page.click(selector, { timeout });
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
case "visibility": {
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
case "submit": {
|
|
213
|
+
const selector = this.resolveSelector(action);
|
|
214
|
+
if (selector) {
|
|
215
|
+
await page.click(selector, { timeout });
|
|
216
|
+
}
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
case "scroll": {
|
|
220
|
+
await page.evaluate(() => {
|
|
221
|
+
window.scrollBy(action.scrollX ?? 0, action.scrollY ?? 0);
|
|
222
|
+
});
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
default:
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/** Replay a mouse trajectory (smooth movement between actions) */
|
|
230
|
+
async replayTrajectory(trajectory) {
|
|
231
|
+
const page = this.page;
|
|
232
|
+
const { points } = trajectory;
|
|
233
|
+
if (!points || points.length < 2) return;
|
|
234
|
+
for (let i = 0; i < points.length; i++) {
|
|
235
|
+
const { x, y, dt } = points[i];
|
|
236
|
+
if (dt > 0) {
|
|
237
|
+
await new Promise((r) => setTimeout(r, Math.min(dt, 200)));
|
|
238
|
+
}
|
|
239
|
+
await page.mouse.move(x, y);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/** Resolve the best selector for an action */
|
|
243
|
+
resolveSelector(action) {
|
|
244
|
+
const el = action.element;
|
|
245
|
+
if (!el) return "";
|
|
246
|
+
if (el.textFallback) {
|
|
247
|
+
}
|
|
248
|
+
if (el.selector) {
|
|
249
|
+
return el.selector;
|
|
250
|
+
}
|
|
251
|
+
if (el.tag) {
|
|
252
|
+
return el.tag;
|
|
253
|
+
}
|
|
254
|
+
return "";
|
|
255
|
+
}
|
|
256
|
+
/** Resolve file payloads from a filechooser action */
|
|
257
|
+
resolveFiles(action) {
|
|
258
|
+
if (!action.files?.fileData) return [];
|
|
259
|
+
return action.files.fileData.filter((f) => f.dataUrl).map((f) => {
|
|
260
|
+
const match = f.dataUrl.match(/^data:[^;]+;base64,(.+)$/);
|
|
261
|
+
if (!match) return null;
|
|
262
|
+
return {
|
|
263
|
+
name: f.name,
|
|
264
|
+
mimeType: f.type || "application/octet-stream",
|
|
265
|
+
buffer: Buffer.from(match[1], "base64")
|
|
266
|
+
};
|
|
267
|
+
}).filter((f) => f !== null);
|
|
268
|
+
}
|
|
269
|
+
/** Clean up */
|
|
270
|
+
async close() {
|
|
271
|
+
this.page = null;
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
export {
|
|
275
|
+
SessionReplayer
|
|
276
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
addKnownIssue,
|
|
3
|
+
getKnowledgeDir,
|
|
4
|
+
getKnowledgePath,
|
|
5
|
+
init_site_knowledge,
|
|
6
|
+
listSiteKnowledge,
|
|
7
|
+
readSiteKnowledge,
|
|
8
|
+
readSiteKnowledgeMarkdown,
|
|
9
|
+
toMarkdown,
|
|
10
|
+
updateSiteKnowledge
|
|
11
|
+
} from "./chunk-OZKD3W4X.js";
|
|
12
|
+
import "./chunk-KFQGP6VL.js";
|
|
13
|
+
init_site_knowledge();
|
|
14
|
+
export {
|
|
15
|
+
addKnownIssue,
|
|
16
|
+
getKnowledgeDir,
|
|
17
|
+
getKnowledgePath,
|
|
18
|
+
listSiteKnowledge,
|
|
19
|
+
readSiteKnowledge,
|
|
20
|
+
readSiteKnowledgeMarkdown,
|
|
21
|
+
toMarkdown,
|
|
22
|
+
updateSiteKnowledge
|
|
23
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xbrowser/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
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": {
|
|
@@ -74,16 +74,17 @@
|
|
|
74
74
|
"dev": "tsup --watch",
|
|
75
75
|
"lint": "eslint src/ bin/ --ext .ts",
|
|
76
76
|
"lint:plugin-contract": "node lint-scripts/check-plugin-contract.mjs",
|
|
77
|
+
"lint:docs": "node lint-scripts/check-docs-consistency.mjs",
|
|
77
78
|
"typecheck": "tsc --noEmit",
|
|
78
79
|
"start": "node dist/cli.js",
|
|
79
80
|
"test": "vitest run",
|
|
80
81
|
"test:watch": "vitest",
|
|
81
|
-
"test:e2e": "vitest run
|
|
82
|
-
"validate": "npm run typecheck && npm run lint && npm run build && npm test",
|
|
82
|
+
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
|
83
|
+
"validate": "npm run typecheck && npm run lint && npm run lint:docs && npm run lint:plugin-contract && npm run build && npm test",
|
|
83
84
|
"prepare": "husky"
|
|
84
85
|
},
|
|
85
86
|
"dependencies": {
|
|
86
|
-
"@dyyz1993/xcli-core": "^0.
|
|
87
|
+
"@dyyz1993/xcli-core": "^0.15.0",
|
|
87
88
|
"@types/react-syntax-highlighter": "^15.5.13",
|
|
88
89
|
"@types/turndown": "^5.0.6",
|
|
89
90
|
"cheerio": "^1.2.0",
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import "./chunk-3RG5ZIWI.js";
|
|
2
|
-
|
|
3
|
-
// src/hooks/screenshot.ts
|
|
4
|
-
var screenshotHook = {
|
|
5
|
-
name: "screenshot",
|
|
6
|
-
async onAfterCommand(ctx) {
|
|
7
|
-
try {
|
|
8
|
-
const quality = parseInt(process.env.XBROWSER_SCREENSHOT_QUALITY || "40");
|
|
9
|
-
const buffer = await ctx.page.screenshot({
|
|
10
|
-
type: "jpeg",
|
|
11
|
-
quality: Math.max(10, Math.min(100, quality))
|
|
12
|
-
});
|
|
13
|
-
const entry = {
|
|
14
|
-
step: ctx.command,
|
|
15
|
-
command: ctx.command,
|
|
16
|
-
base64: buffer.toString("base64"),
|
|
17
|
-
url: ctx.page.url(),
|
|
18
|
-
timestamp: Date.now()
|
|
19
|
-
};
|
|
20
|
-
return { screenshot: entry };
|
|
21
|
-
} catch {
|
|
22
|
-
return void 0;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
export {
|
|
27
|
-
screenshotHook
|
|
28
|
-
};
|