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/browser.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/browser/index.ts
@@ -29,6 +39,24 @@ __export(browser_exports, {
29
39
  });
30
40
  module.exports = __toCommonJS(browser_exports);
31
41
 
42
+ // src/utils/json.ts
43
+ function isRecord(value) {
44
+ return typeof value === "object" && value !== null;
45
+ }
46
+ function stringifyUnknown(value) {
47
+ if (typeof value === "string") return value;
48
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
49
+ return String(value);
50
+ }
51
+ if (value === null) return "null";
52
+ if (value === void 0) return "undefined";
53
+ try {
54
+ return JSON.stringify(value);
55
+ } catch {
56
+ return Object.prototype.toString.call(value);
57
+ }
58
+ }
59
+
32
60
  // src/cdp/protocol.ts
33
61
  var CDPError = class extends Error {
34
62
  code;
@@ -144,8 +172,12 @@ function getReadyStateString(state) {
144
172
 
145
173
  // src/cdp/client.ts
146
174
  async function createCDPClient(wsUrl, options = {}) {
147
- const { debug = false, timeout = 3e4 } = options;
175
+ const { timeout = 3e4 } = options;
148
176
  const transport = await createTransport(wsUrl, { timeout });
177
+ return buildCDPClient(transport, options);
178
+ }
179
+ function buildCDPClient(transport, options = {}) {
180
+ const { debug = false, timeout = 3e4 } = options;
149
181
  let messageId = 0;
150
182
  let currentSessionId;
151
183
  let connected = true;
@@ -155,7 +187,19 @@ async function createCDPClient(wsUrl, options = {}) {
155
187
  transport.onMessage((raw) => {
156
188
  let msg;
157
189
  try {
158
- msg = JSON.parse(raw);
190
+ const parsed = JSON.parse(raw);
191
+ if (!isRecord(parsed)) {
192
+ if (debug) console.error("[CDP] Ignoring non-object message:", raw);
193
+ return;
194
+ }
195
+ if ("id" in parsed && typeof parsed["id"] === "number") {
196
+ msg = parsed;
197
+ } else if ("method" in parsed && typeof parsed["method"] === "string") {
198
+ msg = parsed;
199
+ } else {
200
+ if (debug) console.error("[CDP] Ignoring invalid message shape:", raw);
201
+ return;
202
+ }
159
203
  } catch {
160
204
  if (debug) console.error("[CDP] Failed to parse message:", raw);
161
205
  return;
@@ -170,7 +214,8 @@ async function createCDPClient(wsUrl, options = {}) {
170
214
  pending.delete(response.id);
171
215
  clearTimeout(request.timer);
172
216
  if (response.error) {
173
- request.reject(new CDPError(response.error));
217
+ const error = typeof response.error === "string" ? { code: -32e3, message: response.error } : response.error;
218
+ request.reject(new CDPError(error));
174
219
  } else {
175
220
  request.resolve(response.result);
176
221
  }
@@ -268,6 +313,9 @@ async function createCDPClient(wsUrl, options = {}) {
268
313
  onAny(handler) {
269
314
  anyEventHandlers.add(handler);
270
315
  },
316
+ offAny(handler) {
317
+ anyEventHandlers.delete(handler);
318
+ },
271
319
  async close() {
272
320
  connected = false;
273
321
  await transport.close();
@@ -283,6 +331,9 @@ async function createCDPClient(wsUrl, options = {}) {
283
331
  get sessionId() {
284
332
  return currentSessionId;
285
333
  },
334
+ setSessionId(sessionId) {
335
+ currentSessionId = sessionId;
336
+ },
286
337
  get isConnected() {
287
338
  return connected;
288
339
  }
@@ -472,6 +523,230 @@ function createProvider(options) {
472
523
  }
473
524
  }
474
525
 
526
+ // src/actions/executor.ts
527
+ var fs = __toESM(require("fs"), 1);
528
+ var import_node_path = require("path");
529
+
530
+ // src/recording/redaction.ts
531
+ var REDACTED_VALUE = "[REDACTED]";
532
+ var SENSITIVE_AUTOCOMPLETE_TOKENS = [
533
+ "current-password",
534
+ "new-password",
535
+ "one-time-code",
536
+ "cc-number",
537
+ "cc-csc",
538
+ "cc-exp",
539
+ "cc-exp-month",
540
+ "cc-exp-year"
541
+ ];
542
+ function autocompleteTokens(autocomplete) {
543
+ if (!autocomplete) return [];
544
+ return autocomplete.toLowerCase().split(/\s+/).map((token) => token.trim()).filter(Boolean);
545
+ }
546
+ function isSensitiveFieldMetadata(metadata) {
547
+ if (!metadata) return false;
548
+ if (metadata.sensitiveValue) return true;
549
+ const inputType = metadata.inputType?.toLowerCase();
550
+ if (inputType === "password" || inputType === "hidden") {
551
+ return true;
552
+ }
553
+ const sensitiveAutocompleteTokens = new Set(SENSITIVE_AUTOCOMPLETE_TOKENS);
554
+ return autocompleteTokens(metadata.autocomplete).some(
555
+ (token) => sensitiveAutocompleteTokens.has(token)
556
+ );
557
+ }
558
+ function redactValueForRecording(value, metadata) {
559
+ if (value === void 0) return void 0;
560
+ return isSensitiveFieldMetadata(metadata) ? REDACTED_VALUE : value;
561
+ }
562
+
563
+ // src/browser/action-highlight.ts
564
+ var HIGHLIGHT_STYLES = {
565
+ click: { outline: "3px solid rgba(229,57,53,0.8)", badge: "#e53935", marker: "crosshair" },
566
+ fill: { outline: "3px solid rgba(33,150,243,0.8)", badge: "#2196f3" },
567
+ type: { outline: "3px solid rgba(33,150,243,0.6)", badge: "#2196f3" },
568
+ select: { outline: "3px solid rgba(156,39,176,0.8)", badge: "#9c27b0" },
569
+ hover: { outline: "2px dashed rgba(158,158,158,0.5)", badge: "#9e9e9e" },
570
+ scroll: { outline: "none", badge: "#607d8b", marker: "arrow" },
571
+ navigate: { outline: "none", badge: "#4caf50" },
572
+ submit: { outline: "3px solid rgba(255,152,0,0.8)", badge: "#ff9800" },
573
+ "assert-pass": { outline: "3px solid rgba(76,175,80,0.8)", badge: "#4caf50", marker: "check" },
574
+ "assert-fail": { outline: "3px solid rgba(244,67,54,0.8)", badge: "#f44336", marker: "cross" },
575
+ evaluate: { outline: "none", badge: "#ffc107" },
576
+ focus: { outline: "3px dotted rgba(33,150,243,0.6)", badge: "#2196f3" }
577
+ };
578
+ function buildHighlightScript(options) {
579
+ const style = HIGHLIGHT_STYLES[options.kind];
580
+ const label = options.label ? options.label.slice(0, 80) : void 0;
581
+ const escapedLabel = label ? label.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n") : "";
582
+ return `(function() {
583
+ // Remove any existing highlight
584
+ var existing = document.getElementById('__bp-action-highlight');
585
+ if (existing) existing.remove();
586
+
587
+ var container = document.createElement('div');
588
+ container.id = '__bp-action-highlight';
589
+ container.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:99999;';
590
+
591
+ ${options.bbox ? `
592
+ // Element outline
593
+ var outline = document.createElement('div');
594
+ outline.style.cssText = 'position:fixed;' +
595
+ 'left:${options.bbox.x}px;top:${options.bbox.y}px;' +
596
+ 'width:${options.bbox.width}px;height:${options.bbox.height}px;' +
597
+ '${style.outline !== "none" ? `outline:${style.outline};outline-offset:-1px;` : ""}' +
598
+ 'pointer-events:none;box-sizing:border-box;';
599
+ container.appendChild(outline);
600
+ ` : ""}
601
+
602
+ ${options.point && style.marker === "crosshair" ? `
603
+ // Crosshair at click point
604
+ var hLine = document.createElement('div');
605
+ hLine.style.cssText = 'position:fixed;left:${options.point.x - 12}px;top:${options.point.y}px;' +
606
+ 'width:24px;height:2px;background:${style.badge};pointer-events:none;';
607
+ var vLine = document.createElement('div');
608
+ vLine.style.cssText = 'position:fixed;left:${options.point.x}px;top:${options.point.y - 12}px;' +
609
+ 'width:2px;height:24px;background:${style.badge};pointer-events:none;';
610
+ // Dot at center
611
+ var dot = document.createElement('div');
612
+ dot.style.cssText = 'position:fixed;left:${options.point.x - 4}px;top:${options.point.y - 4}px;' +
613
+ 'width:8px;height:8px;border-radius:50%;background:${style.badge};pointer-events:none;';
614
+ container.appendChild(hLine);
615
+ container.appendChild(vLine);
616
+ container.appendChild(dot);
617
+ ` : ""}
618
+
619
+ ${label ? `
620
+ // Badge with label
621
+ var badge = document.createElement('div');
622
+ badge.style.cssText = 'position:fixed;' +
623
+ ${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;'"} +
624
+ 'background:${style.badge};color:white;padding:4px 8px;' +
625
+ 'font-family:monospace;font-size:12px;font-weight:bold;' +
626
+ 'border-radius:3px;white-space:nowrap;max-width:400px;overflow:hidden;text-overflow:ellipsis;' +
627
+ 'pointer-events:none;';
628
+ badge.textContent = '${escapedLabel}';
629
+ container.appendChild(badge);
630
+ ` : ""}
631
+
632
+ ${style.marker === "check" && options.bbox ? `
633
+ // Checkmark
634
+ var check = document.createElement('div');
635
+ check.style.cssText = 'position:fixed;left:${options.bbox.x + options.bbox.width / 2 - 10}px;' +
636
+ 'top:${options.bbox.y + options.bbox.height / 2 - 10}px;' +
637
+ 'width:20px;height:20px;font-size:18px;color:${style.badge};pointer-events:none;text-align:center;line-height:20px;';
638
+ check.textContent = '\\u2713';
639
+ container.appendChild(check);
640
+ ` : ""}
641
+
642
+ ${style.marker === "cross" && options.bbox ? `
643
+ // Cross mark
644
+ var cross = document.createElement('div');
645
+ cross.style.cssText = 'position:fixed;left:${options.bbox.x + options.bbox.width / 2 - 10}px;' +
646
+ 'top:${options.bbox.y + options.bbox.height / 2 - 10}px;' +
647
+ 'width:20px;height:20px;font-size:18px;color:${style.badge};pointer-events:none;text-align:center;line-height:20px;font-weight:bold;';
648
+ cross.textContent = '\\u2717';
649
+ container.appendChild(cross);
650
+ ` : ""}
651
+
652
+ document.body.appendChild(container);
653
+ window.__bpRemoveActionHighlight = function() {
654
+ var el = document.getElementById('__bp-action-highlight');
655
+ if (el) el.remove();
656
+ delete window.__bpRemoveActionHighlight;
657
+ };
658
+ })();`;
659
+ }
660
+ async function injectActionHighlight(page, options) {
661
+ try {
662
+ await page.evaluate(buildHighlightScript(options));
663
+ } catch {
664
+ }
665
+ }
666
+ async function removeActionHighlight(page) {
667
+ try {
668
+ await page.evaluate(`(function() {
669
+ if (window.__bpRemoveActionHighlight) {
670
+ window.__bpRemoveActionHighlight();
671
+ }
672
+ })()`);
673
+ } catch {
674
+ }
675
+ }
676
+ function stepToHighlightKind(step) {
677
+ switch (step.action) {
678
+ case "click":
679
+ return "click";
680
+ case "fill":
681
+ return "fill";
682
+ case "type":
683
+ return "type";
684
+ case "select":
685
+ return "select";
686
+ case "hover":
687
+ return "hover";
688
+ case "scroll":
689
+ return "scroll";
690
+ case "goto":
691
+ return "navigate";
692
+ case "submit":
693
+ return "submit";
694
+ case "focus":
695
+ return "focus";
696
+ case "evaluate":
697
+ case "press":
698
+ case "shortcut":
699
+ return "evaluate";
700
+ case "assertVisible":
701
+ case "assertExists":
702
+ case "assertText":
703
+ case "assertUrl":
704
+ case "assertValue":
705
+ return step.success ? "assert-pass" : "assert-fail";
706
+ // Observation-only actions — no highlight
707
+ case "wait":
708
+ case "snapshot":
709
+ case "forms":
710
+ case "text":
711
+ case "screenshot":
712
+ case "newTab":
713
+ case "closeTab":
714
+ case "switchFrame":
715
+ case "switchToMain":
716
+ return null;
717
+ default:
718
+ return null;
719
+ }
720
+ }
721
+ function getHighlightLabel(step, result, targetMetadata) {
722
+ switch (step.action) {
723
+ case "fill":
724
+ case "type":
725
+ return typeof step.value === "string" ? `"${redactValueForRecording(step.value, targetMetadata)}"` : void 0;
726
+ case "select":
727
+ return redactValueForRecording(
728
+ typeof step.value === "string" ? step.value : void 0,
729
+ targetMetadata
730
+ );
731
+ case "goto":
732
+ return step.url;
733
+ case "evaluate":
734
+ return "JS";
735
+ case "press":
736
+ return step.key;
737
+ case "shortcut":
738
+ return step.combo;
739
+ case "assertText":
740
+ case "assertUrl":
741
+ case "assertValue":
742
+ case "assertVisible":
743
+ case "assertExists":
744
+ return result.success ? "\u2713" : "\u2717";
745
+ default:
746
+ return void 0;
747
+ }
748
+ }
749
+
475
750
  // src/browser/actionability.ts
476
751
  var ActionabilityError = class extends Error {
477
752
  failureType;
@@ -1084,8 +1359,677 @@ var NavigationError = class extends Error {
1084
1359
  }
1085
1360
  };
1086
1361
 
1362
+ // src/trace/views.ts
1363
+ function takeRecent(events, limit = 5) {
1364
+ return events.slice(-limit).map((event) => ({
1365
+ ts: event.ts,
1366
+ event: event.event,
1367
+ summary: event.summary,
1368
+ severity: event.severity,
1369
+ url: event.url
1370
+ }));
1371
+ }
1372
+ function buildTraceSummaries(events) {
1373
+ return {
1374
+ ws: summarizeWs(events),
1375
+ voice: summarizeVoice(events),
1376
+ console: summarizeConsole(events),
1377
+ permissions: summarizePermissions(events),
1378
+ media: summarizeMedia(events),
1379
+ ui: summarizeUi(events),
1380
+ session: summarizeSession(events)
1381
+ };
1382
+ }
1383
+ function summarizeWs(events) {
1384
+ const relevant = events.filter((event) => event.channel === "ws" || event.event.startsWith("ws."));
1385
+ const connections = /* @__PURE__ */ new Map();
1386
+ for (const event of relevant) {
1387
+ const id = event.connectionId ?? event.requestId ?? event.traceId;
1388
+ let connection = connections.get(id);
1389
+ if (!connection) {
1390
+ connection = { id, sent: 0, received: 0, lastMessages: [] };
1391
+ connections.set(id, connection);
1392
+ }
1393
+ connection.url = event.url ?? connection.url;
1394
+ if (event.event === "ws.connection.created") {
1395
+ connection.createdAt = event.ts;
1396
+ }
1397
+ if (event.event === "ws.connection.closed") {
1398
+ connection.closedAt = event.ts;
1399
+ }
1400
+ if (event.event === "ws.frame.sent") {
1401
+ connection.sent += 1;
1402
+ const payload = typeof event.data["payload"] === "string" ? event.data["payload"] : "";
1403
+ if (payload) connection.lastMessages.push(`sent: ${payload}`);
1404
+ }
1405
+ if (event.event === "ws.frame.received") {
1406
+ connection.received += 1;
1407
+ const payload = typeof event.data["payload"] === "string" ? event.data["payload"] : "";
1408
+ if (payload) connection.lastMessages.push(`recv: ${payload}`);
1409
+ }
1410
+ connection.lastMessages = connection.lastMessages.slice(-3);
1411
+ }
1412
+ const values = [...connections.values()];
1413
+ const reconnects = values.reduce((count, connection) => {
1414
+ return connection.closedAt && !connection.createdAt ? count : count;
1415
+ }, 0);
1416
+ return {
1417
+ view: "ws",
1418
+ totalEvents: relevant.length,
1419
+ connections: values.map((connection) => ({
1420
+ id: connection.id,
1421
+ url: connection.url ?? null,
1422
+ createdAt: connection.createdAt ?? null,
1423
+ closedAt: connection.closedAt ?? null,
1424
+ sent: connection.sent,
1425
+ received: connection.received,
1426
+ lastMessages: connection.lastMessages,
1427
+ connectedButSilent: !!connection.createdAt && !connection.closedAt && connection.sent + connection.received === 0
1428
+ })),
1429
+ reconnects,
1430
+ recent: takeRecent(relevant)
1431
+ };
1432
+ }
1433
+ function summarizeConsole(events) {
1434
+ const relevant = events.filter(
1435
+ (event) => event.channel === "console" || event.event.startsWith("console.") || event.event === "runtime.exception" || event.event === "runtime.unhandledRejection"
1436
+ );
1437
+ return {
1438
+ view: "console",
1439
+ errors: relevant.filter(
1440
+ (event) => event.event === "console.error" || event.event === "runtime.exception" || event.event === "runtime.unhandledRejection"
1441
+ ).length,
1442
+ warnings: relevant.filter((event) => event.event === "console.warn").length,
1443
+ logs: relevant.filter((event) => event.event === "console.log").length,
1444
+ recent: takeRecent(relevant)
1445
+ };
1446
+ }
1447
+ function summarizePermissions(events) {
1448
+ const relevant = events.filter(
1449
+ (event) => event.channel === "permission" || event.event.startsWith("permission.")
1450
+ );
1451
+ const latest = /* @__PURE__ */ new Map();
1452
+ for (const event of relevant) {
1453
+ const name = typeof event.data["name"] === "string" ? event.data["name"] : null;
1454
+ const state = typeof event.data["state"] === "string" ? event.data["state"] : null;
1455
+ if (name && state) {
1456
+ latest.set(name, state);
1457
+ }
1458
+ }
1459
+ return {
1460
+ view: "permissions",
1461
+ states: Object.fromEntries(latest),
1462
+ changes: relevant.filter((event) => event.event === "permission.changed").length,
1463
+ recent: takeRecent(relevant)
1464
+ };
1465
+ }
1466
+ function summarizeMedia(events) {
1467
+ const relevant = events.filter(
1468
+ (event) => event.channel === "media" || event.event.startsWith("media.")
1469
+ );
1470
+ const liveTracks = /* @__PURE__ */ new Map();
1471
+ for (const event of relevant) {
1472
+ const label = typeof event.data["label"] === "string" ? event.data["label"] : "";
1473
+ const kind = typeof event.data["kind"] === "string" ? event.data["kind"] : "";
1474
+ const key = `${kind}:${label}`;
1475
+ if (event.event === "media.track.started") {
1476
+ liveTracks.set(key, kind);
1477
+ }
1478
+ if (event.event === "media.track.ended") {
1479
+ liveTracks.delete(key);
1480
+ }
1481
+ }
1482
+ return {
1483
+ view: "media",
1484
+ tracksStarted: relevant.filter((event) => event.event === "media.track.started").length,
1485
+ tracksEnded: relevant.filter((event) => event.event === "media.track.ended").length,
1486
+ playbackStarted: relevant.filter((event) => event.event === "media.playback.started").length,
1487
+ playbackStopped: relevant.filter((event) => event.event === "media.playback.stopped").length,
1488
+ liveTracks: [...liveTracks.values()],
1489
+ recent: takeRecent(relevant)
1490
+ };
1491
+ }
1492
+ function summarizeVoice(events) {
1493
+ const relevant = events.filter(
1494
+ (event) => event.channel === "voice" || event.event.startsWith("voice.")
1495
+ );
1496
+ return {
1497
+ view: "voice",
1498
+ ready: relevant.filter((event) => event.event === "voice.pipeline.ready").length,
1499
+ notReady: relevant.filter((event) => event.event === "voice.pipeline.notReady").length,
1500
+ captureStarted: relevant.filter((event) => event.event === "voice.capture.started").length,
1501
+ captureStopped: relevant.filter((event) => event.event === "voice.capture.stopped").length,
1502
+ detectedAudio: relevant.filter((event) => event.event === "voice.capture.detectedAudio").length,
1503
+ recent: takeRecent(relevant)
1504
+ };
1505
+ }
1506
+ function summarizeUi(events) {
1507
+ const relevant = events.filter(
1508
+ (event) => event.channel === "dom" || event.event.startsWith("dom.") || event.channel === "action"
1509
+ );
1510
+ return {
1511
+ view: "ui",
1512
+ actions: relevant.filter((event) => event.channel === "action").length,
1513
+ domChanges: relevant.filter((event) => event.channel === "dom").length,
1514
+ recent: takeRecent(relevant)
1515
+ };
1516
+ }
1517
+ function summarizeSession(events) {
1518
+ const byChannel = /* @__PURE__ */ new Map();
1519
+ const failedActions = events.filter((event) => event.event === "action.failed").length;
1520
+ for (const event of events) {
1521
+ byChannel.set(event.channel, (byChannel.get(event.channel) ?? 0) + 1);
1522
+ }
1523
+ return {
1524
+ view: "session",
1525
+ totalEvents: events.length,
1526
+ byChannel: Object.fromEntries(byChannel),
1527
+ failedActions,
1528
+ recent: takeRecent(events)
1529
+ };
1530
+ }
1531
+
1532
+ // src/recording/manifest.ts
1533
+ function isCanonicalRecordingManifest(value) {
1534
+ return Boolean(
1535
+ value && typeof value === "object" && value.version === 2 && typeof value.session === "object"
1536
+ );
1537
+ }
1538
+ function isLegacyRecordingManifest(value) {
1539
+ return Boolean(
1540
+ value && typeof value === "object" && value.version === 1 && Array.isArray(value.frames)
1541
+ );
1542
+ }
1543
+ function createRecordingManifest(input) {
1544
+ const actions = input.frames.map((frame) => {
1545
+ const actionId = frame.actionId ?? `action-${frame.seq}`;
1546
+ return {
1547
+ id: actionId,
1548
+ stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
1549
+ action: frame.action,
1550
+ selector: frame.selector,
1551
+ selectorUsed: frame.selectorUsed ?? frame.selector,
1552
+ value: frame.value,
1553
+ url: frame.url,
1554
+ success: frame.success,
1555
+ durationMs: frame.durationMs,
1556
+ error: frame.error,
1557
+ ts: new Date(frame.timestamp).toISOString(),
1558
+ pageUrl: frame.pageUrl,
1559
+ pageTitle: frame.pageTitle,
1560
+ coordinates: frame.coordinates,
1561
+ boundingBox: frame.boundingBox
1562
+ };
1563
+ });
1564
+ const screenshots = input.frames.map((frame) => ({
1565
+ id: `shot-${frame.seq}`,
1566
+ stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
1567
+ actionId: frame.actionId ?? `action-${frame.seq}`,
1568
+ file: frame.screenshot,
1569
+ ts: new Date(frame.timestamp).toISOString(),
1570
+ success: frame.success,
1571
+ pageUrl: frame.pageUrl,
1572
+ pageTitle: frame.pageTitle,
1573
+ coordinates: frame.coordinates,
1574
+ boundingBox: frame.boundingBox
1575
+ }));
1576
+ return {
1577
+ version: 2,
1578
+ recordedAt: input.recordedAt,
1579
+ session: {
1580
+ id: input.sessionId,
1581
+ startUrl: input.startUrl,
1582
+ endUrl: input.endUrl,
1583
+ targetId: input.targetId,
1584
+ profile: input.profile
1585
+ },
1586
+ recipe: {
1587
+ steps: input.steps
1588
+ },
1589
+ actions,
1590
+ screenshots,
1591
+ trace: {
1592
+ events: input.traceEvents,
1593
+ summaries: buildTraceSummaries(input.traceEvents)
1594
+ },
1595
+ assertions: input.assertions ?? [],
1596
+ notes: input.notes ?? [],
1597
+ artifacts: {
1598
+ recordingManifest: input.recordingManifest ?? "recording.json",
1599
+ screenshotDir: input.screenshotDir ?? "screenshots/"
1600
+ }
1601
+ };
1602
+ }
1603
+ function canonicalizeRecordingArtifact(value) {
1604
+ if (isCanonicalRecordingManifest(value)) {
1605
+ return value;
1606
+ }
1607
+ if (!isLegacyRecordingManifest(value)) {
1608
+ throw new Error("Unsupported recording artifact");
1609
+ }
1610
+ const traceEvents = buildTraceEventsFromLegacy(value);
1611
+ const steps = value.frames.map((frame) => frameToStep(frame));
1612
+ return createRecordingManifest({
1613
+ recordedAt: value.recordedAt,
1614
+ sessionId: value.sessionId,
1615
+ startUrl: value.startUrl,
1616
+ endUrl: value.endUrl,
1617
+ steps,
1618
+ frames: value.frames,
1619
+ traceEvents,
1620
+ notes: ["Converted from legacy recording manifest"]
1621
+ });
1622
+ }
1623
+ function buildTraceEventsFromLegacy(value) {
1624
+ const events = [];
1625
+ for (const frame of value.frames) {
1626
+ events.push({
1627
+ traceId: frame.actionId ?? `legacy-${frame.seq}`,
1628
+ sessionId: value.sessionId,
1629
+ ts: new Date(frame.timestamp).toISOString(),
1630
+ elapsedMs: frame.timestamp - new Date(value.recordedAt).getTime(),
1631
+ channel: "action",
1632
+ event: frame.success ? "action.succeeded" : "action.failed",
1633
+ severity: frame.success ? "info" : "error",
1634
+ summary: `${frame.action}${frame.selector ? ` ${frame.selector}` : ""}`,
1635
+ data: {
1636
+ action: frame.action,
1637
+ selector: frame.selector,
1638
+ value: frame.value ?? null,
1639
+ pageUrl: frame.pageUrl ?? null,
1640
+ pageTitle: frame.pageTitle ?? null,
1641
+ screenshot: frame.screenshot
1642
+ },
1643
+ actionId: frame.actionId ?? `action-${frame.seq}`,
1644
+ stepIndex: frame.stepIndex ?? Math.max(0, frame.seq - 1),
1645
+ selector: frame.selector,
1646
+ selectorUsed: frame.selectorUsed ?? frame.selector,
1647
+ url: frame.pageUrl ?? frame.url
1648
+ });
1649
+ }
1650
+ return events;
1651
+ }
1652
+ function frameToStep(frame) {
1653
+ switch (frame.action) {
1654
+ case "fill":
1655
+ return { action: "fill", selector: frame.selector, value: frame.value };
1656
+ case "submit":
1657
+ return { action: "submit", selector: frame.selector };
1658
+ case "goto":
1659
+ return { action: "goto", url: frame.url ?? frame.pageUrl };
1660
+ case "press":
1661
+ return { action: "press", key: frame.value ?? "Enter" };
1662
+ default:
1663
+ return { action: "click", selector: frame.selector };
1664
+ }
1665
+ }
1666
+
1667
+ // src/trace/script.ts
1668
+ var TRACE_BINDING_NAME = "__bpTraceBinding";
1669
+ var TRACE_SCRIPT = `
1670
+ (() => {
1671
+ if (window.__bpTraceInstalled) return;
1672
+ window.__bpTraceInstalled = true;
1673
+
1674
+ const binding = globalThis.${TRACE_BINDING_NAME};
1675
+ if (typeof binding !== 'function') return;
1676
+
1677
+ const emit = (event, data = {}, severity = 'info', summary) => {
1678
+ try {
1679
+ globalThis.__bpTraceRecentEvents = globalThis.__bpTraceRecentEvents || [];
1680
+ const payload = {
1681
+ event,
1682
+ severity,
1683
+ summary: summary || event,
1684
+ ts: Date.now(),
1685
+ data,
1686
+ };
1687
+ globalThis.__bpTraceRecentEvents.push(payload);
1688
+ if (globalThis.__bpTraceRecentEvents.length > 200) {
1689
+ globalThis.__bpTraceRecentEvents.splice(0, globalThis.__bpTraceRecentEvents.length - 200);
1690
+ }
1691
+ binding(JSON.stringify(payload));
1692
+ } catch {}
1693
+ };
1694
+
1695
+ const patchWebSocket = () => {
1696
+ const NativeWebSocket = window.WebSocket;
1697
+ if (typeof NativeWebSocket !== 'function' || window.__bpTraceWebSocketInstalled) return;
1698
+ window.__bpTraceWebSocketInstalled = true;
1699
+
1700
+ const nextId = () => Math.random().toString(36).slice(2, 10);
1701
+
1702
+ const patchInstance = (socket, urlValue) => {
1703
+ if (!socket || socket.__bpTracePatched) return socket;
1704
+ socket.__bpTracePatched = true;
1705
+ socket.__bpTraceId = socket.__bpTraceId || nextId();
1706
+ socket.__bpTraceUrl = String(urlValue || socket.url || '');
1707
+ globalThis.__bpTrackedWebSockets = globalThis.__bpTrackedWebSockets || new Set();
1708
+ globalThis.__bpTrackedWebSockets.add(socket);
1709
+
1710
+ emit(
1711
+ 'ws.connection.created',
1712
+ { connectionId: socket.__bpTraceId, url: socket.__bpTraceUrl },
1713
+ 'info',
1714
+ 'WebSocket opened ' + socket.__bpTraceUrl
1715
+ );
1716
+
1717
+ const originalSend = socket.send;
1718
+ socket.send = function(data) {
1719
+ const payload =
1720
+ typeof data === 'string'
1721
+ ? data
1722
+ : data && typeof data.toString === 'function'
1723
+ ? data.toString()
1724
+ : '[binary]';
1725
+ emit(
1726
+ 'ws.frame.sent',
1727
+ {
1728
+ connectionId: socket.__bpTraceId,
1729
+ url: socket.__bpTraceUrl,
1730
+ payload,
1731
+ length: payload.length,
1732
+ },
1733
+ 'info',
1734
+ 'WebSocket frame sent'
1735
+ );
1736
+ return originalSend.call(this, data);
1737
+ };
1738
+
1739
+ socket.addEventListener('message', (event) => {
1740
+ if (socket.__bpOfflineNotified || socket.__bpTraceClosed) {
1741
+ return;
1742
+ }
1743
+ const data = event && 'data' in event ? event.data : '';
1744
+ const payload =
1745
+ typeof data === 'string'
1746
+ ? data
1747
+ : data && typeof data.toString === 'function'
1748
+ ? data.toString()
1749
+ : '[binary]';
1750
+ emit(
1751
+ 'ws.frame.received',
1752
+ {
1753
+ connectionId: socket.__bpTraceId,
1754
+ url: socket.__bpTraceUrl,
1755
+ payload,
1756
+ length: payload.length,
1757
+ },
1758
+ 'info',
1759
+ 'WebSocket frame received'
1760
+ );
1761
+ });
1762
+
1763
+ socket.addEventListener('close', (event) => {
1764
+ if (socket.__bpTraceClosed) {
1765
+ return;
1766
+ }
1767
+ socket.__bpTraceClosed = true;
1768
+ try {
1769
+ globalThis.__bpTrackedWebSockets.delete(socket);
1770
+ } catch {}
1771
+ emit(
1772
+ 'ws.connection.closed',
1773
+ {
1774
+ connectionId: socket.__bpTraceId,
1775
+ url: socket.__bpTraceUrl,
1776
+ code: event.code,
1777
+ reason: event.reason,
1778
+ },
1779
+ 'warn',
1780
+ 'WebSocket closed'
1781
+ );
1782
+ });
1783
+
1784
+ return socket;
1785
+ };
1786
+
1787
+ const TracedWebSocket = function(url, protocols) {
1788
+ return arguments.length > 1
1789
+ ? patchInstance(new NativeWebSocket(url, protocols), url)
1790
+ : patchInstance(new NativeWebSocket(url), url);
1791
+ };
1792
+ TracedWebSocket.prototype = NativeWebSocket.prototype;
1793
+ Object.setPrototypeOf(TracedWebSocket, NativeWebSocket);
1794
+ window.WebSocket = TracedWebSocket;
1795
+ };
1796
+
1797
+ window.addEventListener('error', (errorEvent) => {
1798
+ emit(
1799
+ 'runtime.exception',
1800
+ {
1801
+ message: errorEvent.message,
1802
+ filename: errorEvent.filename,
1803
+ line: errorEvent.lineno,
1804
+ column: errorEvent.colno,
1805
+ },
1806
+ 'error',
1807
+ errorEvent.message || 'Uncaught error'
1808
+ );
1809
+ });
1810
+
1811
+ window.addEventListener('unhandledrejection', (event) => {
1812
+ const reason = event && 'reason' in event ? String(event.reason) : 'Unhandled rejection';
1813
+ emit('runtime.unhandledRejection', { reason }, 'error', reason);
1814
+ });
1815
+
1816
+ const patchPermissions = async () => {
1817
+ if (!navigator.permissions || !navigator.permissions.query) return;
1818
+
1819
+ const names = ['geolocation', 'microphone', 'camera', 'notifications'];
1820
+ for (const name of names) {
1821
+ try {
1822
+ const status = await navigator.permissions.query({ name });
1823
+ emit(
1824
+ 'permission.state',
1825
+ { name, state: status.state },
1826
+ status.state === 'denied' ? 'warn' : 'info',
1827
+ name + ': ' + status.state
1828
+ );
1829
+ status.addEventListener('change', () => {
1830
+ emit(
1831
+ 'permission.changed',
1832
+ { name, state: status.state },
1833
+ status.state === 'denied' ? 'warn' : 'info',
1834
+ name + ': ' + status.state
1835
+ );
1836
+ });
1837
+ } catch {}
1838
+ }
1839
+ };
1840
+
1841
+ const patchMediaElement = (element) => {
1842
+ if (!element || element.__bpTracePatched) return;
1843
+ element.__bpTracePatched = true;
1844
+
1845
+ element.addEventListener('play', () => {
1846
+ emit(
1847
+ 'media.playback.started',
1848
+ { tag: element.tagName.toLowerCase(), src: element.currentSrc || element.src || null },
1849
+ 'info',
1850
+ 'Media playback started'
1851
+ );
1852
+ });
1853
+
1854
+ const onStop = () => {
1855
+ emit(
1856
+ 'media.playback.stopped',
1857
+ { tag: element.tagName.toLowerCase(), src: element.currentSrc || element.src || null },
1858
+ 'warn',
1859
+ 'Media playback stopped'
1860
+ );
1861
+ };
1862
+
1863
+ element.addEventListener('pause', onStop);
1864
+ element.addEventListener('ended', onStop);
1865
+ };
1866
+
1867
+ const patchMediaElements = () => {
1868
+ document.querySelectorAll('audio,video').forEach(patchMediaElement);
1869
+ };
1870
+
1871
+ patchMediaElements();
1872
+ patchWebSocket();
1873
+
1874
+ if (document.documentElement) {
1875
+ const observer = new MutationObserver(() => {
1876
+ patchMediaElements();
1877
+ });
1878
+ observer.observe(document.documentElement, { childList: true, subtree: true });
1879
+ }
1880
+
1881
+ if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
1882
+ const original = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
1883
+ navigator.mediaDevices.getUserMedia = async (...args) => {
1884
+ emit('voice.capture.started', { constraints: args[0] || null }, 'info', 'Voice capture started');
1885
+ try {
1886
+ const stream = await original(...args);
1887
+ const tracks = stream.getTracks();
1888
+
1889
+ for (const track of tracks) {
1890
+ emit(
1891
+ 'media.track.started',
1892
+ { kind: track.kind, label: track.label, readyState: track.readyState },
1893
+ 'info',
1894
+ track.kind + ' track started'
1895
+ );
1896
+ track.addEventListener('ended', () => {
1897
+ emit(
1898
+ 'media.track.ended',
1899
+ { kind: track.kind, label: track.label, readyState: track.readyState },
1900
+ 'warn',
1901
+ track.kind + ' track ended'
1902
+ );
1903
+ emit(
1904
+ 'voice.capture.stopped',
1905
+ { kind: track.kind, label: track.label, readyState: track.readyState },
1906
+ 'warn',
1907
+ 'Voice capture stopped'
1908
+ );
1909
+ });
1910
+ }
1911
+
1912
+ emit(
1913
+ 'voice.capture.detectedAudio',
1914
+ { trackCount: tracks.length, kinds: tracks.map((track) => track.kind) },
1915
+ 'info',
1916
+ 'Voice capture detected audio'
1917
+ );
1918
+
1919
+ return stream;
1920
+ } catch (error) {
1921
+ emit(
1922
+ 'voice.pipeline.notReady',
1923
+ { message: String(error && error.message ? error.message : error) },
1924
+ 'error',
1925
+ String(error && error.message ? error.message : error)
1926
+ );
1927
+ throw error;
1928
+ }
1929
+ };
1930
+ }
1931
+
1932
+ document.addEventListener('visibilitychange', () => {
1933
+ emit(
1934
+ 'dom.state.changed',
1935
+ { visibilityState: document.visibilityState },
1936
+ document.visibilityState === 'hidden' ? 'warn' : 'info',
1937
+ 'Visibility ' + document.visibilityState
1938
+ );
1939
+ });
1940
+
1941
+ patchPermissions();
1942
+ emit('voice.pipeline.ready', { url: location.href }, 'info', 'Trace hooks ready');
1943
+ })();
1944
+ `;
1945
+
1946
+ // src/trace/model.ts
1947
+ function createTraceId(prefix = "evt") {
1948
+ const random = Math.random().toString(36).slice(2, 10);
1949
+ return `${prefix}-${Date.now().toString(36)}-${random}`;
1950
+ }
1951
+ function normalizeTraceEvent(event) {
1952
+ return {
1953
+ traceId: event.traceId ?? createTraceId(event.channel),
1954
+ ts: event.ts ?? (/* @__PURE__ */ new Date()).toISOString(),
1955
+ elapsedMs: event.elapsedMs ?? 0,
1956
+ severity: event.severity ?? inferSeverity(event.event),
1957
+ data: event.data ?? {},
1958
+ ...event
1959
+ };
1960
+ }
1961
+ function inferSeverity(eventName) {
1962
+ if (eventName.includes(".failed") || eventName.includes(".error") || eventName.includes("exception") || eventName.includes("notReady")) {
1963
+ return "error";
1964
+ }
1965
+ if (eventName.includes(".closed") || eventName.includes(".warn") || eventName.includes(".changed")) {
1966
+ return "warn";
1967
+ }
1968
+ return "info";
1969
+ }
1970
+
1971
+ // src/trace/live.ts
1972
+ function globToRegex(pattern) {
1973
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
1974
+ const withWildcards = escaped.replace(/\*/g, ".*");
1975
+ return new RegExp(`^${withWildcards}$`);
1976
+ }
1977
+
1087
1978
  // src/actions/executor.ts
1088
1979
  var DEFAULT_TIMEOUT = 3e4;
1980
+ var DEFAULT_RECORDING_SKIP_ACTIONS = [
1981
+ "wait",
1982
+ "snapshot",
1983
+ "forms",
1984
+ "text",
1985
+ "screenshot"
1986
+ ];
1987
+ function loadExistingRecording(manifestPath) {
1988
+ try {
1989
+ const raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
1990
+ if (raw.version === 1) {
1991
+ const legacy = raw;
1992
+ return {
1993
+ frames: Array.isArray(legacy.frames) ? legacy.frames : [],
1994
+ traceEvents: [],
1995
+ recordedAt: legacy.recordedAt,
1996
+ startUrl: legacy.startUrl
1997
+ };
1998
+ }
1999
+ const artifact = canonicalizeRecordingArtifact(raw);
2000
+ const screenshotsByAction = new Map(artifact.screenshots.map((shot) => [shot.actionId, shot]));
2001
+ const frames = artifact.actions.map((action, index) => {
2002
+ const screenshot = screenshotsByAction.get(action.id);
2003
+ return {
2004
+ seq: index + 1,
2005
+ timestamp: Date.parse(action.ts),
2006
+ action: action.action,
2007
+ selector: action.selector,
2008
+ selectorUsed: action.selectorUsed,
2009
+ value: action.value,
2010
+ url: action.url,
2011
+ coordinates: action.coordinates,
2012
+ boundingBox: action.boundingBox,
2013
+ success: action.success,
2014
+ durationMs: action.durationMs,
2015
+ error: action.error,
2016
+ screenshot: screenshot?.file ?? "",
2017
+ pageUrl: action.pageUrl,
2018
+ pageTitle: action.pageTitle,
2019
+ stepIndex: action.stepIndex,
2020
+ actionId: action.id
2021
+ };
2022
+ });
2023
+ return {
2024
+ frames,
2025
+ traceEvents: artifact.trace.events,
2026
+ recordedAt: artifact.recordedAt,
2027
+ startUrl: artifact.session.startUrl
2028
+ };
2029
+ } catch {
2030
+ return { frames: [], traceEvents: [] };
2031
+ }
2032
+ }
1089
2033
  function classifyFailure(error) {
1090
2034
  if (error instanceof ElementNotFoundError) {
1091
2035
  return { reason: "missing" };
@@ -1165,6 +2109,12 @@ var BatchExecutor = class {
1165
2109
  const { timeout = DEFAULT_TIMEOUT, onFail = "stop" } = options;
1166
2110
  const results = [];
1167
2111
  const startTime = Date.now();
2112
+ const recording = options.record ? this.createRecordingContext(options.record) : null;
2113
+ if (steps.some((step) => step.action === "waitForWsMessage")) {
2114
+ await this.ensureTraceHooks();
2115
+ }
2116
+ const startUrl = recording ? await this.getPageUrlSafe() : "";
2117
+ let stoppedAtIndex;
1168
2118
  for (let i = 0; i < steps.length; i++) {
1169
2119
  const step = steps[i];
1170
2120
  const stepStart = Date.now();
@@ -1172,13 +2122,34 @@ var BatchExecutor = class {
1172
2122
  const retryDelay = step.retryDelay ?? 500;
1173
2123
  let lastError;
1174
2124
  let succeeded = false;
2125
+ if (recording) {
2126
+ recording.traceEvents.push(
2127
+ normalizeTraceEvent({
2128
+ traceId: createTraceId("action"),
2129
+ elapsedMs: Date.now() - startTime,
2130
+ channel: "action",
2131
+ event: "action.started",
2132
+ summary: `${step.action}${step.selector ? ` ${Array.isArray(step.selector) ? step.selector[0] : step.selector}` : ""}`,
2133
+ data: {
2134
+ action: step.action,
2135
+ selector: step.selector ?? null,
2136
+ url: step.url ?? null
2137
+ },
2138
+ actionId: `action-${i + 1}`,
2139
+ stepIndex: i,
2140
+ selector: step.selector,
2141
+ url: step.url
2142
+ })
2143
+ );
2144
+ }
1175
2145
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
1176
2146
  if (attempt > 0) {
1177
2147
  await new Promise((resolve) => setTimeout(resolve, retryDelay));
1178
2148
  }
1179
2149
  try {
2150
+ this.page.resetLastActionPosition();
1180
2151
  const result = await this.executeStep(step, timeout);
1181
- results.push({
2152
+ const stepResult = {
1182
2153
  index: i,
1183
2154
  action: step.action,
1184
2155
  selector: step.selector,
@@ -1186,8 +2157,37 @@ var BatchExecutor = class {
1186
2157
  success: true,
1187
2158
  durationMs: Date.now() - stepStart,
1188
2159
  result: result.value,
1189
- text: result.text
1190
- });
2160
+ text: result.text,
2161
+ timestamp: Date.now(),
2162
+ coordinates: this.page.getLastActionCoordinates() ?? void 0,
2163
+ boundingBox: this.page.getLastActionBoundingBox() ?? void 0
2164
+ };
2165
+ if (recording && !recording.skipActions.has(step.action)) {
2166
+ await this.captureRecordingFrame(step, stepResult, recording);
2167
+ }
2168
+ if (recording) {
2169
+ recording.traceEvents.push(
2170
+ normalizeTraceEvent({
2171
+ traceId: createTraceId("action"),
2172
+ elapsedMs: Date.now() - startTime,
2173
+ channel: "action",
2174
+ event: "action.succeeded",
2175
+ summary: `${step.action} succeeded`,
2176
+ data: {
2177
+ action: step.action,
2178
+ selector: step.selector ?? null,
2179
+ selectorUsed: result.selectorUsed ?? null,
2180
+ durationMs: Date.now() - stepStart
2181
+ },
2182
+ actionId: `action-${i + 1}`,
2183
+ stepIndex: i,
2184
+ selector: step.selector,
2185
+ selectorUsed: result.selectorUsed,
2186
+ url: step.url
2187
+ })
2188
+ );
2189
+ }
2190
+ results.push(stepResult);
1191
2191
  succeeded = true;
1192
2192
  break;
1193
2193
  } catch (error) {
@@ -1208,7 +2208,7 @@ var BatchExecutor = class {
1208
2208
  } catch {
1209
2209
  }
1210
2210
  }
1211
- results.push({
2211
+ const failedResult = {
1212
2212
  index: i,
1213
2213
  action: step.action,
1214
2214
  selector: step.selector,
@@ -1218,25 +2218,183 @@ var BatchExecutor = class {
1218
2218
  hints,
1219
2219
  failureReason: reason,
1220
2220
  coveringElement,
1221
- suggestion: getSuggestion(reason)
1222
- });
2221
+ suggestion: getSuggestion(reason),
2222
+ timestamp: Date.now()
2223
+ };
2224
+ if (recording && !recording.skipActions.has(step.action)) {
2225
+ await this.captureRecordingFrame(step, failedResult, recording);
2226
+ }
2227
+ if (recording) {
2228
+ recording.traceEvents.push(
2229
+ normalizeTraceEvent({
2230
+ traceId: createTraceId("action"),
2231
+ elapsedMs: Date.now() - startTime,
2232
+ channel: "action",
2233
+ event: "action.failed",
2234
+ severity: "error",
2235
+ summary: `${step.action} failed: ${errorMessage}`,
2236
+ data: {
2237
+ action: step.action,
2238
+ selector: step.selector ?? null,
2239
+ error: errorMessage,
2240
+ reason
2241
+ },
2242
+ actionId: `action-${i + 1}`,
2243
+ stepIndex: i,
2244
+ selector: step.selector,
2245
+ url: step.url
2246
+ })
2247
+ );
2248
+ }
2249
+ results.push(failedResult);
1223
2250
  if (onFail === "stop" && !step.optional) {
1224
- return {
1225
- success: false,
1226
- stoppedAtIndex: i,
1227
- steps: results,
1228
- totalDurationMs: Date.now() - startTime
1229
- };
2251
+ stoppedAtIndex = i;
2252
+ break;
1230
2253
  }
1231
2254
  }
1232
2255
  }
1233
- const allSuccess = results.every((r) => r.success || steps[r.index]?.optional);
2256
+ const totalDurationMs = Date.now() - startTime;
2257
+ const allSuccess = stoppedAtIndex === void 0 && results.every((result) => result.success || steps[result.index]?.optional);
2258
+ let recordingManifest;
2259
+ if (recording) {
2260
+ recordingManifest = await this.writeRecordingManifest(
2261
+ recording,
2262
+ startTime,
2263
+ startUrl,
2264
+ allSuccess,
2265
+ steps
2266
+ );
2267
+ }
1234
2268
  return {
1235
2269
  success: allSuccess,
2270
+ stoppedAtIndex,
1236
2271
  steps: results,
1237
- totalDurationMs: Date.now() - startTime
2272
+ totalDurationMs,
2273
+ recordingManifest
1238
2274
  };
1239
2275
  }
2276
+ createRecordingContext(record) {
2277
+ const baseDir = record.outputDir ?? (0, import_node_path.join)(process.cwd(), ".browser-pilot");
2278
+ const screenshotDir = (0, import_node_path.join)(baseDir, "screenshots");
2279
+ const manifestPath = (0, import_node_path.join)(baseDir, "recording.json");
2280
+ const existing = loadExistingRecording(manifestPath);
2281
+ fs.mkdirSync(screenshotDir, { recursive: true });
2282
+ return {
2283
+ baseDir,
2284
+ screenshotDir,
2285
+ sessionId: record.sessionId ?? this.page.targetId,
2286
+ frames: existing.frames,
2287
+ traceEvents: existing.traceEvents,
2288
+ format: record.format ?? "webp",
2289
+ quality: Math.max(0, Math.min(100, record.quality ?? 40)),
2290
+ highlights: record.highlights !== false,
2291
+ skipActions: new Set(record.skipActions ?? DEFAULT_RECORDING_SKIP_ACTIONS)
2292
+ };
2293
+ }
2294
+ async getPageUrlSafe() {
2295
+ try {
2296
+ return await this.page.url();
2297
+ } catch {
2298
+ return "";
2299
+ }
2300
+ }
2301
+ /**
2302
+ * Capture a recording screenshot frame with optional highlight overlay
2303
+ */
2304
+ async captureRecordingFrame(step, stepResult, recording) {
2305
+ const targetMetadata = this.page.getLastActionTargetMetadata();
2306
+ let highlightInjected = false;
2307
+ try {
2308
+ const ts = Date.now();
2309
+ const seq = String(recording.frames.length + 1).padStart(4, "0");
2310
+ const filename = `${seq}-${ts}-${stepResult.action}.${recording.format}`;
2311
+ const filepath = (0, import_node_path.join)(recording.screenshotDir, filename);
2312
+ if (recording.highlights) {
2313
+ const kind = stepToHighlightKind(stepResult);
2314
+ if (kind) {
2315
+ await injectActionHighlight(this.page, {
2316
+ kind,
2317
+ bbox: stepResult.boundingBox,
2318
+ point: stepResult.coordinates,
2319
+ label: getHighlightLabel(step, stepResult, targetMetadata)
2320
+ });
2321
+ highlightInjected = true;
2322
+ }
2323
+ }
2324
+ const base64 = await this.page.screenshot({
2325
+ format: recording.format,
2326
+ quality: recording.quality
2327
+ });
2328
+ const buffer = Buffer.from(base64, "base64");
2329
+ fs.writeFileSync(filepath, buffer);
2330
+ stepResult.screenshotPath = filepath;
2331
+ let pageUrl;
2332
+ let pageTitle;
2333
+ try {
2334
+ pageUrl = await this.page.url();
2335
+ pageTitle = await this.page.title();
2336
+ } catch {
2337
+ }
2338
+ recording.frames.push({
2339
+ seq: recording.frames.length + 1,
2340
+ timestamp: ts,
2341
+ action: stepResult.action,
2342
+ selector: stepResult.selectorUsed ?? (Array.isArray(step.selector) ? step.selector[0] : step.selector),
2343
+ selectorUsed: stepResult.selectorUsed,
2344
+ value: redactValueForRecording(
2345
+ typeof step.value === "string" ? step.value : void 0,
2346
+ targetMetadata
2347
+ ),
2348
+ url: step.url,
2349
+ coordinates: stepResult.coordinates,
2350
+ boundingBox: stepResult.boundingBox,
2351
+ success: stepResult.success,
2352
+ durationMs: stepResult.durationMs,
2353
+ error: stepResult.error,
2354
+ screenshot: filename,
2355
+ pageUrl,
2356
+ pageTitle,
2357
+ stepIndex: stepResult.index,
2358
+ actionId: `action-${stepResult.index + 1}`
2359
+ });
2360
+ } catch {
2361
+ } finally {
2362
+ if (recording.highlights || highlightInjected) {
2363
+ await removeActionHighlight(this.page);
2364
+ }
2365
+ }
2366
+ }
2367
+ /**
2368
+ * Write recording manifest to disk
2369
+ */
2370
+ async writeRecordingManifest(recording, startTime, startUrl, success, steps) {
2371
+ let endUrl = startUrl;
2372
+ try {
2373
+ endUrl = await this.page.url();
2374
+ } catch {
2375
+ }
2376
+ const manifestPath = (0, import_node_path.join)(recording.baseDir, "recording.json");
2377
+ let recordedAt = new Date(startTime).toISOString();
2378
+ let originalStartUrl = startUrl;
2379
+ const existing = loadExistingRecording(manifestPath);
2380
+ if (existing.recordedAt) recordedAt = existing.recordedAt;
2381
+ if (existing.startUrl) originalStartUrl = existing.startUrl;
2382
+ const manifest = createRecordingManifest({
2383
+ recordedAt,
2384
+ sessionId: recording.sessionId,
2385
+ startUrl: originalStartUrl,
2386
+ endUrl,
2387
+ targetId: this.page.targetId,
2388
+ steps,
2389
+ frames: recording.frames,
2390
+ traceEvents: recording.traceEvents,
2391
+ notes: success ? [] : ["Replay ended with at least one failed action."],
2392
+ recordingManifest: "recording.json",
2393
+ screenshotDir: "screenshots/"
2394
+ });
2395
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
2396
+ return manifestPath;
2397
+ }
1240
2398
  /**
1241
2399
  * Execute a single step
1242
2400
  */
@@ -1516,6 +2674,39 @@ var BatchExecutor = class {
1516
2674
  }
1517
2675
  return { selectorUsed: usedSelector, value: actual };
1518
2676
  }
2677
+ case "waitForWsMessage": {
2678
+ if (typeof step.match !== "string") {
2679
+ throw new Error("waitForWsMessage requires match");
2680
+ }
2681
+ const message = await this.waitForWsMessage(step.match, step.where, timeout);
2682
+ return { value: message };
2683
+ }
2684
+ case "assertNoConsoleErrors": {
2685
+ await this.assertNoConsoleErrors(step.windowMs ?? timeout);
2686
+ return {};
2687
+ }
2688
+ case "assertTextChanged": {
2689
+ const selector = Array.isArray(step.selector) ? step.selector[0] : step.selector;
2690
+ if (typeof step.to !== "string") {
2691
+ throw new Error("assertTextChanged requires to");
2692
+ }
2693
+ const text = await this.assertTextChanged(selector, step.from, step.to, timeout);
2694
+ return { selectorUsed: selector, text };
2695
+ }
2696
+ case "assertPermission": {
2697
+ if (!step.name || !step.state) {
2698
+ throw new Error("assertPermission requires name and state");
2699
+ }
2700
+ const permission = await this.assertPermission(step.name, step.state);
2701
+ return { value: permission };
2702
+ }
2703
+ case "assertMediaTrackLive": {
2704
+ if (!step.kind) {
2705
+ throw new Error("assertMediaTrackLive requires kind");
2706
+ }
2707
+ const media = await this.assertMediaTrackLive(step.kind);
2708
+ return { value: media };
2709
+ }
1519
2710
  default: {
1520
2711
  const action = step.action;
1521
2712
  const aliases = {
@@ -1569,7 +2760,7 @@ var BatchExecutor = class {
1569
2760
  };
1570
2761
  const suggestion = aliases[action.toLowerCase()];
1571
2762
  const hint = suggestion ? ` Did you mean "${suggestion}"?` : "";
1572
- 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";
2763
+ 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";
1573
2764
  throw new Error(`Unknown action "${action}".${hint}
1574
2765
 
1575
2766
  Valid actions: ${valid}`);
@@ -1585,6 +2776,233 @@ Valid actions: ${valid}`);
1585
2776
  if (matched) return matched;
1586
2777
  return Array.isArray(selector) ? selector[0] : selector;
1587
2778
  }
2779
+ async ensureTraceHooks() {
2780
+ await this.page.cdpClient.send("Runtime.enable");
2781
+ await this.page.cdpClient.send("Page.enable");
2782
+ await this.page.cdpClient.send("Network.enable");
2783
+ try {
2784
+ await this.page.cdpClient.send("Runtime.addBinding", { name: TRACE_BINDING_NAME });
2785
+ } catch {
2786
+ }
2787
+ await this.page.cdpClient.send("Page.addScriptToEvaluateOnNewDocument", { source: TRACE_SCRIPT });
2788
+ await this.page.cdpClient.send("Runtime.evaluate", { expression: TRACE_SCRIPT, awaitPromise: false });
2789
+ }
2790
+ async waitForWsMessage(match, where, timeout) {
2791
+ await this.ensureTraceHooks();
2792
+ const regex = globToRegex(match);
2793
+ const wsUrls = /* @__PURE__ */ new Map();
2794
+ const recentMatch = await this.findRecentWsMessage(regex, where);
2795
+ if (recentMatch) {
2796
+ return recentMatch;
2797
+ }
2798
+ return new Promise((resolve, reject) => {
2799
+ const cleanup = () => {
2800
+ this.page.cdpClient.off("Network.webSocketCreated", onCreated);
2801
+ this.page.cdpClient.off("Network.webSocketFrameReceived", onFrame);
2802
+ this.page.cdpClient.off("Runtime.bindingCalled", onBinding);
2803
+ clearTimeout(timer);
2804
+ };
2805
+ const onCreated = (params) => {
2806
+ wsUrls.set(String(params["requestId"] ?? ""), String(params["url"] ?? ""));
2807
+ };
2808
+ const onFrame = (params) => {
2809
+ const requestId = String(params["requestId"] ?? "");
2810
+ const response = params["response"] ?? {};
2811
+ const payload = String(response.payloadData ?? "");
2812
+ const url = wsUrls.get(requestId) ?? "";
2813
+ if (!regex.test(url) && !regex.test(payload)) {
2814
+ return;
2815
+ }
2816
+ if (where && !this.payloadMatchesWhere(payload, where)) {
2817
+ return;
2818
+ }
2819
+ cleanup();
2820
+ resolve({ requestId, url, payload });
2821
+ };
2822
+ const onBinding = (params) => {
2823
+ if (params["name"] !== TRACE_BINDING_NAME) {
2824
+ return;
2825
+ }
2826
+ try {
2827
+ const parsed = JSON.parse(String(params["payload"] ?? ""));
2828
+ if (parsed.event !== "ws.frame.received") {
2829
+ return;
2830
+ }
2831
+ const data = parsed.data ?? {};
2832
+ const payload = String(data["payload"] ?? "");
2833
+ const url = String(data["url"] ?? "");
2834
+ if (!regex.test(url) && !regex.test(payload)) {
2835
+ return;
2836
+ }
2837
+ if (where && !this.payloadMatchesWhere(payload, where)) {
2838
+ return;
2839
+ }
2840
+ cleanup();
2841
+ resolve({
2842
+ requestId: String(data["connectionId"] ?? ""),
2843
+ url,
2844
+ payload
2845
+ });
2846
+ } catch {
2847
+ }
2848
+ };
2849
+ const timer = setTimeout(() => {
2850
+ cleanup();
2851
+ reject(new Error(`Timed out waiting for WebSocket message matching ${match}`));
2852
+ }, timeout);
2853
+ this.page.cdpClient.on("Network.webSocketCreated", onCreated);
2854
+ this.page.cdpClient.on("Network.webSocketFrameReceived", onFrame);
2855
+ this.page.cdpClient.on("Runtime.bindingCalled", onBinding);
2856
+ });
2857
+ }
2858
+ payloadMatchesWhere(payload, where) {
2859
+ try {
2860
+ const parsed = JSON.parse(payload);
2861
+ return Object.entries(where).every(([key, expected]) => {
2862
+ const actual = key.split(".").reduce((current, part) => {
2863
+ if (!current || typeof current !== "object") {
2864
+ return void 0;
2865
+ }
2866
+ return current[part];
2867
+ }, parsed);
2868
+ return actual === expected;
2869
+ });
2870
+ } catch {
2871
+ return false;
2872
+ }
2873
+ }
2874
+ async findRecentWsMessage(regex, where) {
2875
+ const recent = await this.page.evaluate(
2876
+ "(() => Array.isArray(globalThis.__bpTraceRecentEvents) ? globalThis.__bpTraceRecentEvents : [])()"
2877
+ );
2878
+ if (!Array.isArray(recent)) {
2879
+ return null;
2880
+ }
2881
+ for (let i = recent.length - 1; i >= 0; i--) {
2882
+ const entry = recent[i];
2883
+ if (!entry || typeof entry !== "object") {
2884
+ continue;
2885
+ }
2886
+ const event = String(entry["event"] ?? "");
2887
+ if (event !== "ws.frame.received") {
2888
+ continue;
2889
+ }
2890
+ const data = entry["data"] ?? {};
2891
+ const payload = String(data["payload"] ?? "");
2892
+ const url = String(data["url"] ?? "");
2893
+ if (!regex.test(url) && !regex.test(payload)) {
2894
+ continue;
2895
+ }
2896
+ if (where && !this.payloadMatchesWhere(payload, where)) {
2897
+ continue;
2898
+ }
2899
+ return {
2900
+ requestId: String(data["connectionId"] ?? ""),
2901
+ url,
2902
+ payload
2903
+ };
2904
+ }
2905
+ return null;
2906
+ }
2907
+ async assertNoConsoleErrors(windowMs) {
2908
+ await this.page.cdpClient.send("Runtime.enable");
2909
+ return new Promise((resolve, reject) => {
2910
+ const errors = [];
2911
+ const cleanup = () => {
2912
+ this.page.cdpClient.off("Runtime.consoleAPICalled", onConsole);
2913
+ this.page.cdpClient.off("Runtime.exceptionThrown", onException);
2914
+ clearTimeout(timer);
2915
+ };
2916
+ const onConsole = (params) => {
2917
+ if (params["type"] !== "error") {
2918
+ return;
2919
+ }
2920
+ const args = Array.isArray(params["args"]) ? params["args"] : [];
2921
+ errors.push(
2922
+ args.map((entry) => String(entry["value"] ?? entry["description"] ?? "")).filter(Boolean).join(" ")
2923
+ );
2924
+ };
2925
+ const onException = (params) => {
2926
+ const details = params["exceptionDetails"] ?? {};
2927
+ errors.push(String(details["text"] ?? "Runtime exception"));
2928
+ };
2929
+ const timer = setTimeout(() => {
2930
+ cleanup();
2931
+ if (errors.length > 0) {
2932
+ reject(new Error(`Console errors detected: ${errors.join(" | ")}`));
2933
+ return;
2934
+ }
2935
+ resolve();
2936
+ }, windowMs);
2937
+ this.page.cdpClient.on("Runtime.consoleAPICalled", onConsole);
2938
+ this.page.cdpClient.on("Runtime.exceptionThrown", onException);
2939
+ });
2940
+ }
2941
+ async assertTextChanged(selector, from, to, timeout) {
2942
+ const initialText = from ?? await this.page.text(selector);
2943
+ const deadline = Date.now() + timeout;
2944
+ while (Date.now() < deadline) {
2945
+ const text = await this.page.text(selector);
2946
+ if (text !== initialText && text.includes(to)) {
2947
+ return text;
2948
+ }
2949
+ await new Promise((resolve) => setTimeout(resolve, 200));
2950
+ }
2951
+ throw new Error(`Text did not change to include ${JSON.stringify(to)}`);
2952
+ }
2953
+ async assertPermission(name, state) {
2954
+ const result = await this.page.evaluate(
2955
+ `(() => navigator.permissions.query({ name: ${JSON.stringify(name)} }).then((status) => ({ name: ${JSON.stringify(name)}, state: status.state })))()`
2956
+ );
2957
+ if (!result || typeof result !== "object" || result.state !== state) {
2958
+ throw new Error(`Permission ${name} is not ${state}`);
2959
+ }
2960
+ return result;
2961
+ }
2962
+ async assertMediaTrackLive(kind) {
2963
+ const result = await this.page.evaluate(
2964
+ `(() => {
2965
+ const requestedKind = ${JSON.stringify(kind)};
2966
+ const mediaElements = Array.from(document.querySelectorAll('audio,video')).map((el) => {
2967
+ const tracks = [];
2968
+ if (el.srcObject && typeof el.srcObject.getTracks === 'function') {
2969
+ tracks.push(...el.srcObject.getTracks());
2970
+ }
2971
+ return {
2972
+ tag: el.tagName.toLowerCase(),
2973
+ paused: !!el.paused,
2974
+ tracks: tracks.map((track) => ({
2975
+ kind: track.kind,
2976
+ readyState: track.readyState,
2977
+ enabled: track.enabled,
2978
+ label: track.label,
2979
+ })),
2980
+ };
2981
+ });
2982
+
2983
+ const globalTracks =
2984
+ window.__bpStream && typeof window.__bpStream.getTracks === 'function'
2985
+ ? window.__bpStream.getTracks().map((track) => ({
2986
+ kind: track.kind,
2987
+ readyState: track.readyState,
2988
+ enabled: track.enabled,
2989
+ label: track.label,
2990
+ }))
2991
+ : [];
2992
+
2993
+ const liveTracks = mediaElements
2994
+ .flatMap((entry) => entry.tracks)
2995
+ .concat(globalTracks)
2996
+ .filter((track) => track.kind === requestedKind && track.readyState === 'live');
2997
+
2998
+ return { live: liveTracks.length > 0, mediaElements, globalTracks, liveTracks };
2999
+ })()`
3000
+ );
3001
+ if (!result || typeof result !== "object" || !result.live) {
3002
+ throw new Error(`No live ${kind} media track detected`);
3003
+ }
3004
+ return result;
3005
+ }
1588
3006
  };
1589
3007
 
1590
3008
  // src/audio/encoding.ts
@@ -1622,6 +3040,10 @@ async function grantAudioPermissions(cdp, origin) {
1622
3040
  await cdp.send("Page.addScriptToEvaluateOnNewDocument", {
1623
3041
  source: PERMISSIONS_OVERRIDE_SCRIPT
1624
3042
  });
3043
+ await cdp.send("Runtime.evaluate", {
3044
+ expression: PERMISSIONS_OVERRIDE_SCRIPT,
3045
+ awaitPromise: false
3046
+ });
1625
3047
  }
1626
3048
  var PERMISSIONS_OVERRIDE_SCRIPT = `
1627
3049
  (function() {
@@ -3766,6 +5188,9 @@ var Page = class {
3766
5188
  brokenFrame = null;
3767
5189
  /** Last matched selector from findElement (for selectorUsed tracking) */
3768
5190
  _lastMatchedSelector;
5191
+ _lastActionCoordinates = null;
5192
+ _lastActionBoundingBox = null;
5193
+ _lastActionTargetMetadata = null;
3769
5194
  /** Last snapshot for stale ref recovery */
3770
5195
  lastSnapshot;
3771
5196
  /** Audio input controller (lazy-initialized) */
@@ -3797,6 +5222,76 @@ var Page = class {
3797
5222
  getLastMatchedSelector() {
3798
5223
  return this._lastMatchedSelector;
3799
5224
  }
5225
+ async getActionTargetMetadata(identifiers) {
5226
+ try {
5227
+ const objectId = identifiers.objectId ?? (identifiers.nodeId ? await this.resolveObjectId(identifiers.nodeId) : void 0);
5228
+ if (!objectId) return null;
5229
+ const response = await this.cdp.send("Runtime.callFunctionOn", {
5230
+ objectId,
5231
+ functionDeclaration: `function() {
5232
+ const tagName = this.tagName?.toLowerCase?.() || '';
5233
+ const inputType =
5234
+ tagName === 'input' && typeof this.type === 'string' ? this.type.toLowerCase() : '';
5235
+ const autocomplete =
5236
+ typeof this.autocomplete === 'string' ? this.autocomplete.toLowerCase() : '';
5237
+ return { tagName, inputType, autocomplete };
5238
+ }`,
5239
+ returnByValue: true
5240
+ });
5241
+ return response.result.value ?? null;
5242
+ } catch {
5243
+ return null;
5244
+ }
5245
+ }
5246
+ async getElementPosition(identifiers) {
5247
+ try {
5248
+ const { quads } = await this.cdp.send(
5249
+ "DOM.getContentQuads",
5250
+ identifiers
5251
+ );
5252
+ if (quads?.length > 0) {
5253
+ const q = quads[0];
5254
+ const minX = Math.min(q[0], q[2], q[4], q[6]);
5255
+ const maxX = Math.max(q[0], q[2], q[4], q[6]);
5256
+ const minY = Math.min(q[1], q[3], q[5], q[7]);
5257
+ const maxY = Math.max(q[1], q[3], q[5], q[7]);
5258
+ return {
5259
+ center: { x: (minX + maxX) / 2, y: (minY + maxY) / 2 },
5260
+ bbox: { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
5261
+ };
5262
+ }
5263
+ } catch {
5264
+ }
5265
+ if (identifiers.nodeId) {
5266
+ const box = await this.getBoxModel(identifiers.nodeId);
5267
+ if (box) {
5268
+ return {
5269
+ center: { x: box.content[0] + box.width / 2, y: box.content[1] + box.height / 2 },
5270
+ bbox: { x: box.content[0], y: box.content[1], width: box.width, height: box.height }
5271
+ };
5272
+ }
5273
+ }
5274
+ return null;
5275
+ }
5276
+ setLastActionPosition(coords, bbox) {
5277
+ this._lastActionCoordinates = coords;
5278
+ this._lastActionBoundingBox = bbox;
5279
+ }
5280
+ getLastActionCoordinates() {
5281
+ return this._lastActionCoordinates;
5282
+ }
5283
+ getLastActionBoundingBox() {
5284
+ return this._lastActionBoundingBox;
5285
+ }
5286
+ getLastActionTargetMetadata() {
5287
+ return this._lastActionTargetMetadata;
5288
+ }
5289
+ /** Reset position tracking (call before each executor step) */
5290
+ resetLastActionPosition() {
5291
+ this._lastActionCoordinates = null;
5292
+ this._lastActionBoundingBox = null;
5293
+ this._lastActionTargetMetadata = null;
5294
+ }
3800
5295
  /**
3801
5296
  * Initialize the page (enable required CDP domains)
3802
5297
  */
@@ -3964,6 +5459,14 @@ var Page = class {
3964
5459
  const quad = quads[0];
3965
5460
  clickX = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
3966
5461
  clickY = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
5462
+ const minX = Math.min(quad[0], quad[2], quad[4], quad[6]);
5463
+ const maxX = Math.max(quad[0], quad[2], quad[4], quad[6]);
5464
+ const minY = Math.min(quad[1], quad[3], quad[5], quad[7]);
5465
+ const maxY = Math.max(quad[1], quad[3], quad[5], quad[7]);
5466
+ this.setLastActionPosition(
5467
+ { x: clickX, y: clickY },
5468
+ { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
5469
+ );
3967
5470
  } else {
3968
5471
  throw new Error("No quads");
3969
5472
  }
@@ -3972,6 +5475,10 @@ var Page = class {
3972
5475
  if (!box) throw new Error("Could not get element position");
3973
5476
  clickX = box.content[0] + box.width / 2;
3974
5477
  clickY = box.content[1] + box.height / 2;
5478
+ this.setLastActionPosition(
5479
+ { x: clickX, y: clickY },
5480
+ { x: box.content[0], y: box.content[1], width: box.width, height: box.height }
5481
+ );
3975
5482
  }
3976
5483
  const hitTargetCoordinates = this.currentFrame ? void 0 : { x: clickX, y: clickY };
3977
5484
  const HIT_TARGET_RETRIES = 3;
@@ -4022,13 +5529,20 @@ var Page = class {
4022
5529
  if (options.optional) return false;
4023
5530
  throw e;
4024
5531
  }
5532
+ const fillPos = await this.getElementPosition({ nodeId: element.nodeId });
5533
+ if (fillPos) this.setLastActionPosition(fillPos.center, fillPos.bbox);
4025
5534
  const tagInfo = await this.cdp.send("Runtime.callFunctionOn", {
4026
5535
  objectId,
4027
5536
  functionDeclaration: `function() {
4028
- return { tagName: this.tagName?.toLowerCase() || '', inputType: (this.type || '').toLowerCase() };
5537
+ return {
5538
+ tagName: this.tagName?.toLowerCase() || '',
5539
+ inputType: (this.type || '').toLowerCase(),
5540
+ autocomplete: typeof this.autocomplete === 'string' ? this.autocomplete.toLowerCase() : '',
5541
+ };
4029
5542
  }`,
4030
5543
  returnByValue: true
4031
5544
  });
5545
+ this._lastActionTargetMetadata = tagInfo.result.value;
4032
5546
  const { tagName, inputType } = tagInfo.result.value;
4033
5547
  const specialInputTypes = /* @__PURE__ */ new Set([
4034
5548
  "date",
@@ -4110,6 +5624,9 @@ var Page = class {
4110
5624
  if (options.optional) return false;
4111
5625
  throw e;
4112
5626
  }
5627
+ const typePos = await this.getElementPosition({ nodeId: element.nodeId });
5628
+ if (typePos) this.setLastActionPosition(typePos.center, typePos.bbox);
5629
+ this._lastActionTargetMetadata = await this.getActionTargetMetadata({ objectId });
4113
5630
  await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
4114
5631
  for (const char of text) {
4115
5632
  const def = US_KEYBOARD[char];
@@ -4189,6 +5706,9 @@ var Page = class {
4189
5706
  if (options.optional) return false;
4190
5707
  throw e;
4191
5708
  }
5709
+ const selectPos = await this.getElementPosition({ nodeId: element.nodeId });
5710
+ if (selectPos) this.setLastActionPosition(selectPos.center, selectPos.bbox);
5711
+ this._lastActionTargetMetadata = await this.getActionTargetMetadata({ objectId });
4192
5712
  const metadata = await this.getNativeSelectMetadata(objectId, values);
4193
5713
  if (!metadata.isSelect) {
4194
5714
  throw new Error("select() target must be a native <select> element");
@@ -4325,6 +5845,8 @@ var Page = class {
4325
5845
  if (options.optional) return false;
4326
5846
  throw e;
4327
5847
  }
5848
+ const checkPos = await this.getElementPosition({ nodeId: element.nodeId });
5849
+ if (checkPos) this.setLastActionPosition(checkPos.center, checkPos.bbox);
4328
5850
  const before = await this.cdp.send("Runtime.callFunctionOn", {
4329
5851
  objectId: object.objectId,
4330
5852
  functionDeclaration: "function() { return !!this.checked; }",
@@ -4373,6 +5895,8 @@ var Page = class {
4373
5895
  if (options.optional) return false;
4374
5896
  throw e;
4375
5897
  }
5898
+ const uncheckPos = await this.getElementPosition({ nodeId: element.nodeId });
5899
+ if (uncheckPos) this.setLastActionPosition(uncheckPos.center, uncheckPos.bbox);
4376
5900
  const isRadio = await this.cdp.send(
4377
5901
  "Runtime.callFunctionOn",
4378
5902
  {
@@ -4428,6 +5952,8 @@ var Page = class {
4428
5952
  throw new ElementNotFoundError(selector, hints);
4429
5953
  }
4430
5954
  const objectId = await this.resolveObjectId(element.nodeId);
5955
+ const submitPos = await this.getElementPosition({ nodeId: element.nodeId });
5956
+ if (submitPos) this.setLastActionPosition(submitPos.center, submitPos.bbox);
4431
5957
  const isFormElement = await this.cdp.send(
4432
5958
  "Runtime.callFunctionOn",
4433
5959
  {
@@ -4524,6 +6050,8 @@ var Page = class {
4524
6050
  const hints = await generateHints(this, selectorList, "focus");
4525
6051
  throw new ElementNotFoundError(selector, hints);
4526
6052
  }
6053
+ const focusPos = await this.getElementPosition({ nodeId: element.nodeId });
6054
+ if (focusPos) this.setLastActionPosition(focusPos.center, focusPos.bbox);
4527
6055
  await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
4528
6056
  return true;
4529
6057
  }
@@ -4559,6 +6087,14 @@ var Page = class {
4559
6087
  const quad = quads[0];
4560
6088
  x = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
4561
6089
  y = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
6090
+ const minX = Math.min(quad[0], quad[2], quad[4], quad[6]);
6091
+ const maxX = Math.max(quad[0], quad[2], quad[4], quad[6]);
6092
+ const minY = Math.min(quad[1], quad[3], quad[5], quad[7]);
6093
+ const maxY = Math.max(quad[1], quad[3], quad[5], quad[7]);
6094
+ this.setLastActionPosition(
6095
+ { x, y },
6096
+ { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
6097
+ );
4562
6098
  } else {
4563
6099
  throw new Error("No quads");
4564
6100
  }
@@ -4570,6 +6106,10 @@ var Page = class {
4570
6106
  }
4571
6107
  x = box.content[0] + box.width / 2;
4572
6108
  y = box.content[1] + box.height / 2;
6109
+ this.setLastActionPosition(
6110
+ { x, y },
6111
+ { x: box.content[0], y: box.content[1], width: box.width, height: box.height }
6112
+ );
4573
6113
  }
4574
6114
  await this.cdp.send("Input.dispatchMouseEvent", {
4575
6115
  type: "mouseMoved",
@@ -4595,6 +6135,8 @@ var Page = class {
4595
6135
  if (options.optional) return false;
4596
6136
  throw new ElementNotFoundError(selector);
4597
6137
  }
6138
+ const scrollPos = await this.getElementPosition({ nodeId: element.nodeId });
6139
+ if (scrollPos) this.setLastActionPosition(scrollPos.center, scrollPos.bbox);
4598
6140
  await this.scrollIntoView(element.nodeId);
4599
6141
  return true;
4600
6142
  }
@@ -5356,7 +6898,7 @@ var Page = class {
5356
6898
  return {
5357
6899
  role,
5358
6900
  name,
5359
- value: value !== void 0 ? String(value) : void 0,
6901
+ value: value !== void 0 ? stringifyUnknown(value) : void 0,
5360
6902
  ref,
5361
6903
  children: children.length > 0 ? children : void 0,
5362
6904
  disabled,
@@ -5418,7 +6960,7 @@ var Page = class {
5418
6960
  selector,
5419
6961
  disabled,
5420
6962
  checked,
5421
- value: value !== void 0 ? String(value) : void 0
6963
+ value: value !== void 0 ? stringifyUnknown(value) : void 0
5422
6964
  });
5423
6965
  }
5424
6966
  }
@@ -5888,7 +7430,7 @@ var Page = class {
5888
7430
  */
5889
7431
  formatConsoleArgs(args) {
5890
7432
  return args.map((arg) => {
5891
- if (arg.value !== void 0) return String(arg.value);
7433
+ if (arg.value !== void 0) return stringifyUnknown(arg.value);
5892
7434
  if (arg.description) return arg.description;
5893
7435
  return "[object]";
5894
7436
  }).join(" ");
@@ -6677,6 +8219,25 @@ var Browser = class _Browser {
6677
8219
  this.cdp = cdp;
6678
8220
  this.providerSession = providerSession;
6679
8221
  }
8222
+ /**
8223
+ * Create a Browser from an existing CDPClient (used by daemon fast-path).
8224
+ * The caller is responsible for the CDP connection lifecycle.
8225
+ */
8226
+ static fromCDP(cdp, sessionInfo) {
8227
+ const providerSession = {
8228
+ wsUrl: sessionInfo.wsUrl,
8229
+ sessionId: sessionInfo.sessionId,
8230
+ async close() {
8231
+ }
8232
+ };
8233
+ const provider = {
8234
+ name: sessionInfo.provider ?? "daemon",
8235
+ async createSession() {
8236
+ return providerSession;
8237
+ }
8238
+ };
8239
+ return new _Browser(cdp, provider, providerSession, { provider: "generic" });
8240
+ }
6680
8241
  /**
6681
8242
  * Connect to a browser instance
6682
8243
  */