@hasna/browser 0.0.9 → 0.1.1

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.
@@ -291,6 +291,71 @@ function runMigrations(db) {
291
291
  );
292
292
  CREATE INDEX IF NOT EXISTS idx_session_tags_tag ON session_tags(tag);
293
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
+ `
311
+ },
312
+ {
313
+ version: 7,
314
+ sql: `
315
+ CREATE TABLE IF NOT EXISTS workflows (
316
+ id TEXT PRIMARY KEY,
317
+ name TEXT NOT NULL UNIQUE,
318
+ description TEXT,
319
+ steps TEXT NOT NULL DEFAULT '[]',
320
+ start_url TEXT,
321
+ last_run TEXT,
322
+ last_heal TEXT,
323
+ heal_count INTEGER DEFAULT 0,
324
+ run_count INTEGER DEFAULT 0,
325
+ created_at TEXT DEFAULT (datetime('now')),
326
+ updated_at TEXT DEFAULT (datetime('now'))
327
+ );
328
+ `
329
+ },
330
+ {
331
+ version: 8,
332
+ sql: `
333
+ CREATE TABLE IF NOT EXISTS datasets (
334
+ id TEXT PRIMARY KEY,
335
+ name TEXT NOT NULL UNIQUE,
336
+ source_url TEXT,
337
+ source_type TEXT NOT NULL DEFAULT 'page',
338
+ data TEXT NOT NULL DEFAULT '[]',
339
+ schema TEXT,
340
+ row_count INTEGER DEFAULT 0,
341
+ last_refresh TEXT,
342
+ created_at TEXT DEFAULT (datetime('now')),
343
+ updated_at TEXT DEFAULT (datetime('now'))
344
+ );
345
+
346
+ CREATE TABLE IF NOT EXISTS api_endpoints (
347
+ id TEXT PRIMARY KEY,
348
+ session_id TEXT,
349
+ url TEXT NOT NULL,
350
+ method TEXT DEFAULT 'GET',
351
+ response_schema TEXT,
352
+ sample_response TEXT,
353
+ status_code INTEGER,
354
+ content_type TEXT,
355
+ discovered_at TEXT DEFAULT (datetime('now'))
356
+ );
357
+ CREATE INDEX IF NOT EXISTS idx_api_endpoints_session ON api_endpoints(session_id);
358
+ `
294
359
  }
295
360
  ];
296
361
  for (const m of migrations) {
@@ -339,6 +404,188 @@ var init_console_log = __esm(() => {
339
404
  init_schema();
340
405
  });
341
406
 
407
+ // src/engines/cdp.ts
408
+ var exports_cdp = {};
409
+ __export(exports_cdp, {
410
+ connectToExistingBrowser: () => connectToExistingBrowser,
411
+ CDPClient: () => CDPClient
412
+ });
413
+ async function connectToExistingBrowser(cdpUrl) {
414
+ const { chromium: chromium3 } = await import("playwright");
415
+ try {
416
+ return await chromium3.connectOverCDP(cdpUrl);
417
+ } catch (err) {
418
+ 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);
419
+ }
420
+ }
421
+
422
+ class CDPClient {
423
+ session;
424
+ networkEnabled = false;
425
+ performanceEnabled = false;
426
+ constructor(session) {
427
+ this.session = session;
428
+ }
429
+ static async fromPage(page) {
430
+ try {
431
+ const session = await page.context().newCDPSession(page);
432
+ return new CDPClient(session);
433
+ } catch (err) {
434
+ throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
435
+ }
436
+ }
437
+ async send(method, params) {
438
+ try {
439
+ return await this.session.send(method, params);
440
+ } catch (err) {
441
+ throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
442
+ }
443
+ }
444
+ on(event, handler) {
445
+ this.session.on(event, handler);
446
+ }
447
+ off(event, handler) {
448
+ this.session.off(event, handler);
449
+ }
450
+ async enableNetwork() {
451
+ if (!this.networkEnabled) {
452
+ await this.send("Network.enable");
453
+ this.networkEnabled = true;
454
+ }
455
+ }
456
+ async enablePerformance() {
457
+ if (!this.performanceEnabled) {
458
+ await this.send("Performance.enable");
459
+ this.performanceEnabled = true;
460
+ }
461
+ }
462
+ async getPerformanceMetrics() {
463
+ await this.enablePerformance();
464
+ const result = await this.send("Performance.getMetrics");
465
+ const m = {};
466
+ for (const metric of result.metrics) {
467
+ m[metric.name] = metric.value;
468
+ }
469
+ return {
470
+ js_heap_size_used: m["JSHeapUsedSize"],
471
+ js_heap_size_total: m["JSHeapTotalSize"],
472
+ dom_interactive: m["DOMInteractive"],
473
+ dom_complete: m["DOMComplete"],
474
+ load_event: m["LoadEventEnd"]
475
+ };
476
+ }
477
+ async startJSCoverage() {
478
+ await this.send("Profiler.enable");
479
+ await this.send("Debugger.enable");
480
+ await this.send("Profiler.startPreciseCoverage", {
481
+ callCount: false,
482
+ detailed: true
483
+ });
484
+ }
485
+ async stopJSCoverage() {
486
+ const result = await this.send("Profiler.takePreciseCoverage");
487
+ await this.send("Profiler.stopPreciseCoverage");
488
+ return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
489
+ url: r.url,
490
+ text: "",
491
+ ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
492
+ }));
493
+ }
494
+ async getCoverage() {
495
+ await this.startJSCoverage();
496
+ const js = await this.stopJSCoverage();
497
+ const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
498
+ return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
499
+ }
500
+ async captureHAREntries(page, handler) {
501
+ await this.enableNetwork();
502
+ const requestTimings = new Map;
503
+ const onRequest = (params) => {
504
+ requestTimings.set(params.requestId, params.timestamp);
505
+ };
506
+ const onResponse = (params) => {
507
+ const start = requestTimings.get(params.requestId);
508
+ const duration = start != null ? (params.timestamp - start) * 1000 : 0;
509
+ handler({
510
+ method: "GET",
511
+ url: params.response.url,
512
+ status: params.response.status,
513
+ duration
514
+ });
515
+ };
516
+ this.on("Network.requestWillBeSent", onRequest);
517
+ this.on("Network.responseReceived", onResponse);
518
+ return () => {
519
+ this.off("Network.requestWillBeSent", onRequest);
520
+ this.off("Network.responseReceived", onResponse);
521
+ };
522
+ }
523
+ async detach() {
524
+ try {
525
+ await this.session.detach();
526
+ } catch {}
527
+ }
528
+ }
529
+ var init_cdp = __esm(() => {
530
+ init_types();
531
+ });
532
+
533
+ // src/lib/storage-state.ts
534
+ var exports_storage_state = {};
535
+ __export(exports_storage_state, {
536
+ saveStateFromPage: () => saveStateFromPage,
537
+ saveState: () => saveState,
538
+ loadStatePath: () => loadStatePath,
539
+ listStates: () => listStates,
540
+ deleteState: () => deleteState
541
+ });
542
+ import { mkdirSync as mkdirSync3, existsSync, readdirSync, unlinkSync } from "fs";
543
+ import { join as join3 } from "path";
544
+ import { homedir as homedir3 } from "os";
545
+ function ensureDir() {
546
+ mkdirSync3(STATES_DIR, { recursive: true });
547
+ }
548
+ function statePath(name) {
549
+ return join3(STATES_DIR, `${name}.json`);
550
+ }
551
+ async function saveState(context, name) {
552
+ ensureDir();
553
+ const path = statePath(name);
554
+ const state = await context.storageState({ path });
555
+ return path;
556
+ }
557
+ async function saveStateFromPage(page, name) {
558
+ return saveState(page.context(), name);
559
+ }
560
+ function loadStatePath(name) {
561
+ const path = statePath(name);
562
+ return existsSync(path) ? path : null;
563
+ }
564
+ function listStates() {
565
+ ensureDir();
566
+ return readdirSync(STATES_DIR).filter((f) => f.endsWith(".json")).map((f) => {
567
+ const path = join3(STATES_DIR, f);
568
+ const stat = Bun.file(path);
569
+ return {
570
+ name: f.replace(".json", ""),
571
+ path,
572
+ modified: new Date(stat.lastModified).toISOString()
573
+ };
574
+ }).sort((a, b) => b.modified.localeCompare(a.modified));
575
+ }
576
+ function deleteState(name) {
577
+ const path = statePath(name);
578
+ if (existsSync(path)) {
579
+ unlinkSync(path);
580
+ return true;
581
+ }
582
+ return false;
583
+ }
584
+ var STATES_DIR;
585
+ var init_storage_state = __esm(() => {
586
+ STATES_DIR = join3(process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser"), "states");
587
+ });
588
+
342
589
  // node_modules/sharp/lib/is.js
343
590
  var require_is = __commonJS((exports, module) => {
344
591
  /*!
@@ -6933,8 +7180,8 @@ var init_snapshots = __esm(() => {
6933
7180
  });
6934
7181
 
6935
7182
  // src/server/index.ts
6936
- import { join as join6 } from "path";
6937
- import { existsSync as existsSync3 } from "fs";
7183
+ import { join as join7 } from "path";
7184
+ import { existsSync as existsSync4 } from "fs";
6938
7185
 
6939
7186
  // src/lib/session.ts
6940
7187
  init_types();
@@ -7862,6 +8109,37 @@ function createBunProxy(view) {
7862
8109
  return view;
7863
8110
  }
7864
8111
  async function createSession2(opts = {}) {
8112
+ if (opts.cdpUrl) {
8113
+ const { connectToExistingBrowser: connectToExistingBrowser2 } = await Promise.resolve().then(() => (init_cdp(), exports_cdp));
8114
+ const cdpBrowser = await connectToExistingBrowser2(opts.cdpUrl);
8115
+ const contexts = cdpBrowser.contexts();
8116
+ const context = contexts.length > 0 ? contexts[0] : await cdpBrowser.newContext();
8117
+ const pages = context.pages();
8118
+ const page2 = pages.length > 0 ? pages[0] : await context.newPage();
8119
+ const session2 = createSession({
8120
+ engine: "cdp",
8121
+ projectId: opts.projectId,
8122
+ agentId: opts.agentId,
8123
+ startUrl: page2.url(),
8124
+ name: opts.name ?? "attached"
8125
+ });
8126
+ const cleanups2 = [];
8127
+ if (opts.captureNetwork !== false) {
8128
+ try {
8129
+ cleanups2.push(enableNetworkLogging(page2, session2.id));
8130
+ } catch {}
8131
+ }
8132
+ if (opts.captureConsole !== false) {
8133
+ try {
8134
+ cleanups2.push(enableConsoleCapture(page2, session2.id));
8135
+ } catch {}
8136
+ }
8137
+ try {
8138
+ cleanups2.push(setupDialogHandler(page2, session2.id));
8139
+ } catch {}
8140
+ 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 });
8141
+ return { session: session2, page: page2 };
8142
+ }
7865
8143
  const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
7866
8144
  const resolvedEngine = engine === "auto" ? "playwright" : engine;
7867
8145
  let browser = null;
@@ -7887,7 +8165,22 @@ async function createSession2(opts = {}) {
7887
8165
  page = await context.newPage();
7888
8166
  } else {
7889
8167
  browser = await pool.acquire(opts.headless ?? true);
7890
- page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
8168
+ if (opts.storageState) {
8169
+ const { loadStatePath: loadStatePath2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
8170
+ const statePath2 = loadStatePath2(opts.storageState);
8171
+ if (statePath2) {
8172
+ const context = await browser.newContext({
8173
+ viewport: opts.viewport ?? { width: 1280, height: 720 },
8174
+ userAgent: opts.userAgent,
8175
+ storageState: statePath2
8176
+ });
8177
+ page = await context.newPage();
8178
+ } else {
8179
+ page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
8180
+ }
8181
+ } else {
8182
+ page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
8183
+ }
7891
8184
  }
7892
8185
  const sessionName = opts.name ?? (opts.startUrl ? (() => {
7893
8186
  try {
@@ -8003,6 +8296,66 @@ init_types();
8003
8296
  var lastSnapshots = new Map;
8004
8297
  var sessionRefMaps = new Map;
8005
8298
 
8299
+ // src/lib/self-heal.ts
8300
+ async function healSelector(page, selector, sessionId) {
8301
+ const attempts = [];
8302
+ attempts.push(`selector: ${selector}`);
8303
+ try {
8304
+ const loc = page.locator(selector).first();
8305
+ if (await loc.count() > 0) {
8306
+ return { found: true, locator: loc, method: "original", healed: false, attempts };
8307
+ }
8308
+ } catch {}
8309
+ if (!selector.startsWith("#") && !selector.startsWith(".") && !selector.startsWith("[") && !selector.includes(">") && !selector.includes(" ")) {
8310
+ attempts.push(`text: "${selector}"`);
8311
+ try {
8312
+ const loc = page.getByText(selector, { exact: false }).first();
8313
+ if (await loc.count() > 0) {
8314
+ return { found: true, locator: loc, method: "text", healed: true, attempts };
8315
+ }
8316
+ } catch {}
8317
+ }
8318
+ const roleMap = {
8319
+ button: ["button", "submit", "reset"],
8320
+ link: ["a"],
8321
+ input: ["input", "textarea"],
8322
+ heading: ["h1", "h2", "h3", "h4", "h5", "h6"]
8323
+ };
8324
+ const nameHint = selector.replace(/^[#.]/, "").replace(/[-_]/g, " ").toLowerCase();
8325
+ for (const [role, tags] of Object.entries(roleMap)) {
8326
+ attempts.push(`role: ${role} name~="${nameHint}"`);
8327
+ try {
8328
+ const loc = page.getByRole(role, { name: new RegExp(nameHint.split(" ")[0], "i") }).first();
8329
+ if (await loc.count() > 0) {
8330
+ return { found: true, locator: loc, method: "role", healed: true, attempts };
8331
+ }
8332
+ } catch {}
8333
+ }
8334
+ if (selector.startsWith("#")) {
8335
+ const idPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
8336
+ const partialSel = `[id*="${idPart}"]`;
8337
+ attempts.push(`partial_id: ${partialSel}`);
8338
+ try {
8339
+ const loc = page.locator(partialSel).first();
8340
+ if (await loc.count() > 0) {
8341
+ return { found: true, locator: loc, method: "partial_id", healed: true, attempts };
8342
+ }
8343
+ } catch {}
8344
+ }
8345
+ if (selector.startsWith(".")) {
8346
+ const classPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
8347
+ const partialSel = `[class*="${classPart}"]`;
8348
+ attempts.push(`partial_class: ${partialSel}`);
8349
+ try {
8350
+ const loc = page.locator(partialSel).first();
8351
+ if (await loc.count() > 0) {
8352
+ return { found: true, locator: loc, method: "partial_class", healed: true, attempts };
8353
+ }
8354
+ } catch {}
8355
+ }
8356
+ return { found: false, locator: null, method: "none", healed: false, attempts };
8357
+ }
8358
+
8006
8359
  // src/lib/actions.ts
8007
8360
  async function click(page, selector, opts) {
8008
8361
  try {
@@ -8012,11 +8365,22 @@ async function click(page, selector, opts) {
8012
8365
  delay: opts?.delay,
8013
8366
  timeout: opts?.timeout ?? 1e4
8014
8367
  });
8015
- } catch (err) {
8016
- if (err instanceof Error && err.message.includes("not found")) {
8368
+ return {};
8369
+ } catch (originalError) {
8370
+ if (opts?.selfHeal !== false) {
8371
+ const result = await healSelector(page, selector);
8372
+ if (result.found && result.locator) {
8373
+ await result.locator.click({
8374
+ button: opts?.button ?? "left",
8375
+ timeout: opts?.timeout ?? 1e4
8376
+ });
8377
+ return { healed: true, method: result.method, attempts: result.attempts };
8378
+ }
8379
+ }
8380
+ if (originalError instanceof Error && originalError.message.includes("not found")) {
8017
8381
  throw new ElementNotFoundError(selector);
8018
8382
  }
8019
- throw new BrowserError(`Click failed on '${selector}': ${err instanceof Error ? err.message : String(err)}`, "CLICK_FAILED");
8383
+ throw new BrowserError(`Click failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "CLICK_FAILED");
8020
8384
  }
8021
8385
  }
8022
8386
  async function type(page, selector, text, opts) {
@@ -8025,11 +8389,21 @@ async function type(page, selector, text, opts) {
8025
8389
  await page.fill(selector, "", { timeout: opts?.timeout ?? 1e4 });
8026
8390
  }
8027
8391
  await page.type(selector, text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
8028
- } catch (err) {
8029
- if (err instanceof Error && err.message.includes("not found")) {
8392
+ return {};
8393
+ } catch (originalError) {
8394
+ if (opts?.selfHeal !== false) {
8395
+ const result = await healSelector(page, selector);
8396
+ if (result.found && result.locator) {
8397
+ if (opts?.clear)
8398
+ await result.locator.fill("", { timeout: opts?.timeout ?? 1e4 });
8399
+ await result.locator.pressSequentially(text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
8400
+ return { healed: true, method: result.method, attempts: result.attempts };
8401
+ }
8402
+ }
8403
+ if (originalError instanceof Error && originalError.message.includes("not found")) {
8030
8404
  throw new ElementNotFoundError(selector);
8031
8405
  }
8032
- throw new BrowserError(`Type failed on '${selector}': ${err instanceof Error ? err.message : String(err)}`, "TYPE_FAILED");
8406
+ throw new BrowserError(`Type failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "TYPE_FAILED");
8033
8407
  }
8034
8408
  }
8035
8409
  async function scroll(page, direction = "down", amount = 300) {
@@ -8129,9 +8503,9 @@ async function extract(page, opts = {}) {
8129
8503
  // src/lib/screenshot.ts
8130
8504
  init_types();
8131
8505
  var import_sharp = __toESM(require_lib(), 1);
8132
- import { join as join3 } from "path";
8133
- import { mkdirSync as mkdirSync3 } from "fs";
8134
- import { homedir as homedir3 } from "os";
8506
+ import { join as join4 } from "path";
8507
+ import { mkdirSync as mkdirSync4 } from "fs";
8508
+ import { homedir as homedir4 } from "os";
8135
8509
 
8136
8510
  // src/db/gallery.ts
8137
8511
  init_schema();
@@ -8261,13 +8635,13 @@ function getGalleryStats(projectId) {
8261
8635
 
8262
8636
  // src/lib/screenshot.ts
8263
8637
  function getDataDir2() {
8264
- return process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser");
8638
+ return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
8265
8639
  }
8266
8640
  function getScreenshotDir(projectId) {
8267
- const base = join3(getDataDir2(), "screenshots");
8641
+ const base = join4(getDataDir2(), "screenshots");
8268
8642
  const date = new Date().toISOString().split("T")[0];
8269
- const dir = projectId ? join3(base, projectId, date) : join3(base, date);
8270
- mkdirSync3(dir, { recursive: true });
8643
+ const dir = projectId ? join4(base, projectId, date) : join4(base, date);
8644
+ mkdirSync4(dir, { recursive: true });
8271
8645
  return dir;
8272
8646
  }
8273
8647
  async function compressBuffer(raw, format, quality, maxWidth) {
@@ -8282,7 +8656,7 @@ async function compressBuffer(raw, format, quality, maxWidth) {
8282
8656
  }
8283
8657
  }
8284
8658
  async function generateThumbnail(raw, dir, stem) {
8285
- const thumbPath = join3(dir, `${stem}.thumb.webp`);
8659
+ const thumbPath = join4(dir, `${stem}.thumb.webp`);
8286
8660
  const thumbBuffer = await import_sharp.default(raw).resize({ width: 200, withoutEnlargement: true }).webp({ quality: 70, effort: 3 }).toBuffer();
8287
8661
  await Bun.write(thumbPath, thumbBuffer);
8288
8662
  return { path: thumbPath, base64: thumbBuffer.toString("base64") };
@@ -8339,7 +8713,7 @@ async function takeScreenshot(page, opts) {
8339
8713
  const compressedSizeBytes = finalBuffer.length;
8340
8714
  const compressionRatio = originalSizeBytes > 0 ? compressedSizeBytes / originalSizeBytes : 1;
8341
8715
  const ext = format;
8342
- const screenshotPath = opts?.path ?? join3(dir, `${stem}.${ext}`);
8716
+ const screenshotPath = opts?.path ?? join4(dir, `${stem}.${ext}`);
8343
8717
  await Bun.write(screenshotPath, finalBuffer);
8344
8718
  let thumbnailPath;
8345
8719
  let thumbnailBase64;
@@ -8398,118 +8772,8 @@ async function takeScreenshot(page, opts) {
8398
8772
  }
8399
8773
  }
8400
8774
 
8401
- // src/engines/cdp.ts
8402
- init_types();
8403
-
8404
- class CDPClient {
8405
- session;
8406
- networkEnabled = false;
8407
- performanceEnabled = false;
8408
- constructor(session) {
8409
- this.session = session;
8410
- }
8411
- static async fromPage(page) {
8412
- try {
8413
- const session = await page.context().newCDPSession(page);
8414
- return new CDPClient(session);
8415
- } catch (err) {
8416
- throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
8417
- }
8418
- }
8419
- async send(method, params) {
8420
- try {
8421
- return await this.session.send(method, params);
8422
- } catch (err) {
8423
- throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
8424
- }
8425
- }
8426
- on(event, handler) {
8427
- this.session.on(event, handler);
8428
- }
8429
- off(event, handler) {
8430
- this.session.off(event, handler);
8431
- }
8432
- async enableNetwork() {
8433
- if (!this.networkEnabled) {
8434
- await this.send("Network.enable");
8435
- this.networkEnabled = true;
8436
- }
8437
- }
8438
- async enablePerformance() {
8439
- if (!this.performanceEnabled) {
8440
- await this.send("Performance.enable");
8441
- this.performanceEnabled = true;
8442
- }
8443
- }
8444
- async getPerformanceMetrics() {
8445
- await this.enablePerformance();
8446
- const result = await this.send("Performance.getMetrics");
8447
- const m = {};
8448
- for (const metric of result.metrics) {
8449
- m[metric.name] = metric.value;
8450
- }
8451
- return {
8452
- js_heap_size_used: m["JSHeapUsedSize"],
8453
- js_heap_size_total: m["JSHeapTotalSize"],
8454
- dom_interactive: m["DOMInteractive"],
8455
- dom_complete: m["DOMComplete"],
8456
- load_event: m["LoadEventEnd"]
8457
- };
8458
- }
8459
- async startJSCoverage() {
8460
- await this.send("Profiler.enable");
8461
- await this.send("Debugger.enable");
8462
- await this.send("Profiler.startPreciseCoverage", {
8463
- callCount: false,
8464
- detailed: true
8465
- });
8466
- }
8467
- async stopJSCoverage() {
8468
- const result = await this.send("Profiler.takePreciseCoverage");
8469
- await this.send("Profiler.stopPreciseCoverage");
8470
- return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
8471
- url: r.url,
8472
- text: "",
8473
- ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
8474
- }));
8475
- }
8476
- async getCoverage() {
8477
- await this.startJSCoverage();
8478
- const js = await this.stopJSCoverage();
8479
- const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
8480
- return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
8481
- }
8482
- async captureHAREntries(page, handler) {
8483
- await this.enableNetwork();
8484
- const requestTimings = new Map;
8485
- const onRequest = (params) => {
8486
- requestTimings.set(params.requestId, params.timestamp);
8487
- };
8488
- const onResponse = (params) => {
8489
- const start = requestTimings.get(params.requestId);
8490
- const duration = start != null ? (params.timestamp - start) * 1000 : 0;
8491
- handler({
8492
- method: "GET",
8493
- url: params.response.url,
8494
- status: params.response.status,
8495
- duration
8496
- });
8497
- };
8498
- this.on("Network.requestWillBeSent", onRequest);
8499
- this.on("Network.responseReceived", onResponse);
8500
- return () => {
8501
- this.off("Network.requestWillBeSent", onRequest);
8502
- this.off("Network.responseReceived", onResponse);
8503
- };
8504
- }
8505
- async detach() {
8506
- try {
8507
- await this.session.detach();
8508
- } catch {}
8509
- }
8510
- }
8511
-
8512
8775
  // src/lib/performance.ts
8776
+ init_cdp();
8513
8777
  async function getPerformanceMetrics(page) {
8514
8778
  const navTiming = await page.evaluate(() => {
8515
8779
  const t = performance.timing;
@@ -8742,16 +9006,16 @@ init_console_log();
8742
9006
  init_recordings();
8743
9007
 
8744
9008
  // src/lib/downloads.ts
8745
- import { join as join4, basename, extname } from "path";
8746
- import { mkdirSync as mkdirSync4, existsSync, readdirSync, statSync, unlinkSync, copyFileSync, writeFileSync, readFileSync } from "fs";
8747
- import { homedir as homedir4 } from "os";
9009
+ import { join as join5, basename, extname } from "path";
9010
+ import { mkdirSync as mkdirSync5, existsSync as existsSync2, readdirSync as readdirSync2, statSync, unlinkSync as unlinkSync2, copyFileSync, writeFileSync, readFileSync } from "fs";
9011
+ import { homedir as homedir5 } from "os";
8748
9012
  function getDataDir3() {
8749
- return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
9013
+ return process.env["BROWSER_DATA_DIR"] ?? join5(homedir5(), ".browser");
8750
9014
  }
8751
9015
  function getDownloadsDir(sessionId) {
8752
- const base = join4(getDataDir3(), "downloads");
8753
- const dir = sessionId ? join4(base, sessionId) : base;
8754
- mkdirSync4(dir, { recursive: true });
9016
+ const base = join5(getDataDir3(), "downloads");
9017
+ const dir = sessionId ? join5(base, sessionId) : base;
9018
+ mkdirSync5(dir, { recursive: true });
8755
9019
  return dir;
8756
9020
  }
8757
9021
  function metaPath(filePath) {
@@ -8761,20 +9025,20 @@ function listDownloads(sessionId) {
8761
9025
  const dir = getDownloadsDir(sessionId);
8762
9026
  const results = [];
8763
9027
  function scanDir(d) {
8764
- if (!existsSync(d))
9028
+ if (!existsSync2(d))
8765
9029
  return;
8766
- const entries = readdirSync(d);
9030
+ const entries = readdirSync2(d);
8767
9031
  for (const entry of entries) {
8768
9032
  if (entry.endsWith(".meta.json"))
8769
9033
  continue;
8770
- const full = join4(d, entry);
9034
+ const full = join5(d, entry);
8771
9035
  const stat = statSync(full);
8772
9036
  if (stat.isDirectory()) {
8773
9037
  scanDir(full);
8774
9038
  continue;
8775
9039
  }
8776
9040
  const mpath = metaPath(full);
8777
- if (!existsSync(mpath))
9041
+ if (!existsSync2(mpath))
8778
9042
  continue;
8779
9043
  try {
8780
9044
  const meta = JSON.parse(readFileSync(mpath, "utf8"));
@@ -8804,9 +9068,9 @@ function deleteDownload(id, sessionId) {
8804
9068
  if (!file)
8805
9069
  return false;
8806
9070
  try {
8807
- unlinkSync(file.path);
8808
- if (existsSync(file.meta_path))
8809
- unlinkSync(file.meta_path);
9071
+ unlinkSync2(file.path);
9072
+ if (existsSync2(file.meta_path))
9073
+ unlinkSync2(file.meta_path);
8810
9074
  return true;
8811
9075
  } catch {
8812
9076
  return false;
@@ -8828,9 +9092,9 @@ function cleanStaleDownloads(olderThanDays = 7) {
8828
9092
 
8829
9093
  // src/lib/gallery-diff.ts
8830
9094
  var import_sharp2 = __toESM(require_lib(), 1);
8831
- import { join as join5 } from "path";
8832
- import { mkdirSync as mkdirSync5 } from "fs";
8833
- import { homedir as homedir5 } from "os";
9095
+ import { join as join6 } from "path";
9096
+ import { mkdirSync as mkdirSync6 } from "fs";
9097
+ import { homedir as homedir6 } from "os";
8834
9098
  async function diffImages(path1, path2) {
8835
9099
  const img1 = import_sharp2.default(path1);
8836
9100
  const img2 = import_sharp2.default(path2);
@@ -8861,10 +9125,10 @@ async function diffImages(path1, path2) {
8861
9125
  diffBuffer[i + 2] = Math.round(raw1[i + 2] * 0.4);
8862
9126
  }
8863
9127
  }
8864
- const dataDir = process.env["BROWSER_DATA_DIR"] ?? join5(homedir5(), ".browser");
8865
- const diffDir = join5(dataDir, "diffs");
8866
- mkdirSync5(diffDir, { recursive: true });
8867
- const diffPath = join5(diffDir, `diff-${Date.now()}.webp`);
9128
+ const dataDir = process.env["BROWSER_DATA_DIR"] ?? join6(homedir6(), ".browser");
9129
+ const diffDir = join6(dataDir, "diffs");
9130
+ mkdirSync6(diffDir, { recursive: true });
9131
+ const diffPath = join6(diffDir, `diff-${Date.now()}.webp`);
8868
9132
  const diffImageBuffer = await import_sharp2.default(diffBuffer, { raw: { width: w, height: h, channels } }).webp({ quality: 85 }).toBuffer();
8869
9133
  await Bun.write(diffPath, diffImageBuffer);
8870
9134
  return {
@@ -9108,14 +9372,14 @@ var server = Bun.serve({
9108
9372
  if (path.match(/^\/api\/gallery\/([^/]+)\/thumbnail$/) && method === "GET") {
9109
9373
  const id = path.split("/")[3];
9110
9374
  const entry = getEntry(id);
9111
- if (!entry?.thumbnail_path || !existsSync3(entry.thumbnail_path))
9375
+ if (!entry?.thumbnail_path || !existsSync4(entry.thumbnail_path))
9112
9376
  return notFound("Thumbnail not found");
9113
9377
  return new Response(Bun.file(entry.thumbnail_path), { headers: { ...CORS_HEADERS } });
9114
9378
  }
9115
9379
  if (path.match(/^\/api\/gallery\/([^/]+)\/image$/) && method === "GET") {
9116
9380
  const id = path.split("/")[3];
9117
9381
  const entry = getEntry(id);
9118
- if (!entry?.path || !existsSync3(entry.path))
9382
+ if (!entry?.path || !existsSync4(entry.path))
9119
9383
  return notFound("Image not found");
9120
9384
  return new Response(Bun.file(entry.path), { headers: { ...CORS_HEADERS } });
9121
9385
  }
@@ -9143,7 +9407,7 @@ var server = Bun.serve({
9143
9407
  if (path.match(/^\/api\/downloads\/([^/]+)\/raw$/) && method === "GET") {
9144
9408
  const id = path.split("/")[3];
9145
9409
  const file = getDownload(id);
9146
- if (!file || !existsSync3(file.path))
9410
+ if (!file || !existsSync4(file.path))
9147
9411
  return notFound("Download not found");
9148
9412
  return new Response(Bun.file(file.path), { headers: { ...CORS_HEADERS } });
9149
9413
  }
@@ -9151,13 +9415,13 @@ var server = Bun.serve({
9151
9415
  const id = path.split("/")[3];
9152
9416
  return ok({ deleted: deleteDownload(id) });
9153
9417
  }
9154
- const dashboardDist = join6(import.meta.dir, "../../dashboard/dist");
9155
- if (existsSync3(dashboardDist)) {
9156
- const filePath = path === "/" ? join6(dashboardDist, "index.html") : join6(dashboardDist, path);
9157
- if (existsSync3(filePath)) {
9418
+ const dashboardDist = join7(import.meta.dir, "../../dashboard/dist");
9419
+ if (existsSync4(dashboardDist)) {
9420
+ const filePath = path === "/" ? join7(dashboardDist, "index.html") : join7(dashboardDist, path);
9421
+ if (existsSync4(filePath)) {
9158
9422
  return new Response(Bun.file(filePath), { headers: CORS_HEADERS });
9159
9423
  }
9160
- return new Response(Bun.file(join6(dashboardDist, "index.html")), { headers: CORS_HEADERS });
9424
+ return new Response(Bun.file(join7(dashboardDist, "index.html")), { headers: CORS_HEADERS });
9161
9425
  }
9162
9426
  if (path === "/" || path === "") {
9163
9427
  return new Response("@hasna/browser REST API running. Dashboard not built.", {