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 +59 -3
- package/dist/actions.cjs +418 -14
- package/dist/actions.d.cts +13 -3
- package/dist/actions.d.ts +13 -3
- package/dist/actions.mjs +1 -1
- package/dist/browser-LZTEHUDI.mjs +9 -0
- package/dist/browser.cjs +600 -20
- package/dist/browser.d.cts +12 -3
- package/dist/browser.d.ts +12 -3
- package/dist/browser.mjs +3 -3
- package/dist/cdp.cjs +31 -2
- package/dist/cdp.d.cts +1 -1
- package/dist/cdp.d.ts +1 -1
- package/dist/cdp.mjs +3 -1
- package/dist/chunk-7NDR6V7S.mjs +7788 -0
- package/dist/{chunk-VDAMDOS6.mjs → chunk-IN5HPAPB.mjs} +147 -7
- package/dist/{chunk-HP6R3W32.mjs → chunk-KIFB526Y.mjs} +44 -2
- package/dist/chunk-LUGLEMVR.mjs +11 -0
- package/dist/chunk-SPSZZH22.mjs +308 -0
- package/dist/{chunk-A2ZRAEO3.mjs → chunk-XMJABKCF.mjs} +408 -14
- package/dist/cli.mjs +1063 -7746
- package/dist/client-3AFV2IAF.mjs +10 -0
- package/dist/{client-DRqxBdHv.d.ts → client-Ck2nQksT.d.cts} +8 -6
- package/dist/{client-DRqxBdHv.d.cts → client-Ck2nQksT.d.ts} +8 -6
- package/dist/index.cjs +600 -20
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.mjs +3 -3
- package/dist/transport-WHEBAZUP.mjs +83 -0
- package/dist/{types-CzgQjai9.d.ts → types-BSoh5v1Y.d.cts} +62 -2
- package/dist/{types-BXMGFtnB.d.cts → types-CjT0vClo.d.ts} +62 -2
- package/package.json +2 -2
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
|
-
-
|
|
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/
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
467
|
-
|
|
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
|
|
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
|
|
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
|
package/dist/actions.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { A as ActionType,
|
|
3
|
-
import './client-
|
|
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 {
|
|
2
|
-
export { A as ActionType,
|
|
3
|
-
import './client-
|
|
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
|
*/
|