@jackwener/opencli 0.4.2 → 0.4.3

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.
Files changed (64) hide show
  1. package/CLI-CREATOR.md +10 -10
  2. package/LICENSE +28 -0
  3. package/README.md +113 -63
  4. package/README.zh-CN.md +114 -63
  5. package/SKILL.md +21 -4
  6. package/dist/browser.d.ts +21 -2
  7. package/dist/browser.js +269 -15
  8. package/dist/browser.test.d.ts +1 -0
  9. package/dist/browser.test.js +43 -0
  10. package/dist/build-manifest.js +4 -0
  11. package/dist/cli-manifest.json +279 -3
  12. package/dist/clis/boss/search.js +186 -30
  13. package/dist/clis/twitter/delete.d.ts +1 -0
  14. package/dist/clis/twitter/delete.js +73 -0
  15. package/dist/clis/twitter/followers.d.ts +1 -0
  16. package/dist/clis/twitter/followers.js +104 -0
  17. package/dist/clis/twitter/following.d.ts +1 -0
  18. package/dist/clis/twitter/following.js +90 -0
  19. package/dist/clis/twitter/like.d.ts +1 -0
  20. package/dist/clis/twitter/like.js +69 -0
  21. package/dist/clis/twitter/notifications.d.ts +1 -0
  22. package/dist/clis/twitter/notifications.js +109 -0
  23. package/dist/clis/twitter/post.d.ts +1 -0
  24. package/dist/clis/twitter/post.js +63 -0
  25. package/dist/clis/twitter/reply.d.ts +1 -0
  26. package/dist/clis/twitter/reply.js +57 -0
  27. package/dist/clis/v2ex/daily.d.ts +1 -0
  28. package/dist/clis/v2ex/daily.js +98 -0
  29. package/dist/clis/v2ex/me.d.ts +1 -0
  30. package/dist/clis/v2ex/me.js +99 -0
  31. package/dist/clis/v2ex/notifications.d.ts +1 -0
  32. package/dist/clis/v2ex/notifications.js +72 -0
  33. package/dist/doctor.d.ts +50 -0
  34. package/dist/doctor.js +372 -0
  35. package/dist/doctor.test.d.ts +1 -0
  36. package/dist/doctor.test.js +114 -0
  37. package/dist/main.js +47 -5
  38. package/dist/output.test.d.ts +1 -0
  39. package/dist/output.test.js +20 -0
  40. package/dist/registry.d.ts +4 -0
  41. package/dist/registry.js +1 -0
  42. package/dist/runtime.d.ts +3 -1
  43. package/dist/runtime.js +2 -2
  44. package/package.json +2 -2
  45. package/src/browser.test.ts +51 -0
  46. package/src/browser.ts +318 -22
  47. package/src/build-manifest.ts +4 -0
  48. package/src/clis/boss/search.ts +196 -29
  49. package/src/clis/twitter/delete.ts +78 -0
  50. package/src/clis/twitter/followers.ts +119 -0
  51. package/src/clis/twitter/following.ts +105 -0
  52. package/src/clis/twitter/like.ts +74 -0
  53. package/src/clis/twitter/notifications.ts +119 -0
  54. package/src/clis/twitter/post.ts +68 -0
  55. package/src/clis/twitter/reply.ts +62 -0
  56. package/src/clis/v2ex/daily.ts +105 -0
  57. package/src/clis/v2ex/me.ts +103 -0
  58. package/src/clis/v2ex/notifications.ts +77 -0
  59. package/src/doctor.test.ts +133 -0
  60. package/src/doctor.ts +424 -0
  61. package/src/main.ts +47 -4
  62. package/src/output.test.ts +27 -0
  63. package/src/registry.ts +5 -0
  64. package/src/runtime.ts +2 -1
package/src/browser.ts CHANGED
@@ -1,15 +1,81 @@
1
1
  /**
2
- * Browser interaction via Playwright MCP Bridge extension.
3
- * Connects to an existing Chrome browser through the extension's stdio JSON-RPC.
2
+ * Browser interaction via Chrome DevTools Protocol.
3
+ * Connects to an existing Chrome browser through CDP auto-discovery or extension bridge.
4
4
  */
5
5
 
6
6
  import { spawn, execSync, type ChildProcess } from 'node:child_process';
7
+ import { createHash } from 'node:crypto';
8
+ import * as net from 'node:net';
7
9
  import { fileURLToPath } from 'node:url';
8
10
  import * as fs from 'node:fs';
9
11
  import * as os from 'node:os';
10
12
  import * as path from 'node:path';
11
13
  import { formatSnapshot } from './snapshotFormatter.js';
12
14
 
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
+
13
79
  // Read version from package.json (single source of truth)
14
80
  const __browser_dirname = path.dirname(fileURLToPath(import.meta.url));
15
81
  const PKG_VERSION = (() => { try { return JSON.parse(fs.readFileSync(path.resolve(__browser_dirname, '..', 'package.json'), 'utf-8')).version; } catch { return '0.0.0'; } })();
@@ -19,6 +85,106 @@ const EXTENSION_LOCK_POLL = parseInt(process.env.OPENCLI_EXTENSION_LOCK_POLL_INT
19
85
  const CONNECT_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_CONNECT_TIMEOUT ?? '30', 10);
20
86
  const LOCK_DIR = path.join(os.tmpdir(), 'opencli-mcp-lock');
21
87
 
88
+ type ConnectFailureKind = 'missing-token' | 'extension-timeout' | 'extension-not-installed' | 'cdp-timeout' | 'mcp-init' | 'process-exit' | 'unknown';
89
+
90
+ type ConnectFailureInput = {
91
+ kind: ConnectFailureKind;
92
+ mode: 'extension' | 'cdp';
93
+ timeout: number;
94
+ hasExtensionToken: boolean;
95
+ tokenFingerprint?: string | null;
96
+ stderr?: string;
97
+ exitCode?: number | null;
98
+ rawMessage?: string;
99
+ };
100
+
101
+ export function getTokenFingerprint(token: string | undefined): string | null {
102
+ if (!token) return null;
103
+ return createHash('sha256').update(token).digest('hex').slice(0, 8);
104
+ }
105
+
106
+ export function formatBrowserConnectError(input: ConnectFailureInput): Error {
107
+ const stderr = input.stderr?.trim();
108
+ const suffix = stderr ? `\n\nMCP stderr:\n${stderr}` : '';
109
+ const tokenHint = input.tokenFingerprint ? ` Token fingerprint: ${input.tokenFingerprint}.` : '';
110
+
111
+ if (input.mode === 'extension') {
112
+ if (input.kind === 'missing-token') {
113
+ return new Error(
114
+ 'Failed to connect to Playwright MCP Bridge: PLAYWRIGHT_MCP_EXTENSION_TOKEN is not set.\n\n' +
115
+ 'Without this token, Chrome will show a manual approval dialog for every new MCP connection. ' +
116
+ 'Copy the token from the Playwright MCP Bridge extension and set it in BOTH your shell environment and MCP client config.' +
117
+ suffix,
118
+ );
119
+ }
120
+
121
+ if (input.kind === 'extension-not-installed') {
122
+ return new Error(
123
+ 'Failed to connect to Playwright MCP Bridge: the browser extension did not attach.\n\n' +
124
+ 'Make sure Chrome is running and the "Playwright MCP Bridge" extension is installed and enabled. ' +
125
+ 'If Chrome shows an approval dialog, click Allow.' +
126
+ suffix,
127
+ );
128
+ }
129
+
130
+ if (input.kind === 'extension-timeout') {
131
+ const likelyCause = input.hasExtensionToken
132
+ ? `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.`
133
+ : 'PLAYWRIGHT_MCP_EXTENSION_TOKEN is not configured, so the extension may be waiting for manual approval.';
134
+ return new Error(
135
+ `Timed out connecting to Playwright MCP Bridge (${input.timeout}s).\n\n` +
136
+ `${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.` +
137
+ suffix,
138
+ );
139
+ }
140
+ }
141
+
142
+ if (input.mode === 'cdp' && input.kind === 'cdp-timeout') {
143
+ return new Error(
144
+ `Timed out connecting to browser via CDP (${input.timeout}s).\n\n` +
145
+ 'Make sure Chrome is running and remote debugging is enabled at chrome://inspect#remote-debugging, or set OPENCLI_CDP_ENDPOINT explicitly.' +
146
+ suffix,
147
+ );
148
+ }
149
+
150
+ if (input.kind === 'mcp-init') {
151
+ return new Error(`Failed to initialize Playwright MCP: ${input.rawMessage ?? 'unknown error'}${suffix}`);
152
+ }
153
+
154
+ if (input.kind === 'process-exit') {
155
+ return new Error(
156
+ `Playwright MCP process exited before the browser connection was established${input.exitCode == null ? '' : ` (code ${input.exitCode})`}.` +
157
+ suffix,
158
+ );
159
+ }
160
+
161
+ return new Error(input.rawMessage ?? 'Failed to connect to browser');
162
+ }
163
+
164
+ function inferConnectFailureKind(args: {
165
+ mode: 'extension' | 'cdp';
166
+ hasExtensionToken: boolean;
167
+ stderr: string;
168
+ rawMessage?: string;
169
+ exited?: boolean;
170
+ }): ConnectFailureKind {
171
+ const haystack = `${args.rawMessage ?? ''}\n${args.stderr}`.toLowerCase();
172
+
173
+ if (args.mode === 'extension' && !args.hasExtensionToken)
174
+ return 'missing-token';
175
+ if (haystack.includes('extension connection timeout') || haystack.includes('playwright mcp bridge'))
176
+ return 'extension-not-installed';
177
+ if (args.rawMessage?.startsWith('MCP init failed:'))
178
+ return 'mcp-init';
179
+ if (args.exited)
180
+ return 'process-exit';
181
+ if (args.mode === 'extension')
182
+ return 'extension-timeout';
183
+ if (args.mode === 'cdp')
184
+ return 'cdp-timeout';
185
+ return 'unknown';
186
+ }
187
+
22
188
  // JSON-RPC helpers
23
189
  let _nextId = 1;
24
190
  function jsonRpcRequest(method: string, params: Record<string, any> = {}): string {
@@ -231,6 +397,28 @@ export class Page implements IPage {
231
397
  * Playwright MCP process manager.
232
398
  */
233
399
  export class PlaywrightMCP {
400
+ private static _activeInsts: Set<PlaywrightMCP> = new Set();
401
+ private static _cleanupRegistered = false;
402
+
403
+ private static _registerGlobalCleanup() {
404
+ if (this._cleanupRegistered) return;
405
+ this._cleanupRegistered = true;
406
+ const cleanup = () => {
407
+ for (const inst of this._activeInsts) {
408
+ if (inst._lockAcquired) {
409
+ try { fs.rmdirSync(LOCK_DIR); } catch {}
410
+ inst._lockAcquired = false;
411
+ }
412
+ if (inst._proc && !inst._proc.killed) {
413
+ try { inst._proc.kill('SIGKILL'); } catch {}
414
+ }
415
+ }
416
+ };
417
+ process.on('exit', cleanup);
418
+ process.on('SIGINT', () => { cleanup(); process.exit(130); });
419
+ process.on('SIGTERM', () => { cleanup(); process.exit(143); });
420
+ }
421
+
234
422
  private _proc: ChildProcess | null = null;
235
423
  private _buffer = '';
236
424
  private _waiters: Array<(data: any) => void> = [];
@@ -239,24 +427,89 @@ export class PlaywrightMCP {
239
427
 
240
428
  private _page: Page | null = null;
241
429
 
242
- async connect(opts: { timeout?: number } = {}): Promise<Page> {
430
+ async connect(opts: { timeout?: number; forceExtension?: boolean } = {}): Promise<Page> {
243
431
  await this._acquireLock();
244
432
  const timeout = opts.timeout ?? CONNECT_TIMEOUT;
245
433
  const mcpPath = findMcpServerPath();
246
434
  if (!mcpPath) throw new Error('Playwright MCP server not found. Install: npm install -D @playwright/mcp');
247
435
 
248
- return new Promise<Page>((resolve, reject) => {
249
- const timer = setTimeout(() => reject(new Error(`Timed out connecting to browser (${timeout}s)`)), timeout * 1000);
250
-
251
- const mcpArgs = [mcpPath, '--extension'];
252
- if (process.env.OPENCLI_BROWSER_EXECUTABLE_PATH) {
253
- mcpArgs.push('--executablePath', process.env.OPENCLI_BROWSER_EXECUTABLE_PATH);
436
+ // Connection priority:
437
+ // 1. OPENCLI_CDP_ENDPOINT env var explicit CDP endpoint
438
+ // 2. OPENCLI_USE_CDP=1 → auto-discover via DevToolsActivePort
439
+ // 3. Default --extension mode (Playwright MCP Bridge)
440
+ // Some anti-bot sites (e.g. BOSS Zhipin) detect CDP — use forceExtension to bypass.
441
+ const forceExt = opts.forceExtension || process.env.OPENCLI_FORCE_EXTENSION === '1';
442
+ let cdpEndpoint: string | null = null;
443
+ if (!forceExt) {
444
+ if (process.env.OPENCLI_CDP_ENDPOINT) {
445
+ cdpEndpoint = process.env.OPENCLI_CDP_ENDPOINT;
446
+ } else if (process.env.OPENCLI_USE_CDP === '1') {
447
+ cdpEndpoint = await discoverChromeEndpoint();
448
+ }
254
449
  }
255
450
 
256
- this._proc = spawn('node', mcpArgs, {
257
- stdio: ['pipe', 'pipe', 'pipe'],
258
- env: { ...process.env, ...(process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN ? { PLAYWRIGHT_MCP_EXTENSION_TOKEN: process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN } : {}) },
259
- });
451
+ return new Promise<Page>((resolve, reject) => {
452
+ const isDebug = process.env.DEBUG?.includes('opencli:mcp');
453
+ const debugLog = (msg: string) => isDebug && console.error(`[opencli:mcp] ${msg}`);
454
+ const mode: 'extension' | 'cdp' = cdpEndpoint ? 'cdp' : 'extension';
455
+ const extensionToken = process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN;
456
+ const tokenFingerprint = getTokenFingerprint(extensionToken);
457
+ let stderrBuffer = '';
458
+ let settled = false;
459
+
460
+ const settleError = (kind: ConnectFailureKind, extra: { rawMessage?: string; exitCode?: number | null } = {}) => {
461
+ if (settled) return;
462
+ settled = true;
463
+ clearTimeout(timer);
464
+ reject(formatBrowserConnectError({
465
+ kind,
466
+ mode,
467
+ timeout,
468
+ hasExtensionToken: !!extensionToken,
469
+ tokenFingerprint,
470
+ stderr: stderrBuffer,
471
+ exitCode: extra.exitCode,
472
+ rawMessage: extra.rawMessage,
473
+ }));
474
+ };
475
+
476
+ const settleSuccess = (pageToResolve: Page) => {
477
+ if (settled) return;
478
+ settled = true;
479
+ clearTimeout(timer);
480
+ resolve(pageToResolve);
481
+ };
482
+
483
+ const timer = setTimeout(() => {
484
+ debugLog('Connection timed out');
485
+ settleError(inferConnectFailureKind({
486
+ mode,
487
+ hasExtensionToken: !!extensionToken,
488
+ stderr: stderrBuffer,
489
+ }));
490
+ }, timeout * 1000);
491
+
492
+ const mcpArgs: string[] = [mcpPath];
493
+ if (cdpEndpoint) {
494
+ mcpArgs.push('--cdp-endpoint', cdpEndpoint);
495
+ } else {
496
+ mcpArgs.push('--extension');
497
+ }
498
+ if (process.env.OPENCLI_VERBOSE) {
499
+ console.error(`[opencli] CDP mode: ${cdpEndpoint ? `auto-discovered ${cdpEndpoint}` : 'fallback to --extension'}`);
500
+ if (mode === 'extension') {
501
+ console.error(`[opencli] Extension token: ${extensionToken ? `configured (fingerprint ${tokenFingerprint})` : 'missing'}`);
502
+ }
503
+ }
504
+ if (process.env.OPENCLI_BROWSER_EXECUTABLE_PATH) {
505
+ mcpArgs.push('--executablePath', process.env.OPENCLI_BROWSER_EXECUTABLE_PATH);
506
+ }
507
+ debugLog(`Spawning node ${mcpArgs.join(' ')}`);
508
+
509
+ this._proc = spawn('node', mcpArgs, {
510
+ stdio: ['pipe', 'pipe', 'pipe'],
511
+ env: { ...process.env },
512
+ });
260
513
 
261
514
  // Increase max listeners to avoid warnings
262
515
  this._proc.setMaxListeners(20);
@@ -274,16 +527,37 @@ export class PlaywrightMCP {
274
527
  this._buffer = lines.pop() ?? '';
275
528
  for (const line of lines) {
276
529
  if (!line.trim()) continue;
530
+ debugLog(`RECV: ${line}`);
277
531
  try {
278
532
  const parsed = JSON.parse(line);
279
533
  const waiter = this._waiters.shift();
280
534
  if (waiter) waiter(parsed);
281
- } catch {}
535
+ } catch (e) {
536
+ debugLog(`Parse error: ${e}`);
537
+ }
282
538
  }
283
539
  });
284
540
 
285
- this._proc.stderr?.on('data', () => {});
286
- this._proc.on('error', (err) => { clearTimeout(timer); reject(err); });
541
+ this._proc.stderr?.on('data', (chunk: Buffer) => {
542
+ const text = chunk.toString();
543
+ stderrBuffer += text;
544
+ debugLog(`STDERR: ${text}`);
545
+ });
546
+ this._proc.on('error', (err) => {
547
+ debugLog(`Subprocess error: ${err.message}`);
548
+ settleError('process-exit', { rawMessage: err.message });
549
+ });
550
+ this._proc.on('close', (code) => {
551
+ debugLog(`Subprocess closed with code ${code}`);
552
+ if (!settled) {
553
+ settleError(inferConnectFailureKind({
554
+ mode,
555
+ hasExtensionToken: !!extensionToken,
556
+ stderr: stderrBuffer,
557
+ exited: true,
558
+ }), { exitCode: code });
559
+ }
560
+ });
287
561
 
288
562
  // Initialize: send initialize request
289
563
  const initMsg = jsonRpcRequest('initialize', {
@@ -291,25 +565,46 @@ export class PlaywrightMCP {
291
565
  capabilities: {},
292
566
  clientInfo: { name: 'opencli', version: PKG_VERSION },
293
567
  });
568
+ debugLog(`SEND: ${initMsg.trim()}`);
294
569
  this._proc.stdin?.write(initMsg);
295
570
 
296
571
  // Wait for initialize response, then send initialized notification
297
572
  const origRecv = () => new Promise<any>((res) => { this._waiters.push(res); });
573
+ debugLog('Waiting for initialize response...');
298
574
  origRecv().then((resp) => {
299
- if (resp.error) { clearTimeout(timer); reject(new Error(`MCP init failed: ${resp.error.message}`)); return; }
300
- this._proc?.stdin?.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n');
575
+ debugLog('Got initialize response');
576
+ if (resp.error) {
577
+ settleError(inferConnectFailureKind({
578
+ mode,
579
+ hasExtensionToken: !!extensionToken,
580
+ stderr: stderrBuffer,
581
+ rawMessage: `MCP init failed: ${resp.error.message}`,
582
+ }), { rawMessage: resp.error.message });
583
+ return;
584
+ }
585
+
586
+ const initializedMsg = JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n';
587
+ debugLog(`SEND: ${initializedMsg.trim()}`);
588
+ this._proc?.stdin?.write(initializedMsg);
301
589
 
302
590
  // Get initial tab count for cleanup
591
+ debugLog('Fetching initial tabs count...');
303
592
  page.tabs().then((tabs: any) => {
593
+ debugLog(`Tabs response: ${typeof tabs === 'string' ? tabs : JSON.stringify(tabs)}`);
304
594
  if (typeof tabs === 'string') {
305
595
  this._initialTabCount = (tabs.match(/Tab \d+/g) || []).length;
306
596
  } else if (Array.isArray(tabs)) {
307
597
  this._initialTabCount = tabs.length;
308
598
  }
309
- clearTimeout(timer);
310
- resolve(page);
311
- }).catch(() => { clearTimeout(timer); resolve(page); });
312
- }).catch((err) => { clearTimeout(timer); reject(err); });
599
+ settleSuccess(page);
600
+ }).catch((err) => {
601
+ debugLog(`Tabs fetch error: ${err.message}`);
602
+ settleSuccess(page);
603
+ });
604
+ }).catch((err) => {
605
+ debugLog(`Init promise rejected: ${err.message}`);
606
+ settleError('mcp-init', { rawMessage: err.message });
607
+ });
313
608
  });
314
609
  }
315
610
 
@@ -339,6 +634,7 @@ export class PlaywrightMCP {
339
634
  } finally {
340
635
  this._page = null;
341
636
  this._releaseLock();
637
+ PlaywrightMCP._activeInsts.delete(this);
342
638
  }
343
639
  }
344
640
 
@@ -117,6 +117,10 @@ function scanTs(filePath: string, site: string): ManifestEntry {
117
117
  const stratMatch = src.match(/strategy\s*:\s*Strategy\.(\w+)/);
118
118
  if (stratMatch) entry.strategy = stratMatch[1].toLowerCase();
119
119
 
120
+ // Extract browser: false (some adapters bypass browser entirely)
121
+ const browserMatch = src.match(/browser\s*:\s*(true|false)/);
122
+ if (browserMatch) entry.browser = browserMatch[1] === 'true';
123
+
120
124
  // Extract columns
121
125
  const colMatch = src.match(/columns\s*:\s*\[([^\]]*)\]/);
122
126
  if (colMatch) {