@hasna/browser 0.0.4 → 0.0.6

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.
Files changed (39) hide show
  1. package/dist/cli/index.js +18447 -198
  2. package/dist/engines/bun-webview.d.ts +147 -0
  3. package/dist/engines/bun-webview.d.ts.map +1 -0
  4. package/dist/engines/bun-webview.test.d.ts +2 -0
  5. package/dist/engines/bun-webview.test.d.ts.map +1 -0
  6. package/dist/engines/selector.d.ts +2 -2
  7. package/dist/engines/selector.d.ts.map +1 -1
  8. package/dist/index.js +804 -278
  9. package/dist/lib/ai-task.d.ts +23 -0
  10. package/dist/lib/ai-task.d.ts.map +1 -0
  11. package/dist/lib/auth.d.ts +28 -0
  12. package/dist/lib/auth.d.ts.map +1 -0
  13. package/dist/lib/coordination.d.ts +12 -0
  14. package/dist/lib/coordination.d.ts.map +1 -0
  15. package/dist/lib/cron-manager.d.ts +43 -0
  16. package/dist/lib/cron-manager.d.ts.map +1 -0
  17. package/dist/lib/extractor.d.ts.map +1 -1
  18. package/dist/lib/integrations.test.d.ts +2 -0
  19. package/dist/lib/integrations.test.d.ts.map +1 -0
  20. package/dist/lib/page-memory.d.ts +14 -0
  21. package/dist/lib/page-memory.d.ts.map +1 -0
  22. package/dist/lib/ref-cache.d.ts +9 -0
  23. package/dist/lib/ref-cache.d.ts.map +1 -0
  24. package/dist/lib/screenshot.d.ts.map +1 -1
  25. package/dist/lib/session.d.ts +3 -0
  26. package/dist/lib/session.d.ts.map +1 -1
  27. package/dist/lib/skills-runner.d.ts +14 -0
  28. package/dist/lib/skills-runner.d.ts.map +1 -0
  29. package/dist/lib/snapshot.d.ts +1 -0
  30. package/dist/lib/snapshot.d.ts.map +1 -1
  31. package/dist/lib/task-queue.d.ts +21 -0
  32. package/dist/lib/task-queue.d.ts.map +1 -0
  33. package/dist/lib/url-watcher.d.ts +33 -0
  34. package/dist/lib/url-watcher.d.ts.map +1 -0
  35. package/dist/mcp/index.js +29279 -10967
  36. package/dist/server/index.js +565 -86
  37. package/dist/types/index.d.ts +1 -1
  38. package/dist/types/index.d.ts.map +1 -1
  39. package/package.json +5 -1
@@ -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
@@ -7051,15 +7085,414 @@ async function connectLightpanda(port) {
7051
7085
  }
7052
7086
  }
7053
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
+
7054
7487
  // src/engines/selector.ts
7055
7488
  init_types();
7056
7489
  var ENGINE_MAP = {
7057
- ["scrape" /* SCRAPE */]: "lightpanda",
7058
- ["extract_links" /* EXTRACT_LINKS */]: "lightpanda",
7059
- ["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",
7060
7495
  ["form_fill" /* FORM_FILL */]: "playwright",
7061
- ["spa_navigate" /* SPA_NAVIGATE */]: "playwright",
7062
- ["screenshot" /* SCREENSHOT */]: "playwright",
7063
7496
  ["auth_flow" /* AUTH_FLOW */]: "playwright",
7064
7497
  ["multi_tab" /* MULTI_TAB */]: "playwright",
7065
7498
  ["record_replay" /* RECORD_REPLAY */]: "playwright",
@@ -7073,6 +7506,14 @@ function selectEngine(useCase, explicit) {
7073
7506
  if (explicit && explicit !== "auto")
7074
7507
  return explicit;
7075
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
+ }
7076
7517
  if (preferred === "lightpanda" && !isLightpandaAvailable()) {
7077
7518
  return "playwright";
7078
7519
  }
@@ -7192,28 +7633,8 @@ function startHAR(page) {
7192
7633
  };
7193
7634
  }
7194
7635
 
7195
- // src/db/console-log.ts
7196
- init_schema();
7197
- import { randomUUID as randomUUID3 } from "crypto";
7198
- function logConsoleMessage(data) {
7199
- const db = getDatabase();
7200
- const id = randomUUID3();
7201
- 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);
7202
- return getConsoleMessage(id);
7203
- }
7204
- function getConsoleMessage(id) {
7205
- const db = getDatabase();
7206
- return db.query("SELECT * FROM console_log WHERE id = ?").get(id) ?? null;
7207
- }
7208
- function getConsoleLog(sessionId, level) {
7209
- const db = getDatabase();
7210
- if (level) {
7211
- return db.query("SELECT * FROM console_log WHERE session_id = ? AND level = ? ORDER BY timestamp ASC").all(sessionId, level);
7212
- }
7213
- return db.query("SELECT * FROM console_log WHERE session_id = ? ORDER BY timestamp ASC").all(sessionId);
7214
- }
7215
-
7216
7636
  // src/lib/console.ts
7637
+ init_console_log();
7217
7638
  function enableConsoleCapture(page, sessionId) {
7218
7639
  const onConsole = (msg) => {
7219
7640
  const levelMap = {
@@ -7348,12 +7769,30 @@ function setupDialogHandler(page, sessionId) {
7348
7769
 
7349
7770
  // src/lib/session.ts
7350
7771
  var handles = new Map;
7772
+ function createBunProxy(view) {
7773
+ return view;
7774
+ }
7351
7775
  async function createSession2(opts = {}) {
7352
7776
  const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
7353
7777
  const resolvedEngine = engine === "auto" ? "playwright" : engine;
7354
- let browser;
7778
+ let browser = null;
7779
+ let bunView = null;
7355
7780
  let page;
7356
- 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") {
7357
7796
  browser = await connectLightpanda();
7358
7797
  const context = await browser.newContext({ viewport: opts.viewport ?? { width: 1280, height: 720 } });
7359
7798
  page = await context.newPage();
@@ -7363,12 +7802,9 @@ async function createSession2(opts = {}) {
7363
7802
  viewport: opts.viewport,
7364
7803
  userAgent: opts.userAgent
7365
7804
  });
7366
- page = await getPage(browser, {
7367
- viewport: opts.viewport,
7368
- userAgent: opts.userAgent
7369
- });
7805
+ page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
7370
7806
  }
7371
- let sessionName = opts.name ?? (opts.startUrl ? (() => {
7807
+ const sessionName = opts.name ?? (opts.startUrl ? (() => {
7372
7808
  try {
7373
7809
  return new URL(opts.startUrl).hostname;
7374
7810
  } catch {
@@ -7376,35 +7812,57 @@ async function createSession2(opts = {}) {
7376
7812
  }
7377
7813
  })() : undefined);
7378
7814
  const session = createSession({
7379
- engine: resolvedEngine,
7815
+ engine: bunView ? "bun" : browser ? resolvedEngine : resolvedEngine,
7380
7816
  projectId: opts.projectId,
7381
7817
  agentId: opts.agentId,
7382
7818
  startUrl: opts.startUrl,
7383
7819
  name: sessionName
7384
7820
  });
7385
- if (opts.stealth) {
7821
+ if (opts.stealth && !bunView) {
7386
7822
  try {
7387
7823
  await applyStealthPatches(page);
7388
7824
  } catch {}
7389
7825
  }
7390
7826
  const cleanups = [];
7391
- if (opts.captureNetwork !== false) {
7392
- try {
7393
- cleanups.push(enableNetworkLogging(page, session.id));
7394
- } catch {}
7395
- }
7396
- 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
+ }
7397
7838
  try {
7398
- cleanups.push(enableConsoleCapture(page, session.id));
7839
+ cleanups.push(setupDialogHandler(page, session.id));
7399
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
+ }
7400
7857
  }
7401
- try {
7402
- cleanups.push(setupDialogHandler(page, session.id));
7403
- } catch {}
7404
- 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 } });
7405
7859
  if (opts.startUrl) {
7406
7860
  try {
7407
- 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
+ }
7408
7866
  } catch {}
7409
7867
  }
7410
7868
  return { session, page };
@@ -7414,7 +7872,11 @@ function getSessionPage(sessionId) {
7414
7872
  if (!handle)
7415
7873
  throw new SessionNotFoundError(sessionId);
7416
7874
  try {
7417
- handle.page.url();
7875
+ if (handle.bunView) {
7876
+ handle.bunView.url();
7877
+ } else {
7878
+ handle.page.url();
7879
+ }
7418
7880
  } catch {
7419
7881
  handles.delete(sessionId);
7420
7882
  throw new SessionNotFoundError(sessionId);
@@ -7429,12 +7891,19 @@ async function closeSession2(sessionId) {
7429
7891
  cleanup();
7430
7892
  } catch {}
7431
7893
  }
7432
- try {
7433
- await handle.page.context().close();
7434
- } catch {}
7435
- try {
7436
- await closeBrowser(handle.browser);
7437
- } 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
+ }
7438
7907
  handles.delete(sessionId);
7439
7908
  }
7440
7909
  return closeSession(sessionId);
@@ -7576,9 +8045,9 @@ async function extract(page, opts = {}) {
7576
8045
  // src/lib/screenshot.ts
7577
8046
  init_types();
7578
8047
  var import_sharp = __toESM(require_lib(), 1);
7579
- import { join as join2 } from "path";
7580
- import { mkdirSync as mkdirSync2 } from "fs";
7581
- 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";
7582
8051
 
7583
8052
  // src/db/gallery.ts
7584
8053
  init_schema();
@@ -7708,13 +8177,13 @@ function getGalleryStats(projectId) {
7708
8177
 
7709
8178
  // src/lib/screenshot.ts
7710
8179
  function getDataDir2() {
7711
- return process.env["BROWSER_DATA_DIR"] ?? join2(homedir2(), ".browser");
8180
+ return process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser");
7712
8181
  }
7713
8182
  function getScreenshotDir(projectId) {
7714
- const base = join2(getDataDir2(), "screenshots");
8183
+ const base = join3(getDataDir2(), "screenshots");
7715
8184
  const date = new Date().toISOString().split("T")[0];
7716
- const dir = projectId ? join2(base, projectId, date) : join2(base, date);
7717
- mkdirSync2(dir, { recursive: true });
8185
+ const dir = projectId ? join3(base, projectId, date) : join3(base, date);
8186
+ mkdirSync3(dir, { recursive: true });
7718
8187
  return dir;
7719
8188
  }
7720
8189
  async function compressBuffer(raw, format, quality, maxWidth) {
@@ -7729,7 +8198,7 @@ async function compressBuffer(raw, format, quality, maxWidth) {
7729
8198
  }
7730
8199
  }
7731
8200
  async function generateThumbnail(raw, dir, stem) {
7732
- const thumbPath = join2(dir, `${stem}.thumb.webp`);
8201
+ const thumbPath = join3(dir, `${stem}.thumb.webp`);
7733
8202
  const thumbBuffer = await import_sharp.default(raw).resize({ width: 200, withoutEnlargement: true }).webp({ quality: 70, effort: 3 }).toBuffer();
7734
8203
  await Bun.write(thumbPath, thumbBuffer);
7735
8204
  return { path: thumbPath, base64: thumbBuffer.toString("base64") };
@@ -7748,11 +8217,20 @@ async function takeScreenshot(page, opts) {
7748
8217
  type: "png"
7749
8218
  };
7750
8219
  let rawBuffer;
8220
+ const isBunView = typeof page.getNativeView === "function";
7751
8221
  if (opts?.selector) {
7752
- const el = await page.$(opts.selector);
7753
- if (!el)
7754
- throw new BrowserError(`Element not found: ${opts.selector}`, "ELEMENT_NOT_FOUND");
7755
- 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);
7756
8234
  } else {
7757
8235
  rawBuffer = await page.screenshot(rawOpts);
7758
8236
  }
@@ -7777,7 +8255,7 @@ async function takeScreenshot(page, opts) {
7777
8255
  const compressedSizeBytes = finalBuffer.length;
7778
8256
  const compressionRatio = originalSizeBytes > 0 ? compressedSizeBytes / originalSizeBytes : 1;
7779
8257
  const ext = format;
7780
- const screenshotPath = opts?.path ?? join2(dir, `${stem}.${ext}`);
8258
+ const screenshotPath = opts?.path ?? join3(dir, `${stem}.${ext}`);
7781
8259
  await Bun.write(screenshotPath, finalBuffer);
7782
8260
  let thumbnailPath;
7783
8261
  let thumbnailBase64;
@@ -8176,19 +8654,20 @@ function listProjects() {
8176
8654
  }
8177
8655
 
8178
8656
  // src/server/index.ts
8657
+ init_console_log();
8179
8658
  init_recordings();
8180
8659
 
8181
8660
  // src/lib/downloads.ts
8182
- import { join as join3, basename, extname } from "path";
8183
- import { mkdirSync as mkdirSync3, existsSync, readdirSync, statSync, unlinkSync, copyFileSync, writeFileSync, readFileSync } from "fs";
8184
- 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";
8185
8664
  function getDataDir3() {
8186
- return process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser");
8665
+ return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
8187
8666
  }
8188
8667
  function getDownloadsDir(sessionId) {
8189
- const base = join3(getDataDir3(), "downloads");
8190
- const dir = sessionId ? join3(base, sessionId) : base;
8191
- mkdirSync3(dir, { recursive: true });
8668
+ const base = join4(getDataDir3(), "downloads");
8669
+ const dir = sessionId ? join4(base, sessionId) : base;
8670
+ mkdirSync4(dir, { recursive: true });
8192
8671
  return dir;
8193
8672
  }
8194
8673
  function metaPath(filePath) {
@@ -8204,7 +8683,7 @@ function listDownloads(sessionId) {
8204
8683
  for (const entry of entries) {
8205
8684
  if (entry.endsWith(".meta.json"))
8206
8685
  continue;
8207
- const full = join3(d, entry);
8686
+ const full = join4(d, entry);
8208
8687
  const stat = statSync(full);
8209
8688
  if (stat.isDirectory()) {
8210
8689
  scanDir(full);
@@ -8265,9 +8744,9 @@ function cleanStaleDownloads(olderThanDays = 7) {
8265
8744
 
8266
8745
  // src/lib/gallery-diff.ts
8267
8746
  var import_sharp2 = __toESM(require_lib(), 1);
8268
- import { join as join4 } from "path";
8269
- import { mkdirSync as mkdirSync4 } from "fs";
8270
- 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";
8271
8750
  async function diffImages(path1, path2) {
8272
8751
  const img1 = import_sharp2.default(path1);
8273
8752
  const img2 = import_sharp2.default(path2);
@@ -8298,10 +8777,10 @@ async function diffImages(path1, path2) {
8298
8777
  diffBuffer[i + 2] = Math.round(raw1[i + 2] * 0.4);
8299
8778
  }
8300
8779
  }
8301
- const dataDir = process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
8302
- const diffDir = join4(dataDir, "diffs");
8303
- mkdirSync4(diffDir, { recursive: true });
8304
- 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`);
8305
8784
  const diffImageBuffer = await import_sharp2.default(diffBuffer, { raw: { width: w, height: h, channels } }).webp({ quality: 85 }).toBuffer();
8306
8785
  await Bun.write(diffPath, diffImageBuffer);
8307
8786
  return {
@@ -8588,13 +9067,13 @@ var server = Bun.serve({
8588
9067
  const id = path.split("/")[3];
8589
9068
  return ok({ deleted: deleteDownload(id) });
8590
9069
  }
8591
- const dashboardDist = join5(import.meta.dir, "../../dashboard/dist");
9070
+ const dashboardDist = join6(import.meta.dir, "../../dashboard/dist");
8592
9071
  if (existsSync3(dashboardDist)) {
8593
- const filePath = path === "/" ? join5(dashboardDist, "index.html") : join5(dashboardDist, path);
9072
+ const filePath = path === "/" ? join6(dashboardDist, "index.html") : join6(dashboardDist, path);
8594
9073
  if (existsSync3(filePath)) {
8595
9074
  return new Response(Bun.file(filePath), { headers: CORS_HEADERS });
8596
9075
  }
8597
- 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 });
8598
9077
  }
8599
9078
  if (path === "/" || path === "") {
8600
9079
  return new Response("@hasna/browser REST API running. Dashboard not built.", {