browser-pilot-cli 0.1.0 → 0.1.2

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.
Files changed (3) hide show
  1. package/README.md +102 -58
  2. package/dist/cli.js +125 -10
  3. package/package.json +5 -2
package/README.md CHANGED
@@ -1,89 +1,95 @@
1
1
  # browser-pilot
2
2
 
3
- CLI tool that gives LLM agents control of your real browser — with your logins, cookies, and extensions intact.
3
+ Give your AI agent control of your real browser — with your logins, cookies, and extensions intact. No extension needed.
4
4
 
5
5
  ```bash
6
- bp open https://github.com # navigate — returns interactive elements
7
- bp click 3 # click [3] — returns updated page
8
- bp type 5 "hello" --submit # type + Enter — returns updated page
9
- bp eval "document.title" # run any JavaScript
6
+ npm install -g browser-pilot-cli
10
7
  ```
11
8
 
9
+ ## Agent Setup
10
+
11
+ ### 1. Enable Chrome remote debugging (one-time)
12
+
13
+ Open `chrome://inspect/#remote-debugging` in Chrome (144+) and click Allow. No command-line flags, no restart needed.
14
+
15
+ > Chrome 136 disabled the old `--remote-debugging-port` flag for security. Chrome 144 introduced this new UI toggle as the replacement — browser-pilot uses this.
16
+
17
+ ### 2. Install the plugin for your agent
18
+
19
+ **Claude Code:**
20
+ ```
21
+ /plugin marketplace add https://github.com/relixiaobo/browser-pilot.git
22
+ /plugin install browser-pilot@browser-pilot-marketplace
23
+ ```
24
+
25
+ **Codex CLI:**
26
+ ```bash
27
+ npx skills add relixiaobo/browser-pilot
28
+ ```
29
+
30
+ **OpenClaw:**
31
+ ```bash
32
+ cp -r plugin/skills/browser-pilot ~/.agents/skills/
33
+ ```
34
+
35
+ **Cursor / VS Code Copilot:**
36
+ ```bash
37
+ npx skills add relixiaobo/browser-pilot
38
+ ```
39
+
40
+ ### 3. Use it
41
+
42
+ Just tell your agent what you want to do:
43
+
44
+ - "Open GitHub and check my notifications"
45
+ - "Go to Hacker News and summarize the top 5 posts"
46
+ - "Fill out the form on this page"
47
+
48
+ The agent will use `bp` commands automatically. Your real login sessions are preserved — no need to re-authenticate.
49
+
50
+ ## Why browser-pilot?
51
+
52
+ - **No extension required** — Uses Chrome 144's native remote debugging toggle, not the Extension Debugger API
53
+ - **Real login sessions** — Operates your actual browser profile. Cookies, extensions, logins all intact
54
+ - **CLI-native** — Any agent with bash access can use it. No MCP protocol, no SDK integration needed
55
+ - **Auto-snapshot** — Every action returns page state with numbered `[ref]` elements, so the agent always knows what's on screen
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
58
+
12
59
  ## Comparison
13
60
 
14
61
  | | browser-pilot | Playwright MCP | Chrome DevTools MCP | browser-use |
15
62
  |---|---|---|---|---|
16
63
  | **Interface** | CLI (bash) | MCP protocol | MCP protocol | Python SDK |
17
64
  | **Login session reuse** | Yes | No | Depends | Yes |
65
+ | **Extension required** | No | No | No | No |
18
66
  | **Element refs** | Numbered (accessibility tree) | Named refs (ARIA) | CSS selectors | Numbered (DOM) |
19
67
  | **Auto-snapshot after action** | Yes | Yes | No | Yes |
20
68
  | **Network interception** | Yes (block/mock/headers) | Yes | Yes | No |
21
- | **Drag and drop** | No | Yes | Yes | No |
22
- | **Mobile emulation** | No | Yes | Yes | Yes |
23
69
  | **Multi-browser** | Chromium-only | Chromium + Firefox + WebKit | Chromium-only | Chromium-only |
24
- | **Connection model** | Daemon (one-time Allow) | MCP server | MCP server | Python process |
25
70
  | **Dialog auto-handling** | Yes | Yes | No | Yes |
26
71
  | **JSON output** | Default | MCP structured | MCP structured | Python objects |
27
- | **Visual indicator** | Pulsing glow | None | None | None |
28
72
  | **File upload** | Auto-detect input | Yes | No | Yes |
29
- | **Autonomous agent** | No (tool, not agent) | No (tool) | No (tool) | Yes (plans + executes) |
30
-
31
- **browser-pilot is best when:**
32
- - You need your existing login sessions (paywalled content, internal tools)
33
- - Your LLM has bash access but no MCP support
34
- - You want every action to return page state automatically
35
-
36
- **Use Playwright MCP when:**
37
- - You need multi-browser or mobile emulation
38
- - Your LLM supports MCP natively
39
- - You don't need existing login sessions
40
-
41
- **Use browser-use when:**
42
- - You want an autonomous agent that plans and executes multi-step tasks
43
- - You're building in Python
44
-
45
- ## Quick Start
46
-
47
- ```bash
48
- # Install
49
- npm install -g browser-pilot
50
-
51
- # Enable debugging in Chrome (one-time)
52
- # Open chrome://inspect/#remote-debugging → toggle ON
53
-
54
- # Connect (click Allow in Chrome's dialog)
55
- bp connect
56
-
57
- # Use
58
- bp open https://github.com
59
- bp click 3
60
- bp type 5 "hello" --submit
61
- bp eval "document.title"
62
- bp screenshot page.png
63
- bp disconnect
64
- ```
65
73
 
66
74
  ## How It Works
67
75
 
68
76
  ```
69
- LLM (bash tool)
77
+ Agent (bash tool)
70
78
  │ bp open / bp click / bp eval ...
71
79
 
72
80
  CLI Process ──── HTTP/Unix Socket ──── Daemon Process (persistent)
73
81
 
74
- │ WebSocket (CDP, one-time Allow)
82
+ │ WebSocket (CDP)
75
83
 
76
84
  Chrome (your browser, your profile)
77
85
  ├── Your windows (untouched)
78
- └── Pilot window (bp operates here)
86
+ └── Pilot window (agent operates here)
79
87
  ```
80
88
 
81
- The daemon maintains a single CDP WebSocket connection. Chrome's "Allow" dialog appears once per session. A pulsing blue glow around the Pilot window indicates the agent is active.
89
+ The daemon maintains a single CDP WebSocket connection. A pulsing blue glow around the Pilot window indicates the agent is active.
82
90
 
83
91
  ## Commands
84
92
 
85
- Run `bp --help` for full details including workflow, refs, and eval examples.
86
-
87
93
  ### Core Loop
88
94
 
89
95
  | Command | Returns | Description |
@@ -122,7 +128,7 @@ Popup windows (target="_blank", window.open) are auto-detected. Run `bp tabs` to
122
128
  | `bp net` | List recent requests (`--url`, `--method`, `--status`, `--type`) |
123
129
  | `bp net show <id>` | Full request/response details (`--save <file>`) |
124
130
  | `bp net block <pattern>` | Block requests matching URL pattern |
125
- | `bp net mock <pattern>` | Mock responses (`--body`, `--file`, `--status`) |
131
+ | `bp net mock <pattern>` | Mock responses (`--body`, `--file`) |
126
132
  | `bp net headers <pattern> <header...>` | Add/override request headers |
127
133
  | `bp net rules` | List active interception rules |
128
134
  | `bp net remove [id]` | Remove rule(s) (`--all`) |
@@ -144,13 +150,18 @@ Action commands return a snapshot of interactive elements, each with a `[ref]` n
144
150
 
145
151
  ```
146
152
  [1] link "Home"
147
- [2] textbox "Search"
148
- [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">
149
160
  ```
150
161
 
151
162
  Use the number in subsequent commands: `bp click 1`, `bp type 2 "hello"`.
152
163
 
153
- 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.
154
165
 
155
166
  ## Output
156
167
 
@@ -192,6 +203,22 @@ bp click 5 # click "Search by image"
192
203
  bp upload ~/Downloads/photo.jpg # auto-finds file input, triggers upload
193
204
  ```
194
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
+
195
222
  ## Network Interception
196
223
 
197
224
  Monitor, block, and mock HTTP requests:
@@ -208,7 +235,7 @@ bp net block "*ads*"
208
235
 
209
236
  # Mock API responses
210
237
  bp net mock "*api/data*" --body '{"ok":true}'
211
- bp net mock "*api/users*" --file mock.json --status 200
238
+ bp net mock "*api/users*" --file mock.json
212
239
 
213
240
  # Override request headers
214
241
  bp net headers "*api*" "Authorization:Bearer test123"
@@ -220,11 +247,28 @@ bp net remove --all # clear all rules
220
247
  bp net clear # clear captured request log
221
248
  ```
222
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
+
223
267
  ## Requirements
224
268
 
225
- - Chrome / Chromium / Edge / Brave (any Chromium-based browser)
269
+ - Chrome 144+ / Edge / Brave (any Chromium-based browser)
226
270
  - Node.js >= 18
227
- - Chrome remote debugging enabled (`chrome://inspect/#remote-debugging`)
271
+ - Remote debugging enabled (`chrome://inspect/#remote-debugging`)
228
272
 
229
273
  ## License
230
274
 
package/dist/cli.js CHANGED
@@ -245,6 +245,14 @@ var SET_VALUE = `function(text, clear) {
245
245
  this.dispatchEvent(new Event('input', {bubbles:true}));
246
246
  this.dispatchEvent(new Event('change', {bubbles:true}));
247
247
  }`;
248
+ var IS_CONTENTEDITABLE = `function() {
249
+ return this.isContentEditable && !(this instanceof HTMLInputElement) && !(this instanceof HTMLTextAreaElement);
250
+ }`;
251
+ var CONTENTEDITABLE_CLEAR = `function() {
252
+ this.focus();
253
+ document.execCommand('selectAll');
254
+ document.execCommand('delete');
255
+ }`;
248
256
  var PAGE_INFO = `JSON.stringify({title:document.title,url:location.href})`;
249
257
  var PAGE_DIMENSIONS = `JSON.stringify({
250
258
  width: Math.max(document.documentElement.scrollWidth, document.documentElement.clientWidth),
@@ -431,6 +439,17 @@ var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
431
439
  "menuitemradio",
432
440
  "tab"
433
441
  ]);
442
+ var ALLOW_EMPTY_NAME = /* @__PURE__ */ new Set([
443
+ "textbox",
444
+ "searchbox",
445
+ "combobox",
446
+ "listbox",
447
+ "checkbox",
448
+ "radio",
449
+ "spinbutton",
450
+ "slider",
451
+ "switch"
452
+ ]);
434
453
  function saveRefs(targetId, entries) {
435
454
  writeFileSync2(REFS_FILE, JSON.stringify({ targetId, entries }), { mode: 384 });
436
455
  }
@@ -466,16 +485,20 @@ async function takeSnapshot(transport, sessionId, targetId, limit = 50) {
466
485
  if (!node) return;
467
486
  if (!node.ignored) {
468
487
  const role = node.role?.value;
469
- if (role && INTERACTIVE_ROLES.has(role) && node.backendDOMNodeId !== void 0) {
470
- const props = Object.fromEntries(
471
- (node.properties || []).map((p) => [p.name, p.value?.value])
472
- );
488
+ const props = Object.fromEntries(
489
+ (node.properties || []).map((p) => [p.name, p.value?.value])
490
+ );
491
+ const isEditable = props.editable === "richtext";
492
+ const isInteractive = role && (INTERACTIVE_ROLES.has(role) || isEditable);
493
+ if (isInteractive && node.backendDOMNodeId !== void 0) {
473
494
  const name = node.name?.value || "";
474
495
  const value = node.value?.value;
475
- if (!props.disabled && (name || value) && refs.length < limit) {
496
+ const effectiveRole = isEditable && role === "generic" ? "textbox" : role;
497
+ const hasIdentity = name || value || ALLOW_EMPTY_NAME.has(effectiveRole) || isEditable;
498
+ if (!props.disabled && hasIdentity && refs.length < limit) {
476
499
  const checked = props.checked === "true" || props.checked === true ? true : void 0;
477
- refs.push({ backendNodeId: node.backendDOMNodeId, role, name });
478
- elements.push({ ref: refs.length, backendNodeId: node.backendDOMNodeId, role, name, value, checked });
500
+ refs.push({ backendNodeId: node.backendDOMNodeId, role: effectiveRole, name });
501
+ elements.push({ ref: refs.length, backendNodeId: node.backendDOMNodeId, role: effectiveRole, name, value, checked });
479
502
  }
480
503
  }
481
504
  }
@@ -747,11 +770,36 @@ program.command("type <ref> <text>").description("Type text into element and ret
747
770
  await withPilot(async ({ transport, sessionId, state }) => {
748
771
  const objectId = await resolveTarget(transport, sessionId, ref, state.activeTargetId);
749
772
  try {
750
- await transport.send("Runtime.callFunctionOn", {
773
+ const { result: ceResult } = await transport.send("Runtime.callFunctionOn", {
751
774
  objectId,
752
- functionDeclaration: SET_VALUE,
753
- arguments: [{ value: text }, { value: !!opts.clear }]
775
+ functionDeclaration: IS_CONTENTEDITABLE,
776
+ returnByValue: true
754
777
  }, sessionId);
778
+ if (ceResult.value) {
779
+ if (opts.clear) {
780
+ await transport.send("Runtime.callFunctionOn", {
781
+ objectId,
782
+ functionDeclaration: CONTENTEDITABLE_CLEAR
783
+ }, sessionId);
784
+ } else {
785
+ await transport.send("Runtime.callFunctionOn", {
786
+ objectId,
787
+ functionDeclaration: `function() {
788
+ this.focus();
789
+ const sel = window.getSelection();
790
+ sel.selectAllChildren(this);
791
+ sel.collapseToEnd();
792
+ }`
793
+ }, sessionId);
794
+ }
795
+ await transport.send("Input.insertText", { text }, sessionId);
796
+ } else {
797
+ await transport.send("Runtime.callFunctionOn", {
798
+ objectId,
799
+ functionDeclaration: SET_VALUE,
800
+ arguments: [{ value: text }, { value: !!opts.clear }]
801
+ }, sessionId);
802
+ }
755
803
  if (opts.submit) await dispatchKey(transport, sessionId, "Enter");
756
804
  } finally {
757
805
  await transport.send("Runtime.releaseObject", { objectId }, sessionId).catch(() => {
@@ -760,6 +808,73 @@ program.command("type <ref> <text>").description("Type text into element and ret
760
808
  emitSnapshot(await snap(transport, sessionId, state.activeTargetId, limit));
761
809
  });
762
810
  }));
811
+ async function typeViaKeyboard(t, sid, text) {
812
+ for (const char of text) {
813
+ if (char === "\n") {
814
+ await dispatchKey(t, sid, "Enter");
815
+ } else if (char === " ") {
816
+ await dispatchKey(t, sid, "Tab");
817
+ } else if (char.charCodeAt(0) >= 32 && char.charCodeAt(0) <= 126) {
818
+ const code = char === " " ? "Space" : `Key${char.toUpperCase()}`;
819
+ const keyCode = char.toUpperCase().charCodeAt(0);
820
+ await t.send("Input.dispatchKeyEvent", { type: "keyDown", key: char, code, windowsVirtualKeyCode: keyCode, text: char }, sid);
821
+ await t.send("Input.dispatchKeyEvent", { type: "keyUp", key: char, code, windowsVirtualKeyCode: keyCode }, sid);
822
+ } else {
823
+ await t.send("Input.insertText", { text: char }, sid);
824
+ }
825
+ }
826
+ }
827
+ 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", `
828
+ Unlike 'type', this does not target a specific element. It sends real
829
+ keyboard events to whatever is currently focused \u2014 works with canvas-based
830
+ editors (Google Docs, Google Sheets, Figma) that don't expose DOM inputs.
831
+
832
+ Use --click to focus an element before typing (sends a real CDP mouse click).
833
+
834
+ Examples:
835
+ bp keyboard "hello world"
836
+ bp keyboard "new content" --clear
837
+ bp keyboard "search query" --submit
838
+ bp keyboard "Hello Docs!" --click ".kix-appview-editor"
839
+ bp keyboard "slow typing" --delay 50`).action(action(async (text, opts) => {
840
+ const limit = parseLimit(opts.limit);
841
+ await withPilot(async ({ transport, sessionId, state }) => {
842
+ if (opts.click) {
843
+ const { result } = await transport.send("Runtime.evaluate", {
844
+ 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}})())`,
845
+ returnByValue: true
846
+ }, sessionId);
847
+ const coords = result.value ? JSON.parse(result.value) : null;
848
+ if (!coords) throw new Error(`Element not found: ${opts.click}`);
849
+ await dispatchClick(transport, sessionId, coords.x, coords.y);
850
+ await new Promise((r) => setTimeout(r, 300));
851
+ }
852
+ if (opts.clear) {
853
+ await dispatchKey(transport, sessionId, "Meta+a");
854
+ await dispatchKey(transport, sessionId, "Delete");
855
+ }
856
+ if (opts.delay) {
857
+ const delay = parseInt(opts.delay, 10);
858
+ for (const char of text) {
859
+ if (char === "\n") {
860
+ await dispatchKey(transport, sessionId, "Enter");
861
+ } else if (char.charCodeAt(0) >= 32 && char.charCodeAt(0) <= 126) {
862
+ const code = char === " " ? "Space" : `Key${char.toUpperCase()}`;
863
+ const keyCode = char.toUpperCase().charCodeAt(0);
864
+ await transport.send("Input.dispatchKeyEvent", { type: "keyDown", key: char, code, windowsVirtualKeyCode: keyCode, text: char }, sessionId);
865
+ await transport.send("Input.dispatchKeyEvent", { type: "keyUp", key: char, code, windowsVirtualKeyCode: keyCode }, sessionId);
866
+ } else {
867
+ await transport.send("Input.insertText", { text: char }, sessionId);
868
+ }
869
+ await new Promise((r) => setTimeout(r, delay));
870
+ }
871
+ } else {
872
+ await typeViaKeyboard(transport, sessionId, text);
873
+ }
874
+ if (opts.submit) await dispatchKey(transport, sessionId, "Enter");
875
+ emitSnapshot(await snap(transport, sessionId, state.activeTargetId, limit));
876
+ });
877
+ }));
763
878
  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) => {
764
879
  const limit = parseLimit(opts.limit);
765
880
  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.0",
3
+ "version": "0.1.2",
4
4
  "description": "CLI tool to control your browser via Chrome DevTools Protocol",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,13 +11,16 @@
11
11
  "build": "tsup",
12
12
  "dev": "tsup --watch",
13
13
  "start": "node dist/cli.js",
14
- "test": "bash tests/run.sh"
14
+ "test": "tsup && npx playwright test --project core --project compat --project network",
15
+ "test:integration": "tsup && npx playwright test --project integration",
16
+ "test:all": "tsup && npx playwright test"
15
17
  },
16
18
  "dependencies": {
17
19
  "commander": "^13.1.0",
18
20
  "ws": "^8.18.0"
19
21
  },
20
22
  "devDependencies": {
23
+ "@playwright/test": "^1.59.1",
21
24
  "@types/node": "^22.0.0",
22
25
  "@types/ws": "^8.5.0",
23
26
  "tsup": "^8.0.0",