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