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 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`, `--status`) |
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] button "Submit"
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 --status 200
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 press <key> # keyboard \u2014 returns updated snapshot
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 <ref> <filepath> # file input upload
568
- bp auth <user> <pass> # HTTP Basic Auth
569
- bp frame # list iframes
570
- bp frame 1 # eval in iframe context
571
- bp frame 0 # back to top frame
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "browser-pilot-cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "CLI tool to control your browser via Chrome DevTools Protocol",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- });
@@ -1,4 +0,0 @@
1
- {
2
- "status": "passed",
3
- "failedTests": []
4
- }