@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.
@@ -273,6 +273,40 @@ function runMigrations(db) {
273
273
  var _db = null, _dbPath = null;
274
274
  var init_schema = () => {};
275
275
 
276
+ // src/db/console-log.ts
277
+ var exports_console_log = {};
278
+ __export(exports_console_log, {
279
+ logConsoleMessage: () => logConsoleMessage,
280
+ getConsoleMessage: () => getConsoleMessage,
281
+ getConsoleLog: () => getConsoleLog,
282
+ clearConsoleLog: () => clearConsoleLog
283
+ });
284
+ import { randomUUID as randomUUID3 } from "crypto";
285
+ function logConsoleMessage(data) {
286
+ const db = getDatabase();
287
+ const id = randomUUID3();
288
+ db.prepare("INSERT INTO console_log (id, session_id, level, message, source, line_number) VALUES (?, ?, ?, ?, ?, ?)").run(id, data.session_id, data.level, data.message, data.source ?? null, data.line_number ?? null);
289
+ return getConsoleMessage(id);
290
+ }
291
+ function getConsoleMessage(id) {
292
+ const db = getDatabase();
293
+ return db.query("SELECT * FROM console_log WHERE id = ?").get(id) ?? null;
294
+ }
295
+ function getConsoleLog(sessionId, level) {
296
+ const db = getDatabase();
297
+ if (level) {
298
+ return db.query("SELECT * FROM console_log WHERE session_id = ? AND level = ? ORDER BY timestamp ASC").all(sessionId, level);
299
+ }
300
+ return db.query("SELECT * FROM console_log WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
301
+ }
302
+ function clearConsoleLog(sessionId) {
303
+ const db = getDatabase();
304
+ db.prepare("DELETE FROM console_log WHERE session_id = ?").run(sessionId);
305
+ }
306
+ var init_console_log = __esm(() => {
307
+ init_schema();
308
+ });
309
+
276
310
  // node_modules/sharp/lib/is.js
277
311
  var require_is = __commonJS((exports, module) => {
278
312
  /*!
@@ -6867,7 +6901,7 @@ var init_snapshots = __esm(() => {
6867
6901
  });
6868
6902
 
6869
6903
  // src/server/index.ts
6870
- import { join as join5 } from "path";
6904
+ import { join as join6 } from "path";
6871
6905
  import { existsSync as existsSync3 } from "fs";
6872
6906
 
6873
6907
  // src/lib/session.ts
@@ -6881,7 +6915,14 @@ import { randomUUID } from "crypto";
6881
6915
  function createSession(data) {
6882
6916
  const db = getDatabase();
6883
6917
  const id = randomUUID();
6884
- 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);
6918
+ let name = data.name ?? null;
6919
+ if (name) {
6920
+ const existing = db.query("SELECT id FROM sessions WHERE name = ?").get(name);
6921
+ if (existing) {
6922
+ name = `${name}-${id.slice(0, 6)}`;
6923
+ }
6924
+ }
6925
+ 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);
6885
6926
  return getSession(id);
6886
6927
  }
6887
6928
  function getSession(id) {
@@ -7044,15 +7085,414 @@ async function connectLightpanda(port) {
7044
7085
  }
7045
7086
  }
7046
7087
 
7088
+ // src/engines/bun-webview.ts
7089
+ import { join as join2 } from "path";
7090
+ import { mkdirSync as mkdirSync2 } from "fs";
7091
+ import { homedir as homedir2 } from "os";
7092
+ function isBunWebViewAvailable() {
7093
+ return typeof globalThis.Bun !== "undefined" && typeof globalThis.Bun.WebView !== "undefined";
7094
+ }
7095
+ function getProfileDir(profileName) {
7096
+ const base = process.env["BROWSER_DATA_DIR"] ?? join2(homedir2(), ".browser");
7097
+ const dir = join2(base, "profiles", profileName);
7098
+ mkdirSync2(dir, { recursive: true });
7099
+ return dir;
7100
+ }
7101
+
7102
+ class BunWebViewSession {
7103
+ view;
7104
+ _sessionId;
7105
+ _eventListeners = new Map;
7106
+ constructor(opts = {}) {
7107
+ if (!isBunWebViewAvailable()) {
7108
+ throw new Error("Bun.WebView is not available. Install Bun canary: bun upgrade --canary");
7109
+ }
7110
+ const BunWebView = globalThis.Bun.WebView;
7111
+ const constructorOpts = {
7112
+ width: opts.width ?? 1280,
7113
+ height: opts.height ?? 720
7114
+ };
7115
+ if (opts.profile) {
7116
+ constructorOpts.dataStore = { directory: getProfileDir(opts.profile) };
7117
+ } else {
7118
+ constructorOpts.dataStore = "ephemeral";
7119
+ }
7120
+ if (opts.onConsole) {
7121
+ constructorOpts.console = opts.onConsole;
7122
+ }
7123
+ this.view = new BunWebView(constructorOpts);
7124
+ this.view.onNavigated = (url) => {
7125
+ this._emit("navigated", url);
7126
+ };
7127
+ this.view.onNavigationFailed = (error) => {
7128
+ this._emit("navigationfailed", error);
7129
+ };
7130
+ }
7131
+ async goto(url, opts) {
7132
+ await this.view.navigate(url);
7133
+ await new Promise((r) => setTimeout(r, 200));
7134
+ }
7135
+ async goBack() {
7136
+ await this.view.goBack();
7137
+ }
7138
+ async goForward() {
7139
+ await this.view.goForward();
7140
+ }
7141
+ async reload() {
7142
+ await this.view.reload();
7143
+ }
7144
+ async evaluate(fnOrExpr, ...args) {
7145
+ let expr;
7146
+ if (typeof fnOrExpr === "function") {
7147
+ const serializedArgs = args.map((a) => JSON.stringify(a)).join(", ");
7148
+ expr = `(${fnOrExpr.toString()})(${serializedArgs})`;
7149
+ } else {
7150
+ expr = fnOrExpr;
7151
+ }
7152
+ return this.view.evaluate(expr);
7153
+ }
7154
+ async screenshot(opts) {
7155
+ const uint8 = await this.view.screenshot();
7156
+ return Buffer.from(uint8);
7157
+ }
7158
+ async click(selector, opts) {
7159
+ await this.view.click(selector, opts ? { button: opts.button } : undefined);
7160
+ }
7161
+ async type(selector, text, opts) {
7162
+ try {
7163
+ await this.view.click(selector);
7164
+ } catch {}
7165
+ await this.view.type(text);
7166
+ }
7167
+ async fill(selector, value) {
7168
+ await this.view.evaluate(`
7169
+ (() => {
7170
+ const el = document.querySelector(${JSON.stringify(selector)});
7171
+ if (el) { el.value = ''; el.dispatchEvent(new Event('input')); }
7172
+ })()
7173
+ `);
7174
+ await this.type(selector, value);
7175
+ }
7176
+ async press(key, opts) {
7177
+ await this.view.press(key, opts);
7178
+ }
7179
+ async scroll(direction, amount) {
7180
+ const dx = direction === "left" ? -amount : direction === "right" ? amount : 0;
7181
+ const dy = direction === "up" ? -amount : direction === "down" ? amount : 0;
7182
+ await this.view.scroll(dx, dy);
7183
+ }
7184
+ async scrollIntoView(selector) {
7185
+ await this.view.scrollTo(selector);
7186
+ }
7187
+ async hover(selector) {
7188
+ try {
7189
+ await this.view.scrollTo(selector);
7190
+ } catch {}
7191
+ }
7192
+ async resize(width, height) {
7193
+ await this.view.resize(width, height);
7194
+ }
7195
+ async $(selector) {
7196
+ const exists = await this.view.evaluate(`!!document.querySelector(${JSON.stringify(selector)})`);
7197
+ if (!exists)
7198
+ return null;
7199
+ return {
7200
+ textContent: async () => this.view.evaluate(`document.querySelector(${JSON.stringify(selector)})?.textContent ?? null`)
7201
+ };
7202
+ }
7203
+ async $$(selector) {
7204
+ const count = await this.view.evaluate(`document.querySelectorAll(${JSON.stringify(selector)}).length`);
7205
+ return Array.from({ length: count }, (_, i) => ({
7206
+ textContent: async () => this.view.evaluate(`document.querySelectorAll(${JSON.stringify(selector)})[${i}]?.textContent ?? null`)
7207
+ }));
7208
+ }
7209
+ async inputValue(selector) {
7210
+ return this.view.evaluate(`document.querySelector(${JSON.stringify(selector)})?.value ?? ''`);
7211
+ }
7212
+ async isChecked(selector) {
7213
+ return this.view.evaluate(`!!(document.querySelector(${JSON.stringify(selector)})?.checked)`);
7214
+ }
7215
+ async isVisible(selector) {
7216
+ return this.view.evaluate(`
7217
+ (() => {
7218
+ const el = document.querySelector(${JSON.stringify(selector)});
7219
+ if (!el) return false;
7220
+ const style = window.getComputedStyle(el);
7221
+ return style.display !== 'none' && style.visibility !== 'hidden' && el.offsetWidth > 0;
7222
+ })()
7223
+ `);
7224
+ }
7225
+ async isEnabled(selector) {
7226
+ return this.view.evaluate(`!(document.querySelector(${JSON.stringify(selector)})?.disabled)`);
7227
+ }
7228
+ async selectOption(selector, value) {
7229
+ await this.view.evaluate(`
7230
+ (() => {
7231
+ const el = document.querySelector(${JSON.stringify(selector)});
7232
+ if (el) {
7233
+ el.value = ${JSON.stringify(value)};
7234
+ el.dispatchEvent(new Event('change'));
7235
+ }
7236
+ })()
7237
+ `);
7238
+ return [value];
7239
+ }
7240
+ async check(selector) {
7241
+ await this.view.evaluate(`
7242
+ (() => {
7243
+ const el = document.querySelector(${JSON.stringify(selector)});
7244
+ if (el && !el.checked) { el.checked = true; el.dispatchEvent(new Event('change')); }
7245
+ })()
7246
+ `);
7247
+ }
7248
+ async uncheck(selector) {
7249
+ await this.view.evaluate(`
7250
+ (() => {
7251
+ const el = document.querySelector(${JSON.stringify(selector)});
7252
+ if (el && el.checked) { el.checked = false; el.dispatchEvent(new Event('change')); }
7253
+ })()
7254
+ `);
7255
+ }
7256
+ async setInputFiles(selector, files) {
7257
+ throw new Error("File upload not supported in Bun.WebView engine. Use engine: 'playwright' instead.");
7258
+ }
7259
+ getByRole(role, opts) {
7260
+ const name = opts?.name?.toString() ?? "";
7261
+ const selector = name ? `[role="${role}"][aria-label*="${name}"], ${role}[aria-label*="${name}"]` : `[role="${role}"], ${role}`;
7262
+ return {
7263
+ click: (clickOpts) => this.click(selector, clickOpts),
7264
+ fill: (value) => this.fill(selector, value),
7265
+ check: () => this.check(selector),
7266
+ uncheck: () => this.uncheck(selector),
7267
+ isVisible: () => this.isVisible(selector),
7268
+ textContent: () => this.view.evaluate(`document.querySelector(${JSON.stringify(selector)})?.textContent ?? null`),
7269
+ inputValue: () => this.inputValue(selector),
7270
+ first: () => ({
7271
+ click: (clickOpts) => this.click(selector, clickOpts),
7272
+ fill: (value) => this.fill(selector, value),
7273
+ textContent: () => this.view.evaluate(`document.querySelector(${JSON.stringify(selector)})?.textContent ?? null`),
7274
+ isVisible: () => this.isVisible(selector),
7275
+ hover: () => this.hover(selector),
7276
+ boundingBox: async () => null,
7277
+ scrollIntoViewIfNeeded: () => this.scrollIntoView(selector),
7278
+ evaluate: (fn) => this.view.evaluate(`(${fn.toString()})(document.querySelector(${JSON.stringify(selector)}))`),
7279
+ waitFor: (opts2) => {
7280
+ return new Promise((resolve, reject) => {
7281
+ const timeout = opts2?.timeout ?? 1e4;
7282
+ const start = Date.now();
7283
+ const check = async () => {
7284
+ const visible = await this.isVisible(selector);
7285
+ if (visible)
7286
+ return resolve();
7287
+ if (Date.now() - start > timeout)
7288
+ return reject(new Error(`Timeout waiting for ${selector}`));
7289
+ setTimeout(check, 100);
7290
+ };
7291
+ check();
7292
+ });
7293
+ }
7294
+ }),
7295
+ count: async () => {
7296
+ const count = await this.view.evaluate(`document.querySelectorAll(${JSON.stringify(selector)}).length`);
7297
+ return count;
7298
+ },
7299
+ nth: (n) => ({
7300
+ click: (clickOpts) => this.click(selector, clickOpts),
7301
+ textContent: () => this.view.evaluate(`document.querySelectorAll(${JSON.stringify(selector)})[${n}]?.textContent ?? null`),
7302
+ isVisible: () => this.isVisible(selector)
7303
+ })
7304
+ };
7305
+ }
7306
+ getByText(text, opts) {
7307
+ const selector = opts?.exact ? `*:is(button, a, span, div, p, h1, h2, h3, h4, label)` : "*";
7308
+ return {
7309
+ first: () => ({
7310
+ click: async (clickOpts) => {
7311
+ await this.view.evaluate(`
7312
+ (() => {
7313
+ const text = ${JSON.stringify(text)};
7314
+ const all = document.querySelectorAll('*');
7315
+ for (const el of all) {
7316
+ if (el.children.length === 0 && el.textContent?.trim() === text) {
7317
+ el.click(); return;
7318
+ }
7319
+ }
7320
+ for (const el of all) {
7321
+ if (el.textContent?.includes(text)) { el.click(); return; }
7322
+ }
7323
+ })()
7324
+ `);
7325
+ },
7326
+ waitFor: (waitOpts) => {
7327
+ const timeout = waitOpts?.timeout ?? 1e4;
7328
+ return new Promise((resolve, reject) => {
7329
+ const start = Date.now();
7330
+ const check = async () => {
7331
+ const found = await this.view.evaluate(`document.body?.textContent?.includes(${JSON.stringify(text)})`);
7332
+ if (found)
7333
+ return resolve();
7334
+ if (Date.now() - start > timeout)
7335
+ return reject(new Error(`Timeout: text "${text}" not found`));
7336
+ setTimeout(check, 100);
7337
+ };
7338
+ check();
7339
+ });
7340
+ }
7341
+ })
7342
+ };
7343
+ }
7344
+ locator(selector) {
7345
+ return {
7346
+ click: (opts) => this.click(selector, opts),
7347
+ fill: (value) => this.fill(selector, value),
7348
+ scrollIntoViewIfNeeded: () => this.scrollIntoView(selector),
7349
+ first: () => this.getByRole("*").first(),
7350
+ evaluate: (fn) => this.view.evaluate(`(${fn.toString()})(document.querySelector(${JSON.stringify(selector)}))`),
7351
+ waitFor: (opts) => {
7352
+ const timeout = opts?.timeout ?? 1e4;
7353
+ return new Promise((resolve, reject) => {
7354
+ const start = Date.now();
7355
+ const check = async () => {
7356
+ const exists = await this.view.evaluate(`!!document.querySelector(${JSON.stringify(selector)})`);
7357
+ if (exists)
7358
+ return resolve();
7359
+ if (Date.now() - start > timeout)
7360
+ return reject(new Error(`Timeout: ${selector}`));
7361
+ setTimeout(check, 100);
7362
+ };
7363
+ check();
7364
+ });
7365
+ }
7366
+ };
7367
+ }
7368
+ url() {
7369
+ return this.view.url;
7370
+ }
7371
+ async title() {
7372
+ return this.view.title || await this.evaluate("document.title");
7373
+ }
7374
+ viewportSize() {
7375
+ return { width: 1280, height: 720 };
7376
+ }
7377
+ async waitForLoadState(state, opts) {
7378
+ await new Promise((r) => setTimeout(r, 200));
7379
+ }
7380
+ async waitForURL(pattern, opts) {
7381
+ const timeout = opts?.timeout ?? 30000;
7382
+ const start = Date.now();
7383
+ while (Date.now() - start < timeout) {
7384
+ const url = this.view.url;
7385
+ const matches = pattern instanceof RegExp ? pattern.test(url) : url.includes(pattern);
7386
+ if (matches)
7387
+ return;
7388
+ await new Promise((r) => setTimeout(r, 100));
7389
+ }
7390
+ throw new Error(`Timeout waiting for URL to match ${pattern}`);
7391
+ }
7392
+ async waitForSelector(selector, opts) {
7393
+ const timeout = opts?.timeout ?? 1e4;
7394
+ const start = Date.now();
7395
+ while (Date.now() - start < timeout) {
7396
+ const exists = await this.view.evaluate(`!!document.querySelector(${JSON.stringify(selector)})`);
7397
+ if (exists)
7398
+ return;
7399
+ await new Promise((r) => setTimeout(r, 100));
7400
+ }
7401
+ throw new Error(`Timeout waiting for ${selector}`);
7402
+ }
7403
+ async setContent(html) {
7404
+ await this.view.navigate(`data:text/html,${encodeURIComponent(html)}`);
7405
+ await new Promise((r) => setTimeout(r, 100));
7406
+ }
7407
+ async content() {
7408
+ return this.view.evaluate("document.documentElement.outerHTML");
7409
+ }
7410
+ async addInitScript(script) {
7411
+ const expr = typeof script === "function" ? `(${script.toString()})()` : script;
7412
+ await this.view.evaluate(expr);
7413
+ }
7414
+ keyboard = {
7415
+ press: (key) => this.view.press(key)
7416
+ };
7417
+ context() {
7418
+ return {
7419
+ close: async () => {
7420
+ await this.close();
7421
+ },
7422
+ newPage: async () => {
7423
+ throw new Error("Multi-tab not supported in Bun.WebView. Use engine: 'playwright'");
7424
+ },
7425
+ cookies: async () => [],
7426
+ addCookies: async (_) => {},
7427
+ clearCookies: async () => {},
7428
+ newCDPSession: async () => {
7429
+ throw new Error("CDP session via context not available in Bun.WebView. Use view.cdp() when shipped.");
7430
+ },
7431
+ route: async (_pattern, _handler) => {
7432
+ throw new Error("Network interception not supported in Bun.WebView. Use engine: 'cdp' or 'playwright'.");
7433
+ },
7434
+ unrouteAll: async () => {},
7435
+ pages: () => [],
7436
+ addInitScript: async (script) => {
7437
+ await this.addInitScript(script);
7438
+ }
7439
+ };
7440
+ }
7441
+ on(event, handler) {
7442
+ if (!this._eventListeners.has(event))
7443
+ this._eventListeners.set(event, []);
7444
+ this._eventListeners.get(event).push(handler);
7445
+ return this;
7446
+ }
7447
+ off(event, handler) {
7448
+ const listeners = this._eventListeners.get(event) ?? [];
7449
+ this._eventListeners.set(event, listeners.filter((l) => l !== handler));
7450
+ return this;
7451
+ }
7452
+ _emit(event, ...args) {
7453
+ for (const handler of this._eventListeners.get(event) ?? []) {
7454
+ try {
7455
+ handler(...args);
7456
+ } catch {}
7457
+ }
7458
+ }
7459
+ async pdf(_opts) {
7460
+ throw new Error("PDF generation not supported in Bun.WebView. Use engine: 'playwright'.");
7461
+ }
7462
+ coverage = {
7463
+ startJSCoverage: async () => {},
7464
+ stopJSCoverage: async () => [],
7465
+ startCSSCoverage: async () => {},
7466
+ stopCSSCoverage: async () => []
7467
+ };
7468
+ setSessionId(id) {
7469
+ this._sessionId = id;
7470
+ }
7471
+ getSessionId() {
7472
+ return this._sessionId;
7473
+ }
7474
+ getNativeView() {
7475
+ return this.view;
7476
+ }
7477
+ async close() {
7478
+ try {
7479
+ await this.view.close();
7480
+ } catch {}
7481
+ }
7482
+ [Symbol.asyncDispose]() {
7483
+ return this.close();
7484
+ }
7485
+ }
7486
+
7047
7487
  // src/engines/selector.ts
7048
7488
  init_types();
7049
7489
  var ENGINE_MAP = {
7050
- ["scrape" /* SCRAPE */]: "lightpanda",
7051
- ["extract_links" /* EXTRACT_LINKS */]: "lightpanda",
7052
- ["status_check" /* STATUS_CHECK */]: "lightpanda",
7490
+ ["scrape" /* SCRAPE */]: "bun",
7491
+ ["extract_links" /* EXTRACT_LINKS */]: "bun",
7492
+ ["status_check" /* STATUS_CHECK */]: "bun",
7493
+ ["screenshot" /* SCREENSHOT */]: "bun",
7494
+ ["spa_navigate" /* SPA_NAVIGATE */]: "bun",
7053
7495
  ["form_fill" /* FORM_FILL */]: "playwright",
7054
- ["spa_navigate" /* SPA_NAVIGATE */]: "playwright",
7055
- ["screenshot" /* SCREENSHOT */]: "playwright",
7056
7496
  ["auth_flow" /* AUTH_FLOW */]: "playwright",
7057
7497
  ["multi_tab" /* MULTI_TAB */]: "playwright",
7058
7498
  ["record_replay" /* RECORD_REPLAY */]: "playwright",
@@ -7066,6 +7506,14 @@ function selectEngine(useCase, explicit) {
7066
7506
  if (explicit && explicit !== "auto")
7067
7507
  return explicit;
7068
7508
  const preferred = ENGINE_MAP[useCase];
7509
+ if (preferred === "bun") {
7510
+ if (isBunWebViewAvailable())
7511
+ return "bun";
7512
+ if (useCase === "scrape" /* SCRAPE */ || useCase === "extract_links" /* EXTRACT_LINKS */ || useCase === "status_check" /* STATUS_CHECK */) {
7513
+ return isLightpandaAvailable() ? "lightpanda" : "playwright";
7514
+ }
7515
+ return "playwright";
7516
+ }
7069
7517
  if (preferred === "lightpanda" && !isLightpandaAvailable()) {
7070
7518
  return "playwright";
7071
7519
  }
@@ -7185,28 +7633,8 @@ function startHAR(page) {
7185
7633
  };
7186
7634
  }
7187
7635
 
7188
- // src/db/console-log.ts
7189
- init_schema();
7190
- import { randomUUID as randomUUID3 } from "crypto";
7191
- function logConsoleMessage(data) {
7192
- const db = getDatabase();
7193
- const id = randomUUID3();
7194
- db.prepare("INSERT INTO console_log (id, session_id, level, message, source, line_number) VALUES (?, ?, ?, ?, ?, ?)").run(id, data.session_id, data.level, data.message, data.source ?? null, data.line_number ?? null);
7195
- return getConsoleMessage(id);
7196
- }
7197
- function getConsoleMessage(id) {
7198
- const db = getDatabase();
7199
- return db.query("SELECT * FROM console_log WHERE id = ?").get(id) ?? null;
7200
- }
7201
- function getConsoleLog(sessionId, level) {
7202
- const db = getDatabase();
7203
- if (level) {
7204
- return db.query("SELECT * FROM console_log WHERE session_id = ? AND level = ? ORDER BY timestamp ASC").all(sessionId, level);
7205
- }
7206
- return db.query("SELECT * FROM console_log WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
7207
- }
7208
-
7209
7636
  // src/lib/console.ts
7637
+ init_console_log();
7210
7638
  function enableConsoleCapture(page, sessionId) {
7211
7639
  const onConsole = (msg) => {
7212
7640
  const levelMap = {
@@ -7341,12 +7769,30 @@ function setupDialogHandler(page, sessionId) {
7341
7769
 
7342
7770
  // src/lib/session.ts
7343
7771
  var handles = new Map;
7772
+ function createBunProxy(view) {
7773
+ return view;
7774
+ }
7344
7775
  async function createSession2(opts = {}) {
7345
7776
  const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
7346
7777
  const resolvedEngine = engine === "auto" ? "playwright" : engine;
7347
- let browser;
7778
+ let browser = null;
7779
+ let bunView = null;
7348
7780
  let page;
7349
- if (resolvedEngine === "lightpanda") {
7781
+ if (resolvedEngine === "bun") {
7782
+ if (!isBunWebViewAvailable()) {
7783
+ console.warn("[browser] Bun.WebView requested but not available \u2014 falling back to playwright. Run: bun upgrade --canary");
7784
+ browser = await launchPlaywright({ headless: opts.headless ?? true, viewport: opts.viewport, userAgent: opts.userAgent });
7785
+ page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
7786
+ } else {
7787
+ bunView = new BunWebViewSession({
7788
+ width: opts.viewport?.width ?? 1280,
7789
+ height: opts.viewport?.height ?? 720,
7790
+ profile: opts.name ?? undefined
7791
+ });
7792
+ if (opts.stealth) {}
7793
+ page = createBunProxy(bunView);
7794
+ }
7795
+ } else if (resolvedEngine === "lightpanda") {
7350
7796
  browser = await connectLightpanda();
7351
7797
  const context = await browser.newContext({ viewport: opts.viewport ?? { width: 1280, height: 720 } });
7352
7798
  page = await context.newPage();
@@ -7356,41 +7802,67 @@ async function createSession2(opts = {}) {
7356
7802
  viewport: opts.viewport,
7357
7803
  userAgent: opts.userAgent
7358
7804
  });
7359
- page = await getPage(browser, {
7360
- viewport: opts.viewport,
7361
- userAgent: opts.userAgent
7362
- });
7805
+ page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
7363
7806
  }
7807
+ const sessionName = opts.name ?? (opts.startUrl ? (() => {
7808
+ try {
7809
+ return new URL(opts.startUrl).hostname;
7810
+ } catch {
7811
+ return;
7812
+ }
7813
+ })() : undefined);
7364
7814
  const session = createSession({
7365
- engine: resolvedEngine,
7815
+ engine: bunView ? "bun" : browser ? resolvedEngine : resolvedEngine,
7366
7816
  projectId: opts.projectId,
7367
7817
  agentId: opts.agentId,
7368
7818
  startUrl: opts.startUrl,
7369
- name: opts.name ?? (opts.startUrl ? new URL(opts.startUrl).hostname : undefined)
7819
+ name: sessionName
7370
7820
  });
7371
- if (opts.stealth) {
7821
+ if (opts.stealth && !bunView) {
7372
7822
  try {
7373
7823
  await applyStealthPatches(page);
7374
7824
  } catch {}
7375
7825
  }
7376
7826
  const cleanups = [];
7377
- if (opts.captureNetwork !== false) {
7378
- try {
7379
- cleanups.push(enableNetworkLogging(page, session.id));
7380
- } catch {}
7381
- }
7382
- if (opts.captureConsole !== false) {
7827
+ if (!bunView) {
7828
+ if (opts.captureNetwork !== false) {
7829
+ try {
7830
+ cleanups.push(enableNetworkLogging(page, session.id));
7831
+ } catch {}
7832
+ }
7833
+ if (opts.captureConsole !== false) {
7834
+ try {
7835
+ cleanups.push(enableConsoleCapture(page, session.id));
7836
+ } catch {}
7837
+ }
7383
7838
  try {
7384
- cleanups.push(enableConsoleCapture(page, session.id));
7839
+ cleanups.push(setupDialogHandler(page, session.id));
7385
7840
  } catch {}
7841
+ } else {
7842
+ if (opts.captureConsole !== false) {
7843
+ try {
7844
+ const { logConsoleMessage: logConsoleMessage2 } = await Promise.resolve().then(() => (init_console_log(), exports_console_log));
7845
+ await bunView.addInitScript(`
7846
+ (() => {
7847
+ const orig = { log: console.log, warn: console.warn, error: console.error, debug: console.debug, info: console.info };
7848
+ ['log','warn','error','debug','info'].forEach(level => {
7849
+ console[level] = (...args) => {
7850
+ orig[level](...args);
7851
+ };
7852
+ });
7853
+ })()
7854
+ `);
7855
+ } catch {}
7856
+ }
7386
7857
  }
7387
- try {
7388
- cleanups.push(setupDialogHandler(page, session.id));
7389
- } catch {}
7390
- handles.set(session.id, { browser, page, engine: resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 } });
7858
+ handles.set(session.id, { browser, bunView, page, engine: bunView ? "bun" : resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 } });
7391
7859
  if (opts.startUrl) {
7392
7860
  try {
7393
- await page.goto(opts.startUrl, { waitUntil: "domcontentloaded" });
7861
+ if (bunView) {
7862
+ await bunView.goto(opts.startUrl);
7863
+ } else {
7864
+ await page.goto(opts.startUrl, { waitUntil: "domcontentloaded" });
7865
+ }
7394
7866
  } catch {}
7395
7867
  }
7396
7868
  return { session, page };
@@ -7400,7 +7872,11 @@ function getSessionPage(sessionId) {
7400
7872
  if (!handle)
7401
7873
  throw new SessionNotFoundError(sessionId);
7402
7874
  try {
7403
- handle.page.url();
7875
+ if (handle.bunView) {
7876
+ handle.bunView.url();
7877
+ } else {
7878
+ handle.page.url();
7879
+ }
7404
7880
  } catch {
7405
7881
  handles.delete(sessionId);
7406
7882
  throw new SessionNotFoundError(sessionId);
@@ -7415,12 +7891,19 @@ async function closeSession2(sessionId) {
7415
7891
  cleanup();
7416
7892
  } catch {}
7417
7893
  }
7418
- try {
7419
- await handle.page.context().close();
7420
- } catch {}
7421
- try {
7422
- await closeBrowser(handle.browser);
7423
- } catch {}
7894
+ if (handle.bunView) {
7895
+ try {
7896
+ await handle.bunView.close();
7897
+ } catch {}
7898
+ } else {
7899
+ try {
7900
+ await handle.page.context().close();
7901
+ } catch {}
7902
+ try {
7903
+ if (handle.browser)
7904
+ await closeBrowser(handle.browser);
7905
+ } catch {}
7906
+ }
7424
7907
  handles.delete(sessionId);
7425
7908
  }
7426
7909
  return closeSession(sessionId);
@@ -7562,9 +8045,9 @@ async function extract(page, opts = {}) {
7562
8045
  // src/lib/screenshot.ts
7563
8046
  init_types();
7564
8047
  var import_sharp = __toESM(require_lib(), 1);
7565
- import { join as join2 } from "path";
7566
- import { mkdirSync as mkdirSync2 } from "fs";
7567
- import { homedir as homedir2 } from "os";
8048
+ import { join as join3 } from "path";
8049
+ import { mkdirSync as mkdirSync3 } from "fs";
8050
+ import { homedir as homedir3 } from "os";
7568
8051
 
7569
8052
  // src/db/gallery.ts
7570
8053
  init_schema();
@@ -7694,13 +8177,13 @@ function getGalleryStats(projectId) {
7694
8177
 
7695
8178
  // src/lib/screenshot.ts
7696
8179
  function getDataDir2() {
7697
- return process.env["BROWSER_DATA_DIR"] ?? join2(homedir2(), ".browser");
8180
+ return process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser");
7698
8181
  }
7699
8182
  function getScreenshotDir(projectId) {
7700
- const base = join2(getDataDir2(), "screenshots");
8183
+ const base = join3(getDataDir2(), "screenshots");
7701
8184
  const date = new Date().toISOString().split("T")[0];
7702
- const dir = projectId ? join2(base, projectId, date) : join2(base, date);
7703
- mkdirSync2(dir, { recursive: true });
8185
+ const dir = projectId ? join3(base, projectId, date) : join3(base, date);
8186
+ mkdirSync3(dir, { recursive: true });
7704
8187
  return dir;
7705
8188
  }
7706
8189
  async function compressBuffer(raw, format, quality, maxWidth) {
@@ -7715,7 +8198,7 @@ async function compressBuffer(raw, format, quality, maxWidth) {
7715
8198
  }
7716
8199
  }
7717
8200
  async function generateThumbnail(raw, dir, stem) {
7718
- const thumbPath = join2(dir, `${stem}.thumb.webp`);
8201
+ const thumbPath = join3(dir, `${stem}.thumb.webp`);
7719
8202
  const thumbBuffer = await import_sharp.default(raw).resize({ width: 200, withoutEnlargement: true }).webp({ quality: 70, effort: 3 }).toBuffer();
7720
8203
  await Bun.write(thumbPath, thumbBuffer);
7721
8204
  return { path: thumbPath, base64: thumbBuffer.toString("base64") };
@@ -7734,27 +8217,45 @@ async function takeScreenshot(page, opts) {
7734
8217
  type: "png"
7735
8218
  };
7736
8219
  let rawBuffer;
8220
+ const isBunView = typeof page.getNativeView === "function";
7737
8221
  if (opts?.selector) {
7738
- const el = await page.$(opts.selector);
7739
- if (!el)
7740
- throw new BrowserError(`Element not found: ${opts.selector}`, "ELEMENT_NOT_FOUND");
7741
- rawBuffer = await el.screenshot(rawOpts);
8222
+ if (isBunView) {
8223
+ const uint8 = await page.screenshot();
8224
+ rawBuffer = Buffer.from(uint8 instanceof Uint8Array ? uint8 : await uint8);
8225
+ } else {
8226
+ const el = await page.$(opts.selector);
8227
+ if (!el)
8228
+ throw new BrowserError(`Element not found: ${opts.selector}`, "ELEMENT_NOT_FOUND");
8229
+ rawBuffer = await el.screenshot(rawOpts);
8230
+ }
8231
+ } else if (isBunView) {
8232
+ const uint8 = await page.screenshot();
8233
+ rawBuffer = Buffer.from(uint8 instanceof Uint8Array ? uint8 : await uint8);
7742
8234
  } else {
7743
8235
  rawBuffer = await page.screenshot(rawOpts);
7744
8236
  }
7745
8237
  const originalSizeBytes = rawBuffer.length;
7746
8238
  let finalBuffer;
7747
- if (compress && format !== "png") {
7748
- finalBuffer = await compressBuffer(rawBuffer, format, quality ?? 82, maxWidth);
7749
- } else if (compress && format === "png") {
7750
- finalBuffer = await compressBuffer(rawBuffer, "png", quality ?? 9, maxWidth);
7751
- } else {
8239
+ let compressed = true;
8240
+ let fallback = false;
8241
+ try {
8242
+ if (compress && format !== "png") {
8243
+ finalBuffer = await compressBuffer(rawBuffer, format, quality ?? 82, maxWidth);
8244
+ } else if (compress && format === "png") {
8245
+ finalBuffer = await compressBuffer(rawBuffer, "png", quality ?? 9, maxWidth);
8246
+ } else {
8247
+ finalBuffer = rawBuffer;
8248
+ compressed = false;
8249
+ }
8250
+ } catch (sharpErr) {
8251
+ fallback = true;
8252
+ compressed = false;
7752
8253
  finalBuffer = rawBuffer;
7753
8254
  }
7754
8255
  const compressedSizeBytes = finalBuffer.length;
7755
8256
  const compressionRatio = originalSizeBytes > 0 ? compressedSizeBytes / originalSizeBytes : 1;
7756
8257
  const ext = format;
7757
- const screenshotPath = opts?.path ?? join2(dir, `${stem}.${ext}`);
8258
+ const screenshotPath = opts?.path ?? join3(dir, `${stem}.${ext}`);
7758
8259
  await Bun.write(screenshotPath, finalBuffer);
7759
8260
  let thumbnailPath;
7760
8261
  let thumbnailBase64;
@@ -7776,7 +8277,8 @@ async function takeScreenshot(page, opts) {
7776
8277
  compressed_size_bytes: compressedSizeBytes,
7777
8278
  compression_ratio: compressionRatio,
7778
8279
  thumbnail_path: thumbnailPath,
7779
- thumbnail_base64: thumbnailBase64
8280
+ thumbnail_base64: thumbnailBase64,
8281
+ ...fallback ? { fallback: true, compressed: false } : {}
7780
8282
  };
7781
8283
  if (opts?.track !== false) {
7782
8284
  try {
@@ -8152,19 +8654,20 @@ function listProjects() {
8152
8654
  }
8153
8655
 
8154
8656
  // src/server/index.ts
8657
+ init_console_log();
8155
8658
  init_recordings();
8156
8659
 
8157
8660
  // src/lib/downloads.ts
8158
- import { join as join3, basename, extname } from "path";
8159
- import { mkdirSync as mkdirSync3, existsSync, readdirSync, statSync, unlinkSync, copyFileSync, writeFileSync, readFileSync } from "fs";
8160
- import { homedir as homedir3 } from "os";
8661
+ import { join as join4, basename, extname } from "path";
8662
+ import { mkdirSync as mkdirSync4, existsSync, readdirSync, statSync, unlinkSync, copyFileSync, writeFileSync, readFileSync } from "fs";
8663
+ import { homedir as homedir4 } from "os";
8161
8664
  function getDataDir3() {
8162
- return process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser");
8665
+ return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
8163
8666
  }
8164
8667
  function getDownloadsDir(sessionId) {
8165
- const base = join3(getDataDir3(), "downloads");
8166
- const dir = sessionId ? join3(base, sessionId) : base;
8167
- mkdirSync3(dir, { recursive: true });
8668
+ const base = join4(getDataDir3(), "downloads");
8669
+ const dir = sessionId ? join4(base, sessionId) : base;
8670
+ mkdirSync4(dir, { recursive: true });
8168
8671
  return dir;
8169
8672
  }
8170
8673
  function metaPath(filePath) {
@@ -8180,7 +8683,7 @@ function listDownloads(sessionId) {
8180
8683
  for (const entry of entries) {
8181
8684
  if (entry.endsWith(".meta.json"))
8182
8685
  continue;
8183
- const full = join3(d, entry);
8686
+ const full = join4(d, entry);
8184
8687
  const stat = statSync(full);
8185
8688
  if (stat.isDirectory()) {
8186
8689
  scanDir(full);
@@ -8241,9 +8744,9 @@ function cleanStaleDownloads(olderThanDays = 7) {
8241
8744
 
8242
8745
  // src/lib/gallery-diff.ts
8243
8746
  var import_sharp2 = __toESM(require_lib(), 1);
8244
- import { join as join4 } from "path";
8245
- import { mkdirSync as mkdirSync4 } from "fs";
8246
- import { homedir as homedir4 } from "os";
8747
+ import { join as join5 } from "path";
8748
+ import { mkdirSync as mkdirSync5 } from "fs";
8749
+ import { homedir as homedir5 } from "os";
8247
8750
  async function diffImages(path1, path2) {
8248
8751
  const img1 = import_sharp2.default(path1);
8249
8752
  const img2 = import_sharp2.default(path2);
@@ -8274,10 +8777,10 @@ async function diffImages(path1, path2) {
8274
8777
  diffBuffer[i + 2] = Math.round(raw1[i + 2] * 0.4);
8275
8778
  }
8276
8779
  }
8277
- const dataDir = process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
8278
- const diffDir = join4(dataDir, "diffs");
8279
- mkdirSync4(diffDir, { recursive: true });
8280
- const diffPath = join4(diffDir, `diff-${Date.now()}.webp`);
8780
+ const dataDir = process.env["BROWSER_DATA_DIR"] ?? join5(homedir5(), ".browser");
8781
+ const diffDir = join5(dataDir, "diffs");
8782
+ mkdirSync5(diffDir, { recursive: true });
8783
+ const diffPath = join5(diffDir, `diff-${Date.now()}.webp`);
8281
8784
  const diffImageBuffer = await import_sharp2.default(diffBuffer, { raw: { width: w, height: h, channels } }).webp({ quality: 85 }).toBuffer();
8282
8785
  await Bun.write(diffPath, diffImageBuffer);
8283
8786
  return {
@@ -8564,13 +9067,13 @@ var server = Bun.serve({
8564
9067
  const id = path.split("/")[3];
8565
9068
  return ok({ deleted: deleteDownload(id) });
8566
9069
  }
8567
- const dashboardDist = join5(import.meta.dir, "../../dashboard/dist");
9070
+ const dashboardDist = join6(import.meta.dir, "../../dashboard/dist");
8568
9071
  if (existsSync3(dashboardDist)) {
8569
- const filePath = path === "/" ? join5(dashboardDist, "index.html") : join5(dashboardDist, path);
9072
+ const filePath = path === "/" ? join6(dashboardDist, "index.html") : join6(dashboardDist, path);
8570
9073
  if (existsSync3(filePath)) {
8571
9074
  return new Response(Bun.file(filePath), { headers: CORS_HEADERS });
8572
9075
  }
8573
- return new Response(Bun.file(join5(dashboardDist, "index.html")), { headers: CORS_HEADERS });
9076
+ return new Response(Bun.file(join6(dashboardDist, "index.html")), { headers: CORS_HEADERS });
8574
9077
  }
8575
9078
  if (path === "/" || path === "") {
8576
9079
  return new Response("@hasna/browser REST API running. Dashboard not built.", {