@jackwener/opencli 0.7.11 → 0.9.0
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/CDP.md +103 -0
- package/CDP.zh-CN.md +103 -0
- package/README.md +5 -0
- package/README.zh-CN.md +5 -0
- package/dist/browser/discover.d.ts +15 -0
- package/dist/browser/discover.js +68 -2
- package/dist/browser/errors.d.ts +2 -1
- package/dist/browser/errors.js +13 -0
- package/dist/browser/index.d.ts +1 -0
- package/dist/browser/index.js +1 -0
- package/dist/browser/mcp.js +8 -3
- package/dist/browser/page.js +11 -2
- package/dist/cli-manifest.json +246 -0
- package/dist/clis/antigravity/dump.d.ts +1 -0
- package/dist/clis/antigravity/dump.js +28 -0
- package/dist/clis/antigravity/extract-code.d.ts +1 -0
- package/dist/clis/antigravity/extract-code.js +32 -0
- package/dist/clis/antigravity/model.d.ts +1 -0
- package/dist/clis/antigravity/model.js +44 -0
- package/dist/clis/antigravity/new.d.ts +1 -0
- package/dist/clis/antigravity/new.js +25 -0
- package/dist/clis/antigravity/read.d.ts +1 -0
- package/dist/clis/antigravity/read.js +34 -0
- package/dist/clis/antigravity/send.d.ts +1 -0
- package/dist/clis/antigravity/send.js +35 -0
- package/dist/clis/antigravity/status.d.ts +1 -0
- package/dist/clis/antigravity/status.js +18 -0
- package/dist/clis/antigravity/watch.d.ts +1 -0
- package/dist/clis/antigravity/watch.js +41 -0
- package/dist/clis/barchart/flow.js +56 -58
- package/dist/clis/xiaoyuzhou/episode.d.ts +1 -0
- package/dist/clis/xiaoyuzhou/episode.js +28 -0
- package/dist/clis/xiaoyuzhou/podcast-episodes.d.ts +1 -0
- package/dist/clis/xiaoyuzhou/podcast-episodes.js +36 -0
- package/dist/clis/xiaoyuzhou/podcast.d.ts +1 -0
- package/dist/clis/xiaoyuzhou/podcast.js +27 -0
- package/dist/clis/xiaoyuzhou/utils.d.ts +16 -0
- package/dist/clis/xiaoyuzhou/utils.js +55 -0
- package/dist/clis/xiaoyuzhou/utils.test.d.ts +1 -0
- package/dist/clis/xiaoyuzhou/utils.test.js +99 -0
- package/dist/doctor.js +8 -0
- package/dist/engine.d.ts +1 -1
- package/dist/engine.js +59 -1
- package/dist/main.js +2 -15
- package/dist/pipeline/executor.js +2 -24
- package/dist/pipeline/registry.d.ts +19 -0
- package/dist/pipeline/registry.js +41 -0
- package/package.json +1 -1
- package/src/browser/discover.ts +79 -5
- package/src/browser/errors.ts +17 -1
- package/src/browser/index.ts +1 -0
- package/src/browser/mcp.ts +8 -3
- package/src/browser/page.ts +21 -2
- package/src/clis/antigravity/README.md +49 -0
- package/src/clis/antigravity/README.zh-CN.md +52 -0
- package/src/clis/antigravity/SKILL.md +42 -0
- package/src/clis/antigravity/dump.ts +30 -0
- package/src/clis/antigravity/extract-code.ts +34 -0
- package/src/clis/antigravity/model.ts +47 -0
- package/src/clis/antigravity/new.ts +28 -0
- package/src/clis/antigravity/read.ts +36 -0
- package/src/clis/antigravity/send.ts +40 -0
- package/src/clis/antigravity/status.ts +19 -0
- package/src/clis/antigravity/watch.ts +45 -0
- package/src/clis/barchart/flow.ts +57 -58
- package/src/clis/xiaoyuzhou/episode.ts +28 -0
- package/src/clis/xiaoyuzhou/podcast-episodes.ts +36 -0
- package/src/clis/xiaoyuzhou/podcast.ts +27 -0
- package/src/clis/xiaoyuzhou/utils.test.ts +122 -0
- package/src/clis/xiaoyuzhou/utils.ts +65 -0
- package/src/doctor.ts +9 -0
- package/src/engine.ts +58 -1
- package/src/main.ts +6 -11
- package/src/pipeline/executor.ts +2 -28
- package/src/pipeline/registry.ts +60 -0
- package/tests/e2e/public-commands.test.ts +62 -0
package/CDP.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Connecting OpenCLI via CDP (Remote/Headless Servers)
|
|
2
|
+
|
|
3
|
+
If you cannot use the Playwright MCP Bridge extension (e.g., in a remote headless server environment without a UI), OpenCLI provides an alternative: connecting directly to Chrome via **CDP (Chrome DevTools Protocol)**.
|
|
4
|
+
|
|
5
|
+
Because CDP binds to `localhost` by default for security reasons, accessing it from a remote server requires an additional networking tunnel.
|
|
6
|
+
|
|
7
|
+
This guide is broken down into three phases:
|
|
8
|
+
1. **Preparation**: Start Chrome with CDP enabled locally.
|
|
9
|
+
2. **Network Tunnels**: Expose that CDP port to your remote server using either **SSH Tunnels** or **Reverse Proxies**.
|
|
10
|
+
3. **Execution**: Run OpenCLI on your server.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Phase 1: Preparation (Local Machine)
|
|
15
|
+
|
|
16
|
+
First, you need to start a Chrome browser on your local machine with remote debugging enabled.
|
|
17
|
+
|
|
18
|
+
**macOS:**
|
|
19
|
+
```bash
|
|
20
|
+
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
|
|
21
|
+
--remote-debugging-port=9222 \
|
|
22
|
+
--user-data-dir="$HOME/chrome-debug-profile" \
|
|
23
|
+
--remote-allow-origins="*"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Linux:**
|
|
27
|
+
```bash
|
|
28
|
+
google-chrome \
|
|
29
|
+
--remote-debugging-port=9222 \
|
|
30
|
+
--user-data-dir="$HOME/chrome-debug-profile" \
|
|
31
|
+
--remote-allow-origins="*"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Windows:**
|
|
35
|
+
```cmd
|
|
36
|
+
"C:\Program Files\Google\Chrome\Application\chrome.exe" ^
|
|
37
|
+
--remote-debugging-port=9222 ^
|
|
38
|
+
--user-data-dir="%USERPROFILE%\chrome-debug-profile" ^
|
|
39
|
+
--remote-allow-origins="*"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
> **Note**: The `--remote-allow-origins="*"` flag is often required for modern Chrome versions to accept cross-origin CDP WebSocket connections (e.g. from reverse proxies like ngrok).
|
|
43
|
+
|
|
44
|
+
Once this browser instance opens, **log into the target websites you want to use** (e.g., bilibili.com, zhihu.com) so that the session contains the correct cookies.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Phase 2: Remote Access Methods
|
|
49
|
+
|
|
50
|
+
Once CDP is running locally on port `9222`, you must securely expose this port to your remote server. Choose one of the two methods below depending on your network conditions.
|
|
51
|
+
|
|
52
|
+
### Method A: SSH Tunnel (Recommended)
|
|
53
|
+
|
|
54
|
+
If your local machine has SSH access to the remote server, this is the most secure and straightforward method.
|
|
55
|
+
|
|
56
|
+
Run this command on your **Local Machine** to forward the remote server's port `9222` back to your local port `9222`:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
ssh -R 9222:localhost:9222 your-server-user@your-server-ip
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Leave this SSH session running in the background.
|
|
63
|
+
|
|
64
|
+
### Method B: Reverse Proxy (ngrok / frp / socat)
|
|
65
|
+
|
|
66
|
+
If you cannot establish a direct SSH connection (e.g., due to NAT or firewalls), you can use an intranet penetration tool like `ngrok`.
|
|
67
|
+
|
|
68
|
+
Run this command on your **Local Machine** to expose your local port `9222` to the public internet securely via ngrok:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
ngrok http 9222
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This will print a forwarding URL, such as `https://abcdef.ngrok.app`. **Copy this URL**.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Phase 3: Execution (Remote Server)
|
|
79
|
+
|
|
80
|
+
Now switch to your **Remote Server** where OpenCLI is installed.
|
|
81
|
+
|
|
82
|
+
Depending on the network tunnel method you chose in Phase 2, set the `OPENCLI_CDP_ENDPOINT` environment variable and run your commands.
|
|
83
|
+
|
|
84
|
+
### If you used Method A (SSH Tunnel):
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
export OPENCLI_CDP_ENDPOINT="http://localhost:9222"
|
|
88
|
+
opencli doctor # Verify connection
|
|
89
|
+
opencli bilibili hot --limit 5 # Test a command
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### If you used Method B (Reverse Proxy like ngrok):
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# Use the URL you copied from ngrok earlier
|
|
96
|
+
export OPENCLI_CDP_ENDPOINT="https://abcdef.ngrok.app"
|
|
97
|
+
opencli doctor # Verify connection
|
|
98
|
+
opencli bilibili hot --limit 5 # Test a command
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
> *Tip: OpenCLI automatically requests the `/json/version` HTTP endpoint to discover the underlying WebSocket URL if you provide a standard HTTP/HTTPS address.*
|
|
102
|
+
|
|
103
|
+
If you plan to use this setup frequently, you can persist the environment variable by adding the `export` line to your `~/.bashrc` or `~/.zshrc` on the server.
|
package/CDP.zh-CN.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# 通过 CDP 远程连接 OpenCLI (服务器/无头环境)
|
|
2
|
+
|
|
3
|
+
如果你无法使用 Playwright MCP Bridge 浏览器扩展(例如:在无界面的远程服务器上运行 OpenCLI 时),OpenCLI 提供了备选方案:通过连接 **CDP (Chrome DevTools Protocol,即 Chrome 开发者工具协议)** 来直接控制本地 Chrome。
|
|
4
|
+
|
|
5
|
+
出于安全考虑,CDP 默认仅绑定在 `localhost` 的本地端口。所以,若是想让**远程服务器**调用本地的 CDP 服务,我们需要依靠一层额外的网络隧道。
|
|
6
|
+
|
|
7
|
+
本指南将整个过程拆分为三个阶段:
|
|
8
|
+
1. **阶段一:准备工作**(在本地启动允许 CDP 调试的 Chrome)。
|
|
9
|
+
2. **阶段二:建立网络隧道**(通过 **SSH反向隧道** 或 **反向代理工具**,将本地的 CDP 端口暴露给服务器)。
|
|
10
|
+
3. **阶段三:执行命令**(在服务器端运行 OpenCLI)。
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 阶段一:准备工作 (本地电脑)
|
|
15
|
+
|
|
16
|
+
首先,你需要在你的本地电脑上,通过命令行参数启动一个开启了远程调试端口的 Chrome 实例。
|
|
17
|
+
|
|
18
|
+
**macOS:**
|
|
19
|
+
```bash
|
|
20
|
+
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
|
|
21
|
+
--remote-debugging-port=9222 \
|
|
22
|
+
--user-data-dir="$HOME/chrome-debug-profile" \
|
|
23
|
+
--remote-allow-origins="*"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Linux:**
|
|
27
|
+
```bash
|
|
28
|
+
google-chrome \
|
|
29
|
+
--remote-debugging-port=9222 \
|
|
30
|
+
--user-data-dir="$HOME/chrome-debug-profile" \
|
|
31
|
+
--remote-allow-origins="*"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Windows:**
|
|
35
|
+
```cmd
|
|
36
|
+
"C:\Program Files\Google\Chrome\Application\chrome.exe" ^
|
|
37
|
+
--remote-debugging-port=9222 ^
|
|
38
|
+
--user-data-dir="%USERPROFILE%\chrome-debug-profile" ^
|
|
39
|
+
--remote-allow-origins="*"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
> **注意**:此处增加的 `--remote-allow-origins="*"` 参数对于较新版本的 Chrome 来说通常是[必需的],以允许来自反向代理(如 ngrok)的跨域 WebSocket 连接请求。
|
|
43
|
+
|
|
44
|
+
待这个新的浏览器实例打开后,**手工登录那些你打算使用的网站**(如 bilibili.com、zhihu.com 等),这可以让该浏览器的运行资料(Profile)保留上这些网站登录用的 Cookie。
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 阶段二:建立网络隧道
|
|
49
|
+
|
|
50
|
+
现在你的本地已经有了一个监听在 `9222` 端口的 CDP 服务,接下来,选择以下任意一种方式将其实际暴露给你的远端服务器。
|
|
51
|
+
|
|
52
|
+
### 方法 A:SSH 反向端口转发 (推荐)
|
|
53
|
+
|
|
54
|
+
如果你的本地电脑可以直连远程服务器的 SSH,那么这是最简单且最安全的做法。
|
|
55
|
+
|
|
56
|
+
在你的 **本地电脑** 终端上直接运行这条 ssh 命令,将远程服务器的 `9222` 端口反向映射回本地的 `9222` 端口:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
ssh -R 9222:localhost:9222 your-server-user@your-server-ip
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
保持此 SSH 会话在后台运行即可。
|
|
63
|
+
|
|
64
|
+
### 方法 B:反向代理 / 内网穿透 (ngrok / frp / socat)
|
|
65
|
+
|
|
66
|
+
如果因为 NAT 或防火墙等因素导致无法直连 SSH 服务器,你可以使用 `ngrok` 等内网穿透工具。
|
|
67
|
+
|
|
68
|
+
在 **本地电脑** 运行 ngrok 将本地的 `9222` 端口暴露到公网:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
ngrok http 9222
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
此时终端里会打印出一段专属的转发 URL 地址(如:`https://abcdef.ngrok.app`)。**复制这一段 URL 地址备用**。
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 阶段三:执行命令 (远程服务器)
|
|
79
|
+
|
|
80
|
+
现在,所有的准备工作已结束。请切换到你已安装好 OpenCLI 的 **远程服务器** 终端上。
|
|
81
|
+
|
|
82
|
+
根据你在上方阶段二所选择的隧道方案,在终端中配置对应的 `OPENCLI_CDP_ENDPOINT` 环境变量:
|
|
83
|
+
|
|
84
|
+
### 若使用 方法 A (SSH 反向隧道):
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
export OPENCLI_CDP_ENDPOINT="http://localhost:9222"
|
|
88
|
+
opencli doctor # 查看并验证连接是否通畅
|
|
89
|
+
opencli bilibili hot --limit 5 # 执行目标命令
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 若使用 方法 B (Ngrok 等反向代理):
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# 将刚刚使用 ngrok 得到的地址填入这里
|
|
96
|
+
export OPENCLI_CDP_ENDPOINT="https://abcdef.ngrok.app"
|
|
97
|
+
opencli doctor # 查看并验证连接是否通畅
|
|
98
|
+
opencli bilibili hot --limit 5 # 执行目标命令
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
> *Tip: 如果你填写的是一个普通 HTTP/HTTPS 的 URL 地址,OpenCLI 会自动尝试抓取该地址下的 `/json/version` 节点,来动态解析并连接真正底层依赖的 WebSocket 地址。*
|
|
102
|
+
|
|
103
|
+
如果你想在此服务器上永久启用该配置,可以将对应的 `export` 语句追加进入你的 `~/.bashrc` 或 `~/.zshrc` 配置文件中。
|
package/README.md
CHANGED
|
@@ -21,6 +21,7 @@ A CLI tool that turns **any website** into a command-line interface — Bilibili
|
|
|
21
21
|
- [Built-in Commands](#built-in-commands)
|
|
22
22
|
- [Output Formats](#output-formats)
|
|
23
23
|
- [For AI Agents (Developer Guide)](#for-ai-agents-developer-guide)
|
|
24
|
+
- [Remote Chrome (Server/Headless)](#remote-chrome-serverheadless)
|
|
24
25
|
- [Testing](#testing)
|
|
25
26
|
- [Troubleshooting](#troubleshooting)
|
|
26
27
|
- [Releasing New Versions](#releasing-new-versions)
|
|
@@ -69,6 +70,9 @@ The interactive TUI will:
|
|
|
69
70
|
> opencli doctor --fix -y # Fix all configs non-interactively
|
|
70
71
|
> ```
|
|
71
72
|
|
|
73
|
+
**Alternative: CDP Mode (For Servers/Headless)**
|
|
74
|
+
If you cannot install the browser extension (e.g. running OpenCLI on a remote headless server), you can connect OpenCLI to your local Chrome via CDP using SSH tunnels or reverse proxies. See the [CDP Connection Guide](./CDP.md) for detailed instructions.
|
|
75
|
+
|
|
72
76
|
<details>
|
|
73
77
|
<summary>Manual setup (alternative)</summary>
|
|
74
78
|
|
|
@@ -145,6 +149,7 @@ npm install -g @jackwener/opencli@latest
|
|
|
145
149
|
| **v2ex** | `hot` `latest` `topic` `daily` `me` `notifications` | 6 | 🌐 / 🔐 |
|
|
146
150
|
| **xueqiu** | `feed` `hot-stock` `hot` `search` `stock` `watchlist` | 6 | 🔐 Browser |
|
|
147
151
|
| **xiaohongshu** | `search` `notifications` `feed` `me` `user` | 5 | 🔐 Browser |
|
|
152
|
+
| **xiaoyuzhou** | `podcast` `podcast-episodes` `episode` | 3 | 🌐 Public |
|
|
148
153
|
| **youtube** | `search` `video` `transcript` | 3 | 🔐 Browser |
|
|
149
154
|
| **zhihu** | `hot` `search` `question` | 3 | 🔐 Browser |
|
|
150
155
|
| **boss** | `search` `detail` | 2 | 🔐 Browser |
|
package/README.zh-CN.md
CHANGED
|
@@ -21,6 +21,7 @@ OpenCLI 将任何网站变成命令行工具 — B站、知乎、小红书、Twi
|
|
|
21
21
|
- [内置命令](#内置命令)
|
|
22
22
|
- [输出格式](#输出格式)
|
|
23
23
|
- [致 AI Agent(开发者指南)](#致-ai-agent开发者指南)
|
|
24
|
+
- [远程 Chrome(服务器/无头环境)](#远程-chrome服务器无头环境)
|
|
24
25
|
- [常见问题排查](#常见问题排查)
|
|
25
26
|
- [版本发布](#版本发布)
|
|
26
27
|
- [License](#license)
|
|
@@ -68,6 +69,9 @@ opencli setup
|
|
|
68
69
|
> opencli doctor --fix -y # 无交互直接修复所有配置
|
|
69
70
|
> ```
|
|
70
71
|
|
|
72
|
+
**备选方案:CDP 模式 (适用于服务器/无头环境)**
|
|
73
|
+
如果你无法安装浏览器扩展(比如在远程无头服务器上运行 OpenCLI),你可以通过 SSH 隧道或反向代理,利用 CDP (Chrome DevTools Protocol) 连接到本地的 Chrome 浏览器。详细指南请参考 [CDP 连接教程](./CDP.zh-CN.md)。
|
|
74
|
+
|
|
71
75
|
<details>
|
|
72
76
|
<summary>手动配置(备选方案)</summary>
|
|
73
77
|
|
|
@@ -144,6 +148,7 @@ npm install -g @jackwener/opencli@latest
|
|
|
144
148
|
| **v2ex** | `hot` `latest` `topic` `daily` `me` `notifications` | 6 | 🌐 / 🔐 |
|
|
145
149
|
| **xueqiu** | `feed` `hot-stock` `hot` `search` `stock` `watchlist` | 6 | 🔐 浏览器 |
|
|
146
150
|
| **xiaohongshu** | `search` `notifications` `feed` `me` `user` | 5 | 🔐 浏览器 |
|
|
151
|
+
| **xiaoyuzhou** | `podcast` `podcast-episodes` `episode` | 3 | 🌐 公开 |
|
|
147
152
|
| **youtube** | `search` `video` `transcript` | 3 | 🔐 浏览器 |
|
|
148
153
|
| **zhihu** | `hot` `search` `question` | 3 | 🔐 浏览器 |
|
|
149
154
|
| **boss** | `search` `detail` | 2 | 🔐 浏览器 |
|
|
@@ -9,13 +9,28 @@ export declare function setMcpDiscoveryTestHooks(input?: {
|
|
|
9
9
|
execSync?: typeof execSync;
|
|
10
10
|
}): void;
|
|
11
11
|
export declare function findMcpServerPath(): string | null;
|
|
12
|
+
/**
|
|
13
|
+
* Chrome 144+ auto-discovery: read DevToolsActivePort file to get CDP endpoint.
|
|
14
|
+
*
|
|
15
|
+
* Starting with Chrome 144, users can enable remote debugging from
|
|
16
|
+
* chrome://inspect#remote-debugging without any command-line flags.
|
|
17
|
+
* Chrome writes the active port and browser GUID to a DevToolsActivePort file
|
|
18
|
+
* in the user data directory, which we read to construct the WebSocket endpoint.
|
|
19
|
+
*/
|
|
20
|
+
export declare function discoverChromeEndpoint(): string | null;
|
|
21
|
+
export declare function resolveCdpEndpoint(): {
|
|
22
|
+
endpoint?: string;
|
|
23
|
+
requestedCdp: boolean;
|
|
24
|
+
};
|
|
12
25
|
export declare function buildMcpArgs(input: {
|
|
13
26
|
mcpPath: string;
|
|
14
27
|
executablePath?: string | null;
|
|
28
|
+
cdpEndpoint?: string;
|
|
15
29
|
}): string[];
|
|
16
30
|
export declare function buildMcpLaunchSpec(input: {
|
|
17
31
|
mcpPath?: string | null;
|
|
18
32
|
executablePath?: string | null;
|
|
33
|
+
cdpEndpoint?: string;
|
|
19
34
|
}): {
|
|
20
35
|
command: string;
|
|
21
36
|
args: string[];
|
package/dist/browser/discover.js
CHANGED
|
@@ -98,13 +98,79 @@ export function findMcpServerPath() {
|
|
|
98
98
|
_cachedMcpServerPath = null;
|
|
99
99
|
return _cachedMcpServerPath;
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Chrome 144+ auto-discovery: read DevToolsActivePort file to get CDP endpoint.
|
|
103
|
+
*
|
|
104
|
+
* Starting with Chrome 144, users can enable remote debugging from
|
|
105
|
+
* chrome://inspect#remote-debugging without any command-line flags.
|
|
106
|
+
* Chrome writes the active port and browser GUID to a DevToolsActivePort file
|
|
107
|
+
* in the user data directory, which we read to construct the WebSocket endpoint.
|
|
108
|
+
*/
|
|
109
|
+
export function discoverChromeEndpoint() {
|
|
110
|
+
const candidates = [];
|
|
111
|
+
// User-specified Chrome data dir takes highest priority
|
|
112
|
+
if (process.env.CHROME_USER_DATA_DIR) {
|
|
113
|
+
candidates.push(path.join(process.env.CHROME_USER_DATA_DIR, 'DevToolsActivePort'));
|
|
114
|
+
}
|
|
115
|
+
// Standard Chrome/Edge user data dirs per platform
|
|
116
|
+
if (process.platform === 'win32') {
|
|
117
|
+
const localAppData = process.env.LOCALAPPDATA ?? path.join(os.homedir(), 'AppData', 'Local');
|
|
118
|
+
candidates.push(path.join(localAppData, 'Google', 'Chrome', 'User Data', 'DevToolsActivePort'));
|
|
119
|
+
candidates.push(path.join(localAppData, 'Microsoft', 'Edge', 'User Data', 'DevToolsActivePort'));
|
|
120
|
+
}
|
|
121
|
+
else if (process.platform === 'darwin') {
|
|
122
|
+
candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome', 'DevToolsActivePort'));
|
|
123
|
+
candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'Microsoft Edge', 'DevToolsActivePort'));
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
candidates.push(path.join(os.homedir(), '.config', 'google-chrome', 'DevToolsActivePort'));
|
|
127
|
+
candidates.push(path.join(os.homedir(), '.config', 'chromium', 'DevToolsActivePort'));
|
|
128
|
+
candidates.push(path.join(os.homedir(), '.config', 'microsoft-edge', 'DevToolsActivePort'));
|
|
129
|
+
}
|
|
130
|
+
for (const filePath of candidates) {
|
|
131
|
+
try {
|
|
132
|
+
const content = fs.readFileSync(filePath, 'utf-8').trim();
|
|
133
|
+
const lines = content.split('\n');
|
|
134
|
+
if (lines.length >= 2) {
|
|
135
|
+
const port = parseInt(lines[0], 10);
|
|
136
|
+
const browserPath = lines[1]; // e.g. /devtools/browser/<GUID>
|
|
137
|
+
if (port > 0 && browserPath.startsWith('/devtools/browser/')) {
|
|
138
|
+
return `ws://127.0.0.1:${port}${browserPath}`;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch { }
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
export function resolveCdpEndpoint() {
|
|
147
|
+
const envVal = process.env.OPENCLI_CDP_ENDPOINT;
|
|
148
|
+
if (envVal === '1' || envVal?.toLowerCase() === 'true') {
|
|
149
|
+
const autoDiscovered = discoverChromeEndpoint();
|
|
150
|
+
return { endpoint: autoDiscovered ?? envVal, requestedCdp: true };
|
|
151
|
+
}
|
|
152
|
+
if (envVal) {
|
|
153
|
+
return { endpoint: envVal, requestedCdp: true };
|
|
154
|
+
}
|
|
155
|
+
// Fallback to auto-discovery if not explicitly set
|
|
156
|
+
const autoDiscovered = discoverChromeEndpoint();
|
|
157
|
+
if (autoDiscovered) {
|
|
158
|
+
return { endpoint: autoDiscovered, requestedCdp: true };
|
|
159
|
+
}
|
|
160
|
+
return { requestedCdp: false };
|
|
161
|
+
}
|
|
101
162
|
function buildRuntimeArgs(input) {
|
|
102
163
|
const args = [];
|
|
164
|
+
// Priority 1: CDP endpoint (remote Chrome debugging or local Auto-Discovery)
|
|
165
|
+
if (input?.cdpEndpoint) {
|
|
166
|
+
args.push('--cdp-endpoint', input.cdpEndpoint);
|
|
167
|
+
return args;
|
|
168
|
+
}
|
|
169
|
+
// Priority 2: Extension mode (local Chrome with MCP Bridge extension)
|
|
103
170
|
if (!process.env.CI) {
|
|
104
|
-
// Local: always connect to user's running Chrome via MCP Bridge extension
|
|
105
171
|
args.push('--extension');
|
|
106
172
|
}
|
|
107
|
-
// CI
|
|
173
|
+
// CI/standalone mode: @playwright/mcp launches its own browser (headed by default).
|
|
108
174
|
// xvfb provides a virtual display for headed mode in GitHub Actions.
|
|
109
175
|
if (input?.executablePath) {
|
|
110
176
|
args.push('--executable-path', input.executablePath);
|
package/dist/browser/errors.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Browser connection error classification and formatting.
|
|
3
3
|
*/
|
|
4
|
-
export type ConnectFailureKind = 'missing-token' | 'extension-timeout' | 'extension-not-installed' | 'mcp-init' | 'process-exit' | 'unknown';
|
|
4
|
+
export type ConnectFailureKind = 'missing-token' | 'extension-timeout' | 'extension-not-installed' | 'mcp-init' | 'process-exit' | 'cdp-connection-failed' | 'unknown';
|
|
5
5
|
export type ConnectFailureInput = {
|
|
6
6
|
kind: ConnectFailureKind;
|
|
7
7
|
timeout: number;
|
|
@@ -18,4 +18,5 @@ export declare function inferConnectFailureKind(args: {
|
|
|
18
18
|
stderr: string;
|
|
19
19
|
rawMessage?: string;
|
|
20
20
|
exited?: boolean;
|
|
21
|
+
isCdpMode?: boolean;
|
|
21
22
|
}): ConnectFailureKind;
|
package/dist/browser/errors.js
CHANGED
|
@@ -11,6 +11,12 @@ export function formatBrowserConnectError(input) {
|
|
|
11
11
|
const stderr = input.stderr?.trim();
|
|
12
12
|
const suffix = stderr ? `\n\nMCP stderr:\n${stderr}` : '';
|
|
13
13
|
const tokenHint = input.tokenFingerprint ? ` Token fingerprint: ${input.tokenFingerprint}.` : '';
|
|
14
|
+
if (input.kind === 'cdp-connection-failed') {
|
|
15
|
+
return new Error(`Failed to connect to remote Chrome via CDP endpoint.\n\n` +
|
|
16
|
+
`Check if Chrome is running with remote debugging enabled (--remote-debugging-port=9222) or DevToolsActivePort is available under chrome://inspect#remote-debugging.\n` +
|
|
17
|
+
`If you specified OPENCLI_CDP_ENDPOINT=1, auto-discovery might have failed.` +
|
|
18
|
+
suffix);
|
|
19
|
+
}
|
|
14
20
|
if (input.kind === 'missing-token') {
|
|
15
21
|
return new Error('Failed to connect to Playwright MCP Bridge: PLAYWRIGHT_MCP_EXTENSION_TOKEN is not set.\n\n' +
|
|
16
22
|
'Without this token, Chrome will show a manual approval dialog for every new MCP connection. ' +
|
|
@@ -42,6 +48,13 @@ export function formatBrowserConnectError(input) {
|
|
|
42
48
|
}
|
|
43
49
|
export function inferConnectFailureKind(args) {
|
|
44
50
|
const haystack = `${args.rawMessage ?? ''}\n${args.stderr}`.toLowerCase();
|
|
51
|
+
if (args.isCdpMode) {
|
|
52
|
+
if (args.rawMessage?.startsWith('MCP init failed:'))
|
|
53
|
+
return 'mcp-init';
|
|
54
|
+
if (args.exited)
|
|
55
|
+
return 'cdp-connection-failed';
|
|
56
|
+
return 'cdp-connection-failed';
|
|
57
|
+
}
|
|
45
58
|
if (!args.hasExtensionToken)
|
|
46
59
|
return 'missing-token';
|
|
47
60
|
if (haystack.includes('extension connection timeout') || haystack.includes('playwright mcp bridge'))
|
package/dist/browser/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export { Page } from './page.js';
|
|
|
8
8
|
export { PlaywrightMCP } from './mcp.js';
|
|
9
9
|
export { getTokenFingerprint, formatBrowserConnectError } from './errors.js';
|
|
10
10
|
export type { ConnectFailureKind, ConnectFailureInput } from './errors.js';
|
|
11
|
+
export { resolveCdpEndpoint } from './discover.js';
|
|
11
12
|
import { createJsonRpcRequest } from './mcp.js';
|
|
12
13
|
import { extractTabEntries, diffTabIndexes, appendLimited } from './tabs.js';
|
|
13
14
|
import { buildMcpArgs, buildMcpLaunchSpec, findMcpServerPath, resetMcpServerPathCache, setMcpDiscoveryTestHooks } from './discover.js';
|
package/dist/browser/index.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
export { Page } from './page.js';
|
|
8
8
|
export { PlaywrightMCP } from './mcp.js';
|
|
9
9
|
export { getTokenFingerprint, formatBrowserConnectError } from './errors.js';
|
|
10
|
+
export { resolveCdpEndpoint } from './discover.js';
|
|
10
11
|
// Test-only helpers — exposed for unit tests
|
|
11
12
|
import { createJsonRpcRequest } from './mcp.js';
|
|
12
13
|
import { extractTabEntries, diffTabIndexes, appendLimited } from './tabs.js';
|
package/dist/browser/mcp.js
CHANGED
|
@@ -7,7 +7,7 @@ import { withTimeoutMs, DEFAULT_BROWSER_CONNECT_TIMEOUT } from '../runtime.js';
|
|
|
7
7
|
import { PKG_VERSION } from '../version.js';
|
|
8
8
|
import { Page } from './page.js';
|
|
9
9
|
import { getTokenFingerprint, formatBrowserConnectError, inferConnectFailureKind } from './errors.js';
|
|
10
|
-
import { findMcpServerPath, buildMcpLaunchSpec } from './discover.js';
|
|
10
|
+
import { findMcpServerPath, buildMcpLaunchSpec, resolveCdpEndpoint } from './discover.js';
|
|
11
11
|
import { extractTabIdentities, extractTabEntries, diffTabIndexes, appendLimited } from './tabs.js';
|
|
12
12
|
const STDERR_BUFFER_LIMIT = 16 * 1024;
|
|
13
13
|
const INITIAL_TABS_TIMEOUT_MS = 1500;
|
|
@@ -109,7 +109,8 @@ export class PlaywrightMCP {
|
|
|
109
109
|
return new Promise((resolve, reject) => {
|
|
110
110
|
const isDebug = process.env.DEBUG?.includes('opencli:mcp');
|
|
111
111
|
const debugLog = (msg) => isDebug && console.error(`[opencli:mcp] ${msg}`);
|
|
112
|
-
const
|
|
112
|
+
const { endpoint: cdpEndpoint, requestedCdp } = resolveCdpEndpoint();
|
|
113
|
+
const useExtension = !requestedCdp;
|
|
113
114
|
const extensionToken = process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN;
|
|
114
115
|
const tokenFingerprint = getTokenFingerprint(extensionToken);
|
|
115
116
|
let stderrBuffer = '';
|
|
@@ -144,14 +145,16 @@ export class PlaywrightMCP {
|
|
|
144
145
|
settleError(inferConnectFailureKind({
|
|
145
146
|
hasExtensionToken: !!extensionToken,
|
|
146
147
|
stderr: stderrBuffer,
|
|
148
|
+
isCdpMode: requestedCdp,
|
|
147
149
|
}));
|
|
148
150
|
}, timeout * 1000);
|
|
149
151
|
const launchSpec = buildMcpLaunchSpec({
|
|
150
152
|
mcpPath,
|
|
151
153
|
executablePath: process.env.OPENCLI_BROWSER_EXECUTABLE_PATH,
|
|
154
|
+
cdpEndpoint,
|
|
152
155
|
});
|
|
153
156
|
if (process.env.OPENCLI_VERBOSE) {
|
|
154
|
-
console.error(`[opencli] Mode: ${useExtension ? 'extension' : 'standalone'}`);
|
|
157
|
+
console.error(`[opencli] Mode: ${requestedCdp ? 'CDP' : useExtension ? 'extension' : 'standalone'}`);
|
|
155
158
|
if (useExtension)
|
|
156
159
|
console.error(`[opencli] Extension token: fingerprint ${tokenFingerprint}`);
|
|
157
160
|
if (launchSpec.usedNpxFallback) {
|
|
@@ -210,6 +213,7 @@ export class PlaywrightMCP {
|
|
|
210
213
|
hasExtensionToken: !!extensionToken,
|
|
211
214
|
stderr: stderrBuffer,
|
|
212
215
|
exited: true,
|
|
216
|
+
isCdpMode: requestedCdp,
|
|
213
217
|
}), { exitCode: code });
|
|
214
218
|
}
|
|
215
219
|
});
|
|
@@ -226,6 +230,7 @@ export class PlaywrightMCP {
|
|
|
226
230
|
hasExtensionToken: !!extensionToken,
|
|
227
231
|
stderr: stderrBuffer,
|
|
228
232
|
rawMessage: `MCP init failed: ${resp.error.message}`,
|
|
233
|
+
isCdpMode: requestedCdp,
|
|
229
234
|
}), { rawMessage: resp.error.message });
|
|
230
235
|
return;
|
|
231
236
|
}
|
package/dist/browser/page.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { formatSnapshot } from '../snapshotFormatter.js';
|
|
5
5
|
import { normalizeEvaluateSource } from '../pipeline/template.js';
|
|
6
6
|
import { generateInterceptorJs, generateReadInterceptedJs } from '../interceptor.js';
|
|
7
|
+
import { BrowserConnectError } from '../errors.js';
|
|
7
8
|
/**
|
|
8
9
|
* Page abstraction wrapping JSON-RPC calls to Playwright MCP.
|
|
9
10
|
*/
|
|
@@ -18,10 +19,18 @@ export class Page {
|
|
|
18
19
|
throw new Error(`page.${method}: ${resp.error.message ?? JSON.stringify(resp.error)}`);
|
|
19
20
|
// Extract text content from MCP result
|
|
20
21
|
const result = resp.result;
|
|
22
|
+
if (result?.isError) {
|
|
23
|
+
const errorText = result.content?.find((c) => c.type === 'text')?.text || 'Unknown MCP Error';
|
|
24
|
+
throw new BrowserConnectError(errorText, 'Please check if the browser is running or if the Playwright MCP / CDP connection is configured correctly.');
|
|
25
|
+
}
|
|
21
26
|
if (result?.content) {
|
|
22
27
|
const textParts = result.content.filter((c) => c.type === 'text');
|
|
23
|
-
if (textParts.length
|
|
24
|
-
let text = textParts[
|
|
28
|
+
if (textParts.length >= 1) {
|
|
29
|
+
let text = textParts[textParts.length - 1].text; // Usually the main output is in the last text block
|
|
30
|
+
// Some versions of the MCP return error text without the `isError` boolean flag
|
|
31
|
+
if (typeof text === 'string' && text.trim().startsWith('### Error')) {
|
|
32
|
+
throw new BrowserConnectError(text.trim(), 'Please check if the browser is running or if the Playwright MCP / CDP connection is configured correctly.');
|
|
33
|
+
}
|
|
25
34
|
// MCP browser_evaluate returns: "[JSON]\n### Ran Playwright code\n```js\n...\n```"
|
|
26
35
|
// Strip the "### Ran Playwright code" suffix to get clean JSON
|
|
27
36
|
const codeMarker = text.indexOf('### Ran Playwright code');
|