@jackwener/opencli 0.4.6 → 0.5.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/README.md +2 -14
- package/README.zh-CN.md +3 -15
- package/SKILL.md +3 -7
- package/dist/browser.d.ts +3 -16
- package/dist/browser.js +25 -187
- package/dist/browser.test.js +0 -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 +0 -8
- package/src/browser.ts +32 -202
- 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/dist/doctor.js
CHANGED
|
@@ -3,8 +3,7 @@ import * as os from 'node:os';
|
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import { createInterface } from 'node:readline/promises';
|
|
5
5
|
import { stdin as input, stdout as output } from 'node:process';
|
|
6
|
-
import {
|
|
7
|
-
import { browserSession } from './runtime.js';
|
|
6
|
+
import { getTokenFingerprint } from './browser.js';
|
|
8
7
|
const PLAYWRIGHT_SERVER_NAME = 'playwright';
|
|
9
8
|
const PLAYWRIGHT_TOKEN_ENV = 'PLAYWRIGHT_MCP_EXTENSION_TOKEN';
|
|
10
9
|
const PLAYWRIGHT_EXTENSION_ID = 'mmlmfjhmonkocbjadbfplnigmagldckm';
|
|
@@ -170,46 +169,8 @@ function readConfigStatus(filePath) {
|
|
|
170
169
|
};
|
|
171
170
|
}
|
|
172
171
|
}
|
|
173
|
-
async function extractTokenViaCdp() {
|
|
174
|
-
if (!(process.env.OPENCLI_USE_CDP === '1' || process.env.OPENCLI_CDP_ENDPOINT))
|
|
175
|
-
return null;
|
|
176
|
-
const candidates = [
|
|
177
|
-
`chrome-extension://${PLAYWRIGHT_EXTENSION_ID}/options.html`,
|
|
178
|
-
`chrome-extension://${PLAYWRIGHT_EXTENSION_ID}/popup.html`,
|
|
179
|
-
`chrome-extension://${PLAYWRIGHT_EXTENSION_ID}/connect.html`,
|
|
180
|
-
`chrome-extension://${PLAYWRIGHT_EXTENSION_ID}/index.html`,
|
|
181
|
-
];
|
|
182
|
-
const result = await browserSession(PlaywrightMCP, async (page) => {
|
|
183
|
-
for (const url of candidates) {
|
|
184
|
-
try {
|
|
185
|
-
await page.goto(url);
|
|
186
|
-
await page.wait(1);
|
|
187
|
-
const token = await page.evaluate(`() => {
|
|
188
|
-
const values = new Set();
|
|
189
|
-
const push = (value) => {
|
|
190
|
-
if (!value || typeof value !== 'string') return;
|
|
191
|
-
for (const match of value.matchAll(/[A-Za-z0-9_-]{24,}/g)) values.add(match[0]);
|
|
192
|
-
};
|
|
193
|
-
document.querySelectorAll('input, textarea, code, pre, span, div').forEach((el) => {
|
|
194
|
-
push(el.value);
|
|
195
|
-
push(el.textContent || '');
|
|
196
|
-
push(el.getAttribute && el.getAttribute('value'));
|
|
197
|
-
});
|
|
198
|
-
return Array.from(values);
|
|
199
|
-
}`);
|
|
200
|
-
const matches = Array.isArray(token) ? token.filter((v) => v.length >= 24) : [];
|
|
201
|
-
if (matches.length > 0)
|
|
202
|
-
return matches.sort((a, b) => b.length - a.length)[0];
|
|
203
|
-
}
|
|
204
|
-
catch { }
|
|
205
|
-
}
|
|
206
|
-
return null;
|
|
207
|
-
});
|
|
208
|
-
return typeof result === 'string' && result ? result : null;
|
|
209
|
-
}
|
|
210
172
|
export async function runBrowserDoctor(opts = {}) {
|
|
211
173
|
const envToken = process.env[PLAYWRIGHT_TOKEN_ENV] ?? null;
|
|
212
|
-
const remoteDebuggingEndpoint = await discoverChromeEndpoint().catch(() => null);
|
|
213
174
|
const shellPath = opts.shellRc ?? getDefaultShellRcPath();
|
|
214
175
|
const shellFiles = [shellPath].map((filePath) => {
|
|
215
176
|
if (!fileExists(filePath))
|
|
@@ -220,27 +181,20 @@ export async function runBrowserDoctor(opts = {}) {
|
|
|
220
181
|
});
|
|
221
182
|
const configPaths = opts.configPaths?.length ? opts.configPaths : getDefaultMcpConfigPaths();
|
|
222
183
|
const configs = configPaths.map(readConfigStatus);
|
|
223
|
-
const cdpToken = !opts.token && !envToken ? await extractTokenViaCdp().catch(() => null) : null;
|
|
224
184
|
const allTokens = [
|
|
225
185
|
opts.token ?? null,
|
|
226
186
|
envToken,
|
|
227
187
|
...shellFiles.map(s => s.token),
|
|
228
188
|
...configs.map(c => c.token),
|
|
229
|
-
cdpToken,
|
|
230
189
|
].filter((v) => !!v);
|
|
231
190
|
const uniqueTokens = [...new Set(allTokens)];
|
|
232
|
-
const recommendedToken = opts.token ?? envToken ?? (uniqueTokens.length === 1 ? uniqueTokens[0] :
|
|
191
|
+
const recommendedToken = opts.token ?? envToken ?? (uniqueTokens.length === 1 ? uniqueTokens[0] : null) ?? null;
|
|
233
192
|
const report = {
|
|
234
193
|
cliVersion: opts.cliVersion,
|
|
235
194
|
envToken,
|
|
236
195
|
envFingerprint: getTokenFingerprint(envToken ?? undefined),
|
|
237
196
|
shellFiles,
|
|
238
197
|
configs,
|
|
239
|
-
remoteDebuggingEnabled: !!remoteDebuggingEndpoint,
|
|
240
|
-
remoteDebuggingEndpoint,
|
|
241
|
-
cdpEnabled: process.env.OPENCLI_USE_CDP === '1' || !!process.env.OPENCLI_CDP_ENDPOINT,
|
|
242
|
-
cdpToken,
|
|
243
|
-
cdpFingerprint: getTokenFingerprint(cdpToken ?? undefined),
|
|
244
198
|
recommendedToken,
|
|
245
199
|
recommendedFingerprint: getTokenFingerprint(recommendedToken ?? undefined),
|
|
246
200
|
warnings: [],
|
|
@@ -254,17 +208,12 @@ export async function runBrowserDoctor(opts = {}) {
|
|
|
254
208
|
report.issues.push('No scanned MCP config currently contains a Playwright extension token.');
|
|
255
209
|
if (uniqueTokens.length > 1)
|
|
256
210
|
report.issues.push('Detected inconsistent Playwright MCP tokens across env/config files.');
|
|
257
|
-
if (!report.remoteDebuggingEnabled)
|
|
258
|
-
report.warnings.push('Chrome remote debugging appears to be disabled or Chrome is not currently exposing a DevTools endpoint.');
|
|
259
211
|
for (const config of configs) {
|
|
260
212
|
if (config.parseError)
|
|
261
213
|
report.warnings.push(`Could not parse ${config.path}: ${config.parseError}`);
|
|
262
214
|
}
|
|
263
215
|
if (!recommendedToken) {
|
|
264
|
-
|
|
265
|
-
report.warnings.push('CDP is enabled, but no token could be extracted automatically from the extension UI.');
|
|
266
|
-
else
|
|
267
|
-
report.warnings.push('No token source found. Enable OPENCLI_USE_CDP=1 to allow a best-effort token read from the extension page.');
|
|
216
|
+
report.warnings.push('No token source found.');
|
|
268
217
|
}
|
|
269
218
|
return report;
|
|
270
219
|
}
|
|
@@ -277,9 +226,6 @@ export function renderBrowserDoctorReport(report) {
|
|
|
277
226
|
const uniqueFingerprints = [...new Set(tokenFingerprints)];
|
|
278
227
|
const hasMismatch = uniqueFingerprints.length > 1;
|
|
279
228
|
const lines = [`opencli v${report.cliVersion ?? 'unknown'} doctor`, ''];
|
|
280
|
-
lines.push(statusLine(report.remoteDebuggingEnabled ? 'OK' : 'WARN', `Chrome remote debugging: ${report.remoteDebuggingEnabled ? 'enabled' : 'disabled'}`));
|
|
281
|
-
if (report.remoteDebuggingEndpoint)
|
|
282
|
-
lines.push(` ${report.remoteDebuggingEndpoint}`);
|
|
283
229
|
const envStatus = !report.envToken ? 'MISSING' : hasMismatch ? 'MISMATCH' : 'OK';
|
|
284
230
|
lines.push(statusLine(envStatus, `Environment token: ${tokenSummary(report.envToken, report.envFingerprint)}`));
|
|
285
231
|
for (const shell of report.shellFiles) {
|
|
@@ -306,10 +252,6 @@ export function renderBrowserDoctorReport(report) {
|
|
|
306
252
|
}
|
|
307
253
|
if (missingConfigCount > 0)
|
|
308
254
|
lines.push(` Other scanned config locations not present: ${missingConfigCount}`);
|
|
309
|
-
if (report.cdpEnabled) {
|
|
310
|
-
const cdpStatus = report.cdpToken ? 'OK' : 'WARN';
|
|
311
|
-
lines.push(statusLine(cdpStatus, `CDP token probe: ${tokenSummary(report.cdpToken, report.cdpFingerprint)}`));
|
|
312
|
-
}
|
|
313
255
|
lines.push('');
|
|
314
256
|
lines.push(statusLine(hasMismatch ? 'MISMATCH' : report.recommendedToken ? 'OK' : 'WARN', `Recommended token fingerprint: ${report.recommendedFingerprint ?? 'unavailable'}`));
|
|
315
257
|
if (report.issues.length) {
|
|
@@ -341,7 +283,7 @@ function writeFileWithMkdir(filePath, content) {
|
|
|
341
283
|
export async function applyBrowserDoctorFix(report, opts = {}) {
|
|
342
284
|
const token = opts.token ?? report.recommendedToken;
|
|
343
285
|
if (!token)
|
|
344
|
-
throw new Error('No Playwright MCP token is available to write. Provide --token
|
|
286
|
+
throw new Error('No Playwright MCP token is available to write. Provide --token first.');
|
|
345
287
|
const plannedWrites = [];
|
|
346
288
|
const shellPath = opts.shellRc ?? report.shellFiles[0]?.path ?? getDefaultShellRcPath();
|
|
347
289
|
plannedWrites.push(shellPath);
|
package/dist/doctor.test.js
CHANGED
|
@@ -76,17 +76,11 @@ describe('doctor report rendering', () => {
|
|
|
76
76
|
envFingerprint: 'fp1',
|
|
77
77
|
shellFiles: [{ path: '/tmp/.zshrc', exists: true, token: 'abc123', fingerprint: 'fp1' }],
|
|
78
78
|
configs: [{ path: '/tmp/mcp.json', exists: true, format: 'json', token: 'abc123', fingerprint: 'fp1', writable: true }],
|
|
79
|
-
remoteDebuggingEnabled: true,
|
|
80
|
-
remoteDebuggingEndpoint: 'ws://127.0.0.1:9222/devtools/browser/test',
|
|
81
|
-
cdpEnabled: false,
|
|
82
|
-
cdpToken: null,
|
|
83
|
-
cdpFingerprint: null,
|
|
84
79
|
recommendedToken: 'abc123',
|
|
85
80
|
recommendedFingerprint: 'fp1',
|
|
86
81
|
warnings: [],
|
|
87
82
|
issues: [],
|
|
88
83
|
});
|
|
89
|
-
expect(text).toContain('[OK] Chrome remote debugging: enabled');
|
|
90
84
|
expect(text).toContain('[OK] Environment token: configured (fp1)');
|
|
91
85
|
expect(text).toContain('[OK] MCP config /tmp/mcp.json: configured (fp1)');
|
|
92
86
|
});
|
|
@@ -96,17 +90,11 @@ describe('doctor report rendering', () => {
|
|
|
96
90
|
envFingerprint: 'fp1',
|
|
97
91
|
shellFiles: [{ path: '/tmp/.zshrc', exists: true, token: 'def456', fingerprint: 'fp2' }],
|
|
98
92
|
configs: [{ path: '/tmp/mcp.json', exists: true, format: 'json', token: 'abc123', fingerprint: 'fp1', writable: true }],
|
|
99
|
-
remoteDebuggingEnabled: false,
|
|
100
|
-
remoteDebuggingEndpoint: null,
|
|
101
|
-
cdpEnabled: false,
|
|
102
|
-
cdpToken: null,
|
|
103
|
-
cdpFingerprint: null,
|
|
104
93
|
recommendedToken: 'abc123',
|
|
105
94
|
recommendedFingerprint: 'fp1',
|
|
106
|
-
warnings: [
|
|
95
|
+
warnings: [],
|
|
107
96
|
issues: ['Detected inconsistent Playwright MCP tokens across env/config files.'],
|
|
108
97
|
});
|
|
109
|
-
expect(text).toContain('[WARN] Chrome remote debugging: disabled');
|
|
110
98
|
expect(text).toContain('[MISMATCH] Environment token: configured (fp1)');
|
|
111
99
|
expect(text).toContain('[MISMATCH] Shell file /tmp/.zshrc: configured (fp2)');
|
|
112
100
|
expect(text).toContain('[MISMATCH] Recommended token fingerprint: fp1');
|
package/dist/main.js
CHANGED
|
@@ -152,7 +152,7 @@ for (const [, cmd] of registry) {
|
|
|
152
152
|
process.env.OPENCLI_VERBOSE = '1';
|
|
153
153
|
let result;
|
|
154
154
|
if (cmd.browser) {
|
|
155
|
-
result = await browserSession(PlaywrightMCP, async (page) => runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) })
|
|
155
|
+
result = await browserSession(PlaywrightMCP, async (page) => runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) }));
|
|
156
156
|
}
|
|
157
157
|
else {
|
|
158
158
|
result = await executeCommand(cmd, null, kwargs, actionOpts.verbose);
|
package/dist/registry.d.ts
CHANGED
|
@@ -33,8 +33,6 @@ export interface CliCommand {
|
|
|
33
33
|
/** Internal: lazy-loaded TS module support */
|
|
34
34
|
_lazy?: boolean;
|
|
35
35
|
_modulePath?: string;
|
|
36
|
-
/** Force extension bridge mode (bypass CDP), for anti-bot sites */
|
|
37
|
-
forceExtension?: boolean;
|
|
38
36
|
}
|
|
39
37
|
export interface CliOptions {
|
|
40
38
|
site: string;
|
|
@@ -48,8 +46,6 @@ export interface CliOptions {
|
|
|
48
46
|
func?: (page: IPage | null, kwargs: Record<string, any>, debug?: boolean) => Promise<any>;
|
|
49
47
|
pipeline?: any[];
|
|
50
48
|
timeoutSeconds?: number;
|
|
51
|
-
/** Force extension bridge mode (bypass CDP), for anti-bot sites */
|
|
52
|
-
forceExtension?: boolean;
|
|
53
49
|
}
|
|
54
50
|
export declare function cli(opts: CliOptions): CliCommand;
|
|
55
51
|
export declare function getRegistry(): Map<string, CliCommand>;
|
package/dist/registry.js
CHANGED
package/dist/runtime.d.ts
CHANGED
|
@@ -10,6 +10,4 @@ export declare function runWithTimeout<T>(promise: Promise<T>, opts: {
|
|
|
10
10
|
timeout: number;
|
|
11
11
|
label?: string;
|
|
12
12
|
}): Promise<T>;
|
|
13
|
-
export declare function browserSession<T>(BrowserFactory: new () => any, fn: (page: IPage) => Promise<T
|
|
14
|
-
forceExtension?: boolean;
|
|
15
|
-
}): Promise<T>;
|
|
13
|
+
export declare function browserSession<T>(BrowserFactory: new () => any, fn: (page: IPage) => Promise<T>): Promise<T>;
|
package/dist/runtime.js
CHANGED
|
@@ -15,10 +15,10 @@ export async function runWithTimeout(promise, opts) {
|
|
|
15
15
|
.catch((err) => { clearTimeout(timer); reject(err); });
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
|
-
export async function browserSession(BrowserFactory, fn
|
|
18
|
+
export async function browserSession(BrowserFactory, fn) {
|
|
19
19
|
const mcp = new BrowserFactory();
|
|
20
20
|
try {
|
|
21
|
-
const page = await mcp.connect({ timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT
|
|
21
|
+
const page = await mcp.connect({ timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT });
|
|
22
22
|
return await fn(page);
|
|
23
23
|
}
|
|
24
24
|
finally {
|
package/package.json
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jackwener/opencli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
7
|
"description": "Make any website your CLI. AI-powered.",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=18.0.0"
|
|
10
|
+
},
|
|
8
11
|
"type": "module",
|
|
9
12
|
"main": "dist/main.js",
|
|
10
13
|
"bin": {
|
package/src/browser.test.ts
CHANGED
|
@@ -75,13 +75,5 @@ describe('PlaywrightMCP state', () => {
|
|
|
75
75
|
await expect(mcp.connect()).rejects.toThrow('Playwright MCP is closing');
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
-
it('tracks backend mode for lifecycle policy', async () => {
|
|
79
|
-
const mcp = new PlaywrightMCP();
|
|
80
|
-
|
|
81
|
-
expect((mcp as any)._mode).toBeNull();
|
|
82
78
|
|
|
83
|
-
await mcp.close();
|
|
84
|
-
|
|
85
|
-
expect((mcp as any)._mode).toBeNull();
|
|
86
|
-
});
|
|
87
79
|
});
|
package/src/browser.ts
CHANGED
|
@@ -1,114 +1,16 @@
|
|
|
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
|
|
|
6
6
|
import { spawn, execSync, type ChildProcess } from 'node:child_process';
|
|
7
7
|
import { createHash } from 'node:crypto';
|
|
8
|
-
import * as http from 'node:http';
|
|
9
|
-
import * as net from 'node:net';
|
|
10
8
|
import { fileURLToPath } from 'node:url';
|
|
11
9
|
import * as fs from 'node:fs';
|
|
12
10
|
import * as os from 'node:os';
|
|
13
11
|
import * as path from 'node:path';
|
|
14
12
|
import { formatSnapshot } from './snapshotFormatter.js';
|
|
15
13
|
|
|
16
|
-
/**
|
|
17
|
-
* Chrome 144+ auto-discovery: read DevToolsActivePort file to get CDP endpoint.
|
|
18
|
-
*
|
|
19
|
-
* Starting with Chrome 144, users can enable remote debugging from
|
|
20
|
-
* chrome://inspect#remote-debugging without any command-line flags.
|
|
21
|
-
* Chrome writes the active port and browser GUID to a DevToolsActivePort file
|
|
22
|
-
* in the user data directory, which we read to construct the WebSocket endpoint.
|
|
23
|
-
*
|
|
24
|
-
* Priority: OPENCLI_CDP_ENDPOINT env > DevToolsActivePort auto-discovery > --extension fallback
|
|
25
|
-
*/
|
|
26
|
-
|
|
27
|
-
/** Quick TCP port probe to verify Chrome is actually listening */
|
|
28
|
-
function isPortReachable(port: number, host = '127.0.0.1', timeoutMs = 800): Promise<boolean> {
|
|
29
|
-
return new Promise(resolve => {
|
|
30
|
-
const sock = net.createConnection({ port, host });
|
|
31
|
-
sock.setTimeout(timeoutMs);
|
|
32
|
-
sock.on('connect', () => { sock.destroy(); resolve(true); });
|
|
33
|
-
sock.on('error', () => resolve(false));
|
|
34
|
-
sock.on('timeout', () => { sock.destroy(); resolve(false); });
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Verify the CDP HTTP JSON API is functional.
|
|
40
|
-
* Chrome's chrome://inspect#remote-debugging mode writes DevToolsActivePort
|
|
41
|
-
* but doesn't expose the full CDP HTTP API (/json/version), which means
|
|
42
|
-
* Playwright's connectOverCDP won't work properly (init succeeds but
|
|
43
|
-
* all tool calls hang silently).
|
|
44
|
-
*/
|
|
45
|
-
export function isCdpApiAvailable(port: number, host = '127.0.0.1', timeoutMs = 2000): Promise<boolean> {
|
|
46
|
-
return new Promise(resolve => {
|
|
47
|
-
const req = http.get(`http://${host}:${port}/json/version`, { timeout: timeoutMs }, (res) => {
|
|
48
|
-
let body = '';
|
|
49
|
-
res.on('data', (chunk: Buffer) => { body += chunk.toString(); });
|
|
50
|
-
res.on('end', () => {
|
|
51
|
-
try {
|
|
52
|
-
const data = JSON.parse(body);
|
|
53
|
-
// A valid CDP endpoint returns { Browser, ... } with a webSocketDebuggerUrl
|
|
54
|
-
resolve(!!data && typeof data === 'object' && !!data.Browser);
|
|
55
|
-
} catch {
|
|
56
|
-
resolve(false);
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
req.on('error', () => resolve(false));
|
|
61
|
-
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export async function discoverChromeEndpoint(): Promise<string | null> {
|
|
66
|
-
const candidates: string[] = [];
|
|
67
|
-
|
|
68
|
-
// User-specified Chrome data dir takes highest priority
|
|
69
|
-
if (process.env.CHROME_USER_DATA_DIR) {
|
|
70
|
-
candidates.push(path.join(process.env.CHROME_USER_DATA_DIR, 'DevToolsActivePort'));
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Standard Chrome/Edge user data dirs per platform
|
|
74
|
-
if (process.platform === 'win32') {
|
|
75
|
-
const localAppData = process.env.LOCALAPPDATA ?? path.join(os.homedir(), 'AppData', 'Local');
|
|
76
|
-
candidates.push(path.join(localAppData, 'Google', 'Chrome', 'User Data', 'DevToolsActivePort'));
|
|
77
|
-
candidates.push(path.join(localAppData, 'Microsoft', 'Edge', 'User Data', 'DevToolsActivePort'));
|
|
78
|
-
} else if (process.platform === 'darwin') {
|
|
79
|
-
candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome', 'DevToolsActivePort'));
|
|
80
|
-
candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'Microsoft Edge', 'DevToolsActivePort'));
|
|
81
|
-
} else {
|
|
82
|
-
candidates.push(path.join(os.homedir(), '.config', 'google-chrome', 'DevToolsActivePort'));
|
|
83
|
-
candidates.push(path.join(os.homedir(), '.config', 'chromium', 'DevToolsActivePort'));
|
|
84
|
-
candidates.push(path.join(os.homedir(), '.config', 'microsoft-edge', 'DevToolsActivePort'));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
for (const filePath of candidates) {
|
|
88
|
-
try {
|
|
89
|
-
const content = fs.readFileSync(filePath, 'utf-8').trim();
|
|
90
|
-
const lines = content.split('\n');
|
|
91
|
-
if (lines.length >= 2) {
|
|
92
|
-
const port = parseInt(lines[0], 10);
|
|
93
|
-
const browserPath = lines[1]; // e.g. /devtools/browser/<GUID>
|
|
94
|
-
if (port > 0 && browserPath.startsWith('/devtools/browser/')) {
|
|
95
|
-
const endpoint = `ws://127.0.0.1:${port}${browserPath}`;
|
|
96
|
-
// Verify the port is actually reachable (Chrome may have closed, leaving a stale file)
|
|
97
|
-
if (await isPortReachable(port)) {
|
|
98
|
-
// Verify CDP HTTP API is functional — chrome://inspect#remote-debugging
|
|
99
|
-
// writes DevToolsActivePort but doesn't expose the full CDP API,
|
|
100
|
-
// causing Playwright connectOverCDP to hang on all tool calls.
|
|
101
|
-
if (await isCdpApiAvailable(port)) {
|
|
102
|
-
return endpoint;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
} catch {}
|
|
108
|
-
}
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
14
|
// Read version from package.json (single source of truth)
|
|
113
15
|
const __browser_dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
114
16
|
const PKG_VERSION = (() => { try { return JSON.parse(fs.readFileSync(path.resolve(__browser_dirname, '..', 'package.json'), 'utf-8')).version; } catch { return '0.0.0'; } })();
|
|
@@ -116,17 +18,14 @@ const PKG_VERSION = (() => { try { return JSON.parse(fs.readFileSync(path.resolv
|
|
|
116
18
|
const CONNECT_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_CONNECT_TIMEOUT ?? '30', 10);
|
|
117
19
|
const STDERR_BUFFER_LIMIT = 16 * 1024;
|
|
118
20
|
const INITIAL_TABS_TIMEOUT_MS = 1500;
|
|
119
|
-
const CDP_READINESS_PROBE_TIMEOUT_MS = 5000;
|
|
120
21
|
const TAB_CLEANUP_TIMEOUT_MS = 2000;
|
|
121
22
|
let _cachedMcpServerPath: string | null | undefined;
|
|
122
23
|
|
|
123
|
-
type ConnectFailureKind = 'missing-token' | 'extension-timeout' | 'extension-not-installed' | '
|
|
24
|
+
type ConnectFailureKind = 'missing-token' | 'extension-timeout' | 'extension-not-installed' | 'mcp-init' | 'process-exit' | 'unknown';
|
|
124
25
|
type PlaywrightMCPState = 'idle' | 'connecting' | 'connected' | 'closing' | 'closed';
|
|
125
|
-
type PlaywrightMCPMode = 'extension' | 'cdp' | null;
|
|
126
26
|
|
|
127
27
|
type ConnectFailureInput = {
|
|
128
28
|
kind: ConnectFailureKind;
|
|
129
|
-
mode: 'extension' | 'cdp';
|
|
130
29
|
timeout: number;
|
|
131
30
|
hasExtensionToken: boolean;
|
|
132
31
|
tokenFingerprint?: string | null;
|
|
@@ -145,41 +44,31 @@ export function formatBrowserConnectError(input: ConnectFailureInput): Error {
|
|
|
145
44
|
const suffix = stderr ? `\n\nMCP stderr:\n${stderr}` : '';
|
|
146
45
|
const tokenHint = input.tokenFingerprint ? ` Token fingerprint: ${input.tokenFingerprint}.` : '';
|
|
147
46
|
|
|
148
|
-
if (input.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (input.kind === 'extension-not-installed') {
|
|
159
|
-
return new Error(
|
|
160
|
-
'Failed to connect to Playwright MCP Bridge: the browser extension did not attach.\n\n' +
|
|
161
|
-
'Make sure Chrome is running and the "Playwright MCP Bridge" extension is installed and enabled. ' +
|
|
162
|
-
'If Chrome shows an approval dialog, click Allow.' +
|
|
163
|
-
suffix,
|
|
164
|
-
);
|
|
165
|
-
}
|
|
47
|
+
if (input.kind === 'missing-token') {
|
|
48
|
+
return new Error(
|
|
49
|
+
'Failed to connect to Playwright MCP Bridge: PLAYWRIGHT_MCP_EXTENSION_TOKEN is not set.\n\n' +
|
|
50
|
+
'Without this token, Chrome will show a manual approval dialog for every new MCP connection. ' +
|
|
51
|
+
'Copy the token from the Playwright MCP Bridge extension and set it in BOTH your shell environment and MCP client config.' +
|
|
52
|
+
suffix,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
166
55
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
suffix,
|
|
175
|
-
);
|
|
176
|
-
}
|
|
56
|
+
if (input.kind === 'extension-not-installed') {
|
|
57
|
+
return new Error(
|
|
58
|
+
'Failed to connect to Playwright MCP Bridge: the browser extension did not attach.\n\n' +
|
|
59
|
+
'Make sure Chrome is running and the "Playwright MCP Bridge" extension is installed and enabled. ' +
|
|
60
|
+
'If Chrome shows an approval dialog, click Allow.' +
|
|
61
|
+
suffix,
|
|
62
|
+
);
|
|
177
63
|
}
|
|
178
64
|
|
|
179
|
-
if (input.
|
|
65
|
+
if (input.kind === 'extension-timeout') {
|
|
66
|
+
const likelyCause = input.hasExtensionToken
|
|
67
|
+
? `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.`
|
|
68
|
+
: 'PLAYWRIGHT_MCP_EXTENSION_TOKEN is not configured, so the extension may be waiting for manual approval.';
|
|
180
69
|
return new Error(
|
|
181
|
-
`Timed out connecting to
|
|
182
|
-
|
|
70
|
+
`Timed out connecting to Playwright MCP Bridge (${input.timeout}s).\n\n` +
|
|
71
|
+
`${likelyCause} If a browser prompt is visible, click Allow.` +
|
|
183
72
|
suffix,
|
|
184
73
|
);
|
|
185
74
|
}
|
|
@@ -199,7 +88,6 @@ export function formatBrowserConnectError(input: ConnectFailureInput): Error {
|
|
|
199
88
|
}
|
|
200
89
|
|
|
201
90
|
function inferConnectFailureKind(args: {
|
|
202
|
-
mode: 'extension' | 'cdp';
|
|
203
91
|
hasExtensionToken: boolean;
|
|
204
92
|
stderr: string;
|
|
205
93
|
rawMessage?: string;
|
|
@@ -207,7 +95,7 @@ function inferConnectFailureKind(args: {
|
|
|
207
95
|
}): ConnectFailureKind {
|
|
208
96
|
const haystack = `${args.rawMessage ?? ''}\n${args.stderr}`.toLowerCase();
|
|
209
97
|
|
|
210
|
-
if (
|
|
98
|
+
if (!args.hasExtensionToken)
|
|
211
99
|
return 'missing-token';
|
|
212
100
|
if (haystack.includes('extension connection timeout') || haystack.includes('playwright mcp bridge'))
|
|
213
101
|
return 'extension-not-installed';
|
|
@@ -215,11 +103,7 @@ function inferConnectFailureKind(args: {
|
|
|
215
103
|
return 'mcp-init';
|
|
216
104
|
if (args.exited)
|
|
217
105
|
return 'process-exit';
|
|
218
|
-
|
|
219
|
-
return 'extension-timeout';
|
|
220
|
-
if (args.mode === 'cdp')
|
|
221
|
-
return 'cdp-timeout';
|
|
222
|
-
return 'unknown';
|
|
106
|
+
return 'extension-timeout';
|
|
223
107
|
}
|
|
224
108
|
|
|
225
109
|
// JSON-RPC helpers
|
|
@@ -461,7 +345,6 @@ export class PlaywrightMCP {
|
|
|
461
345
|
private _initialTabIdentities: string[] = [];
|
|
462
346
|
private _closingPromise: Promise<void> | null = null;
|
|
463
347
|
private _state: PlaywrightMCPState = 'idle';
|
|
464
|
-
private _mode: PlaywrightMCPMode = null;
|
|
465
348
|
|
|
466
349
|
private _page: Page | null = null;
|
|
467
350
|
|
|
@@ -496,7 +379,6 @@ export class PlaywrightMCP {
|
|
|
496
379
|
this._page = null;
|
|
497
380
|
this._proc = null;
|
|
498
381
|
this._buffer = '';
|
|
499
|
-
this._mode = null;
|
|
500
382
|
this._initialTabIdentities = [];
|
|
501
383
|
this._rejectPendingRequests(new Error('Playwright MCP connect failed'));
|
|
502
384
|
PlaywrightMCP._activeInsts.delete(this);
|
|
@@ -505,7 +387,7 @@ export class PlaywrightMCP {
|
|
|
505
387
|
}
|
|
506
388
|
}
|
|
507
389
|
|
|
508
|
-
async connect(opts: { timeout?: number
|
|
390
|
+
async connect(opts: { timeout?: number } = {}): Promise<Page> {
|
|
509
391
|
if (this._state === 'connected' && this._page) return this._page;
|
|
510
392
|
if (this._state === 'connecting') throw new Error('Playwright MCP is already connecting');
|
|
511
393
|
if (this._state === 'closing') throw new Error('Playwright MCP is closing');
|
|
@@ -519,26 +401,9 @@ export class PlaywrightMCP {
|
|
|
519
401
|
this._state = 'connecting';
|
|
520
402
|
const timeout = opts.timeout ?? CONNECT_TIMEOUT;
|
|
521
403
|
|
|
522
|
-
// Connection priority:
|
|
523
|
-
// 1. OPENCLI_CDP_ENDPOINT env var → explicit CDP endpoint
|
|
524
|
-
// 2. OPENCLI_USE_CDP=1 → auto-discover via DevToolsActivePort
|
|
525
|
-
// 3. Default → --extension mode (Playwright MCP Bridge)
|
|
526
|
-
// Some anti-bot sites (e.g. BOSS Zhipin) detect CDP — use forceExtension to bypass.
|
|
527
|
-
const forceExt = opts.forceExtension || process.env.OPENCLI_FORCE_EXTENSION === '1';
|
|
528
|
-
let cdpEndpoint: string | null = null;
|
|
529
|
-
if (!forceExt) {
|
|
530
|
-
if (process.env.OPENCLI_CDP_ENDPOINT) {
|
|
531
|
-
cdpEndpoint = process.env.OPENCLI_CDP_ENDPOINT;
|
|
532
|
-
} else if (process.env.OPENCLI_USE_CDP === '1') {
|
|
533
|
-
cdpEndpoint = await discoverChromeEndpoint();
|
|
534
|
-
}
|
|
535
|
-
}
|
|
536
|
-
|
|
537
404
|
return new Promise<Page>((resolve, reject) => {
|
|
538
405
|
const isDebug = process.env.DEBUG?.includes('opencli:mcp');
|
|
539
406
|
const debugLog = (msg: string) => isDebug && console.error(`[opencli:mcp] ${msg}`);
|
|
540
|
-
const mode: 'extension' | 'cdp' = cdpEndpoint ? 'cdp' : 'extension';
|
|
541
|
-
this._mode = mode;
|
|
542
407
|
const extensionToken = process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN;
|
|
543
408
|
const tokenFingerprint = getTokenFingerprint(extensionToken);
|
|
544
409
|
let stderrBuffer = '';
|
|
@@ -552,7 +417,6 @@ export class PlaywrightMCP {
|
|
|
552
417
|
this._resetAfterFailedConnect();
|
|
553
418
|
reject(formatBrowserConnectError({
|
|
554
419
|
kind,
|
|
555
|
-
mode,
|
|
556
420
|
timeout,
|
|
557
421
|
hasExtensionToken: !!extensionToken,
|
|
558
422
|
tokenFingerprint,
|
|
@@ -573,23 +437,14 @@ export class PlaywrightMCP {
|
|
|
573
437
|
const timer = setTimeout(() => {
|
|
574
438
|
debugLog('Connection timed out');
|
|
575
439
|
settleError(inferConnectFailureKind({
|
|
576
|
-
mode,
|
|
577
440
|
hasExtensionToken: !!extensionToken,
|
|
578
441
|
stderr: stderrBuffer,
|
|
579
442
|
}));
|
|
580
443
|
}, timeout * 1000);
|
|
581
444
|
|
|
582
|
-
const mcpArgs: string[] = [mcpPath];
|
|
583
|
-
if (cdpEndpoint) {
|
|
584
|
-
mcpArgs.push('--cdp-endpoint', cdpEndpoint);
|
|
585
|
-
} else {
|
|
586
|
-
mcpArgs.push('--extension');
|
|
587
|
-
}
|
|
445
|
+
const mcpArgs: string[] = [mcpPath, '--extension'];
|
|
588
446
|
if (process.env.OPENCLI_VERBOSE) {
|
|
589
|
-
console.error(`[opencli]
|
|
590
|
-
if (mode === 'extension') {
|
|
591
|
-
console.error(`[opencli] Extension token: ${extensionToken ? `configured (fingerprint ${tokenFingerprint})` : 'missing'}`);
|
|
592
|
-
}
|
|
447
|
+
console.error(`[opencli] Extension token: ${extensionToken ? `configured (fingerprint ${tokenFingerprint})` : 'missing'}`);
|
|
593
448
|
}
|
|
594
449
|
if (process.env.OPENCLI_BROWSER_EXECUTABLE_PATH) {
|
|
595
450
|
mcpArgs.push('--executablePath', process.env.OPENCLI_BROWSER_EXECUTABLE_PATH);
|
|
@@ -645,7 +500,6 @@ export class PlaywrightMCP {
|
|
|
645
500
|
this._rejectPendingRequests(new Error(`Playwright MCP process exited before response${code == null ? '' : ` (code ${code})`}`));
|
|
646
501
|
if (!settled) {
|
|
647
502
|
settleError(inferConnectFailureKind({
|
|
648
|
-
mode,
|
|
649
503
|
hasExtensionToken: !!extensionToken,
|
|
650
504
|
stderr: stderrBuffer,
|
|
651
505
|
exited: true,
|
|
@@ -663,7 +517,6 @@ export class PlaywrightMCP {
|
|
|
663
517
|
debugLog('Got initialize response');
|
|
664
518
|
if (resp.error) {
|
|
665
519
|
settleError(inferConnectFailureKind({
|
|
666
|
-
mode,
|
|
667
520
|
hasExtensionToken: !!extensionToken,
|
|
668
521
|
stderr: stderrBuffer,
|
|
669
522
|
rawMessage: `MCP init failed: ${resp.error.message}`,
|
|
@@ -675,29 +528,7 @@ export class PlaywrightMCP {
|
|
|
675
528
|
debugLog(`SEND: ${initializedMsg.trim()}`);
|
|
676
529
|
this._proc?.stdin?.write(initializedMsg);
|
|
677
530
|
|
|
678
|
-
|
|
679
|
-
// CDP readiness probe: verify tool calls actually work.
|
|
680
|
-
// Some CDP endpoints (e.g. chrome://inspect mode) accept WebSocket
|
|
681
|
-
// connections and respond to MCP init but silently drop tool calls.
|
|
682
|
-
debugLog('CDP readiness probe (tabs)...');
|
|
683
|
-
withTimeout(page.tabs(), CDP_READINESS_PROBE_TIMEOUT_MS, 'CDP readiness probe timed out')
|
|
684
|
-
.then(() => {
|
|
685
|
-
debugLog('CDP readiness probe succeeded');
|
|
686
|
-
settleSuccess(page);
|
|
687
|
-
})
|
|
688
|
-
.catch((err) => {
|
|
689
|
-
debugLog(`CDP readiness probe failed: ${err.message}`);
|
|
690
|
-
settleError('cdp-timeout', {
|
|
691
|
-
rawMessage: 'CDP endpoint connected but tool calls are unresponsive. ' +
|
|
692
|
-
'This usually means Chrome was opened with chrome://inspect#remote-debugging ' +
|
|
693
|
-
'which is not fully compatible. Launch Chrome with --remote-debugging-port=9222 instead, ' +
|
|
694
|
-
'or use the Playwright MCP Bridge extension (default mode).',
|
|
695
|
-
});
|
|
696
|
-
});
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
// Extension mode uses tabs as a readiness probe and for tab cleanup bookkeeping.
|
|
531
|
+
// Use tabs as a readiness probe and for tab cleanup bookkeeping.
|
|
701
532
|
debugLog('Fetching initial tabs count...');
|
|
702
533
|
withTimeout(page.tabs(), INITIAL_TABS_TIMEOUT_MS, 'Timed out fetching initial tabs').then((tabs: any) => {
|
|
703
534
|
debugLog(`Tabs response: ${typeof tabs === 'string' ? tabs : JSON.stringify(tabs)}`);
|
|
@@ -714,6 +545,7 @@ export class PlaywrightMCP {
|
|
|
714
545
|
});
|
|
715
546
|
}
|
|
716
547
|
|
|
548
|
+
|
|
717
549
|
async close(): Promise<void> {
|
|
718
550
|
if (this._closingPromise) return this._closingPromise;
|
|
719
551
|
if (this._state === 'closed') return;
|
|
@@ -721,7 +553,7 @@ export class PlaywrightMCP {
|
|
|
721
553
|
this._closingPromise = (async () => {
|
|
722
554
|
try {
|
|
723
555
|
// Extension mode opens bridge/session tabs that we can clean up best-effort.
|
|
724
|
-
if (this.
|
|
556
|
+
if (this._page && this._proc && !this._proc.killed) {
|
|
725
557
|
try {
|
|
726
558
|
const tabs = await withTimeout(this._page.tabs(), TAB_CLEANUP_TIMEOUT_MS, 'Timed out fetching tabs during cleanup');
|
|
727
559
|
const tabEntries = extractTabEntries(tabs);
|
|
@@ -751,7 +583,6 @@ export class PlaywrightMCP {
|
|
|
751
583
|
this._rejectPendingRequests(new Error('Playwright MCP session closed'));
|
|
752
584
|
this._page = null;
|
|
753
585
|
this._proc = null;
|
|
754
|
-
this._mode = null;
|
|
755
586
|
this._state = 'closed';
|
|
756
587
|
PlaywrightMCP._activeInsts.delete(this);
|
|
757
588
|
}
|
|
@@ -844,7 +675,6 @@ export const __test__ = {
|
|
|
844
675
|
diffTabIndexes,
|
|
845
676
|
appendLimited,
|
|
846
677
|
withTimeout,
|
|
847
|
-
isCdpApiAvailable,
|
|
848
678
|
};
|
|
849
679
|
|
|
850
680
|
function findMcpServerPath(): string | null {
|
package/src/clis/boss/search.ts
CHANGED
|
@@ -69,7 +69,7 @@ cli({
|
|
|
69
69
|
description: 'BOSS直聘搜索职位',
|
|
70
70
|
domain: 'www.zhipin.com',
|
|
71
71
|
strategy: Strategy.COOKIE,
|
|
72
|
-
|
|
72
|
+
|
|
73
73
|
browser: true,
|
|
74
74
|
args: [
|
|
75
75
|
{ name: 'query', required: true, help: 'Search keyword (e.g. AI agent, 前端)' },
|