@browserbasehq/browse-cli 0.1.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.
- package/LICENSE +21 -0
- package/README.md +310 -0
- package/dist/index.js +1297 -0
- package/package.json +70 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1297 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
|
|
4
|
+
// src/index.ts
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { Stagehand } from "@browserbasehq/stagehand";
|
|
7
|
+
import { promises as fs } from "fs";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
import * as os from "os";
|
|
10
|
+
import * as net from "net";
|
|
11
|
+
import { spawn } from "child_process";
|
|
12
|
+
import * as readline from "readline";
|
|
13
|
+
var VERSION = "0.1.0";
|
|
14
|
+
var program = new Command();
|
|
15
|
+
var SOCKET_DIR = os.tmpdir();
|
|
16
|
+
function getSocketPath(session) {
|
|
17
|
+
return path.join(SOCKET_DIR, `browse-${session}.sock`);
|
|
18
|
+
}
|
|
19
|
+
function getPidPath(session) {
|
|
20
|
+
return path.join(SOCKET_DIR, `browse-${session}.pid`);
|
|
21
|
+
}
|
|
22
|
+
function getWsPath(session) {
|
|
23
|
+
return path.join(SOCKET_DIR, `browse-${session}.ws`);
|
|
24
|
+
}
|
|
25
|
+
function getChromePidPath(session) {
|
|
26
|
+
return path.join(SOCKET_DIR, `browse-${session}.chrome.pid`);
|
|
27
|
+
}
|
|
28
|
+
function getNetworkDir(session) {
|
|
29
|
+
return path.join(SOCKET_DIR, `browse-${session}-network`);
|
|
30
|
+
}
|
|
31
|
+
async function isDaemonRunning(session) {
|
|
32
|
+
try {
|
|
33
|
+
const pidFile = getPidPath(session);
|
|
34
|
+
const pid = parseInt(await fs.readFile(pidFile, "utf-8"));
|
|
35
|
+
process.kill(pid, 0);
|
|
36
|
+
const socketPath = getSocketPath(session);
|
|
37
|
+
await fs.access(socketPath);
|
|
38
|
+
return true;
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function cleanupStaleFiles(session) {
|
|
44
|
+
try {
|
|
45
|
+
await fs.unlink(getSocketPath(session));
|
|
46
|
+
} catch {
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
await fs.unlink(getPidPath(session));
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
await fs.unlink(getWsPath(session));
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
await fs.unlink(getChromePidPath(session));
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function killChromeProcesses(session) {
|
|
62
|
+
try {
|
|
63
|
+
const { exec } = await import("child_process");
|
|
64
|
+
const { promisify } = await import("util");
|
|
65
|
+
const execAsync = promisify(exec);
|
|
66
|
+
if (process.platform === "darwin" || process.platform === "linux") {
|
|
67
|
+
const { stdout } = await execAsync(
|
|
68
|
+
`pgrep -f "browse-${session}" || true`
|
|
69
|
+
);
|
|
70
|
+
const pids = stdout.trim().split("\n").filter(Boolean);
|
|
71
|
+
for (const pid of pids) {
|
|
72
|
+
try {
|
|
73
|
+
process.kill(parseInt(pid), "SIGTERM");
|
|
74
|
+
} catch {
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return pids.length > 0;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
} catch {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
var DEFAULT_VIEWPORT = { width: 1288, height: 711 };
|
|
85
|
+
async function runDaemon(session, headless) {
|
|
86
|
+
await cleanupStaleFiles(session);
|
|
87
|
+
await fs.writeFile(getPidPath(session), String(process.pid));
|
|
88
|
+
const stagehand = new Stagehand({
|
|
89
|
+
env: "LOCAL",
|
|
90
|
+
verbose: 0,
|
|
91
|
+
disablePino: true,
|
|
92
|
+
localBrowserLaunchOptions: {
|
|
93
|
+
headless,
|
|
94
|
+
viewport: DEFAULT_VIEWPORT
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
await stagehand.init();
|
|
98
|
+
const context = stagehand.context;
|
|
99
|
+
try {
|
|
100
|
+
const wsUrl = context.conn?.wsUrl || "unknown";
|
|
101
|
+
await fs.writeFile(getWsPath(session), wsUrl);
|
|
102
|
+
} catch {
|
|
103
|
+
}
|
|
104
|
+
networkSession = session;
|
|
105
|
+
const setupNetworkCapture = async (targetPage) => {
|
|
106
|
+
const cdpSession = targetPage.mainFrame().session;
|
|
107
|
+
const requestStartTimes = /* @__PURE__ */ new Map();
|
|
108
|
+
const requestDirs = /* @__PURE__ */ new Map();
|
|
109
|
+
cdpSession.on("Network.requestWillBeSent", async (params) => {
|
|
110
|
+
if (!networkEnabled || !networkDir) return;
|
|
111
|
+
const request = {
|
|
112
|
+
id: params.requestId,
|
|
113
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
114
|
+
method: params.request.method,
|
|
115
|
+
url: params.request.url,
|
|
116
|
+
headers: params.request.headers || {},
|
|
117
|
+
body: params.request.postData || null,
|
|
118
|
+
resourceType: params.type || "Other"
|
|
119
|
+
};
|
|
120
|
+
pendingRequests.set(params.requestId, request);
|
|
121
|
+
requestStartTimes.set(params.requestId, Date.now());
|
|
122
|
+
const requestDir = await writeRequestToFs(request);
|
|
123
|
+
if (requestDir) {
|
|
124
|
+
requestDirs.set(params.requestId, requestDir);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
cdpSession.on("Network.responseReceived", async (params) => {
|
|
128
|
+
if (!networkEnabled) return;
|
|
129
|
+
const requestDir = requestDirs.get(params.requestId);
|
|
130
|
+
if (!requestDir) return;
|
|
131
|
+
const startTime = requestStartTimes.get(params.requestId) || Date.now();
|
|
132
|
+
const duration = Date.now() - startTime;
|
|
133
|
+
const responseInfo = {
|
|
134
|
+
id: params.requestId,
|
|
135
|
+
status: params.response.status,
|
|
136
|
+
statusText: params.response.statusText || "",
|
|
137
|
+
headers: params.response.headers || {},
|
|
138
|
+
mimeType: params.response.mimeType || "",
|
|
139
|
+
body: null,
|
|
140
|
+
duration
|
|
141
|
+
};
|
|
142
|
+
params._responseInfo = responseInfo;
|
|
143
|
+
params._requestDir = requestDir;
|
|
144
|
+
});
|
|
145
|
+
cdpSession.on("Network.loadingFinished", async (params) => {
|
|
146
|
+
if (!networkEnabled) return;
|
|
147
|
+
const requestDir = requestDirs.get(params.requestId);
|
|
148
|
+
const pending = pendingRequests.get(params.requestId);
|
|
149
|
+
if (!requestDir || !pending) return;
|
|
150
|
+
const startTime = requestStartTimes.get(params.requestId) || Date.now();
|
|
151
|
+
const duration = Date.now() - startTime;
|
|
152
|
+
let body = null;
|
|
153
|
+
try {
|
|
154
|
+
const result = await cdpSession.send("Network.getResponseBody", {
|
|
155
|
+
requestId: params.requestId
|
|
156
|
+
});
|
|
157
|
+
body = result.body || null;
|
|
158
|
+
if (result.base64Encoded && body) {
|
|
159
|
+
body = `[base64] ${body.slice(0, 100)}...`;
|
|
160
|
+
}
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
const responseData = {
|
|
164
|
+
id: params.requestId,
|
|
165
|
+
status: 0,
|
|
166
|
+
statusText: "",
|
|
167
|
+
headers: {},
|
|
168
|
+
mimeType: "",
|
|
169
|
+
body,
|
|
170
|
+
duration
|
|
171
|
+
};
|
|
172
|
+
await writeResponseToFs(requestDir, responseData);
|
|
173
|
+
pendingRequests.delete(params.requestId);
|
|
174
|
+
requestStartTimes.delete(params.requestId);
|
|
175
|
+
requestDirs.delete(params.requestId);
|
|
176
|
+
});
|
|
177
|
+
cdpSession.on("Network.loadingFailed", async (params) => {
|
|
178
|
+
if (!networkEnabled) return;
|
|
179
|
+
const requestDir = requestDirs.get(params.requestId);
|
|
180
|
+
if (!requestDir) return;
|
|
181
|
+
const startTime = requestStartTimes.get(params.requestId) || Date.now();
|
|
182
|
+
const duration = Date.now() - startTime;
|
|
183
|
+
const responseData = {
|
|
184
|
+
id: params.requestId,
|
|
185
|
+
status: 0,
|
|
186
|
+
statusText: "Failed",
|
|
187
|
+
headers: {},
|
|
188
|
+
mimeType: "",
|
|
189
|
+
body: null,
|
|
190
|
+
duration,
|
|
191
|
+
error: params.errorText || "Unknown error"
|
|
192
|
+
};
|
|
193
|
+
await writeResponseToFs(requestDir, responseData);
|
|
194
|
+
pendingRequests.delete(params.requestId);
|
|
195
|
+
requestStartTimes.delete(params.requestId);
|
|
196
|
+
requestDirs.delete(params.requestId);
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
context._setupNetworkCapture = setupNetworkCapture;
|
|
200
|
+
const socketPath = getSocketPath(session);
|
|
201
|
+
const server = net.createServer((conn) => {
|
|
202
|
+
const rl = readline.createInterface({ input: conn });
|
|
203
|
+
rl.on("line", async (line) => {
|
|
204
|
+
let response;
|
|
205
|
+
try {
|
|
206
|
+
const request = JSON.parse(line);
|
|
207
|
+
const result = await executeCommand(
|
|
208
|
+
context,
|
|
209
|
+
request.command,
|
|
210
|
+
request.args
|
|
211
|
+
);
|
|
212
|
+
response = { success: true, result };
|
|
213
|
+
} catch (e) {
|
|
214
|
+
response = {
|
|
215
|
+
success: false,
|
|
216
|
+
error: e instanceof Error ? e.message : String(e)
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
conn.write(JSON.stringify(response) + "\n");
|
|
220
|
+
});
|
|
221
|
+
rl.on("close", () => {
|
|
222
|
+
conn.destroy();
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
server.listen(socketPath);
|
|
226
|
+
let shuttingDown = false;
|
|
227
|
+
const shutdown = async (signal) => {
|
|
228
|
+
if (shuttingDown) return;
|
|
229
|
+
shuttingDown = true;
|
|
230
|
+
server.close();
|
|
231
|
+
try {
|
|
232
|
+
await stagehand.close();
|
|
233
|
+
} catch {
|
|
234
|
+
}
|
|
235
|
+
await cleanupStaleFiles(session);
|
|
236
|
+
process.exit(0);
|
|
237
|
+
};
|
|
238
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
239
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
240
|
+
process.on("SIGHUP", () => shutdown("SIGHUP"));
|
|
241
|
+
process.on("uncaughtException", (err) => {
|
|
242
|
+
console.error("Uncaught exception:", err);
|
|
243
|
+
shutdown("uncaughtException");
|
|
244
|
+
});
|
|
245
|
+
process.on("unhandledRejection", (reason) => {
|
|
246
|
+
console.error("Unhandled rejection:", reason);
|
|
247
|
+
shutdown("unhandledRejection");
|
|
248
|
+
});
|
|
249
|
+
console.log(JSON.stringify({ daemon: "started", session, pid: process.pid }));
|
|
250
|
+
}
|
|
251
|
+
var refMap = {
|
|
252
|
+
xpathMap: {},
|
|
253
|
+
cssMap: {},
|
|
254
|
+
urlMap: {}
|
|
255
|
+
};
|
|
256
|
+
var networkEnabled = false;
|
|
257
|
+
var networkDir = null;
|
|
258
|
+
var networkCounter = 0;
|
|
259
|
+
var networkSession = null;
|
|
260
|
+
var pendingRequests = /* @__PURE__ */ new Map();
|
|
261
|
+
function sanitizeForFilename(str, maxLen = 30) {
|
|
262
|
+
return str.replace(/[^a-zA-Z0-9.-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, maxLen);
|
|
263
|
+
}
|
|
264
|
+
function getRequestDirName(counter, method, url) {
|
|
265
|
+
try {
|
|
266
|
+
const parsed = new URL(url);
|
|
267
|
+
const domain = sanitizeForFilename(parsed.hostname, 30);
|
|
268
|
+
const pathPart = parsed.pathname.split("/").filter(Boolean)[0] || "root";
|
|
269
|
+
const pathSlug = sanitizeForFilename(pathPart, 20);
|
|
270
|
+
return `${String(counter).padStart(3, "0")}-${method}-${domain}-${pathSlug}`;
|
|
271
|
+
} catch {
|
|
272
|
+
return `${String(counter).padStart(3, "0")}-${method}-unknown`;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
async function writeRequestToFs(request) {
|
|
276
|
+
if (!networkDir) return null;
|
|
277
|
+
const dirName = getRequestDirName(
|
|
278
|
+
networkCounter++,
|
|
279
|
+
request.method,
|
|
280
|
+
request.url
|
|
281
|
+
);
|
|
282
|
+
const requestDir = path.join(networkDir, dirName);
|
|
283
|
+
try {
|
|
284
|
+
await fs.mkdir(requestDir, { recursive: true });
|
|
285
|
+
const requestData = {
|
|
286
|
+
id: request.id,
|
|
287
|
+
timestamp: request.timestamp,
|
|
288
|
+
method: request.method,
|
|
289
|
+
url: request.url,
|
|
290
|
+
headers: request.headers,
|
|
291
|
+
body: request.body,
|
|
292
|
+
resourceType: request.resourceType
|
|
293
|
+
};
|
|
294
|
+
await fs.writeFile(
|
|
295
|
+
path.join(requestDir, "request.json"),
|
|
296
|
+
JSON.stringify(requestData, null, 2)
|
|
297
|
+
);
|
|
298
|
+
return requestDir;
|
|
299
|
+
} catch (err) {
|
|
300
|
+
console.error("Failed to write request:", err);
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async function writeResponseToFs(requestDir, response) {
|
|
305
|
+
try {
|
|
306
|
+
await fs.writeFile(
|
|
307
|
+
path.join(requestDir, "response.json"),
|
|
308
|
+
JSON.stringify(response, null, 2)
|
|
309
|
+
);
|
|
310
|
+
} catch (err) {
|
|
311
|
+
console.error("Failed to write response:", err);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function parseRef(selector) {
|
|
315
|
+
if (selector.startsWith("@")) {
|
|
316
|
+
const rest = selector.slice(1);
|
|
317
|
+
if (rest.startsWith("[") && rest.endsWith("]")) {
|
|
318
|
+
return rest.slice(1, -1);
|
|
319
|
+
}
|
|
320
|
+
return rest;
|
|
321
|
+
}
|
|
322
|
+
if (selector.startsWith("[") && selector.endsWith("]") && /^\[\d+-\d+\]$/.test(selector)) {
|
|
323
|
+
return selector.slice(1, -1);
|
|
324
|
+
}
|
|
325
|
+
if (selector.startsWith("ref=")) {
|
|
326
|
+
return selector.slice(4);
|
|
327
|
+
}
|
|
328
|
+
if (/^\d+-\d+$/.test(selector)) {
|
|
329
|
+
return selector;
|
|
330
|
+
}
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
function resolveSelector(selector) {
|
|
334
|
+
const ref = parseRef(selector);
|
|
335
|
+
if (ref) {
|
|
336
|
+
const css = refMap.cssMap[ref];
|
|
337
|
+
if (css) {
|
|
338
|
+
return css;
|
|
339
|
+
}
|
|
340
|
+
const xpath = refMap.xpathMap[ref];
|
|
341
|
+
if (!xpath) {
|
|
342
|
+
throw new Error(
|
|
343
|
+
`Unknown ref "${ref}" - run snapshot first to populate refs (have ${Object.keys(refMap.xpathMap).length} refs)`
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
return xpath;
|
|
347
|
+
}
|
|
348
|
+
return selector;
|
|
349
|
+
}
|
|
350
|
+
async function executeCommand(context, command, args) {
|
|
351
|
+
const page = context.activePage();
|
|
352
|
+
if (!page && command !== "pages" && command !== "newpage") {
|
|
353
|
+
throw new Error("No active page");
|
|
354
|
+
}
|
|
355
|
+
switch (command) {
|
|
356
|
+
// Navigation
|
|
357
|
+
case "open": {
|
|
358
|
+
const [url, waitUntil, timeout] = args;
|
|
359
|
+
await page.goto(url, {
|
|
360
|
+
waitUntil,
|
|
361
|
+
timeout: timeout ?? 3e4
|
|
362
|
+
});
|
|
363
|
+
return { url: page.url() };
|
|
364
|
+
}
|
|
365
|
+
case "reload": {
|
|
366
|
+
await page.reload();
|
|
367
|
+
return { url: page.url() };
|
|
368
|
+
}
|
|
369
|
+
case "back": {
|
|
370
|
+
await page.goBack();
|
|
371
|
+
return { url: page.url() };
|
|
372
|
+
}
|
|
373
|
+
case "forward": {
|
|
374
|
+
await page.goForward();
|
|
375
|
+
return { url: page.url() };
|
|
376
|
+
}
|
|
377
|
+
// Click by ref
|
|
378
|
+
case "click": {
|
|
379
|
+
const [selector, opts] = args;
|
|
380
|
+
const resolved = resolveSelector(selector);
|
|
381
|
+
const locator = page.deepLocator(resolved);
|
|
382
|
+
const { x, y } = await locator.centroid();
|
|
383
|
+
await page.click(x, y, {
|
|
384
|
+
button: opts?.button ?? "left",
|
|
385
|
+
clickCount: opts?.clickCount ?? 1
|
|
386
|
+
});
|
|
387
|
+
return {
|
|
388
|
+
clicked: true,
|
|
389
|
+
ref: selector,
|
|
390
|
+
x: Math.round(x),
|
|
391
|
+
y: Math.round(y)
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
// Click by coordinates
|
|
395
|
+
case "click_xy": {
|
|
396
|
+
const [x, y, opts] = args;
|
|
397
|
+
const result = await page.click(x, y, {
|
|
398
|
+
button: opts?.button ?? "left",
|
|
399
|
+
clickCount: opts?.clickCount ?? 1
|
|
400
|
+
});
|
|
401
|
+
if (opts?.returnXPath) {
|
|
402
|
+
return { clicked: true, xpath: result?.xpath };
|
|
403
|
+
}
|
|
404
|
+
return { clicked: true };
|
|
405
|
+
}
|
|
406
|
+
case "hover": {
|
|
407
|
+
const [x, y, opts] = args;
|
|
408
|
+
const result = await page.hover(x, y);
|
|
409
|
+
if (opts?.returnXPath) {
|
|
410
|
+
return { hovered: true, xpath: result?.xpath };
|
|
411
|
+
}
|
|
412
|
+
return { hovered: true };
|
|
413
|
+
}
|
|
414
|
+
case "scroll": {
|
|
415
|
+
const [x, y, deltaX, deltaY, opts] = args;
|
|
416
|
+
const result = await page.scroll(x, y, deltaX, deltaY);
|
|
417
|
+
if (opts?.returnXPath) {
|
|
418
|
+
return { scrolled: true, xpath: result?.xpath };
|
|
419
|
+
}
|
|
420
|
+
return { scrolled: true };
|
|
421
|
+
}
|
|
422
|
+
case "drag": {
|
|
423
|
+
const [fromX, fromY, toX, toY, opts] = args;
|
|
424
|
+
const result = await page.drag(fromX, fromY, toX, toY, {
|
|
425
|
+
steps: opts?.steps ?? 10
|
|
426
|
+
});
|
|
427
|
+
if (opts?.returnXPath) {
|
|
428
|
+
return { dragged: true, xpath: result?.xpath };
|
|
429
|
+
}
|
|
430
|
+
return { dragged: true };
|
|
431
|
+
}
|
|
432
|
+
// Keyboard
|
|
433
|
+
case "type": {
|
|
434
|
+
const [text, opts] = args;
|
|
435
|
+
await page.type(text, { delay: opts?.delay, humanize: opts?.mistakes });
|
|
436
|
+
return { typed: true };
|
|
437
|
+
}
|
|
438
|
+
case "press": {
|
|
439
|
+
const [key] = args;
|
|
440
|
+
await page.keyPress(key);
|
|
441
|
+
return { pressed: key };
|
|
442
|
+
}
|
|
443
|
+
// Element actions
|
|
444
|
+
case "fill": {
|
|
445
|
+
const [selector, value, opts] = args;
|
|
446
|
+
await page.deepLocator(resolveSelector(selector)).fill(value);
|
|
447
|
+
if (opts?.pressEnter) {
|
|
448
|
+
await page.keyPress("Enter");
|
|
449
|
+
}
|
|
450
|
+
return { filled: true, pressedEnter: opts?.pressEnter ?? false };
|
|
451
|
+
}
|
|
452
|
+
case "select": {
|
|
453
|
+
const [selector, values] = args;
|
|
454
|
+
await page.deepLocator(resolveSelector(selector)).selectOption(values);
|
|
455
|
+
return { selected: values };
|
|
456
|
+
}
|
|
457
|
+
case "highlight": {
|
|
458
|
+
const [selector, duration] = args;
|
|
459
|
+
await page.deepLocator(resolveSelector(selector)).highlight({ durationMs: duration ?? 2e3 });
|
|
460
|
+
return { highlighted: true };
|
|
461
|
+
}
|
|
462
|
+
// Page info
|
|
463
|
+
case "get": {
|
|
464
|
+
const [what, selector] = args;
|
|
465
|
+
switch (what) {
|
|
466
|
+
case "url":
|
|
467
|
+
return { url: page.url() };
|
|
468
|
+
case "title":
|
|
469
|
+
return { title: await page.title() };
|
|
470
|
+
case "text":
|
|
471
|
+
return {
|
|
472
|
+
text: await page.deepLocator(resolveSelector(selector)).textContent()
|
|
473
|
+
};
|
|
474
|
+
case "html":
|
|
475
|
+
return {
|
|
476
|
+
html: await page.deepLocator(resolveSelector(selector)).innerHTML()
|
|
477
|
+
};
|
|
478
|
+
case "value":
|
|
479
|
+
return {
|
|
480
|
+
value: await page.deepLocator(resolveSelector(selector)).inputValue()
|
|
481
|
+
};
|
|
482
|
+
case "box": {
|
|
483
|
+
const { x, y } = await page.deepLocator(resolveSelector(selector)).centroid();
|
|
484
|
+
return { x: Math.round(x), y: Math.round(y) };
|
|
485
|
+
}
|
|
486
|
+
case "visible":
|
|
487
|
+
return {
|
|
488
|
+
visible: await page.deepLocator(resolveSelector(selector)).isVisible()
|
|
489
|
+
};
|
|
490
|
+
case "checked":
|
|
491
|
+
return {
|
|
492
|
+
checked: await page.deepLocator(resolveSelector(selector)).isChecked()
|
|
493
|
+
};
|
|
494
|
+
default:
|
|
495
|
+
throw new Error(`Unknown get type: ${what}`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
// Screenshot
|
|
499
|
+
case "screenshot": {
|
|
500
|
+
const [opts] = args;
|
|
501
|
+
const buffer = await page.screenshot({
|
|
502
|
+
fullPage: opts?.fullPage,
|
|
503
|
+
type: opts?.type,
|
|
504
|
+
quality: opts?.quality,
|
|
505
|
+
clip: opts?.clip,
|
|
506
|
+
animations: opts?.animations,
|
|
507
|
+
caret: opts?.caret
|
|
508
|
+
});
|
|
509
|
+
if (opts?.path) {
|
|
510
|
+
await fs.writeFile(opts.path, buffer);
|
|
511
|
+
return { saved: opts.path };
|
|
512
|
+
}
|
|
513
|
+
return { base64: buffer.toString("base64") };
|
|
514
|
+
}
|
|
515
|
+
// Snapshot
|
|
516
|
+
case "snapshot": {
|
|
517
|
+
const [compact] = args;
|
|
518
|
+
const snapshot = await page.snapshot();
|
|
519
|
+
refMap = {
|
|
520
|
+
xpathMap: snapshot.xpathMap ?? {},
|
|
521
|
+
cssMap: snapshot.cssMap ?? {},
|
|
522
|
+
urlMap: snapshot.urlMap ?? {}
|
|
523
|
+
};
|
|
524
|
+
if (compact) {
|
|
525
|
+
return { tree: snapshot.formattedTree };
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
tree: snapshot.formattedTree,
|
|
529
|
+
xpathMap: snapshot.xpathMap,
|
|
530
|
+
urlMap: snapshot.urlMap,
|
|
531
|
+
cssMap: snapshot.cssMap
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
// Viewport
|
|
535
|
+
case "viewport": {
|
|
536
|
+
const [width, height, scale] = args;
|
|
537
|
+
await page.setViewportSize(width, height, {
|
|
538
|
+
deviceScaleFactor: scale ?? 1
|
|
539
|
+
});
|
|
540
|
+
return { viewport: { width, height } };
|
|
541
|
+
}
|
|
542
|
+
// Eval
|
|
543
|
+
case "eval": {
|
|
544
|
+
const [expr] = args;
|
|
545
|
+
const result = await page.evaluate(expr);
|
|
546
|
+
return { result };
|
|
547
|
+
}
|
|
548
|
+
// Wait
|
|
549
|
+
case "wait": {
|
|
550
|
+
const [type, arg, opts] = args;
|
|
551
|
+
switch (type) {
|
|
552
|
+
case "load":
|
|
553
|
+
await page.waitForLoadState(
|
|
554
|
+
arg ?? "load",
|
|
555
|
+
opts?.timeout ?? 3e4
|
|
556
|
+
);
|
|
557
|
+
break;
|
|
558
|
+
case "selector":
|
|
559
|
+
await page.waitForSelector(resolveSelector(arg), {
|
|
560
|
+
state: opts?.state ?? "visible",
|
|
561
|
+
timeout: opts?.timeout ?? 3e4
|
|
562
|
+
});
|
|
563
|
+
break;
|
|
564
|
+
case "timeout":
|
|
565
|
+
await page.waitForTimeout(parseInt(arg));
|
|
566
|
+
break;
|
|
567
|
+
default:
|
|
568
|
+
throw new Error(`Unknown wait type: ${type}`);
|
|
569
|
+
}
|
|
570
|
+
return { waited: true };
|
|
571
|
+
}
|
|
572
|
+
// Element state
|
|
573
|
+
case "is": {
|
|
574
|
+
const [check, selector] = args;
|
|
575
|
+
const locator = page.deepLocator(resolveSelector(selector));
|
|
576
|
+
switch (check) {
|
|
577
|
+
case "visible":
|
|
578
|
+
return { visible: await locator.isVisible() };
|
|
579
|
+
case "checked":
|
|
580
|
+
return { checked: await locator.isChecked() };
|
|
581
|
+
default:
|
|
582
|
+
throw new Error(`Unknown check: ${check}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
// Cursor
|
|
586
|
+
case "cursor": {
|
|
587
|
+
await page.enableCursorOverlay();
|
|
588
|
+
return { cursor: "enabled" };
|
|
589
|
+
}
|
|
590
|
+
// Multi-page
|
|
591
|
+
case "pages": {
|
|
592
|
+
const pages = context.pages();
|
|
593
|
+
return {
|
|
594
|
+
pages: pages.map((p, i) => ({
|
|
595
|
+
index: i,
|
|
596
|
+
url: p.url(),
|
|
597
|
+
targetId: p.targetId()
|
|
598
|
+
}))
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
case "newpage": {
|
|
602
|
+
const [url] = args;
|
|
603
|
+
const newPage = await context.newPage(url);
|
|
604
|
+
return {
|
|
605
|
+
created: true,
|
|
606
|
+
url: newPage.url(),
|
|
607
|
+
targetId: newPage.targetId()
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
case "tab_switch": {
|
|
611
|
+
const [index] = args;
|
|
612
|
+
const pages = context.pages();
|
|
613
|
+
if (index < 0 || index >= pages.length) {
|
|
614
|
+
throw new Error(
|
|
615
|
+
`Tab index ${index} out of range (0-${pages.length - 1})`
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
context.setActivePage(pages[index]);
|
|
619
|
+
return { switched: true, index, url: pages[index].url() };
|
|
620
|
+
}
|
|
621
|
+
case "tab_close": {
|
|
622
|
+
const [index] = args;
|
|
623
|
+
const pages = context.pages();
|
|
624
|
+
const targetIndex = index ?? pages.length - 1;
|
|
625
|
+
if (targetIndex < 0 || targetIndex >= pages.length) {
|
|
626
|
+
throw new Error(
|
|
627
|
+
`Tab index ${targetIndex} out of range (0-${pages.length - 1})`
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
if (pages.length === 1) {
|
|
631
|
+
throw new Error("Cannot close the last tab");
|
|
632
|
+
}
|
|
633
|
+
await pages[targetIndex].close();
|
|
634
|
+
return { closed: true, index: targetIndex };
|
|
635
|
+
}
|
|
636
|
+
// Debug: show current ref map
|
|
637
|
+
case "refs": {
|
|
638
|
+
return {
|
|
639
|
+
count: Object.keys(refMap.xpathMap).length,
|
|
640
|
+
xpathMap: refMap.xpathMap,
|
|
641
|
+
cssMap: refMap.cssMap,
|
|
642
|
+
urlMap: refMap.urlMap
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
// Network capture commands
|
|
646
|
+
case "network_enable": {
|
|
647
|
+
if (networkEnabled && networkDir) {
|
|
648
|
+
return { enabled: true, path: networkDir, alreadyEnabled: true };
|
|
649
|
+
}
|
|
650
|
+
const session = networkSession || "default";
|
|
651
|
+
networkDir = getNetworkDir(session);
|
|
652
|
+
await fs.mkdir(networkDir, { recursive: true });
|
|
653
|
+
networkCounter = 0;
|
|
654
|
+
pendingRequests.clear();
|
|
655
|
+
const cdpSession = page.mainFrame().session;
|
|
656
|
+
await cdpSession.send("Network.enable", {
|
|
657
|
+
maxTotalBufferSize: 1e7,
|
|
658
|
+
maxResourceBufferSize: 5e6
|
|
659
|
+
});
|
|
660
|
+
const setupFn = context._setupNetworkCapture;
|
|
661
|
+
if (setupFn) {
|
|
662
|
+
await setupFn(page);
|
|
663
|
+
}
|
|
664
|
+
networkEnabled = true;
|
|
665
|
+
return { enabled: true, path: networkDir };
|
|
666
|
+
}
|
|
667
|
+
case "network_disable": {
|
|
668
|
+
if (!networkEnabled) {
|
|
669
|
+
return { enabled: false, alreadyDisabled: true };
|
|
670
|
+
}
|
|
671
|
+
try {
|
|
672
|
+
const cdpSession = page.mainFrame().session;
|
|
673
|
+
await cdpSession.send("Network.disable");
|
|
674
|
+
} catch {
|
|
675
|
+
}
|
|
676
|
+
networkEnabled = false;
|
|
677
|
+
return { enabled: false, path: networkDir };
|
|
678
|
+
}
|
|
679
|
+
case "network_path": {
|
|
680
|
+
if (!networkDir) {
|
|
681
|
+
const session = networkSession || "default";
|
|
682
|
+
return { path: getNetworkDir(session), enabled: false };
|
|
683
|
+
}
|
|
684
|
+
return { path: networkDir, enabled: networkEnabled };
|
|
685
|
+
}
|
|
686
|
+
case "network_clear": {
|
|
687
|
+
if (!networkDir) {
|
|
688
|
+
return { cleared: false, error: "Network capture not enabled" };
|
|
689
|
+
}
|
|
690
|
+
try {
|
|
691
|
+
const entries = await fs.readdir(networkDir, { withFileTypes: true });
|
|
692
|
+
for (const entry of entries) {
|
|
693
|
+
if (entry.isDirectory()) {
|
|
694
|
+
await fs.rm(path.join(networkDir, entry.name), { recursive: true });
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
networkCounter = 0;
|
|
698
|
+
pendingRequests.clear();
|
|
699
|
+
return { cleared: true, path: networkDir };
|
|
700
|
+
} catch (err) {
|
|
701
|
+
return {
|
|
702
|
+
cleared: false,
|
|
703
|
+
error: err instanceof Error ? err.message : String(err)
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
// Daemon control
|
|
708
|
+
case "stop": {
|
|
709
|
+
process.nextTick(() => {
|
|
710
|
+
process.emit("SIGTERM");
|
|
711
|
+
});
|
|
712
|
+
return { stopping: true };
|
|
713
|
+
}
|
|
714
|
+
default:
|
|
715
|
+
throw new Error(`Unknown command: ${command}`);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
async function sendCommandOnce(session, command, args) {
|
|
719
|
+
return new Promise((resolve, reject) => {
|
|
720
|
+
const socketPath = getSocketPath(session);
|
|
721
|
+
const client = net.createConnection(socketPath);
|
|
722
|
+
let done = false;
|
|
723
|
+
const timeout = setTimeout(() => {
|
|
724
|
+
cleanup();
|
|
725
|
+
reject(new Error("Command timeout"));
|
|
726
|
+
}, 6e4);
|
|
727
|
+
const cleanup = () => {
|
|
728
|
+
if (!done) {
|
|
729
|
+
done = true;
|
|
730
|
+
clearTimeout(timeout);
|
|
731
|
+
rl.close();
|
|
732
|
+
client.destroy();
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
const rl = readline.createInterface({ input: client });
|
|
736
|
+
rl.on("line", (line) => {
|
|
737
|
+
const response = JSON.parse(line);
|
|
738
|
+
cleanup();
|
|
739
|
+
if (response.success) {
|
|
740
|
+
resolve(response.result);
|
|
741
|
+
} else {
|
|
742
|
+
reject(new Error(response.error));
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
rl.on("error", () => {
|
|
746
|
+
});
|
|
747
|
+
client.on("connect", () => {
|
|
748
|
+
const request = { command, args };
|
|
749
|
+
client.write(JSON.stringify(request) + "\n");
|
|
750
|
+
});
|
|
751
|
+
client.on("error", (err) => {
|
|
752
|
+
cleanup();
|
|
753
|
+
reject(new Error(`Connection failed: ${err.message}`));
|
|
754
|
+
});
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
async function sendCommand(session, command, args, headless = false) {
|
|
758
|
+
try {
|
|
759
|
+
return await sendCommandOnce(session, command, args);
|
|
760
|
+
} catch (err) {
|
|
761
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
762
|
+
if (command === "stop") {
|
|
763
|
+
throw err;
|
|
764
|
+
}
|
|
765
|
+
const isConnectionError = errMsg.includes("ENOENT") || errMsg.includes("ECONNREFUSED") || errMsg.includes("Connection failed");
|
|
766
|
+
if (!isConnectionError) {
|
|
767
|
+
throw err;
|
|
768
|
+
}
|
|
769
|
+
await killChromeProcesses(session);
|
|
770
|
+
await cleanupStaleFiles(session);
|
|
771
|
+
await ensureDaemon(session, headless);
|
|
772
|
+
return await sendCommandOnce(session, command, args);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
async function ensureDaemon(session, headless) {
|
|
776
|
+
if (await isDaemonRunning(session)) {
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
const args = ["--session", session, "daemon"];
|
|
780
|
+
if (headless) args.push("--headless");
|
|
781
|
+
const child = spawn(process.argv[0], [process.argv[1], ...args], {
|
|
782
|
+
detached: true,
|
|
783
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
784
|
+
});
|
|
785
|
+
return new Promise((resolve, reject) => {
|
|
786
|
+
let done = false;
|
|
787
|
+
const cleanup = () => {
|
|
788
|
+
if (!done) {
|
|
789
|
+
done = true;
|
|
790
|
+
rl.close();
|
|
791
|
+
child.stdout?.destroy();
|
|
792
|
+
child.unref();
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
const timeout = setTimeout(() => {
|
|
796
|
+
cleanup();
|
|
797
|
+
reject(new Error("Timeout waiting for daemon to start"));
|
|
798
|
+
}, 3e4);
|
|
799
|
+
const rl = readline.createInterface({ input: child.stdout });
|
|
800
|
+
rl.on("line", (line) => {
|
|
801
|
+
try {
|
|
802
|
+
const data = JSON.parse(line);
|
|
803
|
+
if (data.daemon === "started") {
|
|
804
|
+
clearTimeout(timeout);
|
|
805
|
+
cleanup();
|
|
806
|
+
setTimeout(() => resolve(), 50);
|
|
807
|
+
}
|
|
808
|
+
} catch {
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
child.on("error", (err) => {
|
|
812
|
+
clearTimeout(timeout);
|
|
813
|
+
cleanup();
|
|
814
|
+
reject(err);
|
|
815
|
+
});
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
function getSession(opts) {
|
|
819
|
+
return opts.session ?? process.env.BROWSE_SESSION ?? "default";
|
|
820
|
+
}
|
|
821
|
+
function isHeadless(opts) {
|
|
822
|
+
return opts.headless === true && opts.headed !== true;
|
|
823
|
+
}
|
|
824
|
+
function output(data, json) {
|
|
825
|
+
if (json) {
|
|
826
|
+
console.log(JSON.stringify(data, null, 2));
|
|
827
|
+
} else if (typeof data === "string") {
|
|
828
|
+
console.log(data);
|
|
829
|
+
} else {
|
|
830
|
+
console.log(JSON.stringify(data, null, 2));
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
async function runCommand(command, args) {
|
|
834
|
+
const opts = program.opts();
|
|
835
|
+
const session = getSession(opts);
|
|
836
|
+
const headless = isHeadless(opts);
|
|
837
|
+
if (opts.ws) {
|
|
838
|
+
const stagehand = new Stagehand({
|
|
839
|
+
env: "LOCAL",
|
|
840
|
+
verbose: 0,
|
|
841
|
+
disablePino: true,
|
|
842
|
+
localBrowserLaunchOptions: {
|
|
843
|
+
cdpUrl: opts.ws
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
await stagehand.init();
|
|
847
|
+
try {
|
|
848
|
+
return await executeCommand(stagehand.context, command, args);
|
|
849
|
+
} finally {
|
|
850
|
+
await stagehand.close();
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
await ensureDaemon(session, headless);
|
|
854
|
+
return sendCommand(session, command, args, headless);
|
|
855
|
+
}
|
|
856
|
+
program.name("browse").description("Browser automation CLI for AI agents").version(VERSION).option(
|
|
857
|
+
"--ws <url>",
|
|
858
|
+
"CDP WebSocket URL (bypasses daemon, direct connection)"
|
|
859
|
+
).option("--headless", "Run Chrome in headless mode").option("--headed", "Run Chrome with visible window (default)").option("--json", "Output as JSON", false).option(
|
|
860
|
+
"--session <name>",
|
|
861
|
+
"Session name for multiple browsers (or use BROWSE_SESSION env var)",
|
|
862
|
+
"default"
|
|
863
|
+
);
|
|
864
|
+
program.command("start").description("Start browser daemon (auto-started by other commands)").action(async () => {
|
|
865
|
+
const opts = program.opts();
|
|
866
|
+
const session = getSession(opts);
|
|
867
|
+
if (await isDaemonRunning(session)) {
|
|
868
|
+
console.log(JSON.stringify({ status: "already running", session }));
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
await ensureDaemon(session, isHeadless(opts));
|
|
872
|
+
console.log(JSON.stringify({ status: "started", session }));
|
|
873
|
+
});
|
|
874
|
+
program.command("stop").description("Stop browser daemon").option("--force", "Force kill Chrome processes if daemon is unresponsive").action(async (cmdOpts) => {
|
|
875
|
+
const opts = program.opts();
|
|
876
|
+
const session = getSession(opts);
|
|
877
|
+
try {
|
|
878
|
+
await sendCommand(session, "stop", []);
|
|
879
|
+
console.log(JSON.stringify({ status: "stopped", session }));
|
|
880
|
+
} catch {
|
|
881
|
+
if (cmdOpts.force) {
|
|
882
|
+
await killChromeProcesses(session);
|
|
883
|
+
await cleanupStaleFiles(session);
|
|
884
|
+
console.log(JSON.stringify({ status: "force stopped", session }));
|
|
885
|
+
} else {
|
|
886
|
+
console.log(JSON.stringify({ status: "not running", session }));
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
program.command("status").description("Check daemon status").action(async () => {
|
|
891
|
+
const opts = program.opts();
|
|
892
|
+
const session = getSession(opts);
|
|
893
|
+
const running = await isDaemonRunning(session);
|
|
894
|
+
let wsUrl = null;
|
|
895
|
+
if (running) {
|
|
896
|
+
try {
|
|
897
|
+
wsUrl = await fs.readFile(getWsPath(session), "utf-8");
|
|
898
|
+
} catch {
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
console.log(JSON.stringify({ running, session, wsUrl }));
|
|
902
|
+
});
|
|
903
|
+
program.command("refs").description("Show cached ref map from last snapshot").action(async () => {
|
|
904
|
+
const opts = program.opts();
|
|
905
|
+
try {
|
|
906
|
+
const result = await runCommand("refs", []);
|
|
907
|
+
output(result, opts.json ?? false);
|
|
908
|
+
} catch (e) {
|
|
909
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
910
|
+
process.exit(1);
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
program.command("daemon").description("Run as daemon (internal use)").action(async () => {
|
|
914
|
+
const opts = program.opts();
|
|
915
|
+
await runDaemon(getSession(opts), isHeadless(opts));
|
|
916
|
+
});
|
|
917
|
+
program.command("open <url>").alias("goto").description("Navigate to URL").option(
|
|
918
|
+
"--wait <state>",
|
|
919
|
+
"Wait state: load, domcontentloaded, networkidle",
|
|
920
|
+
"load"
|
|
921
|
+
).option("-t, --timeout <ms>", "Navigation timeout in milliseconds", "30000").action(async (url, cmdOpts) => {
|
|
922
|
+
const opts = program.opts();
|
|
923
|
+
try {
|
|
924
|
+
const result = await runCommand("open", [
|
|
925
|
+
url,
|
|
926
|
+
cmdOpts.wait,
|
|
927
|
+
parseInt(cmdOpts.timeout)
|
|
928
|
+
]);
|
|
929
|
+
output(result, opts.json ?? false);
|
|
930
|
+
} catch (e) {
|
|
931
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
932
|
+
process.exit(1);
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
program.command("reload").description("Reload current page").action(async () => {
|
|
936
|
+
const opts = program.opts();
|
|
937
|
+
try {
|
|
938
|
+
const result = await runCommand("reload", []);
|
|
939
|
+
output(result, opts.json ?? false);
|
|
940
|
+
} catch (e) {
|
|
941
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
942
|
+
process.exit(1);
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
program.command("back").description("Go back in history").action(async () => {
|
|
946
|
+
const opts = program.opts();
|
|
947
|
+
try {
|
|
948
|
+
const result = await runCommand("back", []);
|
|
949
|
+
output(result, opts.json ?? false);
|
|
950
|
+
} catch (e) {
|
|
951
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
952
|
+
process.exit(1);
|
|
953
|
+
}
|
|
954
|
+
});
|
|
955
|
+
program.command("forward").description("Go forward in history").action(async () => {
|
|
956
|
+
const opts = program.opts();
|
|
957
|
+
try {
|
|
958
|
+
const result = await runCommand("forward", []);
|
|
959
|
+
output(result, opts.json ?? false);
|
|
960
|
+
} catch (e) {
|
|
961
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
962
|
+
process.exit(1);
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
program.command("click <ref>").description("Click element by ref (e.g., @0-5, 0-5, or CSS/XPath selector)").option("-b, --button <btn>", "Mouse button: left, right, middle", "left").option("-c, --count <n>", "Click count", "1").action(async (ref, cmdOpts) => {
|
|
966
|
+
const opts = program.opts();
|
|
967
|
+
try {
|
|
968
|
+
const result = await runCommand("click", [
|
|
969
|
+
ref,
|
|
970
|
+
{ button: cmdOpts.button, clickCount: parseInt(cmdOpts.count) }
|
|
971
|
+
]);
|
|
972
|
+
output(result, opts.json ?? false);
|
|
973
|
+
} catch (e) {
|
|
974
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
975
|
+
process.exit(1);
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
program.command("click_xy <x> <y>").description("Click at exact coordinates").option("-b, --button <btn>", "Mouse button: left, right, middle", "left").option("-c, --count <n>", "Click count", "1").option("--xpath", "Return XPath of clicked element").action(async (x, y, cmdOpts) => {
|
|
979
|
+
const opts = program.opts();
|
|
980
|
+
try {
|
|
981
|
+
const result = await runCommand("click_xy", [
|
|
982
|
+
parseFloat(x),
|
|
983
|
+
parseFloat(y),
|
|
984
|
+
{
|
|
985
|
+
button: cmdOpts.button,
|
|
986
|
+
clickCount: parseInt(cmdOpts.count),
|
|
987
|
+
returnXPath: cmdOpts.xpath
|
|
988
|
+
}
|
|
989
|
+
]);
|
|
990
|
+
output(result, opts.json ?? false);
|
|
991
|
+
} catch (e) {
|
|
992
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
993
|
+
process.exit(1);
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
program.command("hover <x> <y>").description("Hover at coordinates").option("--xpath", "Return XPath of hovered element").action(async (x, y, cmdOpts) => {
|
|
997
|
+
const opts = program.opts();
|
|
998
|
+
try {
|
|
999
|
+
const result = await runCommand("hover", [
|
|
1000
|
+
parseFloat(x),
|
|
1001
|
+
parseFloat(y),
|
|
1002
|
+
{ returnXPath: cmdOpts.xpath }
|
|
1003
|
+
]);
|
|
1004
|
+
output(result, opts.json ?? false);
|
|
1005
|
+
} catch (e) {
|
|
1006
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
1007
|
+
process.exit(1);
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
program.command("scroll <x> <y> <deltaX> <deltaY>").description("Scroll at coordinates").option("--xpath", "Return XPath of scrolled element").action(async (x, y, dx, dy, cmdOpts) => {
|
|
1011
|
+
const opts = program.opts();
|
|
1012
|
+
try {
|
|
1013
|
+
const result = await runCommand("scroll", [
|
|
1014
|
+
parseFloat(x),
|
|
1015
|
+
parseFloat(y),
|
|
1016
|
+
parseFloat(dx),
|
|
1017
|
+
parseFloat(dy),
|
|
1018
|
+
{ returnXPath: cmdOpts.xpath }
|
|
1019
|
+
]);
|
|
1020
|
+
output(result, opts.json ?? false);
|
|
1021
|
+
} catch (e) {
|
|
1022
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
1023
|
+
process.exit(1);
|
|
1024
|
+
}
|
|
1025
|
+
});
|
|
1026
|
+
program.command("drag <fromX> <fromY> <toX> <toY>").description("Drag from one point to another").option("--steps <n>", "Number of steps", "10").option("--xpath", "Return XPath of dragged element").action(async (fx, fy, tx, ty, cmdOpts) => {
|
|
1027
|
+
const opts = program.opts();
|
|
1028
|
+
try {
|
|
1029
|
+
const result = await runCommand("drag", [
|
|
1030
|
+
parseFloat(fx),
|
|
1031
|
+
parseFloat(fy),
|
|
1032
|
+
parseFloat(tx),
|
|
1033
|
+
parseFloat(ty),
|
|
1034
|
+
{ steps: parseInt(cmdOpts.steps), returnXPath: cmdOpts.xpath }
|
|
1035
|
+
]);
|
|
1036
|
+
output(result, opts.json ?? false);
|
|
1037
|
+
} catch (e) {
|
|
1038
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
1039
|
+
process.exit(1);
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
program.command("type <text>").description("Type text").option("-d, --delay <ms>", "Delay between keystrokes").option("--mistakes", "Enable human-like typing with mistakes").action(async (text, cmdOpts) => {
|
|
1043
|
+
const opts = program.opts();
|
|
1044
|
+
try {
|
|
1045
|
+
const result = await runCommand("type", [
|
|
1046
|
+
text,
|
|
1047
|
+
{
|
|
1048
|
+
delay: cmdOpts.delay ? parseInt(cmdOpts.delay) : void 0,
|
|
1049
|
+
mistakes: cmdOpts.mistakes
|
|
1050
|
+
}
|
|
1051
|
+
]);
|
|
1052
|
+
output(result, opts.json ?? false);
|
|
1053
|
+
} catch (e) {
|
|
1054
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
1055
|
+
process.exit(1);
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
program.command("press <key>").alias("key").description("Press key (e.g., Enter, Tab, Escape, Cmd+A)").action(async (key) => {
|
|
1059
|
+
const opts = program.opts();
|
|
1060
|
+
try {
|
|
1061
|
+
const result = await runCommand("press", [key]);
|
|
1062
|
+
output(result, opts.json ?? false);
|
|
1063
|
+
} catch (e) {
|
|
1064
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
1065
|
+
process.exit(1);
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
program.command("fill <selector> <value>").description("Fill input element (presses Enter by default)").option("--no-press-enter", "Don't press Enter after filling").action(async (selector, value, cmdOpts) => {
|
|
1069
|
+
const opts = program.opts();
|
|
1070
|
+
try {
|
|
1071
|
+
const pressEnter = cmdOpts.pressEnter !== false;
|
|
1072
|
+
const result = await runCommand("fill", [
|
|
1073
|
+
selector,
|
|
1074
|
+
value,
|
|
1075
|
+
{ pressEnter }
|
|
1076
|
+
]);
|
|
1077
|
+
output(result, opts.json ?? false);
|
|
1078
|
+
} catch (e) {
|
|
1079
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
1080
|
+
process.exit(1);
|
|
1081
|
+
}
|
|
1082
|
+
});
|
|
1083
|
+
program.command("select <selector> <values...>").description("Select option(s)").action(async (selector, values) => {
|
|
1084
|
+
const opts = program.opts();
|
|
1085
|
+
try {
|
|
1086
|
+
const result = await runCommand("select", [selector, values]);
|
|
1087
|
+
output(result, opts.json ?? false);
|
|
1088
|
+
} catch (e) {
|
|
1089
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
1090
|
+
process.exit(1);
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
program.command("highlight <selector>").description("Highlight element").option("-d, --duration <ms>", "Duration", "2000").action(async (selector, cmdOpts) => {
|
|
1094
|
+
const opts = program.opts();
|
|
1095
|
+
try {
|
|
1096
|
+
const result = await runCommand("highlight", [
|
|
1097
|
+
selector,
|
|
1098
|
+
parseInt(cmdOpts.duration)
|
|
1099
|
+
]);
|
|
1100
|
+
output(result, opts.json ?? false);
|
|
1101
|
+
} catch (e) {
|
|
1102
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
1103
|
+
process.exit(1);
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
program.command("get <what> [selector]").description("Get page info: url, title, text, html, value, box").action(async (what, selector) => {
|
|
1107
|
+
const opts = program.opts();
|
|
1108
|
+
try {
|
|
1109
|
+
const result = await runCommand("get", [what, selector]);
|
|
1110
|
+
output(result, opts.json ?? false);
|
|
1111
|
+
} catch (e) {
|
|
1112
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
1113
|
+
process.exit(1);
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
1116
|
+
program.command("screenshot [path]").description("Take screenshot").option("-f, --full-page", "Full page screenshot").option("-t, --type <type>", "Image type: png, jpeg", "png").option("-q, --quality <n>", "JPEG quality (0-100)").option("--clip <json>", "Clip region as JSON").option("--no-animations", "Disable animations").option("--hide-caret", "Hide text caret").action(async (filePath, cmdOpts) => {
|
|
1117
|
+
const opts = program.opts();
|
|
1118
|
+
try {
|
|
1119
|
+
const result = await runCommand("screenshot", [
|
|
1120
|
+
{
|
|
1121
|
+
path: filePath,
|
|
1122
|
+
fullPage: cmdOpts.fullPage,
|
|
1123
|
+
type: cmdOpts.type,
|
|
1124
|
+
quality: cmdOpts.quality ? parseInt(cmdOpts.quality) : void 0,
|
|
1125
|
+
clip: cmdOpts.clip ? JSON.parse(cmdOpts.clip) : void 0,
|
|
1126
|
+
animations: cmdOpts.animations === false ? "disabled" : "allow",
|
|
1127
|
+
caret: cmdOpts.hideCaret ? "hide" : "initial"
|
|
1128
|
+
}
|
|
1129
|
+
]);
|
|
1130
|
+
output(result, opts.json ?? false);
|
|
1131
|
+
} catch (e) {
|
|
1132
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
1133
|
+
process.exit(1);
|
|
1134
|
+
}
|
|
1135
|
+
});
|
|
1136
|
+
program.command("snapshot").description("Get accessibility tree snapshot").option("-c, --compact", "Output tree only (no xpath map)").action(async (cmdOpts) => {
|
|
1137
|
+
const opts = program.opts();
|
|
1138
|
+
try {
|
|
1139
|
+
const result = await runCommand("snapshot", [cmdOpts.compact]);
|
|
1140
|
+
if (cmdOpts.compact && !opts.json) {
|
|
1141
|
+
console.log(result.tree);
|
|
1142
|
+
} else {
|
|
1143
|
+
output(result, opts.json ?? false);
|
|
1144
|
+
}
|
|
1145
|
+
} catch (e) {
|
|
1146
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
1147
|
+
process.exit(1);
|
|
1148
|
+
}
|
|
1149
|
+
});
|
|
1150
|
+
program.command("viewport <width> <height>").description("Set viewport size").option("-s, --scale <n>", "Device scale factor", "1").action(async (w, h, cmdOpts) => {
|
|
1151
|
+
const opts = program.opts();
|
|
1152
|
+
try {
|
|
1153
|
+
const result = await runCommand("viewport", [
|
|
1154
|
+
parseInt(w),
|
|
1155
|
+
parseInt(h),
|
|
1156
|
+
parseFloat(cmdOpts.scale)
|
|
1157
|
+
]);
|
|
1158
|
+
output(result, opts.json ?? false);
|
|
1159
|
+
} catch (e) {
|
|
1160
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
1161
|
+
process.exit(1);
|
|
1162
|
+
}
|
|
1163
|
+
});
|
|
1164
|
+
program.command("eval <expression>").description("Evaluate JavaScript in page").action(async (expr) => {
|
|
1165
|
+
const opts = program.opts();
|
|
1166
|
+
try {
|
|
1167
|
+
const result = await runCommand("eval", [expr]);
|
|
1168
|
+
output(result, opts.json ?? false);
|
|
1169
|
+
} catch (e) {
|
|
1170
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
1171
|
+
process.exit(1);
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
program.command("wait <type> [arg]").description("Wait for: load, selector, timeout").option("-t, --timeout <ms>", "Timeout", "30000").option(
|
|
1175
|
+
"-s, --state <state>",
|
|
1176
|
+
"Element state: visible, hidden, attached, detached",
|
|
1177
|
+
"visible"
|
|
1178
|
+
).action(async (type, arg, cmdOpts) => {
|
|
1179
|
+
const opts = program.opts();
|
|
1180
|
+
try {
|
|
1181
|
+
const result = await runCommand("wait", [
|
|
1182
|
+
type,
|
|
1183
|
+
arg,
|
|
1184
|
+
{ timeout: parseInt(cmdOpts.timeout), state: cmdOpts.state }
|
|
1185
|
+
]);
|
|
1186
|
+
output(result, opts.json ?? false);
|
|
1187
|
+
} catch (e) {
|
|
1188
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
1189
|
+
process.exit(1);
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
program.command("is <check> <selector>").description("Check element state: visible, checked").action(async (check, selector) => {
|
|
1193
|
+
const opts = program.opts();
|
|
1194
|
+
try {
|
|
1195
|
+
const result = await runCommand("is", [check, selector]);
|
|
1196
|
+
output(result, opts.json ?? false);
|
|
1197
|
+
} catch (e) {
|
|
1198
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
1199
|
+
process.exit(1);
|
|
1200
|
+
}
|
|
1201
|
+
});
|
|
1202
|
+
program.command("cursor").description("Enable visual cursor overlay").action(async () => {
|
|
1203
|
+
const opts = program.opts();
|
|
1204
|
+
try {
|
|
1205
|
+
const result = await runCommand("cursor", []);
|
|
1206
|
+
output(result, opts.json ?? false);
|
|
1207
|
+
} catch (e) {
|
|
1208
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
1209
|
+
process.exit(1);
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
1212
|
+
program.command("pages").description("List all open pages").action(async () => {
|
|
1213
|
+
const opts = program.opts();
|
|
1214
|
+
try {
|
|
1215
|
+
const result = await runCommand("pages", []);
|
|
1216
|
+
output(result, opts.json ?? false);
|
|
1217
|
+
} catch (e) {
|
|
1218
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
1219
|
+
process.exit(1);
|
|
1220
|
+
}
|
|
1221
|
+
});
|
|
1222
|
+
program.command("newpage [url]").description("Create a new page/tab").action(async (url) => {
|
|
1223
|
+
const opts = program.opts();
|
|
1224
|
+
try {
|
|
1225
|
+
const result = await runCommand("newpage", [url]);
|
|
1226
|
+
output(result, opts.json ?? false);
|
|
1227
|
+
} catch (e) {
|
|
1228
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
1229
|
+
process.exit(1);
|
|
1230
|
+
}
|
|
1231
|
+
});
|
|
1232
|
+
program.command("tab_switch <index>").alias("switch").description("Switch to tab by index").action(async (index) => {
|
|
1233
|
+
const opts = program.opts();
|
|
1234
|
+
try {
|
|
1235
|
+
const result = await runCommand("tab_switch", [parseInt(index)]);
|
|
1236
|
+
output(result, opts.json ?? false);
|
|
1237
|
+
} catch (e) {
|
|
1238
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
1239
|
+
process.exit(1);
|
|
1240
|
+
}
|
|
1241
|
+
});
|
|
1242
|
+
program.command("tab_close [index]").alias("close").description("Close tab by index (defaults to last tab)").action(async (index) => {
|
|
1243
|
+
const opts = program.opts();
|
|
1244
|
+
try {
|
|
1245
|
+
const result = await runCommand("tab_close", [
|
|
1246
|
+
index ? parseInt(index) : void 0
|
|
1247
|
+
]);
|
|
1248
|
+
output(result, opts.json ?? false);
|
|
1249
|
+
} catch (e) {
|
|
1250
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
1251
|
+
process.exit(1);
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
var networkCmd = program.command("network").description(
|
|
1255
|
+
"Network capture commands (writes to filesystem for agent inspection)"
|
|
1256
|
+
);
|
|
1257
|
+
networkCmd.command("on").description("Enable network capture (creates temp directory for requests)").action(async () => {
|
|
1258
|
+
const opts = program.opts();
|
|
1259
|
+
try {
|
|
1260
|
+
const result = await runCommand("network_enable", []);
|
|
1261
|
+
output(result, opts.json ?? false);
|
|
1262
|
+
} catch (e) {
|
|
1263
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
1264
|
+
process.exit(1);
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
networkCmd.command("off").description("Disable network capture").action(async () => {
|
|
1268
|
+
const opts = program.opts();
|
|
1269
|
+
try {
|
|
1270
|
+
const result = await runCommand("network_disable", []);
|
|
1271
|
+
output(result, opts.json ?? false);
|
|
1272
|
+
} catch (e) {
|
|
1273
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
1274
|
+
process.exit(1);
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
networkCmd.command("path").description("Get network capture directory path").action(async () => {
|
|
1278
|
+
const opts = program.opts();
|
|
1279
|
+
try {
|
|
1280
|
+
const result = await runCommand("network_path", []);
|
|
1281
|
+
output(result, opts.json ?? false);
|
|
1282
|
+
} catch (e) {
|
|
1283
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
1284
|
+
process.exit(1);
|
|
1285
|
+
}
|
|
1286
|
+
});
|
|
1287
|
+
networkCmd.command("clear").description("Clear all captured requests").action(async () => {
|
|
1288
|
+
const opts = program.opts();
|
|
1289
|
+
try {
|
|
1290
|
+
const result = await runCommand("network_clear", []);
|
|
1291
|
+
output(result, opts.json ?? false);
|
|
1292
|
+
} catch (e) {
|
|
1293
|
+
console.error("Error:", e instanceof Error ? e.message : e);
|
|
1294
|
+
process.exit(1);
|
|
1295
|
+
}
|
|
1296
|
+
});
|
|
1297
|
+
program.parse();
|