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/README.md CHANGED
@@ -373,6 +373,8 @@ The CLI provides session persistence for interactive workflows:
373
373
  # Connect to a browser
374
374
  bp connect --provider browserbase --name my-session
375
375
  bp connect --provider generic # auto-discovers local Chrome
376
+ bp connect --no-daemon # skip daemon (direct WebSocket only)
377
+ bp connect --daemon-idle 30 # custom idle timeout (minutes)
376
378
 
377
379
  # Execute actions
378
380
  bp exec -s my-session '{"action":"goto","url":"https://example.com"}'
@@ -398,16 +400,23 @@ bp connect --new-tab --url https://example.com --name fresh
398
400
 
399
401
  # Handle native dialogs (alert/confirm/prompt)
400
402
  bp exec --dialog accept '{"action":"click","selector":"#delete-btn"}'
403
+ bp exec --record '[{"action":"click","selector":"#checkout"},{"action":"assertText","expect":"Thanks"}]'
401
404
 
402
405
  # Other commands
403
406
  bp text -s my-session --selector ".main-content"
404
407
  bp screenshot -s my-session --output page.png
405
408
  bp listen ws -m "*voice*" # monitor WebSocket traffic
406
409
  bp list # list all sessions
410
+ bp clean --max-size 500MB # trim old sessions by disk usage
407
411
  bp close -s my-session # close session
408
412
  bp actions # show complete action reference
409
413
  bp run workflow.json # run a workflow file
410
414
 
415
+ # Daemon management
416
+ bp daemon status # check daemon health
417
+ bp daemon stop # stop daemon for default session
418
+ bp daemon logs # view daemon log
419
+
411
420
  # Actions with inline assertions (no extra bp eval needed)
412
421
  bp exec '[
413
422
  {"action":"goto","url":"https://example.com/login"},
@@ -534,10 +543,32 @@ The output format is compatible with `page.batch()`:
534
543
  ```
535
544
 
536
545
  **Notes:**
537
- - Password fields are automatically redacted as `[REDACTED]`
546
+ - Sensitive fields are automatically redacted as `[REDACTED]` based on input settings such as `type="password"`, `type="hidden"`, and secret/autofill hints like `autocomplete="one-time-code"` or `cc-number`
538
547
  - Selectors are multi-selector arrays ordered by reliability (data attributes > IDs > CSS paths)
539
548
  - Edit the JSON to adjust selectors or add `optional: true` flags
540
549
 
550
+ ### Screenshot Trail During Replay
551
+
552
+ Capture a lightweight visual trail while replaying steps. Enable recording at the session level so all `bp exec` calls are captured automatically:
553
+
554
+ ```bash
555
+ # Enable recording for the entire session
556
+ bp connect --provider generic --name my-session --record
557
+
558
+ # All exec calls now produce screenshots — frames accumulate in one manifest
559
+ bp exec -s my-session '[
560
+ {"action":"goto","url":"https://example.com/login"},
561
+ {"action":"fill","selector":"#email","value":"user@example.com"},
562
+ {"action":"submit","selector":"form"}
563
+ ]'
564
+ bp exec -s my-session '{"action":"assertUrl","expect":"/dashboard"}'
565
+
566
+ # Or enable recording on a single exec call
567
+ bp exec --record '[{"action":"click","selector":"#checkout"}]'
568
+ ```
569
+
570
+ This writes `recording.json` plus a `screenshots/` directory in the session directory. Sensitive field values are redacted in both the manifest and the screenshot overlays. See the [Action Recording Guide](./docs/guides/action-recording.md) for options like `--record-format`, `--record-quality`, and `--no-highlights`.
571
+
541
572
  ## Examples
542
573
 
543
574
  ### Login Flow with Error Handling
@@ -573,8 +604,32 @@ await page.fill('.dropdown-search', 'United');
573
604
  await page.click('.dropdown-option:has-text("United States")');
574
605
  ```
575
606
 
607
+ ### WebSocket Daemon
608
+
609
+ By default, `bp connect` spawns a lightweight background daemon that holds the CDP WebSocket open. Subsequent CLI commands connect via Unix socket (~5-15ms) instead of re-establishing WebSocket (~280-1030ms per command).
610
+
611
+ ```bash
612
+ # Daemon spawns automatically on connect
613
+ bp connect --provider generic --name dev
614
+
615
+ # Subsequent commands use the fast daemon path
616
+ bp exec -s dev '{"action":"snapshot"}' # ~5-15ms overhead instead of ~280ms
617
+
618
+ # Manage the daemon
619
+ bp daemon status # check health, PID, uptime
620
+ bp daemon stop # stop daemon
621
+ bp daemon logs # view daemon log
622
+
623
+ # Disable daemon for direct WebSocket
624
+ bp connect --no-daemon
625
+ ```
626
+
627
+ The daemon is transparent — if it dies or becomes stale, CLI commands fall back to direct WebSocket silently. Each session gets its own daemon with a 60-minute idle timeout.
628
+
576
629
  ### Cloudflare Workers
577
630
 
631
+ > Note: Cloudflare Workers' Node-compat runtime can expose parts of `node:net` with compatibility flags, but browser-pilot's daemon fast-path is intentionally CLI/Node-specific (Unix domain sockets + local background process). In Workers, use the normal direct WebSocket path shown below.
632
+
578
633
  ```typescript
579
634
  export default {
580
635
  async fetch(request: Request, env: Env): Promise<Response> {
@@ -656,9 +711,9 @@ enableTracing({ output: 'console' });
656
711
  browser-pilot is designed for AI agents. Two resources for agent setup:
657
712
 
658
713
  - **[llms.txt](./docs/llms.txt)** - Abbreviated reference for LLM context windows
659
- - **[Claude Code Skill](./docs/skill/SKILL.md)** - Full skill for Claude Code agents
714
+ - **[Claude Code Skill](./docs/automating-browsers/SKILL.md)** - Full skill for Claude Code agents
660
715
 
661
- To use with Claude Code, copy `docs/skill/` to your project or reference it in your agent's context.
716
+ To use with Claude Code, copy `docs/automating-browsers/` to your project or reference it in your agent's context.
662
717
 
663
718
  ## Documentation
664
719
 
@@ -666,6 +721,7 @@ See the [docs](./docs) folder for detailed documentation:
666
721
 
667
722
  - [Getting Started](./docs/getting-started.md)
668
723
  - [Providers](./docs/providers.md)
724
+ - [Action Recording](./docs/guides/action-recording.md)
669
725
  - [Multi-Selector Guide](./docs/guides/multi-selector.md)
670
726
  - [Batch Actions](./docs/guides/batch-actions.md)
671
727
  - [Snapshots](./docs/guides/snapshots.md)
package/dist/actions.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/actions/index.ts
@@ -26,6 +36,230 @@ __export(actions_exports, {
26
36
  });
27
37
  module.exports = __toCommonJS(actions_exports);
28
38
 
39
+ // src/actions/executor.ts
40
+ var fs = __toESM(require("fs"), 1);
41
+ var import_node_path = require("path");
42
+
43
+ // src/recording/redaction.ts
44
+ var REDACTED_VALUE = "[REDACTED]";
45
+ var SENSITIVE_AUTOCOMPLETE_TOKENS = [
46
+ "current-password",
47
+ "new-password",
48
+ "one-time-code",
49
+ "cc-number",
50
+ "cc-csc",
51
+ "cc-exp",
52
+ "cc-exp-month",
53
+ "cc-exp-year"
54
+ ];
55
+ function autocompleteTokens(autocomplete) {
56
+ if (!autocomplete) return [];
57
+ return autocomplete.toLowerCase().split(/\s+/).map((token) => token.trim()).filter(Boolean);
58
+ }
59
+ function isSensitiveFieldMetadata(metadata) {
60
+ if (!metadata) return false;
61
+ if (metadata.sensitiveValue) return true;
62
+ const inputType = metadata.inputType?.toLowerCase();
63
+ if (inputType === "password" || inputType === "hidden") {
64
+ return true;
65
+ }
66
+ const sensitiveAutocompleteTokens = new Set(SENSITIVE_AUTOCOMPLETE_TOKENS);
67
+ return autocompleteTokens(metadata.autocomplete).some(
68
+ (token) => sensitiveAutocompleteTokens.has(token)
69
+ );
70
+ }
71
+ function redactValueForRecording(value, metadata) {
72
+ if (value === void 0) return void 0;
73
+ return isSensitiveFieldMetadata(metadata) ? REDACTED_VALUE : value;
74
+ }
75
+
76
+ // src/browser/action-highlight.ts
77
+ var HIGHLIGHT_STYLES = {
78
+ click: { outline: "3px solid rgba(229,57,53,0.8)", badge: "#e53935", marker: "crosshair" },
79
+ fill: { outline: "3px solid rgba(33,150,243,0.8)", badge: "#2196f3" },
80
+ type: { outline: "3px solid rgba(33,150,243,0.6)", badge: "#2196f3" },
81
+ select: { outline: "3px solid rgba(156,39,176,0.8)", badge: "#9c27b0" },
82
+ hover: { outline: "2px dashed rgba(158,158,158,0.5)", badge: "#9e9e9e" },
83
+ scroll: { outline: "none", badge: "#607d8b", marker: "arrow" },
84
+ navigate: { outline: "none", badge: "#4caf50" },
85
+ submit: { outline: "3px solid rgba(255,152,0,0.8)", badge: "#ff9800" },
86
+ "assert-pass": { outline: "3px solid rgba(76,175,80,0.8)", badge: "#4caf50", marker: "check" },
87
+ "assert-fail": { outline: "3px solid rgba(244,67,54,0.8)", badge: "#f44336", marker: "cross" },
88
+ evaluate: { outline: "none", badge: "#ffc107" },
89
+ focus: { outline: "3px dotted rgba(33,150,243,0.6)", badge: "#2196f3" }
90
+ };
91
+ function buildHighlightScript(options) {
92
+ const style = HIGHLIGHT_STYLES[options.kind];
93
+ const label = options.label ? options.label.slice(0, 80) : void 0;
94
+ const escapedLabel = label ? label.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\n/g, "\\n") : "";
95
+ return `(function() {
96
+ // Remove any existing highlight
97
+ var existing = document.getElementById('__bp-action-highlight');
98
+ if (existing) existing.remove();
99
+
100
+ var container = document.createElement('div');
101
+ container.id = '__bp-action-highlight';
102
+ container.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:99999;';
103
+
104
+ ${options.bbox ? `
105
+ // Element outline
106
+ var outline = document.createElement('div');
107
+ outline.style.cssText = 'position:fixed;' +
108
+ 'left:${options.bbox.x}px;top:${options.bbox.y}px;' +
109
+ 'width:${options.bbox.width}px;height:${options.bbox.height}px;' +
110
+ '${style.outline !== "none" ? `outline:${style.outline};outline-offset:-1px;` : ""}' +
111
+ 'pointer-events:none;box-sizing:border-box;';
112
+ container.appendChild(outline);
113
+ ` : ""}
114
+
115
+ ${options.point && style.marker === "crosshair" ? `
116
+ // Crosshair at click point
117
+ var hLine = document.createElement('div');
118
+ hLine.style.cssText = 'position:fixed;left:${options.point.x - 12}px;top:${options.point.y}px;' +
119
+ 'width:24px;height:2px;background:${style.badge};pointer-events:none;';
120
+ var vLine = document.createElement('div');
121
+ vLine.style.cssText = 'position:fixed;left:${options.point.x}px;top:${options.point.y - 12}px;' +
122
+ 'width:2px;height:24px;background:${style.badge};pointer-events:none;';
123
+ // Dot at center
124
+ var dot = document.createElement('div');
125
+ dot.style.cssText = 'position:fixed;left:${options.point.x - 4}px;top:${options.point.y - 4}px;' +
126
+ 'width:8px;height:8px;border-radius:50%;background:${style.badge};pointer-events:none;';
127
+ container.appendChild(hLine);
128
+ container.appendChild(vLine);
129
+ container.appendChild(dot);
130
+ ` : ""}
131
+
132
+ ${label ? `
133
+ // Badge with label
134
+ var badge = document.createElement('div');
135
+ badge.style.cssText = 'position:fixed;' +
136
+ ${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;'"} +
137
+ 'background:${style.badge};color:white;padding:4px 8px;' +
138
+ 'font-family:monospace;font-size:12px;font-weight:bold;' +
139
+ 'border-radius:3px;white-space:nowrap;max-width:400px;overflow:hidden;text-overflow:ellipsis;' +
140
+ 'pointer-events:none;';
141
+ badge.textContent = '${escapedLabel}';
142
+ container.appendChild(badge);
143
+ ` : ""}
144
+
145
+ ${style.marker === "check" && options.bbox ? `
146
+ // Checkmark
147
+ var check = document.createElement('div');
148
+ check.style.cssText = 'position:fixed;left:${options.bbox.x + options.bbox.width / 2 - 10}px;' +
149
+ 'top:${options.bbox.y + options.bbox.height / 2 - 10}px;' +
150
+ 'width:20px;height:20px;font-size:18px;color:${style.badge};pointer-events:none;text-align:center;line-height:20px;';
151
+ check.textContent = '\\u2713';
152
+ container.appendChild(check);
153
+ ` : ""}
154
+
155
+ ${style.marker === "cross" && options.bbox ? `
156
+ // Cross mark
157
+ var cross = document.createElement('div');
158
+ cross.style.cssText = 'position:fixed;left:${options.bbox.x + options.bbox.width / 2 - 10}px;' +
159
+ 'top:${options.bbox.y + options.bbox.height / 2 - 10}px;' +
160
+ 'width:20px;height:20px;font-size:18px;color:${style.badge};pointer-events:none;text-align:center;line-height:20px;font-weight:bold;';
161
+ cross.textContent = '\\u2717';
162
+ container.appendChild(cross);
163
+ ` : ""}
164
+
165
+ document.body.appendChild(container);
166
+ window.__bpRemoveActionHighlight = function() {
167
+ var el = document.getElementById('__bp-action-highlight');
168
+ if (el) el.remove();
169
+ delete window.__bpRemoveActionHighlight;
170
+ };
171
+ })();`;
172
+ }
173
+ async function injectActionHighlight(page, options) {
174
+ try {
175
+ await page.evaluate(buildHighlightScript(options));
176
+ } catch {
177
+ }
178
+ }
179
+ async function removeActionHighlight(page) {
180
+ try {
181
+ await page.evaluate(`(function() {
182
+ if (window.__bpRemoveActionHighlight) {
183
+ window.__bpRemoveActionHighlight();
184
+ }
185
+ })()`);
186
+ } catch {
187
+ }
188
+ }
189
+ function stepToHighlightKind(step) {
190
+ switch (step.action) {
191
+ case "click":
192
+ return "click";
193
+ case "fill":
194
+ return "fill";
195
+ case "type":
196
+ return "type";
197
+ case "select":
198
+ return "select";
199
+ case "hover":
200
+ return "hover";
201
+ case "scroll":
202
+ return "scroll";
203
+ case "goto":
204
+ return "navigate";
205
+ case "submit":
206
+ return "submit";
207
+ case "focus":
208
+ return "focus";
209
+ case "evaluate":
210
+ case "press":
211
+ case "shortcut":
212
+ return "evaluate";
213
+ case "assertVisible":
214
+ case "assertExists":
215
+ case "assertText":
216
+ case "assertUrl":
217
+ case "assertValue":
218
+ return step.success ? "assert-pass" : "assert-fail";
219
+ // Observation-only actions — no highlight
220
+ case "wait":
221
+ case "snapshot":
222
+ case "forms":
223
+ case "text":
224
+ case "screenshot":
225
+ case "newTab":
226
+ case "closeTab":
227
+ case "switchFrame":
228
+ case "switchToMain":
229
+ return null;
230
+ default:
231
+ return null;
232
+ }
233
+ }
234
+ function getHighlightLabel(step, result, targetMetadata) {
235
+ switch (step.action) {
236
+ case "fill":
237
+ case "type":
238
+ return typeof step.value === "string" ? `"${redactValueForRecording(step.value, targetMetadata)}"` : void 0;
239
+ case "select":
240
+ return redactValueForRecording(
241
+ typeof step.value === "string" ? step.value : void 0,
242
+ targetMetadata
243
+ );
244
+ case "goto":
245
+ return step.url;
246
+ case "evaluate":
247
+ return "JS";
248
+ case "press":
249
+ return step.key;
250
+ case "shortcut":
251
+ return step.combo;
252
+ case "assertText":
253
+ case "assertUrl":
254
+ case "assertValue":
255
+ case "assertVisible":
256
+ case "assertExists":
257
+ return result.success ? "\u2713" : "\u2717";
258
+ default:
259
+ return void 0;
260
+ }
261
+ }
262
+
29
263
  // src/browser/actionability.ts
30
264
  var ActionabilityError = class extends Error {
31
265
  failureType;
@@ -328,6 +562,13 @@ var CDPError = class extends Error {
328
562
 
329
563
  // src/actions/executor.ts
330
564
  var DEFAULT_TIMEOUT = 3e4;
565
+ var DEFAULT_RECORDING_SKIP_ACTIONS = [
566
+ "wait",
567
+ "snapshot",
568
+ "forms",
569
+ "text",
570
+ "screenshot"
571
+ ];
331
572
  function classifyFailure(error) {
332
573
  if (error instanceof ElementNotFoundError) {
333
574
  return { reason: "missing" };
@@ -407,6 +648,9 @@ var BatchExecutor = class {
407
648
  const { timeout = DEFAULT_TIMEOUT, onFail = "stop" } = options;
408
649
  const results = [];
409
650
  const startTime = Date.now();
651
+ const recording = options.record ? this.createRecordingContext(options.record) : null;
652
+ const startUrl = recording ? await this.getPageUrlSafe() : "";
653
+ let stoppedAtIndex;
410
654
  for (let i = 0; i < steps.length; i++) {
411
655
  const step = steps[i];
412
656
  const stepStart = Date.now();
@@ -419,8 +663,9 @@ var BatchExecutor = class {
419
663
  await new Promise((resolve) => setTimeout(resolve, retryDelay));
420
664
  }
421
665
  try {
666
+ this.page.resetLastActionPosition();
422
667
  const result = await this.executeStep(step, timeout);
423
- results.push({
668
+ const stepResult = {
424
669
  index: i,
425
670
  action: step.action,
426
671
  selector: step.selector,
@@ -428,8 +673,15 @@ var BatchExecutor = class {
428
673
  success: true,
429
674
  durationMs: Date.now() - stepStart,
430
675
  result: result.value,
431
- text: result.text
432
- });
676
+ text: result.text,
677
+ timestamp: Date.now(),
678
+ coordinates: this.page.getLastActionCoordinates() ?? void 0,
679
+ boundingBox: this.page.getLastActionBoundingBox() ?? void 0
680
+ };
681
+ if (recording && !recording.skipActions.has(step.action)) {
682
+ await this.captureRecordingFrame(step, stepResult, recording);
683
+ }
684
+ results.push(stepResult);
433
685
  succeeded = true;
434
686
  break;
435
687
  } catch (error) {
@@ -450,7 +702,7 @@ var BatchExecutor = class {
450
702
  } catch {
451
703
  }
452
704
  }
453
- results.push({
705
+ const failedResult = {
454
706
  index: i,
455
707
  action: step.action,
456
708
  selector: step.selector,
@@ -460,24 +712,176 @@ var BatchExecutor = class {
460
712
  hints,
461
713
  failureReason: reason,
462
714
  coveringElement,
463
- suggestion: getSuggestion(reason)
464
- });
715
+ suggestion: getSuggestion(reason),
716
+ timestamp: Date.now()
717
+ };
718
+ if (recording && !recording.skipActions.has(step.action)) {
719
+ await this.captureRecordingFrame(step, failedResult, recording);
720
+ }
721
+ results.push(failedResult);
465
722
  if (onFail === "stop" && !step.optional) {
466
- return {
467
- success: false,
468
- stoppedAtIndex: i,
469
- steps: results,
470
- totalDurationMs: Date.now() - startTime
471
- };
723
+ stoppedAtIndex = i;
724
+ break;
472
725
  }
473
726
  }
474
727
  }
475
- const allSuccess = results.every((r) => r.success || steps[r.index]?.optional);
728
+ const totalDurationMs = Date.now() - startTime;
729
+ const allSuccess = stoppedAtIndex === void 0 && results.every((result) => result.success || steps[result.index]?.optional);
730
+ let recordingManifest;
731
+ if (recording) {
732
+ recordingManifest = await this.writeRecordingManifest(
733
+ recording,
734
+ startTime,
735
+ startUrl,
736
+ allSuccess
737
+ );
738
+ }
476
739
  return {
477
740
  success: allSuccess,
741
+ stoppedAtIndex,
478
742
  steps: results,
479
- totalDurationMs: Date.now() - startTime
743
+ totalDurationMs,
744
+ recordingManifest
745
+ };
746
+ }
747
+ createRecordingContext(record) {
748
+ const baseDir = record.outputDir ?? (0, import_node_path.join)(process.cwd(), ".browser-pilot");
749
+ const screenshotDir = (0, import_node_path.join)(baseDir, "screenshots");
750
+ const manifestPath = (0, import_node_path.join)(baseDir, "recording.json");
751
+ let existingFrames = [];
752
+ try {
753
+ const existing = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
754
+ if (existing.frames && Array.isArray(existing.frames)) {
755
+ existingFrames = existing.frames;
756
+ }
757
+ } catch {
758
+ }
759
+ fs.mkdirSync(screenshotDir, { recursive: true });
760
+ return {
761
+ baseDir,
762
+ screenshotDir,
763
+ sessionId: record.sessionId ?? this.page.targetId,
764
+ frames: existingFrames,
765
+ format: record.format ?? "webp",
766
+ quality: Math.max(0, Math.min(100, record.quality ?? 40)),
767
+ highlights: record.highlights !== false,
768
+ skipActions: new Set(record.skipActions ?? DEFAULT_RECORDING_SKIP_ACTIONS)
769
+ };
770
+ }
771
+ async getPageUrlSafe() {
772
+ try {
773
+ return await this.page.url();
774
+ } catch {
775
+ return "";
776
+ }
777
+ }
778
+ /**
779
+ * Capture a recording screenshot frame with optional highlight overlay
780
+ */
781
+ async captureRecordingFrame(step, stepResult, recording) {
782
+ const targetMetadata = this.page.getLastActionTargetMetadata();
783
+ let highlightInjected = false;
784
+ try {
785
+ const ts = Date.now();
786
+ const seq = String(recording.frames.length + 1).padStart(4, "0");
787
+ const filename = `${seq}-${ts}-${stepResult.action}.${recording.format}`;
788
+ const filepath = (0, import_node_path.join)(recording.screenshotDir, filename);
789
+ if (recording.highlights) {
790
+ const kind = stepToHighlightKind(stepResult);
791
+ if (kind) {
792
+ await injectActionHighlight(this.page, {
793
+ kind,
794
+ bbox: stepResult.boundingBox,
795
+ point: stepResult.coordinates,
796
+ label: getHighlightLabel(step, stepResult, targetMetadata)
797
+ });
798
+ highlightInjected = true;
799
+ }
800
+ }
801
+ const base64 = await this.page.screenshot({
802
+ format: recording.format,
803
+ quality: recording.quality
804
+ });
805
+ const buffer = Buffer.from(base64, "base64");
806
+ fs.writeFileSync(filepath, buffer);
807
+ stepResult.screenshotPath = filepath;
808
+ let pageUrl;
809
+ let pageTitle;
810
+ try {
811
+ pageUrl = await this.page.url();
812
+ pageTitle = await this.page.title();
813
+ } catch {
814
+ }
815
+ recording.frames.push({
816
+ seq: recording.frames.length + 1,
817
+ timestamp: ts,
818
+ action: stepResult.action,
819
+ selector: stepResult.selectorUsed ?? (Array.isArray(step.selector) ? step.selector[0] : step.selector),
820
+ value: redactValueForRecording(
821
+ typeof step.value === "string" ? step.value : void 0,
822
+ targetMetadata
823
+ ),
824
+ url: step.url,
825
+ coordinates: stepResult.coordinates,
826
+ boundingBox: stepResult.boundingBox,
827
+ success: stepResult.success,
828
+ durationMs: stepResult.durationMs,
829
+ error: stepResult.error,
830
+ screenshot: filename,
831
+ pageUrl,
832
+ pageTitle
833
+ });
834
+ } catch {
835
+ } finally {
836
+ if (recording.highlights || highlightInjected) {
837
+ await removeActionHighlight(this.page);
838
+ }
839
+ }
840
+ }
841
+ /**
842
+ * Write recording manifest to disk
843
+ */
844
+ async writeRecordingManifest(recording, startTime, startUrl, success) {
845
+ let endUrl = startUrl;
846
+ let viewport = { width: 1280, height: 720 };
847
+ try {
848
+ endUrl = await this.page.url();
849
+ } catch {
850
+ }
851
+ try {
852
+ const metrics = await this.page.cdpClient.send("Page.getLayoutMetrics");
853
+ viewport = {
854
+ width: metrics.cssVisualViewport.clientWidth,
855
+ height: metrics.cssVisualViewport.clientHeight
856
+ };
857
+ } catch {
858
+ }
859
+ const manifestPath = (0, import_node_path.join)(recording.baseDir, "recording.json");
860
+ let recordedAt = new Date(startTime).toISOString();
861
+ let originalStartUrl = startUrl;
862
+ try {
863
+ const existing = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
864
+ if (existing.recordedAt) recordedAt = existing.recordedAt;
865
+ if (existing.startUrl) originalStartUrl = existing.startUrl;
866
+ } catch {
867
+ }
868
+ const firstFrameTime = recording.frames[0]?.timestamp ?? startTime;
869
+ const totalDurationMs = Date.now() - Math.min(firstFrameTime, startTime);
870
+ const manifest = {
871
+ version: 1,
872
+ recordedAt,
873
+ sessionId: recording.sessionId,
874
+ startUrl: originalStartUrl,
875
+ endUrl,
876
+ viewport,
877
+ format: recording.format,
878
+ quality: recording.quality,
879
+ totalDurationMs,
880
+ success,
881
+ frames: recording.frames
480
882
  };
883
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
884
+ return manifestPath;
481
885
  }
482
886
  /**
483
887
  * Execute a single step
@@ -1,6 +1,6 @@
1
- import { H as Page, S as Step, B as BatchOptions, b as BatchResult } from './types-BXMGFtnB.cjs';
2
- export { A as ActionType, ai as FailureReason, c as StepResult } from './types-BXMGFtnB.cjs';
3
- import './client-DRqxBdHv.cjs';
1
+ import { J as Page, S as Step, B as BatchOptions, b as BatchResult } from './types-BSoh5v1Y.cjs';
2
+ export { A as ActionType, aj as FailureReason, c as RecordOptions, d as StepResult } from './types-BSoh5v1Y.cjs';
3
+ import './client-Ck2nQksT.cjs';
4
4
 
5
5
  /**
6
6
  * Batch action executor
@@ -13,6 +13,16 @@ declare class BatchExecutor {
13
13
  * Execute a batch of steps
14
14
  */
15
15
  execute(steps: Step[], options?: BatchOptions): Promise<BatchResult>;
16
+ private createRecordingContext;
17
+ private getPageUrlSafe;
18
+ /**
19
+ * Capture a recording screenshot frame with optional highlight overlay
20
+ */
21
+ private captureRecordingFrame;
22
+ /**
23
+ * Write recording manifest to disk
24
+ */
25
+ private writeRecordingManifest;
16
26
  /**
17
27
  * Execute a single step
18
28
  */
package/dist/actions.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { H as Page, S as Step, B as BatchOptions, b as BatchResult } from './types-CzgQjai9.js';
2
- export { A as ActionType, ai as FailureReason, c as StepResult } from './types-CzgQjai9.js';
3
- import './client-DRqxBdHv.js';
1
+ import { J as Page, S as Step, B as BatchOptions, b as BatchResult } from './types-CjT0vClo.js';
2
+ export { A as ActionType, aj as FailureReason, c as RecordOptions, d as StepResult } from './types-CjT0vClo.js';
3
+ import './client-Ck2nQksT.js';
4
4
 
5
5
  /**
6
6
  * Batch action executor
@@ -13,6 +13,16 @@ declare class BatchExecutor {
13
13
  * Execute a batch of steps
14
14
  */
15
15
  execute(steps: Step[], options?: BatchOptions): Promise<BatchResult>;
16
+ private createRecordingContext;
17
+ private getPageUrlSafe;
18
+ /**
19
+ * Capture a recording screenshot frame with optional highlight overlay
20
+ */
21
+ private captureRecordingFrame;
22
+ /**
23
+ * Write recording manifest to disk
24
+ */
25
+ private writeRecordingManifest;
16
26
  /**
17
27
  * Execute a single step
18
28
  */
package/dist/actions.mjs CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  BatchExecutor,
3
3
  addBatchToPage,
4
4
  validateSteps
5
- } from "./chunk-A2ZRAEO3.mjs";
5
+ } from "./chunk-XMJABKCF.mjs";
6
6
  import "./chunk-JXAUPHZM.mjs";
7
7
  export {
8
8
  BatchExecutor,