@hasna/browser 0.0.3 → 0.0.5

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/index.js CHANGED
@@ -2121,6 +2121,12 @@ var init_types = __esm(() => {
2121
2121
  });
2122
2122
 
2123
2123
  // src/db/schema.ts
2124
+ var exports_schema = {};
2125
+ __export(exports_schema, {
2126
+ resetDatabase: () => resetDatabase,
2127
+ getDatabase: () => getDatabase,
2128
+ getDataDir: () => getDataDir
2129
+ });
2124
2130
  import { Database } from "bun:sqlite";
2125
2131
  import { join } from "path";
2126
2132
  import { mkdirSync } from "fs";
@@ -2146,6 +2152,15 @@ function getDatabase(path) {
2146
2152
  runMigrations(_db);
2147
2153
  return _db;
2148
2154
  }
2155
+ function resetDatabase() {
2156
+ if (_db) {
2157
+ try {
2158
+ _db.close();
2159
+ } catch {}
2160
+ }
2161
+ _db = null;
2162
+ _dbPath = null;
2163
+ }
2149
2164
  function runMigrations(db) {
2150
2165
  db.exec(`
2151
2166
  CREATE TABLE IF NOT EXISTS schema_migrations (
@@ -2313,7 +2328,14 @@ import { randomUUID } from "crypto";
2313
2328
  function createSession(data) {
2314
2329
  const db = getDatabase();
2315
2330
  const id = randomUUID();
2316
- db.prepare("INSERT INTO sessions (id, engine, project_id, agent_id, start_url, name) VALUES (?, ?, ?, ?, ?, ?)").run(id, data.engine, data.projectId ?? null, data.agentId ?? null, data.startUrl ?? null, data.name ?? null);
2331
+ let name = data.name ?? null;
2332
+ if (name) {
2333
+ const existing = db.query("SELECT id FROM sessions WHERE name = ?").get(name);
2334
+ if (existing) {
2335
+ name = `${name}-${id.slice(0, 6)}`;
2336
+ }
2337
+ }
2338
+ db.prepare("INSERT INTO sessions (id, engine, project_id, agent_id, start_url, name) VALUES (?, ?, ?, ?, ?, ?)").run(id, data.engine, data.projectId ?? null, data.agentId ?? null, data.startUrl ?? null, name);
2317
2339
  return getSession(id);
2318
2340
  }
2319
2341
  function getSessionByName(name) {
@@ -2493,11 +2515,420 @@ var init_lightpanda = __esm(() => {
2493
2515
  LIGHTPANDA_BINARY = process.env["LIGHTPANDA_BINARY"] ?? "lightpanda";
2494
2516
  });
2495
2517
 
2518
+ // src/engines/bun-webview.ts
2519
+ import { join as join2 } from "path";
2520
+ import { mkdirSync as mkdirSync2 } from "fs";
2521
+ import { homedir as homedir2 } from "os";
2522
+ function isBunWebViewAvailable() {
2523
+ return typeof globalThis.Bun !== "undefined" && typeof globalThis.Bun.WebView !== "undefined";
2524
+ }
2525
+ function getProfileDir(profileName) {
2526
+ const base = process.env["BROWSER_DATA_DIR"] ?? join2(homedir2(), ".browser");
2527
+ const dir = join2(base, "profiles", profileName);
2528
+ mkdirSync2(dir, { recursive: true });
2529
+ return dir;
2530
+ }
2531
+ var BunWebViewSession;
2532
+ var init_bun_webview = __esm(() => {
2533
+ BunWebViewSession = class BunWebViewSession {
2534
+ view;
2535
+ _sessionId;
2536
+ _eventListeners = new Map;
2537
+ constructor(opts = {}) {
2538
+ if (!isBunWebViewAvailable()) {
2539
+ throw new Error("Bun.WebView is not available. Install Bun canary: bun upgrade --canary");
2540
+ }
2541
+ const BunWebView = globalThis.Bun.WebView;
2542
+ const constructorOpts = {
2543
+ width: opts.width ?? 1280,
2544
+ height: opts.height ?? 720
2545
+ };
2546
+ if (opts.profile) {
2547
+ constructorOpts.dataStore = { directory: getProfileDir(opts.profile) };
2548
+ } else {
2549
+ constructorOpts.dataStore = "ephemeral";
2550
+ }
2551
+ if (opts.onConsole) {
2552
+ constructorOpts.console = opts.onConsole;
2553
+ }
2554
+ this.view = new BunWebView(constructorOpts);
2555
+ this.view.onNavigated = (url) => {
2556
+ this._emit("navigated", url);
2557
+ };
2558
+ this.view.onNavigationFailed = (error) => {
2559
+ this._emit("navigationfailed", error);
2560
+ };
2561
+ }
2562
+ async goto(url, opts) {
2563
+ await this.view.navigate(url);
2564
+ await new Promise((r) => setTimeout(r, 200));
2565
+ }
2566
+ async goBack() {
2567
+ await this.view.goBack();
2568
+ }
2569
+ async goForward() {
2570
+ await this.view.goForward();
2571
+ }
2572
+ async reload() {
2573
+ await this.view.reload();
2574
+ }
2575
+ async evaluate(fnOrExpr, ...args) {
2576
+ let expr;
2577
+ if (typeof fnOrExpr === "function") {
2578
+ const serializedArgs = args.map((a) => JSON.stringify(a)).join(", ");
2579
+ expr = `(${fnOrExpr.toString()})(${serializedArgs})`;
2580
+ } else {
2581
+ expr = fnOrExpr;
2582
+ }
2583
+ return this.view.evaluate(expr);
2584
+ }
2585
+ async screenshot(opts) {
2586
+ const uint8 = await this.view.screenshot();
2587
+ return Buffer.from(uint8);
2588
+ }
2589
+ async click(selector, opts) {
2590
+ await this.view.click(selector, opts ? { button: opts.button } : undefined);
2591
+ }
2592
+ async type(selector, text, opts) {
2593
+ try {
2594
+ await this.view.click(selector);
2595
+ } catch {}
2596
+ await this.view.type(text);
2597
+ }
2598
+ async fill(selector, value) {
2599
+ await this.view.evaluate(`
2600
+ (() => {
2601
+ const el = document.querySelector(${JSON.stringify(selector)});
2602
+ if (el) { el.value = ''; el.dispatchEvent(new Event('input')); }
2603
+ })()
2604
+ `);
2605
+ await this.type(selector, value);
2606
+ }
2607
+ async press(key, opts) {
2608
+ await this.view.press(key, opts);
2609
+ }
2610
+ async scroll(direction, amount) {
2611
+ const dx = direction === "left" ? -amount : direction === "right" ? amount : 0;
2612
+ const dy = direction === "up" ? -amount : direction === "down" ? amount : 0;
2613
+ await this.view.scroll(dx, dy);
2614
+ }
2615
+ async scrollIntoView(selector) {
2616
+ await this.view.scrollTo(selector);
2617
+ }
2618
+ async hover(selector) {
2619
+ try {
2620
+ await this.view.scrollTo(selector);
2621
+ } catch {}
2622
+ }
2623
+ async resize(width, height) {
2624
+ await this.view.resize(width, height);
2625
+ }
2626
+ async $(selector) {
2627
+ const exists = await this.view.evaluate(`!!document.querySelector(${JSON.stringify(selector)})`);
2628
+ if (!exists)
2629
+ return null;
2630
+ return {
2631
+ textContent: async () => this.view.evaluate(`document.querySelector(${JSON.stringify(selector)})?.textContent ?? null`)
2632
+ };
2633
+ }
2634
+ async $$(selector) {
2635
+ const count = await this.view.evaluate(`document.querySelectorAll(${JSON.stringify(selector)}).length`);
2636
+ return Array.from({ length: count }, (_, i) => ({
2637
+ textContent: async () => this.view.evaluate(`document.querySelectorAll(${JSON.stringify(selector)})[${i}]?.textContent ?? null`)
2638
+ }));
2639
+ }
2640
+ async inputValue(selector) {
2641
+ return this.view.evaluate(`document.querySelector(${JSON.stringify(selector)})?.value ?? ''`);
2642
+ }
2643
+ async isChecked(selector) {
2644
+ return this.view.evaluate(`!!(document.querySelector(${JSON.stringify(selector)})?.checked)`);
2645
+ }
2646
+ async isVisible(selector) {
2647
+ return this.view.evaluate(`
2648
+ (() => {
2649
+ const el = document.querySelector(${JSON.stringify(selector)});
2650
+ if (!el) return false;
2651
+ const style = window.getComputedStyle(el);
2652
+ return style.display !== 'none' && style.visibility !== 'hidden' && el.offsetWidth > 0;
2653
+ })()
2654
+ `);
2655
+ }
2656
+ async isEnabled(selector) {
2657
+ return this.view.evaluate(`!(document.querySelector(${JSON.stringify(selector)})?.disabled)`);
2658
+ }
2659
+ async selectOption(selector, value) {
2660
+ await this.view.evaluate(`
2661
+ (() => {
2662
+ const el = document.querySelector(${JSON.stringify(selector)});
2663
+ if (el) {
2664
+ el.value = ${JSON.stringify(value)};
2665
+ el.dispatchEvent(new Event('change'));
2666
+ }
2667
+ })()
2668
+ `);
2669
+ return [value];
2670
+ }
2671
+ async check(selector) {
2672
+ await this.view.evaluate(`
2673
+ (() => {
2674
+ const el = document.querySelector(${JSON.stringify(selector)});
2675
+ if (el && !el.checked) { el.checked = true; el.dispatchEvent(new Event('change')); }
2676
+ })()
2677
+ `);
2678
+ }
2679
+ async uncheck(selector) {
2680
+ await this.view.evaluate(`
2681
+ (() => {
2682
+ const el = document.querySelector(${JSON.stringify(selector)});
2683
+ if (el && el.checked) { el.checked = false; el.dispatchEvent(new Event('change')); }
2684
+ })()
2685
+ `);
2686
+ }
2687
+ async setInputFiles(selector, files) {
2688
+ throw new Error("File upload not supported in Bun.WebView engine. Use engine: 'playwright' instead.");
2689
+ }
2690
+ getByRole(role, opts) {
2691
+ const name = opts?.name?.toString() ?? "";
2692
+ const selector = name ? `[role="${role}"][aria-label*="${name}"], ${role}[aria-label*="${name}"]` : `[role="${role}"], ${role}`;
2693
+ return {
2694
+ click: (clickOpts) => this.click(selector, clickOpts),
2695
+ fill: (value) => this.fill(selector, value),
2696
+ check: () => this.check(selector),
2697
+ uncheck: () => this.uncheck(selector),
2698
+ isVisible: () => this.isVisible(selector),
2699
+ textContent: () => this.view.evaluate(`document.querySelector(${JSON.stringify(selector)})?.textContent ?? null`),
2700
+ inputValue: () => this.inputValue(selector),
2701
+ first: () => ({
2702
+ click: (clickOpts) => this.click(selector, clickOpts),
2703
+ fill: (value) => this.fill(selector, value),
2704
+ textContent: () => this.view.evaluate(`document.querySelector(${JSON.stringify(selector)})?.textContent ?? null`),
2705
+ isVisible: () => this.isVisible(selector),
2706
+ hover: () => this.hover(selector),
2707
+ boundingBox: async () => null,
2708
+ scrollIntoViewIfNeeded: () => this.scrollIntoView(selector),
2709
+ evaluate: (fn) => this.view.evaluate(`(${fn.toString()})(document.querySelector(${JSON.stringify(selector)}))`),
2710
+ waitFor: (opts2) => {
2711
+ return new Promise((resolve, reject) => {
2712
+ const timeout = opts2?.timeout ?? 1e4;
2713
+ const start = Date.now();
2714
+ const check = async () => {
2715
+ const visible = await this.isVisible(selector);
2716
+ if (visible)
2717
+ return resolve();
2718
+ if (Date.now() - start > timeout)
2719
+ return reject(new Error(`Timeout waiting for ${selector}`));
2720
+ setTimeout(check, 100);
2721
+ };
2722
+ check();
2723
+ });
2724
+ }
2725
+ }),
2726
+ count: async () => {
2727
+ const count = await this.view.evaluate(`document.querySelectorAll(${JSON.stringify(selector)}).length`);
2728
+ return count;
2729
+ },
2730
+ nth: (n) => ({
2731
+ click: (clickOpts) => this.click(selector, clickOpts),
2732
+ textContent: () => this.view.evaluate(`document.querySelectorAll(${JSON.stringify(selector)})[${n}]?.textContent ?? null`),
2733
+ isVisible: () => this.isVisible(selector)
2734
+ })
2735
+ };
2736
+ }
2737
+ getByText(text, opts) {
2738
+ const selector = opts?.exact ? `*:is(button, a, span, div, p, h1, h2, h3, h4, label)` : "*";
2739
+ return {
2740
+ first: () => ({
2741
+ click: async (clickOpts) => {
2742
+ await this.view.evaluate(`
2743
+ (() => {
2744
+ const text = ${JSON.stringify(text)};
2745
+ const all = document.querySelectorAll('*');
2746
+ for (const el of all) {
2747
+ if (el.children.length === 0 && el.textContent?.trim() === text) {
2748
+ el.click(); return;
2749
+ }
2750
+ }
2751
+ for (const el of all) {
2752
+ if (el.textContent?.includes(text)) { el.click(); return; }
2753
+ }
2754
+ })()
2755
+ `);
2756
+ },
2757
+ waitFor: (waitOpts) => {
2758
+ const timeout = waitOpts?.timeout ?? 1e4;
2759
+ return new Promise((resolve, reject) => {
2760
+ const start = Date.now();
2761
+ const check = async () => {
2762
+ const found = await this.view.evaluate(`document.body?.textContent?.includes(${JSON.stringify(text)})`);
2763
+ if (found)
2764
+ return resolve();
2765
+ if (Date.now() - start > timeout)
2766
+ return reject(new Error(`Timeout: text "${text}" not found`));
2767
+ setTimeout(check, 100);
2768
+ };
2769
+ check();
2770
+ });
2771
+ }
2772
+ })
2773
+ };
2774
+ }
2775
+ locator(selector) {
2776
+ return {
2777
+ click: (opts) => this.click(selector, opts),
2778
+ fill: (value) => this.fill(selector, value),
2779
+ scrollIntoViewIfNeeded: () => this.scrollIntoView(selector),
2780
+ first: () => this.getByRole("*").first(),
2781
+ evaluate: (fn) => this.view.evaluate(`(${fn.toString()})(document.querySelector(${JSON.stringify(selector)}))`),
2782
+ waitFor: (opts) => {
2783
+ const timeout = opts?.timeout ?? 1e4;
2784
+ return new Promise((resolve, reject) => {
2785
+ const start = Date.now();
2786
+ const check = async () => {
2787
+ const exists = await this.view.evaluate(`!!document.querySelector(${JSON.stringify(selector)})`);
2788
+ if (exists)
2789
+ return resolve();
2790
+ if (Date.now() - start > timeout)
2791
+ return reject(new Error(`Timeout: ${selector}`));
2792
+ setTimeout(check, 100);
2793
+ };
2794
+ check();
2795
+ });
2796
+ }
2797
+ };
2798
+ }
2799
+ url() {
2800
+ return this.view.url;
2801
+ }
2802
+ async title() {
2803
+ return this.view.title || await this.evaluate("document.title");
2804
+ }
2805
+ viewportSize() {
2806
+ return { width: 1280, height: 720 };
2807
+ }
2808
+ async waitForLoadState(state, opts) {
2809
+ await new Promise((r) => setTimeout(r, 200));
2810
+ }
2811
+ async waitForURL(pattern, opts) {
2812
+ const timeout = opts?.timeout ?? 30000;
2813
+ const start = Date.now();
2814
+ while (Date.now() - start < timeout) {
2815
+ const url = this.view.url;
2816
+ const matches = pattern instanceof RegExp ? pattern.test(url) : url.includes(pattern);
2817
+ if (matches)
2818
+ return;
2819
+ await new Promise((r) => setTimeout(r, 100));
2820
+ }
2821
+ throw new Error(`Timeout waiting for URL to match ${pattern}`);
2822
+ }
2823
+ async waitForSelector(selector, opts) {
2824
+ const timeout = opts?.timeout ?? 1e4;
2825
+ const start = Date.now();
2826
+ while (Date.now() - start < timeout) {
2827
+ const exists = await this.view.evaluate(`!!document.querySelector(${JSON.stringify(selector)})`);
2828
+ if (exists)
2829
+ return;
2830
+ await new Promise((r) => setTimeout(r, 100));
2831
+ }
2832
+ throw new Error(`Timeout waiting for ${selector}`);
2833
+ }
2834
+ async setContent(html) {
2835
+ await this.view.navigate(`data:text/html,${encodeURIComponent(html)}`);
2836
+ await new Promise((r) => setTimeout(r, 100));
2837
+ }
2838
+ async content() {
2839
+ return this.view.evaluate("document.documentElement.outerHTML");
2840
+ }
2841
+ async addInitScript(script) {
2842
+ const expr = typeof script === "function" ? `(${script.toString()})()` : script;
2843
+ await this.view.evaluate(expr);
2844
+ }
2845
+ keyboard = {
2846
+ press: (key) => this.view.press(key)
2847
+ };
2848
+ context() {
2849
+ return {
2850
+ close: async () => {
2851
+ await this.close();
2852
+ },
2853
+ newPage: async () => {
2854
+ throw new Error("Multi-tab not supported in Bun.WebView. Use engine: 'playwright'");
2855
+ },
2856
+ cookies: async () => [],
2857
+ addCookies: async (_) => {},
2858
+ clearCookies: async () => {},
2859
+ newCDPSession: async () => {
2860
+ throw new Error("CDP session via context not available in Bun.WebView. Use view.cdp() when shipped.");
2861
+ },
2862
+ route: async (_pattern, _handler) => {
2863
+ throw new Error("Network interception not supported in Bun.WebView. Use engine: 'cdp' or 'playwright'.");
2864
+ },
2865
+ unrouteAll: async () => {},
2866
+ pages: () => [],
2867
+ addInitScript: async (script) => {
2868
+ await this.addInitScript(script);
2869
+ }
2870
+ };
2871
+ }
2872
+ on(event, handler) {
2873
+ if (!this._eventListeners.has(event))
2874
+ this._eventListeners.set(event, []);
2875
+ this._eventListeners.get(event).push(handler);
2876
+ return this;
2877
+ }
2878
+ off(event, handler) {
2879
+ const listeners = this._eventListeners.get(event) ?? [];
2880
+ this._eventListeners.set(event, listeners.filter((l) => l !== handler));
2881
+ return this;
2882
+ }
2883
+ _emit(event, ...args) {
2884
+ for (const handler of this._eventListeners.get(event) ?? []) {
2885
+ try {
2886
+ handler(...args);
2887
+ } catch {}
2888
+ }
2889
+ }
2890
+ async pdf(_opts) {
2891
+ throw new Error("PDF generation not supported in Bun.WebView. Use engine: 'playwright'.");
2892
+ }
2893
+ coverage = {
2894
+ startJSCoverage: async () => {},
2895
+ stopJSCoverage: async () => [],
2896
+ startCSSCoverage: async () => {},
2897
+ stopCSSCoverage: async () => []
2898
+ };
2899
+ setSessionId(id) {
2900
+ this._sessionId = id;
2901
+ }
2902
+ getSessionId() {
2903
+ return this._sessionId;
2904
+ }
2905
+ getNativeView() {
2906
+ return this.view;
2907
+ }
2908
+ async close() {
2909
+ try {
2910
+ await this.view.close();
2911
+ } catch {}
2912
+ }
2913
+ [Symbol.asyncDispose]() {
2914
+ return this.close();
2915
+ }
2916
+ };
2917
+ });
2918
+
2496
2919
  // src/engines/selector.ts
2497
2920
  function selectEngine(useCase, explicit) {
2498
2921
  if (explicit && explicit !== "auto")
2499
2922
  return explicit;
2500
2923
  const preferred = ENGINE_MAP[useCase];
2924
+ if (preferred === "bun") {
2925
+ if (isBunWebViewAvailable())
2926
+ return "bun";
2927
+ if (useCase === "scrape" /* SCRAPE */ || useCase === "extract_links" /* EXTRACT_LINKS */ || useCase === "status_check" /* STATUS_CHECK */) {
2928
+ return isLightpandaAvailable() ? "lightpanda" : "playwright";
2929
+ }
2930
+ return "playwright";
2931
+ }
2501
2932
  if (preferred === "lightpanda" && !isLightpandaAvailable()) {
2502
2933
  return "playwright";
2503
2934
  }
@@ -2507,13 +2938,14 @@ var ENGINE_MAP;
2507
2938
  var init_selector = __esm(() => {
2508
2939
  init_types();
2509
2940
  init_lightpanda();
2941
+ init_bun_webview();
2510
2942
  ENGINE_MAP = {
2511
- ["scrape" /* SCRAPE */]: "lightpanda",
2512
- ["extract_links" /* EXTRACT_LINKS */]: "lightpanda",
2513
- ["status_check" /* STATUS_CHECK */]: "lightpanda",
2943
+ ["scrape" /* SCRAPE */]: "bun",
2944
+ ["extract_links" /* EXTRACT_LINKS */]: "bun",
2945
+ ["status_check" /* STATUS_CHECK */]: "bun",
2946
+ ["screenshot" /* SCREENSHOT */]: "bun",
2947
+ ["spa_navigate" /* SPA_NAVIGATE */]: "bun",
2514
2948
  ["form_fill" /* FORM_FILL */]: "playwright",
2515
- ["spa_navigate" /* SPA_NAVIGATE */]: "playwright",
2516
- ["screenshot" /* SCREENSHOT */]: "playwright",
2517
2949
  ["auth_flow" /* AUTH_FLOW */]: "playwright",
2518
2950
  ["multi_tab" /* MULTI_TAB */]: "playwright",
2519
2951
  ["record_replay" /* RECORD_REPLAY */]: "playwright",
@@ -2856,12 +3288,30 @@ var init_dialogs = __esm(() => {
2856
3288
  });
2857
3289
 
2858
3290
  // src/lib/session.ts
3291
+ function createBunProxy(view) {
3292
+ return view;
3293
+ }
2859
3294
  async function createSession2(opts = {}) {
2860
3295
  const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
2861
3296
  const resolvedEngine = engine === "auto" ? "playwright" : engine;
2862
- let browser;
3297
+ let browser = null;
3298
+ let bunView = null;
2863
3299
  let page;
2864
- if (resolvedEngine === "lightpanda") {
3300
+ if (resolvedEngine === "bun") {
3301
+ if (!isBunWebViewAvailable()) {
3302
+ console.warn("[browser] Bun.WebView requested but not available \u2014 falling back to playwright. Run: bun upgrade --canary");
3303
+ browser = await launchPlaywright({ headless: opts.headless ?? true, viewport: opts.viewport, userAgent: opts.userAgent });
3304
+ page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
3305
+ } else {
3306
+ bunView = new BunWebViewSession({
3307
+ width: opts.viewport?.width ?? 1280,
3308
+ height: opts.viewport?.height ?? 720,
3309
+ profile: opts.name ?? undefined
3310
+ });
3311
+ if (opts.stealth) {}
3312
+ page = createBunProxy(bunView);
3313
+ }
3314
+ } else if (resolvedEngine === "lightpanda") {
2865
3315
  browser = await connectLightpanda();
2866
3316
  const context = await browser.newContext({ viewport: opts.viewport ?? { width: 1280, height: 720 } });
2867
3317
  page = await context.newPage();
@@ -2871,41 +3321,67 @@ async function createSession2(opts = {}) {
2871
3321
  viewport: opts.viewport,
2872
3322
  userAgent: opts.userAgent
2873
3323
  });
2874
- page = await getPage(browser, {
2875
- viewport: opts.viewport,
2876
- userAgent: opts.userAgent
2877
- });
3324
+ page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
2878
3325
  }
3326
+ const sessionName = opts.name ?? (opts.startUrl ? (() => {
3327
+ try {
3328
+ return new URL(opts.startUrl).hostname;
3329
+ } catch {
3330
+ return;
3331
+ }
3332
+ })() : undefined);
2879
3333
  const session = createSession({
2880
- engine: resolvedEngine,
3334
+ engine: bunView ? "bun" : browser ? resolvedEngine : resolvedEngine,
2881
3335
  projectId: opts.projectId,
2882
3336
  agentId: opts.agentId,
2883
3337
  startUrl: opts.startUrl,
2884
- name: opts.name ?? (opts.startUrl ? new URL(opts.startUrl).hostname : undefined)
3338
+ name: sessionName
2885
3339
  });
2886
- if (opts.stealth) {
3340
+ if (opts.stealth && !bunView) {
2887
3341
  try {
2888
3342
  await applyStealthPatches(page);
2889
3343
  } catch {}
2890
3344
  }
2891
3345
  const cleanups = [];
2892
- if (opts.captureNetwork !== false) {
2893
- try {
2894
- cleanups.push(enableNetworkLogging(page, session.id));
2895
- } catch {}
2896
- }
2897
- if (opts.captureConsole !== false) {
3346
+ if (!bunView) {
3347
+ if (opts.captureNetwork !== false) {
3348
+ try {
3349
+ cleanups.push(enableNetworkLogging(page, session.id));
3350
+ } catch {}
3351
+ }
3352
+ if (opts.captureConsole !== false) {
3353
+ try {
3354
+ cleanups.push(enableConsoleCapture(page, session.id));
3355
+ } catch {}
3356
+ }
2898
3357
  try {
2899
- cleanups.push(enableConsoleCapture(page, session.id));
3358
+ cleanups.push(setupDialogHandler(page, session.id));
2900
3359
  } catch {}
3360
+ } else {
3361
+ if (opts.captureConsole !== false) {
3362
+ try {
3363
+ const { logConsoleMessage: logConsoleMessage2 } = await Promise.resolve().then(() => (init_console_log(), exports_console_log));
3364
+ await bunView.addInitScript(`
3365
+ (() => {
3366
+ const orig = { log: console.log, warn: console.warn, error: console.error, debug: console.debug, info: console.info };
3367
+ ['log','warn','error','debug','info'].forEach(level => {
3368
+ console[level] = (...args) => {
3369
+ orig[level](...args);
3370
+ };
3371
+ });
3372
+ })()
3373
+ `);
3374
+ } catch {}
3375
+ }
2901
3376
  }
2902
- try {
2903
- cleanups.push(setupDialogHandler(page, session.id));
2904
- } catch {}
2905
- handles.set(session.id, { browser, page, engine: resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 } });
3377
+ handles.set(session.id, { browser, bunView, page, engine: bunView ? "bun" : resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 } });
2906
3378
  if (opts.startUrl) {
2907
3379
  try {
2908
- await page.goto(opts.startUrl, { waitUntil: "domcontentloaded" });
3380
+ if (bunView) {
3381
+ await bunView.goto(opts.startUrl);
3382
+ } else {
3383
+ await page.goto(opts.startUrl, { waitUntil: "domcontentloaded" });
3384
+ }
2909
3385
  } catch {}
2910
3386
  }
2911
3387
  return { session, page };
@@ -2915,13 +3391,23 @@ function getSessionPage(sessionId) {
2915
3391
  if (!handle)
2916
3392
  throw new SessionNotFoundError(sessionId);
2917
3393
  try {
2918
- handle.page.url();
3394
+ if (handle.bunView) {
3395
+ handle.bunView.url();
3396
+ } else {
3397
+ handle.page.url();
3398
+ }
2919
3399
  } catch {
2920
3400
  handles.delete(sessionId);
2921
3401
  throw new SessionNotFoundError(sessionId);
2922
3402
  }
2923
3403
  return handle.page;
2924
3404
  }
3405
+ function getSessionBunView(sessionId) {
3406
+ return handles.get(sessionId)?.bunView ?? null;
3407
+ }
3408
+ function isBunSession(sessionId) {
3409
+ return handles.get(sessionId)?.engine === "bun";
3410
+ }
2925
3411
  function setSessionPage(sessionId, page) {
2926
3412
  const handle = handles.get(sessionId);
2927
3413
  if (!handle)
@@ -2936,12 +3422,19 @@ async function closeSession2(sessionId) {
2936
3422
  cleanup();
2937
3423
  } catch {}
2938
3424
  }
2939
- try {
2940
- await handle.page.context().close();
2941
- } catch {}
2942
- try {
2943
- await closeBrowser(handle.browser);
2944
- } catch {}
3425
+ if (handle.bunView) {
3426
+ try {
3427
+ await handle.bunView.close();
3428
+ } catch {}
3429
+ } else {
3430
+ try {
3431
+ await handle.page.context().close();
3432
+ } catch {}
3433
+ try {
3434
+ if (handle.browser)
3435
+ await closeBrowser(handle.browser);
3436
+ } catch {}
3437
+ }
2945
3438
  handles.delete(sessionId);
2946
3439
  }
2947
3440
  return closeSession(sessionId);
@@ -2969,6 +3462,7 @@ var init_session = __esm(() => {
2969
3462
  init_sessions();
2970
3463
  init_playwright();
2971
3464
  init_lightpanda();
3465
+ init_bun_webview();
2972
3466
  init_selector();
2973
3467
  init_network();
2974
3468
  init_console();
@@ -2978,13 +3472,34 @@ var init_session = __esm(() => {
2978
3472
  });
2979
3473
 
2980
3474
  // src/lib/snapshot.ts
3475
+ var exports_snapshot = {};
3476
+ __export(exports_snapshot, {
3477
+ takeSnapshot: () => takeSnapshot,
3478
+ takeBunSnapshot: () => takeBunSnapshot,
3479
+ setLastSnapshot: () => setLastSnapshot,
3480
+ hasRefs: () => hasRefs,
3481
+ getSessionRefs: () => getSessionRefs,
3482
+ getRefLocator: () => getRefLocator,
3483
+ getRefInfo: () => getRefInfo,
3484
+ getLastSnapshot: () => getLastSnapshot,
3485
+ diffSnapshots: () => diffSnapshots,
3486
+ clearSessionRefs: () => clearSessionRefs,
3487
+ clearLastSnapshot: () => clearLastSnapshot
3488
+ });
2981
3489
  function getLastSnapshot(sessionId) {
2982
3490
  return lastSnapshots.get(sessionId) ?? null;
2983
3491
  }
2984
3492
  function setLastSnapshot(sessionId, snapshot) {
2985
3493
  lastSnapshots.set(sessionId, snapshot);
2986
3494
  }
3495
+ function clearLastSnapshot(sessionId) {
3496
+ lastSnapshots.delete(sessionId);
3497
+ }
2987
3498
  async function takeSnapshot(page, sessionId) {
3499
+ const isBunView = typeof page.getNativeView === "function" || typeof page.bunView !== "undefined";
3500
+ if (isBunView) {
3501
+ return takeBunSnapshot(page, sessionId);
3502
+ }
2988
3503
  let ariaTree;
2989
3504
  try {
2990
3505
  ariaTree = await page.locator("body").ariaSnapshot();
@@ -3086,6 +3601,21 @@ function getRefLocator(page, sessionId, ref) {
3086
3601
  throw new Error(`Ref ${ref} not found. Available refs: ${[...refMap.keys()].slice(0, 20).join(", ")}`);
3087
3602
  return page.getByRole(entry.role, { name: entry.name }).first();
3088
3603
  }
3604
+ function getRefInfo(sessionId, ref) {
3605
+ const refMap = sessionRefMaps.get(sessionId);
3606
+ if (!refMap)
3607
+ return null;
3608
+ return refMap.get(ref) ?? null;
3609
+ }
3610
+ function getSessionRefs(sessionId) {
3611
+ return sessionRefMaps.get(sessionId) ?? null;
3612
+ }
3613
+ function clearSessionRefs(sessionId) {
3614
+ sessionRefMaps.delete(sessionId);
3615
+ }
3616
+ function hasRefs(sessionId) {
3617
+ return sessionRefMaps.has(sessionId) && (sessionRefMaps.get(sessionId)?.size ?? 0) > 0;
3618
+ }
3089
3619
  function refKey(info) {
3090
3620
  return `${info.role}::${info.name}`;
3091
3621
  }
@@ -3124,6 +3654,71 @@ function diffSnapshots(before, after) {
3124
3654
  const title_changed = before.tree !== after.tree && (added.length > 0 || removed.length > 0 || modified.length > 0);
3125
3655
  return { added, removed, modified, url_changed, title_changed };
3126
3656
  }
3657
+ async function takeBunSnapshot(page, sessionId) {
3658
+ const refs = {};
3659
+ const refMap = new Map;
3660
+ let refCounter = 0;
3661
+ const lines = [];
3662
+ try {
3663
+ const elements = await page.evaluate(`
3664
+ (() => {
3665
+ const SELECTOR = 'a[href], button, input:not([type=hidden]), select, textarea, [role=button], [role=link], [role=checkbox], [role=combobox], [role=menuitem], [role=tab], [role=option]';
3666
+ const els = Array.from(document.querySelectorAll(SELECTOR));
3667
+ return els.slice(0, 100).map(el => {
3668
+ const tag = el.tagName.toLowerCase();
3669
+ const inputType = el.getAttribute('type') ?? '';
3670
+ let role = el.getAttribute('role') || (['a'].includes(tag) ? 'link' : ['button'].includes(tag) ? 'button' : ['input'].includes(tag) ? (inputType === 'checkbox' ? 'checkbox' : inputType === 'radio' ? 'radio' : 'textbox') : ['select'].includes(tag) ? 'combobox' : ['textarea'].includes(tag) ? 'textbox' : tag);
3671
+ const name = (el.getAttribute('aria-label') || el.textContent?.trim() || el.getAttribute('placeholder') || el.getAttribute('title') || el.getAttribute('value') || el.id || '').slice(0, 80);
3672
+ const enabled = !el.disabled && !el.getAttribute('disabled');
3673
+ const style = window.getComputedStyle(el);
3674
+ const visible = style.display !== 'none' && style.visibility !== 'hidden' && el.offsetWidth > 0;
3675
+ const checked = el.type === 'checkbox' || el.type === 'radio' ? el.checked : undefined;
3676
+ const value = ['input', 'select', 'textarea'].includes(tag) && el.type !== 'checkbox' && el.type !== 'radio' ? el.value : undefined;
3677
+ const selector = el.id ? '#' + el.id : (el.getAttribute('aria-label') ? '[aria-label="' + el.getAttribute('aria-label') + '"]' : tag);
3678
+ return { role, name, enabled, visible, checked, value, selector };
3679
+ }).filter(e => e.visible && e.name);
3680
+ })()
3681
+ `);
3682
+ const pageTitle = await page.evaluate("document.title");
3683
+ const pageUrl = typeof page.url === "function" ? page.url() : "";
3684
+ lines.push(`# ${pageTitle || "Page"} (${pageUrl})`);
3685
+ for (const el of elements) {
3686
+ if (!el.name)
3687
+ continue;
3688
+ const ref = `@e${refCounter}`;
3689
+ refCounter++;
3690
+ refs[ref] = {
3691
+ role: el.role,
3692
+ name: el.name,
3693
+ visible: el.visible,
3694
+ enabled: el.enabled,
3695
+ value: el.value,
3696
+ checked: el.checked
3697
+ };
3698
+ refMap.set(ref, { role: el.role, name: el.name, locatorSelector: el.selector });
3699
+ const extras = [];
3700
+ if (el.checked !== undefined)
3701
+ extras.push(`checked=${el.checked}`);
3702
+ if (!el.enabled)
3703
+ extras.push("disabled");
3704
+ if (el.value && el.value !== el.name)
3705
+ extras.push(`value="${el.value.slice(0, 30)}"`);
3706
+ const extrasStr = extras.length ? ` (${extras.join(", ")})` : "";
3707
+ lines.push(`${el.role} "${el.name}" [${ref}]${extrasStr}`);
3708
+ }
3709
+ } catch (err) {
3710
+ lines.push(`# (snapshot error: ${err instanceof Error ? err.message : String(err)})`);
3711
+ }
3712
+ if (sessionId) {
3713
+ sessionRefMaps.set(sessionId, refMap);
3714
+ }
3715
+ return {
3716
+ tree: lines.join(`
3717
+ `),
3718
+ refs,
3719
+ interactive_count: refCounter
3720
+ };
3721
+ }
3127
3722
  var lastSnapshots, sessionRefMaps, INTERACTIVE_ROLES;
3128
3723
  var init_snapshot = __esm(() => {
3129
3724
  lastSnapshots = new Map;
@@ -3468,9 +4063,19 @@ async function getLinks(page, baseUrl) {
3468
4063
  }, baseUrl ?? page.url());
3469
4064
  }
3470
4065
  async function getTitle(page) {
4066
+ if (typeof page.getNativeView === "function") {
4067
+ const nativeView = page.getNativeView();
4068
+ const t = nativeView?.title;
4069
+ return typeof t === "string" && t ? t : "";
4070
+ }
3471
4071
  return page.title();
3472
4072
  }
3473
4073
  async function getUrl(page) {
4074
+ if (typeof page.getNativeView === "function") {
4075
+ const nativeView = page.getNativeView();
4076
+ const u = nativeView?.url;
4077
+ return typeof u === "string" ? u : "";
4078
+ }
3474
4079
  return page.url();
3475
4080
  }
3476
4081
  async function findElements(page, selector) {
@@ -10112,17 +10717,17 @@ var init_gallery = __esm(() => {
10112
10717
  });
10113
10718
 
10114
10719
  // src/lib/screenshot.ts
10115
- import { join as join2 } from "path";
10116
- import { mkdirSync as mkdirSync2 } from "fs";
10117
- import { homedir as homedir2 } from "os";
10720
+ import { join as join3 } from "path";
10721
+ import { mkdirSync as mkdirSync3 } from "fs";
10722
+ import { homedir as homedir3 } from "os";
10118
10723
  function getDataDir2() {
10119
- return process.env["BROWSER_DATA_DIR"] ?? join2(homedir2(), ".browser");
10724
+ return process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser");
10120
10725
  }
10121
10726
  function getScreenshotDir(projectId) {
10122
- const base = join2(getDataDir2(), "screenshots");
10727
+ const base = join3(getDataDir2(), "screenshots");
10123
10728
  const date = new Date().toISOString().split("T")[0];
10124
- const dir = projectId ? join2(base, projectId, date) : join2(base, date);
10125
- mkdirSync2(dir, { recursive: true });
10729
+ const dir = projectId ? join3(base, projectId, date) : join3(base, date);
10730
+ mkdirSync3(dir, { recursive: true });
10126
10731
  return dir;
10127
10732
  }
10128
10733
  async function compressBuffer(raw, format, quality, maxWidth) {
@@ -10137,7 +10742,7 @@ async function compressBuffer(raw, format, quality, maxWidth) {
10137
10742
  }
10138
10743
  }
10139
10744
  async function generateThumbnail(raw, dir, stem) {
10140
- const thumbPath = join2(dir, `${stem}.thumb.webp`);
10745
+ const thumbPath = join3(dir, `${stem}.thumb.webp`);
10141
10746
  const thumbBuffer = await import_sharp.default(raw).resize({ width: 200, withoutEnlargement: true }).webp({ quality: 70, effort: 3 }).toBuffer();
10142
10747
  await Bun.write(thumbPath, thumbBuffer);
10143
10748
  return { path: thumbPath, base64: thumbBuffer.toString("base64") };
@@ -10156,27 +10761,45 @@ async function takeScreenshot(page, opts) {
10156
10761
  type: "png"
10157
10762
  };
10158
10763
  let rawBuffer;
10764
+ const isBunView = typeof page.getNativeView === "function";
10159
10765
  if (opts?.selector) {
10160
- const el = await page.$(opts.selector);
10161
- if (!el)
10162
- throw new BrowserError(`Element not found: ${opts.selector}`, "ELEMENT_NOT_FOUND");
10163
- rawBuffer = await el.screenshot(rawOpts);
10766
+ if (isBunView) {
10767
+ const uint8 = await page.screenshot();
10768
+ rawBuffer = Buffer.from(uint8 instanceof Uint8Array ? uint8 : await uint8);
10769
+ } else {
10770
+ const el = await page.$(opts.selector);
10771
+ if (!el)
10772
+ throw new BrowserError(`Element not found: ${opts.selector}`, "ELEMENT_NOT_FOUND");
10773
+ rawBuffer = await el.screenshot(rawOpts);
10774
+ }
10775
+ } else if (isBunView) {
10776
+ const uint8 = await page.screenshot();
10777
+ rawBuffer = Buffer.from(uint8 instanceof Uint8Array ? uint8 : await uint8);
10164
10778
  } else {
10165
10779
  rawBuffer = await page.screenshot(rawOpts);
10166
10780
  }
10167
10781
  const originalSizeBytes = rawBuffer.length;
10168
10782
  let finalBuffer;
10169
- if (compress && format !== "png") {
10170
- finalBuffer = await compressBuffer(rawBuffer, format, quality ?? 82, maxWidth);
10171
- } else if (compress && format === "png") {
10172
- finalBuffer = await compressBuffer(rawBuffer, "png", quality ?? 9, maxWidth);
10173
- } else {
10783
+ let compressed = true;
10784
+ let fallback = false;
10785
+ try {
10786
+ if (compress && format !== "png") {
10787
+ finalBuffer = await compressBuffer(rawBuffer, format, quality ?? 82, maxWidth);
10788
+ } else if (compress && format === "png") {
10789
+ finalBuffer = await compressBuffer(rawBuffer, "png", quality ?? 9, maxWidth);
10790
+ } else {
10791
+ finalBuffer = rawBuffer;
10792
+ compressed = false;
10793
+ }
10794
+ } catch (sharpErr) {
10795
+ fallback = true;
10796
+ compressed = false;
10174
10797
  finalBuffer = rawBuffer;
10175
10798
  }
10176
10799
  const compressedSizeBytes = finalBuffer.length;
10177
10800
  const compressionRatio = originalSizeBytes > 0 ? compressedSizeBytes / originalSizeBytes : 1;
10178
10801
  const ext = format;
10179
- const screenshotPath = opts?.path ?? join2(dir, `${stem}.${ext}`);
10802
+ const screenshotPath = opts?.path ?? join3(dir, `${stem}.${ext}`);
10180
10803
  await Bun.write(screenshotPath, finalBuffer);
10181
10804
  let thumbnailPath;
10182
10805
  let thumbnailBase64;
@@ -10198,7 +10821,8 @@ async function takeScreenshot(page, opts) {
10198
10821
  compressed_size_bytes: compressedSizeBytes,
10199
10822
  compression_ratio: compressionRatio,
10200
10823
  thumbnail_path: thumbnailPath,
10201
- thumbnail_base64: thumbnailBase64
10824
+ thumbnail_base64: thumbnailBase64,
10825
+ ...fallback ? { fallback: true, compressed: false } : {}
10202
10826
  };
10203
10827
  if (opts?.track !== false) {
10204
10828
  try {
@@ -10235,12 +10859,12 @@ async function takeScreenshot(page, opts) {
10235
10859
  }
10236
10860
  async function generatePDF(page, opts) {
10237
10861
  try {
10238
- const base = join2(getDataDir2(), "pdfs");
10862
+ const base = join3(getDataDir2(), "pdfs");
10239
10863
  const date = new Date().toISOString().split("T")[0];
10240
- const dir = opts?.projectId ? join2(base, opts.projectId, date) : join2(base, date);
10241
- mkdirSync2(dir, { recursive: true });
10864
+ const dir = opts?.projectId ? join3(base, opts.projectId, date) : join3(base, date);
10865
+ mkdirSync3(dir, { recursive: true });
10242
10866
  const timestamp = Date.now();
10243
- const pdfPath = opts?.path ?? join2(dir, `${timestamp}.pdf`);
10867
+ const pdfPath = opts?.path ?? join3(dir, `${timestamp}.pdf`);
10244
10868
  const buffer = await page.pdf({
10245
10869
  path: pdfPath,
10246
10870
  format: opts?.format ?? "A4",
@@ -14839,16 +15463,16 @@ __export(exports_downloads, {
14839
15463
  cleanStaleDownloads: () => cleanStaleDownloads
14840
15464
  });
14841
15465
  import { randomUUID as randomUUID9 } from "crypto";
14842
- import { join as join3, basename, extname } from "path";
14843
- import { mkdirSync as mkdirSync3, existsSync, readdirSync, statSync, unlinkSync, copyFileSync, writeFileSync, readFileSync } from "fs";
14844
- import { homedir as homedir3 } from "os";
15466
+ import { join as join4, basename, extname } from "path";
15467
+ import { mkdirSync as mkdirSync4, existsSync, readdirSync, statSync, unlinkSync, copyFileSync, writeFileSync, readFileSync } from "fs";
15468
+ import { homedir as homedir4 } from "os";
14845
15469
  function getDataDir3() {
14846
- return process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser");
15470
+ return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
14847
15471
  }
14848
15472
  function getDownloadsDir(sessionId) {
14849
- const base = join3(getDataDir3(), "downloads");
14850
- const dir = sessionId ? join3(base, sessionId) : base;
14851
- mkdirSync3(dir, { recursive: true });
15473
+ const base = join4(getDataDir3(), "downloads");
15474
+ const dir = sessionId ? join4(base, sessionId) : base;
15475
+ mkdirSync4(dir, { recursive: true });
14852
15476
  return dir;
14853
15477
  }
14854
15478
  function ensureDownloadsDir() {
@@ -14863,7 +15487,7 @@ function saveToDownloads(buffer, filename, opts) {
14863
15487
  const ext = extname(filename) || "";
14864
15488
  const stem = basename(filename, ext);
14865
15489
  const uniqueName = `${stem}-${id.slice(0, 8)}${ext}`;
14866
- const filePath = join3(dir, uniqueName);
15490
+ const filePath = join4(dir, uniqueName);
14867
15491
  writeFileSync(filePath, buffer);
14868
15492
  const meta = {
14869
15493
  id,
@@ -14898,7 +15522,7 @@ function listDownloads(sessionId) {
14898
15522
  for (const entry of entries) {
14899
15523
  if (entry.endsWith(".meta.json"))
14900
15524
  continue;
14901
- const full = join3(d, entry);
15525
+ const full = join4(d, entry);
14902
15526
  const stat = statSync(full);
14903
15527
  if (stat.isDirectory()) {
14904
15528
  scanDir(full);
@@ -14987,9 +15611,9 @@ var exports_gallery_diff = {};
14987
15611
  __export(exports_gallery_diff, {
14988
15612
  diffImages: () => diffImages
14989
15613
  });
14990
- import { join as join4 } from "path";
14991
- import { mkdirSync as mkdirSync4 } from "fs";
14992
- import { homedir as homedir4 } from "os";
15614
+ import { join as join5 } from "path";
15615
+ import { mkdirSync as mkdirSync5 } from "fs";
15616
+ import { homedir as homedir5 } from "os";
14993
15617
  async function diffImages(path1, path2) {
14994
15618
  const img1 = import_sharp2.default(path1);
14995
15619
  const img2 = import_sharp2.default(path2);
@@ -15020,10 +15644,10 @@ async function diffImages(path1, path2) {
15020
15644
  diffBuffer[i + 2] = Math.round(raw1[i + 2] * 0.4);
15021
15645
  }
15022
15646
  }
15023
- const dataDir = process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
15024
- const diffDir = join4(dataDir, "diffs");
15025
- mkdirSync4(diffDir, { recursive: true });
15026
- const diffPath = join4(diffDir, `diff-${Date.now()}.webp`);
15647
+ const dataDir = process.env["BROWSER_DATA_DIR"] ?? join5(homedir5(), ".browser");
15648
+ const diffDir = join5(dataDir, "diffs");
15649
+ mkdirSync5(diffDir, { recursive: true });
15650
+ const diffPath = join5(diffDir, `diff-${Date.now()}.webp`);
15027
15651
  const diffImageBuffer = await import_sharp2.default(diffBuffer, { raw: { width: w, height: h, channels } }).webp({ quality: 85 }).toBuffer();
15028
15652
  await Bun.write(diffPath, diffImageBuffer);
15029
15653
  return {
@@ -15040,9 +15664,9 @@ var init_gallery_diff = __esm(() => {
15040
15664
  });
15041
15665
 
15042
15666
  // src/lib/files-integration.ts
15043
- import { join as join5 } from "path";
15044
- import { mkdirSync as mkdirSync5, copyFileSync as copyFileSync2 } from "fs";
15045
- import { homedir as homedir5 } from "os";
15667
+ import { join as join6 } from "path";
15668
+ import { mkdirSync as mkdirSync6, copyFileSync as copyFileSync2 } from "fs";
15669
+ import { homedir as homedir6 } from "os";
15046
15670
  async function persistFile(localPath, opts) {
15047
15671
  try {
15048
15672
  const mod = await import("@hasna/files");
@@ -15051,12 +15675,12 @@ async function persistFile(localPath, opts) {
15051
15675
  return { id: ref.id, path: ref.path ?? localPath, permanent: true, provider: "open-files" };
15052
15676
  }
15053
15677
  } catch {}
15054
- const dataDir = process.env["BROWSER_DATA_DIR"] ?? join5(homedir5(), ".browser");
15678
+ const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
15055
15679
  const date = new Date().toISOString().split("T")[0];
15056
- const dir = join5(dataDir, "persistent", date);
15057
- mkdirSync5(dir, { recursive: true });
15680
+ const dir = join6(dataDir, "persistent", date);
15681
+ mkdirSync6(dir, { recursive: true });
15058
15682
  const filename = localPath.split("/").pop() ?? "file";
15059
- const targetPath = join5(dir, filename);
15683
+ const targetPath = join6(dir, filename);
15060
15684
  copyFileSync2(localPath, targetPath);
15061
15685
  return {
15062
15686
  id: `local-${Date.now()}`,
@@ -15149,23 +15773,23 @@ async function closeTab(page, index) {
15149
15773
  }
15150
15774
 
15151
15775
  // src/lib/profiles.ts
15152
- import { mkdirSync as mkdirSync6, existsSync as existsSync3, readdirSync as readdirSync2, rmSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
15153
- import { join as join6 } from "path";
15154
- import { homedir as homedir6 } from "os";
15776
+ import { mkdirSync as mkdirSync7, existsSync as existsSync3, readdirSync as readdirSync2, rmSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
15777
+ import { join as join7 } from "path";
15778
+ import { homedir as homedir7 } from "os";
15155
15779
  function getProfilesDir() {
15156
- const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
15157
- const dir = join6(dataDir, "profiles");
15158
- mkdirSync6(dir, { recursive: true });
15780
+ const dataDir = process.env["BROWSER_DATA_DIR"] ?? join7(homedir7(), ".browser");
15781
+ const dir = join7(dataDir, "profiles");
15782
+ mkdirSync7(dir, { recursive: true });
15159
15783
  return dir;
15160
15784
  }
15161
- function getProfileDir(name) {
15162
- return join6(getProfilesDir(), name);
15785
+ function getProfileDir2(name) {
15786
+ return join7(getProfilesDir(), name);
15163
15787
  }
15164
15788
  async function saveProfile(page, name) {
15165
- const dir = getProfileDir(name);
15166
- mkdirSync6(dir, { recursive: true });
15789
+ const dir = getProfileDir2(name);
15790
+ mkdirSync7(dir, { recursive: true });
15167
15791
  const cookies = await page.context().cookies();
15168
- writeFileSync2(join6(dir, "cookies.json"), JSON.stringify(cookies, null, 2));
15792
+ writeFileSync2(join7(dir, "cookies.json"), JSON.stringify(cookies, null, 2));
15169
15793
  let localStorage2 = {};
15170
15794
  try {
15171
15795
  localStorage2 = await page.evaluate(() => {
@@ -15177,11 +15801,11 @@ async function saveProfile(page, name) {
15177
15801
  return result;
15178
15802
  });
15179
15803
  } catch {}
15180
- writeFileSync2(join6(dir, "storage.json"), JSON.stringify(localStorage2, null, 2));
15804
+ writeFileSync2(join7(dir, "storage.json"), JSON.stringify(localStorage2, null, 2));
15181
15805
  const savedAt = new Date().toISOString();
15182
15806
  const url = page.url();
15183
15807
  const meta = { saved_at: savedAt, url };
15184
- writeFileSync2(join6(dir, "meta.json"), JSON.stringify(meta, null, 2));
15808
+ writeFileSync2(join7(dir, "meta.json"), JSON.stringify(meta, null, 2));
15185
15809
  return {
15186
15810
  name,
15187
15811
  saved_at: savedAt,
@@ -15191,13 +15815,13 @@ async function saveProfile(page, name) {
15191
15815
  };
15192
15816
  }
15193
15817
  function loadProfile(name) {
15194
- const dir = getProfileDir(name);
15818
+ const dir = getProfileDir2(name);
15195
15819
  if (!existsSync3(dir)) {
15196
15820
  throw new Error(`Profile not found: ${name}`);
15197
15821
  }
15198
- const cookiesPath = join6(dir, "cookies.json");
15199
- const storagePath = join6(dir, "storage.json");
15200
- const metaPath2 = join6(dir, "meta.json");
15822
+ const cookiesPath = join7(dir, "cookies.json");
15823
+ const storagePath = join7(dir, "storage.json");
15824
+ const metaPath2 = join7(dir, "meta.json");
15201
15825
  const cookies = existsSync3(cookiesPath) ? JSON.parse(readFileSync2(cookiesPath, "utf8")) : [];
15202
15826
  const localStorage2 = existsSync3(storagePath) ? JSON.parse(readFileSync2(storagePath, "utf8")) : {};
15203
15827
  let savedAt = new Date().toISOString();
@@ -15238,24 +15862,24 @@ function listProfiles() {
15238
15862
  if (!entry.isDirectory())
15239
15863
  continue;
15240
15864
  const name = entry.name;
15241
- const profileDir = join6(dir, name);
15865
+ const profileDir = join7(dir, name);
15242
15866
  let savedAt = "";
15243
15867
  let url;
15244
15868
  let cookieCount = 0;
15245
15869
  let storageKeyCount = 0;
15246
15870
  try {
15247
- const metaPath2 = join6(profileDir, "meta.json");
15871
+ const metaPath2 = join7(profileDir, "meta.json");
15248
15872
  if (existsSync3(metaPath2)) {
15249
15873
  const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
15250
15874
  savedAt = meta.saved_at ?? "";
15251
15875
  url = meta.url;
15252
15876
  }
15253
- const cookiesPath = join6(profileDir, "cookies.json");
15877
+ const cookiesPath = join7(profileDir, "cookies.json");
15254
15878
  if (existsSync3(cookiesPath)) {
15255
15879
  const cookies = JSON.parse(readFileSync2(cookiesPath, "utf8"));
15256
15880
  cookieCount = Array.isArray(cookies) ? cookies.length : 0;
15257
15881
  }
15258
- const storagePath = join6(profileDir, "storage.json");
15882
+ const storagePath = join7(profileDir, "storage.json");
15259
15883
  if (existsSync3(storagePath)) {
15260
15884
  const storage = JSON.parse(readFileSync2(storagePath, "utf8"));
15261
15885
  storageKeyCount = Object.keys(storage).length;
@@ -15272,7 +15896,7 @@ function listProfiles() {
15272
15896
  return profiles.sort((a, b) => b.saved_at.localeCompare(a.saved_at));
15273
15897
  }
15274
15898
  function deleteProfile(name) {
15275
- const dir = getProfileDir(name);
15899
+ const dir = getProfileDir2(name);
15276
15900
  if (!existsSync3(dir))
15277
15901
  return false;
15278
15902
  try {
@@ -15298,7 +15922,8 @@ async function annotateScreenshot(page, sessionId) {
15298
15922
  const annotations = [];
15299
15923
  const labelToRef = {};
15300
15924
  let labelCounter = 1;
15301
- for (const [ref, info] of Object.entries(snapshot.refs)) {
15925
+ const refsToAnnotate = Object.entries(snapshot.refs).slice(0, MAX_ANNOTATIONS);
15926
+ for (const [ref, info] of refsToAnnotate) {
15302
15927
  try {
15303
15928
  const locator = page.getByRole(info.role, { name: info.name }).first();
15304
15929
  const box = await locator.boundingBox();
@@ -15337,7 +15962,7 @@ async function annotateScreenshot(page, sessionId) {
15337
15962
  const annotatedBuffer = await import_sharp3.default(rawBuffer).composite([{ input: Buffer.from(svg), top: 0, left: 0 }]).webp({ quality: 85 }).toBuffer();
15338
15963
  return { buffer: annotatedBuffer, annotations, labelToRef };
15339
15964
  }
15340
- var import_sharp3;
15965
+ var import_sharp3, MAX_ANNOTATIONS = 40;
15341
15966
  var init_annotate = __esm(() => {
15342
15967
  init_snapshot();
15343
15968
  import_sharp3 = __toESM(require_lib(), 1);
@@ -15347,6 +15972,8 @@ var init_annotate = __esm(() => {
15347
15972
  var exports_mcp = {};
15348
15973
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
15349
15974
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15975
+ import { readFileSync as readFileSync3 } from "fs";
15976
+ import { join as join8 } from "path";
15350
15977
  function json(data) {
15351
15978
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
15352
15979
  }
@@ -15358,7 +15985,7 @@ function err(e) {
15358
15985
  isError: true
15359
15986
  };
15360
15987
  }
15361
- var networkLogCleanup, consoleCaptureCleanup, harCaptures, server, activeWatchHandles, transport;
15988
+ var _pkg, networkLogCleanup, consoleCaptureCleanup, harCaptures, server, activeWatchHandles, _startupToolCount, transport;
15362
15989
  var init_mcp = __esm(async () => {
15363
15990
  init_zod();
15364
15991
  init_session();
@@ -15382,6 +16009,7 @@ var init_mcp = __esm(async () => {
15382
16009
  init_dialogs();
15383
16010
  init_profiles();
15384
16011
  init_types();
16012
+ _pkg = JSON.parse(readFileSync3(join8(import.meta.dir, "../../package.json"), "utf8"));
15385
16013
  networkLogCleanup = new Map;
15386
16014
  consoleCaptureCleanup = new Map;
15387
16015
  harCaptures = new Map;
@@ -15390,7 +16018,7 @@ var init_mcp = __esm(async () => {
15390
16018
  version: "0.0.1"
15391
16019
  });
15392
16020
  server.tool("browser_session_create", "Create a new browser session with the specified engine", {
15393
- engine: exports_external.enum(["playwright", "cdp", "lightpanda", "auto"]).optional().default("auto"),
16021
+ engine: exports_external.enum(["playwright", "cdp", "lightpanda", "bun", "auto"]).optional().default("auto"),
15394
16022
  use_case: exports_external.string().optional(),
15395
16023
  project_id: exports_external.string().optional(),
15396
16024
  agent_id: exports_external.string().optional(),
@@ -15436,23 +16064,71 @@ var init_mcp = __esm(async () => {
15436
16064
  return err(e);
15437
16065
  }
15438
16066
  });
15439
- server.tool("browser_navigate", "Navigate to a URL. Returns title + thumbnail + accessibility snapshot preview with refs.", { session_id: exports_external.string(), url: exports_external.string(), timeout: exports_external.number().optional().default(30000), auto_snapshot: exports_external.boolean().optional().default(true), auto_thumbnail: exports_external.boolean().optional().default(true) }, async ({ session_id, url, timeout, auto_snapshot, auto_thumbnail }) => {
16067
+ server.tool("browser_navigate", "Navigate to a URL. Auto-detects redirects, auto-names session, returns compact refs + thumbnail.", {
16068
+ session_id: exports_external.string(),
16069
+ url: exports_external.string(),
16070
+ timeout: exports_external.number().optional().default(30000),
16071
+ auto_snapshot: exports_external.boolean().optional().default(true),
16072
+ auto_thumbnail: exports_external.boolean().optional().default(true)
16073
+ }, async ({ session_id, url, timeout, auto_snapshot, auto_thumbnail }) => {
15440
16074
  try {
15441
16075
  const page = getSessionPage(session_id);
15442
- await navigate(page, url, timeout);
16076
+ if (isBunSession(session_id)) {
16077
+ const bunView = getSessionBunView(session_id);
16078
+ await bunView.goto(url, { timeout });
16079
+ await new Promise((r) => setTimeout(r, 500));
16080
+ } else {
16081
+ await navigate(page, url, timeout);
16082
+ }
15443
16083
  const title = await getTitle(page);
15444
16084
  const current_url = await getUrl(page);
15445
- const result = { url, title, current_url };
16085
+ const redirected = current_url !== url && current_url !== url + "/" && url !== current_url.replace(/\/$/, "");
16086
+ let redirect_type;
16087
+ if (redirected) {
16088
+ try {
16089
+ const reqHost = new URL(url).hostname;
16090
+ const resHost = new URL(current_url).hostname;
16091
+ const reqPath = new URL(url).pathname;
16092
+ const resPath = new URL(current_url).pathname;
16093
+ if (reqHost !== resHost)
16094
+ redirect_type = "canonical";
16095
+ else if (resPath.match(/\/[a-z]{2}-[a-z]{2}\//))
16096
+ redirect_type = "geo";
16097
+ else if (current_url.includes("login") || current_url.includes("signin"))
16098
+ redirect_type = "auth";
16099
+ else
16100
+ redirect_type = "unknown";
16101
+ } catch {}
16102
+ }
16103
+ try {
16104
+ const session = getSession2(session_id);
16105
+ if (!session.name) {
16106
+ const hostname = new URL(current_url).hostname;
16107
+ renameSession2(session_id, hostname);
16108
+ }
16109
+ } catch {}
16110
+ const result = {
16111
+ url,
16112
+ title,
16113
+ current_url,
16114
+ redirected,
16115
+ ...redirect_type ? { redirect_type } : {}
16116
+ };
15446
16117
  if (auto_thumbnail) {
15447
16118
  try {
15448
16119
  const ss = await takeScreenshot(page, { maxWidth: 400, quality: 60, track: false, thumbnail: false });
15449
16120
  result.thumbnail_base64 = ss.base64.length > 50000 ? "" : ss.base64;
15450
16121
  } catch {}
15451
16122
  }
16123
+ if (isBunSession(session_id) && auto_snapshot) {
16124
+ await new Promise((r) => setTimeout(r, 200));
16125
+ }
15452
16126
  if (auto_snapshot) {
15453
16127
  try {
15454
16128
  const snap = await takeSnapshot(page, session_id);
15455
- result.snapshot_preview = snap.tree.slice(0, 3000);
16129
+ setLastSnapshot(session_id, snap);
16130
+ const refEntries = Object.entries(snap.refs).slice(0, 30);
16131
+ result.snapshot_refs = refEntries.map(([ref, info]) => `${info.role}:${info.name.slice(0, 50)} [${ref}]`).join(", ");
15456
16132
  result.interactive_count = snap.interactive_count;
15457
16133
  result.has_errors = getConsoleLog(session_id, "error").length > 0;
15458
16134
  } catch {}
@@ -15558,7 +16234,7 @@ var init_mcp = __esm(async () => {
15558
16234
  return err(e);
15559
16235
  }
15560
16236
  });
15561
- server.tool("browser_check", "Check or uncheck a checkbox by ref or selector", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), checked: exports_external.boolean() }, async ({ session_id, selector, ref, checked }) => {
16237
+ server.tool("browser_toggle", "Check or uncheck a checkbox by ref or selector", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), checked: exports_external.boolean() }, async ({ session_id, selector, ref, checked }) => {
15562
16238
  try {
15563
16239
  const page = getSessionPage(session_id);
15564
16240
  if (ref) {
@@ -15649,12 +16325,33 @@ var init_mcp = __esm(async () => {
15649
16325
  return err(e);
15650
16326
  }
15651
16327
  });
15652
- server.tool("browser_snapshot", "Get a structured accessibility snapshot with element refs (@e0, @e1...). Use refs in browser_click, browser_type, etc.", { session_id: exports_external.string() }, async ({ session_id }) => {
16328
+ server.tool("browser_snapshot", "Get accessibility snapshot with element refs (@e0, @e1...). Use compact=true (default) for token-efficient output. Use refs in browser_click, browser_type, etc.", {
16329
+ session_id: exports_external.string(),
16330
+ compact: exports_external.boolean().optional().default(true),
16331
+ max_refs: exports_external.number().optional().default(50),
16332
+ full_tree: exports_external.boolean().optional().default(false)
16333
+ }, async ({ session_id, compact, max_refs, full_tree }) => {
15653
16334
  try {
15654
16335
  const page = getSessionPage(session_id);
15655
16336
  const result = await takeSnapshot(page, session_id);
15656
16337
  setLastSnapshot(session_id, result);
15657
- return json({ snapshot: result.tree, refs: result.refs, interactive_count: result.interactive_count });
16338
+ const refEntries = Object.entries(result.refs).slice(0, max_refs);
16339
+ const limitedRefs = Object.fromEntries(refEntries);
16340
+ const truncated = Object.keys(result.refs).length > max_refs;
16341
+ if (compact && !full_tree) {
16342
+ const compactRefs = refEntries.map(([ref, info]) => `${info.role}:${info.name.slice(0, 60)} [${ref}]${info.checked !== undefined ? ` checked=${info.checked}` : ""}${!info.enabled ? " disabled" : ""}`).join(`
16343
+ `);
16344
+ return json({
16345
+ snapshot_compact: compactRefs,
16346
+ interactive_count: result.interactive_count,
16347
+ shown_count: refEntries.length,
16348
+ truncated,
16349
+ refs: limitedRefs
16350
+ });
16351
+ }
16352
+ const tree = full_tree ? result.tree : result.tree.slice(0, 5000) + (result.tree.length > 5000 ? `
16353
+ ... (truncated \u2014 use full_tree=true for complete)` : "");
16354
+ return json({ snapshot: tree, refs: limitedRefs, interactive_count: result.interactive_count, truncated });
15658
16355
  } catch (e) {
15659
16356
  return err(e);
15660
16357
  }
@@ -15933,7 +16630,7 @@ var init_mcp = __esm(async () => {
15933
16630
  max_pages: exports_external.number().optional().default(50),
15934
16631
  same_domain: exports_external.boolean().optional().default(true),
15935
16632
  project_id: exports_external.string().optional(),
15936
- engine: exports_external.enum(["playwright", "cdp", "lightpanda", "auto"]).optional().default("auto")
16633
+ engine: exports_external.enum(["playwright", "cdp", "lightpanda", "bun", "auto"]).optional().default("auto")
15937
16634
  }, async ({ url, max_depth, max_pages, same_domain, project_id, engine }) => {
15938
16635
  try {
15939
16636
  const result = await crawl(url, {
@@ -16136,40 +16833,6 @@ var init_mcp = __esm(async () => {
16136
16833
  return err(e);
16137
16834
  }
16138
16835
  });
16139
- server.tool("browser_page_check", "One-call page summary: page info + console errors + performance metrics + thumbnail + accessibility snapshot preview. Replaces 4-5 separate tool calls.", { session_id: exports_external.string() }, async ({ session_id }) => {
16140
- try {
16141
- const page = getSessionPage(session_id);
16142
- const info = await getPageInfo(page);
16143
- const errors2 = getConsoleLog(session_id, "error");
16144
- info.has_console_errors = errors2.length > 0;
16145
- let perf = {};
16146
- try {
16147
- perf = await getPerformanceMetrics(page);
16148
- } catch {}
16149
- let thumbnail_base64 = "";
16150
- try {
16151
- const ss = await takeScreenshot(page, { maxWidth: 400, quality: 60, track: false, thumbnail: false });
16152
- thumbnail_base64 = ss.base64;
16153
- } catch {}
16154
- let snapshot_preview = "";
16155
- let interactive_count = 0;
16156
- try {
16157
- const snap = await takeSnapshot(page, session_id);
16158
- snapshot_preview = snap.tree.slice(0, 2000);
16159
- interactive_count = snap.interactive_count;
16160
- } catch {}
16161
- return json({
16162
- ...info,
16163
- error_count: errors2.length,
16164
- performance: perf,
16165
- thumbnail_base64: thumbnail_base64.length > 50000 ? "" : thumbnail_base64,
16166
- snapshot_preview,
16167
- interactive_count
16168
- });
16169
- } catch (e) {
16170
- return err(e);
16171
- }
16172
- });
16173
16836
  server.tool("browser_gallery_list", "List screenshot gallery entries with optional filters", {
16174
16837
  project_id: exports_external.string().optional(),
16175
16838
  session_id: exports_external.string().optional(),
@@ -16494,7 +17157,7 @@ var init_mcp = __esm(async () => {
16494
17157
  { tool: "browser_hover", description: "Hover over an element" },
16495
17158
  { tool: "browser_scroll", description: "Scroll the page" },
16496
17159
  { tool: "browser_select", description: "Select a dropdown option" },
16497
- { tool: "browser_check", description: "Check/uncheck a checkbox" },
17160
+ { tool: "browser_toggle", description: "Check/uncheck a checkbox" },
16498
17161
  { tool: "browser_upload", description: "Upload a file to an input" },
16499
17162
  { tool: "browser_press_key", description: "Press a keyboard key" },
16500
17163
  { tool: "browser_wait", description: "Wait for a selector to appear" },
@@ -16514,9 +17177,10 @@ var init_mcp = __esm(async () => {
16514
17177
  { tool: "browser_evaluate", description: "Execute JavaScript in page context" }
16515
17178
  ],
16516
17179
  Capture: [
16517
- { tool: "browser_screenshot", description: "Take a screenshot (PNG/JPEG/WebP)" },
17180
+ { tool: "browser_screenshot", description: "Take a screenshot (PNG/JPEG/WebP, annotate=true for labels)" },
16518
17181
  { tool: "browser_pdf", description: "Generate a PDF of the page" },
16519
- { tool: "browser_scroll_and_screenshot", description: "Scroll then screenshot in one call" }
17182
+ { tool: "browser_scroll_and_screenshot", description: "Scroll then screenshot in one call" },
17183
+ { tool: "browser_scroll_to_element", description: "Scroll element into view + screenshot" }
16520
17184
  ],
16521
17185
  Storage: [
16522
17186
  { tool: "browser_cookies_get", description: "Get cookies" },
@@ -16595,7 +17259,8 @@ var init_mcp = __esm(async () => {
16595
17259
  { tool: "browser_tab_close", description: "Close a tab by index" }
16596
17260
  ],
16597
17261
  Meta: [
16598
- { tool: "browser_page_check", description: "One-call page summary with diagnostics" },
17262
+ { tool: "browser_check", description: "RECOMMENDED: One-call page summary with diagnostics" },
17263
+ { tool: "browser_version", description: "Show running binary version and tool count" },
16599
17264
  { tool: "browser_help", description: "Show this help (all tools)" },
16600
17265
  { tool: "browser_snapshot_diff", description: "Diff current snapshot vs previous" },
16601
17266
  { tool: "browser_watch_start", description: "Watch page for DOM changes" },
@@ -16609,6 +17274,88 @@ var init_mcp = __esm(async () => {
16609
17274
  return err(e);
16610
17275
  }
16611
17276
  });
17277
+ server.tool("browser_version", "Get the running browser MCP version, tool count, and environment info. Use this to verify which binary is active.", {}, async () => {
17278
+ try {
17279
+ const { getDataDir: getDataDir4 } = await Promise.resolve().then(() => (init_schema(), exports_schema));
17280
+ const toolCount = Object.keys(server._registeredTools ?? {}).length;
17281
+ return json({
17282
+ version: _pkg.version,
17283
+ mcp_tools_count: toolCount,
17284
+ bun_version: Bun.version,
17285
+ data_dir: getDataDir4(),
17286
+ node_env: process.env["NODE_ENV"] ?? "production"
17287
+ });
17288
+ } catch (e) {
17289
+ return err(e);
17290
+ }
17291
+ });
17292
+ server.tool("browser_scroll_to_element", "Scroll an element into view (by ref or selector) then optionally take a screenshot of it. Replaces scroll + wait + screenshot pattern.", {
17293
+ session_id: exports_external.string(),
17294
+ selector: exports_external.string().optional(),
17295
+ ref: exports_external.string().optional(),
17296
+ screenshot: exports_external.boolean().optional().default(true),
17297
+ wait_ms: exports_external.number().optional().default(200)
17298
+ }, async ({ session_id, selector, ref, screenshot: doScreenshot, wait_ms }) => {
17299
+ try {
17300
+ const page = getSessionPage(session_id);
17301
+ let locator;
17302
+ if (ref) {
17303
+ const { getRefLocator: getRefLocator2 } = await Promise.resolve().then(() => (init_snapshot(), exports_snapshot));
17304
+ locator = getRefLocator2(page, session_id, ref);
17305
+ } else if (selector) {
17306
+ locator = page.locator(selector).first();
17307
+ } else {
17308
+ return err(new Error("Either ref or selector is required"));
17309
+ }
17310
+ await locator.scrollIntoViewIfNeeded();
17311
+ await new Promise((r) => setTimeout(r, wait_ms));
17312
+ const result = { scrolled: ref ?? selector };
17313
+ if (doScreenshot) {
17314
+ try {
17315
+ const ss = await takeScreenshot(page, { selector, track: false });
17316
+ ss.url = page.url();
17317
+ if (ss.base64.length > 50000) {
17318
+ ss.base64_truncated = true;
17319
+ ss.base64 = ss.thumbnail_base64 ?? "";
17320
+ }
17321
+ result.screenshot = ss;
17322
+ } catch {}
17323
+ }
17324
+ return json(result);
17325
+ } catch (e) {
17326
+ return err(e);
17327
+ }
17328
+ });
17329
+ server.tool("browser_check", "RECOMMENDED FIRST CALL: one-shot page summary \u2014 url, title, errors, performance, thumbnail, refs. Replaces 4+ separate tool calls.", { session_id: exports_external.string() }, async ({ session_id }) => {
17330
+ try {
17331
+ const page = getSessionPage(session_id);
17332
+ const info = await getPageInfo(page);
17333
+ const errors2 = getConsoleLog(session_id, "error");
17334
+ info.has_console_errors = errors2.length > 0;
17335
+ let perf = {};
17336
+ try {
17337
+ perf = await getPerformanceMetrics(page);
17338
+ } catch {}
17339
+ let thumbnail_base64 = "";
17340
+ try {
17341
+ const ss = await takeScreenshot(page, { maxWidth: 400, quality: 60, track: false, thumbnail: false });
17342
+ thumbnail_base64 = ss.base64.length > 50000 ? "" : ss.base64;
17343
+ } catch {}
17344
+ let snapshot_refs = "";
17345
+ let interactive_count = 0;
17346
+ try {
17347
+ const snap = await takeSnapshot(page, session_id);
17348
+ setLastSnapshot(session_id, snap);
17349
+ interactive_count = snap.interactive_count;
17350
+ snapshot_refs = Object.entries(snap.refs).slice(0, 30).map(([ref, i]) => `${i.role}:${i.name.slice(0, 50)} [${ref}]`).join(", ");
17351
+ } catch {}
17352
+ return json({ ...info, error_count: errors2.length, performance: perf, thumbnail_base64, snapshot_refs, interactive_count });
17353
+ } catch (e) {
17354
+ return err(e);
17355
+ }
17356
+ });
17357
+ _startupToolCount = Object.keys(server._registeredTools ?? {}).length;
17358
+ console.error(`@hasna/browser v${_pkg.version} \u2014 ${_startupToolCount} tools | data: ${(await Promise.resolve().then(() => (init_schema(), exports_schema))).getDataDir()}`);
16612
17359
  transport = new StdioServerTransport;
16613
17360
  await server.connect(transport);
16614
17361
  });
@@ -16651,7 +17398,7 @@ var init_snapshots = __esm(() => {
16651
17398
 
16652
17399
  // src/server/index.ts
16653
17400
  var exports_server = {};
16654
- import { join as join7 } from "path";
17401
+ import { join as join9 } from "path";
16655
17402
  import { existsSync as existsSync4 } from "fs";
16656
17403
  function ok(data, status = 200) {
16657
17404
  return new Response(JSON.stringify(data), {
@@ -16945,13 +17692,13 @@ var init_server = __esm(() => {
16945
17692
  const id = path.split("/")[3];
16946
17693
  return ok({ deleted: deleteDownload(id) });
16947
17694
  }
16948
- const dashboardDist = join7(import.meta.dir, "../../dashboard/dist");
17695
+ const dashboardDist = join9(import.meta.dir, "../../dashboard/dist");
16949
17696
  if (existsSync4(dashboardDist)) {
16950
- const filePath = path === "/" ? join7(dashboardDist, "index.html") : join7(dashboardDist, path);
17697
+ const filePath = path === "/" ? join9(dashboardDist, "index.html") : join9(dashboardDist, path);
16951
17698
  if (existsSync4(filePath)) {
16952
17699
  return new Response(Bun.file(filePath), { headers: CORS_HEADERS });
16953
17700
  }
16954
- return new Response(Bun.file(join7(dashboardDist, "index.html")), { headers: CORS_HEADERS });
17701
+ return new Response(Bun.file(join9(dashboardDist, "index.html")), { headers: CORS_HEADERS });
16955
17702
  }
16956
17703
  if (path === "/" || path === "") {
16957
17704
  return new Response("@hasna/browser REST API running. Dashboard not built.", {
@@ -16993,10 +17740,10 @@ init_projects();
16993
17740
  init_recorder();
16994
17741
  init_recordings();
16995
17742
  init_lightpanda();
16996
- import { readFileSync as readFileSync3 } from "fs";
16997
- import { join as join8 } from "path";
17743
+ import { readFileSync as readFileSync4 } from "fs";
17744
+ import { join as join10 } from "path";
16998
17745
  import chalk from "chalk";
16999
- var pkg = JSON.parse(readFileSync3(join8(import.meta.dir, "../../package.json"), "utf8"));
17746
+ var pkg = JSON.parse(readFileSync4(join10(import.meta.dir, "../../package.json"), "utf8"));
17000
17747
  var program2 = new Command;
17001
17748
  program2.name("browser").description("@hasna/browser \u2014 general-purpose browser agent CLI").version(pkg.version);
17002
17749
  program2.command("navigate <url>").description("Navigate to a URL and optionally take a screenshot").option("--engine <engine>", "Browser engine: playwright|cdp|lightpanda|auto", "auto").option("--screenshot", "Take a screenshot after navigation").option("--extract", "Extract page text after navigation").option("--headless", "Run in headless mode (default: true)", true).action(async (url, opts) => {