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/index.cjs CHANGED
@@ -35,6 +35,7 @@ __export(src_exports, {
35
35
  BatchExecutor: () => BatchExecutor,
36
36
  Browser: () => Browser,
37
37
  BrowserBaseProvider: () => BrowserBaseProvider,
38
+ BrowserEndpointResolutionError: () => BrowserEndpointResolutionError,
38
39
  BrowserlessProvider: () => BrowserlessProvider,
39
40
  CDPError: () => CDPError,
40
41
  ElementNotFoundError: () => ElementNotFoundError,
@@ -46,12 +47,14 @@ __export(src_exports, {
46
47
  Tracer: () => Tracer,
47
48
  addBatchToPage: () => addBatchToPage,
48
49
  bufferToBase64: () => bufferToBase64,
50
+ buildLocalBrowserScanTargets: () => buildLocalBrowserScanTargets,
49
51
  calculateRMS: () => calculateRMS,
50
52
  connect: () => connect,
51
53
  createCDPClient: () => createCDPClient,
52
54
  createProvider: () => createProvider,
53
55
  devices: () => devices,
54
56
  disableTracing: () => disableTracing,
57
+ discoverLocalBrowsers: () => discoverLocalBrowsers,
55
58
  discoverTargets: () => discoverTargets,
56
59
  enableTracing: () => enableTracing,
57
60
  generateSilence: () => generateSilence,
@@ -61,8 +64,11 @@ __export(src_exports, {
61
64
  getTracer: () => getTracer,
62
65
  grantAudioPermissions: () => grantAudioPermissions,
63
66
  isTranscriptionAvailable: () => isTranscriptionAvailable,
67
+ parseDevToolsActivePortFile: () => parseDevToolsActivePortFile,
64
68
  parseWavHeader: () => parseWavHeader,
65
69
  pcmToWav: () => pcmToWav,
70
+ resolveBrowserEndpoint: () => resolveBrowserEndpoint,
71
+ resolveChromeUserDataDirs: () => resolveChromeUserDataDirs,
66
72
  transcribe: () => transcribe,
67
73
  validateSteps: () => validateSteps,
68
74
  waitForAnyElement: () => waitForAnyElement,
@@ -920,6 +926,624 @@ var CDPError = class extends Error {
920
926
  }
921
927
  };
922
928
 
929
+ // src/trace/views.ts
930
+ function takeRecent(events, limit = 5) {
931
+ return events.slice(-limit).map((event) => ({
932
+ ts: event.ts,
933
+ event: event.event,
934
+ summary: event.summary,
935
+ severity: event.severity,
936
+ url: event.url
937
+ }));
938
+ }
939
+ function buildTraceSummaries(events) {
940
+ return {
941
+ ws: summarizeWs(events),
942
+ voice: summarizeVoice(events),
943
+ console: summarizeConsole(events),
944
+ permissions: summarizePermissions(events),
945
+ media: summarizeMedia(events),
946
+ ui: summarizeUi(events),
947
+ session: summarizeSession(events)
948
+ };
949
+ }
950
+ function summarizeWs(events) {
951
+ const relevant = events.filter(
952
+ (event) => event.channel === "ws" || event.event.startsWith("ws.")
953
+ );
954
+ const connections = /* @__PURE__ */ new Map();
955
+ for (const event of relevant) {
956
+ const id = event.connectionId ?? event.requestId ?? event.traceId;
957
+ let connection = connections.get(id);
958
+ if (!connection) {
959
+ connection = { id, sent: 0, received: 0, lastMessages: [] };
960
+ connections.set(id, connection);
961
+ }
962
+ connection.url = event.url ?? connection.url;
963
+ if (event.event === "ws.connection.created") {
964
+ connection.createdAt = event.ts;
965
+ }
966
+ if (event.event === "ws.connection.closed") {
967
+ connection.closedAt = event.ts;
968
+ }
969
+ if (event.event === "ws.frame.sent") {
970
+ connection.sent += 1;
971
+ const payload = typeof event.data["payload"] === "string" ? event.data["payload"] : "";
972
+ if (payload) connection.lastMessages.push(`sent: ${payload}`);
973
+ }
974
+ if (event.event === "ws.frame.received") {
975
+ connection.received += 1;
976
+ const payload = typeof event.data["payload"] === "string" ? event.data["payload"] : "";
977
+ if (payload) connection.lastMessages.push(`recv: ${payload}`);
978
+ }
979
+ connection.lastMessages = connection.lastMessages.slice(-3);
980
+ }
981
+ const values = [...connections.values()];
982
+ const reconnects = values.reduce((count, connection) => {
983
+ return connection.closedAt && !connection.createdAt ? count + 1 : count;
984
+ }, 0);
985
+ return {
986
+ view: "ws",
987
+ totalEvents: relevant.length,
988
+ connections: values.map((connection) => ({
989
+ id: connection.id,
990
+ url: connection.url ?? null,
991
+ createdAt: connection.createdAt ?? null,
992
+ closedAt: connection.closedAt ?? null,
993
+ sent: connection.sent,
994
+ received: connection.received,
995
+ lastMessages: connection.lastMessages,
996
+ connectedButSilent: !!connection.createdAt && !connection.closedAt && connection.sent + connection.received === 0
997
+ })),
998
+ reconnects,
999
+ recent: takeRecent(relevant)
1000
+ };
1001
+ }
1002
+ function summarizeConsole(events) {
1003
+ const relevant = events.filter(
1004
+ (event) => event.channel === "console" || event.event.startsWith("console.") || event.event === "runtime.exception" || event.event === "runtime.unhandledRejection"
1005
+ );
1006
+ return {
1007
+ view: "console",
1008
+ errors: relevant.filter(
1009
+ (event) => event.event === "console.error" || event.event === "runtime.exception" || event.event === "runtime.unhandledRejection"
1010
+ ).length,
1011
+ warnings: relevant.filter((event) => event.event === "console.warn").length,
1012
+ logs: relevant.filter((event) => event.event === "console.log").length,
1013
+ recent: takeRecent(relevant)
1014
+ };
1015
+ }
1016
+ function summarizePermissions(events) {
1017
+ const relevant = events.filter(
1018
+ (event) => event.channel === "permission" || event.event.startsWith("permission.")
1019
+ );
1020
+ const latest = /* @__PURE__ */ new Map();
1021
+ for (const event of relevant) {
1022
+ const name = typeof event.data["name"] === "string" ? event.data["name"] : null;
1023
+ const state = typeof event.data["state"] === "string" ? event.data["state"] : null;
1024
+ if (name && state) {
1025
+ latest.set(name, state);
1026
+ }
1027
+ }
1028
+ return {
1029
+ view: "permissions",
1030
+ states: Object.fromEntries(latest),
1031
+ changes: relevant.filter((event) => event.event === "permission.changed").length,
1032
+ recent: takeRecent(relevant)
1033
+ };
1034
+ }
1035
+ function summarizeMedia(events) {
1036
+ const relevant = events.filter(
1037
+ (event) => event.channel === "media" || event.event.startsWith("media.")
1038
+ );
1039
+ const liveTracks = /* @__PURE__ */ new Map();
1040
+ for (const event of relevant) {
1041
+ const label = typeof event.data["label"] === "string" ? event.data["label"] : "";
1042
+ const kind = typeof event.data["kind"] === "string" ? event.data["kind"] : "";
1043
+ const key = `${kind}:${label}`;
1044
+ if (event.event === "media.track.started") {
1045
+ liveTracks.set(key, kind);
1046
+ }
1047
+ if (event.event === "media.track.ended") {
1048
+ liveTracks.delete(key);
1049
+ }
1050
+ }
1051
+ return {
1052
+ view: "media",
1053
+ tracksStarted: relevant.filter((event) => event.event === "media.track.started").length,
1054
+ tracksEnded: relevant.filter((event) => event.event === "media.track.ended").length,
1055
+ playbackStarted: relevant.filter((event) => event.event === "media.playback.started").length,
1056
+ playbackStopped: relevant.filter((event) => event.event === "media.playback.stopped").length,
1057
+ liveTracks: [...liveTracks.values()],
1058
+ recent: takeRecent(relevant)
1059
+ };
1060
+ }
1061
+ function summarizeVoice(events) {
1062
+ const relevant = events.filter(
1063
+ (event) => event.channel === "voice" || event.event.startsWith("voice.")
1064
+ );
1065
+ return {
1066
+ view: "voice",
1067
+ ready: relevant.filter((event) => event.event === "voice.pipeline.ready").length,
1068
+ notReady: relevant.filter((event) => event.event === "voice.pipeline.notReady").length,
1069
+ captureStarted: relevant.filter((event) => event.event === "voice.capture.started").length,
1070
+ captureStopped: relevant.filter((event) => event.event === "voice.capture.stopped").length,
1071
+ detectedAudio: relevant.filter((event) => event.event === "voice.capture.detectedAudio").length,
1072
+ recent: takeRecent(relevant)
1073
+ };
1074
+ }
1075
+ function summarizeUi(events) {
1076
+ const relevant = events.filter(
1077
+ (event) => event.channel === "dom" || event.event.startsWith("dom.") || event.channel === "action"
1078
+ );
1079
+ return {
1080
+ view: "ui",
1081
+ actions: relevant.filter((event) => event.channel === "action").length,
1082
+ domChanges: relevant.filter((event) => event.channel === "dom").length,
1083
+ recent: takeRecent(relevant)
1084
+ };
1085
+ }
1086
+ function summarizeSession(events) {
1087
+ const byChannel = /* @__PURE__ */ new Map();
1088
+ const failedActions = events.filter((event) => event.event === "action.failed").length;
1089
+ for (const event of events) {
1090
+ byChannel.set(event.channel, (byChannel.get(event.channel) ?? 0) + 1);
1091
+ }
1092
+ return {
1093
+ view: "session",
1094
+ totalEvents: events.length,
1095
+ byChannel: Object.fromEntries(byChannel),
1096
+ failedActions,
1097
+ recent: takeRecent(events)
1098
+ };
1099
+ }
1100
+
1101
+ // src/recording/manifest.ts
1102
+ function isCanonicalRecordingManifest(value) {
1103
+ return Boolean(
1104
+ value && typeof value === "object" && value.version === 2 && typeof value.session === "object"
1105
+ );
1106
+ }
1107
+ function isLegacyRecordingManifest(value) {
1108
+ return Boolean(
1109
+ value && typeof value === "object" && value.version === 1 && Array.isArray(value.frames)
1110
+ );
1111
+ }
1112
+ function createRecordingManifest(input) {
1113
+ const actions = input.frames.map((frame) => {
1114
+ const actionId = frame.actionId ?? `action-${frame.seq}`;
1115
+ return {
1116
+ id: actionId,
1117
+ stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
1118
+ action: frame.action,
1119
+ selector: frame.selector,
1120
+ selectorUsed: frame.selectorUsed ?? frame.selector,
1121
+ value: frame.value,
1122
+ url: frame.url,
1123
+ success: frame.success,
1124
+ durationMs: frame.durationMs,
1125
+ error: frame.error,
1126
+ ts: new Date(frame.timestamp).toISOString(),
1127
+ pageUrl: frame.pageUrl,
1128
+ pageTitle: frame.pageTitle,
1129
+ coordinates: frame.coordinates,
1130
+ boundingBox: frame.boundingBox
1131
+ };
1132
+ });
1133
+ const screenshots = input.frames.map((frame) => ({
1134
+ id: `shot-${frame.seq}`,
1135
+ stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
1136
+ actionId: frame.actionId ?? `action-${frame.seq}`,
1137
+ file: frame.screenshot,
1138
+ ts: new Date(frame.timestamp).toISOString(),
1139
+ success: frame.success,
1140
+ pageUrl: frame.pageUrl,
1141
+ pageTitle: frame.pageTitle,
1142
+ coordinates: frame.coordinates,
1143
+ boundingBox: frame.boundingBox
1144
+ }));
1145
+ return {
1146
+ version: 2,
1147
+ recordedAt: input.recordedAt,
1148
+ session: {
1149
+ id: input.sessionId,
1150
+ startUrl: input.startUrl,
1151
+ endUrl: input.endUrl,
1152
+ targetId: input.targetId,
1153
+ profile: input.profile
1154
+ },
1155
+ recipe: {
1156
+ steps: input.steps
1157
+ },
1158
+ actions,
1159
+ screenshots,
1160
+ trace: {
1161
+ events: input.traceEvents,
1162
+ summaries: buildTraceSummaries(input.traceEvents)
1163
+ },
1164
+ assertions: input.assertions ?? [],
1165
+ notes: input.notes ?? [],
1166
+ artifacts: {
1167
+ recordingManifest: input.recordingManifest ?? "recording.json",
1168
+ screenshotDir: input.screenshotDir ?? "screenshots/"
1169
+ }
1170
+ };
1171
+ }
1172
+ function canonicalizeRecordingArtifact(value) {
1173
+ if (isCanonicalRecordingManifest(value)) {
1174
+ return value;
1175
+ }
1176
+ if (!isLegacyRecordingManifest(value)) {
1177
+ throw new Error("Unsupported recording artifact");
1178
+ }
1179
+ const traceEvents = buildTraceEventsFromLegacy(value);
1180
+ const steps = value.frames.map((frame) => frameToStep(frame));
1181
+ return createRecordingManifest({
1182
+ recordedAt: value.recordedAt,
1183
+ sessionId: value.sessionId,
1184
+ startUrl: value.startUrl,
1185
+ endUrl: value.endUrl,
1186
+ steps,
1187
+ frames: value.frames,
1188
+ traceEvents,
1189
+ notes: ["Converted from legacy recording manifest"]
1190
+ });
1191
+ }
1192
+ function buildTraceEventsFromLegacy(value) {
1193
+ const events = [];
1194
+ for (const frame of value.frames) {
1195
+ events.push({
1196
+ traceId: frame.actionId ?? `legacy-${frame.seq}`,
1197
+ sessionId: value.sessionId,
1198
+ ts: new Date(frame.timestamp).toISOString(),
1199
+ elapsedMs: frame.timestamp - new Date(value.recordedAt).getTime(),
1200
+ channel: "action",
1201
+ event: frame.success ? "action.succeeded" : "action.failed",
1202
+ severity: frame.success ? "info" : "error",
1203
+ summary: `${frame.action}${frame.selector ? ` ${frame.selector}` : ""}`,
1204
+ data: {
1205
+ action: frame.action,
1206
+ selector: frame.selector,
1207
+ value: frame.value ?? null,
1208
+ pageUrl: frame.pageUrl ?? null,
1209
+ pageTitle: frame.pageTitle ?? null,
1210
+ screenshot: frame.screenshot
1211
+ },
1212
+ actionId: frame.actionId ?? `action-${frame.seq}`,
1213
+ stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
1214
+ selector: frame.selector,
1215
+ selectorUsed: frame.selectorUsed ?? frame.selector,
1216
+ url: frame.pageUrl ?? frame.url
1217
+ });
1218
+ }
1219
+ return events;
1220
+ }
1221
+ function frameToStep(frame) {
1222
+ switch (frame.action) {
1223
+ case "fill":
1224
+ return { action: "fill", selector: frame.selector, value: frame.value };
1225
+ case "submit":
1226
+ return { action: "submit", selector: frame.selector };
1227
+ case "goto":
1228
+ return { action: "goto", url: frame.url ?? frame.pageUrl };
1229
+ case "press":
1230
+ return { action: "press", key: frame.value ?? "Enter" };
1231
+ default:
1232
+ return { action: "click", selector: frame.selector };
1233
+ }
1234
+ }
1235
+
1236
+ // src/trace/model.ts
1237
+ function createTraceId(prefix = "evt") {
1238
+ const random = Math.random().toString(36).slice(2, 10);
1239
+ return `${prefix}-${Date.now().toString(36)}-${random}`;
1240
+ }
1241
+ function normalizeTraceEvent(event) {
1242
+ return {
1243
+ traceId: event.traceId ?? createTraceId(event.channel),
1244
+ ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
1245
+ elapsedMs: event.elapsedMs ?? 0,
1246
+ severity: event.severity ?? inferSeverity(event.event),
1247
+ data: event.data ?? {},
1248
+ ...event
1249
+ };
1250
+ }
1251
+ function inferSeverity(eventName) {
1252
+ if (eventName.includes(".failed") || eventName.includes(".error") || eventName.includes("exception") || eventName.includes("notReady")) {
1253
+ return "error";
1254
+ }
1255
+ if (eventName.includes(".closed") || eventName.includes(".warn") || eventName.includes(".changed")) {
1256
+ return "warn";
1257
+ }
1258
+ return "info";
1259
+ }
1260
+
1261
+ // src/trace/script.ts
1262
+ var TRACE_BINDING_NAME = "__bpTraceBinding";
1263
+ var TRACE_SCRIPT = `
1264
+ (() => {
1265
+ if (window.__bpTraceInstalled) return;
1266
+ window.__bpTraceInstalled = true;
1267
+
1268
+ const binding = globalThis.${TRACE_BINDING_NAME};
1269
+ if (typeof binding !== 'function') return;
1270
+
1271
+ const emit = (event, data = {}, severity = 'info', summary) => {
1272
+ try {
1273
+ globalThis.__bpTraceRecentEvents = globalThis.__bpTraceRecentEvents || [];
1274
+ const payload = {
1275
+ event,
1276
+ severity,
1277
+ summary: summary || event,
1278
+ ts: Date.now(),
1279
+ data,
1280
+ };
1281
+ globalThis.__bpTraceRecentEvents.push(payload);
1282
+ if (globalThis.__bpTraceRecentEvents.length > 200) {
1283
+ globalThis.__bpTraceRecentEvents.splice(0, globalThis.__bpTraceRecentEvents.length - 200);
1284
+ }
1285
+ binding(JSON.stringify(payload));
1286
+ } catch {}
1287
+ };
1288
+
1289
+ const patchWebSocket = () => {
1290
+ const NativeWebSocket = window.WebSocket;
1291
+ if (typeof NativeWebSocket !== 'function' || window.__bpTraceWebSocketInstalled) return;
1292
+ window.__bpTraceWebSocketInstalled = true;
1293
+
1294
+ const nextId = () => Math.random().toString(36).slice(2, 10);
1295
+
1296
+ const patchInstance = (socket, urlValue) => {
1297
+ if (!socket || socket.__bpTracePatched) return socket;
1298
+ socket.__bpTracePatched = true;
1299
+ socket.__bpTraceId = socket.__bpTraceId || nextId();
1300
+ socket.__bpTraceUrl = String(urlValue || socket.url || '');
1301
+ globalThis.__bpTrackedWebSockets = globalThis.__bpTrackedWebSockets || new Set();
1302
+ globalThis.__bpTrackedWebSockets.add(socket);
1303
+
1304
+ emit(
1305
+ 'ws.connection.created',
1306
+ { connectionId: socket.__bpTraceId, url: socket.__bpTraceUrl },
1307
+ 'info',
1308
+ 'WebSocket opened ' + socket.__bpTraceUrl
1309
+ );
1310
+
1311
+ const originalSend = socket.send;
1312
+ socket.send = function(data) {
1313
+ const payload =
1314
+ typeof data === 'string'
1315
+ ? data
1316
+ : data && typeof data.toString === 'function'
1317
+ ? data.toString()
1318
+ : '[binary]';
1319
+ emit(
1320
+ 'ws.frame.sent',
1321
+ {
1322
+ connectionId: socket.__bpTraceId,
1323
+ url: socket.__bpTraceUrl,
1324
+ payload,
1325
+ length: payload.length,
1326
+ },
1327
+ 'info',
1328
+ 'WebSocket frame sent'
1329
+ );
1330
+ return originalSend.call(this, data);
1331
+ };
1332
+
1333
+ socket.addEventListener('message', (event) => {
1334
+ if (socket.__bpOfflineNotified || socket.__bpTraceClosed) {
1335
+ return;
1336
+ }
1337
+ const data = event && 'data' in event ? event.data : '';
1338
+ const payload =
1339
+ typeof data === 'string'
1340
+ ? data
1341
+ : data && typeof data.toString === 'function'
1342
+ ? data.toString()
1343
+ : '[binary]';
1344
+ emit(
1345
+ 'ws.frame.received',
1346
+ {
1347
+ connectionId: socket.__bpTraceId,
1348
+ url: socket.__bpTraceUrl,
1349
+ payload,
1350
+ length: payload.length,
1351
+ },
1352
+ 'info',
1353
+ 'WebSocket frame received'
1354
+ );
1355
+ });
1356
+
1357
+ socket.addEventListener('close', (event) => {
1358
+ if (socket.__bpTraceClosed) {
1359
+ return;
1360
+ }
1361
+ socket.__bpTraceClosed = true;
1362
+ try {
1363
+ globalThis.__bpTrackedWebSockets.delete(socket);
1364
+ } catch {}
1365
+ emit(
1366
+ 'ws.connection.closed',
1367
+ {
1368
+ connectionId: socket.__bpTraceId,
1369
+ url: socket.__bpTraceUrl,
1370
+ code: event.code,
1371
+ reason: event.reason,
1372
+ },
1373
+ 'warn',
1374
+ 'WebSocket closed'
1375
+ );
1376
+ });
1377
+
1378
+ return socket;
1379
+ };
1380
+
1381
+ const TracedWebSocket = function(url, protocols) {
1382
+ return arguments.length > 1
1383
+ ? patchInstance(new NativeWebSocket(url, protocols), url)
1384
+ : patchInstance(new NativeWebSocket(url), url);
1385
+ };
1386
+ TracedWebSocket.prototype = NativeWebSocket.prototype;
1387
+ Object.setPrototypeOf(TracedWebSocket, NativeWebSocket);
1388
+ window.WebSocket = TracedWebSocket;
1389
+ };
1390
+
1391
+ window.addEventListener('error', (errorEvent) => {
1392
+ emit(
1393
+ 'runtime.exception',
1394
+ {
1395
+ message: errorEvent.message,
1396
+ filename: errorEvent.filename,
1397
+ line: errorEvent.lineno,
1398
+ column: errorEvent.colno,
1399
+ },
1400
+ 'error',
1401
+ errorEvent.message || 'Uncaught error'
1402
+ );
1403
+ });
1404
+
1405
+ window.addEventListener('unhandledrejection', (event) => {
1406
+ const reason = event && 'reason' in event ? String(event.reason) : 'Unhandled rejection';
1407
+ emit('runtime.unhandledRejection', { reason }, 'error', reason);
1408
+ });
1409
+
1410
+ const patchPermissions = async () => {
1411
+ if (!navigator.permissions || !navigator.permissions.query) return;
1412
+
1413
+ const names = ['geolocation', 'microphone', 'camera', 'notifications'];
1414
+ for (const name of names) {
1415
+ try {
1416
+ const status = await navigator.permissions.query({ name });
1417
+ emit(
1418
+ 'permission.state',
1419
+ { name, state: status.state },
1420
+ status.state === 'denied' ? 'warn' : 'info',
1421
+ name + ': ' + status.state
1422
+ );
1423
+ status.addEventListener('change', () => {
1424
+ emit(
1425
+ 'permission.changed',
1426
+ { name, state: status.state },
1427
+ status.state === 'denied' ? 'warn' : 'info',
1428
+ name + ': ' + status.state
1429
+ );
1430
+ });
1431
+ } catch {}
1432
+ }
1433
+ };
1434
+
1435
+ const patchMediaElement = (element) => {
1436
+ if (!element || element.__bpTracePatched) return;
1437
+ element.__bpTracePatched = true;
1438
+
1439
+ element.addEventListener('play', () => {
1440
+ emit(
1441
+ 'media.playback.started',
1442
+ { tag: element.tagName.toLowerCase(), src: element.currentSrc || element.src || null },
1443
+ 'info',
1444
+ 'Media playback started'
1445
+ );
1446
+ });
1447
+
1448
+ const onStop = () => {
1449
+ emit(
1450
+ 'media.playback.stopped',
1451
+ { tag: element.tagName.toLowerCase(), src: element.currentSrc || element.src || null },
1452
+ 'warn',
1453
+ 'Media playback stopped'
1454
+ );
1455
+ };
1456
+
1457
+ element.addEventListener('pause', onStop);
1458
+ element.addEventListener('ended', onStop);
1459
+ };
1460
+
1461
+ const patchMediaElements = () => {
1462
+ document.querySelectorAll('audio,video').forEach(patchMediaElement);
1463
+ };
1464
+
1465
+ patchMediaElements();
1466
+ patchWebSocket();
1467
+
1468
+ if (document.documentElement) {
1469
+ const observer = new MutationObserver(() => {
1470
+ patchMediaElements();
1471
+ });
1472
+ observer.observe(document.documentElement, { childList: true, subtree: true });
1473
+ }
1474
+
1475
+ if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
1476
+ const original = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
1477
+ navigator.mediaDevices.getUserMedia = async (...args) => {
1478
+ emit('voice.capture.started', { constraints: args[0] || null }, 'info', 'Voice capture started');
1479
+ try {
1480
+ const stream = await original(...args);
1481
+ const tracks = stream.getTracks();
1482
+
1483
+ for (const track of tracks) {
1484
+ emit(
1485
+ 'media.track.started',
1486
+ { kind: track.kind, label: track.label, readyState: track.readyState },
1487
+ 'info',
1488
+ track.kind + ' track started'
1489
+ );
1490
+ track.addEventListener('ended', () => {
1491
+ emit(
1492
+ 'media.track.ended',
1493
+ { kind: track.kind, label: track.label, readyState: track.readyState },
1494
+ 'warn',
1495
+ track.kind + ' track ended'
1496
+ );
1497
+ emit(
1498
+ 'voice.capture.stopped',
1499
+ { kind: track.kind, label: track.label, readyState: track.readyState },
1500
+ 'warn',
1501
+ 'Voice capture stopped'
1502
+ );
1503
+ });
1504
+ }
1505
+
1506
+ emit(
1507
+ 'voice.capture.detectedAudio',
1508
+ { trackCount: tracks.length, kinds: tracks.map((track) => track.kind) },
1509
+ 'info',
1510
+ 'Voice capture detected audio'
1511
+ );
1512
+
1513
+ return stream;
1514
+ } catch (error) {
1515
+ emit(
1516
+ 'voice.pipeline.notReady',
1517
+ { message: String(error && error.message ? error.message : error) },
1518
+ 'error',
1519
+ String(error && error.message ? error.message : error)
1520
+ );
1521
+ throw error;
1522
+ }
1523
+ };
1524
+ }
1525
+
1526
+ document.addEventListener('visibilitychange', () => {
1527
+ emit(
1528
+ 'dom.state.changed',
1529
+ { visibilityState: document.visibilityState },
1530
+ document.visibilityState === 'hidden' ? 'warn' : 'info',
1531
+ 'Visibility ' + document.visibilityState
1532
+ );
1533
+ });
1534
+
1535
+ patchPermissions();
1536
+ emit('voice.pipeline.ready', { url: location.href }, 'info', 'Trace hooks ready');
1537
+ })();
1538
+ `;
1539
+
1540
+ // src/trace/live.ts
1541
+ function globToRegex(pattern) {
1542
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
1543
+ const withWildcards = escaped.replace(/\*/g, ".*");
1544
+ return new RegExp(`^${withWildcards}$`);
1545
+ }
1546
+
923
1547
  // src/actions/executor.ts
924
1548
  var DEFAULT_TIMEOUT = 3e4;
925
1549
  var DEFAULT_RECORDING_SKIP_ACTIONS = [
@@ -929,6 +1553,61 @@ var DEFAULT_RECORDING_SKIP_ACTIONS = [
929
1553
  "text",
930
1554
  "screenshot"
931
1555
  ];
1556
+ function readString(value) {
1557
+ return typeof value === "string" ? value : void 0;
1558
+ }
1559
+ function readStringOr(value, fallback = "") {
1560
+ return readString(value) ?? fallback;
1561
+ }
1562
+ function formatConsoleArg(entry) {
1563
+ return readString(entry["value"]) ?? readString(entry["description"]) ?? "";
1564
+ }
1565
+ function loadExistingRecording(manifestPath) {
1566
+ try {
1567
+ const raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
1568
+ if (raw.version === 1) {
1569
+ const legacy = raw;
1570
+ return {
1571
+ frames: Array.isArray(legacy.frames) ? legacy.frames : [],
1572
+ traceEvents: [],
1573
+ recordedAt: legacy.recordedAt,
1574
+ startUrl: legacy.startUrl
1575
+ };
1576
+ }
1577
+ const artifact = canonicalizeRecordingArtifact(raw);
1578
+ const screenshotsByAction = new Map(artifact.screenshots.map((shot) => [shot.actionId, shot]));
1579
+ const frames = artifact.actions.map((action, index) => {
1580
+ const screenshot = screenshotsByAction.get(action.id);
1581
+ return {
1582
+ seq: index + 1,
1583
+ timestamp: Date.parse(action.ts),
1584
+ action: action.action,
1585
+ selector: action.selector,
1586
+ selectorUsed: action.selectorUsed,
1587
+ value: action.value,
1588
+ url: action.url,
1589
+ coordinates: action.coordinates,
1590
+ boundingBox: action.boundingBox,
1591
+ success: action.success,
1592
+ durationMs: action.durationMs,
1593
+ error: action.error,
1594
+ screenshot: screenshot?.file ?? "",
1595
+ pageUrl: action.pageUrl,
1596
+ pageTitle: action.pageTitle,
1597
+ stepIndex: action.stepIndex,
1598
+ actionId: action.id
1599
+ };
1600
+ });
1601
+ return {
1602
+ frames,
1603
+ traceEvents: artifact.trace.events,
1604
+ recordedAt: artifact.recordedAt,
1605
+ startUrl: artifact.session.startUrl
1606
+ };
1607
+ } catch {
1608
+ return { frames: [], traceEvents: [] };
1609
+ }
1610
+ }
932
1611
  function classifyFailure(error) {
933
1612
  if (error instanceof ElementNotFoundError) {
934
1613
  return { reason: "missing" };
@@ -1009,6 +1688,9 @@ var BatchExecutor = class {
1009
1688
  const results = [];
1010
1689
  const startTime = Date.now();
1011
1690
  const recording = options.record ? this.createRecordingContext(options.record) : null;
1691
+ if (steps.some((step) => step.action === "waitForWsMessage")) {
1692
+ await this.ensureTraceHooks();
1693
+ }
1012
1694
  const startUrl = recording ? await this.getPageUrlSafe() : "";
1013
1695
  let stoppedAtIndex;
1014
1696
  for (let i = 0; i < steps.length; i++) {
@@ -1018,6 +1700,26 @@ var BatchExecutor = class {
1018
1700
  const retryDelay = step.retryDelay ?? 500;
1019
1701
  let lastError;
1020
1702
  let succeeded = false;
1703
+ if (recording) {
1704
+ recording.traceEvents.push(
1705
+ normalizeTraceEvent({
1706
+ traceId: createTraceId("action"),
1707
+ elapsedMs: Date.now() - startTime,
1708
+ channel: "action",
1709
+ event: "action.started",
1710
+ summary: `${step.action}${step.selector ? ` ${Array.isArray(step.selector) ? step.selector[0] : step.selector}` : ""}`,
1711
+ data: {
1712
+ action: step.action,
1713
+ selector: step.selector ?? null,
1714
+ url: step.url ?? null
1715
+ },
1716
+ actionId: `action-${i + 1}`,
1717
+ stepIndex: i,
1718
+ selector: step.selector,
1719
+ url: step.url
1720
+ })
1721
+ );
1722
+ }
1021
1723
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
1022
1724
  if (attempt > 0) {
1023
1725
  await new Promise((resolve) => setTimeout(resolve, retryDelay));
@@ -1041,6 +1743,28 @@ var BatchExecutor = class {
1041
1743
  if (recording && !recording.skipActions.has(step.action)) {
1042
1744
  await this.captureRecordingFrame(step, stepResult, recording);
1043
1745
  }
1746
+ if (recording) {
1747
+ recording.traceEvents.push(
1748
+ normalizeTraceEvent({
1749
+ traceId: createTraceId("action"),
1750
+ elapsedMs: Date.now() - startTime,
1751
+ channel: "action",
1752
+ event: "action.succeeded",
1753
+ summary: `${step.action} succeeded`,
1754
+ data: {
1755
+ action: step.action,
1756
+ selector: step.selector ?? null,
1757
+ selectorUsed: result.selectorUsed ?? null,
1758
+ durationMs: Date.now() - stepStart
1759
+ },
1760
+ actionId: `action-${i + 1}`,
1761
+ stepIndex: i,
1762
+ selector: step.selector,
1763
+ selectorUsed: result.selectorUsed,
1764
+ url: step.url
1765
+ })
1766
+ );
1767
+ }
1044
1768
  results.push(stepResult);
1045
1769
  succeeded = true;
1046
1770
  break;
@@ -1078,6 +1802,28 @@ var BatchExecutor = class {
1078
1802
  if (recording && !recording.skipActions.has(step.action)) {
1079
1803
  await this.captureRecordingFrame(step, failedResult, recording);
1080
1804
  }
1805
+ if (recording) {
1806
+ recording.traceEvents.push(
1807
+ normalizeTraceEvent({
1808
+ traceId: createTraceId("action"),
1809
+ elapsedMs: Date.now() - startTime,
1810
+ channel: "action",
1811
+ event: "action.failed",
1812
+ severity: "error",
1813
+ summary: `${step.action} failed: ${errorMessage}`,
1814
+ data: {
1815
+ action: step.action,
1816
+ selector: step.selector ?? null,
1817
+ error: errorMessage,
1818
+ reason
1819
+ },
1820
+ actionId: `action-${i + 1}`,
1821
+ stepIndex: i,
1822
+ selector: step.selector,
1823
+ url: step.url
1824
+ })
1825
+ );
1826
+ }
1081
1827
  results.push(failedResult);
1082
1828
  if (onFail === "stop" && !step.optional) {
1083
1829
  stoppedAtIndex = i;
@@ -1093,7 +1839,8 @@ var BatchExecutor = class {
1093
1839
  recording,
1094
1840
  startTime,
1095
1841
  startUrl,
1096
- allSuccess
1842
+ allSuccess,
1843
+ steps
1097
1844
  );
1098
1845
  }
1099
1846
  return {
@@ -1108,20 +1855,14 @@ var BatchExecutor = class {
1108
1855
  const baseDir = record.outputDir ?? (0, import_node_path.join)(process.cwd(), ".browser-pilot");
1109
1856
  const screenshotDir = (0, import_node_path.join)(baseDir, "screenshots");
1110
1857
  const manifestPath = (0, import_node_path.join)(baseDir, "recording.json");
1111
- let existingFrames = [];
1112
- try {
1113
- const existing = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
1114
- if (existing.frames && Array.isArray(existing.frames)) {
1115
- existingFrames = existing.frames;
1116
- }
1117
- } catch {
1118
- }
1858
+ const existing = loadExistingRecording(manifestPath);
1119
1859
  fs.mkdirSync(screenshotDir, { recursive: true });
1120
1860
  return {
1121
1861
  baseDir,
1122
1862
  screenshotDir,
1123
1863
  sessionId: record.sessionId ?? this.page.targetId,
1124
- frames: existingFrames,
1864
+ frames: existing.frames,
1865
+ traceEvents: existing.traceEvents,
1125
1866
  format: record.format ?? "webp",
1126
1867
  quality: Math.max(0, Math.min(100, record.quality ?? 40)),
1127
1868
  highlights: record.highlights !== false,
@@ -1177,6 +1918,7 @@ var BatchExecutor = class {
1177
1918
  timestamp: ts,
1178
1919
  action: stepResult.action,
1179
1920
  selector: stepResult.selectorUsed ?? (Array.isArray(step.selector) ? step.selector[0] : step.selector),
1921
+ selectorUsed: stepResult.selectorUsed,
1180
1922
  value: redactValueForRecording(
1181
1923
  typeof step.value === "string" ? step.value : void 0,
1182
1924
  targetMetadata
@@ -1189,7 +1931,9 @@ var BatchExecutor = class {
1189
1931
  error: stepResult.error,
1190
1932
  screenshot: filename,
1191
1933
  pageUrl,
1192
- pageTitle
1934
+ pageTitle,
1935
+ stepIndex: stepResult.index,
1936
+ actionId: `action-${stepResult.index + 1}`
1193
1937
  });
1194
1938
  } catch {
1195
1939
  } finally {
@@ -1201,45 +1945,31 @@ var BatchExecutor = class {
1201
1945
  /**
1202
1946
  * Write recording manifest to disk
1203
1947
  */
1204
- async writeRecordingManifest(recording, startTime, startUrl, success) {
1948
+ async writeRecordingManifest(recording, startTime, startUrl, success, steps) {
1205
1949
  let endUrl = startUrl;
1206
- let viewport = { width: 1280, height: 720 };
1207
1950
  try {
1208
1951
  endUrl = await this.page.url();
1209
1952
  } catch {
1210
1953
  }
1211
- try {
1212
- const metrics = await this.page.cdpClient.send("Page.getLayoutMetrics");
1213
- viewport = {
1214
- width: metrics.cssVisualViewport.clientWidth,
1215
- height: metrics.cssVisualViewport.clientHeight
1216
- };
1217
- } catch {
1218
- }
1219
1954
  const manifestPath = (0, import_node_path.join)(recording.baseDir, "recording.json");
1220
1955
  let recordedAt = new Date(startTime).toISOString();
1221
1956
  let originalStartUrl = startUrl;
1222
- try {
1223
- const existing = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
1224
- if (existing.recordedAt) recordedAt = existing.recordedAt;
1225
- if (existing.startUrl) originalStartUrl = existing.startUrl;
1226
- } catch {
1227
- }
1228
- const firstFrameTime = recording.frames[0]?.timestamp ?? startTime;
1229
- const totalDurationMs = Date.now() - Math.min(firstFrameTime, startTime);
1230
- const manifest = {
1231
- version: 1,
1957
+ const existing = loadExistingRecording(manifestPath);
1958
+ if (existing.recordedAt) recordedAt = existing.recordedAt;
1959
+ if (existing.startUrl) originalStartUrl = existing.startUrl;
1960
+ const manifest = createRecordingManifest({
1232
1961
  recordedAt,
1233
1962
  sessionId: recording.sessionId,
1234
1963
  startUrl: originalStartUrl,
1235
1964
  endUrl,
1236
- viewport,
1237
- format: recording.format,
1238
- quality: recording.quality,
1239
- totalDurationMs,
1240
- success,
1241
- frames: recording.frames
1242
- };
1965
+ targetId: this.page.targetId,
1966
+ steps,
1967
+ frames: recording.frames,
1968
+ traceEvents: recording.traceEvents,
1969
+ notes: success ? [] : ["Replay ended with at least one failed action."],
1970
+ recordingManifest: "recording.json",
1971
+ screenshotDir: "screenshots/"
1972
+ });
1243
1973
  fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
1244
1974
  return manifestPath;
1245
1975
  }
@@ -1522,6 +2252,39 @@ var BatchExecutor = class {
1522
2252
  }
1523
2253
  return { selectorUsed: usedSelector, value: actual };
1524
2254
  }
2255
+ case "waitForWsMessage": {
2256
+ if (typeof step.match !== "string") {
2257
+ throw new Error("waitForWsMessage requires match");
2258
+ }
2259
+ const message = await this.waitForWsMessage(step.match, step.where, timeout);
2260
+ return { value: message };
2261
+ }
2262
+ case "assertNoConsoleErrors": {
2263
+ await this.assertNoConsoleErrors(step.windowMs ?? timeout);
2264
+ return {};
2265
+ }
2266
+ case "assertTextChanged": {
2267
+ const selector = Array.isArray(step.selector) ? step.selector[0] : step.selector;
2268
+ if (typeof step.to !== "string") {
2269
+ throw new Error("assertTextChanged requires to");
2270
+ }
2271
+ const text = await this.assertTextChanged(selector, step.from, step.to, timeout);
2272
+ return { selectorUsed: selector, text };
2273
+ }
2274
+ case "assertPermission": {
2275
+ if (!step.name || !step.state) {
2276
+ throw new Error("assertPermission requires name and state");
2277
+ }
2278
+ const permission = await this.assertPermission(step.name, step.state);
2279
+ return { value: permission };
2280
+ }
2281
+ case "assertMediaTrackLive": {
2282
+ if (!step.kind) {
2283
+ throw new Error("assertMediaTrackLive requires kind");
2284
+ }
2285
+ const media = await this.assertMediaTrackLive(step.kind);
2286
+ return { value: media };
2287
+ }
1525
2288
  default: {
1526
2289
  const action = step.action;
1527
2290
  const aliases = {
@@ -1575,7 +2338,7 @@ var BatchExecutor = class {
1575
2338
  };
1576
2339
  const suggestion = aliases[action.toLowerCase()];
1577
2340
  const hint = suggestion ? ` Did you mean "${suggestion}"?` : "";
1578
- 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";
2341
+ 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";
1579
2342
  throw new Error(`Unknown action "${action}".${hint}
1580
2343
 
1581
2344
  Valid actions: ${valid}`);
@@ -1591,6 +2354,237 @@ Valid actions: ${valid}`);
1591
2354
  if (matched) return matched;
1592
2355
  return Array.isArray(selector) ? selector[0] : selector;
1593
2356
  }
2357
+ async ensureTraceHooks() {
2358
+ await this.page.cdpClient.send("Runtime.enable");
2359
+ await this.page.cdpClient.send("Page.enable");
2360
+ await this.page.cdpClient.send("Network.enable");
2361
+ try {
2362
+ await this.page.cdpClient.send("Runtime.addBinding", { name: TRACE_BINDING_NAME });
2363
+ } catch {
2364
+ }
2365
+ await this.page.cdpClient.send("Page.addScriptToEvaluateOnNewDocument", {
2366
+ source: TRACE_SCRIPT
2367
+ });
2368
+ await this.page.cdpClient.send("Runtime.evaluate", {
2369
+ expression: TRACE_SCRIPT,
2370
+ awaitPromise: false
2371
+ });
2372
+ }
2373
+ async waitForWsMessage(match, where, timeout) {
2374
+ await this.ensureTraceHooks();
2375
+ const regex = globToRegex(match);
2376
+ const wsUrls = /* @__PURE__ */ new Map();
2377
+ const recentMatch = await this.findRecentWsMessage(regex, where);
2378
+ if (recentMatch) {
2379
+ return recentMatch;
2380
+ }
2381
+ return new Promise((resolve, reject) => {
2382
+ const cleanup = () => {
2383
+ this.page.cdpClient.off("Network.webSocketCreated", onCreated);
2384
+ this.page.cdpClient.off("Network.webSocketFrameReceived", onFrame);
2385
+ this.page.cdpClient.off("Runtime.bindingCalled", onBinding);
2386
+ clearTimeout(timer);
2387
+ };
2388
+ const onCreated = (params) => {
2389
+ wsUrls.set(readStringOr(params["requestId"]), readStringOr(params["url"]));
2390
+ };
2391
+ const onFrame = (params) => {
2392
+ const requestId = readStringOr(params["requestId"]);
2393
+ const response = params["response"] ?? {};
2394
+ const payload = response.payloadData ?? "";
2395
+ const url = wsUrls.get(requestId) ?? "";
2396
+ if (!regex.test(url) && !regex.test(payload)) {
2397
+ return;
2398
+ }
2399
+ if (where && !this.payloadMatchesWhere(payload, where)) {
2400
+ return;
2401
+ }
2402
+ cleanup();
2403
+ resolve({ requestId, url, payload });
2404
+ };
2405
+ const onBinding = (params) => {
2406
+ if (params["name"] !== TRACE_BINDING_NAME) {
2407
+ return;
2408
+ }
2409
+ try {
2410
+ const parsed = JSON.parse(readStringOr(params["payload"]));
2411
+ if (parsed.event !== "ws.frame.received") {
2412
+ return;
2413
+ }
2414
+ const data = parsed.data ?? {};
2415
+ const payload = readStringOr(data["payload"]);
2416
+ const url = readStringOr(data["url"]);
2417
+ if (!regex.test(url) && !regex.test(payload)) {
2418
+ return;
2419
+ }
2420
+ if (where && !this.payloadMatchesWhere(payload, where)) {
2421
+ return;
2422
+ }
2423
+ cleanup();
2424
+ resolve({
2425
+ requestId: readStringOr(data["connectionId"]),
2426
+ url,
2427
+ payload
2428
+ });
2429
+ } catch {
2430
+ }
2431
+ };
2432
+ const timer = setTimeout(() => {
2433
+ cleanup();
2434
+ reject(new Error(`Timed out waiting for WebSocket message matching ${match}`));
2435
+ }, timeout);
2436
+ this.page.cdpClient.on("Network.webSocketCreated", onCreated);
2437
+ this.page.cdpClient.on("Network.webSocketFrameReceived", onFrame);
2438
+ this.page.cdpClient.on("Runtime.bindingCalled", onBinding);
2439
+ });
2440
+ }
2441
+ payloadMatchesWhere(payload, where) {
2442
+ try {
2443
+ const parsed = JSON.parse(payload);
2444
+ return Object.entries(where).every(([key, expected]) => {
2445
+ const actual = key.split(".").reduce((current, part) => {
2446
+ if (!current || typeof current !== "object") {
2447
+ return void 0;
2448
+ }
2449
+ return current[part];
2450
+ }, parsed);
2451
+ return actual === expected;
2452
+ });
2453
+ } catch {
2454
+ return false;
2455
+ }
2456
+ }
2457
+ async findRecentWsMessage(regex, where) {
2458
+ const recent = await this.page.evaluate(
2459
+ "(() => Array.isArray(globalThis.__bpTraceRecentEvents) ? globalThis.__bpTraceRecentEvents : [])()"
2460
+ );
2461
+ if (!Array.isArray(recent)) {
2462
+ return null;
2463
+ }
2464
+ for (let i = recent.length - 1; i >= 0; i--) {
2465
+ const entry = recent[i];
2466
+ if (!entry || typeof entry !== "object") {
2467
+ continue;
2468
+ }
2469
+ const record = entry;
2470
+ const event = readStringOr(record["event"]);
2471
+ if (event !== "ws.frame.received") {
2472
+ continue;
2473
+ }
2474
+ const data = record["data"] ?? {};
2475
+ const payload = readStringOr(data["payload"]);
2476
+ const url = readStringOr(data["url"]);
2477
+ if (!regex.test(url) && !regex.test(payload)) {
2478
+ continue;
2479
+ }
2480
+ if (where && !this.payloadMatchesWhere(payload, where)) {
2481
+ continue;
2482
+ }
2483
+ return {
2484
+ requestId: readStringOr(data["connectionId"]),
2485
+ url,
2486
+ payload
2487
+ };
2488
+ }
2489
+ return null;
2490
+ }
2491
+ async assertNoConsoleErrors(windowMs) {
2492
+ await this.page.cdpClient.send("Runtime.enable");
2493
+ return new Promise((resolve, reject) => {
2494
+ const errors = [];
2495
+ const cleanup = () => {
2496
+ this.page.cdpClient.off("Runtime.consoleAPICalled", onConsole);
2497
+ this.page.cdpClient.off("Runtime.exceptionThrown", onException);
2498
+ clearTimeout(timer);
2499
+ };
2500
+ const onConsole = (params) => {
2501
+ if (params["type"] !== "error") {
2502
+ return;
2503
+ }
2504
+ const args = Array.isArray(params["args"]) ? params["args"] : [];
2505
+ errors.push(args.map(formatConsoleArg).filter(Boolean).join(" "));
2506
+ };
2507
+ const onException = (params) => {
2508
+ const details = params["exceptionDetails"] ?? {};
2509
+ errors.push(readString(details["text"]) ?? "Runtime exception");
2510
+ };
2511
+ const timer = setTimeout(() => {
2512
+ cleanup();
2513
+ if (errors.length > 0) {
2514
+ reject(new Error(`Console errors detected: ${errors.join(" | ")}`));
2515
+ return;
2516
+ }
2517
+ resolve();
2518
+ }, windowMs);
2519
+ this.page.cdpClient.on("Runtime.consoleAPICalled", onConsole);
2520
+ this.page.cdpClient.on("Runtime.exceptionThrown", onException);
2521
+ });
2522
+ }
2523
+ async assertTextChanged(selector, from, to, timeout) {
2524
+ const initialText = from ?? await this.page.text(selector);
2525
+ const deadline = Date.now() + timeout;
2526
+ while (Date.now() < deadline) {
2527
+ const text = await this.page.text(selector);
2528
+ if (text !== initialText && text.includes(to)) {
2529
+ return text;
2530
+ }
2531
+ await new Promise((resolve) => setTimeout(resolve, 200));
2532
+ }
2533
+ throw new Error(`Text did not change to include ${JSON.stringify(to)}`);
2534
+ }
2535
+ async assertPermission(name, state) {
2536
+ const result = await this.page.evaluate(
2537
+ `(() => navigator.permissions.query({ name: ${JSON.stringify(name)} }).then((status) => ({ name: ${JSON.stringify(name)}, state: status.state })))()`
2538
+ );
2539
+ if (!result || typeof result !== "object" || result.state !== state) {
2540
+ throw new Error(`Permission ${name} is not ${state}`);
2541
+ }
2542
+ return result;
2543
+ }
2544
+ async assertMediaTrackLive(kind) {
2545
+ const result = await this.page.evaluate(
2546
+ `(() => {
2547
+ const requestedKind = ${JSON.stringify(kind)};
2548
+ const mediaElements = Array.from(document.querySelectorAll('audio,video')).map((el) => {
2549
+ const tracks = [];
2550
+ if (el.srcObject && typeof el.srcObject.getTracks === 'function') {
2551
+ tracks.push(...el.srcObject.getTracks());
2552
+ }
2553
+ return {
2554
+ tag: el.tagName.toLowerCase(),
2555
+ paused: !!el.paused,
2556
+ tracks: tracks.map((track) => ({
2557
+ kind: track.kind,
2558
+ readyState: track.readyState,
2559
+ enabled: track.enabled,
2560
+ label: track.label,
2561
+ })),
2562
+ };
2563
+ });
2564
+
2565
+ const globalTracks =
2566
+ window.__bpStream && typeof window.__bpStream.getTracks === 'function'
2567
+ ? window.__bpStream.getTracks().map((track) => ({
2568
+ kind: track.kind,
2569
+ readyState: track.readyState,
2570
+ enabled: track.enabled,
2571
+ label: track.label,
2572
+ }))
2573
+ : [];
2574
+
2575
+ const liveTracks = mediaElements
2576
+ .flatMap((entry) => entry.tracks)
2577
+ .concat(globalTracks)
2578
+ .filter((track) => track.kind === requestedKind && track.readyState === 'live');
2579
+
2580
+ return { live: liveTracks.length > 0, mediaElements, globalTracks, liveTracks };
2581
+ })()`
2582
+ );
2583
+ if (!result || typeof result !== "object" || !result.live) {
2584
+ throw new Error(`No live ${kind} media track detected`);
2585
+ }
2586
+ return result;
2587
+ }
1594
2588
  };
1595
2589
  function addBatchToPage(page) {
1596
2590
  const executor = new BatchExecutor(page);
@@ -1721,7 +2715,7 @@ var ACTION_RULES = {
1721
2715
  value: { type: "string|string[]" },
1722
2716
  trigger: { type: "string|string[]" },
1723
2717
  option: { type: "string|string[]" },
1724
- match: { type: "string", enum: ["text", "value", "contains"] }
2718
+ match: { type: "string" }
1725
2719
  }
1726
2720
  },
1727
2721
  check: {
@@ -1852,6 +2846,38 @@ var ACTION_RULES = {
1852
2846
  expect: { type: "string" },
1853
2847
  value: { type: "string" }
1854
2848
  }
2849
+ },
2850
+ waitForWsMessage: {
2851
+ required: { match: { type: "string" } },
2852
+ optional: {
2853
+ where: { type: "object" }
2854
+ }
2855
+ },
2856
+ assertNoConsoleErrors: {
2857
+ required: {},
2858
+ optional: {
2859
+ windowMs: { type: "number" }
2860
+ }
2861
+ },
2862
+ assertTextChanged: {
2863
+ required: { to: { type: "string" } },
2864
+ optional: {
2865
+ selector: { type: "string|string[]" },
2866
+ from: { type: "string" }
2867
+ }
2868
+ },
2869
+ assertPermission: {
2870
+ required: {
2871
+ name: { type: "string" },
2872
+ state: { type: "string" }
2873
+ },
2874
+ optional: {}
2875
+ },
2876
+ assertMediaTrackLive: {
2877
+ required: {
2878
+ kind: { type: "string", enum: ["audio", "video"] }
2879
+ },
2880
+ optional: {}
1855
2881
  }
1856
2882
  };
1857
2883
  var VALID_ACTIONS = Object.keys(ACTION_RULES);
@@ -1875,6 +2901,7 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
1875
2901
  "trigger",
1876
2902
  "option",
1877
2903
  "match",
2904
+ "where",
1878
2905
  "x",
1879
2906
  "y",
1880
2907
  "direction",
@@ -1884,7 +2911,13 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
1884
2911
  "fullPage",
1885
2912
  "expect",
1886
2913
  "retry",
1887
- "retryDelay"
2914
+ "retryDelay",
2915
+ "from",
2916
+ "to",
2917
+ "name",
2918
+ "state",
2919
+ "kind",
2920
+ "windowMs"
1888
2921
  ]);
1889
2922
  function resolveAction(name) {
1890
2923
  if (VALID_ACTIONS.includes(name)) {
@@ -1957,6 +2990,11 @@ function checkFieldType(value, rule) {
1957
2990
  return `expected boolean or "auto", got ${typeof value}`;
1958
2991
  }
1959
2992
  return null;
2993
+ case "object":
2994
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2995
+ return `expected object, got ${Array.isArray(value) ? "array" : typeof value}`;
2996
+ }
2997
+ return null;
1960
2998
  default: {
1961
2999
  const _exhaustive = rule.type;
1962
3000
  return `unknown type: ${_exhaustive}`;
@@ -2241,12 +3279,12 @@ function parseWavHeader(data) {
2241
3279
  if (data.byteLength < 44) {
2242
3280
  throw new Error("Invalid WAV: file too small");
2243
3281
  }
2244
- const riff = readString(view, 0, 4);
2245
- const wave = readString(view, 8, 4);
3282
+ const riff = readString2(view, 0, 4);
3283
+ const wave = readString2(view, 8, 4);
2246
3284
  if (riff !== "RIFF" || wave !== "WAVE") {
2247
3285
  throw new Error("Invalid WAV: missing RIFF/WAVE header");
2248
3286
  }
2249
- const fmt = readString(view, 12, 4);
3287
+ const fmt = readString2(view, 12, 4);
2250
3288
  if (fmt !== "fmt ") {
2251
3289
  throw new Error("Invalid WAV: missing fmt chunk");
2252
3290
  }
@@ -2255,7 +3293,7 @@ function parseWavHeader(data) {
2255
3293
  const bitsPerSample = view.getUint16(34, true);
2256
3294
  let dataOffset = 36;
2257
3295
  while (dataOffset < data.byteLength - 8) {
2258
- const chunkId = readString(view, dataOffset, 4);
3296
+ const chunkId = readString2(view, dataOffset, 4);
2259
3297
  const chunkSize = view.getUint32(dataOffset + 4, true);
2260
3298
  if (chunkId === "data") {
2261
3299
  return {
@@ -2286,7 +3324,7 @@ function writeString(view, offset, str) {
2286
3324
  view.setUint8(offset + i, str.charCodeAt(i));
2287
3325
  }
2288
3326
  }
2289
- function readString(view, offset, length) {
3327
+ function readString2(view, offset, length) {
2290
3328
  let str = "";
2291
3329
  for (let i = 0; i < length; i++) {
2292
3330
  str += String.fromCharCode(view.getUint8(offset + i));
@@ -2320,6 +3358,10 @@ async function grantAudioPermissions(cdp, origin) {
2320
3358
  await cdp.send("Page.addScriptToEvaluateOnNewDocument", {
2321
3359
  source: PERMISSIONS_OVERRIDE_SCRIPT
2322
3360
  });
3361
+ await cdp.send("Runtime.evaluate", {
3362
+ expression: PERMISSIONS_OVERRIDE_SCRIPT,
3363
+ awaitPromise: false
3364
+ });
2323
3365
  }
2324
3366
  var PERMISSIONS_OVERRIDE_SCRIPT = `
2325
3367
  (function() {
@@ -3756,7 +4798,8 @@ function buildCDPClient(transport, options = {}) {
3756
4798
  pending.delete(response.id);
3757
4799
  clearTimeout(request.timer);
3758
4800
  if (response.error) {
3759
- request.reject(new CDPError(response.error));
4801
+ const error = typeof response.error === "string" ? { code: -32e3, message: response.error } : response.error;
4802
+ request.reject(new CDPError(error));
3760
4803
  } else {
3761
4804
  request.resolve(response.result);
3762
4805
  }
@@ -3872,6 +4915,9 @@ function buildCDPClient(transport, options = {}) {
3872
4915
  get sessionId() {
3873
4916
  return currentSessionId;
3874
4917
  },
4918
+ setSessionId(sessionId) {
4919
+ currentSessionId = sessionId;
4920
+ },
3875
4921
  get isConnected() {
3876
4922
  return connected;
3877
4923
  }
@@ -4067,6 +5113,330 @@ async function getBrowserWebSocketUrl(host = "localhost:9222") {
4067
5113
  return info.webSocketDebuggerUrl;
4068
5114
  }
4069
5115
 
5116
+ // src/providers/local-discovery.ts
5117
+ var CHANNEL_ORDER = ["stable", "beta", "dev", "canary"];
5118
+ var DEFAULT_PROBE_TIMEOUT_MS = 1e3;
5119
+ var DevToolsActivePortParseError = class extends Error {
5120
+ constructor(message, reason) {
5121
+ super(message);
5122
+ this.reason = reason;
5123
+ this.name = "DevToolsActivePortParseError";
5124
+ }
5125
+ };
5126
+ function getRuntimeEnv() {
5127
+ if (typeof process === "undefined") {
5128
+ return {};
5129
+ }
5130
+ return process.env;
5131
+ }
5132
+ function getRuntimePlatform() {
5133
+ if (typeof process === "undefined") {
5134
+ return void 0;
5135
+ }
5136
+ return process.platform;
5137
+ }
5138
+ function normalizePlatform(platform) {
5139
+ if (platform === "darwin" || platform === "linux" || platform === "win32") {
5140
+ return platform;
5141
+ }
5142
+ throw new Error(`Unsupported platform: ${platform ?? "unknown"}`);
5143
+ }
5144
+ function trimTrailingSeparator(path) {
5145
+ return path.replace(/[\\/]+$/, "");
5146
+ }
5147
+ function joinPath(platform, ...parts) {
5148
+ const separator = platform === "win32" ? "\\" : "/";
5149
+ const cleaned = parts.map((part, index) => {
5150
+ if (index === 0) return trimTrailingSeparator(part);
5151
+ return part.replace(/^[\\/]+/, "").replace(/[\\/]+$/, "");
5152
+ }).filter((part) => part.length > 0);
5153
+ return cleaned.join(separator);
5154
+ }
5155
+ function resolveHomeDir(platform, env, explicitHomeDir) {
5156
+ if (explicitHomeDir) {
5157
+ return explicitHomeDir;
5158
+ }
5159
+ if (platform === "win32") {
5160
+ return env["USERPROFILE"] ?? env["HOME"] ?? "";
5161
+ }
5162
+ return env["HOME"] ?? env["USERPROFILE"] ?? "";
5163
+ }
5164
+ function toFileFailure(target, error) {
5165
+ const errno = error?.code;
5166
+ if (errno === "ENOENT") {
5167
+ return {
5168
+ ...target,
5169
+ reason: "missing-file",
5170
+ message: `DevToolsActivePort not found at ${target.portFile}`
5171
+ };
5172
+ }
5173
+ return {
5174
+ ...target,
5175
+ reason: "unreadable-file",
5176
+ message: error instanceof Error ? error.message : `Could not read DevToolsActivePort at ${target.portFile}`
5177
+ };
5178
+ }
5179
+ function toProbeFailure(target, wsUrl, error) {
5180
+ const message = error instanceof Error ? error.message : String(error);
5181
+ const lowerMessage = message.toLowerCase();
5182
+ let reason = "connection-error";
5183
+ if (lowerMessage.includes("refused") || lowerMessage.includes("econnrefused")) {
5184
+ reason = "connection-refused";
5185
+ } else if (lowerMessage.includes("timeout") || lowerMessage.includes("timed out")) {
5186
+ reason = "connection-timeout";
5187
+ } else if (lowerMessage.includes("closed")) {
5188
+ reason = "unexpected-close";
5189
+ } else if (lowerMessage.includes("browser.getversion") || lowerMessage.includes("cdp") || lowerMessage.includes("protocol")) {
5190
+ reason = "cdp-error";
5191
+ }
5192
+ return {
5193
+ ...target,
5194
+ wsUrl,
5195
+ reason,
5196
+ message
5197
+ };
5198
+ }
5199
+ async function readTextFile(path) {
5200
+ const fs2 = await import("fs/promises");
5201
+ return fs2.readFile(path, "utf-8");
5202
+ }
5203
+ async function probeBrowserWebSocket(wsUrl, timeoutMs) {
5204
+ let client;
5205
+ try {
5206
+ client = await createCDPClient(wsUrl, { timeout: timeoutMs });
5207
+ const version = await client.send("Browser.getVersion", void 0, null);
5208
+ return { browserVersion: version.product };
5209
+ } finally {
5210
+ await client?.close().catch(() => {
5211
+ });
5212
+ }
5213
+ }
5214
+ var defaultDependencies = {
5215
+ readTextFile,
5216
+ probeBrowserWebSocket,
5217
+ getLegacyBrowserWebSocketUrl: getBrowserWebSocketUrl
5218
+ };
5219
+ function resolveChromeUserDataDirs(options = {}) {
5220
+ const env = options.env ?? getRuntimeEnv();
5221
+ const platform = normalizePlatform(options.platform ?? getRuntimePlatform());
5222
+ const homeDir = resolveHomeDir(platform, env, options.homeDir);
5223
+ if (!homeDir) {
5224
+ throw new Error("Could not determine home directory for local Chrome discovery");
5225
+ }
5226
+ switch (platform) {
5227
+ case "darwin": {
5228
+ const base = joinPath(platform, homeDir, "Library", "Application Support", "Google");
5229
+ return {
5230
+ stable: joinPath(platform, base, "Chrome"),
5231
+ beta: joinPath(platform, base, "Chrome Beta"),
5232
+ dev: joinPath(platform, base, "Chrome Dev"),
5233
+ canary: joinPath(platform, base, "Chrome Canary")
5234
+ };
5235
+ }
5236
+ case "linux": {
5237
+ const configHome = env["CHROME_CONFIG_HOME"] ?? env["XDG_CONFIG_HOME"] ?? joinPath(platform, homeDir, ".config");
5238
+ return {
5239
+ stable: joinPath(platform, configHome, "google-chrome"),
5240
+ beta: joinPath(platform, configHome, "google-chrome-beta"),
5241
+ dev: joinPath(platform, configHome, "google-chrome-dev"),
5242
+ canary: joinPath(platform, configHome, "google-chrome-canary")
5243
+ };
5244
+ }
5245
+ case "win32": {
5246
+ const localAppData = env["LOCALAPPDATA"] ?? joinPath(platform, homeDir, "AppData", "Local");
5247
+ const base = joinPath(platform, localAppData, "Google");
5248
+ return {
5249
+ stable: joinPath(platform, base, "Chrome", "User Data"),
5250
+ beta: joinPath(platform, base, "Chrome Beta", "User Data"),
5251
+ dev: joinPath(platform, base, "Chrome Dev", "User Data"),
5252
+ canary: joinPath(platform, base, "Chrome SxS", "User Data")
5253
+ };
5254
+ }
5255
+ }
5256
+ throw new Error(`Unsupported platform for local Chrome discovery: ${platform}`);
5257
+ }
5258
+ function buildLocalBrowserScanTargets(options = {}) {
5259
+ const env = options.env ?? getRuntimeEnv();
5260
+ const platform = normalizePlatform(options.platform ?? getRuntimePlatform());
5261
+ if (options.userDataDir) {
5262
+ return [
5263
+ {
5264
+ channel: options.channel ?? "custom",
5265
+ userDataDir: options.userDataDir,
5266
+ portFile: joinPath(platform, options.userDataDir, "DevToolsActivePort")
5267
+ }
5268
+ ];
5269
+ }
5270
+ const dirs = resolveChromeUserDataDirs({
5271
+ platform,
5272
+ env,
5273
+ homeDir: options.homeDir
5274
+ });
5275
+ const channels = options.channel ? [options.channel] : CHANNEL_ORDER;
5276
+ return channels.map((channel) => ({
5277
+ channel,
5278
+ userDataDir: dirs[channel],
5279
+ portFile: joinPath(platform, dirs[channel], "DevToolsActivePort")
5280
+ }));
5281
+ }
5282
+ function parseDevToolsActivePortFile(content) {
5283
+ const lines = content.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0);
5284
+ if (lines.length !== 2) {
5285
+ throw new DevToolsActivePortParseError(
5286
+ `Expected exactly 2 non-empty lines in DevToolsActivePort, got ${lines.length}`,
5287
+ "malformed-file"
5288
+ );
5289
+ }
5290
+ const portText = lines[0];
5291
+ const browserPath = lines[1];
5292
+ const port = Number.parseInt(portText, 10);
5293
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
5294
+ throw new DevToolsActivePortParseError(
5295
+ `Invalid DevToolsActivePort port: ${portText}`,
5296
+ "invalid-port"
5297
+ );
5298
+ }
5299
+ if (!browserPath.startsWith("/devtools/browser/") || browserPath.includes("..") || /[?#\s\\]/u.test(browserPath)) {
5300
+ throw new DevToolsActivePortParseError(
5301
+ `Invalid DevToolsActivePort browser path: ${browserPath}`,
5302
+ "invalid-path"
5303
+ );
5304
+ }
5305
+ return {
5306
+ port,
5307
+ browserPath,
5308
+ wsUrl: `ws://127.0.0.1:${port}${browserPath}`
5309
+ };
5310
+ }
5311
+ async function inspectScanTarget(target, options, deps) {
5312
+ let content;
5313
+ try {
5314
+ content = await deps.readTextFile(target.portFile);
5315
+ } catch (error) {
5316
+ return { kind: "failure", failure: toFileFailure(target, error) };
5317
+ }
5318
+ let parsed;
5319
+ try {
5320
+ parsed = parseDevToolsActivePortFile(content);
5321
+ } catch (error) {
5322
+ if (error instanceof DevToolsActivePortParseError) {
5323
+ return {
5324
+ kind: "failure",
5325
+ failure: {
5326
+ ...target,
5327
+ reason: error.reason,
5328
+ message: error.message
5329
+ }
5330
+ };
5331
+ }
5332
+ throw error;
5333
+ }
5334
+ try {
5335
+ const probe = await deps.probeBrowserWebSocket(
5336
+ parsed.wsUrl,
5337
+ options.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS
5338
+ );
5339
+ return {
5340
+ kind: "candidate",
5341
+ candidate: {
5342
+ ...target,
5343
+ port: parsed.port,
5344
+ browserPath: parsed.browserPath,
5345
+ wsUrl: parsed.wsUrl,
5346
+ browserVersion: probe.browserVersion
5347
+ }
5348
+ };
5349
+ } catch (error) {
5350
+ return {
5351
+ kind: "failure",
5352
+ failure: toProbeFailure(target, parsed.wsUrl, error)
5353
+ };
5354
+ }
5355
+ }
5356
+ async function discoverLocalBrowsers(options = {}, deps = defaultDependencies) {
5357
+ const scanTargets = buildLocalBrowserScanTargets(options);
5358
+ const outcomes = await Promise.all(
5359
+ scanTargets.map((target) => inspectScanTarget(target, options, deps))
5360
+ );
5361
+ const candidates = [];
5362
+ const failures = [];
5363
+ for (const outcome of outcomes) {
5364
+ if (outcome.kind === "candidate") {
5365
+ candidates.push(outcome.candidate);
5366
+ } else {
5367
+ failures.push(outcome.failure);
5368
+ }
5369
+ }
5370
+ return { candidates, failures };
5371
+ }
5372
+ var BrowserEndpointResolutionError = class extends Error {
5373
+ constructor(code, message, details = {}) {
5374
+ super(message);
5375
+ this.code = code;
5376
+ this.details = details;
5377
+ }
5378
+ name = "BrowserEndpointResolutionError";
5379
+ };
5380
+ async function resolveBrowserEndpoint(options = {}, deps = defaultDependencies) {
5381
+ if (options.explicitWsUrl) {
5382
+ return {
5383
+ wsUrl: options.explicitWsUrl,
5384
+ source: "explicit-ws"
5385
+ };
5386
+ }
5387
+ let localDiscovery;
5388
+ if (options.allowLocalDiscovery ?? true) {
5389
+ localDiscovery = await discoverLocalBrowsers(options, deps);
5390
+ if (localDiscovery.candidates.length === 1) {
5391
+ const candidate = localDiscovery.candidates[0];
5392
+ return {
5393
+ wsUrl: candidate.wsUrl,
5394
+ source: "devtools-active-port",
5395
+ channel: candidate.channel,
5396
+ userDataDir: candidate.userDataDir
5397
+ };
5398
+ }
5399
+ if (localDiscovery.candidates.length > 1) {
5400
+ throw new BrowserEndpointResolutionError(
5401
+ "multiple-local-browsers",
5402
+ "Multiple local Chrome profiles are available for auto-discovery",
5403
+ {
5404
+ candidates: localDiscovery.candidates,
5405
+ failures: localDiscovery.failures
5406
+ }
5407
+ );
5408
+ }
5409
+ }
5410
+ if (options.allowLegacyHostFallback ?? true) {
5411
+ const legacyHost = options.legacyHost ?? "localhost:9222";
5412
+ try {
5413
+ return {
5414
+ wsUrl: await deps.getLegacyBrowserWebSocketUrl(legacyHost),
5415
+ source: "json-version"
5416
+ };
5417
+ } catch (error) {
5418
+ throw new BrowserEndpointResolutionError(
5419
+ "browser-not-found",
5420
+ "Could not resolve a browser endpoint",
5421
+ {
5422
+ candidates: localDiscovery?.candidates,
5423
+ failures: localDiscovery?.failures,
5424
+ legacyError: error instanceof Error ? error : new Error(String(error)),
5425
+ legacyHost
5426
+ }
5427
+ );
5428
+ }
5429
+ }
5430
+ throw new BrowserEndpointResolutionError(
5431
+ "browser-not-found",
5432
+ "Could not resolve a browser endpoint",
5433
+ {
5434
+ candidates: localDiscovery?.candidates,
5435
+ failures: localDiscovery?.failures
5436
+ }
5437
+ );
5438
+ }
5439
+
4070
5440
  // src/providers/index.ts
4071
5441
  function createProvider(options) {
4072
5442
  switch (options.provider) {
@@ -8161,13 +9531,26 @@ var Browser = class _Browser {
8161
9531
  * Connect to a browser instance
8162
9532
  */
8163
9533
  static async connect(options) {
8164
- const provider = createProvider(options);
8165
- const session = await provider.createSession(options.session);
9534
+ let connectOptions = options;
9535
+ if (options.provider === "generic" && !options.wsUrl) {
9536
+ const endpoint = await resolveBrowserEndpoint({
9537
+ channel: options.channel,
9538
+ userDataDir: options.userDataDir,
9539
+ allowLocalDiscovery: true,
9540
+ allowLegacyHostFallback: true
9541
+ });
9542
+ connectOptions = {
9543
+ ...options,
9544
+ wsUrl: endpoint.wsUrl
9545
+ };
9546
+ }
9547
+ const provider = createProvider(connectOptions);
9548
+ const session = await provider.createSession(connectOptions.session);
8166
9549
  const cdp = await createCDPClient(session.wsUrl, {
8167
- debug: options.debug,
8168
- timeout: options.timeout
9550
+ debug: connectOptions.debug,
9551
+ timeout: connectOptions.timeout
8169
9552
  });
8170
- return new _Browser(cdp, provider, session, options);
9553
+ return new _Browser(cdp, provider, session, connectOptions);
8171
9554
  }
8172
9555
  /**
8173
9556
  * Get or create a page by name.
@@ -8573,6 +9956,7 @@ function disableTracing() {
8573
9956
  BatchExecutor,
8574
9957
  Browser,
8575
9958
  BrowserBaseProvider,
9959
+ BrowserEndpointResolutionError,
8576
9960
  BrowserlessProvider,
8577
9961
  CDPError,
8578
9962
  ElementNotFoundError,
@@ -8584,12 +9968,14 @@ function disableTracing() {
8584
9968
  Tracer,
8585
9969
  addBatchToPage,
8586
9970
  bufferToBase64,
9971
+ buildLocalBrowserScanTargets,
8587
9972
  calculateRMS,
8588
9973
  connect,
8589
9974
  createCDPClient,
8590
9975
  createProvider,
8591
9976
  devices,
8592
9977
  disableTracing,
9978
+ discoverLocalBrowsers,
8593
9979
  discoverTargets,
8594
9980
  enableTracing,
8595
9981
  generateSilence,
@@ -8599,8 +9985,11 @@ function disableTracing() {
8599
9985
  getTracer,
8600
9986
  grantAudioPermissions,
8601
9987
  isTranscriptionAvailable,
9988
+ parseDevToolsActivePortFile,
8602
9989
  parseWavHeader,
8603
9990
  pcmToWav,
9991
+ resolveBrowserEndpoint,
9992
+ resolveChromeUserDataDirs,
8604
9993
  transcribe,
8605
9994
  validateSteps,
8606
9995
  waitForAnyElement,