@jackwener/opencli 0.4.6 → 0.5.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 +6 -12
- package/README.zh-CN.md +7 -13
- package/SKILL.md +3 -7
- package/dist/browser.d.ts +3 -16
- package/dist/browser.js +42 -194
- package/dist/browser.test.js +7 -6
- package/dist/clis/boss/search.js +0 -1
- package/dist/clis/v2ex/daily.js +0 -1
- package/dist/clis/v2ex/me.js +0 -1
- package/dist/clis/v2ex/notifications.js +0 -1
- package/dist/doctor.d.ts +0 -5
- package/dist/doctor.js +4 -62
- package/dist/doctor.test.js +1 -13
- package/dist/main.js +1 -1
- package/dist/registry.d.ts +0 -4
- package/dist/registry.js +0 -1
- package/dist/runtime.d.ts +1 -3
- package/dist/runtime.js +2 -2
- package/package.json +4 -1
- package/src/browser.test.ts +11 -8
- package/src/browser.ts +49 -208
- package/src/clis/boss/search.ts +1 -1
- package/src/clis/v2ex/daily.ts +1 -1
- package/src/clis/v2ex/me.ts +1 -1
- package/src/clis/v2ex/notifications.ts +1 -1
- package/src/doctor.test.ts +1 -13
- package/src/doctor.ts +5 -60
- package/src/main.ts +1 -1
- package/src/registry.ts +0 -7
- package/src/runtime.ts +1 -2
package/README.md
CHANGED
|
@@ -41,9 +41,9 @@ A CLI tool that turns **any website** into a command-line interface. **57 comman
|
|
|
41
41
|
|
|
42
42
|
> **⚠️ Important**: Browser commands reuse your Chrome login session. You must be logged into the target website in Chrome before running commands. If you get empty data or errors, check your login status first.
|
|
43
43
|
|
|
44
|
-
OpenCLI
|
|
44
|
+
OpenCLI connects to your browser through the Playwright MCP Bridge extension.
|
|
45
45
|
|
|
46
|
-
###
|
|
46
|
+
### Playwright MCP Bridge Extension Setup
|
|
47
47
|
|
|
48
48
|
1. Install **[Playwright MCP Bridge](https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm)** extension in Chrome.
|
|
49
49
|
2. Obtain your token by clicking the extension icon in the browser toolbar or from the extension settings page.
|
|
@@ -72,15 +72,11 @@ And, so that `opencli` commands can use it directly in the terminal, export it i
|
|
|
72
72
|
export PLAYWRIGHT_MCP_EXTENSION_TOKEN="<your-token-here>"
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
After configuring, run `opencli doctor` to verify your token is correctly set up across all locations:
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
2. Check **"Allow remote debugging for this browser instance"**
|
|
81
|
-
3. Set `OPENCLI_USE_CDP=1` before running opencli
|
|
82
|
-
|
|
83
|
-
*You can also manually specify an endpoint via `OPENCLI_CDP_ENDPOINT` env var.*
|
|
77
|
+
```bash
|
|
78
|
+
opencli doctor
|
|
79
|
+
```
|
|
84
80
|
|
|
85
81
|
## Quick Start
|
|
86
82
|
|
|
@@ -184,8 +180,6 @@ Explore outputs to `.opencli/explore/<site>/` (manifest.json, endpoints.json, ca
|
|
|
184
180
|
- **"Failed to connect to Playwright MCP Bridge"**
|
|
185
181
|
- Ensure the Playwright MCP extension is installed and **enabled** in your running Chrome.
|
|
186
182
|
- Restart the Chrome browser if you just installed the extension.
|
|
187
|
-
- **"CDP command failed" or "boss search blocked"**
|
|
188
|
-
- Some sites (like BOSS Zhipin) actively block Chrome DevTools Protocol connections. OpenCLI falls back to cookie extraction, but ensure you didn't force `--chrome-mode` unnecessarily.
|
|
189
183
|
- **Empty data returns or 'Unauthorized' error**
|
|
190
184
|
- Your login session in Chrome might have expired. Open a normal Chrome tab, navigate to the target site, and log in or refresh the page to prove you are human.
|
|
191
185
|
- **Node API errors**
|
package/README.zh-CN.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://nodejs.org)
|
|
10
10
|
[](./LICENSE)
|
|
11
11
|
|
|
12
|
-
OpenCLI
|
|
12
|
+
OpenCLI 将任何网站变成命令行工具。**57 个命令**覆盖 **17 个站点** — B站、知乎、小红书、Twitter、Reddit、雪球、GitHub、V2EX、Hacker News、BBC、微博、BOSS直聘、Yahoo Finance、路透社、什么值得买、携程、YouTube — 复用浏览器登录态,AI 驱动探索。
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
@@ -41,9 +41,9 @@ OpenCLI 通过 Chrome 浏览器 + [Playwright MCP Bridge](https://github.com/nic
|
|
|
41
41
|
|
|
42
42
|
> **⚠️ 重要**:大多数命令复用你的 Chrome 登录状态。运行命令前,你必须已在 Chrome 中打开目标网站并完成登录。如果获取到空数据或报错,请先检查你的浏览器登录状态。
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
OpenCLI 通过 Playwright MCP Bridge 扩展与你的浏览器通信。
|
|
45
45
|
|
|
46
|
-
###
|
|
46
|
+
### Playwright MCP Bridge 扩展配置
|
|
47
47
|
|
|
48
48
|
1. 安装 **[Playwright MCP Bridge](https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm)** 扩展
|
|
49
49
|
2. 在浏览器插件栏点击该插件,或者在插件设置页获取你的 Extension Token。
|
|
@@ -72,15 +72,11 @@ OpenCLI 通过 Chrome 浏览器 + [Playwright MCP Bridge](https://github.com/nic
|
|
|
72
72
|
export PLAYWRIGHT_MCP_EXTENSION_TOKEN="<你的-token>"
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
配置完成后,运行 `opencli doctor` 检测你的 Token 是否在所有位置都正确配置:
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
2. 勾选 **"允许对此浏览器实例进行远程调试" (Allow remote debugging for this browser instance)**
|
|
81
|
-
3. 运行时设置环境变量 `OPENCLI_USE_CDP=1`
|
|
82
|
-
|
|
83
|
-
*也可通过 `OPENCLI_CDP_ENDPOINT` 环境变量手动指定 CDP endpoint 地址。*
|
|
77
|
+
```bash
|
|
78
|
+
opencli doctor
|
|
79
|
+
```
|
|
84
80
|
|
|
85
81
|
## 快速开始
|
|
86
82
|
|
|
@@ -184,8 +180,6 @@ opencli cascade https://api.example.com/data
|
|
|
184
180
|
- **"Failed to connect to Playwright MCP Bridge"** 报错
|
|
185
181
|
- 确保你当前的 Chrome 已安装且**开启了** Playwright MCP Bridge 浏览器插件。
|
|
186
182
|
- 如果是刚装完插件,需要重启 Chrome 浏览器。
|
|
187
|
-
- **"CDP command failed" / "被风控拦截"**
|
|
188
|
-
- 有些网站(例如 BOSS 直聘)会因为开了 DevTools 或者 CDP 端口拦截验证。OpenCLI 有 cookie 降级机制,通常不需要干预,不用去强行加上 CDP 标识参数即可。
|
|
189
183
|
- **返回空数据,或者报错 "Unauthorized"**
|
|
190
184
|
- Chrome 里的登录态可能已经过期(甚至被要求过滑动验证码)。请打开当前 Chrome 页面,在新标签页重新手工登录或刷新该页面。
|
|
191
185
|
- **Node API 错误 (如 parseArgs, fs 等)**
|
package/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: opencli
|
|
3
3
|
description: "OpenCLI — Make any website your CLI. Zero risk, AI-powered, reuse Chrome login."
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
author: jackwener
|
|
6
6
|
tags: [cli, browser, web, mcp, playwright, bilibili, zhihu, twitter, github, v2ex, hackernews, reddit, xiaohongshu, xueqiu, AI, agent]
|
|
7
7
|
---
|
|
@@ -34,8 +34,7 @@ npm update -g @jackwener/opencli
|
|
|
34
34
|
|
|
35
35
|
Browser commands require:
|
|
36
36
|
1. Chrome browser running **(logged into target sites)**
|
|
37
|
-
2. [Playwright MCP Bridge](https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm) extension
|
|
38
|
-
3. **Alternative**: Chrome 144+ CDP auto-discovery — set `OPENCLI_USE_CDP=1` (no extension needed)
|
|
37
|
+
2. [Playwright MCP Bridge](https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm) extension installed and configured
|
|
39
38
|
|
|
40
39
|
> **Note**: You must be logged into the target website in Chrome before running commands. Tabs opened during command execution are auto-closed afterwards.
|
|
41
40
|
|
|
@@ -339,9 +338,6 @@ ${{ index + 1 }}
|
|
|
339
338
|
| `OPENCLI_BROWSER_CONNECT_TIMEOUT` | 30 | Browser connection timeout (sec) |
|
|
340
339
|
| `OPENCLI_BROWSER_COMMAND_TIMEOUT` | 45 | Command execution timeout (sec) |
|
|
341
340
|
| `OPENCLI_BROWSER_EXPLORE_TIMEOUT` | 120 | Explore timeout (sec) |
|
|
342
|
-
| `OPENCLI_CDP_ENDPOINT` | — | Manual CDP WebSocket endpoint (overrides auto-discovery) |
|
|
343
|
-
| `OPENCLI_USE_CDP` | — | Set to `1` to use Chrome 144+ CDP auto-discovery instead of extension |
|
|
344
|
-
| `OPENCLI_FORCE_EXTENSION` | — | Set to `1` to skip CDP and force extension mode |
|
|
345
341
|
| `PLAYWRIGHT_MCP_EXTENSION_TOKEN` | — | Auto-approve extension connection |
|
|
346
342
|
|
|
347
343
|
## Troubleshooting
|
|
@@ -349,6 +345,6 @@ ${{ index + 1 }}
|
|
|
349
345
|
| Issue | Solution |
|
|
350
346
|
|-------|----------|
|
|
351
347
|
| `npx not found` | Install Node.js: `brew install node` |
|
|
352
|
-
| `Timed out connecting to browser` | 1) Chrome must be open 2)
|
|
348
|
+
| `Timed out connecting to browser` | 1) Chrome must be open 2) Install MCP Bridge extension and configure token |
|
|
353
349
|
| `Target page context` error | Add `navigate:` step before `evaluate:` in YAML |
|
|
354
350
|
| Empty table data | Check if evaluate returns JSON string (MCP parsing) or data path is wrong |
|
package/dist/browser.d.ts
CHANGED
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Browser interaction via
|
|
3
|
-
* Connects to an existing Chrome browser through
|
|
2
|
+
* Browser interaction via Playwright MCP Bridge extension.
|
|
3
|
+
* Connects to an existing Chrome browser through the extension.
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
|
-
* Verify the CDP HTTP JSON API is functional.
|
|
7
|
-
* Chrome's chrome://inspect#remote-debugging mode writes DevToolsActivePort
|
|
8
|
-
* but doesn't expose the full CDP HTTP API (/json/version), which means
|
|
9
|
-
* Playwright's connectOverCDP won't work properly (init succeeds but
|
|
10
|
-
* all tool calls hang silently).
|
|
11
|
-
*/
|
|
12
|
-
export declare function isCdpApiAvailable(port: number, host?: string, timeoutMs?: number): Promise<boolean>;
|
|
13
|
-
export declare function discoverChromeEndpoint(): Promise<string | null>;
|
|
14
|
-
type ConnectFailureKind = 'missing-token' | 'extension-timeout' | 'extension-not-installed' | 'cdp-timeout' | 'mcp-init' | 'process-exit' | 'unknown';
|
|
5
|
+
type ConnectFailureKind = 'missing-token' | 'extension-timeout' | 'extension-not-installed' | 'mcp-init' | 'process-exit' | 'unknown';
|
|
15
6
|
type PlaywrightMCPState = 'idle' | 'connecting' | 'connected' | 'closing' | 'closed';
|
|
16
7
|
type ConnectFailureInput = {
|
|
17
8
|
kind: ConnectFailureKind;
|
|
18
|
-
mode: 'extension' | 'cdp';
|
|
19
9
|
timeout: number;
|
|
20
10
|
hasExtensionToken: boolean;
|
|
21
11
|
tokenFingerprint?: string | null;
|
|
@@ -81,7 +71,6 @@ export declare class PlaywrightMCP {
|
|
|
81
71
|
private _initialTabIdentities;
|
|
82
72
|
private _closingPromise;
|
|
83
73
|
private _state;
|
|
84
|
-
private _mode;
|
|
85
74
|
private _page;
|
|
86
75
|
get state(): PlaywrightMCPState;
|
|
87
76
|
private _sendRequest;
|
|
@@ -89,7 +78,6 @@ export declare class PlaywrightMCP {
|
|
|
89
78
|
private _resetAfterFailedConnect;
|
|
90
79
|
connect(opts?: {
|
|
91
80
|
timeout?: number;
|
|
92
|
-
forceExtension?: boolean;
|
|
93
81
|
}): Promise<Page>;
|
|
94
82
|
close(): Promise<void>;
|
|
95
83
|
}
|
|
@@ -109,6 +97,5 @@ export declare const __test__: {
|
|
|
109
97
|
diffTabIndexes: typeof diffTabIndexes;
|
|
110
98
|
appendLimited: typeof appendLimited;
|
|
111
99
|
withTimeout: typeof withTimeout;
|
|
112
|
-
isCdpApiAvailable: typeof isCdpApiAvailable;
|
|
113
100
|
};
|
|
114
101
|
export {};
|
package/dist/browser.js
CHANGED
|
@@ -1,109 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Browser interaction via
|
|
3
|
-
* Connects to an existing Chrome browser through
|
|
2
|
+
* Browser interaction via Playwright MCP Bridge extension.
|
|
3
|
+
* Connects to an existing Chrome browser through the extension.
|
|
4
4
|
*/
|
|
5
5
|
import { spawn, execSync } from 'node:child_process';
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
7
|
-
import * as http from 'node:http';
|
|
8
|
-
import * as net from 'node:net';
|
|
9
7
|
import { fileURLToPath } from 'node:url';
|
|
10
8
|
import * as fs from 'node:fs';
|
|
11
9
|
import * as os from 'node:os';
|
|
12
10
|
import * as path from 'node:path';
|
|
13
11
|
import { formatSnapshot } from './snapshotFormatter.js';
|
|
14
|
-
/**
|
|
15
|
-
* Chrome 144+ auto-discovery: read DevToolsActivePort file to get CDP endpoint.
|
|
16
|
-
*
|
|
17
|
-
* Starting with Chrome 144, users can enable remote debugging from
|
|
18
|
-
* chrome://inspect#remote-debugging without any command-line flags.
|
|
19
|
-
* Chrome writes the active port and browser GUID to a DevToolsActivePort file
|
|
20
|
-
* in the user data directory, which we read to construct the WebSocket endpoint.
|
|
21
|
-
*
|
|
22
|
-
* Priority: OPENCLI_CDP_ENDPOINT env > DevToolsActivePort auto-discovery > --extension fallback
|
|
23
|
-
*/
|
|
24
|
-
/** Quick TCP port probe to verify Chrome is actually listening */
|
|
25
|
-
function isPortReachable(port, host = '127.0.0.1', timeoutMs = 800) {
|
|
26
|
-
return new Promise(resolve => {
|
|
27
|
-
const sock = net.createConnection({ port, host });
|
|
28
|
-
sock.setTimeout(timeoutMs);
|
|
29
|
-
sock.on('connect', () => { sock.destroy(); resolve(true); });
|
|
30
|
-
sock.on('error', () => resolve(false));
|
|
31
|
-
sock.on('timeout', () => { sock.destroy(); resolve(false); });
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Verify the CDP HTTP JSON API is functional.
|
|
36
|
-
* Chrome's chrome://inspect#remote-debugging mode writes DevToolsActivePort
|
|
37
|
-
* but doesn't expose the full CDP HTTP API (/json/version), which means
|
|
38
|
-
* Playwright's connectOverCDP won't work properly (init succeeds but
|
|
39
|
-
* all tool calls hang silently).
|
|
40
|
-
*/
|
|
41
|
-
export function isCdpApiAvailable(port, host = '127.0.0.1', timeoutMs = 2000) {
|
|
42
|
-
return new Promise(resolve => {
|
|
43
|
-
const req = http.get(`http://${host}:${port}/json/version`, { timeout: timeoutMs }, (res) => {
|
|
44
|
-
let body = '';
|
|
45
|
-
res.on('data', (chunk) => { body += chunk.toString(); });
|
|
46
|
-
res.on('end', () => {
|
|
47
|
-
try {
|
|
48
|
-
const data = JSON.parse(body);
|
|
49
|
-
// A valid CDP endpoint returns { Browser, ... } with a webSocketDebuggerUrl
|
|
50
|
-
resolve(!!data && typeof data === 'object' && !!data.Browser);
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
resolve(false);
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
req.on('error', () => resolve(false));
|
|
58
|
-
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
export async function discoverChromeEndpoint() {
|
|
62
|
-
const candidates = [];
|
|
63
|
-
// User-specified Chrome data dir takes highest priority
|
|
64
|
-
if (process.env.CHROME_USER_DATA_DIR) {
|
|
65
|
-
candidates.push(path.join(process.env.CHROME_USER_DATA_DIR, 'DevToolsActivePort'));
|
|
66
|
-
}
|
|
67
|
-
// Standard Chrome/Edge user data dirs per platform
|
|
68
|
-
if (process.platform === 'win32') {
|
|
69
|
-
const localAppData = process.env.LOCALAPPDATA ?? path.join(os.homedir(), 'AppData', 'Local');
|
|
70
|
-
candidates.push(path.join(localAppData, 'Google', 'Chrome', 'User Data', 'DevToolsActivePort'));
|
|
71
|
-
candidates.push(path.join(localAppData, 'Microsoft', 'Edge', 'User Data', 'DevToolsActivePort'));
|
|
72
|
-
}
|
|
73
|
-
else if (process.platform === 'darwin') {
|
|
74
|
-
candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome', 'DevToolsActivePort'));
|
|
75
|
-
candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'Microsoft Edge', 'DevToolsActivePort'));
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
candidates.push(path.join(os.homedir(), '.config', 'google-chrome', 'DevToolsActivePort'));
|
|
79
|
-
candidates.push(path.join(os.homedir(), '.config', 'chromium', 'DevToolsActivePort'));
|
|
80
|
-
candidates.push(path.join(os.homedir(), '.config', 'microsoft-edge', 'DevToolsActivePort'));
|
|
81
|
-
}
|
|
82
|
-
for (const filePath of candidates) {
|
|
83
|
-
try {
|
|
84
|
-
const content = fs.readFileSync(filePath, 'utf-8').trim();
|
|
85
|
-
const lines = content.split('\n');
|
|
86
|
-
if (lines.length >= 2) {
|
|
87
|
-
const port = parseInt(lines[0], 10);
|
|
88
|
-
const browserPath = lines[1]; // e.g. /devtools/browser/<GUID>
|
|
89
|
-
if (port > 0 && browserPath.startsWith('/devtools/browser/')) {
|
|
90
|
-
const endpoint = `ws://127.0.0.1:${port}${browserPath}`;
|
|
91
|
-
// Verify the port is actually reachable (Chrome may have closed, leaving a stale file)
|
|
92
|
-
if (await isPortReachable(port)) {
|
|
93
|
-
// Verify CDP HTTP API is functional — chrome://inspect#remote-debugging
|
|
94
|
-
// writes DevToolsActivePort but doesn't expose the full CDP API,
|
|
95
|
-
// causing Playwright connectOverCDP to hang on all tool calls.
|
|
96
|
-
if (await isCdpApiAvailable(port)) {
|
|
97
|
-
return endpoint;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
catch { }
|
|
104
|
-
}
|
|
105
|
-
return null;
|
|
106
|
-
}
|
|
107
12
|
// Read version from package.json (single source of truth)
|
|
108
13
|
const __browser_dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
109
14
|
const PKG_VERSION = (() => { try {
|
|
@@ -115,7 +20,6 @@ catch {
|
|
|
115
20
|
const CONNECT_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_CONNECT_TIMEOUT ?? '30', 10);
|
|
116
21
|
const STDERR_BUFFER_LIMIT = 16 * 1024;
|
|
117
22
|
const INITIAL_TABS_TIMEOUT_MS = 1500;
|
|
118
|
-
const CDP_READINESS_PROBE_TIMEOUT_MS = 5000;
|
|
119
23
|
const TAB_CLEANUP_TIMEOUT_MS = 2000;
|
|
120
24
|
let _cachedMcpServerPath;
|
|
121
25
|
export function getTokenFingerprint(token) {
|
|
@@ -127,31 +31,24 @@ export function formatBrowserConnectError(input) {
|
|
|
127
31
|
const stderr = input.stderr?.trim();
|
|
128
32
|
const suffix = stderr ? `\n\nMCP stderr:\n${stderr}` : '';
|
|
129
33
|
const tokenHint = input.tokenFingerprint ? ` Token fingerprint: ${input.tokenFingerprint}.` : '';
|
|
130
|
-
if (input.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
suffix);
|
|
142
|
-
}
|
|
143
|
-
if (input.kind === 'extension-timeout') {
|
|
144
|
-
const likelyCause = input.hasExtensionToken
|
|
145
|
-
? `The most likely cause is that PLAYWRIGHT_MCP_EXTENSION_TOKEN does not match the token currently shown by the browser extension.${tokenHint} Re-copy the token from the extension and update BOTH your shell environment and MCP client config.`
|
|
146
|
-
: 'PLAYWRIGHT_MCP_EXTENSION_TOKEN is not configured, so the extension may be waiting for manual approval.';
|
|
147
|
-
return new Error(`Timed out connecting to Playwright MCP Bridge (${input.timeout}s).\n\n` +
|
|
148
|
-
`${likelyCause} If a browser prompt is visible, click Allow. You can also switch to Chrome remote debugging mode with OPENCLI_USE_CDP=1 as a fallback.` +
|
|
149
|
-
suffix);
|
|
150
|
-
}
|
|
34
|
+
if (input.kind === 'missing-token') {
|
|
35
|
+
return new Error('Failed to connect to Playwright MCP Bridge: PLAYWRIGHT_MCP_EXTENSION_TOKEN is not set.\n\n' +
|
|
36
|
+
'Without this token, Chrome will show a manual approval dialog for every new MCP connection. ' +
|
|
37
|
+
'Copy the token from the Playwright MCP Bridge extension and set it in BOTH your shell environment and MCP client config.' +
|
|
38
|
+
suffix);
|
|
39
|
+
}
|
|
40
|
+
if (input.kind === 'extension-not-installed') {
|
|
41
|
+
return new Error('Failed to connect to Playwright MCP Bridge: the browser extension did not attach.\n\n' +
|
|
42
|
+
'Make sure Chrome is running and the "Playwright MCP Bridge" extension is installed and enabled. ' +
|
|
43
|
+
'If Chrome shows an approval dialog, click Allow.' +
|
|
44
|
+
suffix);
|
|
151
45
|
}
|
|
152
|
-
if (input.
|
|
153
|
-
|
|
154
|
-
|
|
46
|
+
if (input.kind === 'extension-timeout') {
|
|
47
|
+
const likelyCause = input.hasExtensionToken
|
|
48
|
+
? `The most likely cause is that PLAYWRIGHT_MCP_EXTENSION_TOKEN does not match the token currently shown by the browser extension.${tokenHint} Re-copy the token from the extension and update BOTH your shell environment and MCP client config.`
|
|
49
|
+
: 'PLAYWRIGHT_MCP_EXTENSION_TOKEN is not configured, so the extension may be waiting for manual approval.';
|
|
50
|
+
return new Error(`Timed out connecting to Playwright MCP Bridge (${input.timeout}s).\n\n` +
|
|
51
|
+
`${likelyCause} If a browser prompt is visible, click Allow.` +
|
|
155
52
|
suffix);
|
|
156
53
|
}
|
|
157
54
|
if (input.kind === 'mcp-init') {
|
|
@@ -165,7 +62,7 @@ export function formatBrowserConnectError(input) {
|
|
|
165
62
|
}
|
|
166
63
|
function inferConnectFailureKind(args) {
|
|
167
64
|
const haystack = `${args.rawMessage ?? ''}\n${args.stderr}`.toLowerCase();
|
|
168
|
-
if (
|
|
65
|
+
if (!args.hasExtensionToken)
|
|
169
66
|
return 'missing-token';
|
|
170
67
|
if (haystack.includes('extension connection timeout') || haystack.includes('playwright mcp bridge'))
|
|
171
68
|
return 'extension-not-installed';
|
|
@@ -173,11 +70,7 @@ function inferConnectFailureKind(args) {
|
|
|
173
70
|
return 'mcp-init';
|
|
174
71
|
if (args.exited)
|
|
175
72
|
return 'process-exit';
|
|
176
|
-
|
|
177
|
-
return 'extension-timeout';
|
|
178
|
-
if (args.mode === 'cdp')
|
|
179
|
-
return 'cdp-timeout';
|
|
180
|
-
return 'unknown';
|
|
73
|
+
return 'extension-timeout';
|
|
181
74
|
}
|
|
182
75
|
// JSON-RPC helpers
|
|
183
76
|
let _nextId = 1;
|
|
@@ -413,7 +306,6 @@ export class PlaywrightMCP {
|
|
|
413
306
|
_initialTabIdentities = [];
|
|
414
307
|
_closingPromise = null;
|
|
415
308
|
_state = 'idle';
|
|
416
|
-
_mode = null;
|
|
417
309
|
_page = null;
|
|
418
310
|
get state() {
|
|
419
311
|
return this._state;
|
|
@@ -445,7 +337,6 @@ export class PlaywrightMCP {
|
|
|
445
337
|
this._page = null;
|
|
446
338
|
this._proc = null;
|
|
447
339
|
this._buffer = '';
|
|
448
|
-
this._mode = null;
|
|
449
340
|
this._initialTabIdentities = [];
|
|
450
341
|
this._rejectPendingRequests(new Error('Playwright MCP connect failed'));
|
|
451
342
|
PlaywrightMCP._activeInsts.delete(this);
|
|
@@ -472,26 +363,9 @@ export class PlaywrightMCP {
|
|
|
472
363
|
PlaywrightMCP._activeInsts.add(this);
|
|
473
364
|
this._state = 'connecting';
|
|
474
365
|
const timeout = opts.timeout ?? CONNECT_TIMEOUT;
|
|
475
|
-
// Connection priority:
|
|
476
|
-
// 1. OPENCLI_CDP_ENDPOINT env var → explicit CDP endpoint
|
|
477
|
-
// 2. OPENCLI_USE_CDP=1 → auto-discover via DevToolsActivePort
|
|
478
|
-
// 3. Default → --extension mode (Playwright MCP Bridge)
|
|
479
|
-
// Some anti-bot sites (e.g. BOSS Zhipin) detect CDP — use forceExtension to bypass.
|
|
480
|
-
const forceExt = opts.forceExtension || process.env.OPENCLI_FORCE_EXTENSION === '1';
|
|
481
|
-
let cdpEndpoint = null;
|
|
482
|
-
if (!forceExt) {
|
|
483
|
-
if (process.env.OPENCLI_CDP_ENDPOINT) {
|
|
484
|
-
cdpEndpoint = process.env.OPENCLI_CDP_ENDPOINT;
|
|
485
|
-
}
|
|
486
|
-
else if (process.env.OPENCLI_USE_CDP === '1') {
|
|
487
|
-
cdpEndpoint = await discoverChromeEndpoint();
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
366
|
return new Promise((resolve, reject) => {
|
|
491
367
|
const isDebug = process.env.DEBUG?.includes('opencli:mcp');
|
|
492
368
|
const debugLog = (msg) => isDebug && console.error(`[opencli:mcp] ${msg}`);
|
|
493
|
-
const mode = cdpEndpoint ? 'cdp' : 'extension';
|
|
494
|
-
this._mode = mode;
|
|
495
369
|
const extensionToken = process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN;
|
|
496
370
|
const tokenFingerprint = getTokenFingerprint(extensionToken);
|
|
497
371
|
let stderrBuffer = '';
|
|
@@ -505,7 +379,6 @@ export class PlaywrightMCP {
|
|
|
505
379
|
this._resetAfterFailedConnect();
|
|
506
380
|
reject(formatBrowserConnectError({
|
|
507
381
|
kind,
|
|
508
|
-
mode,
|
|
509
382
|
timeout,
|
|
510
383
|
hasExtensionToken: !!extensionToken,
|
|
511
384
|
tokenFingerprint,
|
|
@@ -525,23 +398,13 @@ export class PlaywrightMCP {
|
|
|
525
398
|
const timer = setTimeout(() => {
|
|
526
399
|
debugLog('Connection timed out');
|
|
527
400
|
settleError(inferConnectFailureKind({
|
|
528
|
-
mode,
|
|
529
401
|
hasExtensionToken: !!extensionToken,
|
|
530
402
|
stderr: stderrBuffer,
|
|
531
403
|
}));
|
|
532
404
|
}, timeout * 1000);
|
|
533
|
-
const mcpArgs = [mcpPath];
|
|
534
|
-
if (cdpEndpoint) {
|
|
535
|
-
mcpArgs.push('--cdp-endpoint', cdpEndpoint);
|
|
536
|
-
}
|
|
537
|
-
else {
|
|
538
|
-
mcpArgs.push('--extension');
|
|
539
|
-
}
|
|
405
|
+
const mcpArgs = [mcpPath, '--extension'];
|
|
540
406
|
if (process.env.OPENCLI_VERBOSE) {
|
|
541
|
-
console.error(`[opencli]
|
|
542
|
-
if (mode === 'extension') {
|
|
543
|
-
console.error(`[opencli] Extension token: ${extensionToken ? `configured (fingerprint ${tokenFingerprint})` : 'missing'}`);
|
|
544
|
-
}
|
|
407
|
+
console.error(`[opencli] Extension token: ${extensionToken ? `configured (fingerprint ${tokenFingerprint})` : 'missing'}`);
|
|
545
408
|
}
|
|
546
409
|
if (process.env.OPENCLI_BROWSER_EXECUTABLE_PATH) {
|
|
547
410
|
mcpArgs.push('--executablePath', process.env.OPENCLI_BROWSER_EXECUTABLE_PATH);
|
|
@@ -595,7 +458,6 @@ export class PlaywrightMCP {
|
|
|
595
458
|
this._rejectPendingRequests(new Error(`Playwright MCP process exited before response${code == null ? '' : ` (code ${code})`}`));
|
|
596
459
|
if (!settled) {
|
|
597
460
|
settleError(inferConnectFailureKind({
|
|
598
|
-
mode,
|
|
599
461
|
hasExtensionToken: !!extensionToken,
|
|
600
462
|
stderr: stderrBuffer,
|
|
601
463
|
exited: true,
|
|
@@ -612,7 +474,6 @@ export class PlaywrightMCP {
|
|
|
612
474
|
debugLog('Got initialize response');
|
|
613
475
|
if (resp.error) {
|
|
614
476
|
settleError(inferConnectFailureKind({
|
|
615
|
-
mode,
|
|
616
477
|
hasExtensionToken: !!extensionToken,
|
|
617
478
|
stderr: stderrBuffer,
|
|
618
479
|
rawMessage: `MCP init failed: ${resp.error.message}`,
|
|
@@ -622,28 +483,7 @@ export class PlaywrightMCP {
|
|
|
622
483
|
const initializedMsg = JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n';
|
|
623
484
|
debugLog(`SEND: ${initializedMsg.trim()}`);
|
|
624
485
|
this._proc?.stdin?.write(initializedMsg);
|
|
625
|
-
|
|
626
|
-
// CDP readiness probe: verify tool calls actually work.
|
|
627
|
-
// Some CDP endpoints (e.g. chrome://inspect mode) accept WebSocket
|
|
628
|
-
// connections and respond to MCP init but silently drop tool calls.
|
|
629
|
-
debugLog('CDP readiness probe (tabs)...');
|
|
630
|
-
withTimeout(page.tabs(), CDP_READINESS_PROBE_TIMEOUT_MS, 'CDP readiness probe timed out')
|
|
631
|
-
.then(() => {
|
|
632
|
-
debugLog('CDP readiness probe succeeded');
|
|
633
|
-
settleSuccess(page);
|
|
634
|
-
})
|
|
635
|
-
.catch((err) => {
|
|
636
|
-
debugLog(`CDP readiness probe failed: ${err.message}`);
|
|
637
|
-
settleError('cdp-timeout', {
|
|
638
|
-
rawMessage: 'CDP endpoint connected but tool calls are unresponsive. ' +
|
|
639
|
-
'This usually means Chrome was opened with chrome://inspect#remote-debugging ' +
|
|
640
|
-
'which is not fully compatible. Launch Chrome with --remote-debugging-port=9222 instead, ' +
|
|
641
|
-
'or use the Playwright MCP Bridge extension (default mode).',
|
|
642
|
-
});
|
|
643
|
-
});
|
|
644
|
-
return;
|
|
645
|
-
}
|
|
646
|
-
// Extension mode uses tabs as a readiness probe and for tab cleanup bookkeeping.
|
|
486
|
+
// Use tabs as a readiness probe and for tab cleanup bookkeeping.
|
|
647
487
|
debugLog('Fetching initial tabs count...');
|
|
648
488
|
withTimeout(page.tabs(), INITIAL_TABS_TIMEOUT_MS, 'Timed out fetching initial tabs').then((tabs) => {
|
|
649
489
|
debugLog(`Tabs response: ${typeof tabs === 'string' ? tabs : JSON.stringify(tabs)}`);
|
|
@@ -668,7 +508,7 @@ export class PlaywrightMCP {
|
|
|
668
508
|
this._closingPromise = (async () => {
|
|
669
509
|
try {
|
|
670
510
|
// Extension mode opens bridge/session tabs that we can clean up best-effort.
|
|
671
|
-
if (this.
|
|
511
|
+
if (this._page && this._proc && !this._proc.killed) {
|
|
672
512
|
try {
|
|
673
513
|
const tabs = await withTimeout(this._page.tabs(), TAB_CLEANUP_TIMEOUT_MS, 'Timed out fetching tabs during cleanup');
|
|
674
514
|
const tabEntries = extractTabEntries(tabs);
|
|
@@ -707,7 +547,6 @@ export class PlaywrightMCP {
|
|
|
707
547
|
this._rejectPendingRequests(new Error('Playwright MCP session closed'));
|
|
708
548
|
this._page = null;
|
|
709
549
|
this._proc = null;
|
|
710
|
-
this._mode = null;
|
|
711
550
|
this._state = 'closed';
|
|
712
551
|
PlaywrightMCP._activeInsts.delete(this);
|
|
713
552
|
}
|
|
@@ -733,13 +572,23 @@ function extractTabEntries(raw) {
|
|
|
733
572
|
.map(line => line.trim())
|
|
734
573
|
.filter(Boolean)
|
|
735
574
|
.map(line => {
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
575
|
+
// Match actual Playwright MCP format: "- 0: (current) [title](url)" or "- 1: [title](url)"
|
|
576
|
+
const mcpMatch = line.match(/^-\s+(\d+):\s*(.*)$/);
|
|
577
|
+
if (mcpMatch) {
|
|
578
|
+
return {
|
|
579
|
+
index: parseInt(mcpMatch[1], 10),
|
|
580
|
+
identity: mcpMatch[2].trim() || `tab-${mcpMatch[1]}`,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
// Legacy format: "Tab 0 ..."
|
|
584
|
+
const legacyMatch = line.match(/Tab\s+(\d+)\s*(.*)$/);
|
|
585
|
+
if (legacyMatch) {
|
|
586
|
+
return {
|
|
587
|
+
index: parseInt(legacyMatch[1], 10),
|
|
588
|
+
identity: legacyMatch[2].trim() || `tab-${legacyMatch[1]}`,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
return null;
|
|
743
592
|
})
|
|
744
593
|
.filter((entry) => entry !== null);
|
|
745
594
|
}
|
|
@@ -790,7 +639,6 @@ export const __test__ = {
|
|
|
790
639
|
diffTabIndexes,
|
|
791
640
|
appendLimited,
|
|
792
641
|
withTimeout,
|
|
793
|
-
isCdpApiAvailable,
|
|
794
642
|
};
|
|
795
643
|
function findMcpServerPath() {
|
|
796
644
|
if (_cachedMcpServerPath !== undefined)
|
package/dist/browser.test.js
CHANGED
|
@@ -15,6 +15,13 @@ describe('browser helpers', () => {
|
|
|
15
15
|
{ index: 1, identity: 'Chrome Extension' },
|
|
16
16
|
]);
|
|
17
17
|
});
|
|
18
|
+
it('extracts tab entries from MCP markdown format', () => {
|
|
19
|
+
const entries = __test__.extractTabEntries('- 0: (current) [Playwright MCP extension](chrome-extension://abc/connect.html)\n- 1: [知乎 - 首页](https://www.zhihu.com/)');
|
|
20
|
+
expect(entries).toEqual([
|
|
21
|
+
{ index: 0, identity: '(current) [Playwright MCP extension](chrome-extension://abc/connect.html)' },
|
|
22
|
+
{ index: 1, identity: '[知乎 - 首页](https://www.zhihu.com/)' },
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
18
25
|
it('closes only tabs that were opened during the session', () => {
|
|
19
26
|
const tabsToClose = __test__.diffTabIndexes(['https://example.com', 'Chrome Extension'], [
|
|
20
27
|
{ index: 0, identity: 'https://example.com' },
|
|
@@ -53,10 +60,4 @@ describe('PlaywrightMCP state', () => {
|
|
|
53
60
|
mcp._state = 'closing';
|
|
54
61
|
await expect(mcp.connect()).rejects.toThrow('Playwright MCP is closing');
|
|
55
62
|
});
|
|
56
|
-
it('tracks backend mode for lifecycle policy', async () => {
|
|
57
|
-
const mcp = new PlaywrightMCP();
|
|
58
|
-
expect(mcp._mode).toBeNull();
|
|
59
|
-
await mcp.close();
|
|
60
|
-
expect(mcp._mode).toBeNull();
|
|
61
|
-
});
|
|
62
63
|
});
|
package/dist/clis/boss/search.js
CHANGED
|
@@ -67,7 +67,6 @@ cli({
|
|
|
67
67
|
description: 'BOSS直聘搜索职位',
|
|
68
68
|
domain: 'www.zhipin.com',
|
|
69
69
|
strategy: Strategy.COOKIE,
|
|
70
|
-
forceExtension: true, // BOSS Zhipin detects CDP mode — must use extension bridge
|
|
71
70
|
browser: true,
|
|
72
71
|
args: [
|
|
73
72
|
{ name: 'query', required: true, help: 'Search keyword (e.g. AI agent, 前端)' },
|
package/dist/clis/v2ex/daily.js
CHANGED
package/dist/clis/v2ex/me.js
CHANGED
package/dist/doctor.d.ts
CHANGED
|
@@ -28,11 +28,6 @@ export type DoctorReport = {
|
|
|
28
28
|
envFingerprint: string | null;
|
|
29
29
|
shellFiles: ShellFileStatus[];
|
|
30
30
|
configs: McpConfigStatus[];
|
|
31
|
-
remoteDebuggingEnabled: boolean;
|
|
32
|
-
remoteDebuggingEndpoint: string | null;
|
|
33
|
-
cdpEnabled: boolean;
|
|
34
|
-
cdpToken: string | null;
|
|
35
|
-
cdpFingerprint: string | null;
|
|
36
31
|
recommendedToken: string | null;
|
|
37
32
|
recommendedFingerprint: string | null;
|
|
38
33
|
warnings: string[];
|