@jackwener/opencli 0.4.1 → 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.
- package/CLI-CREATOR.md +103 -142
- package/LICENSE +28 -0
- package/README.md +113 -63
- package/README.zh-CN.md +114 -63
- package/SKILL.md +21 -4
- package/dist/browser.d.ts +21 -2
- package/dist/browser.js +269 -15
- package/dist/browser.test.d.ts +1 -0
- package/dist/browser.test.js +43 -0
- package/dist/build-manifest.js +66 -2
- package/dist/cli-manifest.json +905 -109
- package/dist/clis/boss/search.js +186 -30
- package/dist/clis/twitter/delete.d.ts +1 -0
- package/dist/clis/twitter/delete.js +73 -0
- package/dist/clis/twitter/followers.d.ts +1 -0
- package/dist/clis/twitter/followers.js +104 -0
- package/dist/clis/twitter/following.d.ts +1 -0
- package/dist/clis/twitter/following.js +90 -0
- package/dist/clis/twitter/like.d.ts +1 -0
- package/dist/clis/twitter/like.js +69 -0
- package/dist/clis/twitter/notifications.d.ts +1 -0
- package/dist/clis/twitter/notifications.js +109 -0
- package/dist/clis/twitter/post.d.ts +1 -0
- package/dist/clis/twitter/post.js +63 -0
- package/dist/clis/twitter/reply.d.ts +1 -0
- package/dist/clis/twitter/reply.js +57 -0
- package/dist/clis/v2ex/daily.d.ts +1 -0
- package/dist/clis/v2ex/daily.js +98 -0
- package/dist/clis/v2ex/me.d.ts +1 -0
- package/dist/clis/v2ex/me.js +99 -0
- package/dist/clis/v2ex/notifications.d.ts +1 -0
- package/dist/clis/v2ex/notifications.js +72 -0
- package/dist/clis/xiaohongshu/search.d.ts +5 -2
- package/dist/clis/xiaohongshu/search.js +35 -41
- package/dist/doctor.d.ts +50 -0
- package/dist/doctor.js +372 -0
- package/dist/doctor.test.d.ts +1 -0
- package/dist/doctor.test.js +114 -0
- package/dist/main.js +47 -5
- package/dist/output.test.d.ts +1 -0
- package/dist/output.test.js +20 -0
- package/dist/registry.d.ts +4 -0
- package/dist/registry.js +1 -0
- package/dist/runtime.d.ts +3 -1
- package/dist/runtime.js +2 -2
- package/package.json +2 -2
- package/src/browser.test.ts +51 -0
- package/src/browser.ts +318 -22
- package/src/build-manifest.ts +67 -2
- package/src/clis/boss/search.ts +196 -29
- package/src/clis/twitter/delete.ts +78 -0
- package/src/clis/twitter/followers.ts +119 -0
- package/src/clis/twitter/following.ts +105 -0
- package/src/clis/twitter/like.ts +74 -0
- package/src/clis/twitter/notifications.ts +119 -0
- package/src/clis/twitter/post.ts +68 -0
- package/src/clis/twitter/reply.ts +62 -0
- package/src/clis/v2ex/daily.ts +105 -0
- package/src/clis/v2ex/me.ts +103 -0
- package/src/clis/v2ex/notifications.ts +77 -0
- package/src/clis/xiaohongshu/search.ts +41 -44
- package/src/doctor.test.ts +133 -0
- package/src/doctor.ts +424 -0
- package/src/main.ts +47 -4
- package/src/output.test.ts +27 -0
- package/src/registry.ts +5 -0
- package/src/runtime.ts +2 -1
package/dist/browser.js
CHANGED
|
@@ -1,13 +1,76 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Browser interaction via
|
|
3
|
-
* Connects to an existing Chrome browser through
|
|
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
|
import { spawn, execSync } from 'node:child_process';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import * as net from 'node:net';
|
|
6
8
|
import { fileURLToPath } from 'node:url';
|
|
7
9
|
import * as fs from 'node:fs';
|
|
8
10
|
import * as os from 'node:os';
|
|
9
11
|
import * as path from 'node:path';
|
|
10
12
|
import { formatSnapshot } from './snapshotFormatter.js';
|
|
13
|
+
/**
|
|
14
|
+
* Chrome 144+ auto-discovery: read DevToolsActivePort file to get CDP endpoint.
|
|
15
|
+
*
|
|
16
|
+
* Starting with Chrome 144, users can enable remote debugging from
|
|
17
|
+
* chrome://inspect#remote-debugging without any command-line flags.
|
|
18
|
+
* Chrome writes the active port and browser GUID to a DevToolsActivePort file
|
|
19
|
+
* in the user data directory, which we read to construct the WebSocket endpoint.
|
|
20
|
+
*
|
|
21
|
+
* Priority: OPENCLI_CDP_ENDPOINT env > DevToolsActivePort auto-discovery > --extension fallback
|
|
22
|
+
*/
|
|
23
|
+
/** Quick TCP port probe to verify Chrome is actually listening */
|
|
24
|
+
function isPortReachable(port, host = '127.0.0.1', timeoutMs = 800) {
|
|
25
|
+
return new Promise(resolve => {
|
|
26
|
+
const sock = net.createConnection({ port, host });
|
|
27
|
+
sock.setTimeout(timeoutMs);
|
|
28
|
+
sock.on('connect', () => { sock.destroy(); resolve(true); });
|
|
29
|
+
sock.on('error', () => resolve(false));
|
|
30
|
+
sock.on('timeout', () => { sock.destroy(); resolve(false); });
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export async function discoverChromeEndpoint() {
|
|
34
|
+
const candidates = [];
|
|
35
|
+
// User-specified Chrome data dir takes highest priority
|
|
36
|
+
if (process.env.CHROME_USER_DATA_DIR) {
|
|
37
|
+
candidates.push(path.join(process.env.CHROME_USER_DATA_DIR, 'DevToolsActivePort'));
|
|
38
|
+
}
|
|
39
|
+
// Standard Chrome/Edge user data dirs per platform
|
|
40
|
+
if (process.platform === 'win32') {
|
|
41
|
+
const localAppData = process.env.LOCALAPPDATA ?? path.join(os.homedir(), 'AppData', 'Local');
|
|
42
|
+
candidates.push(path.join(localAppData, 'Google', 'Chrome', 'User Data', 'DevToolsActivePort'));
|
|
43
|
+
candidates.push(path.join(localAppData, 'Microsoft', 'Edge', 'User Data', 'DevToolsActivePort'));
|
|
44
|
+
}
|
|
45
|
+
else if (process.platform === 'darwin') {
|
|
46
|
+
candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome', 'DevToolsActivePort'));
|
|
47
|
+
candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'Microsoft Edge', 'DevToolsActivePort'));
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
candidates.push(path.join(os.homedir(), '.config', 'google-chrome', 'DevToolsActivePort'));
|
|
51
|
+
candidates.push(path.join(os.homedir(), '.config', 'chromium', 'DevToolsActivePort'));
|
|
52
|
+
candidates.push(path.join(os.homedir(), '.config', 'microsoft-edge', 'DevToolsActivePort'));
|
|
53
|
+
}
|
|
54
|
+
for (const filePath of candidates) {
|
|
55
|
+
try {
|
|
56
|
+
const content = fs.readFileSync(filePath, 'utf-8').trim();
|
|
57
|
+
const lines = content.split('\n');
|
|
58
|
+
if (lines.length >= 2) {
|
|
59
|
+
const port = parseInt(lines[0], 10);
|
|
60
|
+
const browserPath = lines[1]; // e.g. /devtools/browser/<GUID>
|
|
61
|
+
if (port > 0 && browserPath.startsWith('/devtools/browser/')) {
|
|
62
|
+
const endpoint = `ws://127.0.0.1:${port}${browserPath}`;
|
|
63
|
+
// Verify the port is actually reachable (Chrome may have closed, leaving a stale file)
|
|
64
|
+
if (await isPortReachable(port)) {
|
|
65
|
+
return endpoint;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch { }
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
11
74
|
// Read version from package.json (single source of truth)
|
|
12
75
|
const __browser_dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
76
|
const PKG_VERSION = (() => { try {
|
|
@@ -20,6 +83,67 @@ const EXTENSION_LOCK_TIMEOUT = parseInt(process.env.OPENCLI_EXTENSION_LOCK_TIMEO
|
|
|
20
83
|
const EXTENSION_LOCK_POLL = parseInt(process.env.OPENCLI_EXTENSION_LOCK_POLL_INTERVAL ?? '1', 10);
|
|
21
84
|
const CONNECT_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_CONNECT_TIMEOUT ?? '30', 10);
|
|
22
85
|
const LOCK_DIR = path.join(os.tmpdir(), 'opencli-mcp-lock');
|
|
86
|
+
export function getTokenFingerprint(token) {
|
|
87
|
+
if (!token)
|
|
88
|
+
return null;
|
|
89
|
+
return createHash('sha256').update(token).digest('hex').slice(0, 8);
|
|
90
|
+
}
|
|
91
|
+
export function formatBrowserConnectError(input) {
|
|
92
|
+
const stderr = input.stderr?.trim();
|
|
93
|
+
const suffix = stderr ? `\n\nMCP stderr:\n${stderr}` : '';
|
|
94
|
+
const tokenHint = input.tokenFingerprint ? ` Token fingerprint: ${input.tokenFingerprint}.` : '';
|
|
95
|
+
if (input.mode === 'extension') {
|
|
96
|
+
if (input.kind === 'missing-token') {
|
|
97
|
+
return new Error('Failed to connect to Playwright MCP Bridge: PLAYWRIGHT_MCP_EXTENSION_TOKEN is not set.\n\n' +
|
|
98
|
+
'Without this token, Chrome will show a manual approval dialog for every new MCP connection. ' +
|
|
99
|
+
'Copy the token from the Playwright MCP Bridge extension and set it in BOTH your shell environment and MCP client config.' +
|
|
100
|
+
suffix);
|
|
101
|
+
}
|
|
102
|
+
if (input.kind === 'extension-not-installed') {
|
|
103
|
+
return new Error('Failed to connect to Playwright MCP Bridge: the browser extension did not attach.\n\n' +
|
|
104
|
+
'Make sure Chrome is running and the "Playwright MCP Bridge" extension is installed and enabled. ' +
|
|
105
|
+
'If Chrome shows an approval dialog, click Allow.' +
|
|
106
|
+
suffix);
|
|
107
|
+
}
|
|
108
|
+
if (input.kind === 'extension-timeout') {
|
|
109
|
+
const likelyCause = input.hasExtensionToken
|
|
110
|
+
? `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.`
|
|
111
|
+
: 'PLAYWRIGHT_MCP_EXTENSION_TOKEN is not configured, so the extension may be waiting for manual approval.';
|
|
112
|
+
return new Error(`Timed out connecting to Playwright MCP Bridge (${input.timeout}s).\n\n` +
|
|
113
|
+
`${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.` +
|
|
114
|
+
suffix);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (input.mode === 'cdp' && input.kind === 'cdp-timeout') {
|
|
118
|
+
return new Error(`Timed out connecting to browser via CDP (${input.timeout}s).\n\n` +
|
|
119
|
+
'Make sure Chrome is running and remote debugging is enabled at chrome://inspect#remote-debugging, or set OPENCLI_CDP_ENDPOINT explicitly.' +
|
|
120
|
+
suffix);
|
|
121
|
+
}
|
|
122
|
+
if (input.kind === 'mcp-init') {
|
|
123
|
+
return new Error(`Failed to initialize Playwright MCP: ${input.rawMessage ?? 'unknown error'}${suffix}`);
|
|
124
|
+
}
|
|
125
|
+
if (input.kind === 'process-exit') {
|
|
126
|
+
return new Error(`Playwright MCP process exited before the browser connection was established${input.exitCode == null ? '' : ` (code ${input.exitCode})`}.` +
|
|
127
|
+
suffix);
|
|
128
|
+
}
|
|
129
|
+
return new Error(input.rawMessage ?? 'Failed to connect to browser');
|
|
130
|
+
}
|
|
131
|
+
function inferConnectFailureKind(args) {
|
|
132
|
+
const haystack = `${args.rawMessage ?? ''}\n${args.stderr}`.toLowerCase();
|
|
133
|
+
if (args.mode === 'extension' && !args.hasExtensionToken)
|
|
134
|
+
return 'missing-token';
|
|
135
|
+
if (haystack.includes('extension connection timeout') || haystack.includes('playwright mcp bridge'))
|
|
136
|
+
return 'extension-not-installed';
|
|
137
|
+
if (args.rawMessage?.startsWith('MCP init failed:'))
|
|
138
|
+
return 'mcp-init';
|
|
139
|
+
if (args.exited)
|
|
140
|
+
return 'process-exit';
|
|
141
|
+
if (args.mode === 'extension')
|
|
142
|
+
return 'extension-timeout';
|
|
143
|
+
if (args.mode === 'cdp')
|
|
144
|
+
return 'cdp-timeout';
|
|
145
|
+
return 'unknown';
|
|
146
|
+
}
|
|
23
147
|
// JSON-RPC helpers
|
|
24
148
|
let _nextId = 1;
|
|
25
149
|
function jsonRpcRequest(method, params = {}) {
|
|
@@ -227,6 +351,33 @@ export class Page {
|
|
|
227
351
|
* Playwright MCP process manager.
|
|
228
352
|
*/
|
|
229
353
|
export class PlaywrightMCP {
|
|
354
|
+
static _activeInsts = new Set();
|
|
355
|
+
static _cleanupRegistered = false;
|
|
356
|
+
static _registerGlobalCleanup() {
|
|
357
|
+
if (this._cleanupRegistered)
|
|
358
|
+
return;
|
|
359
|
+
this._cleanupRegistered = true;
|
|
360
|
+
const cleanup = () => {
|
|
361
|
+
for (const inst of this._activeInsts) {
|
|
362
|
+
if (inst._lockAcquired) {
|
|
363
|
+
try {
|
|
364
|
+
fs.rmdirSync(LOCK_DIR);
|
|
365
|
+
}
|
|
366
|
+
catch { }
|
|
367
|
+
inst._lockAcquired = false;
|
|
368
|
+
}
|
|
369
|
+
if (inst._proc && !inst._proc.killed) {
|
|
370
|
+
try {
|
|
371
|
+
inst._proc.kill('SIGKILL');
|
|
372
|
+
}
|
|
373
|
+
catch { }
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
process.on('exit', cleanup);
|
|
378
|
+
process.on('SIGINT', () => { cleanup(); process.exit(130); });
|
|
379
|
+
process.on('SIGTERM', () => { cleanup(); process.exit(143); });
|
|
380
|
+
}
|
|
230
381
|
_proc = null;
|
|
231
382
|
_buffer = '';
|
|
232
383
|
_waiters = [];
|
|
@@ -239,15 +390,80 @@ export class PlaywrightMCP {
|
|
|
239
390
|
const mcpPath = findMcpServerPath();
|
|
240
391
|
if (!mcpPath)
|
|
241
392
|
throw new Error('Playwright MCP server not found. Install: npm install -D @playwright/mcp');
|
|
393
|
+
// Connection priority:
|
|
394
|
+
// 1. OPENCLI_CDP_ENDPOINT env var → explicit CDP endpoint
|
|
395
|
+
// 2. OPENCLI_USE_CDP=1 → auto-discover via DevToolsActivePort
|
|
396
|
+
// 3. Default → --extension mode (Playwright MCP Bridge)
|
|
397
|
+
// Some anti-bot sites (e.g. BOSS Zhipin) detect CDP — use forceExtension to bypass.
|
|
398
|
+
const forceExt = opts.forceExtension || process.env.OPENCLI_FORCE_EXTENSION === '1';
|
|
399
|
+
let cdpEndpoint = null;
|
|
400
|
+
if (!forceExt) {
|
|
401
|
+
if (process.env.OPENCLI_CDP_ENDPOINT) {
|
|
402
|
+
cdpEndpoint = process.env.OPENCLI_CDP_ENDPOINT;
|
|
403
|
+
}
|
|
404
|
+
else if (process.env.OPENCLI_USE_CDP === '1') {
|
|
405
|
+
cdpEndpoint = await discoverChromeEndpoint();
|
|
406
|
+
}
|
|
407
|
+
}
|
|
242
408
|
return new Promise((resolve, reject) => {
|
|
243
|
-
const
|
|
244
|
-
const
|
|
409
|
+
const isDebug = process.env.DEBUG?.includes('opencli:mcp');
|
|
410
|
+
const debugLog = (msg) => isDebug && console.error(`[opencli:mcp] ${msg}`);
|
|
411
|
+
const mode = cdpEndpoint ? 'cdp' : 'extension';
|
|
412
|
+
const extensionToken = process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN;
|
|
413
|
+
const tokenFingerprint = getTokenFingerprint(extensionToken);
|
|
414
|
+
let stderrBuffer = '';
|
|
415
|
+
let settled = false;
|
|
416
|
+
const settleError = (kind, extra = {}) => {
|
|
417
|
+
if (settled)
|
|
418
|
+
return;
|
|
419
|
+
settled = true;
|
|
420
|
+
clearTimeout(timer);
|
|
421
|
+
reject(formatBrowserConnectError({
|
|
422
|
+
kind,
|
|
423
|
+
mode,
|
|
424
|
+
timeout,
|
|
425
|
+
hasExtensionToken: !!extensionToken,
|
|
426
|
+
tokenFingerprint,
|
|
427
|
+
stderr: stderrBuffer,
|
|
428
|
+
exitCode: extra.exitCode,
|
|
429
|
+
rawMessage: extra.rawMessage,
|
|
430
|
+
}));
|
|
431
|
+
};
|
|
432
|
+
const settleSuccess = (pageToResolve) => {
|
|
433
|
+
if (settled)
|
|
434
|
+
return;
|
|
435
|
+
settled = true;
|
|
436
|
+
clearTimeout(timer);
|
|
437
|
+
resolve(pageToResolve);
|
|
438
|
+
};
|
|
439
|
+
const timer = setTimeout(() => {
|
|
440
|
+
debugLog('Connection timed out');
|
|
441
|
+
settleError(inferConnectFailureKind({
|
|
442
|
+
mode,
|
|
443
|
+
hasExtensionToken: !!extensionToken,
|
|
444
|
+
stderr: stderrBuffer,
|
|
445
|
+
}));
|
|
446
|
+
}, timeout * 1000);
|
|
447
|
+
const mcpArgs = [mcpPath];
|
|
448
|
+
if (cdpEndpoint) {
|
|
449
|
+
mcpArgs.push('--cdp-endpoint', cdpEndpoint);
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
mcpArgs.push('--extension');
|
|
453
|
+
}
|
|
454
|
+
if (process.env.OPENCLI_VERBOSE) {
|
|
455
|
+
console.error(`[opencli] CDP mode: ${cdpEndpoint ? `auto-discovered ${cdpEndpoint}` : 'fallback to --extension'}`);
|
|
456
|
+
if (mode === 'extension') {
|
|
457
|
+
console.error(`[opencli] Extension token: ${extensionToken ? `configured (fingerprint ${tokenFingerprint})` : 'missing'}`);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
245
460
|
if (process.env.OPENCLI_BROWSER_EXECUTABLE_PATH) {
|
|
246
461
|
mcpArgs.push('--executablePath', process.env.OPENCLI_BROWSER_EXECUTABLE_PATH);
|
|
247
462
|
}
|
|
463
|
+
debugLog(`Spawning node ${mcpArgs.join(' ')}`);
|
|
248
464
|
this._proc = spawn('node', mcpArgs, {
|
|
249
465
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
250
|
-
env: { ...process.env
|
|
466
|
+
env: { ...process.env },
|
|
251
467
|
});
|
|
252
468
|
// Increase max listeners to avoid warnings
|
|
253
469
|
this._proc.setMaxListeners(20);
|
|
@@ -263,45 +479,82 @@ export class PlaywrightMCP {
|
|
|
263
479
|
for (const line of lines) {
|
|
264
480
|
if (!line.trim())
|
|
265
481
|
continue;
|
|
482
|
+
debugLog(`RECV: ${line}`);
|
|
266
483
|
try {
|
|
267
484
|
const parsed = JSON.parse(line);
|
|
268
485
|
const waiter = this._waiters.shift();
|
|
269
486
|
if (waiter)
|
|
270
487
|
waiter(parsed);
|
|
271
488
|
}
|
|
272
|
-
catch {
|
|
489
|
+
catch (e) {
|
|
490
|
+
debugLog(`Parse error: ${e}`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
this._proc.stderr?.on('data', (chunk) => {
|
|
495
|
+
const text = chunk.toString();
|
|
496
|
+
stderrBuffer += text;
|
|
497
|
+
debugLog(`STDERR: ${text}`);
|
|
498
|
+
});
|
|
499
|
+
this._proc.on('error', (err) => {
|
|
500
|
+
debugLog(`Subprocess error: ${err.message}`);
|
|
501
|
+
settleError('process-exit', { rawMessage: err.message });
|
|
502
|
+
});
|
|
503
|
+
this._proc.on('close', (code) => {
|
|
504
|
+
debugLog(`Subprocess closed with code ${code}`);
|
|
505
|
+
if (!settled) {
|
|
506
|
+
settleError(inferConnectFailureKind({
|
|
507
|
+
mode,
|
|
508
|
+
hasExtensionToken: !!extensionToken,
|
|
509
|
+
stderr: stderrBuffer,
|
|
510
|
+
exited: true,
|
|
511
|
+
}), { exitCode: code });
|
|
273
512
|
}
|
|
274
513
|
});
|
|
275
|
-
this._proc.stderr?.on('data', () => { });
|
|
276
|
-
this._proc.on('error', (err) => { clearTimeout(timer); reject(err); });
|
|
277
514
|
// Initialize: send initialize request
|
|
278
515
|
const initMsg = jsonRpcRequest('initialize', {
|
|
279
516
|
protocolVersion: '2024-11-05',
|
|
280
517
|
capabilities: {},
|
|
281
518
|
clientInfo: { name: 'opencli', version: PKG_VERSION },
|
|
282
519
|
});
|
|
520
|
+
debugLog(`SEND: ${initMsg.trim()}`);
|
|
283
521
|
this._proc.stdin?.write(initMsg);
|
|
284
522
|
// Wait for initialize response, then send initialized notification
|
|
285
523
|
const origRecv = () => new Promise((res) => { this._waiters.push(res); });
|
|
524
|
+
debugLog('Waiting for initialize response...');
|
|
286
525
|
origRecv().then((resp) => {
|
|
526
|
+
debugLog('Got initialize response');
|
|
287
527
|
if (resp.error) {
|
|
288
|
-
|
|
289
|
-
|
|
528
|
+
settleError(inferConnectFailureKind({
|
|
529
|
+
mode,
|
|
530
|
+
hasExtensionToken: !!extensionToken,
|
|
531
|
+
stderr: stderrBuffer,
|
|
532
|
+
rawMessage: `MCP init failed: ${resp.error.message}`,
|
|
533
|
+
}), { rawMessage: resp.error.message });
|
|
290
534
|
return;
|
|
291
535
|
}
|
|
292
|
-
|
|
536
|
+
const initializedMsg = JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n';
|
|
537
|
+
debugLog(`SEND: ${initializedMsg.trim()}`);
|
|
538
|
+
this._proc?.stdin?.write(initializedMsg);
|
|
293
539
|
// Get initial tab count for cleanup
|
|
540
|
+
debugLog('Fetching initial tabs count...');
|
|
294
541
|
page.tabs().then((tabs) => {
|
|
542
|
+
debugLog(`Tabs response: ${typeof tabs === 'string' ? tabs : JSON.stringify(tabs)}`);
|
|
295
543
|
if (typeof tabs === 'string') {
|
|
296
544
|
this._initialTabCount = (tabs.match(/Tab \d+/g) || []).length;
|
|
297
545
|
}
|
|
298
546
|
else if (Array.isArray(tabs)) {
|
|
299
547
|
this._initialTabCount = tabs.length;
|
|
300
548
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
549
|
+
settleSuccess(page);
|
|
550
|
+
}).catch((err) => {
|
|
551
|
+
debugLog(`Tabs fetch error: ${err.message}`);
|
|
552
|
+
settleSuccess(page);
|
|
553
|
+
});
|
|
554
|
+
}).catch((err) => {
|
|
555
|
+
debugLog(`Init promise rejected: ${err.message}`);
|
|
556
|
+
settleError('mcp-init', { rawMessage: err.message });
|
|
557
|
+
});
|
|
305
558
|
});
|
|
306
559
|
}
|
|
307
560
|
async close() {
|
|
@@ -334,6 +587,7 @@ export class PlaywrightMCP {
|
|
|
334
587
|
finally {
|
|
335
588
|
this._page = null;
|
|
336
589
|
this._releaseLock();
|
|
590
|
+
PlaywrightMCP._activeInsts.delete(this);
|
|
337
591
|
}
|
|
338
592
|
}
|
|
339
593
|
async _acquireLock() {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { formatBrowserConnectError, getTokenFingerprint } from './browser.js';
|
|
3
|
+
describe('getTokenFingerprint', () => {
|
|
4
|
+
it('returns null for empty token', () => {
|
|
5
|
+
expect(getTokenFingerprint(undefined)).toBeNull();
|
|
6
|
+
});
|
|
7
|
+
it('returns stable short fingerprint for token', () => {
|
|
8
|
+
expect(getTokenFingerprint('abc123')).toBe('6ca13d52');
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
describe('formatBrowserConnectError', () => {
|
|
12
|
+
it('explains missing extension token clearly', () => {
|
|
13
|
+
const err = formatBrowserConnectError({
|
|
14
|
+
kind: 'missing-token',
|
|
15
|
+
mode: 'extension',
|
|
16
|
+
timeout: 30,
|
|
17
|
+
hasExtensionToken: false,
|
|
18
|
+
});
|
|
19
|
+
expect(err.message).toContain('PLAYWRIGHT_MCP_EXTENSION_TOKEN is not set');
|
|
20
|
+
expect(err.message).toContain('manual approval dialog');
|
|
21
|
+
});
|
|
22
|
+
it('mentions token mismatch as likely cause for extension timeout', () => {
|
|
23
|
+
const err = formatBrowserConnectError({
|
|
24
|
+
kind: 'extension-timeout',
|
|
25
|
+
mode: 'extension',
|
|
26
|
+
timeout: 30,
|
|
27
|
+
hasExtensionToken: true,
|
|
28
|
+
tokenFingerprint: 'deadbeef',
|
|
29
|
+
});
|
|
30
|
+
expect(err.message).toContain('does not match the token currently shown by the browser extension');
|
|
31
|
+
expect(err.message).toContain('deadbeef');
|
|
32
|
+
});
|
|
33
|
+
it('keeps CDP timeout guidance separate', () => {
|
|
34
|
+
const err = formatBrowserConnectError({
|
|
35
|
+
kind: 'cdp-timeout',
|
|
36
|
+
mode: 'cdp',
|
|
37
|
+
timeout: 30,
|
|
38
|
+
hasExtensionToken: false,
|
|
39
|
+
});
|
|
40
|
+
expect(err.message).toContain('via CDP');
|
|
41
|
+
expect(err.message).toContain('chrome://inspect#remote-debugging');
|
|
42
|
+
});
|
|
43
|
+
});
|
package/dist/build-manifest.js
CHANGED
|
@@ -58,10 +58,10 @@ function scanYaml(filePath, site) {
|
|
|
58
58
|
}
|
|
59
59
|
function scanTs(filePath, site) {
|
|
60
60
|
// TS adapters self-register via cli() at import time.
|
|
61
|
-
// We
|
|
61
|
+
// We statically parse the source to extract metadata for the manifest stub.
|
|
62
62
|
const baseName = path.basename(filePath, path.extname(filePath));
|
|
63
63
|
const relativePath = `${site}/${baseName}.js`;
|
|
64
|
-
|
|
64
|
+
const entry = {
|
|
65
65
|
site,
|
|
66
66
|
name: baseName,
|
|
67
67
|
description: '',
|
|
@@ -71,6 +71,70 @@ function scanTs(filePath, site) {
|
|
|
71
71
|
type: 'ts',
|
|
72
72
|
modulePath: relativePath,
|
|
73
73
|
};
|
|
74
|
+
try {
|
|
75
|
+
const src = fs.readFileSync(filePath, 'utf-8');
|
|
76
|
+
// Extract description
|
|
77
|
+
const descMatch = src.match(/description\s*:\s*['"`]([^'"`]*)['"`]/);
|
|
78
|
+
if (descMatch)
|
|
79
|
+
entry.description = descMatch[1];
|
|
80
|
+
// Extract domain
|
|
81
|
+
const domainMatch = src.match(/domain\s*:\s*['"`]([^'"`]*)['"`]/);
|
|
82
|
+
if (domainMatch)
|
|
83
|
+
entry.domain = domainMatch[1];
|
|
84
|
+
// Extract strategy
|
|
85
|
+
const stratMatch = src.match(/strategy\s*:\s*Strategy\.(\w+)/);
|
|
86
|
+
if (stratMatch)
|
|
87
|
+
entry.strategy = stratMatch[1].toLowerCase();
|
|
88
|
+
// Extract browser: false (some adapters bypass browser entirely)
|
|
89
|
+
const browserMatch = src.match(/browser\s*:\s*(true|false)/);
|
|
90
|
+
if (browserMatch)
|
|
91
|
+
entry.browser = browserMatch[1] === 'true';
|
|
92
|
+
// Extract columns
|
|
93
|
+
const colMatch = src.match(/columns\s*:\s*\[([^\]]*)\]/);
|
|
94
|
+
if (colMatch) {
|
|
95
|
+
entry.columns = colMatch[1].split(',').map(s => s.trim().replace(/^['"`]|['"`]$/g, '')).filter(Boolean);
|
|
96
|
+
}
|
|
97
|
+
// Extract args array items: { name: '...', ... }
|
|
98
|
+
const argsBlockMatch = src.match(/args\s*:\s*\[([\s\S]*?)\]\s*,/);
|
|
99
|
+
if (argsBlockMatch) {
|
|
100
|
+
const argsBlock = argsBlockMatch[1];
|
|
101
|
+
const argRegex = /\{\s*name\s*:\s*['"`](\w+)['"`]([^}]*)\}/g;
|
|
102
|
+
let m;
|
|
103
|
+
while ((m = argRegex.exec(argsBlock)) !== null) {
|
|
104
|
+
const argName = m[1];
|
|
105
|
+
const body = m[2];
|
|
106
|
+
const typeMatch = body.match(/type\s*:\s*['"`](\w+)['"`]/);
|
|
107
|
+
const defaultMatch = body.match(/default\s*:\s*([^,}]+)/);
|
|
108
|
+
const requiredMatch = body.match(/required\s*:\s*(true|false)/);
|
|
109
|
+
const helpMatch = body.match(/help\s*:\s*['"`]([^'"`]*)['"`]/);
|
|
110
|
+
let defaultVal = undefined;
|
|
111
|
+
if (defaultMatch) {
|
|
112
|
+
const raw = defaultMatch[1].trim();
|
|
113
|
+
if (raw === 'true')
|
|
114
|
+
defaultVal = true;
|
|
115
|
+
else if (raw === 'false')
|
|
116
|
+
defaultVal = false;
|
|
117
|
+
else if (/^\d+$/.test(raw))
|
|
118
|
+
defaultVal = parseInt(raw, 10);
|
|
119
|
+
else if (/^\d+\.\d+$/.test(raw))
|
|
120
|
+
defaultVal = parseFloat(raw);
|
|
121
|
+
else
|
|
122
|
+
defaultVal = raw.replace(/^['"`]|['"`]$/g, '');
|
|
123
|
+
}
|
|
124
|
+
entry.args.push({
|
|
125
|
+
name: argName,
|
|
126
|
+
type: typeMatch?.[1] ?? 'str',
|
|
127
|
+
default: defaultVal,
|
|
128
|
+
required: requiredMatch?.[1] === 'true',
|
|
129
|
+
help: helpMatch?.[1] ?? '',
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// If parsing fails, fall back to empty metadata — module will self-register at runtime
|
|
136
|
+
}
|
|
137
|
+
return entry;
|
|
74
138
|
}
|
|
75
139
|
// Main
|
|
76
140
|
const manifest = [];
|