browser-pilot-cli 0.1.1 → 0.1.3
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 +44 -5
- package/dist/cli.js +79 -6
- package/package.json +1 -1
- package/playwright.config.ts +0 -36
- package/test-results/.last-run.json +0 -4
package/README.md
CHANGED
|
@@ -54,6 +54,7 @@ The agent will use `bp` commands automatically. Your real login sessions are pre
|
|
|
54
54
|
- **CLI-native** — Any agent with bash access can use it. No MCP protocol, no SDK integration needed
|
|
55
55
|
- **Auto-snapshot** — Every action returns page state with numbered `[ref]` elements, so the agent always knows what's on screen
|
|
56
56
|
- **Lightweight** — 78KB npm package. No bundled Chromium (unlike Playwright's 400MB+)
|
|
57
|
+
- **Rich editor support** — Works with contenteditable editors (Draft.js, ProseMirror, Quill, Slate) and Shadow DOM elements out of the box
|
|
57
58
|
|
|
58
59
|
## Comparison
|
|
59
60
|
|
|
@@ -127,7 +128,7 @@ Popup windows (target="_blank", window.open) are auto-detected. Run `bp tabs` to
|
|
|
127
128
|
| `bp net` | List recent requests (`--url`, `--method`, `--status`, `--type`) |
|
|
128
129
|
| `bp net show <id>` | Full request/response details (`--save <file>`) |
|
|
129
130
|
| `bp net block <pattern>` | Block requests matching URL pattern |
|
|
130
|
-
| `bp net mock <pattern>` | Mock responses (`--body`, `--file
|
|
131
|
+
| `bp net mock <pattern>` | Mock responses (`--body`, `--file`) |
|
|
131
132
|
| `bp net headers <pattern> <header...>` | Add/override request headers |
|
|
132
133
|
| `bp net rules` | List active interception rules |
|
|
133
134
|
| `bp net remove [id]` | Remove rule(s) (`--all`) |
|
|
@@ -149,13 +150,18 @@ Action commands return a snapshot of interactive elements, each with a `[ref]` n
|
|
|
149
150
|
|
|
150
151
|
```
|
|
151
152
|
[1] link "Home"
|
|
152
|
-
[2] textbox "Search"
|
|
153
|
-
[3]
|
|
153
|
+
[2] textbox "Search" ← <input>, <textarea>, or contenteditable
|
|
154
|
+
[3] textbox "" ← unnamed input (still interactive)
|
|
155
|
+
[4] combobox "" ← <select> dropdown
|
|
156
|
+
[5] spinbutton "Quantity" ← <input type="number">
|
|
157
|
+
[6] button "Submit"
|
|
158
|
+
[7] checkbox "Agree" checked
|
|
159
|
+
[8] slider "Volume" ← <input type="range">
|
|
154
160
|
```
|
|
155
161
|
|
|
156
162
|
Use the number in subsequent commands: `bp click 1`, `bp type 2 "hello"`.
|
|
157
163
|
|
|
158
|
-
Refs are scoped to the current page — they refresh automatically after every action.
|
|
164
|
+
Refs are scoped to the current page — they refresh automatically after every action. Elements inside Shadow DOM are included automatically.
|
|
159
165
|
|
|
160
166
|
## Output
|
|
161
167
|
|
|
@@ -197,6 +203,22 @@ bp click 5 # click "Search by image"
|
|
|
197
203
|
bp upload ~/Downloads/photo.jpg # auto-finds file input, triggers upload
|
|
198
204
|
```
|
|
199
205
|
|
|
206
|
+
## Rich Text Editors & Shadow DOM
|
|
207
|
+
|
|
208
|
+
`bp type` works with contenteditable-based editors (Draft.js, ProseMirror, Quill, Slate, Lexical). They appear as `textbox` in snapshots:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
bp type 3 "new content" --clear # replace content in a rich text editor
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Shadow DOM elements are traversed automatically — no special commands needed. Elements inside open shadow roots (even deeply nested) appear in snapshots and can be clicked/typed normally.
|
|
215
|
+
|
|
216
|
+
For `<select>` dropdowns (shown as `combobox`), use `bp eval`:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
bp eval 'document.querySelector("select").value = "opt2"; document.querySelector("select").dispatchEvent(new Event("change",{bubbles:true}))'
|
|
220
|
+
```
|
|
221
|
+
|
|
200
222
|
## Network Interception
|
|
201
223
|
|
|
202
224
|
Monitor, block, and mock HTTP requests:
|
|
@@ -213,7 +235,7 @@ bp net block "*ads*"
|
|
|
213
235
|
|
|
214
236
|
# Mock API responses
|
|
215
237
|
bp net mock "*api/data*" --body '{"ok":true}'
|
|
216
|
-
bp net mock "*api/users*" --file mock.json
|
|
238
|
+
bp net mock "*api/users*" --file mock.json
|
|
217
239
|
|
|
218
240
|
# Override request headers
|
|
219
241
|
bp net headers "*api*" "Authorization:Bearer test123"
|
|
@@ -225,6 +247,23 @@ bp net remove --all # clear all rules
|
|
|
225
247
|
bp net clear # clear captured request log
|
|
226
248
|
```
|
|
227
249
|
|
|
250
|
+
## Testing
|
|
251
|
+
|
|
252
|
+
121 tests across 4 Playwright projects, adapted from Playwright/Puppeteer test fixtures and real-world sites:
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
npm test # core + compat + network (105 tests, ~2min)
|
|
256
|
+
npm run test:integration # real-site tests against the-internet.herokuapp.com
|
|
257
|
+
npm run test:all # all 121 tests
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
| Project | Tests | Coverage |
|
|
261
|
+
|---------|-------|----------|
|
|
262
|
+
| **core** | 41 | lifecycle, nav, click, type, press, eval, screenshot, pdf, cookies, frames, upload, auth, tabs, dialogs |
|
|
263
|
+
| **compat** | 46 | contenteditable (5 variants), Shadow DOM (3 levels), input types, scrollable, overlay, select |
|
|
264
|
+
| **network** | 19 | request monitoring, block, mock, headers, rule management |
|
|
265
|
+
| **integration** | 15 | checkboxes, dropdown, key presses, dynamic controls, Shadow DOM, nested frames, TinyMCE, large DOM |
|
|
266
|
+
|
|
228
267
|
## Requirements
|
|
229
268
|
|
|
230
269
|
- Chrome 144+ / Edge / Brave (any Chromium-based browser)
|
package/dist/cli.js
CHANGED
|
@@ -549,7 +549,8 @@ Workflow:
|
|
|
549
549
|
bp open <url> # navigate \u2014 returns snapshot with [ref] numbers
|
|
550
550
|
bp click <ref> # interact \u2014 returns updated snapshot
|
|
551
551
|
bp type <ref> <text> # input text \u2014 returns updated snapshot
|
|
552
|
-
bp
|
|
552
|
+
bp keyboard <text> # type via keyboard events (Google Docs etc.)
|
|
553
|
+
bp press <key> # press key \u2014 returns updated snapshot
|
|
553
554
|
bp eval <js> # run JavaScript (escape hatch for anything)
|
|
554
555
|
|
|
555
556
|
Refs:
|
|
@@ -563,12 +564,17 @@ Output:
|
|
|
563
564
|
Actions return: {"ok":true, "title":"...", "url":"...", "elements":[...]}
|
|
564
565
|
Errors return: {"ok":false, "error":"...", "hint":"..."}
|
|
565
566
|
|
|
567
|
+
Canvas editors (Google Docs, Sheets, Figma):
|
|
568
|
+
bp keyboard "text" --click ".editor" # click to focus, then type
|
|
569
|
+
bp keyboard "text" --clear # select all + delete, then type
|
|
570
|
+
bp press Meta+b # toggle bold (works in Docs)
|
|
571
|
+
|
|
566
572
|
Edge cases:
|
|
567
|
-
bp upload <
|
|
568
|
-
bp auth <user> <pass>
|
|
569
|
-
bp frame
|
|
570
|
-
bp frame 1
|
|
571
|
-
bp frame 0
|
|
573
|
+
bp upload <filepath> # file input upload (auto-detect)
|
|
574
|
+
bp auth <user> <pass> # HTTP Basic Auth
|
|
575
|
+
bp frame # list iframes
|
|
576
|
+
bp frame 1 # eval in iframe context
|
|
577
|
+
bp frame 0 # back to top frame
|
|
572
578
|
Dialogs (alert/confirm) are auto-handled by the daemon.
|
|
573
579
|
|
|
574
580
|
Eval (replaces scroll, back, forward, extract, etc.):
|
|
@@ -808,6 +814,73 @@ program.command("type <ref> <text>").description("Type text into element and ret
|
|
|
808
814
|
emitSnapshot(await snap(transport, sessionId, state.activeTargetId, limit));
|
|
809
815
|
});
|
|
810
816
|
}));
|
|
817
|
+
async function typeViaKeyboard(t, sid, text) {
|
|
818
|
+
for (const char of text) {
|
|
819
|
+
if (char === "\n") {
|
|
820
|
+
await dispatchKey(t, sid, "Enter");
|
|
821
|
+
} else if (char === " ") {
|
|
822
|
+
await dispatchKey(t, sid, "Tab");
|
|
823
|
+
} else if (char.charCodeAt(0) >= 32 && char.charCodeAt(0) <= 126) {
|
|
824
|
+
const code = char === " " ? "Space" : `Key${char.toUpperCase()}`;
|
|
825
|
+
const keyCode = char.toUpperCase().charCodeAt(0);
|
|
826
|
+
await t.send("Input.dispatchKeyEvent", { type: "keyDown", key: char, code, windowsVirtualKeyCode: keyCode, text: char }, sid);
|
|
827
|
+
await t.send("Input.dispatchKeyEvent", { type: "keyUp", key: char, code, windowsVirtualKeyCode: keyCode }, sid);
|
|
828
|
+
} else {
|
|
829
|
+
await t.send("Input.insertText", { text: char }, sid);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
program.command("keyboard <text>").description("Type text via keyboard events (for canvas editors like Google Docs)").option("-c, --clear", "select all + delete before typing").option("-s, --submit", "press Enter after typing").option("-d, --delay <ms>", "delay between keystrokes in ms").option("--click <selector>", "click element by CSS selector first to focus it").option("-l, --limit <n>", "max elements in snapshot", "50").addHelpText("after", `
|
|
834
|
+
Unlike 'type', this does not target a specific element. It sends real
|
|
835
|
+
keyboard events to whatever is currently focused \u2014 works with canvas-based
|
|
836
|
+
editors (Google Docs, Google Sheets, Figma) that don't expose DOM inputs.
|
|
837
|
+
|
|
838
|
+
Use --click to focus an element before typing (sends a real CDP mouse click).
|
|
839
|
+
|
|
840
|
+
Examples:
|
|
841
|
+
bp keyboard "hello world"
|
|
842
|
+
bp keyboard "new content" --clear
|
|
843
|
+
bp keyboard "search query" --submit
|
|
844
|
+
bp keyboard "Hello Docs!" --click ".kix-appview-editor"
|
|
845
|
+
bp keyboard "slow typing" --delay 50`).action(action(async (text, opts) => {
|
|
846
|
+
const limit = parseLimit(opts.limit);
|
|
847
|
+
await withPilot(async ({ transport, sessionId, state }) => {
|
|
848
|
+
if (opts.click) {
|
|
849
|
+
const { result } = await transport.send("Runtime.evaluate", {
|
|
850
|
+
expression: `JSON.stringify((function(){var el=document.querySelector(${JSON.stringify(opts.click)});if(!el)return null;el.scrollIntoView({block:'center'});var r=el.getBoundingClientRect();return{x:r.x+r.width/2,y:r.y+r.height/2}})())`,
|
|
851
|
+
returnByValue: true
|
|
852
|
+
}, sessionId);
|
|
853
|
+
const coords = result.value ? JSON.parse(result.value) : null;
|
|
854
|
+
if (!coords) throw new Error(`Element not found: ${opts.click}`);
|
|
855
|
+
await dispatchClick(transport, sessionId, coords.x, coords.y);
|
|
856
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
857
|
+
}
|
|
858
|
+
if (opts.clear) {
|
|
859
|
+
await dispatchKey(transport, sessionId, "Meta+a");
|
|
860
|
+
await dispatchKey(transport, sessionId, "Delete");
|
|
861
|
+
}
|
|
862
|
+
if (opts.delay) {
|
|
863
|
+
const delay = parseInt(opts.delay, 10);
|
|
864
|
+
for (const char of text) {
|
|
865
|
+
if (char === "\n") {
|
|
866
|
+
await dispatchKey(transport, sessionId, "Enter");
|
|
867
|
+
} else if (char.charCodeAt(0) >= 32 && char.charCodeAt(0) <= 126) {
|
|
868
|
+
const code = char === " " ? "Space" : `Key${char.toUpperCase()}`;
|
|
869
|
+
const keyCode = char.toUpperCase().charCodeAt(0);
|
|
870
|
+
await transport.send("Input.dispatchKeyEvent", { type: "keyDown", key: char, code, windowsVirtualKeyCode: keyCode, text: char }, sessionId);
|
|
871
|
+
await transport.send("Input.dispatchKeyEvent", { type: "keyUp", key: char, code, windowsVirtualKeyCode: keyCode }, sessionId);
|
|
872
|
+
} else {
|
|
873
|
+
await transport.send("Input.insertText", { text: char }, sessionId);
|
|
874
|
+
}
|
|
875
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
876
|
+
}
|
|
877
|
+
} else {
|
|
878
|
+
await typeViaKeyboard(transport, sessionId, text);
|
|
879
|
+
}
|
|
880
|
+
if (opts.submit) await dispatchKey(transport, sessionId, "Enter");
|
|
881
|
+
emitSnapshot(await snap(transport, sessionId, state.activeTargetId, limit));
|
|
882
|
+
});
|
|
883
|
+
}));
|
|
811
884
|
program.command("press <key>").description("Press key combo (e.g. Enter, Escape, Control+a) and return snapshot").option("-l, --limit <n>", "max elements in snapshot", "50").addHelpText("after", "\nKeys: Enter, Escape, Tab, Space, Backspace, Delete,\n ArrowUp, ArrowDown, ArrowLeft, ArrowRight,\n Home, End, PageUp, PageDown\nModifiers: Control (Ctrl), Shift, Alt, Meta (Cmd)\n\nExamples:\n bp press Enter\n bp press Escape\n bp press Control+a\n bp press Meta+c").action(action(async (key, opts) => {
|
|
812
885
|
const limit = parseLimit(opts.limit);
|
|
813
886
|
await withPilot(async ({ transport, sessionId, state }) => {
|
package/package.json
CHANGED
package/playwright.config.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from '@playwright/test';
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
testDir: './tests',
|
|
5
|
-
timeout: 30_000,
|
|
6
|
-
retries: 0,
|
|
7
|
-
workers: 1, // bp uses a single browser session — must be serial
|
|
8
|
-
globalSetup: './tests/global-setup.ts',
|
|
9
|
-
globalTeardown: './tests/global-teardown.ts',
|
|
10
|
-
use: {
|
|
11
|
-
baseURL: 'http://127.0.0.1:18274',
|
|
12
|
-
},
|
|
13
|
-
webServer: {
|
|
14
|
-
command: 'node tests/server.mjs 18274',
|
|
15
|
-
port: 18274,
|
|
16
|
-
reuseExistingServer: true,
|
|
17
|
-
},
|
|
18
|
-
projects: [
|
|
19
|
-
{
|
|
20
|
-
name: 'core',
|
|
21
|
-
testMatch: /core\.spec/,
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
name: 'compat',
|
|
25
|
-
testMatch: /fill|click|snapshot/,
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
name: 'network',
|
|
29
|
-
testMatch: /network\.spec/,
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
name: 'integration',
|
|
33
|
-
testMatch: /real-site\.spec/,
|
|
34
|
-
},
|
|
35
|
-
],
|
|
36
|
-
});
|