@jackwener/opencli 0.4.5 → 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.
@@ -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.5",
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": {
@@ -74,4 +74,6 @@ describe('PlaywrightMCP state', () => {
74
74
 
75
75
  await expect(mcp.connect()).rejects.toThrow('Playwright MCP is closing');
76
76
  });
77
+
78
+
77
79
  });
package/src/browser.ts CHANGED
@@ -1,96 +1,31 @@
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 net from 'node:net';
9
8
  import { fileURLToPath } from 'node:url';
10
9
  import * as fs from 'node:fs';
11
10
  import * as os from 'node:os';
12
11
  import * as path from 'node:path';
13
12
  import { formatSnapshot } from './snapshotFormatter.js';
14
13
 
15
- /**
16
- * Chrome 144+ auto-discovery: read DevToolsActivePort file to get CDP endpoint.
17
- *
18
- * Starting with Chrome 144, users can enable remote debugging from
19
- * chrome://inspect#remote-debugging without any command-line flags.
20
- * Chrome writes the active port and browser GUID to a DevToolsActivePort file
21
- * in the user data directory, which we read to construct the WebSocket endpoint.
22
- *
23
- * Priority: OPENCLI_CDP_ENDPOINT env > DevToolsActivePort auto-discovery > --extension fallback
24
- */
25
-
26
- /** Quick TCP port probe to verify Chrome is actually listening */
27
- function isPortReachable(port: number, host = '127.0.0.1', timeoutMs = 800): Promise<boolean> {
28
- return new Promise(resolve => {
29
- const sock = net.createConnection({ port, host });
30
- sock.setTimeout(timeoutMs);
31
- sock.on('connect', () => { sock.destroy(); resolve(true); });
32
- sock.on('error', () => resolve(false));
33
- sock.on('timeout', () => { sock.destroy(); resolve(false); });
34
- });
35
- }
36
-
37
- export async function discoverChromeEndpoint(): Promise<string | null> {
38
- const candidates: string[] = [];
39
-
40
- // User-specified Chrome data dir takes highest priority
41
- if (process.env.CHROME_USER_DATA_DIR) {
42
- candidates.push(path.join(process.env.CHROME_USER_DATA_DIR, 'DevToolsActivePort'));
43
- }
44
-
45
- // Standard Chrome/Edge user data dirs per platform
46
- if (process.platform === 'win32') {
47
- const localAppData = process.env.LOCALAPPDATA ?? path.join(os.homedir(), 'AppData', 'Local');
48
- candidates.push(path.join(localAppData, 'Google', 'Chrome', 'User Data', 'DevToolsActivePort'));
49
- candidates.push(path.join(localAppData, 'Microsoft', 'Edge', 'User Data', 'DevToolsActivePort'));
50
- } else if (process.platform === 'darwin') {
51
- candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome', 'DevToolsActivePort'));
52
- candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'Microsoft Edge', 'DevToolsActivePort'));
53
- } else {
54
- candidates.push(path.join(os.homedir(), '.config', 'google-chrome', 'DevToolsActivePort'));
55
- candidates.push(path.join(os.homedir(), '.config', 'chromium', 'DevToolsActivePort'));
56
- candidates.push(path.join(os.homedir(), '.config', 'microsoft-edge', 'DevToolsActivePort'));
57
- }
58
-
59
- for (const filePath of candidates) {
60
- try {
61
- const content = fs.readFileSync(filePath, 'utf-8').trim();
62
- const lines = content.split('\n');
63
- if (lines.length >= 2) {
64
- const port = parseInt(lines[0], 10);
65
- const browserPath = lines[1]; // e.g. /devtools/browser/<GUID>
66
- if (port > 0 && browserPath.startsWith('/devtools/browser/')) {
67
- const endpoint = `ws://127.0.0.1:${port}${browserPath}`;
68
- // Verify the port is actually reachable (Chrome may have closed, leaving a stale file)
69
- if (await isPortReachable(port)) {
70
- return endpoint;
71
- }
72
- }
73
- }
74
- } catch {}
75
- }
76
- return null;
77
- }
78
-
79
14
  // Read version from package.json (single source of truth)
80
15
  const __browser_dirname = path.dirname(fileURLToPath(import.meta.url));
81
16
  const PKG_VERSION = (() => { try { return JSON.parse(fs.readFileSync(path.resolve(__browser_dirname, '..', 'package.json'), 'utf-8')).version; } catch { return '0.0.0'; } })();
82
17
 
83
18
  const CONNECT_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_CONNECT_TIMEOUT ?? '30', 10);
84
19
  const STDERR_BUFFER_LIMIT = 16 * 1024;
20
+ const INITIAL_TABS_TIMEOUT_MS = 1500;
85
21
  const TAB_CLEANUP_TIMEOUT_MS = 2000;
86
22
  let _cachedMcpServerPath: string | null | undefined;
87
23
 
88
- 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';
89
25
  type PlaywrightMCPState = 'idle' | 'connecting' | 'connected' | 'closing' | 'closed';
90
26
 
91
27
  type ConnectFailureInput = {
92
28
  kind: ConnectFailureKind;
93
- mode: 'extension' | 'cdp';
94
29
  timeout: number;
95
30
  hasExtensionToken: boolean;
96
31
  tokenFingerprint?: string | null;
@@ -109,41 +44,31 @@ export function formatBrowserConnectError(input: ConnectFailureInput): Error {
109
44
  const suffix = stderr ? `\n\nMCP stderr:\n${stderr}` : '';
110
45
  const tokenHint = input.tokenFingerprint ? ` Token fingerprint: ${input.tokenFingerprint}.` : '';
111
46
 
112
- if (input.mode === 'extension') {
113
- if (input.kind === 'missing-token') {
114
- return new Error(
115
- 'Failed to connect to Playwright MCP Bridge: PLAYWRIGHT_MCP_EXTENSION_TOKEN is not set.\n\n' +
116
- 'Without this token, Chrome will show a manual approval dialog for every new MCP connection. ' +
117
- 'Copy the token from the Playwright MCP Bridge extension and set it in BOTH your shell environment and MCP client config.' +
118
- suffix,
119
- );
120
- }
121
-
122
- if (input.kind === 'extension-not-installed') {
123
- return new Error(
124
- 'Failed to connect to Playwright MCP Bridge: the browser extension did not attach.\n\n' +
125
- 'Make sure Chrome is running and the "Playwright MCP Bridge" extension is installed and enabled. ' +
126
- 'If Chrome shows an approval dialog, click Allow.' +
127
- suffix,
128
- );
129
- }
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
+ }
130
55
 
131
- if (input.kind === 'extension-timeout') {
132
- const likelyCause = input.hasExtensionToken
133
- ? `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.`
134
- : 'PLAYWRIGHT_MCP_EXTENSION_TOKEN is not configured, so the extension may be waiting for manual approval.';
135
- return new Error(
136
- `Timed out connecting to Playwright MCP Bridge (${input.timeout}s).\n\n` +
137
- `${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.` +
138
- suffix,
139
- );
140
- }
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
+ );
141
63
  }
142
64
 
143
- 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.';
144
69
  return new Error(
145
- `Timed out connecting to browser via CDP (${input.timeout}s).\n\n` +
146
- '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.` +
147
72
  suffix,
148
73
  );
149
74
  }
@@ -163,7 +88,6 @@ export function formatBrowserConnectError(input: ConnectFailureInput): Error {
163
88
  }
164
89
 
165
90
  function inferConnectFailureKind(args: {
166
- mode: 'extension' | 'cdp';
167
91
  hasExtensionToken: boolean;
168
92
  stderr: string;
169
93
  rawMessage?: string;
@@ -171,7 +95,7 @@ function inferConnectFailureKind(args: {
171
95
  }): ConnectFailureKind {
172
96
  const haystack = `${args.rawMessage ?? ''}\n${args.stderr}`.toLowerCase();
173
97
 
174
- if (args.mode === 'extension' && !args.hasExtensionToken)
98
+ if (!args.hasExtensionToken)
175
99
  return 'missing-token';
176
100
  if (haystack.includes('extension connection timeout') || haystack.includes('playwright mcp bridge'))
177
101
  return 'extension-not-installed';
@@ -179,11 +103,7 @@ function inferConnectFailureKind(args: {
179
103
  return 'mcp-init';
180
104
  if (args.exited)
181
105
  return 'process-exit';
182
- if (args.mode === 'extension')
183
- return 'extension-timeout';
184
- if (args.mode === 'cdp')
185
- return 'cdp-timeout';
186
- return 'unknown';
106
+ return 'extension-timeout';
187
107
  }
188
108
 
189
109
  // JSON-RPC helpers
@@ -467,7 +387,7 @@ export class PlaywrightMCP {
467
387
  }
468
388
  }
469
389
 
470
- async connect(opts: { timeout?: number; forceExtension?: boolean } = {}): Promise<Page> {
390
+ async connect(opts: { timeout?: number } = {}): Promise<Page> {
471
391
  if (this._state === 'connected' && this._page) return this._page;
472
392
  if (this._state === 'connecting') throw new Error('Playwright MCP is already connecting');
473
393
  if (this._state === 'closing') throw new Error('Playwright MCP is closing');
@@ -481,25 +401,9 @@ export class PlaywrightMCP {
481
401
  this._state = 'connecting';
482
402
  const timeout = opts.timeout ?? CONNECT_TIMEOUT;
483
403
 
484
- // Connection priority:
485
- // 1. OPENCLI_CDP_ENDPOINT env var → explicit CDP endpoint
486
- // 2. OPENCLI_USE_CDP=1 → auto-discover via DevToolsActivePort
487
- // 3. Default → --extension mode (Playwright MCP Bridge)
488
- // Some anti-bot sites (e.g. BOSS Zhipin) detect CDP — use forceExtension to bypass.
489
- const forceExt = opts.forceExtension || process.env.OPENCLI_FORCE_EXTENSION === '1';
490
- let cdpEndpoint: string | null = null;
491
- if (!forceExt) {
492
- if (process.env.OPENCLI_CDP_ENDPOINT) {
493
- cdpEndpoint = process.env.OPENCLI_CDP_ENDPOINT;
494
- } else if (process.env.OPENCLI_USE_CDP === '1') {
495
- cdpEndpoint = await discoverChromeEndpoint();
496
- }
497
- }
498
-
499
404
  return new Promise<Page>((resolve, reject) => {
500
405
  const isDebug = process.env.DEBUG?.includes('opencli:mcp');
501
406
  const debugLog = (msg: string) => isDebug && console.error(`[opencli:mcp] ${msg}`);
502
- const mode: 'extension' | 'cdp' = cdpEndpoint ? 'cdp' : 'extension';
503
407
  const extensionToken = process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN;
504
408
  const tokenFingerprint = getTokenFingerprint(extensionToken);
505
409
  let stderrBuffer = '';
@@ -513,7 +417,6 @@ export class PlaywrightMCP {
513
417
  this._resetAfterFailedConnect();
514
418
  reject(formatBrowserConnectError({
515
419
  kind,
516
- mode,
517
420
  timeout,
518
421
  hasExtensionToken: !!extensionToken,
519
422
  tokenFingerprint,
@@ -534,23 +437,14 @@ export class PlaywrightMCP {
534
437
  const timer = setTimeout(() => {
535
438
  debugLog('Connection timed out');
536
439
  settleError(inferConnectFailureKind({
537
- mode,
538
440
  hasExtensionToken: !!extensionToken,
539
441
  stderr: stderrBuffer,
540
442
  }));
541
443
  }, timeout * 1000);
542
444
 
543
- const mcpArgs: string[] = [mcpPath];
544
- if (cdpEndpoint) {
545
- mcpArgs.push('--cdp-endpoint', cdpEndpoint);
546
- } else {
547
- mcpArgs.push('--extension');
548
- }
445
+ const mcpArgs: string[] = [mcpPath, '--extension'];
549
446
  if (process.env.OPENCLI_VERBOSE) {
550
- console.error(`[opencli] CDP mode: ${cdpEndpoint ? `auto-discovered ${cdpEndpoint}` : 'fallback to --extension'}`);
551
- if (mode === 'extension') {
552
- console.error(`[opencli] Extension token: ${extensionToken ? `configured (fingerprint ${tokenFingerprint})` : 'missing'}`);
553
- }
447
+ console.error(`[opencli] Extension token: ${extensionToken ? `configured (fingerprint ${tokenFingerprint})` : 'missing'}`);
554
448
  }
555
449
  if (process.env.OPENCLI_BROWSER_EXECUTABLE_PATH) {
556
450
  mcpArgs.push('--executablePath', process.env.OPENCLI_BROWSER_EXECUTABLE_PATH);
@@ -606,7 +500,6 @@ export class PlaywrightMCP {
606
500
  this._rejectPendingRequests(new Error(`Playwright MCP process exited before response${code == null ? '' : ` (code ${code})`}`));
607
501
  if (!settled) {
608
502
  settleError(inferConnectFailureKind({
609
- mode,
610
503
  hasExtensionToken: !!extensionToken,
611
504
  stderr: stderrBuffer,
612
505
  exited: true,
@@ -624,7 +517,6 @@ export class PlaywrightMCP {
624
517
  debugLog('Got initialize response');
625
518
  if (resp.error) {
626
519
  settleError(inferConnectFailureKind({
627
- mode,
628
520
  hasExtensionToken: !!extensionToken,
629
521
  stderr: stderrBuffer,
630
522
  rawMessage: `MCP init failed: ${resp.error.message}`,
@@ -636,9 +528,9 @@ export class PlaywrightMCP {
636
528
  debugLog(`SEND: ${initializedMsg.trim()}`);
637
529
  this._proc?.stdin?.write(initializedMsg);
638
530
 
639
- // Get initial tab count for cleanup (with timeout CDP mode may hang on browser_tabs)
531
+ // Use tabs as a readiness probe and for tab cleanup bookkeeping.
640
532
  debugLog('Fetching initial tabs count...');
641
- withTimeout(page.tabs(), 5000, 'Timed out fetching initial tabs').then((tabs: any) => {
533
+ withTimeout(page.tabs(), INITIAL_TABS_TIMEOUT_MS, 'Timed out fetching initial tabs').then((tabs: any) => {
642
534
  debugLog(`Tabs response: ${typeof tabs === 'string' ? tabs : JSON.stringify(tabs)}`);
643
535
  this._initialTabIdentities = extractTabIdentities(tabs);
644
536
  settleSuccess(page);
@@ -653,13 +545,14 @@ export class PlaywrightMCP {
653
545
  });
654
546
  }
655
547
 
548
+
656
549
  async close(): Promise<void> {
657
550
  if (this._closingPromise) return this._closingPromise;
658
551
  if (this._state === 'closed') return;
659
552
  this._state = 'closing';
660
553
  this._closingPromise = (async () => {
661
554
  try {
662
- // Close tabs opened during this session (site tabs + extension tabs)
555
+ // Extension mode opens bridge/session tabs that we can clean up best-effort.
663
556
  if (this._page && this._proc && !this._proc.killed) {
664
557
  try {
665
558
  const tabs = await withTimeout(this._page.tabs(), TAB_CLEANUP_TIMEOUT_MS, 'Timed out fetching tabs during cleanup');
@@ -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, 前端)' },
@@ -11,7 +11,7 @@ cli({
11
11
  domain: 'www.v2ex.com',
12
12
  strategy: Strategy.COOKIE,
13
13
  browser: true,
14
- forceExtension: true,
14
+
15
15
  args: [],
16
16
  columns: ['status', 'message'],
17
17
  func: async (page: IPage | null) => {
@@ -11,7 +11,7 @@ cli({
11
11
  domain: 'www.v2ex.com',
12
12
  strategy: Strategy.COOKIE,
13
13
  browser: true,
14
- forceExtension: true,
14
+
15
15
  args: [],
16
16
  columns: ['username', 'balance', 'unread_notifications', 'daily_reward_ready'],
17
17
  func: async (page: IPage | null) => {
@@ -11,7 +11,7 @@ cli({
11
11
  domain: 'www.v2ex.com',
12
12
  strategy: Strategy.COOKIE,
13
13
  browser: true,
14
- forceExtension: true,
14
+
15
15
  args: [
16
16
  { name: 'limit', type: 'int', default: 20, help: 'Number of notifications' }
17
17
  ],
@@ -92,18 +92,12 @@ describe('doctor report rendering', () => {
92
92
  envFingerprint: 'fp1',
93
93
  shellFiles: [{ path: '/tmp/.zshrc', exists: true, token: 'abc123', fingerprint: 'fp1' }],
94
94
  configs: [{ path: '/tmp/mcp.json', exists: true, format: 'json', token: 'abc123', fingerprint: 'fp1', writable: true }],
95
- remoteDebuggingEnabled: true,
96
- remoteDebuggingEndpoint: 'ws://127.0.0.1:9222/devtools/browser/test',
97
- cdpEnabled: false,
98
- cdpToken: null,
99
- cdpFingerprint: null,
100
95
  recommendedToken: 'abc123',
101
96
  recommendedFingerprint: 'fp1',
102
97
  warnings: [],
103
98
  issues: [],
104
99
  });
105
100
 
106
- expect(text).toContain('[OK] Chrome remote debugging: enabled');
107
101
  expect(text).toContain('[OK] Environment token: configured (fp1)');
108
102
  expect(text).toContain('[OK] MCP config /tmp/mcp.json: configured (fp1)');
109
103
  });
@@ -114,18 +108,12 @@ describe('doctor report rendering', () => {
114
108
  envFingerprint: 'fp1',
115
109
  shellFiles: [{ path: '/tmp/.zshrc', exists: true, token: 'def456', fingerprint: 'fp2' }],
116
110
  configs: [{ path: '/tmp/mcp.json', exists: true, format: 'json', token: 'abc123', fingerprint: 'fp1', writable: true }],
117
- remoteDebuggingEnabled: false,
118
- remoteDebuggingEndpoint: null,
119
- cdpEnabled: false,
120
- cdpToken: null,
121
- cdpFingerprint: null,
122
111
  recommendedToken: 'abc123',
123
112
  recommendedFingerprint: 'fp1',
124
- warnings: ['Chrome remote debugging appears to be disabled or Chrome is not currently exposing a DevTools endpoint.'],
113
+ warnings: [],
125
114
  issues: ['Detected inconsistent Playwright MCP tokens across env/config files.'],
126
115
  });
127
116
 
128
- expect(text).toContain('[WARN] Chrome remote debugging: disabled');
129
117
  expect(text).toContain('[MISMATCH] Environment token: configured (fp1)');
130
118
  expect(text).toContain('[MISMATCH] Shell file /tmp/.zshrc: configured (fp2)');
131
119
  expect(text).toContain('[MISMATCH] Recommended token fingerprint: fp1');
package/src/doctor.ts CHANGED
@@ -4,7 +4,7 @@ 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
6
  import type { IPage } from './types.js';
7
- import { PlaywrightMCP, discoverChromeEndpoint, getTokenFingerprint } from './browser.js';
7
+ import { PlaywrightMCP, getTokenFingerprint } from './browser.js';
8
8
  import { browserSession } from './runtime.js';
9
9
 
10
10
  const PLAYWRIGHT_SERVER_NAME = 'playwright';
@@ -45,11 +45,6 @@ export type DoctorReport = {
45
45
  envFingerprint: string | null;
46
46
  shellFiles: ShellFileStatus[];
47
47
  configs: McpConfigStatus[];
48
- remoteDebuggingEnabled: boolean;
49
- remoteDebuggingEndpoint: string | null;
50
- cdpEnabled: boolean;
51
- cdpToken: string | null;
52
- cdpFingerprint: string | null;
53
48
  recommendedToken: string | null;
54
49
  recommendedFingerprint: string | null;
55
50
  warnings: string[];
@@ -225,45 +220,10 @@ function readConfigStatus(filePath: string): McpConfigStatus {
225
220
  }
226
221
  }
227
222
 
228
- async function extractTokenViaCdp(): Promise<string | null> {
229
- if (!(process.env.OPENCLI_USE_CDP === '1' || process.env.OPENCLI_CDP_ENDPOINT))
230
- return null;
231
- const candidates = [
232
- `chrome-extension://${PLAYWRIGHT_EXTENSION_ID}/options.html`,
233
- `chrome-extension://${PLAYWRIGHT_EXTENSION_ID}/popup.html`,
234
- `chrome-extension://${PLAYWRIGHT_EXTENSION_ID}/connect.html`,
235
- `chrome-extension://${PLAYWRIGHT_EXTENSION_ID}/index.html`,
236
- ];
237
- const result = await browserSession(PlaywrightMCP, async (page: IPage) => {
238
- for (const url of candidates) {
239
- try {
240
- await page.goto(url);
241
- await page.wait(1);
242
- const token = await page.evaluate(`() => {
243
- const values = new Set();
244
- const push = (value) => {
245
- if (!value || typeof value !== 'string') return;
246
- for (const match of value.matchAll(/[A-Za-z0-9_-]{24,}/g)) values.add(match[0]);
247
- };
248
- document.querySelectorAll('input, textarea, code, pre, span, div').forEach((el) => {
249
- push(el.value);
250
- push(el.textContent || '');
251
- push(el.getAttribute && el.getAttribute('value'));
252
- });
253
- return Array.from(values);
254
- }`);
255
- const matches = Array.isArray(token) ? token.filter((v: string) => v.length >= 24) : [];
256
- if (matches.length > 0) return matches.sort((a: string, b: string) => b.length - a.length)[0];
257
- } catch {}
258
- }
259
- return null;
260
- });
261
- return typeof result === 'string' && result ? result : null;
262
- }
223
+
263
224
 
264
225
  export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<DoctorReport> {
265
226
  const envToken = process.env[PLAYWRIGHT_TOKEN_ENV] ?? null;
266
- const remoteDebuggingEndpoint = await discoverChromeEndpoint().catch(() => null);
267
227
  const shellPath = opts.shellRc ?? getDefaultShellRcPath();
268
228
  const shellFiles: ShellFileStatus[] = [shellPath].map((filePath) => {
269
229
  if (!fileExists(filePath)) return { path: filePath, exists: false, token: null, fingerprint: null };
@@ -273,17 +233,15 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
273
233
  });
274
234
  const configPaths = opts.configPaths?.length ? opts.configPaths : getDefaultMcpConfigPaths();
275
235
  const configs = configPaths.map(readConfigStatus);
276
- const cdpToken = !opts.token && !envToken ? await extractTokenViaCdp().catch(() => null) : null;
277
236
 
278
237
  const allTokens = [
279
238
  opts.token ?? null,
280
239
  envToken,
281
240
  ...shellFiles.map(s => s.token),
282
241
  ...configs.map(c => c.token),
283
- cdpToken,
284
242
  ].filter((v): v is string => !!v);
285
243
  const uniqueTokens = [...new Set(allTokens)];
286
- const recommendedToken = opts.token ?? envToken ?? (uniqueTokens.length === 1 ? uniqueTokens[0] : cdpToken) ?? null;
244
+ const recommendedToken = opts.token ?? envToken ?? (uniqueTokens.length === 1 ? uniqueTokens[0] : null) ?? null;
287
245
 
288
246
  const report: DoctorReport = {
289
247
  cliVersion: opts.cliVersion,
@@ -291,11 +249,6 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
291
249
  envFingerprint: getTokenFingerprint(envToken ?? undefined),
292
250
  shellFiles,
293
251
  configs,
294
- remoteDebuggingEnabled: !!remoteDebuggingEndpoint,
295
- remoteDebuggingEndpoint,
296
- cdpEnabled: process.env.OPENCLI_USE_CDP === '1' || !!process.env.OPENCLI_CDP_ENDPOINT,
297
- cdpToken,
298
- cdpFingerprint: getTokenFingerprint(cdpToken ?? undefined),
299
252
  recommendedToken,
300
253
  recommendedFingerprint: getTokenFingerprint(recommendedToken ?? undefined),
301
254
  warnings: [],
@@ -306,13 +259,11 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
306
259
  if (!shellFiles.some(s => s.token)) report.issues.push('Shell startup file does not export PLAYWRIGHT_MCP_EXTENSION_TOKEN.');
307
260
  if (!configs.some(c => c.token)) report.issues.push('No scanned MCP config currently contains a Playwright extension token.');
308
261
  if (uniqueTokens.length > 1) report.issues.push('Detected inconsistent Playwright MCP tokens across env/config files.');
309
- if (!report.remoteDebuggingEnabled) report.warnings.push('Chrome remote debugging appears to be disabled or Chrome is not currently exposing a DevTools endpoint.');
310
262
  for (const config of configs) {
311
263
  if (config.parseError) report.warnings.push(`Could not parse ${config.path}: ${config.parseError}`);
312
264
  }
313
265
  if (!recommendedToken) {
314
- if (report.cdpEnabled) report.warnings.push('CDP is enabled, but no token could be extracted automatically from the extension UI.');
315
- else report.warnings.push('No token source found. Enable OPENCLI_USE_CDP=1 to allow a best-effort token read from the extension page.');
266
+ report.warnings.push('No token source found.');
316
267
  }
317
268
  return report;
318
269
  }
@@ -326,8 +277,6 @@ export function renderBrowserDoctorReport(report: DoctorReport): string {
326
277
  const uniqueFingerprints = [...new Set(tokenFingerprints)];
327
278
  const hasMismatch = uniqueFingerprints.length > 1;
328
279
  const lines = [`opencli v${report.cliVersion ?? 'unknown'} doctor`, ''];
329
- lines.push(statusLine(report.remoteDebuggingEnabled ? 'OK' : 'WARN', `Chrome remote debugging: ${report.remoteDebuggingEnabled ? 'enabled' : 'disabled'}`));
330
- if (report.remoteDebuggingEndpoint) lines.push(` ${report.remoteDebuggingEndpoint}`);
331
280
 
332
281
  const envStatus: ReportStatus = !report.envToken ? 'MISSING' : hasMismatch ? 'MISMATCH' : 'OK';
333
282
  lines.push(statusLine(envStatus, `Environment token: ${tokenSummary(report.envToken, report.envFingerprint)}`));
@@ -354,10 +303,6 @@ export function renderBrowserDoctorReport(report: DoctorReport): string {
354
303
  lines.push(statusLine('MISSING', 'MCP config: no existing config files found in scanned locations'));
355
304
  }
356
305
  if (missingConfigCount > 0) lines.push(` Other scanned config locations not present: ${missingConfigCount}`);
357
- if (report.cdpEnabled) {
358
- const cdpStatus: ReportStatus = report.cdpToken ? 'OK' : 'WARN';
359
- lines.push(statusLine(cdpStatus, `CDP token probe: ${tokenSummary(report.cdpToken, report.cdpFingerprint)}`));
360
- }
361
306
  lines.push('');
362
307
  lines.push(statusLine(
363
308
  hasMismatch ? 'MISMATCH' : report.recommendedToken ? 'OK' : 'WARN',
@@ -391,7 +336,7 @@ function writeFileWithMkdir(filePath: string, content: string): void {
391
336
 
392
337
  export async function applyBrowserDoctorFix(report: DoctorReport, opts: DoctorOptions = {}): Promise<string[]> {
393
338
  const token = opts.token ?? report.recommendedToken;
394
- if (!token) throw new Error('No Playwright MCP token is available to write. Provide --token or enable CDP token probing first.');
339
+ if (!token) throw new Error('No Playwright MCP token is available to write. Provide --token first.');
395
340
 
396
341
  const plannedWrites: string[] = [];
397
342
  const shellPath = opts.shellRc ?? report.shellFiles[0]?.path ?? getDefaultShellRcPath();
package/src/main.ts CHANGED
@@ -144,7 +144,7 @@ for (const [, cmd] of registry) {
144
144
  if (actionOpts.verbose) process.env.OPENCLI_VERBOSE = '1';
145
145
  let result: any;
146
146
  if (cmd.browser) {
147
- 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 });
147
+ result = await browserSession(PlaywrightMCP, async (page) => runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) }));
148
148
  } else { result = await executeCommand(cmd, null, kwargs, actionOpts.verbose); }
149
149
  if (actionOpts.verbose && (!result || (Array.isArray(result) && result.length === 0))) {
150
150
  console.error(chalk.yellow(`[Verbose] Warning: Command returned an empty result. If the website structural API changed or requires authentication, check the network or update the adapter.`));
package/src/registry.ts CHANGED
@@ -37,10 +37,7 @@ export interface CliCommand {
37
37
  /** Internal: lazy-loaded TS module support */
38
38
  _lazy?: boolean;
39
39
  _modulePath?: string;
40
- /** Force extension bridge mode (bypass CDP), for anti-bot sites */
41
- forceExtension?: boolean;
42
40
  }
43
-
44
41
  export interface CliOptions {
45
42
  site: string;
46
43
  name: string;
@@ -53,10 +50,7 @@ export interface CliOptions {
53
50
  func?: (page: IPage | null, kwargs: Record<string, any>, debug?: boolean) => Promise<any>;
54
51
  pipeline?: any[];
55
52
  timeoutSeconds?: number;
56
- /** Force extension bridge mode (bypass CDP), for anti-bot sites */
57
- forceExtension?: boolean;
58
53
  }
59
-
60
54
  const _registry = new Map<string, CliCommand>();
61
55
 
62
56
  export function cli(opts: CliOptions): CliCommand {
@@ -72,7 +66,6 @@ export function cli(opts: CliOptions): CliCommand {
72
66
  func: opts.func,
73
67
  pipeline: opts.pipeline,
74
68
  timeoutSeconds: opts.timeoutSeconds,
75
- forceExtension: opts.forceExtension,
76
69
  };
77
70
 
78
71
  const key = fullName(cmd);
package/src/runtime.ts CHANGED
@@ -27,11 +27,10 @@ export async function runWithTimeout<T>(
27
27
  export async function browserSession<T>(
28
28
  BrowserFactory: new () => any,
29
29
  fn: (page: IPage) => Promise<T>,
30
- opts?: { forceExtension?: boolean },
31
30
  ): Promise<T> {
32
31
  const mcp = new BrowserFactory();
33
32
  try {
34
- const page = await mcp.connect({ timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT, forceExtension: opts?.forceExtension });
33
+ const page = await mcp.connect({ timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT });
35
34
  return await fn(page);
36
35
  } finally {
37
36
  await mcp.close().catch(() => {});