browser-pilot 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -62,6 +72,230 @@ __export(src_exports, {
62
72
  });
63
73
  module.exports = __toCommonJS(src_exports);
64
74
 
75
+ // src/actions/executor.ts
76
+ var fs = __toESM(require("fs"), 1);
77
+ var import_node_path = require("path");
78
+
79
+ // src/recording/redaction.ts
80
+ var REDACTED_VALUE = "[REDACTED]";
81
+ var SENSITIVE_AUTOCOMPLETE_TOKENS = [
82
+ "current-password",
83
+ "new-password",
84
+ "one-time-code",
85
+ "cc-number",
86
+ "cc-csc",
87
+ "cc-exp",
88
+ "cc-exp-month",
89
+ "cc-exp-year"
90
+ ];
91
+ function autocompleteTokens(autocomplete) {
92
+ if (!autocomplete) return [];
93
+ return autocomplete.toLowerCase().split(/\s+/).map((token) => token.trim()).filter(Boolean);
94
+ }
95
+ function isSensitiveFieldMetadata(metadata) {
96
+ if (!metadata) return false;
97
+ if (metadata.sensitiveValue) return true;
98
+ const inputType = metadata.inputType?.toLowerCase();
99
+ if (inputType === "password" || inputType === "hidden") {
100
+ return true;
101
+ }
102
+ const sensitiveAutocompleteTokens = new Set(SENSITIVE_AUTOCOMPLETE_TOKENS);
103
+ return autocompleteTokens(metadata.autocomplete).some(
104
+ (token) => sensitiveAutocompleteTokens.has(token)
105
+ );
106
+ }
107
+ function redactValueForRecording(value, metadata) {
108
+ if (value === void 0) return void 0;
109
+ return isSensitiveFieldMetadata(metadata) ? REDACTED_VALUE : value;
110
+ }
111
+
112
+ // src/browser/action-highlight.ts
113
+ var HIGHLIGHT_STYLES = {
114
+ click: { outline: "3px solid rgba(229,57,53,0.8)", badge: "#e53935", marker: "crosshair" },
115
+ fill: { outline: "3px solid rgba(33,150,243,0.8)", badge: "#2196f3" },
116
+ type: { outline: "3px solid rgba(33,150,243,0.6)", badge: "#2196f3" },
117
+ select: { outline: "3px solid rgba(156,39,176,0.8)", badge: "#9c27b0" },
118
+ hover: { outline: "2px dashed rgba(158,158,158,0.5)", badge: "#9e9e9e" },
119
+ scroll: { outline: "none", badge: "#607d8b", marker: "arrow" },
120
+ navigate: { outline: "none", badge: "#4caf50" },
121
+ submit: { outline: "3px solid rgba(255,152,0,0.8)", badge: "#ff9800" },
122
+ "assert-pass": { outline: "3px solid rgba(76,175,80,0.8)", badge: "#4caf50", marker: "check" },
123
+ "assert-fail": { outline: "3px solid rgba(244,67,54,0.8)", badge: "#f44336", marker: "cross" },
124
+ evaluate: { outline: "none", badge: "#ffc107" },
125
+ focus: { outline: "3px dotted rgba(33,150,243,0.6)", badge: "#2196f3" }
126
+ };
127
+ function buildHighlightScript(options) {
128
+ const style = HIGHLIGHT_STYLES[options.kind];
129
+ const label = options.label ? options.label.slice(0, 80) : void 0;
130
+ const escapedLabel = label ? label.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n") : "";
131
+ return `(function() {
132
+ // Remove any existing highlight
133
+ var existing = document.getElementById('__bp-action-highlight');
134
+ if (existing) existing.remove();
135
+
136
+ var container = document.createElement('div');
137
+ container.id = '__bp-action-highlight';
138
+ container.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:99999;';
139
+
140
+ ${options.bbox ? `
141
+ // Element outline
142
+ var outline = document.createElement('div');
143
+ outline.style.cssText = 'position:fixed;' +
144
+ 'left:${options.bbox.x}px;top:${options.bbox.y}px;' +
145
+ 'width:${options.bbox.width}px;height:${options.bbox.height}px;' +
146
+ '${style.outline !== "none" ? `outline:${style.outline};outline-offset:-1px;` : ""}' +
147
+ 'pointer-events:none;box-sizing:border-box;';
148
+ container.appendChild(outline);
149
+ ` : ""}
150
+
151
+ ${options.point && style.marker === "crosshair" ? `
152
+ // Crosshair at click point
153
+ var hLine = document.createElement('div');
154
+ hLine.style.cssText = 'position:fixed;left:${options.point.x - 12}px;top:${options.point.y}px;' +
155
+ 'width:24px;height:2px;background:${style.badge};pointer-events:none;';
156
+ var vLine = document.createElement('div');
157
+ vLine.style.cssText = 'position:fixed;left:${options.point.x}px;top:${options.point.y - 12}px;' +
158
+ 'width:2px;height:24px;background:${style.badge};pointer-events:none;';
159
+ // Dot at center
160
+ var dot = document.createElement('div');
161
+ dot.style.cssText = 'position:fixed;left:${options.point.x - 4}px;top:${options.point.y - 4}px;' +
162
+ 'width:8px;height:8px;border-radius:50%;background:${style.badge};pointer-events:none;';
163
+ container.appendChild(hLine);
164
+ container.appendChild(vLine);
165
+ container.appendChild(dot);
166
+ ` : ""}
167
+
168
+ ${label ? `
169
+ // Badge with label
170
+ var badge = document.createElement('div');
171
+ badge.style.cssText = 'position:fixed;' +
172
+ ${options.bbox ? `'left:${options.bbox.x}px;top:${Math.max(0, options.bbox.y - 28)}px;'` : options.kind === "navigate" ? "'left:50%;top:8px;transform:translateX(-50%);'" : "'right:8px;top:8px;'"} +
173
+ 'background:${style.badge};color:white;padding:4px 8px;' +
174
+ 'font-family:monospace;font-size:12px;font-weight:bold;' +
175
+ 'border-radius:3px;white-space:nowrap;max-width:400px;overflow:hidden;text-overflow:ellipsis;' +
176
+ 'pointer-events:none;';
177
+ badge.textContent = '${escapedLabel}';
178
+ container.appendChild(badge);
179
+ ` : ""}
180
+
181
+ ${style.marker === "check" && options.bbox ? `
182
+ // Checkmark
183
+ var check = document.createElement('div');
184
+ check.style.cssText = 'position:fixed;left:${options.bbox.x + options.bbox.width / 2 - 10}px;' +
185
+ 'top:${options.bbox.y + options.bbox.height / 2 - 10}px;' +
186
+ 'width:20px;height:20px;font-size:18px;color:${style.badge};pointer-events:none;text-align:center;line-height:20px;';
187
+ check.textContent = '\\u2713';
188
+ container.appendChild(check);
189
+ ` : ""}
190
+
191
+ ${style.marker === "cross" && options.bbox ? `
192
+ // Cross mark
193
+ var cross = document.createElement('div');
194
+ cross.style.cssText = 'position:fixed;left:${options.bbox.x + options.bbox.width / 2 - 10}px;' +
195
+ 'top:${options.bbox.y + options.bbox.height / 2 - 10}px;' +
196
+ 'width:20px;height:20px;font-size:18px;color:${style.badge};pointer-events:none;text-align:center;line-height:20px;font-weight:bold;';
197
+ cross.textContent = '\\u2717';
198
+ container.appendChild(cross);
199
+ ` : ""}
200
+
201
+ document.body.appendChild(container);
202
+ window.__bpRemoveActionHighlight = function() {
203
+ var el = document.getElementById('__bp-action-highlight');
204
+ if (el) el.remove();
205
+ delete window.__bpRemoveActionHighlight;
206
+ };
207
+ })();`;
208
+ }
209
+ async function injectActionHighlight(page, options) {
210
+ try {
211
+ await page.evaluate(buildHighlightScript(options));
212
+ } catch {
213
+ }
214
+ }
215
+ async function removeActionHighlight(page) {
216
+ try {
217
+ await page.evaluate(`(function() {
218
+ if (window.__bpRemoveActionHighlight) {
219
+ window.__bpRemoveActionHighlight();
220
+ }
221
+ })()`);
222
+ } catch {
223
+ }
224
+ }
225
+ function stepToHighlightKind(step) {
226
+ switch (step.action) {
227
+ case "click":
228
+ return "click";
229
+ case "fill":
230
+ return "fill";
231
+ case "type":
232
+ return "type";
233
+ case "select":
234
+ return "select";
235
+ case "hover":
236
+ return "hover";
237
+ case "scroll":
238
+ return "scroll";
239
+ case "goto":
240
+ return "navigate";
241
+ case "submit":
242
+ return "submit";
243
+ case "focus":
244
+ return "focus";
245
+ case "evaluate":
246
+ case "press":
247
+ case "shortcut":
248
+ return "evaluate";
249
+ case "assertVisible":
250
+ case "assertExists":
251
+ case "assertText":
252
+ case "assertUrl":
253
+ case "assertValue":
254
+ return step.success ? "assert-pass" : "assert-fail";
255
+ // Observation-only actions — no highlight
256
+ case "wait":
257
+ case "snapshot":
258
+ case "forms":
259
+ case "text":
260
+ case "screenshot":
261
+ case "newTab":
262
+ case "closeTab":
263
+ case "switchFrame":
264
+ case "switchToMain":
265
+ return null;
266
+ default:
267
+ return null;
268
+ }
269
+ }
270
+ function getHighlightLabel(step, result, targetMetadata) {
271
+ switch (step.action) {
272
+ case "fill":
273
+ case "type":
274
+ return typeof step.value === "string" ? `"${redactValueForRecording(step.value, targetMetadata)}"` : void 0;
275
+ case "select":
276
+ return redactValueForRecording(
277
+ typeof step.value === "string" ? step.value : void 0,
278
+ targetMetadata
279
+ );
280
+ case "goto":
281
+ return step.url;
282
+ case "evaluate":
283
+ return "JS";
284
+ case "press":
285
+ return step.key;
286
+ case "shortcut":
287
+ return step.combo;
288
+ case "assertText":
289
+ case "assertUrl":
290
+ case "assertValue":
291
+ case "assertVisible":
292
+ case "assertExists":
293
+ return result.success ? "\u2713" : "\u2717";
294
+ default:
295
+ return void 0;
296
+ }
297
+ }
298
+
65
299
  // src/browser/actionability.ts
66
300
  var ActionabilityError = class extends Error {
67
301
  failureType;
@@ -686,8 +920,677 @@ var CDPError = class extends Error {
686
920
  }
687
921
  };
688
922
 
923
+ // src/trace/views.ts
924
+ function takeRecent(events, limit = 5) {
925
+ return events.slice(-limit).map((event) => ({
926
+ ts: event.ts,
927
+ event: event.event,
928
+ summary: event.summary,
929
+ severity: event.severity,
930
+ url: event.url
931
+ }));
932
+ }
933
+ function buildTraceSummaries(events) {
934
+ return {
935
+ ws: summarizeWs(events),
936
+ voice: summarizeVoice(events),
937
+ console: summarizeConsole(events),
938
+ permissions: summarizePermissions(events),
939
+ media: summarizeMedia(events),
940
+ ui: summarizeUi(events),
941
+ session: summarizeSession(events)
942
+ };
943
+ }
944
+ function summarizeWs(events) {
945
+ const relevant = events.filter((event) => event.channel === "ws" || event.event.startsWith("ws."));
946
+ const connections = /* @__PURE__ */ new Map();
947
+ for (const event of relevant) {
948
+ const id = event.connectionId ?? event.requestId ?? event.traceId;
949
+ let connection = connections.get(id);
950
+ if (!connection) {
951
+ connection = { id, sent: 0, received: 0, lastMessages: [] };
952
+ connections.set(id, connection);
953
+ }
954
+ connection.url = event.url ?? connection.url;
955
+ if (event.event === "ws.connection.created") {
956
+ connection.createdAt = event.ts;
957
+ }
958
+ if (event.event === "ws.connection.closed") {
959
+ connection.closedAt = event.ts;
960
+ }
961
+ if (event.event === "ws.frame.sent") {
962
+ connection.sent += 1;
963
+ const payload = typeof event.data["payload"] === "string" ? event.data["payload"] : "";
964
+ if (payload) connection.lastMessages.push(`sent: ${payload}`);
965
+ }
966
+ if (event.event === "ws.frame.received") {
967
+ connection.received += 1;
968
+ const payload = typeof event.data["payload"] === "string" ? event.data["payload"] : "";
969
+ if (payload) connection.lastMessages.push(`recv: ${payload}`);
970
+ }
971
+ connection.lastMessages = connection.lastMessages.slice(-3);
972
+ }
973
+ const values = [...connections.values()];
974
+ const reconnects = values.reduce((count, connection) => {
975
+ return connection.closedAt && !connection.createdAt ? count : count;
976
+ }, 0);
977
+ return {
978
+ view: "ws",
979
+ totalEvents: relevant.length,
980
+ connections: values.map((connection) => ({
981
+ id: connection.id,
982
+ url: connection.url ?? null,
983
+ createdAt: connection.createdAt ?? null,
984
+ closedAt: connection.closedAt ?? null,
985
+ sent: connection.sent,
986
+ received: connection.received,
987
+ lastMessages: connection.lastMessages,
988
+ connectedButSilent: !!connection.createdAt && !connection.closedAt && connection.sent + connection.received === 0
989
+ })),
990
+ reconnects,
991
+ recent: takeRecent(relevant)
992
+ };
993
+ }
994
+ function summarizeConsole(events) {
995
+ const relevant = events.filter(
996
+ (event) => event.channel === "console" || event.event.startsWith("console.") || event.event === "runtime.exception" || event.event === "runtime.unhandledRejection"
997
+ );
998
+ return {
999
+ view: "console",
1000
+ errors: relevant.filter(
1001
+ (event) => event.event === "console.error" || event.event === "runtime.exception" || event.event === "runtime.unhandledRejection"
1002
+ ).length,
1003
+ warnings: relevant.filter((event) => event.event === "console.warn").length,
1004
+ logs: relevant.filter((event) => event.event === "console.log").length,
1005
+ recent: takeRecent(relevant)
1006
+ };
1007
+ }
1008
+ function summarizePermissions(events) {
1009
+ const relevant = events.filter(
1010
+ (event) => event.channel === "permission" || event.event.startsWith("permission.")
1011
+ );
1012
+ const latest = /* @__PURE__ */ new Map();
1013
+ for (const event of relevant) {
1014
+ const name = typeof event.data["name"] === "string" ? event.data["name"] : null;
1015
+ const state = typeof event.data["state"] === "string" ? event.data["state"] : null;
1016
+ if (name && state) {
1017
+ latest.set(name, state);
1018
+ }
1019
+ }
1020
+ return {
1021
+ view: "permissions",
1022
+ states: Object.fromEntries(latest),
1023
+ changes: relevant.filter((event) => event.event === "permission.changed").length,
1024
+ recent: takeRecent(relevant)
1025
+ };
1026
+ }
1027
+ function summarizeMedia(events) {
1028
+ const relevant = events.filter(
1029
+ (event) => event.channel === "media" || event.event.startsWith("media.")
1030
+ );
1031
+ const liveTracks = /* @__PURE__ */ new Map();
1032
+ for (const event of relevant) {
1033
+ const label = typeof event.data["label"] === "string" ? event.data["label"] : "";
1034
+ const kind = typeof event.data["kind"] === "string" ? event.data["kind"] : "";
1035
+ const key = `${kind}:${label}`;
1036
+ if (event.event === "media.track.started") {
1037
+ liveTracks.set(key, kind);
1038
+ }
1039
+ if (event.event === "media.track.ended") {
1040
+ liveTracks.delete(key);
1041
+ }
1042
+ }
1043
+ return {
1044
+ view: "media",
1045
+ tracksStarted: relevant.filter((event) => event.event === "media.track.started").length,
1046
+ tracksEnded: relevant.filter((event) => event.event === "media.track.ended").length,
1047
+ playbackStarted: relevant.filter((event) => event.event === "media.playback.started").length,
1048
+ playbackStopped: relevant.filter((event) => event.event === "media.playback.stopped").length,
1049
+ liveTracks: [...liveTracks.values()],
1050
+ recent: takeRecent(relevant)
1051
+ };
1052
+ }
1053
+ function summarizeVoice(events) {
1054
+ const relevant = events.filter(
1055
+ (event) => event.channel === "voice" || event.event.startsWith("voice.")
1056
+ );
1057
+ return {
1058
+ view: "voice",
1059
+ ready: relevant.filter((event) => event.event === "voice.pipeline.ready").length,
1060
+ notReady: relevant.filter((event) => event.event === "voice.pipeline.notReady").length,
1061
+ captureStarted: relevant.filter((event) => event.event === "voice.capture.started").length,
1062
+ captureStopped: relevant.filter((event) => event.event === "voice.capture.stopped").length,
1063
+ detectedAudio: relevant.filter((event) => event.event === "voice.capture.detectedAudio").length,
1064
+ recent: takeRecent(relevant)
1065
+ };
1066
+ }
1067
+ function summarizeUi(events) {
1068
+ const relevant = events.filter(
1069
+ (event) => event.channel === "dom" || event.event.startsWith("dom.") || event.channel === "action"
1070
+ );
1071
+ return {
1072
+ view: "ui",
1073
+ actions: relevant.filter((event) => event.channel === "action").length,
1074
+ domChanges: relevant.filter((event) => event.channel === "dom").length,
1075
+ recent: takeRecent(relevant)
1076
+ };
1077
+ }
1078
+ function summarizeSession(events) {
1079
+ const byChannel = /* @__PURE__ */ new Map();
1080
+ const failedActions = events.filter((event) => event.event === "action.failed").length;
1081
+ for (const event of events) {
1082
+ byChannel.set(event.channel, (byChannel.get(event.channel) ?? 0) + 1);
1083
+ }
1084
+ return {
1085
+ view: "session",
1086
+ totalEvents: events.length,
1087
+ byChannel: Object.fromEntries(byChannel),
1088
+ failedActions,
1089
+ recent: takeRecent(events)
1090
+ };
1091
+ }
1092
+
1093
+ // src/recording/manifest.ts
1094
+ function isCanonicalRecordingManifest(value) {
1095
+ return Boolean(
1096
+ value && typeof value === "object" && value.version === 2 && typeof value.session === "object"
1097
+ );
1098
+ }
1099
+ function isLegacyRecordingManifest(value) {
1100
+ return Boolean(
1101
+ value && typeof value === "object" && value.version === 1 && Array.isArray(value.frames)
1102
+ );
1103
+ }
1104
+ function createRecordingManifest(input) {
1105
+ const actions = input.frames.map((frame) => {
1106
+ const actionId = frame.actionId ?? `action-${frame.seq}`;
1107
+ return {
1108
+ id: actionId,
1109
+ stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
1110
+ action: frame.action,
1111
+ selector: frame.selector,
1112
+ selectorUsed: frame.selectorUsed ?? frame.selector,
1113
+ value: frame.value,
1114
+ url: frame.url,
1115
+ success: frame.success,
1116
+ durationMs: frame.durationMs,
1117
+ error: frame.error,
1118
+ ts: new Date(frame.timestamp).toISOString(),
1119
+ pageUrl: frame.pageUrl,
1120
+ pageTitle: frame.pageTitle,
1121
+ coordinates: frame.coordinates,
1122
+ boundingBox: frame.boundingBox
1123
+ };
1124
+ });
1125
+ const screenshots = input.frames.map((frame) => ({
1126
+ id: `shot-${frame.seq}`,
1127
+ stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
1128
+ actionId: frame.actionId ?? `action-${frame.seq}`,
1129
+ file: frame.screenshot,
1130
+ ts: new Date(frame.timestamp).toISOString(),
1131
+ success: frame.success,
1132
+ pageUrl: frame.pageUrl,
1133
+ pageTitle: frame.pageTitle,
1134
+ coordinates: frame.coordinates,
1135
+ boundingBox: frame.boundingBox
1136
+ }));
1137
+ return {
1138
+ version: 2,
1139
+ recordedAt: input.recordedAt,
1140
+ session: {
1141
+ id: input.sessionId,
1142
+ startUrl: input.startUrl,
1143
+ endUrl: input.endUrl,
1144
+ targetId: input.targetId,
1145
+ profile: input.profile
1146
+ },
1147
+ recipe: {
1148
+ steps: input.steps
1149
+ },
1150
+ actions,
1151
+ screenshots,
1152
+ trace: {
1153
+ events: input.traceEvents,
1154
+ summaries: buildTraceSummaries(input.traceEvents)
1155
+ },
1156
+ assertions: input.assertions ?? [],
1157
+ notes: input.notes ?? [],
1158
+ artifacts: {
1159
+ recordingManifest: input.recordingManifest ?? "recording.json",
1160
+ screenshotDir: input.screenshotDir ?? "screenshots/"
1161
+ }
1162
+ };
1163
+ }
1164
+ function canonicalizeRecordingArtifact(value) {
1165
+ if (isCanonicalRecordingManifest(value)) {
1166
+ return value;
1167
+ }
1168
+ if (!isLegacyRecordingManifest(value)) {
1169
+ throw new Error("Unsupported recording artifact");
1170
+ }
1171
+ const traceEvents = buildTraceEventsFromLegacy(value);
1172
+ const steps = value.frames.map((frame) => frameToStep(frame));
1173
+ return createRecordingManifest({
1174
+ recordedAt: value.recordedAt,
1175
+ sessionId: value.sessionId,
1176
+ startUrl: value.startUrl,
1177
+ endUrl: value.endUrl,
1178
+ steps,
1179
+ frames: value.frames,
1180
+ traceEvents,
1181
+ notes: ["Converted from legacy recording manifest"]
1182
+ });
1183
+ }
1184
+ function buildTraceEventsFromLegacy(value) {
1185
+ const events = [];
1186
+ for (const frame of value.frames) {
1187
+ events.push({
1188
+ traceId: frame.actionId ?? `legacy-${frame.seq}`,
1189
+ sessionId: value.sessionId,
1190
+ ts: new Date(frame.timestamp).toISOString(),
1191
+ elapsedMs: frame.timestamp - new Date(value.recordedAt).getTime(),
1192
+ channel: "action",
1193
+ event: frame.success ? "action.succeeded" : "action.failed",
1194
+ severity: frame.success ? "info" : "error",
1195
+ summary: `${frame.action}${frame.selector ? ` ${frame.selector}` : ""}`,
1196
+ data: {
1197
+ action: frame.action,
1198
+ selector: frame.selector,
1199
+ value: frame.value ?? null,
1200
+ pageUrl: frame.pageUrl ?? null,
1201
+ pageTitle: frame.pageTitle ?? null,
1202
+ screenshot: frame.screenshot
1203
+ },
1204
+ actionId: frame.actionId ?? `action-${frame.seq}`,
1205
+ stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
1206
+ selector: frame.selector,
1207
+ selectorUsed: frame.selectorUsed ?? frame.selector,
1208
+ url: frame.pageUrl ?? frame.url
1209
+ });
1210
+ }
1211
+ return events;
1212
+ }
1213
+ function frameToStep(frame) {
1214
+ switch (frame.action) {
1215
+ case "fill":
1216
+ return { action: "fill", selector: frame.selector, value: frame.value };
1217
+ case "submit":
1218
+ return { action: "submit", selector: frame.selector };
1219
+ case "goto":
1220
+ return { action: "goto", url: frame.url ?? frame.pageUrl };
1221
+ case "press":
1222
+ return { action: "press", key: frame.value ?? "Enter" };
1223
+ default:
1224
+ return { action: "click", selector: frame.selector };
1225
+ }
1226
+ }
1227
+
1228
+ // src/trace/script.ts
1229
+ var TRACE_BINDING_NAME = "__bpTraceBinding";
1230
+ var TRACE_SCRIPT = `
1231
+ (() => {
1232
+ if (window.__bpTraceInstalled) return;
1233
+ window.__bpTraceInstalled = true;
1234
+
1235
+ const binding = globalThis.${TRACE_BINDING_NAME};
1236
+ if (typeof binding !== 'function') return;
1237
+
1238
+ const emit = (event, data = {}, severity = 'info', summary) => {
1239
+ try {
1240
+ globalThis.__bpTraceRecentEvents = globalThis.__bpTraceRecentEvents || [];
1241
+ const payload = {
1242
+ event,
1243
+ severity,
1244
+ summary: summary || event,
1245
+ ts: Date.now(),
1246
+ data,
1247
+ };
1248
+ globalThis.__bpTraceRecentEvents.push(payload);
1249
+ if (globalThis.__bpTraceRecentEvents.length > 200) {
1250
+ globalThis.__bpTraceRecentEvents.splice(0, globalThis.__bpTraceRecentEvents.length - 200);
1251
+ }
1252
+ binding(JSON.stringify(payload));
1253
+ } catch {}
1254
+ };
1255
+
1256
+ const patchWebSocket = () => {
1257
+ const NativeWebSocket = window.WebSocket;
1258
+ if (typeof NativeWebSocket !== 'function' || window.__bpTraceWebSocketInstalled) return;
1259
+ window.__bpTraceWebSocketInstalled = true;
1260
+
1261
+ const nextId = () => Math.random().toString(36).slice(2, 10);
1262
+
1263
+ const patchInstance = (socket, urlValue) => {
1264
+ if (!socket || socket.__bpTracePatched) return socket;
1265
+ socket.__bpTracePatched = true;
1266
+ socket.__bpTraceId = socket.__bpTraceId || nextId();
1267
+ socket.__bpTraceUrl = String(urlValue || socket.url || '');
1268
+ globalThis.__bpTrackedWebSockets = globalThis.__bpTrackedWebSockets || new Set();
1269
+ globalThis.__bpTrackedWebSockets.add(socket);
1270
+
1271
+ emit(
1272
+ 'ws.connection.created',
1273
+ { connectionId: socket.__bpTraceId, url: socket.__bpTraceUrl },
1274
+ 'info',
1275
+ 'WebSocket opened ' + socket.__bpTraceUrl
1276
+ );
1277
+
1278
+ const originalSend = socket.send;
1279
+ socket.send = function(data) {
1280
+ const payload =
1281
+ typeof data === 'string'
1282
+ ? data
1283
+ : data && typeof data.toString === 'function'
1284
+ ? data.toString()
1285
+ : '[binary]';
1286
+ emit(
1287
+ 'ws.frame.sent',
1288
+ {
1289
+ connectionId: socket.__bpTraceId,
1290
+ url: socket.__bpTraceUrl,
1291
+ payload,
1292
+ length: payload.length,
1293
+ },
1294
+ 'info',
1295
+ 'WebSocket frame sent'
1296
+ );
1297
+ return originalSend.call(this, data);
1298
+ };
1299
+
1300
+ socket.addEventListener('message', (event) => {
1301
+ if (socket.__bpOfflineNotified || socket.__bpTraceClosed) {
1302
+ return;
1303
+ }
1304
+ const data = event && 'data' in event ? event.data : '';
1305
+ const payload =
1306
+ typeof data === 'string'
1307
+ ? data
1308
+ : data && typeof data.toString === 'function'
1309
+ ? data.toString()
1310
+ : '[binary]';
1311
+ emit(
1312
+ 'ws.frame.received',
1313
+ {
1314
+ connectionId: socket.__bpTraceId,
1315
+ url: socket.__bpTraceUrl,
1316
+ payload,
1317
+ length: payload.length,
1318
+ },
1319
+ 'info',
1320
+ 'WebSocket frame received'
1321
+ );
1322
+ });
1323
+
1324
+ socket.addEventListener('close', (event) => {
1325
+ if (socket.__bpTraceClosed) {
1326
+ return;
1327
+ }
1328
+ socket.__bpTraceClosed = true;
1329
+ try {
1330
+ globalThis.__bpTrackedWebSockets.delete(socket);
1331
+ } catch {}
1332
+ emit(
1333
+ 'ws.connection.closed',
1334
+ {
1335
+ connectionId: socket.__bpTraceId,
1336
+ url: socket.__bpTraceUrl,
1337
+ code: event.code,
1338
+ reason: event.reason,
1339
+ },
1340
+ 'warn',
1341
+ 'WebSocket closed'
1342
+ );
1343
+ });
1344
+
1345
+ return socket;
1346
+ };
1347
+
1348
+ const TracedWebSocket = function(url, protocols) {
1349
+ return arguments.length > 1
1350
+ ? patchInstance(new NativeWebSocket(url, protocols), url)
1351
+ : patchInstance(new NativeWebSocket(url), url);
1352
+ };
1353
+ TracedWebSocket.prototype = NativeWebSocket.prototype;
1354
+ Object.setPrototypeOf(TracedWebSocket, NativeWebSocket);
1355
+ window.WebSocket = TracedWebSocket;
1356
+ };
1357
+
1358
+ window.addEventListener('error', (errorEvent) => {
1359
+ emit(
1360
+ 'runtime.exception',
1361
+ {
1362
+ message: errorEvent.message,
1363
+ filename: errorEvent.filename,
1364
+ line: errorEvent.lineno,
1365
+ column: errorEvent.colno,
1366
+ },
1367
+ 'error',
1368
+ errorEvent.message || 'Uncaught error'
1369
+ );
1370
+ });
1371
+
1372
+ window.addEventListener('unhandledrejection', (event) => {
1373
+ const reason = event && 'reason' in event ? String(event.reason) : 'Unhandled rejection';
1374
+ emit('runtime.unhandledRejection', { reason }, 'error', reason);
1375
+ });
1376
+
1377
+ const patchPermissions = async () => {
1378
+ if (!navigator.permissions || !navigator.permissions.query) return;
1379
+
1380
+ const names = ['geolocation', 'microphone', 'camera', 'notifications'];
1381
+ for (const name of names) {
1382
+ try {
1383
+ const status = await navigator.permissions.query({ name });
1384
+ emit(
1385
+ 'permission.state',
1386
+ { name, state: status.state },
1387
+ status.state === 'denied' ? 'warn' : 'info',
1388
+ name + ': ' + status.state
1389
+ );
1390
+ status.addEventListener('change', () => {
1391
+ emit(
1392
+ 'permission.changed',
1393
+ { name, state: status.state },
1394
+ status.state === 'denied' ? 'warn' : 'info',
1395
+ name + ': ' + status.state
1396
+ );
1397
+ });
1398
+ } catch {}
1399
+ }
1400
+ };
1401
+
1402
+ const patchMediaElement = (element) => {
1403
+ if (!element || element.__bpTracePatched) return;
1404
+ element.__bpTracePatched = true;
1405
+
1406
+ element.addEventListener('play', () => {
1407
+ emit(
1408
+ 'media.playback.started',
1409
+ { tag: element.tagName.toLowerCase(), src: element.currentSrc || element.src || null },
1410
+ 'info',
1411
+ 'Media playback started'
1412
+ );
1413
+ });
1414
+
1415
+ const onStop = () => {
1416
+ emit(
1417
+ 'media.playback.stopped',
1418
+ { tag: element.tagName.toLowerCase(), src: element.currentSrc || element.src || null },
1419
+ 'warn',
1420
+ 'Media playback stopped'
1421
+ );
1422
+ };
1423
+
1424
+ element.addEventListener('pause', onStop);
1425
+ element.addEventListener('ended', onStop);
1426
+ };
1427
+
1428
+ const patchMediaElements = () => {
1429
+ document.querySelectorAll('audio,video').forEach(patchMediaElement);
1430
+ };
1431
+
1432
+ patchMediaElements();
1433
+ patchWebSocket();
1434
+
1435
+ if (document.documentElement) {
1436
+ const observer = new MutationObserver(() => {
1437
+ patchMediaElements();
1438
+ });
1439
+ observer.observe(document.documentElement, { childList: true, subtree: true });
1440
+ }
1441
+
1442
+ if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
1443
+ const original = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
1444
+ navigator.mediaDevices.getUserMedia = async (...args) => {
1445
+ emit('voice.capture.started', { constraints: args[0] || null }, 'info', 'Voice capture started');
1446
+ try {
1447
+ const stream = await original(...args);
1448
+ const tracks = stream.getTracks();
1449
+
1450
+ for (const track of tracks) {
1451
+ emit(
1452
+ 'media.track.started',
1453
+ { kind: track.kind, label: track.label, readyState: track.readyState },
1454
+ 'info',
1455
+ track.kind + ' track started'
1456
+ );
1457
+ track.addEventListener('ended', () => {
1458
+ emit(
1459
+ 'media.track.ended',
1460
+ { kind: track.kind, label: track.label, readyState: track.readyState },
1461
+ 'warn',
1462
+ track.kind + ' track ended'
1463
+ );
1464
+ emit(
1465
+ 'voice.capture.stopped',
1466
+ { kind: track.kind, label: track.label, readyState: track.readyState },
1467
+ 'warn',
1468
+ 'Voice capture stopped'
1469
+ );
1470
+ });
1471
+ }
1472
+
1473
+ emit(
1474
+ 'voice.capture.detectedAudio',
1475
+ { trackCount: tracks.length, kinds: tracks.map((track) => track.kind) },
1476
+ 'info',
1477
+ 'Voice capture detected audio'
1478
+ );
1479
+
1480
+ return stream;
1481
+ } catch (error) {
1482
+ emit(
1483
+ 'voice.pipeline.notReady',
1484
+ { message: String(error && error.message ? error.message : error) },
1485
+ 'error',
1486
+ String(error && error.message ? error.message : error)
1487
+ );
1488
+ throw error;
1489
+ }
1490
+ };
1491
+ }
1492
+
1493
+ document.addEventListener('visibilitychange', () => {
1494
+ emit(
1495
+ 'dom.state.changed',
1496
+ { visibilityState: document.visibilityState },
1497
+ document.visibilityState === 'hidden' ? 'warn' : 'info',
1498
+ 'Visibility ' + document.visibilityState
1499
+ );
1500
+ });
1501
+
1502
+ patchPermissions();
1503
+ emit('voice.pipeline.ready', { url: location.href }, 'info', 'Trace hooks ready');
1504
+ })();
1505
+ `;
1506
+
1507
+ // src/trace/model.ts
1508
+ function createTraceId(prefix = "evt") {
1509
+ const random = Math.random().toString(36).slice(2, 10);
1510
+ return `${prefix}-${Date.now().toString(36)}-${random}`;
1511
+ }
1512
+ function normalizeTraceEvent(event) {
1513
+ return {
1514
+ traceId: event.traceId ?? createTraceId(event.channel),
1515
+ ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
1516
+ elapsedMs: event.elapsedMs ?? 0,
1517
+ severity: event.severity ?? inferSeverity(event.event),
1518
+ data: event.data ?? {},
1519
+ ...event
1520
+ };
1521
+ }
1522
+ function inferSeverity(eventName) {
1523
+ if (eventName.includes(".failed") || eventName.includes(".error") || eventName.includes("exception") || eventName.includes("notReady")) {
1524
+ return "error";
1525
+ }
1526
+ if (eventName.includes(".closed") || eventName.includes(".warn") || eventName.includes(".changed")) {
1527
+ return "warn";
1528
+ }
1529
+ return "info";
1530
+ }
1531
+
1532
+ // src/trace/live.ts
1533
+ function globToRegex(pattern) {
1534
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
1535
+ const withWildcards = escaped.replace(/\*/g, ".*");
1536
+ return new RegExp(`^${withWildcards}$`);
1537
+ }
1538
+
689
1539
  // src/actions/executor.ts
690
1540
  var DEFAULT_TIMEOUT = 3e4;
1541
+ var DEFAULT_RECORDING_SKIP_ACTIONS = [
1542
+ "wait",
1543
+ "snapshot",
1544
+ "forms",
1545
+ "text",
1546
+ "screenshot"
1547
+ ];
1548
+ function loadExistingRecording(manifestPath) {
1549
+ try {
1550
+ const raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
1551
+ if (raw.version === 1) {
1552
+ const legacy = raw;
1553
+ return {
1554
+ frames: Array.isArray(legacy.frames) ? legacy.frames : [],
1555
+ traceEvents: [],
1556
+ recordedAt: legacy.recordedAt,
1557
+ startUrl: legacy.startUrl
1558
+ };
1559
+ }
1560
+ const artifact = canonicalizeRecordingArtifact(raw);
1561
+ const screenshotsByAction = new Map(artifact.screenshots.map((shot) => [shot.actionId, shot]));
1562
+ const frames = artifact.actions.map((action, index) => {
1563
+ const screenshot = screenshotsByAction.get(action.id);
1564
+ return {
1565
+ seq: index + 1,
1566
+ timestamp: Date.parse(action.ts),
1567
+ action: action.action,
1568
+ selector: action.selector,
1569
+ selectorUsed: action.selectorUsed,
1570
+ value: action.value,
1571
+ url: action.url,
1572
+ coordinates: action.coordinates,
1573
+ boundingBox: action.boundingBox,
1574
+ success: action.success,
1575
+ durationMs: action.durationMs,
1576
+ error: action.error,
1577
+ screenshot: screenshot?.file ?? "",
1578
+ pageUrl: action.pageUrl,
1579
+ pageTitle: action.pageTitle,
1580
+ stepIndex: action.stepIndex,
1581
+ actionId: action.id
1582
+ };
1583
+ });
1584
+ return {
1585
+ frames,
1586
+ traceEvents: artifact.trace.events,
1587
+ recordedAt: artifact.recordedAt,
1588
+ startUrl: artifact.session.startUrl
1589
+ };
1590
+ } catch {
1591
+ return { frames: [], traceEvents: [] };
1592
+ }
1593
+ }
691
1594
  function classifyFailure(error) {
692
1595
  if (error instanceof ElementNotFoundError) {
693
1596
  return { reason: "missing" };
@@ -767,6 +1670,12 @@ var BatchExecutor = class {
767
1670
  const { timeout = DEFAULT_TIMEOUT, onFail = "stop" } = options;
768
1671
  const results = [];
769
1672
  const startTime = Date.now();
1673
+ const recording = options.record ? this.createRecordingContext(options.record) : null;
1674
+ if (steps.some((step) => step.action === "waitForWsMessage")) {
1675
+ await this.ensureTraceHooks();
1676
+ }
1677
+ const startUrl = recording ? await this.getPageUrlSafe() : "";
1678
+ let stoppedAtIndex;
770
1679
  for (let i = 0; i < steps.length; i++) {
771
1680
  const step = steps[i];
772
1681
  const stepStart = Date.now();
@@ -774,13 +1683,34 @@ var BatchExecutor = class {
774
1683
  const retryDelay = step.retryDelay ?? 500;
775
1684
  let lastError;
776
1685
  let succeeded = false;
1686
+ if (recording) {
1687
+ recording.traceEvents.push(
1688
+ normalizeTraceEvent({
1689
+ traceId: createTraceId("action"),
1690
+ elapsedMs: Date.now() - startTime,
1691
+ channel: "action",
1692
+ event: "action.started",
1693
+ summary: `${step.action}${step.selector ? ` ${Array.isArray(step.selector) ? step.selector[0] : step.selector}` : ""}`,
1694
+ data: {
1695
+ action: step.action,
1696
+ selector: step.selector ?? null,
1697
+ url: step.url ?? null
1698
+ },
1699
+ actionId: `action-${i + 1}`,
1700
+ stepIndex: i,
1701
+ selector: step.selector,
1702
+ url: step.url
1703
+ })
1704
+ );
1705
+ }
777
1706
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
778
1707
  if (attempt > 0) {
779
1708
  await new Promise((resolve) => setTimeout(resolve, retryDelay));
780
1709
  }
781
1710
  try {
1711
+ this.page.resetLastActionPosition();
782
1712
  const result = await this.executeStep(step, timeout);
783
- results.push({
1713
+ const stepResult = {
784
1714
  index: i,
785
1715
  action: step.action,
786
1716
  selector: step.selector,
@@ -788,8 +1718,37 @@ var BatchExecutor = class {
788
1718
  success: true,
789
1719
  durationMs: Date.now() - stepStart,
790
1720
  result: result.value,
791
- text: result.text
792
- });
1721
+ text: result.text,
1722
+ timestamp: Date.now(),
1723
+ coordinates: this.page.getLastActionCoordinates() ?? void 0,
1724
+ boundingBox: this.page.getLastActionBoundingBox() ?? void 0
1725
+ };
1726
+ if (recording && !recording.skipActions.has(step.action)) {
1727
+ await this.captureRecordingFrame(step, stepResult, recording);
1728
+ }
1729
+ if (recording) {
1730
+ recording.traceEvents.push(
1731
+ normalizeTraceEvent({
1732
+ traceId: createTraceId("action"),
1733
+ elapsedMs: Date.now() - startTime,
1734
+ channel: "action",
1735
+ event: "action.succeeded",
1736
+ summary: `${step.action} succeeded`,
1737
+ data: {
1738
+ action: step.action,
1739
+ selector: step.selector ?? null,
1740
+ selectorUsed: result.selectorUsed ?? null,
1741
+ durationMs: Date.now() - stepStart
1742
+ },
1743
+ actionId: `action-${i + 1}`,
1744
+ stepIndex: i,
1745
+ selector: step.selector,
1746
+ selectorUsed: result.selectorUsed,
1747
+ url: step.url
1748
+ })
1749
+ );
1750
+ }
1751
+ results.push(stepResult);
793
1752
  succeeded = true;
794
1753
  break;
795
1754
  } catch (error) {
@@ -810,7 +1769,7 @@ var BatchExecutor = class {
810
1769
  } catch {
811
1770
  }
812
1771
  }
813
- results.push({
1772
+ const failedResult = {
814
1773
  index: i,
815
1774
  action: step.action,
816
1775
  selector: step.selector,
@@ -820,25 +1779,183 @@ var BatchExecutor = class {
820
1779
  hints,
821
1780
  failureReason: reason,
822
1781
  coveringElement,
823
- suggestion: getSuggestion(reason)
824
- });
1782
+ suggestion: getSuggestion(reason),
1783
+ timestamp: Date.now()
1784
+ };
1785
+ if (recording && !recording.skipActions.has(step.action)) {
1786
+ await this.captureRecordingFrame(step, failedResult, recording);
1787
+ }
1788
+ if (recording) {
1789
+ recording.traceEvents.push(
1790
+ normalizeTraceEvent({
1791
+ traceId: createTraceId("action"),
1792
+ elapsedMs: Date.now() - startTime,
1793
+ channel: "action",
1794
+ event: "action.failed",
1795
+ severity: "error",
1796
+ summary: `${step.action} failed: ${errorMessage}`,
1797
+ data: {
1798
+ action: step.action,
1799
+ selector: step.selector ?? null,
1800
+ error: errorMessage,
1801
+ reason
1802
+ },
1803
+ actionId: `action-${i + 1}`,
1804
+ stepIndex: i,
1805
+ selector: step.selector,
1806
+ url: step.url
1807
+ })
1808
+ );
1809
+ }
1810
+ results.push(failedResult);
825
1811
  if (onFail === "stop" && !step.optional) {
826
- return {
827
- success: false,
828
- stoppedAtIndex: i,
829
- steps: results,
830
- totalDurationMs: Date.now() - startTime
831
- };
1812
+ stoppedAtIndex = i;
1813
+ break;
832
1814
  }
833
1815
  }
834
1816
  }
835
- const allSuccess = results.every((r) => r.success || steps[r.index]?.optional);
1817
+ const totalDurationMs = Date.now() - startTime;
1818
+ const allSuccess = stoppedAtIndex === void 0 && results.every((result) => result.success || steps[result.index]?.optional);
1819
+ let recordingManifest;
1820
+ if (recording) {
1821
+ recordingManifest = await this.writeRecordingManifest(
1822
+ recording,
1823
+ startTime,
1824
+ startUrl,
1825
+ allSuccess,
1826
+ steps
1827
+ );
1828
+ }
836
1829
  return {
837
1830
  success: allSuccess,
1831
+ stoppedAtIndex,
838
1832
  steps: results,
839
- totalDurationMs: Date.now() - startTime
1833
+ totalDurationMs,
1834
+ recordingManifest
840
1835
  };
841
1836
  }
1837
+ createRecordingContext(record) {
1838
+ const baseDir = record.outputDir ?? (0, import_node_path.join)(process.cwd(), ".browser-pilot");
1839
+ const screenshotDir = (0, import_node_path.join)(baseDir, "screenshots");
1840
+ const manifestPath = (0, import_node_path.join)(baseDir, "recording.json");
1841
+ const existing = loadExistingRecording(manifestPath);
1842
+ fs.mkdirSync(screenshotDir, { recursive: true });
1843
+ return {
1844
+ baseDir,
1845
+ screenshotDir,
1846
+ sessionId: record.sessionId ?? this.page.targetId,
1847
+ frames: existing.frames,
1848
+ traceEvents: existing.traceEvents,
1849
+ format: record.format ?? "webp",
1850
+ quality: Math.max(0, Math.min(100, record.quality ?? 40)),
1851
+ highlights: record.highlights !== false,
1852
+ skipActions: new Set(record.skipActions ?? DEFAULT_RECORDING_SKIP_ACTIONS)
1853
+ };
1854
+ }
1855
+ async getPageUrlSafe() {
1856
+ try {
1857
+ return await this.page.url();
1858
+ } catch {
1859
+ return "";
1860
+ }
1861
+ }
1862
+ /**
1863
+ * Capture a recording screenshot frame with optional highlight overlay
1864
+ */
1865
+ async captureRecordingFrame(step, stepResult, recording) {
1866
+ const targetMetadata = this.page.getLastActionTargetMetadata();
1867
+ let highlightInjected = false;
1868
+ try {
1869
+ const ts = Date.now();
1870
+ const seq = String(recording.frames.length + 1).padStart(4, "0");
1871
+ const filename = `${seq}-${ts}-${stepResult.action}.${recording.format}`;
1872
+ const filepath = (0, import_node_path.join)(recording.screenshotDir, filename);
1873
+ if (recording.highlights) {
1874
+ const kind = stepToHighlightKind(stepResult);
1875
+ if (kind) {
1876
+ await injectActionHighlight(this.page, {
1877
+ kind,
1878
+ bbox: stepResult.boundingBox,
1879
+ point: stepResult.coordinates,
1880
+ label: getHighlightLabel(step, stepResult, targetMetadata)
1881
+ });
1882
+ highlightInjected = true;
1883
+ }
1884
+ }
1885
+ const base64 = await this.page.screenshot({
1886
+ format: recording.format,
1887
+ quality: recording.quality
1888
+ });
1889
+ const buffer = Buffer.from(base64, "base64");
1890
+ fs.writeFileSync(filepath, buffer);
1891
+ stepResult.screenshotPath = filepath;
1892
+ let pageUrl;
1893
+ let pageTitle;
1894
+ try {
1895
+ pageUrl = await this.page.url();
1896
+ pageTitle = await this.page.title();
1897
+ } catch {
1898
+ }
1899
+ recording.frames.push({
1900
+ seq: recording.frames.length + 1,
1901
+ timestamp: ts,
1902
+ action: stepResult.action,
1903
+ selector: stepResult.selectorUsed ?? (Array.isArray(step.selector) ? step.selector[0] : step.selector),
1904
+ selectorUsed: stepResult.selectorUsed,
1905
+ value: redactValueForRecording(
1906
+ typeof step.value === "string" ? step.value : void 0,
1907
+ targetMetadata
1908
+ ),
1909
+ url: step.url,
1910
+ coordinates: stepResult.coordinates,
1911
+ boundingBox: stepResult.boundingBox,
1912
+ success: stepResult.success,
1913
+ durationMs: stepResult.durationMs,
1914
+ error: stepResult.error,
1915
+ screenshot: filename,
1916
+ pageUrl,
1917
+ pageTitle,
1918
+ stepIndex: stepResult.index,
1919
+ actionId: `action-${stepResult.index + 1}`
1920
+ });
1921
+ } catch {
1922
+ } finally {
1923
+ if (recording.highlights || highlightInjected) {
1924
+ await removeActionHighlight(this.page);
1925
+ }
1926
+ }
1927
+ }
1928
+ /**
1929
+ * Write recording manifest to disk
1930
+ */
1931
+ async writeRecordingManifest(recording, startTime, startUrl, success, steps) {
1932
+ let endUrl = startUrl;
1933
+ try {
1934
+ endUrl = await this.page.url();
1935
+ } catch {
1936
+ }
1937
+ const manifestPath = (0, import_node_path.join)(recording.baseDir, "recording.json");
1938
+ let recordedAt = new Date(startTime).toISOString();
1939
+ let originalStartUrl = startUrl;
1940
+ const existing = loadExistingRecording(manifestPath);
1941
+ if (existing.recordedAt) recordedAt = existing.recordedAt;
1942
+ if (existing.startUrl) originalStartUrl = existing.startUrl;
1943
+ const manifest = createRecordingManifest({
1944
+ recordedAt,
1945
+ sessionId: recording.sessionId,
1946
+ startUrl: originalStartUrl,
1947
+ endUrl,
1948
+ targetId: this.page.targetId,
1949
+ steps,
1950
+ frames: recording.frames,
1951
+ traceEvents: recording.traceEvents,
1952
+ notes: success ? [] : ["Replay ended with at least one failed action."],
1953
+ recordingManifest: "recording.json",
1954
+ screenshotDir: "screenshots/"
1955
+ });
1956
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
1957
+ return manifestPath;
1958
+ }
842
1959
  /**
843
1960
  * Execute a single step
844
1961
  */
@@ -1118,6 +2235,39 @@ var BatchExecutor = class {
1118
2235
  }
1119
2236
  return { selectorUsed: usedSelector, value: actual };
1120
2237
  }
2238
+ case "waitForWsMessage": {
2239
+ if (typeof step.match !== "string") {
2240
+ throw new Error("waitForWsMessage requires match");
2241
+ }
2242
+ const message = await this.waitForWsMessage(step.match, step.where, timeout);
2243
+ return { value: message };
2244
+ }
2245
+ case "assertNoConsoleErrors": {
2246
+ await this.assertNoConsoleErrors(step.windowMs ?? timeout);
2247
+ return {};
2248
+ }
2249
+ case "assertTextChanged": {
2250
+ const selector = Array.isArray(step.selector) ? step.selector[0] : step.selector;
2251
+ if (typeof step.to !== "string") {
2252
+ throw new Error("assertTextChanged requires to");
2253
+ }
2254
+ const text = await this.assertTextChanged(selector, step.from, step.to, timeout);
2255
+ return { selectorUsed: selector, text };
2256
+ }
2257
+ case "assertPermission": {
2258
+ if (!step.name || !step.state) {
2259
+ throw new Error("assertPermission requires name and state");
2260
+ }
2261
+ const permission = await this.assertPermission(step.name, step.state);
2262
+ return { value: permission };
2263
+ }
2264
+ case "assertMediaTrackLive": {
2265
+ if (!step.kind) {
2266
+ throw new Error("assertMediaTrackLive requires kind");
2267
+ }
2268
+ const media = await this.assertMediaTrackLive(step.kind);
2269
+ return { value: media };
2270
+ }
1121
2271
  default: {
1122
2272
  const action = step.action;
1123
2273
  const aliases = {
@@ -1171,7 +2321,7 @@ var BatchExecutor = class {
1171
2321
  };
1172
2322
  const suggestion = aliases[action.toLowerCase()];
1173
2323
  const hint = suggestion ? ` Did you mean "${suggestion}"?` : "";
1174
- 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";
2324
+ 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";
1175
2325
  throw new Error(`Unknown action "${action}".${hint}
1176
2326
 
1177
2327
  Valid actions: ${valid}`);
@@ -1187,6 +2337,233 @@ Valid actions: ${valid}`);
1187
2337
  if (matched) return matched;
1188
2338
  return Array.isArray(selector) ? selector[0] : selector;
1189
2339
  }
2340
+ async ensureTraceHooks() {
2341
+ await this.page.cdpClient.send("Runtime.enable");
2342
+ await this.page.cdpClient.send("Page.enable");
2343
+ await this.page.cdpClient.send("Network.enable");
2344
+ try {
2345
+ await this.page.cdpClient.send("Runtime.addBinding", { name: TRACE_BINDING_NAME });
2346
+ } catch {
2347
+ }
2348
+ await this.page.cdpClient.send("Page.addScriptToEvaluateOnNewDocument", { source: TRACE_SCRIPT });
2349
+ await this.page.cdpClient.send("Runtime.evaluate", { expression: TRACE_SCRIPT, awaitPromise: false });
2350
+ }
2351
+ async waitForWsMessage(match, where, timeout) {
2352
+ await this.ensureTraceHooks();
2353
+ const regex = globToRegex(match);
2354
+ const wsUrls = /* @__PURE__ */ new Map();
2355
+ const recentMatch = await this.findRecentWsMessage(regex, where);
2356
+ if (recentMatch) {
2357
+ return recentMatch;
2358
+ }
2359
+ return new Promise((resolve, reject) => {
2360
+ const cleanup = () => {
2361
+ this.page.cdpClient.off("Network.webSocketCreated", onCreated);
2362
+ this.page.cdpClient.off("Network.webSocketFrameReceived", onFrame);
2363
+ this.page.cdpClient.off("Runtime.bindingCalled", onBinding);
2364
+ clearTimeout(timer);
2365
+ };
2366
+ const onCreated = (params) => {
2367
+ wsUrls.set(String(params["requestId"] ?? ""), String(params["url"] ?? ""));
2368
+ };
2369
+ const onFrame = (params) => {
2370
+ const requestId = String(params["requestId"] ?? "");
2371
+ const response = params["response"] ?? {};
2372
+ const payload = String(response.payloadData ?? "");
2373
+ const url = wsUrls.get(requestId) ?? "";
2374
+ if (!regex.test(url) && !regex.test(payload)) {
2375
+ return;
2376
+ }
2377
+ if (where && !this.payloadMatchesWhere(payload, where)) {
2378
+ return;
2379
+ }
2380
+ cleanup();
2381
+ resolve({ requestId, url, payload });
2382
+ };
2383
+ const onBinding = (params) => {
2384
+ if (params["name"] !== TRACE_BINDING_NAME) {
2385
+ return;
2386
+ }
2387
+ try {
2388
+ const parsed = JSON.parse(String(params["payload"] ?? ""));
2389
+ if (parsed.event !== "ws.frame.received") {
2390
+ return;
2391
+ }
2392
+ const data = parsed.data ?? {};
2393
+ const payload = String(data["payload"] ?? "");
2394
+ const url = String(data["url"] ?? "");
2395
+ if (!regex.test(url) && !regex.test(payload)) {
2396
+ return;
2397
+ }
2398
+ if (where && !this.payloadMatchesWhere(payload, where)) {
2399
+ return;
2400
+ }
2401
+ cleanup();
2402
+ resolve({
2403
+ requestId: String(data["connectionId"] ?? ""),
2404
+ url,
2405
+ payload
2406
+ });
2407
+ } catch {
2408
+ }
2409
+ };
2410
+ const timer = setTimeout(() => {
2411
+ cleanup();
2412
+ reject(new Error(`Timed out waiting for WebSocket message matching ${match}`));
2413
+ }, timeout);
2414
+ this.page.cdpClient.on("Network.webSocketCreated", onCreated);
2415
+ this.page.cdpClient.on("Network.webSocketFrameReceived", onFrame);
2416
+ this.page.cdpClient.on("Runtime.bindingCalled", onBinding);
2417
+ });
2418
+ }
2419
+ payloadMatchesWhere(payload, where) {
2420
+ try {
2421
+ const parsed = JSON.parse(payload);
2422
+ return Object.entries(where).every(([key, expected]) => {
2423
+ const actual = key.split(".").reduce((current, part) => {
2424
+ if (!current || typeof current !== "object") {
2425
+ return void 0;
2426
+ }
2427
+ return current[part];
2428
+ }, parsed);
2429
+ return actual === expected;
2430
+ });
2431
+ } catch {
2432
+ return false;
2433
+ }
2434
+ }
2435
+ async findRecentWsMessage(regex, where) {
2436
+ const recent = await this.page.evaluate(
2437
+ "(() => Array.isArray(globalThis.__bpTraceRecentEvents) ? globalThis.__bpTraceRecentEvents : [])()"
2438
+ );
2439
+ if (!Array.isArray(recent)) {
2440
+ return null;
2441
+ }
2442
+ for (let i = recent.length - 1; i >= 0; i--) {
2443
+ const entry = recent[i];
2444
+ if (!entry || typeof entry !== "object") {
2445
+ continue;
2446
+ }
2447
+ const event = String(entry["event"] ?? "");
2448
+ if (event !== "ws.frame.received") {
2449
+ continue;
2450
+ }
2451
+ const data = entry["data"] ?? {};
2452
+ const payload = String(data["payload"] ?? "");
2453
+ const url = String(data["url"] ?? "");
2454
+ if (!regex.test(url) && !regex.test(payload)) {
2455
+ continue;
2456
+ }
2457
+ if (where && !this.payloadMatchesWhere(payload, where)) {
2458
+ continue;
2459
+ }
2460
+ return {
2461
+ requestId: String(data["connectionId"] ?? ""),
2462
+ url,
2463
+ payload
2464
+ };
2465
+ }
2466
+ return null;
2467
+ }
2468
+ async assertNoConsoleErrors(windowMs) {
2469
+ await this.page.cdpClient.send("Runtime.enable");
2470
+ return new Promise((resolve, reject) => {
2471
+ const errors = [];
2472
+ const cleanup = () => {
2473
+ this.page.cdpClient.off("Runtime.consoleAPICalled", onConsole);
2474
+ this.page.cdpClient.off("Runtime.exceptionThrown", onException);
2475
+ clearTimeout(timer);
2476
+ };
2477
+ const onConsole = (params) => {
2478
+ if (params["type"] !== "error") {
2479
+ return;
2480
+ }
2481
+ const args = Array.isArray(params["args"]) ? params["args"] : [];
2482
+ errors.push(
2483
+ args.map((entry) => String(entry["value"] ?? entry["description"] ?? "")).filter(Boolean).join(" ")
2484
+ );
2485
+ };
2486
+ const onException = (params) => {
2487
+ const details = params["exceptionDetails"] ?? {};
2488
+ errors.push(String(details["text"] ?? "Runtime exception"));
2489
+ };
2490
+ const timer = setTimeout(() => {
2491
+ cleanup();
2492
+ if (errors.length > 0) {
2493
+ reject(new Error(`Console errors detected: ${errors.join(" | ")}`));
2494
+ return;
2495
+ }
2496
+ resolve();
2497
+ }, windowMs);
2498
+ this.page.cdpClient.on("Runtime.consoleAPICalled", onConsole);
2499
+ this.page.cdpClient.on("Runtime.exceptionThrown", onException);
2500
+ });
2501
+ }
2502
+ async assertTextChanged(selector, from, to, timeout) {
2503
+ const initialText = from ?? await this.page.text(selector);
2504
+ const deadline = Date.now() + timeout;
2505
+ while (Date.now() < deadline) {
2506
+ const text = await this.page.text(selector);
2507
+ if (text !== initialText && text.includes(to)) {
2508
+ return text;
2509
+ }
2510
+ await new Promise((resolve) => setTimeout(resolve, 200));
2511
+ }
2512
+ throw new Error(`Text did not change to include ${JSON.stringify(to)}`);
2513
+ }
2514
+ async assertPermission(name, state) {
2515
+ const result = await this.page.evaluate(
2516
+ `(() => navigator.permissions.query({ name: ${JSON.stringify(name)} }).then((status) => ({ name: ${JSON.stringify(name)}, state: status.state })))()`
2517
+ );
2518
+ if (!result || typeof result !== "object" || result.state !== state) {
2519
+ throw new Error(`Permission ${name} is not ${state}`);
2520
+ }
2521
+ return result;
2522
+ }
2523
+ async assertMediaTrackLive(kind) {
2524
+ const result = await this.page.evaluate(
2525
+ `(() => {
2526
+ const requestedKind = ${JSON.stringify(kind)};
2527
+ const mediaElements = Array.from(document.querySelectorAll('audio,video')).map((el) => {
2528
+ const tracks = [];
2529
+ if (el.srcObject && typeof el.srcObject.getTracks === 'function') {
2530
+ tracks.push(...el.srcObject.getTracks());
2531
+ }
2532
+ return {
2533
+ tag: el.tagName.toLowerCase(),
2534
+ paused: !!el.paused,
2535
+ tracks: tracks.map((track) => ({
2536
+ kind: track.kind,
2537
+ readyState: track.readyState,
2538
+ enabled: track.enabled,
2539
+ label: track.label,
2540
+ })),
2541
+ };
2542
+ });
2543
+
2544
+ const globalTracks =
2545
+ window.__bpStream && typeof window.__bpStream.getTracks === 'function'
2546
+ ? window.__bpStream.getTracks().map((track) => ({
2547
+ kind: track.kind,
2548
+ readyState: track.readyState,
2549
+ enabled: track.enabled,
2550
+ label: track.label,
2551
+ }))
2552
+ : [];
2553
+
2554
+ const liveTracks = mediaElements
2555
+ .flatMap((entry) => entry.tracks)
2556
+ .concat(globalTracks)
2557
+ .filter((track) => track.kind === requestedKind && track.readyState === 'live');
2558
+
2559
+ return { live: liveTracks.length > 0, mediaElements, globalTracks, liveTracks };
2560
+ })()`
2561
+ );
2562
+ if (!result || typeof result !== "object" || !result.live) {
2563
+ throw new Error(`No live ${kind} media track detected`);
2564
+ }
2565
+ return result;
2566
+ }
1190
2567
  };
1191
2568
  function addBatchToPage(page) {
1192
2569
  const executor = new BatchExecutor(page);
@@ -1317,7 +2694,7 @@ var ACTION_RULES = {
1317
2694
  value: { type: "string|string[]" },
1318
2695
  trigger: { type: "string|string[]" },
1319
2696
  option: { type: "string|string[]" },
1320
- match: { type: "string", enum: ["text", "value", "contains"] }
2697
+ match: { type: "string" }
1321
2698
  }
1322
2699
  },
1323
2700
  check: {
@@ -1448,6 +2825,38 @@ var ACTION_RULES = {
1448
2825
  expect: { type: "string" },
1449
2826
  value: { type: "string" }
1450
2827
  }
2828
+ },
2829
+ waitForWsMessage: {
2830
+ required: { match: { type: "string" } },
2831
+ optional: {
2832
+ where: { type: "object" }
2833
+ }
2834
+ },
2835
+ assertNoConsoleErrors: {
2836
+ required: {},
2837
+ optional: {
2838
+ windowMs: { type: "number" }
2839
+ }
2840
+ },
2841
+ assertTextChanged: {
2842
+ required: { to: { type: "string" } },
2843
+ optional: {
2844
+ selector: { type: "string|string[]" },
2845
+ from: { type: "string" }
2846
+ }
2847
+ },
2848
+ assertPermission: {
2849
+ required: {
2850
+ name: { type: "string" },
2851
+ state: { type: "string" }
2852
+ },
2853
+ optional: {}
2854
+ },
2855
+ assertMediaTrackLive: {
2856
+ required: {
2857
+ kind: { type: "string", enum: ["audio", "video"] }
2858
+ },
2859
+ optional: {}
1451
2860
  }
1452
2861
  };
1453
2862
  var VALID_ACTIONS = Object.keys(ACTION_RULES);
@@ -1471,6 +2880,7 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
1471
2880
  "trigger",
1472
2881
  "option",
1473
2882
  "match",
2883
+ "where",
1474
2884
  "x",
1475
2885
  "y",
1476
2886
  "direction",
@@ -1480,7 +2890,13 @@ var KNOWN_STEP_FIELDS = /* @__PURE__ */ new Set([
1480
2890
  "fullPage",
1481
2891
  "expect",
1482
2892
  "retry",
1483
- "retryDelay"
2893
+ "retryDelay",
2894
+ "from",
2895
+ "to",
2896
+ "name",
2897
+ "state",
2898
+ "kind",
2899
+ "windowMs"
1484
2900
  ]);
1485
2901
  function resolveAction(name) {
1486
2902
  if (VALID_ACTIONS.includes(name)) {
@@ -1553,6 +2969,11 @@ function checkFieldType(value, rule) {
1553
2969
  return `expected boolean or "auto", got ${typeof value}`;
1554
2970
  }
1555
2971
  return null;
2972
+ case "object":
2973
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2974
+ return `expected object, got ${Array.isArray(value) ? "array" : typeof value}`;
2975
+ }
2976
+ return null;
1556
2977
  default: {
1557
2978
  const _exhaustive = rule.type;
1558
2979
  return `unknown type: ${_exhaustive}`;
@@ -1916,6 +3337,10 @@ async function grantAudioPermissions(cdp, origin) {
1916
3337
  await cdp.send("Page.addScriptToEvaluateOnNewDocument", {
1917
3338
  source: PERMISSIONS_OVERRIDE_SCRIPT
1918
3339
  });
3340
+ await cdp.send("Runtime.evaluate", {
3341
+ expression: PERMISSIONS_OVERRIDE_SCRIPT,
3342
+ awaitPromise: false
3343
+ });
1919
3344
  }
1920
3345
  var PERMISSIONS_OVERRIDE_SCRIPT = `
1921
3346
  (function() {
@@ -3189,6 +4614,24 @@ Content-Type: ${contentType}\r
3189
4614
  parts.push(data);
3190
4615
  }
3191
4616
 
4617
+ // src/utils/json.ts
4618
+ function isRecord(value) {
4619
+ return typeof value === "object" && value !== null;
4620
+ }
4621
+ function stringifyUnknown(value) {
4622
+ if (typeof value === "string") return value;
4623
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
4624
+ return String(value);
4625
+ }
4626
+ if (value === null) return "null";
4627
+ if (value === void 0) return "undefined";
4628
+ try {
4629
+ return JSON.stringify(value);
4630
+ } catch {
4631
+ return Object.prototype.toString.call(value);
4632
+ }
4633
+ }
4634
+
3192
4635
  // src/cdp/transport.ts
3193
4636
  function createTransport(wsUrl, options = {}) {
3194
4637
  const { timeout = 3e4 } = options;
@@ -3292,8 +4735,12 @@ function getReadyStateString(state) {
3292
4735
 
3293
4736
  // src/cdp/client.ts
3294
4737
  async function createCDPClient(wsUrl, options = {}) {
3295
- const { debug = false, timeout = 3e4 } = options;
4738
+ const { timeout = 3e4 } = options;
3296
4739
  const transport = await createTransport(wsUrl, { timeout });
4740
+ return buildCDPClient(transport, options);
4741
+ }
4742
+ function buildCDPClient(transport, options = {}) {
4743
+ const { debug = false, timeout = 3e4 } = options;
3297
4744
  let messageId = 0;
3298
4745
  let currentSessionId;
3299
4746
  let connected = true;
@@ -3303,7 +4750,19 @@ async function createCDPClient(wsUrl, options = {}) {
3303
4750
  transport.onMessage((raw) => {
3304
4751
  let msg;
3305
4752
  try {
3306
- msg = JSON.parse(raw);
4753
+ const parsed = JSON.parse(raw);
4754
+ if (!isRecord(parsed)) {
4755
+ if (debug) console.error("[CDP] Ignoring non-object message:", raw);
4756
+ return;
4757
+ }
4758
+ if ("id" in parsed && typeof parsed["id"] === "number") {
4759
+ msg = parsed;
4760
+ } else if ("method" in parsed && typeof parsed["method"] === "string") {
4761
+ msg = parsed;
4762
+ } else {
4763
+ if (debug) console.error("[CDP] Ignoring invalid message shape:", raw);
4764
+ return;
4765
+ }
3307
4766
  } catch {
3308
4767
  if (debug) console.error("[CDP] Failed to parse message:", raw);
3309
4768
  return;
@@ -3318,7 +4777,8 @@ async function createCDPClient(wsUrl, options = {}) {
3318
4777
  pending.delete(response.id);
3319
4778
  clearTimeout(request.timer);
3320
4779
  if (response.error) {
3321
- request.reject(new CDPError(response.error));
4780
+ const error = typeof response.error === "string" ? { code: -32e3, message: response.error } : response.error;
4781
+ request.reject(new CDPError(error));
3322
4782
  } else {
3323
4783
  request.resolve(response.result);
3324
4784
  }
@@ -3416,6 +4876,9 @@ async function createCDPClient(wsUrl, options = {}) {
3416
4876
  onAny(handler) {
3417
4877
  anyEventHandlers.add(handler);
3418
4878
  },
4879
+ offAny(handler) {
4880
+ anyEventHandlers.delete(handler);
4881
+ },
3419
4882
  async close() {
3420
4883
  connected = false;
3421
4884
  await transport.close();
@@ -3431,6 +4894,9 @@ async function createCDPClient(wsUrl, options = {}) {
3431
4894
  get sessionId() {
3432
4895
  return currentSessionId;
3433
4896
  },
4897
+ setSessionId(sessionId) {
4898
+ currentSessionId = sessionId;
4899
+ },
3434
4900
  get isConnected() {
3435
4901
  return connected;
3436
4902
  }
@@ -4666,6 +6132,9 @@ var Page = class {
4666
6132
  brokenFrame = null;
4667
6133
  /** Last matched selector from findElement (for selectorUsed tracking) */
4668
6134
  _lastMatchedSelector;
6135
+ _lastActionCoordinates = null;
6136
+ _lastActionBoundingBox = null;
6137
+ _lastActionTargetMetadata = null;
4669
6138
  /** Last snapshot for stale ref recovery */
4670
6139
  lastSnapshot;
4671
6140
  /** Audio input controller (lazy-initialized) */
@@ -4697,6 +6166,76 @@ var Page = class {
4697
6166
  getLastMatchedSelector() {
4698
6167
  return this._lastMatchedSelector;
4699
6168
  }
6169
+ async getActionTargetMetadata(identifiers) {
6170
+ try {
6171
+ const objectId = identifiers.objectId ?? (identifiers.nodeId ? await this.resolveObjectId(identifiers.nodeId) : void 0);
6172
+ if (!objectId) return null;
6173
+ const response = await this.cdp.send("Runtime.callFunctionOn", {
6174
+ objectId,
6175
+ functionDeclaration: `function() {
6176
+ const tagName = this.tagName?.toLowerCase?.() || '';
6177
+ const inputType =
6178
+ tagName === 'input' && typeof this.type === 'string' ? this.type.toLowerCase() : '';
6179
+ const autocomplete =
6180
+ typeof this.autocomplete === 'string' ? this.autocomplete.toLowerCase() : '';
6181
+ return { tagName, inputType, autocomplete };
6182
+ }`,
6183
+ returnByValue: true
6184
+ });
6185
+ return response.result.value ?? null;
6186
+ } catch {
6187
+ return null;
6188
+ }
6189
+ }
6190
+ async getElementPosition(identifiers) {
6191
+ try {
6192
+ const { quads } = await this.cdp.send(
6193
+ "DOM.getContentQuads",
6194
+ identifiers
6195
+ );
6196
+ if (quads?.length > 0) {
6197
+ const q = quads[0];
6198
+ const minX = Math.min(q[0], q[2], q[4], q[6]);
6199
+ const maxX = Math.max(q[0], q[2], q[4], q[6]);
6200
+ const minY = Math.min(q[1], q[3], q[5], q[7]);
6201
+ const maxY = Math.max(q[1], q[3], q[5], q[7]);
6202
+ return {
6203
+ center: { x: (minX + maxX) / 2, y: (minY + maxY) / 2 },
6204
+ bbox: { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
6205
+ };
6206
+ }
6207
+ } catch {
6208
+ }
6209
+ if (identifiers.nodeId) {
6210
+ const box = await this.getBoxModel(identifiers.nodeId);
6211
+ if (box) {
6212
+ return {
6213
+ center: { x: box.content[0] + box.width / 2, y: box.content[1] + box.height / 2 },
6214
+ bbox: { x: box.content[0], y: box.content[1], width: box.width, height: box.height }
6215
+ };
6216
+ }
6217
+ }
6218
+ return null;
6219
+ }
6220
+ setLastActionPosition(coords, bbox) {
6221
+ this._lastActionCoordinates = coords;
6222
+ this._lastActionBoundingBox = bbox;
6223
+ }
6224
+ getLastActionCoordinates() {
6225
+ return this._lastActionCoordinates;
6226
+ }
6227
+ getLastActionBoundingBox() {
6228
+ return this._lastActionBoundingBox;
6229
+ }
6230
+ getLastActionTargetMetadata() {
6231
+ return this._lastActionTargetMetadata;
6232
+ }
6233
+ /** Reset position tracking (call before each executor step) */
6234
+ resetLastActionPosition() {
6235
+ this._lastActionCoordinates = null;
6236
+ this._lastActionBoundingBox = null;
6237
+ this._lastActionTargetMetadata = null;
6238
+ }
4700
6239
  /**
4701
6240
  * Initialize the page (enable required CDP domains)
4702
6241
  */
@@ -4864,6 +6403,14 @@ var Page = class {
4864
6403
  const quad = quads[0];
4865
6404
  clickX = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
4866
6405
  clickY = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
6406
+ const minX = Math.min(quad[0], quad[2], quad[4], quad[6]);
6407
+ const maxX = Math.max(quad[0], quad[2], quad[4], quad[6]);
6408
+ const minY = Math.min(quad[1], quad[3], quad[5], quad[7]);
6409
+ const maxY = Math.max(quad[1], quad[3], quad[5], quad[7]);
6410
+ this.setLastActionPosition(
6411
+ { x: clickX, y: clickY },
6412
+ { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
6413
+ );
4867
6414
  } else {
4868
6415
  throw new Error("No quads");
4869
6416
  }
@@ -4872,6 +6419,10 @@ var Page = class {
4872
6419
  if (!box) throw new Error("Could not get element position");
4873
6420
  clickX = box.content[0] + box.width / 2;
4874
6421
  clickY = box.content[1] + box.height / 2;
6422
+ this.setLastActionPosition(
6423
+ { x: clickX, y: clickY },
6424
+ { x: box.content[0], y: box.content[1], width: box.width, height: box.height }
6425
+ );
4875
6426
  }
4876
6427
  const hitTargetCoordinates = this.currentFrame ? void 0 : { x: clickX, y: clickY };
4877
6428
  const HIT_TARGET_RETRIES = 3;
@@ -4922,13 +6473,20 @@ var Page = class {
4922
6473
  if (options.optional) return false;
4923
6474
  throw e;
4924
6475
  }
6476
+ const fillPos = await this.getElementPosition({ nodeId: element.nodeId });
6477
+ if (fillPos) this.setLastActionPosition(fillPos.center, fillPos.bbox);
4925
6478
  const tagInfo = await this.cdp.send("Runtime.callFunctionOn", {
4926
6479
  objectId,
4927
6480
  functionDeclaration: `function() {
4928
- return { tagName: this.tagName?.toLowerCase() || '', inputType: (this.type || '').toLowerCase() };
6481
+ return {
6482
+ tagName: this.tagName?.toLowerCase() || '',
6483
+ inputType: (this.type || '').toLowerCase(),
6484
+ autocomplete: typeof this.autocomplete === 'string' ? this.autocomplete.toLowerCase() : '',
6485
+ };
4929
6486
  }`,
4930
6487
  returnByValue: true
4931
6488
  });
6489
+ this._lastActionTargetMetadata = tagInfo.result.value;
4932
6490
  const { tagName, inputType } = tagInfo.result.value;
4933
6491
  const specialInputTypes = /* @__PURE__ */ new Set([
4934
6492
  "date",
@@ -5010,6 +6568,9 @@ var Page = class {
5010
6568
  if (options.optional) return false;
5011
6569
  throw e;
5012
6570
  }
6571
+ const typePos = await this.getElementPosition({ nodeId: element.nodeId });
6572
+ if (typePos) this.setLastActionPosition(typePos.center, typePos.bbox);
6573
+ this._lastActionTargetMetadata = await this.getActionTargetMetadata({ objectId });
5013
6574
  await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
5014
6575
  for (const char of text) {
5015
6576
  const def = US_KEYBOARD[char];
@@ -5089,6 +6650,9 @@ var Page = class {
5089
6650
  if (options.optional) return false;
5090
6651
  throw e;
5091
6652
  }
6653
+ const selectPos = await this.getElementPosition({ nodeId: element.nodeId });
6654
+ if (selectPos) this.setLastActionPosition(selectPos.center, selectPos.bbox);
6655
+ this._lastActionTargetMetadata = await this.getActionTargetMetadata({ objectId });
5092
6656
  const metadata = await this.getNativeSelectMetadata(objectId, values);
5093
6657
  if (!metadata.isSelect) {
5094
6658
  throw new Error("select() target must be a native <select> element");
@@ -5225,6 +6789,8 @@ var Page = class {
5225
6789
  if (options.optional) return false;
5226
6790
  throw e;
5227
6791
  }
6792
+ const checkPos = await this.getElementPosition({ nodeId: element.nodeId });
6793
+ if (checkPos) this.setLastActionPosition(checkPos.center, checkPos.bbox);
5228
6794
  const before = await this.cdp.send("Runtime.callFunctionOn", {
5229
6795
  objectId: object.objectId,
5230
6796
  functionDeclaration: "function() { return !!this.checked; }",
@@ -5273,6 +6839,8 @@ var Page = class {
5273
6839
  if (options.optional) return false;
5274
6840
  throw e;
5275
6841
  }
6842
+ const uncheckPos = await this.getElementPosition({ nodeId: element.nodeId });
6843
+ if (uncheckPos) this.setLastActionPosition(uncheckPos.center, uncheckPos.bbox);
5276
6844
  const isRadio = await this.cdp.send(
5277
6845
  "Runtime.callFunctionOn",
5278
6846
  {
@@ -5328,6 +6896,8 @@ var Page = class {
5328
6896
  throw new ElementNotFoundError(selector, hints);
5329
6897
  }
5330
6898
  const objectId = await this.resolveObjectId(element.nodeId);
6899
+ const submitPos = await this.getElementPosition({ nodeId: element.nodeId });
6900
+ if (submitPos) this.setLastActionPosition(submitPos.center, submitPos.bbox);
5331
6901
  const isFormElement = await this.cdp.send(
5332
6902
  "Runtime.callFunctionOn",
5333
6903
  {
@@ -5424,6 +6994,8 @@ var Page = class {
5424
6994
  const hints = await generateHints(this, selectorList, "focus");
5425
6995
  throw new ElementNotFoundError(selector, hints);
5426
6996
  }
6997
+ const focusPos = await this.getElementPosition({ nodeId: element.nodeId });
6998
+ if (focusPos) this.setLastActionPosition(focusPos.center, focusPos.bbox);
5427
6999
  await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
5428
7000
  return true;
5429
7001
  }
@@ -5459,6 +7031,14 @@ var Page = class {
5459
7031
  const quad = quads[0];
5460
7032
  x = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
5461
7033
  y = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
7034
+ const minX = Math.min(quad[0], quad[2], quad[4], quad[6]);
7035
+ const maxX = Math.max(quad[0], quad[2], quad[4], quad[6]);
7036
+ const minY = Math.min(quad[1], quad[3], quad[5], quad[7]);
7037
+ const maxY = Math.max(quad[1], quad[3], quad[5], quad[7]);
7038
+ this.setLastActionPosition(
7039
+ { x, y },
7040
+ { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
7041
+ );
5462
7042
  } else {
5463
7043
  throw new Error("No quads");
5464
7044
  }
@@ -5470,6 +7050,10 @@ var Page = class {
5470
7050
  }
5471
7051
  x = box.content[0] + box.width / 2;
5472
7052
  y = box.content[1] + box.height / 2;
7053
+ this.setLastActionPosition(
7054
+ { x, y },
7055
+ { x: box.content[0], y: box.content[1], width: box.width, height: box.height }
7056
+ );
5473
7057
  }
5474
7058
  await this.cdp.send("Input.dispatchMouseEvent", {
5475
7059
  type: "mouseMoved",
@@ -5495,6 +7079,8 @@ var Page = class {
5495
7079
  if (options.optional) return false;
5496
7080
  throw new ElementNotFoundError(selector);
5497
7081
  }
7082
+ const scrollPos = await this.getElementPosition({ nodeId: element.nodeId });
7083
+ if (scrollPos) this.setLastActionPosition(scrollPos.center, scrollPos.bbox);
5498
7084
  await this.scrollIntoView(element.nodeId);
5499
7085
  return true;
5500
7086
  }
@@ -6256,7 +7842,7 @@ var Page = class {
6256
7842
  return {
6257
7843
  role,
6258
7844
  name,
6259
- value: value !== void 0 ? String(value) : void 0,
7845
+ value: value !== void 0 ? stringifyUnknown(value) : void 0,
6260
7846
  ref,
6261
7847
  children: children.length > 0 ? children : void 0,
6262
7848
  disabled,
@@ -6318,7 +7904,7 @@ var Page = class {
6318
7904
  selector,
6319
7905
  disabled,
6320
7906
  checked,
6321
- value: value !== void 0 ? String(value) : void 0
7907
+ value: value !== void 0 ? stringifyUnknown(value) : void 0
6322
7908
  });
6323
7909
  }
6324
7910
  }
@@ -6788,7 +8374,7 @@ var Page = class {
6788
8374
  */
6789
8375
  formatConsoleArgs(args) {
6790
8376
  return args.map((arg) => {
6791
- if (arg.value !== void 0) return String(arg.value);
8377
+ if (arg.value !== void 0) return stringifyUnknown(arg.value);
6792
8378
  if (arg.description) return arg.description;
6793
8379
  return "[object]";
6794
8380
  }).join(" ");
@@ -7577,6 +9163,25 @@ var Browser = class _Browser {
7577
9163
  this.cdp = cdp;
7578
9164
  this.providerSession = providerSession;
7579
9165
  }
9166
+ /**
9167
+ * Create a Browser from an existing CDPClient (used by daemon fast-path).
9168
+ * The caller is responsible for the CDP connection lifecycle.
9169
+ */
9170
+ static fromCDP(cdp, sessionInfo) {
9171
+ const providerSession = {
9172
+ wsUrl: sessionInfo.wsUrl,
9173
+ sessionId: sessionInfo.sessionId,
9174
+ async close() {
9175
+ }
9176
+ };
9177
+ const provider = {
9178
+ name: sessionInfo.provider ?? "daemon",
9179
+ async createSession() {
9180
+ return providerSession;
9181
+ }
9182
+ };
9183
+ return new _Browser(cdp, provider, providerSession, { provider: "generic" });
9184
+ }
7580
9185
  /**
7581
9186
  * Connect to a browser instance
7582
9187
  */