browser-pilot 0.0.14 → 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +89 -667
  2. package/dist/actions.cjs +1073 -41
  3. package/dist/actions.d.cts +11 -3
  4. package/dist/actions.d.ts +11 -3
  5. package/dist/actions.mjs +1 -1
  6. package/dist/browser-ZCR6AA4D.mjs +11 -0
  7. package/dist/browser.cjs +1431 -62
  8. package/dist/browser.d.cts +4 -4
  9. package/dist/browser.d.ts +4 -4
  10. package/dist/browser.mjs +4 -4
  11. package/dist/cdp.cjs +5 -1
  12. package/dist/cdp.d.cts +1 -1
  13. package/dist/cdp.d.ts +1 -1
  14. package/dist/cdp.mjs +1 -1
  15. package/dist/{chunk-7NDR6V7S.mjs → chunk-6GBYX7C2.mjs} +1405 -528
  16. package/dist/{chunk-KIFB526Y.mjs → chunk-BVZALQT4.mjs} +5 -1
  17. package/dist/chunk-DTVRFXKI.mjs +35 -0
  18. package/dist/chunk-EZNZ72VA.mjs +563 -0
  19. package/dist/{chunk-SPSZZH22.mjs → chunk-LCNFBXB5.mjs} +9 -33
  20. package/dist/{chunk-IN5HPAPB.mjs → chunk-NNEHWWHL.mjs} +28 -10
  21. package/dist/chunk-TJ5B56NV.mjs +804 -0
  22. package/dist/{chunk-XMJABKCF.mjs → chunk-V3VLBQAM.mjs} +1073 -41
  23. package/dist/cli.mjs +2799 -1176
  24. package/dist/{client-Ck2nQksT.d.cts → client-B5QBRgIy.d.cts} +2 -0
  25. package/dist/{client-Ck2nQksT.d.ts → client-B5QBRgIy.d.ts} +2 -0
  26. package/dist/{client-3AFV2IAF.mjs → client-JWWZWO6L.mjs} +4 -2
  27. package/dist/index.cjs +1441 -52
  28. package/dist/index.d.cts +5 -5
  29. package/dist/index.d.ts +5 -5
  30. package/dist/index.mjs +19 -7
  31. package/dist/page-IUUTJ3SW.mjs +7 -0
  32. package/dist/providers.cjs +637 -2
  33. package/dist/providers.d.cts +2 -2
  34. package/dist/providers.d.ts +2 -2
  35. package/dist/providers.mjs +17 -3
  36. package/dist/{types-CjT0vClo.d.ts → types-BflRmiDz.d.cts} +17 -3
  37. package/dist/{types-BSoh5v1Y.d.cts → types-BzM-IfsL.d.ts} +17 -3
  38. package/dist/types-DeVSWhXj.d.cts +142 -0
  39. package/dist/types-DeVSWhXj.d.ts +142 -0
  40. package/package.json +1 -1
  41. package/dist/browser-LZTEHUDI.mjs +0 -9
  42. package/dist/chunk-BRAFQUMG.mjs +0 -229
  43. package/dist/types--wXNHUwt.d.cts +0 -56
  44. package/dist/types--wXNHUwt.d.ts +0 -56
package/dist/browser.cjs CHANGED
@@ -214,7 +214,8 @@ function buildCDPClient(transport, options = {}) {
214
214
  pending.delete(response.id);
215
215
  clearTimeout(request.timer);
216
216
  if (response.error) {
217
- request.reject(new CDPError(response.error));
217
+ const error = typeof response.error === "string" ? { code: -32e3, message: response.error } : response.error;
218
+ request.reject(new CDPError(error));
218
219
  } else {
219
220
  request.resolve(response.result);
220
221
  }
@@ -330,6 +331,9 @@ function buildCDPClient(transport, options = {}) {
330
331
  get sessionId() {
331
332
  return currentSessionId;
332
333
  },
334
+ setSessionId(sessionId) {
335
+ currentSessionId = sessionId;
336
+ },
333
337
  get isConnected() {
334
338
  return connected;
335
339
  }
@@ -468,6 +472,34 @@ var BrowserlessProvider = class {
468
472
  };
469
473
 
470
474
  // src/providers/generic.ts
475
+ function sleep(ms) {
476
+ return new Promise((resolve) => setTimeout(resolve, ms));
477
+ }
478
+ async function fetchDevToolsJson(host, path, errorPrefix, options = {}) {
479
+ const protocol = host.includes("://") ? "" : "http://";
480
+ const attempts = options.attempts ?? 1;
481
+ let delayMs = options.initialDelayMs ?? 50;
482
+ const maxDelayMs = options.maxDelayMs ?? 250;
483
+ let lastError;
484
+ for (let attempt = 1; attempt <= attempts; attempt++) {
485
+ try {
486
+ const response = await fetch(`${protocol}${host}${path}`);
487
+ if (response.ok) {
488
+ return await response.json();
489
+ }
490
+ lastError = new Error(`${errorPrefix}: ${response.status}`);
491
+ } catch (error) {
492
+ lastError = new Error(
493
+ `${errorPrefix}: ${error instanceof Error ? error.message : String(error)}`
494
+ );
495
+ }
496
+ if (attempt < attempts) {
497
+ await sleep(delayMs);
498
+ delayMs = Math.min(delayMs * 2, maxDelayMs);
499
+ }
500
+ }
501
+ throw lastError ?? new Error(errorPrefix);
502
+ }
471
503
  var GenericProvider = class {
472
504
  name = "generic";
473
505
  wsUrl;
@@ -485,6 +517,338 @@ var GenericProvider = class {
485
517
  };
486
518
  }
487
519
  };
520
+ async function getBrowserWebSocketUrl(host = "localhost:9222") {
521
+ const info = await fetchDevToolsJson(host, "/json/version", "Failed to get browser info", {
522
+ attempts: 10,
523
+ initialDelayMs: 50,
524
+ maxDelayMs: 250
525
+ });
526
+ return info.webSocketDebuggerUrl;
527
+ }
528
+
529
+ // src/providers/local-discovery.ts
530
+ var CHANNEL_ORDER = ["stable", "beta", "dev", "canary"];
531
+ var DEFAULT_PROBE_TIMEOUT_MS = 1e3;
532
+ var DevToolsActivePortParseError = class extends Error {
533
+ constructor(message, reason) {
534
+ super(message);
535
+ this.reason = reason;
536
+ this.name = "DevToolsActivePortParseError";
537
+ }
538
+ };
539
+ function getRuntimeEnv() {
540
+ if (typeof process === "undefined") {
541
+ return {};
542
+ }
543
+ return process.env;
544
+ }
545
+ function getRuntimePlatform() {
546
+ if (typeof process === "undefined") {
547
+ return void 0;
548
+ }
549
+ return process.platform;
550
+ }
551
+ function normalizePlatform(platform) {
552
+ if (platform === "darwin" || platform === "linux" || platform === "win32") {
553
+ return platform;
554
+ }
555
+ throw new Error(`Unsupported platform: ${platform ?? "unknown"}`);
556
+ }
557
+ function trimTrailingSeparator(path) {
558
+ return path.replace(/[\\/]+$/, "");
559
+ }
560
+ function joinPath(platform, ...parts) {
561
+ const separator = platform === "win32" ? "\\" : "/";
562
+ const cleaned = parts.map((part, index) => {
563
+ if (index === 0) return trimTrailingSeparator(part);
564
+ return part.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
565
+ }).filter((part) => part.length > 0);
566
+ return cleaned.join(separator);
567
+ }
568
+ function resolveHomeDir(platform, env, explicitHomeDir) {
569
+ if (explicitHomeDir) {
570
+ return explicitHomeDir;
571
+ }
572
+ if (platform === "win32") {
573
+ return env["USERPROFILE"] ?? env["HOME"] ?? "";
574
+ }
575
+ return env["HOME"] ?? env["USERPROFILE"] ?? "";
576
+ }
577
+ function toFileFailure(target, error) {
578
+ const errno = error?.code;
579
+ if (errno === "ENOENT") {
580
+ return {
581
+ ...target,
582
+ reason: "missing-file",
583
+ message: `DevToolsActivePort not found at ${target.portFile}`
584
+ };
585
+ }
586
+ return {
587
+ ...target,
588
+ reason: "unreadable-file",
589
+ message: error instanceof Error ? error.message : `Could not read DevToolsActivePort at ${target.portFile}`
590
+ };
591
+ }
592
+ function toProbeFailure(target, wsUrl, error) {
593
+ const message = error instanceof Error ? error.message : String(error);
594
+ const lowerMessage = message.toLowerCase();
595
+ let reason = "connection-error";
596
+ if (lowerMessage.includes("refused") || lowerMessage.includes("econnrefused")) {
597
+ reason = "connection-refused";
598
+ } else if (lowerMessage.includes("timeout") || lowerMessage.includes("timed out")) {
599
+ reason = "connection-timeout";
600
+ } else if (lowerMessage.includes("closed")) {
601
+ reason = "unexpected-close";
602
+ } else if (lowerMessage.includes("browser.getversion") || lowerMessage.includes("cdp") || lowerMessage.includes("protocol")) {
603
+ reason = "cdp-error";
604
+ }
605
+ return {
606
+ ...target,
607
+ wsUrl,
608
+ reason,
609
+ message
610
+ };
611
+ }
612
+ async function readTextFile(path) {
613
+ const fs2 = await import("fs/promises");
614
+ return fs2.readFile(path, "utf-8");
615
+ }
616
+ async function probeBrowserWebSocket(wsUrl, timeoutMs) {
617
+ let client;
618
+ try {
619
+ client = await createCDPClient(wsUrl, { timeout: timeoutMs });
620
+ const version = await client.send("Browser.getVersion", void 0, null);
621
+ return { browserVersion: version.product };
622
+ } finally {
623
+ await client?.close().catch(() => {
624
+ });
625
+ }
626
+ }
627
+ var defaultDependencies = {
628
+ readTextFile,
629
+ probeBrowserWebSocket,
630
+ getLegacyBrowserWebSocketUrl: getBrowserWebSocketUrl
631
+ };
632
+ function resolveChromeUserDataDirs(options = {}) {
633
+ const env = options.env ?? getRuntimeEnv();
634
+ const platform = normalizePlatform(options.platform ?? getRuntimePlatform());
635
+ const homeDir = resolveHomeDir(platform, env, options.homeDir);
636
+ if (!homeDir) {
637
+ throw new Error("Could not determine home directory for local Chrome discovery");
638
+ }
639
+ switch (platform) {
640
+ case "darwin": {
641
+ const base = joinPath(platform, homeDir, "Library", "Application Support", "Google");
642
+ return {
643
+ stable: joinPath(platform, base, "Chrome"),
644
+ beta: joinPath(platform, base, "Chrome Beta"),
645
+ dev: joinPath(platform, base, "Chrome Dev"),
646
+ canary: joinPath(platform, base, "Chrome Canary")
647
+ };
648
+ }
649
+ case "linux": {
650
+ const configHome = env["CHROME_CONFIG_HOME"] ?? env["XDG_CONFIG_HOME"] ?? joinPath(platform, homeDir, ".config");
651
+ return {
652
+ stable: joinPath(platform, configHome, "google-chrome"),
653
+ beta: joinPath(platform, configHome, "google-chrome-beta"),
654
+ dev: joinPath(platform, configHome, "google-chrome-dev"),
655
+ canary: joinPath(platform, configHome, "google-chrome-canary")
656
+ };
657
+ }
658
+ case "win32": {
659
+ const localAppData = env["LOCALAPPDATA"] ?? joinPath(platform, homeDir, "AppData", "Local");
660
+ const base = joinPath(platform, localAppData, "Google");
661
+ return {
662
+ stable: joinPath(platform, base, "Chrome", "User Data"),
663
+ beta: joinPath(platform, base, "Chrome Beta", "User Data"),
664
+ dev: joinPath(platform, base, "Chrome Dev", "User Data"),
665
+ canary: joinPath(platform, base, "Chrome SxS", "User Data")
666
+ };
667
+ }
668
+ }
669
+ throw new Error(`Unsupported platform for local Chrome discovery: ${platform}`);
670
+ }
671
+ function buildLocalBrowserScanTargets(options = {}) {
672
+ const env = options.env ?? getRuntimeEnv();
673
+ const platform = normalizePlatform(options.platform ?? getRuntimePlatform());
674
+ if (options.userDataDir) {
675
+ return [
676
+ {
677
+ channel: options.channel ?? "custom",
678
+ userDataDir: options.userDataDir,
679
+ portFile: joinPath(platform, options.userDataDir, "DevToolsActivePort")
680
+ }
681
+ ];
682
+ }
683
+ const dirs = resolveChromeUserDataDirs({
684
+ platform,
685
+ env,
686
+ homeDir: options.homeDir
687
+ });
688
+ const channels = options.channel ? [options.channel] : CHANNEL_ORDER;
689
+ return channels.map((channel) => ({
690
+ channel,
691
+ userDataDir: dirs[channel],
692
+ portFile: joinPath(platform, dirs[channel], "DevToolsActivePort")
693
+ }));
694
+ }
695
+ function parseDevToolsActivePortFile(content) {
696
+ const lines = content.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0);
697
+ if (lines.length !== 2) {
698
+ throw new DevToolsActivePortParseError(
699
+ `Expected exactly 2 non-empty lines in DevToolsActivePort, got ${lines.length}`,
700
+ "malformed-file"
701
+ );
702
+ }
703
+ const portText = lines[0];
704
+ const browserPath = lines[1];
705
+ const port = Number.parseInt(portText, 10);
706
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
707
+ throw new DevToolsActivePortParseError(
708
+ `Invalid DevToolsActivePort port: ${portText}`,
709
+ "invalid-port"
710
+ );
711
+ }
712
+ if (!browserPath.startsWith("/devtools/browser/") || browserPath.includes("..") || /[?#\s\\]/u.test(browserPath)) {
713
+ throw new DevToolsActivePortParseError(
714
+ `Invalid DevToolsActivePort browser path: ${browserPath}`,
715
+ "invalid-path"
716
+ );
717
+ }
718
+ return {
719
+ port,
720
+ browserPath,
721
+ wsUrl: `ws://127.0.0.1:${port}${browserPath}`
722
+ };
723
+ }
724
+ async function inspectScanTarget(target, options, deps) {
725
+ let content;
726
+ try {
727
+ content = await deps.readTextFile(target.portFile);
728
+ } catch (error) {
729
+ return { kind: "failure", failure: toFileFailure(target, error) };
730
+ }
731
+ let parsed;
732
+ try {
733
+ parsed = parseDevToolsActivePortFile(content);
734
+ } catch (error) {
735
+ if (error instanceof DevToolsActivePortParseError) {
736
+ return {
737
+ kind: "failure",
738
+ failure: {
739
+ ...target,
740
+ reason: error.reason,
741
+ message: error.message
742
+ }
743
+ };
744
+ }
745
+ throw error;
746
+ }
747
+ try {
748
+ const probe = await deps.probeBrowserWebSocket(
749
+ parsed.wsUrl,
750
+ options.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS
751
+ );
752
+ return {
753
+ kind: "candidate",
754
+ candidate: {
755
+ ...target,
756
+ port: parsed.port,
757
+ browserPath: parsed.browserPath,
758
+ wsUrl: parsed.wsUrl,
759
+ browserVersion: probe.browserVersion
760
+ }
761
+ };
762
+ } catch (error) {
763
+ return {
764
+ kind: "failure",
765
+ failure: toProbeFailure(target, parsed.wsUrl, error)
766
+ };
767
+ }
768
+ }
769
+ async function discoverLocalBrowsers(options = {}, deps = defaultDependencies) {
770
+ const scanTargets = buildLocalBrowserScanTargets(options);
771
+ const outcomes = await Promise.all(
772
+ scanTargets.map((target) => inspectScanTarget(target, options, deps))
773
+ );
774
+ const candidates = [];
775
+ const failures = [];
776
+ for (const outcome of outcomes) {
777
+ if (outcome.kind === "candidate") {
778
+ candidates.push(outcome.candidate);
779
+ } else {
780
+ failures.push(outcome.failure);
781
+ }
782
+ }
783
+ return { candidates, failures };
784
+ }
785
+ var BrowserEndpointResolutionError = class extends Error {
786
+ constructor(code, message, details = {}) {
787
+ super(message);
788
+ this.code = code;
789
+ this.details = details;
790
+ }
791
+ name = "BrowserEndpointResolutionError";
792
+ };
793
+ async function resolveBrowserEndpoint(options = {}, deps = defaultDependencies) {
794
+ if (options.explicitWsUrl) {
795
+ return {
796
+ wsUrl: options.explicitWsUrl,
797
+ source: "explicit-ws"
798
+ };
799
+ }
800
+ let localDiscovery;
801
+ if (options.allowLocalDiscovery ?? true) {
802
+ localDiscovery = await discoverLocalBrowsers(options, deps);
803
+ if (localDiscovery.candidates.length === 1) {
804
+ const candidate = localDiscovery.candidates[0];
805
+ return {
806
+ wsUrl: candidate.wsUrl,
807
+ source: "devtools-active-port",
808
+ channel: candidate.channel,
809
+ userDataDir: candidate.userDataDir
810
+ };
811
+ }
812
+ if (localDiscovery.candidates.length > 1) {
813
+ throw new BrowserEndpointResolutionError(
814
+ "multiple-local-browsers",
815
+ "Multiple local Chrome profiles are available for auto-discovery",
816
+ {
817
+ candidates: localDiscovery.candidates,
818
+ failures: localDiscovery.failures
819
+ }
820
+ );
821
+ }
822
+ }
823
+ if (options.allowLegacyHostFallback ?? true) {
824
+ const legacyHost = options.legacyHost ?? "localhost:9222";
825
+ try {
826
+ return {
827
+ wsUrl: await deps.getLegacyBrowserWebSocketUrl(legacyHost),
828
+ source: "json-version"
829
+ };
830
+ } catch (error) {
831
+ throw new BrowserEndpointResolutionError(
832
+ "browser-not-found",
833
+ "Could not resolve a browser endpoint",
834
+ {
835
+ candidates: localDiscovery?.candidates,
836
+ failures: localDiscovery?.failures,
837
+ legacyError: error instanceof Error ? error : new Error(String(error)),
838
+ legacyHost
839
+ }
840
+ );
841
+ }
842
+ }
843
+ throw new BrowserEndpointResolutionError(
844
+ "browser-not-found",
845
+ "Could not resolve a browser endpoint",
846
+ {
847
+ candidates: localDiscovery?.candidates,
848
+ failures: localDiscovery?.failures
849
+ }
850
+ );
851
+ }
488
852
 
489
853
  // src/providers/index.ts
490
854
  function createProvider(options) {
@@ -990,7 +1354,7 @@ var CHECK_EDITABLE = `function() {
990
1354
 
991
1355
  return { actionable: true };
992
1356
  }`;
993
- function sleep(ms) {
1357
+ function sleep2(ms) {
994
1358
  return new Promise((resolve) => setTimeout(resolve, ms));
995
1359
  }
996
1360
  var BACKOFF = [0, 20, 100, 100];
@@ -1074,7 +1438,7 @@ async function ensureActionable(cdp, objectId, checks, options) {
1074
1438
  );
1075
1439
  }
1076
1440
  const delay = attempt < BACKOFF.length ? BACKOFF[attempt] ?? 0 : 500;
1077
- if (delay > 0) await sleep(delay);
1441
+ if (delay > 0) await sleep2(delay);
1078
1442
  attempt++;
1079
1443
  }
1080
1444
  }
@@ -1355,6 +1719,624 @@ var NavigationError = class extends Error {
1355
1719
  }
1356
1720
  };
1357
1721
 
1722
+ // src/trace/views.ts
1723
+ function takeRecent(events, limit = 5) {
1724
+ return events.slice(-limit).map((event) => ({
1725
+ ts: event.ts,
1726
+ event: event.event,
1727
+ summary: event.summary,
1728
+ severity: event.severity,
1729
+ url: event.url
1730
+ }));
1731
+ }
1732
+ function buildTraceSummaries(events) {
1733
+ return {
1734
+ ws: summarizeWs(events),
1735
+ voice: summarizeVoice(events),
1736
+ console: summarizeConsole(events),
1737
+ permissions: summarizePermissions(events),
1738
+ media: summarizeMedia(events),
1739
+ ui: summarizeUi(events),
1740
+ session: summarizeSession(events)
1741
+ };
1742
+ }
1743
+ function summarizeWs(events) {
1744
+ const relevant = events.filter(
1745
+ (event) => event.channel === "ws" || event.event.startsWith("ws.")
1746
+ );
1747
+ const connections = /* @__PURE__ */ new Map();
1748
+ for (const event of relevant) {
1749
+ const id = event.connectionId ?? event.requestId ?? event.traceId;
1750
+ let connection = connections.get(id);
1751
+ if (!connection) {
1752
+ connection = { id, sent: 0, received: 0, lastMessages: [] };
1753
+ connections.set(id, connection);
1754
+ }
1755
+ connection.url = event.url ?? connection.url;
1756
+ if (event.event === "ws.connection.created") {
1757
+ connection.createdAt = event.ts;
1758
+ }
1759
+ if (event.event === "ws.connection.closed") {
1760
+ connection.closedAt = event.ts;
1761
+ }
1762
+ if (event.event === "ws.frame.sent") {
1763
+ connection.sent += 1;
1764
+ const payload = typeof event.data["payload"] === "string" ? event.data["payload"] : "";
1765
+ if (payload) connection.lastMessages.push(`sent: ${payload}`);
1766
+ }
1767
+ if (event.event === "ws.frame.received") {
1768
+ connection.received += 1;
1769
+ const payload = typeof event.data["payload"] === "string" ? event.data["payload"] : "";
1770
+ if (payload) connection.lastMessages.push(`recv: ${payload}`);
1771
+ }
1772
+ connection.lastMessages = connection.lastMessages.slice(-3);
1773
+ }
1774
+ const values = [...connections.values()];
1775
+ const reconnects = values.reduce((count, connection) => {
1776
+ return connection.closedAt && !connection.createdAt ? count + 1 : count;
1777
+ }, 0);
1778
+ return {
1779
+ view: "ws",
1780
+ totalEvents: relevant.length,
1781
+ connections: values.map((connection) => ({
1782
+ id: connection.id,
1783
+ url: connection.url ?? null,
1784
+ createdAt: connection.createdAt ?? null,
1785
+ closedAt: connection.closedAt ?? null,
1786
+ sent: connection.sent,
1787
+ received: connection.received,
1788
+ lastMessages: connection.lastMessages,
1789
+ connectedButSilent: !!connection.createdAt && !connection.closedAt && connection.sent + connection.received === 0
1790
+ })),
1791
+ reconnects,
1792
+ recent: takeRecent(relevant)
1793
+ };
1794
+ }
1795
+ function summarizeConsole(events) {
1796
+ const relevant = events.filter(
1797
+ (event) => event.channel === "console" || event.event.startsWith("console.") || event.event === "runtime.exception" || event.event === "runtime.unhandledRejection"
1798
+ );
1799
+ return {
1800
+ view: "console",
1801
+ errors: relevant.filter(
1802
+ (event) => event.event === "console.error" || event.event === "runtime.exception" || event.event === "runtime.unhandledRejection"
1803
+ ).length,
1804
+ warnings: relevant.filter((event) => event.event === "console.warn").length,
1805
+ logs: relevant.filter((event) => event.event === "console.log").length,
1806
+ recent: takeRecent(relevant)
1807
+ };
1808
+ }
1809
+ function summarizePermissions(events) {
1810
+ const relevant = events.filter(
1811
+ (event) => event.channel === "permission" || event.event.startsWith("permission.")
1812
+ );
1813
+ const latest = /* @__PURE__ */ new Map();
1814
+ for (const event of relevant) {
1815
+ const name = typeof event.data["name"] === "string" ? event.data["name"] : null;
1816
+ const state = typeof event.data["state"] === "string" ? event.data["state"] : null;
1817
+ if (name && state) {
1818
+ latest.set(name, state);
1819
+ }
1820
+ }
1821
+ return {
1822
+ view: "permissions",
1823
+ states: Object.fromEntries(latest),
1824
+ changes: relevant.filter((event) => event.event === "permission.changed").length,
1825
+ recent: takeRecent(relevant)
1826
+ };
1827
+ }
1828
+ function summarizeMedia(events) {
1829
+ const relevant = events.filter(
1830
+ (event) => event.channel === "media" || event.event.startsWith("media.")
1831
+ );
1832
+ const liveTracks = /* @__PURE__ */ new Map();
1833
+ for (const event of relevant) {
1834
+ const label = typeof event.data["label"] === "string" ? event.data["label"] : "";
1835
+ const kind = typeof event.data["kind"] === "string" ? event.data["kind"] : "";
1836
+ const key = `${kind}:${label}`;
1837
+ if (event.event === "media.track.started") {
1838
+ liveTracks.set(key, kind);
1839
+ }
1840
+ if (event.event === "media.track.ended") {
1841
+ liveTracks.delete(key);
1842
+ }
1843
+ }
1844
+ return {
1845
+ view: "media",
1846
+ tracksStarted: relevant.filter((event) => event.event === "media.track.started").length,
1847
+ tracksEnded: relevant.filter((event) => event.event === "media.track.ended").length,
1848
+ playbackStarted: relevant.filter((event) => event.event === "media.playback.started").length,
1849
+ playbackStopped: relevant.filter((event) => event.event === "media.playback.stopped").length,
1850
+ liveTracks: [...liveTracks.values()],
1851
+ recent: takeRecent(relevant)
1852
+ };
1853
+ }
1854
+ function summarizeVoice(events) {
1855
+ const relevant = events.filter(
1856
+ (event) => event.channel === "voice" || event.event.startsWith("voice.")
1857
+ );
1858
+ return {
1859
+ view: "voice",
1860
+ ready: relevant.filter((event) => event.event === "voice.pipeline.ready").length,
1861
+ notReady: relevant.filter((event) => event.event === "voice.pipeline.notReady").length,
1862
+ captureStarted: relevant.filter((event) => event.event === "voice.capture.started").length,
1863
+ captureStopped: relevant.filter((event) => event.event === "voice.capture.stopped").length,
1864
+ detectedAudio: relevant.filter((event) => event.event === "voice.capture.detectedAudio").length,
1865
+ recent: takeRecent(relevant)
1866
+ };
1867
+ }
1868
+ function summarizeUi(events) {
1869
+ const relevant = events.filter(
1870
+ (event) => event.channel === "dom" || event.event.startsWith("dom.") || event.channel === "action"
1871
+ );
1872
+ return {
1873
+ view: "ui",
1874
+ actions: relevant.filter((event) => event.channel === "action").length,
1875
+ domChanges: relevant.filter((event) => event.channel === "dom").length,
1876
+ recent: takeRecent(relevant)
1877
+ };
1878
+ }
1879
+ function summarizeSession(events) {
1880
+ const byChannel = /* @__PURE__ */ new Map();
1881
+ const failedActions = events.filter((event) => event.event === "action.failed").length;
1882
+ for (const event of events) {
1883
+ byChannel.set(event.channel, (byChannel.get(event.channel) ?? 0) + 1);
1884
+ }
1885
+ return {
1886
+ view: "session",
1887
+ totalEvents: events.length,
1888
+ byChannel: Object.fromEntries(byChannel),
1889
+ failedActions,
1890
+ recent: takeRecent(events)
1891
+ };
1892
+ }
1893
+
1894
+ // src/recording/manifest.ts
1895
+ function isCanonicalRecordingManifest(value) {
1896
+ return Boolean(
1897
+ value && typeof value === "object" && value.version === 2 && typeof value.session === "object"
1898
+ );
1899
+ }
1900
+ function isLegacyRecordingManifest(value) {
1901
+ return Boolean(
1902
+ value && typeof value === "object" && value.version === 1 && Array.isArray(value.frames)
1903
+ );
1904
+ }
1905
+ function createRecordingManifest(input) {
1906
+ const actions = input.frames.map((frame) => {
1907
+ const actionId = frame.actionId ?? `action-${frame.seq}`;
1908
+ return {
1909
+ id: actionId,
1910
+ stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
1911
+ action: frame.action,
1912
+ selector: frame.selector,
1913
+ selectorUsed: frame.selectorUsed ?? frame.selector,
1914
+ value: frame.value,
1915
+ url: frame.url,
1916
+ success: frame.success,
1917
+ durationMs: frame.durationMs,
1918
+ error: frame.error,
1919
+ ts: new Date(frame.timestamp).toISOString(),
1920
+ pageUrl: frame.pageUrl,
1921
+ pageTitle: frame.pageTitle,
1922
+ coordinates: frame.coordinates,
1923
+ boundingBox: frame.boundingBox
1924
+ };
1925
+ });
1926
+ const screenshots = input.frames.map((frame) => ({
1927
+ id: `shot-${frame.seq}`,
1928
+ stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
1929
+ actionId: frame.actionId ?? `action-${frame.seq}`,
1930
+ file: frame.screenshot,
1931
+ ts: new Date(frame.timestamp).toISOString(),
1932
+ success: frame.success,
1933
+ pageUrl: frame.pageUrl,
1934
+ pageTitle: frame.pageTitle,
1935
+ coordinates: frame.coordinates,
1936
+ boundingBox: frame.boundingBox
1937
+ }));
1938
+ return {
1939
+ version: 2,
1940
+ recordedAt: input.recordedAt,
1941
+ session: {
1942
+ id: input.sessionId,
1943
+ startUrl: input.startUrl,
1944
+ endUrl: input.endUrl,
1945
+ targetId: input.targetId,
1946
+ profile: input.profile
1947
+ },
1948
+ recipe: {
1949
+ steps: input.steps
1950
+ },
1951
+ actions,
1952
+ screenshots,
1953
+ trace: {
1954
+ events: input.traceEvents,
1955
+ summaries: buildTraceSummaries(input.traceEvents)
1956
+ },
1957
+ assertions: input.assertions ?? [],
1958
+ notes: input.notes ?? [],
1959
+ artifacts: {
1960
+ recordingManifest: input.recordingManifest ?? "recording.json",
1961
+ screenshotDir: input.screenshotDir ?? "screenshots/"
1962
+ }
1963
+ };
1964
+ }
1965
+ function canonicalizeRecordingArtifact(value) {
1966
+ if (isCanonicalRecordingManifest(value)) {
1967
+ return value;
1968
+ }
1969
+ if (!isLegacyRecordingManifest(value)) {
1970
+ throw new Error("Unsupported recording artifact");
1971
+ }
1972
+ const traceEvents = buildTraceEventsFromLegacy(value);
1973
+ const steps = value.frames.map((frame) => frameToStep(frame));
1974
+ return createRecordingManifest({
1975
+ recordedAt: value.recordedAt,
1976
+ sessionId: value.sessionId,
1977
+ startUrl: value.startUrl,
1978
+ endUrl: value.endUrl,
1979
+ steps,
1980
+ frames: value.frames,
1981
+ traceEvents,
1982
+ notes: ["Converted from legacy recording manifest"]
1983
+ });
1984
+ }
1985
+ function buildTraceEventsFromLegacy(value) {
1986
+ const events = [];
1987
+ for (const frame of value.frames) {
1988
+ events.push({
1989
+ traceId: frame.actionId ?? `legacy-${frame.seq}`,
1990
+ sessionId: value.sessionId,
1991
+ ts: new Date(frame.timestamp).toISOString(),
1992
+ elapsedMs: frame.timestamp - new Date(value.recordedAt).getTime(),
1993
+ channel: "action",
1994
+ event: frame.success ? "action.succeeded" : "action.failed",
1995
+ severity: frame.success ? "info" : "error",
1996
+ summary: `${frame.action}${frame.selector ? ` ${frame.selector}` : ""}`,
1997
+ data: {
1998
+ action: frame.action,
1999
+ selector: frame.selector,
2000
+ value: frame.value ?? null,
2001
+ pageUrl: frame.pageUrl ?? null,
2002
+ pageTitle: frame.pageTitle ?? null,
2003
+ screenshot: frame.screenshot
2004
+ },
2005
+ actionId: frame.actionId ?? `action-${frame.seq}`,
2006
+ stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
2007
+ selector: frame.selector,
2008
+ selectorUsed: frame.selectorUsed ?? frame.selector,
2009
+ url: frame.pageUrl ?? frame.url
2010
+ });
2011
+ }
2012
+ return events;
2013
+ }
2014
+ function frameToStep(frame) {
2015
+ switch (frame.action) {
2016
+ case "fill":
2017
+ return { action: "fill", selector: frame.selector, value: frame.value };
2018
+ case "submit":
2019
+ return { action: "submit", selector: frame.selector };
2020
+ case "goto":
2021
+ return { action: "goto", url: frame.url ?? frame.pageUrl };
2022
+ case "press":
2023
+ return { action: "press", key: frame.value ?? "Enter" };
2024
+ default:
2025
+ return { action: "click", selector: frame.selector };
2026
+ }
2027
+ }
2028
+
2029
+ // src/trace/model.ts
2030
+ function createTraceId(prefix = "evt") {
2031
+ const random = Math.random().toString(36).slice(2, 10);
2032
+ return `${prefix}-${Date.now().toString(36)}-${random}`;
2033
+ }
2034
+ function normalizeTraceEvent(event) {
2035
+ return {
2036
+ traceId: event.traceId ?? createTraceId(event.channel),
2037
+ ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
2038
+ elapsedMs: event.elapsedMs ?? 0,
2039
+ severity: event.severity ?? inferSeverity(event.event),
2040
+ data: event.data ?? {},
2041
+ ...event
2042
+ };
2043
+ }
2044
+ function inferSeverity(eventName) {
2045
+ if (eventName.includes(".failed") || eventName.includes(".error") || eventName.includes("exception") || eventName.includes("notReady")) {
2046
+ return "error";
2047
+ }
2048
+ if (eventName.includes(".closed") || eventName.includes(".warn") || eventName.includes(".changed")) {
2049
+ return "warn";
2050
+ }
2051
+ return "info";
2052
+ }
2053
+
2054
+ // src/trace/script.ts
2055
+ var TRACE_BINDING_NAME = "__bpTraceBinding";
2056
+ var TRACE_SCRIPT = `
2057
+ (() => {
2058
+ if (window.__bpTraceInstalled) return;
2059
+ window.__bpTraceInstalled = true;
2060
+
2061
+ const binding = globalThis.${TRACE_BINDING_NAME};
2062
+ if (typeof binding !== 'function') return;
2063
+
2064
+ const emit = (event, data = {}, severity = 'info', summary) => {
2065
+ try {
2066
+ globalThis.__bpTraceRecentEvents = globalThis.__bpTraceRecentEvents || [];
2067
+ const payload = {
2068
+ event,
2069
+ severity,
2070
+ summary: summary || event,
2071
+ ts: Date.now(),
2072
+ data,
2073
+ };
2074
+ globalThis.__bpTraceRecentEvents.push(payload);
2075
+ if (globalThis.__bpTraceRecentEvents.length > 200) {
2076
+ globalThis.__bpTraceRecentEvents.splice(0, globalThis.__bpTraceRecentEvents.length - 200);
2077
+ }
2078
+ binding(JSON.stringify(payload));
2079
+ } catch {}
2080
+ };
2081
+
2082
+ const patchWebSocket = () => {
2083
+ const NativeWebSocket = window.WebSocket;
2084
+ if (typeof NativeWebSocket !== 'function' || window.__bpTraceWebSocketInstalled) return;
2085
+ window.__bpTraceWebSocketInstalled = true;
2086
+
2087
+ const nextId = () => Math.random().toString(36).slice(2, 10);
2088
+
2089
+ const patchInstance = (socket, urlValue) => {
2090
+ if (!socket || socket.__bpTracePatched) return socket;
2091
+ socket.__bpTracePatched = true;
2092
+ socket.__bpTraceId = socket.__bpTraceId || nextId();
2093
+ socket.__bpTraceUrl = String(urlValue || socket.url || '');
2094
+ globalThis.__bpTrackedWebSockets = globalThis.__bpTrackedWebSockets || new Set();
2095
+ globalThis.__bpTrackedWebSockets.add(socket);
2096
+
2097
+ emit(
2098
+ 'ws.connection.created',
2099
+ { connectionId: socket.__bpTraceId, url: socket.__bpTraceUrl },
2100
+ 'info',
2101
+ 'WebSocket opened ' + socket.__bpTraceUrl
2102
+ );
2103
+
2104
+ const originalSend = socket.send;
2105
+ socket.send = function(data) {
2106
+ const payload =
2107
+ typeof data === 'string'
2108
+ ? data
2109
+ : data && typeof data.toString === 'function'
2110
+ ? data.toString()
2111
+ : '[binary]';
2112
+ emit(
2113
+ 'ws.frame.sent',
2114
+ {
2115
+ connectionId: socket.__bpTraceId,
2116
+ url: socket.__bpTraceUrl,
2117
+ payload,
2118
+ length: payload.length,
2119
+ },
2120
+ 'info',
2121
+ 'WebSocket frame sent'
2122
+ );
2123
+ return originalSend.call(this, data);
2124
+ };
2125
+
2126
+ socket.addEventListener('message', (event) => {
2127
+ if (socket.__bpOfflineNotified || socket.__bpTraceClosed) {
2128
+ return;
2129
+ }
2130
+ const data = event && 'data' in event ? event.data : '';
2131
+ const payload =
2132
+ typeof data === 'string'
2133
+ ? data
2134
+ : data && typeof data.toString === 'function'
2135
+ ? data.toString()
2136
+ : '[binary]';
2137
+ emit(
2138
+ 'ws.frame.received',
2139
+ {
2140
+ connectionId: socket.__bpTraceId,
2141
+ url: socket.__bpTraceUrl,
2142
+ payload,
2143
+ length: payload.length,
2144
+ },
2145
+ 'info',
2146
+ 'WebSocket frame received'
2147
+ );
2148
+ });
2149
+
2150
+ socket.addEventListener('close', (event) => {
2151
+ if (socket.__bpTraceClosed) {
2152
+ return;
2153
+ }
2154
+ socket.__bpTraceClosed = true;
2155
+ try {
2156
+ globalThis.__bpTrackedWebSockets.delete(socket);
2157
+ } catch {}
2158
+ emit(
2159
+ 'ws.connection.closed',
2160
+ {
2161
+ connectionId: socket.__bpTraceId,
2162
+ url: socket.__bpTraceUrl,
2163
+ code: event.code,
2164
+ reason: event.reason,
2165
+ },
2166
+ 'warn',
2167
+ 'WebSocket closed'
2168
+ );
2169
+ });
2170
+
2171
+ return socket;
2172
+ };
2173
+
2174
+ const TracedWebSocket = function(url, protocols) {
2175
+ return arguments.length > 1
2176
+ ? patchInstance(new NativeWebSocket(url, protocols), url)
2177
+ : patchInstance(new NativeWebSocket(url), url);
2178
+ };
2179
+ TracedWebSocket.prototype = NativeWebSocket.prototype;
2180
+ Object.setPrototypeOf(TracedWebSocket, NativeWebSocket);
2181
+ window.WebSocket = TracedWebSocket;
2182
+ };
2183
+
2184
+ window.addEventListener('error', (errorEvent) => {
2185
+ emit(
2186
+ 'runtime.exception',
2187
+ {
2188
+ message: errorEvent.message,
2189
+ filename: errorEvent.filename,
2190
+ line: errorEvent.lineno,
2191
+ column: errorEvent.colno,
2192
+ },
2193
+ 'error',
2194
+ errorEvent.message || 'Uncaught error'
2195
+ );
2196
+ });
2197
+
2198
+ window.addEventListener('unhandledrejection', (event) => {
2199
+ const reason = event && 'reason' in event ? String(event.reason) : 'Unhandled rejection';
2200
+ emit('runtime.unhandledRejection', { reason }, 'error', reason);
2201
+ });
2202
+
2203
+ const patchPermissions = async () => {
2204
+ if (!navigator.permissions || !navigator.permissions.query) return;
2205
+
2206
+ const names = ['geolocation', 'microphone', 'camera', 'notifications'];
2207
+ for (const name of names) {
2208
+ try {
2209
+ const status = await navigator.permissions.query({ name });
2210
+ emit(
2211
+ 'permission.state',
2212
+ { name, state: status.state },
2213
+ status.state === 'denied' ? 'warn' : 'info',
2214
+ name + ': ' + status.state
2215
+ );
2216
+ status.addEventListener('change', () => {
2217
+ emit(
2218
+ 'permission.changed',
2219
+ { name, state: status.state },
2220
+ status.state === 'denied' ? 'warn' : 'info',
2221
+ name + ': ' + status.state
2222
+ );
2223
+ });
2224
+ } catch {}
2225
+ }
2226
+ };
2227
+
2228
+ const patchMediaElement = (element) => {
2229
+ if (!element || element.__bpTracePatched) return;
2230
+ element.__bpTracePatched = true;
2231
+
2232
+ element.addEventListener('play', () => {
2233
+ emit(
2234
+ 'media.playback.started',
2235
+ { tag: element.tagName.toLowerCase(), src: element.currentSrc || element.src || null },
2236
+ 'info',
2237
+ 'Media playback started'
2238
+ );
2239
+ });
2240
+
2241
+ const onStop = () => {
2242
+ emit(
2243
+ 'media.playback.stopped',
2244
+ { tag: element.tagName.toLowerCase(), src: element.currentSrc || element.src || null },
2245
+ 'warn',
2246
+ 'Media playback stopped'
2247
+ );
2248
+ };
2249
+
2250
+ element.addEventListener('pause', onStop);
2251
+ element.addEventListener('ended', onStop);
2252
+ };
2253
+
2254
+ const patchMediaElements = () => {
2255
+ document.querySelectorAll('audio,video').forEach(patchMediaElement);
2256
+ };
2257
+
2258
+ patchMediaElements();
2259
+ patchWebSocket();
2260
+
2261
+ if (document.documentElement) {
2262
+ const observer = new MutationObserver(() => {
2263
+ patchMediaElements();
2264
+ });
2265
+ observer.observe(document.documentElement, { childList: true, subtree: true });
2266
+ }
2267
+
2268
+ if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
2269
+ const original = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
2270
+ navigator.mediaDevices.getUserMedia = async (...args) => {
2271
+ emit('voice.capture.started', { constraints: args[0] || null }, 'info', 'Voice capture started');
2272
+ try {
2273
+ const stream = await original(...args);
2274
+ const tracks = stream.getTracks();
2275
+
2276
+ for (const track of tracks) {
2277
+ emit(
2278
+ 'media.track.started',
2279
+ { kind: track.kind, label: track.label, readyState: track.readyState },
2280
+ 'info',
2281
+ track.kind + ' track started'
2282
+ );
2283
+ track.addEventListener('ended', () => {
2284
+ emit(
2285
+ 'media.track.ended',
2286
+ { kind: track.kind, label: track.label, readyState: track.readyState },
2287
+ 'warn',
2288
+ track.kind + ' track ended'
2289
+ );
2290
+ emit(
2291
+ 'voice.capture.stopped',
2292
+ { kind: track.kind, label: track.label, readyState: track.readyState },
2293
+ 'warn',
2294
+ 'Voice capture stopped'
2295
+ );
2296
+ });
2297
+ }
2298
+
2299
+ emit(
2300
+ 'voice.capture.detectedAudio',
2301
+ { trackCount: tracks.length, kinds: tracks.map((track) => track.kind) },
2302
+ 'info',
2303
+ 'Voice capture detected audio'
2304
+ );
2305
+
2306
+ return stream;
2307
+ } catch (error) {
2308
+ emit(
2309
+ 'voice.pipeline.notReady',
2310
+ { message: String(error && error.message ? error.message : error) },
2311
+ 'error',
2312
+ String(error && error.message ? error.message : error)
2313
+ );
2314
+ throw error;
2315
+ }
2316
+ };
2317
+ }
2318
+
2319
+ document.addEventListener('visibilitychange', () => {
2320
+ emit(
2321
+ 'dom.state.changed',
2322
+ { visibilityState: document.visibilityState },
2323
+ document.visibilityState === 'hidden' ? 'warn' : 'info',
2324
+ 'Visibility ' + document.visibilityState
2325
+ );
2326
+ });
2327
+
2328
+ patchPermissions();
2329
+ emit('voice.pipeline.ready', { url: location.href }, 'info', 'Trace hooks ready');
2330
+ })();
2331
+ `;
2332
+
2333
+ // src/trace/live.ts
2334
+ function globToRegex(pattern) {
2335
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
2336
+ const withWildcards = escaped.replace(/\*/g, ".*");
2337
+ return new RegExp(`^${withWildcards}$`);
2338
+ }
2339
+
1358
2340
  // src/actions/executor.ts
1359
2341
  var DEFAULT_TIMEOUT = 3e4;
1360
2342
  var DEFAULT_RECORDING_SKIP_ACTIONS = [
@@ -1364,6 +2346,61 @@ var DEFAULT_RECORDING_SKIP_ACTIONS = [
1364
2346
  "text",
1365
2347
  "screenshot"
1366
2348
  ];
2349
+ function readString(value) {
2350
+ return typeof value === "string" ? value : void 0;
2351
+ }
2352
+ function readStringOr(value, fallback = "") {
2353
+ return readString(value) ?? fallback;
2354
+ }
2355
+ function formatConsoleArg(entry) {
2356
+ return readString(entry["value"]) ?? readString(entry["description"]) ?? "";
2357
+ }
2358
+ function loadExistingRecording(manifestPath) {
2359
+ try {
2360
+ const raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
2361
+ if (raw.version === 1) {
2362
+ const legacy = raw;
2363
+ return {
2364
+ frames: Array.isArray(legacy.frames) ? legacy.frames : [],
2365
+ traceEvents: [],
2366
+ recordedAt: legacy.recordedAt,
2367
+ startUrl: legacy.startUrl
2368
+ };
2369
+ }
2370
+ const artifact = canonicalizeRecordingArtifact(raw);
2371
+ const screenshotsByAction = new Map(artifact.screenshots.map((shot) => [shot.actionId, shot]));
2372
+ const frames = artifact.actions.map((action, index) => {
2373
+ const screenshot = screenshotsByAction.get(action.id);
2374
+ return {
2375
+ seq: index + 1,
2376
+ timestamp: Date.parse(action.ts),
2377
+ action: action.action,
2378
+ selector: action.selector,
2379
+ selectorUsed: action.selectorUsed,
2380
+ value: action.value,
2381
+ url: action.url,
2382
+ coordinates: action.coordinates,
2383
+ boundingBox: action.boundingBox,
2384
+ success: action.success,
2385
+ durationMs: action.durationMs,
2386
+ error: action.error,
2387
+ screenshot: screenshot?.file ?? "",
2388
+ pageUrl: action.pageUrl,
2389
+ pageTitle: action.pageTitle,
2390
+ stepIndex: action.stepIndex,
2391
+ actionId: action.id
2392
+ };
2393
+ });
2394
+ return {
2395
+ frames,
2396
+ traceEvents: artifact.trace.events,
2397
+ recordedAt: artifact.recordedAt,
2398
+ startUrl: artifact.session.startUrl
2399
+ };
2400
+ } catch {
2401
+ return { frames: [], traceEvents: [] };
2402
+ }
2403
+ }
1367
2404
  function classifyFailure(error) {
1368
2405
  if (error instanceof ElementNotFoundError) {
1369
2406
  return { reason: "missing" };
@@ -1444,6 +2481,9 @@ var BatchExecutor = class {
1444
2481
  const results = [];
1445
2482
  const startTime = Date.now();
1446
2483
  const recording = options.record ? this.createRecordingContext(options.record) : null;
2484
+ if (steps.some((step) => step.action === "waitForWsMessage")) {
2485
+ await this.ensureTraceHooks();
2486
+ }
1447
2487
  const startUrl = recording ? await this.getPageUrlSafe() : "";
1448
2488
  let stoppedAtIndex;
1449
2489
  for (let i = 0; i < steps.length; i++) {
@@ -1453,6 +2493,26 @@ var BatchExecutor = class {
1453
2493
  const retryDelay = step.retryDelay ?? 500;
1454
2494
  let lastError;
1455
2495
  let succeeded = false;
2496
+ if (recording) {
2497
+ recording.traceEvents.push(
2498
+ normalizeTraceEvent({
2499
+ traceId: createTraceId("action"),
2500
+ elapsedMs: Date.now() - startTime,
2501
+ channel: "action",
2502
+ event: "action.started",
2503
+ summary: `${step.action}${step.selector ? ` ${Array.isArray(step.selector) ? step.selector[0] : step.selector}` : ""}`,
2504
+ data: {
2505
+ action: step.action,
2506
+ selector: step.selector ?? null,
2507
+ url: step.url ?? null
2508
+ },
2509
+ actionId: `action-${i + 1}`,
2510
+ stepIndex: i,
2511
+ selector: step.selector,
2512
+ url: step.url
2513
+ })
2514
+ );
2515
+ }
1456
2516
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
1457
2517
  if (attempt > 0) {
1458
2518
  await new Promise((resolve) => setTimeout(resolve, retryDelay));
@@ -1476,6 +2536,28 @@ var BatchExecutor = class {
1476
2536
  if (recording && !recording.skipActions.has(step.action)) {
1477
2537
  await this.captureRecordingFrame(step, stepResult, recording);
1478
2538
  }
2539
+ if (recording) {
2540
+ recording.traceEvents.push(
2541
+ normalizeTraceEvent({
2542
+ traceId: createTraceId("action"),
2543
+ elapsedMs: Date.now() - startTime,
2544
+ channel: "action",
2545
+ event: "action.succeeded",
2546
+ summary: `${step.action} succeeded`,
2547
+ data: {
2548
+ action: step.action,
2549
+ selector: step.selector ?? null,
2550
+ selectorUsed: result.selectorUsed ?? null,
2551
+ durationMs: Date.now() - stepStart
2552
+ },
2553
+ actionId: `action-${i + 1}`,
2554
+ stepIndex: i,
2555
+ selector: step.selector,
2556
+ selectorUsed: result.selectorUsed,
2557
+ url: step.url
2558
+ })
2559
+ );
2560
+ }
1479
2561
  results.push(stepResult);
1480
2562
  succeeded = true;
1481
2563
  break;
@@ -1513,6 +2595,28 @@ var BatchExecutor = class {
1513
2595
  if (recording && !recording.skipActions.has(step.action)) {
1514
2596
  await this.captureRecordingFrame(step, failedResult, recording);
1515
2597
  }
2598
+ if (recording) {
2599
+ recording.traceEvents.push(
2600
+ normalizeTraceEvent({
2601
+ traceId: createTraceId("action"),
2602
+ elapsedMs: Date.now() - startTime,
2603
+ channel: "action",
2604
+ event: "action.failed",
2605
+ severity: "error",
2606
+ summary: `${step.action} failed: ${errorMessage}`,
2607
+ data: {
2608
+ action: step.action,
2609
+ selector: step.selector ?? null,
2610
+ error: errorMessage,
2611
+ reason
2612
+ },
2613
+ actionId: `action-${i + 1}`,
2614
+ stepIndex: i,
2615
+ selector: step.selector,
2616
+ url: step.url
2617
+ })
2618
+ );
2619
+ }
1516
2620
  results.push(failedResult);
1517
2621
  if (onFail === "stop" && !step.optional) {
1518
2622
  stoppedAtIndex = i;
@@ -1528,7 +2632,8 @@ var BatchExecutor = class {
1528
2632
  recording,
1529
2633
  startTime,
1530
2634
  startUrl,
1531
- allSuccess
2635
+ allSuccess,
2636
+ steps
1532
2637
  );
1533
2638
  }
1534
2639
  return {
@@ -1543,20 +2648,14 @@ var BatchExecutor = class {
1543
2648
  const baseDir = record.outputDir ?? (0, import_node_path.join)(process.cwd(), ".browser-pilot");
1544
2649
  const screenshotDir = (0, import_node_path.join)(baseDir, "screenshots");
1545
2650
  const manifestPath = (0, import_node_path.join)(baseDir, "recording.json");
1546
- let existingFrames = [];
1547
- try {
1548
- const existing = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
1549
- if (existing.frames && Array.isArray(existing.frames)) {
1550
- existingFrames = existing.frames;
1551
- }
1552
- } catch {
1553
- }
2651
+ const existing = loadExistingRecording(manifestPath);
1554
2652
  fs.mkdirSync(screenshotDir, { recursive: true });
1555
2653
  return {
1556
2654
  baseDir,
1557
2655
  screenshotDir,
1558
2656
  sessionId: record.sessionId ?? this.page.targetId,
1559
- frames: existingFrames,
2657
+ frames: existing.frames,
2658
+ traceEvents: existing.traceEvents,
1560
2659
  format: record.format ?? "webp",
1561
2660
  quality: Math.max(0, Math.min(100, record.quality ?? 40)),
1562
2661
  highlights: record.highlights !== false,
@@ -1612,6 +2711,7 @@ var BatchExecutor = class {
1612
2711
  timestamp: ts,
1613
2712
  action: stepResult.action,
1614
2713
  selector: stepResult.selectorUsed ?? (Array.isArray(step.selector) ? step.selector[0] : step.selector),
2714
+ selectorUsed: stepResult.selectorUsed,
1615
2715
  value: redactValueForRecording(
1616
2716
  typeof step.value === "string" ? step.value : void 0,
1617
2717
  targetMetadata
@@ -1624,7 +2724,9 @@ var BatchExecutor = class {
1624
2724
  error: stepResult.error,
1625
2725
  screenshot: filename,
1626
2726
  pageUrl,
1627
- pageTitle
2727
+ pageTitle,
2728
+ stepIndex: stepResult.index,
2729
+ actionId: `action-${stepResult.index + 1}`
1628
2730
  });
1629
2731
  } catch {
1630
2732
  } finally {
@@ -1636,45 +2738,31 @@ var BatchExecutor = class {
1636
2738
  /**
1637
2739
  * Write recording manifest to disk
1638
2740
  */
1639
- async writeRecordingManifest(recording, startTime, startUrl, success) {
2741
+ async writeRecordingManifest(recording, startTime, startUrl, success, steps) {
1640
2742
  let endUrl = startUrl;
1641
- let viewport = { width: 1280, height: 720 };
1642
2743
  try {
1643
2744
  endUrl = await this.page.url();
1644
2745
  } catch {
1645
2746
  }
1646
- try {
1647
- const metrics = await this.page.cdpClient.send("Page.getLayoutMetrics");
1648
- viewport = {
1649
- width: metrics.cssVisualViewport.clientWidth,
1650
- height: metrics.cssVisualViewport.clientHeight
1651
- };
1652
- } catch {
1653
- }
1654
2747
  const manifestPath = (0, import_node_path.join)(recording.baseDir, "recording.json");
1655
2748
  let recordedAt = new Date(startTime).toISOString();
1656
2749
  let originalStartUrl = startUrl;
1657
- try {
1658
- const existing = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
1659
- if (existing.recordedAt) recordedAt = existing.recordedAt;
1660
- if (existing.startUrl) originalStartUrl = existing.startUrl;
1661
- } catch {
1662
- }
1663
- const firstFrameTime = recording.frames[0]?.timestamp ?? startTime;
1664
- const totalDurationMs = Date.now() - Math.min(firstFrameTime, startTime);
1665
- const manifest = {
1666
- version: 1,
2750
+ const existing = loadExistingRecording(manifestPath);
2751
+ if (existing.recordedAt) recordedAt = existing.recordedAt;
2752
+ if (existing.startUrl) originalStartUrl = existing.startUrl;
2753
+ const manifest = createRecordingManifest({
1667
2754
  recordedAt,
1668
2755
  sessionId: recording.sessionId,
1669
2756
  startUrl: originalStartUrl,
1670
2757
  endUrl,
1671
- viewport,
1672
- format: recording.format,
1673
- quality: recording.quality,
1674
- totalDurationMs,
1675
- success,
1676
- frames: recording.frames
1677
- };
2758
+ targetId: this.page.targetId,
2759
+ steps,
2760
+ frames: recording.frames,
2761
+ traceEvents: recording.traceEvents,
2762
+ notes: success ? [] : ["Replay ended with at least one failed action."],
2763
+ recordingManifest: "recording.json",
2764
+ screenshotDir: "screenshots/"
2765
+ });
1678
2766
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
1679
2767
  return manifestPath;
1680
2768
  }
@@ -1957,6 +3045,39 @@ var BatchExecutor = class {
1957
3045
  }
1958
3046
  return { selectorUsed: usedSelector, value: actual };
1959
3047
  }
3048
+ case "waitForWsMessage": {
3049
+ if (typeof step.match !== "string") {
3050
+ throw new Error("waitForWsMessage requires match");
3051
+ }
3052
+ const message = await this.waitForWsMessage(step.match, step.where, timeout);
3053
+ return { value: message };
3054
+ }
3055
+ case "assertNoConsoleErrors": {
3056
+ await this.assertNoConsoleErrors(step.windowMs ?? timeout);
3057
+ return {};
3058
+ }
3059
+ case "assertTextChanged": {
3060
+ const selector = Array.isArray(step.selector) ? step.selector[0] : step.selector;
3061
+ if (typeof step.to !== "string") {
3062
+ throw new Error("assertTextChanged requires to");
3063
+ }
3064
+ const text = await this.assertTextChanged(selector, step.from, step.to, timeout);
3065
+ return { selectorUsed: selector, text };
3066
+ }
3067
+ case "assertPermission": {
3068
+ if (!step.name || !step.state) {
3069
+ throw new Error("assertPermission requires name and state");
3070
+ }
3071
+ const permission = await this.assertPermission(step.name, step.state);
3072
+ return { value: permission };
3073
+ }
3074
+ case "assertMediaTrackLive": {
3075
+ if (!step.kind) {
3076
+ throw new Error("assertMediaTrackLive requires kind");
3077
+ }
3078
+ const media = await this.assertMediaTrackLive(step.kind);
3079
+ return { value: media };
3080
+ }
1960
3081
  default: {
1961
3082
  const action = step.action;
1962
3083
  const aliases = {
@@ -2010,7 +3131,7 @@ var BatchExecutor = class {
2010
3131
  };
2011
3132
  const suggestion = aliases[action.toLowerCase()];
2012
3133
  const hint = suggestion ? ` Did you mean "${suggestion}"?` : "";
2013
- const valid = "goto, click, fill, type, select, check, uncheck, submit, press, shortcut, focus, hover, scroll, wait, snapshot, forms, screenshot, evaluate, text, newTab, closeTab, switchFrame, switchToMain, assertVisible, assertExists, assertText, assertUrl, assertValue";
3134
+ const valid = "goto, click, fill, type, select, check, uncheck, submit, press, shortcut, focus, hover, scroll, wait, snapshot, forms, screenshot, evaluate, text, newTab, closeTab, switchFrame, switchToMain, assertVisible, assertExists, assertText, assertUrl, assertValue, waitForWsMessage, assertNoConsoleErrors, assertTextChanged, assertPermission, assertMediaTrackLive";
2014
3135
  throw new Error(`Unknown action "${action}".${hint}
2015
3136
 
2016
3137
  Valid actions: ${valid}`);
@@ -2026,6 +3147,237 @@ Valid actions: ${valid}`);
2026
3147
  if (matched) return matched;
2027
3148
  return Array.isArray(selector) ? selector[0] : selector;
2028
3149
  }
3150
+ async ensureTraceHooks() {
3151
+ await this.page.cdpClient.send("Runtime.enable");
3152
+ await this.page.cdpClient.send("Page.enable");
3153
+ await this.page.cdpClient.send("Network.enable");
3154
+ try {
3155
+ await this.page.cdpClient.send("Runtime.addBinding", { name: TRACE_BINDING_NAME });
3156
+ } catch {
3157
+ }
3158
+ await this.page.cdpClient.send("Page.addScriptToEvaluateOnNewDocument", {
3159
+ source: TRACE_SCRIPT
3160
+ });
3161
+ await this.page.cdpClient.send("Runtime.evaluate", {
3162
+ expression: TRACE_SCRIPT,
3163
+ awaitPromise: false
3164
+ });
3165
+ }
3166
+ async waitForWsMessage(match, where, timeout) {
3167
+ await this.ensureTraceHooks();
3168
+ const regex = globToRegex(match);
3169
+ const wsUrls = /* @__PURE__ */ new Map();
3170
+ const recentMatch = await this.findRecentWsMessage(regex, where);
3171
+ if (recentMatch) {
3172
+ return recentMatch;
3173
+ }
3174
+ return new Promise((resolve, reject) => {
3175
+ const cleanup = () => {
3176
+ this.page.cdpClient.off("Network.webSocketCreated", onCreated);
3177
+ this.page.cdpClient.off("Network.webSocketFrameReceived", onFrame);
3178
+ this.page.cdpClient.off("Runtime.bindingCalled", onBinding);
3179
+ clearTimeout(timer);
3180
+ };
3181
+ const onCreated = (params) => {
3182
+ wsUrls.set(readStringOr(params["requestId"]), readStringOr(params["url"]));
3183
+ };
3184
+ const onFrame = (params) => {
3185
+ const requestId = readStringOr(params["requestId"]);
3186
+ const response = params["response"] ?? {};
3187
+ const payload = response.payloadData ?? "";
3188
+ const url = wsUrls.get(requestId) ?? "";
3189
+ if (!regex.test(url) && !regex.test(payload)) {
3190
+ return;
3191
+ }
3192
+ if (where && !this.payloadMatchesWhere(payload, where)) {
3193
+ return;
3194
+ }
3195
+ cleanup();
3196
+ resolve({ requestId, url, payload });
3197
+ };
3198
+ const onBinding = (params) => {
3199
+ if (params["name"] !== TRACE_BINDING_NAME) {
3200
+ return;
3201
+ }
3202
+ try {
3203
+ const parsed = JSON.parse(readStringOr(params["payload"]));
3204
+ if (parsed.event !== "ws.frame.received") {
3205
+ return;
3206
+ }
3207
+ const data = parsed.data ?? {};
3208
+ const payload = readStringOr(data["payload"]);
3209
+ const url = readStringOr(data["url"]);
3210
+ if (!regex.test(url) && !regex.test(payload)) {
3211
+ return;
3212
+ }
3213
+ if (where && !this.payloadMatchesWhere(payload, where)) {
3214
+ return;
3215
+ }
3216
+ cleanup();
3217
+ resolve({
3218
+ requestId: readStringOr(data["connectionId"]),
3219
+ url,
3220
+ payload
3221
+ });
3222
+ } catch {
3223
+ }
3224
+ };
3225
+ const timer = setTimeout(() => {
3226
+ cleanup();
3227
+ reject(new Error(`Timed out waiting for WebSocket message matching ${match}`));
3228
+ }, timeout);
3229
+ this.page.cdpClient.on("Network.webSocketCreated", onCreated);
3230
+ this.page.cdpClient.on("Network.webSocketFrameReceived", onFrame);
3231
+ this.page.cdpClient.on("Runtime.bindingCalled", onBinding);
3232
+ });
3233
+ }
3234
+ payloadMatchesWhere(payload, where) {
3235
+ try {
3236
+ const parsed = JSON.parse(payload);
3237
+ return Object.entries(where).every(([key, expected]) => {
3238
+ const actual = key.split(".").reduce((current, part) => {
3239
+ if (!current || typeof current !== "object") {
3240
+ return void 0;
3241
+ }
3242
+ return current[part];
3243
+ }, parsed);
3244
+ return actual === expected;
3245
+ });
3246
+ } catch {
3247
+ return false;
3248
+ }
3249
+ }
3250
+ async findRecentWsMessage(regex, where) {
3251
+ const recent = await this.page.evaluate(
3252
+ "(() => Array.isArray(globalThis.__bpTraceRecentEvents) ? globalThis.__bpTraceRecentEvents : [])()"
3253
+ );
3254
+ if (!Array.isArray(recent)) {
3255
+ return null;
3256
+ }
3257
+ for (let i = recent.length - 1; i >= 0; i--) {
3258
+ const entry = recent[i];
3259
+ if (!entry || typeof entry !== "object") {
3260
+ continue;
3261
+ }
3262
+ const record = entry;
3263
+ const event = readStringOr(record["event"]);
3264
+ if (event !== "ws.frame.received") {
3265
+ continue;
3266
+ }
3267
+ const data = record["data"] ?? {};
3268
+ const payload = readStringOr(data["payload"]);
3269
+ const url = readStringOr(data["url"]);
3270
+ if (!regex.test(url) && !regex.test(payload)) {
3271
+ continue;
3272
+ }
3273
+ if (where && !this.payloadMatchesWhere(payload, where)) {
3274
+ continue;
3275
+ }
3276
+ return {
3277
+ requestId: readStringOr(data["connectionId"]),
3278
+ url,
3279
+ payload
3280
+ };
3281
+ }
3282
+ return null;
3283
+ }
3284
+ async assertNoConsoleErrors(windowMs) {
3285
+ await this.page.cdpClient.send("Runtime.enable");
3286
+ return new Promise((resolve, reject) => {
3287
+ const errors = [];
3288
+ const cleanup = () => {
3289
+ this.page.cdpClient.off("Runtime.consoleAPICalled", onConsole);
3290
+ this.page.cdpClient.off("Runtime.exceptionThrown", onException);
3291
+ clearTimeout(timer);
3292
+ };
3293
+ const onConsole = (params) => {
3294
+ if (params["type"] !== "error") {
3295
+ return;
3296
+ }
3297
+ const args = Array.isArray(params["args"]) ? params["args"] : [];
3298
+ errors.push(args.map(formatConsoleArg).filter(Boolean).join(" "));
3299
+ };
3300
+ const onException = (params) => {
3301
+ const details = params["exceptionDetails"] ?? {};
3302
+ errors.push(readString(details["text"]) ?? "Runtime exception");
3303
+ };
3304
+ const timer = setTimeout(() => {
3305
+ cleanup();
3306
+ if (errors.length > 0) {
3307
+ reject(new Error(`Console errors detected: ${errors.join(" | ")}`));
3308
+ return;
3309
+ }
3310
+ resolve();
3311
+ }, windowMs);
3312
+ this.page.cdpClient.on("Runtime.consoleAPICalled", onConsole);
3313
+ this.page.cdpClient.on("Runtime.exceptionThrown", onException);
3314
+ });
3315
+ }
3316
+ async assertTextChanged(selector, from, to, timeout) {
3317
+ const initialText = from ?? await this.page.text(selector);
3318
+ const deadline = Date.now() + timeout;
3319
+ while (Date.now() < deadline) {
3320
+ const text = await this.page.text(selector);
3321
+ if (text !== initialText && text.includes(to)) {
3322
+ return text;
3323
+ }
3324
+ await new Promise((resolve) => setTimeout(resolve, 200));
3325
+ }
3326
+ throw new Error(`Text did not change to include ${JSON.stringify(to)}`);
3327
+ }
3328
+ async assertPermission(name, state) {
3329
+ const result = await this.page.evaluate(
3330
+ `(() => navigator.permissions.query({ name: ${JSON.stringify(name)} }).then((status) => ({ name: ${JSON.stringify(name)}, state: status.state })))()`
3331
+ );
3332
+ if (!result || typeof result !== "object" || result.state !== state) {
3333
+ throw new Error(`Permission ${name} is not ${state}`);
3334
+ }
3335
+ return result;
3336
+ }
3337
+ async assertMediaTrackLive(kind) {
3338
+ const result = await this.page.evaluate(
3339
+ `(() => {
3340
+ const requestedKind = ${JSON.stringify(kind)};
3341
+ const mediaElements = Array.from(document.querySelectorAll('audio,video')).map((el) => {
3342
+ const tracks = [];
3343
+ if (el.srcObject && typeof el.srcObject.getTracks === 'function') {
3344
+ tracks.push(...el.srcObject.getTracks());
3345
+ }
3346
+ return {
3347
+ tag: el.tagName.toLowerCase(),
3348
+ paused: !!el.paused,
3349
+ tracks: tracks.map((track) => ({
3350
+ kind: track.kind,
3351
+ readyState: track.readyState,
3352
+ enabled: track.enabled,
3353
+ label: track.label,
3354
+ })),
3355
+ };
3356
+ });
3357
+
3358
+ const globalTracks =
3359
+ window.__bpStream && typeof window.__bpStream.getTracks === 'function'
3360
+ ? window.__bpStream.getTracks().map((track) => ({
3361
+ kind: track.kind,
3362
+ readyState: track.readyState,
3363
+ enabled: track.enabled,
3364
+ label: track.label,
3365
+ }))
3366
+ : [];
3367
+
3368
+ const liveTracks = mediaElements
3369
+ .flatMap((entry) => entry.tracks)
3370
+ .concat(globalTracks)
3371
+ .filter((track) => track.kind === requestedKind && track.readyState === 'live');
3372
+
3373
+ return { live: liveTracks.length > 0, mediaElements, globalTracks, liveTracks };
3374
+ })()`
3375
+ );
3376
+ if (!result || typeof result !== "object" || !result.live) {
3377
+ throw new Error(`No live ${kind} media track detected`);
3378
+ }
3379
+ return result;
3380
+ }
2029
3381
  };
2030
3382
 
2031
3383
  // src/audio/encoding.ts
@@ -2063,6 +3415,10 @@ async function grantAudioPermissions(cdp, origin) {
2063
3415
  await cdp.send("Page.addScriptToEvaluateOnNewDocument", {
2064
3416
  source: PERMISSIONS_OVERRIDE_SCRIPT
2065
3417
  });
3418
+ await cdp.send("Runtime.evaluate", {
3419
+ expression: PERMISSIONS_OVERRIDE_SCRIPT,
3420
+ awaitPromise: false
3421
+ });
2066
3422
  }
2067
3423
  var PERMISSIONS_OVERRIDE_SCRIPT = `
2068
3424
  (function() {
@@ -3021,7 +4377,7 @@ var AudioOutput = class {
3021
4377
  awaitPromise: false
3022
4378
  });
3023
4379
  this.capturing = false;
3024
- await sleep2(250);
4380
+ await sleep3(250);
3025
4381
  return this.mergeChunks();
3026
4382
  }
3027
4383
  /**
@@ -3237,7 +4593,7 @@ function emptyCaptureResult() {
3237
4593
  chunkCount: 0
3238
4594
  };
3239
4595
  }
3240
- function sleep2(ms) {
4596
+ function sleep3(ms) {
3241
4597
  return new Promise((resolve) => setTimeout(resolve, ms));
3242
4598
  }
3243
4599
 
@@ -3778,7 +5134,7 @@ async function isElementAttached(cdp, selector, contextId) {
3778
5134
  const result = await cdp.send("Runtime.evaluate", params);
3779
5135
  return result.result.value === true;
3780
5136
  }
3781
- function sleep3(ms) {
5137
+ function sleep4(ms) {
3782
5138
  return new Promise((resolve) => setTimeout(resolve, ms));
3783
5139
  }
3784
5140
  async function isPageStatic(cdp, windowMs = 200, contextId) {
@@ -3846,7 +5202,7 @@ async function waitForAnyElement(cdp, selectors, options = {}) {
3846
5202
  }
3847
5203
  }
3848
5204
  while (Date.now() < deadline) {
3849
- await sleep3(pollInterval);
5205
+ await sleep4(pollInterval);
3850
5206
  for (const selector of selectors) {
3851
5207
  if (await checkSelector(selector)) {
3852
5208
  return { success: true, selector, waitedMs: Date.now() - startTime };
@@ -3907,7 +5263,7 @@ async function waitForNavigation(cdp, options = {}) {
3907
5263
  cleanup.push(() => cdp.off("Page.lifecycleEvent", onLifecycle));
3908
5264
  const pollUrl = async () => {
3909
5265
  while (!resolved && Date.now() < startTime + timeout) {
3910
- await sleep3(100);
5266
+ await sleep4(100);
3911
5267
  if (resolved) return;
3912
5268
  try {
3913
5269
  const currentUrl = await getCurrentUrl(cdp);
@@ -4512,7 +5868,7 @@ var Page = class {
4512
5868
  } catch (e) {
4513
5869
  if (options.optional) return false;
4514
5870
  if (e instanceof ActionabilityError && e.failureType === "hitTarget" && attempt < HIT_TARGET_RETRIES - 1) {
4515
- await sleep4(HIT_TARGET_DELAY);
5871
+ await sleep5(HIT_TARGET_DELAY);
4516
5872
  await this.cdp.send("DOM.scrollIntoViewIfNeeded", { nodeId: element.nodeId });
4517
5873
  continue;
4518
5874
  }
@@ -4687,7 +6043,7 @@ var Page = class {
4687
6043
  await this.cdp.send("Input.insertText", { text: char });
4688
6044
  }
4689
6045
  if (delay > 0) {
4690
- await sleep4(delay);
6046
+ await sleep5(delay);
4691
6047
  }
4692
6048
  }
4693
6049
  if (options.blur) {
@@ -4783,7 +6139,7 @@ var Page = class {
4783
6139
  state: "visible",
4784
6140
  timeout: 500,
4785
6141
  contextId: this.currentFrameContextId ?? void 0
4786
- }).catch(() => sleep4(100));
6142
+ }).catch(() => sleep5(100));
4787
6143
  const optionHandle = await this.evaluateInFrame(
4788
6144
  `(() => {
4789
6145
  const selectors = ${JSON.stringify(optionSelectors)};
@@ -5000,7 +6356,7 @@ var Page = class {
5000
6356
  () => "navigation"
5001
6357
  ),
5002
6358
  this.waitForDOMMutation({ timeout: 1e3 }).then(() => "mutation"),
5003
- sleep4(1500).then(() => "timeout")
6359
+ sleep5(1500).then(() => "timeout")
5004
6360
  ]);
5005
6361
  }
5006
6362
  return true;
@@ -5020,7 +6376,7 @@ var Page = class {
5020
6376
  (success) => success ? "nav" : null
5021
6377
  ),
5022
6378
  this.waitForDOMMutation({ timeout: 1e3 }).then(() => "mutation"),
5023
- sleep4(1500).then(() => "timeout")
6379
+ sleep5(1500).then(() => "timeout")
5024
6380
  ]);
5025
6381
  if (navigationDetected === "nav") {
5026
6382
  return true;
@@ -5034,7 +6390,7 @@ var Page = class {
5034
6390
  if (shouldWait === true) {
5035
6391
  await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT2 });
5036
6392
  } else if (shouldWait === "auto") {
5037
- await sleep4(100);
6393
+ await sleep5(100);
5038
6394
  }
5039
6395
  }
5040
6396
  return true;
@@ -6431,7 +7787,7 @@ var Page = class {
6431
7787
  try {
6432
7788
  await Promise.race([
6433
7789
  this.dialogHandler(dialog),
6434
- sleep4(DIALOG_TIMEOUT).then(() => {
7790
+ sleep5(DIALOG_TIMEOUT).then(() => {
6435
7791
  console.warn("[browser-pilot] Dialog handler timed out after 5s, auto-dismissing");
6436
7792
  return dialog.dismiss();
6437
7793
  })
@@ -6556,7 +7912,7 @@ var Page = class {
6556
7912
  if (attempt < retries) {
6557
7913
  this.rootNodeId = null;
6558
7914
  this.currentFrameContextId = null;
6559
- await sleep4(delay);
7915
+ await sleep5(delay);
6560
7916
  continue;
6561
7917
  }
6562
7918
  }
@@ -7140,7 +8496,7 @@ var Page = class {
7140
8496
  const start = Date.now();
7141
8497
  await this.audioOutput.start();
7142
8498
  if (options.preDelay && options.preDelay > 0) {
7143
- await sleep4(options.preDelay);
8499
+ await sleep5(options.preDelay);
7144
8500
  }
7145
8501
  const inputDone = this.audioInput.play(options.input, {
7146
8502
  waitForEnd: !!options.sendSelector
@@ -7208,7 +8564,7 @@ var Page = class {
7208
8564
  });
7209
8565
  }
7210
8566
  };
7211
- function sleep4(ms) {
8567
+ function sleep5(ms) {
7212
8568
  return new Promise((resolve) => setTimeout(resolve, ms));
7213
8569
  }
7214
8570
 
@@ -7261,13 +8617,26 @@ var Browser = class _Browser {
7261
8617
  * Connect to a browser instance
7262
8618
  */
7263
8619
  static async connect(options) {
7264
- const provider = createProvider(options);
7265
- const session = await provider.createSession(options.session);
8620
+ let connectOptions = options;
8621
+ if (options.provider === "generic" && !options.wsUrl) {
8622
+ const endpoint = await resolveBrowserEndpoint({
8623
+ channel: options.channel,
8624
+ userDataDir: options.userDataDir,
8625
+ allowLocalDiscovery: true,
8626
+ allowLegacyHostFallback: true
8627
+ });
8628
+ connectOptions = {
8629
+ ...options,
8630
+ wsUrl: endpoint.wsUrl
8631
+ };
8632
+ }
8633
+ const provider = createProvider(connectOptions);
8634
+ const session = await provider.createSession(connectOptions.session);
7266
8635
  const cdp = await createCDPClient(session.wsUrl, {
7267
- debug: options.debug,
7268
- timeout: options.timeout
8636
+ debug: connectOptions.debug,
8637
+ timeout: connectOptions.timeout
7269
8638
  });
7270
- return new _Browser(cdp, provider, session, options);
8639
+ return new _Browser(cdp, provider, session, connectOptions);
7271
8640
  }
7272
8641
  /**
7273
8642
  * Get or create a page by name.