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/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;
@@ -268,6 +312,9 @@ async function createCDPClient(wsUrl, options = {}) {
268
312
  onAny(handler) {
269
313
  anyEventHandlers.add(handler);
270
314
  },
315
+ offAny(handler) {
316
+ anyEventHandlers.delete(handler);
317
+ },
271
318
  async close() {
272
319
  connected = false;
273
320
  await transport.close();
@@ -472,6 +519,230 @@ function createProvider(options) {
472
519
  }
473
520
  }
474
521
 
522
+ // src/actions/executor.ts
523
+ var fs = __toESM(require("fs"), 1);
524
+ var import_node_path = require("path");
525
+
526
+ // src/recording/redaction.ts
527
+ var REDACTED_VALUE = "[REDACTED]";
528
+ var SENSITIVE_AUTOCOMPLETE_TOKENS = [
529
+ "current-password",
530
+ "new-password",
531
+ "one-time-code",
532
+ "cc-number",
533
+ "cc-csc",
534
+ "cc-exp",
535
+ "cc-exp-month",
536
+ "cc-exp-year"
537
+ ];
538
+ function autocompleteTokens(autocomplete) {
539
+ if (!autocomplete) return [];
540
+ return autocomplete.toLowerCase().split(/\s+/).map((token) => token.trim()).filter(Boolean);
541
+ }
542
+ function isSensitiveFieldMetadata(metadata) {
543
+ if (!metadata) return false;
544
+ if (metadata.sensitiveValue) return true;
545
+ const inputType = metadata.inputType?.toLowerCase();
546
+ if (inputType === "password" || inputType === "hidden") {
547
+ return true;
548
+ }
549
+ const sensitiveAutocompleteTokens = new Set(SENSITIVE_AUTOCOMPLETE_TOKENS);
550
+ return autocompleteTokens(metadata.autocomplete).some(
551
+ (token) => sensitiveAutocompleteTokens.has(token)
552
+ );
553
+ }
554
+ function redactValueForRecording(value, metadata) {
555
+ if (value === void 0) return void 0;
556
+ return isSensitiveFieldMetadata(metadata) ? REDACTED_VALUE : value;
557
+ }
558
+
559
+ // src/browser/action-highlight.ts
560
+ var HIGHLIGHT_STYLES = {
561
+ click: { outline: "3px solid rgba(229,57,53,0.8)", badge: "#e53935", marker: "crosshair" },
562
+ fill: { outline: "3px solid rgba(33,150,243,0.8)", badge: "#2196f3" },
563
+ type: { outline: "3px solid rgba(33,150,243,0.6)", badge: "#2196f3" },
564
+ select: { outline: "3px solid rgba(156,39,176,0.8)", badge: "#9c27b0" },
565
+ hover: { outline: "2px dashed rgba(158,158,158,0.5)", badge: "#9e9e9e" },
566
+ scroll: { outline: "none", badge: "#607d8b", marker: "arrow" },
567
+ navigate: { outline: "none", badge: "#4caf50" },
568
+ submit: { outline: "3px solid rgba(255,152,0,0.8)", badge: "#ff9800" },
569
+ "assert-pass": { outline: "3px solid rgba(76,175,80,0.8)", badge: "#4caf50", marker: "check" },
570
+ "assert-fail": { outline: "3px solid rgba(244,67,54,0.8)", badge: "#f44336", marker: "cross" },
571
+ evaluate: { outline: "none", badge: "#ffc107" },
572
+ focus: { outline: "3px dotted rgba(33,150,243,0.6)", badge: "#2196f3" }
573
+ };
574
+ function buildHighlightScript(options) {
575
+ const style = HIGHLIGHT_STYLES[options.kind];
576
+ const label = options.label ? options.label.slice(0, 80) : void 0;
577
+ const escapedLabel = label ? label.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n") : "";
578
+ return `(function() {
579
+ // Remove any existing highlight
580
+ var existing = document.getElementById('__bp-action-highlight');
581
+ if (existing) existing.remove();
582
+
583
+ var container = document.createElement('div');
584
+ container.id = '__bp-action-highlight';
585
+ container.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:99999;';
586
+
587
+ ${options.bbox ? `
588
+ // Element outline
589
+ var outline = document.createElement('div');
590
+ outline.style.cssText = 'position:fixed;' +
591
+ 'left:${options.bbox.x}px;top:${options.bbox.y}px;' +
592
+ 'width:${options.bbox.width}px;height:${options.bbox.height}px;' +
593
+ '${style.outline !== "none" ? `outline:${style.outline};outline-offset:-1px;` : ""}' +
594
+ 'pointer-events:none;box-sizing:border-box;';
595
+ container.appendChild(outline);
596
+ ` : ""}
597
+
598
+ ${options.point && style.marker === "crosshair" ? `
599
+ // Crosshair at click point
600
+ var hLine = document.createElement('div');
601
+ hLine.style.cssText = 'position:fixed;left:${options.point.x - 12}px;top:${options.point.y}px;' +
602
+ 'width:24px;height:2px;background:${style.badge};pointer-events:none;';
603
+ var vLine = document.createElement('div');
604
+ vLine.style.cssText = 'position:fixed;left:${options.point.x}px;top:${options.point.y - 12}px;' +
605
+ 'width:2px;height:24px;background:${style.badge};pointer-events:none;';
606
+ // Dot at center
607
+ var dot = document.createElement('div');
608
+ dot.style.cssText = 'position:fixed;left:${options.point.x - 4}px;top:${options.point.y - 4}px;' +
609
+ 'width:8px;height:8px;border-radius:50%;background:${style.badge};pointer-events:none;';
610
+ container.appendChild(hLine);
611
+ container.appendChild(vLine);
612
+ container.appendChild(dot);
613
+ ` : ""}
614
+
615
+ ${label ? `
616
+ // Badge with label
617
+ var badge = document.createElement('div');
618
+ badge.style.cssText = 'position:fixed;' +
619
+ ${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;'"} +
620
+ 'background:${style.badge};color:white;padding:4px 8px;' +
621
+ 'font-family:monospace;font-size:12px;font-weight:bold;' +
622
+ 'border-radius:3px;white-space:nowrap;max-width:400px;overflow:hidden;text-overflow:ellipsis;' +
623
+ 'pointer-events:none;';
624
+ badge.textContent = '${escapedLabel}';
625
+ container.appendChild(badge);
626
+ ` : ""}
627
+
628
+ ${style.marker === "check" && options.bbox ? `
629
+ // Checkmark
630
+ var check = document.createElement('div');
631
+ check.style.cssText = 'position:fixed;left:${options.bbox.x + options.bbox.width / 2 - 10}px;' +
632
+ 'top:${options.bbox.y + options.bbox.height / 2 - 10}px;' +
633
+ 'width:20px;height:20px;font-size:18px;color:${style.badge};pointer-events:none;text-align:center;line-height:20px;';
634
+ check.textContent = '\\u2713';
635
+ container.appendChild(check);
636
+ ` : ""}
637
+
638
+ ${style.marker === "cross" && options.bbox ? `
639
+ // Cross mark
640
+ var cross = document.createElement('div');
641
+ cross.style.cssText = 'position:fixed;left:${options.bbox.x + options.bbox.width / 2 - 10}px;' +
642
+ 'top:${options.bbox.y + options.bbox.height / 2 - 10}px;' +
643
+ 'width:20px;height:20px;font-size:18px;color:${style.badge};pointer-events:none;text-align:center;line-height:20px;font-weight:bold;';
644
+ cross.textContent = '\\u2717';
645
+ container.appendChild(cross);
646
+ ` : ""}
647
+
648
+ document.body.appendChild(container);
649
+ window.__bpRemoveActionHighlight = function() {
650
+ var el = document.getElementById('__bp-action-highlight');
651
+ if (el) el.remove();
652
+ delete window.__bpRemoveActionHighlight;
653
+ };
654
+ })();`;
655
+ }
656
+ async function injectActionHighlight(page, options) {
657
+ try {
658
+ await page.evaluate(buildHighlightScript(options));
659
+ } catch {
660
+ }
661
+ }
662
+ async function removeActionHighlight(page) {
663
+ try {
664
+ await page.evaluate(`(function() {
665
+ if (window.__bpRemoveActionHighlight) {
666
+ window.__bpRemoveActionHighlight();
667
+ }
668
+ })()`);
669
+ } catch {
670
+ }
671
+ }
672
+ function stepToHighlightKind(step) {
673
+ switch (step.action) {
674
+ case "click":
675
+ return "click";
676
+ case "fill":
677
+ return "fill";
678
+ case "type":
679
+ return "type";
680
+ case "select":
681
+ return "select";
682
+ case "hover":
683
+ return "hover";
684
+ case "scroll":
685
+ return "scroll";
686
+ case "goto":
687
+ return "navigate";
688
+ case "submit":
689
+ return "submit";
690
+ case "focus":
691
+ return "focus";
692
+ case "evaluate":
693
+ case "press":
694
+ case "shortcut":
695
+ return "evaluate";
696
+ case "assertVisible":
697
+ case "assertExists":
698
+ case "assertText":
699
+ case "assertUrl":
700
+ case "assertValue":
701
+ return step.success ? "assert-pass" : "assert-fail";
702
+ // Observation-only actions — no highlight
703
+ case "wait":
704
+ case "snapshot":
705
+ case "forms":
706
+ case "text":
707
+ case "screenshot":
708
+ case "newTab":
709
+ case "closeTab":
710
+ case "switchFrame":
711
+ case "switchToMain":
712
+ return null;
713
+ default:
714
+ return null;
715
+ }
716
+ }
717
+ function getHighlightLabel(step, result, targetMetadata) {
718
+ switch (step.action) {
719
+ case "fill":
720
+ case "type":
721
+ return typeof step.value === "string" ? `"${redactValueForRecording(step.value, targetMetadata)}"` : void 0;
722
+ case "select":
723
+ return redactValueForRecording(
724
+ typeof step.value === "string" ? step.value : void 0,
725
+ targetMetadata
726
+ );
727
+ case "goto":
728
+ return step.url;
729
+ case "evaluate":
730
+ return "JS";
731
+ case "press":
732
+ return step.key;
733
+ case "shortcut":
734
+ return step.combo;
735
+ case "assertText":
736
+ case "assertUrl":
737
+ case "assertValue":
738
+ case "assertVisible":
739
+ case "assertExists":
740
+ return result.success ? "\u2713" : "\u2717";
741
+ default:
742
+ return void 0;
743
+ }
744
+ }
745
+
475
746
  // src/browser/actionability.ts
476
747
  var ActionabilityError = class extends Error {
477
748
  failureType;
@@ -1086,6 +1357,13 @@ var NavigationError = class extends Error {
1086
1357
 
1087
1358
  // src/actions/executor.ts
1088
1359
  var DEFAULT_TIMEOUT = 3e4;
1360
+ var DEFAULT_RECORDING_SKIP_ACTIONS = [
1361
+ "wait",
1362
+ "snapshot",
1363
+ "forms",
1364
+ "text",
1365
+ "screenshot"
1366
+ ];
1089
1367
  function classifyFailure(error) {
1090
1368
  if (error instanceof ElementNotFoundError) {
1091
1369
  return { reason: "missing" };
@@ -1165,6 +1443,9 @@ var BatchExecutor = class {
1165
1443
  const { timeout = DEFAULT_TIMEOUT, onFail = "stop" } = options;
1166
1444
  const results = [];
1167
1445
  const startTime = Date.now();
1446
+ const recording = options.record ? this.createRecordingContext(options.record) : null;
1447
+ const startUrl = recording ? await this.getPageUrlSafe() : "";
1448
+ let stoppedAtIndex;
1168
1449
  for (let i = 0; i < steps.length; i++) {
1169
1450
  const step = steps[i];
1170
1451
  const stepStart = Date.now();
@@ -1177,8 +1458,9 @@ var BatchExecutor = class {
1177
1458
  await new Promise((resolve) => setTimeout(resolve, retryDelay));
1178
1459
  }
1179
1460
  try {
1461
+ this.page.resetLastActionPosition();
1180
1462
  const result = await this.executeStep(step, timeout);
1181
- results.push({
1463
+ const stepResult = {
1182
1464
  index: i,
1183
1465
  action: step.action,
1184
1466
  selector: step.selector,
@@ -1186,8 +1468,15 @@ var BatchExecutor = class {
1186
1468
  success: true,
1187
1469
  durationMs: Date.now() - stepStart,
1188
1470
  result: result.value,
1189
- text: result.text
1190
- });
1471
+ text: result.text,
1472
+ timestamp: Date.now(),
1473
+ coordinates: this.page.getLastActionCoordinates() ?? void 0,
1474
+ boundingBox: this.page.getLastActionBoundingBox() ?? void 0
1475
+ };
1476
+ if (recording && !recording.skipActions.has(step.action)) {
1477
+ await this.captureRecordingFrame(step, stepResult, recording);
1478
+ }
1479
+ results.push(stepResult);
1191
1480
  succeeded = true;
1192
1481
  break;
1193
1482
  } catch (error) {
@@ -1208,7 +1497,7 @@ var BatchExecutor = class {
1208
1497
  } catch {
1209
1498
  }
1210
1499
  }
1211
- results.push({
1500
+ const failedResult = {
1212
1501
  index: i,
1213
1502
  action: step.action,
1214
1503
  selector: step.selector,
@@ -1218,25 +1507,177 @@ var BatchExecutor = class {
1218
1507
  hints,
1219
1508
  failureReason: reason,
1220
1509
  coveringElement,
1221
- suggestion: getSuggestion(reason)
1222
- });
1510
+ suggestion: getSuggestion(reason),
1511
+ timestamp: Date.now()
1512
+ };
1513
+ if (recording && !recording.skipActions.has(step.action)) {
1514
+ await this.captureRecordingFrame(step, failedResult, recording);
1515
+ }
1516
+ results.push(failedResult);
1223
1517
  if (onFail === "stop" && !step.optional) {
1224
- return {
1225
- success: false,
1226
- stoppedAtIndex: i,
1227
- steps: results,
1228
- totalDurationMs: Date.now() - startTime
1229
- };
1518
+ stoppedAtIndex = i;
1519
+ break;
1230
1520
  }
1231
1521
  }
1232
1522
  }
1233
- const allSuccess = results.every((r) => r.success || steps[r.index]?.optional);
1523
+ const totalDurationMs = Date.now() - startTime;
1524
+ const allSuccess = stoppedAtIndex === void 0 && results.every((result) => result.success || steps[result.index]?.optional);
1525
+ let recordingManifest;
1526
+ if (recording) {
1527
+ recordingManifest = await this.writeRecordingManifest(
1528
+ recording,
1529
+ startTime,
1530
+ startUrl,
1531
+ allSuccess
1532
+ );
1533
+ }
1234
1534
  return {
1235
1535
  success: allSuccess,
1536
+ stoppedAtIndex,
1236
1537
  steps: results,
1237
- totalDurationMs: Date.now() - startTime
1538
+ totalDurationMs,
1539
+ recordingManifest
1238
1540
  };
1239
1541
  }
1542
+ createRecordingContext(record) {
1543
+ const baseDir = record.outputDir ?? (0, import_node_path.join)(process.cwd(), ".browser-pilot");
1544
+ const screenshotDir = (0, import_node_path.join)(baseDir, "screenshots");
1545
+ const manifestPath = (0, import_node_path.join)(baseDir, "recording.json");
1546
+ let existingFrames = [];
1547
+ try {
1548
+ const existing = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
1549
+ if (existing.frames && Array.isArray(existing.frames)) {
1550
+ existingFrames = existing.frames;
1551
+ }
1552
+ } catch {
1553
+ }
1554
+ fs.mkdirSync(screenshotDir, { recursive: true });
1555
+ return {
1556
+ baseDir,
1557
+ screenshotDir,
1558
+ sessionId: record.sessionId ?? this.page.targetId,
1559
+ frames: existingFrames,
1560
+ format: record.format ?? "webp",
1561
+ quality: Math.max(0, Math.min(100, record.quality ?? 40)),
1562
+ highlights: record.highlights !== false,
1563
+ skipActions: new Set(record.skipActions ?? DEFAULT_RECORDING_SKIP_ACTIONS)
1564
+ };
1565
+ }
1566
+ async getPageUrlSafe() {
1567
+ try {
1568
+ return await this.page.url();
1569
+ } catch {
1570
+ return "";
1571
+ }
1572
+ }
1573
+ /**
1574
+ * Capture a recording screenshot frame with optional highlight overlay
1575
+ */
1576
+ async captureRecordingFrame(step, stepResult, recording) {
1577
+ const targetMetadata = this.page.getLastActionTargetMetadata();
1578
+ let highlightInjected = false;
1579
+ try {
1580
+ const ts = Date.now();
1581
+ const seq = String(recording.frames.length + 1).padStart(4, "0");
1582
+ const filename = `${seq}-${ts}-${stepResult.action}.${recording.format}`;
1583
+ const filepath = (0, import_node_path.join)(recording.screenshotDir, filename);
1584
+ if (recording.highlights) {
1585
+ const kind = stepToHighlightKind(stepResult);
1586
+ if (kind) {
1587
+ await injectActionHighlight(this.page, {
1588
+ kind,
1589
+ bbox: stepResult.boundingBox,
1590
+ point: stepResult.coordinates,
1591
+ label: getHighlightLabel(step, stepResult, targetMetadata)
1592
+ });
1593
+ highlightInjected = true;
1594
+ }
1595
+ }
1596
+ const base64 = await this.page.screenshot({
1597
+ format: recording.format,
1598
+ quality: recording.quality
1599
+ });
1600
+ const buffer = Buffer.from(base64, "base64");
1601
+ fs.writeFileSync(filepath, buffer);
1602
+ stepResult.screenshotPath = filepath;
1603
+ let pageUrl;
1604
+ let pageTitle;
1605
+ try {
1606
+ pageUrl = await this.page.url();
1607
+ pageTitle = await this.page.title();
1608
+ } catch {
1609
+ }
1610
+ recording.frames.push({
1611
+ seq: recording.frames.length + 1,
1612
+ timestamp: ts,
1613
+ action: stepResult.action,
1614
+ selector: stepResult.selectorUsed ?? (Array.isArray(step.selector) ? step.selector[0] : step.selector),
1615
+ value: redactValueForRecording(
1616
+ typeof step.value === "string" ? step.value : void 0,
1617
+ targetMetadata
1618
+ ),
1619
+ url: step.url,
1620
+ coordinates: stepResult.coordinates,
1621
+ boundingBox: stepResult.boundingBox,
1622
+ success: stepResult.success,
1623
+ durationMs: stepResult.durationMs,
1624
+ error: stepResult.error,
1625
+ screenshot: filename,
1626
+ pageUrl,
1627
+ pageTitle
1628
+ });
1629
+ } catch {
1630
+ } finally {
1631
+ if (recording.highlights || highlightInjected) {
1632
+ await removeActionHighlight(this.page);
1633
+ }
1634
+ }
1635
+ }
1636
+ /**
1637
+ * Write recording manifest to disk
1638
+ */
1639
+ async writeRecordingManifest(recording, startTime, startUrl, success) {
1640
+ let endUrl = startUrl;
1641
+ let viewport = { width: 1280, height: 720 };
1642
+ try {
1643
+ endUrl = await this.page.url();
1644
+ } catch {
1645
+ }
1646
+ try {
1647
+ const metrics = await this.page.cdpClient.send("Page.getLayoutMetrics");
1648
+ viewport = {
1649
+ width: metrics.cssVisualViewport.clientWidth,
1650
+ height: metrics.cssVisualViewport.clientHeight
1651
+ };
1652
+ } catch {
1653
+ }
1654
+ const manifestPath = (0, import_node_path.join)(recording.baseDir, "recording.json");
1655
+ let recordedAt = new Date(startTime).toISOString();
1656
+ let originalStartUrl = startUrl;
1657
+ try {
1658
+ const existing = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
1659
+ if (existing.recordedAt) recordedAt = existing.recordedAt;
1660
+ if (existing.startUrl) originalStartUrl = existing.startUrl;
1661
+ } catch {
1662
+ }
1663
+ const firstFrameTime = recording.frames[0]?.timestamp ?? startTime;
1664
+ const totalDurationMs = Date.now() - Math.min(firstFrameTime, startTime);
1665
+ const manifest = {
1666
+ version: 1,
1667
+ recordedAt,
1668
+ sessionId: recording.sessionId,
1669
+ startUrl: originalStartUrl,
1670
+ endUrl,
1671
+ viewport,
1672
+ format: recording.format,
1673
+ quality: recording.quality,
1674
+ totalDurationMs,
1675
+ success,
1676
+ frames: recording.frames
1677
+ };
1678
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
1679
+ return manifestPath;
1680
+ }
1240
1681
  /**
1241
1682
  * Execute a single step
1242
1683
  */
@@ -3766,6 +4207,9 @@ var Page = class {
3766
4207
  brokenFrame = null;
3767
4208
  /** Last matched selector from findElement (for selectorUsed tracking) */
3768
4209
  _lastMatchedSelector;
4210
+ _lastActionCoordinates = null;
4211
+ _lastActionBoundingBox = null;
4212
+ _lastActionTargetMetadata = null;
3769
4213
  /** Last snapshot for stale ref recovery */
3770
4214
  lastSnapshot;
3771
4215
  /** Audio input controller (lazy-initialized) */
@@ -3797,6 +4241,76 @@ var Page = class {
3797
4241
  getLastMatchedSelector() {
3798
4242
  return this._lastMatchedSelector;
3799
4243
  }
4244
+ async getActionTargetMetadata(identifiers) {
4245
+ try {
4246
+ const objectId = identifiers.objectId ?? (identifiers.nodeId ? await this.resolveObjectId(identifiers.nodeId) : void 0);
4247
+ if (!objectId) return null;
4248
+ const response = await this.cdp.send("Runtime.callFunctionOn", {
4249
+ objectId,
4250
+ functionDeclaration: `function() {
4251
+ const tagName = this.tagName?.toLowerCase?.() || '';
4252
+ const inputType =
4253
+ tagName === 'input' && typeof this.type === 'string' ? this.type.toLowerCase() : '';
4254
+ const autocomplete =
4255
+ typeof this.autocomplete === 'string' ? this.autocomplete.toLowerCase() : '';
4256
+ return { tagName, inputType, autocomplete };
4257
+ }`,
4258
+ returnByValue: true
4259
+ });
4260
+ return response.result.value ?? null;
4261
+ } catch {
4262
+ return null;
4263
+ }
4264
+ }
4265
+ async getElementPosition(identifiers) {
4266
+ try {
4267
+ const { quads } = await this.cdp.send(
4268
+ "DOM.getContentQuads",
4269
+ identifiers
4270
+ );
4271
+ if (quads?.length > 0) {
4272
+ const q = quads[0];
4273
+ const minX = Math.min(q[0], q[2], q[4], q[6]);
4274
+ const maxX = Math.max(q[0], q[2], q[4], q[6]);
4275
+ const minY = Math.min(q[1], q[3], q[5], q[7]);
4276
+ const maxY = Math.max(q[1], q[3], q[5], q[7]);
4277
+ return {
4278
+ center: { x: (minX + maxX) / 2, y: (minY + maxY) / 2 },
4279
+ bbox: { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
4280
+ };
4281
+ }
4282
+ } catch {
4283
+ }
4284
+ if (identifiers.nodeId) {
4285
+ const box = await this.getBoxModel(identifiers.nodeId);
4286
+ if (box) {
4287
+ return {
4288
+ center: { x: box.content[0] + box.width / 2, y: box.content[1] + box.height / 2 },
4289
+ bbox: { x: box.content[0], y: box.content[1], width: box.width, height: box.height }
4290
+ };
4291
+ }
4292
+ }
4293
+ return null;
4294
+ }
4295
+ setLastActionPosition(coords, bbox) {
4296
+ this._lastActionCoordinates = coords;
4297
+ this._lastActionBoundingBox = bbox;
4298
+ }
4299
+ getLastActionCoordinates() {
4300
+ return this._lastActionCoordinates;
4301
+ }
4302
+ getLastActionBoundingBox() {
4303
+ return this._lastActionBoundingBox;
4304
+ }
4305
+ getLastActionTargetMetadata() {
4306
+ return this._lastActionTargetMetadata;
4307
+ }
4308
+ /** Reset position tracking (call before each executor step) */
4309
+ resetLastActionPosition() {
4310
+ this._lastActionCoordinates = null;
4311
+ this._lastActionBoundingBox = null;
4312
+ this._lastActionTargetMetadata = null;
4313
+ }
3800
4314
  /**
3801
4315
  * Initialize the page (enable required CDP domains)
3802
4316
  */
@@ -3964,6 +4478,14 @@ var Page = class {
3964
4478
  const quad = quads[0];
3965
4479
  clickX = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
3966
4480
  clickY = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
4481
+ const minX = Math.min(quad[0], quad[2], quad[4], quad[6]);
4482
+ const maxX = Math.max(quad[0], quad[2], quad[4], quad[6]);
4483
+ const minY = Math.min(quad[1], quad[3], quad[5], quad[7]);
4484
+ const maxY = Math.max(quad[1], quad[3], quad[5], quad[7]);
4485
+ this.setLastActionPosition(
4486
+ { x: clickX, y: clickY },
4487
+ { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
4488
+ );
3967
4489
  } else {
3968
4490
  throw new Error("No quads");
3969
4491
  }
@@ -3972,6 +4494,10 @@ var Page = class {
3972
4494
  if (!box) throw new Error("Could not get element position");
3973
4495
  clickX = box.content[0] + box.width / 2;
3974
4496
  clickY = box.content[1] + box.height / 2;
4497
+ this.setLastActionPosition(
4498
+ { x: clickX, y: clickY },
4499
+ { x: box.content[0], y: box.content[1], width: box.width, height: box.height }
4500
+ );
3975
4501
  }
3976
4502
  const hitTargetCoordinates = this.currentFrame ? void 0 : { x: clickX, y: clickY };
3977
4503
  const HIT_TARGET_RETRIES = 3;
@@ -4022,13 +4548,20 @@ var Page = class {
4022
4548
  if (options.optional) return false;
4023
4549
  throw e;
4024
4550
  }
4551
+ const fillPos = await this.getElementPosition({ nodeId: element.nodeId });
4552
+ if (fillPos) this.setLastActionPosition(fillPos.center, fillPos.bbox);
4025
4553
  const tagInfo = await this.cdp.send("Runtime.callFunctionOn", {
4026
4554
  objectId,
4027
4555
  functionDeclaration: `function() {
4028
- return { tagName: this.tagName?.toLowerCase() || '', inputType: (this.type || '').toLowerCase() };
4556
+ return {
4557
+ tagName: this.tagName?.toLowerCase() || '',
4558
+ inputType: (this.type || '').toLowerCase(),
4559
+ autocomplete: typeof this.autocomplete === 'string' ? this.autocomplete.toLowerCase() : '',
4560
+ };
4029
4561
  }`,
4030
4562
  returnByValue: true
4031
4563
  });
4564
+ this._lastActionTargetMetadata = tagInfo.result.value;
4032
4565
  const { tagName, inputType } = tagInfo.result.value;
4033
4566
  const specialInputTypes = /* @__PURE__ */ new Set([
4034
4567
  "date",
@@ -4110,6 +4643,9 @@ var Page = class {
4110
4643
  if (options.optional) return false;
4111
4644
  throw e;
4112
4645
  }
4646
+ const typePos = await this.getElementPosition({ nodeId: element.nodeId });
4647
+ if (typePos) this.setLastActionPosition(typePos.center, typePos.bbox);
4648
+ this._lastActionTargetMetadata = await this.getActionTargetMetadata({ objectId });
4113
4649
  await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
4114
4650
  for (const char of text) {
4115
4651
  const def = US_KEYBOARD[char];
@@ -4189,6 +4725,9 @@ var Page = class {
4189
4725
  if (options.optional) return false;
4190
4726
  throw e;
4191
4727
  }
4728
+ const selectPos = await this.getElementPosition({ nodeId: element.nodeId });
4729
+ if (selectPos) this.setLastActionPosition(selectPos.center, selectPos.bbox);
4730
+ this._lastActionTargetMetadata = await this.getActionTargetMetadata({ objectId });
4192
4731
  const metadata = await this.getNativeSelectMetadata(objectId, values);
4193
4732
  if (!metadata.isSelect) {
4194
4733
  throw new Error("select() target must be a native <select> element");
@@ -4325,6 +4864,8 @@ var Page = class {
4325
4864
  if (options.optional) return false;
4326
4865
  throw e;
4327
4866
  }
4867
+ const checkPos = await this.getElementPosition({ nodeId: element.nodeId });
4868
+ if (checkPos) this.setLastActionPosition(checkPos.center, checkPos.bbox);
4328
4869
  const before = await this.cdp.send("Runtime.callFunctionOn", {
4329
4870
  objectId: object.objectId,
4330
4871
  functionDeclaration: "function() { return !!this.checked; }",
@@ -4373,6 +4914,8 @@ var Page = class {
4373
4914
  if (options.optional) return false;
4374
4915
  throw e;
4375
4916
  }
4917
+ const uncheckPos = await this.getElementPosition({ nodeId: element.nodeId });
4918
+ if (uncheckPos) this.setLastActionPosition(uncheckPos.center, uncheckPos.bbox);
4376
4919
  const isRadio = await this.cdp.send(
4377
4920
  "Runtime.callFunctionOn",
4378
4921
  {
@@ -4428,6 +4971,8 @@ var Page = class {
4428
4971
  throw new ElementNotFoundError(selector, hints);
4429
4972
  }
4430
4973
  const objectId = await this.resolveObjectId(element.nodeId);
4974
+ const submitPos = await this.getElementPosition({ nodeId: element.nodeId });
4975
+ if (submitPos) this.setLastActionPosition(submitPos.center, submitPos.bbox);
4431
4976
  const isFormElement = await this.cdp.send(
4432
4977
  "Runtime.callFunctionOn",
4433
4978
  {
@@ -4524,6 +5069,8 @@ var Page = class {
4524
5069
  const hints = await generateHints(this, selectorList, "focus");
4525
5070
  throw new ElementNotFoundError(selector, hints);
4526
5071
  }
5072
+ const focusPos = await this.getElementPosition({ nodeId: element.nodeId });
5073
+ if (focusPos) this.setLastActionPosition(focusPos.center, focusPos.bbox);
4527
5074
  await this.cdp.send("DOM.focus", { nodeId: element.nodeId });
4528
5075
  return true;
4529
5076
  }
@@ -4559,6 +5106,14 @@ var Page = class {
4559
5106
  const quad = quads[0];
4560
5107
  x = (quad[0] + quad[2] + quad[4] + quad[6]) / 4;
4561
5108
  y = (quad[1] + quad[3] + quad[5] + quad[7]) / 4;
5109
+ const minX = Math.min(quad[0], quad[2], quad[4], quad[6]);
5110
+ const maxX = Math.max(quad[0], quad[2], quad[4], quad[6]);
5111
+ const minY = Math.min(quad[1], quad[3], quad[5], quad[7]);
5112
+ const maxY = Math.max(quad[1], quad[3], quad[5], quad[7]);
5113
+ this.setLastActionPosition(
5114
+ { x, y },
5115
+ { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
5116
+ );
4562
5117
  } else {
4563
5118
  throw new Error("No quads");
4564
5119
  }
@@ -4570,6 +5125,10 @@ var Page = class {
4570
5125
  }
4571
5126
  x = box.content[0] + box.width / 2;
4572
5127
  y = box.content[1] + box.height / 2;
5128
+ this.setLastActionPosition(
5129
+ { x, y },
5130
+ { x: box.content[0], y: box.content[1], width: box.width, height: box.height }
5131
+ );
4573
5132
  }
4574
5133
  await this.cdp.send("Input.dispatchMouseEvent", {
4575
5134
  type: "mouseMoved",
@@ -4595,6 +5154,8 @@ var Page = class {
4595
5154
  if (options.optional) return false;
4596
5155
  throw new ElementNotFoundError(selector);
4597
5156
  }
5157
+ const scrollPos = await this.getElementPosition({ nodeId: element.nodeId });
5158
+ if (scrollPos) this.setLastActionPosition(scrollPos.center, scrollPos.bbox);
4598
5159
  await this.scrollIntoView(element.nodeId);
4599
5160
  return true;
4600
5161
  }
@@ -5356,7 +5917,7 @@ var Page = class {
5356
5917
  return {
5357
5918
  role,
5358
5919
  name,
5359
- value: value !== void 0 ? String(value) : void 0,
5920
+ value: value !== void 0 ? stringifyUnknown(value) : void 0,
5360
5921
  ref,
5361
5922
  children: children.length > 0 ? children : void 0,
5362
5923
  disabled,
@@ -5418,7 +5979,7 @@ var Page = class {
5418
5979
  selector,
5419
5980
  disabled,
5420
5981
  checked,
5421
- value: value !== void 0 ? String(value) : void 0
5982
+ value: value !== void 0 ? stringifyUnknown(value) : void 0
5422
5983
  });
5423
5984
  }
5424
5985
  }
@@ -5888,7 +6449,7 @@ var Page = class {
5888
6449
  */
5889
6450
  formatConsoleArgs(args) {
5890
6451
  return args.map((arg) => {
5891
- if (arg.value !== void 0) return String(arg.value);
6452
+ if (arg.value !== void 0) return stringifyUnknown(arg.value);
5892
6453
  if (arg.description) return arg.description;
5893
6454
  return "[object]";
5894
6455
  }).join(" ");
@@ -6677,6 +7238,25 @@ var Browser = class _Browser {
6677
7238
  this.cdp = cdp;
6678
7239
  this.providerSession = providerSession;
6679
7240
  }
7241
+ /**
7242
+ * Create a Browser from an existing CDPClient (used by daemon fast-path).
7243
+ * The caller is responsible for the CDP connection lifecycle.
7244
+ */
7245
+ static fromCDP(cdp, sessionInfo) {
7246
+ const providerSession = {
7247
+ wsUrl: sessionInfo.wsUrl,
7248
+ sessionId: sessionInfo.sessionId,
7249
+ async close() {
7250
+ }
7251
+ };
7252
+ const provider = {
7253
+ name: sessionInfo.provider ?? "daemon",
7254
+ async createSession() {
7255
+ return providerSession;
7256
+ }
7257
+ };
7258
+ return new _Browser(cdp, provider, providerSession, { provider: "generic" });
7259
+ }
6680
7260
  /**
6681
7261
  * Connect to a browser instance
6682
7262
  */