@hasna/browser 0.0.9 → 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.
package/dist/index.js CHANGED
@@ -28,6 +28,79 @@ var __export = (target, all) => {
28
28
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
29
29
  var __require = import.meta.require;
30
30
 
31
+ // src/types/index.ts
32
+ var UseCase, BrowserError, SessionNotFoundError, EngineNotAvailableError, NavigationError, ElementNotFoundError, RecordingNotFoundError, AgentNotFoundError, ProjectNotFoundError;
33
+ var init_types = __esm(() => {
34
+ ((UseCase2) => {
35
+ UseCase2["SCRAPE"] = "scrape";
36
+ UseCase2["EXTRACT_LINKS"] = "extract_links";
37
+ UseCase2["STATUS_CHECK"] = "status_check";
38
+ UseCase2["FORM_FILL"] = "form_fill";
39
+ UseCase2["SPA_NAVIGATE"] = "spa_navigate";
40
+ UseCase2["SCREENSHOT"] = "screenshot";
41
+ UseCase2["AUTH_FLOW"] = "auth_flow";
42
+ UseCase2["MULTI_TAB"] = "multi_tab";
43
+ UseCase2["NETWORK_MONITOR"] = "network_monitor";
44
+ UseCase2["HAR_CAPTURE"] = "har_capture";
45
+ UseCase2["PERF_PROFILE"] = "perf_profile";
46
+ UseCase2["SCRIPT_INJECT"] = "script_inject";
47
+ UseCase2["COVERAGE"] = "coverage";
48
+ UseCase2["RECORD_REPLAY"] = "record_replay";
49
+ })(UseCase ||= {});
50
+ BrowserError = class BrowserError extends Error {
51
+ code;
52
+ retryable;
53
+ constructor(message, code = "BROWSER_ERROR", retryable = false) {
54
+ super(message);
55
+ this.code = code;
56
+ this.retryable = retryable;
57
+ this.name = "BrowserError";
58
+ }
59
+ };
60
+ SessionNotFoundError = class SessionNotFoundError extends BrowserError {
61
+ constructor(id) {
62
+ super(`Session not found: ${id}`, "SESSION_NOT_FOUND", false);
63
+ this.name = "SessionNotFoundError";
64
+ }
65
+ };
66
+ EngineNotAvailableError = class EngineNotAvailableError extends BrowserError {
67
+ constructor(engine, reason) {
68
+ super(`Engine '${engine}' is not available${reason ? `: ${reason}` : ""}`, "ENGINE_NOT_AVAILABLE", false);
69
+ this.name = "EngineNotAvailableError";
70
+ }
71
+ };
72
+ NavigationError = class NavigationError extends BrowserError {
73
+ constructor(url, reason) {
74
+ super(`Navigation to '${url}' failed${reason ? `: ${reason}` : ""}`, "NAVIGATION_ERROR", true);
75
+ this.name = "NavigationError";
76
+ }
77
+ };
78
+ ElementNotFoundError = class ElementNotFoundError extends BrowserError {
79
+ constructor(selector) {
80
+ super(`Element not found: ${selector}`, "ELEMENT_NOT_FOUND", false);
81
+ this.name = "ElementNotFoundError";
82
+ }
83
+ };
84
+ RecordingNotFoundError = class RecordingNotFoundError extends BrowserError {
85
+ constructor(id) {
86
+ super(`Recording not found: ${id}`, "RECORDING_NOT_FOUND", false);
87
+ this.name = "RecordingNotFoundError";
88
+ }
89
+ };
90
+ AgentNotFoundError = class AgentNotFoundError extends BrowserError {
91
+ constructor(id) {
92
+ super(`Agent not found: ${id}`, "AGENT_NOT_FOUND", false);
93
+ this.name = "AgentNotFoundError";
94
+ }
95
+ };
96
+ ProjectNotFoundError = class ProjectNotFoundError extends BrowserError {
97
+ constructor(id) {
98
+ super(`Project not found: ${id}`, "PROJECT_NOT_FOUND", false);
99
+ this.name = "ProjectNotFoundError";
100
+ }
101
+ };
102
+ });
103
+
31
104
  // src/db/schema.ts
32
105
  import { Database } from "bun:sqlite";
33
106
  import { join } from "path";
@@ -243,6 +316,23 @@ function runMigrations(db) {
243
316
  );
244
317
  CREATE INDEX IF NOT EXISTS idx_session_tags_tag ON session_tags(tag);
245
318
  `
319
+ },
320
+ {
321
+ version: 6,
322
+ sql: `
323
+ CREATE TABLE IF NOT EXISTS auth_flows (
324
+ id TEXT PRIMARY KEY,
325
+ name TEXT NOT NULL UNIQUE,
326
+ domain TEXT NOT NULL,
327
+ recording_id TEXT REFERENCES recordings(id),
328
+ storage_state_path TEXT,
329
+ created_at TEXT DEFAULT (datetime('now')),
330
+ last_used TEXT
331
+ );
332
+
333
+ CREATE INDEX IF NOT EXISTS idx_auth_flows_domain ON auth_flows(domain);
334
+ CREATE INDEX IF NOT EXISTS idx_auth_flows_name ON auth_flows(name);
335
+ `
246
336
  }
247
337
  ];
248
338
  for (const m of migrations) {
@@ -291,6 +381,188 @@ var init_console_log = __esm(() => {
291
381
  init_schema();
292
382
  });
293
383
 
384
+ // src/engines/cdp.ts
385
+ var exports_cdp = {};
386
+ __export(exports_cdp, {
387
+ connectToExistingBrowser: () => connectToExistingBrowser,
388
+ CDPClient: () => CDPClient
389
+ });
390
+ async function connectToExistingBrowser(cdpUrl) {
391
+ const { chromium: chromium2 } = await import("playwright");
392
+ try {
393
+ return await chromium2.connectOverCDP(cdpUrl);
394
+ } catch (err) {
395
+ 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);
396
+ }
397
+ }
398
+
399
+ class CDPClient {
400
+ session;
401
+ networkEnabled = false;
402
+ performanceEnabled = false;
403
+ constructor(session) {
404
+ this.session = session;
405
+ }
406
+ static async fromPage(page) {
407
+ try {
408
+ const session = await page.context().newCDPSession(page);
409
+ return new CDPClient(session);
410
+ } catch (err) {
411
+ throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
412
+ }
413
+ }
414
+ async send(method, params) {
415
+ try {
416
+ return await this.session.send(method, params);
417
+ } catch (err) {
418
+ throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
419
+ }
420
+ }
421
+ on(event, handler) {
422
+ this.session.on(event, handler);
423
+ }
424
+ off(event, handler) {
425
+ this.session.off(event, handler);
426
+ }
427
+ async enableNetwork() {
428
+ if (!this.networkEnabled) {
429
+ await this.send("Network.enable");
430
+ this.networkEnabled = true;
431
+ }
432
+ }
433
+ async enablePerformance() {
434
+ if (!this.performanceEnabled) {
435
+ await this.send("Performance.enable");
436
+ this.performanceEnabled = true;
437
+ }
438
+ }
439
+ async getPerformanceMetrics() {
440
+ await this.enablePerformance();
441
+ const result = await this.send("Performance.getMetrics");
442
+ const m = {};
443
+ for (const metric of result.metrics) {
444
+ m[metric.name] = metric.value;
445
+ }
446
+ return {
447
+ js_heap_size_used: m["JSHeapUsedSize"],
448
+ js_heap_size_total: m["JSHeapTotalSize"],
449
+ dom_interactive: m["DOMInteractive"],
450
+ dom_complete: m["DOMComplete"],
451
+ load_event: m["LoadEventEnd"]
452
+ };
453
+ }
454
+ async startJSCoverage() {
455
+ await this.send("Profiler.enable");
456
+ await this.send("Debugger.enable");
457
+ await this.send("Profiler.startPreciseCoverage", {
458
+ callCount: false,
459
+ detailed: true
460
+ });
461
+ }
462
+ async stopJSCoverage() {
463
+ const result = await this.send("Profiler.takePreciseCoverage");
464
+ await this.send("Profiler.stopPreciseCoverage");
465
+ return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
466
+ url: r.url,
467
+ text: "",
468
+ ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
469
+ }));
470
+ }
471
+ async getCoverage() {
472
+ await this.startJSCoverage();
473
+ const js = await this.stopJSCoverage();
474
+ const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
475
+ return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
476
+ }
477
+ async captureHAREntries(page, handler) {
478
+ await this.enableNetwork();
479
+ const requestTimings = new Map;
480
+ const onRequest = (params) => {
481
+ requestTimings.set(params.requestId, params.timestamp);
482
+ };
483
+ const onResponse = (params) => {
484
+ const start = requestTimings.get(params.requestId);
485
+ const duration = start != null ? (params.timestamp - start) * 1000 : 0;
486
+ handler({
487
+ method: "GET",
488
+ url: params.response.url,
489
+ status: params.response.status,
490
+ duration
491
+ });
492
+ };
493
+ this.on("Network.requestWillBeSent", onRequest);
494
+ this.on("Network.responseReceived", onResponse);
495
+ return () => {
496
+ this.off("Network.requestWillBeSent", onRequest);
497
+ this.off("Network.responseReceived", onResponse);
498
+ };
499
+ }
500
+ async detach() {
501
+ try {
502
+ await this.session.detach();
503
+ } catch {}
504
+ }
505
+ }
506
+ var init_cdp = __esm(() => {
507
+ init_types();
508
+ });
509
+
510
+ // src/lib/storage-state.ts
511
+ var exports_storage_state = {};
512
+ __export(exports_storage_state, {
513
+ saveStateFromPage: () => saveStateFromPage,
514
+ saveState: () => saveState,
515
+ loadStatePath: () => loadStatePath,
516
+ listStates: () => listStates,
517
+ deleteState: () => deleteState
518
+ });
519
+ import { mkdirSync as mkdirSync3, existsSync, readdirSync, unlinkSync } from "fs";
520
+ import { join as join3 } from "path";
521
+ import { homedir as homedir3 } from "os";
522
+ function ensureDir() {
523
+ mkdirSync3(STATES_DIR, { recursive: true });
524
+ }
525
+ function statePath(name) {
526
+ return join3(STATES_DIR, `${name}.json`);
527
+ }
528
+ async function saveState(context, name) {
529
+ ensureDir();
530
+ const path = statePath(name);
531
+ const state = await context.storageState({ path });
532
+ return path;
533
+ }
534
+ async function saveStateFromPage(page, name) {
535
+ return saveState(page.context(), name);
536
+ }
537
+ function loadStatePath(name) {
538
+ const path = statePath(name);
539
+ return existsSync(path) ? path : null;
540
+ }
541
+ function listStates() {
542
+ ensureDir();
543
+ return readdirSync(STATES_DIR).filter((f) => f.endsWith(".json")).map((f) => {
544
+ const path = join3(STATES_DIR, f);
545
+ const stat = Bun.file(path);
546
+ return {
547
+ name: f.replace(".json", ""),
548
+ path,
549
+ modified: new Date(stat.lastModified).toISOString()
550
+ };
551
+ }).sort((a, b) => b.modified.localeCompare(a.modified));
552
+ }
553
+ function deleteState(name) {
554
+ const path = statePath(name);
555
+ if (existsSync(path)) {
556
+ unlinkSync(path);
557
+ return true;
558
+ }
559
+ return false;
560
+ }
561
+ var STATES_DIR;
562
+ var init_storage_state = __esm(() => {
563
+ STATES_DIR = join3(process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser"), "states");
564
+ });
565
+
294
566
  // node_modules/sharp/lib/is.js
295
567
  var require_is = __commonJS((exports, module) => {
296
568
  /*!
@@ -6686,90 +6958,13 @@ var require_lib = __commonJS((exports, module) => {
6686
6958
  module.exports = Sharp;
6687
6959
  });
6688
6960
 
6689
- // src/types/index.ts
6690
- var UseCase;
6691
- ((UseCase2) => {
6692
- UseCase2["SCRAPE"] = "scrape";
6693
- UseCase2["EXTRACT_LINKS"] = "extract_links";
6694
- UseCase2["STATUS_CHECK"] = "status_check";
6695
- UseCase2["FORM_FILL"] = "form_fill";
6696
- UseCase2["SPA_NAVIGATE"] = "spa_navigate";
6697
- UseCase2["SCREENSHOT"] = "screenshot";
6698
- UseCase2["AUTH_FLOW"] = "auth_flow";
6699
- UseCase2["MULTI_TAB"] = "multi_tab";
6700
- UseCase2["NETWORK_MONITOR"] = "network_monitor";
6701
- UseCase2["HAR_CAPTURE"] = "har_capture";
6702
- UseCase2["PERF_PROFILE"] = "perf_profile";
6703
- UseCase2["SCRIPT_INJECT"] = "script_inject";
6704
- UseCase2["COVERAGE"] = "coverage";
6705
- UseCase2["RECORD_REPLAY"] = "record_replay";
6706
- })(UseCase ||= {});
6707
-
6708
- class BrowserError extends Error {
6709
- code;
6710
- retryable;
6711
- constructor(message, code = "BROWSER_ERROR", retryable = false) {
6712
- super(message);
6713
- this.code = code;
6714
- this.retryable = retryable;
6715
- this.name = "BrowserError";
6716
- }
6717
- }
6718
-
6719
- class SessionNotFoundError extends BrowserError {
6720
- constructor(id) {
6721
- super(`Session not found: ${id}`, "SESSION_NOT_FOUND", false);
6722
- this.name = "SessionNotFoundError";
6723
- }
6724
- }
6725
-
6726
- class EngineNotAvailableError extends BrowserError {
6727
- constructor(engine, reason) {
6728
- super(`Engine '${engine}' is not available${reason ? `: ${reason}` : ""}`, "ENGINE_NOT_AVAILABLE", false);
6729
- this.name = "EngineNotAvailableError";
6730
- }
6731
- }
6732
-
6733
- class NavigationError extends BrowserError {
6734
- constructor(url, reason) {
6735
- super(`Navigation to '${url}' failed${reason ? `: ${reason}` : ""}`, "NAVIGATION_ERROR", true);
6736
- this.name = "NavigationError";
6737
- }
6738
- }
6739
-
6740
- class ElementNotFoundError extends BrowserError {
6741
- constructor(selector) {
6742
- super(`Element not found: ${selector}`, "ELEMENT_NOT_FOUND", false);
6743
- this.name = "ElementNotFoundError";
6744
- }
6745
- }
6746
-
6747
- class RecordingNotFoundError extends BrowserError {
6748
- constructor(id) {
6749
- super(`Recording not found: ${id}`, "RECORDING_NOT_FOUND", false);
6750
- this.name = "RecordingNotFoundError";
6751
- }
6752
- }
6753
-
6754
- class AgentNotFoundError extends BrowserError {
6755
- constructor(id) {
6756
- super(`Agent not found: ${id}`, "AGENT_NOT_FOUND", false);
6757
- this.name = "AgentNotFoundError";
6758
- }
6759
- }
6760
-
6761
- class ProjectNotFoundError extends BrowserError {
6762
- constructor(id) {
6763
- super(`Project not found: ${id}`, "PROJECT_NOT_FOUND", false);
6764
- this.name = "ProjectNotFoundError";
6765
- }
6766
- }
6767
-
6768
6961
  // src/index.ts
6769
6962
  init_schema();
6963
+ init_types();
6770
6964
 
6771
6965
  // src/db/projects.ts
6772
6966
  init_schema();
6967
+ init_types();
6773
6968
  import { randomUUID } from "crypto";
6774
6969
  function createProject(data) {
6775
6970
  const db = getDatabase();
@@ -6827,6 +7022,7 @@ function deleteProject(id) {
6827
7022
  }
6828
7023
  // src/db/agents.ts
6829
7024
  init_schema();
7025
+ init_types();
6830
7026
  import { randomUUID as randomUUID2 } from "crypto";
6831
7027
  function registerAgent(name, opts = {}) {
6832
7028
  const db = getDatabase();
@@ -6907,6 +7103,7 @@ function cleanStaleAgents(thresholdMs) {
6907
7103
  }
6908
7104
  // src/db/sessions.ts
6909
7105
  init_schema();
7106
+ init_types();
6910
7107
  import { randomUUID as randomUUID3 } from "crypto";
6911
7108
  function createSession(data) {
6912
7109
  const db = getDatabase();
@@ -7039,6 +7236,7 @@ init_console_log();
7039
7236
 
7040
7237
  // src/db/recordings.ts
7041
7238
  init_schema();
7239
+ init_types();
7042
7240
  import { randomUUID as randomUUID7 } from "crypto";
7043
7241
  function deserialize(row) {
7044
7242
  return {
@@ -7153,6 +7351,7 @@ function cleanOldHeartbeats(olderThanMs) {
7153
7351
  return result.changes;
7154
7352
  }
7155
7353
  // src/engines/playwright.ts
7354
+ init_types();
7156
7355
  import { chromium } from "playwright";
7157
7356
  var DEFAULT_VIEWPORT = { width: 1280, height: 720 };
7158
7357
  async function launchPlaywright(options) {
@@ -7230,115 +7429,12 @@ class BrowserPool {
7230
7429
  return this.pool.filter((e) => !e.inUse).length;
7231
7430
  }
7232
7431
  }
7233
- // src/engines/cdp.ts
7234
- class CDPClient {
7235
- session;
7236
- networkEnabled = false;
7237
- performanceEnabled = false;
7238
- constructor(session) {
7239
- this.session = session;
7240
- }
7241
- static async fromPage(page) {
7242
- try {
7243
- const session = await page.context().newCDPSession(page);
7244
- return new CDPClient(session);
7245
- } catch (err) {
7246
- throw new BrowserError(`Failed to create CDP session: ${err instanceof Error ? err.message : String(err)}`, "CDP_SESSION_FAILED");
7247
- }
7248
- }
7249
- async send(method, params) {
7250
- try {
7251
- return await this.session.send(method, params);
7252
- } catch (err) {
7253
- throw new BrowserError(`CDP command '${method}' failed: ${err instanceof Error ? err.message : String(err)}`, "CDP_COMMAND_FAILED");
7254
- }
7255
- }
7256
- on(event, handler) {
7257
- this.session.on(event, handler);
7258
- }
7259
- off(event, handler) {
7260
- this.session.off(event, handler);
7261
- }
7262
- async enableNetwork() {
7263
- if (!this.networkEnabled) {
7264
- await this.send("Network.enable");
7265
- this.networkEnabled = true;
7266
- }
7267
- }
7268
- async enablePerformance() {
7269
- if (!this.performanceEnabled) {
7270
- await this.send("Performance.enable");
7271
- this.performanceEnabled = true;
7272
- }
7273
- }
7274
- async getPerformanceMetrics() {
7275
- await this.enablePerformance();
7276
- const result = await this.send("Performance.getMetrics");
7277
- const m = {};
7278
- for (const metric of result.metrics) {
7279
- m[metric.name] = metric.value;
7280
- }
7281
- return {
7282
- js_heap_size_used: m["JSHeapUsedSize"],
7283
- js_heap_size_total: m["JSHeapTotalSize"],
7284
- dom_interactive: m["DOMInteractive"],
7285
- dom_complete: m["DOMComplete"],
7286
- load_event: m["LoadEventEnd"]
7287
- };
7288
- }
7289
- async startJSCoverage() {
7290
- await this.send("Profiler.enable");
7291
- await this.send("Debugger.enable");
7292
- await this.send("Profiler.startPreciseCoverage", {
7293
- callCount: false,
7294
- detailed: true
7295
- });
7296
- }
7297
- async stopJSCoverage() {
7298
- const result = await this.send("Profiler.takePreciseCoverage");
7299
- await this.send("Profiler.stopPreciseCoverage");
7300
- return result.result.filter((r) => r.url && !r.url.startsWith("v8-snapshot://")).map((r) => ({
7301
- url: r.url,
7302
- text: "",
7303
- ranges: r.functions.flatMap((f) => f.ranges.filter((rng) => rng.count > 0).map((rng) => ({ start: rng.startOffset, end: rng.endOffset })))
7304
- }));
7305
- }
7306
- async getCoverage() {
7307
- await this.startJSCoverage();
7308
- const js = await this.stopJSCoverage();
7309
- const totalBytes = js.reduce((acc, e) => acc + e.ranges.reduce((sum, r) => sum + (r.end - r.start), 0), 0);
7310
- return { js, css: [], totalBytes, usedBytes: totalBytes, unusedPercent: 0 };
7311
- }
7312
- async captureHAREntries(page, handler) {
7313
- await this.enableNetwork();
7314
- const requestTimings = new Map;
7315
- const onRequest = (params) => {
7316
- requestTimings.set(params.requestId, params.timestamp);
7317
- };
7318
- const onResponse = (params) => {
7319
- const start = requestTimings.get(params.requestId);
7320
- const duration = start != null ? (params.timestamp - start) * 1000 : 0;
7321
- handler({
7322
- method: "GET",
7323
- url: params.response.url,
7324
- status: params.response.status,
7325
- duration
7326
- });
7327
- };
7328
- this.on("Network.requestWillBeSent", onRequest);
7329
- this.on("Network.responseReceived", onResponse);
7330
- return () => {
7331
- this.off("Network.requestWillBeSent", onRequest);
7332
- this.off("Network.responseReceived", onResponse);
7333
- };
7334
- }
7335
- async detach() {
7336
- try {
7337
- await this.session.detach();
7338
- } catch {}
7339
- }
7340
- }
7432
+
7433
+ // src/index.ts
7434
+ init_cdp();
7435
+
7341
7436
  // src/engines/lightpanda.ts
7437
+ init_types();
7342
7438
  import { execSync, spawn } from "child_process";
7343
7439
  import { chromium as chromium2 } from "playwright";
7344
7440
  var DEFAULT_LIGHTPANDA_PORT = 9222;
@@ -7480,6 +7576,9 @@ class LightpandaPage {
7480
7576
  await this.page.context().close();
7481
7577
  }
7482
7578
  }
7579
+ // src/engines/selector.ts
7580
+ init_types();
7581
+
7483
7582
  // src/engines/bun-webview.ts
7484
7583
  import { join as join2 } from "path";
7485
7584
  import { mkdirSync as mkdirSync2 } from "fs";
@@ -7953,6 +8052,10 @@ function inferUseCase(label) {
7953
8052
  };
7954
8053
  return map[label.toLowerCase()] ?? "spa_navigate" /* SPA_NAVIGATE */;
7955
8054
  }
8055
+ // src/lib/session.ts
8056
+ init_types();
8057
+ init_types();
8058
+
7956
8059
  // src/lib/network.ts
7957
8060
  function enableNetworkLogging(page, sessionId) {
7958
8061
  const requestStart = new Map;
@@ -8229,6 +8332,37 @@ function createBunProxy(view) {
8229
8332
  return view;
8230
8333
  }
8231
8334
  async function createSession2(opts = {}) {
8335
+ if (opts.cdpUrl) {
8336
+ const { connectToExistingBrowser: connectToExistingBrowser2 } = await Promise.resolve().then(() => (init_cdp(), exports_cdp));
8337
+ const cdpBrowser = await connectToExistingBrowser2(opts.cdpUrl);
8338
+ const contexts = cdpBrowser.contexts();
8339
+ const context = contexts.length > 0 ? contexts[0] : await cdpBrowser.newContext();
8340
+ const pages = context.pages();
8341
+ const page2 = pages.length > 0 ? pages[0] : await context.newPage();
8342
+ const session2 = createSession({
8343
+ engine: "cdp",
8344
+ projectId: opts.projectId,
8345
+ agentId: opts.agentId,
8346
+ startUrl: page2.url(),
8347
+ name: opts.name ?? "attached"
8348
+ });
8349
+ const cleanups2 = [];
8350
+ if (opts.captureNetwork !== false) {
8351
+ try {
8352
+ cleanups2.push(enableNetworkLogging(page2, session2.id));
8353
+ } catch {}
8354
+ }
8355
+ if (opts.captureConsole !== false) {
8356
+ try {
8357
+ cleanups2.push(enableConsoleCapture(page2, session2.id));
8358
+ } catch {}
8359
+ }
8360
+ try {
8361
+ cleanups2.push(setupDialogHandler(page2, session2.id));
8362
+ } catch {}
8363
+ 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 });
8364
+ return { session: session2, page: page2 };
8365
+ }
8232
8366
  const engine = opts.engine === "auto" || !opts.engine ? selectEngine(opts.useCase ?? "spa_navigate" /* SPA_NAVIGATE */, opts.engine) : opts.engine;
8233
8367
  const resolvedEngine = engine === "auto" ? "playwright" : engine;
8234
8368
  let browser = null;
@@ -8254,7 +8388,22 @@ async function createSession2(opts = {}) {
8254
8388
  page = await context.newPage();
8255
8389
  } else {
8256
8390
  browser = await pool.acquire(opts.headless ?? true);
8257
- page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
8391
+ if (opts.storageState) {
8392
+ const { loadStatePath: loadStatePath2 } = await Promise.resolve().then(() => (init_storage_state(), exports_storage_state));
8393
+ const statePath2 = loadStatePath2(opts.storageState);
8394
+ if (statePath2) {
8395
+ const context = await browser.newContext({
8396
+ viewport: opts.viewport ?? { width: 1280, height: 720 },
8397
+ userAgent: opts.userAgent,
8398
+ storageState: statePath2
8399
+ });
8400
+ page = await context.newPage();
8401
+ } else {
8402
+ page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
8403
+ }
8404
+ } else {
8405
+ page = await getPage(browser, { viewport: opts.viewport, userAgent: opts.userAgent });
8406
+ }
8258
8407
  }
8259
8408
  const sessionName = opts.name ?? (opts.startUrl ? (() => {
8260
8409
  try {
@@ -8452,6 +8601,9 @@ function isAutoGallery(sessionId) {
8452
8601
  function countActiveSessions2() {
8453
8602
  return countActiveSessions();
8454
8603
  }
8604
+ // src/lib/actions.ts
8605
+ init_types();
8606
+
8455
8607
  // src/lib/snapshot.ts
8456
8608
  var lastSnapshots = new Map;
8457
8609
  var sessionRefMaps = new Map;
@@ -8465,6 +8617,66 @@ function getRefLocator(page, sessionId, ref) {
8465
8617
  return page.getByRole(entry.role, { name: entry.name }).first();
8466
8618
  }
8467
8619
 
8620
+ // src/lib/self-heal.ts
8621
+ async function healSelector(page, selector, sessionId) {
8622
+ const attempts = [];
8623
+ attempts.push(`selector: ${selector}`);
8624
+ try {
8625
+ const loc = page.locator(selector).first();
8626
+ if (await loc.count() > 0) {
8627
+ return { found: true, locator: loc, method: "original", healed: false, attempts };
8628
+ }
8629
+ } catch {}
8630
+ if (!selector.startsWith("#") && !selector.startsWith(".") && !selector.startsWith("[") && !selector.includes(">") && !selector.includes(" ")) {
8631
+ attempts.push(`text: "${selector}"`);
8632
+ try {
8633
+ const loc = page.getByText(selector, { exact: false }).first();
8634
+ if (await loc.count() > 0) {
8635
+ return { found: true, locator: loc, method: "text", healed: true, attempts };
8636
+ }
8637
+ } catch {}
8638
+ }
8639
+ const roleMap = {
8640
+ button: ["button", "submit", "reset"],
8641
+ link: ["a"],
8642
+ input: ["input", "textarea"],
8643
+ heading: ["h1", "h2", "h3", "h4", "h5", "h6"]
8644
+ };
8645
+ const nameHint = selector.replace(/^[#.]/, "").replace(/[-_]/g, " ").toLowerCase();
8646
+ for (const [role, tags] of Object.entries(roleMap)) {
8647
+ attempts.push(`role: ${role} name~="${nameHint}"`);
8648
+ try {
8649
+ const loc = page.getByRole(role, { name: new RegExp(nameHint.split(" ")[0], "i") }).first();
8650
+ if (await loc.count() > 0) {
8651
+ return { found: true, locator: loc, method: "role", healed: true, attempts };
8652
+ }
8653
+ } catch {}
8654
+ }
8655
+ if (selector.startsWith("#")) {
8656
+ const idPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
8657
+ const partialSel = `[id*="${idPart}"]`;
8658
+ attempts.push(`partial_id: ${partialSel}`);
8659
+ try {
8660
+ const loc = page.locator(partialSel).first();
8661
+ if (await loc.count() > 0) {
8662
+ return { found: true, locator: loc, method: "partial_id", healed: true, attempts };
8663
+ }
8664
+ } catch {}
8665
+ }
8666
+ if (selector.startsWith(".")) {
8667
+ const classPart = selector.slice(1).split("-").pop() ?? selector.slice(1);
8668
+ const partialSel = `[class*="${classPart}"]`;
8669
+ attempts.push(`partial_class: ${partialSel}`);
8670
+ try {
8671
+ const loc = page.locator(partialSel).first();
8672
+ if (await loc.count() > 0) {
8673
+ return { found: true, locator: loc, method: "partial_class", healed: true, attempts };
8674
+ }
8675
+ } catch {}
8676
+ }
8677
+ return { found: false, locator: null, method: "none", healed: false, attempts };
8678
+ }
8679
+
8468
8680
  // src/lib/actions.ts
8469
8681
  async function click(page, selector, opts) {
8470
8682
  try {
@@ -8474,11 +8686,22 @@ async function click(page, selector, opts) {
8474
8686
  delay: opts?.delay,
8475
8687
  timeout: opts?.timeout ?? 1e4
8476
8688
  });
8477
- } catch (err) {
8478
- if (err instanceof Error && err.message.includes("not found")) {
8689
+ return {};
8690
+ } catch (originalError) {
8691
+ if (opts?.selfHeal !== false) {
8692
+ const result = await healSelector(page, selector);
8693
+ if (result.found && result.locator) {
8694
+ await result.locator.click({
8695
+ button: opts?.button ?? "left",
8696
+ timeout: opts?.timeout ?? 1e4
8697
+ });
8698
+ return { healed: true, method: result.method, attempts: result.attempts };
8699
+ }
8700
+ }
8701
+ if (originalError instanceof Error && originalError.message.includes("not found")) {
8479
8702
  throw new ElementNotFoundError(selector);
8480
8703
  }
8481
- throw new BrowserError(`Click failed on '${selector}': ${err instanceof Error ? err.message : String(err)}`, "CLICK_FAILED");
8704
+ throw new BrowserError(`Click failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "CLICK_FAILED");
8482
8705
  }
8483
8706
  }
8484
8707
  async function type(page, selector, text, opts) {
@@ -8487,17 +8710,35 @@ async function type(page, selector, text, opts) {
8487
8710
  await page.fill(selector, "", { timeout: opts?.timeout ?? 1e4 });
8488
8711
  }
8489
8712
  await page.type(selector, text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
8490
- } catch (err) {
8491
- if (err instanceof Error && err.message.includes("not found")) {
8713
+ return {};
8714
+ } catch (originalError) {
8715
+ if (opts?.selfHeal !== false) {
8716
+ const result = await healSelector(page, selector);
8717
+ if (result.found && result.locator) {
8718
+ if (opts?.clear)
8719
+ await result.locator.fill("", { timeout: opts?.timeout ?? 1e4 });
8720
+ await result.locator.pressSequentially(text, { delay: opts?.delay, timeout: opts?.timeout ?? 1e4 });
8721
+ return { healed: true, method: result.method, attempts: result.attempts };
8722
+ }
8723
+ }
8724
+ if (originalError instanceof Error && originalError.message.includes("not found")) {
8492
8725
  throw new ElementNotFoundError(selector);
8493
8726
  }
8494
- throw new BrowserError(`Type failed on '${selector}': ${err instanceof Error ? err.message : String(err)}`, "TYPE_FAILED");
8727
+ throw new BrowserError(`Type failed on '${selector}': ${originalError instanceof Error ? originalError.message : String(originalError)}`, "TYPE_FAILED");
8495
8728
  }
8496
8729
  }
8497
- async function fill(page, selector, value, timeout = 1e4) {
8730
+ async function fill(page, selector, value, timeout = 1e4, selfHeal = true) {
8498
8731
  try {
8499
8732
  await page.fill(selector, value, { timeout });
8500
- } catch (err) {
8733
+ return {};
8734
+ } catch (originalError) {
8735
+ if (selfHeal) {
8736
+ const result = await healSelector(page, selector);
8737
+ if (result.found && result.locator) {
8738
+ await result.locator.fill(value, { timeout });
8739
+ return { healed: true, method: result.method, attempts: result.attempts };
8740
+ }
8741
+ }
8501
8742
  throw new ElementNotFoundError(selector);
8502
8743
  }
8503
8744
  }
@@ -8623,12 +8864,39 @@ async function clickText(page, text, opts) {
8623
8864
  }
8624
8865
  }, { retries: opts?.retries ?? 1 });
8625
8866
  }
8626
- async function fillForm(page, fields, submitSelector) {
8867
+ async function fillForm(page, fields, submitSelector, selfHeal = true) {
8627
8868
  let filled = 0;
8628
8869
  const errors = [];
8870
+ const healedFields = [];
8629
8871
  for (const [selector, value] of Object.entries(fields)) {
8630
8872
  try {
8631
- const el = await page.$(selector);
8873
+ let el = await page.$(selector);
8874
+ if (!el && selfHeal) {
8875
+ const result = await healSelector(page, selector);
8876
+ if (result.found && result.locator) {
8877
+ const handle = await result.locator.elementHandle();
8878
+ if (handle) {
8879
+ el = handle;
8880
+ healedFields.push(selector);
8881
+ const tagName2 = await result.locator.evaluate((e) => e.tagName.toLowerCase());
8882
+ const inputType2 = await result.locator.evaluate((e) => e.type?.toLowerCase() ?? "text");
8883
+ if (tagName2 === "select") {
8884
+ await result.locator.selectOption(String(value));
8885
+ } else if (tagName2 === "input" && (inputType2 === "checkbox" || inputType2 === "radio")) {
8886
+ if (Boolean(value))
8887
+ await result.locator.check();
8888
+ else
8889
+ await result.locator.uncheck();
8890
+ } else {
8891
+ await result.locator.fill(String(value));
8892
+ }
8893
+ filled++;
8894
+ continue;
8895
+ }
8896
+ }
8897
+ errors.push(`${selector}: element not found`);
8898
+ continue;
8899
+ }
8632
8900
  if (!el) {
8633
8901
  errors.push(`${selector}: element not found`);
8634
8902
  continue;
@@ -8655,11 +8923,21 @@ async function fillForm(page, fields, submitSelector) {
8655
8923
  if (submitSelector) {
8656
8924
  try {
8657
8925
  await page.click(submitSelector);
8658
- } catch (err) {
8659
- errors.push(`submit(${submitSelector}): ${err instanceof Error ? err.message : String(err)}`);
8926
+ } catch (submitErr) {
8927
+ if (selfHeal) {
8928
+ const result = await healSelector(page, submitSelector);
8929
+ if (result.found && result.locator) {
8930
+ await result.locator.click();
8931
+ healedFields.push(submitSelector);
8932
+ } else {
8933
+ errors.push(`submit(${submitSelector}): ${submitErr instanceof Error ? submitErr.message : String(submitErr)}`);
8934
+ }
8935
+ } else {
8936
+ errors.push(`submit(${submitSelector}): ${submitErr instanceof Error ? submitErr.message : String(submitErr)}`);
8937
+ }
8660
8938
  }
8661
8939
  }
8662
- return { filled, errors, fields_attempted: Object.keys(fields).length };
8940
+ return { filled, errors, fields_attempted: Object.keys(fields).length, ...healedFields.length > 0 ? { healed_fields: healedFields } : {} };
8663
8941
  }
8664
8942
  async function waitForText(page, text, opts) {
8665
8943
  const timeout = opts?.timeout ?? 1e4;
@@ -8939,6 +9217,7 @@ async function getPageInfo(page) {
8939
9217
  };
8940
9218
  }
8941
9219
  // src/lib/performance.ts
9220
+ init_cdp();
8942
9221
  async function getPerformanceMetrics(page) {
8943
9222
  const navTiming = await page.evaluate(() => {
8944
9223
  const t = performance.timing;
@@ -9026,10 +9305,11 @@ async function startCoverage(page) {
9026
9305
  };
9027
9306
  }
9028
9307
  // src/lib/screenshot.ts
9308
+ init_types();
9029
9309
  var import_sharp = __toESM(require_lib(), 1);
9030
- import { join as join3 } from "path";
9031
- import { mkdirSync as mkdirSync3 } from "fs";
9032
- import { homedir as homedir3 } from "os";
9310
+ import { join as join4 } from "path";
9311
+ import { mkdirSync as mkdirSync4 } from "fs";
9312
+ import { homedir as homedir4 } from "os";
9033
9313
 
9034
9314
  // src/db/gallery.ts
9035
9315
  init_schema();
@@ -9075,13 +9355,13 @@ function getEntry(id) {
9075
9355
 
9076
9356
  // src/lib/screenshot.ts
9077
9357
  function getDataDir2() {
9078
- return process.env["BROWSER_DATA_DIR"] ?? join3(homedir3(), ".browser");
9358
+ return process.env["BROWSER_DATA_DIR"] ?? join4(homedir4(), ".browser");
9079
9359
  }
9080
9360
  function getScreenshotDir(projectId) {
9081
- const base = join3(getDataDir2(), "screenshots");
9361
+ const base = join4(getDataDir2(), "screenshots");
9082
9362
  const date = new Date().toISOString().split("T")[0];
9083
- const dir = projectId ? join3(base, projectId, date) : join3(base, date);
9084
- mkdirSync3(dir, { recursive: true });
9363
+ const dir = projectId ? join4(base, projectId, date) : join4(base, date);
9364
+ mkdirSync4(dir, { recursive: true });
9085
9365
  return dir;
9086
9366
  }
9087
9367
  async function compressBuffer(raw, format, quality, maxWidth) {
@@ -9096,7 +9376,7 @@ async function compressBuffer(raw, format, quality, maxWidth) {
9096
9376
  }
9097
9377
  }
9098
9378
  async function generateThumbnail(raw, dir, stem) {
9099
- const thumbPath = join3(dir, `${stem}.thumb.webp`);
9379
+ const thumbPath = join4(dir, `${stem}.thumb.webp`);
9100
9380
  const thumbBuffer = await import_sharp.default(raw).resize({ width: 200, withoutEnlargement: true }).webp({ quality: 70, effort: 3 }).toBuffer();
9101
9381
  await Bun.write(thumbPath, thumbBuffer);
9102
9382
  return { path: thumbPath, base64: thumbBuffer.toString("base64") };
@@ -9153,7 +9433,7 @@ async function takeScreenshot(page, opts) {
9153
9433
  const compressedSizeBytes = finalBuffer.length;
9154
9434
  const compressionRatio = originalSizeBytes > 0 ? compressedSizeBytes / originalSizeBytes : 1;
9155
9435
  const ext = format;
9156
- const screenshotPath = opts?.path ?? join3(dir, `${stem}.${ext}`);
9436
+ const screenshotPath = opts?.path ?? join4(dir, `${stem}.${ext}`);
9157
9437
  await Bun.write(screenshotPath, finalBuffer);
9158
9438
  let thumbnailPath;
9159
9439
  let thumbnailBase64;
@@ -9213,12 +9493,12 @@ async function takeScreenshot(page, opts) {
9213
9493
  }
9214
9494
  async function generatePDF(page, opts) {
9215
9495
  try {
9216
- const base = join3(getDataDir2(), "pdfs");
9496
+ const base = join4(getDataDir2(), "pdfs");
9217
9497
  const date = new Date().toISOString().split("T")[0];
9218
- const dir = opts?.projectId ? join3(base, opts.projectId, date) : join3(base, date);
9219
- mkdirSync3(dir, { recursive: true });
9498
+ const dir = opts?.projectId ? join4(base, opts.projectId, date) : join4(base, date);
9499
+ mkdirSync4(dir, { recursive: true });
9220
9500
  const timestamp = Date.now();
9221
- const pdfPath = opts?.path ?? join3(dir, `${timestamp}.pdf`);
9501
+ const pdfPath = opts?.path ?? join4(dir, `${timestamp}.pdf`);
9222
9502
  const buffer = await page.pdf({
9223
9503
  path: pdfPath,
9224
9504
  format: opts?.format ?? "A4",
@@ -9308,6 +9588,7 @@ async function getIndexedDB(page, dbName, storeName) {
9308
9588
  }), [dbName, storeName]);
9309
9589
  }
9310
9590
  // src/lib/recorder.ts
9591
+ init_types();
9311
9592
  var activeRecordings = new Map;
9312
9593
  function startRecording(sessionId, name, startUrl) {
9313
9594
  const steps = [];
@@ -9466,6 +9747,7 @@ function exportRecording(recordingId, format = "json") {
9466
9747
  `);
9467
9748
  }
9468
9749
  // src/lib/crawler.ts
9750
+ init_types();
9469
9751
  async function crawl(startUrl, opts = {}) {
9470
9752
  const maxDepth = opts.maxDepth ?? 2;
9471
9753
  const maxPages = opts.maxPages ?? 50;
@@ -9678,6 +9960,7 @@ export {
9678
9960
  createCrawlResult,
9679
9961
  crawl,
9680
9962
  countActiveSessions2 as countActiveSessions,
9963
+ connectToExistingBrowser,
9681
9964
  connectLightpanda,
9682
9965
  closeSession2 as closeSession,
9683
9966
  closePage,