cdpilot 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.
- package/README.md +93 -83
- package/bin/{browserctl.js → cdpilot.js} +20 -17
- package/package.json +10 -21
- package/src/{browserctl.py → cdpilot.py} +187 -35
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
#
|
|
1
|
+
# cdpilot
|
|
2
2
|
|
|
3
3
|
> Zero-dependency browser automation from your terminal. One command, full control.
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/cdpilot)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
8
|
<!-- Demo GIF will be added after first release -->
|
|
@@ -10,27 +10,27 @@
|
|
|
10
10
|
## Quick Start
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
|
-
npx
|
|
14
|
-
npx
|
|
15
|
-
npx
|
|
13
|
+
npx cdpilot launch # Start browser with CDP
|
|
14
|
+
npx cdpilot go https://example.com
|
|
15
|
+
npx cdpilot shot # Take screenshot
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
No config files. No boilerplate. Just `npx` and go.
|
|
19
19
|
|
|
20
|
-
## Why
|
|
20
|
+
## Why cdpilot?
|
|
21
21
|
|
|
22
22
|
AI agents and developers need browser control that **just works**:
|
|
23
23
|
|
|
24
|
-
- **Zero config** — `npx
|
|
24
|
+
- **Zero config** — `npx cdpilot launch` starts an isolated browser session
|
|
25
25
|
- **Zero dependency** — No Puppeteer, no Playwright, no Selenium. Pure CDP over HTTP
|
|
26
26
|
- **40+ commands** — Navigate, click, type, screenshot, network, console, accessibility, and more
|
|
27
27
|
- **AI-agent friendly** — Designed for Claude, GPT, Gemini, and any LLM tool-use workflow
|
|
28
|
-
- **Isolated sessions** — Your personal browser stays untouched.
|
|
28
|
+
- **Isolated sessions** — Your personal browser stays untouched. cdpilot runs in its own profile
|
|
29
29
|
- **Privacy-first** — Everything runs locally. No data leaves your machine
|
|
30
30
|
|
|
31
31
|
### Why Brave?
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
cdpilot uses [Brave Browser](https://brave.com) as its engine. Here's why:
|
|
34
34
|
|
|
35
35
|
| Feature | Brave | Chrome | Why it matters |
|
|
36
36
|
|---------|-------|--------|---------------|
|
|
@@ -43,16 +43,16 @@ browserctl uses [Brave Browser](https://brave.com) as its engine. Here's why:
|
|
|
43
43
|
|
|
44
44
|
**TL;DR:** Brave = Chrome's power + built-in privacy + less bloat. Perfect for automation.
|
|
45
45
|
|
|
46
|
-
>
|
|
46
|
+
> cdpilot also works with Chrome and Chromium as fallback. Brave is recommended, not required.
|
|
47
47
|
|
|
48
48
|
## Installation
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
51
|
# Use directly (no install needed)
|
|
52
|
-
npx
|
|
52
|
+
npx cdpilot <command>
|
|
53
53
|
|
|
54
54
|
# Or install globally
|
|
55
|
-
npm i -g
|
|
55
|
+
npm i -g cdpilot
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
**Requirements:** Node.js 18+ and one of: Brave Browser, Google Chrome, or Chromium.
|
|
@@ -60,9 +60,9 @@ npm i -g browserctl
|
|
|
60
60
|
### First-time setup
|
|
61
61
|
|
|
62
62
|
```bash
|
|
63
|
-
npx
|
|
64
|
-
npx
|
|
65
|
-
npx
|
|
63
|
+
npx cdpilot setup # Auto-detect browser, create isolated profile
|
|
64
|
+
npx cdpilot launch # Start browser with CDP enabled
|
|
65
|
+
npx cdpilot status # Check connection
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
## Commands
|
|
@@ -70,126 +70,126 @@ npx browserctl status # Check connection
|
|
|
70
70
|
### Navigation & Content
|
|
71
71
|
|
|
72
72
|
```bash
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
73
|
+
cdpilot go <url> # Navigate to URL
|
|
74
|
+
cdpilot content # Get page text content
|
|
75
|
+
cdpilot html # Get page HTML
|
|
76
|
+
cdpilot shot [file] # Take screenshot (PNG)
|
|
77
|
+
cdpilot pdf [file] # Save page as PDF
|
|
78
78
|
```
|
|
79
79
|
|
|
80
80
|
### Interaction
|
|
81
81
|
|
|
82
82
|
```bash
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
83
|
+
cdpilot click <selector> # Click element
|
|
84
|
+
cdpilot type <selector> <text># Type into input
|
|
85
|
+
cdpilot fill <selector> <val> # Set input value (React-compatible)
|
|
86
|
+
cdpilot submit <form> # Submit form
|
|
87
|
+
cdpilot hover <selector> # Hover element
|
|
88
|
+
cdpilot keys <combo> # Keyboard shortcut (ctrl+a, enter, etc.)
|
|
89
|
+
cdpilot scroll-to <selector> # Scroll element into view
|
|
90
|
+
cdpilot drag <from> <to> # Drag and drop
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
### Debugging
|
|
94
94
|
|
|
95
95
|
```bash
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
cdpilot console [url] # Capture console logs
|
|
97
|
+
cdpilot network [url] # Monitor network requests
|
|
98
|
+
cdpilot debug [url] # Full diagnostic (console+network+perf+shot)
|
|
99
|
+
cdpilot perf # Performance metrics
|
|
100
|
+
cdpilot eval <js> # Execute JavaScript
|
|
101
101
|
```
|
|
102
102
|
|
|
103
103
|
### Tab Management
|
|
104
104
|
|
|
105
105
|
```bash
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
106
|
+
cdpilot tabs # List open tabs
|
|
107
|
+
cdpilot new-tab [url] # Open new tab
|
|
108
|
+
cdpilot switch-tab <id> # Switch to tab
|
|
109
|
+
cdpilot close-tab [id] # Close tab
|
|
110
|
+
cdpilot close # Close active tab
|
|
111
111
|
```
|
|
112
112
|
|
|
113
113
|
### Network Control
|
|
114
114
|
|
|
115
115
|
```bash
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
116
|
+
cdpilot throttle slow3g # Simulate slow 3G
|
|
117
|
+
cdpilot throttle fast3g # Simulate fast 3G
|
|
118
|
+
cdpilot throttle offline # Go offline
|
|
119
|
+
cdpilot throttle off # Back to normal
|
|
120
|
+
cdpilot proxy <url> # Set proxy
|
|
121
|
+
cdpilot proxy off # Remove proxy
|
|
122
122
|
```
|
|
123
123
|
|
|
124
124
|
### Request Interception
|
|
125
125
|
|
|
126
126
|
```bash
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
127
|
+
cdpilot intercept block <pattern> # Block requests
|
|
128
|
+
cdpilot intercept mock <pattern> <json-file> # Mock responses
|
|
129
|
+
cdpilot intercept headers <pattern> <header:value> # Add headers
|
|
130
|
+
cdpilot intercept list # List active rules
|
|
131
|
+
cdpilot intercept clear # Clear all rules
|
|
132
132
|
```
|
|
133
133
|
|
|
134
134
|
### Device Emulation
|
|
135
135
|
|
|
136
136
|
```bash
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
cdpilot emulate iphone # iPhone emulation
|
|
138
|
+
cdpilot emulate ipad # iPad emulation
|
|
139
|
+
cdpilot emulate android # Android emulation
|
|
140
|
+
cdpilot emulate reset # Back to desktop
|
|
141
141
|
```
|
|
142
142
|
|
|
143
143
|
### Geolocation
|
|
144
144
|
|
|
145
145
|
```bash
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
cdpilot geo istanbul # Set location to Istanbul
|
|
147
|
+
cdpilot geo london # Set location to London
|
|
148
|
+
cdpilot geo 41.01 28.97 # Custom coordinates
|
|
149
|
+
cdpilot geo off # Remove override
|
|
150
150
|
```
|
|
151
151
|
|
|
152
152
|
### Accessibility
|
|
153
153
|
|
|
154
154
|
```bash
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
155
|
+
cdpilot a11y # Full accessibility tree
|
|
156
|
+
cdpilot a11y summary # Quick summary
|
|
157
|
+
cdpilot a11y find <role> # Find elements by ARIA role
|
|
158
158
|
```
|
|
159
159
|
|
|
160
160
|
### Session Management
|
|
161
161
|
|
|
162
162
|
```bash
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
163
|
+
cdpilot session # Current session info
|
|
164
|
+
cdpilot sessions # List all sessions
|
|
165
|
+
cdpilot session-close [id] # Close session
|
|
166
166
|
```
|
|
167
167
|
|
|
168
168
|
### Advanced
|
|
169
169
|
|
|
170
170
|
```bash
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
171
|
+
cdpilot cookies [domain] # List cookies
|
|
172
|
+
cdpilot storage # localStorage contents
|
|
173
|
+
cdpilot upload <sel> <file> # Upload file to input
|
|
174
|
+
cdpilot multi-eval <js> # Execute JS in all tabs
|
|
175
|
+
cdpilot headless [on|off] # Toggle headless mode
|
|
176
|
+
cdpilot frame list # List iframes
|
|
177
|
+
cdpilot dialog auto-accept # Auto-accept dialogs
|
|
178
|
+
cdpilot permission grant geo # Grant geolocation
|
|
179
179
|
```
|
|
180
180
|
|
|
181
181
|
## Use with AI Agents
|
|
182
182
|
|
|
183
|
-
|
|
183
|
+
cdpilot is designed to be called by AI agents as a tool:
|
|
184
184
|
|
|
185
185
|
### Claude Code (MCP)
|
|
186
186
|
|
|
187
187
|
```json
|
|
188
188
|
{
|
|
189
189
|
"mcpServers": {
|
|
190
|
-
"
|
|
190
|
+
"cdpilot": {
|
|
191
191
|
"command": "npx",
|
|
192
|
-
"args": ["
|
|
192
|
+
"args": ["cdpilot", "mcp"]
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
195
|
}
|
|
@@ -211,7 +211,7 @@ browserctl is designed to be called by AI agents as a tool:
|
|
|
211
211
|
|
|
212
212
|
```python
|
|
213
213
|
import subprocess
|
|
214
|
-
result = subprocess.run(["npx", "
|
|
214
|
+
result = subprocess.run(["npx", "cdpilot", "go", url], capture_output=True, text=True)
|
|
215
215
|
print(result.stdout)
|
|
216
216
|
```
|
|
217
217
|
|
|
@@ -221,14 +221,14 @@ print(result.stdout)
|
|
|
221
221
|
|----------|---------|-------------|
|
|
222
222
|
| `CDP_PORT` | `9222` | CDP debugging port |
|
|
223
223
|
| `CHROME_BIN` | Auto-detect | Browser binary path |
|
|
224
|
-
| `
|
|
224
|
+
| `CDPILOT_PROFILE` | `~/.cdpilot/profile` | Isolated browser profile |
|
|
225
225
|
| `BROWSER_SESSION` | Auto | Session identifier |
|
|
226
226
|
|
|
227
227
|
## How It Works
|
|
228
228
|
|
|
229
229
|
```
|
|
230
230
|
┌─────────────┐ HTTP/WebSocket ┌──────────────┐
|
|
231
|
-
│
|
|
231
|
+
│ cdpilot │ ◄──────────────────────► │ Brave/Chrome │
|
|
232
232
|
│ (CLI) │ Chrome DevTools │ (CDP mode) │
|
|
233
233
|
└─────────────┘ Protocol └──────────────┘
|
|
234
234
|
│ │
|
|
@@ -242,7 +242,7 @@ print(result.stdout)
|
|
|
242
242
|
|
|
243
243
|
## Comparison
|
|
244
244
|
|
|
245
|
-
| Feature |
|
|
245
|
+
| Feature | cdpilot | Puppeteer | Playwright | Selenium |
|
|
246
246
|
|---------|-----------|-----------|------------|----------|
|
|
247
247
|
| Install size | **~50KB** | 400MB+ | 200MB+ | 100MB+ |
|
|
248
248
|
| Dependencies | **0** | 50+ | 30+ | Java + drivers |
|
|
@@ -254,18 +254,28 @@ print(result.stdout)
|
|
|
254
254
|
|
|
255
255
|
## Monetization / Pro (Coming Soon)
|
|
256
256
|
|
|
257
|
-
|
|
257
|
+
cdpilot CLI is and will always be **free and open source** (MIT).
|
|
258
258
|
|
|
259
259
|
Future paid offerings:
|
|
260
|
-
- **
|
|
260
|
+
- **cdpilot cloud** — Remote browser instances, no local browser needed
|
|
261
261
|
- **Team dashboard** — Shared sessions, audit logs, usage analytics
|
|
262
262
|
- **Priority support** — Direct help for enterprise integrations
|
|
263
263
|
|
|
264
|
+
## Security
|
|
265
|
+
|
|
266
|
+
- **Isolated browser profile** — cdpilot runs in `~/.cdpilot/profile`, separate from your daily browser. Your cookies, passwords, and history are never exposed.
|
|
267
|
+
- **No arbitrary file access** — MCP screenshot filenames are sanitized and restricted to the screenshots directory. Path traversal is blocked.
|
|
268
|
+
- **Safe CSS selectors** — All selectors passed to `querySelector` are JSON-escaped to prevent injection.
|
|
269
|
+
- **No network exposure** — CDP listens on `127.0.0.1` only. Remote connections are not possible by default.
|
|
270
|
+
- **No dependencies** — Zero npm/Python runtime dependencies means zero supply-chain attack surface.
|
|
271
|
+
|
|
272
|
+
Found a vulnerability? Please email the maintainer directly instead of opening a public issue.
|
|
273
|
+
|
|
264
274
|
## Contributing
|
|
265
275
|
|
|
266
276
|
```bash
|
|
267
|
-
git clone https://github.com/
|
|
268
|
-
cd
|
|
277
|
+
git clone https://github.com/mehmetnadir/cdpilot.git
|
|
278
|
+
cd cdpilot
|
|
269
279
|
npm install
|
|
270
280
|
npm test
|
|
271
281
|
```
|
|
@@ -279,5 +289,5 @@ MIT — do whatever you want.
|
|
|
279
289
|
---
|
|
280
290
|
|
|
281
291
|
<p align="center">
|
|
282
|
-
Built with the <a href="https://github.com/
|
|
292
|
+
Built with the <a href="https://github.com/mehmetnadir/cdpilot">cdpilot</a> mindset: one tool, one job, done right.
|
|
283
293
|
</p>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
* Entry point: detects Python, finds browser, delegates to
|
|
4
|
+
* cdpilot — Zero-dependency browser automation CLI
|
|
5
|
+
* Entry point: detects Python, finds browser, delegates to cdpilot.py
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const { execSync, spawn } = require('child_process');
|
|
@@ -10,7 +10,7 @@ const path = require('path');
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const os = require('os');
|
|
12
12
|
|
|
13
|
-
const SCRIPT = path.join(__dirname, '..', 'src', '
|
|
13
|
+
const SCRIPT = path.join(__dirname, '..', 'src', 'cdpilot.py');
|
|
14
14
|
const VERSION = require('../package.json').version;
|
|
15
15
|
|
|
16
16
|
// ── Browser Detection ──
|
|
@@ -85,11 +85,11 @@ function findPython() {
|
|
|
85
85
|
|
|
86
86
|
function runSetup() {
|
|
87
87
|
const browser = findBrowser();
|
|
88
|
-
const profileDir = process.env.
|
|
89
|
-
|| path.join(os.homedir(), '.
|
|
88
|
+
const profileDir = process.env.CDPILOT_PROFILE
|
|
89
|
+
|| path.join(os.homedir(), '.cdpilot', 'profile');
|
|
90
90
|
const port = process.env.CDP_PORT || '9222';
|
|
91
91
|
|
|
92
|
-
console.log('\n
|
|
92
|
+
console.log('\n cdpilot setup\n');
|
|
93
93
|
console.log(` Browser: ${browser || '❌ Not found'}`);
|
|
94
94
|
console.log(` Profile: ${profileDir}`);
|
|
95
95
|
console.log(` CDP Port: ${port}`);
|
|
@@ -116,14 +116,14 @@ function runSetup() {
|
|
|
116
116
|
console.log(`\n ✓ Profile exists: ${profileDir}`);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
console.log(' ✓ Setup complete! Run:
|
|
119
|
+
console.log(' ✓ Setup complete! Run: cdpilot launch\n');
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
// ── Status Command ──
|
|
123
123
|
|
|
124
124
|
function runStatus() {
|
|
125
125
|
const port = process.env.CDP_PORT || '9222';
|
|
126
|
-
console.log(`\n
|
|
126
|
+
console.log(`\n cdpilot status (port ${port})\n`);
|
|
127
127
|
|
|
128
128
|
try {
|
|
129
129
|
const http = require('http');
|
|
@@ -144,12 +144,12 @@ function runStatus() {
|
|
|
144
144
|
});
|
|
145
145
|
req.on('error', () => {
|
|
146
146
|
console.log(' ❌ No browser connected on this port.');
|
|
147
|
-
console.log(' Run:
|
|
147
|
+
console.log(' Run: cdpilot launch\n');
|
|
148
148
|
});
|
|
149
149
|
req.on('timeout', () => {
|
|
150
150
|
req.destroy();
|
|
151
151
|
console.log(' ❌ Connection timeout.');
|
|
152
|
-
console.log(' Run:
|
|
152
|
+
console.log(' Run: cdpilot launch\n');
|
|
153
153
|
});
|
|
154
154
|
} catch {
|
|
155
155
|
console.log(' ❌ Could not check status.\n');
|
|
@@ -159,17 +159,17 @@ function runStatus() {
|
|
|
159
159
|
// ── Version ──
|
|
160
160
|
|
|
161
161
|
function showVersion() {
|
|
162
|
-
console.log(`
|
|
162
|
+
console.log(`cdpilot v${VERSION}`);
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
// ── Help ──
|
|
166
166
|
|
|
167
167
|
function showHelp() {
|
|
168
168
|
console.log(`
|
|
169
|
-
|
|
169
|
+
cdpilot v${VERSION} — Zero-dependency browser automation
|
|
170
170
|
|
|
171
171
|
USAGE
|
|
172
|
-
|
|
172
|
+
cdpilot <command> [args]
|
|
173
173
|
|
|
174
174
|
SETUP
|
|
175
175
|
setup Auto-detect browser, create isolated profile
|
|
@@ -203,7 +203,10 @@ function showHelp() {
|
|
|
203
203
|
new-tab [url] Open new tab
|
|
204
204
|
close-tab [id] Close tab
|
|
205
205
|
|
|
206
|
-
|
|
206
|
+
AI AGENT
|
|
207
|
+
mcp Start MCP server (stdin/stdout JSON-RPC)
|
|
208
|
+
|
|
209
|
+
More: https://github.com/mehmetnadir/cdpilot#commands
|
|
207
210
|
`);
|
|
208
211
|
}
|
|
209
212
|
|
|
@@ -239,14 +242,14 @@ if (cmd === 'status') {
|
|
|
239
242
|
}
|
|
240
243
|
|
|
241
244
|
const browser = findBrowser();
|
|
242
|
-
const profileDir = process.env.
|
|
243
|
-
|| path.join(os.homedir(), '.
|
|
245
|
+
const profileDir = process.env.CDPILOT_PROFILE
|
|
246
|
+
|| path.join(os.homedir(), '.cdpilot', 'profile');
|
|
244
247
|
const port = process.env.CDP_PORT || '9222';
|
|
245
248
|
|
|
246
249
|
const env = {
|
|
247
250
|
...process.env,
|
|
248
251
|
CDP_PORT: port,
|
|
249
|
-
|
|
252
|
+
CDPILOT_PROFILE: profileDir,
|
|
250
253
|
};
|
|
251
254
|
|
|
252
255
|
if (browser && !process.env.CHROME_BIN) {
|
package/package.json
CHANGED
|
@@ -1,43 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cdpilot",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Zero-dependency browser automation from your terminal. One command, full control.",
|
|
5
5
|
"bin": {
|
|
6
|
-
"
|
|
7
|
-
"
|
|
6
|
+
"cdpilot": "./bin/cdpilot.js",
|
|
7
|
+
"bctl": "./bin/cdpilot.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"test": "node test/test.js",
|
|
11
|
-
"start": "node bin/
|
|
11
|
+
"start": "node bin/cdpilot.js"
|
|
12
12
|
},
|
|
13
13
|
"keywords": [
|
|
14
|
-
"browser",
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"chrome-devtools-protocol",
|
|
18
|
-
"cli",
|
|
19
|
-
"devtools",
|
|
20
|
-
"headless",
|
|
21
|
-
"screenshot",
|
|
22
|
-
"testing",
|
|
23
|
-
"web-scraping",
|
|
24
|
-
"ai-agent",
|
|
25
|
-
"mcp",
|
|
26
|
-
"claude",
|
|
27
|
-
"brave"
|
|
14
|
+
"browser", "automation", "cdp", "chrome-devtools-protocol", "cli",
|
|
15
|
+
"devtools", "headless", "screenshot", "testing", "web-scraping",
|
|
16
|
+
"ai-agent", "mcp", "claude", "brave"
|
|
28
17
|
],
|
|
29
|
-
"author": "",
|
|
18
|
+
"author": "Mehmet Nadir",
|
|
30
19
|
"license": "MIT",
|
|
31
20
|
"repository": {
|
|
32
21
|
"type": "git",
|
|
33
|
-
"url": "https://github.com/
|
|
22
|
+
"url": "https://github.com/mehmetnadir/cdpilot"
|
|
34
23
|
},
|
|
35
24
|
"engines": {
|
|
36
25
|
"node": ">=18.0.0"
|
|
37
26
|
},
|
|
38
27
|
"files": [
|
|
39
28
|
"bin/",
|
|
40
|
-
"src/",
|
|
29
|
+
"src/cdpilot.py",
|
|
41
30
|
"README.md",
|
|
42
31
|
"LICENSE"
|
|
43
32
|
]
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
|
|
3
|
+
cdpilot — Zero-dependency browser automation from your terminal.
|
|
4
4
|
|
|
5
5
|
Controls any Chromium-based browser (Brave, Chrome, Chromium) via the
|
|
6
6
|
Chrome DevTools Protocol (CDP). No Puppeteer, no Playwright, no Selenium.
|
|
7
7
|
|
|
8
8
|
Usage:
|
|
9
|
-
|
|
9
|
+
cdpilot <command> [arguments]
|
|
10
10
|
|
|
11
11
|
Environment:
|
|
12
12
|
CDP_PORT CDP debugging port (default: 9222)
|
|
13
13
|
CHROME_BIN Browser binary path (auto-detected if not set)
|
|
14
|
-
|
|
14
|
+
CDPILOT_PROFILE Isolated browser profile directory
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
-
__version__ = "0.1.
|
|
17
|
+
__version__ = "0.1.2"
|
|
18
18
|
|
|
19
19
|
import asyncio
|
|
20
20
|
import json
|
|
@@ -27,13 +27,13 @@ import subprocess
|
|
|
27
27
|
import shutil
|
|
28
28
|
|
|
29
29
|
# ─── Session Configuration ───
|
|
30
|
-
#
|
|
30
|
+
# cdpilot runs in its own Chrome instance on the configured CDP port.
|
|
31
31
|
# The user's existing Chrome/browser session is not affected.
|
|
32
32
|
|
|
33
33
|
CDP_PORT = int(os.environ.get("CDP_PORT", "9222"))
|
|
34
34
|
CDP_BASE = f"http://127.0.0.1:{CDP_PORT}"
|
|
35
35
|
CHROME_BIN = os.environ.get("CHROME_BIN")
|
|
36
|
-
PROFILE_DIR = os.environ.get("
|
|
36
|
+
PROFILE_DIR = os.environ.get("CDPILOT_PROFILE", os.path.expanduser("~/.cdpilot/profile"))
|
|
37
37
|
SCREENSHOT_DIR = "/tmp"
|
|
38
38
|
|
|
39
39
|
DEV_EXTENSIONS_FILE = os.path.join(PROFILE_DIR, 'dev-extensions.json')
|
|
@@ -52,7 +52,7 @@ def _get_session_id():
|
|
|
52
52
|
if sid:
|
|
53
53
|
return sid
|
|
54
54
|
# Default session — all commands share the same window
|
|
55
|
-
return "
|
|
55
|
+
return "cdpilot-default"
|
|
56
56
|
|
|
57
57
|
def _load_sessions():
|
|
58
58
|
"""Read the session registry."""
|
|
@@ -93,11 +93,11 @@ _current_session_id = None # lazy init
|
|
|
93
93
|
|
|
94
94
|
GLOW_CSS = """
|
|
95
95
|
(function() {
|
|
96
|
-
if (document.getElementById('
|
|
96
|
+
if (document.getElementById('cdpilot-glow')) return 'already active';
|
|
97
97
|
const style = document.createElement('style');
|
|
98
|
-
style.id = '
|
|
98
|
+
style.id = 'cdpilot-glow';
|
|
99
99
|
style.textContent = `
|
|
100
|
-
@keyframes
|
|
100
|
+
@keyframes cdpilot-pulse {
|
|
101
101
|
0%, 100% { box-shadow: inset 0 0 20px 4px rgba(34, 197, 94, 0.25), inset 0 0 60px 8px rgba(34, 197, 94, 0.08); }
|
|
102
102
|
50% { box-shadow: inset 0 0 30px 6px rgba(34, 197, 94, 0.35), inset 0 0 80px 12px rgba(34, 197, 94, 0.12); }
|
|
103
103
|
}
|
|
@@ -107,7 +107,7 @@ GLOW_CSS = """
|
|
|
107
107
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
108
108
|
pointer-events: none;
|
|
109
109
|
z-index: 2147483647;
|
|
110
|
-
animation:
|
|
110
|
+
animation: cdpilot-pulse 2s ease-in-out infinite;
|
|
111
111
|
border: 2px solid rgba(34, 197, 94, 0.3);
|
|
112
112
|
border-radius: 0;
|
|
113
113
|
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
|
|
@@ -120,7 +120,7 @@ GLOW_CSS = """
|
|
|
120
120
|
|
|
121
121
|
GLOW_OFF_CSS = """
|
|
122
122
|
(function() {
|
|
123
|
-
const el = document.getElementById('
|
|
123
|
+
const el = document.getElementById('cdpilot-glow');
|
|
124
124
|
if (el) { el.remove(); return 'glow off'; }
|
|
125
125
|
return 'already off';
|
|
126
126
|
})()
|
|
@@ -130,9 +130,9 @@ GLOW_OFF_CSS = """
|
|
|
130
130
|
|
|
131
131
|
INPUT_BLOCKER_ON = """
|
|
132
132
|
(function() {
|
|
133
|
-
if (document.getElementById('
|
|
133
|
+
if (document.getElementById('cdpilot-input-blocker')) return 'blocker already active';
|
|
134
134
|
const overlay = document.createElement('div');
|
|
135
|
-
overlay.id = '
|
|
135
|
+
overlay.id = 'cdpilot-input-blocker';
|
|
136
136
|
overlay.style.cssText = `
|
|
137
137
|
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
|
138
138
|
z-index: 2147483646; cursor: not-allowed;
|
|
@@ -145,21 +145,21 @@ INPUT_BLOCKER_ON = """
|
|
|
145
145
|
overlay.addEventListener('contextmenu', e => { e.stopPropagation(); e.preventDefault(); }, true);
|
|
146
146
|
overlay.addEventListener('wheel', e => { e.stopPropagation(); e.preventDefault(); }, {capture: true, passive: false});
|
|
147
147
|
document.addEventListener('keydown', function _cb(e) {
|
|
148
|
-
if (!document.getElementById('
|
|
148
|
+
if (!document.getElementById('cdpilot-input-blocker')) {
|
|
149
149
|
document.removeEventListener('keydown', _cb, true);
|
|
150
150
|
return;
|
|
151
151
|
}
|
|
152
152
|
e.stopPropagation(); e.preventDefault();
|
|
153
153
|
}, true);
|
|
154
154
|
document.addEventListener('keyup', function _cb(e) {
|
|
155
|
-
if (!document.getElementById('
|
|
155
|
+
if (!document.getElementById('cdpilot-input-blocker')) {
|
|
156
156
|
document.removeEventListener('keyup', _cb, true);
|
|
157
157
|
return;
|
|
158
158
|
}
|
|
159
159
|
e.stopPropagation(); e.preventDefault();
|
|
160
160
|
}, true);
|
|
161
161
|
document.addEventListener('keypress', function _cb(e) {
|
|
162
|
-
if (!document.getElementById('
|
|
162
|
+
if (!document.getElementById('cdpilot-input-blocker')) {
|
|
163
163
|
document.removeEventListener('keypress', _cb, true);
|
|
164
164
|
return;
|
|
165
165
|
}
|
|
@@ -172,7 +172,7 @@ INPUT_BLOCKER_ON = """
|
|
|
172
172
|
|
|
173
173
|
INPUT_BLOCKER_OFF = """
|
|
174
174
|
(function() {
|
|
175
|
-
const el = document.getElementById('
|
|
175
|
+
const el = document.getElementById('cdpilot-input-blocker');
|
|
176
176
|
if (el) { el.remove(); return 'input blocker off'; }
|
|
177
177
|
return 'blocker already off';
|
|
178
178
|
})()
|
|
@@ -182,11 +182,11 @@ INPUT_BLOCKER_OFF = """
|
|
|
182
182
|
|
|
183
183
|
_glow_script_id = None # addScriptToEvaluateOnNewDocument identifier
|
|
184
184
|
|
|
185
|
-
GLOW_ACTIVATE_JS = "document.documentElement.dataset.
|
|
185
|
+
GLOW_ACTIVATE_JS = "document.documentElement.dataset.cdpilotActive = 'true';"
|
|
186
186
|
GLOW_DEACTIVATE_JS = """
|
|
187
|
-
document.documentElement.removeAttribute('data-
|
|
188
|
-
var _go = document.getElementById('
|
|
189
|
-
var _gs = document.getElementById('
|
|
187
|
+
document.documentElement.removeAttribute('data-cdpilot-active');
|
|
188
|
+
var _go = document.getElementById('cdpilot-glow-overlay');
|
|
189
|
+
var _gs = document.getElementById('cdpilot-glow-style');
|
|
190
190
|
if (_go) _go.remove();
|
|
191
191
|
if (_gs) _gs.remove();
|
|
192
192
|
"""
|
|
@@ -495,7 +495,7 @@ async def navigate_collect(ws_url, url, network=False, console=False, glow=True)
|
|
|
495
495
|
if loaded:
|
|
496
496
|
break
|
|
497
497
|
|
|
498
|
-
# Inject visual indicator (sets data-
|
|
498
|
+
# Inject visual indicator (sets data-cdpilot-active attribute)
|
|
499
499
|
if glow:
|
|
500
500
|
await ws.send(json.dumps({
|
|
501
501
|
"id": 200, "method": "Runtime.evaluate",
|
|
@@ -903,9 +903,10 @@ async def cmd_eval(js_code):
|
|
|
903
903
|
|
|
904
904
|
async def cmd_click(selector):
|
|
905
905
|
ws, _ = get_page_ws()
|
|
906
|
+
safe_sel = json.dumps(selector)
|
|
906
907
|
js = f"""(function() {{
|
|
907
|
-
const el = document.querySelector(
|
|
908
|
-
if (!el) return 'Not found: {
|
|
908
|
+
const el = document.querySelector({safe_sel});
|
|
909
|
+
if (!el) return 'Not found: ' + {safe_sel};
|
|
909
910
|
el.scrollIntoView({{behavior:'smooth', block:'center'}});
|
|
910
911
|
el.click();
|
|
911
912
|
return 'Clicked: ' + el.tagName + ' ' + (el.textContent || '').substring(0, 60).trim();
|
|
@@ -917,10 +918,11 @@ async def cmd_click(selector):
|
|
|
917
918
|
async def cmd_fill(selector, value):
|
|
918
919
|
"""Fill an input field (React/Vue compatible)."""
|
|
919
920
|
ws, _ = get_page_ws()
|
|
921
|
+
safe_sel = json.dumps(selector)
|
|
920
922
|
safe_value = json.dumps(value)
|
|
921
923
|
js = f"""(function() {{
|
|
922
|
-
const el = document.querySelector(
|
|
923
|
-
if (!el) return 'Not found: {
|
|
924
|
+
const el = document.querySelector({safe_sel});
|
|
925
|
+
if (!el) return 'Not found: ' + {safe_sel};
|
|
924
926
|
el.focus();
|
|
925
927
|
const nativeSet = Object.getOwnPropertyDescriptor(
|
|
926
928
|
window.HTMLInputElement.prototype, 'value'
|
|
@@ -936,9 +938,10 @@ async def cmd_fill(selector, value):
|
|
|
936
938
|
|
|
937
939
|
async def cmd_submit(selector="form"):
|
|
938
940
|
ws, _ = get_page_ws()
|
|
941
|
+
safe_sel = json.dumps(selector)
|
|
939
942
|
js = f"""(function() {{
|
|
940
|
-
const form = document.querySelector(
|
|
941
|
-
if (!form) return 'Form not found: {
|
|
943
|
+
const form = document.querySelector({safe_sel});
|
|
944
|
+
if (!form) return 'Form not found: ' + {safe_sel};
|
|
942
945
|
const btn = form.querySelector('button[type=submit], input[type=submit], button:last-of-type');
|
|
943
946
|
if (btn) {{ btn.click(); return 'Submit clicked: ' + btn.textContent.trim(); }}
|
|
944
947
|
form.submit();
|
|
@@ -950,15 +953,16 @@ async def cmd_submit(selector="form"):
|
|
|
950
953
|
|
|
951
954
|
async def cmd_wait(selector, timeout=5):
|
|
952
955
|
ws, _ = get_page_ws()
|
|
956
|
+
safe_sel = json.dumps(selector)
|
|
953
957
|
js = f"""new Promise((resolve) => {{
|
|
954
|
-
const el = document.querySelector(
|
|
958
|
+
const el = document.querySelector({safe_sel});
|
|
955
959
|
if (el) return resolve('Found: ' + el.tagName + ' ' + (el.textContent||'').substring(0,60).trim());
|
|
956
960
|
const obs = new MutationObserver(() => {{
|
|
957
|
-
const el = document.querySelector(
|
|
961
|
+
const el = document.querySelector({safe_sel});
|
|
958
962
|
if (el) {{ obs.disconnect(); resolve('Found: ' + el.tagName + ' ' + (el.textContent||'').substring(0,60).trim()); }}
|
|
959
963
|
}});
|
|
960
964
|
obs.observe(document.body, {{childList:true, subtree:true}});
|
|
961
|
-
setTimeout(() => {{ obs.disconnect(); resolve('Timeout: {
|
|
965
|
+
setTimeout(() => {{ obs.disconnect(); resolve('Timeout: ' + {safe_sel} + ' not found ({timeout}s)'); }}, {int(timeout)*1000});
|
|
962
966
|
}})"""
|
|
963
967
|
r = await cdp_send(ws, [(1, "Runtime.evaluate", {"expression": js, "returnByValue": True, "awaitPromise": True})])
|
|
964
968
|
print(r.get(1, {}).get("result", {}).get("value", "?"))
|
|
@@ -1614,7 +1618,7 @@ def cmd_headless(state=None):
|
|
|
1614
1618
|
|
|
1615
1619
|
|
|
1616
1620
|
def cmd_stop():
|
|
1617
|
-
"""Stop the browser instance managed by
|
|
1621
|
+
"""Stop the browser instance managed by cdpilot."""
|
|
1618
1622
|
import signal
|
|
1619
1623
|
try:
|
|
1620
1624
|
result = subprocess.run(
|
|
@@ -1632,8 +1636,8 @@ def cmd_stop():
|
|
|
1632
1636
|
|
|
1633
1637
|
|
|
1634
1638
|
def cmd_version():
|
|
1635
|
-
"""Show
|
|
1636
|
-
print(f"
|
|
1639
|
+
"""Show cdpilot version."""
|
|
1640
|
+
print(f"cdpilot v{__version__}")
|
|
1637
1641
|
|
|
1638
1642
|
|
|
1639
1643
|
# ─── New CDP Commands ───
|
|
@@ -2205,6 +2209,149 @@ async def cmd_permission(subcmd, perm=None):
|
|
|
2205
2209
|
sys.exit(1)
|
|
2206
2210
|
|
|
2207
2211
|
|
|
2212
|
+
# ─── MCP Server ───
|
|
2213
|
+
|
|
2214
|
+
class MCPServer:
|
|
2215
|
+
"""Minimal MCP (Model Context Protocol) server over stdin/stdout.
|
|
2216
|
+
Implements JSON-RPC 2.0 for tool discovery and execution.
|
|
2217
|
+
Usage: cdpilot mcp
|
|
2218
|
+
"""
|
|
2219
|
+
|
|
2220
|
+
def __init__(self):
|
|
2221
|
+
self.tools = self._register_tools()
|
|
2222
|
+
|
|
2223
|
+
def _register_tools(self):
|
|
2224
|
+
return [
|
|
2225
|
+
{"name": "browser_navigate", "description": "Navigate to a URL",
|
|
2226
|
+
"inputSchema": {"type": "object", "properties": {"url": {"type": "string", "description": "URL to navigate to"}}, "required": ["url"]}},
|
|
2227
|
+
{"name": "browser_screenshot", "description": "Take a screenshot of the current page",
|
|
2228
|
+
"inputSchema": {"type": "object", "properties": {"filename": {"type": "string", "description": "Output filename", "default": "screenshot.png"}}}},
|
|
2229
|
+
{"name": "browser_click", "description": "Click an element by CSS selector",
|
|
2230
|
+
"inputSchema": {"type": "object", "properties": {"selector": {"type": "string", "description": "CSS selector"}}, "required": ["selector"]}},
|
|
2231
|
+
{"name": "browser_type", "description": "Type text into an input element",
|
|
2232
|
+
"inputSchema": {"type": "object", "properties": {"selector": {"type": "string", "description": "CSS selector"}, "text": {"type": "string", "description": "Text to type"}}, "required": ["selector", "text"]}},
|
|
2233
|
+
{"name": "browser_content", "description": "Get text content of the current page",
|
|
2234
|
+
"inputSchema": {"type": "object", "properties": {}}},
|
|
2235
|
+
{"name": "browser_html", "description": "Get HTML source of the current page",
|
|
2236
|
+
"inputSchema": {"type": "object", "properties": {}}},
|
|
2237
|
+
{"name": "browser_eval", "description": "Execute JavaScript in the browser",
|
|
2238
|
+
"inputSchema": {"type": "object", "properties": {"expression": {"type": "string", "description": "JavaScript expression"}}, "required": ["expression"]}},
|
|
2239
|
+
{"name": "browser_tabs", "description": "List all open browser tabs",
|
|
2240
|
+
"inputSchema": {"type": "object", "properties": {}}},
|
|
2241
|
+
{"name": "browser_console", "description": "Get console logs from the browser",
|
|
2242
|
+
"inputSchema": {"type": "object", "properties": {"url": {"type": "string", "description": "URL to navigate and capture logs"}}}},
|
|
2243
|
+
{"name": "browser_network", "description": "Monitor network requests",
|
|
2244
|
+
"inputSchema": {"type": "object", "properties": {"url": {"type": "string", "description": "URL to navigate and monitor"}}}},
|
|
2245
|
+
{"name": "browser_a11y", "description": "Get accessibility tree of the current page",
|
|
2246
|
+
"inputSchema": {"type": "object", "properties": {"mode": {"type": "string", "enum": ["full", "summary"], "default": "full"}}}},
|
|
2247
|
+
{"name": "browser_fill", "description": "Set input value (React-compatible)",
|
|
2248
|
+
"inputSchema": {"type": "object", "properties": {"selector": {"type": "string", "description": "CSS selector"}, "value": {"type": "string", "description": "Value to set"}}, "required": ["selector", "value"]}},
|
|
2249
|
+
{"name": "browser_launch", "description": "Launch browser with CDP enabled",
|
|
2250
|
+
"inputSchema": {"type": "object", "properties": {}}},
|
|
2251
|
+
{"name": "browser_close", "description": "Close the active tab",
|
|
2252
|
+
"inputSchema": {"type": "object", "properties": {}}},
|
|
2253
|
+
]
|
|
2254
|
+
|
|
2255
|
+
def _handle_request(self, request):
|
|
2256
|
+
method = request.get("method", "")
|
|
2257
|
+
req_id = request.get("id")
|
|
2258
|
+
params = request.get("params", {})
|
|
2259
|
+
|
|
2260
|
+
if method == "initialize":
|
|
2261
|
+
return {"jsonrpc": "2.0", "id": req_id, "result": {
|
|
2262
|
+
"protocolVersion": "2024-11-05",
|
|
2263
|
+
"capabilities": {"tools": {}},
|
|
2264
|
+
"serverInfo": {"name": "cdpilot", "version": __version__}
|
|
2265
|
+
}}
|
|
2266
|
+
elif method == "notifications/initialized":
|
|
2267
|
+
return None
|
|
2268
|
+
elif method == "tools/list":
|
|
2269
|
+
return {"jsonrpc": "2.0", "id": req_id, "result": {"tools": self.tools}}
|
|
2270
|
+
elif method == "tools/call":
|
|
2271
|
+
return self._execute_tool(req_id, params.get("name", ""), params.get("arguments", {}))
|
|
2272
|
+
elif method == "ping":
|
|
2273
|
+
return {"jsonrpc": "2.0", "id": req_id, "result": {}}
|
|
2274
|
+
else:
|
|
2275
|
+
return {"jsonrpc": "2.0", "id": req_id, "error": {"code": -32601, "message": f"Method not found: {method}"}}
|
|
2276
|
+
|
|
2277
|
+
@staticmethod
|
|
2278
|
+
def _safe_filename(name):
|
|
2279
|
+
import re
|
|
2280
|
+
base = os.path.basename(name)
|
|
2281
|
+
base = re.sub(r'[^\w.\-]', '_', base)
|
|
2282
|
+
if not base.lower().endswith('.png'):
|
|
2283
|
+
base += '.png'
|
|
2284
|
+
return os.path.join(SCREENSHOT_DIR, base)
|
|
2285
|
+
|
|
2286
|
+
def _execute_tool(self, req_id, tool_name, args):
|
|
2287
|
+
import io, subprocess
|
|
2288
|
+
tool_map = {
|
|
2289
|
+
"browser_navigate": lambda a: ["go", a.get("url", "")],
|
|
2290
|
+
"browser_screenshot": lambda a: ["shot"] + ([self._safe_filename(a["filename"])] if a.get("filename") else []),
|
|
2291
|
+
"browser_click": lambda a: ["click", a.get("selector", "")],
|
|
2292
|
+
"browser_type": lambda a: ["type", a.get("selector", ""), a.get("text", "")],
|
|
2293
|
+
"browser_content": lambda a: ["content"],
|
|
2294
|
+
"browser_html": lambda a: ["html"],
|
|
2295
|
+
"browser_eval": lambda a: ["eval", a.get("expression", "")],
|
|
2296
|
+
"browser_tabs": lambda a: ["tabs"],
|
|
2297
|
+
"browser_console": lambda a: ["console"] + ([a["url"]] if a.get("url") else []),
|
|
2298
|
+
"browser_network": lambda a: ["network"] + ([a["url"]] if a.get("url") else []),
|
|
2299
|
+
"browser_a11y": lambda a: ["a11y"] + ([a["mode"]] if a.get("mode") and a["mode"] != "full" else []),
|
|
2300
|
+
"browser_fill": lambda a: ["fill", a.get("selector", ""), a.get("value", "")],
|
|
2301
|
+
"browser_launch": lambda a: ["launch"],
|
|
2302
|
+
"browser_close": lambda a: ["close"],
|
|
2303
|
+
}
|
|
2304
|
+
if tool_name not in tool_map:
|
|
2305
|
+
return {"jsonrpc": "2.0", "id": req_id, "error": {"code": -32602, "message": f"Unknown tool: {tool_name}"}}
|
|
2306
|
+
|
|
2307
|
+
cli_args = [a for a in tool_map[tool_name](args) if a]
|
|
2308
|
+
try:
|
|
2309
|
+
result = subprocess.run(
|
|
2310
|
+
[sys.executable, __file__] + cli_args,
|
|
2311
|
+
capture_output=True, text=True, timeout=30
|
|
2312
|
+
)
|
|
2313
|
+
output = result.stdout.strip()
|
|
2314
|
+
errors = result.stderr.strip()
|
|
2315
|
+
content = []
|
|
2316
|
+
if output:
|
|
2317
|
+
content.append({"type": "text", "text": output})
|
|
2318
|
+
if errors:
|
|
2319
|
+
content.append({"type": "text", "text": f"stderr: {errors}"})
|
|
2320
|
+
if not content:
|
|
2321
|
+
content.append({"type": "text", "text": "Command executed successfully"})
|
|
2322
|
+
return {"jsonrpc": "2.0", "id": req_id, "result": {"content": content, "isError": result.returncode != 0}}
|
|
2323
|
+
except subprocess.TimeoutExpired:
|
|
2324
|
+
return {"jsonrpc": "2.0", "id": req_id, "result": {"content": [{"type": "text", "text": "Error: Command timed out (30s)"}], "isError": True}}
|
|
2325
|
+
except Exception as e:
|
|
2326
|
+
return {"jsonrpc": "2.0", "id": req_id, "result": {"content": [{"type": "text", "text": f"Error: {str(e)}"}], "isError": True}}
|
|
2327
|
+
|
|
2328
|
+
def run(self):
|
|
2329
|
+
import json as json_mod
|
|
2330
|
+
sys.stderr.write(f"cdpilot MCP server v{__version__} ready\n")
|
|
2331
|
+
sys.stderr.flush()
|
|
2332
|
+
while True:
|
|
2333
|
+
try:
|
|
2334
|
+
line = sys.stdin.readline()
|
|
2335
|
+
if not line:
|
|
2336
|
+
break
|
|
2337
|
+
line = line.strip()
|
|
2338
|
+
if not line:
|
|
2339
|
+
continue
|
|
2340
|
+
request = json_mod.loads(line)
|
|
2341
|
+
response = self._handle_request(request)
|
|
2342
|
+
if response is not None:
|
|
2343
|
+
sys.stdout.write(json_mod.dumps(response) + "\n")
|
|
2344
|
+
sys.stdout.flush()
|
|
2345
|
+
except json_mod.JSONDecodeError as e:
|
|
2346
|
+
sys.stdout.write(json_mod.dumps({"jsonrpc": "2.0", "id": None, "error": {"code": -32700, "message": f"Parse error: {str(e)}"}}) + "\n")
|
|
2347
|
+
sys.stdout.flush()
|
|
2348
|
+
except KeyboardInterrupt:
|
|
2349
|
+
break
|
|
2350
|
+
except Exception as e:
|
|
2351
|
+
sys.stderr.write(f"MCP error: {str(e)}\n")
|
|
2352
|
+
sys.stderr.flush()
|
|
2353
|
+
|
|
2354
|
+
|
|
2208
2355
|
# ─── CLI ───
|
|
2209
2356
|
|
|
2210
2357
|
if __name__ == "__main__":
|
|
@@ -2228,6 +2375,11 @@ if __name__ == "__main__":
|
|
|
2228
2375
|
'session-close': lambda: cmd_session_close(args[0] if args else None),
|
|
2229
2376
|
}
|
|
2230
2377
|
|
|
2378
|
+
if cmd == "mcp":
|
|
2379
|
+
server = MCPServer()
|
|
2380
|
+
server.run()
|
|
2381
|
+
sys.exit(0)
|
|
2382
|
+
|
|
2231
2383
|
if cmd == "ext-install":
|
|
2232
2384
|
if not args:
|
|
2233
2385
|
print("Usage: ext-install <crx-file-or-directory>")
|