@jackwener/opencli 0.4.4 → 0.4.6
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/browser.d.ts +10 -0
- package/dist/browser.js +66 -5
- package/dist/browser.test.js +6 -0
- package/package.json +1 -1
- package/src/browser.test.ts +10 -0
- package/src/browser.ts +68 -5
package/dist/browser.d.ts
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
* Browser interaction via Chrome DevTools Protocol.
|
|
3
3
|
* Connects to an existing Chrome browser through CDP auto-discovery or extension bridge.
|
|
4
4
|
*/
|
|
5
|
+
/**
|
|
6
|
+
* Verify the CDP HTTP JSON API is functional.
|
|
7
|
+
* Chrome's chrome://inspect#remote-debugging mode writes DevToolsActivePort
|
|
8
|
+
* but doesn't expose the full CDP HTTP API (/json/version), which means
|
|
9
|
+
* Playwright's connectOverCDP won't work properly (init succeeds but
|
|
10
|
+
* all tool calls hang silently).
|
|
11
|
+
*/
|
|
12
|
+
export declare function isCdpApiAvailable(port: number, host?: string, timeoutMs?: number): Promise<boolean>;
|
|
5
13
|
export declare function discoverChromeEndpoint(): Promise<string | null>;
|
|
6
14
|
type ConnectFailureKind = 'missing-token' | 'extension-timeout' | 'extension-not-installed' | 'cdp-timeout' | 'mcp-init' | 'process-exit' | 'unknown';
|
|
7
15
|
type PlaywrightMCPState = 'idle' | 'connecting' | 'connected' | 'closing' | 'closed';
|
|
@@ -73,6 +81,7 @@ export declare class PlaywrightMCP {
|
|
|
73
81
|
private _initialTabIdentities;
|
|
74
82
|
private _closingPromise;
|
|
75
83
|
private _state;
|
|
84
|
+
private _mode;
|
|
76
85
|
private _page;
|
|
77
86
|
get state(): PlaywrightMCPState;
|
|
78
87
|
private _sendRequest;
|
|
@@ -100,5 +109,6 @@ export declare const __test__: {
|
|
|
100
109
|
diffTabIndexes: typeof diffTabIndexes;
|
|
101
110
|
appendLimited: typeof appendLimited;
|
|
102
111
|
withTimeout: typeof withTimeout;
|
|
112
|
+
isCdpApiAvailable: typeof isCdpApiAvailable;
|
|
103
113
|
};
|
|
104
114
|
export {};
|
package/dist/browser.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { spawn, execSync } from 'node:child_process';
|
|
6
6
|
import { createHash } from 'node:crypto';
|
|
7
|
+
import * as http from 'node:http';
|
|
7
8
|
import * as net from 'node:net';
|
|
8
9
|
import { fileURLToPath } from 'node:url';
|
|
9
10
|
import * as fs from 'node:fs';
|
|
@@ -30,6 +31,33 @@ function isPortReachable(port, host = '127.0.0.1', timeoutMs = 800) {
|
|
|
30
31
|
sock.on('timeout', () => { sock.destroy(); resolve(false); });
|
|
31
32
|
});
|
|
32
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Verify the CDP HTTP JSON API is functional.
|
|
36
|
+
* Chrome's chrome://inspect#remote-debugging mode writes DevToolsActivePort
|
|
37
|
+
* but doesn't expose the full CDP HTTP API (/json/version), which means
|
|
38
|
+
* Playwright's connectOverCDP won't work properly (init succeeds but
|
|
39
|
+
* all tool calls hang silently).
|
|
40
|
+
*/
|
|
41
|
+
export function isCdpApiAvailable(port, host = '127.0.0.1', timeoutMs = 2000) {
|
|
42
|
+
return new Promise(resolve => {
|
|
43
|
+
const req = http.get(`http://${host}:${port}/json/version`, { timeout: timeoutMs }, (res) => {
|
|
44
|
+
let body = '';
|
|
45
|
+
res.on('data', (chunk) => { body += chunk.toString(); });
|
|
46
|
+
res.on('end', () => {
|
|
47
|
+
try {
|
|
48
|
+
const data = JSON.parse(body);
|
|
49
|
+
// A valid CDP endpoint returns { Browser, ... } with a webSocketDebuggerUrl
|
|
50
|
+
resolve(!!data && typeof data === 'object' && !!data.Browser);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
resolve(false);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
req.on('error', () => resolve(false));
|
|
58
|
+
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
59
|
+
});
|
|
60
|
+
}
|
|
33
61
|
export async function discoverChromeEndpoint() {
|
|
34
62
|
const candidates = [];
|
|
35
63
|
// User-specified Chrome data dir takes highest priority
|
|
@@ -62,7 +90,12 @@ export async function discoverChromeEndpoint() {
|
|
|
62
90
|
const endpoint = `ws://127.0.0.1:${port}${browserPath}`;
|
|
63
91
|
// Verify the port is actually reachable (Chrome may have closed, leaving a stale file)
|
|
64
92
|
if (await isPortReachable(port)) {
|
|
65
|
-
|
|
93
|
+
// Verify CDP HTTP API is functional — chrome://inspect#remote-debugging
|
|
94
|
+
// writes DevToolsActivePort but doesn't expose the full CDP API,
|
|
95
|
+
// causing Playwright connectOverCDP to hang on all tool calls.
|
|
96
|
+
if (await isCdpApiAvailable(port)) {
|
|
97
|
+
return endpoint;
|
|
98
|
+
}
|
|
66
99
|
}
|
|
67
100
|
}
|
|
68
101
|
}
|
|
@@ -81,6 +114,8 @@ catch {
|
|
|
81
114
|
} })();
|
|
82
115
|
const CONNECT_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_CONNECT_TIMEOUT ?? '30', 10);
|
|
83
116
|
const STDERR_BUFFER_LIMIT = 16 * 1024;
|
|
117
|
+
const INITIAL_TABS_TIMEOUT_MS = 1500;
|
|
118
|
+
const CDP_READINESS_PROBE_TIMEOUT_MS = 5000;
|
|
84
119
|
const TAB_CLEANUP_TIMEOUT_MS = 2000;
|
|
85
120
|
let _cachedMcpServerPath;
|
|
86
121
|
export function getTokenFingerprint(token) {
|
|
@@ -378,6 +413,7 @@ export class PlaywrightMCP {
|
|
|
378
413
|
_initialTabIdentities = [];
|
|
379
414
|
_closingPromise = null;
|
|
380
415
|
_state = 'idle';
|
|
416
|
+
_mode = null;
|
|
381
417
|
_page = null;
|
|
382
418
|
get state() {
|
|
383
419
|
return this._state;
|
|
@@ -409,6 +445,7 @@ export class PlaywrightMCP {
|
|
|
409
445
|
this._page = null;
|
|
410
446
|
this._proc = null;
|
|
411
447
|
this._buffer = '';
|
|
448
|
+
this._mode = null;
|
|
412
449
|
this._initialTabIdentities = [];
|
|
413
450
|
this._rejectPendingRequests(new Error('Playwright MCP connect failed'));
|
|
414
451
|
PlaywrightMCP._activeInsts.delete(this);
|
|
@@ -454,6 +491,7 @@ export class PlaywrightMCP {
|
|
|
454
491
|
const isDebug = process.env.DEBUG?.includes('opencli:mcp');
|
|
455
492
|
const debugLog = (msg) => isDebug && console.error(`[opencli:mcp] ${msg}`);
|
|
456
493
|
const mode = cdpEndpoint ? 'cdp' : 'extension';
|
|
494
|
+
this._mode = mode;
|
|
457
495
|
const extensionToken = process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN;
|
|
458
496
|
const tokenFingerprint = getTokenFingerprint(extensionToken);
|
|
459
497
|
let stderrBuffer = '';
|
|
@@ -584,9 +622,30 @@ export class PlaywrightMCP {
|
|
|
584
622
|
const initializedMsg = JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n';
|
|
585
623
|
debugLog(`SEND: ${initializedMsg.trim()}`);
|
|
586
624
|
this._proc?.stdin?.write(initializedMsg);
|
|
587
|
-
|
|
625
|
+
if (mode === 'cdp') {
|
|
626
|
+
// CDP readiness probe: verify tool calls actually work.
|
|
627
|
+
// Some CDP endpoints (e.g. chrome://inspect mode) accept WebSocket
|
|
628
|
+
// connections and respond to MCP init but silently drop tool calls.
|
|
629
|
+
debugLog('CDP readiness probe (tabs)...');
|
|
630
|
+
withTimeout(page.tabs(), CDP_READINESS_PROBE_TIMEOUT_MS, 'CDP readiness probe timed out')
|
|
631
|
+
.then(() => {
|
|
632
|
+
debugLog('CDP readiness probe succeeded');
|
|
633
|
+
settleSuccess(page);
|
|
634
|
+
})
|
|
635
|
+
.catch((err) => {
|
|
636
|
+
debugLog(`CDP readiness probe failed: ${err.message}`);
|
|
637
|
+
settleError('cdp-timeout', {
|
|
638
|
+
rawMessage: 'CDP endpoint connected but tool calls are unresponsive. ' +
|
|
639
|
+
'This usually means Chrome was opened with chrome://inspect#remote-debugging ' +
|
|
640
|
+
'which is not fully compatible. Launch Chrome with --remote-debugging-port=9222 instead, ' +
|
|
641
|
+
'or use the Playwright MCP Bridge extension (default mode).',
|
|
642
|
+
});
|
|
643
|
+
});
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
// Extension mode uses tabs as a readiness probe and for tab cleanup bookkeeping.
|
|
588
647
|
debugLog('Fetching initial tabs count...');
|
|
589
|
-
page.tabs().then((tabs) => {
|
|
648
|
+
withTimeout(page.tabs(), INITIAL_TABS_TIMEOUT_MS, 'Timed out fetching initial tabs').then((tabs) => {
|
|
590
649
|
debugLog(`Tabs response: ${typeof tabs === 'string' ? tabs : JSON.stringify(tabs)}`);
|
|
591
650
|
this._initialTabIdentities = extractTabIdentities(tabs);
|
|
592
651
|
settleSuccess(page);
|
|
@@ -608,8 +667,8 @@ export class PlaywrightMCP {
|
|
|
608
667
|
this._state = 'closing';
|
|
609
668
|
this._closingPromise = (async () => {
|
|
610
669
|
try {
|
|
611
|
-
//
|
|
612
|
-
if (this._page && this._proc && !this._proc.killed) {
|
|
670
|
+
// Extension mode opens bridge/session tabs that we can clean up best-effort.
|
|
671
|
+
if (this._mode === 'extension' && this._page && this._proc && !this._proc.killed) {
|
|
613
672
|
try {
|
|
614
673
|
const tabs = await withTimeout(this._page.tabs(), TAB_CLEANUP_TIMEOUT_MS, 'Timed out fetching tabs during cleanup');
|
|
615
674
|
const tabEntries = extractTabEntries(tabs);
|
|
@@ -648,6 +707,7 @@ export class PlaywrightMCP {
|
|
|
648
707
|
this._rejectPendingRequests(new Error('Playwright MCP session closed'));
|
|
649
708
|
this._page = null;
|
|
650
709
|
this._proc = null;
|
|
710
|
+
this._mode = null;
|
|
651
711
|
this._state = 'closed';
|
|
652
712
|
PlaywrightMCP._activeInsts.delete(this);
|
|
653
713
|
}
|
|
@@ -730,6 +790,7 @@ export const __test__ = {
|
|
|
730
790
|
diffTabIndexes,
|
|
731
791
|
appendLimited,
|
|
732
792
|
withTimeout,
|
|
793
|
+
isCdpApiAvailable,
|
|
733
794
|
};
|
|
734
795
|
function findMcpServerPath() {
|
|
735
796
|
if (_cachedMcpServerPath !== undefined)
|
package/dist/browser.test.js
CHANGED
|
@@ -53,4 +53,10 @@ describe('PlaywrightMCP state', () => {
|
|
|
53
53
|
mcp._state = 'closing';
|
|
54
54
|
await expect(mcp.connect()).rejects.toThrow('Playwright MCP is closing');
|
|
55
55
|
});
|
|
56
|
+
it('tracks backend mode for lifecycle policy', async () => {
|
|
57
|
+
const mcp = new PlaywrightMCP();
|
|
58
|
+
expect(mcp._mode).toBeNull();
|
|
59
|
+
await mcp.close();
|
|
60
|
+
expect(mcp._mode).toBeNull();
|
|
61
|
+
});
|
|
56
62
|
});
|
package/package.json
CHANGED
package/src/browser.test.ts
CHANGED
|
@@ -74,4 +74,14 @@ describe('PlaywrightMCP state', () => {
|
|
|
74
74
|
|
|
75
75
|
await expect(mcp.connect()).rejects.toThrow('Playwright MCP is closing');
|
|
76
76
|
});
|
|
77
|
+
|
|
78
|
+
it('tracks backend mode for lifecycle policy', async () => {
|
|
79
|
+
const mcp = new PlaywrightMCP();
|
|
80
|
+
|
|
81
|
+
expect((mcp as any)._mode).toBeNull();
|
|
82
|
+
|
|
83
|
+
await mcp.close();
|
|
84
|
+
|
|
85
|
+
expect((mcp as any)._mode).toBeNull();
|
|
86
|
+
});
|
|
77
87
|
});
|
package/src/browser.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
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';
|
|
8
9
|
import * as net from 'node:net';
|
|
9
10
|
import { fileURLToPath } from 'node:url';
|
|
10
11
|
import * as fs from 'node:fs';
|
|
@@ -34,6 +35,33 @@ function isPortReachable(port: number, host = '127.0.0.1', timeoutMs = 800): Pro
|
|
|
34
35
|
});
|
|
35
36
|
}
|
|
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
|
+
|
|
37
65
|
export async function discoverChromeEndpoint(): Promise<string | null> {
|
|
38
66
|
const candidates: string[] = [];
|
|
39
67
|
|
|
@@ -67,7 +95,12 @@ export async function discoverChromeEndpoint(): Promise<string | null> {
|
|
|
67
95
|
const endpoint = `ws://127.0.0.1:${port}${browserPath}`;
|
|
68
96
|
// Verify the port is actually reachable (Chrome may have closed, leaving a stale file)
|
|
69
97
|
if (await isPortReachable(port)) {
|
|
70
|
-
|
|
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
|
+
}
|
|
71
104
|
}
|
|
72
105
|
}
|
|
73
106
|
}
|
|
@@ -82,11 +115,14 @@ const PKG_VERSION = (() => { try { return JSON.parse(fs.readFileSync(path.resolv
|
|
|
82
115
|
|
|
83
116
|
const CONNECT_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_CONNECT_TIMEOUT ?? '30', 10);
|
|
84
117
|
const STDERR_BUFFER_LIMIT = 16 * 1024;
|
|
118
|
+
const INITIAL_TABS_TIMEOUT_MS = 1500;
|
|
119
|
+
const CDP_READINESS_PROBE_TIMEOUT_MS = 5000;
|
|
85
120
|
const TAB_CLEANUP_TIMEOUT_MS = 2000;
|
|
86
121
|
let _cachedMcpServerPath: string | null | undefined;
|
|
87
122
|
|
|
88
123
|
type ConnectFailureKind = 'missing-token' | 'extension-timeout' | 'extension-not-installed' | 'cdp-timeout' | 'mcp-init' | 'process-exit' | 'unknown';
|
|
89
124
|
type PlaywrightMCPState = 'idle' | 'connecting' | 'connected' | 'closing' | 'closed';
|
|
125
|
+
type PlaywrightMCPMode = 'extension' | 'cdp' | null;
|
|
90
126
|
|
|
91
127
|
type ConnectFailureInput = {
|
|
92
128
|
kind: ConnectFailureKind;
|
|
@@ -425,6 +461,7 @@ export class PlaywrightMCP {
|
|
|
425
461
|
private _initialTabIdentities: string[] = [];
|
|
426
462
|
private _closingPromise: Promise<void> | null = null;
|
|
427
463
|
private _state: PlaywrightMCPState = 'idle';
|
|
464
|
+
private _mode: PlaywrightMCPMode = null;
|
|
428
465
|
|
|
429
466
|
private _page: Page | null = null;
|
|
430
467
|
|
|
@@ -459,6 +496,7 @@ export class PlaywrightMCP {
|
|
|
459
496
|
this._page = null;
|
|
460
497
|
this._proc = null;
|
|
461
498
|
this._buffer = '';
|
|
499
|
+
this._mode = null;
|
|
462
500
|
this._initialTabIdentities = [];
|
|
463
501
|
this._rejectPendingRequests(new Error('Playwright MCP connect failed'));
|
|
464
502
|
PlaywrightMCP._activeInsts.delete(this);
|
|
@@ -500,6 +538,7 @@ export class PlaywrightMCP {
|
|
|
500
538
|
const isDebug = process.env.DEBUG?.includes('opencli:mcp');
|
|
501
539
|
const debugLog = (msg: string) => isDebug && console.error(`[opencli:mcp] ${msg}`);
|
|
502
540
|
const mode: 'extension' | 'cdp' = cdpEndpoint ? 'cdp' : 'extension';
|
|
541
|
+
this._mode = mode;
|
|
503
542
|
const extensionToken = process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN;
|
|
504
543
|
const tokenFingerprint = getTokenFingerprint(extensionToken);
|
|
505
544
|
let stderrBuffer = '';
|
|
@@ -636,9 +675,31 @@ export class PlaywrightMCP {
|
|
|
636
675
|
debugLog(`SEND: ${initializedMsg.trim()}`);
|
|
637
676
|
this._proc?.stdin?.write(initializedMsg);
|
|
638
677
|
|
|
639
|
-
|
|
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.
|
|
640
701
|
debugLog('Fetching initial tabs count...');
|
|
641
|
-
page.tabs().then((tabs: any) => {
|
|
702
|
+
withTimeout(page.tabs(), INITIAL_TABS_TIMEOUT_MS, 'Timed out fetching initial tabs').then((tabs: any) => {
|
|
642
703
|
debugLog(`Tabs response: ${typeof tabs === 'string' ? tabs : JSON.stringify(tabs)}`);
|
|
643
704
|
this._initialTabIdentities = extractTabIdentities(tabs);
|
|
644
705
|
settleSuccess(page);
|
|
@@ -659,8 +720,8 @@ export class PlaywrightMCP {
|
|
|
659
720
|
this._state = 'closing';
|
|
660
721
|
this._closingPromise = (async () => {
|
|
661
722
|
try {
|
|
662
|
-
//
|
|
663
|
-
if (this._page && this._proc && !this._proc.killed) {
|
|
723
|
+
// 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) {
|
|
664
725
|
try {
|
|
665
726
|
const tabs = await withTimeout(this._page.tabs(), TAB_CLEANUP_TIMEOUT_MS, 'Timed out fetching tabs during cleanup');
|
|
666
727
|
const tabEntries = extractTabEntries(tabs);
|
|
@@ -690,6 +751,7 @@ export class PlaywrightMCP {
|
|
|
690
751
|
this._rejectPendingRequests(new Error('Playwright MCP session closed'));
|
|
691
752
|
this._page = null;
|
|
692
753
|
this._proc = null;
|
|
754
|
+
this._mode = null;
|
|
693
755
|
this._state = 'closed';
|
|
694
756
|
PlaywrightMCP._activeInsts.delete(this);
|
|
695
757
|
}
|
|
@@ -782,6 +844,7 @@ export const __test__ = {
|
|
|
782
844
|
diffTabIndexes,
|
|
783
845
|
appendLimited,
|
|
784
846
|
withTimeout,
|
|
847
|
+
isCdpApiAvailable,
|
|
785
848
|
};
|
|
786
849
|
|
|
787
850
|
function findMcpServerPath(): string | null {
|