@hasna/browser 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +669 -106
- package/dist/engines/bun-webview.d.ts +147 -0
- package/dist/engines/bun-webview.d.ts.map +1 -0
- package/dist/engines/bun-webview.test.d.ts +2 -0
- package/dist/engines/bun-webview.test.d.ts.map +1 -0
- package/dist/engines/selector.d.ts +2 -2
- package/dist/engines/selector.d.ts.map +1 -1
- package/dist/index.js +804 -278
- package/dist/lib/extractor.d.ts.map +1 -1
- package/dist/lib/screenshot.d.ts.map +1 -1
- package/dist/lib/session.d.ts +3 -0
- package/dist/lib/session.d.ts.map +1 -1
- package/dist/lib/snapshot.d.ts +1 -0
- package/dist/lib/snapshot.d.ts.map +1 -1
- package/dist/mcp/index.js +659 -100
- package/dist/server/index.js +565 -86
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2515,11 +2515,420 @@ var init_lightpanda = __esm(() => {
|
|
|
2515
2515
|
LIGHTPANDA_BINARY = process.env["LIGHTPANDA_BINARY"] ?? "lightpanda";
|
|
2516
2516
|
});
|
|
2517
2517
|
|
|
2518
|
+
// src/engines/bun-webview.ts
|
|
2519
|
+
import { join as join2 } from "path";
|
|
2520
|
+
import { mkdirSync as mkdirSync2 } from "fs";
|
|
2521
|
+
import { homedir as homedir2 } from "os";
|
|
2522
|
+
function isBunWebViewAvailable() {
|
|
2523
|
+
return typeof globalThis.Bun !== "undefined" && typeof globalThis.Bun.WebView !== "undefined";
|
|
2524
|
+
}
|
|
2525
|
+
function getProfileDir(profileName) {
|
|
2526
|
+
const base = process.env["BROWSER_DATA_DIR"] ?? join2(homedir2(), ".browser");
|
|
2527
|
+
const dir = join2(base, "profiles", profileName);
|
|
2528
|
+
mkdirSync2(dir, { recursive: true });
|
|
2529
|
+
return dir;
|
|
2530
|
+
}
|
|
2531
|
+
var BunWebViewSession;
|
|
2532
|
+
var init_bun_webview = __esm(() => {
|
|
2533
|
+
BunWebViewSession = class BunWebViewSession {
|
|
2534
|
+
view;
|
|
2535
|
+
_sessionId;
|
|
2536
|
+
_eventListeners = new Map;
|
|
2537
|
+
constructor(opts = {}) {
|
|
2538
|
+
if (!isBunWebViewAvailable()) {
|
|
2539
|
+
throw new Error("Bun.WebView is not available. Install Bun canary: bun upgrade --canary");
|
|
2540
|
+
}
|
|
2541
|
+
const BunWebView = globalThis.Bun.WebView;
|
|
2542
|
+
const constructorOpts = {
|
|
2543
|
+
width: opts.width ?? 1280,
|
|
2544
|
+
height: opts.height ?? 720
|
|
2545
|
+
};
|
|
2546
|
+
if (opts.profile) {
|
|
2547
|
+
constructorOpts.dataStore = { directory: getProfileDir(opts.profile) };
|
|
2548
|
+
} else {
|
|
2549
|
+
constructorOpts.dataStore = "ephemeral";
|
|
2550
|
+
}
|
|
2551
|
+
if (opts.onConsole) {
|
|
2552
|
+
constructorOpts.console = opts.onConsole;
|
|
2553
|
+
}
|
|
2554
|
+
this.view = new BunWebView(constructorOpts);
|
|
2555
|
+
this.view.onNavigated = (url) => {
|
|
2556
|
+
this._emit("navigated", url);
|
|
2557
|
+
};
|
|
2558
|
+
this.view.onNavigationFailed = (error) => {
|
|
2559
|
+
this._emit("navigationfailed", error);
|
|
2560
|
+
};
|
|
2561
|
+
}
|
|
2562
|
+
async goto(url, opts) {
|
|
2563
|
+
await this.view.navigate(url);
|
|
2564
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
2565
|
+
}
|
|
2566
|
+
async goBack() {
|
|
2567
|
+
await this.view.goBack();
|
|
2568
|
+
}
|
|
2569
|
+
async goForward() {
|
|
2570
|
+
await this.view.goForward();
|
|
2571
|
+
}
|
|
2572
|
+
async reload() {
|
|
2573
|
+
await this.view.reload();
|
|
2574
|
+
}
|
|
2575
|
+
async evaluate(fnOrExpr, ...args) {
|
|
2576
|
+
let expr;
|
|
2577
|
+
if (typeof fnOrExpr === "function") {
|
|
2578
|
+
const serializedArgs = args.map((a) => JSON.stringify(a)).join(", ");
|
|
2579
|
+
expr = `(${fnOrExpr.toString()})(${serializedArgs})`;
|
|
2580
|
+
} else {
|
|
2581
|
+
expr = fnOrExpr;
|
|
2582
|
+
}
|
|
2583
|
+
return this.view.evaluate(expr);
|
|
2584
|
+
}
|
|
2585
|
+
async screenshot(opts) {
|
|
2586
|
+
const uint8 = await this.view.screenshot();
|
|
2587
|
+
return Buffer.from(uint8);
|
|
2588
|
+
}
|
|
2589
|
+
async click(selector, opts) {
|
|
2590
|
+
await this.view.click(selector, opts ? { button: opts.button } : undefined);
|
|
2591
|
+
}
|
|
2592
|
+
async type(selector, text, opts) {
|
|
2593
|
+
try {
|
|
2594
|
+
await this.view.click(selector);
|
|
2595
|
+
} catch {}
|
|
2596
|
+
await this.view.type(text);
|
|
2597
|
+
}
|
|
2598
|
+
async fill(selector, value) {
|
|
2599
|
+
await this.view.evaluate(`
|
|
2600
|
+
(() => {
|
|
2601
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
2602
|
+
if (el) { el.value = ''; el.dispatchEvent(new Event('input')); }
|
|
2603
|
+
})()
|
|
2604
|
+
`);
|
|
2605
|
+
await this.type(selector, value);
|
|
2606
|
+
}
|
|
2607
|
+
async press(key, opts) {
|
|
2608
|
+
await this.view.press(key, opts);
|
|
2609
|
+
}
|
|
2610
|
+
async scroll(direction, amount) {
|
|
2611
|
+
const dx = direction === "left" ? -amount : direction === "right" ? amount : 0;
|
|
2612
|
+
const dy = direction === "up" ? -amount : direction === "down" ? amount : 0;
|
|
2613
|
+
await this.view.scroll(dx, dy);
|
|
2614
|
+
}
|
|
2615
|
+
async scrollIntoView(selector) {
|
|
2616
|
+
await this.view.scrollTo(selector);
|
|
2617
|
+
}
|
|
2618
|
+
async hover(selector) {
|
|
2619
|
+
try {
|
|
2620
|
+
await this.view.scrollTo(selector);
|
|
2621
|
+
} catch {}
|
|
2622
|
+
}
|
|
2623
|
+
async resize(width, height) {
|
|
2624
|
+
await this.view.resize(width, height);
|
|
2625
|
+
}
|
|
2626
|
+
async $(selector) {
|
|
2627
|
+
const exists = await this.view.evaluate(`!!document.querySelector(${JSON.stringify(selector)})`);
|
|
2628
|
+
if (!exists)
|
|
2629
|
+
return null;
|
|
2630
|
+
return {
|
|
2631
|
+
textContent: async () => this.view.evaluate(`document.querySelector(${JSON.stringify(selector)})?.textContent ?? null`)
|
|
2632
|
+
};
|
|
2633
|
+
}
|
|
2634
|
+
async $$(selector) {
|
|
2635
|
+
const count = await this.view.evaluate(`document.querySelectorAll(${JSON.stringify(selector)}).length`);
|
|
2636
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
2637
|
+
textContent: async () => this.view.evaluate(`document.querySelectorAll(${JSON.stringify(selector)})[${i}]?.textContent ?? null`)
|
|
2638
|
+
}));
|
|
2639
|
+
}
|
|
2640
|
+
async inputValue(selector) {
|
|
2641
|
+
return this.view.evaluate(`document.querySelector(${JSON.stringify(selector)})?.value ?? ''`);
|
|
2642
|
+
}
|
|
2643
|
+
async isChecked(selector) {
|
|
2644
|
+
return this.view.evaluate(`!!(document.querySelector(${JSON.stringify(selector)})?.checked)`);
|
|
2645
|
+
}
|
|
2646
|
+
async isVisible(selector) {
|
|
2647
|
+
return this.view.evaluate(`
|
|
2648
|
+
(() => {
|
|
2649
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
2650
|
+
if (!el) return false;
|
|
2651
|
+
const style = window.getComputedStyle(el);
|
|
2652
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && el.offsetWidth > 0;
|
|
2653
|
+
})()
|
|
2654
|
+
`);
|
|
2655
|
+
}
|
|
2656
|
+
async isEnabled(selector) {
|
|
2657
|
+
return this.view.evaluate(`!(document.querySelector(${JSON.stringify(selector)})?.disabled)`);
|
|
2658
|
+
}
|
|
2659
|
+
async selectOption(selector, value) {
|
|
2660
|
+
await this.view.evaluate(`
|
|
2661
|
+
(() => {
|
|
2662
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
2663
|
+
if (el) {
|
|
2664
|
+
el.value = ${JSON.stringify(value)};
|
|
2665
|
+
el.dispatchEvent(new Event('change'));
|
|
2666
|
+
}
|
|
2667
|
+
})()
|
|
2668
|
+
`);
|
|
2669
|
+
return [value];
|
|
2670
|
+
}
|
|
2671
|
+
async check(selector) {
|
|
2672
|
+
await this.view.evaluate(`
|
|
2673
|
+
(() => {
|
|
2674
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
2675
|
+
if (el && !el.checked) { el.checked = true; el.dispatchEvent(new Event('change')); }
|
|
2676
|
+
})()
|
|
2677
|
+
`);
|
|
2678
|
+
}
|
|
2679
|
+
async uncheck(selector) {
|
|
2680
|
+
await this.view.evaluate(`
|
|
2681
|
+
(() => {
|
|
2682
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
2683
|
+
if (el && el.checked) { el.checked = false; el.dispatchEvent(new Event('change')); }
|
|
2684
|
+
})()
|
|
2685
|
+
`);
|
|
2686
|
+
}
|
|
2687
|
+
async setInputFiles(selector, files) {
|
|
2688
|
+
throw new Error("File upload not supported in Bun.WebView engine. Use engine: 'playwright' instead.");
|
|
2689
|
+
}
|
|
2690
|
+
getByRole(role, opts) {
|
|
2691
|
+
const name = opts?.name?.toString() ?? "";
|
|
2692
|
+
const selector = name ? `[role="${role}"][aria-label*="${name}"], ${role}[aria-label*="${name}"]` : `[role="${role}"], ${role}`;
|
|
2693
|
+
return {
|
|
2694
|
+
click: (clickOpts) => this.click(selector, clickOpts),
|
|
2695
|
+
fill: (value) => this.fill(selector, value),
|
|
2696
|
+
check: () => this.check(selector),
|
|
2697
|
+
uncheck: () => this.uncheck(selector),
|
|
2698
|
+
isVisible: () => this.isVisible(selector),
|
|
2699
|
+
textContent: () => this.view.evaluate(`document.querySelector(${JSON.stringify(selector)})?.textContent ?? null`),
|
|
2700
|
+
inputValue: () => this.inputValue(selector),
|
|
2701
|
+
first: () => ({
|
|
2702
|
+
click: (clickOpts) => this.click(selector, clickOpts),
|
|
2703
|
+
fill: (value) => this.fill(selector, value),
|
|
2704
|
+
textContent: () => this.view.evaluate(`document.querySelector(${JSON.stringify(selector)})?.textContent ?? null`),
|
|
2705
|
+
isVisible: () => this.isVisible(selector),
|
|
2706
|
+
hover: () => this.hover(selector),
|
|
2707
|
+
boundingBox: async () => null,
|
|
2708
|
+
scrollIntoViewIfNeeded: () => this.scrollIntoView(selector),
|
|
2709
|
+
evaluate: (fn) => this.view.evaluate(`(${fn.toString()})(document.querySelector(${JSON.stringify(selector)}))`),
|
|
2710
|
+
waitFor: (opts2) => {
|
|
2711
|
+
return new Promise((resolve, reject) => {
|
|
2712
|
+
const timeout = opts2?.timeout ?? 1e4;
|
|
2713
|
+
const start = Date.now();
|
|
2714
|
+
const check = async () => {
|
|
2715
|
+
const visible = await this.isVisible(selector);
|
|
2716
|
+
if (visible)
|
|
2717
|
+
return resolve();
|
|
2718
|
+
if (Date.now() - start > timeout)
|
|
2719
|
+
return reject(new Error(`Timeout waiting for ${selector}`));
|
|
2720
|
+
setTimeout(check, 100);
|
|
2721
|
+
};
|
|
2722
|
+
check();
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
2725
|
+
}),
|
|
2726
|
+
count: async () => {
|
|
2727
|
+
const count = await this.view.evaluate(`document.querySelectorAll(${JSON.stringify(selector)}).length`);
|
|
2728
|
+
return count;
|
|
2729
|
+
},
|
|
2730
|
+
nth: (n) => ({
|
|
2731
|
+
click: (clickOpts) => this.click(selector, clickOpts),
|
|
2732
|
+
textContent: () => this.view.evaluate(`document.querySelectorAll(${JSON.stringify(selector)})[${n}]?.textContent ?? null`),
|
|
2733
|
+
isVisible: () => this.isVisible(selector)
|
|
2734
|
+
})
|
|
2735
|
+
};
|
|
2736
|
+
}
|
|
2737
|
+
getByText(text, opts) {
|
|
2738
|
+
const selector = opts?.exact ? `*:is(button, a, span, div, p, h1, h2, h3, h4, label)` : "*";
|
|
2739
|
+
return {
|
|
2740
|
+
first: () => ({
|
|
2741
|
+
click: async (clickOpts) => {
|
|
2742
|
+
await this.view.evaluate(`
|
|
2743
|
+
(() => {
|
|
2744
|
+
const text = ${JSON.stringify(text)};
|
|
2745
|
+
const all = document.querySelectorAll('*');
|
|
2746
|
+
for (const el of all) {
|
|
2747
|
+
if (el.children.length === 0 && el.textContent?.trim() === text) {
|
|
2748
|
+
el.click(); return;
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
for (const el of all) {
|
|
2752
|
+
if (el.textContent?.includes(text)) { el.click(); return; }
|
|
2753
|
+
}
|
|
2754
|
+
})()
|
|
2755
|
+
`);
|
|
2756
|
+
},
|
|
2757
|
+
waitFor: (waitOpts) => {
|
|
2758
|
+
const timeout = waitOpts?.timeout ?? 1e4;
|
|
2759
|
+
return new Promise((resolve, reject) => {
|
|
2760
|
+
const start = Date.now();
|
|
2761
|
+
const check = async () => {
|
|
2762
|
+
const found = await this.view.evaluate(`document.body?.textContent?.includes(${JSON.stringify(text)})`);
|
|
2763
|
+
if (found)
|
|
2764
|
+
return resolve();
|
|
2765
|
+
if (Date.now() - start > timeout)
|
|
2766
|
+
return reject(new Error(`Timeout: text "${text}" not found`));
|
|
2767
|
+
setTimeout(check, 100);
|
|
2768
|
+
};
|
|
2769
|
+
check();
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
2772
|
+
})
|
|
2773
|
+
};
|
|
2774
|
+
}
|
|
2775
|
+
locator(selector) {
|
|
2776
|
+
return {
|
|
2777
|
+
click: (opts) => this.click(selector, opts),
|
|
2778
|
+
fill: (value) => this.fill(selector, value),
|
|
2779
|
+
scrollIntoViewIfNeeded: () => this.scrollIntoView(selector),
|
|
2780
|
+
first: () => this.getByRole("*").first(),
|
|
2781
|
+
evaluate: (fn) => this.view.evaluate(`(${fn.toString()})(document.querySelector(${JSON.stringify(selector)}))`),
|
|
2782
|
+
waitFor: (opts) => {
|
|
2783
|
+
const timeout = opts?.timeout ?? 1e4;
|
|
2784
|
+
return new Promise((resolve, reject) => {
|
|
2785
|
+
const start = Date.now();
|
|
2786
|
+
const check = async () => {
|
|
2787
|
+
const exists = await this.view.evaluate(`!!document.querySelector(${JSON.stringify(selector)})`);
|
|
2788
|
+
if (exists)
|
|
2789
|
+
return resolve();
|
|
2790
|
+
if (Date.now() - start > timeout)
|
|
2791
|
+
return reject(new Error(`Timeout: ${selector}`));
|
|
2792
|
+
setTimeout(check, 100);
|
|
2793
|
+
};
|
|
2794
|
+
check();
|
|
2795
|
+
});
|
|
2796
|
+
}
|
|
2797
|
+
};
|
|
2798
|
+
}
|
|
2799
|
+
url() {
|
|
2800
|
+
return this.view.url;
|
|
2801
|
+
}
|
|
2802
|
+
async title() {
|
|
2803
|
+
return this.view.title || await this.evaluate("document.title");
|
|
2804
|
+
}
|
|
2805
|
+
viewportSize() {
|
|
2806
|
+
return { width: 1280, height: 720 };
|
|
2807
|
+
}
|
|
2808
|
+
async waitForLoadState(state, opts) {
|
|
2809
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
2810
|
+
}
|
|
2811
|
+
async waitForURL(pattern, opts) {
|
|
2812
|
+
const timeout = opts?.timeout ?? 30000;
|
|
2813
|
+
const start = Date.now();
|
|
2814
|
+
while (Date.now() - start < timeout) {
|
|
2815
|
+
const url = this.view.url;
|
|
2816
|
+
const matches = pattern instanceof RegExp ? pattern.test(url) : url.includes(pattern);
|
|
2817
|
+
if (matches)
|
|
2818
|
+
return;
|
|
2819
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
2820
|
+
}
|
|
2821
|
+
throw new Error(`Timeout waiting for URL to match ${pattern}`);
|
|
2822
|
+
}
|
|
2823
|
+
async waitForSelector(selector, opts) {
|
|
2824
|
+
const timeout = opts?.timeout ?? 1e4;
|
|
2825
|
+
const start = Date.now();
|
|
2826
|
+
while (Date.now() - start < timeout) {
|
|
2827
|
+
const exists = await this.view.evaluate(`!!document.querySelector(${JSON.stringify(selector)})`);
|
|
2828
|
+
if (exists)
|
|
2829
|
+
return;
|
|
2830
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
2831
|
+
}
|
|
2832
|
+
throw new Error(`Timeout waiting for ${selector}`);
|
|
2833
|
+
}
|
|
2834
|
+
async setContent(html) {
|
|
2835
|
+
await this.view.navigate(`data:text/html,${encodeURIComponent(html)}`);
|
|
2836
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
2837
|
+
}
|
|
2838
|
+
async content() {
|
|
2839
|
+
return this.view.evaluate("document.documentElement.outerHTML");
|
|
2840
|
+
}
|
|
2841
|
+
async addInitScript(script) {
|
|
2842
|
+
const expr = typeof script === "function" ? `(${script.toString()})()` : script;
|
|
2843
|
+
await this.view.evaluate(expr);
|
|
2844
|
+
}
|
|
2845
|
+
keyboard = {
|
|
2846
|
+
press: (key) => this.view.press(key)
|
|
2847
|
+
};
|
|
2848
|
+
context() {
|
|
2849
|
+
return {
|
|
2850
|
+
close: async () => {
|
|
2851
|
+
await this.close();
|
|
2852
|
+
},
|
|
2853
|
+
newPage: async () => {
|
|
2854
|
+
throw new Error("Multi-tab not supported in Bun.WebView. Use engine: 'playwright'");
|
|
2855
|
+
},
|
|
2856
|
+
cookies: async () => [],
|
|
2857
|
+
addCookies: async (_) => {},
|
|
2858
|
+
clearCookies: async () => {},
|
|
2859
|
+
newCDPSession: async () => {
|
|
2860
|
+
throw new Error("CDP session via context not available in Bun.WebView. Use view.cdp() when shipped.");
|
|
2861
|
+
},
|
|
2862
|
+
route: async (_pattern, _handler) => {
|
|
2863
|
+
throw new Error("Network interception not supported in Bun.WebView. Use engine: 'cdp' or 'playwright'.");
|
|
2864
|
+
},
|
|
2865
|
+
unrouteAll: async () => {},
|
|
2866
|
+
pages: () => [],
|
|
2867
|
+
addInitScript: async (script) => {
|
|
2868
|
+
await this.addInitScript(script);
|
|
2869
|
+
}
|
|
2870
|
+
};
|
|
2871
|
+
}
|
|
2872
|
+
on(event, handler) {
|
|
2873
|
+
if (!this._eventListeners.has(event))
|
|
2874
|
+
this._eventListeners.set(event, []);
|
|
2875
|
+
this._eventListeners.get(event).push(handler);
|
|
2876
|
+
return this;
|
|
2877
|
+
}
|
|
2878
|
+
off(event, handler) {
|
|
2879
|
+
const listeners = this._eventListeners.get(event) ?? [];
|
|
2880
|
+
this._eventListeners.set(event, listeners.filter((l) => l !== handler));
|
|
2881
|
+
return this;
|
|
2882
|
+
}
|
|
2883
|
+
_emit(event, ...args) {
|
|
2884
|
+
for (const handler of this._eventListeners.get(event) ?? []) {
|
|
2885
|
+
try {
|
|
2886
|
+
handler(...args);
|
|
2887
|
+
} catch {}
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
async pdf(_opts) {
|
|
2891
|
+
throw new Error("PDF generation not supported in Bun.WebView. Use engine: 'playwright'.");
|
|
2892
|
+
}
|
|
2893
|
+
coverage = {
|
|
2894
|
+
startJSCoverage: async () => {},
|
|
2895
|
+
stopJSCoverage: async () => [],
|
|
2896
|
+
startCSSCoverage: async () => {},
|
|
2897
|
+
stopCSSCoverage: async () => []
|
|
2898
|
+
};
|
|
2899
|
+
setSessionId(id) {
|
|
2900
|
+
this._sessionId = id;
|
|
2901
|
+
}
|
|
2902
|
+
getSessionId() {
|
|
2903
|
+
return this._sessionId;
|
|
2904
|
+
}
|
|
2905
|
+
getNativeView() {
|
|
2906
|
+
return this.view;
|
|
2907
|
+
}
|
|
2908
|
+
async close() {
|
|
2909
|
+
try {
|
|
2910
|
+
await this.view.close();
|
|
2911
|
+
} catch {}
|
|
2912
|
+
}
|
|
2913
|
+
[Symbol.asyncDispose]() {
|
|
2914
|
+
return this.close();
|
|
2915
|
+
}
|
|
2916
|
+
};
|
|
2917
|
+
});
|
|
2918
|
+
|
|
2518
2919
|
// src/engines/selector.ts
|
|
2519
2920
|
function selectEngine(useCase, explicit) {
|
|
2520
2921
|
if (explicit && explicit !== "auto")
|
|
2521
2922
|
return explicit;
|
|
2522
2923
|
const preferred = ENGINE_MAP[useCase];
|
|
2924
|
+
if (preferred === "bun") {
|
|
2925
|
+
if (isBunWebViewAvailable())
|
|
2926
|
+
return "bun";
|
|
2927
|
+
if (useCase === "scrape" /* SCRAPE */ || useCase === "extract_links" /* EXTRACT_LINKS */ || useCase === "status_check" /* STATUS_CHECK */) {
|
|
2928
|
+
return isLightpandaAvailable() ? "lightpanda" : "playwright";
|
|
2929
|
+
}
|
|
2930
|
+
return "playwright";
|
|
2931
|
+
}
|
|
2523
2932
|
if (preferred === "lightpanda" && !isLightpandaAvailable()) {
|
|
2524
2933
|
return "playwright";
|
|
2525
2934
|
}
|
|
@@ -2529,13 +2938,14 @@ var ENGINE_MAP;
|
|
|
2529
2938
|
var init_selector = __esm(() => {
|
|
2530
2939
|
init_types();
|
|
2531
2940
|
init_lightpanda();
|
|
2941
|
+
init_bun_webview();
|
|
2532
2942
|
ENGINE_MAP = {
|
|
2533
|
-
["scrape" /* SCRAPE */]: "
|
|
2534
|
-
["extract_links" /* EXTRACT_LINKS */]: "
|
|
2535
|
-
["status_check" /* STATUS_CHECK */]: "
|
|
2943
|
+
["scrape" /* SCRAPE */]: "bun",
|
|
2944
|
+
["extract_links" /* EXTRACT_LINKS */]: "bun",
|
|
2945
|
+
["status_check" /* STATUS_CHECK */]: "bun",
|
|
2946
|
+
["screenshot" /* SCREENSHOT */]: "bun",
|
|
2947
|
+
["spa_navigate" /* SPA_NAVIGATE */]: "bun",
|
|
2536
2948
|
["form_fill" /* FORM_FILL */]: "playwright",
|
|
2537
|
-
["spa_navigate" /* SPA_NAVIGATE */]: "playwright",
|
|
2538
|
-
["screenshot" /* SCREENSHOT */]: "playwright",
|
|
2539
2949
|
["auth_flow" /* AUTH_FLOW */]: "playwright",
|
|
2540
2950
|
["multi_tab" /* MULTI_TAB */]: "playwright",
|
|
2541
2951
|
["record_replay" /* RECORD_REPLAY */]: "playwright",
|
|
@@ -2878,12 +3288,30 @@ var init_dialogs = __esm(() => {
|
|
|
2878
3288
|
});
|
|
2879
3289
|
|
|
2880
3290
|
// src/lib/session.ts
|
|
3291
|
+
function createBunProxy(view) {
|
|
3292
|
+
return view;
|
|
3293
|
+
}
|
|
2881
3294
|
async function createSession2(opts = {}) {
|
|
2882
3295
|
const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
|
|
2883
3296
|
const resolvedEngine = engine === "auto" ? "playwright" : engine;
|
|
2884
|
-
let browser;
|
|
3297
|
+
let browser = null;
|
|
3298
|
+
let bunView = null;
|
|
2885
3299
|
let page;
|
|
2886
|
-
if (resolvedEngine === "
|
|
3300
|
+
if (resolvedEngine === "bun") {
|
|
3301
|
+
if (!isBunWebViewAvailable()) {
|
|
3302
|
+
console.warn("[browser] Bun.WebView requested but not available \u2014 falling back to playwright. Run: bun upgrade --canary");
|
|
3303
|
+
browser = await launchPlaywright({ headless: opts.headless ?? true, viewport: opts.viewport, userAgent: opts.userAgent });
|
|
3304
|
+
page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
|
|
3305
|
+
} else {
|
|
3306
|
+
bunView = new BunWebViewSession({
|
|
3307
|
+
width: opts.viewport?.width ?? 1280,
|
|
3308
|
+
height: opts.viewport?.height ?? 720,
|
|
3309
|
+
profile: opts.name ?? undefined
|
|
3310
|
+
});
|
|
3311
|
+
if (opts.stealth) {}
|
|
3312
|
+
page = createBunProxy(bunView);
|
|
3313
|
+
}
|
|
3314
|
+
} else if (resolvedEngine === "lightpanda") {
|
|
2887
3315
|
browser = await connectLightpanda();
|
|
2888
3316
|
const context = await browser.newContext({ viewport: opts.viewport ?? { width: 1280, height: 720 } });
|
|
2889
3317
|
page = await context.newPage();
|
|
@@ -2893,12 +3321,9 @@ async function createSession2(opts = {}) {
|
|
|
2893
3321
|
viewport: opts.viewport,
|
|
2894
3322
|
userAgent: opts.userAgent
|
|
2895
3323
|
});
|
|
2896
|
-
page = await getPage(browser, {
|
|
2897
|
-
viewport: opts.viewport,
|
|
2898
|
-
userAgent: opts.userAgent
|
|
2899
|
-
});
|
|
3324
|
+
page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
|
|
2900
3325
|
}
|
|
2901
|
-
|
|
3326
|
+
const sessionName = opts.name ?? (opts.startUrl ? (() => {
|
|
2902
3327
|
try {
|
|
2903
3328
|
return new URL(opts.startUrl).hostname;
|
|
2904
3329
|
} catch {
|
|
@@ -2906,35 +3331,57 @@ async function createSession2(opts = {}) {
|
|
|
2906
3331
|
}
|
|
2907
3332
|
})() : undefined);
|
|
2908
3333
|
const session = createSession({
|
|
2909
|
-
engine: resolvedEngine,
|
|
3334
|
+
engine: bunView ? "bun" : browser ? resolvedEngine : resolvedEngine,
|
|
2910
3335
|
projectId: opts.projectId,
|
|
2911
3336
|
agentId: opts.agentId,
|
|
2912
3337
|
startUrl: opts.startUrl,
|
|
2913
3338
|
name: sessionName
|
|
2914
3339
|
});
|
|
2915
|
-
if (opts.stealth) {
|
|
3340
|
+
if (opts.stealth && !bunView) {
|
|
2916
3341
|
try {
|
|
2917
3342
|
await applyStealthPatches(page);
|
|
2918
3343
|
} catch {}
|
|
2919
3344
|
}
|
|
2920
3345
|
const cleanups = [];
|
|
2921
|
-
if (
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
3346
|
+
if (!bunView) {
|
|
3347
|
+
if (opts.captureNetwork !== false) {
|
|
3348
|
+
try {
|
|
3349
|
+
cleanups.push(enableNetworkLogging(page, session.id));
|
|
3350
|
+
} catch {}
|
|
3351
|
+
}
|
|
3352
|
+
if (opts.captureConsole !== false) {
|
|
3353
|
+
try {
|
|
3354
|
+
cleanups.push(enableConsoleCapture(page, session.id));
|
|
3355
|
+
} catch {}
|
|
3356
|
+
}
|
|
2927
3357
|
try {
|
|
2928
|
-
cleanups.push(
|
|
3358
|
+
cleanups.push(setupDialogHandler(page, session.id));
|
|
2929
3359
|
} catch {}
|
|
3360
|
+
} else {
|
|
3361
|
+
if (opts.captureConsole !== false) {
|
|
3362
|
+
try {
|
|
3363
|
+
const { logConsoleMessage: logConsoleMessage2 } = await Promise.resolve().then(() => (init_console_log(), exports_console_log));
|
|
3364
|
+
await bunView.addInitScript(`
|
|
3365
|
+
(() => {
|
|
3366
|
+
const orig = { log: console.log, warn: console.warn, error: console.error, debug: console.debug, info: console.info };
|
|
3367
|
+
['log','warn','error','debug','info'].forEach(level => {
|
|
3368
|
+
console[level] = (...args) => {
|
|
3369
|
+
orig[level](...args);
|
|
3370
|
+
};
|
|
3371
|
+
});
|
|
3372
|
+
})()
|
|
3373
|
+
`);
|
|
3374
|
+
} catch {}
|
|
3375
|
+
}
|
|
2930
3376
|
}
|
|
2931
|
-
|
|
2932
|
-
cleanups.push(setupDialogHandler(page, session.id));
|
|
2933
|
-
} catch {}
|
|
2934
|
-
handles.set(session.id, { browser, page, engine: resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 } });
|
|
3377
|
+
handles.set(session.id, { browser, bunView, page, engine: bunView ? "bun" : resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 } });
|
|
2935
3378
|
if (opts.startUrl) {
|
|
2936
3379
|
try {
|
|
2937
|
-
|
|
3380
|
+
if (bunView) {
|
|
3381
|
+
await bunView.goto(opts.startUrl);
|
|
3382
|
+
} else {
|
|
3383
|
+
await page.goto(opts.startUrl, { waitUntil: "domcontentloaded" });
|
|
3384
|
+
}
|
|
2938
3385
|
} catch {}
|
|
2939
3386
|
}
|
|
2940
3387
|
return { session, page };
|
|
@@ -2944,13 +3391,23 @@ function getSessionPage(sessionId) {
|
|
|
2944
3391
|
if (!handle)
|
|
2945
3392
|
throw new SessionNotFoundError(sessionId);
|
|
2946
3393
|
try {
|
|
2947
|
-
handle.
|
|
3394
|
+
if (handle.bunView) {
|
|
3395
|
+
handle.bunView.url();
|
|
3396
|
+
} else {
|
|
3397
|
+
handle.page.url();
|
|
3398
|
+
}
|
|
2948
3399
|
} catch {
|
|
2949
3400
|
handles.delete(sessionId);
|
|
2950
3401
|
throw new SessionNotFoundError(sessionId);
|
|
2951
3402
|
}
|
|
2952
3403
|
return handle.page;
|
|
2953
3404
|
}
|
|
3405
|
+
function getSessionBunView(sessionId) {
|
|
3406
|
+
return handles.get(sessionId)?.bunView ?? null;
|
|
3407
|
+
}
|
|
3408
|
+
function isBunSession(sessionId) {
|
|
3409
|
+
return handles.get(sessionId)?.engine === "bun";
|
|
3410
|
+
}
|
|
2954
3411
|
function setSessionPage(sessionId, page) {
|
|
2955
3412
|
const handle = handles.get(sessionId);
|
|
2956
3413
|
if (!handle)
|
|
@@ -2965,12 +3422,19 @@ async function closeSession2(sessionId) {
|
|
|
2965
3422
|
cleanup();
|
|
2966
3423
|
} catch {}
|
|
2967
3424
|
}
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
3425
|
+
if (handle.bunView) {
|
|
3426
|
+
try {
|
|
3427
|
+
await handle.bunView.close();
|
|
3428
|
+
} catch {}
|
|
3429
|
+
} else {
|
|
3430
|
+
try {
|
|
3431
|
+
await handle.page.context().close();
|
|
3432
|
+
} catch {}
|
|
3433
|
+
try {
|
|
3434
|
+
if (handle.browser)
|
|
3435
|
+
await closeBrowser(handle.browser);
|
|
3436
|
+
} catch {}
|
|
3437
|
+
}
|
|
2974
3438
|
handles.delete(sessionId);
|
|
2975
3439
|
}
|
|
2976
3440
|
return closeSession(sessionId);
|
|
@@ -2998,6 +3462,7 @@ var init_session = __esm(() => {
|
|
|
2998
3462
|
init_sessions();
|
|
2999
3463
|
init_playwright();
|
|
3000
3464
|
init_lightpanda();
|
|
3465
|
+
init_bun_webview();
|
|
3001
3466
|
init_selector();
|
|
3002
3467
|
init_network();
|
|
3003
3468
|
init_console();
|
|
@@ -3010,6 +3475,7 @@ var init_session = __esm(() => {
|
|
|
3010
3475
|
var exports_snapshot = {};
|
|
3011
3476
|
__export(exports_snapshot, {
|
|
3012
3477
|
takeSnapshot: () => takeSnapshot,
|
|
3478
|
+
takeBunSnapshot: () => takeBunSnapshot,
|
|
3013
3479
|
setLastSnapshot: () => setLastSnapshot,
|
|
3014
3480
|
hasRefs: () => hasRefs,
|
|
3015
3481
|
getSessionRefs: () => getSessionRefs,
|
|
@@ -3030,6 +3496,10 @@ function clearLastSnapshot(sessionId) {
|
|
|
3030
3496
|
lastSnapshots.delete(sessionId);
|
|
3031
3497
|
}
|
|
3032
3498
|
async function takeSnapshot(page, sessionId) {
|
|
3499
|
+
const isBunView = typeof page.getNativeView === "function" || typeof page.bunView !== "undefined";
|
|
3500
|
+
if (isBunView) {
|
|
3501
|
+
return takeBunSnapshot(page, sessionId);
|
|
3502
|
+
}
|
|
3033
3503
|
let ariaTree;
|
|
3034
3504
|
try {
|
|
3035
3505
|
ariaTree = await page.locator("body").ariaSnapshot();
|
|
@@ -3184,6 +3654,71 @@ function diffSnapshots(before, after) {
|
|
|
3184
3654
|
const title_changed = before.tree !== after.tree && (added.length > 0 || removed.length > 0 || modified.length > 0);
|
|
3185
3655
|
return { added, removed, modified, url_changed, title_changed };
|
|
3186
3656
|
}
|
|
3657
|
+
async function takeBunSnapshot(page, sessionId) {
|
|
3658
|
+
const refs = {};
|
|
3659
|
+
const refMap = new Map;
|
|
3660
|
+
let refCounter = 0;
|
|
3661
|
+
const lines = [];
|
|
3662
|
+
try {
|
|
3663
|
+
const elements = await page.evaluate(`
|
|
3664
|
+
(() => {
|
|
3665
|
+
const SELECTOR = 'a[href], button, input:not([type=hidden]), select, textarea, [role=button], [role=link], [role=checkbox], [role=combobox], [role=menuitem], [role=tab], [role=option]';
|
|
3666
|
+
const els = Array.from(document.querySelectorAll(SELECTOR));
|
|
3667
|
+
return els.slice(0, 100).map(el => {
|
|
3668
|
+
const tag = el.tagName.toLowerCase();
|
|
3669
|
+
const inputType = el.getAttribute('type') ?? '';
|
|
3670
|
+
let role = el.getAttribute('role') || (['a'].includes(tag) ? 'link' : ['button'].includes(tag) ? 'button' : ['input'].includes(tag) ? (inputType === 'checkbox' ? 'checkbox' : inputType === 'radio' ? 'radio' : 'textbox') : ['select'].includes(tag) ? 'combobox' : ['textarea'].includes(tag) ? 'textbox' : tag);
|
|
3671
|
+
const name = (el.getAttribute('aria-label') || el.textContent?.trim() || el.getAttribute('placeholder') || el.getAttribute('title') || el.getAttribute('value') || el.id || '').slice(0, 80);
|
|
3672
|
+
const enabled = !el.disabled && !el.getAttribute('disabled');
|
|
3673
|
+
const style = window.getComputedStyle(el);
|
|
3674
|
+
const visible = style.display !== 'none' && style.visibility !== 'hidden' && el.offsetWidth > 0;
|
|
3675
|
+
const checked = el.type === 'checkbox' || el.type === 'radio' ? el.checked : undefined;
|
|
3676
|
+
const value = ['input', 'select', 'textarea'].includes(tag) && el.type !== 'checkbox' && el.type !== 'radio' ? el.value : undefined;
|
|
3677
|
+
const selector = el.id ? '#' + el.id : (el.getAttribute('aria-label') ? '[aria-label="' + el.getAttribute('aria-label') + '"]' : tag);
|
|
3678
|
+
return { role, name, enabled, visible, checked, value, selector };
|
|
3679
|
+
}).filter(e => e.visible && e.name);
|
|
3680
|
+
})()
|
|
3681
|
+
`);
|
|
3682
|
+
const pageTitle = await page.evaluate("document.title");
|
|
3683
|
+
const pageUrl = typeof page.url === "function" ? page.url() : "";
|
|
3684
|
+
lines.push(`# ${pageTitle || "Page"} (${pageUrl})`);
|
|
3685
|
+
for (const el of elements) {
|
|
3686
|
+
if (!el.name)
|
|
3687
|
+
continue;
|
|
3688
|
+
const ref = `@e${refCounter}`;
|
|
3689
|
+
refCounter++;
|
|
3690
|
+
refs[ref] = {
|
|
3691
|
+
role: el.role,
|
|
3692
|
+
name: el.name,
|
|
3693
|
+
visible: el.visible,
|
|
3694
|
+
enabled: el.enabled,
|
|
3695
|
+
value: el.value,
|
|
3696
|
+
checked: el.checked
|
|
3697
|
+
};
|
|
3698
|
+
refMap.set(ref, { role: el.role, name: el.name, locatorSelector: el.selector });
|
|
3699
|
+
const extras = [];
|
|
3700
|
+
if (el.checked !== undefined)
|
|
3701
|
+
extras.push(`checked=${el.checked}`);
|
|
3702
|
+
if (!el.enabled)
|
|
3703
|
+
extras.push("disabled");
|
|
3704
|
+
if (el.value && el.value !== el.name)
|
|
3705
|
+
extras.push(`value="${el.value.slice(0, 30)}"`);
|
|
3706
|
+
const extrasStr = extras.length ? ` (${extras.join(", ")})` : "";
|
|
3707
|
+
lines.push(`${el.role} "${el.name}" [${ref}]${extrasStr}`);
|
|
3708
|
+
}
|
|
3709
|
+
} catch (err) {
|
|
3710
|
+
lines.push(`# (snapshot error: ${err instanceof Error ? err.message : String(err)})`);
|
|
3711
|
+
}
|
|
3712
|
+
if (sessionId) {
|
|
3713
|
+
sessionRefMaps.set(sessionId, refMap);
|
|
3714
|
+
}
|
|
3715
|
+
return {
|
|
3716
|
+
tree: lines.join(`
|
|
3717
|
+
`),
|
|
3718
|
+
refs,
|
|
3719
|
+
interactive_count: refCounter
|
|
3720
|
+
};
|
|
3721
|
+
}
|
|
3187
3722
|
var lastSnapshots, sessionRefMaps, INTERACTIVE_ROLES;
|
|
3188
3723
|
var init_snapshot = __esm(() => {
|
|
3189
3724
|
lastSnapshots = new Map;
|
|
@@ -3528,9 +4063,19 @@ async function getLinks(page, baseUrl) {
|
|
|
3528
4063
|
}, baseUrl ?? page.url());
|
|
3529
4064
|
}
|
|
3530
4065
|
async function getTitle(page) {
|
|
4066
|
+
if (typeof page.getNativeView === "function") {
|
|
4067
|
+
const nativeView = page.getNativeView();
|
|
4068
|
+
const t = nativeView?.title;
|
|
4069
|
+
return typeof t === "string" && t ? t : "";
|
|
4070
|
+
}
|
|
3531
4071
|
return page.title();
|
|
3532
4072
|
}
|
|
3533
4073
|
async function getUrl(page) {
|
|
4074
|
+
if (typeof page.getNativeView === "function") {
|
|
4075
|
+
const nativeView = page.getNativeView();
|
|
4076
|
+
const u = nativeView?.url;
|
|
4077
|
+
return typeof u === "string" ? u : "";
|
|
4078
|
+
}
|
|
3534
4079
|
return page.url();
|
|
3535
4080
|
}
|
|
3536
4081
|
async function findElements(page, selector) {
|
|
@@ -10172,17 +10717,17 @@ var init_gallery = __esm(() => {
|
|
|
10172
10717
|
});
|
|
10173
10718
|
|
|
10174
10719
|
// src/lib/screenshot.ts
|
|
10175
|
-
import { join as
|
|
10176
|
-
import { mkdirSync as
|
|
10177
|
-
import { homedir as
|
|
10720
|
+
import { join as join3 } from "path";
|
|
10721
|
+
import { mkdirSync as mkdirSync3 } from "fs";
|
|
10722
|
+
import { homedir as homedir3 } from "os";
|
|
10178
10723
|
function getDataDir2() {
|
|
10179
|
-
return process.env["BROWSER_DATA_DIR"] ??
|
|
10724
|
+
return process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser");
|
|
10180
10725
|
}
|
|
10181
10726
|
function getScreenshotDir(projectId) {
|
|
10182
|
-
const base =
|
|
10727
|
+
const base = join3(getDataDir2(), "screenshots");
|
|
10183
10728
|
const date = new Date().toISOString().split("T")[0];
|
|
10184
|
-
const dir = projectId ?
|
|
10185
|
-
|
|
10729
|
+
const dir = projectId ? join3(base, projectId, date) : join3(base, date);
|
|
10730
|
+
mkdirSync3(dir, { recursive: true });
|
|
10186
10731
|
return dir;
|
|
10187
10732
|
}
|
|
10188
10733
|
async function compressBuffer(raw, format, quality, maxWidth) {
|
|
@@ -10197,7 +10742,7 @@ async function compressBuffer(raw, format, quality, maxWidth) {
|
|
|
10197
10742
|
}
|
|
10198
10743
|
}
|
|
10199
10744
|
async function generateThumbnail(raw, dir, stem) {
|
|
10200
|
-
const thumbPath =
|
|
10745
|
+
const thumbPath = join3(dir, `${stem}.thumb.webp`);
|
|
10201
10746
|
const thumbBuffer = await import_sharp.default(raw).resize({ width: 200, withoutEnlargement: true }).webp({ quality: 70, effort: 3 }).toBuffer();
|
|
10202
10747
|
await Bun.write(thumbPath, thumbBuffer);
|
|
10203
10748
|
return { path: thumbPath, base64: thumbBuffer.toString("base64") };
|
|
@@ -10216,11 +10761,20 @@ async function takeScreenshot(page, opts) {
|
|
|
10216
10761
|
type: "png"
|
|
10217
10762
|
};
|
|
10218
10763
|
let rawBuffer;
|
|
10764
|
+
const isBunView = typeof page.getNativeView === "function";
|
|
10219
10765
|
if (opts?.selector) {
|
|
10220
|
-
|
|
10221
|
-
|
|
10222
|
-
|
|
10223
|
-
|
|
10766
|
+
if (isBunView) {
|
|
10767
|
+
const uint8 = await page.screenshot();
|
|
10768
|
+
rawBuffer = Buffer.from(uint8 instanceof Uint8Array ? uint8 : await uint8);
|
|
10769
|
+
} else {
|
|
10770
|
+
const el = await page.$(opts.selector);
|
|
10771
|
+
if (!el)
|
|
10772
|
+
throw new BrowserError(`Element not found: ${opts.selector}`, "ELEMENT_NOT_FOUND");
|
|
10773
|
+
rawBuffer = await el.screenshot(rawOpts);
|
|
10774
|
+
}
|
|
10775
|
+
} else if (isBunView) {
|
|
10776
|
+
const uint8 = await page.screenshot();
|
|
10777
|
+
rawBuffer = Buffer.from(uint8 instanceof Uint8Array ? uint8 : await uint8);
|
|
10224
10778
|
} else {
|
|
10225
10779
|
rawBuffer = await page.screenshot(rawOpts);
|
|
10226
10780
|
}
|
|
@@ -10245,7 +10799,7 @@ async function takeScreenshot(page, opts) {
|
|
|
10245
10799
|
const compressedSizeBytes = finalBuffer.length;
|
|
10246
10800
|
const compressionRatio = originalSizeBytes > 0 ? compressedSizeBytes / originalSizeBytes : 1;
|
|
10247
10801
|
const ext = format;
|
|
10248
|
-
const screenshotPath = opts?.path ??
|
|
10802
|
+
const screenshotPath = opts?.path ?? join3(dir, `${stem}.${ext}`);
|
|
10249
10803
|
await Bun.write(screenshotPath, finalBuffer);
|
|
10250
10804
|
let thumbnailPath;
|
|
10251
10805
|
let thumbnailBase64;
|
|
@@ -10305,12 +10859,12 @@ async function takeScreenshot(page, opts) {
|
|
|
10305
10859
|
}
|
|
10306
10860
|
async function generatePDF(page, opts) {
|
|
10307
10861
|
try {
|
|
10308
|
-
const base =
|
|
10862
|
+
const base = join3(getDataDir2(), "pdfs");
|
|
10309
10863
|
const date = new Date().toISOString().split("T")[0];
|
|
10310
|
-
const dir = opts?.projectId ?
|
|
10311
|
-
|
|
10864
|
+
const dir = opts?.projectId ? join3(base, opts.projectId, date) : join3(base, date);
|
|
10865
|
+
mkdirSync3(dir, { recursive: true });
|
|
10312
10866
|
const timestamp = Date.now();
|
|
10313
|
-
const pdfPath = opts?.path ??
|
|
10867
|
+
const pdfPath = opts?.path ?? join3(dir, `${timestamp}.pdf`);
|
|
10314
10868
|
const buffer = await page.pdf({
|
|
10315
10869
|
path: pdfPath,
|
|
10316
10870
|
format: opts?.format ?? "A4",
|
|
@@ -14909,16 +15463,16 @@ __export(exports_downloads, {
|
|
|
14909
15463
|
cleanStaleDownloads: () => cleanStaleDownloads
|
|
14910
15464
|
});
|
|
14911
15465
|
import { randomUUID as randomUUID9 } from "crypto";
|
|
14912
|
-
import { join as
|
|
14913
|
-
import { mkdirSync as
|
|
14914
|
-
import { homedir as
|
|
15466
|
+
import { join as join4, basename, extname } from "path";
|
|
15467
|
+
import { mkdirSync as mkdirSync4, existsSync, readdirSync, statSync, unlinkSync, copyFileSync, writeFileSync, readFileSync } from "fs";
|
|
15468
|
+
import { homedir as homedir4 } from "os";
|
|
14915
15469
|
function getDataDir3() {
|
|
14916
|
-
return process.env["BROWSER_DATA_DIR"] ??
|
|
15470
|
+
return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
|
|
14917
15471
|
}
|
|
14918
15472
|
function getDownloadsDir(sessionId) {
|
|
14919
|
-
const base =
|
|
14920
|
-
const dir = sessionId ?
|
|
14921
|
-
|
|
15473
|
+
const base = join4(getDataDir3(), "downloads");
|
|
15474
|
+
const dir = sessionId ? join4(base, sessionId) : base;
|
|
15475
|
+
mkdirSync4(dir, { recursive: true });
|
|
14922
15476
|
return dir;
|
|
14923
15477
|
}
|
|
14924
15478
|
function ensureDownloadsDir() {
|
|
@@ -14933,7 +15487,7 @@ function saveToDownloads(buffer, filename, opts) {
|
|
|
14933
15487
|
const ext = extname(filename) || "";
|
|
14934
15488
|
const stem = basename(filename, ext);
|
|
14935
15489
|
const uniqueName = `${stem}-${id.slice(0, 8)}${ext}`;
|
|
14936
|
-
const filePath =
|
|
15490
|
+
const filePath = join4(dir, uniqueName);
|
|
14937
15491
|
writeFileSync(filePath, buffer);
|
|
14938
15492
|
const meta = {
|
|
14939
15493
|
id,
|
|
@@ -14968,7 +15522,7 @@ function listDownloads(sessionId) {
|
|
|
14968
15522
|
for (const entry of entries) {
|
|
14969
15523
|
if (entry.endsWith(".meta.json"))
|
|
14970
15524
|
continue;
|
|
14971
|
-
const full =
|
|
15525
|
+
const full = join4(d, entry);
|
|
14972
15526
|
const stat = statSync(full);
|
|
14973
15527
|
if (stat.isDirectory()) {
|
|
14974
15528
|
scanDir(full);
|
|
@@ -15057,9 +15611,9 @@ var exports_gallery_diff = {};
|
|
|
15057
15611
|
__export(exports_gallery_diff, {
|
|
15058
15612
|
diffImages: () => diffImages
|
|
15059
15613
|
});
|
|
15060
|
-
import { join as
|
|
15061
|
-
import { mkdirSync as
|
|
15062
|
-
import { homedir as
|
|
15614
|
+
import { join as join5 } from "path";
|
|
15615
|
+
import { mkdirSync as mkdirSync5 } from "fs";
|
|
15616
|
+
import { homedir as homedir5 } from "os";
|
|
15063
15617
|
async function diffImages(path1, path2) {
|
|
15064
15618
|
const img1 = import_sharp2.default(path1);
|
|
15065
15619
|
const img2 = import_sharp2.default(path2);
|
|
@@ -15090,10 +15644,10 @@ async function diffImages(path1, path2) {
|
|
|
15090
15644
|
diffBuffer[i + 2] = Math.round(raw1[i + 2] * 0.4);
|
|
15091
15645
|
}
|
|
15092
15646
|
}
|
|
15093
|
-
const dataDir = process.env["BROWSER_DATA_DIR"] ??
|
|
15094
|
-
const diffDir =
|
|
15095
|
-
|
|
15096
|
-
const diffPath =
|
|
15647
|
+
const dataDir = process.env["BROWSER_DATA_DIR"] ?? join5(homedir5(), ".browser");
|
|
15648
|
+
const diffDir = join5(dataDir, "diffs");
|
|
15649
|
+
mkdirSync5(diffDir, { recursive: true });
|
|
15650
|
+
const diffPath = join5(diffDir, `diff-${Date.now()}.webp`);
|
|
15097
15651
|
const diffImageBuffer = await import_sharp2.default(diffBuffer, { raw: { width: w, height: h, channels } }).webp({ quality: 85 }).toBuffer();
|
|
15098
15652
|
await Bun.write(diffPath, diffImageBuffer);
|
|
15099
15653
|
return {
|
|
@@ -15110,9 +15664,9 @@ var init_gallery_diff = __esm(() => {
|
|
|
15110
15664
|
});
|
|
15111
15665
|
|
|
15112
15666
|
// src/lib/files-integration.ts
|
|
15113
|
-
import { join as
|
|
15114
|
-
import { mkdirSync as
|
|
15115
|
-
import { homedir as
|
|
15667
|
+
import { join as join6 } from "path";
|
|
15668
|
+
import { mkdirSync as mkdirSync6, copyFileSync as copyFileSync2 } from "fs";
|
|
15669
|
+
import { homedir as homedir6 } from "os";
|
|
15116
15670
|
async function persistFile(localPath, opts) {
|
|
15117
15671
|
try {
|
|
15118
15672
|
const mod = await import("@hasna/files");
|
|
@@ -15121,12 +15675,12 @@ async function persistFile(localPath, opts) {
|
|
|
15121
15675
|
return { id: ref.id, path: ref.path ?? localPath, permanent: true, provider: "open-files" };
|
|
15122
15676
|
}
|
|
15123
15677
|
} catch {}
|
|
15124
|
-
const dataDir = process.env["BROWSER_DATA_DIR"] ??
|
|
15678
|
+
const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
|
|
15125
15679
|
const date = new Date().toISOString().split("T")[0];
|
|
15126
|
-
const dir =
|
|
15127
|
-
|
|
15680
|
+
const dir = join6(dataDir, "persistent", date);
|
|
15681
|
+
mkdirSync6(dir, { recursive: true });
|
|
15128
15682
|
const filename = localPath.split("/").pop() ?? "file";
|
|
15129
|
-
const targetPath =
|
|
15683
|
+
const targetPath = join6(dir, filename);
|
|
15130
15684
|
copyFileSync2(localPath, targetPath);
|
|
15131
15685
|
return {
|
|
15132
15686
|
id: `local-${Date.now()}`,
|
|
@@ -15219,23 +15773,23 @@ async function closeTab(page, index) {
|
|
|
15219
15773
|
}
|
|
15220
15774
|
|
|
15221
15775
|
// src/lib/profiles.ts
|
|
15222
|
-
import { mkdirSync as
|
|
15223
|
-
import { join as
|
|
15224
|
-
import { homedir as
|
|
15776
|
+
import { mkdirSync as mkdirSync7, existsSync as existsSync3, readdirSync as readdirSync2, rmSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
15777
|
+
import { join as join7 } from "path";
|
|
15778
|
+
import { homedir as homedir7 } from "os";
|
|
15225
15779
|
function getProfilesDir() {
|
|
15226
|
-
const dataDir = process.env["BROWSER_DATA_DIR"] ??
|
|
15227
|
-
const dir =
|
|
15228
|
-
|
|
15780
|
+
const dataDir = process.env["BROWSER_DATA_DIR"] ?? join7(homedir7(), ".browser");
|
|
15781
|
+
const dir = join7(dataDir, "profiles");
|
|
15782
|
+
mkdirSync7(dir, { recursive: true });
|
|
15229
15783
|
return dir;
|
|
15230
15784
|
}
|
|
15231
|
-
function
|
|
15232
|
-
return
|
|
15785
|
+
function getProfileDir2(name) {
|
|
15786
|
+
return join7(getProfilesDir(), name);
|
|
15233
15787
|
}
|
|
15234
15788
|
async function saveProfile(page, name) {
|
|
15235
|
-
const dir =
|
|
15236
|
-
|
|
15789
|
+
const dir = getProfileDir2(name);
|
|
15790
|
+
mkdirSync7(dir, { recursive: true });
|
|
15237
15791
|
const cookies = await page.context().cookies();
|
|
15238
|
-
writeFileSync2(
|
|
15792
|
+
writeFileSync2(join7(dir, "cookies.json"), JSON.stringify(cookies, null, 2));
|
|
15239
15793
|
let localStorage2 = {};
|
|
15240
15794
|
try {
|
|
15241
15795
|
localStorage2 = await page.evaluate(() => {
|
|
@@ -15247,11 +15801,11 @@ async function saveProfile(page, name) {
|
|
|
15247
15801
|
return result;
|
|
15248
15802
|
});
|
|
15249
15803
|
} catch {}
|
|
15250
|
-
writeFileSync2(
|
|
15804
|
+
writeFileSync2(join7(dir, "storage.json"), JSON.stringify(localStorage2, null, 2));
|
|
15251
15805
|
const savedAt = new Date().toISOString();
|
|
15252
15806
|
const url = page.url();
|
|
15253
15807
|
const meta = { saved_at: savedAt, url };
|
|
15254
|
-
writeFileSync2(
|
|
15808
|
+
writeFileSync2(join7(dir, "meta.json"), JSON.stringify(meta, null, 2));
|
|
15255
15809
|
return {
|
|
15256
15810
|
name,
|
|
15257
15811
|
saved_at: savedAt,
|
|
@@ -15261,13 +15815,13 @@ async function saveProfile(page, name) {
|
|
|
15261
15815
|
};
|
|
15262
15816
|
}
|
|
15263
15817
|
function loadProfile(name) {
|
|
15264
|
-
const dir =
|
|
15818
|
+
const dir = getProfileDir2(name);
|
|
15265
15819
|
if (!existsSync3(dir)) {
|
|
15266
15820
|
throw new Error(`Profile not found: ${name}`);
|
|
15267
15821
|
}
|
|
15268
|
-
const cookiesPath =
|
|
15269
|
-
const storagePath =
|
|
15270
|
-
const metaPath2 =
|
|
15822
|
+
const cookiesPath = join7(dir, "cookies.json");
|
|
15823
|
+
const storagePath = join7(dir, "storage.json");
|
|
15824
|
+
const metaPath2 = join7(dir, "meta.json");
|
|
15271
15825
|
const cookies = existsSync3(cookiesPath) ? JSON.parse(readFileSync2(cookiesPath, "utf8")) : [];
|
|
15272
15826
|
const localStorage2 = existsSync3(storagePath) ? JSON.parse(readFileSync2(storagePath, "utf8")) : {};
|
|
15273
15827
|
let savedAt = new Date().toISOString();
|
|
@@ -15308,24 +15862,24 @@ function listProfiles() {
|
|
|
15308
15862
|
if (!entry.isDirectory())
|
|
15309
15863
|
continue;
|
|
15310
15864
|
const name = entry.name;
|
|
15311
|
-
const profileDir =
|
|
15865
|
+
const profileDir = join7(dir, name);
|
|
15312
15866
|
let savedAt = "";
|
|
15313
15867
|
let url;
|
|
15314
15868
|
let cookieCount = 0;
|
|
15315
15869
|
let storageKeyCount = 0;
|
|
15316
15870
|
try {
|
|
15317
|
-
const metaPath2 =
|
|
15871
|
+
const metaPath2 = join7(profileDir, "meta.json");
|
|
15318
15872
|
if (existsSync3(metaPath2)) {
|
|
15319
15873
|
const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
|
|
15320
15874
|
savedAt = meta.saved_at ?? "";
|
|
15321
15875
|
url = meta.url;
|
|
15322
15876
|
}
|
|
15323
|
-
const cookiesPath =
|
|
15877
|
+
const cookiesPath = join7(profileDir, "cookies.json");
|
|
15324
15878
|
if (existsSync3(cookiesPath)) {
|
|
15325
15879
|
const cookies = JSON.parse(readFileSync2(cookiesPath, "utf8"));
|
|
15326
15880
|
cookieCount = Array.isArray(cookies) ? cookies.length : 0;
|
|
15327
15881
|
}
|
|
15328
|
-
const storagePath =
|
|
15882
|
+
const storagePath = join7(profileDir, "storage.json");
|
|
15329
15883
|
if (existsSync3(storagePath)) {
|
|
15330
15884
|
const storage = JSON.parse(readFileSync2(storagePath, "utf8"));
|
|
15331
15885
|
storageKeyCount = Object.keys(storage).length;
|
|
@@ -15342,7 +15896,7 @@ function listProfiles() {
|
|
|
15342
15896
|
return profiles.sort((a, b) => b.saved_at.localeCompare(a.saved_at));
|
|
15343
15897
|
}
|
|
15344
15898
|
function deleteProfile(name) {
|
|
15345
|
-
const dir =
|
|
15899
|
+
const dir = getProfileDir2(name);
|
|
15346
15900
|
if (!existsSync3(dir))
|
|
15347
15901
|
return false;
|
|
15348
15902
|
try {
|
|
@@ -15419,7 +15973,7 @@ var exports_mcp = {};
|
|
|
15419
15973
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
15420
15974
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15421
15975
|
import { readFileSync as readFileSync3 } from "fs";
|
|
15422
|
-
import { join as
|
|
15976
|
+
import { join as join8 } from "path";
|
|
15423
15977
|
function json(data) {
|
|
15424
15978
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
15425
15979
|
}
|
|
@@ -15455,7 +16009,7 @@ var init_mcp = __esm(async () => {
|
|
|
15455
16009
|
init_dialogs();
|
|
15456
16010
|
init_profiles();
|
|
15457
16011
|
init_types();
|
|
15458
|
-
_pkg = JSON.parse(readFileSync3(
|
|
16012
|
+
_pkg = JSON.parse(readFileSync3(join8(import.meta.dir, "../../package.json"), "utf8"));
|
|
15459
16013
|
networkLogCleanup = new Map;
|
|
15460
16014
|
consoleCaptureCleanup = new Map;
|
|
15461
16015
|
harCaptures = new Map;
|
|
@@ -15464,7 +16018,7 @@ var init_mcp = __esm(async () => {
|
|
|
15464
16018
|
version: "0.0.1"
|
|
15465
16019
|
});
|
|
15466
16020
|
server.tool("browser_session_create", "Create a new browser session with the specified engine", {
|
|
15467
|
-
engine: exports_external.enum(["playwright", "cdp", "lightpanda", "auto"]).optional().default("auto"),
|
|
16021
|
+
engine: exports_external.enum(["playwright", "cdp", "lightpanda", "bun", "auto"]).optional().default("auto"),
|
|
15468
16022
|
use_case: exports_external.string().optional(),
|
|
15469
16023
|
project_id: exports_external.string().optional(),
|
|
15470
16024
|
agent_id: exports_external.string().optional(),
|
|
@@ -15519,7 +16073,13 @@ var init_mcp = __esm(async () => {
|
|
|
15519
16073
|
}, async ({ session_id, url, timeout, auto_snapshot, auto_thumbnail }) => {
|
|
15520
16074
|
try {
|
|
15521
16075
|
const page = getSessionPage(session_id);
|
|
15522
|
-
|
|
16076
|
+
if (isBunSession(session_id)) {
|
|
16077
|
+
const bunView = getSessionBunView(session_id);
|
|
16078
|
+
await bunView.goto(url, { timeout });
|
|
16079
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
16080
|
+
} else {
|
|
16081
|
+
await navigate(page, url, timeout);
|
|
16082
|
+
}
|
|
15523
16083
|
const title = await getTitle(page);
|
|
15524
16084
|
const current_url = await getUrl(page);
|
|
15525
16085
|
const redirected = current_url !== url && current_url !== url + "/" && url !== current_url.replace(/\/$/, "");
|
|
@@ -15560,6 +16120,9 @@ var init_mcp = __esm(async () => {
|
|
|
15560
16120
|
result.thumbnail_base64 = ss.base64.length > 50000 ? "" : ss.base64;
|
|
15561
16121
|
} catch {}
|
|
15562
16122
|
}
|
|
16123
|
+
if (isBunSession(session_id) && auto_snapshot) {
|
|
16124
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
16125
|
+
}
|
|
15563
16126
|
if (auto_snapshot) {
|
|
15564
16127
|
try {
|
|
15565
16128
|
const snap = await takeSnapshot(page, session_id);
|
|
@@ -16067,7 +16630,7 @@ var init_mcp = __esm(async () => {
|
|
|
16067
16630
|
max_pages: exports_external.number().optional().default(50),
|
|
16068
16631
|
same_domain: exports_external.boolean().optional().default(true),
|
|
16069
16632
|
project_id: exports_external.string().optional(),
|
|
16070
|
-
engine: exports_external.enum(["playwright", "cdp", "lightpanda", "auto"]).optional().default("auto")
|
|
16633
|
+
engine: exports_external.enum(["playwright", "cdp", "lightpanda", "bun", "auto"]).optional().default("auto")
|
|
16071
16634
|
}, async ({ url, max_depth, max_pages, same_domain, project_id, engine }) => {
|
|
16072
16635
|
try {
|
|
16073
16636
|
const result = await crawl(url, {
|
|
@@ -16835,7 +17398,7 @@ var init_snapshots = __esm(() => {
|
|
|
16835
17398
|
|
|
16836
17399
|
// src/server/index.ts
|
|
16837
17400
|
var exports_server = {};
|
|
16838
|
-
import { join as
|
|
17401
|
+
import { join as join9 } from "path";
|
|
16839
17402
|
import { existsSync as existsSync4 } from "fs";
|
|
16840
17403
|
function ok(data, status = 200) {
|
|
16841
17404
|
return new Response(JSON.stringify(data), {
|
|
@@ -17129,13 +17692,13 @@ var init_server = __esm(() => {
|
|
|
17129
17692
|
const id = path.split("/")[3];
|
|
17130
17693
|
return ok({ deleted: deleteDownload(id) });
|
|
17131
17694
|
}
|
|
17132
|
-
const dashboardDist =
|
|
17695
|
+
const dashboardDist = join9(import.meta.dir, "../../dashboard/dist");
|
|
17133
17696
|
if (existsSync4(dashboardDist)) {
|
|
17134
|
-
const filePath = path === "/" ?
|
|
17697
|
+
const filePath = path === "/" ? join9(dashboardDist, "index.html") : join9(dashboardDist, path);
|
|
17135
17698
|
if (existsSync4(filePath)) {
|
|
17136
17699
|
return new Response(Bun.file(filePath), { headers: CORS_HEADERS });
|
|
17137
17700
|
}
|
|
17138
|
-
return new Response(Bun.file(
|
|
17701
|
+
return new Response(Bun.file(join9(dashboardDist, "index.html")), { headers: CORS_HEADERS });
|
|
17139
17702
|
}
|
|
17140
17703
|
if (path === "/" || path === "") {
|
|
17141
17704
|
return new Response("@hasna/browser REST API running. Dashboard not built.", {
|
|
@@ -17178,9 +17741,9 @@ init_recorder();
|
|
|
17178
17741
|
init_recordings();
|
|
17179
17742
|
init_lightpanda();
|
|
17180
17743
|
import { readFileSync as readFileSync4 } from "fs";
|
|
17181
|
-
import { join as
|
|
17744
|
+
import { join as join10 } from "path";
|
|
17182
17745
|
import chalk from "chalk";
|
|
17183
|
-
var pkg = JSON.parse(readFileSync4(
|
|
17746
|
+
var pkg = JSON.parse(readFileSync4(join10(import.meta.dir, "../../package.json"), "utf8"));
|
|
17184
17747
|
var program2 = new Command;
|
|
17185
17748
|
program2.name("browser").description("@hasna/browser \u2014 general-purpose browser agent CLI").version(pkg.version);
|
|
17186
17749
|
program2.command("navigate <url>").description("Navigate to a URL and optionally take a screenshot").option("--engine <engine>", "Browser engine: playwright|cdp|lightpanda|auto", "auto").option("--screenshot", "Take a screenshot after navigation").option("--extract", "Extract page text after navigation").option("--headless", "Run in headless mode (default: true)", true).action(async (url, opts) => {
|