@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/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 { PlaywrightMCP, discoverChromeEndpoint, getTokenFingerprint } from './browser.js';
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] : cdpToken) ?? null;
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
- if (report.cdpEnabled)
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 or enable CDP token probing first.');
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);
@@ -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: ['Chrome remote debugging appears to be disabled or Chrome is not currently exposing a DevTools endpoint.'],
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) }), { forceExtension: cmd.forceExtension });
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);
@@ -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
@@ -23,7 +23,6 @@ export function cli(opts) {
23
23
  func: opts.func,
24
24
  pipeline: opts.pipeline,
25
25
  timeoutSeconds: opts.timeoutSeconds,
26
- forceExtension: opts.forceExtension,
27
26
  };
28
27
  const key = fullName(cmd);
29
28
  _registry.set(key, cmd);
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>, opts?: {
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, opts) {
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, forceExtension: opts?.forceExtension });
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.4.6",
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": {
@@ -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 Chrome DevTools Protocol.
3
- * Connects to an existing Chrome browser through CDP auto-discovery or extension bridge.
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' | 'cdp-timeout' | 'mcp-init' | 'process-exit' | 'unknown';
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.mode === 'extension') {
149
- if (input.kind === 'missing-token') {
150
- return new Error(
151
- 'Failed to connect to Playwright MCP Bridge: PLAYWRIGHT_MCP_EXTENSION_TOKEN is not set.\n\n' +
152
- 'Without this token, Chrome will show a manual approval dialog for every new MCP connection. ' +
153
- 'Copy the token from the Playwright MCP Bridge extension and set it in BOTH your shell environment and MCP client config.' +
154
- suffix,
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
- if (input.kind === 'extension-timeout') {
168
- const likelyCause = input.hasExtensionToken
169
- ? `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.`
170
- : 'PLAYWRIGHT_MCP_EXTENSION_TOKEN is not configured, so the extension may be waiting for manual approval.';
171
- return new Error(
172
- `Timed out connecting to Playwright MCP Bridge (${input.timeout}s).\n\n` +
173
- `${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.` +
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.mode === 'cdp' && input.kind === 'cdp-timeout') {
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 browser via CDP (${input.timeout}s).\n\n` +
182
- 'Make sure Chrome is running and remote debugging is enabled at chrome://inspect#remote-debugging, or set OPENCLI_CDP_ENDPOINT explicitly.' +
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 (args.mode === 'extension' && !args.hasExtensionToken)
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
- if (args.mode === 'extension')
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; forceExtension?: boolean } = {}): Promise<Page> {
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] CDP mode: ${cdpEndpoint ? `auto-discovered ${cdpEndpoint}` : 'fallback to --extension'}`);
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
- if (mode === 'cdp') {
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._mode === 'extension' && this._page && this._proc && !this._proc.killed) {
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 {
@@ -69,7 +69,7 @@ cli({
69
69
  description: 'BOSS直聘搜索职位',
70
70
  domain: 'www.zhipin.com',
71
71
  strategy: Strategy.COOKIE,
72
- forceExtension: true, // BOSS Zhipin detects CDP mode — must use extension bridge
72
+
73
73
  browser: true,
74
74
  args: [
75
75
  { name: 'query', required: true, help: 'Search keyword (e.g. AI agent, 前端)' },