@hasna/browser 0.0.2 → 0.0.4
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 +1576 -289
- package/dist/db/sessions.d.ts.map +1 -1
- package/dist/index.js +418 -157
- package/dist/lib/actions-ref.test.d.ts +2 -0
- package/dist/lib/actions-ref.test.d.ts.map +1 -0
- package/dist/lib/actions.d.ts +12 -0
- package/dist/lib/actions.d.ts.map +1 -1
- package/dist/lib/annotate.d.ts +18 -0
- package/dist/lib/annotate.d.ts.map +1 -0
- package/dist/lib/annotate.test.d.ts +2 -0
- package/dist/lib/annotate.test.d.ts.map +1 -0
- package/dist/lib/dialogs.d.ts +15 -0
- package/dist/lib/dialogs.d.ts.map +1 -0
- package/dist/lib/profiles.d.ts +23 -0
- package/dist/lib/profiles.d.ts.map +1 -0
- 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-v3.test.d.ts +2 -0
- package/dist/lib/session-v3.test.d.ts.map +1 -0
- package/dist/lib/session.d.ts +5 -0
- package/dist/lib/session.d.ts.map +1 -1
- package/dist/lib/snapshot-diff.test.d.ts +2 -0
- package/dist/lib/snapshot-diff.test.d.ts.map +1 -0
- package/dist/lib/snapshot.d.ts +33 -0
- package/dist/lib/snapshot.d.ts.map +1 -0
- package/dist/lib/snapshot.test.d.ts +2 -0
- package/dist/lib/snapshot.test.d.ts.map +1 -0
- package/dist/lib/stealth.d.ts +5 -0
- package/dist/lib/stealth.d.ts.map +1 -0
- package/dist/lib/stealth.test.d.ts +2 -0
- package/dist/lib/stealth.test.d.ts.map +1 -0
- package/dist/lib/tabs.d.ts +18 -0
- package/dist/lib/tabs.d.ts.map +1 -0
- package/dist/mcp/index.js +1591 -312
- package/dist/mcp/v4.test.d.ts +2 -0
- package/dist/mcp/v4.test.d.ts.map +1 -0
- package/dist/server/index.js +340 -173
- package/dist/types/index.d.ts +35 -0
- 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) {
|
|
@@ -2525,6 +2547,336 @@ var init_selector = __esm(() => {
|
|
|
2525
2547
|
};
|
|
2526
2548
|
});
|
|
2527
2549
|
|
|
2550
|
+
// src/db/network-log.ts
|
|
2551
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2552
|
+
function logRequest(data) {
|
|
2553
|
+
const db = getDatabase();
|
|
2554
|
+
const id = randomUUID2();
|
|
2555
|
+
db.prepare(`INSERT INTO network_log (id, session_id, method, url, status_code, request_headers,
|
|
2556
|
+
response_headers, request_body, body_size, duration_ms, resource_type)
|
|
2557
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, data.session_id, data.method, data.url, data.status_code ?? null, data.request_headers ?? null, data.response_headers ?? null, data.request_body ?? null, data.body_size ?? null, data.duration_ms ?? null, data.resource_type ?? null);
|
|
2558
|
+
return getNetworkRequest(id);
|
|
2559
|
+
}
|
|
2560
|
+
function getNetworkRequest(id) {
|
|
2561
|
+
const db = getDatabase();
|
|
2562
|
+
return db.query("SELECT * FROM network_log WHERE id = ?").get(id) ?? null;
|
|
2563
|
+
}
|
|
2564
|
+
function getNetworkLog(sessionId) {
|
|
2565
|
+
const db = getDatabase();
|
|
2566
|
+
return db.query("SELECT * FROM network_log WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
|
|
2567
|
+
}
|
|
2568
|
+
function clearNetworkLog(sessionId) {
|
|
2569
|
+
const db = getDatabase();
|
|
2570
|
+
db.prepare("DELETE FROM network_log WHERE session_id = ?").run(sessionId);
|
|
2571
|
+
}
|
|
2572
|
+
var init_network_log = __esm(() => {
|
|
2573
|
+
init_schema();
|
|
2574
|
+
});
|
|
2575
|
+
|
|
2576
|
+
// src/lib/network.ts
|
|
2577
|
+
function enableNetworkLogging(page, sessionId) {
|
|
2578
|
+
const requestStart = new Map;
|
|
2579
|
+
const onRequest = (req) => {
|
|
2580
|
+
requestStart.set(req.url(), Date.now());
|
|
2581
|
+
};
|
|
2582
|
+
const onResponse = (res) => {
|
|
2583
|
+
const start = requestStart.get(res.url()) ?? Date.now();
|
|
2584
|
+
const duration = Date.now() - start;
|
|
2585
|
+
const req = res.request();
|
|
2586
|
+
try {
|
|
2587
|
+
logRequest({
|
|
2588
|
+
session_id: sessionId,
|
|
2589
|
+
method: req.method(),
|
|
2590
|
+
url: res.url(),
|
|
2591
|
+
status_code: res.status(),
|
|
2592
|
+
request_headers: JSON.stringify(req.headers()),
|
|
2593
|
+
response_headers: JSON.stringify(res.headers()),
|
|
2594
|
+
body_size: res.headers()["content-length"] != null ? parseInt(res.headers()["content-length"]) : undefined,
|
|
2595
|
+
duration_ms: duration,
|
|
2596
|
+
resource_type: req.resourceType()
|
|
2597
|
+
});
|
|
2598
|
+
} catch {}
|
|
2599
|
+
};
|
|
2600
|
+
page.on("request", onRequest);
|
|
2601
|
+
page.on("response", onResponse);
|
|
2602
|
+
return () => {
|
|
2603
|
+
page.off("request", onRequest);
|
|
2604
|
+
page.off("response", onResponse);
|
|
2605
|
+
};
|
|
2606
|
+
}
|
|
2607
|
+
async function addInterceptRule(page, rule) {
|
|
2608
|
+
await page.route(rule.pattern, async (route) => {
|
|
2609
|
+
if (rule.action === "block") {
|
|
2610
|
+
await route.abort();
|
|
2611
|
+
} else if (rule.action === "modify" && rule.response) {
|
|
2612
|
+
await route.fulfill({
|
|
2613
|
+
status: rule.response.status,
|
|
2614
|
+
body: rule.response.body,
|
|
2615
|
+
headers: rule.response.headers
|
|
2616
|
+
});
|
|
2617
|
+
} else {
|
|
2618
|
+
await route.continue();
|
|
2619
|
+
}
|
|
2620
|
+
});
|
|
2621
|
+
}
|
|
2622
|
+
function startHAR(page) {
|
|
2623
|
+
const entries = [];
|
|
2624
|
+
const requestStart = new Map;
|
|
2625
|
+
const onRequest = (req) => {
|
|
2626
|
+
requestStart.set(req.url() + req.method(), {
|
|
2627
|
+
time: Date.now(),
|
|
2628
|
+
method: req.method(),
|
|
2629
|
+
headers: req.headers(),
|
|
2630
|
+
postData: req.postData() ?? undefined
|
|
2631
|
+
});
|
|
2632
|
+
};
|
|
2633
|
+
const onResponse = async (res) => {
|
|
2634
|
+
const key = res.url() + res.request().method();
|
|
2635
|
+
const start = requestStart.get(key);
|
|
2636
|
+
if (!start)
|
|
2637
|
+
return;
|
|
2638
|
+
const duration = Date.now() - start.time;
|
|
2639
|
+
const entry = {
|
|
2640
|
+
startedDateTime: new Date(start.time).toISOString(),
|
|
2641
|
+
time: duration,
|
|
2642
|
+
request: {
|
|
2643
|
+
method: start.method,
|
|
2644
|
+
url: res.url(),
|
|
2645
|
+
headers: Object.entries(start.headers).map(([name, value]) => ({ name, value })),
|
|
2646
|
+
postData: start.postData ? { text: start.postData } : undefined
|
|
2647
|
+
},
|
|
2648
|
+
response: {
|
|
2649
|
+
status: res.status(),
|
|
2650
|
+
statusText: res.statusText(),
|
|
2651
|
+
headers: Object.entries(res.headers()).map(([name, value]) => ({ name, value })),
|
|
2652
|
+
content: {
|
|
2653
|
+
size: parseInt(res.headers()["content-length"] ?? "0") || 0,
|
|
2654
|
+
mimeType: res.headers()["content-type"] ?? "application/octet-stream"
|
|
2655
|
+
}
|
|
2656
|
+
},
|
|
2657
|
+
timings: { send: 0, wait: duration, receive: 0 }
|
|
2658
|
+
};
|
|
2659
|
+
entries.push(entry);
|
|
2660
|
+
requestStart.delete(key);
|
|
2661
|
+
};
|
|
2662
|
+
page.on("request", onRequest);
|
|
2663
|
+
page.on("response", onResponse);
|
|
2664
|
+
return {
|
|
2665
|
+
entries,
|
|
2666
|
+
stop: () => {
|
|
2667
|
+
page.off("request", onRequest);
|
|
2668
|
+
page.off("response", onResponse);
|
|
2669
|
+
return {
|
|
2670
|
+
log: {
|
|
2671
|
+
version: "1.2",
|
|
2672
|
+
creator: { name: "@hasna/browser", version: "0.0.1" },
|
|
2673
|
+
entries
|
|
2674
|
+
}
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
2677
|
+
};
|
|
2678
|
+
}
|
|
2679
|
+
var init_network = __esm(() => {
|
|
2680
|
+
init_network_log();
|
|
2681
|
+
});
|
|
2682
|
+
|
|
2683
|
+
// src/db/console-log.ts
|
|
2684
|
+
var exports_console_log = {};
|
|
2685
|
+
__export(exports_console_log, {
|
|
2686
|
+
logConsoleMessage: () => logConsoleMessage,
|
|
2687
|
+
getConsoleMessage: () => getConsoleMessage,
|
|
2688
|
+
getConsoleLog: () => getConsoleLog,
|
|
2689
|
+
clearConsoleLog: () => clearConsoleLog
|
|
2690
|
+
});
|
|
2691
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
2692
|
+
function logConsoleMessage(data) {
|
|
2693
|
+
const db = getDatabase();
|
|
2694
|
+
const id = randomUUID3();
|
|
2695
|
+
db.prepare("INSERT INTO console_log (id, session_id, level, message, source, line_number) VALUES (?, ?, ?, ?, ?, ?)").run(id, data.session_id, data.level, data.message, data.source ?? null, data.line_number ?? null);
|
|
2696
|
+
return getConsoleMessage(id);
|
|
2697
|
+
}
|
|
2698
|
+
function getConsoleMessage(id) {
|
|
2699
|
+
const db = getDatabase();
|
|
2700
|
+
return db.query("SELECT * FROM console_log WHERE id = ?").get(id) ?? null;
|
|
2701
|
+
}
|
|
2702
|
+
function getConsoleLog(sessionId, level) {
|
|
2703
|
+
const db = getDatabase();
|
|
2704
|
+
if (level) {
|
|
2705
|
+
return db.query("SELECT * FROM console_log WHERE session_id = ? AND level = ? ORDER BY timestamp ASC").all(sessionId, level);
|
|
2706
|
+
}
|
|
2707
|
+
return db.query("SELECT * FROM console_log WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
|
|
2708
|
+
}
|
|
2709
|
+
function clearConsoleLog(sessionId) {
|
|
2710
|
+
const db = getDatabase();
|
|
2711
|
+
db.prepare("DELETE FROM console_log WHERE session_id = ?").run(sessionId);
|
|
2712
|
+
}
|
|
2713
|
+
var init_console_log = __esm(() => {
|
|
2714
|
+
init_schema();
|
|
2715
|
+
});
|
|
2716
|
+
|
|
2717
|
+
// src/lib/console.ts
|
|
2718
|
+
function enableConsoleCapture(page, sessionId) {
|
|
2719
|
+
const onConsole = (msg) => {
|
|
2720
|
+
const levelMap = {
|
|
2721
|
+
log: "log",
|
|
2722
|
+
warn: "warn",
|
|
2723
|
+
error: "error",
|
|
2724
|
+
debug: "debug",
|
|
2725
|
+
info: "info",
|
|
2726
|
+
warning: "warn"
|
|
2727
|
+
};
|
|
2728
|
+
const level = levelMap[msg.type()] ?? "log";
|
|
2729
|
+
const location = msg.location();
|
|
2730
|
+
try {
|
|
2731
|
+
logConsoleMessage({
|
|
2732
|
+
session_id: sessionId,
|
|
2733
|
+
level,
|
|
2734
|
+
message: msg.text(),
|
|
2735
|
+
source: location.url || undefined,
|
|
2736
|
+
line_number: location.lineNumber || undefined
|
|
2737
|
+
});
|
|
2738
|
+
} catch {}
|
|
2739
|
+
};
|
|
2740
|
+
page.on("console", onConsole);
|
|
2741
|
+
return () => page.off("console", onConsole);
|
|
2742
|
+
}
|
|
2743
|
+
var init_console = __esm(() => {
|
|
2744
|
+
init_console_log();
|
|
2745
|
+
});
|
|
2746
|
+
|
|
2747
|
+
// src/lib/stealth.ts
|
|
2748
|
+
async function applyStealthPatches(page) {
|
|
2749
|
+
await page.context().addInitScript(STEALTH_SCRIPT);
|
|
2750
|
+
await page.context().setExtraHTTPHeaders({
|
|
2751
|
+
"User-Agent": REALISTIC_USER_AGENT
|
|
2752
|
+
});
|
|
2753
|
+
}
|
|
2754
|
+
var REALISTIC_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36", STEALTH_SCRIPT = `
|
|
2755
|
+
// \u2500\u2500 1. Remove navigator.webdriver flag \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2756
|
+
Object.defineProperty(navigator, 'webdriver', {
|
|
2757
|
+
get: () => false,
|
|
2758
|
+
configurable: true,
|
|
2759
|
+
});
|
|
2760
|
+
|
|
2761
|
+
// \u2500\u2500 2. Override navigator.plugins to show typical Chrome plugins \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2762
|
+
Object.defineProperty(navigator, 'plugins', {
|
|
2763
|
+
get: () => {
|
|
2764
|
+
const plugins = [
|
|
2765
|
+
{ name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format', length: 1 },
|
|
2766
|
+
{ name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '', length: 1 },
|
|
2767
|
+
{ name: 'Native Client', filename: 'internal-nacl-plugin', description: '', length: 2 },
|
|
2768
|
+
];
|
|
2769
|
+
// Mimic PluginArray interface
|
|
2770
|
+
const pluginArray = Object.create(PluginArray.prototype);
|
|
2771
|
+
plugins.forEach((p, i) => {
|
|
2772
|
+
const plugin = Object.create(Plugin.prototype);
|
|
2773
|
+
Object.defineProperties(plugin, {
|
|
2774
|
+
name: { value: p.name, enumerable: true },
|
|
2775
|
+
filename: { value: p.filename, enumerable: true },
|
|
2776
|
+
description: { value: p.description, enumerable: true },
|
|
2777
|
+
length: { value: p.length, enumerable: true },
|
|
2778
|
+
});
|
|
2779
|
+
pluginArray[i] = plugin;
|
|
2780
|
+
});
|
|
2781
|
+
Object.defineProperty(pluginArray, 'length', { value: plugins.length });
|
|
2782
|
+
pluginArray.item = (i) => pluginArray[i] || null;
|
|
2783
|
+
pluginArray.namedItem = (name) => plugins.find(p => p.name === name) ? pluginArray[plugins.findIndex(p => p.name === name)] : null;
|
|
2784
|
+
pluginArray.refresh = () => {};
|
|
2785
|
+
return pluginArray;
|
|
2786
|
+
},
|
|
2787
|
+
configurable: true,
|
|
2788
|
+
});
|
|
2789
|
+
|
|
2790
|
+
// \u2500\u2500 3. Override navigator.languages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2791
|
+
Object.defineProperty(navigator, 'languages', {
|
|
2792
|
+
get: () => ['en-US', 'en'],
|
|
2793
|
+
configurable: true,
|
|
2794
|
+
});
|
|
2795
|
+
|
|
2796
|
+
// \u2500\u2500 4. Override chrome.runtime to appear like real Chrome \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2797
|
+
if (!window.chrome) {
|
|
2798
|
+
window.chrome = {};
|
|
2799
|
+
}
|
|
2800
|
+
if (!window.chrome.runtime) {
|
|
2801
|
+
window.chrome.runtime = {
|
|
2802
|
+
connect: function() { return { onMessage: { addListener: function() {} }, postMessage: function() {} }; },
|
|
2803
|
+
sendMessage: function() {},
|
|
2804
|
+
onMessage: { addListener: function() {}, removeListener: function() {} },
|
|
2805
|
+
id: undefined,
|
|
2806
|
+
};
|
|
2807
|
+
}
|
|
2808
|
+
`;
|
|
2809
|
+
var init_stealth = () => {};
|
|
2810
|
+
|
|
2811
|
+
// src/lib/dialogs.ts
|
|
2812
|
+
function setupDialogHandler(page, sessionId) {
|
|
2813
|
+
const onDialog = (dialog) => {
|
|
2814
|
+
const info = {
|
|
2815
|
+
type: dialog.type(),
|
|
2816
|
+
message: dialog.message(),
|
|
2817
|
+
default_value: dialog.defaultValue(),
|
|
2818
|
+
timestamp: new Date().toISOString()
|
|
2819
|
+
};
|
|
2820
|
+
const autoTimer = setTimeout(() => {
|
|
2821
|
+
try {
|
|
2822
|
+
dialog.dismiss().catch(() => {});
|
|
2823
|
+
} catch {}
|
|
2824
|
+
const list = pendingDialogs.get(sessionId);
|
|
2825
|
+
if (list) {
|
|
2826
|
+
const idx = list.findIndex((p) => p.dialog === dialog);
|
|
2827
|
+
if (idx >= 0)
|
|
2828
|
+
list.splice(idx, 1);
|
|
2829
|
+
if (list.length === 0)
|
|
2830
|
+
pendingDialogs.delete(sessionId);
|
|
2831
|
+
}
|
|
2832
|
+
}, AUTO_DISMISS_MS);
|
|
2833
|
+
const pending = { dialog, info, autoTimer };
|
|
2834
|
+
if (!pendingDialogs.has(sessionId)) {
|
|
2835
|
+
pendingDialogs.set(sessionId, []);
|
|
2836
|
+
}
|
|
2837
|
+
pendingDialogs.get(sessionId).push(pending);
|
|
2838
|
+
};
|
|
2839
|
+
page.on("dialog", onDialog);
|
|
2840
|
+
return () => {
|
|
2841
|
+
page.off("dialog", onDialog);
|
|
2842
|
+
const list = pendingDialogs.get(sessionId);
|
|
2843
|
+
if (list) {
|
|
2844
|
+
for (const p of list)
|
|
2845
|
+
clearTimeout(p.autoTimer);
|
|
2846
|
+
pendingDialogs.delete(sessionId);
|
|
2847
|
+
}
|
|
2848
|
+
};
|
|
2849
|
+
}
|
|
2850
|
+
function getDialogs(sessionId) {
|
|
2851
|
+
const list = pendingDialogs.get(sessionId);
|
|
2852
|
+
if (!list)
|
|
2853
|
+
return [];
|
|
2854
|
+
return list.map((p) => p.info);
|
|
2855
|
+
}
|
|
2856
|
+
async function handleDialog(sessionId, action, promptText) {
|
|
2857
|
+
const list = pendingDialogs.get(sessionId);
|
|
2858
|
+
if (!list || list.length === 0) {
|
|
2859
|
+
return { handled: false };
|
|
2860
|
+
}
|
|
2861
|
+
const pending = list.shift();
|
|
2862
|
+
clearTimeout(pending.autoTimer);
|
|
2863
|
+
if (list.length === 0) {
|
|
2864
|
+
pendingDialogs.delete(sessionId);
|
|
2865
|
+
}
|
|
2866
|
+
try {
|
|
2867
|
+
if (action === "accept") {
|
|
2868
|
+
await pending.dialog.accept(promptText);
|
|
2869
|
+
} else {
|
|
2870
|
+
await pending.dialog.dismiss();
|
|
2871
|
+
}
|
|
2872
|
+
} catch {}
|
|
2873
|
+
return { handled: true, dialog: pending.info };
|
|
2874
|
+
}
|
|
2875
|
+
var pendingDialogs, AUTO_DISMISS_MS = 5000;
|
|
2876
|
+
var init_dialogs = __esm(() => {
|
|
2877
|
+
pendingDialogs = new Map;
|
|
2878
|
+
});
|
|
2879
|
+
|
|
2528
2880
|
// src/lib/session.ts
|
|
2529
2881
|
async function createSession2(opts = {}) {
|
|
2530
2882
|
const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
|
|
@@ -2546,17 +2898,44 @@ async function createSession2(opts = {}) {
|
|
|
2546
2898
|
userAgent: opts.userAgent
|
|
2547
2899
|
});
|
|
2548
2900
|
}
|
|
2901
|
+
let sessionName = opts.name ?? (opts.startUrl ? (() => {
|
|
2902
|
+
try {
|
|
2903
|
+
return new URL(opts.startUrl).hostname;
|
|
2904
|
+
} catch {
|
|
2905
|
+
return;
|
|
2906
|
+
}
|
|
2907
|
+
})() : undefined);
|
|
2549
2908
|
const session = createSession({
|
|
2550
2909
|
engine: resolvedEngine,
|
|
2551
2910
|
projectId: opts.projectId,
|
|
2552
2911
|
agentId: opts.agentId,
|
|
2553
|
-
startUrl: opts.startUrl
|
|
2912
|
+
startUrl: opts.startUrl,
|
|
2913
|
+
name: sessionName
|
|
2554
2914
|
});
|
|
2555
|
-
|
|
2915
|
+
if (opts.stealth) {
|
|
2916
|
+
try {
|
|
2917
|
+
await applyStealthPatches(page);
|
|
2918
|
+
} catch {}
|
|
2919
|
+
}
|
|
2920
|
+
const cleanups = [];
|
|
2921
|
+
if (opts.captureNetwork !== false) {
|
|
2922
|
+
try {
|
|
2923
|
+
cleanups.push(enableNetworkLogging(page, session.id));
|
|
2924
|
+
} catch {}
|
|
2925
|
+
}
|
|
2926
|
+
if (opts.captureConsole !== false) {
|
|
2927
|
+
try {
|
|
2928
|
+
cleanups.push(enableConsoleCapture(page, session.id));
|
|
2929
|
+
} catch {}
|
|
2930
|
+
}
|
|
2931
|
+
try {
|
|
2932
|
+
cleanups.push(setupDialogHandler(page, session.id));
|
|
2933
|
+
} catch {}
|
|
2934
|
+
handles.set(session.id, { browser, page, engine: resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 } });
|
|
2556
2935
|
if (opts.startUrl) {
|
|
2557
2936
|
try {
|
|
2558
2937
|
await page.goto(opts.startUrl, { waitUntil: "domcontentloaded" });
|
|
2559
|
-
} catch
|
|
2938
|
+
} catch {}
|
|
2560
2939
|
}
|
|
2561
2940
|
return { session, page };
|
|
2562
2941
|
}
|
|
@@ -2564,11 +2943,28 @@ function getSessionPage(sessionId) {
|
|
|
2564
2943
|
const handle = handles.get(sessionId);
|
|
2565
2944
|
if (!handle)
|
|
2566
2945
|
throw new SessionNotFoundError(sessionId);
|
|
2946
|
+
try {
|
|
2947
|
+
handle.page.url();
|
|
2948
|
+
} catch {
|
|
2949
|
+
handles.delete(sessionId);
|
|
2950
|
+
throw new SessionNotFoundError(sessionId);
|
|
2951
|
+
}
|
|
2567
2952
|
return handle.page;
|
|
2568
2953
|
}
|
|
2954
|
+
function setSessionPage(sessionId, page) {
|
|
2955
|
+
const handle = handles.get(sessionId);
|
|
2956
|
+
if (!handle)
|
|
2957
|
+
throw new SessionNotFoundError(sessionId);
|
|
2958
|
+
handle.page = page;
|
|
2959
|
+
}
|
|
2569
2960
|
async function closeSession2(sessionId) {
|
|
2570
2961
|
const handle = handles.get(sessionId);
|
|
2571
2962
|
if (handle) {
|
|
2963
|
+
for (const cleanup of handle.cleanups) {
|
|
2964
|
+
try {
|
|
2965
|
+
cleanup();
|
|
2966
|
+
} catch {}
|
|
2967
|
+
}
|
|
2572
2968
|
try {
|
|
2573
2969
|
await handle.page.context().close();
|
|
2574
2970
|
} catch {}
|
|
@@ -2579,6 +2975,9 @@ async function closeSession2(sessionId) {
|
|
|
2579
2975
|
}
|
|
2580
2976
|
return closeSession(sessionId);
|
|
2581
2977
|
}
|
|
2978
|
+
function getSession2(sessionId) {
|
|
2979
|
+
return getSession(sessionId);
|
|
2980
|
+
}
|
|
2582
2981
|
function listSessions2(filter) {
|
|
2583
2982
|
return listSessions(filter);
|
|
2584
2983
|
}
|
|
@@ -2588,15 +2987,227 @@ function getSessionByName2(name) {
|
|
|
2588
2987
|
function renameSession2(id, name) {
|
|
2589
2988
|
return renameSession(id, name);
|
|
2590
2989
|
}
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2990
|
+
function getTokenBudget(sessionId) {
|
|
2991
|
+
const handle = handles.get(sessionId);
|
|
2992
|
+
return handle ? handle.tokenBudget : null;
|
|
2993
|
+
}
|
|
2994
|
+
var handles;
|
|
2995
|
+
var init_session = __esm(() => {
|
|
2996
|
+
init_types();
|
|
2997
|
+
init_types();
|
|
2998
|
+
init_sessions();
|
|
2999
|
+
init_playwright();
|
|
3000
|
+
init_lightpanda();
|
|
3001
|
+
init_selector();
|
|
3002
|
+
init_network();
|
|
3003
|
+
init_console();
|
|
3004
|
+
init_stealth();
|
|
3005
|
+
init_dialogs();
|
|
3006
|
+
handles = new Map;
|
|
3007
|
+
});
|
|
3008
|
+
|
|
3009
|
+
// src/lib/snapshot.ts
|
|
3010
|
+
var exports_snapshot = {};
|
|
3011
|
+
__export(exports_snapshot, {
|
|
3012
|
+
takeSnapshot: () => takeSnapshot,
|
|
3013
|
+
setLastSnapshot: () => setLastSnapshot,
|
|
3014
|
+
hasRefs: () => hasRefs,
|
|
3015
|
+
getSessionRefs: () => getSessionRefs,
|
|
3016
|
+
getRefLocator: () => getRefLocator,
|
|
3017
|
+
getRefInfo: () => getRefInfo,
|
|
3018
|
+
getLastSnapshot: () => getLastSnapshot,
|
|
3019
|
+
diffSnapshots: () => diffSnapshots,
|
|
3020
|
+
clearSessionRefs: () => clearSessionRefs,
|
|
3021
|
+
clearLastSnapshot: () => clearLastSnapshot
|
|
3022
|
+
});
|
|
3023
|
+
function getLastSnapshot(sessionId) {
|
|
3024
|
+
return lastSnapshots.get(sessionId) ?? null;
|
|
3025
|
+
}
|
|
3026
|
+
function setLastSnapshot(sessionId, snapshot) {
|
|
3027
|
+
lastSnapshots.set(sessionId, snapshot);
|
|
3028
|
+
}
|
|
3029
|
+
function clearLastSnapshot(sessionId) {
|
|
3030
|
+
lastSnapshots.delete(sessionId);
|
|
3031
|
+
}
|
|
3032
|
+
async function takeSnapshot(page, sessionId) {
|
|
3033
|
+
let ariaTree;
|
|
3034
|
+
try {
|
|
3035
|
+
ariaTree = await page.locator("body").ariaSnapshot();
|
|
3036
|
+
} catch {
|
|
3037
|
+
ariaTree = "";
|
|
3038
|
+
}
|
|
3039
|
+
const refs = {};
|
|
3040
|
+
const refMap = new Map;
|
|
3041
|
+
let refCounter = 0;
|
|
3042
|
+
for (const role of INTERACTIVE_ROLES) {
|
|
3043
|
+
const locators = page.getByRole(role);
|
|
3044
|
+
const count = await locators.count();
|
|
3045
|
+
for (let i = 0;i < count; i++) {
|
|
3046
|
+
const el = locators.nth(i);
|
|
3047
|
+
let name = "";
|
|
3048
|
+
let visible = false;
|
|
3049
|
+
let enabled = true;
|
|
3050
|
+
let value;
|
|
3051
|
+
let checked;
|
|
3052
|
+
try {
|
|
3053
|
+
visible = await el.isVisible();
|
|
3054
|
+
if (!visible)
|
|
3055
|
+
continue;
|
|
3056
|
+
} catch {
|
|
3057
|
+
continue;
|
|
3058
|
+
}
|
|
3059
|
+
try {
|
|
3060
|
+
name = await el.evaluate((e) => {
|
|
3061
|
+
const el2 = e;
|
|
3062
|
+
return el2.getAttribute("aria-label") ?? el2.textContent?.trim().slice(0, 80) ?? el2.getAttribute("title") ?? el2.getAttribute("placeholder") ?? "";
|
|
3063
|
+
});
|
|
3064
|
+
} catch {
|
|
3065
|
+
continue;
|
|
3066
|
+
}
|
|
3067
|
+
if (!name)
|
|
3068
|
+
continue;
|
|
3069
|
+
try {
|
|
3070
|
+
enabled = await el.isEnabled();
|
|
3071
|
+
} catch {}
|
|
3072
|
+
try {
|
|
3073
|
+
if (role === "checkbox" || role === "radio" || role === "switch") {
|
|
3074
|
+
checked = await el.isChecked();
|
|
3075
|
+
}
|
|
3076
|
+
} catch {}
|
|
3077
|
+
try {
|
|
3078
|
+
if (role === "textbox" || role === "searchbox" || role === "spinbutton" || role === "combobox") {
|
|
3079
|
+
value = await el.inputValue();
|
|
3080
|
+
}
|
|
3081
|
+
} catch {}
|
|
3082
|
+
const ref = `@e${refCounter}`;
|
|
3083
|
+
refCounter++;
|
|
3084
|
+
refs[ref] = { role, name, visible, enabled, value, checked };
|
|
3085
|
+
const escapedName = name.replace(/"/g, "\\\"");
|
|
3086
|
+
refMap.set(ref, { role, name, locatorSelector: `role=${role}[name="${escapedName}"]` });
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
let annotatedTree = ariaTree;
|
|
3090
|
+
for (const [ref, info] of Object.entries(refs)) {
|
|
3091
|
+
const escapedName = info.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3092
|
+
const pattern = new RegExp(`(${info.role}\\s+"${escapedName.slice(0, 40)}[^"]*")`, "m");
|
|
3093
|
+
const match = annotatedTree.match(pattern);
|
|
3094
|
+
if (match) {
|
|
3095
|
+
annotatedTree = annotatedTree.replace(match[0], `${match[0]} [${ref}]`);
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
const unmatchedRefs = Object.entries(refs).filter(([ref]) => !annotatedTree.includes(`[${ref}]`));
|
|
3099
|
+
if (unmatchedRefs.length > 0) {
|
|
3100
|
+
annotatedTree += `
|
|
3101
|
+
|
|
3102
|
+
--- Interactive elements ---`;
|
|
3103
|
+
for (const [ref, info] of unmatchedRefs) {
|
|
3104
|
+
const extras = [];
|
|
3105
|
+
if (info.checked !== undefined)
|
|
3106
|
+
extras.push(`checked=${info.checked}`);
|
|
3107
|
+
if (!info.enabled)
|
|
3108
|
+
extras.push("disabled");
|
|
3109
|
+
if (info.value)
|
|
3110
|
+
extras.push(`value="${info.value}"`);
|
|
3111
|
+
const extrasStr = extras.length ? ` (${extras.join(", ")})` : "";
|
|
3112
|
+
annotatedTree += `
|
|
3113
|
+
${info.role} "${info.name}" [${ref}]${extrasStr}`;
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
if (sessionId) {
|
|
3117
|
+
sessionRefMaps.set(sessionId, refMap);
|
|
3118
|
+
}
|
|
3119
|
+
return {
|
|
3120
|
+
tree: annotatedTree,
|
|
3121
|
+
refs,
|
|
3122
|
+
interactive_count: refCounter
|
|
3123
|
+
};
|
|
3124
|
+
}
|
|
3125
|
+
function getRefLocator(page, sessionId, ref) {
|
|
3126
|
+
const refMap = sessionRefMaps.get(sessionId);
|
|
3127
|
+
if (!refMap)
|
|
3128
|
+
throw new Error(`No snapshot taken for session ${sessionId}. Call browser_snapshot first.`);
|
|
3129
|
+
const entry = refMap.get(ref);
|
|
3130
|
+
if (!entry)
|
|
3131
|
+
throw new Error(`Ref ${ref} not found. Available refs: ${[...refMap.keys()].slice(0, 20).join(", ")}`);
|
|
3132
|
+
return page.getByRole(entry.role, { name: entry.name }).first();
|
|
3133
|
+
}
|
|
3134
|
+
function getRefInfo(sessionId, ref) {
|
|
3135
|
+
const refMap = sessionRefMaps.get(sessionId);
|
|
3136
|
+
if (!refMap)
|
|
3137
|
+
return null;
|
|
3138
|
+
return refMap.get(ref) ?? null;
|
|
3139
|
+
}
|
|
3140
|
+
function getSessionRefs(sessionId) {
|
|
3141
|
+
return sessionRefMaps.get(sessionId) ?? null;
|
|
3142
|
+
}
|
|
3143
|
+
function clearSessionRefs(sessionId) {
|
|
3144
|
+
sessionRefMaps.delete(sessionId);
|
|
3145
|
+
}
|
|
3146
|
+
function hasRefs(sessionId) {
|
|
3147
|
+
return sessionRefMaps.has(sessionId) && (sessionRefMaps.get(sessionId)?.size ?? 0) > 0;
|
|
3148
|
+
}
|
|
3149
|
+
function refKey(info) {
|
|
3150
|
+
return `${info.role}::${info.name}`;
|
|
3151
|
+
}
|
|
3152
|
+
function diffSnapshots(before, after) {
|
|
3153
|
+
const beforeMap = new Map;
|
|
3154
|
+
for (const [ref, info] of Object.entries(before.refs)) {
|
|
3155
|
+
beforeMap.set(refKey(info), { ref, info });
|
|
3156
|
+
}
|
|
3157
|
+
const afterMap = new Map;
|
|
3158
|
+
for (const [ref, info] of Object.entries(after.refs)) {
|
|
3159
|
+
afterMap.set(refKey(info), { ref, info });
|
|
3160
|
+
}
|
|
3161
|
+
const added = [];
|
|
3162
|
+
const removed = [];
|
|
3163
|
+
const modified = [];
|
|
3164
|
+
for (const [key, afterEntry] of afterMap) {
|
|
3165
|
+
const beforeEntry = beforeMap.get(key);
|
|
3166
|
+
if (!beforeEntry) {
|
|
3167
|
+
added.push({ ref: afterEntry.ref, info: afterEntry.info });
|
|
3168
|
+
} else {
|
|
3169
|
+
const b = beforeEntry.info;
|
|
3170
|
+
const a = afterEntry.info;
|
|
3171
|
+
if (b.visible !== a.visible || b.enabled !== a.enabled || b.value !== a.value || b.checked !== a.checked || b.description !== a.description) {
|
|
3172
|
+
modified.push({ ref: afterEntry.ref, before: b, after: a });
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
for (const [key, beforeEntry] of beforeMap) {
|
|
3177
|
+
if (!afterMap.has(key)) {
|
|
3178
|
+
removed.push({ ref: beforeEntry.ref, info: beforeEntry.info });
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
const url_changed = before.tree.split(`
|
|
3182
|
+
`)[0] !== after.tree.split(`
|
|
3183
|
+
`)[0];
|
|
3184
|
+
const title_changed = before.tree !== after.tree && (added.length > 0 || removed.length > 0 || modified.length > 0);
|
|
3185
|
+
return { added, removed, modified, url_changed, title_changed };
|
|
3186
|
+
}
|
|
3187
|
+
var lastSnapshots, sessionRefMaps, INTERACTIVE_ROLES;
|
|
3188
|
+
var init_snapshot = __esm(() => {
|
|
3189
|
+
lastSnapshots = new Map;
|
|
3190
|
+
sessionRefMaps = new Map;
|
|
3191
|
+
INTERACTIVE_ROLES = [
|
|
3192
|
+
"button",
|
|
3193
|
+
"link",
|
|
3194
|
+
"textbox",
|
|
3195
|
+
"checkbox",
|
|
3196
|
+
"radio",
|
|
3197
|
+
"combobox",
|
|
3198
|
+
"menuitem",
|
|
3199
|
+
"menuitemcheckbox",
|
|
3200
|
+
"menuitemradio",
|
|
3201
|
+
"option",
|
|
3202
|
+
"searchbox",
|
|
3203
|
+
"slider",
|
|
3204
|
+
"spinbutton",
|
|
3205
|
+
"switch",
|
|
3206
|
+
"tab",
|
|
3207
|
+
"treeitem",
|
|
3208
|
+
"listbox",
|
|
3209
|
+
"menu"
|
|
3210
|
+
];
|
|
2600
3211
|
});
|
|
2601
3212
|
|
|
2602
3213
|
// src/lib/actions.ts
|
|
@@ -2818,9 +3429,67 @@ function stopWatch(watchId) {
|
|
|
2818
3429
|
activeWatches.delete(watchId);
|
|
2819
3430
|
}
|
|
2820
3431
|
}
|
|
3432
|
+
async function clickRef(page, sessionId, ref, opts) {
|
|
3433
|
+
try {
|
|
3434
|
+
const locator = getRefLocator(page, sessionId, ref);
|
|
3435
|
+
await locator.click({ timeout: opts?.timeout ?? 1e4 });
|
|
3436
|
+
} catch (err) {
|
|
3437
|
+
if (err instanceof Error && err.message.includes("Ref "))
|
|
3438
|
+
throw new ElementNotFoundError(ref);
|
|
3439
|
+
if (err instanceof Error && err.message.includes("No snapshot"))
|
|
3440
|
+
throw new BrowserError(err.message, "NO_SNAPSHOT");
|
|
3441
|
+
throw new BrowserError(`clickRef ${ref} failed: ${err instanceof Error ? err.message : String(err)}`, "CLICK_REF_FAILED");
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
async function typeRef(page, sessionId, ref, text, opts) {
|
|
3445
|
+
try {
|
|
3446
|
+
const locator = getRefLocator(page, sessionId, ref);
|
|
3447
|
+
if (opts?.clear)
|
|
3448
|
+
await locator.fill("", { timeout: opts.timeout ?? 1e4 });
|
|
3449
|
+
await locator.pressSequentially(text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
|
|
3450
|
+
} catch (err) {
|
|
3451
|
+
if (err instanceof Error && err.message.includes("Ref "))
|
|
3452
|
+
throw new ElementNotFoundError(ref);
|
|
3453
|
+
throw new BrowserError(`typeRef ${ref} failed: ${err instanceof Error ? err.message : String(err)}`, "TYPE_REF_FAILED");
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
async function selectRef(page, sessionId, ref, value, timeout = 1e4) {
|
|
3457
|
+
try {
|
|
3458
|
+
const locator = getRefLocator(page, sessionId, ref);
|
|
3459
|
+
return await locator.selectOption(value, { timeout });
|
|
3460
|
+
} catch (err) {
|
|
3461
|
+
if (err instanceof Error && err.message.includes("Ref "))
|
|
3462
|
+
throw new ElementNotFoundError(ref);
|
|
3463
|
+
throw new BrowserError(`selectRef ${ref} failed: ${err instanceof Error ? err.message : String(err)}`, "SELECT_REF_FAILED");
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
async function checkRef(page, sessionId, ref, checked, timeout = 1e4) {
|
|
3467
|
+
try {
|
|
3468
|
+
const locator = getRefLocator(page, sessionId, ref);
|
|
3469
|
+
if (checked)
|
|
3470
|
+
await locator.check({ timeout });
|
|
3471
|
+
else
|
|
3472
|
+
await locator.uncheck({ timeout });
|
|
3473
|
+
} catch (err) {
|
|
3474
|
+
if (err instanceof Error && err.message.includes("Ref "))
|
|
3475
|
+
throw new ElementNotFoundError(ref);
|
|
3476
|
+
throw new BrowserError(`checkRef ${ref} failed: ${err instanceof Error ? err.message : String(err)}`, "CHECK_REF_FAILED");
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
async function hoverRef(page, sessionId, ref, timeout = 1e4) {
|
|
3480
|
+
try {
|
|
3481
|
+
const locator = getRefLocator(page, sessionId, ref);
|
|
3482
|
+
await locator.hover({ timeout });
|
|
3483
|
+
} catch (err) {
|
|
3484
|
+
if (err instanceof Error && err.message.includes("Ref "))
|
|
3485
|
+
throw new ElementNotFoundError(ref);
|
|
3486
|
+
throw new BrowserError(`hoverRef ${ref} failed: ${err instanceof Error ? err.message : String(err)}`, "HOVER_REF_FAILED");
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
2821
3489
|
var RETRYABLE_ERRORS, activeWatches;
|
|
2822
3490
|
var init_actions = __esm(() => {
|
|
2823
3491
|
init_types();
|
|
3492
|
+
init_snapshot();
|
|
2824
3493
|
RETRYABLE_ERRORS = ["Timeout", "timeout", "navigation", "net::ERR", "Target closed"];
|
|
2825
3494
|
activeWatches = new Map;
|
|
2826
3495
|
});
|
|
@@ -2890,24 +3559,6 @@ async function extractTable(page, selector) {
|
|
|
2890
3559
|
return rows.map((row) => Array.from(row.querySelectorAll("th, td")).map((cell) => cell.textContent?.trim() ?? ""));
|
|
2891
3560
|
}, selector);
|
|
2892
3561
|
}
|
|
2893
|
-
async function getAriaSnapshot(page) {
|
|
2894
|
-
try {
|
|
2895
|
-
return await page.ariaSnapshot?.() ?? page.evaluate(() => {
|
|
2896
|
-
function walk(el, indent = 0) {
|
|
2897
|
-
const role = el.getAttribute("role") ?? el.tagName.toLowerCase();
|
|
2898
|
-
const label = el.getAttribute("aria-label") ?? el.getAttribute("aria-labelledby") ?? el.textContent?.trim().slice(0, 50) ?? "";
|
|
2899
|
-
const line = " ".repeat(indent) + `[${role}] ${label}`;
|
|
2900
|
-
const children = Array.from(el.children).map((c) => walk(c, indent + 1)).join(`
|
|
2901
|
-
`);
|
|
2902
|
-
return children ? `${line}
|
|
2903
|
-
${children}` : line;
|
|
2904
|
-
}
|
|
2905
|
-
return walk(document.body);
|
|
2906
|
-
});
|
|
2907
|
-
} catch {
|
|
2908
|
-
return page.evaluate(() => document.body.innerText?.slice(0, 2000) ?? "");
|
|
2909
|
-
}
|
|
2910
|
-
}
|
|
2911
3562
|
async function extract(page, opts = {}) {
|
|
2912
3563
|
const result = {};
|
|
2913
3564
|
const format = opts.format ?? "text";
|
|
@@ -9377,7 +10028,7 @@ __export(exports_gallery, {
|
|
|
9377
10028
|
deleteEntry: () => deleteEntry,
|
|
9378
10029
|
createEntry: () => createEntry
|
|
9379
10030
|
});
|
|
9380
|
-
import { randomUUID as
|
|
10031
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
9381
10032
|
function deserialize(row) {
|
|
9382
10033
|
return {
|
|
9383
10034
|
id: row.id,
|
|
@@ -9401,7 +10052,7 @@ function deserialize(row) {
|
|
|
9401
10052
|
}
|
|
9402
10053
|
function createEntry(data) {
|
|
9403
10054
|
const db = getDatabase();
|
|
9404
|
-
const id =
|
|
10055
|
+
const id = randomUUID4();
|
|
9405
10056
|
db.prepare(`
|
|
9406
10057
|
INSERT INTO gallery_entries
|
|
9407
10058
|
(id, session_id, project_id, url, title, path, thumbnail_path, format,
|
|
@@ -9575,11 +10226,20 @@ async function takeScreenshot(page, opts) {
|
|
|
9575
10226
|
}
|
|
9576
10227
|
const originalSizeBytes = rawBuffer.length;
|
|
9577
10228
|
let finalBuffer;
|
|
9578
|
-
|
|
9579
|
-
|
|
9580
|
-
|
|
9581
|
-
|
|
9582
|
-
|
|
10229
|
+
let compressed = true;
|
|
10230
|
+
let fallback = false;
|
|
10231
|
+
try {
|
|
10232
|
+
if (compress && format !== "png") {
|
|
10233
|
+
finalBuffer = await compressBuffer(rawBuffer, format, quality ?? 82, maxWidth);
|
|
10234
|
+
} else if (compress && format === "png") {
|
|
10235
|
+
finalBuffer = await compressBuffer(rawBuffer, "png", quality ?? 9, maxWidth);
|
|
10236
|
+
} else {
|
|
10237
|
+
finalBuffer = rawBuffer;
|
|
10238
|
+
compressed = false;
|
|
10239
|
+
}
|
|
10240
|
+
} catch (sharpErr) {
|
|
10241
|
+
fallback = true;
|
|
10242
|
+
compressed = false;
|
|
9583
10243
|
finalBuffer = rawBuffer;
|
|
9584
10244
|
}
|
|
9585
10245
|
const compressedSizeBytes = finalBuffer.length;
|
|
@@ -9607,7 +10267,8 @@ async function takeScreenshot(page, opts) {
|
|
|
9607
10267
|
compressed_size_bytes: compressedSizeBytes,
|
|
9608
10268
|
compression_ratio: compressionRatio,
|
|
9609
10269
|
thumbnail_path: thumbnailPath,
|
|
9610
|
-
thumbnail_base64: thumbnailBase64
|
|
10270
|
+
thumbnail_base64: thumbnailBase64,
|
|
10271
|
+
...fallback ? { fallback: true, compressed: false } : {}
|
|
9611
10272
|
};
|
|
9612
10273
|
if (opts?.track !== false) {
|
|
9613
10274
|
try {
|
|
@@ -9674,7 +10335,7 @@ var init_screenshot = __esm(() => {
|
|
|
9674
10335
|
});
|
|
9675
10336
|
|
|
9676
10337
|
// src/db/crawl-results.ts
|
|
9677
|
-
import { randomUUID as
|
|
10338
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
9678
10339
|
function deserialize2(row) {
|
|
9679
10340
|
const pages = JSON.parse(row.pages);
|
|
9680
10341
|
return {
|
|
@@ -9690,7 +10351,7 @@ function deserialize2(row) {
|
|
|
9690
10351
|
}
|
|
9691
10352
|
function createCrawlResult(data) {
|
|
9692
10353
|
const db = getDatabase();
|
|
9693
|
-
const id =
|
|
10354
|
+
const id = randomUUID5();
|
|
9694
10355
|
db.prepare("INSERT INTO crawl_results (id, project_id, start_url, depth, pages, links, errors) VALUES (?, ?, ?, ?, ?, ?, ?)").run(id, data.project_id ?? null, data.start_url, data.depth, JSON.stringify(data.pages), JSON.stringify(data.pages.flatMap((p) => p.links)), JSON.stringify(data.errors));
|
|
9695
10356
|
return getCrawlResult(id);
|
|
9696
10357
|
}
|
|
@@ -9783,7 +10444,7 @@ __export(exports_agents, {
|
|
|
9783
10444
|
deleteAgent: () => deleteAgent,
|
|
9784
10445
|
cleanStaleAgents: () => cleanStaleAgents
|
|
9785
10446
|
});
|
|
9786
|
-
import { randomUUID as
|
|
10447
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
9787
10448
|
function registerAgent(name, opts = {}) {
|
|
9788
10449
|
const db = getDatabase();
|
|
9789
10450
|
const existing = db.query("SELECT * FROM agents WHERE name = ?").get(name);
|
|
@@ -9791,7 +10452,7 @@ function registerAgent(name, opts = {}) {
|
|
|
9791
10452
|
db.prepare("UPDATE agents SET last_seen = datetime('now'), session_id = ?, project_id = ?, working_dir = ? WHERE name = ?").run(opts.sessionId ?? existing.session_id ?? null, opts.projectId ?? existing.project_id ?? null, opts.workingDir ?? existing.working_dir ?? null, name);
|
|
9792
10453
|
return getAgentByName(name);
|
|
9793
10454
|
}
|
|
9794
|
-
const id =
|
|
10455
|
+
const id = randomUUID6();
|
|
9795
10456
|
db.prepare("INSERT INTO agents (id, name, description, session_id, project_id, working_dir) VALUES (?, ?, ?, ?, ?, ?)").run(id, name, opts.description ?? null, opts.sessionId ?? null, opts.projectId ?? null, opts.workingDir ?? null);
|
|
9796
10457
|
return getAgent(id);
|
|
9797
10458
|
}
|
|
@@ -9801,7 +10462,7 @@ function heartbeat(agentId) {
|
|
|
9801
10462
|
if (!agent)
|
|
9802
10463
|
throw new AgentNotFoundError(agentId);
|
|
9803
10464
|
db.prepare("UPDATE agents SET last_seen = datetime('now') WHERE id = ?").run(agentId);
|
|
9804
|
-
db.prepare("INSERT INTO heartbeats (id, agent_id, session_id) VALUES (?, ?, ?)").run(
|
|
10465
|
+
db.prepare("INSERT INTO heartbeats (id, agent_id, session_id) VALUES (?, ?, ?)").run(randomUUID6(), agentId, agent.session_id ?? null);
|
|
9805
10466
|
}
|
|
9806
10467
|
function getAgent(id) {
|
|
9807
10468
|
const db = getDatabase();
|
|
@@ -9878,10 +10539,10 @@ var init_agents2 = __esm(() => {
|
|
|
9878
10539
|
});
|
|
9879
10540
|
|
|
9880
10541
|
// src/db/projects.ts
|
|
9881
|
-
import { randomUUID as
|
|
10542
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
9882
10543
|
function createProject(data) {
|
|
9883
10544
|
const db = getDatabase();
|
|
9884
|
-
const id =
|
|
10545
|
+
const id = randomUUID7();
|
|
9885
10546
|
db.prepare("INSERT INTO projects (id, name, path, description) VALUES (?, ?, ?, ?)").run(id, data.name, data.path, data.description ?? null);
|
|
9886
10547
|
return getProject(id);
|
|
9887
10548
|
}
|
|
@@ -9917,7 +10578,7 @@ __export(exports_recordings, {
|
|
|
9917
10578
|
deleteRecording: () => deleteRecording,
|
|
9918
10579
|
createRecording: () => createRecording
|
|
9919
10580
|
});
|
|
9920
|
-
import { randomUUID as
|
|
10581
|
+
import { randomUUID as randomUUID8 } from "crypto";
|
|
9921
10582
|
function deserialize3(row) {
|
|
9922
10583
|
return {
|
|
9923
10584
|
...row,
|
|
@@ -9928,7 +10589,7 @@ function deserialize3(row) {
|
|
|
9928
10589
|
}
|
|
9929
10590
|
function createRecording(data) {
|
|
9930
10591
|
const db = getDatabase();
|
|
9931
|
-
const id =
|
|
10592
|
+
const id = randomUUID8();
|
|
9932
10593
|
db.prepare("INSERT INTO recordings (id, name, project_id, start_url, steps) VALUES (?, ?, ?, ?, ?)").run(id, data.name, data.project_id ?? null, data.start_url ?? null, JSON.stringify(data.steps ?? []));
|
|
9933
10594
|
return getRecording(id);
|
|
9934
10595
|
}
|
|
@@ -14031,139 +14692,6 @@ var init_zod = __esm(() => {
|
|
|
14031
14692
|
init_external();
|
|
14032
14693
|
});
|
|
14033
14694
|
|
|
14034
|
-
// src/db/network-log.ts
|
|
14035
|
-
import { randomUUID as randomUUID7 } from "crypto";
|
|
14036
|
-
function logRequest(data) {
|
|
14037
|
-
const db = getDatabase();
|
|
14038
|
-
const id = randomUUID7();
|
|
14039
|
-
db.prepare(`INSERT INTO network_log (id, session_id, method, url, status_code, request_headers,
|
|
14040
|
-
response_headers, request_body, body_size, duration_ms, resource_type)
|
|
14041
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(id, data.session_id, data.method, data.url, data.status_code ?? null, data.request_headers ?? null, data.response_headers ?? null, data.request_body ?? null, data.body_size ?? null, data.duration_ms ?? null, data.resource_type ?? null);
|
|
14042
|
-
return getNetworkRequest(id);
|
|
14043
|
-
}
|
|
14044
|
-
function getNetworkRequest(id) {
|
|
14045
|
-
const db = getDatabase();
|
|
14046
|
-
return db.query("SELECT * FROM network_log WHERE id = ?").get(id) ?? null;
|
|
14047
|
-
}
|
|
14048
|
-
function getNetworkLog(sessionId) {
|
|
14049
|
-
const db = getDatabase();
|
|
14050
|
-
return db.query("SELECT * FROM network_log WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
|
|
14051
|
-
}
|
|
14052
|
-
function clearNetworkLog(sessionId) {
|
|
14053
|
-
const db = getDatabase();
|
|
14054
|
-
db.prepare("DELETE FROM network_log WHERE session_id = ?").run(sessionId);
|
|
14055
|
-
}
|
|
14056
|
-
var init_network_log = __esm(() => {
|
|
14057
|
-
init_schema();
|
|
14058
|
-
});
|
|
14059
|
-
|
|
14060
|
-
// src/lib/network.ts
|
|
14061
|
-
function enableNetworkLogging(page, sessionId) {
|
|
14062
|
-
const requestStart = new Map;
|
|
14063
|
-
const onRequest = (req) => {
|
|
14064
|
-
requestStart.set(req.url(), Date.now());
|
|
14065
|
-
};
|
|
14066
|
-
const onResponse = (res) => {
|
|
14067
|
-
const start = requestStart.get(res.url()) ?? Date.now();
|
|
14068
|
-
const duration = Date.now() - start;
|
|
14069
|
-
const req = res.request();
|
|
14070
|
-
try {
|
|
14071
|
-
logRequest({
|
|
14072
|
-
session_id: sessionId,
|
|
14073
|
-
method: req.method(),
|
|
14074
|
-
url: res.url(),
|
|
14075
|
-
status_code: res.status(),
|
|
14076
|
-
request_headers: JSON.stringify(req.headers()),
|
|
14077
|
-
response_headers: JSON.stringify(res.headers()),
|
|
14078
|
-
body_size: res.headers()["content-length"] != null ? parseInt(res.headers()["content-length"]) : undefined,
|
|
14079
|
-
duration_ms: duration,
|
|
14080
|
-
resource_type: req.resourceType()
|
|
14081
|
-
});
|
|
14082
|
-
} catch {}
|
|
14083
|
-
};
|
|
14084
|
-
page.on("request", onRequest);
|
|
14085
|
-
page.on("response", onResponse);
|
|
14086
|
-
return () => {
|
|
14087
|
-
page.off("request", onRequest);
|
|
14088
|
-
page.off("response", onResponse);
|
|
14089
|
-
};
|
|
14090
|
-
}
|
|
14091
|
-
async function addInterceptRule(page, rule) {
|
|
14092
|
-
await page.route(rule.pattern, async (route) => {
|
|
14093
|
-
if (rule.action === "block") {
|
|
14094
|
-
await route.abort();
|
|
14095
|
-
} else if (rule.action === "modify" && rule.response) {
|
|
14096
|
-
await route.fulfill({
|
|
14097
|
-
status: rule.response.status,
|
|
14098
|
-
body: rule.response.body,
|
|
14099
|
-
headers: rule.response.headers
|
|
14100
|
-
});
|
|
14101
|
-
} else {
|
|
14102
|
-
await route.continue();
|
|
14103
|
-
}
|
|
14104
|
-
});
|
|
14105
|
-
}
|
|
14106
|
-
function startHAR(page) {
|
|
14107
|
-
const entries = [];
|
|
14108
|
-
const requestStart = new Map;
|
|
14109
|
-
const onRequest = (req) => {
|
|
14110
|
-
requestStart.set(req.url() + req.method(), {
|
|
14111
|
-
time: Date.now(),
|
|
14112
|
-
method: req.method(),
|
|
14113
|
-
headers: req.headers(),
|
|
14114
|
-
postData: req.postData() ?? undefined
|
|
14115
|
-
});
|
|
14116
|
-
};
|
|
14117
|
-
const onResponse = async (res) => {
|
|
14118
|
-
const key = res.url() + res.request().method();
|
|
14119
|
-
const start = requestStart.get(key);
|
|
14120
|
-
if (!start)
|
|
14121
|
-
return;
|
|
14122
|
-
const duration = Date.now() - start.time;
|
|
14123
|
-
const entry = {
|
|
14124
|
-
startedDateTime: new Date(start.time).toISOString(),
|
|
14125
|
-
time: duration,
|
|
14126
|
-
request: {
|
|
14127
|
-
method: start.method,
|
|
14128
|
-
url: res.url(),
|
|
14129
|
-
headers: Object.entries(start.headers).map(([name, value]) => ({ name, value })),
|
|
14130
|
-
postData: start.postData ? { text: start.postData } : undefined
|
|
14131
|
-
},
|
|
14132
|
-
response: {
|
|
14133
|
-
status: res.status(),
|
|
14134
|
-
statusText: res.statusText(),
|
|
14135
|
-
headers: Object.entries(res.headers()).map(([name, value]) => ({ name, value })),
|
|
14136
|
-
content: {
|
|
14137
|
-
size: parseInt(res.headers()["content-length"] ?? "0") || 0,
|
|
14138
|
-
mimeType: res.headers()["content-type"] ?? "application/octet-stream"
|
|
14139
|
-
}
|
|
14140
|
-
},
|
|
14141
|
-
timings: { send: 0, wait: duration, receive: 0 }
|
|
14142
|
-
};
|
|
14143
|
-
entries.push(entry);
|
|
14144
|
-
requestStart.delete(key);
|
|
14145
|
-
};
|
|
14146
|
-
page.on("request", onRequest);
|
|
14147
|
-
page.on("response", onResponse);
|
|
14148
|
-
return {
|
|
14149
|
-
entries,
|
|
14150
|
-
stop: () => {
|
|
14151
|
-
page.off("request", onRequest);
|
|
14152
|
-
page.off("response", onResponse);
|
|
14153
|
-
return {
|
|
14154
|
-
log: {
|
|
14155
|
-
version: "1.2",
|
|
14156
|
-
creator: { name: "@hasna/browser", version: "0.0.1" },
|
|
14157
|
-
entries
|
|
14158
|
-
}
|
|
14159
|
-
};
|
|
14160
|
-
}
|
|
14161
|
-
};
|
|
14162
|
-
}
|
|
14163
|
-
var init_network = __esm(() => {
|
|
14164
|
-
init_network_log();
|
|
14165
|
-
});
|
|
14166
|
-
|
|
14167
14695
|
// src/engines/cdp.ts
|
|
14168
14696
|
class CDPClient {
|
|
14169
14697
|
session;
|
|
@@ -14301,82 +14829,18 @@ async function getPerformanceMetrics(page) {
|
|
|
14301
14829
|
js_heap_size_used: cdpMetrics.js_heap_size_used,
|
|
14302
14830
|
js_heap_size_total: cdpMetrics.js_heap_size_total
|
|
14303
14831
|
};
|
|
14304
|
-
} catch {}
|
|
14305
|
-
return {
|
|
14306
|
-
fcp: paintEntries.fcp,
|
|
14307
|
-
ttfb: navTiming.ttfb,
|
|
14308
|
-
dom_interactive: navTiming.domInteractive,
|
|
14309
|
-
dom_complete: navTiming.domComplete,
|
|
14310
|
-
load_event: navTiming.loadEvent,
|
|
14311
|
-
...heapMetrics
|
|
14312
|
-
};
|
|
14313
|
-
}
|
|
14314
|
-
var init_performance = __esm(() => {
|
|
14315
|
-
init_cdp();
|
|
14316
|
-
});
|
|
14317
|
-
|
|
14318
|
-
// src/db/console-log.ts
|
|
14319
|
-
var exports_console_log = {};
|
|
14320
|
-
__export(exports_console_log, {
|
|
14321
|
-
logConsoleMessage: () => logConsoleMessage,
|
|
14322
|
-
getConsoleMessage: () => getConsoleMessage,
|
|
14323
|
-
getConsoleLog: () => getConsoleLog,
|
|
14324
|
-
clearConsoleLog: () => clearConsoleLog
|
|
14325
|
-
});
|
|
14326
|
-
import { randomUUID as randomUUID8 } from "crypto";
|
|
14327
|
-
function logConsoleMessage(data) {
|
|
14328
|
-
const db = getDatabase();
|
|
14329
|
-
const id = randomUUID8();
|
|
14330
|
-
db.prepare("INSERT INTO console_log (id, session_id, level, message, source, line_number) VALUES (?, ?, ?, ?, ?, ?)").run(id, data.session_id, data.level, data.message, data.source ?? null, data.line_number ?? null);
|
|
14331
|
-
return getConsoleMessage(id);
|
|
14332
|
-
}
|
|
14333
|
-
function getConsoleMessage(id) {
|
|
14334
|
-
const db = getDatabase();
|
|
14335
|
-
return db.query("SELECT * FROM console_log WHERE id = ?").get(id) ?? null;
|
|
14336
|
-
}
|
|
14337
|
-
function getConsoleLog(sessionId, level) {
|
|
14338
|
-
const db = getDatabase();
|
|
14339
|
-
if (level) {
|
|
14340
|
-
return db.query("SELECT * FROM console_log WHERE session_id = ? AND level = ? ORDER BY timestamp ASC").all(sessionId, level);
|
|
14341
|
-
}
|
|
14342
|
-
return db.query("SELECT * FROM console_log WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
|
|
14343
|
-
}
|
|
14344
|
-
function clearConsoleLog(sessionId) {
|
|
14345
|
-
const db = getDatabase();
|
|
14346
|
-
db.prepare("DELETE FROM console_log WHERE session_id = ?").run(sessionId);
|
|
14347
|
-
}
|
|
14348
|
-
var init_console_log = __esm(() => {
|
|
14349
|
-
init_schema();
|
|
14350
|
-
});
|
|
14351
|
-
|
|
14352
|
-
// src/lib/console.ts
|
|
14353
|
-
function enableConsoleCapture(page, sessionId) {
|
|
14354
|
-
const onConsole = (msg) => {
|
|
14355
|
-
const levelMap = {
|
|
14356
|
-
log: "log",
|
|
14357
|
-
warn: "warn",
|
|
14358
|
-
error: "error",
|
|
14359
|
-
debug: "debug",
|
|
14360
|
-
info: "info",
|
|
14361
|
-
warning: "warn"
|
|
14362
|
-
};
|
|
14363
|
-
const level = levelMap[msg.type()] ?? "log";
|
|
14364
|
-
const location = msg.location();
|
|
14365
|
-
try {
|
|
14366
|
-
logConsoleMessage({
|
|
14367
|
-
session_id: sessionId,
|
|
14368
|
-
level,
|
|
14369
|
-
message: msg.text(),
|
|
14370
|
-
source: location.url || undefined,
|
|
14371
|
-
line_number: location.lineNumber || undefined
|
|
14372
|
-
});
|
|
14373
|
-
} catch {}
|
|
14832
|
+
} catch {}
|
|
14833
|
+
return {
|
|
14834
|
+
fcp: paintEntries.fcp,
|
|
14835
|
+
ttfb: navTiming.ttfb,
|
|
14836
|
+
dom_interactive: navTiming.domInteractive,
|
|
14837
|
+
dom_complete: navTiming.domComplete,
|
|
14838
|
+
load_event: navTiming.loadEvent,
|
|
14839
|
+
...heapMetrics
|
|
14374
14840
|
};
|
|
14375
|
-
page.on("console", onConsole);
|
|
14376
|
-
return () => page.off("console", onConsole);
|
|
14377
14841
|
}
|
|
14378
|
-
var
|
|
14379
|
-
|
|
14842
|
+
var init_performance = __esm(() => {
|
|
14843
|
+
init_cdp();
|
|
14380
14844
|
});
|
|
14381
14845
|
|
|
14382
14846
|
// src/lib/storage.ts
|
|
@@ -14673,10 +15137,289 @@ async function persistFile(localPath, opts) {
|
|
|
14673
15137
|
}
|
|
14674
15138
|
var init_files_integration = () => {};
|
|
14675
15139
|
|
|
15140
|
+
// src/lib/tabs.ts
|
|
15141
|
+
async function newTab(page, url) {
|
|
15142
|
+
const context = page.context();
|
|
15143
|
+
const newPage = await context.newPage();
|
|
15144
|
+
if (url) {
|
|
15145
|
+
await newPage.goto(url, { waitUntil: "domcontentloaded" });
|
|
15146
|
+
}
|
|
15147
|
+
const pages = context.pages();
|
|
15148
|
+
const index = pages.indexOf(newPage);
|
|
15149
|
+
return {
|
|
15150
|
+
index,
|
|
15151
|
+
url: newPage.url(),
|
|
15152
|
+
title: await newPage.title(),
|
|
15153
|
+
is_active: true
|
|
15154
|
+
};
|
|
15155
|
+
}
|
|
15156
|
+
async function listTabs(page) {
|
|
15157
|
+
const context = page.context();
|
|
15158
|
+
const pages = context.pages();
|
|
15159
|
+
const activePage = page;
|
|
15160
|
+
const tabs = [];
|
|
15161
|
+
for (let i = 0;i < pages.length; i++) {
|
|
15162
|
+
let url = "";
|
|
15163
|
+
let title = "";
|
|
15164
|
+
try {
|
|
15165
|
+
url = pages[i].url();
|
|
15166
|
+
title = await pages[i].title();
|
|
15167
|
+
} catch {}
|
|
15168
|
+
tabs.push({
|
|
15169
|
+
index: i,
|
|
15170
|
+
url,
|
|
15171
|
+
title,
|
|
15172
|
+
is_active: pages[i] === activePage
|
|
15173
|
+
});
|
|
15174
|
+
}
|
|
15175
|
+
return tabs;
|
|
15176
|
+
}
|
|
15177
|
+
async function switchTab(page, index) {
|
|
15178
|
+
const context = page.context();
|
|
15179
|
+
const pages = context.pages();
|
|
15180
|
+
if (index < 0 || index >= pages.length) {
|
|
15181
|
+
throw new Error(`Tab index ${index} out of range (0-${pages.length - 1})`);
|
|
15182
|
+
}
|
|
15183
|
+
const targetPage = pages[index];
|
|
15184
|
+
await targetPage.bringToFront();
|
|
15185
|
+
return {
|
|
15186
|
+
page: targetPage,
|
|
15187
|
+
tab: {
|
|
15188
|
+
index,
|
|
15189
|
+
url: targetPage.url(),
|
|
15190
|
+
title: await targetPage.title(),
|
|
15191
|
+
is_active: true
|
|
15192
|
+
}
|
|
15193
|
+
};
|
|
15194
|
+
}
|
|
15195
|
+
async function closeTab(page, index) {
|
|
15196
|
+
const context = page.context();
|
|
15197
|
+
const pages = context.pages();
|
|
15198
|
+
if (index < 0 || index >= pages.length) {
|
|
15199
|
+
throw new Error(`Tab index ${index} out of range (0-${pages.length - 1})`);
|
|
15200
|
+
}
|
|
15201
|
+
if (pages.length <= 1) {
|
|
15202
|
+
throw new Error("Cannot close the last tab");
|
|
15203
|
+
}
|
|
15204
|
+
const targetPage = pages[index];
|
|
15205
|
+
const isActivePage = targetPage === page;
|
|
15206
|
+
await targetPage.close();
|
|
15207
|
+
const remainingPages = context.pages();
|
|
15208
|
+
const activeIndex = isActivePage ? Math.min(index, remainingPages.length - 1) : remainingPages.indexOf(page);
|
|
15209
|
+
const activePage = remainingPages[activeIndex >= 0 ? activeIndex : 0];
|
|
15210
|
+
return {
|
|
15211
|
+
closed_index: index,
|
|
15212
|
+
active_tab: {
|
|
15213
|
+
index: activeIndex >= 0 ? activeIndex : 0,
|
|
15214
|
+
url: activePage.url(),
|
|
15215
|
+
title: await activePage.title(),
|
|
15216
|
+
is_active: true
|
|
15217
|
+
}
|
|
15218
|
+
};
|
|
15219
|
+
}
|
|
15220
|
+
|
|
15221
|
+
// src/lib/profiles.ts
|
|
15222
|
+
import { mkdirSync as mkdirSync6, existsSync as existsSync3, readdirSync as readdirSync2, rmSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
15223
|
+
import { join as join6 } from "path";
|
|
15224
|
+
import { homedir as homedir6 } from "os";
|
|
15225
|
+
function getProfilesDir() {
|
|
15226
|
+
const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
|
|
15227
|
+
const dir = join6(dataDir, "profiles");
|
|
15228
|
+
mkdirSync6(dir, { recursive: true });
|
|
15229
|
+
return dir;
|
|
15230
|
+
}
|
|
15231
|
+
function getProfileDir(name) {
|
|
15232
|
+
return join6(getProfilesDir(), name);
|
|
15233
|
+
}
|
|
15234
|
+
async function saveProfile(page, name) {
|
|
15235
|
+
const dir = getProfileDir(name);
|
|
15236
|
+
mkdirSync6(dir, { recursive: true });
|
|
15237
|
+
const cookies = await page.context().cookies();
|
|
15238
|
+
writeFileSync2(join6(dir, "cookies.json"), JSON.stringify(cookies, null, 2));
|
|
15239
|
+
let localStorage2 = {};
|
|
15240
|
+
try {
|
|
15241
|
+
localStorage2 = await page.evaluate(() => {
|
|
15242
|
+
const result = {};
|
|
15243
|
+
for (let i = 0;i < window.localStorage.length; i++) {
|
|
15244
|
+
const key = window.localStorage.key(i);
|
|
15245
|
+
result[key] = window.localStorage.getItem(key);
|
|
15246
|
+
}
|
|
15247
|
+
return result;
|
|
15248
|
+
});
|
|
15249
|
+
} catch {}
|
|
15250
|
+
writeFileSync2(join6(dir, "storage.json"), JSON.stringify(localStorage2, null, 2));
|
|
15251
|
+
const savedAt = new Date().toISOString();
|
|
15252
|
+
const url = page.url();
|
|
15253
|
+
const meta = { saved_at: savedAt, url };
|
|
15254
|
+
writeFileSync2(join6(dir, "meta.json"), JSON.stringify(meta, null, 2));
|
|
15255
|
+
return {
|
|
15256
|
+
name,
|
|
15257
|
+
saved_at: savedAt,
|
|
15258
|
+
url,
|
|
15259
|
+
cookie_count: cookies.length,
|
|
15260
|
+
storage_key_count: Object.keys(localStorage2).length
|
|
15261
|
+
};
|
|
15262
|
+
}
|
|
15263
|
+
function loadProfile(name) {
|
|
15264
|
+
const dir = getProfileDir(name);
|
|
15265
|
+
if (!existsSync3(dir)) {
|
|
15266
|
+
throw new Error(`Profile not found: ${name}`);
|
|
15267
|
+
}
|
|
15268
|
+
const cookiesPath = join6(dir, "cookies.json");
|
|
15269
|
+
const storagePath = join6(dir, "storage.json");
|
|
15270
|
+
const metaPath2 = join6(dir, "meta.json");
|
|
15271
|
+
const cookies = existsSync3(cookiesPath) ? JSON.parse(readFileSync2(cookiesPath, "utf8")) : [];
|
|
15272
|
+
const localStorage2 = existsSync3(storagePath) ? JSON.parse(readFileSync2(storagePath, "utf8")) : {};
|
|
15273
|
+
let savedAt = new Date().toISOString();
|
|
15274
|
+
let url;
|
|
15275
|
+
if (existsSync3(metaPath2)) {
|
|
15276
|
+
const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
|
|
15277
|
+
savedAt = meta.saved_at ?? savedAt;
|
|
15278
|
+
url = meta.url;
|
|
15279
|
+
}
|
|
15280
|
+
return { cookies, localStorage: localStorage2, saved_at: savedAt, url };
|
|
15281
|
+
}
|
|
15282
|
+
async function applyProfile(page, profileData) {
|
|
15283
|
+
if (profileData.cookies.length > 0) {
|
|
15284
|
+
await page.context().addCookies(profileData.cookies);
|
|
15285
|
+
}
|
|
15286
|
+
const storageKeys = Object.keys(profileData.localStorage);
|
|
15287
|
+
if (storageKeys.length > 0) {
|
|
15288
|
+
try {
|
|
15289
|
+
await page.evaluate((storage) => {
|
|
15290
|
+
for (const [key, value] of Object.entries(storage)) {
|
|
15291
|
+
window.localStorage.setItem(key, value);
|
|
15292
|
+
}
|
|
15293
|
+
}, profileData.localStorage);
|
|
15294
|
+
} catch {}
|
|
15295
|
+
}
|
|
15296
|
+
return {
|
|
15297
|
+
cookies_applied: profileData.cookies.length,
|
|
15298
|
+
storage_keys_applied: storageKeys.length
|
|
15299
|
+
};
|
|
15300
|
+
}
|
|
15301
|
+
function listProfiles() {
|
|
15302
|
+
const dir = getProfilesDir();
|
|
15303
|
+
if (!existsSync3(dir))
|
|
15304
|
+
return [];
|
|
15305
|
+
const entries = readdirSync2(dir, { withFileTypes: true });
|
|
15306
|
+
const profiles = [];
|
|
15307
|
+
for (const entry of entries) {
|
|
15308
|
+
if (!entry.isDirectory())
|
|
15309
|
+
continue;
|
|
15310
|
+
const name = entry.name;
|
|
15311
|
+
const profileDir = join6(dir, name);
|
|
15312
|
+
let savedAt = "";
|
|
15313
|
+
let url;
|
|
15314
|
+
let cookieCount = 0;
|
|
15315
|
+
let storageKeyCount = 0;
|
|
15316
|
+
try {
|
|
15317
|
+
const metaPath2 = join6(profileDir, "meta.json");
|
|
15318
|
+
if (existsSync3(metaPath2)) {
|
|
15319
|
+
const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
|
|
15320
|
+
savedAt = meta.saved_at ?? "";
|
|
15321
|
+
url = meta.url;
|
|
15322
|
+
}
|
|
15323
|
+
const cookiesPath = join6(profileDir, "cookies.json");
|
|
15324
|
+
if (existsSync3(cookiesPath)) {
|
|
15325
|
+
const cookies = JSON.parse(readFileSync2(cookiesPath, "utf8"));
|
|
15326
|
+
cookieCount = Array.isArray(cookies) ? cookies.length : 0;
|
|
15327
|
+
}
|
|
15328
|
+
const storagePath = join6(profileDir, "storage.json");
|
|
15329
|
+
if (existsSync3(storagePath)) {
|
|
15330
|
+
const storage = JSON.parse(readFileSync2(storagePath, "utf8"));
|
|
15331
|
+
storageKeyCount = Object.keys(storage).length;
|
|
15332
|
+
}
|
|
15333
|
+
} catch {}
|
|
15334
|
+
profiles.push({
|
|
15335
|
+
name,
|
|
15336
|
+
saved_at: savedAt,
|
|
15337
|
+
url,
|
|
15338
|
+
cookie_count: cookieCount,
|
|
15339
|
+
storage_key_count: storageKeyCount
|
|
15340
|
+
});
|
|
15341
|
+
}
|
|
15342
|
+
return profiles.sort((a, b) => b.saved_at.localeCompare(a.saved_at));
|
|
15343
|
+
}
|
|
15344
|
+
function deleteProfile(name) {
|
|
15345
|
+
const dir = getProfileDir(name);
|
|
15346
|
+
if (!existsSync3(dir))
|
|
15347
|
+
return false;
|
|
15348
|
+
try {
|
|
15349
|
+
rmSync(dir, { recursive: true, force: true });
|
|
15350
|
+
return true;
|
|
15351
|
+
} catch {
|
|
15352
|
+
return false;
|
|
15353
|
+
}
|
|
15354
|
+
}
|
|
15355
|
+
var init_profiles = () => {};
|
|
15356
|
+
|
|
15357
|
+
// src/lib/annotate.ts
|
|
15358
|
+
var exports_annotate = {};
|
|
15359
|
+
__export(exports_annotate, {
|
|
15360
|
+
annotateScreenshot: () => annotateScreenshot
|
|
15361
|
+
});
|
|
15362
|
+
async function annotateScreenshot(page, sessionId) {
|
|
15363
|
+
const snapshot = await takeSnapshot(page, sessionId);
|
|
15364
|
+
const rawBuffer = await page.screenshot({ type: "png" });
|
|
15365
|
+
const meta = await import_sharp3.default(rawBuffer).metadata();
|
|
15366
|
+
const imgWidth = meta.width ?? 1280;
|
|
15367
|
+
const imgHeight = meta.height ?? 720;
|
|
15368
|
+
const annotations = [];
|
|
15369
|
+
const labelToRef = {};
|
|
15370
|
+
let labelCounter = 1;
|
|
15371
|
+
const refsToAnnotate = Object.entries(snapshot.refs).slice(0, MAX_ANNOTATIONS);
|
|
15372
|
+
for (const [ref, info] of refsToAnnotate) {
|
|
15373
|
+
try {
|
|
15374
|
+
const locator = page.getByRole(info.role, { name: info.name }).first();
|
|
15375
|
+
const box = await locator.boundingBox();
|
|
15376
|
+
if (!box)
|
|
15377
|
+
continue;
|
|
15378
|
+
const annotation = {
|
|
15379
|
+
ref,
|
|
15380
|
+
label: labelCounter,
|
|
15381
|
+
x: Math.round(box.x),
|
|
15382
|
+
y: Math.round(box.y),
|
|
15383
|
+
width: Math.round(box.width),
|
|
15384
|
+
height: Math.round(box.height),
|
|
15385
|
+
role: info.role,
|
|
15386
|
+
name: info.name
|
|
15387
|
+
};
|
|
15388
|
+
annotations.push(annotation);
|
|
15389
|
+
labelToRef[labelCounter] = ref;
|
|
15390
|
+
labelCounter++;
|
|
15391
|
+
} catch {}
|
|
15392
|
+
}
|
|
15393
|
+
const circleR = 10;
|
|
15394
|
+
const fontSize = 12;
|
|
15395
|
+
const svgParts = [];
|
|
15396
|
+
for (const ann of annotations) {
|
|
15397
|
+
const cx = Math.min(Math.max(ann.x + circleR, circleR), imgWidth - circleR);
|
|
15398
|
+
const cy = Math.min(Math.max(ann.y - circleR - 2, circleR), imgHeight - circleR);
|
|
15399
|
+
svgParts.push(`
|
|
15400
|
+
<circle cx="${cx}" cy="${cy}" r="${circleR}" fill="#e11d48" stroke="white" stroke-width="1.5"/>
|
|
15401
|
+
<text x="${cx}" y="${cy + 4}" text-anchor="middle" fill="white" font-size="${fontSize}" font-family="Arial,sans-serif" font-weight="bold">${ann.label}</text>
|
|
15402
|
+
`);
|
|
15403
|
+
svgParts.push(`
|
|
15404
|
+
<rect x="${ann.x}" y="${ann.y}" width="${ann.width}" height="${ann.height}" fill="none" stroke="#e11d48" stroke-width="1.5" stroke-opacity="0.6" rx="2"/>
|
|
15405
|
+
`);
|
|
15406
|
+
}
|
|
15407
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${imgWidth}" height="${imgHeight}">${svgParts.join("")}</svg>`;
|
|
15408
|
+
const annotatedBuffer = await import_sharp3.default(rawBuffer).composite([{ input: Buffer.from(svg), top: 0, left: 0 }]).webp({ quality: 85 }).toBuffer();
|
|
15409
|
+
return { buffer: annotatedBuffer, annotations, labelToRef };
|
|
15410
|
+
}
|
|
15411
|
+
var import_sharp3, MAX_ANNOTATIONS = 40;
|
|
15412
|
+
var init_annotate = __esm(() => {
|
|
15413
|
+
init_snapshot();
|
|
15414
|
+
import_sharp3 = __toESM(require_lib(), 1);
|
|
15415
|
+
});
|
|
15416
|
+
|
|
14676
15417
|
// src/mcp/index.ts
|
|
14677
15418
|
var exports_mcp = {};
|
|
14678
15419
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14679
15420
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15421
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
15422
|
+
import { join as join7 } from "path";
|
|
14680
15423
|
function json(data) {
|
|
14681
15424
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
14682
15425
|
}
|
|
@@ -14688,7 +15431,7 @@ function err(e) {
|
|
|
14688
15431
|
isError: true
|
|
14689
15432
|
};
|
|
14690
15433
|
}
|
|
14691
|
-
var networkLogCleanup, consoleCaptureCleanup, harCaptures, server, activeWatchHandles, transport;
|
|
15434
|
+
var _pkg, networkLogCleanup, consoleCaptureCleanup, harCaptures, server, activeWatchHandles, _startupToolCount, transport;
|
|
14692
15435
|
var init_mcp = __esm(async () => {
|
|
14693
15436
|
init_zod();
|
|
14694
15437
|
init_session();
|
|
@@ -14706,9 +15449,13 @@ var init_mcp = __esm(async () => {
|
|
|
14706
15449
|
init_gallery();
|
|
14707
15450
|
init_downloads();
|
|
14708
15451
|
init_gallery_diff();
|
|
15452
|
+
init_snapshot();
|
|
14709
15453
|
init_files_integration();
|
|
14710
15454
|
init_recordings();
|
|
15455
|
+
init_dialogs();
|
|
15456
|
+
init_profiles();
|
|
14711
15457
|
init_types();
|
|
15458
|
+
_pkg = JSON.parse(readFileSync3(join7(import.meta.dir, "../../package.json"), "utf8"));
|
|
14712
15459
|
networkLogCleanup = new Map;
|
|
14713
15460
|
consoleCaptureCleanup = new Map;
|
|
14714
15461
|
harCaptures = new Map;
|
|
@@ -14724,8 +15471,9 @@ var init_mcp = __esm(async () => {
|
|
|
14724
15471
|
start_url: exports_external.string().optional(),
|
|
14725
15472
|
headless: exports_external.boolean().optional().default(true),
|
|
14726
15473
|
viewport_width: exports_external.number().optional().default(1280),
|
|
14727
|
-
viewport_height: exports_external.number().optional().default(720)
|
|
14728
|
-
|
|
15474
|
+
viewport_height: exports_external.number().optional().default(720),
|
|
15475
|
+
stealth: exports_external.boolean().optional().default(false)
|
|
15476
|
+
}, async ({ engine, use_case, project_id, agent_id, start_url, headless, viewport_width, viewport_height, stealth }) => {
|
|
14729
15477
|
try {
|
|
14730
15478
|
const { session } = await createSession2({
|
|
14731
15479
|
engine,
|
|
@@ -14734,7 +15482,8 @@ var init_mcp = __esm(async () => {
|
|
|
14734
15482
|
agentId: agent_id,
|
|
14735
15483
|
startUrl: start_url,
|
|
14736
15484
|
headless,
|
|
14737
|
-
viewport: { width: viewport_width, height: viewport_height }
|
|
15485
|
+
viewport: { width: viewport_width, height: viewport_height },
|
|
15486
|
+
stealth
|
|
14738
15487
|
});
|
|
14739
15488
|
return json({ session });
|
|
14740
15489
|
} catch (e) {
|
|
@@ -14761,11 +15510,67 @@ var init_mcp = __esm(async () => {
|
|
|
14761
15510
|
return err(e);
|
|
14762
15511
|
}
|
|
14763
15512
|
});
|
|
14764
|
-
server.tool("browser_navigate", "Navigate to a URL
|
|
15513
|
+
server.tool("browser_navigate", "Navigate to a URL. Auto-detects redirects, auto-names session, returns compact refs + thumbnail.", {
|
|
15514
|
+
session_id: exports_external.string(),
|
|
15515
|
+
url: exports_external.string(),
|
|
15516
|
+
timeout: exports_external.number().optional().default(30000),
|
|
15517
|
+
auto_snapshot: exports_external.boolean().optional().default(true),
|
|
15518
|
+
auto_thumbnail: exports_external.boolean().optional().default(true)
|
|
15519
|
+
}, async ({ session_id, url, timeout, auto_snapshot, auto_thumbnail }) => {
|
|
14765
15520
|
try {
|
|
14766
15521
|
const page = getSessionPage(session_id);
|
|
14767
15522
|
await navigate(page, url, timeout);
|
|
14768
|
-
|
|
15523
|
+
const title = await getTitle(page);
|
|
15524
|
+
const current_url = await getUrl(page);
|
|
15525
|
+
const redirected = current_url !== url && current_url !== url + "/" && url !== current_url.replace(/\/$/, "");
|
|
15526
|
+
let redirect_type;
|
|
15527
|
+
if (redirected) {
|
|
15528
|
+
try {
|
|
15529
|
+
const reqHost = new URL(url).hostname;
|
|
15530
|
+
const resHost = new URL(current_url).hostname;
|
|
15531
|
+
const reqPath = new URL(url).pathname;
|
|
15532
|
+
const resPath = new URL(current_url).pathname;
|
|
15533
|
+
if (reqHost !== resHost)
|
|
15534
|
+
redirect_type = "canonical";
|
|
15535
|
+
else if (resPath.match(/\/[a-z]{2}-[a-z]{2}\//))
|
|
15536
|
+
redirect_type = "geo";
|
|
15537
|
+
else if (current_url.includes("login") || current_url.includes("signin"))
|
|
15538
|
+
redirect_type = "auth";
|
|
15539
|
+
else
|
|
15540
|
+
redirect_type = "unknown";
|
|
15541
|
+
} catch {}
|
|
15542
|
+
}
|
|
15543
|
+
try {
|
|
15544
|
+
const session = getSession2(session_id);
|
|
15545
|
+
if (!session.name) {
|
|
15546
|
+
const hostname = new URL(current_url).hostname;
|
|
15547
|
+
renameSession2(session_id, hostname);
|
|
15548
|
+
}
|
|
15549
|
+
} catch {}
|
|
15550
|
+
const result = {
|
|
15551
|
+
url,
|
|
15552
|
+
title,
|
|
15553
|
+
current_url,
|
|
15554
|
+
redirected,
|
|
15555
|
+
...redirect_type ? { redirect_type } : {}
|
|
15556
|
+
};
|
|
15557
|
+
if (auto_thumbnail) {
|
|
15558
|
+
try {
|
|
15559
|
+
const ss = await takeScreenshot(page, { maxWidth: 400, quality: 60, track: false, thumbnail: false });
|
|
15560
|
+
result.thumbnail_base64 = ss.base64.length > 50000 ? "" : ss.base64;
|
|
15561
|
+
} catch {}
|
|
15562
|
+
}
|
|
15563
|
+
if (auto_snapshot) {
|
|
15564
|
+
try {
|
|
15565
|
+
const snap = await takeSnapshot(page, session_id);
|
|
15566
|
+
setLastSnapshot(session_id, snap);
|
|
15567
|
+
const refEntries = Object.entries(snap.refs).slice(0, 30);
|
|
15568
|
+
result.snapshot_refs = refEntries.map(([ref, info]) => `${info.role}:${info.name.slice(0, 50)} [${ref}]`).join(", ");
|
|
15569
|
+
result.interactive_count = snap.interactive_count;
|
|
15570
|
+
result.has_errors = getConsoleLog(session_id, "error").length > 0;
|
|
15571
|
+
} catch {}
|
|
15572
|
+
}
|
|
15573
|
+
return json(result);
|
|
14769
15574
|
} catch (e) {
|
|
14770
15575
|
return err(e);
|
|
14771
15576
|
}
|
|
@@ -14797,29 +15602,47 @@ var init_mcp = __esm(async () => {
|
|
|
14797
15602
|
return err(e);
|
|
14798
15603
|
}
|
|
14799
15604
|
});
|
|
14800
|
-
server.tool("browser_click", "Click an element
|
|
15605
|
+
server.tool("browser_click", "Click an element by ref (from snapshot) or CSS selector. Prefer ref for reliability.", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), button: exports_external.enum(["left", "right", "middle"]).optional(), timeout: exports_external.number().optional() }, async ({ session_id, selector, ref, button, timeout }) => {
|
|
14801
15606
|
try {
|
|
14802
15607
|
const page = getSessionPage(session_id);
|
|
15608
|
+
if (ref) {
|
|
15609
|
+
await clickRef(page, session_id, ref, { timeout });
|
|
15610
|
+
return json({ clicked: ref, method: "ref" });
|
|
15611
|
+
}
|
|
15612
|
+
if (!selector)
|
|
15613
|
+
return err(new Error("Either ref or selector is required"));
|
|
14803
15614
|
await click(page, selector, { button, timeout });
|
|
14804
|
-
return json({ clicked: selector });
|
|
15615
|
+
return json({ clicked: selector, method: "selector" });
|
|
14805
15616
|
} catch (e) {
|
|
14806
15617
|
return err(e);
|
|
14807
15618
|
}
|
|
14808
15619
|
});
|
|
14809
|
-
server.tool("browser_type", "Type text into an element", { session_id: exports_external.string(), selector: exports_external.string(), text: exports_external.string(), clear: exports_external.boolean().optional().default(false), delay: exports_external.number().optional() }, async ({ session_id, selector, text, clear, delay }) => {
|
|
15620
|
+
server.tool("browser_type", "Type text into an element by ref or selector. Prefer ref.", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), text: exports_external.string(), clear: exports_external.boolean().optional().default(false), delay: exports_external.number().optional() }, async ({ session_id, selector, ref, text, clear, delay }) => {
|
|
14810
15621
|
try {
|
|
14811
15622
|
const page = getSessionPage(session_id);
|
|
15623
|
+
if (ref) {
|
|
15624
|
+
await typeRef(page, session_id, ref, text, { clear, delay });
|
|
15625
|
+
return json({ typed: text, ref, method: "ref" });
|
|
15626
|
+
}
|
|
15627
|
+
if (!selector)
|
|
15628
|
+
return err(new Error("Either ref or selector is required"));
|
|
14812
15629
|
await type(page, selector, text, { clear, delay });
|
|
14813
|
-
return json({ typed: text, selector });
|
|
15630
|
+
return json({ typed: text, selector, method: "selector" });
|
|
14814
15631
|
} catch (e) {
|
|
14815
15632
|
return err(e);
|
|
14816
15633
|
}
|
|
14817
15634
|
});
|
|
14818
|
-
server.tool("browser_hover", "Hover over an element", { session_id: exports_external.string(), selector: exports_external.string() }, async ({ session_id, selector }) => {
|
|
15635
|
+
server.tool("browser_hover", "Hover over an element by ref or selector", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional() }, async ({ session_id, selector, ref }) => {
|
|
14819
15636
|
try {
|
|
14820
15637
|
const page = getSessionPage(session_id);
|
|
15638
|
+
if (ref) {
|
|
15639
|
+
await hoverRef(page, session_id, ref);
|
|
15640
|
+
return json({ hovered: ref, method: "ref" });
|
|
15641
|
+
}
|
|
15642
|
+
if (!selector)
|
|
15643
|
+
return err(new Error("Either ref or selector is required"));
|
|
14821
15644
|
await hover(page, selector);
|
|
14822
|
-
return json({ hovered: selector });
|
|
15645
|
+
return json({ hovered: selector, method: "selector" });
|
|
14823
15646
|
} catch (e) {
|
|
14824
15647
|
return err(e);
|
|
14825
15648
|
}
|
|
@@ -14833,20 +15656,32 @@ var init_mcp = __esm(async () => {
|
|
|
14833
15656
|
return err(e);
|
|
14834
15657
|
}
|
|
14835
15658
|
});
|
|
14836
|
-
server.tool("browser_select", "Select a dropdown option", { session_id: exports_external.string(), selector: exports_external.string(), value: exports_external.string() }, async ({ session_id, selector, value }) => {
|
|
15659
|
+
server.tool("browser_select", "Select a dropdown option by ref or selector", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), value: exports_external.string() }, async ({ session_id, selector, ref, value }) => {
|
|
14837
15660
|
try {
|
|
14838
15661
|
const page = getSessionPage(session_id);
|
|
15662
|
+
if (ref) {
|
|
15663
|
+
const selected2 = await selectRef(page, session_id, ref, value);
|
|
15664
|
+
return json({ selected: selected2, method: "ref" });
|
|
15665
|
+
}
|
|
15666
|
+
if (!selector)
|
|
15667
|
+
return err(new Error("Either ref or selector is required"));
|
|
14839
15668
|
const selected = await selectOption(page, selector, value);
|
|
14840
|
-
return json({ selected });
|
|
15669
|
+
return json({ selected, method: "selector" });
|
|
14841
15670
|
} catch (e) {
|
|
14842
15671
|
return err(e);
|
|
14843
15672
|
}
|
|
14844
15673
|
});
|
|
14845
|
-
server.tool("
|
|
15674
|
+
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 }) => {
|
|
14846
15675
|
try {
|
|
14847
15676
|
const page = getSessionPage(session_id);
|
|
15677
|
+
if (ref) {
|
|
15678
|
+
await checkRef(page, session_id, ref, checked);
|
|
15679
|
+
return json({ checked, ref, method: "ref" });
|
|
15680
|
+
}
|
|
15681
|
+
if (!selector)
|
|
15682
|
+
return err(new Error("Either ref or selector is required"));
|
|
14848
15683
|
await checkBox(page, selector, checked);
|
|
14849
|
-
return json({ checked, selector });
|
|
15684
|
+
return json({ checked, selector, method: "selector" });
|
|
14850
15685
|
} catch (e) {
|
|
14851
15686
|
return err(e);
|
|
14852
15687
|
}
|
|
@@ -14927,15 +15762,38 @@ var init_mcp = __esm(async () => {
|
|
|
14927
15762
|
return err(e);
|
|
14928
15763
|
}
|
|
14929
15764
|
});
|
|
14930
|
-
server.tool("browser_snapshot", "Get
|
|
15765
|
+
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.", {
|
|
15766
|
+
session_id: exports_external.string(),
|
|
15767
|
+
compact: exports_external.boolean().optional().default(true),
|
|
15768
|
+
max_refs: exports_external.number().optional().default(50),
|
|
15769
|
+
full_tree: exports_external.boolean().optional().default(false)
|
|
15770
|
+
}, async ({ session_id, compact, max_refs, full_tree }) => {
|
|
14931
15771
|
try {
|
|
14932
15772
|
const page = getSessionPage(session_id);
|
|
14933
|
-
|
|
15773
|
+
const result = await takeSnapshot(page, session_id);
|
|
15774
|
+
setLastSnapshot(session_id, result);
|
|
15775
|
+
const refEntries = Object.entries(result.refs).slice(0, max_refs);
|
|
15776
|
+
const limitedRefs = Object.fromEntries(refEntries);
|
|
15777
|
+
const truncated = Object.keys(result.refs).length > max_refs;
|
|
15778
|
+
if (compact && !full_tree) {
|
|
15779
|
+
const compactRefs = refEntries.map(([ref, info]) => `${info.role}:${info.name.slice(0, 60)} [${ref}]${info.checked !== undefined ? ` checked=${info.checked}` : ""}${!info.enabled ? " disabled" : ""}`).join(`
|
|
15780
|
+
`);
|
|
15781
|
+
return json({
|
|
15782
|
+
snapshot_compact: compactRefs,
|
|
15783
|
+
interactive_count: result.interactive_count,
|
|
15784
|
+
shown_count: refEntries.length,
|
|
15785
|
+
truncated,
|
|
15786
|
+
refs: limitedRefs
|
|
15787
|
+
});
|
|
15788
|
+
}
|
|
15789
|
+
const tree = full_tree ? result.tree : result.tree.slice(0, 5000) + (result.tree.length > 5000 ? `
|
|
15790
|
+
... (truncated \u2014 use full_tree=true for complete)` : "");
|
|
15791
|
+
return json({ snapshot: tree, refs: limitedRefs, interactive_count: result.interactive_count, truncated });
|
|
14934
15792
|
} catch (e) {
|
|
14935
15793
|
return err(e);
|
|
14936
15794
|
}
|
|
14937
15795
|
});
|
|
14938
|
-
server.tool("browser_screenshot", "Take a screenshot
|
|
15796
|
+
server.tool("browser_screenshot", "Take a screenshot. Use annotate=true to overlay numbered labels on interactive elements for visual+ref workflows.", {
|
|
14939
15797
|
session_id: exports_external.string(),
|
|
14940
15798
|
selector: exports_external.string().optional(),
|
|
14941
15799
|
full_page: exports_external.boolean().optional().default(false),
|
|
@@ -14943,17 +15801,37 @@ var init_mcp = __esm(async () => {
|
|
|
14943
15801
|
quality: exports_external.number().optional(),
|
|
14944
15802
|
max_width: exports_external.number().optional().default(1280),
|
|
14945
15803
|
compress: exports_external.boolean().optional().default(true),
|
|
14946
|
-
thumbnail: exports_external.boolean().optional().default(true)
|
|
14947
|
-
|
|
15804
|
+
thumbnail: exports_external.boolean().optional().default(true),
|
|
15805
|
+
annotate: exports_external.boolean().optional().default(false)
|
|
15806
|
+
}, async ({ session_id, selector, full_page, format, quality, max_width, compress, thumbnail, annotate }) => {
|
|
14948
15807
|
try {
|
|
14949
15808
|
const page = getSessionPage(session_id);
|
|
15809
|
+
if (annotate && !selector && !full_page) {
|
|
15810
|
+
const { annotateScreenshot: annotateScreenshot2 } = await Promise.resolve().then(() => (init_annotate(), exports_annotate));
|
|
15811
|
+
const annotated = await annotateScreenshot2(page, session_id);
|
|
15812
|
+
const base64 = annotated.buffer.toString("base64");
|
|
15813
|
+
return json({
|
|
15814
|
+
base64: base64.length > 50000 ? undefined : base64,
|
|
15815
|
+
base64_truncated: base64.length > 50000,
|
|
15816
|
+
size_bytes: annotated.buffer.length,
|
|
15817
|
+
annotations: annotated.annotations,
|
|
15818
|
+
label_to_ref: annotated.labelToRef,
|
|
15819
|
+
annotation_count: annotated.annotations.length
|
|
15820
|
+
});
|
|
15821
|
+
}
|
|
14950
15822
|
const result = await takeScreenshot(page, { selector, fullPage: full_page, format, quality, maxWidth: max_width, compress, thumbnail });
|
|
15823
|
+
result.url = page.url();
|
|
14951
15824
|
try {
|
|
14952
15825
|
const buf = Buffer.from(result.base64, "base64");
|
|
14953
15826
|
const filename = result.path.split("/").pop() ?? `screenshot.${format ?? "webp"}`;
|
|
14954
15827
|
const dl = saveToDownloads(buf, filename, { sessionId: session_id, type: "screenshot", sourceUrl: page.url() });
|
|
14955
15828
|
result.download_id = dl.id;
|
|
14956
15829
|
} catch {}
|
|
15830
|
+
if (result.base64.length > 50000) {
|
|
15831
|
+
result.base64_truncated = true;
|
|
15832
|
+
result.full_image_path = result.path;
|
|
15833
|
+
result.base64 = result.thumbnail_base64 ?? "";
|
|
15834
|
+
}
|
|
14957
15835
|
return json(result);
|
|
14958
15836
|
} catch (e) {
|
|
14959
15837
|
return err(e);
|
|
@@ -15248,6 +16126,37 @@ var init_mcp = __esm(async () => {
|
|
|
15248
16126
|
return err(e);
|
|
15249
16127
|
}
|
|
15250
16128
|
});
|
|
16129
|
+
server.tool("browser_scroll_and_screenshot", "Scroll the page and take a screenshot in one call. Saves 3 separate tool calls.", { session_id: exports_external.string(), direction: exports_external.enum(["up", "down", "left", "right"]).optional().default("down"), amount: exports_external.number().optional().default(500), wait_ms: exports_external.number().optional().default(300) }, async ({ session_id, direction, amount, wait_ms }) => {
|
|
16130
|
+
try {
|
|
16131
|
+
const page = getSessionPage(session_id);
|
|
16132
|
+
await scroll(page, direction, amount);
|
|
16133
|
+
await new Promise((r) => setTimeout(r, wait_ms));
|
|
16134
|
+
const result = await takeScreenshot(page, { maxWidth: 1280, track: true });
|
|
16135
|
+
result.url = page.url();
|
|
16136
|
+
if (result.base64.length > 50000) {
|
|
16137
|
+
result.base64_truncated = true;
|
|
16138
|
+
result.full_image_path = result.path;
|
|
16139
|
+
result.base64 = result.thumbnail_base64 ?? "";
|
|
16140
|
+
}
|
|
16141
|
+
return json({ scrolled: { direction, amount }, screenshot: result });
|
|
16142
|
+
} catch (e) {
|
|
16143
|
+
return err(e);
|
|
16144
|
+
}
|
|
16145
|
+
});
|
|
16146
|
+
server.tool("browser_wait_for_navigation", "Wait for URL change after a click or action. Returns the new URL and title.", { session_id: exports_external.string(), timeout: exports_external.number().optional().default(30000), url_pattern: exports_external.string().optional() }, async ({ session_id, timeout, url_pattern }) => {
|
|
16147
|
+
try {
|
|
16148
|
+
const page = getSessionPage(session_id);
|
|
16149
|
+
const start = Date.now();
|
|
16150
|
+
if (url_pattern) {
|
|
16151
|
+
await page.waitForURL(url_pattern, { timeout });
|
|
16152
|
+
} else {
|
|
16153
|
+
await page.waitForLoadState("domcontentloaded", { timeout });
|
|
16154
|
+
}
|
|
16155
|
+
return json({ url: page.url(), title: await getTitle(page), elapsed_ms: Date.now() - start });
|
|
16156
|
+
} catch (e) {
|
|
16157
|
+
return err(e);
|
|
16158
|
+
}
|
|
16159
|
+
});
|
|
15251
16160
|
server.tool("browser_session_get_by_name", "Get a session by its name", { name: exports_external.string() }, async ({ name }) => {
|
|
15252
16161
|
try {
|
|
15253
16162
|
const session = getSessionByName2(name);
|
|
@@ -15509,6 +16418,381 @@ var init_mcp = __esm(async () => {
|
|
|
15509
16418
|
return err(e);
|
|
15510
16419
|
}
|
|
15511
16420
|
});
|
|
16421
|
+
server.tool("browser_snapshot_diff", "Take a new accessibility snapshot and diff it against the last snapshot for this session. Shows added/removed/modified interactive elements.", { session_id: exports_external.string() }, async ({ session_id }) => {
|
|
16422
|
+
try {
|
|
16423
|
+
const page = getSessionPage(session_id);
|
|
16424
|
+
const before = getLastSnapshot(session_id);
|
|
16425
|
+
const after = await takeSnapshot(page, session_id);
|
|
16426
|
+
setLastSnapshot(session_id, after);
|
|
16427
|
+
if (!before) {
|
|
16428
|
+
return json({
|
|
16429
|
+
message: "No previous snapshot \u2014 returning current snapshot only.",
|
|
16430
|
+
snapshot: after.tree,
|
|
16431
|
+
refs: after.refs,
|
|
16432
|
+
interactive_count: after.interactive_count
|
|
16433
|
+
});
|
|
16434
|
+
}
|
|
16435
|
+
const diff = diffSnapshots(before, after);
|
|
16436
|
+
return json({
|
|
16437
|
+
diff,
|
|
16438
|
+
added_count: diff.added.length,
|
|
16439
|
+
removed_count: diff.removed.length,
|
|
16440
|
+
modified_count: diff.modified.length,
|
|
16441
|
+
url_changed: diff.url_changed,
|
|
16442
|
+
title_changed: diff.title_changed,
|
|
16443
|
+
current_interactive_count: after.interactive_count
|
|
16444
|
+
});
|
|
16445
|
+
} catch (e) {
|
|
16446
|
+
return err(e);
|
|
16447
|
+
}
|
|
16448
|
+
});
|
|
16449
|
+
server.tool("browser_session_stats", "Get session info and estimated token usage (based on network log, console log, and gallery entry sizes).", { session_id: exports_external.string() }, async ({ session_id }) => {
|
|
16450
|
+
try {
|
|
16451
|
+
const session = getSession2(session_id);
|
|
16452
|
+
const networkLog = getNetworkLog(session_id);
|
|
16453
|
+
const consoleLog = getConsoleLog(session_id);
|
|
16454
|
+
const galleryEntries = listEntries({ sessionId: session_id, limit: 1000 });
|
|
16455
|
+
let totalChars = 0;
|
|
16456
|
+
for (const req of networkLog) {
|
|
16457
|
+
totalChars += (req.url?.length ?? 0) + (req.request_headers?.length ?? 0) + (req.response_headers?.length ?? 0) + (req.request_body?.length ?? 0);
|
|
16458
|
+
}
|
|
16459
|
+
for (const msg of consoleLog) {
|
|
16460
|
+
totalChars += (msg.message?.length ?? 0) + (msg.source?.length ?? 0);
|
|
16461
|
+
}
|
|
16462
|
+
for (const entry of galleryEntries) {
|
|
16463
|
+
totalChars += (entry.url?.length ?? 0) + (entry.title?.length ?? 0) + (entry.notes?.length ?? 0) + (entry.tags?.join(",").length ?? 0);
|
|
16464
|
+
}
|
|
16465
|
+
const estimatedTokens = Math.ceil(totalChars / 4);
|
|
16466
|
+
const tokenBudget = getTokenBudget(session_id);
|
|
16467
|
+
return json({
|
|
16468
|
+
session,
|
|
16469
|
+
network_request_count: networkLog.length,
|
|
16470
|
+
console_message_count: consoleLog.length,
|
|
16471
|
+
gallery_entry_count: galleryEntries.length,
|
|
16472
|
+
estimated_tokens_used: estimatedTokens,
|
|
16473
|
+
token_budget: tokenBudget,
|
|
16474
|
+
data_size_chars: totalChars
|
|
16475
|
+
});
|
|
16476
|
+
} catch (e) {
|
|
16477
|
+
return err(e);
|
|
16478
|
+
}
|
|
16479
|
+
});
|
|
16480
|
+
server.tool("browser_tab_new", "Open a new tab in the session's browser context, optionally navigating to a URL", { session_id: exports_external.string(), url: exports_external.string().optional() }, async ({ session_id, url }) => {
|
|
16481
|
+
try {
|
|
16482
|
+
const page = getSessionPage(session_id);
|
|
16483
|
+
const tab = await newTab(page, url);
|
|
16484
|
+
return json(tab);
|
|
16485
|
+
} catch (e) {
|
|
16486
|
+
return err(e);
|
|
16487
|
+
}
|
|
16488
|
+
});
|
|
16489
|
+
server.tool("browser_tab_list", "List all open tabs in the session's browser context", { session_id: exports_external.string() }, async ({ session_id }) => {
|
|
16490
|
+
try {
|
|
16491
|
+
const page = getSessionPage(session_id);
|
|
16492
|
+
const tabs = await listTabs(page);
|
|
16493
|
+
return json({ tabs, count: tabs.length });
|
|
16494
|
+
} catch (e) {
|
|
16495
|
+
return err(e);
|
|
16496
|
+
}
|
|
16497
|
+
});
|
|
16498
|
+
server.tool("browser_tab_switch", "Switch to a different tab by index. Updates the session's active page.", { session_id: exports_external.string(), tab_id: exports_external.number() }, async ({ session_id, tab_id }) => {
|
|
16499
|
+
try {
|
|
16500
|
+
const page = getSessionPage(session_id);
|
|
16501
|
+
const result = await switchTab(page, tab_id);
|
|
16502
|
+
setSessionPage(session_id, result.page);
|
|
16503
|
+
return json(result.tab);
|
|
16504
|
+
} catch (e) {
|
|
16505
|
+
return err(e);
|
|
16506
|
+
}
|
|
16507
|
+
});
|
|
16508
|
+
server.tool("browser_tab_close", "Close a tab by index. Cannot close the last tab.", { session_id: exports_external.string(), tab_id: exports_external.number() }, async ({ session_id, tab_id }) => {
|
|
16509
|
+
try {
|
|
16510
|
+
const page = getSessionPage(session_id);
|
|
16511
|
+
const context = page.context();
|
|
16512
|
+
const result = await closeTab(page, tab_id);
|
|
16513
|
+
const remainingPages = context.pages();
|
|
16514
|
+
const newActivePage = remainingPages[result.active_tab.index];
|
|
16515
|
+
if (newActivePage) {
|
|
16516
|
+
setSessionPage(session_id, newActivePage);
|
|
16517
|
+
}
|
|
16518
|
+
return json(result);
|
|
16519
|
+
} catch (e) {
|
|
16520
|
+
return err(e);
|
|
16521
|
+
}
|
|
16522
|
+
});
|
|
16523
|
+
server.tool("browser_handle_dialog", "Accept or dismiss a pending dialog (alert, confirm, prompt). Handles the oldest pending dialog.", { session_id: exports_external.string(), action: exports_external.enum(["accept", "dismiss"]), prompt_text: exports_external.string().optional() }, async ({ session_id, action, prompt_text }) => {
|
|
16524
|
+
try {
|
|
16525
|
+
const result = await handleDialog(session_id, action, prompt_text);
|
|
16526
|
+
if (!result.handled)
|
|
16527
|
+
return err(new Error("No pending dialogs for this session"));
|
|
16528
|
+
return json(result);
|
|
16529
|
+
} catch (e) {
|
|
16530
|
+
return err(e);
|
|
16531
|
+
}
|
|
16532
|
+
});
|
|
16533
|
+
server.tool("browser_get_dialogs", "Get all pending dialogs for a session", { session_id: exports_external.string() }, async ({ session_id }) => {
|
|
16534
|
+
try {
|
|
16535
|
+
const dialogs = getDialogs(session_id);
|
|
16536
|
+
return json({ dialogs, count: dialogs.length });
|
|
16537
|
+
} catch (e) {
|
|
16538
|
+
return err(e);
|
|
16539
|
+
}
|
|
16540
|
+
});
|
|
16541
|
+
server.tool("browser_profile_save", "Save cookies + localStorage from the current session as a named profile", { session_id: exports_external.string(), name: exports_external.string() }, async ({ session_id, name }) => {
|
|
16542
|
+
try {
|
|
16543
|
+
const page = getSessionPage(session_id);
|
|
16544
|
+
const info = await saveProfile(page, name);
|
|
16545
|
+
return json(info);
|
|
16546
|
+
} catch (e) {
|
|
16547
|
+
return err(e);
|
|
16548
|
+
}
|
|
16549
|
+
});
|
|
16550
|
+
server.tool("browser_profile_load", "Load a saved profile and apply cookies + localStorage to the current session", { session_id: exports_external.string().optional(), name: exports_external.string() }, async ({ session_id, name }) => {
|
|
16551
|
+
try {
|
|
16552
|
+
const profileData = loadProfile(name);
|
|
16553
|
+
if (session_id) {
|
|
16554
|
+
const page = getSessionPage(session_id);
|
|
16555
|
+
const applied = await applyProfile(page, profileData);
|
|
16556
|
+
return json({ ...applied, profile: name });
|
|
16557
|
+
}
|
|
16558
|
+
return json({ profile: name, cookies: profileData.cookies.length, storage_keys: Object.keys(profileData.localStorage).length });
|
|
16559
|
+
} catch (e) {
|
|
16560
|
+
return err(e);
|
|
16561
|
+
}
|
|
16562
|
+
});
|
|
16563
|
+
server.tool("browser_profile_list", "List all saved browser profiles", {}, async () => {
|
|
16564
|
+
try {
|
|
16565
|
+
return json({ profiles: listProfiles() });
|
|
16566
|
+
} catch (e) {
|
|
16567
|
+
return err(e);
|
|
16568
|
+
}
|
|
16569
|
+
});
|
|
16570
|
+
server.tool("browser_profile_delete", "Delete a saved browser profile", { name: exports_external.string() }, async ({ name }) => {
|
|
16571
|
+
try {
|
|
16572
|
+
const deleted = deleteProfile(name);
|
|
16573
|
+
if (!deleted)
|
|
16574
|
+
return err(new Error(`Profile not found: ${name}`));
|
|
16575
|
+
return json({ deleted: name });
|
|
16576
|
+
} catch (e) {
|
|
16577
|
+
return err(e);
|
|
16578
|
+
}
|
|
16579
|
+
});
|
|
16580
|
+
server.tool("browser_help", "Show all available browser tools grouped by category with one-line descriptions", {}, async () => {
|
|
16581
|
+
try {
|
|
16582
|
+
const groups = {
|
|
16583
|
+
Navigation: [
|
|
16584
|
+
{ tool: "browser_navigate", description: "Navigate to a URL" },
|
|
16585
|
+
{ tool: "browser_back", description: "Navigate back in history" },
|
|
16586
|
+
{ tool: "browser_forward", description: "Navigate forward in history" },
|
|
16587
|
+
{ tool: "browser_reload", description: "Reload the current page" },
|
|
16588
|
+
{ tool: "browser_wait_for_navigation", description: "Wait for URL change after action" }
|
|
16589
|
+
],
|
|
16590
|
+
Interaction: [
|
|
16591
|
+
{ tool: "browser_click", description: "Click element by ref or selector" },
|
|
16592
|
+
{ tool: "browser_click_text", description: "Click element by visible text" },
|
|
16593
|
+
{ tool: "browser_type", description: "Type text into an element" },
|
|
16594
|
+
{ tool: "browser_hover", description: "Hover over an element" },
|
|
16595
|
+
{ tool: "browser_scroll", description: "Scroll the page" },
|
|
16596
|
+
{ tool: "browser_select", description: "Select a dropdown option" },
|
|
16597
|
+
{ tool: "browser_toggle", description: "Check/uncheck a checkbox" },
|
|
16598
|
+
{ tool: "browser_upload", description: "Upload a file to an input" },
|
|
16599
|
+
{ tool: "browser_press_key", description: "Press a keyboard key" },
|
|
16600
|
+
{ tool: "browser_wait", description: "Wait for a selector to appear" },
|
|
16601
|
+
{ tool: "browser_wait_for_text", description: "Wait for text to appear" },
|
|
16602
|
+
{ tool: "browser_fill_form", description: "Fill multiple form fields at once" },
|
|
16603
|
+
{ tool: "browser_handle_dialog", description: "Accept or dismiss a dialog" }
|
|
16604
|
+
],
|
|
16605
|
+
Extraction: [
|
|
16606
|
+
{ tool: "browser_get_text", description: "Get text content from page/selector" },
|
|
16607
|
+
{ tool: "browser_get_html", description: "Get HTML content from page/selector" },
|
|
16608
|
+
{ tool: "browser_get_links", description: "Get all links on the page" },
|
|
16609
|
+
{ tool: "browser_get_page_info", description: "Full page summary in one call" },
|
|
16610
|
+
{ tool: "browser_extract", description: "Extract content in various formats" },
|
|
16611
|
+
{ tool: "browser_find", description: "Find elements by selector" },
|
|
16612
|
+
{ tool: "browser_element_exists", description: "Check if a selector exists" },
|
|
16613
|
+
{ tool: "browser_snapshot", description: "Get accessibility snapshot with refs" },
|
|
16614
|
+
{ tool: "browser_evaluate", description: "Execute JavaScript in page context" }
|
|
16615
|
+
],
|
|
16616
|
+
Capture: [
|
|
16617
|
+
{ tool: "browser_screenshot", description: "Take a screenshot (PNG/JPEG/WebP, annotate=true for labels)" },
|
|
16618
|
+
{ tool: "browser_pdf", description: "Generate a PDF of the page" },
|
|
16619
|
+
{ tool: "browser_scroll_and_screenshot", description: "Scroll then screenshot in one call" },
|
|
16620
|
+
{ tool: "browser_scroll_to_element", description: "Scroll element into view + screenshot" }
|
|
16621
|
+
],
|
|
16622
|
+
Storage: [
|
|
16623
|
+
{ tool: "browser_cookies_get", description: "Get cookies" },
|
|
16624
|
+
{ tool: "browser_cookies_set", description: "Set a cookie" },
|
|
16625
|
+
{ tool: "browser_cookies_clear", description: "Clear cookies" },
|
|
16626
|
+
{ tool: "browser_storage_get", description: "Get localStorage/sessionStorage" },
|
|
16627
|
+
{ tool: "browser_storage_set", description: "Set localStorage/sessionStorage" },
|
|
16628
|
+
{ tool: "browser_profile_save", description: "Save cookies + localStorage as profile" },
|
|
16629
|
+
{ tool: "browser_profile_load", description: "Load and apply a saved profile" },
|
|
16630
|
+
{ tool: "browser_profile_list", description: "List saved profiles" },
|
|
16631
|
+
{ tool: "browser_profile_delete", description: "Delete a saved profile" }
|
|
16632
|
+
],
|
|
16633
|
+
Network: [
|
|
16634
|
+
{ tool: "browser_network_log", description: "Get captured network requests" },
|
|
16635
|
+
{ tool: "browser_network_intercept", description: "Add a network interception rule" },
|
|
16636
|
+
{ tool: "browser_har_start", description: "Start HAR capture" },
|
|
16637
|
+
{ tool: "browser_har_stop", description: "Stop HAR capture and get data" }
|
|
16638
|
+
],
|
|
16639
|
+
Performance: [
|
|
16640
|
+
{ tool: "browser_performance", description: "Get performance metrics" }
|
|
16641
|
+
],
|
|
16642
|
+
Console: [
|
|
16643
|
+
{ tool: "browser_console_log", description: "Get console messages" },
|
|
16644
|
+
{ tool: "browser_has_errors", description: "Check for console errors" },
|
|
16645
|
+
{ tool: "browser_clear_errors", description: "Clear console error log" },
|
|
16646
|
+
{ tool: "browser_get_dialogs", description: "Get pending dialogs" }
|
|
16647
|
+
],
|
|
16648
|
+
Recording: [
|
|
16649
|
+
{ tool: "browser_record_start", description: "Start recording actions" },
|
|
16650
|
+
{ tool: "browser_record_step", description: "Add a step to recording" },
|
|
16651
|
+
{ tool: "browser_record_stop", description: "Stop and save recording" },
|
|
16652
|
+
{ tool: "browser_record_replay", description: "Replay a recorded sequence" },
|
|
16653
|
+
{ tool: "browser_recordings_list", description: "List all recordings" }
|
|
16654
|
+
],
|
|
16655
|
+
Crawl: [
|
|
16656
|
+
{ tool: "browser_crawl", description: "Crawl a URL recursively" }
|
|
16657
|
+
],
|
|
16658
|
+
Agent: [
|
|
16659
|
+
{ tool: "browser_register_agent", description: "Register an agent" },
|
|
16660
|
+
{ tool: "browser_heartbeat", description: "Send agent heartbeat" },
|
|
16661
|
+
{ tool: "browser_agent_list", description: "List registered agents" }
|
|
16662
|
+
],
|
|
16663
|
+
Project: [
|
|
16664
|
+
{ tool: "browser_project_create", description: "Create or ensure a project" },
|
|
16665
|
+
{ tool: "browser_project_list", description: "List all projects" }
|
|
16666
|
+
],
|
|
16667
|
+
Gallery: [
|
|
16668
|
+
{ tool: "browser_gallery_list", description: "List screenshot gallery entries" },
|
|
16669
|
+
{ tool: "browser_gallery_get", description: "Get a gallery entry by id" },
|
|
16670
|
+
{ tool: "browser_gallery_tag", description: "Add a tag to gallery entry" },
|
|
16671
|
+
{ tool: "browser_gallery_untag", description: "Remove a tag from gallery entry" },
|
|
16672
|
+
{ tool: "browser_gallery_favorite", description: "Mark/unmark as favorite" },
|
|
16673
|
+
{ tool: "browser_gallery_delete", description: "Delete a gallery entry" },
|
|
16674
|
+
{ tool: "browser_gallery_search", description: "Search gallery entries" },
|
|
16675
|
+
{ tool: "browser_gallery_stats", description: "Get gallery statistics" },
|
|
16676
|
+
{ tool: "browser_gallery_diff", description: "Pixel-diff two screenshots" }
|
|
16677
|
+
],
|
|
16678
|
+
Downloads: [
|
|
16679
|
+
{ tool: "browser_downloads_list", description: "List downloaded files" },
|
|
16680
|
+
{ tool: "browser_downloads_get", description: "Get a download by id" },
|
|
16681
|
+
{ tool: "browser_downloads_delete", description: "Delete a download" },
|
|
16682
|
+
{ tool: "browser_downloads_clean", description: "Clean old downloads" },
|
|
16683
|
+
{ tool: "browser_downloads_export", description: "Copy download to a path" },
|
|
16684
|
+
{ tool: "browser_persist_file", description: "Persist file permanently" }
|
|
16685
|
+
],
|
|
16686
|
+
Session: [
|
|
16687
|
+
{ tool: "browser_session_create", description: "Create a new browser session" },
|
|
16688
|
+
{ tool: "browser_session_list", description: "List all sessions" },
|
|
16689
|
+
{ tool: "browser_session_close", description: "Close a session" },
|
|
16690
|
+
{ tool: "browser_session_get_by_name", description: "Get session by name" },
|
|
16691
|
+
{ tool: "browser_session_rename", description: "Rename a session" },
|
|
16692
|
+
{ tool: "browser_session_stats", description: "Get session stats and token usage" },
|
|
16693
|
+
{ tool: "browser_tab_new", description: "Open a new tab" },
|
|
16694
|
+
{ tool: "browser_tab_list", description: "List all open tabs" },
|
|
16695
|
+
{ tool: "browser_tab_switch", description: "Switch to a tab by index" },
|
|
16696
|
+
{ tool: "browser_tab_close", description: "Close a tab by index" }
|
|
16697
|
+
],
|
|
16698
|
+
Meta: [
|
|
16699
|
+
{ tool: "browser_check", description: "RECOMMENDED: One-call page summary with diagnostics" },
|
|
16700
|
+
{ tool: "browser_version", description: "Show running binary version and tool count" },
|
|
16701
|
+
{ tool: "browser_help", description: "Show this help (all tools)" },
|
|
16702
|
+
{ tool: "browser_snapshot_diff", description: "Diff current snapshot vs previous" },
|
|
16703
|
+
{ tool: "browser_watch_start", description: "Watch page for DOM changes" },
|
|
16704
|
+
{ tool: "browser_watch_get_changes", description: "Get captured DOM changes" },
|
|
16705
|
+
{ tool: "browser_watch_stop", description: "Stop DOM watcher" }
|
|
16706
|
+
]
|
|
16707
|
+
};
|
|
16708
|
+
const totalTools = Object.values(groups).reduce((sum, g) => sum + g.length, 0);
|
|
16709
|
+
return json({ groups, total_tools: totalTools });
|
|
16710
|
+
} catch (e) {
|
|
16711
|
+
return err(e);
|
|
16712
|
+
}
|
|
16713
|
+
});
|
|
16714
|
+
server.tool("browser_version", "Get the running browser MCP version, tool count, and environment info. Use this to verify which binary is active.", {}, async () => {
|
|
16715
|
+
try {
|
|
16716
|
+
const { getDataDir: getDataDir4 } = await Promise.resolve().then(() => (init_schema(), exports_schema));
|
|
16717
|
+
const toolCount = Object.keys(server._registeredTools ?? {}).length;
|
|
16718
|
+
return json({
|
|
16719
|
+
version: _pkg.version,
|
|
16720
|
+
mcp_tools_count: toolCount,
|
|
16721
|
+
bun_version: Bun.version,
|
|
16722
|
+
data_dir: getDataDir4(),
|
|
16723
|
+
node_env: process.env["NODE_ENV"] ?? "production"
|
|
16724
|
+
});
|
|
16725
|
+
} catch (e) {
|
|
16726
|
+
return err(e);
|
|
16727
|
+
}
|
|
16728
|
+
});
|
|
16729
|
+
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.", {
|
|
16730
|
+
session_id: exports_external.string(),
|
|
16731
|
+
selector: exports_external.string().optional(),
|
|
16732
|
+
ref: exports_external.string().optional(),
|
|
16733
|
+
screenshot: exports_external.boolean().optional().default(true),
|
|
16734
|
+
wait_ms: exports_external.number().optional().default(200)
|
|
16735
|
+
}, async ({ session_id, selector, ref, screenshot: doScreenshot, wait_ms }) => {
|
|
16736
|
+
try {
|
|
16737
|
+
const page = getSessionPage(session_id);
|
|
16738
|
+
let locator;
|
|
16739
|
+
if (ref) {
|
|
16740
|
+
const { getRefLocator: getRefLocator2 } = await Promise.resolve().then(() => (init_snapshot(), exports_snapshot));
|
|
16741
|
+
locator = getRefLocator2(page, session_id, ref);
|
|
16742
|
+
} else if (selector) {
|
|
16743
|
+
locator = page.locator(selector).first();
|
|
16744
|
+
} else {
|
|
16745
|
+
return err(new Error("Either ref or selector is required"));
|
|
16746
|
+
}
|
|
16747
|
+
await locator.scrollIntoViewIfNeeded();
|
|
16748
|
+
await new Promise((r) => setTimeout(r, wait_ms));
|
|
16749
|
+
const result = { scrolled: ref ?? selector };
|
|
16750
|
+
if (doScreenshot) {
|
|
16751
|
+
try {
|
|
16752
|
+
const ss = await takeScreenshot(page, { selector, track: false });
|
|
16753
|
+
ss.url = page.url();
|
|
16754
|
+
if (ss.base64.length > 50000) {
|
|
16755
|
+
ss.base64_truncated = true;
|
|
16756
|
+
ss.base64 = ss.thumbnail_base64 ?? "";
|
|
16757
|
+
}
|
|
16758
|
+
result.screenshot = ss;
|
|
16759
|
+
} catch {}
|
|
16760
|
+
}
|
|
16761
|
+
return json(result);
|
|
16762
|
+
} catch (e) {
|
|
16763
|
+
return err(e);
|
|
16764
|
+
}
|
|
16765
|
+
});
|
|
16766
|
+
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 }) => {
|
|
16767
|
+
try {
|
|
16768
|
+
const page = getSessionPage(session_id);
|
|
16769
|
+
const info = await getPageInfo(page);
|
|
16770
|
+
const errors2 = getConsoleLog(session_id, "error");
|
|
16771
|
+
info.has_console_errors = errors2.length > 0;
|
|
16772
|
+
let perf = {};
|
|
16773
|
+
try {
|
|
16774
|
+
perf = await getPerformanceMetrics(page);
|
|
16775
|
+
} catch {}
|
|
16776
|
+
let thumbnail_base64 = "";
|
|
16777
|
+
try {
|
|
16778
|
+
const ss = await takeScreenshot(page, { maxWidth: 400, quality: 60, track: false, thumbnail: false });
|
|
16779
|
+
thumbnail_base64 = ss.base64.length > 50000 ? "" : ss.base64;
|
|
16780
|
+
} catch {}
|
|
16781
|
+
let snapshot_refs = "";
|
|
16782
|
+
let interactive_count = 0;
|
|
16783
|
+
try {
|
|
16784
|
+
const snap = await takeSnapshot(page, session_id);
|
|
16785
|
+
setLastSnapshot(session_id, snap);
|
|
16786
|
+
interactive_count = snap.interactive_count;
|
|
16787
|
+
snapshot_refs = Object.entries(snap.refs).slice(0, 30).map(([ref, i]) => `${i.role}:${i.name.slice(0, 50)} [${ref}]`).join(", ");
|
|
16788
|
+
} catch {}
|
|
16789
|
+
return json({ ...info, error_count: errors2.length, performance: perf, thumbnail_base64, snapshot_refs, interactive_count });
|
|
16790
|
+
} catch (e) {
|
|
16791
|
+
return err(e);
|
|
16792
|
+
}
|
|
16793
|
+
});
|
|
16794
|
+
_startupToolCount = Object.keys(server._registeredTools ?? {}).length;
|
|
16795
|
+
console.error(`@hasna/browser v${_pkg.version} \u2014 ${_startupToolCount} tools | data: ${(await Promise.resolve().then(() => (init_schema(), exports_schema))).getDataDir()}`);
|
|
15512
16796
|
transport = new StdioServerTransport;
|
|
15513
16797
|
await server.connect(transport);
|
|
15514
16798
|
});
|
|
@@ -15551,8 +16835,8 @@ var init_snapshots = __esm(() => {
|
|
|
15551
16835
|
|
|
15552
16836
|
// src/server/index.ts
|
|
15553
16837
|
var exports_server = {};
|
|
15554
|
-
import { join as
|
|
15555
|
-
import { existsSync as
|
|
16838
|
+
import { join as join8 } from "path";
|
|
16839
|
+
import { existsSync as existsSync4 } from "fs";
|
|
15556
16840
|
function ok(data, status = 200) {
|
|
15557
16841
|
return new Response(JSON.stringify(data), {
|
|
15558
16842
|
status,
|
|
@@ -15802,14 +17086,14 @@ var init_server = __esm(() => {
|
|
|
15802
17086
|
if (path.match(/^\/api\/gallery\/([^/]+)\/thumbnail$/) && method === "GET") {
|
|
15803
17087
|
const id = path.split("/")[3];
|
|
15804
17088
|
const entry = getEntry(id);
|
|
15805
|
-
if (!entry?.thumbnail_path || !
|
|
17089
|
+
if (!entry?.thumbnail_path || !existsSync4(entry.thumbnail_path))
|
|
15806
17090
|
return notFound("Thumbnail not found");
|
|
15807
17091
|
return new Response(Bun.file(entry.thumbnail_path), { headers: { ...CORS_HEADERS } });
|
|
15808
17092
|
}
|
|
15809
17093
|
if (path.match(/^\/api\/gallery\/([^/]+)\/image$/) && method === "GET") {
|
|
15810
17094
|
const id = path.split("/")[3];
|
|
15811
17095
|
const entry = getEntry(id);
|
|
15812
|
-
if (!entry?.path || !
|
|
17096
|
+
if (!entry?.path || !existsSync4(entry.path))
|
|
15813
17097
|
return notFound("Image not found");
|
|
15814
17098
|
return new Response(Bun.file(entry.path), { headers: { ...CORS_HEADERS } });
|
|
15815
17099
|
}
|
|
@@ -15837,7 +17121,7 @@ var init_server = __esm(() => {
|
|
|
15837
17121
|
if (path.match(/^\/api\/downloads\/([^/]+)\/raw$/) && method === "GET") {
|
|
15838
17122
|
const id = path.split("/")[3];
|
|
15839
17123
|
const file = getDownload(id);
|
|
15840
|
-
if (!file || !
|
|
17124
|
+
if (!file || !existsSync4(file.path))
|
|
15841
17125
|
return notFound("Download not found");
|
|
15842
17126
|
return new Response(Bun.file(file.path), { headers: { ...CORS_HEADERS } });
|
|
15843
17127
|
}
|
|
@@ -15845,13 +17129,13 @@ var init_server = __esm(() => {
|
|
|
15845
17129
|
const id = path.split("/")[3];
|
|
15846
17130
|
return ok({ deleted: deleteDownload(id) });
|
|
15847
17131
|
}
|
|
15848
|
-
const dashboardDist =
|
|
15849
|
-
if (
|
|
15850
|
-
const filePath = path === "/" ?
|
|
15851
|
-
if (
|
|
17132
|
+
const dashboardDist = join8(import.meta.dir, "../../dashboard/dist");
|
|
17133
|
+
if (existsSync4(dashboardDist)) {
|
|
17134
|
+
const filePath = path === "/" ? join8(dashboardDist, "index.html") : join8(dashboardDist, path);
|
|
17135
|
+
if (existsSync4(filePath)) {
|
|
15852
17136
|
return new Response(Bun.file(filePath), { headers: CORS_HEADERS });
|
|
15853
17137
|
}
|
|
15854
|
-
return new Response(Bun.file(
|
|
17138
|
+
return new Response(Bun.file(join8(dashboardDist, "index.html")), { headers: CORS_HEADERS });
|
|
15855
17139
|
}
|
|
15856
17140
|
if (path === "/" || path === "") {
|
|
15857
17141
|
return new Response("@hasna/browser REST API running. Dashboard not built.", {
|
|
@@ -15893,9 +17177,12 @@ init_projects();
|
|
|
15893
17177
|
init_recorder();
|
|
15894
17178
|
init_recordings();
|
|
15895
17179
|
init_lightpanda();
|
|
17180
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
17181
|
+
import { join as join9 } from "path";
|
|
15896
17182
|
import chalk from "chalk";
|
|
17183
|
+
var pkg = JSON.parse(readFileSync4(join9(import.meta.dir, "../../package.json"), "utf8"));
|
|
15897
17184
|
var program2 = new Command;
|
|
15898
|
-
program2.name("browser").description("@hasna/browser \u2014 general-purpose browser agent CLI").version(
|
|
17185
|
+
program2.name("browser").description("@hasna/browser \u2014 general-purpose browser agent CLI").version(pkg.version);
|
|
15899
17186
|
program2.command("navigate <url>").description("Navigate to a URL and optionally take a screenshot").option("--engine <engine>", "Browser engine: playwright|cdp|lightpanda|auto", "auto").option("--screenshot", "Take a screenshot after navigation").option("--extract", "Extract page text after navigation").option("--headless", "Run in headless mode (default: true)", true).action(async (url, opts) => {
|
|
15900
17187
|
const { session, page } = await createSession2({ engine: opts.engine, headless: true });
|
|
15901
17188
|
console.log(chalk.gray(`Session: ${session.id} (${session.engine})`));
|
|
@@ -16136,11 +17423,11 @@ galleryCmd.command("stats").description("Show gallery statistics").option("--pro
|
|
|
16136
17423
|
});
|
|
16137
17424
|
galleryCmd.command("clean").description("Delete gallery entries with missing files").action(async () => {
|
|
16138
17425
|
const { listEntries: listEntries2, deleteEntry: deleteEntry2 } = await Promise.resolve().then(() => (init_gallery(), exports_gallery));
|
|
16139
|
-
const { existsSync:
|
|
17426
|
+
const { existsSync: existsSync5 } = await import("fs");
|
|
16140
17427
|
const entries = listEntries2({ limit: 9999 });
|
|
16141
17428
|
let removed = 0;
|
|
16142
17429
|
for (const e of entries) {
|
|
16143
|
-
if (!
|
|
17430
|
+
if (!existsSync5(e.path)) {
|
|
16144
17431
|
deleteEntry2(e.id);
|
|
16145
17432
|
removed++;
|
|
16146
17433
|
}
|