@hasna/browser 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +906 -159
- package/dist/db/sessions.d.ts.map +1 -1
- package/dist/engines/bun-webview.d.ts +147 -0
- package/dist/engines/bun-webview.d.ts.map +1 -0
- package/dist/engines/bun-webview.test.d.ts +2 -0
- package/dist/engines/bun-webview.test.d.ts.map +1 -0
- package/dist/engines/selector.d.ts +2 -2
- package/dist/engines/selector.d.ts.map +1 -1
- package/dist/index.js +835 -285
- package/dist/lib/annotate.d.ts.map +1 -1
- package/dist/lib/extractor.d.ts.map +1 -1
- package/dist/lib/screenshot-v4.test.d.ts +2 -0
- package/dist/lib/screenshot-v4.test.d.ts.map +1 -0
- package/dist/lib/screenshot.d.ts.map +1 -1
- package/dist/lib/session.d.ts +3 -0
- package/dist/lib/session.d.ts.map +1 -1
- package/dist/lib/snapshot.d.ts +1 -0
- package/dist/lib/snapshot.d.ts.map +1 -1
- package/dist/mcp/index.js +896 -151
- package/dist/mcp/v4.test.d.ts +2 -0
- package/dist/mcp/v4.test.d.ts.map +1 -0
- package/dist/server/index.js +596 -93
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/mcp/index.js
CHANGED
|
@@ -30,6 +30,12 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
|
30
30
|
var __require = import.meta.require;
|
|
31
31
|
|
|
32
32
|
// src/db/schema.ts
|
|
33
|
+
var exports_schema = {};
|
|
34
|
+
__export(exports_schema, {
|
|
35
|
+
resetDatabase: () => resetDatabase,
|
|
36
|
+
getDatabase: () => getDatabase,
|
|
37
|
+
getDataDir: () => getDataDir
|
|
38
|
+
});
|
|
33
39
|
import { Database } from "bun:sqlite";
|
|
34
40
|
import { join } from "path";
|
|
35
41
|
import { mkdirSync } from "fs";
|
|
@@ -55,6 +61,15 @@ function getDatabase(path) {
|
|
|
55
61
|
runMigrations(_db);
|
|
56
62
|
return _db;
|
|
57
63
|
}
|
|
64
|
+
function resetDatabase() {
|
|
65
|
+
if (_db) {
|
|
66
|
+
try {
|
|
67
|
+
_db.close();
|
|
68
|
+
} catch {}
|
|
69
|
+
}
|
|
70
|
+
_db = null;
|
|
71
|
+
_dbPath = null;
|
|
72
|
+
}
|
|
58
73
|
function runMigrations(db) {
|
|
59
74
|
db.exec(`
|
|
60
75
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
@@ -252,13 +267,34 @@ var init_console_log = __esm(() => {
|
|
|
252
267
|
});
|
|
253
268
|
|
|
254
269
|
// src/lib/snapshot.ts
|
|
270
|
+
var exports_snapshot = {};
|
|
271
|
+
__export(exports_snapshot, {
|
|
272
|
+
takeSnapshot: () => takeSnapshot,
|
|
273
|
+
takeBunSnapshot: () => takeBunSnapshot,
|
|
274
|
+
setLastSnapshot: () => setLastSnapshot,
|
|
275
|
+
hasRefs: () => hasRefs,
|
|
276
|
+
getSessionRefs: () => getSessionRefs,
|
|
277
|
+
getRefLocator: () => getRefLocator,
|
|
278
|
+
getRefInfo: () => getRefInfo,
|
|
279
|
+
getLastSnapshot: () => getLastSnapshot,
|
|
280
|
+
diffSnapshots: () => diffSnapshots,
|
|
281
|
+
clearSessionRefs: () => clearSessionRefs,
|
|
282
|
+
clearLastSnapshot: () => clearLastSnapshot
|
|
283
|
+
});
|
|
255
284
|
function getLastSnapshot(sessionId) {
|
|
256
285
|
return lastSnapshots.get(sessionId) ?? null;
|
|
257
286
|
}
|
|
258
287
|
function setLastSnapshot(sessionId, snapshot) {
|
|
259
288
|
lastSnapshots.set(sessionId, snapshot);
|
|
260
289
|
}
|
|
290
|
+
function clearLastSnapshot(sessionId) {
|
|
291
|
+
lastSnapshots.delete(sessionId);
|
|
292
|
+
}
|
|
261
293
|
async function takeSnapshot(page, sessionId) {
|
|
294
|
+
const isBunView = typeof page.getNativeView === "function" || typeof page.bunView !== "undefined";
|
|
295
|
+
if (isBunView) {
|
|
296
|
+
return takeBunSnapshot(page, sessionId);
|
|
297
|
+
}
|
|
262
298
|
let ariaTree;
|
|
263
299
|
try {
|
|
264
300
|
ariaTree = await page.locator("body").ariaSnapshot();
|
|
@@ -360,6 +396,21 @@ function getRefLocator(page, sessionId, ref) {
|
|
|
360
396
|
throw new Error(`Ref ${ref} not found. Available refs: ${[...refMap.keys()].slice(0, 20).join(", ")}`);
|
|
361
397
|
return page.getByRole(entry.role, { name: entry.name }).first();
|
|
362
398
|
}
|
|
399
|
+
function getRefInfo(sessionId, ref) {
|
|
400
|
+
const refMap = sessionRefMaps.get(sessionId);
|
|
401
|
+
if (!refMap)
|
|
402
|
+
return null;
|
|
403
|
+
return refMap.get(ref) ?? null;
|
|
404
|
+
}
|
|
405
|
+
function getSessionRefs(sessionId) {
|
|
406
|
+
return sessionRefMaps.get(sessionId) ?? null;
|
|
407
|
+
}
|
|
408
|
+
function clearSessionRefs(sessionId) {
|
|
409
|
+
sessionRefMaps.delete(sessionId);
|
|
410
|
+
}
|
|
411
|
+
function hasRefs(sessionId) {
|
|
412
|
+
return sessionRefMaps.has(sessionId) && (sessionRefMaps.get(sessionId)?.size ?? 0) > 0;
|
|
413
|
+
}
|
|
363
414
|
function refKey(info) {
|
|
364
415
|
return `${info.role}::${info.name}`;
|
|
365
416
|
}
|
|
@@ -398,6 +449,71 @@ function diffSnapshots(before, after) {
|
|
|
398
449
|
const title_changed = before.tree !== after.tree && (added.length > 0 || removed.length > 0 || modified.length > 0);
|
|
399
450
|
return { added, removed, modified, url_changed, title_changed };
|
|
400
451
|
}
|
|
452
|
+
async function takeBunSnapshot(page, sessionId) {
|
|
453
|
+
const refs = {};
|
|
454
|
+
const refMap = new Map;
|
|
455
|
+
let refCounter = 0;
|
|
456
|
+
const lines = [];
|
|
457
|
+
try {
|
|
458
|
+
const elements = await page.evaluate(`
|
|
459
|
+
(() => {
|
|
460
|
+
const SELECTOR = 'a[href], button, input:not([type=hidden]), select, textarea, [role=button], [role=link], [role=checkbox], [role=combobox], [role=menuitem], [role=tab], [role=option]';
|
|
461
|
+
const els = Array.from(document.querySelectorAll(SELECTOR));
|
|
462
|
+
return els.slice(0, 100).map(el => {
|
|
463
|
+
const tag = el.tagName.toLowerCase();
|
|
464
|
+
const inputType = el.getAttribute('type') ?? '';
|
|
465
|
+
let role = el.getAttribute('role') || (['a'].includes(tag) ? 'link' : ['button'].includes(tag) ? 'button' : ['input'].includes(tag) ? (inputType === 'checkbox' ? 'checkbox' : inputType === 'radio' ? 'radio' : 'textbox') : ['select'].includes(tag) ? 'combobox' : ['textarea'].includes(tag) ? 'textbox' : tag);
|
|
466
|
+
const name = (el.getAttribute('aria-label') || el.textContent?.trim() || el.getAttribute('placeholder') || el.getAttribute('title') || el.getAttribute('value') || el.id || '').slice(0, 80);
|
|
467
|
+
const enabled = !el.disabled && !el.getAttribute('disabled');
|
|
468
|
+
const style = window.getComputedStyle(el);
|
|
469
|
+
const visible = style.display !== 'none' && style.visibility !== 'hidden' && el.offsetWidth > 0;
|
|
470
|
+
const checked = el.type === 'checkbox' || el.type === 'radio' ? el.checked : undefined;
|
|
471
|
+
const value = ['input', 'select', 'textarea'].includes(tag) && el.type !== 'checkbox' && el.type !== 'radio' ? el.value : undefined;
|
|
472
|
+
const selector = el.id ? '#' + el.id : (el.getAttribute('aria-label') ? '[aria-label="' + el.getAttribute('aria-label') + '"]' : tag);
|
|
473
|
+
return { role, name, enabled, visible, checked, value, selector };
|
|
474
|
+
}).filter(e => e.visible && e.name);
|
|
475
|
+
})()
|
|
476
|
+
`);
|
|
477
|
+
const pageTitle = await page.evaluate("document.title");
|
|
478
|
+
const pageUrl = typeof page.url === "function" ? page.url() : "";
|
|
479
|
+
lines.push(`# ${pageTitle || "Page"} (${pageUrl})`);
|
|
480
|
+
for (const el of elements) {
|
|
481
|
+
if (!el.name)
|
|
482
|
+
continue;
|
|
483
|
+
const ref = `@e${refCounter}`;
|
|
484
|
+
refCounter++;
|
|
485
|
+
refs[ref] = {
|
|
486
|
+
role: el.role,
|
|
487
|
+
name: el.name,
|
|
488
|
+
visible: el.visible,
|
|
489
|
+
enabled: el.enabled,
|
|
490
|
+
value: el.value,
|
|
491
|
+
checked: el.checked
|
|
492
|
+
};
|
|
493
|
+
refMap.set(ref, { role: el.role, name: el.name, locatorSelector: el.selector });
|
|
494
|
+
const extras = [];
|
|
495
|
+
if (el.checked !== undefined)
|
|
496
|
+
extras.push(`checked=${el.checked}`);
|
|
497
|
+
if (!el.enabled)
|
|
498
|
+
extras.push("disabled");
|
|
499
|
+
if (el.value && el.value !== el.name)
|
|
500
|
+
extras.push(`value="${el.value.slice(0, 30)}"`);
|
|
501
|
+
const extrasStr = extras.length ? ` (${extras.join(", ")})` : "";
|
|
502
|
+
lines.push(`${el.role} "${el.name}" [${ref}]${extrasStr}`);
|
|
503
|
+
}
|
|
504
|
+
} catch (err) {
|
|
505
|
+
lines.push(`# (snapshot error: ${err instanceof Error ? err.message : String(err)})`);
|
|
506
|
+
}
|
|
507
|
+
if (sessionId) {
|
|
508
|
+
sessionRefMaps.set(sessionId, refMap);
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
tree: lines.join(`
|
|
512
|
+
`),
|
|
513
|
+
refs,
|
|
514
|
+
interactive_count: refCounter
|
|
515
|
+
};
|
|
516
|
+
}
|
|
401
517
|
var lastSnapshots, sessionRefMaps, INTERACTIVE_ROLES;
|
|
402
518
|
var init_snapshot = __esm(() => {
|
|
403
519
|
lastSnapshots = new Map;
|
|
@@ -6833,7 +6949,8 @@ async function annotateScreenshot(page, sessionId) {
|
|
|
6833
6949
|
const annotations = [];
|
|
6834
6950
|
const labelToRef = {};
|
|
6835
6951
|
let labelCounter = 1;
|
|
6836
|
-
|
|
6952
|
+
const refsToAnnotate = Object.entries(snapshot.refs).slice(0, MAX_ANNOTATIONS);
|
|
6953
|
+
for (const [ref, info] of refsToAnnotate) {
|
|
6837
6954
|
try {
|
|
6838
6955
|
const locator = page.getByRole(info.role, { name: info.name }).first();
|
|
6839
6956
|
const box = await locator.boundingBox();
|
|
@@ -6872,7 +6989,7 @@ async function annotateScreenshot(page, sessionId) {
|
|
|
6872
6989
|
const annotatedBuffer = await import_sharp3.default(rawBuffer).composite([{ input: Buffer.from(svg), top: 0, left: 0 }]).webp({ quality: 85 }).toBuffer();
|
|
6873
6990
|
return { buffer: annotatedBuffer, annotations, labelToRef };
|
|
6874
6991
|
}
|
|
6875
|
-
var import_sharp3;
|
|
6992
|
+
var import_sharp3, MAX_ANNOTATIONS = 40;
|
|
6876
6993
|
var init_annotate = __esm(() => {
|
|
6877
6994
|
init_snapshot();
|
|
6878
6995
|
import_sharp3 = __toESM(require_lib(), 1);
|
|
@@ -10855,6 +10972,10 @@ var coerce = {
|
|
|
10855
10972
|
date: (arg) => ZodDate.create({ ...arg, coerce: true })
|
|
10856
10973
|
};
|
|
10857
10974
|
var NEVER = INVALID;
|
|
10975
|
+
// src/mcp/index.ts
|
|
10976
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
10977
|
+
import { join as join8 } from "path";
|
|
10978
|
+
|
|
10858
10979
|
// src/types/index.ts
|
|
10859
10980
|
class BrowserError extends Error {
|
|
10860
10981
|
code;
|
|
@@ -10922,7 +11043,14 @@ import { randomUUID } from "crypto";
|
|
|
10922
11043
|
function createSession(data) {
|
|
10923
11044
|
const db = getDatabase();
|
|
10924
11045
|
const id = randomUUID();
|
|
10925
|
-
|
|
11046
|
+
let name = data.name ?? null;
|
|
11047
|
+
if (name) {
|
|
11048
|
+
const existing = db.query("SELECT id FROM sessions WHERE name = ?").get(name);
|
|
11049
|
+
if (existing) {
|
|
11050
|
+
name = `${name}-${id.slice(0, 6)}`;
|
|
11051
|
+
}
|
|
11052
|
+
}
|
|
11053
|
+
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);
|
|
10926
11054
|
return getSession(id);
|
|
10927
11055
|
}
|
|
10928
11056
|
function getSessionByName(name) {
|
|
@@ -11092,14 +11220,413 @@ async function connectLightpanda(port) {
|
|
|
11092
11220
|
}
|
|
11093
11221
|
}
|
|
11094
11222
|
|
|
11223
|
+
// src/engines/bun-webview.ts
|
|
11224
|
+
import { join as join2 } from "path";
|
|
11225
|
+
import { mkdirSync as mkdirSync2 } from "fs";
|
|
11226
|
+
import { homedir as homedir2 } from "os";
|
|
11227
|
+
function isBunWebViewAvailable() {
|
|
11228
|
+
return typeof globalThis.Bun !== "undefined" && typeof globalThis.Bun.WebView !== "undefined";
|
|
11229
|
+
}
|
|
11230
|
+
function getProfileDir(profileName) {
|
|
11231
|
+
const base = process.env["BROWSER_DATA_DIR"] ?? join2(homedir2(), ".browser");
|
|
11232
|
+
const dir = join2(base, "profiles", profileName);
|
|
11233
|
+
mkdirSync2(dir, { recursive: true });
|
|
11234
|
+
return dir;
|
|
11235
|
+
}
|
|
11236
|
+
|
|
11237
|
+
class BunWebViewSession {
|
|
11238
|
+
view;
|
|
11239
|
+
_sessionId;
|
|
11240
|
+
_eventListeners = new Map;
|
|
11241
|
+
constructor(opts = {}) {
|
|
11242
|
+
if (!isBunWebViewAvailable()) {
|
|
11243
|
+
throw new Error("Bun.WebView is not available. Install Bun canary: bun upgrade --canary");
|
|
11244
|
+
}
|
|
11245
|
+
const BunWebView = globalThis.Bun.WebView;
|
|
11246
|
+
const constructorOpts = {
|
|
11247
|
+
width: opts.width ?? 1280,
|
|
11248
|
+
height: opts.height ?? 720
|
|
11249
|
+
};
|
|
11250
|
+
if (opts.profile) {
|
|
11251
|
+
constructorOpts.dataStore = { directory: getProfileDir(opts.profile) };
|
|
11252
|
+
} else {
|
|
11253
|
+
constructorOpts.dataStore = "ephemeral";
|
|
11254
|
+
}
|
|
11255
|
+
if (opts.onConsole) {
|
|
11256
|
+
constructorOpts.console = opts.onConsole;
|
|
11257
|
+
}
|
|
11258
|
+
this.view = new BunWebView(constructorOpts);
|
|
11259
|
+
this.view.onNavigated = (url) => {
|
|
11260
|
+
this._emit("navigated", url);
|
|
11261
|
+
};
|
|
11262
|
+
this.view.onNavigationFailed = (error) => {
|
|
11263
|
+
this._emit("navigationfailed", error);
|
|
11264
|
+
};
|
|
11265
|
+
}
|
|
11266
|
+
async goto(url, opts) {
|
|
11267
|
+
await this.view.navigate(url);
|
|
11268
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
11269
|
+
}
|
|
11270
|
+
async goBack() {
|
|
11271
|
+
await this.view.goBack();
|
|
11272
|
+
}
|
|
11273
|
+
async goForward() {
|
|
11274
|
+
await this.view.goForward();
|
|
11275
|
+
}
|
|
11276
|
+
async reload() {
|
|
11277
|
+
await this.view.reload();
|
|
11278
|
+
}
|
|
11279
|
+
async evaluate(fnOrExpr, ...args) {
|
|
11280
|
+
let expr;
|
|
11281
|
+
if (typeof fnOrExpr === "function") {
|
|
11282
|
+
const serializedArgs = args.map((a) => JSON.stringify(a)).join(", ");
|
|
11283
|
+
expr = `(${fnOrExpr.toString()})(${serializedArgs})`;
|
|
11284
|
+
} else {
|
|
11285
|
+
expr = fnOrExpr;
|
|
11286
|
+
}
|
|
11287
|
+
return this.view.evaluate(expr);
|
|
11288
|
+
}
|
|
11289
|
+
async screenshot(opts) {
|
|
11290
|
+
const uint8 = await this.view.screenshot();
|
|
11291
|
+
return Buffer.from(uint8);
|
|
11292
|
+
}
|
|
11293
|
+
async click(selector, opts) {
|
|
11294
|
+
await this.view.click(selector, opts ? { button: opts.button } : undefined);
|
|
11295
|
+
}
|
|
11296
|
+
async type(selector, text, opts) {
|
|
11297
|
+
try {
|
|
11298
|
+
await this.view.click(selector);
|
|
11299
|
+
} catch {}
|
|
11300
|
+
await this.view.type(text);
|
|
11301
|
+
}
|
|
11302
|
+
async fill(selector, value) {
|
|
11303
|
+
await this.view.evaluate(`
|
|
11304
|
+
(() => {
|
|
11305
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
11306
|
+
if (el) { el.value = ''; el.dispatchEvent(new Event('input')); }
|
|
11307
|
+
})()
|
|
11308
|
+
`);
|
|
11309
|
+
await this.type(selector, value);
|
|
11310
|
+
}
|
|
11311
|
+
async press(key, opts) {
|
|
11312
|
+
await this.view.press(key, opts);
|
|
11313
|
+
}
|
|
11314
|
+
async scroll(direction, amount) {
|
|
11315
|
+
const dx = direction === "left" ? -amount : direction === "right" ? amount : 0;
|
|
11316
|
+
const dy = direction === "up" ? -amount : direction === "down" ? amount : 0;
|
|
11317
|
+
await this.view.scroll(dx, dy);
|
|
11318
|
+
}
|
|
11319
|
+
async scrollIntoView(selector) {
|
|
11320
|
+
await this.view.scrollTo(selector);
|
|
11321
|
+
}
|
|
11322
|
+
async hover(selector) {
|
|
11323
|
+
try {
|
|
11324
|
+
await this.view.scrollTo(selector);
|
|
11325
|
+
} catch {}
|
|
11326
|
+
}
|
|
11327
|
+
async resize(width, height) {
|
|
11328
|
+
await this.view.resize(width, height);
|
|
11329
|
+
}
|
|
11330
|
+
async $(selector) {
|
|
11331
|
+
const exists = await this.view.evaluate(`!!document.querySelector(${JSON.stringify(selector)})`);
|
|
11332
|
+
if (!exists)
|
|
11333
|
+
return null;
|
|
11334
|
+
return {
|
|
11335
|
+
textContent: async () => this.view.evaluate(`document.querySelector(${JSON.stringify(selector)})?.textContent ?? null`)
|
|
11336
|
+
};
|
|
11337
|
+
}
|
|
11338
|
+
async $$(selector) {
|
|
11339
|
+
const count = await this.view.evaluate(`document.querySelectorAll(${JSON.stringify(selector)}).length`);
|
|
11340
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
11341
|
+
textContent: async () => this.view.evaluate(`document.querySelectorAll(${JSON.stringify(selector)})[${i}]?.textContent ?? null`)
|
|
11342
|
+
}));
|
|
11343
|
+
}
|
|
11344
|
+
async inputValue(selector) {
|
|
11345
|
+
return this.view.evaluate(`document.querySelector(${JSON.stringify(selector)})?.value ?? ''`);
|
|
11346
|
+
}
|
|
11347
|
+
async isChecked(selector) {
|
|
11348
|
+
return this.view.evaluate(`!!(document.querySelector(${JSON.stringify(selector)})?.checked)`);
|
|
11349
|
+
}
|
|
11350
|
+
async isVisible(selector) {
|
|
11351
|
+
return this.view.evaluate(`
|
|
11352
|
+
(() => {
|
|
11353
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
11354
|
+
if (!el) return false;
|
|
11355
|
+
const style = window.getComputedStyle(el);
|
|
11356
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && el.offsetWidth > 0;
|
|
11357
|
+
})()
|
|
11358
|
+
`);
|
|
11359
|
+
}
|
|
11360
|
+
async isEnabled(selector) {
|
|
11361
|
+
return this.view.evaluate(`!(document.querySelector(${JSON.stringify(selector)})?.disabled)`);
|
|
11362
|
+
}
|
|
11363
|
+
async selectOption(selector, value) {
|
|
11364
|
+
await this.view.evaluate(`
|
|
11365
|
+
(() => {
|
|
11366
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
11367
|
+
if (el) {
|
|
11368
|
+
el.value = ${JSON.stringify(value)};
|
|
11369
|
+
el.dispatchEvent(new Event('change'));
|
|
11370
|
+
}
|
|
11371
|
+
})()
|
|
11372
|
+
`);
|
|
11373
|
+
return [value];
|
|
11374
|
+
}
|
|
11375
|
+
async check(selector) {
|
|
11376
|
+
await this.view.evaluate(`
|
|
11377
|
+
(() => {
|
|
11378
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
11379
|
+
if (el && !el.checked) { el.checked = true; el.dispatchEvent(new Event('change')); }
|
|
11380
|
+
})()
|
|
11381
|
+
`);
|
|
11382
|
+
}
|
|
11383
|
+
async uncheck(selector) {
|
|
11384
|
+
await this.view.evaluate(`
|
|
11385
|
+
(() => {
|
|
11386
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
11387
|
+
if (el && el.checked) { el.checked = false; el.dispatchEvent(new Event('change')); }
|
|
11388
|
+
})()
|
|
11389
|
+
`);
|
|
11390
|
+
}
|
|
11391
|
+
async setInputFiles(selector, files) {
|
|
11392
|
+
throw new Error("File upload not supported in Bun.WebView engine. Use engine: 'playwright' instead.");
|
|
11393
|
+
}
|
|
11394
|
+
getByRole(role, opts) {
|
|
11395
|
+
const name = opts?.name?.toString() ?? "";
|
|
11396
|
+
const selector = name ? `[role="${role}"][aria-label*="${name}"], ${role}[aria-label*="${name}"]` : `[role="${role}"], ${role}`;
|
|
11397
|
+
return {
|
|
11398
|
+
click: (clickOpts) => this.click(selector, clickOpts),
|
|
11399
|
+
fill: (value) => this.fill(selector, value),
|
|
11400
|
+
check: () => this.check(selector),
|
|
11401
|
+
uncheck: () => this.uncheck(selector),
|
|
11402
|
+
isVisible: () => this.isVisible(selector),
|
|
11403
|
+
textContent: () => this.view.evaluate(`document.querySelector(${JSON.stringify(selector)})?.textContent ?? null`),
|
|
11404
|
+
inputValue: () => this.inputValue(selector),
|
|
11405
|
+
first: () => ({
|
|
11406
|
+
click: (clickOpts) => this.click(selector, clickOpts),
|
|
11407
|
+
fill: (value) => this.fill(selector, value),
|
|
11408
|
+
textContent: () => this.view.evaluate(`document.querySelector(${JSON.stringify(selector)})?.textContent ?? null`),
|
|
11409
|
+
isVisible: () => this.isVisible(selector),
|
|
11410
|
+
hover: () => this.hover(selector),
|
|
11411
|
+
boundingBox: async () => null,
|
|
11412
|
+
scrollIntoViewIfNeeded: () => this.scrollIntoView(selector),
|
|
11413
|
+
evaluate: (fn) => this.view.evaluate(`(${fn.toString()})(document.querySelector(${JSON.stringify(selector)}))`),
|
|
11414
|
+
waitFor: (opts2) => {
|
|
11415
|
+
return new Promise((resolve, reject) => {
|
|
11416
|
+
const timeout = opts2?.timeout ?? 1e4;
|
|
11417
|
+
const start = Date.now();
|
|
11418
|
+
const check = async () => {
|
|
11419
|
+
const visible = await this.isVisible(selector);
|
|
11420
|
+
if (visible)
|
|
11421
|
+
return resolve();
|
|
11422
|
+
if (Date.now() - start > timeout)
|
|
11423
|
+
return reject(new Error(`Timeout waiting for ${selector}`));
|
|
11424
|
+
setTimeout(check, 100);
|
|
11425
|
+
};
|
|
11426
|
+
check();
|
|
11427
|
+
});
|
|
11428
|
+
}
|
|
11429
|
+
}),
|
|
11430
|
+
count: async () => {
|
|
11431
|
+
const count = await this.view.evaluate(`document.querySelectorAll(${JSON.stringify(selector)}).length`);
|
|
11432
|
+
return count;
|
|
11433
|
+
},
|
|
11434
|
+
nth: (n) => ({
|
|
11435
|
+
click: (clickOpts) => this.click(selector, clickOpts),
|
|
11436
|
+
textContent: () => this.view.evaluate(`document.querySelectorAll(${JSON.stringify(selector)})[${n}]?.textContent ?? null`),
|
|
11437
|
+
isVisible: () => this.isVisible(selector)
|
|
11438
|
+
})
|
|
11439
|
+
};
|
|
11440
|
+
}
|
|
11441
|
+
getByText(text, opts) {
|
|
11442
|
+
const selector = opts?.exact ? `*:is(button, a, span, div, p, h1, h2, h3, h4, label)` : "*";
|
|
11443
|
+
return {
|
|
11444
|
+
first: () => ({
|
|
11445
|
+
click: async (clickOpts) => {
|
|
11446
|
+
await this.view.evaluate(`
|
|
11447
|
+
(() => {
|
|
11448
|
+
const text = ${JSON.stringify(text)};
|
|
11449
|
+
const all = document.querySelectorAll('*');
|
|
11450
|
+
for (const el of all) {
|
|
11451
|
+
if (el.children.length === 0 && el.textContent?.trim() === text) {
|
|
11452
|
+
el.click(); return;
|
|
11453
|
+
}
|
|
11454
|
+
}
|
|
11455
|
+
for (const el of all) {
|
|
11456
|
+
if (el.textContent?.includes(text)) { el.click(); return; }
|
|
11457
|
+
}
|
|
11458
|
+
})()
|
|
11459
|
+
`);
|
|
11460
|
+
},
|
|
11461
|
+
waitFor: (waitOpts) => {
|
|
11462
|
+
const timeout = waitOpts?.timeout ?? 1e4;
|
|
11463
|
+
return new Promise((resolve, reject) => {
|
|
11464
|
+
const start = Date.now();
|
|
11465
|
+
const check = async () => {
|
|
11466
|
+
const found = await this.view.evaluate(`document.body?.textContent?.includes(${JSON.stringify(text)})`);
|
|
11467
|
+
if (found)
|
|
11468
|
+
return resolve();
|
|
11469
|
+
if (Date.now() - start > timeout)
|
|
11470
|
+
return reject(new Error(`Timeout: text "${text}" not found`));
|
|
11471
|
+
setTimeout(check, 100);
|
|
11472
|
+
};
|
|
11473
|
+
check();
|
|
11474
|
+
});
|
|
11475
|
+
}
|
|
11476
|
+
})
|
|
11477
|
+
};
|
|
11478
|
+
}
|
|
11479
|
+
locator(selector) {
|
|
11480
|
+
return {
|
|
11481
|
+
click: (opts) => this.click(selector, opts),
|
|
11482
|
+
fill: (value) => this.fill(selector, value),
|
|
11483
|
+
scrollIntoViewIfNeeded: () => this.scrollIntoView(selector),
|
|
11484
|
+
first: () => this.getByRole("*").first(),
|
|
11485
|
+
evaluate: (fn) => this.view.evaluate(`(${fn.toString()})(document.querySelector(${JSON.stringify(selector)}))`),
|
|
11486
|
+
waitFor: (opts) => {
|
|
11487
|
+
const timeout = opts?.timeout ?? 1e4;
|
|
11488
|
+
return new Promise((resolve, reject) => {
|
|
11489
|
+
const start = Date.now();
|
|
11490
|
+
const check = async () => {
|
|
11491
|
+
const exists = await this.view.evaluate(`!!document.querySelector(${JSON.stringify(selector)})`);
|
|
11492
|
+
if (exists)
|
|
11493
|
+
return resolve();
|
|
11494
|
+
if (Date.now() - start > timeout)
|
|
11495
|
+
return reject(new Error(`Timeout: ${selector}`));
|
|
11496
|
+
setTimeout(check, 100);
|
|
11497
|
+
};
|
|
11498
|
+
check();
|
|
11499
|
+
});
|
|
11500
|
+
}
|
|
11501
|
+
};
|
|
11502
|
+
}
|
|
11503
|
+
url() {
|
|
11504
|
+
return this.view.url;
|
|
11505
|
+
}
|
|
11506
|
+
async title() {
|
|
11507
|
+
return this.view.title || await this.evaluate("document.title");
|
|
11508
|
+
}
|
|
11509
|
+
viewportSize() {
|
|
11510
|
+
return { width: 1280, height: 720 };
|
|
11511
|
+
}
|
|
11512
|
+
async waitForLoadState(state, opts) {
|
|
11513
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
11514
|
+
}
|
|
11515
|
+
async waitForURL(pattern, opts) {
|
|
11516
|
+
const timeout = opts?.timeout ?? 30000;
|
|
11517
|
+
const start = Date.now();
|
|
11518
|
+
while (Date.now() - start < timeout) {
|
|
11519
|
+
const url = this.view.url;
|
|
11520
|
+
const matches = pattern instanceof RegExp ? pattern.test(url) : url.includes(pattern);
|
|
11521
|
+
if (matches)
|
|
11522
|
+
return;
|
|
11523
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
11524
|
+
}
|
|
11525
|
+
throw new Error(`Timeout waiting for URL to match ${pattern}`);
|
|
11526
|
+
}
|
|
11527
|
+
async waitForSelector(selector, opts) {
|
|
11528
|
+
const timeout = opts?.timeout ?? 1e4;
|
|
11529
|
+
const start = Date.now();
|
|
11530
|
+
while (Date.now() - start < timeout) {
|
|
11531
|
+
const exists = await this.view.evaluate(`!!document.querySelector(${JSON.stringify(selector)})`);
|
|
11532
|
+
if (exists)
|
|
11533
|
+
return;
|
|
11534
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
11535
|
+
}
|
|
11536
|
+
throw new Error(`Timeout waiting for ${selector}`);
|
|
11537
|
+
}
|
|
11538
|
+
async setContent(html) {
|
|
11539
|
+
await this.view.navigate(`data:text/html,${encodeURIComponent(html)}`);
|
|
11540
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
11541
|
+
}
|
|
11542
|
+
async content() {
|
|
11543
|
+
return this.view.evaluate("document.documentElement.outerHTML");
|
|
11544
|
+
}
|
|
11545
|
+
async addInitScript(script) {
|
|
11546
|
+
const expr = typeof script === "function" ? `(${script.toString()})()` : script;
|
|
11547
|
+
await this.view.evaluate(expr);
|
|
11548
|
+
}
|
|
11549
|
+
keyboard = {
|
|
11550
|
+
press: (key) => this.view.press(key)
|
|
11551
|
+
};
|
|
11552
|
+
context() {
|
|
11553
|
+
return {
|
|
11554
|
+
close: async () => {
|
|
11555
|
+
await this.close();
|
|
11556
|
+
},
|
|
11557
|
+
newPage: async () => {
|
|
11558
|
+
throw new Error("Multi-tab not supported in Bun.WebView. Use engine: 'playwright'");
|
|
11559
|
+
},
|
|
11560
|
+
cookies: async () => [],
|
|
11561
|
+
addCookies: async (_) => {},
|
|
11562
|
+
clearCookies: async () => {},
|
|
11563
|
+
newCDPSession: async () => {
|
|
11564
|
+
throw new Error("CDP session via context not available in Bun.WebView. Use view.cdp() when shipped.");
|
|
11565
|
+
},
|
|
11566
|
+
route: async (_pattern, _handler) => {
|
|
11567
|
+
throw new Error("Network interception not supported in Bun.WebView. Use engine: 'cdp' or 'playwright'.");
|
|
11568
|
+
},
|
|
11569
|
+
unrouteAll: async () => {},
|
|
11570
|
+
pages: () => [],
|
|
11571
|
+
addInitScript: async (script) => {
|
|
11572
|
+
await this.addInitScript(script);
|
|
11573
|
+
}
|
|
11574
|
+
};
|
|
11575
|
+
}
|
|
11576
|
+
on(event, handler) {
|
|
11577
|
+
if (!this._eventListeners.has(event))
|
|
11578
|
+
this._eventListeners.set(event, []);
|
|
11579
|
+
this._eventListeners.get(event).push(handler);
|
|
11580
|
+
return this;
|
|
11581
|
+
}
|
|
11582
|
+
off(event, handler) {
|
|
11583
|
+
const listeners = this._eventListeners.get(event) ?? [];
|
|
11584
|
+
this._eventListeners.set(event, listeners.filter((l) => l !== handler));
|
|
11585
|
+
return this;
|
|
11586
|
+
}
|
|
11587
|
+
_emit(event, ...args) {
|
|
11588
|
+
for (const handler of this._eventListeners.get(event) ?? []) {
|
|
11589
|
+
try {
|
|
11590
|
+
handler(...args);
|
|
11591
|
+
} catch {}
|
|
11592
|
+
}
|
|
11593
|
+
}
|
|
11594
|
+
async pdf(_opts) {
|
|
11595
|
+
throw new Error("PDF generation not supported in Bun.WebView. Use engine: 'playwright'.");
|
|
11596
|
+
}
|
|
11597
|
+
coverage = {
|
|
11598
|
+
startJSCoverage: async () => {},
|
|
11599
|
+
stopJSCoverage: async () => [],
|
|
11600
|
+
startCSSCoverage: async () => {},
|
|
11601
|
+
stopCSSCoverage: async () => []
|
|
11602
|
+
};
|
|
11603
|
+
setSessionId(id) {
|
|
11604
|
+
this._sessionId = id;
|
|
11605
|
+
}
|
|
11606
|
+
getSessionId() {
|
|
11607
|
+
return this._sessionId;
|
|
11608
|
+
}
|
|
11609
|
+
getNativeView() {
|
|
11610
|
+
return this.view;
|
|
11611
|
+
}
|
|
11612
|
+
async close() {
|
|
11613
|
+
try {
|
|
11614
|
+
await this.view.close();
|
|
11615
|
+
} catch {}
|
|
11616
|
+
}
|
|
11617
|
+
[Symbol.asyncDispose]() {
|
|
11618
|
+
return this.close();
|
|
11619
|
+
}
|
|
11620
|
+
}
|
|
11621
|
+
|
|
11095
11622
|
// src/engines/selector.ts
|
|
11096
11623
|
var ENGINE_MAP = {
|
|
11097
|
-
["scrape" /* SCRAPE */]: "
|
|
11098
|
-
["extract_links" /* EXTRACT_LINKS */]: "
|
|
11099
|
-
["status_check" /* STATUS_CHECK */]: "
|
|
11624
|
+
["scrape" /* SCRAPE */]: "bun",
|
|
11625
|
+
["extract_links" /* EXTRACT_LINKS */]: "bun",
|
|
11626
|
+
["status_check" /* STATUS_CHECK */]: "bun",
|
|
11627
|
+
["screenshot" /* SCREENSHOT */]: "bun",
|
|
11628
|
+
["spa_navigate" /* SPA_NAVIGATE */]: "bun",
|
|
11100
11629
|
["form_fill" /* FORM_FILL */]: "playwright",
|
|
11101
|
-
["spa_navigate" /* SPA_NAVIGATE */]: "playwright",
|
|
11102
|
-
["screenshot" /* SCREENSHOT */]: "playwright",
|
|
11103
11630
|
["auth_flow" /* AUTH_FLOW */]: "playwright",
|
|
11104
11631
|
["multi_tab" /* MULTI_TAB */]: "playwright",
|
|
11105
11632
|
["record_replay" /* RECORD_REPLAY */]: "playwright",
|
|
@@ -11113,6 +11640,14 @@ function selectEngine(useCase, explicit) {
|
|
|
11113
11640
|
if (explicit && explicit !== "auto")
|
|
11114
11641
|
return explicit;
|
|
11115
11642
|
const preferred = ENGINE_MAP[useCase];
|
|
11643
|
+
if (preferred === "bun") {
|
|
11644
|
+
if (isBunWebViewAvailable())
|
|
11645
|
+
return "bun";
|
|
11646
|
+
if (useCase === "scrape" /* SCRAPE */ || useCase === "extract_links" /* EXTRACT_LINKS */ || useCase === "status_check" /* STATUS_CHECK */) {
|
|
11647
|
+
return isLightpandaAvailable() ? "lightpanda" : "playwright";
|
|
11648
|
+
}
|
|
11649
|
+
return "playwright";
|
|
11650
|
+
}
|
|
11116
11651
|
if (preferred === "lightpanda" && !isLightpandaAvailable()) {
|
|
11117
11652
|
return "playwright";
|
|
11118
11653
|
}
|
|
@@ -11404,12 +11939,30 @@ async function handleDialog(sessionId, action, promptText) {
|
|
|
11404
11939
|
|
|
11405
11940
|
// src/lib/session.ts
|
|
11406
11941
|
var handles = new Map;
|
|
11942
|
+
function createBunProxy(view) {
|
|
11943
|
+
return view;
|
|
11944
|
+
}
|
|
11407
11945
|
async function createSession2(opts = {}) {
|
|
11408
11946
|
const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
|
|
11409
11947
|
const resolvedEngine = engine === "auto" ? "playwright" : engine;
|
|
11410
|
-
let browser;
|
|
11948
|
+
let browser = null;
|
|
11949
|
+
let bunView = null;
|
|
11411
11950
|
let page;
|
|
11412
|
-
if (resolvedEngine === "
|
|
11951
|
+
if (resolvedEngine === "bun") {
|
|
11952
|
+
if (!isBunWebViewAvailable()) {
|
|
11953
|
+
console.warn("[browser] Bun.WebView requested but not available \u2014 falling back to playwright. Run: bun upgrade --canary");
|
|
11954
|
+
browser = await launchPlaywright({ headless: opts.headless ?? true, viewport: opts.viewport, userAgent: opts.userAgent });
|
|
11955
|
+
page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
|
|
11956
|
+
} else {
|
|
11957
|
+
bunView = new BunWebViewSession({
|
|
11958
|
+
width: opts.viewport?.width ?? 1280,
|
|
11959
|
+
height: opts.viewport?.height ?? 720,
|
|
11960
|
+
profile: opts.name ?? undefined
|
|
11961
|
+
});
|
|
11962
|
+
if (opts.stealth) {}
|
|
11963
|
+
page = createBunProxy(bunView);
|
|
11964
|
+
}
|
|
11965
|
+
} else if (resolvedEngine === "lightpanda") {
|
|
11413
11966
|
browser = await connectLightpanda();
|
|
11414
11967
|
const context = await browser.newContext({ viewport: opts.viewport ?? { width: 1280, height: 720 } });
|
|
11415
11968
|
page = await context.newPage();
|
|
@@ -11419,41 +11972,67 @@ async function createSession2(opts = {}) {
|
|
|
11419
11972
|
viewport: opts.viewport,
|
|
11420
11973
|
userAgent: opts.userAgent
|
|
11421
11974
|
});
|
|
11422
|
-
page = await getPage(browser, {
|
|
11423
|
-
viewport: opts.viewport,
|
|
11424
|
-
userAgent: opts.userAgent
|
|
11425
|
-
});
|
|
11975
|
+
page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
|
|
11426
11976
|
}
|
|
11977
|
+
const sessionName = opts.name ?? (opts.startUrl ? (() => {
|
|
11978
|
+
try {
|
|
11979
|
+
return new URL(opts.startUrl).hostname;
|
|
11980
|
+
} catch {
|
|
11981
|
+
return;
|
|
11982
|
+
}
|
|
11983
|
+
})() : undefined);
|
|
11427
11984
|
const session = createSession({
|
|
11428
|
-
engine: resolvedEngine,
|
|
11985
|
+
engine: bunView ? "bun" : browser ? resolvedEngine : resolvedEngine,
|
|
11429
11986
|
projectId: opts.projectId,
|
|
11430
11987
|
agentId: opts.agentId,
|
|
11431
11988
|
startUrl: opts.startUrl,
|
|
11432
|
-
name:
|
|
11989
|
+
name: sessionName
|
|
11433
11990
|
});
|
|
11434
|
-
if (opts.stealth) {
|
|
11991
|
+
if (opts.stealth && !bunView) {
|
|
11435
11992
|
try {
|
|
11436
11993
|
await applyStealthPatches(page);
|
|
11437
11994
|
} catch {}
|
|
11438
11995
|
}
|
|
11439
11996
|
const cleanups = [];
|
|
11440
|
-
if (
|
|
11441
|
-
|
|
11442
|
-
|
|
11443
|
-
|
|
11444
|
-
|
|
11445
|
-
|
|
11997
|
+
if (!bunView) {
|
|
11998
|
+
if (opts.captureNetwork !== false) {
|
|
11999
|
+
try {
|
|
12000
|
+
cleanups.push(enableNetworkLogging(page, session.id));
|
|
12001
|
+
} catch {}
|
|
12002
|
+
}
|
|
12003
|
+
if (opts.captureConsole !== false) {
|
|
12004
|
+
try {
|
|
12005
|
+
cleanups.push(enableConsoleCapture(page, session.id));
|
|
12006
|
+
} catch {}
|
|
12007
|
+
}
|
|
11446
12008
|
try {
|
|
11447
|
-
cleanups.push(
|
|
12009
|
+
cleanups.push(setupDialogHandler(page, session.id));
|
|
11448
12010
|
} catch {}
|
|
12011
|
+
} else {
|
|
12012
|
+
if (opts.captureConsole !== false) {
|
|
12013
|
+
try {
|
|
12014
|
+
const { logConsoleMessage: logConsoleMessage2 } = await Promise.resolve().then(() => (init_console_log(), exports_console_log));
|
|
12015
|
+
await bunView.addInitScript(`
|
|
12016
|
+
(() => {
|
|
12017
|
+
const orig = { log: console.log, warn: console.warn, error: console.error, debug: console.debug, info: console.info };
|
|
12018
|
+
['log','warn','error','debug','info'].forEach(level => {
|
|
12019
|
+
console[level] = (...args) => {
|
|
12020
|
+
orig[level](...args);
|
|
12021
|
+
};
|
|
12022
|
+
});
|
|
12023
|
+
})()
|
|
12024
|
+
`);
|
|
12025
|
+
} catch {}
|
|
12026
|
+
}
|
|
11449
12027
|
}
|
|
11450
|
-
|
|
11451
|
-
cleanups.push(setupDialogHandler(page, session.id));
|
|
11452
|
-
} catch {}
|
|
11453
|
-
handles.set(session.id, { browser, page, engine: resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 } });
|
|
12028
|
+
handles.set(session.id, { browser, bunView, page, engine: bunView ? "bun" : resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 } });
|
|
11454
12029
|
if (opts.startUrl) {
|
|
11455
12030
|
try {
|
|
11456
|
-
|
|
12031
|
+
if (bunView) {
|
|
12032
|
+
await bunView.goto(opts.startUrl);
|
|
12033
|
+
} else {
|
|
12034
|
+
await page.goto(opts.startUrl, { waitUntil: "domcontentloaded" });
|
|
12035
|
+
}
|
|
11457
12036
|
} catch {}
|
|
11458
12037
|
}
|
|
11459
12038
|
return { session, page };
|
|
@@ -11463,13 +12042,23 @@ function getSessionPage(sessionId) {
|
|
|
11463
12042
|
if (!handle)
|
|
11464
12043
|
throw new SessionNotFoundError(sessionId);
|
|
11465
12044
|
try {
|
|
11466
|
-
handle.
|
|
12045
|
+
if (handle.bunView) {
|
|
12046
|
+
handle.bunView.url();
|
|
12047
|
+
} else {
|
|
12048
|
+
handle.page.url();
|
|
12049
|
+
}
|
|
11467
12050
|
} catch {
|
|
11468
12051
|
handles.delete(sessionId);
|
|
11469
12052
|
throw new SessionNotFoundError(sessionId);
|
|
11470
12053
|
}
|
|
11471
12054
|
return handle.page;
|
|
11472
12055
|
}
|
|
12056
|
+
function getSessionBunView(sessionId) {
|
|
12057
|
+
return handles.get(sessionId)?.bunView ?? null;
|
|
12058
|
+
}
|
|
12059
|
+
function isBunSession(sessionId) {
|
|
12060
|
+
return handles.get(sessionId)?.engine === "bun";
|
|
12061
|
+
}
|
|
11473
12062
|
function setSessionPage(sessionId, page) {
|
|
11474
12063
|
const handle = handles.get(sessionId);
|
|
11475
12064
|
if (!handle)
|
|
@@ -11484,12 +12073,19 @@ async function closeSession2(sessionId) {
|
|
|
11484
12073
|
cleanup();
|
|
11485
12074
|
} catch {}
|
|
11486
12075
|
}
|
|
11487
|
-
|
|
11488
|
-
|
|
11489
|
-
|
|
11490
|
-
|
|
11491
|
-
|
|
11492
|
-
|
|
12076
|
+
if (handle.bunView) {
|
|
12077
|
+
try {
|
|
12078
|
+
await handle.bunView.close();
|
|
12079
|
+
} catch {}
|
|
12080
|
+
} else {
|
|
12081
|
+
try {
|
|
12082
|
+
await handle.page.context().close();
|
|
12083
|
+
} catch {}
|
|
12084
|
+
try {
|
|
12085
|
+
if (handle.browser)
|
|
12086
|
+
await closeBrowser(handle.browser);
|
|
12087
|
+
} catch {}
|
|
12088
|
+
}
|
|
11493
12089
|
handles.delete(sessionId);
|
|
11494
12090
|
}
|
|
11495
12091
|
return closeSession(sessionId);
|
|
@@ -11825,9 +12421,19 @@ async function getLinks(page, baseUrl) {
|
|
|
11825
12421
|
}, baseUrl ?? page.url());
|
|
11826
12422
|
}
|
|
11827
12423
|
async function getTitle(page) {
|
|
12424
|
+
if (typeof page.getNativeView === "function") {
|
|
12425
|
+
const nativeView = page.getNativeView();
|
|
12426
|
+
const t = nativeView?.title;
|
|
12427
|
+
return typeof t === "string" && t ? t : "";
|
|
12428
|
+
}
|
|
11828
12429
|
return page.title();
|
|
11829
12430
|
}
|
|
11830
12431
|
async function getUrl(page) {
|
|
12432
|
+
if (typeof page.getNativeView === "function") {
|
|
12433
|
+
const nativeView = page.getNativeView();
|
|
12434
|
+
const u = nativeView?.url;
|
|
12435
|
+
return typeof u === "string" ? u : "";
|
|
12436
|
+
}
|
|
11831
12437
|
return page.url();
|
|
11832
12438
|
}
|
|
11833
12439
|
async function findElements(page, selector) {
|
|
@@ -11918,9 +12524,9 @@ async function getPageInfo(page) {
|
|
|
11918
12524
|
|
|
11919
12525
|
// src/lib/screenshot.ts
|
|
11920
12526
|
var import_sharp = __toESM(require_lib(), 1);
|
|
11921
|
-
import { join as
|
|
11922
|
-
import { mkdirSync as
|
|
11923
|
-
import { homedir as
|
|
12527
|
+
import { join as join3 } from "path";
|
|
12528
|
+
import { mkdirSync as mkdirSync3 } from "fs";
|
|
12529
|
+
import { homedir as homedir3 } from "os";
|
|
11924
12530
|
|
|
11925
12531
|
// src/db/gallery.ts
|
|
11926
12532
|
init_schema();
|
|
@@ -12066,13 +12672,13 @@ function getGalleryStats(projectId) {
|
|
|
12066
12672
|
|
|
12067
12673
|
// src/lib/screenshot.ts
|
|
12068
12674
|
function getDataDir2() {
|
|
12069
|
-
return process.env["BROWSER_DATA_DIR"] ??
|
|
12675
|
+
return process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser");
|
|
12070
12676
|
}
|
|
12071
12677
|
function getScreenshotDir(projectId) {
|
|
12072
|
-
const base =
|
|
12678
|
+
const base = join3(getDataDir2(), "screenshots");
|
|
12073
12679
|
const date = new Date().toISOString().split("T")[0];
|
|
12074
|
-
const dir = projectId ?
|
|
12075
|
-
|
|
12680
|
+
const dir = projectId ? join3(base, projectId, date) : join3(base, date);
|
|
12681
|
+
mkdirSync3(dir, { recursive: true });
|
|
12076
12682
|
return dir;
|
|
12077
12683
|
}
|
|
12078
12684
|
async function compressBuffer(raw, format, quality, maxWidth) {
|
|
@@ -12087,7 +12693,7 @@ async function compressBuffer(raw, format, quality, maxWidth) {
|
|
|
12087
12693
|
}
|
|
12088
12694
|
}
|
|
12089
12695
|
async function generateThumbnail(raw, dir, stem) {
|
|
12090
|
-
const thumbPath =
|
|
12696
|
+
const thumbPath = join3(dir, `${stem}.thumb.webp`);
|
|
12091
12697
|
const thumbBuffer = await import_sharp.default(raw).resize({ width: 200, withoutEnlargement: true }).webp({ quality: 70, effort: 3 }).toBuffer();
|
|
12092
12698
|
await Bun.write(thumbPath, thumbBuffer);
|
|
12093
12699
|
return { path: thumbPath, base64: thumbBuffer.toString("base64") };
|
|
@@ -12106,27 +12712,45 @@ async function takeScreenshot(page, opts) {
|
|
|
12106
12712
|
type: "png"
|
|
12107
12713
|
};
|
|
12108
12714
|
let rawBuffer;
|
|
12715
|
+
const isBunView = typeof page.getNativeView === "function";
|
|
12109
12716
|
if (opts?.selector) {
|
|
12110
|
-
|
|
12111
|
-
|
|
12112
|
-
|
|
12113
|
-
|
|
12717
|
+
if (isBunView) {
|
|
12718
|
+
const uint8 = await page.screenshot();
|
|
12719
|
+
rawBuffer = Buffer.from(uint8 instanceof Uint8Array ? uint8 : await uint8);
|
|
12720
|
+
} else {
|
|
12721
|
+
const el = await page.$(opts.selector);
|
|
12722
|
+
if (!el)
|
|
12723
|
+
throw new BrowserError(`Element not found: ${opts.selector}`, "ELEMENT_NOT_FOUND");
|
|
12724
|
+
rawBuffer = await el.screenshot(rawOpts);
|
|
12725
|
+
}
|
|
12726
|
+
} else if (isBunView) {
|
|
12727
|
+
const uint8 = await page.screenshot();
|
|
12728
|
+
rawBuffer = Buffer.from(uint8 instanceof Uint8Array ? uint8 : await uint8);
|
|
12114
12729
|
} else {
|
|
12115
12730
|
rawBuffer = await page.screenshot(rawOpts);
|
|
12116
12731
|
}
|
|
12117
12732
|
const originalSizeBytes = rawBuffer.length;
|
|
12118
12733
|
let finalBuffer;
|
|
12119
|
-
|
|
12120
|
-
|
|
12121
|
-
|
|
12122
|
-
|
|
12123
|
-
|
|
12734
|
+
let compressed = true;
|
|
12735
|
+
let fallback = false;
|
|
12736
|
+
try {
|
|
12737
|
+
if (compress && format !== "png") {
|
|
12738
|
+
finalBuffer = await compressBuffer(rawBuffer, format, quality ?? 82, maxWidth);
|
|
12739
|
+
} else if (compress && format === "png") {
|
|
12740
|
+
finalBuffer = await compressBuffer(rawBuffer, "png", quality ?? 9, maxWidth);
|
|
12741
|
+
} else {
|
|
12742
|
+
finalBuffer = rawBuffer;
|
|
12743
|
+
compressed = false;
|
|
12744
|
+
}
|
|
12745
|
+
} catch (sharpErr) {
|
|
12746
|
+
fallback = true;
|
|
12747
|
+
compressed = false;
|
|
12124
12748
|
finalBuffer = rawBuffer;
|
|
12125
12749
|
}
|
|
12126
12750
|
const compressedSizeBytes = finalBuffer.length;
|
|
12127
12751
|
const compressionRatio = originalSizeBytes > 0 ? compressedSizeBytes / originalSizeBytes : 1;
|
|
12128
12752
|
const ext = format;
|
|
12129
|
-
const screenshotPath = opts?.path ??
|
|
12753
|
+
const screenshotPath = opts?.path ?? join3(dir, `${stem}.${ext}`);
|
|
12130
12754
|
await Bun.write(screenshotPath, finalBuffer);
|
|
12131
12755
|
let thumbnailPath;
|
|
12132
12756
|
let thumbnailBase64;
|
|
@@ -12148,7 +12772,8 @@ async function takeScreenshot(page, opts) {
|
|
|
12148
12772
|
compressed_size_bytes: compressedSizeBytes,
|
|
12149
12773
|
compression_ratio: compressionRatio,
|
|
12150
12774
|
thumbnail_path: thumbnailPath,
|
|
12151
|
-
thumbnail_base64: thumbnailBase64
|
|
12775
|
+
thumbnail_base64: thumbnailBase64,
|
|
12776
|
+
...fallback ? { fallback: true, compressed: false } : {}
|
|
12152
12777
|
};
|
|
12153
12778
|
if (opts?.track !== false) {
|
|
12154
12779
|
try {
|
|
@@ -12185,12 +12810,12 @@ async function takeScreenshot(page, opts) {
|
|
|
12185
12810
|
}
|
|
12186
12811
|
async function generatePDF(page, opts) {
|
|
12187
12812
|
try {
|
|
12188
|
-
const base =
|
|
12813
|
+
const base = join3(getDataDir2(), "pdfs");
|
|
12189
12814
|
const date = new Date().toISOString().split("T")[0];
|
|
12190
|
-
const dir = opts?.projectId ?
|
|
12191
|
-
|
|
12815
|
+
const dir = opts?.projectId ? join3(base, opts.projectId, date) : join3(base, date);
|
|
12816
|
+
mkdirSync3(dir, { recursive: true });
|
|
12192
12817
|
const timestamp = Date.now();
|
|
12193
|
-
const pdfPath = opts?.path ??
|
|
12818
|
+
const pdfPath = opts?.path ?? join3(dir, `${timestamp}.pdf`);
|
|
12194
12819
|
const buffer = await page.pdf({
|
|
12195
12820
|
path: pdfPath,
|
|
12196
12821
|
format: opts?.format ?? "A4",
|
|
@@ -12713,16 +13338,16 @@ init_console_log();
|
|
|
12713
13338
|
|
|
12714
13339
|
// src/lib/downloads.ts
|
|
12715
13340
|
import { randomUUID as randomUUID9 } from "crypto";
|
|
12716
|
-
import { join as
|
|
12717
|
-
import { mkdirSync as
|
|
12718
|
-
import { homedir as
|
|
13341
|
+
import { join as join4, basename, extname } from "path";
|
|
13342
|
+
import { mkdirSync as mkdirSync4, existsSync, readdirSync, statSync, unlinkSync, copyFileSync, writeFileSync, readFileSync } from "fs";
|
|
13343
|
+
import { homedir as homedir4 } from "os";
|
|
12719
13344
|
function getDataDir3() {
|
|
12720
|
-
return process.env["BROWSER_DATA_DIR"] ??
|
|
13345
|
+
return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
|
|
12721
13346
|
}
|
|
12722
13347
|
function getDownloadsDir(sessionId) {
|
|
12723
|
-
const base =
|
|
12724
|
-
const dir = sessionId ?
|
|
12725
|
-
|
|
13348
|
+
const base = join4(getDataDir3(), "downloads");
|
|
13349
|
+
const dir = sessionId ? join4(base, sessionId) : base;
|
|
13350
|
+
mkdirSync4(dir, { recursive: true });
|
|
12726
13351
|
return dir;
|
|
12727
13352
|
}
|
|
12728
13353
|
function metaPath(filePath) {
|
|
@@ -12734,7 +13359,7 @@ function saveToDownloads(buffer, filename, opts) {
|
|
|
12734
13359
|
const ext = extname(filename) || "";
|
|
12735
13360
|
const stem = basename(filename, ext);
|
|
12736
13361
|
const uniqueName = `${stem}-${id.slice(0, 8)}${ext}`;
|
|
12737
|
-
const filePath =
|
|
13362
|
+
const filePath = join4(dir, uniqueName);
|
|
12738
13363
|
writeFileSync(filePath, buffer);
|
|
12739
13364
|
const meta = {
|
|
12740
13365
|
id,
|
|
@@ -12769,7 +13394,7 @@ function listDownloads(sessionId) {
|
|
|
12769
13394
|
for (const entry of entries) {
|
|
12770
13395
|
if (entry.endsWith(".meta.json"))
|
|
12771
13396
|
continue;
|
|
12772
|
-
const full =
|
|
13397
|
+
const full = join4(d, entry);
|
|
12773
13398
|
const stat = statSync(full);
|
|
12774
13399
|
if (stat.isDirectory()) {
|
|
12775
13400
|
scanDir(full);
|
|
@@ -12854,9 +13479,9 @@ function detectType(filename) {
|
|
|
12854
13479
|
|
|
12855
13480
|
// src/lib/gallery-diff.ts
|
|
12856
13481
|
var import_sharp2 = __toESM(require_lib(), 1);
|
|
12857
|
-
import { join as
|
|
12858
|
-
import { mkdirSync as
|
|
12859
|
-
import { homedir as
|
|
13482
|
+
import { join as join5 } from "path";
|
|
13483
|
+
import { mkdirSync as mkdirSync5 } from "fs";
|
|
13484
|
+
import { homedir as homedir5 } from "os";
|
|
12860
13485
|
async function diffImages(path1, path2) {
|
|
12861
13486
|
const img1 = import_sharp2.default(path1);
|
|
12862
13487
|
const img2 = import_sharp2.default(path2);
|
|
@@ -12887,10 +13512,10 @@ async function diffImages(path1, path2) {
|
|
|
12887
13512
|
diffBuffer[i + 2] = Math.round(raw1[i + 2] * 0.4);
|
|
12888
13513
|
}
|
|
12889
13514
|
}
|
|
12890
|
-
const dataDir = process.env["BROWSER_DATA_DIR"] ??
|
|
12891
|
-
const diffDir =
|
|
12892
|
-
|
|
12893
|
-
const diffPath =
|
|
13515
|
+
const dataDir = process.env["BROWSER_DATA_DIR"] ?? join5(homedir5(), ".browser");
|
|
13516
|
+
const diffDir = join5(dataDir, "diffs");
|
|
13517
|
+
mkdirSync5(diffDir, { recursive: true });
|
|
13518
|
+
const diffPath = join5(diffDir, `diff-${Date.now()}.webp`);
|
|
12894
13519
|
const diffImageBuffer = await import_sharp2.default(diffBuffer, { raw: { width: w, height: h, channels } }).webp({ quality: 85 }).toBuffer();
|
|
12895
13520
|
await Bun.write(diffPath, diffImageBuffer);
|
|
12896
13521
|
return {
|
|
@@ -12906,9 +13531,9 @@ async function diffImages(path1, path2) {
|
|
|
12906
13531
|
init_snapshot();
|
|
12907
13532
|
|
|
12908
13533
|
// src/lib/files-integration.ts
|
|
12909
|
-
import { join as
|
|
12910
|
-
import { mkdirSync as
|
|
12911
|
-
import { homedir as
|
|
13534
|
+
import { join as join6 } from "path";
|
|
13535
|
+
import { mkdirSync as mkdirSync6, copyFileSync as copyFileSync2 } from "fs";
|
|
13536
|
+
import { homedir as homedir6 } from "os";
|
|
12912
13537
|
async function persistFile(localPath, opts) {
|
|
12913
13538
|
try {
|
|
12914
13539
|
const mod = await import("@hasna/files");
|
|
@@ -12917,12 +13542,12 @@ async function persistFile(localPath, opts) {
|
|
|
12917
13542
|
return { id: ref.id, path: ref.path ?? localPath, permanent: true, provider: "open-files" };
|
|
12918
13543
|
}
|
|
12919
13544
|
} catch {}
|
|
12920
|
-
const dataDir = process.env["BROWSER_DATA_DIR"] ??
|
|
13545
|
+
const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
|
|
12921
13546
|
const date = new Date().toISOString().split("T")[0];
|
|
12922
|
-
const dir =
|
|
12923
|
-
|
|
13547
|
+
const dir = join6(dataDir, "persistent", date);
|
|
13548
|
+
mkdirSync6(dir, { recursive: true });
|
|
12924
13549
|
const filename = localPath.split("/").pop() ?? "file";
|
|
12925
|
-
const targetPath =
|
|
13550
|
+
const targetPath = join6(dir, filename);
|
|
12926
13551
|
copyFileSync2(localPath, targetPath);
|
|
12927
13552
|
return {
|
|
12928
13553
|
id: `local-${Date.now()}`,
|
|
@@ -13014,23 +13639,23 @@ async function closeTab(page, index) {
|
|
|
13014
13639
|
}
|
|
13015
13640
|
|
|
13016
13641
|
// src/lib/profiles.ts
|
|
13017
|
-
import { mkdirSync as
|
|
13018
|
-
import { join as
|
|
13019
|
-
import { homedir as
|
|
13642
|
+
import { mkdirSync as mkdirSync7, existsSync as existsSync3, readdirSync as readdirSync2, rmSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
13643
|
+
import { join as join7 } from "path";
|
|
13644
|
+
import { homedir as homedir7 } from "os";
|
|
13020
13645
|
function getProfilesDir() {
|
|
13021
|
-
const dataDir = process.env["BROWSER_DATA_DIR"] ??
|
|
13022
|
-
const dir =
|
|
13023
|
-
|
|
13646
|
+
const dataDir = process.env["BROWSER_DATA_DIR"] ?? join7(homedir7(), ".browser");
|
|
13647
|
+
const dir = join7(dataDir, "profiles");
|
|
13648
|
+
mkdirSync7(dir, { recursive: true });
|
|
13024
13649
|
return dir;
|
|
13025
13650
|
}
|
|
13026
|
-
function
|
|
13027
|
-
return
|
|
13651
|
+
function getProfileDir2(name) {
|
|
13652
|
+
return join7(getProfilesDir(), name);
|
|
13028
13653
|
}
|
|
13029
13654
|
async function saveProfile(page, name) {
|
|
13030
|
-
const dir =
|
|
13031
|
-
|
|
13655
|
+
const dir = getProfileDir2(name);
|
|
13656
|
+
mkdirSync7(dir, { recursive: true });
|
|
13032
13657
|
const cookies = await page.context().cookies();
|
|
13033
|
-
writeFileSync2(
|
|
13658
|
+
writeFileSync2(join7(dir, "cookies.json"), JSON.stringify(cookies, null, 2));
|
|
13034
13659
|
let localStorage2 = {};
|
|
13035
13660
|
try {
|
|
13036
13661
|
localStorage2 = await page.evaluate(() => {
|
|
@@ -13042,11 +13667,11 @@ async function saveProfile(page, name) {
|
|
|
13042
13667
|
return result;
|
|
13043
13668
|
});
|
|
13044
13669
|
} catch {}
|
|
13045
|
-
writeFileSync2(
|
|
13670
|
+
writeFileSync2(join7(dir, "storage.json"), JSON.stringify(localStorage2, null, 2));
|
|
13046
13671
|
const savedAt = new Date().toISOString();
|
|
13047
13672
|
const url = page.url();
|
|
13048
13673
|
const meta = { saved_at: savedAt, url };
|
|
13049
|
-
writeFileSync2(
|
|
13674
|
+
writeFileSync2(join7(dir, "meta.json"), JSON.stringify(meta, null, 2));
|
|
13050
13675
|
return {
|
|
13051
13676
|
name,
|
|
13052
13677
|
saved_at: savedAt,
|
|
@@ -13056,13 +13681,13 @@ async function saveProfile(page, name) {
|
|
|
13056
13681
|
};
|
|
13057
13682
|
}
|
|
13058
13683
|
function loadProfile(name) {
|
|
13059
|
-
const dir =
|
|
13684
|
+
const dir = getProfileDir2(name);
|
|
13060
13685
|
if (!existsSync3(dir)) {
|
|
13061
13686
|
throw new Error(`Profile not found: ${name}`);
|
|
13062
13687
|
}
|
|
13063
|
-
const cookiesPath =
|
|
13064
|
-
const storagePath =
|
|
13065
|
-
const metaPath2 =
|
|
13688
|
+
const cookiesPath = join7(dir, "cookies.json");
|
|
13689
|
+
const storagePath = join7(dir, "storage.json");
|
|
13690
|
+
const metaPath2 = join7(dir, "meta.json");
|
|
13066
13691
|
const cookies = existsSync3(cookiesPath) ? JSON.parse(readFileSync2(cookiesPath, "utf8")) : [];
|
|
13067
13692
|
const localStorage2 = existsSync3(storagePath) ? JSON.parse(readFileSync2(storagePath, "utf8")) : {};
|
|
13068
13693
|
let savedAt = new Date().toISOString();
|
|
@@ -13103,24 +13728,24 @@ function listProfiles() {
|
|
|
13103
13728
|
if (!entry.isDirectory())
|
|
13104
13729
|
continue;
|
|
13105
13730
|
const name = entry.name;
|
|
13106
|
-
const profileDir =
|
|
13731
|
+
const profileDir = join7(dir, name);
|
|
13107
13732
|
let savedAt = "";
|
|
13108
13733
|
let url;
|
|
13109
13734
|
let cookieCount = 0;
|
|
13110
13735
|
let storageKeyCount = 0;
|
|
13111
13736
|
try {
|
|
13112
|
-
const metaPath2 =
|
|
13737
|
+
const metaPath2 = join7(profileDir, "meta.json");
|
|
13113
13738
|
if (existsSync3(metaPath2)) {
|
|
13114
13739
|
const meta = JSON.parse(readFileSync2(metaPath2, "utf8"));
|
|
13115
13740
|
savedAt = meta.saved_at ?? "";
|
|
13116
13741
|
url = meta.url;
|
|
13117
13742
|
}
|
|
13118
|
-
const cookiesPath =
|
|
13743
|
+
const cookiesPath = join7(profileDir, "cookies.json");
|
|
13119
13744
|
if (existsSync3(cookiesPath)) {
|
|
13120
13745
|
const cookies = JSON.parse(readFileSync2(cookiesPath, "utf8"));
|
|
13121
13746
|
cookieCount = Array.isArray(cookies) ? cookies.length : 0;
|
|
13122
13747
|
}
|
|
13123
|
-
const storagePath =
|
|
13748
|
+
const storagePath = join7(profileDir, "storage.json");
|
|
13124
13749
|
if (existsSync3(storagePath)) {
|
|
13125
13750
|
const storage = JSON.parse(readFileSync2(storagePath, "utf8"));
|
|
13126
13751
|
storageKeyCount = Object.keys(storage).length;
|
|
@@ -13137,7 +13762,7 @@ function listProfiles() {
|
|
|
13137
13762
|
return profiles.sort((a, b) => b.saved_at.localeCompare(a.saved_at));
|
|
13138
13763
|
}
|
|
13139
13764
|
function deleteProfile(name) {
|
|
13140
|
-
const dir =
|
|
13765
|
+
const dir = getProfileDir2(name);
|
|
13141
13766
|
if (!existsSync3(dir))
|
|
13142
13767
|
return false;
|
|
13143
13768
|
try {
|
|
@@ -13149,6 +13774,7 @@ function deleteProfile(name) {
|
|
|
13149
13774
|
}
|
|
13150
13775
|
|
|
13151
13776
|
// src/mcp/index.ts
|
|
13777
|
+
var _pkg = JSON.parse(readFileSync3(join8(import.meta.dir, "../../package.json"), "utf8"));
|
|
13152
13778
|
var networkLogCleanup = new Map;
|
|
13153
13779
|
var consoleCaptureCleanup = new Map;
|
|
13154
13780
|
var harCaptures = new Map;
|
|
@@ -13168,7 +13794,7 @@ var server = new McpServer({
|
|
|
13168
13794
|
version: "0.0.1"
|
|
13169
13795
|
});
|
|
13170
13796
|
server.tool("browser_session_create", "Create a new browser session with the specified engine", {
|
|
13171
|
-
engine: exports_external.enum(["playwright", "cdp", "lightpanda", "auto"]).optional().default("auto"),
|
|
13797
|
+
engine: exports_external.enum(["playwright", "cdp", "lightpanda", "bun", "auto"]).optional().default("auto"),
|
|
13172
13798
|
use_case: exports_external.string().optional(),
|
|
13173
13799
|
project_id: exports_external.string().optional(),
|
|
13174
13800
|
agent_id: exports_external.string().optional(),
|
|
@@ -13214,23 +13840,71 @@ server.tool("browser_session_close", "Close a browser session", { session_id: ex
|
|
|
13214
13840
|
return err(e);
|
|
13215
13841
|
}
|
|
13216
13842
|
});
|
|
13217
|
-
server.tool("browser_navigate", "Navigate to a URL.
|
|
13843
|
+
server.tool("browser_navigate", "Navigate to a URL. Auto-detects redirects, auto-names session, returns compact refs + thumbnail.", {
|
|
13844
|
+
session_id: exports_external.string(),
|
|
13845
|
+
url: exports_external.string(),
|
|
13846
|
+
timeout: exports_external.number().optional().default(30000),
|
|
13847
|
+
auto_snapshot: exports_external.boolean().optional().default(true),
|
|
13848
|
+
auto_thumbnail: exports_external.boolean().optional().default(true)
|
|
13849
|
+
}, async ({ session_id, url, timeout, auto_snapshot, auto_thumbnail }) => {
|
|
13218
13850
|
try {
|
|
13219
13851
|
const page = getSessionPage(session_id);
|
|
13220
|
-
|
|
13852
|
+
if (isBunSession(session_id)) {
|
|
13853
|
+
const bunView = getSessionBunView(session_id);
|
|
13854
|
+
await bunView.goto(url, { timeout });
|
|
13855
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
13856
|
+
} else {
|
|
13857
|
+
await navigate(page, url, timeout);
|
|
13858
|
+
}
|
|
13221
13859
|
const title = await getTitle(page);
|
|
13222
13860
|
const current_url = await getUrl(page);
|
|
13223
|
-
const
|
|
13861
|
+
const redirected = current_url !== url && current_url !== url + "/" && url !== current_url.replace(/\/$/, "");
|
|
13862
|
+
let redirect_type;
|
|
13863
|
+
if (redirected) {
|
|
13864
|
+
try {
|
|
13865
|
+
const reqHost = new URL(url).hostname;
|
|
13866
|
+
const resHost = new URL(current_url).hostname;
|
|
13867
|
+
const reqPath = new URL(url).pathname;
|
|
13868
|
+
const resPath = new URL(current_url).pathname;
|
|
13869
|
+
if (reqHost !== resHost)
|
|
13870
|
+
redirect_type = "canonical";
|
|
13871
|
+
else if (resPath.match(/\/[a-z]{2}-[a-z]{2}\//))
|
|
13872
|
+
redirect_type = "geo";
|
|
13873
|
+
else if (current_url.includes("login") || current_url.includes("signin"))
|
|
13874
|
+
redirect_type = "auth";
|
|
13875
|
+
else
|
|
13876
|
+
redirect_type = "unknown";
|
|
13877
|
+
} catch {}
|
|
13878
|
+
}
|
|
13879
|
+
try {
|
|
13880
|
+
const session = getSession2(session_id);
|
|
13881
|
+
if (!session.name) {
|
|
13882
|
+
const hostname = new URL(current_url).hostname;
|
|
13883
|
+
renameSession2(session_id, hostname);
|
|
13884
|
+
}
|
|
13885
|
+
} catch {}
|
|
13886
|
+
const result = {
|
|
13887
|
+
url,
|
|
13888
|
+
title,
|
|
13889
|
+
current_url,
|
|
13890
|
+
redirected,
|
|
13891
|
+
...redirect_type ? { redirect_type } : {}
|
|
13892
|
+
};
|
|
13224
13893
|
if (auto_thumbnail) {
|
|
13225
13894
|
try {
|
|
13226
13895
|
const ss = await takeScreenshot(page, { maxWidth: 400, quality: 60, track: false, thumbnail: false });
|
|
13227
13896
|
result.thumbnail_base64 = ss.base64.length > 50000 ? "" : ss.base64;
|
|
13228
13897
|
} catch {}
|
|
13229
13898
|
}
|
|
13899
|
+
if (isBunSession(session_id) && auto_snapshot) {
|
|
13900
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
13901
|
+
}
|
|
13230
13902
|
if (auto_snapshot) {
|
|
13231
13903
|
try {
|
|
13232
13904
|
const snap = await takeSnapshot(page, session_id);
|
|
13233
|
-
|
|
13905
|
+
setLastSnapshot(session_id, snap);
|
|
13906
|
+
const refEntries = Object.entries(snap.refs).slice(0, 30);
|
|
13907
|
+
result.snapshot_refs = refEntries.map(([ref, info]) => `${info.role}:${info.name.slice(0, 50)} [${ref}]`).join(", ");
|
|
13234
13908
|
result.interactive_count = snap.interactive_count;
|
|
13235
13909
|
result.has_errors = getConsoleLog(session_id, "error").length > 0;
|
|
13236
13910
|
} catch {}
|
|
@@ -13336,7 +14010,7 @@ server.tool("browser_select", "Select a dropdown option by ref or selector", { s
|
|
|
13336
14010
|
return err(e);
|
|
13337
14011
|
}
|
|
13338
14012
|
});
|
|
13339
|
-
server.tool("
|
|
14013
|
+
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 }) => {
|
|
13340
14014
|
try {
|
|
13341
14015
|
const page = getSessionPage(session_id);
|
|
13342
14016
|
if (ref) {
|
|
@@ -13427,12 +14101,33 @@ server.tool("browser_find", "Find elements matching a selector and return their
|
|
|
13427
14101
|
return err(e);
|
|
13428
14102
|
}
|
|
13429
14103
|
});
|
|
13430
|
-
server.tool("browser_snapshot", "Get
|
|
14104
|
+
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.", {
|
|
14105
|
+
session_id: exports_external.string(),
|
|
14106
|
+
compact: exports_external.boolean().optional().default(true),
|
|
14107
|
+
max_refs: exports_external.number().optional().default(50),
|
|
14108
|
+
full_tree: exports_external.boolean().optional().default(false)
|
|
14109
|
+
}, async ({ session_id, compact, max_refs, full_tree }) => {
|
|
13431
14110
|
try {
|
|
13432
14111
|
const page = getSessionPage(session_id);
|
|
13433
14112
|
const result = await takeSnapshot(page, session_id);
|
|
13434
14113
|
setLastSnapshot(session_id, result);
|
|
13435
|
-
|
|
14114
|
+
const refEntries = Object.entries(result.refs).slice(0, max_refs);
|
|
14115
|
+
const limitedRefs = Object.fromEntries(refEntries);
|
|
14116
|
+
const truncated = Object.keys(result.refs).length > max_refs;
|
|
14117
|
+
if (compact && !full_tree) {
|
|
14118
|
+
const compactRefs = refEntries.map(([ref, info]) => `${info.role}:${info.name.slice(0, 60)} [${ref}]${info.checked !== undefined ? ` checked=${info.checked}` : ""}${!info.enabled ? " disabled" : ""}`).join(`
|
|
14119
|
+
`);
|
|
14120
|
+
return json({
|
|
14121
|
+
snapshot_compact: compactRefs,
|
|
14122
|
+
interactive_count: result.interactive_count,
|
|
14123
|
+
shown_count: refEntries.length,
|
|
14124
|
+
truncated,
|
|
14125
|
+
refs: limitedRefs
|
|
14126
|
+
});
|
|
14127
|
+
}
|
|
14128
|
+
const tree = full_tree ? result.tree : result.tree.slice(0, 5000) + (result.tree.length > 5000 ? `
|
|
14129
|
+
... (truncated \u2014 use full_tree=true for complete)` : "");
|
|
14130
|
+
return json({ snapshot: tree, refs: limitedRefs, interactive_count: result.interactive_count, truncated });
|
|
13436
14131
|
} catch (e) {
|
|
13437
14132
|
return err(e);
|
|
13438
14133
|
}
|
|
@@ -13711,7 +14406,7 @@ server.tool("browser_crawl", "Crawl a URL recursively and return discovered page
|
|
|
13711
14406
|
max_pages: exports_external.number().optional().default(50),
|
|
13712
14407
|
same_domain: exports_external.boolean().optional().default(true),
|
|
13713
14408
|
project_id: exports_external.string().optional(),
|
|
13714
|
-
engine: exports_external.enum(["playwright", "cdp", "lightpanda", "auto"]).optional().default("auto")
|
|
14409
|
+
engine: exports_external.enum(["playwright", "cdp", "lightpanda", "bun", "auto"]).optional().default("auto")
|
|
13715
14410
|
}, async ({ url, max_depth, max_pages, same_domain, project_id, engine }) => {
|
|
13716
14411
|
try {
|
|
13717
14412
|
const result = await crawl(url, {
|
|
@@ -13914,40 +14609,6 @@ server.tool("browser_watch_stop", "Stop a DOM change watcher", { watch_id: expor
|
|
|
13914
14609
|
return err(e);
|
|
13915
14610
|
}
|
|
13916
14611
|
});
|
|
13917
|
-
server.tool("browser_page_check", "One-call page summary: page info + console errors + performance metrics + thumbnail + accessibility snapshot preview. Replaces 4-5 separate tool calls.", { session_id: exports_external.string() }, async ({ session_id }) => {
|
|
13918
|
-
try {
|
|
13919
|
-
const page = getSessionPage(session_id);
|
|
13920
|
-
const info = await getPageInfo(page);
|
|
13921
|
-
const errors2 = getConsoleLog(session_id, "error");
|
|
13922
|
-
info.has_console_errors = errors2.length > 0;
|
|
13923
|
-
let perf = {};
|
|
13924
|
-
try {
|
|
13925
|
-
perf = await getPerformanceMetrics(page);
|
|
13926
|
-
} catch {}
|
|
13927
|
-
let thumbnail_base64 = "";
|
|
13928
|
-
try {
|
|
13929
|
-
const ss = await takeScreenshot(page, { maxWidth: 400, quality: 60, track: false, thumbnail: false });
|
|
13930
|
-
thumbnail_base64 = ss.base64;
|
|
13931
|
-
} catch {}
|
|
13932
|
-
let snapshot_preview = "";
|
|
13933
|
-
let interactive_count = 0;
|
|
13934
|
-
try {
|
|
13935
|
-
const snap = await takeSnapshot(page, session_id);
|
|
13936
|
-
snapshot_preview = snap.tree.slice(0, 2000);
|
|
13937
|
-
interactive_count = snap.interactive_count;
|
|
13938
|
-
} catch {}
|
|
13939
|
-
return json({
|
|
13940
|
-
...info,
|
|
13941
|
-
error_count: errors2.length,
|
|
13942
|
-
performance: perf,
|
|
13943
|
-
thumbnail_base64: thumbnail_base64.length > 50000 ? "" : thumbnail_base64,
|
|
13944
|
-
snapshot_preview,
|
|
13945
|
-
interactive_count
|
|
13946
|
-
});
|
|
13947
|
-
} catch (e) {
|
|
13948
|
-
return err(e);
|
|
13949
|
-
}
|
|
13950
|
-
});
|
|
13951
14612
|
server.tool("browser_gallery_list", "List screenshot gallery entries with optional filters", {
|
|
13952
14613
|
project_id: exports_external.string().optional(),
|
|
13953
14614
|
session_id: exports_external.string().optional(),
|
|
@@ -14272,7 +14933,7 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
|
|
|
14272
14933
|
{ tool: "browser_hover", description: "Hover over an element" },
|
|
14273
14934
|
{ tool: "browser_scroll", description: "Scroll the page" },
|
|
14274
14935
|
{ tool: "browser_select", description: "Select a dropdown option" },
|
|
14275
|
-
{ tool: "
|
|
14936
|
+
{ tool: "browser_toggle", description: "Check/uncheck a checkbox" },
|
|
14276
14937
|
{ tool: "browser_upload", description: "Upload a file to an input" },
|
|
14277
14938
|
{ tool: "browser_press_key", description: "Press a keyboard key" },
|
|
14278
14939
|
{ tool: "browser_wait", description: "Wait for a selector to appear" },
|
|
@@ -14292,9 +14953,10 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
|
|
|
14292
14953
|
{ tool: "browser_evaluate", description: "Execute JavaScript in page context" }
|
|
14293
14954
|
],
|
|
14294
14955
|
Capture: [
|
|
14295
|
-
{ tool: "browser_screenshot", description: "Take a screenshot (PNG/JPEG/WebP)" },
|
|
14956
|
+
{ tool: "browser_screenshot", description: "Take a screenshot (PNG/JPEG/WebP, annotate=true for labels)" },
|
|
14296
14957
|
{ tool: "browser_pdf", description: "Generate a PDF of the page" },
|
|
14297
|
-
{ tool: "browser_scroll_and_screenshot", description: "Scroll then screenshot in one call" }
|
|
14958
|
+
{ tool: "browser_scroll_and_screenshot", description: "Scroll then screenshot in one call" },
|
|
14959
|
+
{ tool: "browser_scroll_to_element", description: "Scroll element into view + screenshot" }
|
|
14298
14960
|
],
|
|
14299
14961
|
Storage: [
|
|
14300
14962
|
{ tool: "browser_cookies_get", description: "Get cookies" },
|
|
@@ -14373,7 +15035,8 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
|
|
|
14373
15035
|
{ tool: "browser_tab_close", description: "Close a tab by index" }
|
|
14374
15036
|
],
|
|
14375
15037
|
Meta: [
|
|
14376
|
-
{ tool: "
|
|
15038
|
+
{ tool: "browser_check", description: "RECOMMENDED: One-call page summary with diagnostics" },
|
|
15039
|
+
{ tool: "browser_version", description: "Show running binary version and tool count" },
|
|
14377
15040
|
{ tool: "browser_help", description: "Show this help (all tools)" },
|
|
14378
15041
|
{ tool: "browser_snapshot_diff", description: "Diff current snapshot vs previous" },
|
|
14379
15042
|
{ tool: "browser_watch_start", description: "Watch page for DOM changes" },
|
|
@@ -14387,5 +15050,87 @@ server.tool("browser_help", "Show all available browser tools grouped by categor
|
|
|
14387
15050
|
return err(e);
|
|
14388
15051
|
}
|
|
14389
15052
|
});
|
|
15053
|
+
server.tool("browser_version", "Get the running browser MCP version, tool count, and environment info. Use this to verify which binary is active.", {}, async () => {
|
|
15054
|
+
try {
|
|
15055
|
+
const { getDataDir: getDataDir4 } = await Promise.resolve().then(() => (init_schema(), exports_schema));
|
|
15056
|
+
const toolCount = Object.keys(server._registeredTools ?? {}).length;
|
|
15057
|
+
return json({
|
|
15058
|
+
version: _pkg.version,
|
|
15059
|
+
mcp_tools_count: toolCount,
|
|
15060
|
+
bun_version: Bun.version,
|
|
15061
|
+
data_dir: getDataDir4(),
|
|
15062
|
+
node_env: process.env["NODE_ENV"] ?? "production"
|
|
15063
|
+
});
|
|
15064
|
+
} catch (e) {
|
|
15065
|
+
return err(e);
|
|
15066
|
+
}
|
|
15067
|
+
});
|
|
15068
|
+
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.", {
|
|
15069
|
+
session_id: exports_external.string(),
|
|
15070
|
+
selector: exports_external.string().optional(),
|
|
15071
|
+
ref: exports_external.string().optional(),
|
|
15072
|
+
screenshot: exports_external.boolean().optional().default(true),
|
|
15073
|
+
wait_ms: exports_external.number().optional().default(200)
|
|
15074
|
+
}, async ({ session_id, selector, ref, screenshot: doScreenshot, wait_ms }) => {
|
|
15075
|
+
try {
|
|
15076
|
+
const page = getSessionPage(session_id);
|
|
15077
|
+
let locator;
|
|
15078
|
+
if (ref) {
|
|
15079
|
+
const { getRefLocator: getRefLocator2 } = await Promise.resolve().then(() => (init_snapshot(), exports_snapshot));
|
|
15080
|
+
locator = getRefLocator2(page, session_id, ref);
|
|
15081
|
+
} else if (selector) {
|
|
15082
|
+
locator = page.locator(selector).first();
|
|
15083
|
+
} else {
|
|
15084
|
+
return err(new Error("Either ref or selector is required"));
|
|
15085
|
+
}
|
|
15086
|
+
await locator.scrollIntoViewIfNeeded();
|
|
15087
|
+
await new Promise((r) => setTimeout(r, wait_ms));
|
|
15088
|
+
const result = { scrolled: ref ?? selector };
|
|
15089
|
+
if (doScreenshot) {
|
|
15090
|
+
try {
|
|
15091
|
+
const ss = await takeScreenshot(page, { selector, track: false });
|
|
15092
|
+
ss.url = page.url();
|
|
15093
|
+
if (ss.base64.length > 50000) {
|
|
15094
|
+
ss.base64_truncated = true;
|
|
15095
|
+
ss.base64 = ss.thumbnail_base64 ?? "";
|
|
15096
|
+
}
|
|
15097
|
+
result.screenshot = ss;
|
|
15098
|
+
} catch {}
|
|
15099
|
+
}
|
|
15100
|
+
return json(result);
|
|
15101
|
+
} catch (e) {
|
|
15102
|
+
return err(e);
|
|
15103
|
+
}
|
|
15104
|
+
});
|
|
15105
|
+
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 }) => {
|
|
15106
|
+
try {
|
|
15107
|
+
const page = getSessionPage(session_id);
|
|
15108
|
+
const info = await getPageInfo(page);
|
|
15109
|
+
const errors2 = getConsoleLog(session_id, "error");
|
|
15110
|
+
info.has_console_errors = errors2.length > 0;
|
|
15111
|
+
let perf = {};
|
|
15112
|
+
try {
|
|
15113
|
+
perf = await getPerformanceMetrics(page);
|
|
15114
|
+
} catch {}
|
|
15115
|
+
let thumbnail_base64 = "";
|
|
15116
|
+
try {
|
|
15117
|
+
const ss = await takeScreenshot(page, { maxWidth: 400, quality: 60, track: false, thumbnail: false });
|
|
15118
|
+
thumbnail_base64 = ss.base64.length > 50000 ? "" : ss.base64;
|
|
15119
|
+
} catch {}
|
|
15120
|
+
let snapshot_refs = "";
|
|
15121
|
+
let interactive_count = 0;
|
|
15122
|
+
try {
|
|
15123
|
+
const snap = await takeSnapshot(page, session_id);
|
|
15124
|
+
setLastSnapshot(session_id, snap);
|
|
15125
|
+
interactive_count = snap.interactive_count;
|
|
15126
|
+
snapshot_refs = Object.entries(snap.refs).slice(0, 30).map(([ref, i]) => `${i.role}:${i.name.slice(0, 50)} [${ref}]`).join(", ");
|
|
15127
|
+
} catch {}
|
|
15128
|
+
return json({ ...info, error_count: errors2.length, performance: perf, thumbnail_base64, snapshot_refs, interactive_count });
|
|
15129
|
+
} catch (e) {
|
|
15130
|
+
return err(e);
|
|
15131
|
+
}
|
|
15132
|
+
});
|
|
15133
|
+
var _startupToolCount = Object.keys(server._registeredTools ?? {}).length;
|
|
15134
|
+
console.error(`@hasna/browser v${_pkg.version} \u2014 ${_startupToolCount} tools | data: ${(await Promise.resolve().then(() => (init_schema(), exports_schema))).getDataDir()}`);
|
|
14390
15135
|
var transport = new StdioServerTransport;
|
|
14391
15136
|
await server.connect(transport);
|