browser-pilot 0.0.13 → 0.0.14

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;
@@ -688,6 +922,13 @@ var CDPError = class extends Error {
688
922
 
689
923
  // src/actions/executor.ts
690
924
  var DEFAULT_TIMEOUT = 3e4;
925
+ var DEFAULT_RECORDING_SKIP_ACTIONS = [
926
+ "wait",
927
+ "snapshot",
928
+ "forms",
929
+ "text",
930
+ "screenshot"
931
+ ];
691
932
  function classifyFailure(error) {
692
933
  if (error instanceof ElementNotFoundError) {
693
934
  return { reason: "missing" };
@@ -767,6 +1008,9 @@ var BatchExecutor = class {
767
1008
  const { timeout = DEFAULT_TIMEOUT, onFail = "stop" } = options;
768
1009
  const results = [];
769
1010
  const startTime = Date.now();
1011
+ const recording = options.record ? this.createRecordingContext(options.record) : null;
1012
+ const startUrl = recording ? await this.getPageUrlSafe() : "";
1013
+ let stoppedAtIndex;
770
1014
  for (let i = 0; i < steps.length; i++) {
771
1015
  const step = steps[i];
772
1016
  const stepStart = Date.now();
@@ -779,8 +1023,9 @@ var BatchExecutor = class {
779
1023
  await new Promise((resolve) => setTimeout(resolve, retryDelay));
780
1024
  }
781
1025
  try {
1026
+ this.page.resetLastActionPosition();
782
1027
  const result = await this.executeStep(step, timeout);
783
- results.push({
1028
+ const stepResult = {
784
1029
  index: i,
785
1030
  action: step.action,
786
1031
  selector: step.selector,
@@ -788,8 +1033,15 @@ var BatchExecutor = class {
788
1033
  success: true,
789
1034
  durationMs: Date.now() - stepStart,
790
1035
  result: result.value,
791
- text: result.text
792
- });
1036
+ text: result.text,
1037
+ timestamp: Date.now(),
1038
+ coordinates: this.page.getLastActionCoordinates() ?? void 0,
1039
+ boundingBox: this.page.getLastActionBoundingBox() ?? void 0
1040
+ };
1041
+ if (recording && !recording.skipActions.has(step.action)) {
1042
+ await this.captureRecordingFrame(step, stepResult, recording);
1043
+ }
1044
+ results.push(stepResult);
793
1045
  succeeded = true;
794
1046
  break;
795
1047
  } catch (error) {
@@ -810,7 +1062,7 @@ var BatchExecutor = class {
810
1062
  } catch {
811
1063
  }
812
1064
  }
813
- results.push({
1065
+ const failedResult = {
814
1066
  index: i,
815
1067
  action: step.action,
816
1068
  selector: step.selector,
@@ -820,25 +1072,177 @@ var BatchExecutor = class {
820
1072
  hints,
821
1073
  failureReason: reason,
822
1074
  coveringElement,
823
- suggestion: getSuggestion(reason)
824
- });
1075
+ suggestion: getSuggestion(reason),
1076
+ timestamp: Date.now()
1077
+ };
1078
+ if (recording && !recording.skipActions.has(step.action)) {
1079
+ await this.captureRecordingFrame(step, failedResult, recording);
1080
+ }
1081
+ results.push(failedResult);
825
1082
  if (onFail === "stop" && !step.optional) {
826
- return {
827
- success: false,
828
- stoppedAtIndex: i,
829
- steps: results,
830
- totalDurationMs: Date.now() - startTime
831
- };
1083
+ stoppedAtIndex = i;
1084
+ break;
832
1085
  }
833
1086
  }
834
1087
  }
835
- const allSuccess = results.every((r) => r.success || steps[r.index]?.optional);
1088
+ const totalDurationMs = Date.now() - startTime;
1089
+ const allSuccess = stoppedAtIndex === void 0 && results.every((result) => result.success || steps[result.index]?.optional);
1090
+ let recordingManifest;
1091
+ if (recording) {
1092
+ recordingManifest = await this.writeRecordingManifest(
1093
+ recording,
1094
+ startTime,
1095
+ startUrl,
1096
+ allSuccess
1097
+ );
1098
+ }
836
1099
  return {
837
1100
  success: allSuccess,
1101
+ stoppedAtIndex,
838
1102
  steps: results,
839
- totalDurationMs: Date.now() - startTime
1103
+ totalDurationMs,
1104
+ recordingManifest
1105
+ };
1106
+ }
1107
+ createRecordingContext(record) {
1108
+ const baseDir = record.outputDir ?? (0, import_node_path.join)(process.cwd(), ".browser-pilot");
1109
+ const screenshotDir = (0, import_node_path.join)(baseDir, "screenshots");
1110
+ 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
+ }
1119
+ fs.mkdirSync(screenshotDir, { recursive: true });
1120
+ return {
1121
+ baseDir,
1122
+ screenshotDir,
1123
+ sessionId: record.sessionId ?? this.page.targetId,
1124
+ frames: existingFrames,
1125
+ format: record.format ?? "webp",
1126
+ quality: Math.max(0, Math.min(100, record.quality ?? 40)),
1127
+ highlights: record.highlights !== false,
1128
+ skipActions: new Set(record.skipActions ?? DEFAULT_RECORDING_SKIP_ACTIONS)
840
1129
  };
841
1130
  }
1131
+ async getPageUrlSafe() {
1132
+ try {
1133
+ return await this.page.url();
1134
+ } catch {
1135
+ return "";
1136
+ }
1137
+ }
1138
+ /**
1139
+ * Capture a recording screenshot frame with optional highlight overlay
1140
+ */
1141
+ async captureRecordingFrame(step, stepResult, recording) {
1142
+ const targetMetadata = this.page.getLastActionTargetMetadata();
1143
+ let highlightInjected = false;
1144
+ try {
1145
+ const ts = Date.now();
1146
+ const seq = String(recording.frames.length + 1).padStart(4, "0");
1147
+ const filename = `${seq}-${ts}-${stepResult.action}.${recording.format}`;
1148
+ const filepath = (0, import_node_path.join)(recording.screenshotDir, filename);
1149
+ if (recording.highlights) {
1150
+ const kind = stepToHighlightKind(stepResult);
1151
+ if (kind) {
1152
+ await injectActionHighlight(this.page, {
1153
+ kind,
1154
+ bbox: stepResult.boundingBox,
1155
+ point: stepResult.coordinates,
1156
+ label: getHighlightLabel(step, stepResult, targetMetadata)
1157
+ });
1158
+ highlightInjected = true;
1159
+ }
1160
+ }
1161
+ const base64 = await this.page.screenshot({
1162
+ format: recording.format,
1163
+ quality: recording.quality
1164
+ });
1165
+ const buffer = Buffer.from(base64, "base64");
1166
+ fs.writeFileSync(filepath, buffer);
1167
+ stepResult.screenshotPath = filepath;
1168
+ let pageUrl;
1169
+ let pageTitle;
1170
+ try {
1171
+ pageUrl = await this.page.url();
1172
+ pageTitle = await this.page.title();
1173
+ } catch {
1174
+ }
1175
+ recording.frames.push({
1176
+ seq: recording.frames.length + 1,
1177
+ timestamp: ts,
1178
+ action: stepResult.action,
1179
+ selector: stepResult.selectorUsed ?? (Array.isArray(step.selector) ? step.selector[0] : step.selector),
1180
+ value: redactValueForRecording(
1181
+ typeof step.value === "string" ? step.value : void 0,
1182
+ targetMetadata
1183
+ ),
1184
+ url: step.url,
1185
+ coordinates: stepResult.coordinates,
1186
+ boundingBox: stepResult.boundingBox,
1187
+ success: stepResult.success,
1188
+ durationMs: stepResult.durationMs,
1189
+ error: stepResult.error,
1190
+ screenshot: filename,
1191
+ pageUrl,
1192
+ pageTitle
1193
+ });
1194
+ } catch {
1195
+ } finally {
1196
+ if (recording.highlights || highlightInjected) {
1197
+ await removeActionHighlight(this.page);
1198
+ }
1199
+ }
1200
+ }
1201
+ /**
1202
+ * Write recording manifest to disk
1203
+ */
1204
+ async writeRecordingManifest(recording, startTime, startUrl, success) {
1205
+ let endUrl = startUrl;
1206
+ let viewport = { width: 1280, height: 720 };
1207
+ try {
1208
+ endUrl = await this.page.url();
1209
+ } catch {
1210
+ }
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
+ const manifestPath = (0, import_node_path.join)(recording.baseDir, "recording.json");
1220
+ let recordedAt = new Date(startTime).toISOString();
1221
+ 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,
1232
+ recordedAt,
1233
+ sessionId: recording.sessionId,
1234
+ startUrl: originalStartUrl,
1235
+ endUrl,
1236
+ viewport,
1237
+ format: recording.format,
1238
+ quality: recording.quality,
1239
+ totalDurationMs,
1240
+ success,
1241
+ frames: recording.frames
1242
+ };
1243
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
1244
+ return manifestPath;
1245
+ }
842
1246
  /**
843
1247
  * Execute a single step
844
1248
  */
@@ -3189,6 +3593,24 @@ Content-Type: ${contentType}\r
3189
3593
  parts.push(data);
3190
3594
  }
3191
3595
 
3596
+ // src/utils/json.ts
3597
+ function isRecord(value) {
3598
+ return typeof value === "object" && value !== null;
3599
+ }
3600
+ function stringifyUnknown(value) {
3601
+ if (typeof value === "string") return value;
3602
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
3603
+ return String(value);
3604
+ }
3605
+ if (value === null) return "null";
3606
+ if (value === void 0) return "undefined";
3607
+ try {
3608
+ return JSON.stringify(value);
3609
+ } catch {
3610
+ return Object.prototype.toString.call(value);
3611
+ }
3612
+ }
3613
+
3192
3614
  // src/cdp/transport.ts
3193
3615
  function createTransport(wsUrl, options = {}) {
3194
3616
  const { timeout = 3e4 } = options;
@@ -3292,8 +3714,12 @@ function getReadyStateString(state) {
3292
3714
 
3293
3715
  // src/cdp/client.ts
3294
3716
  async function createCDPClient(wsUrl, options = {}) {
3295
- const { debug = false, timeout = 3e4 } = options;
3717
+ const { timeout = 3e4 } = options;
3296
3718
  const transport = await createTransport(wsUrl, { timeout });
3719
+ return buildCDPClient(transport, options);
3720
+ }
3721
+ function buildCDPClient(transport, options = {}) {
3722
+ const { debug = false, timeout = 3e4 } = options;
3297
3723
  let messageId = 0;
3298
3724
  let currentSessionId;
3299
3725
  let connected = true;
@@ -3303,7 +3729,19 @@ async function createCDPClient(wsUrl, options = {}) {
3303
3729
  transport.onMessage((raw) => {
3304
3730
  let msg;
3305
3731
  try {
3306
- msg = JSON.parse(raw);
3732
+ const parsed = JSON.parse(raw);
3733
+ if (!isRecord(parsed)) {
3734
+ if (debug) console.error("[CDP] Ignoring non-object message:", raw);
3735
+ return;
3736
+ }
3737
+ if ("id" in parsed && typeof parsed["id"] === "number") {
3738
+ msg = parsed;
3739
+ } else if ("method" in parsed && typeof parsed["method"] === "string") {
3740
+ msg = parsed;
3741
+ } else {
3742
+ if (debug) console.error("[CDP] Ignoring invalid message shape:", raw);
3743
+ return;
3744
+ }
3307
3745
  } catch {
3308
3746
  if (debug) console.error("[CDP] Failed to parse message:", raw);
3309
3747
  return;
@@ -3416,6 +3854,9 @@ async function createCDPClient(wsUrl, options = {}) {
3416
3854
  onAny(handler) {
3417
3855
  anyEventHandlers.add(handler);
3418
3856
  },
3857
+ offAny(handler) {
3858
+ anyEventHandlers.delete(handler);
3859
+ },
3419
3860
  async close() {
3420
3861
  connected = false;
3421
3862
  await transport.close();
@@ -4666,6 +5107,9 @@ var Page = class {
4666
5107
  brokenFrame = null;
4667
5108
  /** Last matched selector from findElement (for selectorUsed tracking) */
4668
5109
  _lastMatchedSelector;
5110
+ _lastActionCoordinates = null;
5111
+ _lastActionBoundingBox = null;
5112
+ _lastActionTargetMetadata = null;
4669
5113
  /** Last snapshot for stale ref recovery */
4670
5114
  lastSnapshot;
4671
5115
  /** Audio input controller (lazy-initialized) */
@@ -4697,6 +5141,76 @@ var Page = class {
4697
5141
  getLastMatchedSelector() {
4698
5142
  return this._lastMatchedSelector;
4699
5143
  }
5144
+ async getActionTargetMetadata(identifiers) {
5145
+ try {
5146
+ const objectId = identifiers.objectId ?? (identifiers.nodeId ? await this.resolveObjectId(identifiers.nodeId) : void 0);
5147
+ if (!objectId) return null;
5148
+ const response = await this.cdp.send("Runtime.callFunctionOn", {
5149
+ objectId,
5150
+ functionDeclaration: `function() {
5151
+ const tagName = this.tagName?.toLowerCase?.() || '';
5152
+ const inputType =
5153
+ tagName === 'input' && typeof this.type === 'string' ? this.type.toLowerCase() : '';
5154
+ const autocomplete =
5155
+ typeof this.autocomplete === 'string' ? this.autocomplete.toLowerCase() : '';
5156
+ return { tagName, inputType, autocomplete };
5157
+ }`,
5158
+ returnByValue: true
5159
+ });
5160
+ return response.result.value ?? null;
5161
+ } catch {
5162
+ return null;
5163
+ }
5164
+ }
5165
+ async getElementPosition(identifiers) {
5166
+ try {
5167
+ const { quads } = await this.cdp.send(
5168
+ "DOM.getContentQuads",
5169
+ identifiers
5170
+ );
5171
+ if (quads?.length > 0) {
5172
+ const q = quads[0];
5173
+ const minX = Math.min(q[0], q[2], q[4], q[6]);
5174
+ const maxX = Math.max(q[0], q[2], q[4], q[6]);
5175
+ const minY = Math.min(q[1], q[3], q[5], q[7]);
5176
+ const maxY = Math.max(q[1], q[3], q[5], q[7]);
5177
+ return {
5178
+ center: { x: (minX + maxX) / 2, y: (minY + maxY) / 2 },
5179
+ bbox: { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
5180
+ };
5181
+ }
5182
+ } catch {
5183
+ }
5184
+ if (identifiers.nodeId) {
5185
+ const box = await this.getBoxModel(identifiers.nodeId);
5186
+ if (box) {
5187
+ return {
5188
+ center: { x: box.content[0] + box.width / 2, y: box.content[1] + box.height / 2 },
5189
+ bbox: { x: box.content[0], y: box.content[1], width: box.width, height: box.height }
5190
+ };
5191
+ }
5192
+ }
5193
+ return null;
5194
+ }
5195
+ setLastActionPosition(coords, bbox) {
5196
+ this._lastActionCoordinates = coords;
5197
+ this._lastActionBoundingBox = bbox;
5198
+ }
5199
+ getLastActionCoordinates() {
5200
+ return this._lastActionCoordinates;
5201
+ }
5202
+ getLastActionBoundingBox() {
5203
+ return this._lastActionBoundingBox;
5204
+ }
5205
+ getLastActionTargetMetadata() {
5206
+ return this._lastActionTargetMetadata;
5207
+ }
5208
+ /** Reset position tracking (call before each executor step) */
5209
+ resetLastActionPosition() {
5210
+ this._lastActionCoordinates = null;
5211
+ this._lastActionBoundingBox = null;
5212
+ this._lastActionTargetMetadata = null;
5213
+ }
4700
5214
  /**
4701
5215
  * Initialize the page (enable required CDP domains)
4702
5216
  */
@@ -4864,6 +5378,14 @@ var Page = class {
4864
5378
  const quad = quads[0];
4865
5379
  clickX = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
4866
5380
  clickY = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
5381
+ const minX = Math.min(quad[0], quad[2], quad[4], quad[6]);
5382
+ const maxX = Math.max(quad[0], quad[2], quad[4], quad[6]);
5383
+ const minY = Math.min(quad[1], quad[3], quad[5], quad[7]);
5384
+ const maxY = Math.max(quad[1], quad[3], quad[5], quad[7]);
5385
+ this.setLastActionPosition(
5386
+ { x: clickX, y: clickY },
5387
+ { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
5388
+ );
4867
5389
  } else {
4868
5390
  throw new Error("No quads");
4869
5391
  }
@@ -4872,6 +5394,10 @@ var Page = class {
4872
5394
  if (!box) throw new Error("Could not get element position");
4873
5395
  clickX = box.content[0] + box.width / 2;
4874
5396
  clickY = box.content[1] + box.height / 2;
5397
+ this.setLastActionPosition(
5398
+ { x: clickX, y: clickY },
5399
+ { x: box.content[0], y: box.content[1], width: box.width, height: box.height }
5400
+ );
4875
5401
  }
4876
5402
  const hitTargetCoordinates = this.currentFrame ? void 0 : { x: clickX, y: clickY };
4877
5403
  const HIT_TARGET_RETRIES = 3;
@@ -4922,13 +5448,20 @@ var Page = class {
4922
5448
  if (options.optional) return false;
4923
5449
  throw e;
4924
5450
  }
5451
+ const fillPos = await this.getElementPosition({ nodeId: element.nodeId });
5452
+ if (fillPos) this.setLastActionPosition(fillPos.center, fillPos.bbox);
4925
5453
  const tagInfo = await this.cdp.send("Runtime.callFunctionOn", {
4926
5454
  objectId,
4927
5455
  functionDeclaration: `function() {
4928
- return { tagName: this.tagName?.toLowerCase() || '', inputType: (this.type || '').toLowerCase() };
5456
+ return {
5457
+ tagName: this.tagName?.toLowerCase() || '',
5458
+ inputType: (this.type || '').toLowerCase(),
5459
+ autocomplete: typeof this.autocomplete === 'string' ? this.autocomplete.toLowerCase() : '',
5460
+ };
4929
5461
  }`,
4930
5462
  returnByValue: true
4931
5463
  });
5464
+ this._lastActionTargetMetadata = tagInfo.result.value;
4932
5465
  const { tagName, inputType } = tagInfo.result.value;
4933
5466
  const specialInputTypes = /* @__PURE__ */ new Set([
4934
5467
  "date",
@@ -5010,6 +5543,9 @@ var Page = class {
5010
5543
  if (options.optional) return false;
5011
5544
  throw e;
5012
5545
  }
5546
+ const typePos = await this.getElementPosition({ nodeId: element.nodeId });
5547
+ if (typePos) this.setLastActionPosition(typePos.center, typePos.bbox);
5548
+ this._lastActionTargetMetadata = await this.getActionTargetMetadata({ objectId });
5013
5549
  await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
5014
5550
  for (const char of text) {
5015
5551
  const def = US_KEYBOARD[char];
@@ -5089,6 +5625,9 @@ var Page = class {
5089
5625
  if (options.optional) return false;
5090
5626
  throw e;
5091
5627
  }
5628
+ const selectPos = await this.getElementPosition({ nodeId: element.nodeId });
5629
+ if (selectPos) this.setLastActionPosition(selectPos.center, selectPos.bbox);
5630
+ this._lastActionTargetMetadata = await this.getActionTargetMetadata({ objectId });
5092
5631
  const metadata = await this.getNativeSelectMetadata(objectId, values);
5093
5632
  if (!metadata.isSelect) {
5094
5633
  throw new Error("select() target must be a native <select> element");
@@ -5225,6 +5764,8 @@ var Page = class {
5225
5764
  if (options.optional) return false;
5226
5765
  throw e;
5227
5766
  }
5767
+ const checkPos = await this.getElementPosition({ nodeId: element.nodeId });
5768
+ if (checkPos) this.setLastActionPosition(checkPos.center, checkPos.bbox);
5228
5769
  const before = await this.cdp.send("Runtime.callFunctionOn", {
5229
5770
  objectId: object.objectId,
5230
5771
  functionDeclaration: "function() { return !!this.checked; }",
@@ -5273,6 +5814,8 @@ var Page = class {
5273
5814
  if (options.optional) return false;
5274
5815
  throw e;
5275
5816
  }
5817
+ const uncheckPos = await this.getElementPosition({ nodeId: element.nodeId });
5818
+ if (uncheckPos) this.setLastActionPosition(uncheckPos.center, uncheckPos.bbox);
5276
5819
  const isRadio = await this.cdp.send(
5277
5820
  "Runtime.callFunctionOn",
5278
5821
  {
@@ -5328,6 +5871,8 @@ var Page = class {
5328
5871
  throw new ElementNotFoundError(selector, hints);
5329
5872
  }
5330
5873
  const objectId = await this.resolveObjectId(element.nodeId);
5874
+ const submitPos = await this.getElementPosition({ nodeId: element.nodeId });
5875
+ if (submitPos) this.setLastActionPosition(submitPos.center, submitPos.bbox);
5331
5876
  const isFormElement = await this.cdp.send(
5332
5877
  "Runtime.callFunctionOn",
5333
5878
  {
@@ -5424,6 +5969,8 @@ var Page = class {
5424
5969
  const hints = await generateHints(this, selectorList, "focus");
5425
5970
  throw new ElementNotFoundError(selector, hints);
5426
5971
  }
5972
+ const focusPos = await this.getElementPosition({ nodeId: element.nodeId });
5973
+ if (focusPos) this.setLastActionPosition(focusPos.center, focusPos.bbox);
5427
5974
  await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
5428
5975
  return true;
5429
5976
  }
@@ -5459,6 +6006,14 @@ var Page = class {
5459
6006
  const quad = quads[0];
5460
6007
  x = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
5461
6008
  y = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
6009
+ const minX = Math.min(quad[0], quad[2], quad[4], quad[6]);
6010
+ const maxX = Math.max(quad[0], quad[2], quad[4], quad[6]);
6011
+ const minY = Math.min(quad[1], quad[3], quad[5], quad[7]);
6012
+ const maxY = Math.max(quad[1], quad[3], quad[5], quad[7]);
6013
+ this.setLastActionPosition(
6014
+ { x, y },
6015
+ { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
6016
+ );
5462
6017
  } else {
5463
6018
  throw new Error("No quads");
5464
6019
  }
@@ -5470,6 +6025,10 @@ var Page = class {
5470
6025
  }
5471
6026
  x = box.content[0] + box.width / 2;
5472
6027
  y = box.content[1] + box.height / 2;
6028
+ this.setLastActionPosition(
6029
+ { x, y },
6030
+ { x: box.content[0], y: box.content[1], width: box.width, height: box.height }
6031
+ );
5473
6032
  }
5474
6033
  await this.cdp.send("Input.dispatchMouseEvent", {
5475
6034
  type: "mouseMoved",
@@ -5495,6 +6054,8 @@ var Page = class {
5495
6054
  if (options.optional) return false;
5496
6055
  throw new ElementNotFoundError(selector);
5497
6056
  }
6057
+ const scrollPos = await this.getElementPosition({ nodeId: element.nodeId });
6058
+ if (scrollPos) this.setLastActionPosition(scrollPos.center, scrollPos.bbox);
5498
6059
  await this.scrollIntoView(element.nodeId);
5499
6060
  return true;
5500
6061
  }
@@ -6256,7 +6817,7 @@ var Page = class {
6256
6817
  return {
6257
6818
  role,
6258
6819
  name,
6259
- value: value !== void 0 ? String(value) : void 0,
6820
+ value: value !== void 0 ? stringifyUnknown(value) : void 0,
6260
6821
  ref,
6261
6822
  children: children.length > 0 ? children : void 0,
6262
6823
  disabled,
@@ -6318,7 +6879,7 @@ var Page = class {
6318
6879
  selector,
6319
6880
  disabled,
6320
6881
  checked,
6321
- value: value !== void 0 ? String(value) : void 0
6882
+ value: value !== void 0 ? stringifyUnknown(value) : void 0
6322
6883
  });
6323
6884
  }
6324
6885
  }
@@ -6788,7 +7349,7 @@ var Page = class {
6788
7349
  */
6789
7350
  formatConsoleArgs(args) {
6790
7351
  return args.map((arg) => {
6791
- if (arg.value !== void 0) return String(arg.value);
7352
+ if (arg.value !== void 0) return stringifyUnknown(arg.value);
6792
7353
  if (arg.description) return arg.description;
6793
7354
  return "[object]";
6794
7355
  }).join(" ");
@@ -7577,6 +8138,25 @@ var Browser = class _Browser {
7577
8138
  this.cdp = cdp;
7578
8139
  this.providerSession = providerSession;
7579
8140
  }
8141
+ /**
8142
+ * Create a Browser from an existing CDPClient (used by daemon fast-path).
8143
+ * The caller is responsible for the CDP connection lifecycle.
8144
+ */
8145
+ static fromCDP(cdp, sessionInfo) {
8146
+ const providerSession = {
8147
+ wsUrl: sessionInfo.wsUrl,
8148
+ sessionId: sessionInfo.sessionId,
8149
+ async close() {
8150
+ }
8151
+ };
8152
+ const provider = {
8153
+ name: sessionInfo.provider ?? "daemon",
8154
+ async createSession() {
8155
+ return providerSession;
8156
+ }
8157
+ };
8158
+ return new _Browser(cdp, provider, providerSession, { provider: "generic" });
8159
+ }
7580
8160
  /**
7581
8161
  * Connect to a browser instance
7582
8162
  */