@jackwener/opencli 0.9.8 → 1.0.1
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/CDP.md +1 -1
- package/CDP.zh-CN.md +1 -1
- package/CLI-ELECTRON.md +2 -2
- package/CLI-EXPLORER.md +4 -4
- package/README.md +35 -58
- package/README.zh-CN.md +36 -60
- package/SKILL.md +10 -8
- package/TESTING.md +7 -7
- package/dist/browser/daemon-client.d.ts +37 -0
- package/dist/browser/daemon-client.js +82 -0
- package/dist/browser/discover.d.ts +11 -34
- package/dist/browser/discover.js +15 -205
- package/dist/browser/errors.d.ts +6 -20
- package/dist/browser/errors.js +24 -63
- package/dist/browser/index.d.ts +2 -12
- package/dist/browser/index.js +2 -12
- package/dist/browser/mcp.d.ts +9 -21
- package/dist/browser/mcp.js +70 -285
- package/dist/browser/page.d.ts +36 -7
- package/dist/browser/page.js +212 -81
- package/dist/browser.test.js +10 -231
- package/dist/cli-manifest.json +561 -14
- package/dist/clis/apple-podcasts/episodes.d.ts +1 -0
- package/dist/clis/apple-podcasts/episodes.js +28 -0
- package/dist/clis/apple-podcasts/search.d.ts +1 -0
- package/dist/clis/apple-podcasts/search.js +29 -0
- package/dist/clis/apple-podcasts/top.d.ts +1 -0
- package/dist/clis/apple-podcasts/top.js +34 -0
- package/dist/clis/apple-podcasts/utils.d.ts +11 -0
- package/dist/clis/apple-podcasts/utils.js +30 -0
- package/dist/clis/apple-podcasts/utils.test.d.ts +1 -0
- package/dist/clis/apple-podcasts/utils.test.js +57 -0
- package/dist/clis/chatwise/history.js +18 -1
- package/dist/clis/discord-app/channels.js +33 -21
- package/dist/clis/neteasemusic/like.d.ts +1 -0
- package/dist/clis/neteasemusic/like.js +25 -0
- package/dist/clis/neteasemusic/lyrics.d.ts +1 -0
- package/dist/clis/neteasemusic/lyrics.js +47 -0
- package/dist/clis/neteasemusic/next.d.ts +1 -0
- package/dist/clis/neteasemusic/next.js +26 -0
- package/dist/clis/neteasemusic/play.d.ts +1 -0
- package/dist/clis/neteasemusic/play.js +26 -0
- package/dist/clis/neteasemusic/playing.d.ts +1 -0
- package/dist/clis/neteasemusic/playing.js +59 -0
- package/dist/clis/neteasemusic/playlist.d.ts +1 -0
- package/dist/clis/neteasemusic/playlist.js +46 -0
- package/dist/clis/neteasemusic/prev.d.ts +1 -0
- package/dist/clis/neteasemusic/prev.js +25 -0
- package/dist/clis/neteasemusic/search.d.ts +1 -0
- package/dist/clis/neteasemusic/search.js +52 -0
- package/dist/clis/neteasemusic/status.d.ts +1 -0
- package/dist/clis/neteasemusic/status.js +16 -0
- package/dist/clis/neteasemusic/volume.d.ts +1 -0
- package/dist/clis/neteasemusic/volume.js +54 -0
- package/dist/clis/twitter/accept.d.ts +1 -0
- package/dist/clis/twitter/accept.js +202 -0
- package/dist/clis/twitter/followers.js +30 -22
- package/dist/clis/twitter/following.js +19 -14
- package/dist/clis/twitter/notifications.js +29 -22
- package/dist/clis/twitter/reply-dm.d.ts +1 -0
- package/dist/clis/twitter/reply-dm.js +181 -0
- package/dist/clis/twitter/search.js +50 -12
- package/dist/clis/weread/book.d.ts +1 -0
- package/dist/clis/weread/book.js +26 -0
- package/dist/clis/weread/highlights.d.ts +1 -0
- package/dist/clis/weread/highlights.js +23 -0
- package/dist/clis/weread/notebooks.d.ts +1 -0
- package/dist/clis/weread/notebooks.js +21 -0
- package/dist/clis/weread/notes.d.ts +1 -0
- package/dist/clis/weread/notes.js +29 -0
- package/dist/clis/weread/ranking.d.ts +1 -0
- package/dist/clis/weread/ranking.js +28 -0
- package/dist/clis/weread/search.d.ts +1 -0
- package/dist/clis/weread/search.js +25 -0
- package/dist/clis/weread/shelf.d.ts +1 -0
- package/dist/clis/weread/shelf.js +24 -0
- package/dist/clis/weread/utils.d.ts +20 -0
- package/dist/clis/weread/utils.js +72 -0
- package/dist/clis/weread/utils.test.d.ts +1 -0
- package/dist/clis/weread/utils.test.js +85 -0
- package/dist/daemon.d.ts +13 -0
- package/dist/daemon.js +187 -0
- package/dist/doctor.d.ts +10 -65
- package/dist/doctor.js +49 -602
- package/dist/doctor.test.js +30 -170
- package/dist/main.js +12 -41
- package/dist/pipeline/executor.test.js +1 -0
- package/dist/pipeline/steps/browser.js +2 -2
- package/dist/pipeline/steps/intercept.js +1 -2
- package/dist/runtime.d.ts +1 -4
- package/dist/runtime.js +1 -4
- package/dist/setup.d.ts +6 -0
- package/dist/setup.js +46 -160
- package/dist/types.d.ts +6 -0
- package/extension/dist/background.js +484 -0
- package/extension/icons/icon-128.png +0 -0
- package/extension/icons/icon-16.png +0 -0
- package/extension/icons/icon-32.png +0 -0
- package/extension/icons/icon-48.png +0 -0
- package/extension/manifest.json +31 -0
- package/extension/package.json +16 -0
- package/extension/src/background.ts +370 -0
- package/extension/src/cdp.ts +125 -0
- package/extension/src/protocol.ts +57 -0
- package/extension/store-assets/screenshot-1280x800.png +0 -0
- package/extension/tsconfig.json +15 -0
- package/extension/vite.config.ts +18 -0
- package/package.json +5 -5
- package/src/browser/daemon-client.ts +113 -0
- package/src/browser/discover.ts +18 -232
- package/src/browser/errors.ts +30 -100
- package/src/browser/index.ts +2 -13
- package/src/browser/mcp.ts +81 -282
- package/src/browser/page.ts +223 -83
- package/src/browser.test.ts +9 -239
- package/src/clis/apple-podcasts/episodes.ts +28 -0
- package/src/clis/apple-podcasts/search.ts +29 -0
- package/src/clis/apple-podcasts/top.ts +34 -0
- package/src/clis/apple-podcasts/utils.test.ts +72 -0
- package/src/clis/apple-podcasts/utils.ts +37 -0
- package/src/clis/chatgpt/README.md +1 -1
- package/src/clis/chatgpt/README.zh-CN.md +1 -1
- package/src/clis/chatwise/history.ts +15 -1
- package/src/clis/discord-app/channels.ts +33 -21
- package/src/clis/neteasemusic/README.md +31 -0
- package/src/clis/neteasemusic/README.zh-CN.md +31 -0
- package/src/clis/neteasemusic/like.ts +28 -0
- package/src/clis/neteasemusic/lyrics.ts +53 -0
- package/src/clis/neteasemusic/next.ts +30 -0
- package/src/clis/neteasemusic/play.ts +30 -0
- package/src/clis/neteasemusic/playing.ts +62 -0
- package/src/clis/neteasemusic/playlist.ts +51 -0
- package/src/clis/neteasemusic/prev.ts +29 -0
- package/src/clis/neteasemusic/search.ts +58 -0
- package/src/clis/neteasemusic/status.ts +18 -0
- package/src/clis/neteasemusic/volume.ts +61 -0
- package/src/clis/twitter/accept.ts +213 -0
- package/src/clis/twitter/followers.ts +36 -29
- package/src/clis/twitter/following.ts +25 -20
- package/src/clis/twitter/notifications.ts +34 -27
- package/src/clis/twitter/reply-dm.ts +193 -0
- package/src/clis/twitter/search.ts +53 -13
- package/src/clis/weread/book.ts +28 -0
- package/src/clis/weread/highlights.ts +25 -0
- package/src/clis/weread/notebooks.ts +23 -0
- package/src/clis/weread/notes.ts +31 -0
- package/src/clis/weread/ranking.ts +29 -0
- package/src/clis/weread/search.ts +26 -0
- package/src/clis/weread/shelf.ts +26 -0
- package/src/clis/weread/utils.test.ts +104 -0
- package/src/clis/weread/utils.ts +74 -0
- package/src/daemon.ts +217 -0
- package/src/doctor.test.ts +32 -193
- package/src/doctor.ts +58 -669
- package/src/main.ts +11 -34
- package/src/pipeline/executor.test.ts +1 -0
- package/src/pipeline/steps/browser.ts +2 -2
- package/src/pipeline/steps/intercept.ts +1 -2
- package/src/runtime.ts +2 -6
- package/src/setup.ts +47 -183
- package/src/types.ts +1 -0
- package/tests/e2e/public-commands.test.ts +68 -1
- package/dist/clis/grok/debug.d.ts +0 -1
- package/dist/clis/grok/debug.js +0 -45
- package/src/clis/grok/debug.ts +0 -49
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for communicating with the opencli daemon.
|
|
3
|
+
*
|
|
4
|
+
* Provides a typed send() function that posts a Command and returns a Result.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const DAEMON_PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? '19825', 10);
|
|
8
|
+
const DAEMON_URL = `http://127.0.0.1:${DAEMON_PORT}`;
|
|
9
|
+
|
|
10
|
+
let _idCounter = 0;
|
|
11
|
+
|
|
12
|
+
function generateId(): string {
|
|
13
|
+
return `cmd_${Date.now()}_${++_idCounter}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface DaemonCommand {
|
|
17
|
+
id: string;
|
|
18
|
+
action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window';
|
|
19
|
+
tabId?: number;
|
|
20
|
+
code?: string;
|
|
21
|
+
url?: string;
|
|
22
|
+
op?: string;
|
|
23
|
+
index?: number;
|
|
24
|
+
domain?: string;
|
|
25
|
+
format?: 'png' | 'jpeg';
|
|
26
|
+
quality?: number;
|
|
27
|
+
fullPage?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface DaemonResult {
|
|
31
|
+
id: string;
|
|
32
|
+
ok: boolean;
|
|
33
|
+
data?: unknown;
|
|
34
|
+
error?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if daemon is running.
|
|
39
|
+
*/
|
|
40
|
+
export async function isDaemonRunning(): Promise<boolean> {
|
|
41
|
+
try {
|
|
42
|
+
const controller = new AbortController();
|
|
43
|
+
const timer = setTimeout(() => controller.abort(), 2000);
|
|
44
|
+
const res = await fetch(`${DAEMON_URL}/status`, { signal: controller.signal });
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
return res.ok;
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if daemon is running AND the extension is connected.
|
|
54
|
+
*/
|
|
55
|
+
export async function isExtensionConnected(): Promise<boolean> {
|
|
56
|
+
try {
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const timer = setTimeout(() => controller.abort(), 2000);
|
|
59
|
+
const res = await fetch(`${DAEMON_URL}/status`, { signal: controller.signal });
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
if (!res.ok) return false;
|
|
62
|
+
const data = await res.json() as { extensionConnected?: boolean };
|
|
63
|
+
return !!data.extensionConnected;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Send a command to the daemon and wait for a result.
|
|
71
|
+
* Retries up to 3 times with 500ms delay for transient failures.
|
|
72
|
+
*/
|
|
73
|
+
export async function sendCommand(
|
|
74
|
+
action: DaemonCommand['action'],
|
|
75
|
+
params: Omit<DaemonCommand, 'id' | 'action'> = {},
|
|
76
|
+
): Promise<unknown> {
|
|
77
|
+
const id = generateId();
|
|
78
|
+
const command: DaemonCommand = { id, action, ...params };
|
|
79
|
+
const maxRetries = 3;
|
|
80
|
+
|
|
81
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
82
|
+
try {
|
|
83
|
+
const controller = new AbortController();
|
|
84
|
+
const timer = setTimeout(() => controller.abort(), 30000);
|
|
85
|
+
|
|
86
|
+
const res = await fetch(`${DAEMON_URL}/command`, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: { 'Content-Type': 'application/json' },
|
|
89
|
+
body: JSON.stringify(command),
|
|
90
|
+
signal: controller.signal,
|
|
91
|
+
});
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
|
|
94
|
+
const result = (await res.json()) as DaemonResult;
|
|
95
|
+
|
|
96
|
+
if (!result.ok) {
|
|
97
|
+
throw new Error(result.error ?? 'Daemon command failed');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return result.data;
|
|
101
|
+
} catch (err) {
|
|
102
|
+
const isRetryable = err instanceof TypeError // fetch network error
|
|
103
|
+
|| (err instanceof Error && err.name === 'AbortError');
|
|
104
|
+
if (isRetryable && attempt < maxRetries) {
|
|
105
|
+
await new Promise(r => setTimeout(r, 500));
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Unreachable — the loop always returns or throws
|
|
112
|
+
throw new Error('sendCommand: max retries exhausted');
|
|
113
|
+
}
|
package/src/browser/discover.ts
CHANGED
|
@@ -1,241 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MCP server path discovery
|
|
2
|
+
* Daemon discovery — simplified from MCP server path discovery.
|
|
3
|
+
*
|
|
4
|
+
* Only needs to check if the daemon is running. No more file system
|
|
5
|
+
* scanning for @playwright/mcp locations.
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
|
-
import {
|
|
6
|
-
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import * as fs from 'node:fs';
|
|
8
|
-
import * as os from 'node:os';
|
|
9
|
-
import * as path from 'node:path';
|
|
10
|
-
|
|
11
|
-
let _cachedMcpServerPath: string | null | undefined;
|
|
12
|
-
let _existsSync = fs.existsSync;
|
|
13
|
-
let _execSync = execSync;
|
|
14
|
-
|
|
15
|
-
function isSupportedMcpEntrypoint(candidate: string): boolean {
|
|
16
|
-
const normalized = candidate.replace(/\\/g, '/').toLowerCase();
|
|
17
|
-
return normalized.endsWith('/@playwright/mcp/cli.js') ||
|
|
18
|
-
normalized.endsWith('/mcp-server-playwright') ||
|
|
19
|
-
normalized.endsWith('/mcp-server-playwright.js');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function resolveSupportedMcpPath(candidate: string | null | undefined): string | null {
|
|
23
|
-
const trimmed = candidate?.trim();
|
|
24
|
-
if (!trimmed || !_existsSync(trimmed)) return null;
|
|
25
|
-
return isSupportedMcpEntrypoint(trimmed) ? trimmed : null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function resetMcpServerPathCache(): void {
|
|
29
|
-
_cachedMcpServerPath = undefined;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function setMcpDiscoveryTestHooks(input?: {
|
|
33
|
-
existsSync?: typeof fs.existsSync;
|
|
34
|
-
execSync?: typeof execSync;
|
|
35
|
-
}): void {
|
|
36
|
-
_existsSync = input?.existsSync ?? fs.existsSync;
|
|
37
|
-
_execSync = input?.execSync ?? execSync;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function findMcpServerPath(): string | null {
|
|
41
|
-
if (_cachedMcpServerPath !== undefined) return _cachedMcpServerPath;
|
|
42
|
-
|
|
43
|
-
const envMcp = process.env.OPENCLI_MCP_SERVER_PATH;
|
|
44
|
-
if (envMcp && _existsSync(envMcp)) {
|
|
45
|
-
_cachedMcpServerPath = envMcp;
|
|
46
|
-
return _cachedMcpServerPath;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Check local node_modules first (@playwright/mcp is the modern package)
|
|
50
|
-
const localMcp = path.resolve('node_modules', '@playwright', 'mcp', 'cli.js');
|
|
51
|
-
if (_existsSync(localMcp)) {
|
|
52
|
-
_cachedMcpServerPath = localMcp;
|
|
53
|
-
return _cachedMcpServerPath;
|
|
54
|
-
}
|
|
8
|
+
import { isDaemonRunning } from './daemon-client.js';
|
|
55
9
|
|
|
56
|
-
|
|
57
|
-
const __dirname2 = path.dirname(fileURLToPath(import.meta.url));
|
|
58
|
-
const projectMcp = path.resolve(__dirname2, '..', '..', 'node_modules', '@playwright', 'mcp', 'cli.js');
|
|
59
|
-
if (_existsSync(projectMcp)) {
|
|
60
|
-
_cachedMcpServerPath = projectMcp;
|
|
61
|
-
return _cachedMcpServerPath;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Check global npm/yarn locations derived from current Node runtime.
|
|
65
|
-
const nodePrefix = path.resolve(path.dirname(process.execPath), '..');
|
|
66
|
-
const globalNodeModules = path.join(nodePrefix, 'lib', 'node_modules');
|
|
67
|
-
const globalMcp = path.join(globalNodeModules, '@playwright', 'mcp', 'cli.js');
|
|
68
|
-
if (_existsSync(globalMcp)) {
|
|
69
|
-
_cachedMcpServerPath = globalMcp;
|
|
70
|
-
return _cachedMcpServerPath;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Check npm global root directly.
|
|
74
|
-
try {
|
|
75
|
-
const npmRootGlobal = _execSync('npm root -g 2>/dev/null', {
|
|
76
|
-
encoding: 'utf-8',
|
|
77
|
-
timeout: 5000,
|
|
78
|
-
}).trim();
|
|
79
|
-
const npmGlobalMcp = path.join(npmRootGlobal, '@playwright', 'mcp', 'cli.js');
|
|
80
|
-
if (npmRootGlobal && _existsSync(npmGlobalMcp)) {
|
|
81
|
-
_cachedMcpServerPath = npmGlobalMcp;
|
|
82
|
-
return _cachedMcpServerPath;
|
|
83
|
-
}
|
|
84
|
-
} catch {}
|
|
85
|
-
|
|
86
|
-
// Check common locations
|
|
87
|
-
const candidates = [
|
|
88
|
-
path.join(os.homedir(), '.npm', '_npx'),
|
|
89
|
-
path.join(os.homedir(), 'node_modules', '.bin'),
|
|
90
|
-
'/usr/local/lib/node_modules',
|
|
91
|
-
];
|
|
92
|
-
|
|
93
|
-
// Try npx resolution (legacy package name)
|
|
94
|
-
try {
|
|
95
|
-
const result = _execSync('npx -y --package=@playwright/mcp which mcp-server-playwright 2>/dev/null', { encoding: 'utf-8', timeout: 10000 }).trim();
|
|
96
|
-
const resolved = resolveSupportedMcpPath(result);
|
|
97
|
-
if (resolved) {
|
|
98
|
-
_cachedMcpServerPath = resolved;
|
|
99
|
-
return _cachedMcpServerPath;
|
|
100
|
-
}
|
|
101
|
-
} catch {}
|
|
102
|
-
|
|
103
|
-
// Try which
|
|
104
|
-
try {
|
|
105
|
-
const result = _execSync('which mcp-server-playwright 2>/dev/null', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
106
|
-
const resolved = resolveSupportedMcpPath(result);
|
|
107
|
-
if (resolved) {
|
|
108
|
-
_cachedMcpServerPath = resolved;
|
|
109
|
-
return _cachedMcpServerPath;
|
|
110
|
-
}
|
|
111
|
-
} catch {}
|
|
112
|
-
|
|
113
|
-
// Search in common npx cache
|
|
114
|
-
for (const base of candidates) {
|
|
115
|
-
if (!_existsSync(base)) continue;
|
|
116
|
-
try {
|
|
117
|
-
const found = _execSync(`find "${base}" -type f -path "*/@playwright/mcp/cli.js" 2>/dev/null | head -1`, { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
118
|
-
const resolved = resolveSupportedMcpPath(found);
|
|
119
|
-
if (resolved) {
|
|
120
|
-
_cachedMcpServerPath = resolved;
|
|
121
|
-
return _cachedMcpServerPath;
|
|
122
|
-
}
|
|
123
|
-
} catch {}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
_cachedMcpServerPath = null;
|
|
127
|
-
return _cachedMcpServerPath;
|
|
128
|
-
}
|
|
10
|
+
export { isDaemonRunning };
|
|
129
11
|
|
|
130
12
|
/**
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* Starting with Chrome 144, users can enable remote debugging from
|
|
134
|
-
* chrome://inspect#remote-debugging without any command-line flags.
|
|
135
|
-
* Chrome writes the active port and browser GUID to a DevToolsActivePort file
|
|
136
|
-
* in the user data directory, which we read to construct the WebSocket endpoint.
|
|
13
|
+
* Check daemon status and return connection info.
|
|
137
14
|
*/
|
|
138
|
-
export function
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
candidates.push(path.join(localAppData, 'Google', 'Chrome', 'User Data', 'DevToolsActivePort'));
|
|
150
|
-
candidates.push(path.join(localAppData, 'Microsoft', 'Edge', 'User Data', 'DevToolsActivePort'));
|
|
151
|
-
} else if (process.platform === 'darwin') {
|
|
152
|
-
candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome', 'DevToolsActivePort'));
|
|
153
|
-
candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'Microsoft Edge', 'DevToolsActivePort'));
|
|
154
|
-
} else {
|
|
155
|
-
candidates.push(path.join(os.homedir(), '.config', 'google-chrome', 'DevToolsActivePort'));
|
|
156
|
-
candidates.push(path.join(os.homedir(), '.config', 'chromium', 'DevToolsActivePort'));
|
|
157
|
-
candidates.push(path.join(os.homedir(), '.config', 'microsoft-edge', 'DevToolsActivePort'));
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
for (const filePath of candidates) {
|
|
161
|
-
try {
|
|
162
|
-
const content = fs.readFileSync(filePath, 'utf-8').trim();
|
|
163
|
-
const lines = content.split('\n');
|
|
164
|
-
if (lines.length >= 2) {
|
|
165
|
-
const port = parseInt(lines[0], 10);
|
|
166
|
-
const browserPath = lines[1]; // e.g. /devtools/browser/<GUID>
|
|
167
|
-
if (port > 0 && browserPath.startsWith('/devtools/browser/')) {
|
|
168
|
-
return `ws://127.0.0.1:${port}${browserPath}`;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
} catch {}
|
|
172
|
-
}
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export function resolveCdpEndpoint(): { endpoint?: string; requestedCdp: boolean } {
|
|
177
|
-
const envVal = process.env.OPENCLI_CDP_ENDPOINT;
|
|
178
|
-
if (envVal === '1' || envVal?.toLowerCase() === 'true') {
|
|
179
|
-
const autoDiscovered = discoverChromeEndpoint();
|
|
180
|
-
return { endpoint: autoDiscovered ?? envVal, requestedCdp: true };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (envVal) {
|
|
184
|
-
return { endpoint: envVal, requestedCdp: true };
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Fallback to auto-discovery if not explicitly set
|
|
188
|
-
const autoDiscovered = discoverChromeEndpoint();
|
|
189
|
-
if (autoDiscovered) {
|
|
190
|
-
return { endpoint: autoDiscovered, requestedCdp: true };
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return { requestedCdp: false };
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function buildRuntimeArgs(input?: { executablePath?: string | null; cdpEndpoint?: string }): string[] {
|
|
197
|
-
const args: string[] = [];
|
|
198
|
-
|
|
199
|
-
// Priority 1: CDP endpoint (remote Chrome debugging or local Auto-Discovery)
|
|
200
|
-
if (input?.cdpEndpoint) {
|
|
201
|
-
args.push('--cdp-endpoint', input.cdpEndpoint);
|
|
202
|
-
return args;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// Priority 2: Extension mode (local Chrome with MCP Bridge extension)
|
|
206
|
-
if (!process.env.CI) {
|
|
207
|
-
args.push('--extension');
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// CI/standalone mode: @playwright/mcp launches its own browser (headed by default).
|
|
211
|
-
// xvfb provides a virtual display for headed mode in GitHub Actions.
|
|
212
|
-
if (input?.executablePath) {
|
|
213
|
-
args.push('--executable-path', input.executablePath);
|
|
214
|
-
}
|
|
215
|
-
return args;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export function buildMcpArgs(input: { mcpPath: string; executablePath?: string | null; cdpEndpoint?: string }): string[] {
|
|
219
|
-
return [input.mcpPath, ...buildRuntimeArgs(input)];
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
export function buildMcpLaunchSpec(input: { mcpPath?: string | null; executablePath?: string | null; cdpEndpoint?: string }): {
|
|
223
|
-
command: string;
|
|
224
|
-
args: string[];
|
|
225
|
-
usedNpxFallback: boolean;
|
|
226
|
-
} {
|
|
227
|
-
const runtimeArgs = buildRuntimeArgs(input);
|
|
228
|
-
if (input.mcpPath) {
|
|
229
|
-
return {
|
|
230
|
-
command: 'node',
|
|
231
|
-
args: [input.mcpPath, ...runtimeArgs],
|
|
232
|
-
usedNpxFallback: false,
|
|
233
|
-
};
|
|
15
|
+
export async function checkDaemonStatus(): Promise<{
|
|
16
|
+
running: boolean;
|
|
17
|
+
extensionConnected: boolean;
|
|
18
|
+
}> {
|
|
19
|
+
try {
|
|
20
|
+
const port = parseInt(process.env.OPENCLI_DAEMON_PORT ?? '19825', 10);
|
|
21
|
+
const res = await fetch(`http://127.0.0.1:${port}/status`);
|
|
22
|
+
const data = await res.json() as { ok: boolean; extensionConnected: boolean };
|
|
23
|
+
return { running: true, extensionConnected: data.extensionConnected };
|
|
24
|
+
} catch {
|
|
25
|
+
return { running: false, extensionConnected: false };
|
|
234
26
|
}
|
|
235
|
-
|
|
236
|
-
return {
|
|
237
|
-
command: 'npx',
|
|
238
|
-
args: ['-y', '@playwright/mcp@latest', ...runtimeArgs],
|
|
239
|
-
usedNpxFallback: true,
|
|
240
|
-
};
|
|
241
27
|
}
|
package/src/browser/errors.ts
CHANGED
|
@@ -1,105 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Browser connection error
|
|
2
|
+
* Browser connection error helpers.
|
|
3
|
+
*
|
|
4
|
+
* Simplified — no more token/extension/CDP classification.
|
|
5
|
+
* The daemon architecture has a single failure mode: daemon not reachable or extension not connected.
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
`Failed to connect to remote Chrome via CDP endpoint.\n\n` +
|
|
32
|
-
`Check if Chrome is running with remote debugging enabled (--remote-debugging-port=9222) or DevToolsActivePort is available under chrome://inspect#remote-debugging.\n` +
|
|
33
|
-
`If you specified OPENCLI_CDP_ENDPOINT=1, auto-discovery might have failed.` +
|
|
34
|
-
suffix,
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (input.kind === 'missing-token') {
|
|
39
|
-
return new Error(
|
|
40
|
-
'Failed to connect to Playwright MCP Bridge: PLAYWRIGHT_MCP_EXTENSION_TOKEN is not set.\n\n' +
|
|
41
|
-
'Without this token, Chrome will show a manual approval dialog for every new MCP connection. ' +
|
|
42
|
-
'Copy the token from the Playwright MCP Bridge extension and set it in BOTH your shell environment and MCP client config.' +
|
|
43
|
-
suffix,
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (input.kind === 'extension-not-installed') {
|
|
48
|
-
return new Error(
|
|
49
|
-
'Failed to connect to Playwright MCP Bridge: the browser extension did not attach.\n\n' +
|
|
50
|
-
'Make sure Chrome is running and the "Playwright MCP Bridge" extension is installed and enabled. ' +
|
|
51
|
-
'If Chrome shows an approval dialog, click Allow.' +
|
|
52
|
-
suffix,
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (input.kind === 'extension-timeout') {
|
|
57
|
-
const likelyCause = input.hasExtensionToken
|
|
58
|
-
? `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.`
|
|
59
|
-
: 'PLAYWRIGHT_MCP_EXTENSION_TOKEN is not configured, so the extension may be waiting for manual approval.';
|
|
60
|
-
return new Error(
|
|
61
|
-
`Timed out connecting to Playwright MCP Bridge (${input.timeout}s).\n\n` +
|
|
62
|
-
`${likelyCause} If a browser prompt is visible, click Allow.` +
|
|
63
|
-
suffix,
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (input.kind === 'mcp-init') {
|
|
68
|
-
return new Error(`Failed to initialize Playwright MCP: ${input.rawMessage ?? 'unknown error'}${suffix}`);
|
|
8
|
+
export type ConnectFailureKind = 'daemon-not-running' | 'extension-not-connected' | 'command-failed' | 'unknown';
|
|
9
|
+
|
|
10
|
+
export function formatBrowserConnectError(kind: ConnectFailureKind, detail?: string): Error {
|
|
11
|
+
switch (kind) {
|
|
12
|
+
case 'daemon-not-running':
|
|
13
|
+
return new Error(
|
|
14
|
+
'Cannot connect to opencli daemon.\n\n' +
|
|
15
|
+
'The daemon should start automatically. If it doesn\'t, try:\n' +
|
|
16
|
+
' node dist/daemon.js\n' +
|
|
17
|
+
'Make sure port 19825 is available.' +
|
|
18
|
+
(detail ? `\n\n${detail}` : ''),
|
|
19
|
+
);
|
|
20
|
+
case 'extension-not-connected':
|
|
21
|
+
return new Error(
|
|
22
|
+
'opencli Browser Bridge extension is not connected.\n\n' +
|
|
23
|
+
'Please install the extension:\n' +
|
|
24
|
+
' 1. Download from GitHub Releases\n' +
|
|
25
|
+
' 2. Open chrome://extensions/ → Enable Developer Mode\n' +
|
|
26
|
+
' 3. Click "Load unpacked" → select the extension folder\n' +
|
|
27
|
+
' 4. Make sure Chrome is running' +
|
|
28
|
+
(detail ? `\n\n${detail}` : ''),
|
|
29
|
+
);
|
|
30
|
+
case 'command-failed':
|
|
31
|
+
return new Error(`Browser command failed: ${detail ?? 'unknown error'}`);
|
|
32
|
+
default:
|
|
33
|
+
return new Error(detail ?? 'Failed to connect to browser');
|
|
69
34
|
}
|
|
70
|
-
|
|
71
|
-
if (input.kind === 'process-exit') {
|
|
72
|
-
return new Error(
|
|
73
|
-
`Playwright MCP process exited before the browser connection was established${input.exitCode == null ? '' : ` (code ${input.exitCode})`}.` +
|
|
74
|
-
suffix,
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return new Error(input.rawMessage ?? 'Failed to connect to browser');
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function inferConnectFailureKind(args: {
|
|
82
|
-
hasExtensionToken: boolean;
|
|
83
|
-
stderr: string;
|
|
84
|
-
rawMessage?: string;
|
|
85
|
-
exited?: boolean;
|
|
86
|
-
isCdpMode?: boolean;
|
|
87
|
-
}): ConnectFailureKind {
|
|
88
|
-
const haystack = `${args.rawMessage ?? ''}\n${args.stderr}`.toLowerCase();
|
|
89
|
-
|
|
90
|
-
if (args.isCdpMode) {
|
|
91
|
-
if (args.rawMessage?.startsWith('MCP init failed:')) return 'mcp-init';
|
|
92
|
-
if (args.exited) return 'cdp-connection-failed';
|
|
93
|
-
return 'cdp-connection-failed';
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (!args.hasExtensionToken)
|
|
97
|
-
return 'missing-token';
|
|
98
|
-
if (haystack.includes('extension connection timeout') || haystack.includes('playwright mcp bridge'))
|
|
99
|
-
return 'extension-not-installed';
|
|
100
|
-
if (args.rawMessage?.startsWith('MCP init failed:'))
|
|
101
|
-
return 'mcp-init';
|
|
102
|
-
if (args.exited)
|
|
103
|
-
return 'process-exit';
|
|
104
|
-
return 'extension-timeout';
|
|
105
35
|
}
|
package/src/browser/index.ts
CHANGED
|
@@ -6,26 +6,15 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
export { Page } from './page.js';
|
|
9
|
-
export { PlaywrightMCP } from './mcp.js';
|
|
10
|
-
export {
|
|
11
|
-
export type { ConnectFailureKind, ConnectFailureInput } from './errors.js';
|
|
12
|
-
export { resolveCdpEndpoint } from './discover.js';
|
|
9
|
+
export { BrowserBridge, BrowserBridge as PlaywrightMCP } from './mcp.js';
|
|
10
|
+
export { isDaemonRunning } from './daemon-client.js';
|
|
13
11
|
|
|
14
|
-
// Test-only helpers — exposed for unit tests
|
|
15
|
-
import { createJsonRpcRequest } from './mcp.js';
|
|
16
12
|
import { extractTabEntries, diffTabIndexes, appendLimited } from './tabs.js';
|
|
17
|
-
import { buildMcpArgs, buildMcpLaunchSpec, findMcpServerPath, resetMcpServerPathCache, setMcpDiscoveryTestHooks } from './discover.js';
|
|
18
13
|
import { withTimeoutMs } from '../runtime.js';
|
|
19
14
|
|
|
20
15
|
export const __test__ = {
|
|
21
|
-
createJsonRpcRequest,
|
|
22
16
|
extractTabEntries,
|
|
23
17
|
diffTabIndexes,
|
|
24
18
|
appendLimited,
|
|
25
|
-
buildMcpArgs,
|
|
26
|
-
buildMcpLaunchSpec,
|
|
27
|
-
findMcpServerPath,
|
|
28
|
-
resetMcpServerPathCache,
|
|
29
|
-
setMcpDiscoveryTestHooks,
|
|
30
19
|
withTimeoutMs,
|
|
31
20
|
};
|