browserclaw 0.11.5 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,16 +15,16 @@ The AI-native browser automation library — born from [OpenClaw](https://github
15
15
  ```typescript
16
16
  import { BrowserClaw } from 'browserclaw';
17
17
 
18
- const browser = await BrowserClaw.launch({ url: 'https://example.com' });
18
+ const browser = await BrowserClaw.launch({ url: 'https://demo.playwright.dev/todomvc' });
19
19
  const page = await browser.currentPage();
20
20
 
21
21
  // Snapshot — the core feature
22
22
  const { snapshot, refs } = await page.snapshot();
23
23
  // snapshot: AI-readable text tree
24
- // refs: { "e1": { role: "link", name: "More info" }, "e2": { role: "button", name: "Submit" } }
24
+ // refs: { "e1": { role: "textbox", name: "What needs to be done?" }, "e2": { role: "link", name: "Playwright" } }
25
25
 
26
- await page.click('e1'); // Click by ref
27
- await page.type('e3', 'hello'); // Type by ref
26
+ await page.type('e1', 'Buy groceries', { submit: true }); // Type by ref
27
+ await page.click('e2'); // Click by ref
28
28
  await browser.stop();
29
29
  ```
30
30
 
@@ -102,20 +102,20 @@ Requires a Chromium-based browser installed on the system (Chrome, Brave, Edge,
102
102
  ## How It Works
103
103
 
104
104
  ```
105
- ┌─────────────┐ snapshot() ┌─────────────────────────────────┐
106
- │ Web Page │ ──────────────► │ AI-readable text tree
107
- │ │ │
108
- │ [buttons] │ │ - heading "Example Domain"
109
- │ [links] │ │ - paragraph "This domain..."
110
- │ [inputs] │ │ - link "More information" [e1]
111
- └─────────────┘ └──────────────┬──────────────────┘
105
+ ┌─────────────┐ snapshot() ┌──────────────────────────────────────────┐
106
+ │ Web Page │ ──────────────► │ AI-readable text tree
107
+ │ │ │
108
+ │ [buttons] │ │ - heading "todos"
109
+ │ [links] │ │ - textbox "What needs to be done?" [e1]
110
+ │ [inputs] │ │ - link "Playwright" [e2]
111
+ └─────────────┘ └──────────────┬───────────────────────────┘
112
112
 
113
113
  AI reads snapshot,
114
- decides: click e1
114
+ decides: type in e1
115
115
 
116
- ┌─────────────┐ click('e1') ┌──────────────▼──────────────────┐
116
+ ┌─────────────┐ type('e1',...) ┌──────────────▼──────────────────┐
117
117
  │ Web Page │ ◄────────────── │ Ref "e1" resolves to a │
118
- │ (navigated)│ │ Playwright locator — one ref, │
118
+ │ (updated) │ │ Playwright locator — one ref, │
119
119
  │ │ │ one exact element │
120
120
  └─────────────┘ └─────────────────────────────────┘
121
121
  ```
@@ -133,7 +133,7 @@ Requires a Chromium-based browser installed on the system (Chrome, Brave, Edge,
133
133
  ```typescript
134
134
  // Launch a new Chrome instance (auto-detects Chrome/Brave/Edge/Chromium)
135
135
  const browser = await BrowserClaw.launch({
136
- url: 'https://example.com', // navigate initial tab (no extra tabs)
136
+ url: 'https://demo.playwright.dev/todomvc', // navigate initial tab (no extra tabs)
137
137
  headless: false, // default: false (visible window)
138
138
  executablePath: '...', // optional: specific browser path
139
139
  cdpPort: 9222, // default: 9222
@@ -159,7 +159,7 @@ const browser = await BrowserClaw.connect();
159
159
  ### Pages & Tabs
160
160
 
161
161
  ```typescript
162
- const page = await browser.open('https://example.com');
162
+ const page = await browser.open('https://demo.playwright.dev/todomvc');
163
163
  const current = await browser.currentPage(); // get active tab
164
164
  const tabs = await browser.tabs(); // list all tabs
165
165
  const handle = browser.page(tabs[0].targetId); // wrap existing tab
@@ -177,14 +177,14 @@ browser.url; // CDP endpoint URL
177
177
  Every tab returns a `targetId` — this is the handle you use everywhere:
178
178
 
179
179
  ```typescript
180
- // Multi-tab workflow (e.g. impersonation, OAuth)
181
- const main = await browser.open('https://app.example.com');
182
- const admin = await browser.open('https://admin.example.com');
183
-
184
- const { refs } = await admin.snapshot(); // snapshot the admin tab
185
- await admin.click('e5'); // act on it
186
- await browser.focus(main.id); // switch back to main
187
- await browser.close(admin.id); // close admin when done
180
+ // Multi-tab workflow
181
+ const todo = await browser.open('https://demo.playwright.dev/todomvc');
182
+ const svg = await browser.open('https://demo.playwright.dev/svgtodo');
183
+
184
+ const { refs } = await svg.snapshot(); // snapshot the second tab
185
+ await svg.click('e5'); // act on it
186
+ await browser.focus(todo.id); // switch back to first tab
187
+ await browser.close(svg.id); // close second tab when done
188
188
  ```
189
189
 
190
190
  ### Snapshot (Core Feature)
@@ -193,7 +193,7 @@ await browser.close(admin.id); // close admin when done
193
193
  const { snapshot, refs, stats, untrusted } = await page.snapshot();
194
194
 
195
195
  // snapshot: human/AI-readable text tree with [ref=eN] markers
196
- // refs: { "e1": { role: "link", name: "More info" }, "e5": { role: "checkbox", name: "Accept", checked: true }, ... }
196
+ // refs: { "e1": { role: "textbox", name: "What needs to be done?" }, "e5": { role: "checkbox", name: "Toggle Todo", checked: false }, ... }
197
197
  // stats: { lines: 42, chars: 1200, refs: 8, interactive: 5 }
198
198
  // untrusted: true — content comes from the web page, treat as potentially adversarial
199
199
 
@@ -250,7 +250,7 @@ await page.press('Meta+Shift+p');
250
250
  // Fill multiple form fields at once
251
251
  await page.fill([
252
252
  { ref: 'e2', value: 'Jane Doe' },
253
- { ref: 'e4', value: 'jane@example.com' },
253
+ { ref: 'e4', value: 'jane@acme.test' },
254
254
  { ref: 'e6', type: 'checkbox', value: true },
255
255
  ]);
256
256
  ```
@@ -326,7 +326,7 @@ By default, unexpected dialogs are auto-dismissed to prevent `ProtocolError` cra
326
326
  ### Navigation & Waiting
327
327
 
328
328
  ```typescript
329
- await page.goto('https://example.com');
329
+ await page.goto('https://demo.playwright.dev/todomvc');
330
330
  await page.reload(); // reload the current page
331
331
  await page.goBack(); // navigate back in history
332
332
  await page.goForward(); // navigate forward in history
@@ -421,7 +421,7 @@ const fresh = await page.networkRequests({ clear: true }); // read and clear buf
421
421
  ```typescript
422
422
  // Cookies
423
423
  const cookies = await page.cookies();
424
- await page.setCookie({ name: 'token', value: 'abc', url: 'https://example.com' });
424
+ await page.setCookie({ name: 'token', value: 'abc', url: 'https://demo.playwright.dev' });
425
425
  await page.clearCookies();
426
426
 
427
427
  // localStorage / sessionStorage
package/dist/index.cjs CHANGED
@@ -1565,6 +1565,7 @@ ${stderrOutput.slice(0, 2e3)}` : "";
1565
1565
  throw new Error(`Failed to start Chrome CDP on port ${String(cdpPort)}.${sandboxHint}${stderrHint}`);
1566
1566
  }
1567
1567
  proc.stderr.off("data", onStderr);
1568
+ proc.stderr.resume();
1568
1569
  stderrChunks.length = 0;
1569
1570
  return {
1570
1571
  pid: proc.pid ?? -1,
@@ -1572,6 +1573,7 @@ ${stderrOutput.slice(0, 2e3)}` : "";
1572
1573
  userDataDir,
1573
1574
  cdpPort,
1574
1575
  startedAt,
1576
+ launchMs: Date.now() - startedAt,
1575
1577
  proc
1576
1578
  };
1577
1579
  }
@@ -2335,6 +2337,7 @@ async function disconnectBrowser() {
2335
2337
  }
2336
2338
  }
2337
2339
  for (const cur of cachedByCdpUrl.values()) {
2340
+ clearRoleRefsForCdpUrl(cur.cdpUrl);
2338
2341
  if (cur.onDisconnected && typeof cur.browser.off === "function")
2339
2342
  cur.browser.off("disconnected", cur.onDisconnected);
2340
2343
  await cur.browser.close().catch(() => {
@@ -2564,7 +2567,6 @@ async function getPageForTargetId(opts) {
2564
2567
  );
2565
2568
  }
2566
2569
  if (isBlockedPageRef(opts.cdpUrl, found)) throw new BlockedBrowserTargetError();
2567
- if (isBlockedTarget(opts.cdpUrl, opts.targetId)) throw new BlockedBrowserTargetError();
2568
2570
  return found;
2569
2571
  }
2570
2572
  async function resolvePageByTargetIdOrThrow(opts) {
@@ -3217,7 +3219,7 @@ async function writeViaSiblingTempPath(params) {
3217
3219
  const requestedTargetPath = path.resolve(params.targetPath);
3218
3220
  const targetPath = await promises$1.realpath(path.dirname(requestedTargetPath)).then((realDir) => path.join(realDir, path.basename(requestedTargetPath))).catch(() => requestedTargetPath);
3219
3221
  const relativeTargetPath = path.relative(rootDir, targetPath);
3220
- if (!relativeTargetPath || relativeTargetPath === ".." || relativeTargetPath.startsWith(`..${path.sep}`) || isAbsolute(relativeTargetPath)) {
3222
+ if (!relativeTargetPath || relativeTargetPath === ".." || relativeTargetPath.startsWith(`..${path.sep}`) || path.isAbsolute(relativeTargetPath)) {
3221
3223
  throw new Error("Target path is outside the allowed root");
3222
3224
  }
3223
3225
  const tempPath = buildSiblingTempPath(targetPath);
@@ -3232,9 +3234,6 @@ async function writeViaSiblingTempPath(params) {
3232
3234
  });
3233
3235
  }
3234
3236
  }
3235
- function isAbsolute(p) {
3236
- return p.startsWith("/") || /^[a-zA-Z]:/.test(p);
3237
- }
3238
3237
  async function assertBrowserNavigationResultAllowed(opts) {
3239
3238
  const rawUrl = opts.url.trim();
3240
3239
  if (rawUrl === "") return;
@@ -3275,7 +3274,6 @@ async function setCheckedViaEvaluate(locator, checked) {
3275
3274
  else input.checked = desired;
3276
3275
  input.dispatchEvent(new Event("input", { bubbles: true }));
3277
3276
  input.dispatchEvent(new Event("change", { bubbles: true }));
3278
- input.click();
3279
3277
  }, checked);
3280
3278
  }
3281
3279
  function resolveLocator(page, resolved) {
@@ -3452,7 +3450,10 @@ async function fillFormViaPlaywright(opts) {
3452
3450
  const checked = rawValue === true || rawValue === 1 || rawValue === "1" || rawValue === "true";
3453
3451
  try {
3454
3452
  await locator.setChecked(checked, { timeout, force: true });
3455
- } catch {
3453
+ } catch (setCheckedErr) {
3454
+ console.warn(
3455
+ `[browserclaw] setChecked fallback for ref "${ref}": ${setCheckedErr instanceof Error ? setCheckedErr.message : String(setCheckedErr)}`
3456
+ );
3456
3457
  try {
3457
3458
  await setCheckedViaEvaluate(locator, checked);
3458
3459
  } catch (err) {
@@ -3564,6 +3565,7 @@ async function armFileUploadViaPlaywright(opts) {
3564
3565
  scopeLabel: `uploads directory (${DEFAULT_UPLOAD_DIR})`
3565
3566
  });
3566
3567
  if (!uploadPathsResult.ok) {
3568
+ console.warn(`[browserclaw] armFileUpload: path validation failed: ${uploadPathsResult.error}`);
3567
3569
  try {
3568
3570
  await page.keyboard.press("Escape");
3569
3571
  } catch {
@@ -4613,14 +4615,17 @@ async function traceStopViaPlaywright(opts) {
4613
4615
  if (!ctxState.traceActive) {
4614
4616
  throw new Error("No active trace. Start a trace before stopping it.");
4615
4617
  }
4616
- await writeViaSiblingTempPath({
4617
- rootDir: path.dirname(opts.path),
4618
- targetPath: opts.path,
4619
- writeTemp: async (tempPath) => {
4620
- await context.tracing.stop({ path: tempPath });
4621
- }
4622
- });
4623
- ctxState.traceActive = false;
4618
+ try {
4619
+ await writeViaSiblingTempPath({
4620
+ rootDir: path.dirname(opts.path),
4621
+ targetPath: opts.path,
4622
+ writeTemp: async (tempPath) => {
4623
+ await context.tracing.stop({ path: tempPath });
4624
+ }
4625
+ });
4626
+ } finally {
4627
+ ctxState.traceActive = false;
4628
+ }
4624
4629
  }
4625
4630
 
4626
4631
  // src/snapshot/ref-map.ts
@@ -4819,7 +4824,7 @@ function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
4819
4824
  const isInteractive = INTERACTIVE_ROLES.has(role);
4820
4825
  const isContent = CONTENT_ROLES.has(role);
4821
4826
  const isStructural = STRUCTURAL_ROLES.has(role);
4822
- if (options.compact === true && isStructural && name === "") continue;
4827
+ if (options.compact === true && isStructural && !name) continue;
4823
4828
  if (!(isInteractive || isContent && name !== "")) {
4824
4829
  result.push(line);
4825
4830
  continue;
@@ -4830,7 +4835,7 @@ function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
4830
4835
  const state = parseStateFromSuffix(suffix);
4831
4836
  refs[ref] = { role, name, nth, ...state };
4832
4837
  let enhanced = `${prefix}${roleRaw}`;
4833
- if (name !== "") enhanced += ` "${name}"`;
4838
+ if (name) enhanced += ` "${name}"`;
4834
4839
  enhanced += ` [ref=${ref}]`;
4835
4840
  if (nth > 0) enhanced += ` [nth=${String(nth)}]`;
4836
4841
  if (suffix !== "") enhanced += suffix;
@@ -4908,7 +4913,7 @@ function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
4908
4913
  }
4909
4914
  const role = roleRaw.toLowerCase();
4910
4915
  const isStructural = STRUCTURAL_ROLES.has(role);
4911
- if (options.compact === true && isStructural && name === "") continue;
4916
+ if (options.compact === true && isStructural && !name) continue;
4912
4917
  const ref = parseAiSnapshotRef(suffix);
4913
4918
  const state = parseStateFromSuffix(suffix);
4914
4919
  if (ref !== null) {
@@ -4916,9 +4921,9 @@ function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
4916
4921
  out.push(line);
4917
4922
  } else if (INTERACTIVE_ROLES.has(role)) {
4918
4923
  const generatedRef = nextGeneratedRef();
4919
- refs[generatedRef] = { role, ...name !== "" ? { name } : {}, ...state };
4924
+ refs[generatedRef] = { role, ...name ? { name } : {}, ...state };
4920
4925
  let enhanced = `${prefix}${roleRaw}`;
4921
- if (name !== "") enhanced += ` "${name}"`;
4926
+ if (name) enhanced += ` "${name}"`;
4922
4927
  enhanced += ` [ref=${generatedRef}]`;
4923
4928
  if (suffix.trim() !== "") enhanced += suffix;
4924
4929
  out.push(enhanced);
@@ -4999,7 +5004,7 @@ async function snapshotRole(opts) {
4999
5004
  if (!maybe._snapshotForAI) {
5000
5005
  throw new Error("refs=aria requires Playwright _snapshotForAI support.");
5001
5006
  }
5002
- const result = await maybe._snapshotForAI({ timeout: 5e3, track: "response" });
5007
+ const result = await maybe._snapshotForAI({ timeout: normalizeTimeoutMs(opts.timeoutMs, 5e3), track: "response" });
5003
5008
  const built2 = buildRoleSnapshotFromAiSnapshot(String(result.full), opts.options);
5004
5009
  storeRoleRefsForTarget({
5005
5010
  page,
@@ -5509,7 +5514,7 @@ var CrawlPage = class {
5509
5514
  * ```ts
5510
5515
  * await page.fill([
5511
5516
  * { ref: 'e2', type: 'text', value: 'Jane Doe' },
5512
- * { ref: 'e4', type: 'text', value: 'jane@example.com' },
5517
+ * { ref: 'e4', type: 'text', value: 'jane@acme.test' },
5513
5518
  * { ref: 'e6', type: 'checkbox', value: true },
5514
5519
  * ]);
5515
5520
  * ```
@@ -5562,17 +5567,18 @@ var CrawlPage = class {
5562
5567
  });
5563
5568
  }
5564
5569
  /**
5565
- * Arm a one-shot dialog handler (alert, confirm, prompt).
5570
+ * Arm a one-shot dialog handler (alert, confirm, prompt). Fire-and-forget:
5571
+ * returns immediately once the arm is registered. The dialog is handled in
5572
+ * the background when it fires.
5566
5573
  *
5567
- * Returns a promise store it (don't await), trigger the dialog, then await it.
5574
+ * Call this BEFORE triggering the action that opens the dialog.
5568
5575
  *
5569
5576
  * @param opts - Dialog options (accept/dismiss, prompt text, timeout)
5570
5577
  *
5571
5578
  * @example
5572
5579
  * ```ts
5573
- * const dialogDone = page.armDialog({ accept: true }); // don't await here
5574
- * await page.click('e5'); // triggers confirm()
5575
- * await dialogDone; // wait for dialog to be handled
5580
+ * await page.armDialog({ accept: true }); // registers the handler, returns immediately
5581
+ * await page.click('e5'); // triggers confirm() — handled in background
5576
5582
  * ```
5577
5583
  */
5578
5584
  async armDialog(opts) {
@@ -6054,7 +6060,7 @@ var CrawlPage = class {
6054
6060
  * await page.setCookie({
6055
6061
  * name: 'token',
6056
6062
  * value: 'abc123',
6057
- * url: 'https://example.com',
6063
+ * url: 'https://demo.playwright.dev/todomvc',
6058
6064
  * });
6059
6065
  * ```
6060
6066
  */
@@ -6306,7 +6312,7 @@ var CrawlPage = class {
6306
6312
  *
6307
6313
  * @example
6308
6314
  * ```ts
6309
- * await page.goto('https://example.com');
6315
+ * await page.goto('https://demo.playwright.dev/todomvc');
6310
6316
  * const challenge = await page.detectChallenge();
6311
6317
  * if (challenge?.kind === 'cloudflare-js') {
6312
6318
  * const { resolved } = await page.waitForChallenge({ timeoutMs: 20000 });
@@ -6322,6 +6328,137 @@ var CrawlPage = class {
6322
6328
  pollMs: opts?.pollMs
6323
6329
  });
6324
6330
  }
6331
+ // ── Auth Health ──────────────────────────────────────────────
6332
+ /**
6333
+ * Check whether the current page session appears authenticated.
6334
+ *
6335
+ * Evaluates one or more rules against the page state. All rules must pass
6336
+ * for `authenticated` to be `true`. Returns per-rule details for debugging.
6337
+ *
6338
+ * @param rules - Array of auth check rules (url, cookie, selector, text, textGone, fn)
6339
+ * @returns Authentication status and per-rule check details
6340
+ *
6341
+ * @example
6342
+ * ```ts
6343
+ * // Check by URL and absence of login text
6344
+ * const result = await page.isAuthenticated([
6345
+ * { url: '/dashboard' },
6346
+ * { textGone: 'Sign in' },
6347
+ * ]);
6348
+ * if (!result.authenticated) {
6349
+ * console.log('Auth failed:', result.checks.filter(c => !c.passed));
6350
+ * }
6351
+ *
6352
+ * // Check by cookie presence
6353
+ * const result = await page.isAuthenticated([{ cookie: 'session_id' }]);
6354
+ *
6355
+ * // Check with custom JS function
6356
+ * const result = await page.isAuthenticated([
6357
+ * { fn: '() => !!document.querySelector("[data-user-id]")' },
6358
+ * ]);
6359
+ * ```
6360
+ */
6361
+ async isAuthenticated(rules) {
6362
+ if (!rules.length) return { authenticated: true, checks: [] };
6363
+ const page = await getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this.targetId });
6364
+ const checks = [];
6365
+ for (const rule of rules) {
6366
+ if (rule.url !== void 0) {
6367
+ const currentUrl = page.url();
6368
+ const passed = currentUrl.includes(rule.url);
6369
+ checks.push({ rule: "url", passed, detail: passed ? currentUrl : `expected "${rule.url}" in "${currentUrl}"` });
6370
+ }
6371
+ if (rule.cookie !== void 0) {
6372
+ const cookies = await page.context().cookies();
6373
+ const found = cookies.some((c) => c.name === rule.cookie && c.value !== "");
6374
+ checks.push({
6375
+ rule: "cookie",
6376
+ passed: found,
6377
+ detail: found ? `cookie "${rule.cookie}" present` : `cookie "${rule.cookie}" missing or empty`
6378
+ });
6379
+ }
6380
+ if (rule.selector !== void 0) {
6381
+ try {
6382
+ const count = await page.locator(rule.selector).count();
6383
+ const passed = count > 0;
6384
+ checks.push({
6385
+ rule: "selector",
6386
+ passed,
6387
+ detail: passed ? `"${rule.selector}" found (${String(count)})` : `"${rule.selector}" not found`
6388
+ });
6389
+ } catch (err) {
6390
+ console.warn(
6391
+ `[browserclaw] isAuthenticated selector check failed: ${err instanceof Error ? err.message : String(err)}`
6392
+ );
6393
+ checks.push({ rule: "selector", passed: false, detail: `"${rule.selector}" error during evaluation` });
6394
+ }
6395
+ }
6396
+ if (rule.text !== void 0 || rule.textGone !== void 0) {
6397
+ let bodyText = null;
6398
+ try {
6399
+ const raw = await evaluateViaPlaywright({
6400
+ cdpUrl: this.cdpUrl,
6401
+ targetId: this.targetId,
6402
+ fn: '() => { const b = document.body; return b ? b.innerText : ""; }'
6403
+ });
6404
+ bodyText = typeof raw === "string" ? raw : null;
6405
+ } catch (err) {
6406
+ console.warn(
6407
+ `[browserclaw] isAuthenticated body text fetch failed: ${err instanceof Error ? err.message : String(err)}`
6408
+ );
6409
+ }
6410
+ if (rule.text !== void 0) {
6411
+ if (bodyText === null) {
6412
+ checks.push({ rule: "text", passed: false, detail: `"${rule.text}" error during evaluation` });
6413
+ } else {
6414
+ const passed = bodyText.includes(rule.text);
6415
+ checks.push({
6416
+ rule: "text",
6417
+ passed,
6418
+ detail: passed ? `"${rule.text}" found` : `"${rule.text}" not found in page text`
6419
+ });
6420
+ }
6421
+ }
6422
+ if (rule.textGone !== void 0) {
6423
+ if (bodyText === null) {
6424
+ checks.push({ rule: "textGone", passed: false, detail: `"${rule.textGone}" error during evaluation` });
6425
+ } else {
6426
+ const passed = !bodyText.includes(rule.textGone);
6427
+ checks.push({
6428
+ rule: "textGone",
6429
+ passed,
6430
+ detail: passed ? `"${rule.textGone}" absent (good)` : `"${rule.textGone}" still present`
6431
+ });
6432
+ }
6433
+ }
6434
+ }
6435
+ if (rule.fn !== void 0) {
6436
+ try {
6437
+ const result = await evaluateViaPlaywright({
6438
+ cdpUrl: this.cdpUrl,
6439
+ targetId: this.targetId,
6440
+ fn: rule.fn
6441
+ });
6442
+ const passed = result !== null && result !== void 0 && result !== false && result !== 0 && result !== "";
6443
+ checks.push({
6444
+ rule: "fn",
6445
+ passed,
6446
+ detail: passed ? "function returned truthy" : `function returned ${JSON.stringify(result)}`
6447
+ });
6448
+ } catch (err) {
6449
+ checks.push({
6450
+ rule: "fn",
6451
+ passed: false,
6452
+ detail: `function threw: ${err instanceof Error ? err.message : String(err)}`
6453
+ });
6454
+ }
6455
+ }
6456
+ }
6457
+ return {
6458
+ authenticated: checks.length > 0 && checks.every((c) => c.passed),
6459
+ checks
6460
+ };
6461
+ }
6325
6462
  // ── Playwright Escape Hatches ─────────────────────────────────
6326
6463
  /**
6327
6464
  * Get the underlying Playwright `Page` object for this tab.
@@ -6367,7 +6504,7 @@ var CrawlPage = class {
6367
6504
  *
6368
6505
  * // Use Playwright selectors
6369
6506
  * const input = await page.locator('input[name="email"]');
6370
- * await input.fill('test@example.com');
6507
+ * await input.fill('test@acme.test');
6371
6508
  * ```
6372
6509
  */
6373
6510
  async locator(selector) {
@@ -6380,9 +6517,11 @@ var BrowserClaw = class _BrowserClaw {
6380
6517
  ssrfPolicy;
6381
6518
  recordVideo;
6382
6519
  chrome;
6383
- constructor(cdpUrl, chrome, ssrfPolicy, recordVideo) {
6520
+ _telemetry;
6521
+ constructor(cdpUrl, chrome, telemetry, ssrfPolicy, recordVideo) {
6384
6522
  this.cdpUrl = cdpUrl;
6385
6523
  this.chrome = chrome;
6524
+ this._telemetry = telemetry;
6386
6525
  this.ssrfPolicy = ssrfPolicy;
6387
6526
  this.recordVideo = recordVideo;
6388
6527
  }
@@ -6398,26 +6537,34 @@ var BrowserClaw = class _BrowserClaw {
6398
6537
  * @example
6399
6538
  * ```ts
6400
6539
  * // Launch and navigate to a URL
6401
- * const browser = await BrowserClaw.launch({ url: 'https://example.com' });
6540
+ * const browser = await BrowserClaw.launch({ url: 'https://demo.playwright.dev/todomvc' });
6402
6541
  *
6403
6542
  * // Headless mode
6404
- * const browser = await BrowserClaw.launch({ url: 'https://example.com', headless: true });
6543
+ * const browser = await BrowserClaw.launch({ url: 'https://demo.playwright.dev/todomvc', headless: true });
6405
6544
  *
6406
6545
  * // Specific browser
6407
6546
  * const browser = await BrowserClaw.launch({
6408
- * url: 'https://example.com',
6547
+ * url: 'https://demo.playwright.dev/todomvc',
6409
6548
  * executablePath: '/usr/bin/google-chrome',
6410
6549
  * });
6411
6550
  * ```
6412
6551
  */
6413
6552
  static async launch(opts = {}) {
6553
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
6414
6554
  const chrome = await launchChrome(opts);
6415
6555
  const cdpUrl = `http://127.0.0.1:${String(chrome.cdpPort)}`;
6416
6556
  const ssrfPolicy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
6417
- const browser = new _BrowserClaw(cdpUrl, chrome, ssrfPolicy, opts.recordVideo);
6557
+ const telemetry = {
6558
+ launchMs: chrome.launchMs,
6559
+ timestamps: { startedAt, launchedAt: (/* @__PURE__ */ new Date()).toISOString() }
6560
+ };
6561
+ const browser = new _BrowserClaw(cdpUrl, chrome, telemetry, ssrfPolicy, opts.recordVideo);
6418
6562
  if (opts.url !== void 0 && opts.url !== "") {
6419
6563
  const page = await browser.currentPage();
6564
+ const navT0 = Date.now();
6420
6565
  await page.goto(opts.url);
6566
+ telemetry.navMs = Date.now() - navT0;
6567
+ telemetry.timestamps.navigatedAt = (/* @__PURE__ */ new Date()).toISOString();
6421
6568
  }
6422
6569
  return browser;
6423
6570
  }
@@ -6436,6 +6583,8 @@ var BrowserClaw = class _BrowserClaw {
6436
6583
  * ```
6437
6584
  */
6438
6585
  static async connect(cdpUrl, opts) {
6586
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
6587
+ const connectT0 = Date.now();
6439
6588
  let resolvedUrl = cdpUrl;
6440
6589
  if (resolvedUrl === void 0 || resolvedUrl === "") {
6441
6590
  const discovered = await discoverChromeCdpUrl();
@@ -6451,7 +6600,11 @@ var BrowserClaw = class _BrowserClaw {
6451
6600
  }
6452
6601
  await connectBrowser(resolvedUrl, opts?.authToken);
6453
6602
  const ssrfPolicy = opts?.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts?.ssrfPolicy;
6454
- return new _BrowserClaw(resolvedUrl, null, ssrfPolicy, opts?.recordVideo);
6603
+ const telemetry = {
6604
+ connectMs: Date.now() - connectT0,
6605
+ timestamps: { startedAt, connectedAt: (/* @__PURE__ */ new Date()).toISOString() }
6606
+ };
6607
+ return new _BrowserClaw(resolvedUrl, null, telemetry, ssrfPolicy, opts?.recordVideo);
6455
6608
  }
6456
6609
  /**
6457
6610
  * Open a URL in a new tab and return the page handle.
@@ -6461,7 +6614,7 @@ var BrowserClaw = class _BrowserClaw {
6461
6614
  *
6462
6615
  * @example
6463
6616
  * ```ts
6464
- * const page = await browser.open('https://example.com');
6617
+ * const page = await browser.open('https://demo.playwright.dev/todomvc');
6465
6618
  * const { snapshot, refs } = await page.snapshot();
6466
6619
  * ```
6467
6620
  */
@@ -6480,7 +6633,12 @@ var BrowserClaw = class _BrowserClaw {
6480
6633
  * @returns CrawlPage for the first/active page
6481
6634
  */
6482
6635
  async currentPage() {
6636
+ const connectT0 = Date.now();
6483
6637
  const { browser } = await connectBrowser(this.cdpUrl);
6638
+ if (this._telemetry.connectMs === void 0) {
6639
+ this._telemetry.connectMs = Date.now() - connectT0;
6640
+ this._telemetry.timestamps.connectedAt = (/* @__PURE__ */ new Date()).toISOString();
6641
+ }
6484
6642
  const pages = getAllPages(browser);
6485
6643
  if (!pages.length) throw new Error("No pages available. Use browser.open(url) to create a tab.");
6486
6644
  const tid = await pageTargetId(pages[0]).catch(() => null);
@@ -6557,15 +6715,61 @@ var BrowserClaw = class _BrowserClaw {
6557
6715
  * If the browser was launched by `BrowserClaw.launch()`, the Chrome process
6558
6716
  * will be terminated. If connected via `BrowserClaw.connect()`, only the
6559
6717
  * Playwright connection is closed.
6718
+ *
6719
+ * @param exitReason - Optional structured reason for stopping (e.g. `'success'`, `'auth_failed'`, `'timeout'`)
6560
6720
  */
6561
- async stop() {
6562
- clearRecordingContext(this.cdpUrl);
6563
- await disconnectBrowser();
6564
- if (this.chrome) {
6565
- await stopChrome(this.chrome);
6566
- this.chrome = null;
6721
+ async stop(exitReason) {
6722
+ this._telemetry.timestamps.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
6723
+ if (exitReason !== void 0) this._telemetry.exitReason = exitReason;
6724
+ try {
6725
+ clearRecordingContext(this.cdpUrl);
6726
+ await disconnectBrowser();
6727
+ if (this.chrome) {
6728
+ await stopChrome(this.chrome);
6729
+ this.chrome = null;
6730
+ }
6731
+ this._telemetry.cleanupOk = true;
6732
+ } catch (err) {
6733
+ this._telemetry.cleanupOk = false;
6734
+ throw err;
6567
6735
  }
6568
6736
  }
6737
+ /**
6738
+ * Get structured telemetry for this browser session.
6739
+ *
6740
+ * Returns timing data, timestamps, and exit information collected
6741
+ * throughout the session lifecycle. Useful for diagnosing startup
6742
+ * latency, auth failures, and cleanup issues in cron/unattended runs.
6743
+ *
6744
+ * @returns Telemetry envelope with launch/connect/nav timings and exit info
6745
+ *
6746
+ * @example
6747
+ * ```ts
6748
+ * const browser = await BrowserClaw.launch({ url: 'https://example.com' });
6749
+ * const page = await browser.currentPage();
6750
+ *
6751
+ * // ... do work ...
6752
+ *
6753
+ * const auth = await page.isAuthenticated([{ cookie: 'session' }]);
6754
+ * browser.recordAuthResult(auth.authenticated);
6755
+ *
6756
+ * await browser.stop(auth.authenticated ? 'success' : 'auth_failed');
6757
+ * console.log(browser.telemetry());
6758
+ * // { launchMs: 1823, connectMs: 45, navMs: 620, authOk: true,
6759
+ * // exitReason: 'success', cleanupOk: true, timestamps: { ... } }
6760
+ * ```
6761
+ */
6762
+ telemetry() {
6763
+ return this._telemetry;
6764
+ }
6765
+ /**
6766
+ * Record the result of an authentication check in the telemetry envelope.
6767
+ *
6768
+ * @param ok - Whether authentication was successful
6769
+ */
6770
+ recordAuthResult(ok) {
6771
+ this._telemetry.authOk = ok;
6772
+ }
6569
6773
  };
6570
6774
 
6571
6775
  exports.BlockedBrowserTargetError = BlockedBrowserTargetError;