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/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
+ };