@hasna/browser 0.0.2 → 0.0.3

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
@@ -2525,6 +2525,336 @@ var init_selector = __esm(() => {
2525
2525
  };
2526
2526
  });
2527
2527
 
2528
+ // src/db/network-log.ts
2529
+ import { randomUUID as randomUUID2 } from "crypto";
2530
+ function logRequest(data) {
2531
+ const db = getDatabase();
2532
+ const id = randomUUID2();
2533
+ db.prepare(`INSERT INTO network_log (id, session_id, method, url, status_code, request_headers,
2534
+ response_headers, request_body, body_size, duration_ms, resource_type)
2535
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, data.session_id, data.method, data.url, data.status_code ?? null, data.request_headers ?? null, data.response_headers ?? null, data.request_body ?? null, data.body_size ?? null, data.duration_ms ?? null, data.resource_type ?? null);
2536
+ return getNetworkRequest(id);
2537
+ }
2538
+ function getNetworkRequest(id) {
2539
+ const db = getDatabase();
2540
+ return db.query("SELECT * FROM network_log WHERE id = ?").get(id) ?? null;
2541
+ }
2542
+ function getNetworkLog(sessionId) {
2543
+ const db = getDatabase();
2544
+ return db.query("SELECT * FROM network_log WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
2545
+ }
2546
+ function clearNetworkLog(sessionId) {
2547
+ const db = getDatabase();
2548
+ db.prepare("DELETE FROM network_log WHERE session_id = ?").run(sessionId);
2549
+ }
2550
+ var init_network_log = __esm(() => {
2551
+ init_schema();
2552
+ });
2553
+
2554
+ // src/lib/network.ts
2555
+ function enableNetworkLogging(page, sessionId) {
2556
+ const requestStart = new Map;
2557
+ const onRequest = (req) => {
2558
+ requestStart.set(req.url(), Date.now());
2559
+ };
2560
+ const onResponse = (res) => {
2561
+ const start = requestStart.get(res.url()) ?? Date.now();
2562
+ const duration = Date.now() - start;
2563
+ const req = res.request();
2564
+ try {
2565
+ logRequest({
2566
+ session_id: sessionId,
2567
+ method: req.method(),
2568
+ url: res.url(),
2569
+ status_code: res.status(),
2570
+ request_headers: JSON.stringify(req.headers()),
2571
+ response_headers: JSON.stringify(res.headers()),
2572
+ body_size: res.headers()["content-length"] != null ? parseInt(res.headers()["content-length"]) : undefined,
2573
+ duration_ms: duration,
2574
+ resource_type: req.resourceType()
2575
+ });
2576
+ } catch {}
2577
+ };
2578
+ page.on("request", onRequest);
2579
+ page.on("response", onResponse);
2580
+ return () => {
2581
+ page.off("request", onRequest);
2582
+ page.off("response", onResponse);
2583
+ };
2584
+ }
2585
+ async function addInterceptRule(page, rule) {
2586
+ await page.route(rule.pattern, async (route) => {
2587
+ if (rule.action === "block") {
2588
+ await route.abort();
2589
+ } else if (rule.action === "modify" && rule.response) {
2590
+ await route.fulfill({
2591
+ status: rule.response.status,
2592
+ body: rule.response.body,
2593
+ headers: rule.response.headers
2594
+ });
2595
+ } else {
2596
+ await route.continue();
2597
+ }
2598
+ });
2599
+ }
2600
+ function startHAR(page) {
2601
+ const entries = [];
2602
+ const requestStart = new Map;
2603
+ const onRequest = (req) => {
2604
+ requestStart.set(req.url() + req.method(), {
2605
+ time: Date.now(),
2606
+ method: req.method(),
2607
+ headers: req.headers(),
2608
+ postData: req.postData() ?? undefined
2609
+ });
2610
+ };
2611
+ const onResponse = async (res) => {
2612
+ const key = res.url() + res.request().method();
2613
+ const start = requestStart.get(key);
2614
+ if (!start)
2615
+ return;
2616
+ const duration = Date.now() - start.time;
2617
+ const entry = {
2618
+ startedDateTime: new Date(start.time).toISOString(),
2619
+ time: duration,
2620
+ request: {
2621
+ method: start.method,
2622
+ url: res.url(),
2623
+ headers: Object.entries(start.headers).map(([name, value]) => ({ name, value })),
2624
+ postData: start.postData ? { text: start.postData } : undefined
2625
+ },
2626
+ response: {
2627
+ status: res.status(),
2628
+ statusText: res.statusText(),
2629
+ headers: Object.entries(res.headers()).map(([name, value]) => ({ name, value })),
2630
+ content: {
2631
+ size: parseInt(res.headers()["content-length"] ?? "0") || 0,
2632
+ mimeType: res.headers()["content-type"] ?? "application/octet-stream"
2633
+ }
2634
+ },
2635
+ timings: { send: 0, wait: duration, receive: 0 }
2636
+ };
2637
+ entries.push(entry);
2638
+ requestStart.delete(key);
2639
+ };
2640
+ page.on("request", onRequest);
2641
+ page.on("response", onResponse);
2642
+ return {
2643
+ entries,
2644
+ stop: () => {
2645
+ page.off("request", onRequest);
2646
+ page.off("response", onResponse);
2647
+ return {
2648
+ log: {
2649
+ version: "1.2",
2650
+ creator: { name: "@hasna/browser", version: "0.0.1" },
2651
+ entries
2652
+ }
2653
+ };
2654
+ }
2655
+ };
2656
+ }
2657
+ var init_network = __esm(() => {
2658
+ init_network_log();
2659
+ });
2660
+
2661
+ // src/db/console-log.ts
2662
+ var exports_console_log = {};
2663
+ __export(exports_console_log, {
2664
+ logConsoleMessage: () => logConsoleMessage,
2665
+ getConsoleMessage: () => getConsoleMessage,
2666
+ getConsoleLog: () => getConsoleLog,
2667
+ clearConsoleLog: () => clearConsoleLog
2668
+ });
2669
+ import { randomUUID as randomUUID3 } from "crypto";
2670
+ function logConsoleMessage(data) {
2671
+ const db = getDatabase();
2672
+ const id = randomUUID3();
2673
+ db.prepare("INSERT INTO console_log (id, session_id, level, message, source, line_number) VALUES (?, ?, ?, ?, ?, ?)").run(id, data.session_id, data.level, data.message, data.source ?? null, data.line_number ?? null);
2674
+ return getConsoleMessage(id);
2675
+ }
2676
+ function getConsoleMessage(id) {
2677
+ const db = getDatabase();
2678
+ return db.query("SELECT * FROM console_log WHERE id = ?").get(id) ?? null;
2679
+ }
2680
+ function getConsoleLog(sessionId, level) {
2681
+ const db = getDatabase();
2682
+ if (level) {
2683
+ return db.query("SELECT * FROM console_log WHERE session_id = ? AND level = ? ORDER BY timestamp ASC").all(sessionId, level);
2684
+ }
2685
+ return db.query("SELECT * FROM console_log WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
2686
+ }
2687
+ function clearConsoleLog(sessionId) {
2688
+ const db = getDatabase();
2689
+ db.prepare("DELETE FROM console_log WHERE session_id = ?").run(sessionId);
2690
+ }
2691
+ var init_console_log = __esm(() => {
2692
+ init_schema();
2693
+ });
2694
+
2695
+ // src/lib/console.ts
2696
+ function enableConsoleCapture(page, sessionId) {
2697
+ const onConsole = (msg) => {
2698
+ const levelMap = {
2699
+ log: "log",
2700
+ warn: "warn",
2701
+ error: "error",
2702
+ debug: "debug",
2703
+ info: "info",
2704
+ warning: "warn"
2705
+ };
2706
+ const level = levelMap[msg.type()] ?? "log";
2707
+ const location = msg.location();
2708
+ try {
2709
+ logConsoleMessage({
2710
+ session_id: sessionId,
2711
+ level,
2712
+ message: msg.text(),
2713
+ source: location.url || undefined,
2714
+ line_number: location.lineNumber || undefined
2715
+ });
2716
+ } catch {}
2717
+ };
2718
+ page.on("console", onConsole);
2719
+ return () => page.off("console", onConsole);
2720
+ }
2721
+ var init_console = __esm(() => {
2722
+ init_console_log();
2723
+ });
2724
+
2725
+ // src/lib/stealth.ts
2726
+ async function applyStealthPatches(page) {
2727
+ await page.context().addInitScript(STEALTH_SCRIPT);
2728
+ await page.context().setExtraHTTPHeaders({
2729
+ "User-Agent": REALISTIC_USER_AGENT
2730
+ });
2731
+ }
2732
+ var REALISTIC_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36", STEALTH_SCRIPT = `
2733
+ // \u2500\u2500 1. Remove navigator.webdriver flag \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2734
+ Object.defineProperty(navigator, 'webdriver', {
2735
+ get: () => false,
2736
+ configurable: true,
2737
+ });
2738
+
2739
+ // \u2500\u2500 2. Override navigator.plugins to show typical Chrome plugins \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2740
+ Object.defineProperty(navigator, 'plugins', {
2741
+ get: () => {
2742
+ const plugins = [
2743
+ { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format', length: 1 },
2744
+ { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '', length: 1 },
2745
+ { name: 'Native Client', filename: 'internal-nacl-plugin', description: '', length: 2 },
2746
+ ];
2747
+ // Mimic PluginArray interface
2748
+ const pluginArray = Object.create(PluginArray.prototype);
2749
+ plugins.forEach((p, i) => {
2750
+ const plugin = Object.create(Plugin.prototype);
2751
+ Object.defineProperties(plugin, {
2752
+ name: { value: p.name, enumerable: true },
2753
+ filename: { value: p.filename, enumerable: true },
2754
+ description: { value: p.description, enumerable: true },
2755
+ length: { value: p.length, enumerable: true },
2756
+ });
2757
+ pluginArray[i] = plugin;
2758
+ });
2759
+ Object.defineProperty(pluginArray, 'length', { value: plugins.length });
2760
+ pluginArray.item = (i) => pluginArray[i] || null;
2761
+ pluginArray.namedItem = (name) => plugins.find(p => p.name === name) ? pluginArray[plugins.findIndex(p => p.name === name)] : null;
2762
+ pluginArray.refresh = () => {};
2763
+ return pluginArray;
2764
+ },
2765
+ configurable: true,
2766
+ });
2767
+
2768
+ // \u2500\u2500 3. Override navigator.languages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2769
+ Object.defineProperty(navigator, 'languages', {
2770
+ get: () => ['en-US', 'en'],
2771
+ configurable: true,
2772
+ });
2773
+
2774
+ // \u2500\u2500 4. Override chrome.runtime to appear like real Chrome \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2775
+ if (!window.chrome) {
2776
+ window.chrome = {};
2777
+ }
2778
+ if (!window.chrome.runtime) {
2779
+ window.chrome.runtime = {
2780
+ connect: function() { return { onMessage: { addListener: function() {} }, postMessage: function() {} }; },
2781
+ sendMessage: function() {},
2782
+ onMessage: { addListener: function() {}, removeListener: function() {} },
2783
+ id: undefined,
2784
+ };
2785
+ }
2786
+ `;
2787
+ var init_stealth = () => {};
2788
+
2789
+ // src/lib/dialogs.ts
2790
+ function setupDialogHandler(page, sessionId) {
2791
+ const onDialog = (dialog) => {
2792
+ const info = {
2793
+ type: dialog.type(),
2794
+ message: dialog.message(),
2795
+ default_value: dialog.defaultValue(),
2796
+ timestamp: new Date().toISOString()
2797
+ };
2798
+ const autoTimer = setTimeout(() => {
2799
+ try {
2800
+ dialog.dismiss().catch(() => {});
2801
+ } catch {}
2802
+ const list = pendingDialogs.get(sessionId);
2803
+ if (list) {
2804
+ const idx = list.findIndex((p) => p.dialog === dialog);
2805
+ if (idx >= 0)
2806
+ list.splice(idx, 1);
2807
+ if (list.length === 0)
2808
+ pendingDialogs.delete(sessionId);
2809
+ }
2810
+ }, AUTO_DISMISS_MS);
2811
+ const pending = { dialog, info, autoTimer };
2812
+ if (!pendingDialogs.has(sessionId)) {
2813
+ pendingDialogs.set(sessionId, []);
2814
+ }
2815
+ pendingDialogs.get(sessionId).push(pending);
2816
+ };
2817
+ page.on("dialog", onDialog);
2818
+ return () => {
2819
+ page.off("dialog", onDialog);
2820
+ const list = pendingDialogs.get(sessionId);
2821
+ if (list) {
2822
+ for (const p of list)
2823
+ clearTimeout(p.autoTimer);
2824
+ pendingDialogs.delete(sessionId);
2825
+ }
2826
+ };
2827
+ }
2828
+ function getDialogs(sessionId) {
2829
+ const list = pendingDialogs.get(sessionId);
2830
+ if (!list)
2831
+ return [];
2832
+ return list.map((p) => p.info);
2833
+ }
2834
+ async function handleDialog(sessionId, action, promptText) {
2835
+ const list = pendingDialogs.get(sessionId);
2836
+ if (!list || list.length === 0) {
2837
+ return { handled: false };
2838
+ }
2839
+ const pending = list.shift();
2840
+ clearTimeout(pending.autoTimer);
2841
+ if (list.length === 0) {
2842
+ pendingDialogs.delete(sessionId);
2843
+ }
2844
+ try {
2845
+ if (action === "accept") {
2846
+ await pending.dialog.accept(promptText);
2847
+ } else {
2848
+ await pending.dialog.dismiss();
2849
+ }
2850
+ } catch {}
2851
+ return { handled: true, dialog: pending.info };
2852
+ }
2853
+ var pendingDialogs, AUTO_DISMISS_MS = 5000;
2854
+ var init_dialogs = __esm(() => {
2855
+ pendingDialogs = new Map;
2856
+ });
2857
+
2528
2858
  // src/lib/session.ts
2529
2859
  async function createSession2(opts = {}) {
2530
2860
  const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
@@ -2550,13 +2880,33 @@ async function createSession2(opts = {}) {
2550
2880
  engine: resolvedEngine,
2551
2881
  projectId: opts.projectId,
2552
2882
  agentId: opts.agentId,
2553
- startUrl: opts.startUrl
2883
+ startUrl: opts.startUrl,
2884
+ name: opts.name ?? (opts.startUrl ? new URL(opts.startUrl).hostname : undefined)
2554
2885
  });
2555
- handles.set(session.id, { browser, page, engine: resolvedEngine });
2886
+ if (opts.stealth) {
2887
+ try {
2888
+ await applyStealthPatches(page);
2889
+ } catch {}
2890
+ }
2891
+ const cleanups = [];
2892
+ if (opts.captureNetwork !== false) {
2893
+ try {
2894
+ cleanups.push(enableNetworkLogging(page, session.id));
2895
+ } catch {}
2896
+ }
2897
+ if (opts.captureConsole !== false) {
2898
+ try {
2899
+ cleanups.push(enableConsoleCapture(page, session.id));
2900
+ } catch {}
2901
+ }
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 } });
2556
2906
  if (opts.startUrl) {
2557
2907
  try {
2558
2908
  await page.goto(opts.startUrl, { waitUntil: "domcontentloaded" });
2559
- } catch (err) {}
2909
+ } catch {}
2560
2910
  }
2561
2911
  return { session, page };
2562
2912
  }
@@ -2564,11 +2914,28 @@ function getSessionPage(sessionId) {
2564
2914
  const handle = handles.get(sessionId);
2565
2915
  if (!handle)
2566
2916
  throw new SessionNotFoundError(sessionId);
2917
+ try {
2918
+ handle.page.url();
2919
+ } catch {
2920
+ handles.delete(sessionId);
2921
+ throw new SessionNotFoundError(sessionId);
2922
+ }
2567
2923
  return handle.page;
2568
2924
  }
2925
+ function setSessionPage(sessionId, page) {
2926
+ const handle = handles.get(sessionId);
2927
+ if (!handle)
2928
+ throw new SessionNotFoundError(sessionId);
2929
+ handle.page = page;
2930
+ }
2569
2931
  async function closeSession2(sessionId) {
2570
2932
  const handle = handles.get(sessionId);
2571
2933
  if (handle) {
2934
+ for (const cleanup of handle.cleanups) {
2935
+ try {
2936
+ cleanup();
2937
+ } catch {}
2938
+ }
2572
2939
  try {
2573
2940
  await handle.page.context().close();
2574
2941
  } catch {}
@@ -2579,6 +2946,9 @@ async function closeSession2(sessionId) {
2579
2946
  }
2580
2947
  return closeSession(sessionId);
2581
2948
  }
2949
+ function getSession2(sessionId) {
2950
+ return getSession(sessionId);
2951
+ }
2582
2952
  function listSessions2(filter) {
2583
2953
  return listSessions(filter);
2584
2954
  }
@@ -2588,6 +2958,10 @@ function getSessionByName2(name) {
2588
2958
  function renameSession2(id, name) {
2589
2959
  return renameSession(id, name);
2590
2960
  }
2961
+ function getTokenBudget(sessionId) {
2962
+ const handle = handles.get(sessionId);
2963
+ return handle ? handle.tokenBudget : null;
2964
+ }
2591
2965
  var handles;
2592
2966
  var init_session = __esm(() => {
2593
2967
  init_types();
@@ -2596,9 +2970,186 @@ var init_session = __esm(() => {
2596
2970
  init_playwright();
2597
2971
  init_lightpanda();
2598
2972
  init_selector();
2973
+ init_network();
2974
+ init_console();
2975
+ init_stealth();
2976
+ init_dialogs();
2599
2977
  handles = new Map;
2600
2978
  });
2601
2979
 
2980
+ // src/lib/snapshot.ts
2981
+ function getLastSnapshot(sessionId) {
2982
+ return lastSnapshots.get(sessionId) ?? null;
2983
+ }
2984
+ function setLastSnapshot(sessionId, snapshot) {
2985
+ lastSnapshots.set(sessionId, snapshot);
2986
+ }
2987
+ async function takeSnapshot(page, sessionId) {
2988
+ let ariaTree;
2989
+ try {
2990
+ ariaTree = await page.locator("body").ariaSnapshot();
2991
+ } catch {
2992
+ ariaTree = "";
2993
+ }
2994
+ const refs = {};
2995
+ const refMap = new Map;
2996
+ let refCounter = 0;
2997
+ for (const role of INTERACTIVE_ROLES) {
2998
+ const locators = page.getByRole(role);
2999
+ const count = await locators.count();
3000
+ for (let i = 0;i < count; i++) {
3001
+ const el = locators.nth(i);
3002
+ let name = "";
3003
+ let visible = false;
3004
+ let enabled = true;
3005
+ let value;
3006
+ let checked;
3007
+ try {
3008
+ visible = await el.isVisible();
3009
+ if (!visible)
3010
+ continue;
3011
+ } catch {
3012
+ continue;
3013
+ }
3014
+ try {
3015
+ name = await el.evaluate((e) => {
3016
+ const el2 = e;
3017
+ return el2.getAttribute("aria-label") ?? el2.textContent?.trim().slice(0, 80) ?? el2.getAttribute("title") ?? el2.getAttribute("placeholder") ?? "";
3018
+ });
3019
+ } catch {
3020
+ continue;
3021
+ }
3022
+ if (!name)
3023
+ continue;
3024
+ try {
3025
+ enabled = await el.isEnabled();
3026
+ } catch {}
3027
+ try {
3028
+ if (role === "checkbox" || role === "radio" || role === "switch") {
3029
+ checked = await el.isChecked();
3030
+ }
3031
+ } catch {}
3032
+ try {
3033
+ if (role === "textbox" || role === "searchbox" || role === "spinbutton" || role === "combobox") {
3034
+ value = await el.inputValue();
3035
+ }
3036
+ } catch {}
3037
+ const ref = `@e${refCounter}`;
3038
+ refCounter++;
3039
+ refs[ref] = { role, name, visible, enabled, value, checked };
3040
+ const escapedName = name.replace(/"/g, "\\\"");
3041
+ refMap.set(ref, { role, name, locatorSelector: `role=${role}[name="${escapedName}"]` });
3042
+ }
3043
+ }
3044
+ let annotatedTree = ariaTree;
3045
+ for (const [ref, info] of Object.entries(refs)) {
3046
+ const escapedName = info.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3047
+ const pattern = new RegExp(`(${info.role}\\s+"${escapedName.slice(0, 40)}[^"]*")`, "m");
3048
+ const match = annotatedTree.match(pattern);
3049
+ if (match) {
3050
+ annotatedTree = annotatedTree.replace(match[0], `${match[0]} [${ref}]`);
3051
+ }
3052
+ }
3053
+ const unmatchedRefs = Object.entries(refs).filter(([ref]) => !annotatedTree.includes(`[${ref}]`));
3054
+ if (unmatchedRefs.length > 0) {
3055
+ annotatedTree += `
3056
+
3057
+ --- Interactive elements ---`;
3058
+ for (const [ref, info] of unmatchedRefs) {
3059
+ const extras = [];
3060
+ if (info.checked !== undefined)
3061
+ extras.push(`checked=${info.checked}`);
3062
+ if (!info.enabled)
3063
+ extras.push("disabled");
3064
+ if (info.value)
3065
+ extras.push(`value="${info.value}"`);
3066
+ const extrasStr = extras.length ? ` (${extras.join(", ")})` : "";
3067
+ annotatedTree += `
3068
+ ${info.role} "${info.name}" [${ref}]${extrasStr}`;
3069
+ }
3070
+ }
3071
+ if (sessionId) {
3072
+ sessionRefMaps.set(sessionId, refMap);
3073
+ }
3074
+ return {
3075
+ tree: annotatedTree,
3076
+ refs,
3077
+ interactive_count: refCounter
3078
+ };
3079
+ }
3080
+ function getRefLocator(page, sessionId, ref) {
3081
+ const refMap = sessionRefMaps.get(sessionId);
3082
+ if (!refMap)
3083
+ throw new Error(`No snapshot taken for session ${sessionId}. Call browser_snapshot first.`);
3084
+ const entry = refMap.get(ref);
3085
+ if (!entry)
3086
+ throw new Error(`Ref ${ref} not found. Available refs: ${[...refMap.keys()].slice(0, 20).join(", ")}`);
3087
+ return page.getByRole(entry.role, { name: entry.name }).first();
3088
+ }
3089
+ function refKey(info) {
3090
+ return `${info.role}::${info.name}`;
3091
+ }
3092
+ function diffSnapshots(before, after) {
3093
+ const beforeMap = new Map;
3094
+ for (const [ref, info] of Object.entries(before.refs)) {
3095
+ beforeMap.set(refKey(info), { ref, info });
3096
+ }
3097
+ const afterMap = new Map;
3098
+ for (const [ref, info] of Object.entries(after.refs)) {
3099
+ afterMap.set(refKey(info), { ref, info });
3100
+ }
3101
+ const added = [];
3102
+ const removed = [];
3103
+ const modified = [];
3104
+ for (const [key, afterEntry] of afterMap) {
3105
+ const beforeEntry = beforeMap.get(key);
3106
+ if (!beforeEntry) {
3107
+ added.push({ ref: afterEntry.ref, info: afterEntry.info });
3108
+ } else {
3109
+ const b = beforeEntry.info;
3110
+ const a = afterEntry.info;
3111
+ if (b.visible !== a.visible || b.enabled !== a.enabled || b.value !== a.value || b.checked !== a.checked || b.description !== a.description) {
3112
+ modified.push({ ref: afterEntry.ref, before: b, after: a });
3113
+ }
3114
+ }
3115
+ }
3116
+ for (const [key, beforeEntry] of beforeMap) {
3117
+ if (!afterMap.has(key)) {
3118
+ removed.push({ ref: beforeEntry.ref, info: beforeEntry.info });
3119
+ }
3120
+ }
3121
+ const url_changed = before.tree.split(`
3122
+ `)[0] !== after.tree.split(`
3123
+ `)[0];
3124
+ const title_changed = before.tree !== after.tree && (added.length > 0 || removed.length > 0 || modified.length > 0);
3125
+ return { added, removed, modified, url_changed, title_changed };
3126
+ }
3127
+ var lastSnapshots, sessionRefMaps, INTERACTIVE_ROLES;
3128
+ var init_snapshot = __esm(() => {
3129
+ lastSnapshots = new Map;
3130
+ sessionRefMaps = new Map;
3131
+ INTERACTIVE_ROLES = [
3132
+ "button",
3133
+ "link",
3134
+ "textbox",
3135
+ "checkbox",
3136
+ "radio",
3137
+ "combobox",
3138
+ "menuitem",
3139
+ "menuitemcheckbox",
3140
+ "menuitemradio",
3141
+ "option",
3142
+ "searchbox",
3143
+ "slider",
3144
+ "spinbutton",
3145
+ "switch",
3146
+ "tab",
3147
+ "treeitem",
3148
+ "listbox",
3149
+ "menu"
3150
+ ];
3151
+ });
3152
+
2602
3153
  // src/lib/actions.ts
2603
3154
  async function click(page, selector, opts) {
2604
3155
  try {
@@ -2818,9 +3369,67 @@ function stopWatch(watchId) {
2818
3369
  activeWatches.delete(watchId);
2819
3370
  }
2820
3371
  }
3372
+ async function clickRef(page, sessionId, ref, opts) {
3373
+ try {
3374
+ const locator = getRefLocator(page, sessionId, ref);
3375
+ await locator.click({ timeout: opts?.timeout ?? 1e4 });
3376
+ } catch (err) {
3377
+ if (err instanceof Error && err.message.includes("Ref "))
3378
+ throw new ElementNotFoundError(ref);
3379
+ if (err instanceof Error && err.message.includes("No snapshot"))
3380
+ throw new BrowserError(err.message, "NO_SNAPSHOT");
3381
+ throw new BrowserError(`clickRef ${ref} failed: ${err instanceof Error ? err.message : String(err)}`, "CLICK_REF_FAILED");
3382
+ }
3383
+ }
3384
+ async function typeRef(page, sessionId, ref, text, opts) {
3385
+ try {
3386
+ const locator = getRefLocator(page, sessionId, ref);
3387
+ if (opts?.clear)
3388
+ await locator.fill("", { timeout: opts.timeout ?? 1e4 });
3389
+ await locator.pressSequentially(text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
3390
+ } catch (err) {
3391
+ if (err instanceof Error && err.message.includes("Ref "))
3392
+ throw new ElementNotFoundError(ref);
3393
+ throw new BrowserError(`typeRef ${ref} failed: ${err instanceof Error ? err.message : String(err)}`, "TYPE_REF_FAILED");
3394
+ }
3395
+ }
3396
+ async function selectRef(page, sessionId, ref, value, timeout = 1e4) {
3397
+ try {
3398
+ const locator = getRefLocator(page, sessionId, ref);
3399
+ return await locator.selectOption(value, { timeout });
3400
+ } catch (err) {
3401
+ if (err instanceof Error && err.message.includes("Ref "))
3402
+ throw new ElementNotFoundError(ref);
3403
+ throw new BrowserError(`selectRef ${ref} failed: ${err instanceof Error ? err.message : String(err)}`, "SELECT_REF_FAILED");
3404
+ }
3405
+ }
3406
+ async function checkRef(page, sessionId, ref, checked, timeout = 1e4) {
3407
+ try {
3408
+ const locator = getRefLocator(page, sessionId, ref);
3409
+ if (checked)
3410
+ await locator.check({ timeout });
3411
+ else
3412
+ await locator.uncheck({ timeout });
3413
+ } catch (err) {
3414
+ if (err instanceof Error && err.message.includes("Ref "))
3415
+ throw new ElementNotFoundError(ref);
3416
+ throw new BrowserError(`checkRef ${ref} failed: ${err instanceof Error ? err.message : String(err)}`, "CHECK_REF_FAILED");
3417
+ }
3418
+ }
3419
+ async function hoverRef(page, sessionId, ref, timeout = 1e4) {
3420
+ try {
3421
+ const locator = getRefLocator(page, sessionId, ref);
3422
+ await locator.hover({ timeout });
3423
+ } catch (err) {
3424
+ if (err instanceof Error && err.message.includes("Ref "))
3425
+ throw new ElementNotFoundError(ref);
3426
+ throw new BrowserError(`hoverRef ${ref} failed: ${err instanceof Error ? err.message : String(err)}`, "HOVER_REF_FAILED");
3427
+ }
3428
+ }
2821
3429
  var RETRYABLE_ERRORS, activeWatches;
2822
3430
  var init_actions = __esm(() => {
2823
3431
  init_types();
3432
+ init_snapshot();
2824
3433
  RETRYABLE_ERRORS = ["Timeout", "timeout", "navigation", "net::ERR", "Target closed"];
2825
3434
  activeWatches = new Map;
2826
3435
  });
@@ -2890,24 +3499,6 @@ async function extractTable(page, selector) {
2890
3499
  return rows.map((row) => Array.from(row.querySelectorAll("th, td")).map((cell) => cell.textContent?.trim() ?? ""));
2891
3500
  }, selector);
2892
3501
  }
2893
- async function getAriaSnapshot(page) {
2894
- try {
2895
- return await page.ariaSnapshot?.() ?? page.evaluate(() => {
2896
- function walk(el, indent = 0) {
2897
- const role = el.getAttribute("role") ?? el.tagName.toLowerCase();
2898
- const label = el.getAttribute("aria-label") ?? el.getAttribute("aria-labelledby") ?? el.textContent?.trim().slice(0, 50) ?? "";
2899
- const line = " ".repeat(indent) + `[${role}] ${label}`;
2900
- const children = Array.from(el.children).map((c) => walk(c, indent + 1)).join(`
2901
- `);
2902
- return children ? `${line}
2903
- ${children}` : line;
2904
- }
2905
- return walk(document.body);
2906
- });
2907
- } catch {
2908
- return page.evaluate(() => document.body.innerText?.slice(0, 2000) ?? "");
2909
- }
2910
- }
2911
3502
  async function extract(page, opts = {}) {
2912
3503
  const result = {};
2913
3504
  const format = opts.format ?? "text";
@@ -9377,7 +9968,7 @@ __export(exports_gallery, {
9377
9968
  deleteEntry: () => deleteEntry,
9378
9969
  createEntry: () => createEntry
9379
9970
  });
9380
- import { randomUUID as randomUUID2 } from "crypto";
9971
+ import { randomUUID as randomUUID4 } from "crypto";
9381
9972
  function deserialize(row) {
9382
9973
  return {
9383
9974
  id: row.id,
@@ -9401,7 +9992,7 @@ function deserialize(row) {
9401
9992
  }
9402
9993
  function createEntry(data) {
9403
9994
  const db = getDatabase();
9404
- const id = randomUUID2();
9995
+ const id = randomUUID4();
9405
9996
  db.prepare(`
9406
9997
  INSERT INTO gallery_entries
9407
9998
  (id, session_id, project_id, url, title, path, thumbnail_path, format,
@@ -9674,7 +10265,7 @@ var init_screenshot = __esm(() => {
9674
10265
  });
9675
10266
 
9676
10267
  // src/db/crawl-results.ts
9677
- import { randomUUID as randomUUID3 } from "crypto";
10268
+ import { randomUUID as randomUUID5 } from "crypto";
9678
10269
  function deserialize2(row) {
9679
10270
  const pages = JSON.parse(row.pages);
9680
10271
  return {
@@ -9690,7 +10281,7 @@ function deserialize2(row) {
9690
10281
  }
9691
10282
  function createCrawlResult(data) {
9692
10283
  const db = getDatabase();
9693
- const id = randomUUID3();
10284
+ const id = randomUUID5();
9694
10285
  db.prepare("INSERT INTO crawl_results (id, project_id, start_url, depth, pages, links, errors) VALUES (?, ?, ?, ?, ?, ?, ?)").run(id, data.project_id ?? null, data.start_url, data.depth, JSON.stringify(data.pages), JSON.stringify(data.pages.flatMap((p) => p.links)), JSON.stringify(data.errors));
9695
10286
  return getCrawlResult(id);
9696
10287
  }
@@ -9783,7 +10374,7 @@ __export(exports_agents, {
9783
10374
  deleteAgent: () => deleteAgent,
9784
10375
  cleanStaleAgents: () => cleanStaleAgents
9785
10376
  });
9786
- import { randomUUID as randomUUID4 } from "crypto";
10377
+ import { randomUUID as randomUUID6 } from "crypto";
9787
10378
  function registerAgent(name, opts = {}) {
9788
10379
  const db = getDatabase();
9789
10380
  const existing = db.query("SELECT * FROM agents WHERE name = ?").get(name);
@@ -9791,7 +10382,7 @@ function registerAgent(name, opts = {}) {
9791
10382
  db.prepare("UPDATE agents SET last_seen = datetime('now'), session_id = ?, project_id = ?, working_dir = ? WHERE name = ?").run(opts.sessionId ?? existing.session_id ?? null, opts.projectId ?? existing.project_id ?? null, opts.workingDir ?? existing.working_dir ?? null, name);
9792
10383
  return getAgentByName(name);
9793
10384
  }
9794
- const id = randomUUID4();
10385
+ const id = randomUUID6();
9795
10386
  db.prepare("INSERT INTO agents (id, name, description, session_id, project_id, working_dir) VALUES (?, ?, ?, ?, ?, ?)").run(id, name, opts.description ?? null, opts.sessionId ?? null, opts.projectId ?? null, opts.workingDir ?? null);
9796
10387
  return getAgent(id);
9797
10388
  }
@@ -9801,7 +10392,7 @@ function heartbeat(agentId) {
9801
10392
  if (!agent)
9802
10393
  throw new AgentNotFoundError(agentId);
9803
10394
  db.prepare("UPDATE agents SET last_seen = datetime('now') WHERE id = ?").run(agentId);
9804
- db.prepare("INSERT INTO heartbeats (id, agent_id, session_id) VALUES (?, ?, ?)").run(randomUUID4(), agentId, agent.session_id ?? null);
10395
+ db.prepare("INSERT INTO heartbeats (id, agent_id, session_id) VALUES (?, ?, ?)").run(randomUUID6(), agentId, agent.session_id ?? null);
9805
10396
  }
9806
10397
  function getAgent(id) {
9807
10398
  const db = getDatabase();
@@ -9878,10 +10469,10 @@ var init_agents2 = __esm(() => {
9878
10469
  });
9879
10470
 
9880
10471
  // src/db/projects.ts
9881
- import { randomUUID as randomUUID5 } from "crypto";
10472
+ import { randomUUID as randomUUID7 } from "crypto";
9882
10473
  function createProject(data) {
9883
10474
  const db = getDatabase();
9884
- const id = randomUUID5();
10475
+ const id = randomUUID7();
9885
10476
  db.prepare("INSERT INTO projects (id, name, path, description) VALUES (?, ?, ?, ?)").run(id, data.name, data.path, data.description ?? null);
9886
10477
  return getProject(id);
9887
10478
  }
@@ -9917,7 +10508,7 @@ __export(exports_recordings, {
9917
10508
  deleteRecording: () => deleteRecording,
9918
10509
  createRecording: () => createRecording
9919
10510
  });
9920
- import { randomUUID as randomUUID6 } from "crypto";
10511
+ import { randomUUID as randomUUID8 } from "crypto";
9921
10512
  function deserialize3(row) {
9922
10513
  return {
9923
10514
  ...row,
@@ -9928,7 +10519,7 @@ function deserialize3(row) {
9928
10519
  }
9929
10520
  function createRecording(data) {
9930
10521
  const db = getDatabase();
9931
- const id = randomUUID6();
10522
+ const id = randomUUID8();
9932
10523
  db.prepare("INSERT INTO recordings (id, name, project_id, start_url, steps) VALUES (?, ?, ?, ?, ?)").run(id, data.name, data.project_id ?? null, data.start_url ?? null, JSON.stringify(data.steps ?? []));
9933
10524
  return getRecording(id);
9934
10525
  }
@@ -14031,139 +14622,6 @@ var init_zod = __esm(() => {
14031
14622
  init_external();
14032
14623
  });
14033
14624
 
14034
- // src/db/network-log.ts
14035
- import { randomUUID as randomUUID7 } from "crypto";
14036
- function logRequest(data) {
14037
- const db = getDatabase();
14038
- const id = randomUUID7();
14039
- db.prepare(`INSERT INTO network_log (id, session_id, method, url, status_code, request_headers,
14040
- response_headers, request_body, body_size, duration_ms, resource_type)
14041
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, data.session_id, data.method, data.url, data.status_code ?? null, data.request_headers ?? null, data.response_headers ?? null, data.request_body ?? null, data.body_size ?? null, data.duration_ms ?? null, data.resource_type ?? null);
14042
- return getNetworkRequest(id);
14043
- }
14044
- function getNetworkRequest(id) {
14045
- const db = getDatabase();
14046
- return db.query("SELECT * FROM network_log WHERE id = ?").get(id) ?? null;
14047
- }
14048
- function getNetworkLog(sessionId) {
14049
- const db = getDatabase();
14050
- return db.query("SELECT * FROM network_log WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
14051
- }
14052
- function clearNetworkLog(sessionId) {
14053
- const db = getDatabase();
14054
- db.prepare("DELETE FROM network_log WHERE session_id = ?").run(sessionId);
14055
- }
14056
- var init_network_log = __esm(() => {
14057
- init_schema();
14058
- });
14059
-
14060
- // src/lib/network.ts
14061
- function enableNetworkLogging(page, sessionId) {
14062
- const requestStart = new Map;
14063
- const onRequest = (req) => {
14064
- requestStart.set(req.url(), Date.now());
14065
- };
14066
- const onResponse = (res) => {
14067
- const start = requestStart.get(res.url()) ?? Date.now();
14068
- const duration = Date.now() - start;
14069
- const req = res.request();
14070
- try {
14071
- logRequest({
14072
- session_id: sessionId,
14073
- method: req.method(),
14074
- url: res.url(),
14075
- status_code: res.status(),
14076
- request_headers: JSON.stringify(req.headers()),
14077
- response_headers: JSON.stringify(res.headers()),
14078
- body_size: res.headers()["content-length"] != null ? parseInt(res.headers()["content-length"]) : undefined,
14079
- duration_ms: duration,
14080
- resource_type: req.resourceType()
14081
- });
14082
- } catch {}
14083
- };
14084
- page.on("request", onRequest);
14085
- page.on("response", onResponse);
14086
- return () => {
14087
- page.off("request", onRequest);
14088
- page.off("response", onResponse);
14089
- };
14090
- }
14091
- async function addInterceptRule(page, rule) {
14092
- await page.route(rule.pattern, async (route) => {
14093
- if (rule.action === "block") {
14094
- await route.abort();
14095
- } else if (rule.action === "modify" && rule.response) {
14096
- await route.fulfill({
14097
- status: rule.response.status,
14098
- body: rule.response.body,
14099
- headers: rule.response.headers
14100
- });
14101
- } else {
14102
- await route.continue();
14103
- }
14104
- });
14105
- }
14106
- function startHAR(page) {
14107
- const entries = [];
14108
- const requestStart = new Map;
14109
- const onRequest = (req) => {
14110
- requestStart.set(req.url() + req.method(), {
14111
- time: Date.now(),
14112
- method: req.method(),
14113
- headers: req.headers(),
14114
- postData: req.postData() ?? undefined
14115
- });
14116
- };
14117
- const onResponse = async (res) => {
14118
- const key = res.url() + res.request().method();
14119
- const start = requestStart.get(key);
14120
- if (!start)
14121
- return;
14122
- const duration = Date.now() - start.time;
14123
- const entry = {
14124
- startedDateTime: new Date(start.time).toISOString(),
14125
- time: duration,
14126
- request: {
14127
- method: start.method,
14128
- url: res.url(),
14129
- headers: Object.entries(start.headers).map(([name, value]) => ({ name, value })),
14130
- postData: start.postData ? { text: start.postData } : undefined
14131
- },
14132
- response: {
14133
- status: res.status(),
14134
- statusText: res.statusText(),
14135
- headers: Object.entries(res.headers()).map(([name, value]) => ({ name, value })),
14136
- content: {
14137
- size: parseInt(res.headers()["content-length"] ?? "0") || 0,
14138
- mimeType: res.headers()["content-type"] ?? "application/octet-stream"
14139
- }
14140
- },
14141
- timings: { send: 0, wait: duration, receive: 0 }
14142
- };
14143
- entries.push(entry);
14144
- requestStart.delete(key);
14145
- };
14146
- page.on("request", onRequest);
14147
- page.on("response", onResponse);
14148
- return {
14149
- entries,
14150
- stop: () => {
14151
- page.off("request", onRequest);
14152
- page.off("response", onResponse);
14153
- return {
14154
- log: {
14155
- version: "1.2",
14156
- creator: { name: "@hasna/browser", version: "0.0.1" },
14157
- entries
14158
- }
14159
- };
14160
- }
14161
- };
14162
- }
14163
- var init_network = __esm(() => {
14164
- init_network_log();
14165
- });
14166
-
14167
14625
  // src/engines/cdp.ts
14168
14626
  class CDPClient {
14169
14627
  session;
@@ -14263,120 +14721,56 @@ class CDPClient {
14263
14721
  this.on("Network.responseReceived", onResponse);
14264
14722
  return () => {
14265
14723
  this.off("Network.requestWillBeSent", onRequest);
14266
- this.off("Network.responseReceived", onResponse);
14267
- };
14268
- }
14269
- async detach() {
14270
- try {
14271
- await this.session.detach();
14272
- } catch {}
14273
- }
14274
- }
14275
- var init_cdp = __esm(() => {
14276
- init_types();
14277
- });
14278
-
14279
- // src/lib/performance.ts
14280
- async function getPerformanceMetrics(page) {
14281
- const navTiming = await page.evaluate(() => {
14282
- const t = performance.timing;
14283
- const nav = performance.getEntriesByType("navigation")[0];
14284
- return {
14285
- ttfb: nav ? nav.responseStart - nav.requestStart : t.responseStart - t.requestStart,
14286
- domInteractive: nav ? nav.domInteractive : t.domInteractive - t.navigationStart,
14287
- domComplete: nav ? nav.domComplete : t.domComplete - t.navigationStart,
14288
- loadEvent: nav ? nav.loadEventEnd : t.loadEventEnd - t.navigationStart
14289
- };
14290
- });
14291
- const paintEntries = await page.evaluate(() => {
14292
- const entries = performance.getEntriesByType("paint");
14293
- const fcp = entries.find((e) => e.name === "first-contentful-paint");
14294
- return { fcp: fcp?.startTime };
14295
- });
14296
- let heapMetrics = {};
14297
- try {
14298
- const cdp = await CDPClient.fromPage(page);
14299
- const cdpMetrics = await cdp.getPerformanceMetrics();
14300
- heapMetrics = {
14301
- js_heap_size_used: cdpMetrics.js_heap_size_used,
14302
- js_heap_size_total: cdpMetrics.js_heap_size_total
14303
- };
14304
- } catch {}
14305
- return {
14306
- fcp: paintEntries.fcp,
14307
- ttfb: navTiming.ttfb,
14308
- dom_interactive: navTiming.domInteractive,
14309
- dom_complete: navTiming.domComplete,
14310
- load_event: navTiming.loadEvent,
14311
- ...heapMetrics
14312
- };
14313
- }
14314
- var init_performance = __esm(() => {
14315
- init_cdp();
14316
- });
14317
-
14318
- // src/db/console-log.ts
14319
- var exports_console_log = {};
14320
- __export(exports_console_log, {
14321
- logConsoleMessage: () => logConsoleMessage,
14322
- getConsoleMessage: () => getConsoleMessage,
14323
- getConsoleLog: () => getConsoleLog,
14324
- clearConsoleLog: () => clearConsoleLog
14325
- });
14326
- import { randomUUID as randomUUID8 } from "crypto";
14327
- function logConsoleMessage(data) {
14328
- const db = getDatabase();
14329
- const id = randomUUID8();
14330
- db.prepare("INSERT INTO console_log (id, session_id, level, message, source, line_number) VALUES (?, ?, ?, ?, ?, ?)").run(id, data.session_id, data.level, data.message, data.source ?? null, data.line_number ?? null);
14331
- return getConsoleMessage(id);
14332
- }
14333
- function getConsoleMessage(id) {
14334
- const db = getDatabase();
14335
- return db.query("SELECT * FROM console_log WHERE id = ?").get(id) ?? null;
14336
- }
14337
- function getConsoleLog(sessionId, level) {
14338
- const db = getDatabase();
14339
- if (level) {
14340
- return db.query("SELECT * FROM console_log WHERE session_id = ? AND level = ? ORDER BY timestamp ASC").all(sessionId, level);
14341
- }
14342
- return db.query("SELECT * FROM console_log WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
14343
- }
14344
- function clearConsoleLog(sessionId) {
14345
- const db = getDatabase();
14346
- db.prepare("DELETE FROM console_log WHERE session_id = ?").run(sessionId);
14347
- }
14348
- var init_console_log = __esm(() => {
14349
- init_schema();
14350
- });
14351
-
14352
- // src/lib/console.ts
14353
- function enableConsoleCapture(page, sessionId) {
14354
- const onConsole = (msg) => {
14355
- const levelMap = {
14356
- log: "log",
14357
- warn: "warn",
14358
- error: "error",
14359
- debug: "debug",
14360
- info: "info",
14361
- warning: "warn"
14724
+ this.off("Network.responseReceived", onResponse);
14362
14725
  };
14363
- const level = levelMap[msg.type()] ?? "log";
14364
- const location = msg.location();
14726
+ }
14727
+ async detach() {
14365
14728
  try {
14366
- logConsoleMessage({
14367
- session_id: sessionId,
14368
- level,
14369
- message: msg.text(),
14370
- source: location.url || undefined,
14371
- line_number: location.lineNumber || undefined
14372
- });
14729
+ await this.session.detach();
14373
14730
  } catch {}
14731
+ }
14732
+ }
14733
+ var init_cdp = __esm(() => {
14734
+ init_types();
14735
+ });
14736
+
14737
+ // src/lib/performance.ts
14738
+ async function getPerformanceMetrics(page) {
14739
+ const navTiming = await page.evaluate(() => {
14740
+ const t = performance.timing;
14741
+ const nav = performance.getEntriesByType("navigation")[0];
14742
+ return {
14743
+ ttfb: nav ? nav.responseStart - nav.requestStart : t.responseStart - t.requestStart,
14744
+ domInteractive: nav ? nav.domInteractive : t.domInteractive - t.navigationStart,
14745
+ domComplete: nav ? nav.domComplete : t.domComplete - t.navigationStart,
14746
+ loadEvent: nav ? nav.loadEventEnd : t.loadEventEnd - t.navigationStart
14747
+ };
14748
+ });
14749
+ const paintEntries = await page.evaluate(() => {
14750
+ const entries = performance.getEntriesByType("paint");
14751
+ const fcp = entries.find((e) => e.name === "first-contentful-paint");
14752
+ return { fcp: fcp?.startTime };
14753
+ });
14754
+ let heapMetrics = {};
14755
+ try {
14756
+ const cdp = await CDPClient.fromPage(page);
14757
+ const cdpMetrics = await cdp.getPerformanceMetrics();
14758
+ heapMetrics = {
14759
+ js_heap_size_used: cdpMetrics.js_heap_size_used,
14760
+ js_heap_size_total: cdpMetrics.js_heap_size_total
14761
+ };
14762
+ } catch {}
14763
+ return {
14764
+ fcp: paintEntries.fcp,
14765
+ ttfb: navTiming.ttfb,
14766
+ dom_interactive: navTiming.domInteractive,
14767
+ dom_complete: navTiming.domComplete,
14768
+ load_event: navTiming.loadEvent,
14769
+ ...heapMetrics
14374
14770
  };
14375
- page.on("console", onConsole);
14376
- return () => page.off("console", onConsole);
14377
14771
  }
14378
- var init_console = __esm(() => {
14379
- init_console_log();
14772
+ var init_performance = __esm(() => {
14773
+ init_cdp();
14380
14774
  });
14381
14775
 
14382
14776
  // src/lib/storage.ts
@@ -14673,6 +15067,282 @@ async function persistFile(localPath, opts) {
14673
15067
  }
14674
15068
  var init_files_integration = () => {};
14675
15069
 
15070
+ // src/lib/tabs.ts
15071
+ async function newTab(page, url) {
15072
+ const context = page.context();
15073
+ const newPage = await context.newPage();
15074
+ if (url) {
15075
+ await newPage.goto(url, { waitUntil: "domcontentloaded" });
15076
+ }
15077
+ const pages = context.pages();
15078
+ const index = pages.indexOf(newPage);
15079
+ return {
15080
+ index,
15081
+ url: newPage.url(),
15082
+ title: await newPage.title(),
15083
+ is_active: true
15084
+ };
15085
+ }
15086
+ async function listTabs(page) {
15087
+ const context = page.context();
15088
+ const pages = context.pages();
15089
+ const activePage = page;
15090
+ const tabs = [];
15091
+ for (let i = 0;i < pages.length; i++) {
15092
+ let url = "";
15093
+ let title = "";
15094
+ try {
15095
+ url = pages[i].url();
15096
+ title = await pages[i].title();
15097
+ } catch {}
15098
+ tabs.push({
15099
+ index: i,
15100
+ url,
15101
+ title,
15102
+ is_active: pages[i] === activePage
15103
+ });
15104
+ }
15105
+ return tabs;
15106
+ }
15107
+ async function switchTab(page, index) {
15108
+ const context = page.context();
15109
+ const pages = context.pages();
15110
+ if (index < 0 || index >= pages.length) {
15111
+ throw new Error(`Tab index ${index} out of range (0-${pages.length - 1})`);
15112
+ }
15113
+ const targetPage = pages[index];
15114
+ await targetPage.bringToFront();
15115
+ return {
15116
+ page: targetPage,
15117
+ tab: {
15118
+ index,
15119
+ url: targetPage.url(),
15120
+ title: await targetPage.title(),
15121
+ is_active: true
15122
+ }
15123
+ };
15124
+ }
15125
+ async function closeTab(page, index) {
15126
+ const context = page.context();
15127
+ const pages = context.pages();
15128
+ if (index < 0 || index >= pages.length) {
15129
+ throw new Error(`Tab index ${index} out of range (0-${pages.length - 1})`);
15130
+ }
15131
+ if (pages.length <= 1) {
15132
+ throw new Error("Cannot close the last tab");
15133
+ }
15134
+ const targetPage = pages[index];
15135
+ const isActivePage = targetPage === page;
15136
+ await targetPage.close();
15137
+ const remainingPages = context.pages();
15138
+ const activeIndex = isActivePage ? Math.min(index, remainingPages.length - 1) : remainingPages.indexOf(page);
15139
+ const activePage = remainingPages[activeIndex >= 0 ? activeIndex : 0];
15140
+ return {
15141
+ closed_index: index,
15142
+ active_tab: {
15143
+ index: activeIndex >= 0 ? activeIndex : 0,
15144
+ url: activePage.url(),
15145
+ title: await activePage.title(),
15146
+ is_active: true
15147
+ }
15148
+ };
15149
+ }
15150
+
15151
+ // 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";
15155
+ function getProfilesDir() {
15156
+ const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
15157
+ const dir = join6(dataDir, "profiles");
15158
+ mkdirSync6(dir, { recursive: true });
15159
+ return dir;
15160
+ }
15161
+ function getProfileDir(name) {
15162
+ return join6(getProfilesDir(), name);
15163
+ }
15164
+ async function saveProfile(page, name) {
15165
+ const dir = getProfileDir(name);
15166
+ mkdirSync6(dir, { recursive: true });
15167
+ const cookies = await page.context().cookies();
15168
+ writeFileSync2(join6(dir, "cookies.json"), JSON.stringify(cookies, null, 2));
15169
+ let localStorage2 = {};
15170
+ try {
15171
+ localStorage2 = await page.evaluate(() => {
15172
+ const result = {};
15173
+ for (let i = 0;i < window.localStorage.length; i++) {
15174
+ const key = window.localStorage.key(i);
15175
+ result[key] = window.localStorage.getItem(key);
15176
+ }
15177
+ return result;
15178
+ });
15179
+ } catch {}
15180
+ writeFileSync2(join6(dir, "storage.json"), JSON.stringify(localStorage2, null, 2));
15181
+ const savedAt = new Date().toISOString();
15182
+ const url = page.url();
15183
+ const meta = { saved_at: savedAt, url };
15184
+ writeFileSync2(join6(dir, "meta.json"), JSON.stringify(meta, null, 2));
15185
+ return {
15186
+ name,
15187
+ saved_at: savedAt,
15188
+ url,
15189
+ cookie_count: cookies.length,
15190
+ storage_key_count: Object.keys(localStorage2).length
15191
+ };
15192
+ }
15193
+ function loadProfile(name) {
15194
+ const dir = getProfileDir(name);
15195
+ if (!existsSync3(dir)) {
15196
+ throw new Error(`Profile not found: ${name}`);
15197
+ }
15198
+ const cookiesPath = join6(dir, "cookies.json");
15199
+ const storagePath = join6(dir, "storage.json");
15200
+ const metaPath2 = join6(dir, "meta.json");
15201
+ const cookies = existsSync3(cookiesPath) ? JSON.parse(readFileSync2(cookiesPath, "utf8")) : [];
15202
+ const localStorage2 = existsSync3(storagePath) ? JSON.parse(readFileSync2(storagePath, "utf8")) : {};
15203
+ let savedAt = new Date().toISOString();
15204
+ let url;
15205
+ if (existsSync3(metaPath2)) {
15206
+ const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
15207
+ savedAt = meta.saved_at ?? savedAt;
15208
+ url = meta.url;
15209
+ }
15210
+ return { cookies, localStorage: localStorage2, saved_at: savedAt, url };
15211
+ }
15212
+ async function applyProfile(page, profileData) {
15213
+ if (profileData.cookies.length > 0) {
15214
+ await page.context().addCookies(profileData.cookies);
15215
+ }
15216
+ const storageKeys = Object.keys(profileData.localStorage);
15217
+ if (storageKeys.length > 0) {
15218
+ try {
15219
+ await page.evaluate((storage) => {
15220
+ for (const [key, value] of Object.entries(storage)) {
15221
+ window.localStorage.setItem(key, value);
15222
+ }
15223
+ }, profileData.localStorage);
15224
+ } catch {}
15225
+ }
15226
+ return {
15227
+ cookies_applied: profileData.cookies.length,
15228
+ storage_keys_applied: storageKeys.length
15229
+ };
15230
+ }
15231
+ function listProfiles() {
15232
+ const dir = getProfilesDir();
15233
+ if (!existsSync3(dir))
15234
+ return [];
15235
+ const entries = readdirSync2(dir, { withFileTypes: true });
15236
+ const profiles = [];
15237
+ for (const entry of entries) {
15238
+ if (!entry.isDirectory())
15239
+ continue;
15240
+ const name = entry.name;
15241
+ const profileDir = join6(dir, name);
15242
+ let savedAt = "";
15243
+ let url;
15244
+ let cookieCount = 0;
15245
+ let storageKeyCount = 0;
15246
+ try {
15247
+ const metaPath2 = join6(profileDir, "meta.json");
15248
+ if (existsSync3(metaPath2)) {
15249
+ const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
15250
+ savedAt = meta.saved_at ?? "";
15251
+ url = meta.url;
15252
+ }
15253
+ const cookiesPath = join6(profileDir, "cookies.json");
15254
+ if (existsSync3(cookiesPath)) {
15255
+ const cookies = JSON.parse(readFileSync2(cookiesPath, "utf8"));
15256
+ cookieCount = Array.isArray(cookies) ? cookies.length : 0;
15257
+ }
15258
+ const storagePath = join6(profileDir, "storage.json");
15259
+ if (existsSync3(storagePath)) {
15260
+ const storage = JSON.parse(readFileSync2(storagePath, "utf8"));
15261
+ storageKeyCount = Object.keys(storage).length;
15262
+ }
15263
+ } catch {}
15264
+ profiles.push({
15265
+ name,
15266
+ saved_at: savedAt,
15267
+ url,
15268
+ cookie_count: cookieCount,
15269
+ storage_key_count: storageKeyCount
15270
+ });
15271
+ }
15272
+ return profiles.sort((a, b) => b.saved_at.localeCompare(a.saved_at));
15273
+ }
15274
+ function deleteProfile(name) {
15275
+ const dir = getProfileDir(name);
15276
+ if (!existsSync3(dir))
15277
+ return false;
15278
+ try {
15279
+ rmSync(dir, { recursive: true, force: true });
15280
+ return true;
15281
+ } catch {
15282
+ return false;
15283
+ }
15284
+ }
15285
+ var init_profiles = () => {};
15286
+
15287
+ // src/lib/annotate.ts
15288
+ var exports_annotate = {};
15289
+ __export(exports_annotate, {
15290
+ annotateScreenshot: () => annotateScreenshot
15291
+ });
15292
+ async function annotateScreenshot(page, sessionId) {
15293
+ const snapshot = await takeSnapshot(page, sessionId);
15294
+ const rawBuffer = await page.screenshot({ type: "png" });
15295
+ const meta = await import_sharp3.default(rawBuffer).metadata();
15296
+ const imgWidth = meta.width ?? 1280;
15297
+ const imgHeight = meta.height ?? 720;
15298
+ const annotations = [];
15299
+ const labelToRef = {};
15300
+ let labelCounter = 1;
15301
+ for (const [ref, info] of Object.entries(snapshot.refs)) {
15302
+ try {
15303
+ const locator = page.getByRole(info.role, { name: info.name }).first();
15304
+ const box = await locator.boundingBox();
15305
+ if (!box)
15306
+ continue;
15307
+ const annotation = {
15308
+ ref,
15309
+ label: labelCounter,
15310
+ x: Math.round(box.x),
15311
+ y: Math.round(box.y),
15312
+ width: Math.round(box.width),
15313
+ height: Math.round(box.height),
15314
+ role: info.role,
15315
+ name: info.name
15316
+ };
15317
+ annotations.push(annotation);
15318
+ labelToRef[labelCounter] = ref;
15319
+ labelCounter++;
15320
+ } catch {}
15321
+ }
15322
+ const circleR = 10;
15323
+ const fontSize = 12;
15324
+ const svgParts = [];
15325
+ for (const ann of annotations) {
15326
+ const cx = Math.min(Math.max(ann.x + circleR, circleR), imgWidth - circleR);
15327
+ const cy = Math.min(Math.max(ann.y - circleR - 2, circleR), imgHeight - circleR);
15328
+ svgParts.push(`
15329
+ <circle cx="${cx}" cy="${cy}" r="${circleR}" fill="#e11d48" stroke="white" stroke-width="1.5"/>
15330
+ <text x="${cx}" y="${cy + 4}" text-anchor="middle" fill="white" font-size="${fontSize}" font-family="Arial,sans-serif" font-weight="bold">${ann.label}</text>
15331
+ `);
15332
+ svgParts.push(`
15333
+ <rect x="${ann.x}" y="${ann.y}" width="${ann.width}" height="${ann.height}" fill="none" stroke="#e11d48" stroke-width="1.5" stroke-opacity="0.6" rx="2"/>
15334
+ `);
15335
+ }
15336
+ const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${imgWidth}" height="${imgHeight}">${svgParts.join("")}</svg>`;
15337
+ const annotatedBuffer = await import_sharp3.default(rawBuffer).composite([{ input: Buffer.from(svg), top: 0, left: 0 }]).webp({ quality: 85 }).toBuffer();
15338
+ return { buffer: annotatedBuffer, annotations, labelToRef };
15339
+ }
15340
+ var import_sharp3;
15341
+ var init_annotate = __esm(() => {
15342
+ init_snapshot();
15343
+ import_sharp3 = __toESM(require_lib(), 1);
15344
+ });
15345
+
14676
15346
  // src/mcp/index.ts
14677
15347
  var exports_mcp = {};
14678
15348
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -14706,8 +15376,11 @@ var init_mcp = __esm(async () => {
14706
15376
  init_gallery();
14707
15377
  init_downloads();
14708
15378
  init_gallery_diff();
15379
+ init_snapshot();
14709
15380
  init_files_integration();
14710
15381
  init_recordings();
15382
+ init_dialogs();
15383
+ init_profiles();
14711
15384
  init_types();
14712
15385
  networkLogCleanup = new Map;
14713
15386
  consoleCaptureCleanup = new Map;
@@ -14724,8 +15397,9 @@ var init_mcp = __esm(async () => {
14724
15397
  start_url: exports_external.string().optional(),
14725
15398
  headless: exports_external.boolean().optional().default(true),
14726
15399
  viewport_width: exports_external.number().optional().default(1280),
14727
- viewport_height: exports_external.number().optional().default(720)
14728
- }, async ({ engine, use_case, project_id, agent_id, start_url, headless, viewport_width, viewport_height }) => {
15400
+ viewport_height: exports_external.number().optional().default(720),
15401
+ stealth: exports_external.boolean().optional().default(false)
15402
+ }, async ({ engine, use_case, project_id, agent_id, start_url, headless, viewport_width, viewport_height, stealth }) => {
14729
15403
  try {
14730
15404
  const { session } = await createSession2({
14731
15405
  engine,
@@ -14734,7 +15408,8 @@ var init_mcp = __esm(async () => {
14734
15408
  agentId: agent_id,
14735
15409
  startUrl: start_url,
14736
15410
  headless,
14737
- viewport: { width: viewport_width, height: viewport_height }
15411
+ viewport: { width: viewport_width, height: viewport_height },
15412
+ stealth
14738
15413
  });
14739
15414
  return json({ session });
14740
15415
  } catch (e) {
@@ -14761,11 +15436,28 @@ var init_mcp = __esm(async () => {
14761
15436
  return err(e);
14762
15437
  }
14763
15438
  });
14764
- server.tool("browser_navigate", "Navigate to a URL in the session", { session_id: exports_external.string(), url: exports_external.string(), timeout: exports_external.number().optional().default(30000) }, async ({ session_id, url, timeout }) => {
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 }) => {
14765
15440
  try {
14766
15441
  const page = getSessionPage(session_id);
14767
15442
  await navigate(page, url, timeout);
14768
- return json({ url, title: await getTitle(page), current_url: await getUrl(page) });
15443
+ const title = await getTitle(page);
15444
+ const current_url = await getUrl(page);
15445
+ const result = { url, title, current_url };
15446
+ if (auto_thumbnail) {
15447
+ try {
15448
+ const ss = await takeScreenshot(page, { maxWidth: 400, quality: 60, track: false, thumbnail: false });
15449
+ result.thumbnail_base64 = ss.base64.length > 50000 ? "" : ss.base64;
15450
+ } catch {}
15451
+ }
15452
+ if (auto_snapshot) {
15453
+ try {
15454
+ const snap = await takeSnapshot(page, session_id);
15455
+ result.snapshot_preview = snap.tree.slice(0, 3000);
15456
+ result.interactive_count = snap.interactive_count;
15457
+ result.has_errors = getConsoleLog(session_id, "error").length > 0;
15458
+ } catch {}
15459
+ }
15460
+ return json(result);
14769
15461
  } catch (e) {
14770
15462
  return err(e);
14771
15463
  }
@@ -14797,29 +15489,47 @@ var init_mcp = __esm(async () => {
14797
15489
  return err(e);
14798
15490
  }
14799
15491
  });
14800
- server.tool("browser_click", "Click an element matching the selector", { session_id: exports_external.string(), selector: exports_external.string(), button: exports_external.enum(["left", "right", "middle"]).optional(), timeout: exports_external.number().optional() }, async ({ session_id, selector, button, timeout }) => {
15492
+ server.tool("browser_click", "Click an element by ref (from snapshot) or CSS selector. Prefer ref for reliability.", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), button: exports_external.enum(["left", "right", "middle"]).optional(), timeout: exports_external.number().optional() }, async ({ session_id, selector, ref, button, timeout }) => {
14801
15493
  try {
14802
15494
  const page = getSessionPage(session_id);
15495
+ if (ref) {
15496
+ await clickRef(page, session_id, ref, { timeout });
15497
+ return json({ clicked: ref, method: "ref" });
15498
+ }
15499
+ if (!selector)
15500
+ return err(new Error("Either ref or selector is required"));
14803
15501
  await click(page, selector, { button, timeout });
14804
- return json({ clicked: selector });
15502
+ return json({ clicked: selector, method: "selector" });
14805
15503
  } catch (e) {
14806
15504
  return err(e);
14807
15505
  }
14808
15506
  });
14809
- server.tool("browser_type", "Type text into an element", { session_id: exports_external.string(), selector: exports_external.string(), text: exports_external.string(), clear: exports_external.boolean().optional().default(false), delay: exports_external.number().optional() }, async ({ session_id, selector, text, clear, delay }) => {
15507
+ server.tool("browser_type", "Type text into an element by ref or selector. Prefer ref.", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), text: exports_external.string(), clear: exports_external.boolean().optional().default(false), delay: exports_external.number().optional() }, async ({ session_id, selector, ref, text, clear, delay }) => {
14810
15508
  try {
14811
15509
  const page = getSessionPage(session_id);
15510
+ if (ref) {
15511
+ await typeRef(page, session_id, ref, text, { clear, delay });
15512
+ return json({ typed: text, ref, method: "ref" });
15513
+ }
15514
+ if (!selector)
15515
+ return err(new Error("Either ref or selector is required"));
14812
15516
  await type(page, selector, text, { clear, delay });
14813
- return json({ typed: text, selector });
15517
+ return json({ typed: text, selector, method: "selector" });
14814
15518
  } catch (e) {
14815
15519
  return err(e);
14816
15520
  }
14817
15521
  });
14818
- server.tool("browser_hover", "Hover over an element", { session_id: exports_external.string(), selector: exports_external.string() }, async ({ session_id, selector }) => {
15522
+ server.tool("browser_hover", "Hover over an element by ref or selector", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional() }, async ({ session_id, selector, ref }) => {
14819
15523
  try {
14820
15524
  const page = getSessionPage(session_id);
15525
+ if (ref) {
15526
+ await hoverRef(page, session_id, ref);
15527
+ return json({ hovered: ref, method: "ref" });
15528
+ }
15529
+ if (!selector)
15530
+ return err(new Error("Either ref or selector is required"));
14821
15531
  await hover(page, selector);
14822
- return json({ hovered: selector });
15532
+ return json({ hovered: selector, method: "selector" });
14823
15533
  } catch (e) {
14824
15534
  return err(e);
14825
15535
  }
@@ -14833,20 +15543,32 @@ var init_mcp = __esm(async () => {
14833
15543
  return err(e);
14834
15544
  }
14835
15545
  });
14836
- server.tool("browser_select", "Select a dropdown option", { session_id: exports_external.string(), selector: exports_external.string(), value: exports_external.string() }, async ({ session_id, selector, value }) => {
15546
+ server.tool("browser_select", "Select a dropdown option by ref or selector", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), value: exports_external.string() }, async ({ session_id, selector, ref, value }) => {
14837
15547
  try {
14838
15548
  const page = getSessionPage(session_id);
15549
+ if (ref) {
15550
+ const selected2 = await selectRef(page, session_id, ref, value);
15551
+ return json({ selected: selected2, method: "ref" });
15552
+ }
15553
+ if (!selector)
15554
+ return err(new Error("Either ref or selector is required"));
14839
15555
  const selected = await selectOption(page, selector, value);
14840
- return json({ selected });
15556
+ return json({ selected, method: "selector" });
14841
15557
  } catch (e) {
14842
15558
  return err(e);
14843
15559
  }
14844
15560
  });
14845
- server.tool("browser_check", "Check or uncheck a checkbox", { session_id: exports_external.string(), selector: exports_external.string(), checked: exports_external.boolean() }, async ({ session_id, selector, checked }) => {
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 }) => {
14846
15562
  try {
14847
15563
  const page = getSessionPage(session_id);
15564
+ if (ref) {
15565
+ await checkRef(page, session_id, ref, checked);
15566
+ return json({ checked, ref, method: "ref" });
15567
+ }
15568
+ if (!selector)
15569
+ return err(new Error("Either ref or selector is required"));
14848
15570
  await checkBox(page, selector, checked);
14849
- return json({ checked, selector });
15571
+ return json({ checked, selector, method: "selector" });
14850
15572
  } catch (e) {
14851
15573
  return err(e);
14852
15574
  }
@@ -14927,15 +15649,17 @@ var init_mcp = __esm(async () => {
14927
15649
  return err(e);
14928
15650
  }
14929
15651
  });
14930
- server.tool("browser_snapshot", "Get an accessibility (ARIA) snapshot of the page", { session_id: exports_external.string() }, async ({ session_id }) => {
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 }) => {
14931
15653
  try {
14932
15654
  const page = getSessionPage(session_id);
14933
- return json({ snapshot: await getAriaSnapshot(page) });
15655
+ const result = await takeSnapshot(page, session_id);
15656
+ setLastSnapshot(session_id, result);
15657
+ return json({ snapshot: result.tree, refs: result.refs, interactive_count: result.interactive_count });
14934
15658
  } catch (e) {
14935
15659
  return err(e);
14936
15660
  }
14937
15661
  });
14938
- server.tool("browser_screenshot", "Take a screenshot of the page or an element", {
15662
+ server.tool("browser_screenshot", "Take a screenshot. Use annotate=true to overlay numbered labels on interactive elements for visual+ref workflows.", {
14939
15663
  session_id: exports_external.string(),
14940
15664
  selector: exports_external.string().optional(),
14941
15665
  full_page: exports_external.boolean().optional().default(false),
@@ -14943,17 +15667,37 @@ var init_mcp = __esm(async () => {
14943
15667
  quality: exports_external.number().optional(),
14944
15668
  max_width: exports_external.number().optional().default(1280),
14945
15669
  compress: exports_external.boolean().optional().default(true),
14946
- thumbnail: exports_external.boolean().optional().default(true)
14947
- }, async ({ session_id, selector, full_page, format, quality, max_width, compress, thumbnail }) => {
15670
+ thumbnail: exports_external.boolean().optional().default(true),
15671
+ annotate: exports_external.boolean().optional().default(false)
15672
+ }, async ({ session_id, selector, full_page, format, quality, max_width, compress, thumbnail, annotate }) => {
14948
15673
  try {
14949
15674
  const page = getSessionPage(session_id);
15675
+ if (annotate && !selector && !full_page) {
15676
+ const { annotateScreenshot: annotateScreenshot2 } = await Promise.resolve().then(() => (init_annotate(), exports_annotate));
15677
+ const annotated = await annotateScreenshot2(page, session_id);
15678
+ const base64 = annotated.buffer.toString("base64");
15679
+ return json({
15680
+ base64: base64.length > 50000 ? undefined : base64,
15681
+ base64_truncated: base64.length > 50000,
15682
+ size_bytes: annotated.buffer.length,
15683
+ annotations: annotated.annotations,
15684
+ label_to_ref: annotated.labelToRef,
15685
+ annotation_count: annotated.annotations.length
15686
+ });
15687
+ }
14950
15688
  const result = await takeScreenshot(page, { selector, fullPage: full_page, format, quality, maxWidth: max_width, compress, thumbnail });
15689
+ result.url = page.url();
14951
15690
  try {
14952
15691
  const buf = Buffer.from(result.base64, "base64");
14953
15692
  const filename = result.path.split("/").pop() ?? `screenshot.${format ?? "webp"}`;
14954
15693
  const dl = saveToDownloads(buf, filename, { sessionId: session_id, type: "screenshot", sourceUrl: page.url() });
14955
15694
  result.download_id = dl.id;
14956
15695
  } catch {}
15696
+ if (result.base64.length > 50000) {
15697
+ result.base64_truncated = true;
15698
+ result.full_image_path = result.path;
15699
+ result.base64 = result.thumbnail_base64 ?? "";
15700
+ }
14957
15701
  return json(result);
14958
15702
  } catch (e) {
14959
15703
  return err(e);
@@ -15248,6 +15992,37 @@ var init_mcp = __esm(async () => {
15248
15992
  return err(e);
15249
15993
  }
15250
15994
  });
15995
+ server.tool("browser_scroll_and_screenshot", "Scroll the page and take a screenshot in one call. Saves 3 separate tool calls.", { session_id: exports_external.string(), direction: exports_external.enum(["up", "down", "left", "right"]).optional().default("down"), amount: exports_external.number().optional().default(500), wait_ms: exports_external.number().optional().default(300) }, async ({ session_id, direction, amount, wait_ms }) => {
15996
+ try {
15997
+ const page = getSessionPage(session_id);
15998
+ await scroll(page, direction, amount);
15999
+ await new Promise((r) => setTimeout(r, wait_ms));
16000
+ const result = await takeScreenshot(page, { maxWidth: 1280, track: true });
16001
+ result.url = page.url();
16002
+ if (result.base64.length > 50000) {
16003
+ result.base64_truncated = true;
16004
+ result.full_image_path = result.path;
16005
+ result.base64 = result.thumbnail_base64 ?? "";
16006
+ }
16007
+ return json({ scrolled: { direction, amount }, screenshot: result });
16008
+ } catch (e) {
16009
+ return err(e);
16010
+ }
16011
+ });
16012
+ server.tool("browser_wait_for_navigation", "Wait for URL change after a click or action. Returns the new URL and title.", { session_id: exports_external.string(), timeout: exports_external.number().optional().default(30000), url_pattern: exports_external.string().optional() }, async ({ session_id, timeout, url_pattern }) => {
16013
+ try {
16014
+ const page = getSessionPage(session_id);
16015
+ const start = Date.now();
16016
+ if (url_pattern) {
16017
+ await page.waitForURL(url_pattern, { timeout });
16018
+ } else {
16019
+ await page.waitForLoadState("domcontentloaded", { timeout });
16020
+ }
16021
+ return json({ url: page.url(), title: await getTitle(page), elapsed_ms: Date.now() - start });
16022
+ } catch (e) {
16023
+ return err(e);
16024
+ }
16025
+ });
15251
16026
  server.tool("browser_session_get_by_name", "Get a session by its name", { name: exports_external.string() }, async ({ name }) => {
15252
16027
  try {
15253
16028
  const session = getSessionByName2(name);
@@ -15361,6 +16136,40 @@ var init_mcp = __esm(async () => {
15361
16136
  return err(e);
15362
16137
  }
15363
16138
  });
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
+ });
15364
16173
  server.tool("browser_gallery_list", "List screenshot gallery entries with optional filters", {
15365
16174
  project_id: exports_external.string().optional(),
15366
16175
  session_id: exports_external.string().optional(),
@@ -15509,6 +16318,297 @@ var init_mcp = __esm(async () => {
15509
16318
  return err(e);
15510
16319
  }
15511
16320
  });
16321
+ server.tool("browser_snapshot_diff", "Take a new accessibility snapshot and diff it against the last snapshot for this session. Shows added/removed/modified interactive elements.", { session_id: exports_external.string() }, async ({ session_id }) => {
16322
+ try {
16323
+ const page = getSessionPage(session_id);
16324
+ const before = getLastSnapshot(session_id);
16325
+ const after = await takeSnapshot(page, session_id);
16326
+ setLastSnapshot(session_id, after);
16327
+ if (!before) {
16328
+ return json({
16329
+ message: "No previous snapshot \u2014 returning current snapshot only.",
16330
+ snapshot: after.tree,
16331
+ refs: after.refs,
16332
+ interactive_count: after.interactive_count
16333
+ });
16334
+ }
16335
+ const diff = diffSnapshots(before, after);
16336
+ return json({
16337
+ diff,
16338
+ added_count: diff.added.length,
16339
+ removed_count: diff.removed.length,
16340
+ modified_count: diff.modified.length,
16341
+ url_changed: diff.url_changed,
16342
+ title_changed: diff.title_changed,
16343
+ current_interactive_count: after.interactive_count
16344
+ });
16345
+ } catch (e) {
16346
+ return err(e);
16347
+ }
16348
+ });
16349
+ server.tool("browser_session_stats", "Get session info and estimated token usage (based on network log, console log, and gallery entry sizes).", { session_id: exports_external.string() }, async ({ session_id }) => {
16350
+ try {
16351
+ const session = getSession2(session_id);
16352
+ const networkLog = getNetworkLog(session_id);
16353
+ const consoleLog = getConsoleLog(session_id);
16354
+ const galleryEntries = listEntries({ sessionId: session_id, limit: 1000 });
16355
+ let totalChars = 0;
16356
+ for (const req of networkLog) {
16357
+ totalChars += (req.url?.length ?? 0) + (req.request_headers?.length ?? 0) + (req.response_headers?.length ?? 0) + (req.request_body?.length ?? 0);
16358
+ }
16359
+ for (const msg of consoleLog) {
16360
+ totalChars += (msg.message?.length ?? 0) + (msg.source?.length ?? 0);
16361
+ }
16362
+ for (const entry of galleryEntries) {
16363
+ totalChars += (entry.url?.length ?? 0) + (entry.title?.length ?? 0) + (entry.notes?.length ?? 0) + (entry.tags?.join(",").length ?? 0);
16364
+ }
16365
+ const estimatedTokens = Math.ceil(totalChars / 4);
16366
+ const tokenBudget = getTokenBudget(session_id);
16367
+ return json({
16368
+ session,
16369
+ network_request_count: networkLog.length,
16370
+ console_message_count: consoleLog.length,
16371
+ gallery_entry_count: galleryEntries.length,
16372
+ estimated_tokens_used: estimatedTokens,
16373
+ token_budget: tokenBudget,
16374
+ data_size_chars: totalChars
16375
+ });
16376
+ } catch (e) {
16377
+ return err(e);
16378
+ }
16379
+ });
16380
+ server.tool("browser_tab_new", "Open a new tab in the session's browser context, optionally navigating to a URL", { session_id: exports_external.string(), url: exports_external.string().optional() }, async ({ session_id, url }) => {
16381
+ try {
16382
+ const page = getSessionPage(session_id);
16383
+ const tab = await newTab(page, url);
16384
+ return json(tab);
16385
+ } catch (e) {
16386
+ return err(e);
16387
+ }
16388
+ });
16389
+ server.tool("browser_tab_list", "List all open tabs in the session's browser context", { session_id: exports_external.string() }, async ({ session_id }) => {
16390
+ try {
16391
+ const page = getSessionPage(session_id);
16392
+ const tabs = await listTabs(page);
16393
+ return json({ tabs, count: tabs.length });
16394
+ } catch (e) {
16395
+ return err(e);
16396
+ }
16397
+ });
16398
+ server.tool("browser_tab_switch", "Switch to a different tab by index. Updates the session's active page.", { session_id: exports_external.string(), tab_id: exports_external.number() }, async ({ session_id, tab_id }) => {
16399
+ try {
16400
+ const page = getSessionPage(session_id);
16401
+ const result = await switchTab(page, tab_id);
16402
+ setSessionPage(session_id, result.page);
16403
+ return json(result.tab);
16404
+ } catch (e) {
16405
+ return err(e);
16406
+ }
16407
+ });
16408
+ server.tool("browser_tab_close", "Close a tab by index. Cannot close the last tab.", { session_id: exports_external.string(), tab_id: exports_external.number() }, async ({ session_id, tab_id }) => {
16409
+ try {
16410
+ const page = getSessionPage(session_id);
16411
+ const context = page.context();
16412
+ const result = await closeTab(page, tab_id);
16413
+ const remainingPages = context.pages();
16414
+ const newActivePage = remainingPages[result.active_tab.index];
16415
+ if (newActivePage) {
16416
+ setSessionPage(session_id, newActivePage);
16417
+ }
16418
+ return json(result);
16419
+ } catch (e) {
16420
+ return err(e);
16421
+ }
16422
+ });
16423
+ server.tool("browser_handle_dialog", "Accept or dismiss a pending dialog (alert, confirm, prompt). Handles the oldest pending dialog.", { session_id: exports_external.string(), action: exports_external.enum(["accept", "dismiss"]), prompt_text: exports_external.string().optional() }, async ({ session_id, action, prompt_text }) => {
16424
+ try {
16425
+ const result = await handleDialog(session_id, action, prompt_text);
16426
+ if (!result.handled)
16427
+ return err(new Error("No pending dialogs for this session"));
16428
+ return json(result);
16429
+ } catch (e) {
16430
+ return err(e);
16431
+ }
16432
+ });
16433
+ server.tool("browser_get_dialogs", "Get all pending dialogs for a session", { session_id: exports_external.string() }, async ({ session_id }) => {
16434
+ try {
16435
+ const dialogs = getDialogs(session_id);
16436
+ return json({ dialogs, count: dialogs.length });
16437
+ } catch (e) {
16438
+ return err(e);
16439
+ }
16440
+ });
16441
+ server.tool("browser_profile_save", "Save cookies + localStorage from the current session as a named profile", { session_id: exports_external.string(), name: exports_external.string() }, async ({ session_id, name }) => {
16442
+ try {
16443
+ const page = getSessionPage(session_id);
16444
+ const info = await saveProfile(page, name);
16445
+ return json(info);
16446
+ } catch (e) {
16447
+ return err(e);
16448
+ }
16449
+ });
16450
+ server.tool("browser_profile_load", "Load a saved profile and apply cookies + localStorage to the current session", { session_id: exports_external.string().optional(), name: exports_external.string() }, async ({ session_id, name }) => {
16451
+ try {
16452
+ const profileData = loadProfile(name);
16453
+ if (session_id) {
16454
+ const page = getSessionPage(session_id);
16455
+ const applied = await applyProfile(page, profileData);
16456
+ return json({ ...applied, profile: name });
16457
+ }
16458
+ return json({ profile: name, cookies: profileData.cookies.length, storage_keys: Object.keys(profileData.localStorage).length });
16459
+ } catch (e) {
16460
+ return err(e);
16461
+ }
16462
+ });
16463
+ server.tool("browser_profile_list", "List all saved browser profiles", {}, async () => {
16464
+ try {
16465
+ return json({ profiles: listProfiles() });
16466
+ } catch (e) {
16467
+ return err(e);
16468
+ }
16469
+ });
16470
+ server.tool("browser_profile_delete", "Delete a saved browser profile", { name: exports_external.string() }, async ({ name }) => {
16471
+ try {
16472
+ const deleted = deleteProfile(name);
16473
+ if (!deleted)
16474
+ return err(new Error(`Profile not found: ${name}`));
16475
+ return json({ deleted: name });
16476
+ } catch (e) {
16477
+ return err(e);
16478
+ }
16479
+ });
16480
+ server.tool("browser_help", "Show all available browser tools grouped by category with one-line descriptions", {}, async () => {
16481
+ try {
16482
+ const groups = {
16483
+ Navigation: [
16484
+ { tool: "browser_navigate", description: "Navigate to a URL" },
16485
+ { tool: "browser_back", description: "Navigate back in history" },
16486
+ { tool: "browser_forward", description: "Navigate forward in history" },
16487
+ { tool: "browser_reload", description: "Reload the current page" },
16488
+ { tool: "browser_wait_for_navigation", description: "Wait for URL change after action" }
16489
+ ],
16490
+ Interaction: [
16491
+ { tool: "browser_click", description: "Click element by ref or selector" },
16492
+ { tool: "browser_click_text", description: "Click element by visible text" },
16493
+ { tool: "browser_type", description: "Type text into an element" },
16494
+ { tool: "browser_hover", description: "Hover over an element" },
16495
+ { tool: "browser_scroll", description: "Scroll the page" },
16496
+ { tool: "browser_select", description: "Select a dropdown option" },
16497
+ { tool: "browser_check", description: "Check/uncheck a checkbox" },
16498
+ { tool: "browser_upload", description: "Upload a file to an input" },
16499
+ { tool: "browser_press_key", description: "Press a keyboard key" },
16500
+ { tool: "browser_wait", description: "Wait for a selector to appear" },
16501
+ { tool: "browser_wait_for_text", description: "Wait for text to appear" },
16502
+ { tool: "browser_fill_form", description: "Fill multiple form fields at once" },
16503
+ { tool: "browser_handle_dialog", description: "Accept or dismiss a dialog" }
16504
+ ],
16505
+ Extraction: [
16506
+ { tool: "browser_get_text", description: "Get text content from page/selector" },
16507
+ { tool: "browser_get_html", description: "Get HTML content from page/selector" },
16508
+ { tool: "browser_get_links", description: "Get all links on the page" },
16509
+ { tool: "browser_get_page_info", description: "Full page summary in one call" },
16510
+ { tool: "browser_extract", description: "Extract content in various formats" },
16511
+ { tool: "browser_find", description: "Find elements by selector" },
16512
+ { tool: "browser_element_exists", description: "Check if a selector exists" },
16513
+ { tool: "browser_snapshot", description: "Get accessibility snapshot with refs" },
16514
+ { tool: "browser_evaluate", description: "Execute JavaScript in page context" }
16515
+ ],
16516
+ Capture: [
16517
+ { tool: "browser_screenshot", description: "Take a screenshot (PNG/JPEG/WebP)" },
16518
+ { tool: "browser_pdf", description: "Generate a PDF of the page" },
16519
+ { tool: "browser_scroll_and_screenshot", description: "Scroll then screenshot in one call" }
16520
+ ],
16521
+ Storage: [
16522
+ { tool: "browser_cookies_get", description: "Get cookies" },
16523
+ { tool: "browser_cookies_set", description: "Set a cookie" },
16524
+ { tool: "browser_cookies_clear", description: "Clear cookies" },
16525
+ { tool: "browser_storage_get", description: "Get localStorage/sessionStorage" },
16526
+ { tool: "browser_storage_set", description: "Set localStorage/sessionStorage" },
16527
+ { tool: "browser_profile_save", description: "Save cookies + localStorage as profile" },
16528
+ { tool: "browser_profile_load", description: "Load and apply a saved profile" },
16529
+ { tool: "browser_profile_list", description: "List saved profiles" },
16530
+ { tool: "browser_profile_delete", description: "Delete a saved profile" }
16531
+ ],
16532
+ Network: [
16533
+ { tool: "browser_network_log", description: "Get captured network requests" },
16534
+ { tool: "browser_network_intercept", description: "Add a network interception rule" },
16535
+ { tool: "browser_har_start", description: "Start HAR capture" },
16536
+ { tool: "browser_har_stop", description: "Stop HAR capture and get data" }
16537
+ ],
16538
+ Performance: [
16539
+ { tool: "browser_performance", description: "Get performance metrics" }
16540
+ ],
16541
+ Console: [
16542
+ { tool: "browser_console_log", description: "Get console messages" },
16543
+ { tool: "browser_has_errors", description: "Check for console errors" },
16544
+ { tool: "browser_clear_errors", description: "Clear console error log" },
16545
+ { tool: "browser_get_dialogs", description: "Get pending dialogs" }
16546
+ ],
16547
+ Recording: [
16548
+ { tool: "browser_record_start", description: "Start recording actions" },
16549
+ { tool: "browser_record_step", description: "Add a step to recording" },
16550
+ { tool: "browser_record_stop", description: "Stop and save recording" },
16551
+ { tool: "browser_record_replay", description: "Replay a recorded sequence" },
16552
+ { tool: "browser_recordings_list", description: "List all recordings" }
16553
+ ],
16554
+ Crawl: [
16555
+ { tool: "browser_crawl", description: "Crawl a URL recursively" }
16556
+ ],
16557
+ Agent: [
16558
+ { tool: "browser_register_agent", description: "Register an agent" },
16559
+ { tool: "browser_heartbeat", description: "Send agent heartbeat" },
16560
+ { tool: "browser_agent_list", description: "List registered agents" }
16561
+ ],
16562
+ Project: [
16563
+ { tool: "browser_project_create", description: "Create or ensure a project" },
16564
+ { tool: "browser_project_list", description: "List all projects" }
16565
+ ],
16566
+ Gallery: [
16567
+ { tool: "browser_gallery_list", description: "List screenshot gallery entries" },
16568
+ { tool: "browser_gallery_get", description: "Get a gallery entry by id" },
16569
+ { tool: "browser_gallery_tag", description: "Add a tag to gallery entry" },
16570
+ { tool: "browser_gallery_untag", description: "Remove a tag from gallery entry" },
16571
+ { tool: "browser_gallery_favorite", description: "Mark/unmark as favorite" },
16572
+ { tool: "browser_gallery_delete", description: "Delete a gallery entry" },
16573
+ { tool: "browser_gallery_search", description: "Search gallery entries" },
16574
+ { tool: "browser_gallery_stats", description: "Get gallery statistics" },
16575
+ { tool: "browser_gallery_diff", description: "Pixel-diff two screenshots" }
16576
+ ],
16577
+ Downloads: [
16578
+ { tool: "browser_downloads_list", description: "List downloaded files" },
16579
+ { tool: "browser_downloads_get", description: "Get a download by id" },
16580
+ { tool: "browser_downloads_delete", description: "Delete a download" },
16581
+ { tool: "browser_downloads_clean", description: "Clean old downloads" },
16582
+ { tool: "browser_downloads_export", description: "Copy download to a path" },
16583
+ { tool: "browser_persist_file", description: "Persist file permanently" }
16584
+ ],
16585
+ Session: [
16586
+ { tool: "browser_session_create", description: "Create a new browser session" },
16587
+ { tool: "browser_session_list", description: "List all sessions" },
16588
+ { tool: "browser_session_close", description: "Close a session" },
16589
+ { tool: "browser_session_get_by_name", description: "Get session by name" },
16590
+ { tool: "browser_session_rename", description: "Rename a session" },
16591
+ { tool: "browser_session_stats", description: "Get session stats and token usage" },
16592
+ { tool: "browser_tab_new", description: "Open a new tab" },
16593
+ { tool: "browser_tab_list", description: "List all open tabs" },
16594
+ { tool: "browser_tab_switch", description: "Switch to a tab by index" },
16595
+ { tool: "browser_tab_close", description: "Close a tab by index" }
16596
+ ],
16597
+ Meta: [
16598
+ { tool: "browser_page_check", description: "One-call page summary with diagnostics" },
16599
+ { tool: "browser_help", description: "Show this help (all tools)" },
16600
+ { tool: "browser_snapshot_diff", description: "Diff current snapshot vs previous" },
16601
+ { tool: "browser_watch_start", description: "Watch page for DOM changes" },
16602
+ { tool: "browser_watch_get_changes", description: "Get captured DOM changes" },
16603
+ { tool: "browser_watch_stop", description: "Stop DOM watcher" }
16604
+ ]
16605
+ };
16606
+ const totalTools = Object.values(groups).reduce((sum, g) => sum + g.length, 0);
16607
+ return json({ groups, total_tools: totalTools });
16608
+ } catch (e) {
16609
+ return err(e);
16610
+ }
16611
+ });
15512
16612
  transport = new StdioServerTransport;
15513
16613
  await server.connect(transport);
15514
16614
  });
@@ -15551,8 +16651,8 @@ var init_snapshots = __esm(() => {
15551
16651
 
15552
16652
  // src/server/index.ts
15553
16653
  var exports_server = {};
15554
- import { join as join6 } from "path";
15555
- import { existsSync as existsSync3 } from "fs";
16654
+ import { join as join7 } from "path";
16655
+ import { existsSync as existsSync4 } from "fs";
15556
16656
  function ok(data, status = 200) {
15557
16657
  return new Response(JSON.stringify(data), {
15558
16658
  status,
@@ -15802,14 +16902,14 @@ var init_server = __esm(() => {
15802
16902
  if (path.match(/^\/api\/gallery\/([^/]+)\/thumbnail$/) && method === "GET") {
15803
16903
  const id = path.split("/")[3];
15804
16904
  const entry = getEntry(id);
15805
- if (!entry?.thumbnail_path || !existsSync3(entry.thumbnail_path))
16905
+ if (!entry?.thumbnail_path || !existsSync4(entry.thumbnail_path))
15806
16906
  return notFound("Thumbnail not found");
15807
16907
  return new Response(Bun.file(entry.thumbnail_path), { headers: { ...CORS_HEADERS } });
15808
16908
  }
15809
16909
  if (path.match(/^\/api\/gallery\/([^/]+)\/image$/) && method === "GET") {
15810
16910
  const id = path.split("/")[3];
15811
16911
  const entry = getEntry(id);
15812
- if (!entry?.path || !existsSync3(entry.path))
16912
+ if (!entry?.path || !existsSync4(entry.path))
15813
16913
  return notFound("Image not found");
15814
16914
  return new Response(Bun.file(entry.path), { headers: { ...CORS_HEADERS } });
15815
16915
  }
@@ -15837,7 +16937,7 @@ var init_server = __esm(() => {
15837
16937
  if (path.match(/^\/api\/downloads\/([^/]+)\/raw$/) && method === "GET") {
15838
16938
  const id = path.split("/")[3];
15839
16939
  const file = getDownload(id);
15840
- if (!file || !existsSync3(file.path))
16940
+ if (!file || !existsSync4(file.path))
15841
16941
  return notFound("Download not found");
15842
16942
  return new Response(Bun.file(file.path), { headers: { ...CORS_HEADERS } });
15843
16943
  }
@@ -15845,13 +16945,13 @@ var init_server = __esm(() => {
15845
16945
  const id = path.split("/")[3];
15846
16946
  return ok({ deleted: deleteDownload(id) });
15847
16947
  }
15848
- const dashboardDist = join6(import.meta.dir, "../../dashboard/dist");
15849
- if (existsSync3(dashboardDist)) {
15850
- const filePath = path === "/" ? join6(dashboardDist, "index.html") : join6(dashboardDist, path);
15851
- if (existsSync3(filePath)) {
16948
+ const dashboardDist = join7(import.meta.dir, "../../dashboard/dist");
16949
+ if (existsSync4(dashboardDist)) {
16950
+ const filePath = path === "/" ? join7(dashboardDist, "index.html") : join7(dashboardDist, path);
16951
+ if (existsSync4(filePath)) {
15852
16952
  return new Response(Bun.file(filePath), { headers: CORS_HEADERS });
15853
16953
  }
15854
- return new Response(Bun.file(join6(dashboardDist, "index.html")), { headers: CORS_HEADERS });
16954
+ return new Response(Bun.file(join7(dashboardDist, "index.html")), { headers: CORS_HEADERS });
15855
16955
  }
15856
16956
  if (path === "/" || path === "") {
15857
16957
  return new Response("@hasna/browser REST API running. Dashboard not built.", {
@@ -15893,9 +16993,12 @@ init_projects();
15893
16993
  init_recorder();
15894
16994
  init_recordings();
15895
16995
  init_lightpanda();
16996
+ import { readFileSync as readFileSync3 } from "fs";
16997
+ import { join as join8 } from "path";
15896
16998
  import chalk from "chalk";
16999
+ var pkg = JSON.parse(readFileSync3(join8(import.meta.dir, "../../package.json"), "utf8"));
15897
17000
  var program2 = new Command;
15898
- program2.name("browser").description("@hasna/browser \u2014 general-purpose browser agent CLI").version("0.0.1");
17001
+ program2.name("browser").description("@hasna/browser \u2014 general-purpose browser agent CLI").version(pkg.version);
15899
17002
  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) => {
15900
17003
  const { session, page } = await createSession2({ engine: opts.engine, headless: true });
15901
17004
  console.log(chalk.gray(`Session: ${session.id} (${session.engine})`));
@@ -16136,11 +17239,11 @@ galleryCmd.command("stats").description("Show gallery statistics").option("--pro
16136
17239
  });
16137
17240
  galleryCmd.command("clean").description("Delete gallery entries with missing files").action(async () => {
16138
17241
  const { listEntries: listEntries2, deleteEntry: deleteEntry2 } = await Promise.resolve().then(() => (init_gallery(), exports_gallery));
16139
- const { existsSync: existsSync4 } = await import("fs");
17242
+ const { existsSync: existsSync5 } = await import("fs");
16140
17243
  const entries = listEntries2({ limit: 9999 });
16141
17244
  let removed = 0;
16142
17245
  for (const e of entries) {
16143
- if (!existsSync4(e.path)) {
17246
+ if (!existsSync5(e.path)) {
16144
17247
  deleteEntry2(e.id);
16145
17248
  removed++;
16146
17249
  }