browser-pilot-cli 0.1.0 → 0.1.1

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
@@ -1,89 +1,94 @@
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
+
12
58
  ## Comparison
13
59
 
14
60
  | | browser-pilot | Playwright MCP | Chrome DevTools MCP | browser-use |
15
61
  |---|---|---|---|---|
16
62
  | **Interface** | CLI (bash) | MCP protocol | MCP protocol | Python SDK |
17
63
  | **Login session reuse** | Yes | No | Depends | Yes |
64
+ | **Extension required** | No | No | No | No |
18
65
  | **Element refs** | Numbered (accessibility tree) | Named refs (ARIA) | CSS selectors | Numbered (DOM) |
19
66
  | **Auto-snapshot after action** | Yes | Yes | No | Yes |
20
67
  | **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
68
  | **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
69
  | **Dialog auto-handling** | Yes | Yes | No | Yes |
26
70
  | **JSON output** | Default | MCP structured | MCP structured | Python objects |
27
- | **Visual indicator** | Pulsing glow | None | None | None |
28
71
  | **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
72
 
66
73
  ## How It Works
67
74
 
68
75
  ```
69
- LLM (bash tool)
76
+ Agent (bash tool)
70
77
  │ bp open / bp click / bp eval ...
71
78
 
72
79
  CLI Process ──── HTTP/Unix Socket ──── Daemon Process (persistent)
73
80
 
74
- │ WebSocket (CDP, one-time Allow)
81
+ │ WebSocket (CDP)
75
82
 
76
83
  Chrome (your browser, your profile)
77
84
  ├── Your windows (untouched)
78
- └── Pilot window (bp operates here)
85
+ └── Pilot window (agent operates here)
79
86
  ```
80
87
 
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.
88
+ The daemon maintains a single CDP WebSocket connection. A pulsing blue glow around the Pilot window indicates the agent is active.
82
89
 
83
90
  ## Commands
84
91
 
85
- Run `bp --help` for full details including workflow, refs, and eval examples.
86
-
87
92
  ### Core Loop
88
93
 
89
94
  | Command | Returns | Description |
@@ -222,9 +227,9 @@ bp net clear # clear captured request log
222
227
 
223
228
  ## Requirements
224
229
 
225
- - Chrome / Chromium / Edge / Brave (any Chromium-based browser)
230
+ - Chrome 144+ / Edge / Brave (any Chromium-based browser)
226
231
  - Node.js >= 18
227
- - Chrome remote debugging enabled (`chrome://inspect/#remote-debugging`)
232
+ - Remote debugging enabled (`chrome://inspect/#remote-debugging`)
228
233
 
229
234
  ## License
230
235
 
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(() => {
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.1",
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",
@@ -0,0 +1,36 @@
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
+ });
@@ -0,0 +1,4 @@
1
+ {
2
+ "status": "passed",
3
+ "failedTests": []
4
+ }