tuna-agent 0.1.0 → 0.1.2

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.
Files changed (68) hide show
  1. package/dist/agents/claude-code-adapter.d.ts +3 -1
  2. package/dist/agents/claude-code-adapter.js +28 -4
  3. package/dist/agents/factory.d.ts +2 -1
  4. package/dist/agents/factory.js +2 -2
  5. package/dist/browser/actions/download.d.ts +16 -0
  6. package/dist/browser/actions/download.js +39 -0
  7. package/dist/browser/actions/emulation.d.ts +53 -0
  8. package/dist/browser/actions/emulation.js +103 -0
  9. package/dist/browser/actions/evaluate.d.ts +29 -0
  10. package/dist/browser/actions/evaluate.js +92 -0
  11. package/dist/browser/actions/interaction.d.ts +79 -0
  12. package/dist/browser/actions/interaction.js +210 -0
  13. package/dist/browser/actions/keyboard.d.ts +6 -0
  14. package/dist/browser/actions/keyboard.js +9 -0
  15. package/dist/browser/actions/navigation.d.ts +40 -0
  16. package/dist/browser/actions/navigation.js +92 -0
  17. package/dist/browser/actions/wait.d.ts +12 -0
  18. package/dist/browser/actions/wait.js +33 -0
  19. package/dist/browser/browser.d.ts +722 -0
  20. package/dist/browser/browser.js +1066 -0
  21. package/dist/browser/capture/activity.d.ts +22 -0
  22. package/dist/browser/capture/activity.js +39 -0
  23. package/dist/browser/capture/pdf.d.ts +6 -0
  24. package/dist/browser/capture/pdf.js +6 -0
  25. package/dist/browser/capture/response.d.ts +8 -0
  26. package/dist/browser/capture/response.js +28 -0
  27. package/dist/browser/capture/screenshot.d.ts +30 -0
  28. package/dist/browser/capture/screenshot.js +72 -0
  29. package/dist/browser/capture/trace.d.ts +13 -0
  30. package/dist/browser/capture/trace.js +19 -0
  31. package/dist/browser/chrome-launcher.d.ts +8 -0
  32. package/dist/browser/chrome-launcher.js +543 -0
  33. package/dist/browser/connection.d.ts +42 -0
  34. package/dist/browser/connection.js +359 -0
  35. package/dist/browser/index.d.ts +6 -0
  36. package/dist/browser/index.js +3 -0
  37. package/dist/browser/security.d.ts +51 -0
  38. package/dist/browser/security.js +357 -0
  39. package/dist/browser/snapshot/ai-snapshot.d.ts +12 -0
  40. package/dist/browser/snapshot/ai-snapshot.js +47 -0
  41. package/dist/browser/snapshot/aria-snapshot.d.ts +26 -0
  42. package/dist/browser/snapshot/aria-snapshot.js +121 -0
  43. package/dist/browser/snapshot/ref-map.d.ts +31 -0
  44. package/dist/browser/snapshot/ref-map.js +250 -0
  45. package/dist/browser/storage/index.d.ts +36 -0
  46. package/dist/browser/storage/index.js +65 -0
  47. package/dist/browser/types.d.ts +429 -0
  48. package/dist/browser/types.js +2 -0
  49. package/dist/cli/commands/extension.d.ts +10 -0
  50. package/dist/cli/commands/extension.js +86 -0
  51. package/dist/cli/index.js +12 -0
  52. package/dist/daemon/extension-handlers.d.ts +63 -0
  53. package/dist/daemon/extension-handlers.js +630 -0
  54. package/dist/daemon/index.js +173 -44
  55. package/dist/daemon/ws-client.d.ts +28 -8
  56. package/dist/daemon/ws-client.js +68 -62
  57. package/dist/mcp/browser-server.d.ts +11 -0
  58. package/dist/mcp/browser-server.js +467 -0
  59. package/dist/mcp/knowledge-server.d.ts +11 -0
  60. package/dist/mcp/knowledge-server.js +263 -0
  61. package/dist/mcp/setup.d.ts +20 -0
  62. package/dist/mcp/setup.js +94 -0
  63. package/dist/types/index.d.ts +2 -0
  64. package/dist/utils/claude-cli.d.ts +2 -0
  65. package/dist/utils/claude-cli.js +29 -9
  66. package/dist/utils/message-schemas.d.ts +4 -1
  67. package/dist/utils/message-schemas.js +6 -1
  68. package/package.json +2 -1
@@ -0,0 +1,1066 @@
1
+ import { launchChrome, stopChrome, isChromeReachable } from './chrome-launcher.js';
2
+ import { connectBrowser, disconnectBrowser, getPageForTargetId, ensurePageState, pageTargetId, getAllPages, normalizeTimeoutMs } from './connection.js';
3
+ import { snapshotAi } from './snapshot/ai-snapshot.js';
4
+ import { snapshotRole, snapshotAria } from './snapshot/aria-snapshot.js';
5
+ import { clickViaPlaywright, hoverViaPlaywright, typeViaPlaywright, selectOptionViaPlaywright, dragViaPlaywright, fillFormViaPlaywright, scrollIntoViewViaPlaywright, highlightViaPlaywright, setInputFilesViaPlaywright, armDialogViaPlaywright, armFileUploadViaPlaywright } from './actions/interaction.js';
6
+ import { pressKeyViaPlaywright } from './actions/keyboard.js';
7
+ import { navigateViaPlaywright, listPagesViaPlaywright, createPageViaPlaywright, closePageByTargetIdViaPlaywright, focusPageByTargetIdViaPlaywright, resizeViewportViaPlaywright } from './actions/navigation.js';
8
+ import { waitForViaPlaywright } from './actions/wait.js';
9
+ import { evaluateViaPlaywright, evaluateInAllFramesViaPlaywright } from './actions/evaluate.js';
10
+ import { downloadViaPlaywright, waitForDownloadViaPlaywright } from './actions/download.js';
11
+ import { emulateMediaViaPlaywright, setDeviceViaPlaywright, setExtraHTTPHeadersViaPlaywright, setGeolocationViaPlaywright, setHttpCredentialsViaPlaywright, setLocaleViaPlaywright, setOfflineViaPlaywright, setTimezoneViaPlaywright } from './actions/emulation.js';
12
+ import { takeScreenshotViaPlaywright, screenshotWithLabelsViaPlaywright } from './capture/screenshot.js';
13
+ import { pdfViaPlaywright } from './capture/pdf.js';
14
+ import { traceStartViaPlaywright, traceStopViaPlaywright } from './capture/trace.js';
15
+ import { responseBodyViaPlaywright } from './capture/response.js';
16
+ import { getConsoleMessagesViaPlaywright, getPageErrorsViaPlaywright, getNetworkRequestsViaPlaywright } from './capture/activity.js';
17
+ import { cookiesGetViaPlaywright, cookiesSetViaPlaywright, cookiesClearViaPlaywright, storageGetViaPlaywright, storageSetViaPlaywright, storageClearViaPlaywright } from './storage/index.js';
18
+ /**
19
+ * Represents a single browser page/tab with ref-based automation.
20
+ *
21
+ * The workflow is: **snapshot → read refs → act on refs**.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * const page = await browser.open('https://example.com');
26
+ *
27
+ * // 1. Take a snapshot to get refs
28
+ * const { snapshot, refs } = await page.snapshot();
29
+ * // snapshot: AI-readable text tree
30
+ * // refs: { "e1": { role: "link", name: "More info" }, ... }
31
+ *
32
+ * // 2. Act on refs
33
+ * await page.click('e1');
34
+ * await page.type('e3', 'hello');
35
+ * ```
36
+ */
37
+ export class CrawlPage {
38
+ cdpUrl;
39
+ targetId;
40
+ ssrfPolicy;
41
+ /** @internal */
42
+ constructor(cdpUrl, targetId, ssrfPolicy) {
43
+ this.cdpUrl = cdpUrl;
44
+ this.targetId = targetId;
45
+ this.ssrfPolicy = ssrfPolicy;
46
+ }
47
+ /** The CDP target ID for this page. Use this to identify the page in multi-tab scenarios. */
48
+ get id() {
49
+ return this.targetId;
50
+ }
51
+ // ── Snapshot ──────────────────────────────────────────────────
52
+ /**
53
+ * Take an AI-readable snapshot of the page.
54
+ *
55
+ * Returns a text tree with numbered refs (`e1`, `e2`, ...) that map to
56
+ * interactive elements. Use these refs with actions like `click()` and `type()`.
57
+ *
58
+ * @param opts - Snapshot options (mode, filtering, depth limits)
59
+ * @returns Snapshot text, ref map, and statistics
60
+ *
61
+ * @example
62
+ * ```ts
63
+ * // Default snapshot (aria mode)
64
+ * const { snapshot, refs } = await page.snapshot();
65
+ *
66
+ * // Interactive elements only, compact
67
+ * const result = await page.snapshot({ interactive: true, compact: true });
68
+ *
69
+ * // Role-based mode (uses getByRole resolution)
70
+ * const result = await page.snapshot({ mode: 'role' });
71
+ * ```
72
+ */
73
+ async snapshot(opts) {
74
+ if (opts?.mode === 'role') {
75
+ return snapshotRole({
76
+ cdpUrl: this.cdpUrl,
77
+ targetId: this.targetId,
78
+ selector: opts.selector,
79
+ frameSelector: opts.frameSelector,
80
+ refsMode: opts.refsMode,
81
+ timeoutMs: opts.timeoutMs,
82
+ options: {
83
+ interactive: opts.interactive,
84
+ compact: opts.compact,
85
+ maxDepth: opts.maxDepth,
86
+ },
87
+ });
88
+ }
89
+ if (opts?.selector || opts?.frameSelector) {
90
+ throw new Error('selector and frameSelector are only supported in role mode. Use { mode: "role" } or omit these options.');
91
+ }
92
+ return snapshotAi({
93
+ cdpUrl: this.cdpUrl,
94
+ targetId: this.targetId,
95
+ maxChars: opts?.maxChars,
96
+ options: {
97
+ interactive: opts?.interactive,
98
+ compact: opts?.compact,
99
+ maxDepth: opts?.maxDepth,
100
+ },
101
+ });
102
+ }
103
+ /**
104
+ * Take a raw ARIA accessibility tree snapshot via CDP.
105
+ *
106
+ * Unlike `snapshot()`, this returns structured node data rather than
107
+ * an AI-readable text tree. Useful for programmatic accessibility analysis.
108
+ *
109
+ * @param opts - Options (limit: max nodes to return, default 500)
110
+ * @returns Array of accessibility tree nodes
111
+ */
112
+ async ariaSnapshot(opts) {
113
+ return snapshotAria({ cdpUrl: this.cdpUrl, targetId: this.targetId, limit: opts?.limit });
114
+ }
115
+ // ── Interactions ─────────────────────────────────────────────
116
+ /**
117
+ * Click an element by ref.
118
+ *
119
+ * @param ref - Ref ID from a snapshot (e.g. `'e1'`)
120
+ * @param opts - Click options (double-click, button, modifiers)
121
+ *
122
+ * @example
123
+ * ```ts
124
+ * await page.click('e1');
125
+ * await page.click('e2', { doubleClick: true });
126
+ * await page.click('e3', { button: 'right' });
127
+ * await page.click('e4', { modifiers: ['Control'] });
128
+ * ```
129
+ */
130
+ async click(ref, opts) {
131
+ return clickViaPlaywright({
132
+ cdpUrl: this.cdpUrl,
133
+ targetId: this.targetId,
134
+ ref,
135
+ doubleClick: opts?.doubleClick,
136
+ button: opts?.button,
137
+ modifiers: opts?.modifiers,
138
+ timeoutMs: opts?.timeoutMs,
139
+ });
140
+ }
141
+ /**
142
+ * Type text into an input element by ref.
143
+ *
144
+ * By default, uses Playwright's `fill()` for instant input. Use `slowly: true`
145
+ * to simulate real keystroke typing with a 75ms delay per character.
146
+ *
147
+ * @param ref - Ref ID of the input element (e.g. `'e3'`)
148
+ * @param text - Text to type
149
+ * @param opts - Type options (submit, slowly)
150
+ *
151
+ * @example
152
+ * ```ts
153
+ * await page.type('e3', 'hello world');
154
+ * await page.type('e3', 'slow typing', { slowly: true });
155
+ * await page.type('e3', 'search query', { submit: true }); // press Enter after
156
+ * ```
157
+ */
158
+ async type(ref, text, opts) {
159
+ return typeViaPlaywright({
160
+ cdpUrl: this.cdpUrl,
161
+ targetId: this.targetId,
162
+ ref,
163
+ text,
164
+ submit: opts?.submit,
165
+ slowly: opts?.slowly,
166
+ timeoutMs: opts?.timeoutMs,
167
+ });
168
+ }
169
+ /**
170
+ * Hover over an element by ref.
171
+ *
172
+ * @param ref - Ref ID from a snapshot
173
+ * @param opts - Timeout options
174
+ */
175
+ async hover(ref, opts) {
176
+ return hoverViaPlaywright({
177
+ cdpUrl: this.cdpUrl,
178
+ targetId: this.targetId,
179
+ ref,
180
+ timeoutMs: opts?.timeoutMs,
181
+ });
182
+ }
183
+ /**
184
+ * Select option(s) in a `<select>` dropdown by ref.
185
+ *
186
+ * @param ref - Ref ID of the select element
187
+ * @param values - One or more option labels/values to select
188
+ *
189
+ * @example
190
+ * ```ts
191
+ * await page.select('e5', 'Option A');
192
+ * await page.select('e5', 'Option A', 'Option B'); // multi-select
193
+ * ```
194
+ */
195
+ async select(ref, ...values) {
196
+ return selectOptionViaPlaywright({
197
+ cdpUrl: this.cdpUrl,
198
+ targetId: this.targetId,
199
+ ref,
200
+ values,
201
+ });
202
+ }
203
+ /**
204
+ * Drag one element to another.
205
+ *
206
+ * @param startRef - Ref ID of the element to drag
207
+ * @param endRef - Ref ID of the drop target
208
+ * @param opts - Timeout options
209
+ */
210
+ async drag(startRef, endRef, opts) {
211
+ return dragViaPlaywright({
212
+ cdpUrl: this.cdpUrl,
213
+ targetId: this.targetId,
214
+ startRef,
215
+ endRef,
216
+ timeoutMs: opts?.timeoutMs,
217
+ });
218
+ }
219
+ /**
220
+ * Fill multiple form fields at once.
221
+ *
222
+ * Supports text inputs, checkboxes, and radio buttons.
223
+ *
224
+ * @param fields - Array of form fields to fill
225
+ *
226
+ * @example
227
+ * ```ts
228
+ * await page.fill([
229
+ * { ref: 'e2', type: 'text', value: 'Jane Doe' },
230
+ * { ref: 'e4', type: 'text', value: 'jane@example.com' },
231
+ * { ref: 'e6', type: 'checkbox', value: true },
232
+ * ]);
233
+ * ```
234
+ */
235
+ async fill(fields) {
236
+ return fillFormViaPlaywright({
237
+ cdpUrl: this.cdpUrl,
238
+ targetId: this.targetId,
239
+ fields,
240
+ });
241
+ }
242
+ /**
243
+ * Scroll an element into the visible viewport.
244
+ *
245
+ * @param ref - Ref ID of the element to scroll to
246
+ * @param opts - Timeout options
247
+ */
248
+ async scrollIntoView(ref, opts) {
249
+ return scrollIntoViewViaPlaywright({
250
+ cdpUrl: this.cdpUrl,
251
+ targetId: this.targetId,
252
+ ref,
253
+ timeoutMs: opts?.timeoutMs,
254
+ });
255
+ }
256
+ /**
257
+ * Highlight an element in the browser (Playwright built-in highlight).
258
+ *
259
+ * @param ref - Ref ID of the element to highlight
260
+ */
261
+ async highlight(ref) {
262
+ return highlightViaPlaywright({
263
+ cdpUrl: this.cdpUrl,
264
+ targetId: this.targetId,
265
+ ref,
266
+ });
267
+ }
268
+ /**
269
+ * Set files on an `<input type="file">` element.
270
+ *
271
+ * @param ref - Ref ID of the file input element
272
+ * @param paths - Array of file paths to upload
273
+ */
274
+ async uploadFile(ref, paths) {
275
+ return setInputFilesViaPlaywright({
276
+ cdpUrl: this.cdpUrl,
277
+ targetId: this.targetId,
278
+ ref,
279
+ paths,
280
+ });
281
+ }
282
+ /**
283
+ * Arm a one-shot dialog handler (alert, confirm, prompt).
284
+ *
285
+ * Returns a promise — store it (don't await), trigger the dialog, then await it.
286
+ *
287
+ * @param opts - Dialog options (accept/dismiss, prompt text, timeout)
288
+ *
289
+ * @example
290
+ * ```ts
291
+ * const dialogDone = page.armDialog({ accept: true }); // don't await here
292
+ * await page.click('e5'); // triggers confirm()
293
+ * await dialogDone; // wait for dialog to be handled
294
+ * ```
295
+ */
296
+ async armDialog(opts) {
297
+ return armDialogViaPlaywright({
298
+ cdpUrl: this.cdpUrl,
299
+ targetId: this.targetId,
300
+ accept: opts.accept,
301
+ promptText: opts.promptText,
302
+ timeoutMs: opts.timeoutMs,
303
+ });
304
+ }
305
+ /**
306
+ * Arm a one-shot file chooser handler.
307
+ *
308
+ * Returns a promise — store it (don't await), trigger the file picker, then await it.
309
+ *
310
+ * @param paths - File paths to set when the chooser appears (empty to clear)
311
+ * @param opts - Timeout options
312
+ *
313
+ * @example
314
+ * ```ts
315
+ * const uploadDone = page.armFileUpload(['/path/to/file.pdf']); // don't await here
316
+ * await page.click('e3'); // triggers file picker
317
+ * await uploadDone; // wait for files to be set
318
+ * ```
319
+ */
320
+ async armFileUpload(paths, opts) {
321
+ return armFileUploadViaPlaywright({
322
+ cdpUrl: this.cdpUrl,
323
+ targetId: this.targetId,
324
+ paths,
325
+ timeoutMs: opts?.timeoutMs,
326
+ });
327
+ }
328
+ // ── Keyboard ─────────────────────────────────────────────────
329
+ /**
330
+ * Press a keyboard key or key combination.
331
+ *
332
+ * Uses Playwright's key names. Supports combinations with `+`.
333
+ *
334
+ * @param key - Key to press (e.g. `'Enter'`, `'Tab'`, `'Control+a'`, `'Meta+c'`)
335
+ * @param opts - Options (delayMs: hold time between keydown and keyup)
336
+ *
337
+ * @example
338
+ * ```ts
339
+ * await page.press('Enter');
340
+ * await page.press('Control+a');
341
+ * await page.press('Meta+Shift+p');
342
+ * ```
343
+ */
344
+ async press(key, opts) {
345
+ return pressKeyViaPlaywright({
346
+ cdpUrl: this.cdpUrl,
347
+ targetId: this.targetId,
348
+ key,
349
+ delayMs: opts?.delayMs,
350
+ });
351
+ }
352
+ // ── Navigation ───────────────────────────────────────────────
353
+ /**
354
+ * Get the current URL of the page.
355
+ */
356
+ async url() {
357
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this.targetId });
358
+ return page.url();
359
+ }
360
+ /**
361
+ * Get the page title.
362
+ */
363
+ async title() {
364
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this.targetId });
365
+ return page.title();
366
+ }
367
+ /**
368
+ * Navigate to a URL.
369
+ *
370
+ * @param url - The URL to navigate to
371
+ * @param opts - Timeout options
372
+ * @returns The final URL after navigation (may differ due to redirects)
373
+ */
374
+ async goto(url, opts) {
375
+ return navigateViaPlaywright({
376
+ cdpUrl: this.cdpUrl,
377
+ targetId: this.targetId,
378
+ url,
379
+ timeoutMs: opts?.timeoutMs,
380
+ ssrfPolicy: this.ssrfPolicy,
381
+ });
382
+ }
383
+ /**
384
+ * Reload the current page.
385
+ *
386
+ * @param opts - Timeout options
387
+ */
388
+ async reload(opts) {
389
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this.targetId });
390
+ ensurePageState(page);
391
+ await page.reload({ timeout: normalizeTimeoutMs(opts?.timeoutMs, 20000) });
392
+ }
393
+ /**
394
+ * Navigate back in browser history.
395
+ *
396
+ * @param opts - Timeout options
397
+ */
398
+ async goBack(opts) {
399
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this.targetId });
400
+ ensurePageState(page);
401
+ await page.goBack({ timeout: normalizeTimeoutMs(opts?.timeoutMs, 20000) });
402
+ }
403
+ /**
404
+ * Navigate forward in browser history.
405
+ *
406
+ * @param opts - Timeout options
407
+ */
408
+ async goForward(opts) {
409
+ const page = await getPageForTargetId({ cdpUrl: this.cdpUrl, targetId: this.targetId });
410
+ ensurePageState(page);
411
+ await page.goForward({ timeout: normalizeTimeoutMs(opts?.timeoutMs, 20000) });
412
+ }
413
+ // ── Wait ─────────────────────────────────────────────────────
414
+ /**
415
+ * Wait for various conditions on the page.
416
+ *
417
+ * Multiple conditions can be specified — they are checked in order.
418
+ *
419
+ * @param opts - Wait conditions (text, URL, load state, selector, etc.)
420
+ *
421
+ * @example
422
+ * ```ts
423
+ * await page.waitFor({ loadState: 'networkidle' });
424
+ * await page.waitFor({ text: 'Welcome back' });
425
+ * await page.waitFor({ url: '**\/dashboard' });
426
+ * await page.waitFor({ timeMs: 1000 }); // sleep
427
+ * ```
428
+ */
429
+ async waitFor(opts) {
430
+ return waitForViaPlaywright({
431
+ cdpUrl: this.cdpUrl,
432
+ targetId: this.targetId,
433
+ ...opts,
434
+ });
435
+ }
436
+ // ── Evaluate ─────────────────────────────────────────────────
437
+ /**
438
+ * Run JavaScript in the browser page context.
439
+ *
440
+ * The function string is evaluated in the browser's sandbox, not in Node.js.
441
+ * Pass a `ref` to receive the element as the first argument.
442
+ *
443
+ * @param fn - JavaScript function body as a string
444
+ * @param opts - Options (ref: scope evaluation to a specific element)
445
+ * @returns The return value of the evaluated function
446
+ *
447
+ * @example
448
+ * ```ts
449
+ * const title = await page.evaluate('() => document.title');
450
+ * const text = await page.evaluate('(el) => el.textContent', { ref: 'e1' });
451
+ * const count = await page.evaluate('() => document.querySelectorAll("img").length');
452
+ * ```
453
+ */
454
+ async evaluate(fn, opts) {
455
+ return evaluateViaPlaywright({
456
+ cdpUrl: this.cdpUrl,
457
+ targetId: this.targetId,
458
+ fn,
459
+ ref: opts?.ref,
460
+ timeoutMs: opts?.timeoutMs,
461
+ signal: opts?.signal,
462
+ });
463
+ }
464
+ /**
465
+ * Run JavaScript in ALL frames on the page (including cross-origin iframes).
466
+ *
467
+ * Playwright can access cross-origin frames via CDP, bypassing the same-origin policy.
468
+ * This is essential for filling payment iframes (Stripe, etc.).
469
+ *
470
+ * @param fn - JavaScript function body as a string
471
+ * @returns Array of results from each frame where evaluation succeeded
472
+ *
473
+ * @example
474
+ * ```ts
475
+ * const results = await page.evaluateInAllFrames(`() => {
476
+ * const el = document.querySelector('input[name="cardnumber"]');
477
+ * return el ? 'found' : null;
478
+ * }`);
479
+ * ```
480
+ */
481
+ async evaluateInAllFrames(fn) {
482
+ return evaluateInAllFramesViaPlaywright({
483
+ cdpUrl: this.cdpUrl,
484
+ targetId: this.targetId,
485
+ fn,
486
+ });
487
+ }
488
+ // ── Capture ──────────────────────────────────────────────────
489
+ /**
490
+ * Take a screenshot of the page or a specific element.
491
+ *
492
+ * @param opts - Screenshot options (fullPage, ref, element, type)
493
+ * @returns PNG or JPEG image as a Buffer
494
+ *
495
+ * @example
496
+ * ```ts
497
+ * const screenshot = await page.screenshot();
498
+ * const fullPage = await page.screenshot({ fullPage: true });
499
+ * const element = await page.screenshot({ ref: 'e1' });
500
+ * ```
501
+ */
502
+ async screenshot(opts) {
503
+ const result = await takeScreenshotViaPlaywright({
504
+ cdpUrl: this.cdpUrl,
505
+ targetId: this.targetId,
506
+ fullPage: opts?.fullPage,
507
+ ref: opts?.ref,
508
+ element: opts?.element,
509
+ type: opts?.type,
510
+ });
511
+ return result.buffer;
512
+ }
513
+ /**
514
+ * Export the page as a PDF.
515
+ *
516
+ * Only works in headless mode.
517
+ *
518
+ * @returns PDF document as a Buffer
519
+ */
520
+ async pdf() {
521
+ const result = await pdfViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId });
522
+ return result.buffer;
523
+ }
524
+ /**
525
+ * Take a screenshot with numbered labels overlaid on referenced elements.
526
+ *
527
+ * Useful for visual debugging — each ref gets a numbered badge and border.
528
+ *
529
+ * @param refs - Array of ref IDs to label
530
+ * @param opts - Options (maxLabels: limit, type: image format)
531
+ * @returns Screenshot buffer, label positions, and any skipped refs
532
+ *
533
+ * @example
534
+ * ```ts
535
+ * const { buffer, labels, skipped } = await page.screenshotWithLabels(['e1', 'e2', 'e3']);
536
+ * fs.writeFileSync('labeled.png', buffer);
537
+ * ```
538
+ */
539
+ async screenshotWithLabels(refs, opts) {
540
+ return screenshotWithLabelsViaPlaywright({
541
+ cdpUrl: this.cdpUrl,
542
+ targetId: this.targetId,
543
+ refs,
544
+ maxLabels: opts?.maxLabels,
545
+ type: opts?.type,
546
+ });
547
+ }
548
+ /**
549
+ * Start recording a Playwright trace.
550
+ *
551
+ * Traces capture screenshots, DOM snapshots, and network activity.
552
+ * Stop with `traceStop()` to save the trace file.
553
+ *
554
+ * @param opts - Trace options (screenshots, snapshots, sources)
555
+ */
556
+ async traceStart(opts) {
557
+ return traceStartViaPlaywright({
558
+ cdpUrl: this.cdpUrl,
559
+ targetId: this.targetId,
560
+ screenshots: opts?.screenshots,
561
+ snapshots: opts?.snapshots,
562
+ sources: opts?.sources,
563
+ });
564
+ }
565
+ /**
566
+ * Stop recording a trace and save it to a file.
567
+ *
568
+ * @param path - File path to save the trace (e.g. `'trace.zip'`)
569
+ * @param opts - Options (allowedOutputRoots: constrain output to specific directories)
570
+ */
571
+ async traceStop(path, opts) {
572
+ return traceStopViaPlaywright({
573
+ cdpUrl: this.cdpUrl,
574
+ targetId: this.targetId,
575
+ path,
576
+ allowedOutputRoots: opts?.allowedOutputRoots,
577
+ });
578
+ }
579
+ /**
580
+ * Wait for a network response matching a URL pattern and return its body.
581
+ *
582
+ * @param url - URL string or pattern to match
583
+ * @param opts - Options (timeoutMs, maxChars)
584
+ * @returns Response body, status, headers, and truncation info
585
+ *
586
+ * @example
587
+ * ```ts
588
+ * const resp = await page.responseBody('/api/data');
589
+ * console.log(resp.status, resp.body);
590
+ * ```
591
+ */
592
+ async responseBody(url, opts) {
593
+ return responseBodyViaPlaywright({
594
+ cdpUrl: this.cdpUrl,
595
+ targetId: this.targetId,
596
+ url,
597
+ timeoutMs: opts?.timeoutMs,
598
+ maxChars: opts?.maxChars,
599
+ });
600
+ }
601
+ /**
602
+ * Get console messages captured from the page.
603
+ *
604
+ * Messages are buffered automatically. Use `level` to filter by minimum severity.
605
+ *
606
+ * @param opts - Filter options (level, clear)
607
+ * @returns Array of captured console messages
608
+ */
609
+ async consoleLogs(opts) {
610
+ return getConsoleMessagesViaPlaywright({
611
+ cdpUrl: this.cdpUrl,
612
+ targetId: this.targetId,
613
+ level: opts?.level,
614
+ clear: opts?.clear,
615
+ });
616
+ }
617
+ /**
618
+ * Get uncaught errors from the page.
619
+ *
620
+ * @param opts - Options (clear: reset the error buffer after reading)
621
+ * @returns Array of captured page errors
622
+ */
623
+ async pageErrors(opts) {
624
+ const result = await getPageErrorsViaPlaywright({
625
+ cdpUrl: this.cdpUrl,
626
+ targetId: this.targetId,
627
+ clear: opts?.clear,
628
+ });
629
+ return result.errors;
630
+ }
631
+ /**
632
+ * Get network requests captured from the page.
633
+ *
634
+ * @param opts - Options (filter: URL substring match, clear: reset the buffer)
635
+ * @returns Array of captured network requests
636
+ *
637
+ * @example
638
+ * ```ts
639
+ * const all = await page.networkRequests();
640
+ * const apiCalls = await page.networkRequests({ filter: '/api/' });
641
+ * const fresh = await page.networkRequests({ clear: true }); // read and clear
642
+ * ```
643
+ */
644
+ async networkRequests(opts) {
645
+ const result = await getNetworkRequestsViaPlaywright({
646
+ cdpUrl: this.cdpUrl,
647
+ targetId: this.targetId,
648
+ filter: opts?.filter,
649
+ clear: opts?.clear,
650
+ });
651
+ return result.requests;
652
+ }
653
+ // ── Viewport ─────────────────────────────────────────────────
654
+ /**
655
+ * Resize the browser viewport.
656
+ *
657
+ * @param width - Viewport width in pixels
658
+ * @param height - Viewport height in pixels
659
+ */
660
+ async resize(width, height) {
661
+ return resizeViewportViaPlaywright({
662
+ cdpUrl: this.cdpUrl,
663
+ targetId: this.targetId,
664
+ width,
665
+ height,
666
+ });
667
+ }
668
+ // ── Storage ──────────────────────────────────────────────────
669
+ /**
670
+ * Get all cookies for the current browser context.
671
+ *
672
+ * @returns Array of cookie objects
673
+ */
674
+ async cookies() {
675
+ const result = await cookiesGetViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId });
676
+ return result.cookies;
677
+ }
678
+ /**
679
+ * Set a cookie in the browser context.
680
+ *
681
+ * @param cookie - Cookie data (must include `name`, `value`, and either `url` or `domain`+`path`)
682
+ *
683
+ * @example
684
+ * ```ts
685
+ * await page.setCookie({
686
+ * name: 'token',
687
+ * value: 'abc123',
688
+ * url: 'https://example.com',
689
+ * });
690
+ * ```
691
+ */
692
+ async setCookie(cookie) {
693
+ return cookiesSetViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId, cookie });
694
+ }
695
+ /** Clear all cookies in the browser context. */
696
+ async clearCookies() {
697
+ return cookiesClearViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId });
698
+ }
699
+ /**
700
+ * Get values from localStorage or sessionStorage.
701
+ *
702
+ * @param kind - `'local'` for localStorage, `'session'` for sessionStorage
703
+ * @param key - Optional specific key to retrieve (returns all if omitted)
704
+ * @returns Key-value map of storage entries
705
+ */
706
+ async storageGet(kind, key) {
707
+ const result = await storageGetViaPlaywright({
708
+ cdpUrl: this.cdpUrl, targetId: this.targetId, kind, key,
709
+ });
710
+ return result.values;
711
+ }
712
+ /**
713
+ * Set a value in localStorage or sessionStorage.
714
+ *
715
+ * @param kind - `'local'` for localStorage, `'session'` for sessionStorage
716
+ * @param key - Storage key
717
+ * @param value - Storage value
718
+ */
719
+ async storageSet(kind, key, value) {
720
+ return storageSetViaPlaywright({
721
+ cdpUrl: this.cdpUrl, targetId: this.targetId, kind, key, value,
722
+ });
723
+ }
724
+ /**
725
+ * Clear all entries in localStorage or sessionStorage.
726
+ *
727
+ * @param kind - `'local'` for localStorage, `'session'` for sessionStorage
728
+ */
729
+ async storageClear(kind) {
730
+ return storageClearViaPlaywright({
731
+ cdpUrl: this.cdpUrl, targetId: this.targetId, kind,
732
+ });
733
+ }
734
+ // ── Downloads ───────────────────────────────────────────────
735
+ /**
736
+ * Click a ref and save the resulting file download.
737
+ *
738
+ * @param ref - Ref ID of the element that triggers the download
739
+ * @param path - Local file path to save the download to
740
+ * @param opts - Timeout options
741
+ * @returns Download result with URL, suggested filename, and saved path
742
+ *
743
+ * @example
744
+ * ```ts
745
+ * const result = await page.download('e7', '/tmp/report.pdf');
746
+ * console.log(result.suggestedFilename); // 'report.pdf'
747
+ * ```
748
+ */
749
+ async download(ref, path, opts) {
750
+ return downloadViaPlaywright({
751
+ cdpUrl: this.cdpUrl,
752
+ targetId: this.targetId,
753
+ ref,
754
+ path,
755
+ timeoutMs: opts?.timeoutMs,
756
+ allowedOutputRoots: opts?.allowedOutputRoots,
757
+ });
758
+ }
759
+ /**
760
+ * Wait for the next download event (without clicking).
761
+ *
762
+ * Returns a promise — store it (don't await), trigger the download, then await it.
763
+ *
764
+ * @param opts - Options (path: save location, timeoutMs)
765
+ * @returns Download result with URL, suggested filename, and saved path
766
+ */
767
+ async waitForDownload(opts) {
768
+ return waitForDownloadViaPlaywright({
769
+ cdpUrl: this.cdpUrl,
770
+ targetId: this.targetId,
771
+ path: opts?.path,
772
+ timeoutMs: opts?.timeoutMs,
773
+ allowedOutputRoots: opts?.allowedOutputRoots,
774
+ });
775
+ }
776
+ // ── Emulation ───────────────────────────────────────────────
777
+ /**
778
+ * Set the browser to offline or online mode.
779
+ *
780
+ * @param offline - `true` to go offline, `false` to go online
781
+ */
782
+ async setOffline(offline) {
783
+ return setOfflineViaPlaywright({
784
+ cdpUrl: this.cdpUrl,
785
+ targetId: this.targetId,
786
+ offline,
787
+ });
788
+ }
789
+ /**
790
+ * Set extra HTTP headers for all requests.
791
+ *
792
+ * @param headers - Headers to add to every request
793
+ *
794
+ * @example
795
+ * ```ts
796
+ * await page.setExtraHeaders({ 'X-Custom': 'value' });
797
+ * ```
798
+ */
799
+ async setExtraHeaders(headers) {
800
+ return setExtraHTTPHeadersViaPlaywright({
801
+ cdpUrl: this.cdpUrl,
802
+ targetId: this.targetId,
803
+ headers,
804
+ });
805
+ }
806
+ /**
807
+ * Set HTTP authentication credentials.
808
+ *
809
+ * @param opts - Credentials (username, password) or `{ clear: true }` to remove
810
+ */
811
+ async setHttpCredentials(opts) {
812
+ return setHttpCredentialsViaPlaywright({
813
+ cdpUrl: this.cdpUrl,
814
+ targetId: this.targetId,
815
+ username: opts.username,
816
+ password: opts.password,
817
+ clear: opts.clear,
818
+ });
819
+ }
820
+ /**
821
+ * Emulate a geolocation.
822
+ *
823
+ * @param opts - Geolocation coordinates or `{ clear: true }` to clear
824
+ *
825
+ * @example
826
+ * ```ts
827
+ * await page.setGeolocation({ latitude: 48.8566, longitude: 2.3522 }); // Paris
828
+ * await page.setGeolocation({ clear: true }); // reset
829
+ * ```
830
+ */
831
+ async setGeolocation(opts) {
832
+ return setGeolocationViaPlaywright({
833
+ cdpUrl: this.cdpUrl,
834
+ targetId: this.targetId,
835
+ latitude: opts.latitude,
836
+ longitude: opts.longitude,
837
+ accuracy: opts.accuracy,
838
+ origin: opts.origin,
839
+ clear: opts.clear,
840
+ });
841
+ }
842
+ /**
843
+ * Emulate a preferred color scheme.
844
+ *
845
+ * @param opts - Color scheme options
846
+ *
847
+ * @example
848
+ * ```ts
849
+ * await page.emulateMedia({ colorScheme: 'dark' });
850
+ * ```
851
+ */
852
+ async emulateMedia(opts) {
853
+ return emulateMediaViaPlaywright({
854
+ cdpUrl: this.cdpUrl,
855
+ targetId: this.targetId,
856
+ colorScheme: opts.colorScheme,
857
+ });
858
+ }
859
+ /**
860
+ * Override the browser locale.
861
+ *
862
+ * @param locale - BCP-47 locale string (e.g. `'fr-FR'`, `'ja-JP'`)
863
+ */
864
+ async setLocale(locale) {
865
+ return setLocaleViaPlaywright({
866
+ cdpUrl: this.cdpUrl,
867
+ targetId: this.targetId,
868
+ locale,
869
+ });
870
+ }
871
+ /**
872
+ * Override the browser timezone.
873
+ *
874
+ * @param timezoneId - IANA timezone ID (e.g. `'America/New_York'`, `'Asia/Tokyo'`)
875
+ */
876
+ async setTimezone(timezoneId) {
877
+ return setTimezoneViaPlaywright({
878
+ cdpUrl: this.cdpUrl,
879
+ targetId: this.targetId,
880
+ timezoneId,
881
+ });
882
+ }
883
+ /**
884
+ * Emulate a specific device (viewport + user agent).
885
+ *
886
+ * @param name - Playwright device name (e.g. `'iPhone 13'`, `'Pixel 5'`)
887
+ *
888
+ * @example
889
+ * ```ts
890
+ * await page.setDevice('iPhone 13');
891
+ * ```
892
+ */
893
+ async setDevice(name) {
894
+ return setDeviceViaPlaywright({
895
+ cdpUrl: this.cdpUrl,
896
+ targetId: this.targetId,
897
+ name,
898
+ });
899
+ }
900
+ }
901
+ /**
902
+ * Main entry point for browserclaw.
903
+ *
904
+ * Launch or connect to a browser, then open pages and automate them
905
+ * using the snapshot + ref pattern.
906
+ *
907
+ * @example
908
+ * ```ts
909
+ * import { BrowserClaw } from 'browserclaw';
910
+ *
911
+ * const browser = await BrowserClaw.launch({ headless: false });
912
+ * const page = await browser.open('https://example.com');
913
+ *
914
+ * const { snapshot, refs } = await page.snapshot();
915
+ * console.log(snapshot); // AI-readable page tree
916
+ * console.log(refs); // { "e1": { role: "link", name: "More info" }, ... }
917
+ *
918
+ * await page.click('e1');
919
+ * await browser.stop();
920
+ * ```
921
+ */
922
+ export class BrowserClaw {
923
+ cdpUrl;
924
+ ssrfPolicy;
925
+ chrome;
926
+ constructor(cdpUrl, chrome, ssrfPolicy) {
927
+ this.cdpUrl = cdpUrl;
928
+ this.chrome = chrome;
929
+ this.ssrfPolicy = ssrfPolicy;
930
+ }
931
+ /**
932
+ * Launch a new Chrome instance and connect to it.
933
+ *
934
+ * Automatically detects Chrome, Brave, Edge, or Chromium on the system.
935
+ * Creates a dedicated browser profile to avoid conflicts with your daily browser.
936
+ *
937
+ * @param opts - Launch options (headless, executablePath, cdpPort, etc.)
938
+ * @returns A connected BrowserClaw instance
939
+ *
940
+ * @example
941
+ * ```ts
942
+ * // Default: visible Chrome window
943
+ * const browser = await BrowserClaw.launch();
944
+ *
945
+ * // Headless mode
946
+ * const browser = await BrowserClaw.launch({ headless: true });
947
+ *
948
+ * // Specific browser
949
+ * const browser = await BrowserClaw.launch({
950
+ * executablePath: '/usr/bin/google-chrome',
951
+ * });
952
+ * ```
953
+ */
954
+ static async launch(opts = {}) {
955
+ const chrome = await launchChrome(opts);
956
+ const cdpUrl = `http://127.0.0.1:${chrome.cdpPort}`;
957
+ const ssrfPolicy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
958
+ return new BrowserClaw(cdpUrl, chrome, ssrfPolicy);
959
+ }
960
+ /**
961
+ * Connect to an already-running Chrome instance via its CDP endpoint.
962
+ *
963
+ * The Chrome instance must have been started with `--remote-debugging-port`.
964
+ *
965
+ * @param cdpUrl - CDP endpoint URL (e.g. `'http://localhost:9222'`)
966
+ * @returns A connected BrowserClaw instance
967
+ *
968
+ * @example
969
+ * ```ts
970
+ * // Chrome started with: chrome --remote-debugging-port=9222
971
+ * const browser = await BrowserClaw.connect('http://localhost:9222');
972
+ * ```
973
+ */
974
+ static async connect(cdpUrl, opts) {
975
+ if (!await isChromeReachable(cdpUrl, 3000, opts?.authToken)) {
976
+ throw new Error(`Cannot connect to Chrome at ${cdpUrl}. Is Chrome running with --remote-debugging-port?`);
977
+ }
978
+ await connectBrowser(cdpUrl, opts?.authToken);
979
+ const ssrfPolicy = opts?.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts?.ssrfPolicy;
980
+ return new BrowserClaw(cdpUrl, null, ssrfPolicy);
981
+ }
982
+ /**
983
+ * Open a URL in a new tab and return the page handle.
984
+ *
985
+ * @param url - URL to navigate to
986
+ * @returns A CrawlPage for the new tab
987
+ *
988
+ * @example
989
+ * ```ts
990
+ * const page = await browser.open('https://example.com');
991
+ * const { snapshot, refs } = await page.snapshot();
992
+ * ```
993
+ */
994
+ async open(url) {
995
+ const tab = await createPageViaPlaywright({ cdpUrl: this.cdpUrl, url, ssrfPolicy: this.ssrfPolicy });
996
+ return new CrawlPage(this.cdpUrl, tab.targetId, this.ssrfPolicy);
997
+ }
998
+ /**
999
+ * Get a CrawlPage handle for the currently active tab.
1000
+ *
1001
+ * @returns CrawlPage for the first/active page
1002
+ */
1003
+ async currentPage() {
1004
+ const { browser } = await connectBrowser(this.cdpUrl);
1005
+ const pages = await getAllPages(browser);
1006
+ if (!pages.length)
1007
+ throw new Error('No pages available. Use browser.open(url) to create a tab.');
1008
+ const tid = await pageTargetId(pages[0]).catch(() => null);
1009
+ if (!tid)
1010
+ throw new Error('Failed to get targetId for the current page.');
1011
+ return new CrawlPage(this.cdpUrl, tid, this.ssrfPolicy);
1012
+ }
1013
+ /**
1014
+ * List all open tabs.
1015
+ *
1016
+ * @returns Array of tab information objects
1017
+ */
1018
+ async tabs() {
1019
+ return listPagesViaPlaywright({ cdpUrl: this.cdpUrl });
1020
+ }
1021
+ /**
1022
+ * Bring a tab to the foreground.
1023
+ *
1024
+ * @param targetId - CDP target ID of the tab (from `tabs()` or `page.id`)
1025
+ */
1026
+ async focus(targetId) {
1027
+ return focusPageByTargetIdViaPlaywright({ cdpUrl: this.cdpUrl, targetId });
1028
+ }
1029
+ /**
1030
+ * Close a tab.
1031
+ *
1032
+ * @param targetId - CDP target ID of the tab to close
1033
+ */
1034
+ async close(targetId) {
1035
+ return closePageByTargetIdViaPlaywright({ cdpUrl: this.cdpUrl, targetId });
1036
+ }
1037
+ /**
1038
+ * Get a CrawlPage handle for a specific tab by its target ID.
1039
+ *
1040
+ * Unlike `open()`, this doesn't create a new tab — it wraps an existing one.
1041
+ *
1042
+ * @param targetId - CDP target ID of the tab
1043
+ * @returns CrawlPage for the specified tab
1044
+ */
1045
+ page(targetId) {
1046
+ return new CrawlPage(this.cdpUrl, targetId, this.ssrfPolicy);
1047
+ }
1048
+ /** The CDP endpoint URL for this browser connection. */
1049
+ get url() {
1050
+ return this.cdpUrl;
1051
+ }
1052
+ /**
1053
+ * Stop the browser and clean up all resources.
1054
+ *
1055
+ * If the browser was launched by `BrowserClaw.launch()`, the Chrome process
1056
+ * will be terminated. If connected via `BrowserClaw.connect()`, only the
1057
+ * Playwright connection is closed.
1058
+ */
1059
+ async stop() {
1060
+ await disconnectBrowser();
1061
+ if (this.chrome) {
1062
+ await stopChrome(this.chrome);
1063
+ this.chrome = null;
1064
+ }
1065
+ }
1066
+ }