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
@@ -1,226 +1,7 @@
1
1
  import {
2
2
  CDPError,
3
- createCDPClient,
4
3
  stringifyUnknown
5
- } from "./chunk-SPSZZH22.mjs";
6
-
7
- // src/providers/browserbase.ts
8
- var BrowserBaseProvider = class {
9
- name = "browserbase";
10
- apiKey;
11
- projectId;
12
- baseUrl;
13
- constructor(options) {
14
- this.apiKey = options.apiKey;
15
- this.projectId = options.projectId;
16
- this.baseUrl = options.baseUrl ?? "https://api.browserbase.com";
17
- }
18
- async createSession(options = {}) {
19
- const response = await fetch(`${this.baseUrl}/v1/sessions`, {
20
- method: "POST",
21
- headers: {
22
- "X-BB-API-Key": this.apiKey,
23
- "Content-Type": "application/json"
24
- },
25
- body: JSON.stringify({
26
- projectId: this.projectId,
27
- browserSettings: {
28
- viewport: options.width && options.height ? {
29
- width: options.width,
30
- height: options.height
31
- } : void 0
32
- },
33
- ...options
34
- })
35
- });
36
- if (!response.ok) {
37
- const text = await response.text();
38
- throw new Error(`BrowserBase createSession failed: ${response.status} ${text}`);
39
- }
40
- const session = await response.json();
41
- const connectResponse = await fetch(`${this.baseUrl}/v1/sessions/${session.id}`, {
42
- headers: {
43
- "X-BB-API-Key": this.apiKey
44
- }
45
- });
46
- if (!connectResponse.ok) {
47
- throw new Error(`BrowserBase getSession failed: ${connectResponse.status}`);
48
- }
49
- const sessionDetails = await connectResponse.json();
50
- if (!sessionDetails.connectUrl) {
51
- throw new Error("BrowserBase session does not have a connectUrl");
52
- }
53
- return {
54
- wsUrl: sessionDetails.connectUrl,
55
- sessionId: session.id,
56
- metadata: {
57
- debugUrl: sessionDetails.debugUrl,
58
- projectId: this.projectId,
59
- status: sessionDetails.status
60
- },
61
- close: async () => {
62
- await fetch(`${this.baseUrl}/v1/sessions/${session.id}`, {
63
- method: "DELETE",
64
- headers: {
65
- "X-BB-API-Key": this.apiKey
66
- }
67
- });
68
- }
69
- };
70
- }
71
- async resumeSession(sessionId) {
72
- const response = await fetch(`${this.baseUrl}/v1/sessions/${sessionId}`, {
73
- headers: {
74
- "X-BB-API-Key": this.apiKey
75
- }
76
- });
77
- if (!response.ok) {
78
- throw new Error(`BrowserBase resumeSession failed: ${response.status}`);
79
- }
80
- const session = await response.json();
81
- if (!session.connectUrl) {
82
- throw new Error("BrowserBase session does not have a connectUrl (may be closed)");
83
- }
84
- return {
85
- wsUrl: session.connectUrl,
86
- sessionId: session.id,
87
- metadata: {
88
- debugUrl: session.debugUrl,
89
- projectId: this.projectId,
90
- status: session.status
91
- },
92
- close: async () => {
93
- await fetch(`${this.baseUrl}/v1/sessions/${sessionId}`, {
94
- method: "DELETE",
95
- headers: {
96
- "X-BB-API-Key": this.apiKey
97
- }
98
- });
99
- }
100
- };
101
- }
102
- };
103
-
104
- // src/providers/browserless.ts
105
- var BrowserlessProvider = class {
106
- name = "browserless";
107
- token;
108
- baseUrl;
109
- constructor(options) {
110
- this.token = options.token;
111
- this.baseUrl = options.baseUrl ?? "wss://chrome.browserless.io";
112
- }
113
- async createSession(options = {}) {
114
- const params = new URLSearchParams({
115
- token: this.token
116
- });
117
- if (options.width && options.height) {
118
- params.set("--window-size", `${options.width},${options.height}`);
119
- }
120
- if (options.proxy?.server) {
121
- params.set("--proxy-server", options.proxy.server);
122
- }
123
- const wsUrl = `${this.baseUrl}?${params.toString()}`;
124
- return {
125
- wsUrl,
126
- metadata: {
127
- provider: "browserless"
128
- },
129
- close: async () => {
130
- }
131
- };
132
- }
133
- // Browserless doesn't support session resumption in the same way
134
- // Each connection is a fresh browser instance
135
- };
136
-
137
- // src/providers/generic.ts
138
- function sleep(ms) {
139
- return new Promise((resolve) => setTimeout(resolve, ms));
140
- }
141
- async function fetchDevToolsJson(host, path, errorPrefix, options = {}) {
142
- const protocol = host.includes("://") ? "" : "http://";
143
- const attempts = options.attempts ?? 1;
144
- let delayMs = options.initialDelayMs ?? 50;
145
- const maxDelayMs = options.maxDelayMs ?? 250;
146
- let lastError;
147
- for (let attempt = 1; attempt <= attempts; attempt++) {
148
- try {
149
- const response = await fetch(`${protocol}${host}${path}`);
150
- if (response.ok) {
151
- return await response.json();
152
- }
153
- lastError = new Error(`${errorPrefix}: ${response.status}`);
154
- } catch (error) {
155
- lastError = new Error(
156
- `${errorPrefix}: ${error instanceof Error ? error.message : String(error)}`
157
- );
158
- }
159
- if (attempt < attempts) {
160
- await sleep(delayMs);
161
- delayMs = Math.min(delayMs * 2, maxDelayMs);
162
- }
163
- }
164
- throw lastError ?? new Error(errorPrefix);
165
- }
166
- var GenericProvider = class {
167
- name = "generic";
168
- wsUrl;
169
- constructor(options) {
170
- this.wsUrl = options.wsUrl;
171
- }
172
- async createSession(_options = {}) {
173
- return {
174
- wsUrl: this.wsUrl,
175
- metadata: {
176
- provider: "generic"
177
- },
178
- close: async () => {
179
- }
180
- };
181
- }
182
- };
183
- async function getBrowserWebSocketUrl(host = "localhost:9222") {
184
- const info = await fetchDevToolsJson(host, "/json/version", "Failed to get browser info", {
185
- attempts: 10,
186
- initialDelayMs: 50,
187
- maxDelayMs: 250
188
- });
189
- return info.webSocketDebuggerUrl;
190
- }
191
-
192
- // src/providers/index.ts
193
- function createProvider(options) {
194
- switch (options.provider) {
195
- case "browserbase":
196
- if (!options.apiKey) {
197
- throw new Error("BrowserBase provider requires apiKey");
198
- }
199
- if (!options.projectId) {
200
- throw new Error("BrowserBase provider requires projectId");
201
- }
202
- return new BrowserBaseProvider({
203
- apiKey: options.apiKey,
204
- projectId: options.projectId
205
- });
206
- case "browserless":
207
- if (!options.apiKey) {
208
- throw new Error("Browserless provider requires apiKey (token)");
209
- }
210
- return new BrowserlessProvider({
211
- token: options.apiKey
212
- });
213
- case "generic":
214
- if (!options.wsUrl) {
215
- throw new Error("Generic provider requires wsUrl");
216
- }
217
- return new GenericProvider({
218
- wsUrl: options.wsUrl
219
- });
220
- default:
221
- throw new Error(`Unknown provider: ${options.provider}`);
222
- }
223
- }
4
+ } from "./chunk-DTVRFXKI.mjs";
224
5
 
225
6
  // src/actions/executor.ts
226
7
  import * as fs from "fs";
@@ -693,7 +474,7 @@ var CHECK_EDITABLE = `function() {
693
474
 
694
475
  return { actionable: true };
695
476
  }`;
696
- function sleep2(ms) {
477
+ function sleep(ms) {
697
478
  return new Promise((resolve) => setTimeout(resolve, ms));
698
479
  }
699
480
  var BACKOFF = [0, 20, 100, 100];
@@ -777,7 +558,7 @@ async function ensureActionable(cdp, objectId, checks, options) {
777
558
  );
778
559
  }
779
560
  const delay = attempt < BACKOFF.length ? BACKOFF[attempt] ?? 0 : 500;
780
- if (delay > 0) await sleep2(delay);
561
+ if (delay > 0) await sleep(delay);
781
562
  attempt++;
782
563
  }
783
564
  }
@@ -1056,17 +837,978 @@ var NavigationError = class extends Error {
1056
837
  super(message);
1057
838
  this.name = "NavigationError";
1058
839
  }
1059
- };
1060
-
1061
- // src/actions/executor.ts
1062
- var DEFAULT_TIMEOUT = 3e4;
1063
- var DEFAULT_RECORDING_SKIP_ACTIONS = [
1064
- "wait",
1065
- "snapshot",
1066
- "forms",
1067
- "text",
1068
- "screenshot"
1069
- ];
840
+ };
841
+
842
+ // src/trace/views.ts
843
+ function takeRecent(events, limit = 5) {
844
+ return events.slice(-limit).map((event) => ({
845
+ ts: event.ts,
846
+ event: event.event,
847
+ summary: event.summary,
848
+ severity: event.severity,
849
+ url: event.url
850
+ }));
851
+ }
852
+ function buildTraceSummary(events, view) {
853
+ switch (view) {
854
+ case "ws":
855
+ return summarizeWs(events);
856
+ case "voice":
857
+ return summarizeVoice(events);
858
+ case "console":
859
+ return summarizeConsole(events);
860
+ case "permissions":
861
+ return summarizePermissions(events);
862
+ case "media":
863
+ return summarizeMedia(events);
864
+ case "ui":
865
+ return summarizeUi(events);
866
+ case "session":
867
+ return summarizeSession(events);
868
+ }
869
+ throw new Error(`Unsupported trace view: ${view}`);
870
+ }
871
+ function buildTraceSummaries(events) {
872
+ return {
873
+ ws: summarizeWs(events),
874
+ voice: summarizeVoice(events),
875
+ console: summarizeConsole(events),
876
+ permissions: summarizePermissions(events),
877
+ media: summarizeMedia(events),
878
+ ui: summarizeUi(events),
879
+ session: summarizeSession(events)
880
+ };
881
+ }
882
+ function summarizeWs(events) {
883
+ const relevant = events.filter(
884
+ (event) => event.channel === "ws" || event.event.startsWith("ws.")
885
+ );
886
+ const connections = /* @__PURE__ */ new Map();
887
+ for (const event of relevant) {
888
+ const id = event.connectionId ?? event.requestId ?? event.traceId;
889
+ let connection = connections.get(id);
890
+ if (!connection) {
891
+ connection = { id, sent: 0, received: 0, lastMessages: [] };
892
+ connections.set(id, connection);
893
+ }
894
+ connection.url = event.url ?? connection.url;
895
+ if (event.event === "ws.connection.created") {
896
+ connection.createdAt = event.ts;
897
+ }
898
+ if (event.event === "ws.connection.closed") {
899
+ connection.closedAt = event.ts;
900
+ }
901
+ if (event.event === "ws.frame.sent") {
902
+ connection.sent += 1;
903
+ const payload = typeof event.data["payload"] === "string" ? event.data["payload"] : "";
904
+ if (payload) connection.lastMessages.push(`sent: ${payload}`);
905
+ }
906
+ if (event.event === "ws.frame.received") {
907
+ connection.received += 1;
908
+ const payload = typeof event.data["payload"] === "string" ? event.data["payload"] : "";
909
+ if (payload) connection.lastMessages.push(`recv: ${payload}`);
910
+ }
911
+ connection.lastMessages = connection.lastMessages.slice(-3);
912
+ }
913
+ const values = [...connections.values()];
914
+ const reconnects = values.reduce((count, connection) => {
915
+ return connection.closedAt && !connection.createdAt ? count + 1 : count;
916
+ }, 0);
917
+ return {
918
+ view: "ws",
919
+ totalEvents: relevant.length,
920
+ connections: values.map((connection) => ({
921
+ id: connection.id,
922
+ url: connection.url ?? null,
923
+ createdAt: connection.createdAt ?? null,
924
+ closedAt: connection.closedAt ?? null,
925
+ sent: connection.sent,
926
+ received: connection.received,
927
+ lastMessages: connection.lastMessages,
928
+ connectedButSilent: !!connection.createdAt && !connection.closedAt && connection.sent + connection.received === 0
929
+ })),
930
+ reconnects,
931
+ recent: takeRecent(relevant)
932
+ };
933
+ }
934
+ function summarizeConsole(events) {
935
+ const relevant = events.filter(
936
+ (event) => event.channel === "console" || event.event.startsWith("console.") || event.event === "runtime.exception" || event.event === "runtime.unhandledRejection"
937
+ );
938
+ return {
939
+ view: "console",
940
+ errors: relevant.filter(
941
+ (event) => event.event === "console.error" || event.event === "runtime.exception" || event.event === "runtime.unhandledRejection"
942
+ ).length,
943
+ warnings: relevant.filter((event) => event.event === "console.warn").length,
944
+ logs: relevant.filter((event) => event.event === "console.log").length,
945
+ recent: takeRecent(relevant)
946
+ };
947
+ }
948
+ function summarizePermissions(events) {
949
+ const relevant = events.filter(
950
+ (event) => event.channel === "permission" || event.event.startsWith("permission.")
951
+ );
952
+ const latest = /* @__PURE__ */ new Map();
953
+ for (const event of relevant) {
954
+ const name = typeof event.data["name"] === "string" ? event.data["name"] : null;
955
+ const state = typeof event.data["state"] === "string" ? event.data["state"] : null;
956
+ if (name && state) {
957
+ latest.set(name, state);
958
+ }
959
+ }
960
+ return {
961
+ view: "permissions",
962
+ states: Object.fromEntries(latest),
963
+ changes: relevant.filter((event) => event.event === "permission.changed").length,
964
+ recent: takeRecent(relevant)
965
+ };
966
+ }
967
+ function summarizeMedia(events) {
968
+ const relevant = events.filter(
969
+ (event) => event.channel === "media" || event.event.startsWith("media.")
970
+ );
971
+ const liveTracks = /* @__PURE__ */ new Map();
972
+ for (const event of relevant) {
973
+ const label = typeof event.data["label"] === "string" ? event.data["label"] : "";
974
+ const kind = typeof event.data["kind"] === "string" ? event.data["kind"] : "";
975
+ const key = `${kind}:${label}`;
976
+ if (event.event === "media.track.started") {
977
+ liveTracks.set(key, kind);
978
+ }
979
+ if (event.event === "media.track.ended") {
980
+ liveTracks.delete(key);
981
+ }
982
+ }
983
+ return {
984
+ view: "media",
985
+ tracksStarted: relevant.filter((event) => event.event === "media.track.started").length,
986
+ tracksEnded: relevant.filter((event) => event.event === "media.track.ended").length,
987
+ playbackStarted: relevant.filter((event) => event.event === "media.playback.started").length,
988
+ playbackStopped: relevant.filter((event) => event.event === "media.playback.stopped").length,
989
+ liveTracks: [...liveTracks.values()],
990
+ recent: takeRecent(relevant)
991
+ };
992
+ }
993
+ function summarizeVoice(events) {
994
+ const relevant = events.filter(
995
+ (event) => event.channel === "voice" || event.event.startsWith("voice.")
996
+ );
997
+ return {
998
+ view: "voice",
999
+ ready: relevant.filter((event) => event.event === "voice.pipeline.ready").length,
1000
+ notReady: relevant.filter((event) => event.event === "voice.pipeline.notReady").length,
1001
+ captureStarted: relevant.filter((event) => event.event === "voice.capture.started").length,
1002
+ captureStopped: relevant.filter((event) => event.event === "voice.capture.stopped").length,
1003
+ detectedAudio: relevant.filter((event) => event.event === "voice.capture.detectedAudio").length,
1004
+ recent: takeRecent(relevant)
1005
+ };
1006
+ }
1007
+ function summarizeUi(events) {
1008
+ const relevant = events.filter(
1009
+ (event) => event.channel === "dom" || event.event.startsWith("dom.") || event.channel === "action"
1010
+ );
1011
+ return {
1012
+ view: "ui",
1013
+ actions: relevant.filter((event) => event.channel === "action").length,
1014
+ domChanges: relevant.filter((event) => event.channel === "dom").length,
1015
+ recent: takeRecent(relevant)
1016
+ };
1017
+ }
1018
+ function summarizeSession(events) {
1019
+ const byChannel = /* @__PURE__ */ new Map();
1020
+ const failedActions = events.filter((event) => event.event === "action.failed").length;
1021
+ for (const event of events) {
1022
+ byChannel.set(event.channel, (byChannel.get(event.channel) ?? 0) + 1);
1023
+ }
1024
+ return {
1025
+ view: "session",
1026
+ totalEvents: events.length,
1027
+ byChannel: Object.fromEntries(byChannel),
1028
+ failedActions,
1029
+ recent: takeRecent(events)
1030
+ };
1031
+ }
1032
+
1033
+ // src/recording/manifest.ts
1034
+ function isCanonicalRecordingManifest(value) {
1035
+ return Boolean(
1036
+ value && typeof value === "object" && value.version === 2 && typeof value.session === "object"
1037
+ );
1038
+ }
1039
+ function isLegacyRecordingManifest(value) {
1040
+ return Boolean(
1041
+ value && typeof value === "object" && value.version === 1 && Array.isArray(value.frames)
1042
+ );
1043
+ }
1044
+ function createRecordingManifest(input) {
1045
+ const actions = input.frames.map((frame) => {
1046
+ const actionId = frame.actionId ?? `action-${frame.seq}`;
1047
+ return {
1048
+ id: actionId,
1049
+ stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
1050
+ action: frame.action,
1051
+ selector: frame.selector,
1052
+ selectorUsed: frame.selectorUsed ?? frame.selector,
1053
+ value: frame.value,
1054
+ url: frame.url,
1055
+ success: frame.success,
1056
+ durationMs: frame.durationMs,
1057
+ error: frame.error,
1058
+ ts: new Date(frame.timestamp).toISOString(),
1059
+ pageUrl: frame.pageUrl,
1060
+ pageTitle: frame.pageTitle,
1061
+ coordinates: frame.coordinates,
1062
+ boundingBox: frame.boundingBox
1063
+ };
1064
+ });
1065
+ const screenshots = input.frames.map((frame) => ({
1066
+ id: `shot-${frame.seq}`,
1067
+ stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
1068
+ actionId: frame.actionId ?? `action-${frame.seq}`,
1069
+ file: frame.screenshot,
1070
+ ts: new Date(frame.timestamp).toISOString(),
1071
+ success: frame.success,
1072
+ pageUrl: frame.pageUrl,
1073
+ pageTitle: frame.pageTitle,
1074
+ coordinates: frame.coordinates,
1075
+ boundingBox: frame.boundingBox
1076
+ }));
1077
+ return {
1078
+ version: 2,
1079
+ recordedAt: input.recordedAt,
1080
+ session: {
1081
+ id: input.sessionId,
1082
+ startUrl: input.startUrl,
1083
+ endUrl: input.endUrl,
1084
+ targetId: input.targetId,
1085
+ profile: input.profile
1086
+ },
1087
+ recipe: {
1088
+ steps: input.steps
1089
+ },
1090
+ actions,
1091
+ screenshots,
1092
+ trace: {
1093
+ events: input.traceEvents,
1094
+ summaries: buildTraceSummaries(input.traceEvents)
1095
+ },
1096
+ assertions: input.assertions ?? [],
1097
+ notes: input.notes ?? [],
1098
+ artifacts: {
1099
+ recordingManifest: input.recordingManifest ?? "recording.json",
1100
+ screenshotDir: input.screenshotDir ?? "screenshots/"
1101
+ }
1102
+ };
1103
+ }
1104
+ function canonicalizeRecordingArtifact(value) {
1105
+ if (isCanonicalRecordingManifest(value)) {
1106
+ return value;
1107
+ }
1108
+ if (!isLegacyRecordingManifest(value)) {
1109
+ throw new Error("Unsupported recording artifact");
1110
+ }
1111
+ const traceEvents = buildTraceEventsFromLegacy(value);
1112
+ const steps = value.frames.map((frame) => frameToStep(frame));
1113
+ return createRecordingManifest({
1114
+ recordedAt: value.recordedAt,
1115
+ sessionId: value.sessionId,
1116
+ startUrl: value.startUrl,
1117
+ endUrl: value.endUrl,
1118
+ steps,
1119
+ frames: value.frames,
1120
+ traceEvents,
1121
+ notes: ["Converted from legacy recording manifest"]
1122
+ });
1123
+ }
1124
+ function buildTraceEventsFromLegacy(value) {
1125
+ const events = [];
1126
+ for (const frame of value.frames) {
1127
+ events.push({
1128
+ traceId: frame.actionId ?? `legacy-${frame.seq}`,
1129
+ sessionId: value.sessionId,
1130
+ ts: new Date(frame.timestamp).toISOString(),
1131
+ elapsedMs: frame.timestamp - new Date(value.recordedAt).getTime(),
1132
+ channel: "action",
1133
+ event: frame.success ? "action.succeeded" : "action.failed",
1134
+ severity: frame.success ? "info" : "error",
1135
+ summary: `${frame.action}${frame.selector ? ` ${frame.selector}` : ""}`,
1136
+ data: {
1137
+ action: frame.action,
1138
+ selector: frame.selector,
1139
+ value: frame.value ?? null,
1140
+ pageUrl: frame.pageUrl ?? null,
1141
+ pageTitle: frame.pageTitle ?? null,
1142
+ screenshot: frame.screenshot
1143
+ },
1144
+ actionId: frame.actionId ?? `action-${frame.seq}`,
1145
+ stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
1146
+ selector: frame.selector,
1147
+ selectorUsed: frame.selectorUsed ?? frame.selector,
1148
+ url: frame.pageUrl ?? frame.url
1149
+ });
1150
+ }
1151
+ return events;
1152
+ }
1153
+ function frameToStep(frame) {
1154
+ switch (frame.action) {
1155
+ case "fill":
1156
+ return { action: "fill", selector: frame.selector, value: frame.value };
1157
+ case "submit":
1158
+ return { action: "submit", selector: frame.selector };
1159
+ case "goto":
1160
+ return { action: "goto", url: frame.url ?? frame.pageUrl };
1161
+ case "press":
1162
+ return { action: "press", key: frame.value ?? "Enter" };
1163
+ default:
1164
+ return { action: "click", selector: frame.selector };
1165
+ }
1166
+ }
1167
+
1168
+ // src/trace/model.ts
1169
+ function createTraceId(prefix = "evt") {
1170
+ const random = Math.random().toString(36).slice(2, 10);
1171
+ return `${prefix}-${Date.now().toString(36)}-${random}`;
1172
+ }
1173
+ function normalizeTraceEvent(event) {
1174
+ return {
1175
+ traceId: event.traceId ?? createTraceId(event.channel),
1176
+ ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
1177
+ elapsedMs: event.elapsedMs ?? 0,
1178
+ severity: event.severity ?? inferSeverity(event.event),
1179
+ data: event.data ?? {},
1180
+ ...event
1181
+ };
1182
+ }
1183
+ function inferSeverity(eventName) {
1184
+ if (eventName.includes(".failed") || eventName.includes(".error") || eventName.includes("exception") || eventName.includes("notReady")) {
1185
+ return "error";
1186
+ }
1187
+ if (eventName.includes(".closed") || eventName.includes(".warn") || eventName.includes(".changed")) {
1188
+ return "warn";
1189
+ }
1190
+ return "info";
1191
+ }
1192
+
1193
+ // src/trace/script.ts
1194
+ var TRACE_BINDING_NAME = "__bpTraceBinding";
1195
+ var TRACE_SCRIPT = `
1196
+ (() => {
1197
+ if (window.__bpTraceInstalled) return;
1198
+ window.__bpTraceInstalled = true;
1199
+
1200
+ const binding = globalThis.${TRACE_BINDING_NAME};
1201
+ if (typeof binding !== 'function') return;
1202
+
1203
+ const emit = (event, data = {}, severity = 'info', summary) => {
1204
+ try {
1205
+ globalThis.__bpTraceRecentEvents = globalThis.__bpTraceRecentEvents || [];
1206
+ const payload = {
1207
+ event,
1208
+ severity,
1209
+ summary: summary || event,
1210
+ ts: Date.now(),
1211
+ data,
1212
+ };
1213
+ globalThis.__bpTraceRecentEvents.push(payload);
1214
+ if (globalThis.__bpTraceRecentEvents.length > 200) {
1215
+ globalThis.__bpTraceRecentEvents.splice(0, globalThis.__bpTraceRecentEvents.length - 200);
1216
+ }
1217
+ binding(JSON.stringify(payload));
1218
+ } catch {}
1219
+ };
1220
+
1221
+ const patchWebSocket = () => {
1222
+ const NativeWebSocket = window.WebSocket;
1223
+ if (typeof NativeWebSocket !== 'function' || window.__bpTraceWebSocketInstalled) return;
1224
+ window.__bpTraceWebSocketInstalled = true;
1225
+
1226
+ const nextId = () => Math.random().toString(36).slice(2, 10);
1227
+
1228
+ const patchInstance = (socket, urlValue) => {
1229
+ if (!socket || socket.__bpTracePatched) return socket;
1230
+ socket.__bpTracePatched = true;
1231
+ socket.__bpTraceId = socket.__bpTraceId || nextId();
1232
+ socket.__bpTraceUrl = String(urlValue || socket.url || '');
1233
+ globalThis.__bpTrackedWebSockets = globalThis.__bpTrackedWebSockets || new Set();
1234
+ globalThis.__bpTrackedWebSockets.add(socket);
1235
+
1236
+ emit(
1237
+ 'ws.connection.created',
1238
+ { connectionId: socket.__bpTraceId, url: socket.__bpTraceUrl },
1239
+ 'info',
1240
+ 'WebSocket opened ' + socket.__bpTraceUrl
1241
+ );
1242
+
1243
+ const originalSend = socket.send;
1244
+ socket.send = function(data) {
1245
+ const payload =
1246
+ typeof data === 'string'
1247
+ ? data
1248
+ : data && typeof data.toString === 'function'
1249
+ ? data.toString()
1250
+ : '[binary]';
1251
+ emit(
1252
+ 'ws.frame.sent',
1253
+ {
1254
+ connectionId: socket.__bpTraceId,
1255
+ url: socket.__bpTraceUrl,
1256
+ payload,
1257
+ length: payload.length,
1258
+ },
1259
+ 'info',
1260
+ 'WebSocket frame sent'
1261
+ );
1262
+ return originalSend.call(this, data);
1263
+ };
1264
+
1265
+ socket.addEventListener('message', (event) => {
1266
+ if (socket.__bpOfflineNotified || socket.__bpTraceClosed) {
1267
+ return;
1268
+ }
1269
+ const data = event && 'data' in event ? event.data : '';
1270
+ const payload =
1271
+ typeof data === 'string'
1272
+ ? data
1273
+ : data && typeof data.toString === 'function'
1274
+ ? data.toString()
1275
+ : '[binary]';
1276
+ emit(
1277
+ 'ws.frame.received',
1278
+ {
1279
+ connectionId: socket.__bpTraceId,
1280
+ url: socket.__bpTraceUrl,
1281
+ payload,
1282
+ length: payload.length,
1283
+ },
1284
+ 'info',
1285
+ 'WebSocket frame received'
1286
+ );
1287
+ });
1288
+
1289
+ socket.addEventListener('close', (event) => {
1290
+ if (socket.__bpTraceClosed) {
1291
+ return;
1292
+ }
1293
+ socket.__bpTraceClosed = true;
1294
+ try {
1295
+ globalThis.__bpTrackedWebSockets.delete(socket);
1296
+ } catch {}
1297
+ emit(
1298
+ 'ws.connection.closed',
1299
+ {
1300
+ connectionId: socket.__bpTraceId,
1301
+ url: socket.__bpTraceUrl,
1302
+ code: event.code,
1303
+ reason: event.reason,
1304
+ },
1305
+ 'warn',
1306
+ 'WebSocket closed'
1307
+ );
1308
+ });
1309
+
1310
+ return socket;
1311
+ };
1312
+
1313
+ const TracedWebSocket = function(url, protocols) {
1314
+ return arguments.length > 1
1315
+ ? patchInstance(new NativeWebSocket(url, protocols), url)
1316
+ : patchInstance(new NativeWebSocket(url), url);
1317
+ };
1318
+ TracedWebSocket.prototype = NativeWebSocket.prototype;
1319
+ Object.setPrototypeOf(TracedWebSocket, NativeWebSocket);
1320
+ window.WebSocket = TracedWebSocket;
1321
+ };
1322
+
1323
+ window.addEventListener('error', (errorEvent) => {
1324
+ emit(
1325
+ 'runtime.exception',
1326
+ {
1327
+ message: errorEvent.message,
1328
+ filename: errorEvent.filename,
1329
+ line: errorEvent.lineno,
1330
+ column: errorEvent.colno,
1331
+ },
1332
+ 'error',
1333
+ errorEvent.message || 'Uncaught error'
1334
+ );
1335
+ });
1336
+
1337
+ window.addEventListener('unhandledrejection', (event) => {
1338
+ const reason = event && 'reason' in event ? String(event.reason) : 'Unhandled rejection';
1339
+ emit('runtime.unhandledRejection', { reason }, 'error', reason);
1340
+ });
1341
+
1342
+ const patchPermissions = async () => {
1343
+ if (!navigator.permissions || !navigator.permissions.query) return;
1344
+
1345
+ const names = ['geolocation', 'microphone', 'camera', 'notifications'];
1346
+ for (const name of names) {
1347
+ try {
1348
+ const status = await navigator.permissions.query({ name });
1349
+ emit(
1350
+ 'permission.state',
1351
+ { name, state: status.state },
1352
+ status.state === 'denied' ? 'warn' : 'info',
1353
+ name + ': ' + status.state
1354
+ );
1355
+ status.addEventListener('change', () => {
1356
+ emit(
1357
+ 'permission.changed',
1358
+ { name, state: status.state },
1359
+ status.state === 'denied' ? 'warn' : 'info',
1360
+ name + ': ' + status.state
1361
+ );
1362
+ });
1363
+ } catch {}
1364
+ }
1365
+ };
1366
+
1367
+ const patchMediaElement = (element) => {
1368
+ if (!element || element.__bpTracePatched) return;
1369
+ element.__bpTracePatched = true;
1370
+
1371
+ element.addEventListener('play', () => {
1372
+ emit(
1373
+ 'media.playback.started',
1374
+ { tag: element.tagName.toLowerCase(), src: element.currentSrc || element.src || null },
1375
+ 'info',
1376
+ 'Media playback started'
1377
+ );
1378
+ });
1379
+
1380
+ const onStop = () => {
1381
+ emit(
1382
+ 'media.playback.stopped',
1383
+ { tag: element.tagName.toLowerCase(), src: element.currentSrc || element.src || null },
1384
+ 'warn',
1385
+ 'Media playback stopped'
1386
+ );
1387
+ };
1388
+
1389
+ element.addEventListener('pause', onStop);
1390
+ element.addEventListener('ended', onStop);
1391
+ };
1392
+
1393
+ const patchMediaElements = () => {
1394
+ document.querySelectorAll('audio,video').forEach(patchMediaElement);
1395
+ };
1396
+
1397
+ patchMediaElements();
1398
+ patchWebSocket();
1399
+
1400
+ if (document.documentElement) {
1401
+ const observer = new MutationObserver(() => {
1402
+ patchMediaElements();
1403
+ });
1404
+ observer.observe(document.documentElement, { childList: true, subtree: true });
1405
+ }
1406
+
1407
+ if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
1408
+ const original = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
1409
+ navigator.mediaDevices.getUserMedia = async (...args) => {
1410
+ emit('voice.capture.started', { constraints: args[0] || null }, 'info', 'Voice capture started');
1411
+ try {
1412
+ const stream = await original(...args);
1413
+ const tracks = stream.getTracks();
1414
+
1415
+ for (const track of tracks) {
1416
+ emit(
1417
+ 'media.track.started',
1418
+ { kind: track.kind, label: track.label, readyState: track.readyState },
1419
+ 'info',
1420
+ track.kind + ' track started'
1421
+ );
1422
+ track.addEventListener('ended', () => {
1423
+ emit(
1424
+ 'media.track.ended',
1425
+ { kind: track.kind, label: track.label, readyState: track.readyState },
1426
+ 'warn',
1427
+ track.kind + ' track ended'
1428
+ );
1429
+ emit(
1430
+ 'voice.capture.stopped',
1431
+ { kind: track.kind, label: track.label, readyState: track.readyState },
1432
+ 'warn',
1433
+ 'Voice capture stopped'
1434
+ );
1435
+ });
1436
+ }
1437
+
1438
+ emit(
1439
+ 'voice.capture.detectedAudio',
1440
+ { trackCount: tracks.length, kinds: tracks.map((track) => track.kind) },
1441
+ 'info',
1442
+ 'Voice capture detected audio'
1443
+ );
1444
+
1445
+ return stream;
1446
+ } catch (error) {
1447
+ emit(
1448
+ 'voice.pipeline.notReady',
1449
+ { message: String(error && error.message ? error.message : error) },
1450
+ 'error',
1451
+ String(error && error.message ? error.message : error)
1452
+ );
1453
+ throw error;
1454
+ }
1455
+ };
1456
+ }
1457
+
1458
+ document.addEventListener('visibilitychange', () => {
1459
+ emit(
1460
+ 'dom.state.changed',
1461
+ { visibilityState: document.visibilityState },
1462
+ document.visibilityState === 'hidden' ? 'warn' : 'info',
1463
+ 'Visibility ' + document.visibilityState
1464
+ );
1465
+ });
1466
+
1467
+ patchPermissions();
1468
+ emit('voice.pipeline.ready', { url: location.href }, 'info', 'Trace hooks ready');
1469
+ })();
1470
+ `;
1471
+
1472
+ // src/trace/live.ts
1473
+ function globToRegex(pattern) {
1474
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
1475
+ const withWildcards = escaped.replace(/\*/g, ".*");
1476
+ return new RegExp(`^${withWildcards}$`);
1477
+ }
1478
+ function readString(value) {
1479
+ return typeof value === "string" ? value : void 0;
1480
+ }
1481
+ function readStringOr(value, fallback = "") {
1482
+ return readString(value) ?? fallback;
1483
+ }
1484
+ function formatConsoleArg(entry) {
1485
+ return readString(entry["value"]) ?? readString(entry["description"]) ?? "";
1486
+ }
1487
+ var LiveTraceCollector = class {
1488
+ cdp;
1489
+ options;
1490
+ handlers = [];
1491
+ wsUrls = /* @__PURE__ */ new Map();
1492
+ httpUrls = /* @__PURE__ */ new Map();
1493
+ events = [];
1494
+ startTime = Date.now();
1495
+ matchRegex;
1496
+ constructor(cdp, options = {}) {
1497
+ this.cdp = cdp;
1498
+ this.options = options;
1499
+ this.matchRegex = options.match ? globToRegex(options.match) : null;
1500
+ }
1501
+ async start() {
1502
+ await this.cdp.send("Runtime.enable");
1503
+ await this.cdp.send("Page.enable");
1504
+ await this.cdp.send("Network.enable");
1505
+ await this.cdp.send("Runtime.addBinding", { name: TRACE_BINDING_NAME });
1506
+ await this.cdp.send("Page.addScriptToEvaluateOnNewDocument", { source: TRACE_SCRIPT });
1507
+ await this.cdp.send("Runtime.evaluate", { expression: TRACE_SCRIPT, awaitPromise: false });
1508
+ if ((this.options.mode ?? "all") !== "http") {
1509
+ this.subscribe("Network.webSocketCreated", (params) => {
1510
+ const requestId = readStringOr(params["requestId"]);
1511
+ const url = readStringOr(params["url"]);
1512
+ if (!this.matchesUrl(url)) {
1513
+ return;
1514
+ }
1515
+ this.wsUrls.set(requestId, url);
1516
+ void this.emit({
1517
+ channel: "ws",
1518
+ event: "ws.connection.created",
1519
+ summary: `WebSocket opened ${url}`,
1520
+ connectionId: requestId,
1521
+ requestId,
1522
+ url,
1523
+ data: { url }
1524
+ });
1525
+ });
1526
+ this.subscribe("Network.webSocketFrameSent", (params) => {
1527
+ const requestId = readStringOr(params["requestId"]);
1528
+ const response = params["response"];
1529
+ const payload = this.formatPayload(response?.payloadData, response?.opcode ?? 1);
1530
+ const url = this.wsUrls.get(requestId);
1531
+ if (this.matchRegex && !this.matchRegex.test(url ?? "") && !this.matchRegex.test(payload)) {
1532
+ return;
1533
+ }
1534
+ void this.emit({
1535
+ channel: "ws",
1536
+ event: "ws.frame.sent",
1537
+ summary: `WebSocket frame sent ${requestId}`,
1538
+ connectionId: requestId,
1539
+ requestId,
1540
+ url,
1541
+ data: {
1542
+ opcode: response?.opcode ?? 1,
1543
+ payload,
1544
+ length: response?.payloadData?.length ?? 0
1545
+ }
1546
+ });
1547
+ });
1548
+ this.subscribe("Network.webSocketFrameReceived", (params) => {
1549
+ const requestId = readStringOr(params["requestId"]);
1550
+ const response = params["response"];
1551
+ const payload = this.formatPayload(response?.payloadData, response?.opcode ?? 1);
1552
+ const url = this.wsUrls.get(requestId);
1553
+ if (this.matchRegex && !this.matchRegex.test(url ?? "") && !this.matchRegex.test(payload)) {
1554
+ return;
1555
+ }
1556
+ void this.emit({
1557
+ channel: "ws",
1558
+ event: "ws.frame.received",
1559
+ summary: `WebSocket frame received ${requestId}`,
1560
+ connectionId: requestId,
1561
+ requestId,
1562
+ url,
1563
+ data: {
1564
+ opcode: response?.opcode ?? 1,
1565
+ payload,
1566
+ length: response?.payloadData?.length ?? 0
1567
+ }
1568
+ });
1569
+ });
1570
+ this.subscribe("Network.webSocketClosed", (params) => {
1571
+ const requestId = readStringOr(params["requestId"]);
1572
+ const url = this.wsUrls.get(requestId);
1573
+ this.wsUrls.delete(requestId);
1574
+ void this.emit({
1575
+ channel: "ws",
1576
+ event: "ws.connection.closed",
1577
+ summary: `WebSocket closed ${requestId}`,
1578
+ severity: "warn",
1579
+ connectionId: requestId,
1580
+ requestId,
1581
+ url,
1582
+ data: { url }
1583
+ });
1584
+ });
1585
+ }
1586
+ if ((this.options.mode ?? "all") !== "ws") {
1587
+ this.subscribe("Network.requestWillBeSent", (params) => {
1588
+ const request = params["request"];
1589
+ const requestId = readStringOr(params["requestId"]);
1590
+ const url = request?.url ?? "";
1591
+ if (!this.matchesUrl(url)) {
1592
+ return;
1593
+ }
1594
+ this.httpUrls.set(requestId, url);
1595
+ void this.emit({
1596
+ channel: "http",
1597
+ event: "http.request.sent",
1598
+ summary: `${request?.method ?? "GET"} ${url}`,
1599
+ requestId,
1600
+ url,
1601
+ data: {
1602
+ method: request?.method ?? "GET",
1603
+ headers: request?.headers ?? {},
1604
+ body: request?.postData ?? null
1605
+ }
1606
+ });
1607
+ });
1608
+ this.subscribe("Network.responseReceived", (params) => {
1609
+ const requestId = readStringOr(params["requestId"]);
1610
+ if (!this.httpUrls.has(requestId)) {
1611
+ return;
1612
+ }
1613
+ const response = params["response"];
1614
+ void this.emit({
1615
+ channel: "http",
1616
+ event: "http.response.received",
1617
+ summary: `${response?.status ?? 0} ${response?.url ?? this.httpUrls.get(requestId) ?? ""}`,
1618
+ requestId,
1619
+ url: response?.url ?? this.httpUrls.get(requestId),
1620
+ data: {
1621
+ status: response?.status ?? 0,
1622
+ headers: response?.headers ?? {},
1623
+ mimeType: response?.mimeType ?? null
1624
+ }
1625
+ });
1626
+ });
1627
+ this.subscribe("Network.loadingFailed", (params) => {
1628
+ const requestId = readStringOr(params["requestId"]);
1629
+ const url = readString(params["blockedReason"]) ?? this.httpUrls.get(requestId) ?? "";
1630
+ void this.emit({
1631
+ channel: "http",
1632
+ event: "http.response.failed",
1633
+ summary: `HTTP request failed ${requestId}`,
1634
+ severity: "error",
1635
+ requestId,
1636
+ url,
1637
+ data: {
1638
+ errorText: params["errorText"] ?? null,
1639
+ blockedReason: params["blockedReason"] ?? null,
1640
+ canceled: params["canceled"] ?? false
1641
+ }
1642
+ });
1643
+ });
1644
+ }
1645
+ this.subscribe("Runtime.consoleAPICalled", (params) => {
1646
+ const type = readStringOr(params["type"], "log");
1647
+ if (type !== "log" && type !== "warn" && type !== "error") {
1648
+ return;
1649
+ }
1650
+ const args = Array.isArray(params["args"]) ? params["args"] : [];
1651
+ const text = args.map(formatConsoleArg).filter(Boolean).join(" ");
1652
+ void this.emit({
1653
+ channel: "console",
1654
+ event: `console.${type}`,
1655
+ severity: type === "error" ? "error" : type === "warn" ? "warn" : "info",
1656
+ summary: text || `console.${type}`,
1657
+ data: { args }
1658
+ });
1659
+ });
1660
+ this.subscribe("Runtime.exceptionThrown", (params) => {
1661
+ const details = params["exceptionDetails"] ?? {};
1662
+ const text = readString(details["text"]) ?? "Runtime exception";
1663
+ void this.emit({
1664
+ channel: "runtime",
1665
+ event: "runtime.exception",
1666
+ severity: "error",
1667
+ summary: text,
1668
+ data: details
1669
+ });
1670
+ });
1671
+ this.subscribe("Runtime.bindingCalled", (params) => {
1672
+ if (params["name"] !== TRACE_BINDING_NAME) {
1673
+ return;
1674
+ }
1675
+ const raw = readStringOr(params["payload"]);
1676
+ try {
1677
+ const payload = JSON.parse(raw);
1678
+ const channel = this.channelForTraceEvent(payload.event);
1679
+ void this.emit({
1680
+ channel,
1681
+ event: payload.event,
1682
+ severity: payload.severity,
1683
+ summary: payload.summary ?? payload.event,
1684
+ ts: payload.ts ? new Date(payload.ts).toISOString() : void 0,
1685
+ data: payload.data ?? {},
1686
+ url: readString(payload.data?.["url"])
1687
+ });
1688
+ } catch {
1689
+ }
1690
+ });
1691
+ }
1692
+ async stop() {
1693
+ for (const { event, handler } of this.handlers) {
1694
+ this.cdp.off(event, handler);
1695
+ }
1696
+ this.handlers.length = 0;
1697
+ return [...this.events];
1698
+ }
1699
+ getEvents() {
1700
+ return [...this.events];
1701
+ }
1702
+ subscribe(event, handler) {
1703
+ this.cdp.on(event, handler);
1704
+ this.handlers.push({ event, handler });
1705
+ }
1706
+ matchesUrl(url) {
1707
+ if (!this.matchRegex) {
1708
+ return true;
1709
+ }
1710
+ return this.matchRegex.test(url);
1711
+ }
1712
+ formatPayload(payloadData, opcode) {
1713
+ const data = payloadData ?? "";
1714
+ const maxPayload = this.options.maxPayload ?? 256;
1715
+ if (opcode === 2) {
1716
+ const byteLength = Math.floor(data.length * 3 / 4);
1717
+ return `[binary: ${byteLength} bytes]`;
1718
+ }
1719
+ if (data.length > maxPayload) {
1720
+ return `${data.slice(0, maxPayload)}... [truncated, ${data.length} total]`;
1721
+ }
1722
+ return data;
1723
+ }
1724
+ channelForTraceEvent(eventName) {
1725
+ if (eventName.startsWith("ws.")) return "ws";
1726
+ if (eventName.startsWith("http.")) return "http";
1727
+ if (eventName.startsWith("console.")) return "console";
1728
+ if (eventName.startsWith("permission.")) return "permission";
1729
+ if (eventName.startsWith("media.")) return "media";
1730
+ if (eventName.startsWith("voice.")) return "voice";
1731
+ if (eventName.startsWith("dom.")) return "dom";
1732
+ if (eventName.startsWith("runtime.")) return "runtime";
1733
+ return "session";
1734
+ }
1735
+ async emit(event) {
1736
+ const normalized = normalizeTraceEvent({
1737
+ traceId: event.traceId ?? createTraceId(event.channel),
1738
+ sessionId: this.options.sessionId,
1739
+ targetId: this.options.targetId,
1740
+ elapsedMs: event.elapsedMs ?? Date.now() - this.startTime,
1741
+ ...event
1742
+ });
1743
+ this.events.push(normalized);
1744
+ await this.options.onEvent?.(normalized);
1745
+ }
1746
+ };
1747
+
1748
+ // src/actions/executor.ts
1749
+ var DEFAULT_TIMEOUT = 3e4;
1750
+ var DEFAULT_RECORDING_SKIP_ACTIONS = [
1751
+ "wait",
1752
+ "snapshot",
1753
+ "forms",
1754
+ "text",
1755
+ "screenshot"
1756
+ ];
1757
+ function readString2(value) {
1758
+ return typeof value === "string" ? value : void 0;
1759
+ }
1760
+ function readStringOr2(value, fallback = "") {
1761
+ return readString2(value) ?? fallback;
1762
+ }
1763
+ function formatConsoleArg2(entry) {
1764
+ return readString2(entry["value"]) ?? readString2(entry["description"]) ?? "";
1765
+ }
1766
+ function loadExistingRecording(manifestPath) {
1767
+ try {
1768
+ const raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
1769
+ if (raw.version === 1) {
1770
+ const legacy = raw;
1771
+ return {
1772
+ frames: Array.isArray(legacy.frames) ? legacy.frames : [],
1773
+ traceEvents: [],
1774
+ recordedAt: legacy.recordedAt,
1775
+ startUrl: legacy.startUrl
1776
+ };
1777
+ }
1778
+ const artifact = canonicalizeRecordingArtifact(raw);
1779
+ const screenshotsByAction = new Map(artifact.screenshots.map((shot) => [shot.actionId, shot]));
1780
+ const frames = artifact.actions.map((action, index) => {
1781
+ const screenshot = screenshotsByAction.get(action.id);
1782
+ return {
1783
+ seq: index + 1,
1784
+ timestamp: Date.parse(action.ts),
1785
+ action: action.action,
1786
+ selector: action.selector,
1787
+ selectorUsed: action.selectorUsed,
1788
+ value: action.value,
1789
+ url: action.url,
1790
+ coordinates: action.coordinates,
1791
+ boundingBox: action.boundingBox,
1792
+ success: action.success,
1793
+ durationMs: action.durationMs,
1794
+ error: action.error,
1795
+ screenshot: screenshot?.file ?? "",
1796
+ pageUrl: action.pageUrl,
1797
+ pageTitle: action.pageTitle,
1798
+ stepIndex: action.stepIndex,
1799
+ actionId: action.id
1800
+ };
1801
+ });
1802
+ return {
1803
+ frames,
1804
+ traceEvents: artifact.trace.events,
1805
+ recordedAt: artifact.recordedAt,
1806
+ startUrl: artifact.session.startUrl
1807
+ };
1808
+ } catch {
1809
+ return { frames: [], traceEvents: [] };
1810
+ }
1811
+ }
1070
1812
  function classifyFailure(error) {
1071
1813
  if (error instanceof ElementNotFoundError) {
1072
1814
  return { reason: "missing" };
@@ -1147,6 +1889,9 @@ var BatchExecutor = class {
1147
1889
  const results = [];
1148
1890
  const startTime = Date.now();
1149
1891
  const recording = options.record ? this.createRecordingContext(options.record) : null;
1892
+ if (steps.some((step) => step.action === "waitForWsMessage")) {
1893
+ await this.ensureTraceHooks();
1894
+ }
1150
1895
  const startUrl = recording ? await this.getPageUrlSafe() : "";
1151
1896
  let stoppedAtIndex;
1152
1897
  for (let i = 0; i < steps.length; i++) {
@@ -1156,6 +1901,26 @@ var BatchExecutor = class {
1156
1901
  const retryDelay = step.retryDelay ?? 500;
1157
1902
  let lastError;
1158
1903
  let succeeded = false;
1904
+ if (recording) {
1905
+ recording.traceEvents.push(
1906
+ normalizeTraceEvent({
1907
+ traceId: createTraceId("action"),
1908
+ elapsedMs: Date.now() - startTime,
1909
+ channel: "action",
1910
+ event: "action.started",
1911
+ summary: `${step.action}${step.selector ? ` ${Array.isArray(step.selector) ? step.selector[0] : step.selector}` : ""}`,
1912
+ data: {
1913
+ action: step.action,
1914
+ selector: step.selector ?? null,
1915
+ url: step.url ?? null
1916
+ },
1917
+ actionId: `action-${i + 1}`,
1918
+ stepIndex: i,
1919
+ selector: step.selector,
1920
+ url: step.url
1921
+ })
1922
+ );
1923
+ }
1159
1924
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
1160
1925
  if (attempt > 0) {
1161
1926
  await new Promise((resolve) => setTimeout(resolve, retryDelay));
@@ -1179,6 +1944,28 @@ var BatchExecutor = class {
1179
1944
  if (recording && !recording.skipActions.has(step.action)) {
1180
1945
  await this.captureRecordingFrame(step, stepResult, recording);
1181
1946
  }
1947
+ if (recording) {
1948
+ recording.traceEvents.push(
1949
+ normalizeTraceEvent({
1950
+ traceId: createTraceId("action"),
1951
+ elapsedMs: Date.now() - startTime,
1952
+ channel: "action",
1953
+ event: "action.succeeded",
1954
+ summary: `${step.action} succeeded`,
1955
+ data: {
1956
+ action: step.action,
1957
+ selector: step.selector ?? null,
1958
+ selectorUsed: result.selectorUsed ?? null,
1959
+ durationMs: Date.now() - stepStart
1960
+ },
1961
+ actionId: `action-${i + 1}`,
1962
+ stepIndex: i,
1963
+ selector: step.selector,
1964
+ selectorUsed: result.selectorUsed,
1965
+ url: step.url
1966
+ })
1967
+ );
1968
+ }
1182
1969
  results.push(stepResult);
1183
1970
  succeeded = true;
1184
1971
  break;
@@ -1216,6 +2003,28 @@ var BatchExecutor = class {
1216
2003
  if (recording && !recording.skipActions.has(step.action)) {
1217
2004
  await this.captureRecordingFrame(step, failedResult, recording);
1218
2005
  }
2006
+ if (recording) {
2007
+ recording.traceEvents.push(
2008
+ normalizeTraceEvent({
2009
+ traceId: createTraceId("action"),
2010
+ elapsedMs: Date.now() - startTime,
2011
+ channel: "action",
2012
+ event: "action.failed",
2013
+ severity: "error",
2014
+ summary: `${step.action} failed: ${errorMessage}`,
2015
+ data: {
2016
+ action: step.action,
2017
+ selector: step.selector ?? null,
2018
+ error: errorMessage,
2019
+ reason
2020
+ },
2021
+ actionId: `action-${i + 1}`,
2022
+ stepIndex: i,
2023
+ selector: step.selector,
2024
+ url: step.url
2025
+ })
2026
+ );
2027
+ }
1219
2028
  results.push(failedResult);
1220
2029
  if (onFail === "stop" && !step.optional) {
1221
2030
  stoppedAtIndex = i;
@@ -1231,7 +2040,8 @@ var BatchExecutor = class {
1231
2040
  recording,
1232
2041
  startTime,
1233
2042
  startUrl,
1234
- allSuccess
2043
+ allSuccess,
2044
+ steps
1235
2045
  );
1236
2046
  }
1237
2047
  return {
@@ -1246,20 +2056,14 @@ var BatchExecutor = class {
1246
2056
  const baseDir = record.outputDir ?? join(process.cwd(), ".browser-pilot");
1247
2057
  const screenshotDir = join(baseDir, "screenshots");
1248
2058
  const manifestPath = join(baseDir, "recording.json");
1249
- let existingFrames = [];
1250
- try {
1251
- const existing = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
1252
- if (existing.frames && Array.isArray(existing.frames)) {
1253
- existingFrames = existing.frames;
1254
- }
1255
- } catch {
1256
- }
2059
+ const existing = loadExistingRecording(manifestPath);
1257
2060
  fs.mkdirSync(screenshotDir, { recursive: true });
1258
2061
  return {
1259
2062
  baseDir,
1260
2063
  screenshotDir,
1261
2064
  sessionId: record.sessionId ?? this.page.targetId,
1262
- frames: existingFrames,
2065
+ frames: existing.frames,
2066
+ traceEvents: existing.traceEvents,
1263
2067
  format: record.format ?? "webp",
1264
2068
  quality: Math.max(0, Math.min(100, record.quality ?? 40)),
1265
2069
  highlights: record.highlights !== false,
@@ -1315,6 +2119,7 @@ var BatchExecutor = class {
1315
2119
  timestamp: ts,
1316
2120
  action: stepResult.action,
1317
2121
  selector: stepResult.selectorUsed ?? (Array.isArray(step.selector) ? step.selector[0] : step.selector),
2122
+ selectorUsed: stepResult.selectorUsed,
1318
2123
  value: redactValueForRecording(
1319
2124
  typeof step.value === "string" ? step.value : void 0,
1320
2125
  targetMetadata
@@ -1327,7 +2132,9 @@ var BatchExecutor = class {
1327
2132
  error: stepResult.error,
1328
2133
  screenshot: filename,
1329
2134
  pageUrl,
1330
- pageTitle
2135
+ pageTitle,
2136
+ stepIndex: stepResult.index,
2137
+ actionId: `action-${stepResult.index + 1}`
1331
2138
  });
1332
2139
  } catch {
1333
2140
  } finally {
@@ -1339,45 +2146,31 @@ var BatchExecutor = class {
1339
2146
  /**
1340
2147
  * Write recording manifest to disk
1341
2148
  */
1342
- async writeRecordingManifest(recording, startTime, startUrl, success) {
2149
+ async writeRecordingManifest(recording, startTime, startUrl, success, steps) {
1343
2150
  let endUrl = startUrl;
1344
- let viewport = { width: 1280, height: 720 };
1345
2151
  try {
1346
2152
  endUrl = await this.page.url();
1347
2153
  } catch {
1348
2154
  }
1349
- try {
1350
- const metrics = await this.page.cdpClient.send("Page.getLayoutMetrics");
1351
- viewport = {
1352
- width: metrics.cssVisualViewport.clientWidth,
1353
- height: metrics.cssVisualViewport.clientHeight
1354
- };
1355
- } catch {
1356
- }
1357
2155
  const manifestPath = join(recording.baseDir, "recording.json");
1358
2156
  let recordedAt = new Date(startTime).toISOString();
1359
2157
  let originalStartUrl = startUrl;
1360
- try {
1361
- const existing = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
1362
- if (existing.recordedAt) recordedAt = existing.recordedAt;
1363
- if (existing.startUrl) originalStartUrl = existing.startUrl;
1364
- } catch {
1365
- }
1366
- const firstFrameTime = recording.frames[0]?.timestamp ?? startTime;
1367
- const totalDurationMs = Date.now() - Math.min(firstFrameTime, startTime);
1368
- const manifest = {
1369
- version: 1,
2158
+ const existing = loadExistingRecording(manifestPath);
2159
+ if (existing.recordedAt) recordedAt = existing.recordedAt;
2160
+ if (existing.startUrl) originalStartUrl = existing.startUrl;
2161
+ const manifest = createRecordingManifest({
1370
2162
  recordedAt,
1371
2163
  sessionId: recording.sessionId,
1372
2164
  startUrl: originalStartUrl,
1373
2165
  endUrl,
1374
- viewport,
1375
- format: recording.format,
1376
- quality: recording.quality,
1377
- totalDurationMs,
1378
- success,
1379
- frames: recording.frames
1380
- };
2166
+ targetId: this.page.targetId,
2167
+ steps,
2168
+ frames: recording.frames,
2169
+ traceEvents: recording.traceEvents,
2170
+ notes: success ? [] : ["Replay ended with at least one failed action."],
2171
+ recordingManifest: "recording.json",
2172
+ screenshotDir: "screenshots/"
2173
+ });
1381
2174
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
1382
2175
  return manifestPath;
1383
2176
  }
@@ -1660,6 +2453,39 @@ var BatchExecutor = class {
1660
2453
  }
1661
2454
  return { selectorUsed: usedSelector, value: actual };
1662
2455
  }
2456
+ case "waitForWsMessage": {
2457
+ if (typeof step.match !== "string") {
2458
+ throw new Error("waitForWsMessage requires match");
2459
+ }
2460
+ const message = await this.waitForWsMessage(step.match, step.where, timeout);
2461
+ return { value: message };
2462
+ }
2463
+ case "assertNoConsoleErrors": {
2464
+ await this.assertNoConsoleErrors(step.windowMs ?? timeout);
2465
+ return {};
2466
+ }
2467
+ case "assertTextChanged": {
2468
+ const selector = Array.isArray(step.selector) ? step.selector[0] : step.selector;
2469
+ if (typeof step.to !== "string") {
2470
+ throw new Error("assertTextChanged requires to");
2471
+ }
2472
+ const text = await this.assertTextChanged(selector, step.from, step.to, timeout);
2473
+ return { selectorUsed: selector, text };
2474
+ }
2475
+ case "assertPermission": {
2476
+ if (!step.name || !step.state) {
2477
+ throw new Error("assertPermission requires name and state");
2478
+ }
2479
+ const permission = await this.assertPermission(step.name, step.state);
2480
+ return { value: permission };
2481
+ }
2482
+ case "assertMediaTrackLive": {
2483
+ if (!step.kind) {
2484
+ throw new Error("assertMediaTrackLive requires kind");
2485
+ }
2486
+ const media = await this.assertMediaTrackLive(step.kind);
2487
+ return { value: media };
2488
+ }
1663
2489
  default: {
1664
2490
  const action = step.action;
1665
2491
  const aliases = {
@@ -1713,7 +2539,7 @@ var BatchExecutor = class {
1713
2539
  };
1714
2540
  const suggestion = aliases[action.toLowerCase()];
1715
2541
  const hint = suggestion ? ` Did you mean "${suggestion}"?` : "";
1716
- 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";
2542
+ 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";
1717
2543
  throw new Error(`Unknown action "${action}".${hint}
1718
2544
 
1719
2545
  Valid actions: ${valid}`);
@@ -1729,6 +2555,237 @@ Valid actions: ${valid}`);
1729
2555
  if (matched) return matched;
1730
2556
  return Array.isArray(selector) ? selector[0] : selector;
1731
2557
  }
2558
+ async ensureTraceHooks() {
2559
+ await this.page.cdpClient.send("Runtime.enable");
2560
+ await this.page.cdpClient.send("Page.enable");
2561
+ await this.page.cdpClient.send("Network.enable");
2562
+ try {
2563
+ await this.page.cdpClient.send("Runtime.addBinding", { name: TRACE_BINDING_NAME });
2564
+ } catch {
2565
+ }
2566
+ await this.page.cdpClient.send("Page.addScriptToEvaluateOnNewDocument", {
2567
+ source: TRACE_SCRIPT
2568
+ });
2569
+ await this.page.cdpClient.send("Runtime.evaluate", {
2570
+ expression: TRACE_SCRIPT,
2571
+ awaitPromise: false
2572
+ });
2573
+ }
2574
+ async waitForWsMessage(match, where, timeout) {
2575
+ await this.ensureTraceHooks();
2576
+ const regex = globToRegex(match);
2577
+ const wsUrls = /* @__PURE__ */ new Map();
2578
+ const recentMatch = await this.findRecentWsMessage(regex, where);
2579
+ if (recentMatch) {
2580
+ return recentMatch;
2581
+ }
2582
+ return new Promise((resolve, reject) => {
2583
+ const cleanup = () => {
2584
+ this.page.cdpClient.off("Network.webSocketCreated", onCreated);
2585
+ this.page.cdpClient.off("Network.webSocketFrameReceived", onFrame);
2586
+ this.page.cdpClient.off("Runtime.bindingCalled", onBinding);
2587
+ clearTimeout(timer);
2588
+ };
2589
+ const onCreated = (params) => {
2590
+ wsUrls.set(readStringOr2(params["requestId"]), readStringOr2(params["url"]));
2591
+ };
2592
+ const onFrame = (params) => {
2593
+ const requestId = readStringOr2(params["requestId"]);
2594
+ const response = params["response"] ?? {};
2595
+ const payload = response.payloadData ?? "";
2596
+ const url = wsUrls.get(requestId) ?? "";
2597
+ if (!regex.test(url) && !regex.test(payload)) {
2598
+ return;
2599
+ }
2600
+ if (where && !this.payloadMatchesWhere(payload, where)) {
2601
+ return;
2602
+ }
2603
+ cleanup();
2604
+ resolve({ requestId, url, payload });
2605
+ };
2606
+ const onBinding = (params) => {
2607
+ if (params["name"] !== TRACE_BINDING_NAME) {
2608
+ return;
2609
+ }
2610
+ try {
2611
+ const parsed = JSON.parse(readStringOr2(params["payload"]));
2612
+ if (parsed.event !== "ws.frame.received") {
2613
+ return;
2614
+ }
2615
+ const data = parsed.data ?? {};
2616
+ const payload = readStringOr2(data["payload"]);
2617
+ const url = readStringOr2(data["url"]);
2618
+ if (!regex.test(url) && !regex.test(payload)) {
2619
+ return;
2620
+ }
2621
+ if (where && !this.payloadMatchesWhere(payload, where)) {
2622
+ return;
2623
+ }
2624
+ cleanup();
2625
+ resolve({
2626
+ requestId: readStringOr2(data["connectionId"]),
2627
+ url,
2628
+ payload
2629
+ });
2630
+ } catch {
2631
+ }
2632
+ };
2633
+ const timer = setTimeout(() => {
2634
+ cleanup();
2635
+ reject(new Error(`Timed out waiting for WebSocket message matching ${match}`));
2636
+ }, timeout);
2637
+ this.page.cdpClient.on("Network.webSocketCreated", onCreated);
2638
+ this.page.cdpClient.on("Network.webSocketFrameReceived", onFrame);
2639
+ this.page.cdpClient.on("Runtime.bindingCalled", onBinding);
2640
+ });
2641
+ }
2642
+ payloadMatchesWhere(payload, where) {
2643
+ try {
2644
+ const parsed = JSON.parse(payload);
2645
+ return Object.entries(where).every(([key, expected]) => {
2646
+ const actual = key.split(".").reduce((current, part) => {
2647
+ if (!current || typeof current !== "object") {
2648
+ return void 0;
2649
+ }
2650
+ return current[part];
2651
+ }, parsed);
2652
+ return actual === expected;
2653
+ });
2654
+ } catch {
2655
+ return false;
2656
+ }
2657
+ }
2658
+ async findRecentWsMessage(regex, where) {
2659
+ const recent = await this.page.evaluate(
2660
+ "(() => Array.isArray(globalThis.__bpTraceRecentEvents) ? globalThis.__bpTraceRecentEvents : [])()"
2661
+ );
2662
+ if (!Array.isArray(recent)) {
2663
+ return null;
2664
+ }
2665
+ for (let i = recent.length - 1; i >= 0; i--) {
2666
+ const entry = recent[i];
2667
+ if (!entry || typeof entry !== "object") {
2668
+ continue;
2669
+ }
2670
+ const record = entry;
2671
+ const event = readStringOr2(record["event"]);
2672
+ if (event !== "ws.frame.received") {
2673
+ continue;
2674
+ }
2675
+ const data = record["data"] ?? {};
2676
+ const payload = readStringOr2(data["payload"]);
2677
+ const url = readStringOr2(data["url"]);
2678
+ if (!regex.test(url) && !regex.test(payload)) {
2679
+ continue;
2680
+ }
2681
+ if (where && !this.payloadMatchesWhere(payload, where)) {
2682
+ continue;
2683
+ }
2684
+ return {
2685
+ requestId: readStringOr2(data["connectionId"]),
2686
+ url,
2687
+ payload
2688
+ };
2689
+ }
2690
+ return null;
2691
+ }
2692
+ async assertNoConsoleErrors(windowMs) {
2693
+ await this.page.cdpClient.send("Runtime.enable");
2694
+ return new Promise((resolve, reject) => {
2695
+ const errors = [];
2696
+ const cleanup = () => {
2697
+ this.page.cdpClient.off("Runtime.consoleAPICalled", onConsole);
2698
+ this.page.cdpClient.off("Runtime.exceptionThrown", onException);
2699
+ clearTimeout(timer);
2700
+ };
2701
+ const onConsole = (params) => {
2702
+ if (params["type"] !== "error") {
2703
+ return;
2704
+ }
2705
+ const args = Array.isArray(params["args"]) ? params["args"] : [];
2706
+ errors.push(args.map(formatConsoleArg2).filter(Boolean).join(" "));
2707
+ };
2708
+ const onException = (params) => {
2709
+ const details = params["exceptionDetails"] ?? {};
2710
+ errors.push(readString2(details["text"]) ?? "Runtime exception");
2711
+ };
2712
+ const timer = setTimeout(() => {
2713
+ cleanup();
2714
+ if (errors.length > 0) {
2715
+ reject(new Error(`Console errors detected: ${errors.join(" | ")}`));
2716
+ return;
2717
+ }
2718
+ resolve();
2719
+ }, windowMs);
2720
+ this.page.cdpClient.on("Runtime.consoleAPICalled", onConsole);
2721
+ this.page.cdpClient.on("Runtime.exceptionThrown", onException);
2722
+ });
2723
+ }
2724
+ async assertTextChanged(selector, from, to, timeout) {
2725
+ const initialText = from ?? await this.page.text(selector);
2726
+ const deadline = Date.now() + timeout;
2727
+ while (Date.now() < deadline) {
2728
+ const text = await this.page.text(selector);
2729
+ if (text !== initialText && text.includes(to)) {
2730
+ return text;
2731
+ }
2732
+ await new Promise((resolve) => setTimeout(resolve, 200));
2733
+ }
2734
+ throw new Error(`Text did not change to include ${JSON.stringify(to)}`);
2735
+ }
2736
+ async assertPermission(name, state) {
2737
+ const result = await this.page.evaluate(
2738
+ `(() => navigator.permissions.query({ name: ${JSON.stringify(name)} }).then((status) => ({ name: ${JSON.stringify(name)}, state: status.state })))()`
2739
+ );
2740
+ if (!result || typeof result !== "object" || result.state !== state) {
2741
+ throw new Error(`Permission ${name} is not ${state}`);
2742
+ }
2743
+ return result;
2744
+ }
2745
+ async assertMediaTrackLive(kind) {
2746
+ const result = await this.page.evaluate(
2747
+ `(() => {
2748
+ const requestedKind = ${JSON.stringify(kind)};
2749
+ const mediaElements = Array.from(document.querySelectorAll('audio,video')).map((el) => {
2750
+ const tracks = [];
2751
+ if (el.srcObject && typeof el.srcObject.getTracks === 'function') {
2752
+ tracks.push(...el.srcObject.getTracks());
2753
+ }
2754
+ return {
2755
+ tag: el.tagName.toLowerCase(),
2756
+ paused: !!el.paused,
2757
+ tracks: tracks.map((track) => ({
2758
+ kind: track.kind,
2759
+ readyState: track.readyState,
2760
+ enabled: track.enabled,
2761
+ label: track.label,
2762
+ })),
2763
+ };
2764
+ });
2765
+
2766
+ const globalTracks =
2767
+ window.__bpStream && typeof window.__bpStream.getTracks === 'function'
2768
+ ? window.__bpStream.getTracks().map((track) => ({
2769
+ kind: track.kind,
2770
+ readyState: track.readyState,
2771
+ enabled: track.enabled,
2772
+ label: track.label,
2773
+ }))
2774
+ : [];
2775
+
2776
+ const liveTracks = mediaElements
2777
+ .flatMap((entry) => entry.tracks)
2778
+ .concat(globalTracks)
2779
+ .filter((track) => track.kind === requestedKind && track.readyState === 'live');
2780
+
2781
+ return { live: liveTracks.length > 0, mediaElements, globalTracks, liveTracks };
2782
+ })()`
2783
+ );
2784
+ if (!result || typeof result !== "object" || !result.live) {
2785
+ throw new Error(`No live ${kind} media track detected`);
2786
+ }
2787
+ return result;
2788
+ }
1732
2789
  };
1733
2790
  function addBatchToPage(page) {
1734
2791
  const executor = new BatchExecutor(page);
@@ -1859,7 +2916,7 @@ var ACTION_RULES = {
1859
2916
  value: { type: "string|string[]" },
1860
2917
  trigger: { type: "string|string[]" },
1861
2918
  option: { type: "string|string[]" },
1862
- match: { type: "string", enum: ["text", "value", "contains"] }
2919
+ match: { type: "string" }
1863
2920
  }
1864
2921
  },
1865
2922
  check: {
@@ -1990,6 +3047,38 @@ var ACTION_RULES = {
1990
3047
  expect: { type: "string" },
1991
3048
  value: { type: "string" }
1992
3049
  }
3050
+ },
3051
+ waitForWsMessage: {
3052
+ required: { match: { type: "string" } },
3053
+ optional: {
3054
+ where: { type: "object" }
3055
+ }
3056
+ },
3057
+ assertNoConsoleErrors: {
3058
+ required: {},
3059
+ optional: {
3060
+ windowMs: { type: "number" }
3061
+ }
3062
+ },
3063
+ assertTextChanged: {
3064
+ required: { to: { type: "string" } },
3065
+ optional: {
3066
+ selector: { type: "string|string[]" },
3067
+ from: { type: "string" }
3068
+ }
3069
+ },
3070
+ assertPermission: {
3071
+ required: {
3072
+ name: { type: "string" },
3073
+ state: { type: "string" }
3074
+ },
3075
+ optional: {}
3076
+ },
3077
+ assertMediaTrackLive: {
3078
+ required: {
3079
+ kind: { type: "string", enum: ["audio", "video"] }
3080
+ },
3081
+ optional: {}
1993
3082
  }
1994
3083
  };
1995
3084
  var VALID_ACTIONS = Object.keys(ACTION_RULES);
@@ -2013,6 +3102,7 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
2013
3102
  "trigger",
2014
3103
  "option",
2015
3104
  "match",
3105
+ "where",
2016
3106
  "x",
2017
3107
  "y",
2018
3108
  "direction",
@@ -2022,7 +3112,13 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
2022
3112
  "fullPage",
2023
3113
  "expect",
2024
3114
  "retry",
2025
- "retryDelay"
3115
+ "retryDelay",
3116
+ "from",
3117
+ "to",
3118
+ "name",
3119
+ "state",
3120
+ "kind",
3121
+ "windowMs"
2026
3122
  ]);
2027
3123
  function resolveAction(name) {
2028
3124
  if (VALID_ACTIONS.includes(name)) {
@@ -2095,6 +3191,11 @@ function checkFieldType(value, rule) {
2095
3191
  return `expected boolean or "auto", got ${typeof value}`;
2096
3192
  }
2097
3193
  return null;
3194
+ case "object":
3195
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
3196
+ return `expected object, got ${Array.isArray(value) ? "array" : typeof value}`;
3197
+ }
3198
+ return null;
2098
3199
  default: {
2099
3200
  const _exhaustive = rule.type;
2100
3201
  return `unknown type: ${_exhaustive}`;
@@ -2389,6 +3490,10 @@ async function grantAudioPermissions(cdp, origin) {
2389
3490
  await cdp.send("Page.addScriptToEvaluateOnNewDocument", {
2390
3491
  source: PERMISSIONS_OVERRIDE_SCRIPT
2391
3492
  });
3493
+ await cdp.send("Runtime.evaluate", {
3494
+ expression: PERMISSIONS_OVERRIDE_SCRIPT,
3495
+ awaitPromise: false
3496
+ });
2392
3497
  }
2393
3498
  var PERMISSIONS_OVERRIDE_SCRIPT = `
2394
3499
  (function() {
@@ -3347,7 +4452,7 @@ var AudioOutput = class {
3347
4452
  awaitPromise: false
3348
4453
  });
3349
4454
  this.capturing = false;
3350
- await sleep3(250);
4455
+ await sleep2(250);
3351
4456
  return this.mergeChunks();
3352
4457
  }
3353
4458
  /**
@@ -3563,7 +4668,7 @@ function emptyCaptureResult() {
3563
4668
  chunkCount: 0
3564
4669
  };
3565
4670
  }
3566
- function sleep3(ms) {
4671
+ function sleep2(ms) {
3567
4672
  return new Promise((resolve) => setTimeout(resolve, ms));
3568
4673
  }
3569
4674
 
@@ -4104,7 +5209,7 @@ async function isElementAttached(cdp, selector, contextId) {
4104
5209
  const result = await cdp.send("Runtime.evaluate", params);
4105
5210
  return result.result.value === true;
4106
5211
  }
4107
- function sleep4(ms) {
5212
+ function sleep3(ms) {
4108
5213
  return new Promise((resolve) => setTimeout(resolve, ms));
4109
5214
  }
4110
5215
  async function isPageStatic(cdp, windowMs = 200, contextId) {
@@ -4172,7 +5277,7 @@ async function waitForAnyElement(cdp, selectors, options = {}) {
4172
5277
  }
4173
5278
  }
4174
5279
  while (Date.now() < deadline) {
4175
- await sleep4(pollInterval);
5280
+ await sleep3(pollInterval);
4176
5281
  for (const selector of selectors) {
4177
5282
  if (await checkSelector(selector)) {
4178
5283
  return { success: true, selector, waitedMs: Date.now() - startTime };
@@ -4233,7 +5338,7 @@ async function waitForNavigation(cdp, options = {}) {
4233
5338
  cleanup.push(() => cdp.off("Page.lifecycleEvent", onLifecycle));
4234
5339
  const pollUrl = async () => {
4235
5340
  while (!resolved && Date.now() < startTime + timeout) {
4236
- await sleep4(100);
5341
+ await sleep3(100);
4237
5342
  if (resolved) return;
4238
5343
  try {
4239
5344
  const currentUrl = await getCurrentUrl(cdp);
@@ -4838,7 +5943,7 @@ var Page = class {
4838
5943
  } catch (e) {
4839
5944
  if (options.optional) return false;
4840
5945
  if (e instanceof ActionabilityError && e.failureType === "hitTarget" && attempt < HIT_TARGET_RETRIES - 1) {
4841
- await sleep5(HIT_TARGET_DELAY);
5946
+ await sleep4(HIT_TARGET_DELAY);
4842
5947
  await this.cdp.send("DOM.scrollIntoViewIfNeeded", { nodeId: element.nodeId });
4843
5948
  continue;
4844
5949
  }
@@ -5013,7 +6118,7 @@ var Page = class {
5013
6118
  await this.cdp.send("Input.insertText", { text: char });
5014
6119
  }
5015
6120
  if (delay > 0) {
5016
- await sleep5(delay);
6121
+ await sleep4(delay);
5017
6122
  }
5018
6123
  }
5019
6124
  if (options.blur) {
@@ -5109,7 +6214,7 @@ var Page = class {
5109
6214
  state: "visible",
5110
6215
  timeout: 500,
5111
6216
  contextId: this.currentFrameContextId ?? void 0
5112
- }).catch(() => sleep5(100));
6217
+ }).catch(() => sleep4(100));
5113
6218
  const optionHandle = await this.evaluateInFrame(
5114
6219
  `(() => {
5115
6220
  const selectors = ${JSON.stringify(optionSelectors)};
@@ -5326,7 +6431,7 @@ var Page = class {
5326
6431
  () => "navigation"
5327
6432
  ),
5328
6433
  this.waitForDOMMutation({ timeout: 1e3 }).then(() => "mutation"),
5329
- sleep5(1500).then(() => "timeout")
6434
+ sleep4(1500).then(() => "timeout")
5330
6435
  ]);
5331
6436
  }
5332
6437
  return true;
@@ -5346,7 +6451,7 @@ var Page = class {
5346
6451
  (success) => success ? "nav" : null
5347
6452
  ),
5348
6453
  this.waitForDOMMutation({ timeout: 1e3 }).then(() => "mutation"),
5349
- sleep5(1500).then(() => "timeout")
6454
+ sleep4(1500).then(() => "timeout")
5350
6455
  ]);
5351
6456
  if (navigationDetected === "nav") {
5352
6457
  return true;
@@ -5360,7 +6465,7 @@ var Page = class {
5360
6465
  if (shouldWait === true) {
5361
6466
  await this.waitForNavigation({ timeout: options.timeout ?? DEFAULT_TIMEOUT2 });
5362
6467
  } else if (shouldWait === "auto") {
5363
- await sleep5(100);
6468
+ await sleep4(100);
5364
6469
  }
5365
6470
  }
5366
6471
  return true;
@@ -6757,7 +7862,7 @@ var Page = class {
6757
7862
  try {
6758
7863
  await Promise.race([
6759
7864
  this.dialogHandler(dialog),
6760
- sleep5(DIALOG_TIMEOUT).then(() => {
7865
+ sleep4(DIALOG_TIMEOUT).then(() => {
6761
7866
  console.warn("[browser-pilot] Dialog handler timed out after 5s, auto-dismissing");
6762
7867
  return dialog.dismiss();
6763
7868
  })
@@ -6882,7 +7987,7 @@ var Page = class {
6882
7987
  if (attempt < retries) {
6883
7988
  this.rootNodeId = null;
6884
7989
  this.currentFrameContextId = null;
6885
- await sleep5(delay);
7990
+ await sleep4(delay);
6886
7991
  continue;
6887
7992
  }
6888
7993
  }
@@ -7466,7 +8571,7 @@ var Page = class {
7466
8571
  const start = Date.now();
7467
8572
  await this.audioOutput.start();
7468
8573
  if (options.preDelay && options.preDelay > 0) {
7469
- await sleep5(options.preDelay);
8574
+ await sleep4(options.preDelay);
7470
8575
  }
7471
8576
  const inputDone = this.audioInput.play(options.input, {
7472
8577
  waitForEnd: !!options.sendSelector
@@ -7534,255 +8639,27 @@ var Page = class {
7534
8639
  });
7535
8640
  }
7536
8641
  };
7537
- function sleep5(ms) {
8642
+ function sleep4(ms) {
7538
8643
  return new Promise((resolve) => setTimeout(resolve, ms));
7539
8644
  }
7540
8645
 
7541
- // src/browser/browser.ts
7542
- function scoreTarget(t) {
7543
- let score = 0;
7544
- if (t.url.startsWith("http://") || t.url.startsWith("https://")) score += 10;
7545
- if (t.url.startsWith("chrome://")) score -= 20;
7546
- if (t.url.startsWith("chrome-extension://")) score -= 15;
7547
- if (t.url.startsWith("devtools://")) score -= 25;
7548
- if (t.url === "about:blank") score -= 5;
7549
- if (!t.attached) score += 3;
7550
- if (t.title && t.title.length > 0) score += 2;
7551
- return score;
7552
- }
7553
- function pickBestTarget(targets) {
7554
- if (targets.length === 0) return void 0;
7555
- const sorted = [...targets].sort((a, b) => scoreTarget(b) - scoreTarget(a));
7556
- return sorted[0].targetId;
7557
- }
7558
- var Browser = class _Browser {
7559
- cdp;
7560
- providerSession;
7561
- pages = /* @__PURE__ */ new Map();
7562
- pageCounter = 0;
7563
- constructor(cdp, _provider, providerSession, _options) {
7564
- this.cdp = cdp;
7565
- this.providerSession = providerSession;
7566
- }
7567
- /**
7568
- * Create a Browser from an existing CDPClient (used by daemon fast-path).
7569
- * The caller is responsible for the CDP connection lifecycle.
7570
- */
7571
- static fromCDP(cdp, sessionInfo) {
7572
- const providerSession = {
7573
- wsUrl: sessionInfo.wsUrl,
7574
- sessionId: sessionInfo.sessionId,
7575
- async close() {
7576
- }
7577
- };
7578
- const provider = {
7579
- name: sessionInfo.provider ?? "daemon",
7580
- async createSession() {
7581
- return providerSession;
7582
- }
7583
- };
7584
- return new _Browser(cdp, provider, providerSession, { provider: "generic" });
7585
- }
7586
- /**
7587
- * Connect to a browser instance
7588
- */
7589
- static async connect(options) {
7590
- const provider = createProvider(options);
7591
- const session = await provider.createSession(options.session);
7592
- const cdp = await createCDPClient(session.wsUrl, {
7593
- debug: options.debug,
7594
- timeout: options.timeout
7595
- });
7596
- return new _Browser(cdp, provider, session, options);
7597
- }
7598
- /**
7599
- * Get or create a page by name.
7600
- * If no name is provided, returns the first available page or creates a new one.
7601
- *
7602
- * Target selection heuristics (when no targetId is specified):
7603
- * - Prefer http/https URLs over chrome://, devtools://, about:blank
7604
- * - Prefer unattached targets (not already controlled by another client)
7605
- * - Filter by targetUrl if provided
7606
- */
7607
- async page(name, options) {
7608
- const pageName = name ?? "default";
7609
- const cached = this.pages.get(pageName);
7610
- if (cached) return cached;
7611
- const targets = await this.cdp.send(
7612
- "Target.getTargets",
7613
- void 0,
7614
- null
7615
- );
7616
- let pageTargets = targets.targetInfos.filter((t) => t.type === "page");
7617
- if (options?.targetUrl) {
7618
- const urlFilter = options.targetUrl;
7619
- const filtered = pageTargets.filter((t) => t.url.includes(urlFilter));
7620
- if (filtered.length > 0) {
7621
- pageTargets = filtered;
7622
- } else {
7623
- console.warn(
7624
- `[browser-pilot] No targets match URL filter "${urlFilter}", falling back to all page targets`
7625
- );
7626
- }
7627
- }
7628
- let targetId;
7629
- if (options?.targetId) {
7630
- const targetExists = targets.targetInfos.some(
7631
- (t) => t.type === "page" && t.targetId === options.targetId
7632
- );
7633
- if (targetExists) {
7634
- targetId = options.targetId;
7635
- } else {
7636
- console.warn(`[browser-pilot] Target ${options.targetId} no longer exists, falling back`);
7637
- targetId = pickBestTarget(pageTargets) ?? (await this.cdp.send(
7638
- "Target.createTarget",
7639
- {
7640
- url: "about:blank"
7641
- },
7642
- null
7643
- )).targetId;
7644
- }
7645
- } else if (pageTargets.length > 0) {
7646
- targetId = pickBestTarget(pageTargets);
7647
- } else {
7648
- const result = await this.cdp.send(
7649
- "Target.createTarget",
7650
- {
7651
- url: "about:blank"
7652
- },
7653
- null
7654
- );
7655
- targetId = result.targetId;
7656
- }
7657
- await this.cdp.attachToTarget(targetId);
7658
- const page = new Page(this.cdp, targetId);
7659
- await page.init();
7660
- const minViewport = options?.minViewport !== void 0 ? options.minViewport : { width: 200, height: 200 };
7661
- if (minViewport !== false) {
7662
- try {
7663
- const viewport = await page.evaluate(
7664
- "({ w: window.innerWidth, h: window.innerHeight })"
7665
- );
7666
- if (viewport.w < minViewport.width || viewport.h < minViewport.height) {
7667
- console.warn(
7668
- `[browser-pilot] Attached target has small viewport (${viewport.w}x${viewport.h}). Applying default viewport override (1280x720). Use { minViewport: false } to disable this check.`
7669
- );
7670
- await page.setViewport({ width: 1280, height: 720 });
7671
- }
7672
- } catch {
7673
- }
7674
- }
7675
- this.pages.set(pageName, page);
7676
- return page;
7677
- }
7678
- /**
7679
- * Create a new page (tab)
7680
- */
7681
- async newPage(url = "about:blank") {
7682
- const result = await this.cdp.send(
7683
- "Target.createTarget",
7684
- {
7685
- url
7686
- },
7687
- null
7688
- );
7689
- await this.cdp.attachToTarget(result.targetId);
7690
- const page = new Page(this.cdp, result.targetId);
7691
- await page.init();
7692
- const name = `page-${++this.pageCounter}`;
7693
- this.pages.set(name, page);
7694
- return page;
7695
- }
7696
- /**
7697
- * Close a page by name
7698
- */
7699
- async closePage(name) {
7700
- const page = this.pages.get(name);
7701
- if (!page) return;
7702
- const targetId = page.targetId;
7703
- await this.cdp.send("Target.closeTarget", { targetId }, null);
7704
- this.pages.delete(name);
7705
- const deadline = Date.now() + 5e3;
7706
- while (Date.now() < deadline) {
7707
- const { targetInfos } = await this.cdp.send(
7708
- "Target.getTargets",
7709
- void 0,
7710
- null
7711
- );
7712
- if (!targetInfos.some((t) => t.targetId === targetId)) return;
7713
- await new Promise((r) => setTimeout(r, 50));
7714
- }
7715
- }
7716
- /**
7717
- * List all page targets in the connected browser.
7718
- */
7719
- async listTargets() {
7720
- const { targetInfos } = await this.cdp.send(
7721
- "Target.getTargets",
7722
- void 0,
7723
- null
7724
- );
7725
- return targetInfos.filter((target) => target.type === "page");
7726
- }
7727
- /**
7728
- * Get the WebSocket URL for this browser connection
7729
- */
7730
- get wsUrl() {
7731
- return this.providerSession.wsUrl;
7732
- }
7733
- /**
7734
- * Get the provider session ID (for resumption)
7735
- */
7736
- get sessionId() {
7737
- return this.providerSession.sessionId;
7738
- }
7739
- /**
7740
- * Get provider metadata
7741
- */
7742
- get metadata() {
7743
- return this.providerSession.metadata;
7744
- }
7745
- /**
7746
- * Check if connected
7747
- */
7748
- get isConnected() {
7749
- return this.cdp.isConnected;
7750
- }
7751
- /**
7752
- * Disconnect from the browser (keeps provider session alive for reconnection)
7753
- */
7754
- async disconnect() {
7755
- this.pages.clear();
7756
- await this.cdp.close();
7757
- }
7758
- /**
7759
- * Close the browser session completely
7760
- */
7761
- async close() {
7762
- this.pages.clear();
7763
- await this.cdp.close();
7764
- await this.providerSession.close();
7765
- }
7766
- /**
7767
- * Get the underlying CDP client (for advanced usage)
7768
- */
7769
- get cdpClient() {
7770
- return this.cdp;
7771
- }
7772
- };
7773
- function connect(options) {
7774
- return Browser.connect(options);
7775
- }
7776
-
7777
8646
  export {
7778
8647
  pcmToWav,
7779
8648
  SENSITIVE_AUTOCOMPLETE_TOKENS,
7780
8649
  redactValueForRecording,
7781
8650
  fuzzyMatchElements,
8651
+ buildTraceSummary,
8652
+ buildTraceSummaries,
8653
+ createRecordingManifest,
8654
+ canonicalizeRecordingArtifact,
8655
+ createTraceId,
8656
+ normalizeTraceEvent,
8657
+ TRACE_BINDING_NAME,
8658
+ TRACE_SCRIPT,
8659
+ LiveTraceCollector,
7782
8660
  addBatchToPage,
7783
8661
  validateSteps,
7784
- getBrowserWebSocketUrl,
8662
+ grantAudioPermissions,
7785
8663
  DEEP_QUERY_SCRIPT,
7786
- Browser,
7787
- connect
8664
+ Page
7788
8665
  };