browser-pilot 0.0.1
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 +539 -0
- package/dist/actions.cjs +277 -0
- package/dist/actions.d.cts +33 -0
- package/dist/actions.d.ts +33 -0
- package/dist/actions.mjs +8 -0
- package/dist/browser.cjs +2765 -0
- package/dist/browser.d.cts +71 -0
- package/dist/browser.d.ts +71 -0
- package/dist/browser.mjs +19 -0
- package/dist/cdp.cjs +279 -0
- package/dist/cdp.d.cts +230 -0
- package/dist/cdp.d.ts +230 -0
- package/dist/cdp.mjs +10 -0
- package/dist/chunk-BCOZUKWS.mjs +251 -0
- package/dist/chunk-FI55U7JS.mjs +2108 -0
- package/dist/chunk-R3PS4PCM.mjs +207 -0
- package/dist/chunk-YEHK2XY3.mjs +250 -0
- package/dist/chunk-ZIQA4JOT.mjs +226 -0
- package/dist/cli.cjs +3587 -0
- package/dist/cli.d.cts +23 -0
- package/dist/cli.d.ts +23 -0
- package/dist/cli.mjs +827 -0
- package/dist/client-7Nqka5MV.d.cts +53 -0
- package/dist/client-7Nqka5MV.d.ts +53 -0
- package/dist/index.cjs +3074 -0
- package/dist/index.d.cts +157 -0
- package/dist/index.d.ts +157 -0
- package/dist/index.mjs +64 -0
- package/dist/providers.cjs +238 -0
- package/dist/providers.d.cts +86 -0
- package/dist/providers.d.ts +86 -0
- package/dist/providers.mjs +16 -0
- package/dist/types-Cs89wle0.d.cts +925 -0
- package/dist/types-DL_-3BZk.d.ts +925 -0
- package/dist/types-D_uDqh0Z.d.cts +56 -0
- package/dist/types-D_uDqh0Z.d.ts +56 -0
- package/package.json +91 -0
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import "./chunk-ZIQA4JOT.mjs";
|
|
3
|
+
import {
|
|
4
|
+
connect
|
|
5
|
+
} from "./chunk-FI55U7JS.mjs";
|
|
6
|
+
import "./chunk-BCOZUKWS.mjs";
|
|
7
|
+
import {
|
|
8
|
+
getBrowserWebSocketUrl
|
|
9
|
+
} from "./chunk-R3PS4PCM.mjs";
|
|
10
|
+
import {
|
|
11
|
+
addBatchToPage
|
|
12
|
+
} from "./chunk-YEHK2XY3.mjs";
|
|
13
|
+
|
|
14
|
+
// src/cli/commands/actions.ts
|
|
15
|
+
var ACTIONS_HELP = `
|
|
16
|
+
bp actions - Complete action reference
|
|
17
|
+
|
|
18
|
+
All actions are JSON objects with "action" field. Use with 'bp exec'.
|
|
19
|
+
|
|
20
|
+
NAVIGATION
|
|
21
|
+
{"action": "goto", "url": "https://..."}
|
|
22
|
+
Navigate to URL.
|
|
23
|
+
|
|
24
|
+
{"action": "wait", "waitFor": "navigation"}
|
|
25
|
+
Wait for page navigation to complete.
|
|
26
|
+
|
|
27
|
+
{"action": "wait", "waitFor": "networkIdle"}
|
|
28
|
+
Wait for network activity to settle.
|
|
29
|
+
|
|
30
|
+
{"action": "wait", "timeout": 2000}
|
|
31
|
+
Simple delay in milliseconds.
|
|
32
|
+
|
|
33
|
+
INTERACTION
|
|
34
|
+
{"action": "click", "selector": "#button"}
|
|
35
|
+
{"action": "click", "selector": ["#primary", ".fallback"]}
|
|
36
|
+
Click element. Multi-selector tries each until success.
|
|
37
|
+
|
|
38
|
+
{"action": "fill", "selector": "#input", "value": "text"}
|
|
39
|
+
{"action": "fill", "selector": "#input", "value": "text", "clear": false}
|
|
40
|
+
Fill input field. Clears first by default.
|
|
41
|
+
|
|
42
|
+
{"action": "type", "selector": "#input", "value": "text", "delay": 50}
|
|
43
|
+
Type character-by-character (for autocomplete).
|
|
44
|
+
|
|
45
|
+
{"action": "select", "selector": "#dropdown", "value": "option-value"}
|
|
46
|
+
Select native <select> option by value.
|
|
47
|
+
|
|
48
|
+
{"action": "select", "trigger": ".dropdown", "option": ".item", "value": "Label", "match": "text"}
|
|
49
|
+
Custom dropdown: click trigger, then click matching option.
|
|
50
|
+
|
|
51
|
+
{"action": "check", "selector": "#checkbox"}
|
|
52
|
+
{"action": "uncheck", "selector": "#checkbox"}
|
|
53
|
+
Check/uncheck checkbox or radio.
|
|
54
|
+
|
|
55
|
+
{"action": "submit", "selector": "form"}
|
|
56
|
+
{"action": "submit", "selector": "#btn", "method": "click"}
|
|
57
|
+
Submit form. Methods: enter | click | enter+click (default).
|
|
58
|
+
|
|
59
|
+
{"action": "press", "key": "Enter"}
|
|
60
|
+
{"action": "press", "key": "Escape"}
|
|
61
|
+
{"action": "press", "key": "Tab"}
|
|
62
|
+
Press key. Common keys: Enter, Tab, Escape, Backspace, Delete, ArrowUp/Down/Left/Right.
|
|
63
|
+
|
|
64
|
+
{"action": "focus", "selector": "#input"}
|
|
65
|
+
{"action": "hover", "selector": ".menu-item"}
|
|
66
|
+
Focus or hover element.
|
|
67
|
+
|
|
68
|
+
{"action": "scroll", "selector": "#footer"}
|
|
69
|
+
{"action": "scroll", "x": 0, "y": 1000}
|
|
70
|
+
{"action": "scroll", "direction": "down", "amount": 500}
|
|
71
|
+
Scroll to element, coordinates, or by direction (up/down/left/right).
|
|
72
|
+
|
|
73
|
+
WAITING
|
|
74
|
+
{"action": "wait", "selector": ".loaded", "waitFor": "visible"}
|
|
75
|
+
{"action": "wait", "selector": ".spinner", "waitFor": "hidden"}
|
|
76
|
+
{"action": "wait", "selector": "#element", "waitFor": "attached"}
|
|
77
|
+
{"action": "wait", "selector": "#removed", "waitFor": "detached"}
|
|
78
|
+
Wait for element state. States: visible | hidden | attached | detached.
|
|
79
|
+
|
|
80
|
+
{"action": "wait", "timeout": 1000}
|
|
81
|
+
Simple delay (milliseconds).
|
|
82
|
+
|
|
83
|
+
CONTENT EXTRACTION
|
|
84
|
+
{"action": "snapshot"}
|
|
85
|
+
Get accessibility tree (best for understanding page structure).
|
|
86
|
+
|
|
87
|
+
{"action": "screenshot"}
|
|
88
|
+
{"action": "screenshot", "fullPage": true, "format": "jpeg", "quality": 80}
|
|
89
|
+
Capture screenshot. Formats: png | jpeg | webp.
|
|
90
|
+
|
|
91
|
+
{"action": "evaluate", "value": "document.title"}
|
|
92
|
+
Run JavaScript and return result.
|
|
93
|
+
|
|
94
|
+
IFRAME NAVIGATION
|
|
95
|
+
{"action": "switchFrame", "selector": "iframe#checkout"}
|
|
96
|
+
Switch context to an iframe. All subsequent actions target the iframe content.
|
|
97
|
+
|
|
98
|
+
{"action": "switchToMain"}
|
|
99
|
+
Switch back to the main document from an iframe.
|
|
100
|
+
|
|
101
|
+
Example iframe workflow:
|
|
102
|
+
[
|
|
103
|
+
{"action": "switchFrame", "selector": "iframe#payment"},
|
|
104
|
+
{"action": "fill", "selector": "#card-number", "value": "4242424242424242"},
|
|
105
|
+
{"action": "fill", "selector": "#expiry", "value": "12/25"},
|
|
106
|
+
{"action": "switchToMain"},
|
|
107
|
+
{"action": "click", "selector": "#submit-order"}
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
Note: Cross-origin iframes cannot be accessed due to browser security.
|
|
111
|
+
|
|
112
|
+
DIALOG HANDLING
|
|
113
|
+
Use --dialog flag: bp exec --dialog accept '[...]'
|
|
114
|
+
Modes: accept (click OK), dismiss (click Cancel)
|
|
115
|
+
|
|
116
|
+
WARNING: Without --dialog flag, native dialogs (alert/confirm/prompt) will
|
|
117
|
+
block ALL automation until manual intervention.
|
|
118
|
+
|
|
119
|
+
COMMON OPTIONS (all actions)
|
|
120
|
+
"timeout": 5000 Override default timeout (ms)
|
|
121
|
+
"optional": true Don't fail if element not found
|
|
122
|
+
|
|
123
|
+
REF SELECTORS (from snapshot)
|
|
124
|
+
After taking a snapshot, use refs directly:
|
|
125
|
+
bp snapshot -s dev --format text # Shows: button "Submit" [ref=e4]
|
|
126
|
+
bp exec '{"action":"click","selector":"ref:e4"}'
|
|
127
|
+
|
|
128
|
+
Refs are stable until navigation. Prefix with "ref:" to use.
|
|
129
|
+
Example: {"action":"fill","selector":"ref:e23","value":"hello"}
|
|
130
|
+
|
|
131
|
+
MULTI-SELECTOR PATTERN
|
|
132
|
+
All selectors accept arrays: ["#id", ".class", "[aria-label=X]"]
|
|
133
|
+
Tries each in order until one succeeds.
|
|
134
|
+
Combine refs with CSS fallbacks: ["ref:e4", "#submit", ".btn"]
|
|
135
|
+
|
|
136
|
+
SELECTOR PRIORITY (Most to Least Reliable)
|
|
137
|
+
1. ref:eN - From snapshot, most reliable for AI agents
|
|
138
|
+
2. [data-testid="..."] - Explicit test hooks
|
|
139
|
+
3. #id - Reliable if IDs are stable
|
|
140
|
+
4. [aria-label="..."] - Good for buttons without testids
|
|
141
|
+
5. Multi-selector array - Fallback pattern for compatibility
|
|
142
|
+
|
|
143
|
+
SHADOW DOM
|
|
144
|
+
Selectors automatically pierce shadow DOM (1-2 levels). No special syntax needed.
|
|
145
|
+
For deeper nesting (3+ levels), use refs from snapshot - they work at any depth.
|
|
146
|
+
|
|
147
|
+
:has-text() SELECTOR
|
|
148
|
+
Matches elements containing text content.
|
|
149
|
+
Does NOT match aria-label - use [aria-label="..."] instead.
|
|
150
|
+
Example: button:has-text("Submit") matches <button>Submit</button>
|
|
151
|
+
button[aria-label="Submit"] matches <button aria-label="Submit">X</button>
|
|
152
|
+
|
|
153
|
+
EXAMPLES
|
|
154
|
+
# Login flow
|
|
155
|
+
bp exec '[
|
|
156
|
+
{"action":"goto","url":"https://app.example.com/login"},
|
|
157
|
+
{"action":"fill","selector":"#email","value":"user@example.com"},
|
|
158
|
+
{"action":"fill","selector":"#password","value":"secret"},
|
|
159
|
+
{"action":"submit","selector":"form"},
|
|
160
|
+
{"action":"wait","waitFor":"navigation"},
|
|
161
|
+
{"action":"snapshot"}
|
|
162
|
+
]'
|
|
163
|
+
|
|
164
|
+
# Handle cookie banner then extract content
|
|
165
|
+
bp exec '[
|
|
166
|
+
{"action":"goto","url":"https://example.com"},
|
|
167
|
+
{"action":"click","selector":"#accept-cookies","optional":true,"timeout":3000},
|
|
168
|
+
{"action":"snapshot"}
|
|
169
|
+
]'
|
|
170
|
+
|
|
171
|
+
# Use ref from snapshot
|
|
172
|
+
bp snapshot --format text # Note the refs
|
|
173
|
+
bp exec '{"action":"click","selector":"ref:e4"}'
|
|
174
|
+
|
|
175
|
+
# Scroll and wait
|
|
176
|
+
bp exec '[
|
|
177
|
+
{"action":"scroll","direction":"down","amount":1000},
|
|
178
|
+
{"action":"wait","timeout":500},
|
|
179
|
+
{"action":"scroll","direction":"down","amount":1000}
|
|
180
|
+
]'
|
|
181
|
+
|
|
182
|
+
# Handle dialogs
|
|
183
|
+
bp exec --dialog accept '[
|
|
184
|
+
{"action":"click","selector":"#delete-btn"},
|
|
185
|
+
{"action":"wait","selector":"#success-message","waitFor":"visible"}
|
|
186
|
+
]'
|
|
187
|
+
`;
|
|
188
|
+
async function actionsCommand() {
|
|
189
|
+
console.log(ACTIONS_HELP);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/cli/session.ts
|
|
193
|
+
import { homedir } from "os";
|
|
194
|
+
import { join } from "path";
|
|
195
|
+
var SESSION_DIR = join(homedir(), ".browser-pilot", "sessions");
|
|
196
|
+
async function ensureSessionDir() {
|
|
197
|
+
const fs = await import("fs/promises");
|
|
198
|
+
await fs.mkdir(SESSION_DIR, { recursive: true });
|
|
199
|
+
}
|
|
200
|
+
async function saveSession(session) {
|
|
201
|
+
await ensureSessionDir();
|
|
202
|
+
const fs = await import("fs/promises");
|
|
203
|
+
const filePath = join(SESSION_DIR, `${session.id}.json`);
|
|
204
|
+
await fs.writeFile(filePath, JSON.stringify(session, null, 2));
|
|
205
|
+
}
|
|
206
|
+
async function loadSession(id) {
|
|
207
|
+
const fs = await import("fs/promises");
|
|
208
|
+
const filePath = join(SESSION_DIR, `${id}.json`);
|
|
209
|
+
try {
|
|
210
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
211
|
+
return JSON.parse(content);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (error.code === "ENOENT") {
|
|
214
|
+
throw new Error(`Session not found: ${id}`);
|
|
215
|
+
}
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
async function updateSession(id, updates) {
|
|
220
|
+
const session = await loadSession(id);
|
|
221
|
+
const updated = {
|
|
222
|
+
...session,
|
|
223
|
+
...updates,
|
|
224
|
+
lastActivity: (/* @__PURE__ */ new Date()).toISOString()
|
|
225
|
+
};
|
|
226
|
+
await saveSession(updated);
|
|
227
|
+
return updated;
|
|
228
|
+
}
|
|
229
|
+
async function deleteSession(id) {
|
|
230
|
+
const fs = await import("fs/promises");
|
|
231
|
+
const filePath = join(SESSION_DIR, `${id}.json`);
|
|
232
|
+
try {
|
|
233
|
+
await fs.unlink(filePath);
|
|
234
|
+
} catch (error) {
|
|
235
|
+
if (error.code !== "ENOENT") {
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async function listSessions() {
|
|
241
|
+
await ensureSessionDir();
|
|
242
|
+
const fs = await import("fs/promises");
|
|
243
|
+
try {
|
|
244
|
+
const files = await fs.readdir(SESSION_DIR);
|
|
245
|
+
const sessions = [];
|
|
246
|
+
for (const file of files) {
|
|
247
|
+
if (file.endsWith(".json")) {
|
|
248
|
+
try {
|
|
249
|
+
const content = await fs.readFile(join(SESSION_DIR, file), "utf-8");
|
|
250
|
+
sessions.push(JSON.parse(content));
|
|
251
|
+
} catch {
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return sessions.sort(
|
|
256
|
+
(a, b) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime()
|
|
257
|
+
);
|
|
258
|
+
} catch {
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function generateSessionId() {
|
|
263
|
+
const timestamp = Date.now().toString(36);
|
|
264
|
+
const random = Math.random().toString(36).slice(2, 8);
|
|
265
|
+
return `${timestamp}-${random}`;
|
|
266
|
+
}
|
|
267
|
+
async function getDefaultSession() {
|
|
268
|
+
const sessions = await listSessions();
|
|
269
|
+
return sessions[0] ?? null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/cli/commands/close.ts
|
|
273
|
+
async function closeCommand(args, globalOptions) {
|
|
274
|
+
let session;
|
|
275
|
+
if (globalOptions.session) {
|
|
276
|
+
session = await loadSession(globalOptions.session);
|
|
277
|
+
} else if (args[0]) {
|
|
278
|
+
session = await loadSession(args[0]);
|
|
279
|
+
} else {
|
|
280
|
+
session = await getDefaultSession();
|
|
281
|
+
if (!session) {
|
|
282
|
+
throw new Error('No session found. Specify a session ID or run "bp list" to see sessions.');
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
const browser = await connect({
|
|
287
|
+
provider: session.provider,
|
|
288
|
+
wsUrl: session.wsUrl,
|
|
289
|
+
debug: globalOptions.trace
|
|
290
|
+
});
|
|
291
|
+
await browser.close();
|
|
292
|
+
} catch {
|
|
293
|
+
}
|
|
294
|
+
await deleteSession(session.id);
|
|
295
|
+
output(
|
|
296
|
+
{
|
|
297
|
+
success: true,
|
|
298
|
+
sessionId: session.id,
|
|
299
|
+
message: "Session closed"
|
|
300
|
+
},
|
|
301
|
+
globalOptions.output
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// src/cli/commands/connect.ts
|
|
306
|
+
function parseConnectArgs(args) {
|
|
307
|
+
const options = {};
|
|
308
|
+
for (let i = 0; i < args.length; i++) {
|
|
309
|
+
const arg = args[i];
|
|
310
|
+
if (arg === "--provider" || arg === "-p") {
|
|
311
|
+
options.provider = args[++i];
|
|
312
|
+
} else if (arg === "--url") {
|
|
313
|
+
options.url = args[++i];
|
|
314
|
+
} else if (arg === "--name" || arg === "-n") {
|
|
315
|
+
options.name = args[++i];
|
|
316
|
+
} else if (arg === "--resume" || arg === "-r") {
|
|
317
|
+
options.resume = args[++i];
|
|
318
|
+
} else if (arg === "--api-key") {
|
|
319
|
+
options.apiKey = args[++i];
|
|
320
|
+
} else if (arg === "--project-id") {
|
|
321
|
+
options.projectId = args[++i];
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return options;
|
|
325
|
+
}
|
|
326
|
+
async function connectCommand(args, globalOptions) {
|
|
327
|
+
const options = parseConnectArgs(args);
|
|
328
|
+
if (options.resume || globalOptions.session) {
|
|
329
|
+
const sessionId2 = options.resume || globalOptions.session;
|
|
330
|
+
const session2 = await loadSession(sessionId2);
|
|
331
|
+
output(
|
|
332
|
+
{
|
|
333
|
+
success: true,
|
|
334
|
+
resumed: true,
|
|
335
|
+
sessionId: session2.id,
|
|
336
|
+
provider: session2.provider,
|
|
337
|
+
currentUrl: session2.currentUrl
|
|
338
|
+
},
|
|
339
|
+
globalOptions.output
|
|
340
|
+
);
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const provider = options.provider ?? "generic";
|
|
344
|
+
let wsUrl = options.url;
|
|
345
|
+
if (provider === "generic" && !wsUrl) {
|
|
346
|
+
try {
|
|
347
|
+
wsUrl = await getBrowserWebSocketUrl("localhost:9222");
|
|
348
|
+
} catch {
|
|
349
|
+
throw new Error(
|
|
350
|
+
"Could not auto-discover browser. Specify --url or start Chrome with --remote-debugging-port=9222"
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
const connectOptions = {
|
|
355
|
+
provider,
|
|
356
|
+
debug: globalOptions.trace,
|
|
357
|
+
wsUrl,
|
|
358
|
+
apiKey: options.apiKey,
|
|
359
|
+
projectId: options.projectId
|
|
360
|
+
};
|
|
361
|
+
const browser = await connect(connectOptions);
|
|
362
|
+
const page = await browser.page();
|
|
363
|
+
const currentUrl = await page.url();
|
|
364
|
+
const sessionId = options.name ?? generateSessionId();
|
|
365
|
+
const session = {
|
|
366
|
+
id: sessionId,
|
|
367
|
+
provider,
|
|
368
|
+
wsUrl: browser.wsUrl,
|
|
369
|
+
providerSessionId: browser.sessionId,
|
|
370
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
371
|
+
lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
|
|
372
|
+
currentUrl,
|
|
373
|
+
metadata: browser.metadata
|
|
374
|
+
};
|
|
375
|
+
await saveSession(session);
|
|
376
|
+
await browser.disconnect();
|
|
377
|
+
output(
|
|
378
|
+
{
|
|
379
|
+
success: true,
|
|
380
|
+
sessionId,
|
|
381
|
+
provider,
|
|
382
|
+
currentUrl,
|
|
383
|
+
metadata: browser.metadata
|
|
384
|
+
},
|
|
385
|
+
globalOptions.output
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// src/cli/commands/exec.ts
|
|
390
|
+
function parseExecArgs(args) {
|
|
391
|
+
const options = {};
|
|
392
|
+
let actionsJson;
|
|
393
|
+
for (let i = 0; i < args.length; i++) {
|
|
394
|
+
const arg = args[i];
|
|
395
|
+
if (arg === "--dialog") {
|
|
396
|
+
const value = args[++i];
|
|
397
|
+
if (value === "accept" || value === "dismiss") {
|
|
398
|
+
options.dialog = value;
|
|
399
|
+
} else {
|
|
400
|
+
throw new Error('--dialog must be "accept" or "dismiss"');
|
|
401
|
+
}
|
|
402
|
+
} else if (!actionsJson && !arg.startsWith("-")) {
|
|
403
|
+
actionsJson = arg;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return { actionsJson, options };
|
|
407
|
+
}
|
|
408
|
+
async function execCommand(args, globalOptions) {
|
|
409
|
+
const { actionsJson, options: execOptions } = parseExecArgs(args);
|
|
410
|
+
let session;
|
|
411
|
+
if (globalOptions.session) {
|
|
412
|
+
session = await loadSession(globalOptions.session);
|
|
413
|
+
} else {
|
|
414
|
+
session = await getDefaultSession();
|
|
415
|
+
if (!session) {
|
|
416
|
+
throw new Error('No session found. Run "bp connect" first.');
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (!actionsJson) {
|
|
420
|
+
throw new Error(
|
|
421
|
+
`No actions provided. Usage: bp exec '{"action":"goto","url":"..."}'
|
|
422
|
+
|
|
423
|
+
Run 'bp actions' for complete action reference.`
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
let actions;
|
|
427
|
+
try {
|
|
428
|
+
actions = JSON.parse(actionsJson);
|
|
429
|
+
} catch {
|
|
430
|
+
throw new Error(
|
|
431
|
+
"Invalid JSON. Actions must be valid JSON.\n\nRun 'bp actions' for complete action reference."
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
const browser = await connect({
|
|
435
|
+
provider: session.provider,
|
|
436
|
+
wsUrl: session.wsUrl,
|
|
437
|
+
debug: globalOptions.trace
|
|
438
|
+
});
|
|
439
|
+
try {
|
|
440
|
+
const page = addBatchToPage(await browser.page());
|
|
441
|
+
if (execOptions.dialog) {
|
|
442
|
+
await page.onDialog(async (dialog) => {
|
|
443
|
+
if (execOptions.dialog === "accept") {
|
|
444
|
+
await dialog.accept();
|
|
445
|
+
} else {
|
|
446
|
+
await dialog.dismiss();
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
const steps = Array.isArray(actions) ? actions : [actions];
|
|
451
|
+
const result = await page.batch(steps);
|
|
452
|
+
const currentUrl = await page.url();
|
|
453
|
+
await updateSession(session.id, { currentUrl });
|
|
454
|
+
output(
|
|
455
|
+
{
|
|
456
|
+
success: result.success,
|
|
457
|
+
stoppedAtIndex: result.stoppedAtIndex,
|
|
458
|
+
steps: result.steps.map((s) => ({
|
|
459
|
+
action: s.action,
|
|
460
|
+
success: s.success,
|
|
461
|
+
durationMs: s.durationMs,
|
|
462
|
+
selectorUsed: s.selectorUsed,
|
|
463
|
+
error: s.error,
|
|
464
|
+
text: s.text,
|
|
465
|
+
result: s.result
|
|
466
|
+
})),
|
|
467
|
+
totalDurationMs: result.totalDurationMs,
|
|
468
|
+
currentUrl
|
|
469
|
+
},
|
|
470
|
+
globalOptions.output
|
|
471
|
+
);
|
|
472
|
+
} finally {
|
|
473
|
+
await browser.disconnect();
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/cli/commands/list.ts
|
|
478
|
+
async function listCommand(_args, globalOptions) {
|
|
479
|
+
const sessions = await listSessions();
|
|
480
|
+
if (globalOptions.output === "json") {
|
|
481
|
+
output(sessions, "json");
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (sessions.length === 0) {
|
|
485
|
+
console.log("No active sessions.");
|
|
486
|
+
console.log('Run "bp connect" to create a new session.');
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
console.log("Active Sessions:\n");
|
|
490
|
+
for (const session of sessions) {
|
|
491
|
+
const age = getAge(new Date(session.lastActivity));
|
|
492
|
+
console.log(` ${session.id}`);
|
|
493
|
+
console.log(` Provider: ${session.provider}`);
|
|
494
|
+
console.log(` URL: ${session.currentUrl}`);
|
|
495
|
+
console.log(` Last activity: ${age}`);
|
|
496
|
+
console.log("");
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
function getAge(date) {
|
|
500
|
+
const now = Date.now();
|
|
501
|
+
const diff = now - date.getTime();
|
|
502
|
+
const seconds = Math.floor(diff / 1e3);
|
|
503
|
+
if (seconds < 60) return "just now";
|
|
504
|
+
const minutes = Math.floor(seconds / 60);
|
|
505
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
506
|
+
const hours = Math.floor(minutes / 60);
|
|
507
|
+
if (hours < 24) return `${hours}h ago`;
|
|
508
|
+
const days = Math.floor(hours / 24);
|
|
509
|
+
return `${days}d ago`;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// src/cli/commands/screenshot.ts
|
|
513
|
+
function parseScreenshotArgs(args) {
|
|
514
|
+
const options = {};
|
|
515
|
+
for (let i = 0; i < args.length; i++) {
|
|
516
|
+
const arg = args[i];
|
|
517
|
+
if (arg === "--output" || arg === "-o") {
|
|
518
|
+
options.outputPath = args[++i];
|
|
519
|
+
} else if (arg === "--format" || arg === "-f") {
|
|
520
|
+
options.format = args[++i];
|
|
521
|
+
} else if (arg === "--quality" || arg === "-q") {
|
|
522
|
+
options.quality = parseInt(args[++i], 10);
|
|
523
|
+
} else if (arg === "--full-page" || arg === "--fullpage") {
|
|
524
|
+
options.fullPage = true;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return options;
|
|
528
|
+
}
|
|
529
|
+
async function screenshotCommand(args, globalOptions) {
|
|
530
|
+
const options = parseScreenshotArgs(args);
|
|
531
|
+
let session;
|
|
532
|
+
if (globalOptions.session) {
|
|
533
|
+
session = await loadSession(globalOptions.session);
|
|
534
|
+
} else {
|
|
535
|
+
session = await getDefaultSession();
|
|
536
|
+
if (!session) {
|
|
537
|
+
throw new Error('No session found. Run "bp connect" first.');
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
const browser = await connect({
|
|
541
|
+
provider: session.provider,
|
|
542
|
+
wsUrl: session.wsUrl,
|
|
543
|
+
debug: globalOptions.trace
|
|
544
|
+
});
|
|
545
|
+
try {
|
|
546
|
+
const page = await browser.page();
|
|
547
|
+
const screenshotData = await page.screenshot({
|
|
548
|
+
format: options.format ?? "png",
|
|
549
|
+
quality: options.quality,
|
|
550
|
+
fullPage: options.fullPage ?? false
|
|
551
|
+
});
|
|
552
|
+
if (options.outputPath) {
|
|
553
|
+
const buffer = Buffer.from(screenshotData, "base64");
|
|
554
|
+
await Bun.write(options.outputPath, buffer);
|
|
555
|
+
output(
|
|
556
|
+
{
|
|
557
|
+
success: true,
|
|
558
|
+
path: options.outputPath,
|
|
559
|
+
size: buffer.length,
|
|
560
|
+
format: options.format ?? "png"
|
|
561
|
+
},
|
|
562
|
+
globalOptions.output
|
|
563
|
+
);
|
|
564
|
+
} else {
|
|
565
|
+
if (globalOptions.output === "json") {
|
|
566
|
+
output({ data: screenshotData, format: options.format ?? "png" }, "json");
|
|
567
|
+
} else {
|
|
568
|
+
console.log(screenshotData);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
} finally {
|
|
572
|
+
await browser.disconnect();
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// src/cli/commands/snapshot.ts
|
|
577
|
+
function parseSnapshotArgs(args) {
|
|
578
|
+
const options = {};
|
|
579
|
+
for (let i = 0; i < args.length; i++) {
|
|
580
|
+
const arg = args[i];
|
|
581
|
+
if (arg === "--format" || arg === "-f") {
|
|
582
|
+
options.format = args[++i];
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return options;
|
|
586
|
+
}
|
|
587
|
+
async function snapshotCommand(args, globalOptions) {
|
|
588
|
+
const options = parseSnapshotArgs(args);
|
|
589
|
+
let session;
|
|
590
|
+
if (globalOptions.session) {
|
|
591
|
+
session = await loadSession(globalOptions.session);
|
|
592
|
+
} else {
|
|
593
|
+
session = await getDefaultSession();
|
|
594
|
+
if (!session) {
|
|
595
|
+
throw new Error('No session found. Run "bp connect" first.');
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
const browser = await connect({
|
|
599
|
+
provider: session.provider,
|
|
600
|
+
wsUrl: session.wsUrl,
|
|
601
|
+
debug: globalOptions.trace
|
|
602
|
+
});
|
|
603
|
+
try {
|
|
604
|
+
const page = await browser.page();
|
|
605
|
+
const snapshot = await page.snapshot();
|
|
606
|
+
await updateSession(session.id, { currentUrl: snapshot.url });
|
|
607
|
+
switch (options.format) {
|
|
608
|
+
case "interactive":
|
|
609
|
+
output(snapshot.interactiveElements, globalOptions.output);
|
|
610
|
+
break;
|
|
611
|
+
case "text":
|
|
612
|
+
console.log(snapshot.text);
|
|
613
|
+
break;
|
|
614
|
+
default:
|
|
615
|
+
output(snapshot, globalOptions.output);
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
} finally {
|
|
619
|
+
await browser.disconnect();
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// src/cli/commands/text.ts
|
|
624
|
+
function parseTextArgs(args) {
|
|
625
|
+
const options = {};
|
|
626
|
+
for (let i = 0; i < args.length; i++) {
|
|
627
|
+
const arg = args[i];
|
|
628
|
+
if (arg === "--selector" || arg === "-s") {
|
|
629
|
+
options.selector = args[++i];
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return options;
|
|
633
|
+
}
|
|
634
|
+
async function textCommand(args, globalOptions) {
|
|
635
|
+
const options = parseTextArgs(args);
|
|
636
|
+
let session;
|
|
637
|
+
if (globalOptions.session) {
|
|
638
|
+
session = await loadSession(globalOptions.session);
|
|
639
|
+
} else {
|
|
640
|
+
session = await getDefaultSession();
|
|
641
|
+
if (!session) {
|
|
642
|
+
throw new Error('No session found. Run "bp connect" first.');
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
const browser = await connect({
|
|
646
|
+
provider: session.provider,
|
|
647
|
+
wsUrl: session.wsUrl,
|
|
648
|
+
debug: globalOptions.trace
|
|
649
|
+
});
|
|
650
|
+
try {
|
|
651
|
+
const page = await browser.page();
|
|
652
|
+
const text = await page.text(options.selector);
|
|
653
|
+
const currentUrl = await page.url();
|
|
654
|
+
await updateSession(session.id, { currentUrl });
|
|
655
|
+
if (globalOptions.output === "json") {
|
|
656
|
+
output({ text, url: currentUrl, selector: options.selector }, "json");
|
|
657
|
+
} else {
|
|
658
|
+
console.log(text);
|
|
659
|
+
}
|
|
660
|
+
} finally {
|
|
661
|
+
await browser.disconnect();
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// src/cli/index.ts
|
|
666
|
+
var HELP = `
|
|
667
|
+
bp - Browser automation CLI for AI agents
|
|
668
|
+
|
|
669
|
+
Usage:
|
|
670
|
+
bp <command> [options]
|
|
671
|
+
|
|
672
|
+
Commands:
|
|
673
|
+
connect Create or resume browser session
|
|
674
|
+
exec Execute actions on current session
|
|
675
|
+
snapshot Get page accessibility snapshot (includes element refs)
|
|
676
|
+
text Extract text content from page
|
|
677
|
+
screenshot Take screenshot
|
|
678
|
+
close Close session
|
|
679
|
+
list List all sessions
|
|
680
|
+
actions Show all available actions with examples
|
|
681
|
+
|
|
682
|
+
Global Options:
|
|
683
|
+
-s, --session <id> Session ID to use
|
|
684
|
+
-o, --output <fmt> Output format: json | pretty (default: pretty)
|
|
685
|
+
--trace Enable execution tracing
|
|
686
|
+
-h, --help Show this help message
|
|
687
|
+
|
|
688
|
+
Exec Options:
|
|
689
|
+
--dialog <mode> Auto-handle dialogs: accept | dismiss
|
|
690
|
+
|
|
691
|
+
Ref Selectors (Recommended for AI Agents):
|
|
692
|
+
1. Take snapshot: bp snapshot --format text
|
|
693
|
+
Output shows: button "Submit" [ref=e4], textbox "Email" [ref=e5]
|
|
694
|
+
2. Use ref directly: bp exec '{"action":"click","selector":"ref:e4"}'
|
|
695
|
+
|
|
696
|
+
Refs are stable until navigation. Combine with CSS fallbacks:
|
|
697
|
+
{"selector": ["ref:e4", "#submit", "button[type=submit]"]}
|
|
698
|
+
|
|
699
|
+
Examples:
|
|
700
|
+
# Connect to browser
|
|
701
|
+
bp connect --provider generic --name dev
|
|
702
|
+
|
|
703
|
+
# Navigate and get snapshot with refs
|
|
704
|
+
bp exec '{"action":"goto","url":"https://example.com"}'
|
|
705
|
+
bp snapshot --format text
|
|
706
|
+
|
|
707
|
+
# Use ref from snapshot (most reliable)
|
|
708
|
+
bp exec '{"action":"click","selector":"ref:e4"}'
|
|
709
|
+
bp exec '{"action":"fill","selector":"ref:e5","value":"test@example.com"}'
|
|
710
|
+
|
|
711
|
+
# Handle native dialogs (alert/confirm/prompt)
|
|
712
|
+
bp exec --dialog accept '{"action":"click","selector":"#delete-btn"}'
|
|
713
|
+
|
|
714
|
+
# Batch multiple actions
|
|
715
|
+
bp exec '[
|
|
716
|
+
{"action":"fill","selector":"ref:e5","value":"user@example.com"},
|
|
717
|
+
{"action":"click","selector":"ref:e4"},
|
|
718
|
+
{"action":"snapshot"}
|
|
719
|
+
]'
|
|
720
|
+
|
|
721
|
+
Run 'bp actions' for complete action reference.
|
|
722
|
+
`;
|
|
723
|
+
function parseGlobalOptions(args) {
|
|
724
|
+
const options = {
|
|
725
|
+
output: "pretty"
|
|
726
|
+
};
|
|
727
|
+
const remaining = [];
|
|
728
|
+
for (let i = 0; i < args.length; i++) {
|
|
729
|
+
const arg = args[i];
|
|
730
|
+
if (arg === "-s" || arg === "--session") {
|
|
731
|
+
options.session = args[++i];
|
|
732
|
+
} else if (arg === "-o" || arg === "--output") {
|
|
733
|
+
options.output = args[++i];
|
|
734
|
+
} else if (arg === "--trace") {
|
|
735
|
+
options.trace = true;
|
|
736
|
+
} else if (arg === "-h" || arg === "--help") {
|
|
737
|
+
options.help = true;
|
|
738
|
+
} else {
|
|
739
|
+
remaining.push(arg);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return { options, remaining };
|
|
743
|
+
}
|
|
744
|
+
function output(data, format = "pretty") {
|
|
745
|
+
if (format === "json") {
|
|
746
|
+
console.log(JSON.stringify(data, null, 2));
|
|
747
|
+
} else {
|
|
748
|
+
if (typeof data === "string") {
|
|
749
|
+
console.log(data);
|
|
750
|
+
} else if (typeof data === "object" && data !== null) {
|
|
751
|
+
prettyPrint(data);
|
|
752
|
+
} else {
|
|
753
|
+
console.log(data);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
function prettyPrint(obj, indent = 0) {
|
|
758
|
+
const prefix = " ".repeat(indent);
|
|
759
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
760
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
761
|
+
console.log(`${prefix}${key}:`);
|
|
762
|
+
prettyPrint(value, indent + 1);
|
|
763
|
+
} else if (Array.isArray(value)) {
|
|
764
|
+
console.log(`${prefix}${key}: [${value.length} items]`);
|
|
765
|
+
} else {
|
|
766
|
+
console.log(`${prefix}${key}: ${value}`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
async function main() {
|
|
771
|
+
const args = process.argv.slice(2);
|
|
772
|
+
if (args.length === 0) {
|
|
773
|
+
console.log(HELP);
|
|
774
|
+
process.exit(0);
|
|
775
|
+
}
|
|
776
|
+
const command = args[0];
|
|
777
|
+
const { options, remaining } = parseGlobalOptions(args.slice(1));
|
|
778
|
+
if (options.help && !command) {
|
|
779
|
+
console.log(HELP);
|
|
780
|
+
process.exit(0);
|
|
781
|
+
}
|
|
782
|
+
try {
|
|
783
|
+
switch (command) {
|
|
784
|
+
case "connect":
|
|
785
|
+
await connectCommand(remaining, options);
|
|
786
|
+
break;
|
|
787
|
+
case "exec":
|
|
788
|
+
await execCommand(remaining, options);
|
|
789
|
+
break;
|
|
790
|
+
case "snapshot":
|
|
791
|
+
await snapshotCommand(remaining, options);
|
|
792
|
+
break;
|
|
793
|
+
case "text":
|
|
794
|
+
await textCommand(remaining, options);
|
|
795
|
+
break;
|
|
796
|
+
case "screenshot":
|
|
797
|
+
await screenshotCommand(remaining, options);
|
|
798
|
+
break;
|
|
799
|
+
case "close":
|
|
800
|
+
await closeCommand(remaining, options);
|
|
801
|
+
break;
|
|
802
|
+
case "list":
|
|
803
|
+
await listCommand(remaining, options);
|
|
804
|
+
break;
|
|
805
|
+
case "actions":
|
|
806
|
+
await actionsCommand();
|
|
807
|
+
break;
|
|
808
|
+
case "help":
|
|
809
|
+
case "--help":
|
|
810
|
+
case "-h":
|
|
811
|
+
console.log(HELP);
|
|
812
|
+
break;
|
|
813
|
+
default:
|
|
814
|
+
console.error(`Unknown command: ${command}`);
|
|
815
|
+
console.log(HELP);
|
|
816
|
+
process.exit(1);
|
|
817
|
+
}
|
|
818
|
+
} catch (error) {
|
|
819
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
820
|
+
console.error(`Error: ${message}`);
|
|
821
|
+
process.exit(1);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
main();
|
|
825
|
+
export {
|
|
826
|
+
output
|
|
827
|
+
};
|