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.cjs ADDED
@@ -0,0 +1,3587 @@
1
+ #!/usr/bin/env bun
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/cli/index.ts
32
+ var cli_exports = {};
33
+ __export(cli_exports, {
34
+ output: () => output
35
+ });
36
+ module.exports = __toCommonJS(cli_exports);
37
+
38
+ // src/cli/commands/actions.ts
39
+ var ACTIONS_HELP = `
40
+ bp actions - Complete action reference
41
+
42
+ All actions are JSON objects with "action" field. Use with 'bp exec'.
43
+
44
+ NAVIGATION
45
+ {"action": "goto", "url": "https://..."}
46
+ Navigate to URL.
47
+
48
+ {"action": "wait", "waitFor": "navigation"}
49
+ Wait for page navigation to complete.
50
+
51
+ {"action": "wait", "waitFor": "networkIdle"}
52
+ Wait for network activity to settle.
53
+
54
+ {"action": "wait", "timeout": 2000}
55
+ Simple delay in milliseconds.
56
+
57
+ INTERACTION
58
+ {"action": "click", "selector": "#button"}
59
+ {"action": "click", "selector": ["#primary", ".fallback"]}
60
+ Click element. Multi-selector tries each until success.
61
+
62
+ {"action": "fill", "selector": "#input", "value": "text"}
63
+ {"action": "fill", "selector": "#input", "value": "text", "clear": false}
64
+ Fill input field. Clears first by default.
65
+
66
+ {"action": "type", "selector": "#input", "value": "text", "delay": 50}
67
+ Type character-by-character (for autocomplete).
68
+
69
+ {"action": "select", "selector": "#dropdown", "value": "option-value"}
70
+ Select native <select> option by value.
71
+
72
+ {"action": "select", "trigger": ".dropdown", "option": ".item", "value": "Label", "match": "text"}
73
+ Custom dropdown: click trigger, then click matching option.
74
+
75
+ {"action": "check", "selector": "#checkbox"}
76
+ {"action": "uncheck", "selector": "#checkbox"}
77
+ Check/uncheck checkbox or radio.
78
+
79
+ {"action": "submit", "selector": "form"}
80
+ {"action": "submit", "selector": "#btn", "method": "click"}
81
+ Submit form. Methods: enter | click | enter+click (default).
82
+
83
+ {"action": "press", "key": "Enter"}
84
+ {"action": "press", "key": "Escape"}
85
+ {"action": "press", "key": "Tab"}
86
+ Press key. Common keys: Enter, Tab, Escape, Backspace, Delete, ArrowUp/Down/Left/Right.
87
+
88
+ {"action": "focus", "selector": "#input"}
89
+ {"action": "hover", "selector": ".menu-item"}
90
+ Focus or hover element.
91
+
92
+ {"action": "scroll", "selector": "#footer"}
93
+ {"action": "scroll", "x": 0, "y": 1000}
94
+ {"action": "scroll", "direction": "down", "amount": 500}
95
+ Scroll to element, coordinates, or by direction (up/down/left/right).
96
+
97
+ WAITING
98
+ {"action": "wait", "selector": ".loaded", "waitFor": "visible"}
99
+ {"action": "wait", "selector": ".spinner", "waitFor": "hidden"}
100
+ {"action": "wait", "selector": "#element", "waitFor": "attached"}
101
+ {"action": "wait", "selector": "#removed", "waitFor": "detached"}
102
+ Wait for element state. States: visible | hidden | attached | detached.
103
+
104
+ {"action": "wait", "timeout": 1000}
105
+ Simple delay (milliseconds).
106
+
107
+ CONTENT EXTRACTION
108
+ {"action": "snapshot"}
109
+ Get accessibility tree (best for understanding page structure).
110
+
111
+ {"action": "screenshot"}
112
+ {"action": "screenshot", "fullPage": true, "format": "jpeg", "quality": 80}
113
+ Capture screenshot. Formats: png | jpeg | webp.
114
+
115
+ {"action": "evaluate", "value": "document.title"}
116
+ Run JavaScript and return result.
117
+
118
+ IFRAME NAVIGATION
119
+ {"action": "switchFrame", "selector": "iframe#checkout"}
120
+ Switch context to an iframe. All subsequent actions target the iframe content.
121
+
122
+ {"action": "switchToMain"}
123
+ Switch back to the main document from an iframe.
124
+
125
+ Example iframe workflow:
126
+ [
127
+ {"action": "switchFrame", "selector": "iframe#payment"},
128
+ {"action": "fill", "selector": "#card-number", "value": "4242424242424242"},
129
+ {"action": "fill", "selector": "#expiry", "value": "12/25"},
130
+ {"action": "switchToMain"},
131
+ {"action": "click", "selector": "#submit-order"}
132
+ ]
133
+
134
+ Note: Cross-origin iframes cannot be accessed due to browser security.
135
+
136
+ DIALOG HANDLING
137
+ Use --dialog flag: bp exec --dialog accept '[...]'
138
+ Modes: accept (click OK), dismiss (click Cancel)
139
+
140
+ WARNING: Without --dialog flag, native dialogs (alert/confirm/prompt) will
141
+ block ALL automation until manual intervention.
142
+
143
+ COMMON OPTIONS (all actions)
144
+ "timeout": 5000 Override default timeout (ms)
145
+ "optional": true Don't fail if element not found
146
+
147
+ REF SELECTORS (from snapshot)
148
+ After taking a snapshot, use refs directly:
149
+ bp snapshot -s dev --format text # Shows: button "Submit" [ref=e4]
150
+ bp exec '{"action":"click","selector":"ref:e4"}'
151
+
152
+ Refs are stable until navigation. Prefix with "ref:" to use.
153
+ Example: {"action":"fill","selector":"ref:e23","value":"hello"}
154
+
155
+ MULTI-SELECTOR PATTERN
156
+ All selectors accept arrays: ["#id", ".class", "[aria-label=X]"]
157
+ Tries each in order until one succeeds.
158
+ Combine refs with CSS fallbacks: ["ref:e4", "#submit", ".btn"]
159
+
160
+ SELECTOR PRIORITY (Most to Least Reliable)
161
+ 1. ref:eN - From snapshot, most reliable for AI agents
162
+ 2. [data-testid="..."] - Explicit test hooks
163
+ 3. #id - Reliable if IDs are stable
164
+ 4. [aria-label="..."] - Good for buttons without testids
165
+ 5. Multi-selector array - Fallback pattern for compatibility
166
+
167
+ SHADOW DOM
168
+ Selectors automatically pierce shadow DOM (1-2 levels). No special syntax needed.
169
+ For deeper nesting (3+ levels), use refs from snapshot - they work at any depth.
170
+
171
+ :has-text() SELECTOR
172
+ Matches elements containing text content.
173
+ Does NOT match aria-label - use [aria-label="..."] instead.
174
+ Example: button:has-text("Submit") matches <button>Submit</button>
175
+ button[aria-label="Submit"] matches <button aria-label="Submit">X</button>
176
+
177
+ EXAMPLES
178
+ # Login flow
179
+ bp exec '[
180
+ {"action":"goto","url":"https://app.example.com/login"},
181
+ {"action":"fill","selector":"#email","value":"user@example.com"},
182
+ {"action":"fill","selector":"#password","value":"secret"},
183
+ {"action":"submit","selector":"form"},
184
+ {"action":"wait","waitFor":"navigation"},
185
+ {"action":"snapshot"}
186
+ ]'
187
+
188
+ # Handle cookie banner then extract content
189
+ bp exec '[
190
+ {"action":"goto","url":"https://example.com"},
191
+ {"action":"click","selector":"#accept-cookies","optional":true,"timeout":3000},
192
+ {"action":"snapshot"}
193
+ ]'
194
+
195
+ # Use ref from snapshot
196
+ bp snapshot --format text # Note the refs
197
+ bp exec '{"action":"click","selector":"ref:e4"}'
198
+
199
+ # Scroll and wait
200
+ bp exec '[
201
+ {"action":"scroll","direction":"down","amount":1000},
202
+ {"action":"wait","timeout":500},
203
+ {"action":"scroll","direction":"down","amount":1000}
204
+ ]'
205
+
206
+ # Handle dialogs
207
+ bp exec --dialog accept '[
208
+ {"action":"click","selector":"#delete-btn"},
209
+ {"action":"wait","selector":"#success-message","waitFor":"visible"}
210
+ ]'
211
+ `;
212
+ async function actionsCommand() {
213
+ console.log(ACTIONS_HELP);
214
+ }
215
+
216
+ // src/actions/executor.ts
217
+ var DEFAULT_TIMEOUT = 3e4;
218
+ var BatchExecutor = class {
219
+ page;
220
+ constructor(page) {
221
+ this.page = page;
222
+ }
223
+ /**
224
+ * Execute a batch of steps
225
+ */
226
+ async execute(steps, options = {}) {
227
+ const { timeout = DEFAULT_TIMEOUT, onFail = "stop" } = options;
228
+ const results = [];
229
+ const startTime = Date.now();
230
+ for (let i = 0; i < steps.length; i++) {
231
+ const step = steps[i];
232
+ const stepStart = Date.now();
233
+ try {
234
+ const result = await this.executeStep(step, timeout);
235
+ results.push({
236
+ index: i,
237
+ action: step.action,
238
+ selector: step.selector,
239
+ selectorUsed: result.selectorUsed,
240
+ success: true,
241
+ durationMs: Date.now() - stepStart,
242
+ result: result.value,
243
+ text: result.text
244
+ });
245
+ } catch (error) {
246
+ const errorMessage = error instanceof Error ? error.message : String(error);
247
+ results.push({
248
+ index: i,
249
+ action: step.action,
250
+ selector: step.selector,
251
+ success: false,
252
+ durationMs: Date.now() - stepStart,
253
+ error: errorMessage
254
+ });
255
+ if (onFail === "stop" && !step.optional) {
256
+ return {
257
+ success: false,
258
+ stoppedAtIndex: i,
259
+ steps: results,
260
+ totalDurationMs: Date.now() - startTime
261
+ };
262
+ }
263
+ }
264
+ }
265
+ const allSuccess = results.every((r) => r.success || steps[r.index]?.optional);
266
+ return {
267
+ success: allSuccess,
268
+ steps: results,
269
+ totalDurationMs: Date.now() - startTime
270
+ };
271
+ }
272
+ /**
273
+ * Execute a single step
274
+ */
275
+ async executeStep(step, defaultTimeout) {
276
+ const timeout = step.timeout ?? defaultTimeout;
277
+ const optional = step.optional ?? false;
278
+ switch (step.action) {
279
+ case "goto": {
280
+ if (!step.url) throw new Error("goto requires url");
281
+ await this.page.goto(step.url, { timeout, optional });
282
+ return {};
283
+ }
284
+ case "click": {
285
+ if (!step.selector) throw new Error("click requires selector");
286
+ if (step.waitForNavigation) {
287
+ const navPromise = this.page.waitForNavigation({ timeout, optional });
288
+ await this.page.click(step.selector, { timeout, optional });
289
+ await navPromise;
290
+ } else {
291
+ await this.page.click(step.selector, { timeout, optional });
292
+ }
293
+ return { selectorUsed: this.getUsedSelector(step.selector) };
294
+ }
295
+ case "fill": {
296
+ if (!step.selector) throw new Error("fill requires selector");
297
+ if (typeof step.value !== "string") throw new Error("fill requires string value");
298
+ await this.page.fill(step.selector, step.value, {
299
+ timeout,
300
+ optional,
301
+ clear: step.clear ?? true
302
+ });
303
+ return { selectorUsed: this.getUsedSelector(step.selector) };
304
+ }
305
+ case "type": {
306
+ if (!step.selector) throw new Error("type requires selector");
307
+ if (typeof step.value !== "string") throw new Error("type requires string value");
308
+ await this.page.type(step.selector, step.value, {
309
+ timeout,
310
+ optional,
311
+ delay: step.delay ?? 50
312
+ });
313
+ return { selectorUsed: this.getUsedSelector(step.selector) };
314
+ }
315
+ case "select": {
316
+ if (step.trigger && step.option && typeof step.value === "string") {
317
+ await this.page.select(
318
+ {
319
+ trigger: step.trigger,
320
+ option: step.option,
321
+ value: step.value,
322
+ match: step.match
323
+ },
324
+ { timeout, optional }
325
+ );
326
+ return { selectorUsed: this.getUsedSelector(step.trigger) };
327
+ }
328
+ if (!step.selector) throw new Error("select requires selector");
329
+ if (!step.value) throw new Error("select requires value");
330
+ await this.page.select(step.selector, step.value, { timeout, optional });
331
+ return { selectorUsed: this.getUsedSelector(step.selector) };
332
+ }
333
+ case "check": {
334
+ if (!step.selector) throw new Error("check requires selector");
335
+ await this.page.check(step.selector, { timeout, optional });
336
+ return { selectorUsed: this.getUsedSelector(step.selector) };
337
+ }
338
+ case "uncheck": {
339
+ if (!step.selector) throw new Error("uncheck requires selector");
340
+ await this.page.uncheck(step.selector, { timeout, optional });
341
+ return { selectorUsed: this.getUsedSelector(step.selector) };
342
+ }
343
+ case "submit": {
344
+ if (!step.selector) throw new Error("submit requires selector");
345
+ await this.page.submit(step.selector, {
346
+ timeout,
347
+ optional,
348
+ method: step.method ?? "enter+click"
349
+ });
350
+ return { selectorUsed: this.getUsedSelector(step.selector) };
351
+ }
352
+ case "press": {
353
+ if (!step.key) throw new Error("press requires key");
354
+ await this.page.press(step.key);
355
+ return {};
356
+ }
357
+ case "focus": {
358
+ if (!step.selector) throw new Error("focus requires selector");
359
+ await this.page.focus(step.selector, { timeout, optional });
360
+ return { selectorUsed: this.getUsedSelector(step.selector) };
361
+ }
362
+ case "hover": {
363
+ if (!step.selector) throw new Error("hover requires selector");
364
+ await this.page.hover(step.selector, { timeout, optional });
365
+ return { selectorUsed: this.getUsedSelector(step.selector) };
366
+ }
367
+ case "scroll": {
368
+ if (step.x !== void 0 || step.y !== void 0) {
369
+ await this.page.scroll("body", { x: step.x, y: step.y, timeout, optional });
370
+ return {};
371
+ }
372
+ if (!step.selector && (step.direction || step.amount !== void 0)) {
373
+ const amount = step.amount ?? 500;
374
+ const direction = step.direction ?? "down";
375
+ const deltaY = direction === "down" ? amount : direction === "up" ? -amount : 0;
376
+ const deltaX = direction === "right" ? amount : direction === "left" ? -amount : 0;
377
+ await this.page.evaluate(`window.scrollBy(${deltaX}, ${deltaY})`);
378
+ return {};
379
+ }
380
+ if (!step.selector) throw new Error("scroll requires selector, coordinates, or direction");
381
+ await this.page.scroll(step.selector, { timeout, optional });
382
+ return { selectorUsed: this.getUsedSelector(step.selector) };
383
+ }
384
+ case "wait": {
385
+ if (!step.selector && !step.waitFor) {
386
+ const delay = step.timeout ?? 1e3;
387
+ await new Promise((resolve) => setTimeout(resolve, delay));
388
+ return {};
389
+ }
390
+ if (step.waitFor === "navigation") {
391
+ await this.page.waitForNavigation({ timeout, optional });
392
+ return {};
393
+ }
394
+ if (step.waitFor === "networkIdle") {
395
+ await this.page.waitForNetworkIdle({ timeout, optional });
396
+ return {};
397
+ }
398
+ if (!step.selector)
399
+ throw new Error(
400
+ "wait requires selector (or waitFor: navigation/networkIdle, or timeout for simple delay)"
401
+ );
402
+ await this.page.waitFor(step.selector, {
403
+ timeout,
404
+ optional,
405
+ state: step.waitFor ?? "visible"
406
+ });
407
+ return { selectorUsed: this.getUsedSelector(step.selector) };
408
+ }
409
+ case "snapshot": {
410
+ const snapshot = await this.page.snapshot();
411
+ return { value: snapshot };
412
+ }
413
+ case "screenshot": {
414
+ const data = await this.page.screenshot({
415
+ format: step.format,
416
+ quality: step.quality,
417
+ fullPage: step.fullPage
418
+ });
419
+ return { value: data };
420
+ }
421
+ case "evaluate": {
422
+ if (typeof step.value !== "string")
423
+ throw new Error("evaluate requires string value (expression)");
424
+ const result = await this.page.evaluate(step.value);
425
+ return { value: result };
426
+ }
427
+ case "text": {
428
+ const selector = Array.isArray(step.selector) ? step.selector[0] : step.selector;
429
+ const text = await this.page.text(selector);
430
+ return { text, selectorUsed: selector };
431
+ }
432
+ case "switchFrame": {
433
+ if (!step.selector) throw new Error("switchFrame requires selector");
434
+ await this.page.switchToFrame(step.selector, { timeout, optional });
435
+ return { selectorUsed: this.getUsedSelector(step.selector) };
436
+ }
437
+ case "switchToMain": {
438
+ await this.page.switchToMain();
439
+ return {};
440
+ }
441
+ default:
442
+ throw new Error(
443
+ `Unknown action: ${step.action}. Run 'bp actions' for available actions.`
444
+ );
445
+ }
446
+ }
447
+ /**
448
+ * Get the first selector if multiple were provided
449
+ * (actual used selector tracking would need to be implemented in Page)
450
+ */
451
+ getUsedSelector(selector) {
452
+ return Array.isArray(selector) ? selector[0] : selector;
453
+ }
454
+ };
455
+ function addBatchToPage(page) {
456
+ const executor = new BatchExecutor(page);
457
+ return Object.assign(page, {
458
+ batch: (steps, options) => executor.execute(steps, options)
459
+ });
460
+ }
461
+
462
+ // src/cdp/protocol.ts
463
+ var CDPError = class extends Error {
464
+ code;
465
+ data;
466
+ constructor(error) {
467
+ super(error.message);
468
+ this.name = "CDPError";
469
+ this.code = error.code;
470
+ this.data = error.data;
471
+ }
472
+ };
473
+
474
+ // src/cdp/transport.ts
475
+ function createTransport(wsUrl, options = {}) {
476
+ const { timeout = 3e4 } = options;
477
+ return new Promise((resolve, reject) => {
478
+ const timeoutId = setTimeout(() => {
479
+ reject(new Error(`WebSocket connection timeout after ${timeout}ms`));
480
+ }, timeout);
481
+ const ws = new WebSocket(wsUrl);
482
+ const messageHandlers = [];
483
+ const closeHandlers = [];
484
+ const errorHandlers = [];
485
+ ws.addEventListener("open", () => {
486
+ clearTimeout(timeoutId);
487
+ const transport = {
488
+ send(message) {
489
+ if (ws.readyState === WebSocket.OPEN) {
490
+ ws.send(message);
491
+ } else {
492
+ throw new Error(
493
+ `Cannot send message, WebSocket is ${getReadyStateString(ws.readyState)}`
494
+ );
495
+ }
496
+ },
497
+ async close() {
498
+ return new Promise((resolveClose) => {
499
+ if (ws.readyState === WebSocket.CLOSED) {
500
+ resolveClose();
501
+ return;
502
+ }
503
+ const onClose = () => {
504
+ ws.removeEventListener("close", onClose);
505
+ resolveClose();
506
+ };
507
+ ws.addEventListener("close", onClose);
508
+ ws.close();
509
+ setTimeout(resolveClose, 5e3);
510
+ });
511
+ },
512
+ onMessage(handler) {
513
+ messageHandlers.push(handler);
514
+ },
515
+ onClose(handler) {
516
+ closeHandlers.push(handler);
517
+ },
518
+ onError(handler) {
519
+ errorHandlers.push(handler);
520
+ }
521
+ };
522
+ resolve(transport);
523
+ });
524
+ ws.addEventListener("message", (event) => {
525
+ const data = typeof event.data === "string" ? event.data : String(event.data);
526
+ for (const handler of messageHandlers) {
527
+ handler(data);
528
+ }
529
+ });
530
+ ws.addEventListener("close", () => {
531
+ for (const handler of closeHandlers) {
532
+ handler();
533
+ }
534
+ });
535
+ ws.addEventListener("error", (_event) => {
536
+ clearTimeout(timeoutId);
537
+ const error = new Error("WebSocket connection error");
538
+ for (const handler of errorHandlers) {
539
+ handler(error);
540
+ }
541
+ reject(error);
542
+ });
543
+ });
544
+ }
545
+ function getReadyStateString(state) {
546
+ switch (state) {
547
+ case WebSocket.CONNECTING:
548
+ return "CONNECTING";
549
+ case WebSocket.OPEN:
550
+ return "OPEN";
551
+ case WebSocket.CLOSING:
552
+ return "CLOSING";
553
+ case WebSocket.CLOSED:
554
+ return "CLOSED";
555
+ default:
556
+ return "UNKNOWN";
557
+ }
558
+ }
559
+
560
+ // src/cdp/client.ts
561
+ async function createCDPClient(wsUrl, options = {}) {
562
+ const { debug = false, timeout = 3e4 } = options;
563
+ const transport = await createTransport(wsUrl, { timeout });
564
+ let messageId = 0;
565
+ let currentSessionId;
566
+ let connected = true;
567
+ const pending = /* @__PURE__ */ new Map();
568
+ const eventHandlers = /* @__PURE__ */ new Map();
569
+ const anyEventHandlers = /* @__PURE__ */ new Set();
570
+ transport.onMessage((raw) => {
571
+ let msg;
572
+ try {
573
+ msg = JSON.parse(raw);
574
+ } catch {
575
+ if (debug) console.error("[CDP] Failed to parse message:", raw);
576
+ return;
577
+ }
578
+ if (debug) {
579
+ console.log("[CDP] <--", JSON.stringify(msg, null, 2).slice(0, 500));
580
+ }
581
+ if ("id" in msg && typeof msg.id === "number") {
582
+ const response = msg;
583
+ const request = pending.get(response.id);
584
+ if (request) {
585
+ pending.delete(response.id);
586
+ clearTimeout(request.timer);
587
+ if (response.error) {
588
+ request.reject(new CDPError(response.error));
589
+ } else {
590
+ request.resolve(response.result);
591
+ }
592
+ }
593
+ return;
594
+ }
595
+ if ("method" in msg) {
596
+ const event = msg;
597
+ const params = event.params ?? {};
598
+ for (const handler of anyEventHandlers) {
599
+ try {
600
+ handler(event.method, params);
601
+ } catch (e) {
602
+ if (debug) console.error("[CDP] Error in any-event handler:", e);
603
+ }
604
+ }
605
+ const handlers = eventHandlers.get(event.method);
606
+ if (handlers) {
607
+ for (const handler of handlers) {
608
+ try {
609
+ handler(params);
610
+ } catch (e) {
611
+ if (debug) console.error(`[CDP] Error in handler for ${event.method}:`, e);
612
+ }
613
+ }
614
+ }
615
+ }
616
+ });
617
+ transport.onClose(() => {
618
+ connected = false;
619
+ for (const [id, request] of pending) {
620
+ clearTimeout(request.timer);
621
+ request.reject(new Error("WebSocket connection closed"));
622
+ pending.delete(id);
623
+ }
624
+ });
625
+ transport.onError((error) => {
626
+ if (debug) console.error("[CDP] Transport error:", error);
627
+ });
628
+ const client = {
629
+ async send(method, params, sessionId) {
630
+ if (!connected) {
631
+ throw new Error("CDP client is not connected");
632
+ }
633
+ const id = ++messageId;
634
+ const effectiveSessionId = sessionId ?? currentSessionId;
635
+ const request = { id, method };
636
+ if (params !== void 0) {
637
+ request.params = params;
638
+ }
639
+ if (effectiveSessionId !== void 0) {
640
+ request.sessionId = effectiveSessionId;
641
+ }
642
+ const message = JSON.stringify(request);
643
+ if (debug) {
644
+ console.log("[CDP] -->", message.slice(0, 500));
645
+ }
646
+ return new Promise((resolve, reject) => {
647
+ const timer = setTimeout(() => {
648
+ pending.delete(id);
649
+ reject(new Error(`CDP command ${method} timed out after ${timeout}ms`));
650
+ }, timeout);
651
+ pending.set(id, {
652
+ resolve,
653
+ reject,
654
+ method,
655
+ timer
656
+ });
657
+ try {
658
+ transport.send(message);
659
+ } catch (e) {
660
+ pending.delete(id);
661
+ clearTimeout(timer);
662
+ reject(e);
663
+ }
664
+ });
665
+ },
666
+ on(event, handler) {
667
+ let handlers = eventHandlers.get(event);
668
+ if (!handlers) {
669
+ handlers = /* @__PURE__ */ new Set();
670
+ eventHandlers.set(event, handlers);
671
+ }
672
+ handlers.add(handler);
673
+ },
674
+ off(event, handler) {
675
+ const handlers = eventHandlers.get(event);
676
+ if (handlers) {
677
+ handlers.delete(handler);
678
+ if (handlers.size === 0) {
679
+ eventHandlers.delete(event);
680
+ }
681
+ }
682
+ },
683
+ onAny(handler) {
684
+ anyEventHandlers.add(handler);
685
+ },
686
+ async close() {
687
+ connected = false;
688
+ await transport.close();
689
+ },
690
+ async attachToTarget(targetId) {
691
+ const result = await this.send("Target.attachToTarget", {
692
+ targetId,
693
+ flatten: true
694
+ });
695
+ currentSessionId = result.sessionId;
696
+ return result.sessionId;
697
+ },
698
+ get sessionId() {
699
+ return currentSessionId;
700
+ },
701
+ get isConnected() {
702
+ return connected;
703
+ }
704
+ };
705
+ return client;
706
+ }
707
+
708
+ // src/providers/browserbase.ts
709
+ var BrowserBaseProvider = class {
710
+ name = "browserbase";
711
+ apiKey;
712
+ projectId;
713
+ baseUrl;
714
+ constructor(options) {
715
+ this.apiKey = options.apiKey;
716
+ this.projectId = options.projectId;
717
+ this.baseUrl = options.baseUrl ?? "https://api.browserbase.com";
718
+ }
719
+ async createSession(options = {}) {
720
+ const response = await fetch(`${this.baseUrl}/v1/sessions`, {
721
+ method: "POST",
722
+ headers: {
723
+ "X-BB-API-Key": this.apiKey,
724
+ "Content-Type": "application/json"
725
+ },
726
+ body: JSON.stringify({
727
+ projectId: this.projectId,
728
+ browserSettings: {
729
+ viewport: options.width && options.height ? {
730
+ width: options.width,
731
+ height: options.height
732
+ } : void 0
733
+ },
734
+ ...options
735
+ })
736
+ });
737
+ if (!response.ok) {
738
+ const text = await response.text();
739
+ throw new Error(`BrowserBase createSession failed: ${response.status} ${text}`);
740
+ }
741
+ const session = await response.json();
742
+ const connectResponse = await fetch(`${this.baseUrl}/v1/sessions/${session.id}`, {
743
+ headers: {
744
+ "X-BB-API-Key": this.apiKey
745
+ }
746
+ });
747
+ if (!connectResponse.ok) {
748
+ throw new Error(`BrowserBase getSession failed: ${connectResponse.status}`);
749
+ }
750
+ const sessionDetails = await connectResponse.json();
751
+ if (!sessionDetails.connectUrl) {
752
+ throw new Error("BrowserBase session does not have a connectUrl");
753
+ }
754
+ return {
755
+ wsUrl: sessionDetails.connectUrl,
756
+ sessionId: session.id,
757
+ metadata: {
758
+ debugUrl: sessionDetails.debugUrl,
759
+ projectId: this.projectId,
760
+ status: sessionDetails.status
761
+ },
762
+ close: async () => {
763
+ await fetch(`${this.baseUrl}/v1/sessions/${session.id}`, {
764
+ method: "DELETE",
765
+ headers: {
766
+ "X-BB-API-Key": this.apiKey
767
+ }
768
+ });
769
+ }
770
+ };
771
+ }
772
+ async resumeSession(sessionId) {
773
+ const response = await fetch(`${this.baseUrl}/v1/sessions/${sessionId}`, {
774
+ headers: {
775
+ "X-BB-API-Key": this.apiKey
776
+ }
777
+ });
778
+ if (!response.ok) {
779
+ throw new Error(`BrowserBase resumeSession failed: ${response.status}`);
780
+ }
781
+ const session = await response.json();
782
+ if (!session.connectUrl) {
783
+ throw new Error("BrowserBase session does not have a connectUrl (may be closed)");
784
+ }
785
+ return {
786
+ wsUrl: session.connectUrl,
787
+ sessionId: session.id,
788
+ metadata: {
789
+ debugUrl: session.debugUrl,
790
+ projectId: this.projectId,
791
+ status: session.status
792
+ },
793
+ close: async () => {
794
+ await fetch(`${this.baseUrl}/v1/sessions/${sessionId}`, {
795
+ method: "DELETE",
796
+ headers: {
797
+ "X-BB-API-Key": this.apiKey
798
+ }
799
+ });
800
+ }
801
+ };
802
+ }
803
+ };
804
+
805
+ // src/providers/browserless.ts
806
+ var BrowserlessProvider = class {
807
+ name = "browserless";
808
+ token;
809
+ baseUrl;
810
+ constructor(options) {
811
+ this.token = options.token;
812
+ this.baseUrl = options.baseUrl ?? "wss://chrome.browserless.io";
813
+ }
814
+ async createSession(options = {}) {
815
+ const params = new URLSearchParams({
816
+ token: this.token
817
+ });
818
+ if (options.width && options.height) {
819
+ params.set("--window-size", `${options.width},${options.height}`);
820
+ }
821
+ if (options.proxy?.server) {
822
+ params.set("--proxy-server", options.proxy.server);
823
+ }
824
+ const wsUrl = `${this.baseUrl}?${params.toString()}`;
825
+ return {
826
+ wsUrl,
827
+ metadata: {
828
+ provider: "browserless"
829
+ },
830
+ close: async () => {
831
+ }
832
+ };
833
+ }
834
+ // Browserless doesn't support session resumption in the same way
835
+ // Each connection is a fresh browser instance
836
+ };
837
+
838
+ // src/providers/generic.ts
839
+ var GenericProvider = class {
840
+ name = "generic";
841
+ wsUrl;
842
+ constructor(options) {
843
+ this.wsUrl = options.wsUrl;
844
+ }
845
+ async createSession(_options = {}) {
846
+ return {
847
+ wsUrl: this.wsUrl,
848
+ metadata: {
849
+ provider: "generic"
850
+ },
851
+ close: async () => {
852
+ }
853
+ };
854
+ }
855
+ };
856
+ async function getBrowserWebSocketUrl(host = "localhost:9222") {
857
+ const protocol = host.includes("://") ? "" : "http://";
858
+ const response = await fetch(`${protocol}${host}/json/version`);
859
+ if (!response.ok) {
860
+ throw new Error(`Failed to get browser info: ${response.status}`);
861
+ }
862
+ const info = await response.json();
863
+ return info.webSocketDebuggerUrl;
864
+ }
865
+
866
+ // src/providers/index.ts
867
+ function createProvider(options) {
868
+ switch (options.provider) {
869
+ case "browserbase":
870
+ if (!options.apiKey) {
871
+ throw new Error("BrowserBase provider requires apiKey");
872
+ }
873
+ if (!options.projectId) {
874
+ throw new Error("BrowserBase provider requires projectId");
875
+ }
876
+ return new BrowserBaseProvider({
877
+ apiKey: options.apiKey,
878
+ projectId: options.projectId
879
+ });
880
+ case "browserless":
881
+ if (!options.apiKey) {
882
+ throw new Error("Browserless provider requires apiKey (token)");
883
+ }
884
+ return new BrowserlessProvider({
885
+ token: options.apiKey
886
+ });
887
+ case "generic":
888
+ if (!options.wsUrl) {
889
+ throw new Error("Generic provider requires wsUrl");
890
+ }
891
+ return new GenericProvider({
892
+ wsUrl: options.wsUrl
893
+ });
894
+ default:
895
+ throw new Error(`Unknown provider: ${options.provider}`);
896
+ }
897
+ }
898
+
899
+ // src/network/interceptor.ts
900
+ var RequestInterceptor = class {
901
+ cdp;
902
+ enabled = false;
903
+ handlers = [];
904
+ pendingRequests = /* @__PURE__ */ new Map();
905
+ boundHandleRequestPaused;
906
+ boundHandleAuthRequired;
907
+ constructor(cdp) {
908
+ this.cdp = cdp;
909
+ this.boundHandleRequestPaused = this.handleRequestPaused.bind(this);
910
+ this.boundHandleAuthRequired = this.handleAuthRequired.bind(this);
911
+ }
912
+ /**
913
+ * Enable request interception with optional patterns
914
+ */
915
+ async enable(patterns) {
916
+ if (this.enabled) return;
917
+ this.cdp.on("Fetch.requestPaused", this.boundHandleRequestPaused);
918
+ this.cdp.on("Fetch.authRequired", this.boundHandleAuthRequired);
919
+ await this.cdp.send("Fetch.enable", {
920
+ patterns: patterns?.map((p) => ({
921
+ urlPattern: p.urlPattern ?? "*",
922
+ resourceType: p.resourceType,
923
+ requestStage: p.requestStage ?? "Request"
924
+ })) ?? [{ urlPattern: "*" }],
925
+ handleAuthRequests: true
926
+ });
927
+ this.enabled = true;
928
+ }
929
+ /**
930
+ * Disable request interception
931
+ */
932
+ async disable() {
933
+ if (!this.enabled) return;
934
+ await this.cdp.send("Fetch.disable");
935
+ this.cdp.off("Fetch.requestPaused", this.boundHandleRequestPaused);
936
+ this.cdp.off("Fetch.authRequired", this.boundHandleAuthRequired);
937
+ this.enabled = false;
938
+ this.handlers = [];
939
+ this.pendingRequests.clear();
940
+ }
941
+ /**
942
+ * Add a request handler
943
+ */
944
+ addHandler(pattern, handler) {
945
+ const entry = { pattern, handler };
946
+ this.handlers.push(entry);
947
+ return () => {
948
+ const idx = this.handlers.indexOf(entry);
949
+ if (idx !== -1) this.handlers.splice(idx, 1);
950
+ };
951
+ }
952
+ /**
953
+ * Handle paused request from CDP
954
+ */
955
+ async handleRequestPaused(params) {
956
+ const requestId = params["requestId"];
957
+ const request = params["request"];
958
+ const responseStatusCode = params["responseStatusCode"];
959
+ const responseHeaders = params["responseHeaders"];
960
+ const intercepted = {
961
+ requestId,
962
+ url: request["url"],
963
+ method: request["method"],
964
+ headers: request["headers"],
965
+ postData: request["postData"],
966
+ resourceType: params["resourceType"],
967
+ frameId: params["frameId"],
968
+ isNavigationRequest: params["isNavigationRequest"],
969
+ responseStatusCode,
970
+ responseHeaders: responseHeaders ? Object.fromEntries(responseHeaders.map((h) => [h.name, h.value])) : void 0
971
+ };
972
+ this.pendingRequests.set(requestId, { request: intercepted, handled: false });
973
+ const matchingHandler = this.handlers.find((h) => this.matchesPattern(intercepted, h.pattern));
974
+ if (matchingHandler) {
975
+ const actions = this.createActions(requestId);
976
+ try {
977
+ await matchingHandler.handler(intercepted, actions);
978
+ } catch (err) {
979
+ console.error("[RequestInterceptor] Handler error:", err);
980
+ if (!this.pendingRequests.get(requestId)?.handled) {
981
+ await actions.continue();
982
+ }
983
+ }
984
+ } else {
985
+ await this.continueRequest(requestId);
986
+ }
987
+ this.pendingRequests.delete(requestId);
988
+ }
989
+ /**
990
+ * Handle auth challenge
991
+ */
992
+ async handleAuthRequired(params) {
993
+ const requestId = params["requestId"];
994
+ await this.cdp.send("Fetch.continueWithAuth", {
995
+ requestId,
996
+ authChallengeResponse: { response: "CancelAuth" }
997
+ });
998
+ }
999
+ /**
1000
+ * Check if request matches pattern
1001
+ */
1002
+ matchesPattern(request, pattern) {
1003
+ if (pattern.resourceType && request.resourceType !== pattern.resourceType) {
1004
+ return false;
1005
+ }
1006
+ if (pattern.urlPattern) {
1007
+ const regex = this.globToRegex(pattern.urlPattern);
1008
+ if (!regex.test(request.url)) {
1009
+ return false;
1010
+ }
1011
+ }
1012
+ return true;
1013
+ }
1014
+ /**
1015
+ * Convert glob pattern to regex
1016
+ */
1017
+ globToRegex(pattern) {
1018
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
1019
+ return new RegExp(`^${escaped}$`);
1020
+ }
1021
+ /**
1022
+ * Create actions object for handler
1023
+ */
1024
+ createActions(requestId) {
1025
+ const pending = this.pendingRequests.get(requestId);
1026
+ const markHandled = () => {
1027
+ if (pending) pending.handled = true;
1028
+ };
1029
+ return {
1030
+ continue: async (options) => {
1031
+ markHandled();
1032
+ await this.continueRequest(requestId, options);
1033
+ },
1034
+ fulfill: async (options) => {
1035
+ markHandled();
1036
+ await this.fulfillRequest(requestId, options);
1037
+ },
1038
+ fail: async (options) => {
1039
+ markHandled();
1040
+ await this.failRequest(requestId, options);
1041
+ }
1042
+ };
1043
+ }
1044
+ /**
1045
+ * Continue a paused request
1046
+ */
1047
+ async continueRequest(requestId, options) {
1048
+ await this.cdp.send("Fetch.continueRequest", {
1049
+ requestId,
1050
+ url: options?.url,
1051
+ method: options?.method,
1052
+ headers: options?.headers ? Object.entries(options.headers).map(([name, value]) => ({ name, value })) : void 0,
1053
+ postData: options?.postData ? btoa(options.postData) : void 0
1054
+ });
1055
+ }
1056
+ /**
1057
+ * Fulfill a request with custom response
1058
+ */
1059
+ async fulfillRequest(requestId, options) {
1060
+ const headers = Object.entries(options.headers ?? {}).map(([name, value]) => ({
1061
+ name,
1062
+ value
1063
+ }));
1064
+ await this.cdp.send("Fetch.fulfillRequest", {
1065
+ requestId,
1066
+ responseCode: options.status,
1067
+ responseHeaders: headers,
1068
+ body: options.isBase64Encoded ? options.body : options.body ? btoa(options.body) : void 0
1069
+ });
1070
+ }
1071
+ /**
1072
+ * Fail/abort a request
1073
+ */
1074
+ async failRequest(requestId, options) {
1075
+ await this.cdp.send("Fetch.failRequest", {
1076
+ requestId,
1077
+ errorReason: options?.reason ?? "BlockedByClient"
1078
+ });
1079
+ }
1080
+ };
1081
+
1082
+ // src/wait/strategies.ts
1083
+ var DEEP_QUERY_SCRIPT = `
1084
+ function deepQuery(selector, root = document) {
1085
+ // Try direct query first (fastest path)
1086
+ let el = root.querySelector(selector);
1087
+ if (el) return el;
1088
+
1089
+ // Search in shadow roots recursively
1090
+ const searchShadows = (node) => {
1091
+ // Check if this node has a shadow root
1092
+ if (node.shadowRoot) {
1093
+ el = node.shadowRoot.querySelector(selector);
1094
+ if (el) return el;
1095
+ // Search children of shadow root
1096
+ for (const child of node.shadowRoot.querySelectorAll('*')) {
1097
+ el = searchShadows(child);
1098
+ if (el) return el;
1099
+ }
1100
+ }
1101
+ // Search children that might have shadow roots
1102
+ for (const child of node.querySelectorAll('*')) {
1103
+ if (child.shadowRoot) {
1104
+ el = searchShadows(child);
1105
+ if (el) return el;
1106
+ }
1107
+ }
1108
+ return null;
1109
+ };
1110
+
1111
+ return searchShadows(root);
1112
+ }
1113
+ `;
1114
+ async function isElementVisible(cdp, selector, contextId) {
1115
+ const params = {
1116
+ expression: `(() => {
1117
+ ${DEEP_QUERY_SCRIPT}
1118
+ const el = deepQuery(${JSON.stringify(selector)});
1119
+ if (!el) return false;
1120
+ const style = getComputedStyle(el);
1121
+ if (style.display === 'none') return false;
1122
+ if (style.visibility === 'hidden') return false;
1123
+ if (parseFloat(style.opacity) === 0) return false;
1124
+ const rect = el.getBoundingClientRect();
1125
+ return rect.width > 0 && rect.height > 0;
1126
+ })()`,
1127
+ returnByValue: true
1128
+ };
1129
+ if (contextId !== void 0) {
1130
+ params["contextId"] = contextId;
1131
+ }
1132
+ const result = await cdp.send("Runtime.evaluate", params);
1133
+ return result.result.value === true;
1134
+ }
1135
+ async function isElementAttached(cdp, selector, contextId) {
1136
+ const params = {
1137
+ expression: `(() => {
1138
+ ${DEEP_QUERY_SCRIPT}
1139
+ return deepQuery(${JSON.stringify(selector)}) !== null;
1140
+ })()`,
1141
+ returnByValue: true
1142
+ };
1143
+ if (contextId !== void 0) {
1144
+ params["contextId"] = contextId;
1145
+ }
1146
+ const result = await cdp.send("Runtime.evaluate", params);
1147
+ return result.result.value === true;
1148
+ }
1149
+ function sleep(ms) {
1150
+ return new Promise((resolve) => setTimeout(resolve, ms));
1151
+ }
1152
+ async function waitForAnyElement(cdp, selectors, options = {}) {
1153
+ const { state = "visible", timeout = 3e4, pollInterval = 100, contextId } = options;
1154
+ const startTime = Date.now();
1155
+ const deadline = startTime + timeout;
1156
+ while (Date.now() < deadline) {
1157
+ for (const selector of selectors) {
1158
+ let conditionMet = false;
1159
+ switch (state) {
1160
+ case "visible":
1161
+ conditionMet = await isElementVisible(cdp, selector, contextId);
1162
+ break;
1163
+ case "hidden":
1164
+ conditionMet = !await isElementVisible(cdp, selector, contextId);
1165
+ break;
1166
+ case "attached":
1167
+ conditionMet = await isElementAttached(cdp, selector, contextId);
1168
+ break;
1169
+ case "detached":
1170
+ conditionMet = !await isElementAttached(cdp, selector, contextId);
1171
+ break;
1172
+ }
1173
+ if (conditionMet) {
1174
+ return { success: true, selector, waitedMs: Date.now() - startTime };
1175
+ }
1176
+ }
1177
+ await sleep(pollInterval);
1178
+ }
1179
+ return { success: false, waitedMs: Date.now() - startTime };
1180
+ }
1181
+ async function getCurrentUrl(cdp) {
1182
+ const result = await cdp.send("Runtime.evaluate", {
1183
+ expression: "location.href",
1184
+ returnByValue: true
1185
+ });
1186
+ return result.result.value;
1187
+ }
1188
+ async function waitForNavigation(cdp, options = {}) {
1189
+ const { timeout = 3e4, allowSameDocument = true } = options;
1190
+ const startTime = Date.now();
1191
+ let startUrl;
1192
+ try {
1193
+ startUrl = await getCurrentUrl(cdp);
1194
+ } catch {
1195
+ startUrl = "";
1196
+ }
1197
+ return new Promise((resolve) => {
1198
+ let resolved = false;
1199
+ const cleanup = [];
1200
+ const done = (success) => {
1201
+ if (resolved) return;
1202
+ resolved = true;
1203
+ for (const fn of cleanup) fn();
1204
+ resolve({ success, waitedMs: Date.now() - startTime });
1205
+ };
1206
+ const timer = setTimeout(() => done(false), timeout);
1207
+ cleanup.push(() => clearTimeout(timer));
1208
+ const onLoad = () => done(true);
1209
+ cdp.on("Page.loadEventFired", onLoad);
1210
+ cleanup.push(() => cdp.off("Page.loadEventFired", onLoad));
1211
+ const onFrameNavigated = (params) => {
1212
+ const frame = params["frame"];
1213
+ if (frame && !frame.parentId && frame.url !== startUrl) {
1214
+ done(true);
1215
+ }
1216
+ };
1217
+ cdp.on("Page.frameNavigated", onFrameNavigated);
1218
+ cleanup.push(() => cdp.off("Page.frameNavigated", onFrameNavigated));
1219
+ if (allowSameDocument) {
1220
+ const onSameDoc = () => done(true);
1221
+ cdp.on("Page.navigatedWithinDocument", onSameDoc);
1222
+ cleanup.push(() => cdp.off("Page.navigatedWithinDocument", onSameDoc));
1223
+ }
1224
+ const pollUrl = async () => {
1225
+ while (!resolved && Date.now() < startTime + timeout) {
1226
+ await sleep(100);
1227
+ if (resolved) return;
1228
+ try {
1229
+ const currentUrl = await getCurrentUrl(cdp);
1230
+ if (startUrl && currentUrl !== startUrl) {
1231
+ done(true);
1232
+ return;
1233
+ }
1234
+ } catch {
1235
+ }
1236
+ }
1237
+ };
1238
+ pollUrl();
1239
+ });
1240
+ }
1241
+ async function waitForNetworkIdle(cdp, options = {}) {
1242
+ const { timeout = 3e4, idleTime = 500 } = options;
1243
+ const startTime = Date.now();
1244
+ await cdp.send("Network.enable");
1245
+ return new Promise((resolve) => {
1246
+ let inFlight = 0;
1247
+ let idleTimer = null;
1248
+ const timeoutTimer = setTimeout(() => {
1249
+ cleanup();
1250
+ resolve({ success: false, waitedMs: Date.now() - startTime });
1251
+ }, timeout);
1252
+ const checkIdle = () => {
1253
+ if (inFlight === 0) {
1254
+ if (idleTimer) clearTimeout(idleTimer);
1255
+ idleTimer = setTimeout(() => {
1256
+ cleanup();
1257
+ resolve({ success: true, waitedMs: Date.now() - startTime });
1258
+ }, idleTime);
1259
+ }
1260
+ };
1261
+ const onRequestStart = () => {
1262
+ inFlight++;
1263
+ if (idleTimer) {
1264
+ clearTimeout(idleTimer);
1265
+ idleTimer = null;
1266
+ }
1267
+ };
1268
+ const onRequestEnd = () => {
1269
+ inFlight = Math.max(0, inFlight - 1);
1270
+ checkIdle();
1271
+ };
1272
+ const cleanup = () => {
1273
+ clearTimeout(timeoutTimer);
1274
+ if (idleTimer) clearTimeout(idleTimer);
1275
+ cdp.off("Network.requestWillBeSent", onRequestStart);
1276
+ cdp.off("Network.loadingFinished", onRequestEnd);
1277
+ cdp.off("Network.loadingFailed", onRequestEnd);
1278
+ };
1279
+ cdp.on("Network.requestWillBeSent", onRequestStart);
1280
+ cdp.on("Network.loadingFinished", onRequestEnd);
1281
+ cdp.on("Network.loadingFailed", onRequestEnd);
1282
+ checkIdle();
1283
+ });
1284
+ }
1285
+
1286
+ // src/browser/types.ts
1287
+ var ElementNotFoundError = class extends Error {
1288
+ selectors;
1289
+ constructor(selectors) {
1290
+ const selectorList = Array.isArray(selectors) ? selectors : [selectors];
1291
+ super(`Element not found: ${selectorList.join(", ")}`);
1292
+ this.name = "ElementNotFoundError";
1293
+ this.selectors = selectorList;
1294
+ }
1295
+ };
1296
+ var TimeoutError = class extends Error {
1297
+ constructor(message = "Operation timed out") {
1298
+ super(message);
1299
+ this.name = "TimeoutError";
1300
+ }
1301
+ };
1302
+
1303
+ // src/browser/page.ts
1304
+ var DEFAULT_TIMEOUT2 = 3e4;
1305
+ var Page = class {
1306
+ cdp;
1307
+ rootNodeId = null;
1308
+ batchExecutor;
1309
+ emulationState = {};
1310
+ interceptor = null;
1311
+ consoleHandlers = /* @__PURE__ */ new Set();
1312
+ errorHandlers = /* @__PURE__ */ new Set();
1313
+ dialogHandler = null;
1314
+ consoleEnabled = false;
1315
+ /** Map of ref (e.g., "e4") to backendNodeId for ref-based selectors */
1316
+ refMap = /* @__PURE__ */ new Map();
1317
+ /** Current frame context (null = main frame) */
1318
+ currentFrame = null;
1319
+ /** Stored frame document node IDs for context switching */
1320
+ frameContexts = /* @__PURE__ */ new Map();
1321
+ /** Map of frameId → executionContextId for JS evaluation in frames */
1322
+ frameExecutionContexts = /* @__PURE__ */ new Map();
1323
+ /** Current frame's execution context ID (null = main frame default) */
1324
+ currentFrameContextId = null;
1325
+ constructor(cdp) {
1326
+ this.cdp = cdp;
1327
+ this.batchExecutor = new BatchExecutor(this);
1328
+ }
1329
+ /**
1330
+ * Initialize the page (enable required CDP domains)
1331
+ */
1332
+ async init() {
1333
+ this.cdp.on("Runtime.executionContextCreated", (params) => {
1334
+ const context = params["context"];
1335
+ if (context.auxData?.frameId && context.auxData?.isDefault) {
1336
+ this.frameExecutionContexts.set(context.auxData.frameId, context.id);
1337
+ }
1338
+ });
1339
+ this.cdp.on("Runtime.executionContextDestroyed", (params) => {
1340
+ const contextId = params["executionContextId"];
1341
+ for (const [frameId, ctxId] of this.frameExecutionContexts.entries()) {
1342
+ if (ctxId === contextId) {
1343
+ this.frameExecutionContexts.delete(frameId);
1344
+ break;
1345
+ }
1346
+ }
1347
+ });
1348
+ this.cdp.on("Page.javascriptDialogOpening", this.handleDialogOpening.bind(this));
1349
+ await Promise.all([
1350
+ this.cdp.send("Page.enable"),
1351
+ this.cdp.send("DOM.enable"),
1352
+ this.cdp.send("Runtime.enable"),
1353
+ this.cdp.send("Network.enable")
1354
+ ]);
1355
+ }
1356
+ // ============ Navigation ============
1357
+ /**
1358
+ * Navigate to a URL
1359
+ */
1360
+ async goto(url, options = {}) {
1361
+ const { timeout = DEFAULT_TIMEOUT2 } = options;
1362
+ const navPromise = this.waitForNavigation({ timeout });
1363
+ await this.cdp.send("Page.navigate", { url });
1364
+ const result = await navPromise;
1365
+ if (!result) {
1366
+ throw new TimeoutError(`Navigation to ${url} timed out after ${timeout}ms`);
1367
+ }
1368
+ this.rootNodeId = null;
1369
+ this.refMap.clear();
1370
+ }
1371
+ /**
1372
+ * Get the current URL
1373
+ */
1374
+ async url() {
1375
+ const result = await this.cdp.send("Runtime.evaluate", {
1376
+ expression: "location.href",
1377
+ returnByValue: true
1378
+ });
1379
+ return result.result.value;
1380
+ }
1381
+ /**
1382
+ * Get the page title
1383
+ */
1384
+ async title() {
1385
+ const result = await this.cdp.send("Runtime.evaluate", {
1386
+ expression: "document.title",
1387
+ returnByValue: true
1388
+ });
1389
+ return result.result.value;
1390
+ }
1391
+ /**
1392
+ * Reload the page
1393
+ */
1394
+ async reload(options = {}) {
1395
+ const { timeout = DEFAULT_TIMEOUT2 } = options;
1396
+ const navPromise = this.waitForNavigation({ timeout });
1397
+ await this.cdp.send("Page.reload");
1398
+ await navPromise;
1399
+ this.rootNodeId = null;
1400
+ this.refMap.clear();
1401
+ }
1402
+ /**
1403
+ * Go back in history
1404
+ */
1405
+ async goBack(options = {}) {
1406
+ const { timeout = DEFAULT_TIMEOUT2 } = options;
1407
+ const history = await this.cdp.send("Page.getNavigationHistory");
1408
+ if (history.currentIndex <= 0) {
1409
+ return;
1410
+ }
1411
+ const navPromise = this.waitForNavigation({ timeout });
1412
+ await this.cdp.send("Page.navigateToHistoryEntry", {
1413
+ entryId: history.entries[history.currentIndex - 1].id
1414
+ });
1415
+ await navPromise;
1416
+ this.rootNodeId = null;
1417
+ this.refMap.clear();
1418
+ }
1419
+ /**
1420
+ * Go forward in history
1421
+ */
1422
+ async goForward(options = {}) {
1423
+ const { timeout = DEFAULT_TIMEOUT2 } = options;
1424
+ const history = await this.cdp.send("Page.getNavigationHistory");
1425
+ if (history.currentIndex >= history.entries.length - 1) {
1426
+ return;
1427
+ }
1428
+ const navPromise = this.waitForNavigation({ timeout });
1429
+ await this.cdp.send("Page.navigateToHistoryEntry", {
1430
+ entryId: history.entries[history.currentIndex + 1].id
1431
+ });
1432
+ await navPromise;
1433
+ this.rootNodeId = null;
1434
+ this.refMap.clear();
1435
+ }
1436
+ // ============ Core Actions ============
1437
+ /**
1438
+ * Click an element (supports multi-selector)
1439
+ *
1440
+ * Uses CDP mouse events for regular elements. For form submit buttons,
1441
+ * uses dispatchEvent to reliably trigger form submission in headless Chrome.
1442
+ */
1443
+ async click(selector, options = {}) {
1444
+ return this.withStaleNodeRetry(async () => {
1445
+ const element = await this.findElement(selector, options);
1446
+ if (!element) {
1447
+ if (options.optional) return false;
1448
+ throw new ElementNotFoundError(selector);
1449
+ }
1450
+ await this.scrollIntoView(element.nodeId);
1451
+ const submitResult = await this.evaluateInFrame(
1452
+ `(() => {
1453
+ const el = document.querySelector(${JSON.stringify(element.selector)});
1454
+ if (!el) return { isSubmit: false };
1455
+
1456
+ // Check if this is a form submit button
1457
+ const isSubmitButton = (el instanceof HTMLButtonElement && (el.type === 'submit' || (el.form && el.type !== 'button'))) ||
1458
+ (el instanceof HTMLInputElement && el.type === 'submit');
1459
+
1460
+ if (isSubmitButton && el.form) {
1461
+ // Dispatch submit event directly - works reliably in headless Chrome
1462
+ el.form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
1463
+ return { isSubmit: true };
1464
+ }
1465
+ return { isSubmit: false };
1466
+ })()`
1467
+ );
1468
+ const isSubmit = submitResult.result.value?.isSubmit;
1469
+ if (!isSubmit) {
1470
+ await this.clickElement(element.nodeId);
1471
+ }
1472
+ return true;
1473
+ });
1474
+ }
1475
+ /**
1476
+ * Fill an input field (clears first by default)
1477
+ */
1478
+ async fill(selector, value, options = {}) {
1479
+ const { clear = true } = options;
1480
+ return this.withStaleNodeRetry(async () => {
1481
+ const element = await this.findElement(selector, options);
1482
+ if (!element) {
1483
+ if (options.optional) return false;
1484
+ throw new ElementNotFoundError(selector);
1485
+ }
1486
+ await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
1487
+ if (clear) {
1488
+ await this.evaluateInFrame(
1489
+ `(() => {
1490
+ const el = document.querySelector(${JSON.stringify(element.selector)});
1491
+ if (el) {
1492
+ el.value = '';
1493
+ el.dispatchEvent(new Event('input', { bubbles: true }));
1494
+ }
1495
+ })()`
1496
+ );
1497
+ }
1498
+ await this.cdp.send("Input.insertText", { text: value });
1499
+ await this.evaluateInFrame(
1500
+ `(() => {
1501
+ const el = document.querySelector(${JSON.stringify(element.selector)});
1502
+ if (el) {
1503
+ el.dispatchEvent(new Event('input', { bubbles: true }));
1504
+ el.dispatchEvent(new Event('change', { bubbles: true }));
1505
+ }
1506
+ })()`
1507
+ );
1508
+ return true;
1509
+ });
1510
+ }
1511
+ /**
1512
+ * Type text character by character (for autocomplete fields, etc.)
1513
+ */
1514
+ async type(selector, text, options = {}) {
1515
+ const { delay = 50 } = options;
1516
+ const element = await this.findElement(selector, options);
1517
+ if (!element) {
1518
+ if (options.optional) return false;
1519
+ throw new ElementNotFoundError(selector);
1520
+ }
1521
+ await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
1522
+ for (const char of text) {
1523
+ await this.cdp.send("Input.dispatchKeyEvent", {
1524
+ type: "keyDown",
1525
+ key: char,
1526
+ text: char
1527
+ });
1528
+ await this.cdp.send("Input.dispatchKeyEvent", {
1529
+ type: "keyUp",
1530
+ key: char
1531
+ });
1532
+ if (delay > 0) {
1533
+ await sleep2(delay);
1534
+ }
1535
+ }
1536
+ return true;
1537
+ }
1538
+ async select(selectorOrConfig, valueOrOptions, maybeOptions) {
1539
+ if (typeof selectorOrConfig === "object" && !Array.isArray(selectorOrConfig) && "trigger" in selectorOrConfig) {
1540
+ return this.selectCustom(selectorOrConfig, valueOrOptions);
1541
+ }
1542
+ const selector = selectorOrConfig;
1543
+ const value = valueOrOptions;
1544
+ const options = maybeOptions ?? {};
1545
+ const element = await this.findElement(selector, options);
1546
+ if (!element) {
1547
+ if (options.optional) return false;
1548
+ throw new ElementNotFoundError(selector);
1549
+ }
1550
+ const values = Array.isArray(value) ? value : [value];
1551
+ await this.cdp.send("Runtime.evaluate", {
1552
+ expression: `(() => {
1553
+ const el = document.querySelector(${JSON.stringify(element.selector)});
1554
+ if (!el || el.tagName !== 'SELECT') return false;
1555
+ const values = ${JSON.stringify(values)};
1556
+ for (const opt of el.options) {
1557
+ opt.selected = values.includes(opt.value) || values.includes(opt.text);
1558
+ }
1559
+ el.dispatchEvent(new Event('change', { bubbles: true }));
1560
+ return true;
1561
+ })()`,
1562
+ returnByValue: true
1563
+ });
1564
+ return true;
1565
+ }
1566
+ /**
1567
+ * Handle custom (non-native) select/dropdown components
1568
+ */
1569
+ async selectCustom(config, options = {}) {
1570
+ const { trigger, option, value, match = "text" } = config;
1571
+ await this.click(trigger, options);
1572
+ await sleep2(100);
1573
+ let optionSelector;
1574
+ const optionSelectors = Array.isArray(option) ? option : [option];
1575
+ if (match === "contains") {
1576
+ optionSelector = optionSelectors.map((s) => `${s}:has-text("${value}")`).join(", ");
1577
+ } else if (match === "value") {
1578
+ optionSelector = optionSelectors.map((s) => `${s}[data-value="${value}"], ${s}[value="${value}"]`).join(", ");
1579
+ } else {
1580
+ optionSelector = optionSelectors.map((s) => `${s}`).join(", ");
1581
+ }
1582
+ const result = await this.cdp.send("Runtime.evaluate", {
1583
+ expression: `(() => {
1584
+ const options = document.querySelectorAll(${JSON.stringify(optionSelector)});
1585
+ for (const opt of options) {
1586
+ const text = opt.textContent?.trim();
1587
+ if (${match === "text" ? `text === ${JSON.stringify(value)}` : match === "contains" ? `text?.includes(${JSON.stringify(value)})` : "true"}) {
1588
+ opt.click();
1589
+ return true;
1590
+ }
1591
+ }
1592
+ return false;
1593
+ })()`,
1594
+ returnByValue: true
1595
+ });
1596
+ if (!result.result.value) {
1597
+ if (options.optional) return false;
1598
+ throw new ElementNotFoundError(`Option with ${match} "${value}"`);
1599
+ }
1600
+ return true;
1601
+ }
1602
+ /**
1603
+ * Check a checkbox or radio button
1604
+ */
1605
+ async check(selector, options = {}) {
1606
+ const element = await this.findElement(selector, options);
1607
+ if (!element) {
1608
+ if (options.optional) return false;
1609
+ throw new ElementNotFoundError(selector);
1610
+ }
1611
+ const result = await this.cdp.send("Runtime.evaluate", {
1612
+ expression: `(() => {
1613
+ const el = document.querySelector(${JSON.stringify(element.selector)});
1614
+ if (!el) return false;
1615
+ if (!el.checked) el.click();
1616
+ return true;
1617
+ })()`,
1618
+ returnByValue: true
1619
+ });
1620
+ return result.result.value;
1621
+ }
1622
+ /**
1623
+ * Uncheck a checkbox
1624
+ */
1625
+ async uncheck(selector, options = {}) {
1626
+ const element = await this.findElement(selector, options);
1627
+ if (!element) {
1628
+ if (options.optional) return false;
1629
+ throw new ElementNotFoundError(selector);
1630
+ }
1631
+ const result = await this.cdp.send("Runtime.evaluate", {
1632
+ expression: `(() => {
1633
+ const el = document.querySelector(${JSON.stringify(element.selector)});
1634
+ if (!el) return false;
1635
+ if (el.checked) el.click();
1636
+ return true;
1637
+ })()`,
1638
+ returnByValue: true
1639
+ });
1640
+ return result.result.value;
1641
+ }
1642
+ /**
1643
+ * Submit a form (tries Enter key first, then click)
1644
+ *
1645
+ * Navigation waiting behavior:
1646
+ * - 'auto' (default): Attempt to detect navigation for 1 second, then assume client-side handling
1647
+ * - true: Wait for full navigation (traditional forms)
1648
+ * - false: Return immediately (AJAX forms where you'll wait for something else)
1649
+ */
1650
+ async submit(selector, options = {}) {
1651
+ const { method = "enter+click", waitForNavigation: shouldWait = "auto" } = options;
1652
+ const element = await this.findElement(selector, options);
1653
+ if (!element) {
1654
+ if (options.optional) return false;
1655
+ throw new ElementNotFoundError(selector);
1656
+ }
1657
+ await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
1658
+ if (method.includes("enter")) {
1659
+ await this.press("Enter");
1660
+ if (shouldWait === true) {
1661
+ try {
1662
+ await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT2 });
1663
+ return true;
1664
+ } catch {
1665
+ }
1666
+ } else if (shouldWait === "auto") {
1667
+ const navigationDetected = await Promise.race([
1668
+ this.waitForNavigation({ timeout: 1e3, optional: true }).then(
1669
+ (success) => success ? "nav" : null
1670
+ ),
1671
+ sleep2(500).then(() => "timeout")
1672
+ ]);
1673
+ if (navigationDetected === "nav") {
1674
+ return true;
1675
+ }
1676
+ } else {
1677
+ if (method === "enter") return true;
1678
+ }
1679
+ }
1680
+ if (method.includes("click")) {
1681
+ await this.click(element.selector, { ...options, optional: false });
1682
+ if (shouldWait === true) {
1683
+ await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT2 });
1684
+ } else if (shouldWait === "auto") {
1685
+ await sleep2(100);
1686
+ }
1687
+ }
1688
+ return true;
1689
+ }
1690
+ /**
1691
+ * Press a key
1692
+ */
1693
+ async press(key) {
1694
+ const keyMap = {
1695
+ Enter: { key: "Enter", code: "Enter", keyCode: 13 },
1696
+ Tab: { key: "Tab", code: "Tab", keyCode: 9 },
1697
+ Escape: { key: "Escape", code: "Escape", keyCode: 27 },
1698
+ Backspace: { key: "Backspace", code: "Backspace", keyCode: 8 },
1699
+ Delete: { key: "Delete", code: "Delete", keyCode: 46 },
1700
+ ArrowUp: { key: "ArrowUp", code: "ArrowUp", keyCode: 38 },
1701
+ ArrowDown: { key: "ArrowDown", code: "ArrowDown", keyCode: 40 },
1702
+ ArrowLeft: { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37 },
1703
+ ArrowRight: { key: "ArrowRight", code: "ArrowRight", keyCode: 39 }
1704
+ };
1705
+ const keyInfo = keyMap[key] ?? { key, code: key, keyCode: 0 };
1706
+ await this.cdp.send("Input.dispatchKeyEvent", {
1707
+ type: "keyDown",
1708
+ key: keyInfo.key,
1709
+ code: keyInfo.code,
1710
+ windowsVirtualKeyCode: keyInfo.keyCode
1711
+ });
1712
+ await this.cdp.send("Input.dispatchKeyEvent", {
1713
+ type: "keyUp",
1714
+ key: keyInfo.key,
1715
+ code: keyInfo.code,
1716
+ windowsVirtualKeyCode: keyInfo.keyCode
1717
+ });
1718
+ }
1719
+ /**
1720
+ * Focus an element
1721
+ */
1722
+ async focus(selector, options = {}) {
1723
+ const element = await this.findElement(selector, options);
1724
+ if (!element) {
1725
+ if (options.optional) return false;
1726
+ throw new ElementNotFoundError(selector);
1727
+ }
1728
+ await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
1729
+ return true;
1730
+ }
1731
+ /**
1732
+ * Hover over an element
1733
+ */
1734
+ async hover(selector, options = {}) {
1735
+ return this.withStaleNodeRetry(async () => {
1736
+ const element = await this.findElement(selector, options);
1737
+ if (!element) {
1738
+ if (options.optional) return false;
1739
+ throw new ElementNotFoundError(selector);
1740
+ }
1741
+ await this.scrollIntoView(element.nodeId);
1742
+ const box = await this.getBoxModel(element.nodeId);
1743
+ if (!box) {
1744
+ if (options.optional) return false;
1745
+ throw new Error("Could not get element box model");
1746
+ }
1747
+ const x = box.content[0] + box.width / 2;
1748
+ const y = box.content[1] + box.height / 2;
1749
+ await this.cdp.send("Input.dispatchMouseEvent", {
1750
+ type: "mouseMoved",
1751
+ x,
1752
+ y
1753
+ });
1754
+ return true;
1755
+ });
1756
+ }
1757
+ /**
1758
+ * Scroll an element into view (or scroll to coordinates)
1759
+ */
1760
+ async scroll(selector, options = {}) {
1761
+ const { x, y } = options;
1762
+ if (x !== void 0 || y !== void 0) {
1763
+ await this.cdp.send("Runtime.evaluate", {
1764
+ expression: `window.scrollTo(${x ?? 0}, ${y ?? 0})`
1765
+ });
1766
+ return true;
1767
+ }
1768
+ const element = await this.findElement(selector, options);
1769
+ if (!element) {
1770
+ if (options.optional) return false;
1771
+ throw new ElementNotFoundError(selector);
1772
+ }
1773
+ await this.scrollIntoView(element.nodeId);
1774
+ return true;
1775
+ }
1776
+ // ============ Frame Navigation ============
1777
+ /**
1778
+ * Switch context to an iframe for subsequent actions
1779
+ * @param selector - Selector for the iframe element
1780
+ * @param options - Optional timeout and optional flags
1781
+ * @returns true if switch succeeded
1782
+ */
1783
+ async switchToFrame(selector, options = {}) {
1784
+ const element = await this.findElement(selector, options);
1785
+ if (!element) {
1786
+ if (options.optional) return false;
1787
+ throw new ElementNotFoundError(selector);
1788
+ }
1789
+ const descResult = await this.cdp.send("DOM.describeNode", {
1790
+ nodeId: element.nodeId,
1791
+ depth: 1
1792
+ });
1793
+ if (!descResult.node.contentDocument) {
1794
+ if (options.optional) return false;
1795
+ throw new Error(
1796
+ "Cannot access iframe content. This may be a cross-origin iframe which requires different handling."
1797
+ );
1798
+ }
1799
+ const frameKey = Array.isArray(selector) ? selector[0] : selector;
1800
+ this.frameContexts.set(frameKey, descResult.node.contentDocument.nodeId);
1801
+ this.currentFrame = frameKey;
1802
+ this.rootNodeId = descResult.node.contentDocument.nodeId;
1803
+ if (descResult.node.frameId) {
1804
+ let contextId = this.frameExecutionContexts.get(descResult.node.frameId);
1805
+ if (!contextId) {
1806
+ await new Promise((resolve) => setTimeout(resolve, 50));
1807
+ contextId = this.frameExecutionContexts.get(descResult.node.frameId);
1808
+ }
1809
+ if (contextId) {
1810
+ this.currentFrameContextId = contextId;
1811
+ }
1812
+ }
1813
+ this.refMap.clear();
1814
+ return true;
1815
+ }
1816
+ /**
1817
+ * Switch back to the main document from an iframe
1818
+ */
1819
+ async switchToMain() {
1820
+ this.currentFrame = null;
1821
+ this.rootNodeId = null;
1822
+ this.currentFrameContextId = null;
1823
+ this.refMap.clear();
1824
+ }
1825
+ /**
1826
+ * Get the current frame context (null = main frame)
1827
+ */
1828
+ getCurrentFrame() {
1829
+ return this.currentFrame;
1830
+ }
1831
+ // ============ Waiting ============
1832
+ /**
1833
+ * Wait for an element to reach a state
1834
+ */
1835
+ async waitFor(selector, options = {}) {
1836
+ const { timeout = DEFAULT_TIMEOUT2, state = "visible" } = options;
1837
+ const selectors = Array.isArray(selector) ? selector : [selector];
1838
+ const result = await waitForAnyElement(this.cdp, selectors, {
1839
+ state,
1840
+ timeout,
1841
+ contextId: this.currentFrameContextId ?? void 0
1842
+ });
1843
+ if (!result.success && !options.optional) {
1844
+ throw new TimeoutError(`Timeout waiting for ${selectors.join(" or ")} to be ${state}`);
1845
+ }
1846
+ return result.success;
1847
+ }
1848
+ /**
1849
+ * Wait for navigation to complete
1850
+ */
1851
+ async waitForNavigation(options = {}) {
1852
+ const { timeout = DEFAULT_TIMEOUT2 } = options;
1853
+ const result = await waitForNavigation(this.cdp, { timeout });
1854
+ if (!result.success && !options.optional) {
1855
+ throw new TimeoutError("Navigation timeout");
1856
+ }
1857
+ this.rootNodeId = null;
1858
+ this.refMap.clear();
1859
+ return result.success;
1860
+ }
1861
+ /**
1862
+ * Wait for network to be idle
1863
+ */
1864
+ async waitForNetworkIdle(options = {}) {
1865
+ const { timeout = DEFAULT_TIMEOUT2, idleTime = 500 } = options;
1866
+ const result = await waitForNetworkIdle(this.cdp, { timeout, idleTime });
1867
+ if (!result.success && !options.optional) {
1868
+ throw new TimeoutError("Network idle timeout");
1869
+ }
1870
+ return result.success;
1871
+ }
1872
+ // ============ JavaScript Execution ============
1873
+ /**
1874
+ * Evaluate JavaScript in the page context (or current frame context if in iframe)
1875
+ */
1876
+ async evaluate(expression, ...args) {
1877
+ let script;
1878
+ if (typeof expression === "function") {
1879
+ const argString = args.map((a) => JSON.stringify(a)).join(", ");
1880
+ script = `(${expression.toString()})(${argString})`;
1881
+ } else {
1882
+ script = expression;
1883
+ }
1884
+ const params = {
1885
+ expression: script,
1886
+ returnByValue: true,
1887
+ awaitPromise: true
1888
+ };
1889
+ if (this.currentFrameContextId !== null) {
1890
+ params["contextId"] = this.currentFrameContextId;
1891
+ }
1892
+ const result = await this.cdp.send("Runtime.evaluate", params);
1893
+ if (result.exceptionDetails) {
1894
+ throw new Error(`Evaluation failed: ${result.exceptionDetails.text}`);
1895
+ }
1896
+ return result.result.value;
1897
+ }
1898
+ // ============ Screenshots ============
1899
+ /**
1900
+ * Take a screenshot
1901
+ */
1902
+ async screenshot(options = {}) {
1903
+ const { format = "png", quality, fullPage = false } = options;
1904
+ let clip;
1905
+ if (fullPage) {
1906
+ const metrics = await this.cdp.send("Page.getLayoutMetrics");
1907
+ clip = {
1908
+ x: 0,
1909
+ y: 0,
1910
+ width: metrics.contentSize.width,
1911
+ height: metrics.contentSize.height,
1912
+ scale: 1
1913
+ };
1914
+ }
1915
+ const result = await this.cdp.send("Page.captureScreenshot", {
1916
+ format,
1917
+ quality: format === "png" ? void 0 : quality,
1918
+ clip,
1919
+ captureBeyondViewport: fullPage
1920
+ });
1921
+ return result.data;
1922
+ }
1923
+ // ============ Text Extraction ============
1924
+ /**
1925
+ * Get text content from the page or a specific element
1926
+ */
1927
+ async text(selector) {
1928
+ const expression = selector ? `document.querySelector(${JSON.stringify(selector)})?.innerText ?? ''` : "document.body.innerText";
1929
+ const result = await this.evaluateInFrame(expression);
1930
+ return result.result.value ?? "";
1931
+ }
1932
+ // ============ File Handling ============
1933
+ /**
1934
+ * Set files on a file input
1935
+ */
1936
+ async setInputFiles(selector, files, options = {}) {
1937
+ const element = await this.findElement(selector, options);
1938
+ if (!element) {
1939
+ if (options.optional) return false;
1940
+ throw new ElementNotFoundError(selector);
1941
+ }
1942
+ const fileData = await Promise.all(
1943
+ files.map(async (f) => {
1944
+ let base64;
1945
+ if (typeof f.buffer === "string") {
1946
+ base64 = f.buffer;
1947
+ } else {
1948
+ const bytes = new Uint8Array(f.buffer);
1949
+ base64 = btoa(String.fromCharCode(...bytes));
1950
+ }
1951
+ return { name: f.name, mimeType: f.mimeType, data: base64 };
1952
+ })
1953
+ );
1954
+ await this.cdp.send("Runtime.evaluate", {
1955
+ expression: `(() => {
1956
+ const input = document.querySelector(${JSON.stringify(element.selector)});
1957
+ if (!input) return false;
1958
+
1959
+ const files = ${JSON.stringify(fileData)};
1960
+ const dt = new DataTransfer();
1961
+
1962
+ for (const f of files) {
1963
+ const bytes = Uint8Array.from(atob(f.data), c => c.charCodeAt(0));
1964
+ const file = new File([bytes], f.name, { type: f.mimeType });
1965
+ dt.items.add(file);
1966
+ }
1967
+
1968
+ input.files = dt.files;
1969
+ input.dispatchEvent(new Event('change', { bubbles: true }));
1970
+ return true;
1971
+ })()`,
1972
+ returnByValue: true
1973
+ });
1974
+ return true;
1975
+ }
1976
+ /**
1977
+ * Wait for a download to complete, triggered by an action
1978
+ */
1979
+ async waitForDownload(trigger, options = {}) {
1980
+ const { timeout = DEFAULT_TIMEOUT2 } = options;
1981
+ await this.cdp.send("Browser.setDownloadBehavior", {
1982
+ behavior: "allowAndName",
1983
+ eventsEnabled: true
1984
+ });
1985
+ return new Promise((resolve, reject) => {
1986
+ let downloadGuid;
1987
+ let suggestedFilename;
1988
+ let resolved = false;
1989
+ const timeoutTimer = setTimeout(() => {
1990
+ if (!resolved) {
1991
+ cleanup();
1992
+ reject(new TimeoutError(`Download timed out after ${timeout}ms`));
1993
+ }
1994
+ }, timeout);
1995
+ const onDownloadWillBegin = (params) => {
1996
+ downloadGuid = params["guid"];
1997
+ suggestedFilename = params["suggestedFilename"];
1998
+ };
1999
+ const onDownloadProgress = (params) => {
2000
+ if (params["guid"] === downloadGuid && params["state"] === "completed") {
2001
+ resolved = true;
2002
+ cleanup();
2003
+ const download = {
2004
+ filename: suggestedFilename ?? "unknown",
2005
+ content: async () => {
2006
+ return new ArrayBuffer(0);
2007
+ }
2008
+ };
2009
+ resolve(download);
2010
+ } else if (params["guid"] === downloadGuid && params["state"] === "canceled") {
2011
+ resolved = true;
2012
+ cleanup();
2013
+ reject(new Error("Download was canceled"));
2014
+ }
2015
+ };
2016
+ const cleanup = () => {
2017
+ clearTimeout(timeoutTimer);
2018
+ this.cdp.off("Browser.downloadWillBegin", onDownloadWillBegin);
2019
+ this.cdp.off("Browser.downloadProgress", onDownloadProgress);
2020
+ };
2021
+ this.cdp.on("Browser.downloadWillBegin", onDownloadWillBegin);
2022
+ this.cdp.on("Browser.downloadProgress", onDownloadProgress);
2023
+ trigger().catch((err) => {
2024
+ if (!resolved) {
2025
+ resolved = true;
2026
+ cleanup();
2027
+ reject(err);
2028
+ }
2029
+ });
2030
+ });
2031
+ }
2032
+ // ============ Snapshot ============
2033
+ /**
2034
+ * Get an accessibility tree snapshot of the page
2035
+ */
2036
+ async snapshot() {
2037
+ const [url, title, axTree] = await Promise.all([
2038
+ this.url(),
2039
+ this.title(),
2040
+ this.cdp.send("Accessibility.getFullAXTree")
2041
+ ]);
2042
+ const nodes = axTree.nodes.filter((n) => !n.ignored);
2043
+ const nodeMap = new Map(nodes.map((n) => [n.nodeId, n]));
2044
+ let refCounter = 0;
2045
+ const nodeRefs = /* @__PURE__ */ new Map();
2046
+ this.refMap.clear();
2047
+ for (const node of nodes) {
2048
+ const ref = `e${++refCounter}`;
2049
+ nodeRefs.set(node.nodeId, ref);
2050
+ if (node.backendDOMNodeId !== void 0) {
2051
+ this.refMap.set(ref, node.backendDOMNodeId);
2052
+ }
2053
+ }
2054
+ const buildNode = (nodeId) => {
2055
+ const node = nodeMap.get(nodeId);
2056
+ if (!node) return null;
2057
+ const role = node.role?.value ?? "generic";
2058
+ const name = node.name?.value;
2059
+ const value = node.value?.value;
2060
+ const ref = nodeRefs.get(nodeId);
2061
+ const children = [];
2062
+ if (node.childIds) {
2063
+ for (const childId of node.childIds) {
2064
+ const child = buildNode(childId);
2065
+ if (child) children.push(child);
2066
+ }
2067
+ }
2068
+ const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
2069
+ const checked = node.properties?.find((p) => p.name === "checked")?.value.value;
2070
+ return {
2071
+ role,
2072
+ name,
2073
+ value,
2074
+ ref,
2075
+ children: children.length > 0 ? children : void 0,
2076
+ disabled,
2077
+ checked
2078
+ };
2079
+ };
2080
+ const rootNodes = nodes.filter((n) => !n.parentId || !nodeMap.has(n.parentId));
2081
+ const accessibilityTree = rootNodes.map((n) => buildNode(n.nodeId)).filter((n) => n !== null);
2082
+ const interactiveRoles = /* @__PURE__ */ new Set([
2083
+ "button",
2084
+ "link",
2085
+ "textbox",
2086
+ "checkbox",
2087
+ "radio",
2088
+ "combobox",
2089
+ "listbox",
2090
+ "menuitem",
2091
+ "menuitemcheckbox",
2092
+ "menuitemradio",
2093
+ "option",
2094
+ "searchbox",
2095
+ "slider",
2096
+ "spinbutton",
2097
+ "switch",
2098
+ "tab",
2099
+ "treeitem"
2100
+ ]);
2101
+ const interactiveElements = [];
2102
+ for (const node of nodes) {
2103
+ const role = node.role?.value;
2104
+ if (role && interactiveRoles.has(role)) {
2105
+ const ref = nodeRefs.get(node.nodeId);
2106
+ const name = node.name?.value ?? "";
2107
+ const disabled = node.properties?.find((p) => p.name === "disabled")?.value.value;
2108
+ const selector = node.backendDOMNodeId ? `[data-backend-node-id="${node.backendDOMNodeId}"]` : `[aria-label="${name}"]`;
2109
+ interactiveElements.push({
2110
+ ref,
2111
+ role,
2112
+ name,
2113
+ selector,
2114
+ disabled
2115
+ });
2116
+ }
2117
+ }
2118
+ const formatTree = (nodes2, depth = 0) => {
2119
+ const lines = [];
2120
+ for (const node of nodes2) {
2121
+ let line = `${" ".repeat(depth)}- ${node.role}`;
2122
+ if (node.name) line += ` "${node.name}"`;
2123
+ line += ` [ref=${node.ref}]`;
2124
+ if (node.disabled) line += " (disabled)";
2125
+ if (node.checked !== void 0) line += node.checked ? " (checked)" : " (unchecked)";
2126
+ lines.push(line);
2127
+ if (node.children) {
2128
+ lines.push(formatTree(node.children, depth + 1));
2129
+ }
2130
+ }
2131
+ return lines.join("\n");
2132
+ };
2133
+ const text = formatTree(accessibilityTree);
2134
+ return {
2135
+ url,
2136
+ title,
2137
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2138
+ accessibilityTree,
2139
+ interactiveElements,
2140
+ text
2141
+ };
2142
+ }
2143
+ // ============ Batch Execution ============
2144
+ /**
2145
+ * Execute a batch of steps
2146
+ */
2147
+ async batch(steps, options) {
2148
+ return this.batchExecutor.execute(steps, options);
2149
+ }
2150
+ // ============ Emulation ============
2151
+ /**
2152
+ * Set the viewport size and device metrics
2153
+ */
2154
+ async setViewport(options) {
2155
+ const {
2156
+ width,
2157
+ height,
2158
+ deviceScaleFactor = 1,
2159
+ isMobile = false,
2160
+ hasTouch = false,
2161
+ isLandscape = false
2162
+ } = options;
2163
+ await this.cdp.send("Emulation.setDeviceMetricsOverride", {
2164
+ width,
2165
+ height,
2166
+ deviceScaleFactor,
2167
+ mobile: isMobile,
2168
+ screenWidth: width,
2169
+ screenHeight: height,
2170
+ screenOrientation: {
2171
+ type: isLandscape ? "landscapePrimary" : "portraitPrimary",
2172
+ angle: isLandscape ? 90 : 0
2173
+ }
2174
+ });
2175
+ if (hasTouch) {
2176
+ await this.cdp.send("Emulation.setTouchEmulationEnabled", {
2177
+ enabled: true,
2178
+ maxTouchPoints: 5
2179
+ });
2180
+ }
2181
+ this.emulationState.viewport = options;
2182
+ }
2183
+ /**
2184
+ * Clear viewport override, return to default
2185
+ */
2186
+ async clearViewport() {
2187
+ await this.cdp.send("Emulation.clearDeviceMetricsOverride");
2188
+ await this.cdp.send("Emulation.setTouchEmulationEnabled", { enabled: false });
2189
+ this.emulationState.viewport = void 0;
2190
+ }
2191
+ /**
2192
+ * Set the user agent string and optional metadata
2193
+ */
2194
+ async setUserAgent(options) {
2195
+ const opts = typeof options === "string" ? { userAgent: options } : options;
2196
+ await this.cdp.send("Emulation.setUserAgentOverride", {
2197
+ userAgent: opts.userAgent,
2198
+ acceptLanguage: opts.acceptLanguage,
2199
+ platform: opts.platform,
2200
+ userAgentMetadata: opts.userAgentMetadata
2201
+ });
2202
+ this.emulationState.userAgent = opts;
2203
+ }
2204
+ /**
2205
+ * Set geolocation coordinates
2206
+ */
2207
+ async setGeolocation(options) {
2208
+ const { latitude, longitude, accuracy = 1 } = options;
2209
+ await this.cdp.send("Browser.grantPermissions", {
2210
+ permissions: ["geolocation"]
2211
+ });
2212
+ await this.cdp.send("Emulation.setGeolocationOverride", {
2213
+ latitude,
2214
+ longitude,
2215
+ accuracy
2216
+ });
2217
+ this.emulationState.geolocation = options;
2218
+ }
2219
+ /**
2220
+ * Clear geolocation override
2221
+ */
2222
+ async clearGeolocation() {
2223
+ await this.cdp.send("Emulation.clearGeolocationOverride");
2224
+ this.emulationState.geolocation = void 0;
2225
+ }
2226
+ /**
2227
+ * Set timezone override
2228
+ */
2229
+ async setTimezone(timezoneId) {
2230
+ await this.cdp.send("Emulation.setTimezoneOverride", { timezoneId });
2231
+ this.emulationState.timezone = timezoneId;
2232
+ }
2233
+ /**
2234
+ * Set locale override
2235
+ */
2236
+ async setLocale(locale) {
2237
+ await this.cdp.send("Emulation.setLocaleOverride", { locale });
2238
+ this.emulationState.locale = locale;
2239
+ }
2240
+ /**
2241
+ * Emulate a specific device
2242
+ */
2243
+ async emulate(device) {
2244
+ await this.setViewport(device.viewport);
2245
+ await this.setUserAgent(device.userAgent);
2246
+ }
2247
+ /**
2248
+ * Get current emulation state
2249
+ */
2250
+ getEmulationState() {
2251
+ return { ...this.emulationState };
2252
+ }
2253
+ // ============ Request Interception ============
2254
+ /**
2255
+ * Add request interception handler
2256
+ * @param pattern URL pattern or resource type to match
2257
+ * @param handler Handler function for matched requests
2258
+ * @returns Unsubscribe function
2259
+ */
2260
+ async intercept(pattern, handler) {
2261
+ if (!this.interceptor) {
2262
+ this.interceptor = new RequestInterceptor(this.cdp);
2263
+ await this.interceptor.enable();
2264
+ }
2265
+ const normalizedPattern = typeof pattern === "string" ? { urlPattern: pattern } : pattern;
2266
+ return this.interceptor.addHandler(normalizedPattern, handler);
2267
+ }
2268
+ /**
2269
+ * Route requests matching pattern to a mock response
2270
+ * Convenience wrapper around intercept()
2271
+ */
2272
+ async route(urlPattern, options) {
2273
+ return this.intercept({ urlPattern }, async (_request, actions) => {
2274
+ let body = options.body;
2275
+ const headers = { ...options.headers };
2276
+ if (typeof body === "object") {
2277
+ body = JSON.stringify(body);
2278
+ headers["content-type"] ??= "application/json";
2279
+ }
2280
+ if (options.contentType) {
2281
+ headers["content-type"] = options.contentType;
2282
+ }
2283
+ await actions.fulfill({
2284
+ status: options.status ?? 200,
2285
+ headers,
2286
+ body
2287
+ });
2288
+ });
2289
+ }
2290
+ /**
2291
+ * Block requests matching resource types
2292
+ */
2293
+ async blockResources(types) {
2294
+ return this.intercept({}, async (request, actions) => {
2295
+ if (types.includes(request.resourceType)) {
2296
+ await actions.fail({ reason: "BlockedByClient" });
2297
+ } else {
2298
+ await actions.continue();
2299
+ }
2300
+ });
2301
+ }
2302
+ /**
2303
+ * Disable all request interception
2304
+ */
2305
+ async disableInterception() {
2306
+ if (this.interceptor) {
2307
+ await this.interceptor.disable();
2308
+ this.interceptor = null;
2309
+ }
2310
+ }
2311
+ // ============ Cookies & Storage ============
2312
+ /**
2313
+ * Get all cookies for the current page
2314
+ */
2315
+ async cookies(urls) {
2316
+ const targetUrls = urls ?? [await this.url()];
2317
+ const result = await this.cdp.send("Network.getCookies", {
2318
+ urls: targetUrls
2319
+ });
2320
+ return result.cookies;
2321
+ }
2322
+ /**
2323
+ * Set a cookie
2324
+ */
2325
+ async setCookie(options) {
2326
+ const { name, value, domain, path = "/", expires, httpOnly, secure, sameSite, url } = options;
2327
+ let expireTime;
2328
+ if (expires instanceof Date) {
2329
+ expireTime = Math.floor(expires.getTime() / 1e3);
2330
+ } else if (typeof expires === "number") {
2331
+ expireTime = expires;
2332
+ }
2333
+ const result = await this.cdp.send("Network.setCookie", {
2334
+ name,
2335
+ value,
2336
+ domain,
2337
+ path,
2338
+ expires: expireTime,
2339
+ httpOnly,
2340
+ secure,
2341
+ sameSite,
2342
+ url: url ?? (domain ? void 0 : await this.url())
2343
+ });
2344
+ return result.success;
2345
+ }
2346
+ /**
2347
+ * Set multiple cookies
2348
+ */
2349
+ async setCookies(cookies) {
2350
+ for (const cookie of cookies) {
2351
+ await this.setCookie(cookie);
2352
+ }
2353
+ }
2354
+ /**
2355
+ * Delete a specific cookie
2356
+ */
2357
+ async deleteCookie(options) {
2358
+ const { name, domain, path, url } = options;
2359
+ await this.cdp.send("Network.deleteCookies", {
2360
+ name,
2361
+ domain,
2362
+ path,
2363
+ url: url ?? (domain ? void 0 : await this.url())
2364
+ });
2365
+ }
2366
+ /**
2367
+ * Delete multiple cookies
2368
+ */
2369
+ async deleteCookies(cookies) {
2370
+ for (const cookie of cookies) {
2371
+ await this.deleteCookie(cookie);
2372
+ }
2373
+ }
2374
+ /**
2375
+ * Clear all cookies
2376
+ */
2377
+ async clearCookies(options) {
2378
+ if (options?.domain) {
2379
+ const domainCookies = await this.cookies([`https://${options.domain}`]);
2380
+ for (const cookie of domainCookies) {
2381
+ await this.deleteCookie({
2382
+ name: cookie.name,
2383
+ domain: cookie.domain,
2384
+ path: cookie.path
2385
+ });
2386
+ }
2387
+ } else {
2388
+ await this.cdp.send("Storage.clearCookies", {});
2389
+ }
2390
+ }
2391
+ /**
2392
+ * Get localStorage value
2393
+ */
2394
+ async getLocalStorage(key) {
2395
+ const result = await this.cdp.send("Runtime.evaluate", {
2396
+ expression: `localStorage.getItem(${JSON.stringify(key)})`,
2397
+ returnByValue: true
2398
+ });
2399
+ return result.result.value;
2400
+ }
2401
+ /**
2402
+ * Set localStorage value
2403
+ */
2404
+ async setLocalStorage(key, value) {
2405
+ await this.cdp.send("Runtime.evaluate", {
2406
+ expression: `localStorage.setItem(${JSON.stringify(key)}, ${JSON.stringify(value)})`
2407
+ });
2408
+ }
2409
+ /**
2410
+ * Remove localStorage item
2411
+ */
2412
+ async removeLocalStorage(key) {
2413
+ await this.cdp.send("Runtime.evaluate", {
2414
+ expression: `localStorage.removeItem(${JSON.stringify(key)})`
2415
+ });
2416
+ }
2417
+ /**
2418
+ * Clear localStorage
2419
+ */
2420
+ async clearLocalStorage() {
2421
+ await this.cdp.send("Runtime.evaluate", {
2422
+ expression: "localStorage.clear()"
2423
+ });
2424
+ }
2425
+ /**
2426
+ * Get sessionStorage value
2427
+ */
2428
+ async getSessionStorage(key) {
2429
+ const result = await this.cdp.send("Runtime.evaluate", {
2430
+ expression: `sessionStorage.getItem(${JSON.stringify(key)})`,
2431
+ returnByValue: true
2432
+ });
2433
+ return result.result.value;
2434
+ }
2435
+ /**
2436
+ * Set sessionStorage value
2437
+ */
2438
+ async setSessionStorage(key, value) {
2439
+ await this.cdp.send("Runtime.evaluate", {
2440
+ expression: `sessionStorage.setItem(${JSON.stringify(key)}, ${JSON.stringify(value)})`
2441
+ });
2442
+ }
2443
+ /**
2444
+ * Remove sessionStorage item
2445
+ */
2446
+ async removeSessionStorage(key) {
2447
+ await this.cdp.send("Runtime.evaluate", {
2448
+ expression: `sessionStorage.removeItem(${JSON.stringify(key)})`
2449
+ });
2450
+ }
2451
+ /**
2452
+ * Clear sessionStorage
2453
+ */
2454
+ async clearSessionStorage() {
2455
+ await this.cdp.send("Runtime.evaluate", {
2456
+ expression: "sessionStorage.clear()"
2457
+ });
2458
+ }
2459
+ // ============ Console & Errors ============
2460
+ /**
2461
+ * Enable console message capture
2462
+ */
2463
+ async enableConsole() {
2464
+ if (this.consoleEnabled) return;
2465
+ this.cdp.on("Runtime.consoleAPICalled", this.handleConsoleMessage.bind(this));
2466
+ this.cdp.on("Runtime.exceptionThrown", this.handleException.bind(this));
2467
+ this.consoleEnabled = true;
2468
+ }
2469
+ /**
2470
+ * Handle console API calls
2471
+ */
2472
+ handleConsoleMessage(params) {
2473
+ const args = params["args"];
2474
+ const stackTrace = params["stackTrace"];
2475
+ const message = {
2476
+ type: params["type"],
2477
+ text: this.formatConsoleArgs(args ?? []),
2478
+ args: args?.map((a) => a.value) ?? [],
2479
+ timestamp: params["timestamp"],
2480
+ stackTrace: stackTrace?.callFrames?.map((f) => `${f.url}:${f.lineNumber}`)
2481
+ };
2482
+ for (const handler of this.consoleHandlers) {
2483
+ try {
2484
+ handler(message);
2485
+ } catch (e) {
2486
+ console.error("[Console handler error]", e);
2487
+ }
2488
+ }
2489
+ }
2490
+ /**
2491
+ * Handle JavaScript exceptions
2492
+ */
2493
+ handleException(params) {
2494
+ const details = params["exceptionDetails"];
2495
+ const exception = details["exception"];
2496
+ const stackTrace = details["stackTrace"];
2497
+ const error = {
2498
+ message: exception?.description ?? details["text"],
2499
+ url: details["url"],
2500
+ lineNumber: details["lineNumber"],
2501
+ columnNumber: details["columnNumber"],
2502
+ timestamp: params["timestamp"],
2503
+ stackTrace: stackTrace?.callFrames?.map((f) => `${f.url}:${f.lineNumber}`)
2504
+ };
2505
+ for (const handler of this.errorHandlers) {
2506
+ try {
2507
+ handler(error);
2508
+ } catch (e) {
2509
+ console.error("[Error handler error]", e);
2510
+ }
2511
+ }
2512
+ }
2513
+ /**
2514
+ * Handle dialog opening
2515
+ */
2516
+ async handleDialogOpening(params) {
2517
+ const dialog = {
2518
+ type: params["type"],
2519
+ message: params["message"],
2520
+ defaultValue: params["defaultPrompt"],
2521
+ accept: async (promptText) => {
2522
+ await this.cdp.send("Page.handleJavaScriptDialog", {
2523
+ accept: true,
2524
+ promptText
2525
+ });
2526
+ },
2527
+ dismiss: async () => {
2528
+ await this.cdp.send("Page.handleJavaScriptDialog", {
2529
+ accept: false
2530
+ });
2531
+ }
2532
+ };
2533
+ if (this.dialogHandler) {
2534
+ try {
2535
+ await this.dialogHandler(dialog);
2536
+ } catch (e) {
2537
+ console.error("[Dialog handler error]", e);
2538
+ await dialog.dismiss();
2539
+ }
2540
+ } else {
2541
+ await dialog.dismiss();
2542
+ }
2543
+ }
2544
+ /**
2545
+ * Format console arguments to string
2546
+ */
2547
+ formatConsoleArgs(args) {
2548
+ return args.map((arg) => {
2549
+ if (arg.value !== void 0) return String(arg.value);
2550
+ if (arg.description) return arg.description;
2551
+ return "[object]";
2552
+ }).join(" ");
2553
+ }
2554
+ /**
2555
+ * Subscribe to console messages
2556
+ */
2557
+ async onConsole(handler) {
2558
+ await this.enableConsole();
2559
+ this.consoleHandlers.add(handler);
2560
+ return () => this.consoleHandlers.delete(handler);
2561
+ }
2562
+ /**
2563
+ * Subscribe to page errors
2564
+ */
2565
+ async onError(handler) {
2566
+ await this.enableConsole();
2567
+ this.errorHandlers.add(handler);
2568
+ return () => this.errorHandlers.delete(handler);
2569
+ }
2570
+ /**
2571
+ * Set dialog handler (only one at a time)
2572
+ */
2573
+ async onDialog(handler) {
2574
+ await this.enableConsole();
2575
+ this.dialogHandler = handler;
2576
+ }
2577
+ /**
2578
+ * Collect console messages during an action
2579
+ */
2580
+ async collectConsole(fn) {
2581
+ const messages = [];
2582
+ const unsubscribe = await this.onConsole((msg) => messages.push(msg));
2583
+ try {
2584
+ const result = await fn();
2585
+ return { result, messages };
2586
+ } finally {
2587
+ unsubscribe();
2588
+ }
2589
+ }
2590
+ /**
2591
+ * Collect errors during an action
2592
+ */
2593
+ async collectErrors(fn) {
2594
+ const errors = [];
2595
+ const unsubscribe = await this.onError((err) => errors.push(err));
2596
+ try {
2597
+ const result = await fn();
2598
+ return { result, errors };
2599
+ } finally {
2600
+ unsubscribe();
2601
+ }
2602
+ }
2603
+ // ============ Lifecycle ============
2604
+ /**
2605
+ * Reset page state for clean test isolation
2606
+ * - Stops any pending operations
2607
+ * - Clears localStorage and sessionStorage
2608
+ * - Resets internal state
2609
+ */
2610
+ async reset() {
2611
+ this.rootNodeId = null;
2612
+ this.refMap.clear();
2613
+ this.currentFrame = null;
2614
+ this.currentFrameContextId = null;
2615
+ this.frameContexts.clear();
2616
+ this.dialogHandler = null;
2617
+ try {
2618
+ await this.cdp.send("Page.stopLoading");
2619
+ } catch {
2620
+ }
2621
+ try {
2622
+ await this.cdp.send("Runtime.evaluate", {
2623
+ expression: `(() => {
2624
+ try { localStorage.clear(); } catch {}
2625
+ try { sessionStorage.clear(); } catch {}
2626
+ })()`
2627
+ });
2628
+ } catch {
2629
+ }
2630
+ }
2631
+ /**
2632
+ * Close this page (no-op for now, managed by Browser)
2633
+ * This is a placeholder for API compatibility
2634
+ */
2635
+ async close() {
2636
+ }
2637
+ // ============ Private Helpers ============
2638
+ /**
2639
+ * Retry wrapper for operations that may encounter stale nodes
2640
+ * Catches "Could not find node with given id" errors and retries
2641
+ */
2642
+ async withStaleNodeRetry(fn, options = {}) {
2643
+ const { retries = 2, delay = 50 } = options;
2644
+ let lastError;
2645
+ for (let attempt = 0; attempt <= retries; attempt++) {
2646
+ try {
2647
+ return await fn();
2648
+ } catch (e) {
2649
+ if (e instanceof Error && (e.message.includes("Could not find node with given id") || e.message.includes("Node with given id does not belong to the document") || e.message.includes("No node with given id found"))) {
2650
+ lastError = e;
2651
+ if (attempt < retries) {
2652
+ this.rootNodeId = null;
2653
+ await sleep2(delay);
2654
+ continue;
2655
+ }
2656
+ }
2657
+ throw e;
2658
+ }
2659
+ }
2660
+ throw lastError ?? new Error("Stale node retry exhausted");
2661
+ }
2662
+ /**
2663
+ * Find an element using single or multiple selectors
2664
+ * Supports ref: prefix for ref-based selectors (e.g., "ref:e4")
2665
+ */
2666
+ async findElement(selectors, options = {}) {
2667
+ const { timeout = DEFAULT_TIMEOUT2 } = options;
2668
+ const selectorList = Array.isArray(selectors) ? selectors : [selectors];
2669
+ for (const selector of selectorList) {
2670
+ if (selector.startsWith("ref:")) {
2671
+ const ref = selector.slice(4);
2672
+ const backendNodeId = this.refMap.get(ref);
2673
+ if (!backendNodeId) {
2674
+ continue;
2675
+ }
2676
+ try {
2677
+ await this.ensureRootNode();
2678
+ const pushResult = await this.cdp.send(
2679
+ "DOM.pushNodesByBackendIdsToFrontend",
2680
+ {
2681
+ backendNodeIds: [backendNodeId]
2682
+ }
2683
+ );
2684
+ if (pushResult.nodeIds?.[0]) {
2685
+ return {
2686
+ nodeId: pushResult.nodeIds[0],
2687
+ backendNodeId,
2688
+ selector,
2689
+ waitedMs: 0
2690
+ };
2691
+ }
2692
+ } catch {
2693
+ }
2694
+ }
2695
+ }
2696
+ const cssSelectors = selectorList.filter((s) => !s.startsWith("ref:"));
2697
+ if (cssSelectors.length === 0) {
2698
+ return null;
2699
+ }
2700
+ const result = await waitForAnyElement(this.cdp, cssSelectors, {
2701
+ state: "visible",
2702
+ timeout,
2703
+ contextId: this.currentFrameContextId ?? void 0
2704
+ });
2705
+ if (!result.success || !result.selector) {
2706
+ return null;
2707
+ }
2708
+ await this.ensureRootNode();
2709
+ const queryResult = await this.cdp.send("DOM.querySelector", {
2710
+ nodeId: this.rootNodeId,
2711
+ selector: result.selector
2712
+ });
2713
+ if (queryResult.nodeId) {
2714
+ const describeResult2 = await this.cdp.send(
2715
+ "DOM.describeNode",
2716
+ { nodeId: queryResult.nodeId }
2717
+ );
2718
+ return {
2719
+ nodeId: queryResult.nodeId,
2720
+ backendNodeId: describeResult2.node.backendNodeId,
2721
+ selector: result.selector,
2722
+ waitedMs: result.waitedMs
2723
+ };
2724
+ }
2725
+ const deepQueryResult = await this.evaluateInFrame(
2726
+ `(() => {
2727
+ ${DEEP_QUERY_SCRIPT}
2728
+ return deepQuery(${JSON.stringify(result.selector)});
2729
+ })()`,
2730
+ { returnByValue: false }
2731
+ );
2732
+ if (!deepQueryResult.result.objectId) {
2733
+ return null;
2734
+ }
2735
+ const nodeResult = await this.cdp.send("DOM.requestNode", {
2736
+ objectId: deepQueryResult.result.objectId
2737
+ });
2738
+ if (!nodeResult.nodeId) {
2739
+ return null;
2740
+ }
2741
+ const describeResult = await this.cdp.send(
2742
+ "DOM.describeNode",
2743
+ { nodeId: nodeResult.nodeId }
2744
+ );
2745
+ return {
2746
+ nodeId: nodeResult.nodeId,
2747
+ backendNodeId: describeResult.node.backendNodeId,
2748
+ selector: result.selector,
2749
+ waitedMs: result.waitedMs
2750
+ };
2751
+ }
2752
+ /**
2753
+ * Ensure we have a valid root node ID
2754
+ */
2755
+ async ensureRootNode() {
2756
+ if (this.rootNodeId) return;
2757
+ const doc = await this.cdp.send("DOM.getDocument", {
2758
+ depth: 0
2759
+ });
2760
+ this.rootNodeId = doc.root.nodeId;
2761
+ }
2762
+ /**
2763
+ * Execute Runtime.evaluate in the current frame context
2764
+ * Automatically injects contextId when in an iframe
2765
+ */
2766
+ async evaluateInFrame(expression, options = {}) {
2767
+ const params = {
2768
+ expression,
2769
+ returnByValue: options.returnByValue ?? true,
2770
+ awaitPromise: options.awaitPromise ?? false
2771
+ };
2772
+ if (this.currentFrameContextId !== null) {
2773
+ params["contextId"] = this.currentFrameContextId;
2774
+ }
2775
+ return this.cdp.send("Runtime.evaluate", params);
2776
+ }
2777
+ /**
2778
+ * Scroll an element into view
2779
+ */
2780
+ async scrollIntoView(nodeId) {
2781
+ await this.cdp.send("DOM.scrollIntoViewIfNeeded", { nodeId });
2782
+ }
2783
+ /**
2784
+ * Get element box model (position and dimensions)
2785
+ */
2786
+ async getBoxModel(nodeId) {
2787
+ try {
2788
+ const result = await this.cdp.send("DOM.getBoxModel", {
2789
+ nodeId
2790
+ });
2791
+ return result.model;
2792
+ } catch {
2793
+ return null;
2794
+ }
2795
+ }
2796
+ /**
2797
+ * Click an element by node ID
2798
+ */
2799
+ async clickElement(nodeId) {
2800
+ const box = await this.getBoxModel(nodeId);
2801
+ if (!box) {
2802
+ throw new Error("Could not get element box model for click");
2803
+ }
2804
+ const x = box.content[0] + box.width / 2;
2805
+ const y = box.content[1] + box.height / 2;
2806
+ await this.cdp.send("Input.dispatchMouseEvent", {
2807
+ type: "mousePressed",
2808
+ x,
2809
+ y,
2810
+ button: "left",
2811
+ clickCount: 1
2812
+ });
2813
+ await this.cdp.send("Input.dispatchMouseEvent", {
2814
+ type: "mouseReleased",
2815
+ x,
2816
+ y,
2817
+ button: "left",
2818
+ clickCount: 1
2819
+ });
2820
+ }
2821
+ };
2822
+ function sleep2(ms) {
2823
+ return new Promise((resolve) => setTimeout(resolve, ms));
2824
+ }
2825
+
2826
+ // src/browser/browser.ts
2827
+ var Browser = class _Browser {
2828
+ cdp;
2829
+ providerSession;
2830
+ pages = /* @__PURE__ */ new Map();
2831
+ constructor(cdp, _provider, providerSession, _options) {
2832
+ this.cdp = cdp;
2833
+ this.providerSession = providerSession;
2834
+ }
2835
+ /**
2836
+ * Connect to a browser instance
2837
+ */
2838
+ static async connect(options) {
2839
+ const provider = createProvider(options);
2840
+ const session = await provider.createSession(options.session);
2841
+ const cdp = await createCDPClient(session.wsUrl, {
2842
+ debug: options.debug,
2843
+ timeout: options.timeout
2844
+ });
2845
+ return new _Browser(cdp, provider, session, options);
2846
+ }
2847
+ /**
2848
+ * Get or create a page by name
2849
+ * If no name is provided, returns the first available page or creates a new one
2850
+ */
2851
+ async page(name) {
2852
+ const pageName = name ?? "default";
2853
+ const cached = this.pages.get(pageName);
2854
+ if (cached) return cached;
2855
+ const targets = await this.cdp.send("Target.getTargets");
2856
+ const pageTargets = targets.targetInfos.filter((t) => t.type === "page");
2857
+ let targetId;
2858
+ if (pageTargets.length > 0) {
2859
+ targetId = pageTargets[0].targetId;
2860
+ } else {
2861
+ const result = await this.cdp.send("Target.createTarget", {
2862
+ url: "about:blank"
2863
+ });
2864
+ targetId = result.targetId;
2865
+ }
2866
+ await this.cdp.attachToTarget(targetId);
2867
+ const page = new Page(this.cdp);
2868
+ await page.init();
2869
+ this.pages.set(pageName, page);
2870
+ return page;
2871
+ }
2872
+ /**
2873
+ * Create a new page (tab)
2874
+ */
2875
+ async newPage(url = "about:blank") {
2876
+ const result = await this.cdp.send("Target.createTarget", {
2877
+ url
2878
+ });
2879
+ await this.cdp.attachToTarget(result.targetId);
2880
+ const page = new Page(this.cdp);
2881
+ await page.init();
2882
+ const name = `page-${this.pages.size + 1}`;
2883
+ this.pages.set(name, page);
2884
+ return page;
2885
+ }
2886
+ /**
2887
+ * Close a page by name
2888
+ */
2889
+ async closePage(name) {
2890
+ const page = this.pages.get(name);
2891
+ if (!page) return;
2892
+ const targets = await this.cdp.send("Target.getTargets");
2893
+ const pageTargets = targets.targetInfos.filter((t) => t.type === "page");
2894
+ if (pageTargets.length > 0) {
2895
+ await this.cdp.send("Target.closeTarget", {
2896
+ targetId: pageTargets[0].targetId
2897
+ });
2898
+ }
2899
+ this.pages.delete(name);
2900
+ }
2901
+ /**
2902
+ * Get the WebSocket URL for this browser connection
2903
+ */
2904
+ get wsUrl() {
2905
+ return this.providerSession.wsUrl;
2906
+ }
2907
+ /**
2908
+ * Get the provider session ID (for resumption)
2909
+ */
2910
+ get sessionId() {
2911
+ return this.providerSession.sessionId;
2912
+ }
2913
+ /**
2914
+ * Get provider metadata
2915
+ */
2916
+ get metadata() {
2917
+ return this.providerSession.metadata;
2918
+ }
2919
+ /**
2920
+ * Check if connected
2921
+ */
2922
+ get isConnected() {
2923
+ return this.cdp.isConnected;
2924
+ }
2925
+ /**
2926
+ * Disconnect from the browser (keeps provider session alive for reconnection)
2927
+ */
2928
+ async disconnect() {
2929
+ this.pages.clear();
2930
+ await this.cdp.close();
2931
+ }
2932
+ /**
2933
+ * Close the browser session completely
2934
+ */
2935
+ async close() {
2936
+ this.pages.clear();
2937
+ await this.cdp.close();
2938
+ await this.providerSession.close();
2939
+ }
2940
+ /**
2941
+ * Get the underlying CDP client (for advanced usage)
2942
+ */
2943
+ get cdpClient() {
2944
+ return this.cdp;
2945
+ }
2946
+ };
2947
+ function connect(options) {
2948
+ return Browser.connect(options);
2949
+ }
2950
+
2951
+ // src/cli/session.ts
2952
+ var import_node_os = require("os");
2953
+ var import_node_path = require("path");
2954
+ var SESSION_DIR = (0, import_node_path.join)((0, import_node_os.homedir)(), ".browser-pilot", "sessions");
2955
+ async function ensureSessionDir() {
2956
+ const fs = await import("fs/promises");
2957
+ await fs.mkdir(SESSION_DIR, { recursive: true });
2958
+ }
2959
+ async function saveSession(session) {
2960
+ await ensureSessionDir();
2961
+ const fs = await import("fs/promises");
2962
+ const filePath = (0, import_node_path.join)(SESSION_DIR, `${session.id}.json`);
2963
+ await fs.writeFile(filePath, JSON.stringify(session, null, 2));
2964
+ }
2965
+ async function loadSession(id) {
2966
+ const fs = await import("fs/promises");
2967
+ const filePath = (0, import_node_path.join)(SESSION_DIR, `${id}.json`);
2968
+ try {
2969
+ const content = await fs.readFile(filePath, "utf-8");
2970
+ return JSON.parse(content);
2971
+ } catch (error) {
2972
+ if (error.code === "ENOENT") {
2973
+ throw new Error(`Session not found: ${id}`);
2974
+ }
2975
+ throw error;
2976
+ }
2977
+ }
2978
+ async function updateSession(id, updates) {
2979
+ const session = await loadSession(id);
2980
+ const updated = {
2981
+ ...session,
2982
+ ...updates,
2983
+ lastActivity: (/* @__PURE__ */ new Date()).toISOString()
2984
+ };
2985
+ await saveSession(updated);
2986
+ return updated;
2987
+ }
2988
+ async function deleteSession(id) {
2989
+ const fs = await import("fs/promises");
2990
+ const filePath = (0, import_node_path.join)(SESSION_DIR, `${id}.json`);
2991
+ try {
2992
+ await fs.unlink(filePath);
2993
+ } catch (error) {
2994
+ if (error.code !== "ENOENT") {
2995
+ throw error;
2996
+ }
2997
+ }
2998
+ }
2999
+ async function listSessions() {
3000
+ await ensureSessionDir();
3001
+ const fs = await import("fs/promises");
3002
+ try {
3003
+ const files = await fs.readdir(SESSION_DIR);
3004
+ const sessions = [];
3005
+ for (const file of files) {
3006
+ if (file.endsWith(".json")) {
3007
+ try {
3008
+ const content = await fs.readFile((0, import_node_path.join)(SESSION_DIR, file), "utf-8");
3009
+ sessions.push(JSON.parse(content));
3010
+ } catch {
3011
+ }
3012
+ }
3013
+ }
3014
+ return sessions.sort(
3015
+ (a, b) => new Date(b.lastActivity).getTime() - new Date(a.lastActivity).getTime()
3016
+ );
3017
+ } catch {
3018
+ return [];
3019
+ }
3020
+ }
3021
+ function generateSessionId() {
3022
+ const timestamp = Date.now().toString(36);
3023
+ const random = Math.random().toString(36).slice(2, 8);
3024
+ return `${timestamp}-${random}`;
3025
+ }
3026
+ async function getDefaultSession() {
3027
+ const sessions = await listSessions();
3028
+ return sessions[0] ?? null;
3029
+ }
3030
+
3031
+ // src/cli/commands/close.ts
3032
+ async function closeCommand(args, globalOptions) {
3033
+ let session;
3034
+ if (globalOptions.session) {
3035
+ session = await loadSession(globalOptions.session);
3036
+ } else if (args[0]) {
3037
+ session = await loadSession(args[0]);
3038
+ } else {
3039
+ session = await getDefaultSession();
3040
+ if (!session) {
3041
+ throw new Error('No session found. Specify a session ID or run "bp list" to see sessions.');
3042
+ }
3043
+ }
3044
+ try {
3045
+ const browser = await connect({
3046
+ provider: session.provider,
3047
+ wsUrl: session.wsUrl,
3048
+ debug: globalOptions.trace
3049
+ });
3050
+ await browser.close();
3051
+ } catch {
3052
+ }
3053
+ await deleteSession(session.id);
3054
+ output(
3055
+ {
3056
+ success: true,
3057
+ sessionId: session.id,
3058
+ message: "Session closed"
3059
+ },
3060
+ globalOptions.output
3061
+ );
3062
+ }
3063
+
3064
+ // src/cli/commands/connect.ts
3065
+ function parseConnectArgs(args) {
3066
+ const options = {};
3067
+ for (let i = 0; i < args.length; i++) {
3068
+ const arg = args[i];
3069
+ if (arg === "--provider" || arg === "-p") {
3070
+ options.provider = args[++i];
3071
+ } else if (arg === "--url") {
3072
+ options.url = args[++i];
3073
+ } else if (arg === "--name" || arg === "-n") {
3074
+ options.name = args[++i];
3075
+ } else if (arg === "--resume" || arg === "-r") {
3076
+ options.resume = args[++i];
3077
+ } else if (arg === "--api-key") {
3078
+ options.apiKey = args[++i];
3079
+ } else if (arg === "--project-id") {
3080
+ options.projectId = args[++i];
3081
+ }
3082
+ }
3083
+ return options;
3084
+ }
3085
+ async function connectCommand(args, globalOptions) {
3086
+ const options = parseConnectArgs(args);
3087
+ if (options.resume || globalOptions.session) {
3088
+ const sessionId2 = options.resume || globalOptions.session;
3089
+ const session2 = await loadSession(sessionId2);
3090
+ output(
3091
+ {
3092
+ success: true,
3093
+ resumed: true,
3094
+ sessionId: session2.id,
3095
+ provider: session2.provider,
3096
+ currentUrl: session2.currentUrl
3097
+ },
3098
+ globalOptions.output
3099
+ );
3100
+ return;
3101
+ }
3102
+ const provider = options.provider ?? "generic";
3103
+ let wsUrl = options.url;
3104
+ if (provider === "generic" && !wsUrl) {
3105
+ try {
3106
+ wsUrl = await getBrowserWebSocketUrl("localhost:9222");
3107
+ } catch {
3108
+ throw new Error(
3109
+ "Could not auto-discover browser. Specify --url or start Chrome with --remote-debugging-port=9222"
3110
+ );
3111
+ }
3112
+ }
3113
+ const connectOptions = {
3114
+ provider,
3115
+ debug: globalOptions.trace,
3116
+ wsUrl,
3117
+ apiKey: options.apiKey,
3118
+ projectId: options.projectId
3119
+ };
3120
+ const browser = await connect(connectOptions);
3121
+ const page = await browser.page();
3122
+ const currentUrl = await page.url();
3123
+ const sessionId = options.name ?? generateSessionId();
3124
+ const session = {
3125
+ id: sessionId,
3126
+ provider,
3127
+ wsUrl: browser.wsUrl,
3128
+ providerSessionId: browser.sessionId,
3129
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
3130
+ lastActivity: (/* @__PURE__ */ new Date()).toISOString(),
3131
+ currentUrl,
3132
+ metadata: browser.metadata
3133
+ };
3134
+ await saveSession(session);
3135
+ await browser.disconnect();
3136
+ output(
3137
+ {
3138
+ success: true,
3139
+ sessionId,
3140
+ provider,
3141
+ currentUrl,
3142
+ metadata: browser.metadata
3143
+ },
3144
+ globalOptions.output
3145
+ );
3146
+ }
3147
+
3148
+ // src/cli/commands/exec.ts
3149
+ function parseExecArgs(args) {
3150
+ const options = {};
3151
+ let actionsJson;
3152
+ for (let i = 0; i < args.length; i++) {
3153
+ const arg = args[i];
3154
+ if (arg === "--dialog") {
3155
+ const value = args[++i];
3156
+ if (value === "accept" || value === "dismiss") {
3157
+ options.dialog = value;
3158
+ } else {
3159
+ throw new Error('--dialog must be "accept" or "dismiss"');
3160
+ }
3161
+ } else if (!actionsJson && !arg.startsWith("-")) {
3162
+ actionsJson = arg;
3163
+ }
3164
+ }
3165
+ return { actionsJson, options };
3166
+ }
3167
+ async function execCommand(args, globalOptions) {
3168
+ const { actionsJson, options: execOptions } = parseExecArgs(args);
3169
+ let session;
3170
+ if (globalOptions.session) {
3171
+ session = await loadSession(globalOptions.session);
3172
+ } else {
3173
+ session = await getDefaultSession();
3174
+ if (!session) {
3175
+ throw new Error('No session found. Run "bp connect" first.');
3176
+ }
3177
+ }
3178
+ if (!actionsJson) {
3179
+ throw new Error(
3180
+ `No actions provided. Usage: bp exec '{"action":"goto","url":"..."}'
3181
+
3182
+ Run 'bp actions' for complete action reference.`
3183
+ );
3184
+ }
3185
+ let actions;
3186
+ try {
3187
+ actions = JSON.parse(actionsJson);
3188
+ } catch {
3189
+ throw new Error(
3190
+ "Invalid JSON. Actions must be valid JSON.\n\nRun 'bp actions' for complete action reference."
3191
+ );
3192
+ }
3193
+ const browser = await connect({
3194
+ provider: session.provider,
3195
+ wsUrl: session.wsUrl,
3196
+ debug: globalOptions.trace
3197
+ });
3198
+ try {
3199
+ const page = addBatchToPage(await browser.page());
3200
+ if (execOptions.dialog) {
3201
+ await page.onDialog(async (dialog) => {
3202
+ if (execOptions.dialog === "accept") {
3203
+ await dialog.accept();
3204
+ } else {
3205
+ await dialog.dismiss();
3206
+ }
3207
+ });
3208
+ }
3209
+ const steps = Array.isArray(actions) ? actions : [actions];
3210
+ const result = await page.batch(steps);
3211
+ const currentUrl = await page.url();
3212
+ await updateSession(session.id, { currentUrl });
3213
+ output(
3214
+ {
3215
+ success: result.success,
3216
+ stoppedAtIndex: result.stoppedAtIndex,
3217
+ steps: result.steps.map((s) => ({
3218
+ action: s.action,
3219
+ success: s.success,
3220
+ durationMs: s.durationMs,
3221
+ selectorUsed: s.selectorUsed,
3222
+ error: s.error,
3223
+ text: s.text,
3224
+ result: s.result
3225
+ })),
3226
+ totalDurationMs: result.totalDurationMs,
3227
+ currentUrl
3228
+ },
3229
+ globalOptions.output
3230
+ );
3231
+ } finally {
3232
+ await browser.disconnect();
3233
+ }
3234
+ }
3235
+
3236
+ // src/cli/commands/list.ts
3237
+ async function listCommand(_args, globalOptions) {
3238
+ const sessions = await listSessions();
3239
+ if (globalOptions.output === "json") {
3240
+ output(sessions, "json");
3241
+ return;
3242
+ }
3243
+ if (sessions.length === 0) {
3244
+ console.log("No active sessions.");
3245
+ console.log('Run "bp connect" to create a new session.');
3246
+ return;
3247
+ }
3248
+ console.log("Active Sessions:\n");
3249
+ for (const session of sessions) {
3250
+ const age = getAge(new Date(session.lastActivity));
3251
+ console.log(` ${session.id}`);
3252
+ console.log(` Provider: ${session.provider}`);
3253
+ console.log(` URL: ${session.currentUrl}`);
3254
+ console.log(` Last activity: ${age}`);
3255
+ console.log("");
3256
+ }
3257
+ }
3258
+ function getAge(date) {
3259
+ const now = Date.now();
3260
+ const diff = now - date.getTime();
3261
+ const seconds = Math.floor(diff / 1e3);
3262
+ if (seconds < 60) return "just now";
3263
+ const minutes = Math.floor(seconds / 60);
3264
+ if (minutes < 60) return `${minutes}m ago`;
3265
+ const hours = Math.floor(minutes / 60);
3266
+ if (hours < 24) return `${hours}h ago`;
3267
+ const days = Math.floor(hours / 24);
3268
+ return `${days}d ago`;
3269
+ }
3270
+
3271
+ // src/cli/commands/screenshot.ts
3272
+ function parseScreenshotArgs(args) {
3273
+ const options = {};
3274
+ for (let i = 0; i < args.length; i++) {
3275
+ const arg = args[i];
3276
+ if (arg === "--output" || arg === "-o") {
3277
+ options.outputPath = args[++i];
3278
+ } else if (arg === "--format" || arg === "-f") {
3279
+ options.format = args[++i];
3280
+ } else if (arg === "--quality" || arg === "-q") {
3281
+ options.quality = parseInt(args[++i], 10);
3282
+ } else if (arg === "--full-page" || arg === "--fullpage") {
3283
+ options.fullPage = true;
3284
+ }
3285
+ }
3286
+ return options;
3287
+ }
3288
+ async function screenshotCommand(args, globalOptions) {
3289
+ const options = parseScreenshotArgs(args);
3290
+ let session;
3291
+ if (globalOptions.session) {
3292
+ session = await loadSession(globalOptions.session);
3293
+ } else {
3294
+ session = await getDefaultSession();
3295
+ if (!session) {
3296
+ throw new Error('No session found. Run "bp connect" first.');
3297
+ }
3298
+ }
3299
+ const browser = await connect({
3300
+ provider: session.provider,
3301
+ wsUrl: session.wsUrl,
3302
+ debug: globalOptions.trace
3303
+ });
3304
+ try {
3305
+ const page = await browser.page();
3306
+ const screenshotData = await page.screenshot({
3307
+ format: options.format ?? "png",
3308
+ quality: options.quality,
3309
+ fullPage: options.fullPage ?? false
3310
+ });
3311
+ if (options.outputPath) {
3312
+ const buffer = Buffer.from(screenshotData, "base64");
3313
+ await Bun.write(options.outputPath, buffer);
3314
+ output(
3315
+ {
3316
+ success: true,
3317
+ path: options.outputPath,
3318
+ size: buffer.length,
3319
+ format: options.format ?? "png"
3320
+ },
3321
+ globalOptions.output
3322
+ );
3323
+ } else {
3324
+ if (globalOptions.output === "json") {
3325
+ output({ data: screenshotData, format: options.format ?? "png" }, "json");
3326
+ } else {
3327
+ console.log(screenshotData);
3328
+ }
3329
+ }
3330
+ } finally {
3331
+ await browser.disconnect();
3332
+ }
3333
+ }
3334
+
3335
+ // src/cli/commands/snapshot.ts
3336
+ function parseSnapshotArgs(args) {
3337
+ const options = {};
3338
+ for (let i = 0; i < args.length; i++) {
3339
+ const arg = args[i];
3340
+ if (arg === "--format" || arg === "-f") {
3341
+ options.format = args[++i];
3342
+ }
3343
+ }
3344
+ return options;
3345
+ }
3346
+ async function snapshotCommand(args, globalOptions) {
3347
+ const options = parseSnapshotArgs(args);
3348
+ let session;
3349
+ if (globalOptions.session) {
3350
+ session = await loadSession(globalOptions.session);
3351
+ } else {
3352
+ session = await getDefaultSession();
3353
+ if (!session) {
3354
+ throw new Error('No session found. Run "bp connect" first.');
3355
+ }
3356
+ }
3357
+ const browser = await connect({
3358
+ provider: session.provider,
3359
+ wsUrl: session.wsUrl,
3360
+ debug: globalOptions.trace
3361
+ });
3362
+ try {
3363
+ const page = await browser.page();
3364
+ const snapshot = await page.snapshot();
3365
+ await updateSession(session.id, { currentUrl: snapshot.url });
3366
+ switch (options.format) {
3367
+ case "interactive":
3368
+ output(snapshot.interactiveElements, globalOptions.output);
3369
+ break;
3370
+ case "text":
3371
+ console.log(snapshot.text);
3372
+ break;
3373
+ default:
3374
+ output(snapshot, globalOptions.output);
3375
+ break;
3376
+ }
3377
+ } finally {
3378
+ await browser.disconnect();
3379
+ }
3380
+ }
3381
+
3382
+ // src/cli/commands/text.ts
3383
+ function parseTextArgs(args) {
3384
+ const options = {};
3385
+ for (let i = 0; i < args.length; i++) {
3386
+ const arg = args[i];
3387
+ if (arg === "--selector" || arg === "-s") {
3388
+ options.selector = args[++i];
3389
+ }
3390
+ }
3391
+ return options;
3392
+ }
3393
+ async function textCommand(args, globalOptions) {
3394
+ const options = parseTextArgs(args);
3395
+ let session;
3396
+ if (globalOptions.session) {
3397
+ session = await loadSession(globalOptions.session);
3398
+ } else {
3399
+ session = await getDefaultSession();
3400
+ if (!session) {
3401
+ throw new Error('No session found. Run "bp connect" first.');
3402
+ }
3403
+ }
3404
+ const browser = await connect({
3405
+ provider: session.provider,
3406
+ wsUrl: session.wsUrl,
3407
+ debug: globalOptions.trace
3408
+ });
3409
+ try {
3410
+ const page = await browser.page();
3411
+ const text = await page.text(options.selector);
3412
+ const currentUrl = await page.url();
3413
+ await updateSession(session.id, { currentUrl });
3414
+ if (globalOptions.output === "json") {
3415
+ output({ text, url: currentUrl, selector: options.selector }, "json");
3416
+ } else {
3417
+ console.log(text);
3418
+ }
3419
+ } finally {
3420
+ await browser.disconnect();
3421
+ }
3422
+ }
3423
+
3424
+ // src/cli/index.ts
3425
+ var HELP = `
3426
+ bp - Browser automation CLI for AI agents
3427
+
3428
+ Usage:
3429
+ bp <command> [options]
3430
+
3431
+ Commands:
3432
+ connect Create or resume browser session
3433
+ exec Execute actions on current session
3434
+ snapshot Get page accessibility snapshot (includes element refs)
3435
+ text Extract text content from page
3436
+ screenshot Take screenshot
3437
+ close Close session
3438
+ list List all sessions
3439
+ actions Show all available actions with examples
3440
+
3441
+ Global Options:
3442
+ -s, --session <id> Session ID to use
3443
+ -o, --output <fmt> Output format: json | pretty (default: pretty)
3444
+ --trace Enable execution tracing
3445
+ -h, --help Show this help message
3446
+
3447
+ Exec Options:
3448
+ --dialog <mode> Auto-handle dialogs: accept | dismiss
3449
+
3450
+ Ref Selectors (Recommended for AI Agents):
3451
+ 1. Take snapshot: bp snapshot --format text
3452
+ Output shows: button "Submit" [ref=e4], textbox "Email" [ref=e5]
3453
+ 2. Use ref directly: bp exec '{"action":"click","selector":"ref:e4"}'
3454
+
3455
+ Refs are stable until navigation. Combine with CSS fallbacks:
3456
+ {"selector": ["ref:e4", "#submit", "button[type=submit]"]}
3457
+
3458
+ Examples:
3459
+ # Connect to browser
3460
+ bp connect --provider generic --name dev
3461
+
3462
+ # Navigate and get snapshot with refs
3463
+ bp exec '{"action":"goto","url":"https://example.com"}'
3464
+ bp snapshot --format text
3465
+
3466
+ # Use ref from snapshot (most reliable)
3467
+ bp exec '{"action":"click","selector":"ref:e4"}'
3468
+ bp exec '{"action":"fill","selector":"ref:e5","value":"test@example.com"}'
3469
+
3470
+ # Handle native dialogs (alert/confirm/prompt)
3471
+ bp exec --dialog accept '{"action":"click","selector":"#delete-btn"}'
3472
+
3473
+ # Batch multiple actions
3474
+ bp exec '[
3475
+ {"action":"fill","selector":"ref:e5","value":"user@example.com"},
3476
+ {"action":"click","selector":"ref:e4"},
3477
+ {"action":"snapshot"}
3478
+ ]'
3479
+
3480
+ Run 'bp actions' for complete action reference.
3481
+ `;
3482
+ function parseGlobalOptions(args) {
3483
+ const options = {
3484
+ output: "pretty"
3485
+ };
3486
+ const remaining = [];
3487
+ for (let i = 0; i < args.length; i++) {
3488
+ const arg = args[i];
3489
+ if (arg === "-s" || arg === "--session") {
3490
+ options.session = args[++i];
3491
+ } else if (arg === "-o" || arg === "--output") {
3492
+ options.output = args[++i];
3493
+ } else if (arg === "--trace") {
3494
+ options.trace = true;
3495
+ } else if (arg === "-h" || arg === "--help") {
3496
+ options.help = true;
3497
+ } else {
3498
+ remaining.push(arg);
3499
+ }
3500
+ }
3501
+ return { options, remaining };
3502
+ }
3503
+ function output(data, format = "pretty") {
3504
+ if (format === "json") {
3505
+ console.log(JSON.stringify(data, null, 2));
3506
+ } else {
3507
+ if (typeof data === "string") {
3508
+ console.log(data);
3509
+ } else if (typeof data === "object" && data !== null) {
3510
+ prettyPrint(data);
3511
+ } else {
3512
+ console.log(data);
3513
+ }
3514
+ }
3515
+ }
3516
+ function prettyPrint(obj, indent = 0) {
3517
+ const prefix = " ".repeat(indent);
3518
+ for (const [key, value] of Object.entries(obj)) {
3519
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
3520
+ console.log(`${prefix}${key}:`);
3521
+ prettyPrint(value, indent + 1);
3522
+ } else if (Array.isArray(value)) {
3523
+ console.log(`${prefix}${key}: [${value.length} items]`);
3524
+ } else {
3525
+ console.log(`${prefix}${key}: ${value}`);
3526
+ }
3527
+ }
3528
+ }
3529
+ async function main() {
3530
+ const args = process.argv.slice(2);
3531
+ if (args.length === 0) {
3532
+ console.log(HELP);
3533
+ process.exit(0);
3534
+ }
3535
+ const command = args[0];
3536
+ const { options, remaining } = parseGlobalOptions(args.slice(1));
3537
+ if (options.help && !command) {
3538
+ console.log(HELP);
3539
+ process.exit(0);
3540
+ }
3541
+ try {
3542
+ switch (command) {
3543
+ case "connect":
3544
+ await connectCommand(remaining, options);
3545
+ break;
3546
+ case "exec":
3547
+ await execCommand(remaining, options);
3548
+ break;
3549
+ case "snapshot":
3550
+ await snapshotCommand(remaining, options);
3551
+ break;
3552
+ case "text":
3553
+ await textCommand(remaining, options);
3554
+ break;
3555
+ case "screenshot":
3556
+ await screenshotCommand(remaining, options);
3557
+ break;
3558
+ case "close":
3559
+ await closeCommand(remaining, options);
3560
+ break;
3561
+ case "list":
3562
+ await listCommand(remaining, options);
3563
+ break;
3564
+ case "actions":
3565
+ await actionsCommand();
3566
+ break;
3567
+ case "help":
3568
+ case "--help":
3569
+ case "-h":
3570
+ console.log(HELP);
3571
+ break;
3572
+ default:
3573
+ console.error(`Unknown command: ${command}`);
3574
+ console.log(HELP);
3575
+ process.exit(1);
3576
+ }
3577
+ } catch (error) {
3578
+ const message = error instanceof Error ? error.message : String(error);
3579
+ console.error(`Error: ${message}`);
3580
+ process.exit(1);
3581
+ }
3582
+ }
3583
+ main();
3584
+ // Annotate the CommonJS export names for ESM import in node:
3585
+ 0 && (module.exports = {
3586
+ output
3587
+ });