browserclaw 0.11.5 → 0.11.6

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,
@@ -2335,6 +2336,7 @@ async function disconnectBrowser() {
2335
2336
  }
2336
2337
  }
2337
2338
  for (const cur of cachedByCdpUrl.values()) {
2339
+ clearRoleRefsForCdpUrl(cur.cdpUrl);
2338
2340
  if (cur.onDisconnected && typeof cur.browser.off === "function")
2339
2341
  cur.browser.off("disconnected", cur.onDisconnected);
2340
2342
  await cur.browser.close().catch(() => {
@@ -2564,7 +2566,6 @@ async function getPageForTargetId(opts) {
2564
2566
  );
2565
2567
  }
2566
2568
  if (isBlockedPageRef(opts.cdpUrl, found)) throw new BlockedBrowserTargetError();
2567
- if (isBlockedTarget(opts.cdpUrl, opts.targetId)) throw new BlockedBrowserTargetError();
2568
2569
  return found;
2569
2570
  }
2570
2571
  async function resolvePageByTargetIdOrThrow(opts) {
@@ -3217,7 +3218,7 @@ async function writeViaSiblingTempPath(params) {
3217
3218
  const requestedTargetPath = path.resolve(params.targetPath);
3218
3219
  const targetPath = await promises$1.realpath(path.dirname(requestedTargetPath)).then((realDir) => path.join(realDir, path.basename(requestedTargetPath))).catch(() => requestedTargetPath);
3219
3220
  const relativeTargetPath = path.relative(rootDir, targetPath);
3220
- if (!relativeTargetPath || relativeTargetPath === ".." || relativeTargetPath.startsWith(`..${path.sep}`) || isAbsolute(relativeTargetPath)) {
3221
+ if (!relativeTargetPath || relativeTargetPath === ".." || relativeTargetPath.startsWith(`..${path.sep}`) || path.isAbsolute(relativeTargetPath)) {
3221
3222
  throw new Error("Target path is outside the allowed root");
3222
3223
  }
3223
3224
  const tempPath = buildSiblingTempPath(targetPath);
@@ -3232,9 +3233,6 @@ async function writeViaSiblingTempPath(params) {
3232
3233
  });
3233
3234
  }
3234
3235
  }
3235
- function isAbsolute(p) {
3236
- return p.startsWith("/") || /^[a-zA-Z]:/.test(p);
3237
- }
3238
3236
  async function assertBrowserNavigationResultAllowed(opts) {
3239
3237
  const rawUrl = opts.url.trim();
3240
3238
  if (rawUrl === "") return;
@@ -3275,7 +3273,6 @@ async function setCheckedViaEvaluate(locator, checked) {
3275
3273
  else input.checked = desired;
3276
3274
  input.dispatchEvent(new Event("input", { bubbles: true }));
3277
3275
  input.dispatchEvent(new Event("change", { bubbles: true }));
3278
- input.click();
3279
3276
  }, checked);
3280
3277
  }
3281
3278
  function resolveLocator(page, resolved) {
@@ -3452,7 +3449,10 @@ async function fillFormViaPlaywright(opts) {
3452
3449
  const checked = rawValue === true || rawValue === 1 || rawValue === "1" || rawValue === "true";
3453
3450
  try {
3454
3451
  await locator.setChecked(checked, { timeout, force: true });
3455
- } catch {
3452
+ } catch (setCheckedErr) {
3453
+ console.warn(
3454
+ `[browserclaw] setChecked fallback for ref "${ref}": ${setCheckedErr instanceof Error ? setCheckedErr.message : String(setCheckedErr)}`
3455
+ );
3456
3456
  try {
3457
3457
  await setCheckedViaEvaluate(locator, checked);
3458
3458
  } catch (err) {
@@ -3564,6 +3564,7 @@ async function armFileUploadViaPlaywright(opts) {
3564
3564
  scopeLabel: `uploads directory (${DEFAULT_UPLOAD_DIR})`
3565
3565
  });
3566
3566
  if (!uploadPathsResult.ok) {
3567
+ console.warn(`[browserclaw] armFileUpload: path validation failed: ${uploadPathsResult.error}`);
3567
3568
  try {
3568
3569
  await page.keyboard.press("Escape");
3569
3570
  } catch {
@@ -4613,14 +4614,17 @@ async function traceStopViaPlaywright(opts) {
4613
4614
  if (!ctxState.traceActive) {
4614
4615
  throw new Error("No active trace. Start a trace before stopping it.");
4615
4616
  }
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;
4617
+ try {
4618
+ await writeViaSiblingTempPath({
4619
+ rootDir: path.dirname(opts.path),
4620
+ targetPath: opts.path,
4621
+ writeTemp: async (tempPath) => {
4622
+ await context.tracing.stop({ path: tempPath });
4623
+ }
4624
+ });
4625
+ } finally {
4626
+ ctxState.traceActive = false;
4627
+ }
4624
4628
  }
4625
4629
 
4626
4630
  // src/snapshot/ref-map.ts
@@ -4819,7 +4823,7 @@ function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
4819
4823
  const isInteractive = INTERACTIVE_ROLES.has(role);
4820
4824
  const isContent = CONTENT_ROLES.has(role);
4821
4825
  const isStructural = STRUCTURAL_ROLES.has(role);
4822
- if (options.compact === true && isStructural && name === "") continue;
4826
+ if (options.compact === true && isStructural && !name) continue;
4823
4827
  if (!(isInteractive || isContent && name !== "")) {
4824
4828
  result.push(line);
4825
4829
  continue;
@@ -4830,7 +4834,7 @@ function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
4830
4834
  const state = parseStateFromSuffix(suffix);
4831
4835
  refs[ref] = { role, name, nth, ...state };
4832
4836
  let enhanced = `${prefix}${roleRaw}`;
4833
- if (name !== "") enhanced += ` "${name}"`;
4837
+ if (name) enhanced += ` "${name}"`;
4834
4838
  enhanced += ` [ref=${ref}]`;
4835
4839
  if (nth > 0) enhanced += ` [nth=${String(nth)}]`;
4836
4840
  if (suffix !== "") enhanced += suffix;
@@ -4908,7 +4912,7 @@ function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
4908
4912
  }
4909
4913
  const role = roleRaw.toLowerCase();
4910
4914
  const isStructural = STRUCTURAL_ROLES.has(role);
4911
- if (options.compact === true && isStructural && name === "") continue;
4915
+ if (options.compact === true && isStructural && !name) continue;
4912
4916
  const ref = parseAiSnapshotRef(suffix);
4913
4917
  const state = parseStateFromSuffix(suffix);
4914
4918
  if (ref !== null) {
@@ -4916,9 +4920,9 @@ function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
4916
4920
  out.push(line);
4917
4921
  } else if (INTERACTIVE_ROLES.has(role)) {
4918
4922
  const generatedRef = nextGeneratedRef();
4919
- refs[generatedRef] = { role, ...name !== "" ? { name } : {}, ...state };
4923
+ refs[generatedRef] = { role, ...name ? { name } : {}, ...state };
4920
4924
  let enhanced = `${prefix}${roleRaw}`;
4921
- if (name !== "") enhanced += ` "${name}"`;
4925
+ if (name) enhanced += ` "${name}"`;
4922
4926
  enhanced += ` [ref=${generatedRef}]`;
4923
4927
  if (suffix.trim() !== "") enhanced += suffix;
4924
4928
  out.push(enhanced);
@@ -4999,7 +5003,7 @@ async function snapshotRole(opts) {
4999
5003
  if (!maybe._snapshotForAI) {
5000
5004
  throw new Error("refs=aria requires Playwright _snapshotForAI support.");
5001
5005
  }
5002
- const result = await maybe._snapshotForAI({ timeout: 5e3, track: "response" });
5006
+ const result = await maybe._snapshotForAI({ timeout: normalizeTimeoutMs(opts.timeoutMs, 5e3), track: "response" });
5003
5007
  const built2 = buildRoleSnapshotFromAiSnapshot(String(result.full), opts.options);
5004
5008
  storeRoleRefsForTarget({
5005
5009
  page,
@@ -5509,7 +5513,7 @@ var CrawlPage = class {
5509
5513
  * ```ts
5510
5514
  * await page.fill([
5511
5515
  * { ref: 'e2', type: 'text', value: 'Jane Doe' },
5512
- * { ref: 'e4', type: 'text', value: 'jane@example.com' },
5516
+ * { ref: 'e4', type: 'text', value: 'jane@acme.test' },
5513
5517
  * { ref: 'e6', type: 'checkbox', value: true },
5514
5518
  * ]);
5515
5519
  * ```
@@ -5562,17 +5566,18 @@ var CrawlPage = class {
5562
5566
  });
5563
5567
  }
5564
5568
  /**
5565
- * Arm a one-shot dialog handler (alert, confirm, prompt).
5569
+ * Arm a one-shot dialog handler (alert, confirm, prompt). Fire-and-forget:
5570
+ * returns immediately once the arm is registered. The dialog is handled in
5571
+ * the background when it fires.
5566
5572
  *
5567
- * Returns a promise store it (don't await), trigger the dialog, then await it.
5573
+ * Call this BEFORE triggering the action that opens the dialog.
5568
5574
  *
5569
5575
  * @param opts - Dialog options (accept/dismiss, prompt text, timeout)
5570
5576
  *
5571
5577
  * @example
5572
5578
  * ```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
5579
+ * await page.armDialog({ accept: true }); // registers the handler, returns immediately
5580
+ * await page.click('e5'); // triggers confirm() — handled in background
5576
5581
  * ```
5577
5582
  */
5578
5583
  async armDialog(opts) {
@@ -6054,7 +6059,7 @@ var CrawlPage = class {
6054
6059
  * await page.setCookie({
6055
6060
  * name: 'token',
6056
6061
  * value: 'abc123',
6057
- * url: 'https://example.com',
6062
+ * url: 'https://demo.playwright.dev/todomvc',
6058
6063
  * });
6059
6064
  * ```
6060
6065
  */
@@ -6306,7 +6311,7 @@ var CrawlPage = class {
6306
6311
  *
6307
6312
  * @example
6308
6313
  * ```ts
6309
- * await page.goto('https://example.com');
6314
+ * await page.goto('https://demo.playwright.dev/todomvc');
6310
6315
  * const challenge = await page.detectChallenge();
6311
6316
  * if (challenge?.kind === 'cloudflare-js') {
6312
6317
  * const { resolved } = await page.waitForChallenge({ timeoutMs: 20000 });
@@ -6367,7 +6372,7 @@ var CrawlPage = class {
6367
6372
  *
6368
6373
  * // Use Playwright selectors
6369
6374
  * const input = await page.locator('input[name="email"]');
6370
- * await input.fill('test@example.com');
6375
+ * await input.fill('test@acme.test');
6371
6376
  * ```
6372
6377
  */
6373
6378
  async locator(selector) {
@@ -6398,14 +6403,14 @@ var BrowserClaw = class _BrowserClaw {
6398
6403
  * @example
6399
6404
  * ```ts
6400
6405
  * // Launch and navigate to a URL
6401
- * const browser = await BrowserClaw.launch({ url: 'https://example.com' });
6406
+ * const browser = await BrowserClaw.launch({ url: 'https://demo.playwright.dev/todomvc' });
6402
6407
  *
6403
6408
  * // Headless mode
6404
- * const browser = await BrowserClaw.launch({ url: 'https://example.com', headless: true });
6409
+ * const browser = await BrowserClaw.launch({ url: 'https://demo.playwright.dev/todomvc', headless: true });
6405
6410
  *
6406
6411
  * // Specific browser
6407
6412
  * const browser = await BrowserClaw.launch({
6408
- * url: 'https://example.com',
6413
+ * url: 'https://demo.playwright.dev/todomvc',
6409
6414
  * executablePath: '/usr/bin/google-chrome',
6410
6415
  * });
6411
6416
  * ```
@@ -6461,7 +6466,7 @@ var BrowserClaw = class _BrowserClaw {
6461
6466
  *
6462
6467
  * @example
6463
6468
  * ```ts
6464
- * const page = await browser.open('https://example.com');
6469
+ * const page = await browser.open('https://demo.playwright.dev/todomvc');
6465
6470
  * const { snapshot, refs } = await page.snapshot();
6466
6471
  * ```
6467
6472
  */