@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/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
- for (const [ref, info] of Object.entries(snapshot.refs)) {
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
- 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, data.name ?? null);
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 */]: "lightpanda",
11098
- ["extract_links" /* EXTRACT_LINKS */]: "lightpanda",
11099
- ["status_check" /* STATUS_CHECK */]: "lightpanda",
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 === "lightpanda") {
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: opts.name ?? (opts.startUrl ? new URL(opts.startUrl).hostname : undefined)
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 (opts.captureNetwork !== false) {
11441
- try {
11442
- cleanups.push(enableNetworkLogging(page, session.id));
11443
- } catch {}
11444
- }
11445
- if (opts.captureConsole !== false) {
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(enableConsoleCapture(page, session.id));
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
- try {
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
- await page.goto(opts.startUrl, { waitUntil: "domcontentloaded" });
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.page.url();
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
- try {
11488
- await handle.page.context().close();
11489
- } catch {}
11490
- try {
11491
- await closeBrowser(handle.browser);
11492
- } catch {}
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 join2 } from "path";
11922
- import { mkdirSync as mkdirSync2 } from "fs";
11923
- import { homedir as homedir2 } from "os";
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"] ?? join2(homedir2(), ".browser");
12675
+ return process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser");
12070
12676
  }
12071
12677
  function getScreenshotDir(projectId) {
12072
- const base = join2(getDataDir2(), "screenshots");
12678
+ const base = join3(getDataDir2(), "screenshots");
12073
12679
  const date = new Date().toISOString().split("T")[0];
12074
- const dir = projectId ? join2(base, projectId, date) : join2(base, date);
12075
- mkdirSync2(dir, { recursive: true });
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 = join2(dir, `${stem}.thumb.webp`);
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
- const el = await page.$(opts.selector);
12111
- if (!el)
12112
- throw new BrowserError(`Element not found: ${opts.selector}`, "ELEMENT_NOT_FOUND");
12113
- rawBuffer = await el.screenshot(rawOpts);
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
- if (compress && format !== "png") {
12120
- finalBuffer = await compressBuffer(rawBuffer, format, quality ?? 82, maxWidth);
12121
- } else if (compress && format === "png") {
12122
- finalBuffer = await compressBuffer(rawBuffer, "png", quality ?? 9, maxWidth);
12123
- } else {
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 ?? join2(dir, `${stem}.${ext}`);
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 = join2(getDataDir2(), "pdfs");
12813
+ const base = join3(getDataDir2(), "pdfs");
12189
12814
  const date = new Date().toISOString().split("T")[0];
12190
- const dir = opts?.projectId ? join2(base, opts.projectId, date) : join2(base, date);
12191
- mkdirSync2(dir, { recursive: true });
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 ?? join2(dir, `${timestamp}.pdf`);
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 join3, basename, extname } from "path";
12717
- import { mkdirSync as mkdirSync3, existsSync, readdirSync, statSync, unlinkSync, copyFileSync, writeFileSync, readFileSync } from "fs";
12718
- import { homedir as homedir3 } from "os";
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"] ?? join3(homedir3(), ".browser");
13345
+ return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
12721
13346
  }
12722
13347
  function getDownloadsDir(sessionId) {
12723
- const base = join3(getDataDir3(), "downloads");
12724
- const dir = sessionId ? join3(base, sessionId) : base;
12725
- mkdirSync3(dir, { recursive: true });
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 = join3(dir, uniqueName);
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 = join3(d, entry);
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 join4 } from "path";
12858
- import { mkdirSync as mkdirSync4 } from "fs";
12859
- import { homedir as homedir4 } from "os";
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"] ?? join4(homedir4(), ".browser");
12891
- const diffDir = join4(dataDir, "diffs");
12892
- mkdirSync4(diffDir, { recursive: true });
12893
- const diffPath = join4(diffDir, `diff-${Date.now()}.webp`);
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 join5 } from "path";
12910
- import { mkdirSync as mkdirSync5, copyFileSync as copyFileSync2 } from "fs";
12911
- import { homedir as homedir5 } from "os";
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"] ?? join5(homedir5(), ".browser");
13545
+ const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
12921
13546
  const date = new Date().toISOString().split("T")[0];
12922
- const dir = join5(dataDir, "persistent", date);
12923
- mkdirSync5(dir, { recursive: true });
13547
+ const dir = join6(dataDir, "persistent", date);
13548
+ mkdirSync6(dir, { recursive: true });
12924
13549
  const filename = localPath.split("/").pop() ?? "file";
12925
- const targetPath = join5(dir, filename);
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 mkdirSync6, existsSync as existsSync3, readdirSync as readdirSync2, rmSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
13018
- import { join as join6 } from "path";
13019
- import { homedir as homedir6 } from "os";
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"] ?? join6(homedir6(), ".browser");
13022
- const dir = join6(dataDir, "profiles");
13023
- mkdirSync6(dir, { recursive: true });
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 getProfileDir(name) {
13027
- return join6(getProfilesDir(), name);
13651
+ function getProfileDir2(name) {
13652
+ return join7(getProfilesDir(), name);
13028
13653
  }
13029
13654
  async function saveProfile(page, name) {
13030
- const dir = getProfileDir(name);
13031
- mkdirSync6(dir, { recursive: true });
13655
+ const dir = getProfileDir2(name);
13656
+ mkdirSync7(dir, { recursive: true });
13032
13657
  const cookies = await page.context().cookies();
13033
- writeFileSync2(join6(dir, "cookies.json"), JSON.stringify(cookies, null, 2));
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(join6(dir, "storage.json"), JSON.stringify(localStorage2, null, 2));
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(join6(dir, "meta.json"), JSON.stringify(meta, null, 2));
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 = getProfileDir(name);
13684
+ const dir = getProfileDir2(name);
13060
13685
  if (!existsSync3(dir)) {
13061
13686
  throw new Error(`Profile not found: ${name}`);
13062
13687
  }
13063
- const cookiesPath = join6(dir, "cookies.json");
13064
- const storagePath = join6(dir, "storage.json");
13065
- const metaPath2 = join6(dir, "meta.json");
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 = join6(dir, name);
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 = join6(profileDir, "meta.json");
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 = join6(profileDir, "cookies.json");
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 = join6(profileDir, "storage.json");
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 = getProfileDir(name);
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. Returns title + thumbnail + accessibility snapshot preview with refs.", { session_id: exports_external.string(), url: exports_external.string(), timeout: exports_external.number().optional().default(30000), auto_snapshot: exports_external.boolean().optional().default(true), auto_thumbnail: exports_external.boolean().optional().default(true) }, async ({ session_id, url, timeout, auto_snapshot, auto_thumbnail }) => {
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
- await navigate(page, url, timeout);
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 result = { url, title, current_url };
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
- result.snapshot_preview = snap.tree.slice(0, 3000);
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("browser_check", "Check or uncheck a checkbox by ref or selector", { session_id: exports_external.string(), selector: exports_external.string().optional(), ref: exports_external.string().optional(), checked: exports_external.boolean() }, async ({ session_id, selector, ref, checked }) => {
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 a structured accessibility snapshot with element refs (@e0, @e1...). Use refs in browser_click, browser_type, etc.", { session_id: exports_external.string() }, async ({ session_id }) => {
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
- return json({ snapshot: result.tree, refs: result.refs, interactive_count: result.interactive_count });
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: "browser_check", description: "Check/uncheck a checkbox" },
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: "browser_page_check", description: "One-call page summary with diagnostics" },
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);