@hasna/browser 0.0.7 → 0.1.0

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.
@@ -259,6 +259,55 @@ function runMigrations(db) {
259
259
  CREATE INDEX IF NOT EXISTS idx_gallery_favorite ON gallery_entries(is_favorite);
260
260
  CREATE INDEX IF NOT EXISTS idx_gallery_created ON gallery_entries(created_at);
261
261
  `
262
+ },
263
+ {
264
+ version: 3,
265
+ sql: `
266
+ -- Session lock/claim for multi-agent ownership
267
+ ALTER TABLE sessions ADD COLUMN locked_by TEXT;
268
+ ALTER TABLE sessions ADD COLUMN locked_at TEXT;
269
+ `
270
+ },
271
+ {
272
+ version: 4,
273
+ sql: `
274
+ CREATE TABLE IF NOT EXISTS session_events (
275
+ id TEXT PRIMARY KEY,
276
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
277
+ event_type TEXT NOT NULL,
278
+ details TEXT DEFAULT '{}',
279
+ timestamp TEXT DEFAULT (datetime('now'))
280
+ );
281
+ CREATE INDEX IF NOT EXISTS idx_session_events_session ON session_events(session_id, timestamp);
282
+ `
283
+ },
284
+ {
285
+ version: 5,
286
+ sql: `
287
+ CREATE TABLE IF NOT EXISTS session_tags (
288
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
289
+ tag TEXT NOT NULL,
290
+ PRIMARY KEY (session_id, tag)
291
+ );
292
+ CREATE INDEX IF NOT EXISTS idx_session_tags_tag ON session_tags(tag);
293
+ `
294
+ },
295
+ {
296
+ version: 6,
297
+ sql: `
298
+ CREATE TABLE IF NOT EXISTS auth_flows (
299
+ id TEXT PRIMARY KEY,
300
+ name TEXT NOT NULL UNIQUE,
301
+ domain TEXT NOT NULL,
302
+ recording_id TEXT REFERENCES recordings(id),
303
+ storage_state_path TEXT,
304
+ created_at TEXT DEFAULT (datetime('now')),
305
+ last_used TEXT
306
+ );
307
+
308
+ CREATE INDEX IF NOT EXISTS idx_auth_flows_domain ON auth_flows(domain);
309
+ CREATE INDEX IF NOT EXISTS idx_auth_flows_name ON auth_flows(name);
310
+ `
262
311
  }
263
312
  ];
264
313
  for (const m of migrations) {
@@ -307,6 +356,188 @@ var init_console_log = __esm(() => {
307
356
  init_schema();
308
357
  });
309
358
 
359
+ // src/engines/cdp.ts
360
+ var exports_cdp = {};
361
+ __export(exports_cdp, {
362
+ connectToExistingBrowser: () => connectToExistingBrowser,
363
+ CDPClient: () => CDPClient
364
+ });
365
+ async function connectToExistingBrowser(cdpUrl) {
366
+ const { chromium: chromium3 } = await import("playwright");
367
+ try {
368
+ return await chromium3.connectOverCDP(cdpUrl);
369
+ } catch (err) {
370
+ throw new BrowserError(`Failed to connect to browser at ${cdpUrl}: ${err instanceof Error ? err.message : String(err)}. Start Chrome with: google-chrome --remote-debugging-port=9222`, "CDP_CONNECT_FAILED", true);
371
+ }
372
+ }
373
+
374
+ class CDPClient {
375
+ session;
376
+ networkEnabled = false;
377
+ performanceEnabled = false;
378
+ constructor(session) {
379
+ this.session = session;
380
+ }
381
+ static async fromPage(page) {
382
+ try {
383
+ const session = await page.context().newCDPSession(page);
384
+ return new CDPClient(session);
385
+ } catch (err) {
386
+ throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
387
+ }
388
+ }
389
+ async send(method, params) {
390
+ try {
391
+ return await this.session.send(method, params);
392
+ } catch (err) {
393
+ throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
394
+ }
395
+ }
396
+ on(event, handler) {
397
+ this.session.on(event, handler);
398
+ }
399
+ off(event, handler) {
400
+ this.session.off(event, handler);
401
+ }
402
+ async enableNetwork() {
403
+ if (!this.networkEnabled) {
404
+ await this.send("Network.enable");
405
+ this.networkEnabled = true;
406
+ }
407
+ }
408
+ async enablePerformance() {
409
+ if (!this.performanceEnabled) {
410
+ await this.send("Performance.enable");
411
+ this.performanceEnabled = true;
412
+ }
413
+ }
414
+ async getPerformanceMetrics() {
415
+ await this.enablePerformance();
416
+ const result = await this.send("Performance.getMetrics");
417
+ const m = {};
418
+ for (const metric of result.metrics) {
419
+ m[metric.name] = metric.value;
420
+ }
421
+ return {
422
+ js_heap_size_used: m["JSHeapUsedSize"],
423
+ js_heap_size_total: m["JSHeapTotalSize"],
424
+ dom_interactive: m["DOMInteractive"],
425
+ dom_complete: m["DOMComplete"],
426
+ load_event: m["LoadEventEnd"]
427
+ };
428
+ }
429
+ async startJSCoverage() {
430
+ await this.send("Profiler.enable");
431
+ await this.send("Debugger.enable");
432
+ await this.send("Profiler.startPreciseCoverage", {
433
+ callCount: false,
434
+ detailed: true
435
+ });
436
+ }
437
+ async stopJSCoverage() {
438
+ const result = await this.send("Profiler.takePreciseCoverage");
439
+ await this.send("Profiler.stopPreciseCoverage");
440
+ return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
441
+ url: r.url,
442
+ text: "",
443
+ ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
444
+ }));
445
+ }
446
+ async getCoverage() {
447
+ await this.startJSCoverage();
448
+ const js = await this.stopJSCoverage();
449
+ const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
450
+ return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
451
+ }
452
+ async captureHAREntries(page, handler) {
453
+ await this.enableNetwork();
454
+ const requestTimings = new Map;
455
+ const onRequest = (params) => {
456
+ requestTimings.set(params.requestId, params.timestamp);
457
+ };
458
+ const onResponse = (params) => {
459
+ const start = requestTimings.get(params.requestId);
460
+ const duration = start != null ? (params.timestamp - start) * 1000 : 0;
461
+ handler({
462
+ method: "GET",
463
+ url: params.response.url,
464
+ status: params.response.status,
465
+ duration
466
+ });
467
+ };
468
+ this.on("Network.requestWillBeSent", onRequest);
469
+ this.on("Network.responseReceived", onResponse);
470
+ return () => {
471
+ this.off("Network.requestWillBeSent", onRequest);
472
+ this.off("Network.responseReceived", onResponse);
473
+ };
474
+ }
475
+ async detach() {
476
+ try {
477
+ await this.session.detach();
478
+ } catch {}
479
+ }
480
+ }
481
+ var init_cdp = __esm(() => {
482
+ init_types();
483
+ });
484
+
485
+ // src/lib/storage-state.ts
486
+ var exports_storage_state = {};
487
+ __export(exports_storage_state, {
488
+ saveStateFromPage: () => saveStateFromPage,
489
+ saveState: () => saveState,
490
+ loadStatePath: () => loadStatePath,
491
+ listStates: () => listStates,
492
+ deleteState: () => deleteState
493
+ });
494
+ import { mkdirSync as mkdirSync3, existsSync, readdirSync, unlinkSync } from "fs";
495
+ import { join as join3 } from "path";
496
+ import { homedir as homedir3 } from "os";
497
+ function ensureDir() {
498
+ mkdirSync3(STATES_DIR, { recursive: true });
499
+ }
500
+ function statePath(name) {
501
+ return join3(STATES_DIR, `${name}.json`);
502
+ }
503
+ async function saveState(context, name) {
504
+ ensureDir();
505
+ const path = statePath(name);
506
+ const state = await context.storageState({ path });
507
+ return path;
508
+ }
509
+ async function saveStateFromPage(page, name) {
510
+ return saveState(page.context(), name);
511
+ }
512
+ function loadStatePath(name) {
513
+ const path = statePath(name);
514
+ return existsSync(path) ? path : null;
515
+ }
516
+ function listStates() {
517
+ ensureDir();
518
+ return readdirSync(STATES_DIR).filter((f) => f.endsWith(".json")).map((f) => {
519
+ const path = join3(STATES_DIR, f);
520
+ const stat = Bun.file(path);
521
+ return {
522
+ name: f.replace(".json", ""),
523
+ path,
524
+ modified: new Date(stat.lastModified).toISOString()
525
+ };
526
+ }).sort((a, b) => b.modified.localeCompare(a.modified));
527
+ }
528
+ function deleteState(name) {
529
+ const path = statePath(name);
530
+ if (existsSync(path)) {
531
+ unlinkSync(path);
532
+ return true;
533
+ }
534
+ return false;
535
+ }
536
+ var STATES_DIR;
537
+ var init_storage_state = __esm(() => {
538
+ STATES_DIR = join3(process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser"), "states");
539
+ });
540
+
310
541
  // node_modules/sharp/lib/is.js
311
542
  var require_is = __commonJS((exports, module) => {
312
543
  /*!
@@ -6901,8 +7132,8 @@ var init_snapshots = __esm(() => {
6901
7132
  });
6902
7133
 
6903
7134
  // src/server/index.ts
6904
- import { join as join6 } from "path";
6905
- import { existsSync as existsSync3 } from "fs";
7135
+ import { join as join7 } from "path";
7136
+ import { existsSync as existsSync4 } from "fs";
6906
7137
 
6907
7138
  // src/lib/session.ts
6908
7139
  init_types();
@@ -6954,6 +7185,8 @@ function updateSessionStatus(id, status) {
6954
7185
  return getSession(id);
6955
7186
  }
6956
7187
  function closeSession(id) {
7188
+ const db = getDatabase();
7189
+ db.prepare("UPDATE sessions SET locked_by = NULL, locked_at = NULL WHERE id = ?").run(id);
6957
7190
  return updateSessionStatus(id, "closed");
6958
7191
  }
6959
7192
 
@@ -6979,10 +7212,51 @@ async function getPage(browser, options) {
6979
7212
  });
6980
7213
  return context.newPage();
6981
7214
  }
6982
- async function closeBrowser(browser) {
6983
- try {
6984
- await browser.close();
6985
- } catch {}
7215
+ class BrowserPool {
7216
+ pool = [];
7217
+ maxSize;
7218
+ options;
7219
+ constructor(maxSize = 3, options) {
7220
+ this.maxSize = maxSize;
7221
+ this.options = options;
7222
+ }
7223
+ async acquire(headless = true) {
7224
+ const available = this.pool.find((e) => !e.inUse);
7225
+ if (available) {
7226
+ available.inUse = true;
7227
+ return available.browser;
7228
+ }
7229
+ if (this.pool.length < this.maxSize) {
7230
+ const browser = await launchPlaywright({ ...this.options, headless });
7231
+ this.pool.push({ browser, inUse: true, createdAt: Date.now() });
7232
+ return browser;
7233
+ }
7234
+ return new Promise((resolve) => {
7235
+ const interval = setInterval(() => {
7236
+ const free = this.pool.find((e) => !e.inUse);
7237
+ if (free) {
7238
+ clearInterval(interval);
7239
+ free.inUse = true;
7240
+ resolve(free.browser);
7241
+ }
7242
+ }, 100);
7243
+ });
7244
+ }
7245
+ release(browser) {
7246
+ const entry = this.pool.find((e) => e.browser === browser);
7247
+ if (entry)
7248
+ entry.inUse = false;
7249
+ }
7250
+ async destroyAll() {
7251
+ await Promise.all(this.pool.map((e) => e.browser.close().catch(() => {})));
7252
+ this.pool = [];
7253
+ }
7254
+ get size() {
7255
+ return this.pool.length;
7256
+ }
7257
+ get available() {
7258
+ return this.pool.filter((e) => !e.inUse).length;
7259
+ }
6986
7260
  }
6987
7261
 
6988
7262
  // src/engines/lightpanda.ts
@@ -7769,10 +8043,55 @@ function setupDialogHandler(page, sessionId) {
7769
8043
 
7770
8044
  // src/lib/session.ts
7771
8045
  var handles = new Map;
8046
+ var pool = new BrowserPool(5);
8047
+ var SESSION_TTL_MS = parseInt(process.env["SESSION_TTL_MINUTES"] ?? "10", 10) * 60000;
8048
+ var ttlInterval = setInterval(async () => {
8049
+ const now = Date.now();
8050
+ for (const [id, handle] of handles) {
8051
+ if (now - handle.lastActivity > SESSION_TTL_MS) {
8052
+ try {
8053
+ await closeSession2(id);
8054
+ } catch {}
8055
+ }
8056
+ }
8057
+ }, 60000);
8058
+ if (ttlInterval.unref)
8059
+ ttlInterval.unref();
7772
8060
  function createBunProxy(view) {
7773
8061
  return view;
7774
8062
  }
7775
8063
  async function createSession2(opts = {}) {
8064
+ if (opts.cdpUrl) {
8065
+ const { connectToExistingBrowser: connectToExistingBrowser2 } = await Promise.resolve().then(() => (init_cdp(), exports_cdp));
8066
+ const cdpBrowser = await connectToExistingBrowser2(opts.cdpUrl);
8067
+ const contexts = cdpBrowser.contexts();
8068
+ const context = contexts.length > 0 ? contexts[0] : await cdpBrowser.newContext();
8069
+ const pages = context.pages();
8070
+ const page2 = pages.length > 0 ? pages[0] : await context.newPage();
8071
+ const session2 = createSession({
8072
+ engine: "cdp",
8073
+ projectId: opts.projectId,
8074
+ agentId: opts.agentId,
8075
+ startUrl: page2.url(),
8076
+ name: opts.name ?? "attached"
8077
+ });
8078
+ const cleanups2 = [];
8079
+ if (opts.captureNetwork !== false) {
8080
+ try {
8081
+ cleanups2.push(enableNetworkLogging(page2, session2.id));
8082
+ } catch {}
8083
+ }
8084
+ if (opts.captureConsole !== false) {
8085
+ try {
8086
+ cleanups2.push(enableConsoleCapture(page2, session2.id));
8087
+ } catch {}
8088
+ }
8089
+ try {
8090
+ cleanups2.push(setupDialogHandler(page2, session2.id));
8091
+ } catch {}
8092
+ handles.set(session2.id, { browser: cdpBrowser, bunView: null, page: page2, engine: "cdp", cleanups: cleanups2, tokenBudget: { total: 0, used: 0 }, lastActivity: Date.now(), autoGallery: opts.autoGallery ?? false });
8093
+ return { session: session2, page: page2 };
8094
+ }
7776
8095
  const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
7777
8096
  const resolvedEngine = engine === "auto" ? "playwright" : engine;
7778
8097
  let browser = null;
@@ -7797,12 +8116,23 @@ async function createSession2(opts = {}) {
7797
8116
  const context = await browser.newContext({ viewport: opts.viewport ?? { width: 1280, height: 720 } });
7798
8117
  page = await context.newPage();
7799
8118
  } else {
7800
- browser = await launchPlaywright({
7801
- headless: opts.headless ?? true,
7802
- viewport: opts.viewport,
7803
- userAgent: opts.userAgent
7804
- });
7805
- page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
8119
+ browser = await pool.acquire(opts.headless ?? true);
8120
+ if (opts.storageState) {
8121
+ const { loadStatePath: loadStatePath2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
8122
+ const statePath2 = loadStatePath2(opts.storageState);
8123
+ if (statePath2) {
8124
+ const context = await browser.newContext({
8125
+ viewport: opts.viewport ?? { width: 1280, height: 720 },
8126
+ userAgent: opts.userAgent,
8127
+ storageState: statePath2
8128
+ });
8129
+ page = await context.newPage();
8130
+ } else {
8131
+ page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
8132
+ }
8133
+ } else {
8134
+ page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
8135
+ }
7806
8136
  }
7807
8137
  const sessionName = opts.name ?? (opts.startUrl ? (() => {
7808
8138
  try {
@@ -7855,7 +8185,7 @@ async function createSession2(opts = {}) {
7855
8185
  } catch {}
7856
8186
  }
7857
8187
  }
7858
- handles.set(session.id, { browser, bunView, page, engine: bunView ? "bun" : resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 } });
8188
+ handles.set(session.id, { browser, bunView, page, engine: bunView ? "bun" : resolvedEngine, cleanups, tokenBudget: { total: 0, used: 0 }, lastActivity: Date.now(), autoGallery: opts.autoGallery ?? false });
7859
8189
  if (opts.startUrl) {
7860
8190
  try {
7861
8191
  if (bunView) {
@@ -7881,6 +8211,7 @@ function getSessionPage(sessionId) {
7881
8211
  handles.delete(sessionId);
7882
8212
  throw new SessionNotFoundError(sessionId);
7883
8213
  }
8214
+ handle.lastActivity = Date.now();
7884
8215
  return handle.page;
7885
8216
  }
7886
8217
  async function closeSession2(sessionId) {
@@ -7899,10 +8230,8 @@ async function closeSession2(sessionId) {
7899
8230
  try {
7900
8231
  await handle.page.context().close();
7901
8232
  } catch {}
7902
- try {
7903
- if (handle.browser)
7904
- await closeBrowser(handle.browser);
7905
- } catch {}
8233
+ if (handle.browser)
8234
+ pool.release(handle.browser);
7906
8235
  }
7907
8236
  handles.delete(sessionId);
7908
8237
  }
@@ -7919,6 +8248,66 @@ init_types();
7919
8248
  var lastSnapshots = new Map;
7920
8249
  var sessionRefMaps = new Map;
7921
8250
 
8251
+ // src/lib/self-heal.ts
8252
+ async function healSelector(page, selector, sessionId) {
8253
+ const attempts = [];
8254
+ attempts.push(`selector: ${selector}`);
8255
+ try {
8256
+ const loc = page.locator(selector).first();
8257
+ if (await loc.count() > 0) {
8258
+ return { found: true, locator: loc, method: "original", healed: false, attempts };
8259
+ }
8260
+ } catch {}
8261
+ if (!selector.startsWith("#") && !selector.startsWith(".") && !selector.startsWith("[") && !selector.includes(">") && !selector.includes(" ")) {
8262
+ attempts.push(`text: "${selector}"`);
8263
+ try {
8264
+ const loc = page.getByText(selector, { exact: false }).first();
8265
+ if (await loc.count() > 0) {
8266
+ return { found: true, locator: loc, method: "text", healed: true, attempts };
8267
+ }
8268
+ } catch {}
8269
+ }
8270
+ const roleMap = {
8271
+ button: ["button", "submit", "reset"],
8272
+ link: ["a"],
8273
+ input: ["input", "textarea"],
8274
+ heading: ["h1", "h2", "h3", "h4", "h5", "h6"]
8275
+ };
8276
+ const nameHint = selector.replace(/^[#.]/, "").replace(/[-_]/g, " ").toLowerCase();
8277
+ for (const [role, tags] of Object.entries(roleMap)) {
8278
+ attempts.push(`role: ${role} name~="${nameHint}"`);
8279
+ try {
8280
+ const loc = page.getByRole(role, { name: new RegExp(nameHint.split(" ")[0], "i") }).first();
8281
+ if (await loc.count() > 0) {
8282
+ return { found: true, locator: loc, method: "role", healed: true, attempts };
8283
+ }
8284
+ } catch {}
8285
+ }
8286
+ if (selector.startsWith("#")) {
8287
+ const idPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
8288
+ const partialSel = `[id*="${idPart}"]`;
8289
+ attempts.push(`partial_id: ${partialSel}`);
8290
+ try {
8291
+ const loc = page.locator(partialSel).first();
8292
+ if (await loc.count() > 0) {
8293
+ return { found: true, locator: loc, method: "partial_id", healed: true, attempts };
8294
+ }
8295
+ } catch {}
8296
+ }
8297
+ if (selector.startsWith(".")) {
8298
+ const classPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
8299
+ const partialSel = `[class*="${classPart}"]`;
8300
+ attempts.push(`partial_class: ${partialSel}`);
8301
+ try {
8302
+ const loc = page.locator(partialSel).first();
8303
+ if (await loc.count() > 0) {
8304
+ return { found: true, locator: loc, method: "partial_class", healed: true, attempts };
8305
+ }
8306
+ } catch {}
8307
+ }
8308
+ return { found: false, locator: null, method: "none", healed: false, attempts };
8309
+ }
8310
+
7922
8311
  // src/lib/actions.ts
7923
8312
  async function click(page, selector, opts) {
7924
8313
  try {
@@ -7928,11 +8317,22 @@ async function click(page, selector, opts) {
7928
8317
  delay: opts?.delay,
7929
8318
  timeout: opts?.timeout ?? 1e4
7930
8319
  });
7931
- } catch (err) {
7932
- if (err instanceof Error && err.message.includes("not found")) {
8320
+ return {};
8321
+ } catch (originalError) {
8322
+ if (opts?.selfHeal !== false) {
8323
+ const result = await healSelector(page, selector);
8324
+ if (result.found && result.locator) {
8325
+ await result.locator.click({
8326
+ button: opts?.button ?? "left",
8327
+ timeout: opts?.timeout ?? 1e4
8328
+ });
8329
+ return { healed: true, method: result.method, attempts: result.attempts };
8330
+ }
8331
+ }
8332
+ if (originalError instanceof Error && originalError.message.includes("not found")) {
7933
8333
  throw new ElementNotFoundError(selector);
7934
8334
  }
7935
- throw new BrowserError(`Click failed on '${selector}': ${err instanceof Error ? err.message : String(err)}`, "CLICK_FAILED");
8335
+ throw new BrowserError(`Click failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "CLICK_FAILED");
7936
8336
  }
7937
8337
  }
7938
8338
  async function type(page, selector, text, opts) {
@@ -7941,11 +8341,21 @@ async function type(page, selector, text, opts) {
7941
8341
  await page.fill(selector, "", { timeout: opts?.timeout ?? 1e4 });
7942
8342
  }
7943
8343
  await page.type(selector, text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
7944
- } catch (err) {
7945
- if (err instanceof Error && err.message.includes("not found")) {
8344
+ return {};
8345
+ } catch (originalError) {
8346
+ if (opts?.selfHeal !== false) {
8347
+ const result = await healSelector(page, selector);
8348
+ if (result.found && result.locator) {
8349
+ if (opts?.clear)
8350
+ await result.locator.fill("", { timeout: opts?.timeout ?? 1e4 });
8351
+ await result.locator.pressSequentially(text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
8352
+ return { healed: true, method: result.method, attempts: result.attempts };
8353
+ }
8354
+ }
8355
+ if (originalError instanceof Error && originalError.message.includes("not found")) {
7946
8356
  throw new ElementNotFoundError(selector);
7947
8357
  }
7948
- throw new BrowserError(`Type failed on '${selector}': ${err instanceof Error ? err.message : String(err)}`, "TYPE_FAILED");
8358
+ throw new BrowserError(`Type failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "TYPE_FAILED");
7949
8359
  }
7950
8360
  }
7951
8361
  async function scroll(page, direction = "down", amount = 300) {
@@ -8045,9 +8455,9 @@ async function extract(page, opts = {}) {
8045
8455
  // src/lib/screenshot.ts
8046
8456
  init_types();
8047
8457
  var import_sharp = __toESM(require_lib(), 1);
8048
- import { join as join3 } from "path";
8049
- import { mkdirSync as mkdirSync3 } from "fs";
8050
- import { homedir as homedir3 } from "os";
8458
+ import { join as join4 } from "path";
8459
+ import { mkdirSync as mkdirSync4 } from "fs";
8460
+ import { homedir as homedir4 } from "os";
8051
8461
 
8052
8462
  // src/db/gallery.ts
8053
8463
  init_schema();
@@ -8177,13 +8587,13 @@ function getGalleryStats(projectId) {
8177
8587
 
8178
8588
  // src/lib/screenshot.ts
8179
8589
  function getDataDir2() {
8180
- return process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser");
8590
+ return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
8181
8591
  }
8182
8592
  function getScreenshotDir(projectId) {
8183
- const base = join3(getDataDir2(), "screenshots");
8593
+ const base = join4(getDataDir2(), "screenshots");
8184
8594
  const date = new Date().toISOString().split("T")[0];
8185
- const dir = projectId ? join3(base, projectId, date) : join3(base, date);
8186
- mkdirSync3(dir, { recursive: true });
8595
+ const dir = projectId ? join4(base, projectId, date) : join4(base, date);
8596
+ mkdirSync4(dir, { recursive: true });
8187
8597
  return dir;
8188
8598
  }
8189
8599
  async function compressBuffer(raw, format, quality, maxWidth) {
@@ -8198,7 +8608,7 @@ async function compressBuffer(raw, format, quality, maxWidth) {
8198
8608
  }
8199
8609
  }
8200
8610
  async function generateThumbnail(raw, dir, stem) {
8201
- const thumbPath = join3(dir, `${stem}.thumb.webp`);
8611
+ const thumbPath = join4(dir, `${stem}.thumb.webp`);
8202
8612
  const thumbBuffer = await import_sharp.default(raw).resize({ width: 200, withoutEnlargement: true }).webp({ quality: 70, effort: 3 }).toBuffer();
8203
8613
  await Bun.write(thumbPath, thumbBuffer);
8204
8614
  return { path: thumbPath, base64: thumbBuffer.toString("base64") };
@@ -8255,7 +8665,7 @@ async function takeScreenshot(page, opts) {
8255
8665
  const compressedSizeBytes = finalBuffer.length;
8256
8666
  const compressionRatio = originalSizeBytes > 0 ? compressedSizeBytes / originalSizeBytes : 1;
8257
8667
  const ext = format;
8258
- const screenshotPath = opts?.path ?? join3(dir, `${stem}.${ext}`);
8668
+ const screenshotPath = opts?.path ?? join4(dir, `${stem}.${ext}`);
8259
8669
  await Bun.write(screenshotPath, finalBuffer);
8260
8670
  let thumbnailPath;
8261
8671
  let thumbnailBase64;
@@ -8314,118 +8724,8 @@ async function takeScreenshot(page, opts) {
8314
8724
  }
8315
8725
  }
8316
8726
 
8317
- // src/engines/cdp.ts
8318
- init_types();
8319
-
8320
- class CDPClient {
8321
- session;
8322
- networkEnabled = false;
8323
- performanceEnabled = false;
8324
- constructor(session) {
8325
- this.session = session;
8326
- }
8327
- static async fromPage(page) {
8328
- try {
8329
- const session = await page.context().newCDPSession(page);
8330
- return new CDPClient(session);
8331
- } catch (err) {
8332
- throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
8333
- }
8334
- }
8335
- async send(method, params) {
8336
- try {
8337
- return await this.session.send(method, params);
8338
- } catch (err) {
8339
- throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
8340
- }
8341
- }
8342
- on(event, handler) {
8343
- this.session.on(event, handler);
8344
- }
8345
- off(event, handler) {
8346
- this.session.off(event, handler);
8347
- }
8348
- async enableNetwork() {
8349
- if (!this.networkEnabled) {
8350
- await this.send("Network.enable");
8351
- this.networkEnabled = true;
8352
- }
8353
- }
8354
- async enablePerformance() {
8355
- if (!this.performanceEnabled) {
8356
- await this.send("Performance.enable");
8357
- this.performanceEnabled = true;
8358
- }
8359
- }
8360
- async getPerformanceMetrics() {
8361
- await this.enablePerformance();
8362
- const result = await this.send("Performance.getMetrics");
8363
- const m = {};
8364
- for (const metric of result.metrics) {
8365
- m[metric.name] = metric.value;
8366
- }
8367
- return {
8368
- js_heap_size_used: m["JSHeapUsedSize"],
8369
- js_heap_size_total: m["JSHeapTotalSize"],
8370
- dom_interactive: m["DOMInteractive"],
8371
- dom_complete: m["DOMComplete"],
8372
- load_event: m["LoadEventEnd"]
8373
- };
8374
- }
8375
- async startJSCoverage() {
8376
- await this.send("Profiler.enable");
8377
- await this.send("Debugger.enable");
8378
- await this.send("Profiler.startPreciseCoverage", {
8379
- callCount: false,
8380
- detailed: true
8381
- });
8382
- }
8383
- async stopJSCoverage() {
8384
- const result = await this.send("Profiler.takePreciseCoverage");
8385
- await this.send("Profiler.stopPreciseCoverage");
8386
- return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
8387
- url: r.url,
8388
- text: "",
8389
- ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
8390
- }));
8391
- }
8392
- async getCoverage() {
8393
- await this.startJSCoverage();
8394
- const js = await this.stopJSCoverage();
8395
- const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
8396
- return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
8397
- }
8398
- async captureHAREntries(page, handler) {
8399
- await this.enableNetwork();
8400
- const requestTimings = new Map;
8401
- const onRequest = (params) => {
8402
- requestTimings.set(params.requestId, params.timestamp);
8403
- };
8404
- const onResponse = (params) => {
8405
- const start = requestTimings.get(params.requestId);
8406
- const duration = start != null ? (params.timestamp - start) * 1000 : 0;
8407
- handler({
8408
- method: "GET",
8409
- url: params.response.url,
8410
- status: params.response.status,
8411
- duration
8412
- });
8413
- };
8414
- this.on("Network.requestWillBeSent", onRequest);
8415
- this.on("Network.responseReceived", onResponse);
8416
- return () => {
8417
- this.off("Network.requestWillBeSent", onRequest);
8418
- this.off("Network.responseReceived", onResponse);
8419
- };
8420
- }
8421
- async detach() {
8422
- try {
8423
- await this.session.detach();
8424
- } catch {}
8425
- }
8426
- }
8427
-
8428
8727
  // src/lib/performance.ts
8728
+ init_cdp();
8429
8729
  async function getPerformanceMetrics(page) {
8430
8730
  const navTiming = await page.evaluate(() => {
8431
8731
  const t = performance.timing;
@@ -8658,16 +8958,16 @@ init_console_log();
8658
8958
  init_recordings();
8659
8959
 
8660
8960
  // src/lib/downloads.ts
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";
8961
+ import { join as join5, basename, extname } from "path";
8962
+ import { mkdirSync as mkdirSync5, existsSync as existsSync2, readdirSync as readdirSync2, statSync, unlinkSync as unlinkSync2, copyFileSync, writeFileSync, readFileSync } from "fs";
8963
+ import { homedir as homedir5 } from "os";
8664
8964
  function getDataDir3() {
8665
- return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
8965
+ return process.env["BROWSER_DATA_DIR"] ?? join5(homedir5(), ".browser");
8666
8966
  }
8667
8967
  function getDownloadsDir(sessionId) {
8668
- const base = join4(getDataDir3(), "downloads");
8669
- const dir = sessionId ? join4(base, sessionId) : base;
8670
- mkdirSync4(dir, { recursive: true });
8968
+ const base = join5(getDataDir3(), "downloads");
8969
+ const dir = sessionId ? join5(base, sessionId) : base;
8970
+ mkdirSync5(dir, { recursive: true });
8671
8971
  return dir;
8672
8972
  }
8673
8973
  function metaPath(filePath) {
@@ -8677,20 +8977,20 @@ function listDownloads(sessionId) {
8677
8977
  const dir = getDownloadsDir(sessionId);
8678
8978
  const results = [];
8679
8979
  function scanDir(d) {
8680
- if (!existsSync(d))
8980
+ if (!existsSync2(d))
8681
8981
  return;
8682
- const entries = readdirSync(d);
8982
+ const entries = readdirSync2(d);
8683
8983
  for (const entry of entries) {
8684
8984
  if (entry.endsWith(".meta.json"))
8685
8985
  continue;
8686
- const full = join4(d, entry);
8986
+ const full = join5(d, entry);
8687
8987
  const stat = statSync(full);
8688
8988
  if (stat.isDirectory()) {
8689
8989
  scanDir(full);
8690
8990
  continue;
8691
8991
  }
8692
8992
  const mpath = metaPath(full);
8693
- if (!existsSync(mpath))
8993
+ if (!existsSync2(mpath))
8694
8994
  continue;
8695
8995
  try {
8696
8996
  const meta = JSON.parse(readFileSync(mpath, "utf8"));
@@ -8720,9 +9020,9 @@ function deleteDownload(id, sessionId) {
8720
9020
  if (!file)
8721
9021
  return false;
8722
9022
  try {
8723
- unlinkSync(file.path);
8724
- if (existsSync(file.meta_path))
8725
- unlinkSync(file.meta_path);
9023
+ unlinkSync2(file.path);
9024
+ if (existsSync2(file.meta_path))
9025
+ unlinkSync2(file.meta_path);
8726
9026
  return true;
8727
9027
  } catch {
8728
9028
  return false;
@@ -8744,9 +9044,9 @@ function cleanStaleDownloads(olderThanDays = 7) {
8744
9044
 
8745
9045
  // src/lib/gallery-diff.ts
8746
9046
  var import_sharp2 = __toESM(require_lib(), 1);
8747
- import { join as join5 } from "path";
8748
- import { mkdirSync as mkdirSync5 } from "fs";
8749
- import { homedir as homedir5 } from "os";
9047
+ import { join as join6 } from "path";
9048
+ import { mkdirSync as mkdirSync6 } from "fs";
9049
+ import { homedir as homedir6 } from "os";
8750
9050
  async function diffImages(path1, path2) {
8751
9051
  const img1 = import_sharp2.default(path1);
8752
9052
  const img2 = import_sharp2.default(path2);
@@ -8777,10 +9077,10 @@ async function diffImages(path1, path2) {
8777
9077
  diffBuffer[i + 2] = Math.round(raw1[i + 2] * 0.4);
8778
9078
  }
8779
9079
  }
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`);
9080
+ const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
9081
+ const diffDir = join6(dataDir, "diffs");
9082
+ mkdirSync6(diffDir, { recursive: true });
9083
+ const diffPath = join6(diffDir, `diff-${Date.now()}.webp`);
8784
9084
  const diffImageBuffer = await import_sharp2.default(diffBuffer, { raw: { width: w, height: h, channels } }).webp({ quality: 85 }).toBuffer();
8785
9085
  await Bun.write(diffPath, diffImageBuffer);
8786
9086
  return {
@@ -9024,14 +9324,14 @@ var server = Bun.serve({
9024
9324
  if (path.match(/^\/api\/gallery\/([^/]+)\/thumbnail$/) && method === "GET") {
9025
9325
  const id = path.split("/")[3];
9026
9326
  const entry = getEntry(id);
9027
- if (!entry?.thumbnail_path || !existsSync3(entry.thumbnail_path))
9327
+ if (!entry?.thumbnail_path || !existsSync4(entry.thumbnail_path))
9028
9328
  return notFound("Thumbnail not found");
9029
9329
  return new Response(Bun.file(entry.thumbnail_path), { headers: { ...CORS_HEADERS } });
9030
9330
  }
9031
9331
  if (path.match(/^\/api\/gallery\/([^/]+)\/image$/) && method === "GET") {
9032
9332
  const id = path.split("/")[3];
9033
9333
  const entry = getEntry(id);
9034
- if (!entry?.path || !existsSync3(entry.path))
9334
+ if (!entry?.path || !existsSync4(entry.path))
9035
9335
  return notFound("Image not found");
9036
9336
  return new Response(Bun.file(entry.path), { headers: { ...CORS_HEADERS } });
9037
9337
  }
@@ -9059,7 +9359,7 @@ var server = Bun.serve({
9059
9359
  if (path.match(/^\/api\/downloads\/([^/]+)\/raw$/) && method === "GET") {
9060
9360
  const id = path.split("/")[3];
9061
9361
  const file = getDownload(id);
9062
- if (!file || !existsSync3(file.path))
9362
+ if (!file || !existsSync4(file.path))
9063
9363
  return notFound("Download not found");
9064
9364
  return new Response(Bun.file(file.path), { headers: { ...CORS_HEADERS } });
9065
9365
  }
@@ -9067,13 +9367,13 @@ var server = Bun.serve({
9067
9367
  const id = path.split("/")[3];
9068
9368
  return ok({ deleted: deleteDownload(id) });
9069
9369
  }
9070
- const dashboardDist = join6(import.meta.dir, "../../dashboard/dist");
9071
- if (existsSync3(dashboardDist)) {
9072
- const filePath = path === "/" ? join6(dashboardDist, "index.html") : join6(dashboardDist, path);
9073
- if (existsSync3(filePath)) {
9370
+ const dashboardDist = join7(import.meta.dir, "../../dashboard/dist");
9371
+ if (existsSync4(dashboardDist)) {
9372
+ const filePath = path === "/" ? join7(dashboardDist, "index.html") : join7(dashboardDist, path);
9373
+ if (existsSync4(filePath)) {
9074
9374
  return new Response(Bun.file(filePath), { headers: CORS_HEADERS });
9075
9375
  }
9076
- return new Response(Bun.file(join6(dashboardDist, "index.html")), { headers: CORS_HEADERS });
9376
+ return new Response(Bun.file(join7(dashboardDist, "index.html")), { headers: CORS_HEADERS });
9077
9377
  }
9078
9378
  if (path === "/" || path === "") {
9079
9379
  return new Response("@hasna/browser REST API running. Dashboard not built.", {