cdpilot 0.1.1 → 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 +10 -0
- package/package.json +3 -3
- package/src/cdpilot.py +25 -12
- package/src/__pycache__/cdpilot.cpython-314.pyc +0 -0
package/README.md
CHANGED
|
@@ -261,6 +261,16 @@ Future paid offerings:
|
|
|
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
6
|
"cdpilot": "./bin/cdpilot.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"devtools", "headless", "screenshot", "testing", "web-scraping",
|
|
16
16
|
"ai-agent", "mcp", "claude", "brave"
|
|
17
17
|
],
|
|
18
|
-
"author": "",
|
|
18
|
+
"author": "Mehmet Nadir",
|
|
19
19
|
"license": "MIT",
|
|
20
20
|
"repository": {
|
|
21
21
|
"type": "git",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
28
|
"bin/",
|
|
29
|
-
"src/",
|
|
29
|
+
"src/cdpilot.py",
|
|
30
30
|
"README.md",
|
|
31
31
|
"LICENSE"
|
|
32
32
|
]
|
package/src/cdpilot.py
CHANGED
|
@@ -11,10 +11,10 @@ Usage:
|
|
|
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
|
|
@@ -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", "?"))
|
|
@@ -2270,11 +2274,20 @@ class MCPServer:
|
|
|
2270
2274
|
else:
|
|
2271
2275
|
return {"jsonrpc": "2.0", "id": req_id, "error": {"code": -32601, "message": f"Method not found: {method}"}}
|
|
2272
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
|
+
|
|
2273
2286
|
def _execute_tool(self, req_id, tool_name, args):
|
|
2274
2287
|
import io, subprocess
|
|
2275
2288
|
tool_map = {
|
|
2276
2289
|
"browser_navigate": lambda a: ["go", a.get("url", "")],
|
|
2277
|
-
"browser_screenshot": lambda a: ["shot"] + ([a["filename"]] if a.get("filename") else []),
|
|
2290
|
+
"browser_screenshot": lambda a: ["shot"] + ([self._safe_filename(a["filename"])] if a.get("filename") else []),
|
|
2278
2291
|
"browser_click": lambda a: ["click", a.get("selector", "")],
|
|
2279
2292
|
"browser_type": lambda a: ["type", a.get("selector", ""), a.get("text", "")],
|
|
2280
2293
|
"browser_content": lambda a: ["content"],
|
|
Binary file
|