@jackwener/opencli 0.9.6 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/bug_report.yml +83 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +42 -0
- package/.github/ISSUE_TEMPLATE/new_site_adapter.yml +57 -0
- package/.github/dependabot.yml +27 -0
- package/.github/pull_request_template.md +24 -0
- package/.github/workflows/ci.yml +14 -8
- package/.github/workflows/e2e-headed.yml +6 -2
- package/.github/workflows/pkg-pr-new.yml +2 -2
- package/.github/workflows/release-please.yml +25 -0
- package/.github/workflows/release.yml +2 -2
- package/.github/workflows/security.yml +36 -0
- package/CDP.md +1 -1
- package/CDP.zh-CN.md +1 -1
- package/CLI-ELECTRON.md +89 -36
- package/CLI-EXPLORER.md +4 -4
- package/CONTRIBUTING.md +167 -0
- package/README.md +113 -89
- package/README.zh-CN.md +114 -91
- 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 -190
- package/dist/browser/errors.d.ts +6 -20
- package/dist/browser/errors.js +24 -63
- package/dist/browser/index.d.ts +2 -11
- package/dist/browser/index.js +5 -11
- package/dist/browser/mcp.d.ts +9 -18
- package/dist/browser/mcp.js +70 -284
- package/dist/browser/page.d.ts +28 -6
- package/dist/browser/page.js +210 -85
- package/dist/browser.test.js +4 -202
- package/dist/build-manifest.d.ts +26 -0
- package/dist/build-manifest.js +132 -60
- package/dist/build-manifest.test.d.ts +1 -0
- package/dist/build-manifest.test.js +26 -0
- package/dist/cli-manifest.json +1582 -29
- package/dist/clis/bilibili/download.d.ts +10 -0
- package/dist/clis/bilibili/download.js +135 -0
- package/dist/clis/chatwise/ask.d.ts +1 -0
- package/dist/clis/chatwise/ask.js +76 -0
- package/dist/clis/chatwise/export.d.ts +1 -0
- package/dist/clis/chatwise/export.js +46 -0
- package/dist/clis/chatwise/history.d.ts +1 -0
- package/dist/clis/chatwise/history.js +43 -0
- package/dist/clis/chatwise/model.d.ts +1 -0
- package/dist/clis/chatwise/model.js +81 -0
- package/dist/clis/chatwise/new.d.ts +1 -0
- package/dist/clis/chatwise/new.js +18 -0
- package/dist/clis/chatwise/read.d.ts +1 -0
- package/dist/clis/chatwise/read.js +39 -0
- package/dist/clis/chatwise/screenshot.d.ts +1 -0
- package/dist/clis/chatwise/screenshot.js +27 -0
- package/dist/clis/chatwise/send.d.ts +1 -0
- package/dist/clis/chatwise/send.js +45 -0
- package/dist/clis/chatwise/status.d.ts +1 -0
- package/dist/clis/chatwise/status.js +22 -0
- package/dist/clis/discord-app/channels.d.ts +1 -0
- package/dist/clis/discord-app/channels.js +45 -0
- package/dist/clis/discord-app/members.d.ts +1 -0
- package/dist/clis/discord-app/members.js +38 -0
- package/dist/clis/discord-app/read.d.ts +1 -0
- package/dist/clis/discord-app/read.js +45 -0
- package/dist/clis/discord-app/search.d.ts +1 -0
- package/dist/clis/discord-app/search.js +56 -0
- package/dist/clis/discord-app/send.d.ts +1 -0
- package/dist/clis/discord-app/send.js +27 -0
- package/dist/clis/discord-app/servers.d.ts +1 -0
- package/dist/clis/discord-app/servers.js +36 -0
- package/dist/clis/discord-app/status.d.ts +1 -0
- package/dist/clis/discord-app/status.js +16 -0
- package/dist/clis/feishu/new.d.ts +1 -0
- package/dist/clis/feishu/new.js +27 -0
- package/dist/clis/feishu/read.d.ts +1 -0
- package/dist/clis/feishu/read.js +40 -0
- package/dist/clis/feishu/search.d.ts +1 -0
- package/dist/clis/feishu/search.js +30 -0
- package/dist/clis/feishu/send.d.ts +1 -0
- package/dist/clis/feishu/send.js +39 -0
- package/dist/clis/feishu/status.d.ts +1 -0
- package/dist/clis/feishu/status.js +28 -0
- package/dist/clis/grok/ask.d.ts +1 -0
- package/dist/clis/grok/ask.js +82 -0
- package/dist/clis/grok/debug.d.ts +1 -0
- package/dist/clis/grok/debug.js +45 -0
- package/dist/clis/jimeng/generate.yaml +84 -0
- package/dist/clis/jimeng/history.yaml +47 -0
- package/dist/clis/linux-do/categories.yaml +41 -0
- package/dist/clis/linux-do/category.yaml +49 -0
- package/dist/clis/linux-do/hot.yaml +50 -0
- package/dist/clis/linux-do/latest.yaml +40 -0
- package/dist/clis/linux-do/search.yaml +45 -0
- package/dist/clis/linux-do/topic.yaml +38 -0
- 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/notion/export.d.ts +1 -0
- package/dist/clis/notion/export.js +31 -0
- package/dist/clis/notion/favorites.d.ts +1 -0
- package/dist/clis/notion/favorites.js +84 -0
- package/dist/clis/notion/new.d.ts +1 -0
- package/dist/clis/notion/new.js +34 -0
- package/dist/clis/notion/read.d.ts +1 -0
- package/dist/clis/notion/read.js +30 -0
- package/dist/clis/notion/search.d.ts +1 -0
- package/dist/clis/notion/search.js +46 -0
- package/dist/clis/notion/sidebar.d.ts +1 -0
- package/dist/clis/notion/sidebar.js +41 -0
- package/dist/clis/notion/status.d.ts +1 -0
- package/dist/clis/notion/status.js +16 -0
- package/dist/clis/notion/write.d.ts +1 -0
- package/dist/clis/notion/write.js +40 -0
- package/dist/clis/twitter/download.d.ts +8 -0
- package/dist/clis/twitter/download.js +204 -0
- package/dist/clis/wechat/chats.d.ts +1 -0
- package/dist/clis/wechat/chats.js +28 -0
- package/dist/clis/wechat/contacts.d.ts +1 -0
- package/dist/clis/wechat/contacts.js +28 -0
- package/dist/clis/wechat/read.d.ts +1 -0
- package/dist/clis/wechat/read.js +58 -0
- package/dist/clis/wechat/search.d.ts +1 -0
- package/dist/clis/wechat/search.js +31 -0
- package/dist/clis/wechat/send.d.ts +1 -0
- package/dist/clis/wechat/send.js +42 -0
- package/dist/clis/wechat/status.d.ts +1 -0
- package/dist/clis/wechat/status.js +29 -0
- package/dist/clis/xiaohongshu/creator-note-detail.d.ts +10 -0
- package/dist/clis/xiaohongshu/creator-note-detail.js +88 -0
- package/dist/clis/xiaohongshu/creator-notes.d.ts +11 -0
- package/dist/clis/xiaohongshu/creator-notes.js +109 -0
- package/dist/clis/xiaohongshu/creator-profile.d.ts +10 -0
- package/dist/clis/xiaohongshu/creator-profile.js +54 -0
- package/dist/clis/xiaohongshu/creator-stats.d.ts +10 -0
- package/dist/clis/xiaohongshu/creator-stats.js +74 -0
- package/dist/clis/xiaohongshu/download.d.ts +7 -0
- package/dist/clis/xiaohongshu/download.js +155 -0
- package/dist/clis/xiaohongshu/search.js +1 -1
- package/dist/clis/xiaohongshu/user-helpers.d.ts +15 -0
- package/dist/clis/xiaohongshu/user-helpers.js +67 -0
- package/dist/clis/xiaohongshu/user-helpers.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/user-helpers.test.js +81 -0
- package/dist/clis/xiaohongshu/user.js +46 -29
- package/dist/clis/zhihu/download.d.ts +11 -0
- package/dist/clis/zhihu/download.js +186 -0
- package/dist/clis/zhihu/download.test.d.ts +1 -0
- package/dist/clis/zhihu/download.test.js +10 -0
- package/dist/daemon.d.ts +13 -0
- package/dist/daemon.js +187 -0
- package/dist/doctor.d.ts +27 -61
- package/dist/doctor.js +70 -601
- package/dist/doctor.test.js +30 -170
- package/dist/download/index.d.ts +79 -0
- package/dist/download/index.js +325 -0
- package/dist/download/progress.d.ts +36 -0
- package/dist/download/progress.js +111 -0
- package/dist/engine.test.js +15 -0
- package/dist/main.js +22 -28
- package/dist/pipeline/executor.test.js +1 -0
- package/dist/pipeline/registry.js +2 -0
- package/dist/pipeline/steps/browser.js +2 -2
- package/dist/pipeline/steps/download.d.ts +34 -0
- package/dist/pipeline/steps/download.js +251 -0
- package/dist/pipeline/steps/intercept.js +1 -2
- package/dist/pipeline/template.js +28 -0
- package/dist/setup.d.ts +6 -0
- package/dist/setup.js +46 -160
- package/dist/types.d.ts +6 -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 +293 -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 +8 -7
- package/scripts/test-site.mjs +70 -0
- package/src/browser/daemon-client.ts +113 -0
- package/src/browser/discover.ts +18 -216
- package/src/browser/errors.ts +30 -100
- package/src/browser/index.ts +6 -12
- package/src/browser/mcp.ts +78 -278
- package/src/browser/page.ts +222 -88
- package/src/browser.test.ts +3 -210
- package/src/build-manifest.test.ts +28 -0
- package/src/build-manifest.ts +147 -57
- package/src/clis/bilibili/download.ts +161 -0
- package/src/clis/chatgpt/README.md +1 -1
- package/src/clis/chatgpt/README.zh-CN.md +1 -1
- package/src/clis/chatwise/README.md +38 -0
- package/src/clis/chatwise/README.zh-CN.md +38 -0
- package/src/clis/chatwise/ask.ts +87 -0
- package/src/clis/chatwise/export.ts +51 -0
- package/src/clis/chatwise/history.ts +47 -0
- package/src/clis/chatwise/model.ts +87 -0
- package/src/clis/chatwise/new.ts +21 -0
- package/src/clis/chatwise/read.ts +42 -0
- package/src/clis/chatwise/screenshot.ts +33 -0
- package/src/clis/chatwise/send.ts +50 -0
- package/src/clis/chatwise/status.ts +25 -0
- package/src/clis/discord-app/README.md +28 -0
- package/src/clis/discord-app/README.zh-CN.md +28 -0
- package/src/clis/discord-app/channels.ts +48 -0
- package/src/clis/discord-app/members.ts +41 -0
- package/src/clis/discord-app/read.ts +49 -0
- package/src/clis/discord-app/search.ts +64 -0
- package/src/clis/discord-app/send.ts +32 -0
- package/src/clis/discord-app/servers.ts +39 -0
- package/src/clis/discord-app/status.ts +18 -0
- package/src/clis/feishu/README.md +20 -0
- package/src/clis/feishu/README.zh-CN.md +20 -0
- package/src/clis/feishu/new.ts +32 -0
- package/src/clis/feishu/read.ts +48 -0
- package/src/clis/feishu/search.ts +35 -0
- package/src/clis/feishu/send.ts +46 -0
- package/src/clis/feishu/status.ts +34 -0
- package/src/clis/grok/ask.ts +90 -0
- package/src/clis/grok/debug.ts +49 -0
- package/src/clis/jimeng/generate.yaml +84 -0
- package/src/clis/jimeng/history.yaml +47 -0
- package/src/clis/linux-do/categories.yaml +41 -0
- package/src/clis/linux-do/category.yaml +49 -0
- package/src/clis/linux-do/hot.yaml +50 -0
- package/src/clis/linux-do/latest.yaml +40 -0
- package/src/clis/linux-do/search.yaml +45 -0
- package/src/clis/linux-do/topic.yaml +38 -0
- 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/notion/README.md +29 -0
- package/src/clis/notion/README.zh-CN.md +29 -0
- package/src/clis/notion/export.ts +36 -0
- package/src/clis/notion/favorites.ts +87 -0
- package/src/clis/notion/new.ts +39 -0
- package/src/clis/notion/read.ts +33 -0
- package/src/clis/notion/search.ts +54 -0
- package/src/clis/notion/sidebar.ts +44 -0
- package/src/clis/notion/status.ts +18 -0
- package/src/clis/notion/write.ts +45 -0
- package/src/clis/twitter/download.ts +227 -0
- package/src/clis/wechat/README.md +28 -0
- package/src/clis/wechat/README.zh-CN.md +28 -0
- package/src/clis/wechat/chats.ts +33 -0
- package/src/clis/wechat/contacts.ts +33 -0
- package/src/clis/wechat/read.ts +72 -0
- package/src/clis/wechat/search.ts +36 -0
- package/src/clis/wechat/send.ts +49 -0
- package/src/clis/wechat/status.ts +35 -0
- package/src/clis/xiaohongshu/creator-note-detail.ts +95 -0
- package/src/clis/xiaohongshu/creator-notes.ts +116 -0
- package/src/clis/xiaohongshu/creator-profile.ts +60 -0
- package/src/clis/xiaohongshu/creator-stats.ts +81 -0
- package/src/clis/xiaohongshu/download.ts +173 -0
- package/src/clis/xiaohongshu/search.ts +1 -1
- package/src/clis/xiaohongshu/user-helpers.test.ts +106 -0
- package/src/clis/xiaohongshu/user-helpers.ts +85 -0
- package/src/clis/xiaohongshu/user.ts +52 -32
- package/src/clis/zhihu/download.test.ts +12 -0
- package/src/clis/zhihu/download.ts +223 -0
- package/src/daemon.ts +217 -0
- package/src/doctor.test.ts +32 -193
- package/src/doctor.ts +74 -668
- package/src/download/index.ts +395 -0
- package/src/download/progress.ts +125 -0
- package/src/engine.test.ts +17 -0
- package/src/main.ts +18 -26
- package/src/pipeline/executor.test.ts +1 -0
- package/src/pipeline/registry.ts +2 -0
- package/src/pipeline/steps/browser.ts +2 -2
- package/src/pipeline/steps/download.ts +310 -0
- package/src/pipeline/steps/intercept.ts +1 -2
- package/src/pipeline/template.ts +26 -0
- package/src/setup.ts +47 -183
- package/src/types.ts +1 -0
- package/tests/e2e/browser-auth.test.ts +25 -0
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
|
@@ -7,25 +7,19 @@
|
|
|
7
7
|
|
|
8
8
|
export { Page } from './page.js';
|
|
9
9
|
export { PlaywrightMCP } from './mcp.js';
|
|
10
|
-
export {
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
export { isDaemonRunning } from './daemon-client.js';
|
|
11
|
+
|
|
12
|
+
// Backward compatibility: getTokenFingerprint is no longer needed but kept as no-op export
|
|
13
|
+
export function getTokenFingerprint(_token: string | undefined): string | null {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
13
16
|
|
|
14
|
-
// Test-only helpers — exposed for unit tests
|
|
15
|
-
import { createJsonRpcRequest } from './mcp.js';
|
|
16
17
|
import { extractTabEntries, diffTabIndexes, appendLimited } from './tabs.js';
|
|
17
|
-
import { buildMcpArgs, buildMcpLaunchSpec, findMcpServerPath, resetMcpServerPathCache, setMcpDiscoveryTestHooks } from './discover.js';
|
|
18
18
|
import { withTimeoutMs } from '../runtime.js';
|
|
19
19
|
|
|
20
20
|
export const __test__ = {
|
|
21
|
-
createJsonRpcRequest,
|
|
22
21
|
extractTabEntries,
|
|
23
22
|
diffTabIndexes,
|
|
24
23
|
appendLimited,
|
|
25
|
-
buildMcpArgs,
|
|
26
|
-
buildMcpLaunchSpec,
|
|
27
|
-
findMcpServerPath,
|
|
28
|
-
resetMcpServerPathCache,
|
|
29
|
-
setMcpDiscoveryTestHooks,
|
|
30
24
|
withTimeoutMs,
|
|
31
25
|
};
|
package/src/browser/mcp.ts
CHANGED
|
@@ -1,312 +1,112 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Browser session manager — auto-spawns daemon and provides IPage.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the old PlaywrightMCP class. Still exports as PlaywrightMCP
|
|
5
|
+
* for backward compatibility with main.ts and other consumers.
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
8
|
import { spawn, type ChildProcess } from 'node:child_process';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import * as path from 'node:path';
|
|
11
|
+
import * as fs from 'node:fs';
|
|
7
12
|
import type { IPage } from '../types.js';
|
|
8
|
-
import { withTimeoutMs, DEFAULT_BROWSER_CONNECT_TIMEOUT } from '../runtime.js';
|
|
9
|
-
import { PKG_VERSION } from '../version.js';
|
|
10
13
|
import { Page } from './page.js';
|
|
11
|
-
import {
|
|
12
|
-
import { findMcpServerPath, buildMcpLaunchSpec, resolveCdpEndpoint } from './discover.js';
|
|
13
|
-
import { extractTabIdentities, extractTabEntries, diffTabIndexes, appendLimited } from './tabs.js';
|
|
14
|
+
import { isDaemonRunning, isExtensionConnected } from './daemon-client.js';
|
|
14
15
|
|
|
15
|
-
const
|
|
16
|
-
const INITIAL_TABS_TIMEOUT_MS = 1500;
|
|
17
|
-
const TAB_CLEANUP_TIMEOUT_MS = 2000;
|
|
16
|
+
const DAEMON_SPAWN_TIMEOUT = 10000; // 10s to wait for daemon + extension
|
|
18
17
|
|
|
19
18
|
export type PlaywrightMCPState = 'idle' | 'connecting' | 'connected' | 'closing' | 'closed';
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
let _nextId = 1;
|
|
23
|
-
export function createJsonRpcRequest(method: string, params: Record<string, unknown> = {}): { id: number; message: string } {
|
|
24
|
-
const id = _nextId++;
|
|
25
|
-
return {
|
|
26
|
-
id,
|
|
27
|
-
message: JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n',
|
|
28
|
-
};
|
|
29
|
-
}
|
|
20
|
+
|
|
30
21
|
|
|
31
22
|
/**
|
|
32
|
-
*
|
|
23
|
+
* Browser factory: manages daemon lifecycle and provides IPage instances.
|
|
24
|
+
*
|
|
25
|
+
* Kept as `PlaywrightMCP` class name for backward compatibility.
|
|
33
26
|
*/
|
|
34
27
|
export class PlaywrightMCP {
|
|
35
|
-
private static _activeInsts: Set<PlaywrightMCP> = new Set();
|
|
36
|
-
private static _cleanupRegistered = false;
|
|
37
|
-
|
|
38
|
-
private static _registerGlobalCleanup() {
|
|
39
|
-
if (this._cleanupRegistered) return;
|
|
40
|
-
this._cleanupRegistered = true;
|
|
41
|
-
const cleanup = () => {
|
|
42
|
-
for (const inst of this._activeInsts) {
|
|
43
|
-
if (inst._proc && !inst._proc.killed) {
|
|
44
|
-
try { inst._proc.kill('SIGKILL'); } catch {}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
process.on('exit', cleanup);
|
|
49
|
-
process.on('SIGINT', () => { cleanup(); process.exit(130); });
|
|
50
|
-
process.on('SIGTERM', () => { cleanup(); process.exit(143); });
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
private _proc: ChildProcess | null = null;
|
|
54
|
-
private _buffer = '';
|
|
55
|
-
private _pending = new Map<number, { resolve: (data: any) => void; reject: (error: Error) => void }>();
|
|
56
|
-
private _initialTabIdentities: string[] = [];
|
|
57
|
-
private _closingPromise: Promise<void> | null = null;
|
|
58
28
|
private _state: PlaywrightMCPState = 'idle';
|
|
59
|
-
|
|
60
29
|
private _page: Page | null = null;
|
|
30
|
+
private _daemonProc: ChildProcess | null = null;
|
|
61
31
|
|
|
62
32
|
get state(): PlaywrightMCPState {
|
|
63
33
|
return this._state;
|
|
64
34
|
}
|
|
65
35
|
|
|
66
|
-
private _sendRequest(method: string, params: Record<string, unknown> = {}): Promise<any> {
|
|
67
|
-
return new Promise<any>((resolve, reject) => {
|
|
68
|
-
if (!this._proc?.stdin?.writable) {
|
|
69
|
-
reject(new Error('Playwright MCP process is not writable'));
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
const { id, message } = createJsonRpcRequest(method, params);
|
|
73
|
-
this._pending.set(id, { resolve, reject });
|
|
74
|
-
this._proc.stdin.write(message, (err) => {
|
|
75
|
-
if (!err) return;
|
|
76
|
-
this._pending.delete(id);
|
|
77
|
-
reject(err);
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
private _rejectPendingRequests(error: Error): void {
|
|
83
|
-
const pending = [...this._pending.values()];
|
|
84
|
-
this._pending.clear();
|
|
85
|
-
for (const waiter of pending) waiter.reject(error);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
private _resetAfterFailedConnect(): void {
|
|
89
|
-
const proc = this._proc;
|
|
90
|
-
this._page = null;
|
|
91
|
-
this._proc = null;
|
|
92
|
-
this._buffer = '';
|
|
93
|
-
this._initialTabIdentities = [];
|
|
94
|
-
this._rejectPendingRequests(new Error('Playwright MCP connect failed'));
|
|
95
|
-
PlaywrightMCP._activeInsts.delete(this);
|
|
96
|
-
if (proc && !proc.killed) {
|
|
97
|
-
try { proc.kill('SIGKILL'); } catch {}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
36
|
async connect(opts: { timeout?: number } = {}): Promise<IPage> {
|
|
102
37
|
if (this._state === 'connected' && this._page) return this._page;
|
|
103
|
-
if (this._state === 'connecting') throw new Error('
|
|
104
|
-
if (this._state === 'closing') throw new Error('
|
|
105
|
-
if (this._state === 'closed') throw new Error('
|
|
38
|
+
if (this._state === 'connecting') throw new Error('Already connecting');
|
|
39
|
+
if (this._state === 'closing') throw new Error('Session is closing');
|
|
40
|
+
if (this._state === 'closed') throw new Error('Session is closed');
|
|
106
41
|
|
|
107
|
-
const mcpPath = findMcpServerPath();
|
|
108
|
-
|
|
109
|
-
PlaywrightMCP._registerGlobalCleanup();
|
|
110
|
-
PlaywrightMCP._activeInsts.add(this);
|
|
111
42
|
this._state = 'connecting';
|
|
112
|
-
const timeout = opts.timeout ?? DEFAULT_BROWSER_CONNECT_TIMEOUT;
|
|
113
|
-
|
|
114
|
-
return new Promise<Page>((resolve, reject) => {
|
|
115
|
-
const isDebug = process.env.DEBUG?.includes('opencli:mcp');
|
|
116
|
-
const debugLog = (msg: string) => isDebug && console.error(`[opencli:mcp] ${msg}`);
|
|
117
|
-
const { endpoint: cdpEndpoint, requestedCdp } = resolveCdpEndpoint();
|
|
118
|
-
const useExtension = !requestedCdp;
|
|
119
|
-
const extensionToken = process.env.PLAYWRIGHT_MCP_EXTENSION_TOKEN;
|
|
120
|
-
const tokenFingerprint = getTokenFingerprint(extensionToken);
|
|
121
|
-
let stderrBuffer = '';
|
|
122
|
-
let settled = false;
|
|
123
|
-
|
|
124
|
-
const settleError = (kind: Parameters<typeof formatBrowserConnectError>[0]['kind'], extra: { rawMessage?: string; exitCode?: number | null } = {}) => {
|
|
125
|
-
if (settled) return;
|
|
126
|
-
settled = true;
|
|
127
|
-
this._state = 'idle';
|
|
128
|
-
clearTimeout(timer);
|
|
129
|
-
this._resetAfterFailedConnect();
|
|
130
|
-
reject(formatBrowserConnectError({
|
|
131
|
-
kind,
|
|
132
|
-
timeout,
|
|
133
|
-
hasExtensionToken: !!extensionToken,
|
|
134
|
-
tokenFingerprint,
|
|
135
|
-
stderr: stderrBuffer,
|
|
136
|
-
exitCode: extra.exitCode,
|
|
137
|
-
rawMessage: extra.rawMessage,
|
|
138
|
-
}));
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const settleSuccess = (pageToResolve: Page) => {
|
|
142
|
-
if (settled) return;
|
|
143
|
-
settled = true;
|
|
144
|
-
this._state = 'connected';
|
|
145
|
-
clearTimeout(timer);
|
|
146
|
-
resolve(pageToResolve);
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
const timer = setTimeout(() => {
|
|
150
|
-
debugLog('Connection timed out');
|
|
151
|
-
settleError(inferConnectFailureKind({
|
|
152
|
-
hasExtensionToken: !!extensionToken,
|
|
153
|
-
stderr: stderrBuffer,
|
|
154
|
-
isCdpMode: requestedCdp,
|
|
155
|
-
}));
|
|
156
|
-
}, timeout * 1000);
|
|
157
43
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
debugLog(`Spawning ${launchSpec.command} ${launchSpec.args.join(' ')}`);
|
|
171
|
-
|
|
172
|
-
this._proc = spawn(launchSpec.command, launchSpec.args, {
|
|
173
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
174
|
-
env: { ...process.env },
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
// Increase max listeners to avoid warnings
|
|
178
|
-
this._proc.setMaxListeners(20);
|
|
179
|
-
if (this._proc.stdout) this._proc.stdout.setMaxListeners(20);
|
|
180
|
-
|
|
181
|
-
const page = new Page((method, params = {}) => this._sendRequest(method, params));
|
|
182
|
-
this._page = page;
|
|
183
|
-
|
|
184
|
-
this._proc.stdout?.on('data', (chunk: Buffer) => {
|
|
185
|
-
this._buffer += chunk.toString();
|
|
186
|
-
const lines = this._buffer.split('\n');
|
|
187
|
-
this._buffer = lines.pop() ?? '';
|
|
188
|
-
for (const line of lines) {
|
|
189
|
-
if (!line.trim()) continue;
|
|
190
|
-
debugLog(`RECV: ${line}`);
|
|
191
|
-
try {
|
|
192
|
-
const parsed = JSON.parse(line);
|
|
193
|
-
if (typeof parsed?.id === 'number') {
|
|
194
|
-
const waiter = this._pending.get(parsed.id);
|
|
195
|
-
if (waiter) {
|
|
196
|
-
this._pending.delete(parsed.id);
|
|
197
|
-
waiter.resolve(parsed);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
} catch (e) {
|
|
201
|
-
debugLog(`Parse error: ${e}`);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
});
|
|
44
|
+
try {
|
|
45
|
+
await this._ensureDaemon();
|
|
46
|
+
this._page = new Page();
|
|
47
|
+
this._state = 'connected';
|
|
48
|
+
return this._page;
|
|
49
|
+
} catch (err) {
|
|
50
|
+
this._state = 'idle';
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
205
54
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
settleError('process-exit', { rawMessage: err.message });
|
|
215
|
-
});
|
|
216
|
-
this._proc.on('close', (code) => {
|
|
217
|
-
debugLog(`Subprocess closed with code ${code}`);
|
|
218
|
-
this._rejectPendingRequests(new Error(`Playwright MCP process exited before response${code == null ? '' : ` (code ${code})`}`));
|
|
219
|
-
if (!settled) {
|
|
220
|
-
settleError(inferConnectFailureKind({
|
|
221
|
-
hasExtensionToken: !!extensionToken,
|
|
222
|
-
stderr: stderrBuffer,
|
|
223
|
-
exited: true,
|
|
224
|
-
isCdpMode: requestedCdp,
|
|
225
|
-
}), { exitCode: code });
|
|
226
|
-
}
|
|
227
|
-
});
|
|
55
|
+
async close(): Promise<void> {
|
|
56
|
+
if (this._state === 'closed') return;
|
|
57
|
+
this._state = 'closing';
|
|
58
|
+
// We don't kill the daemon — it auto-exits on idle.
|
|
59
|
+
// Just clean up our reference.
|
|
60
|
+
this._page = null;
|
|
61
|
+
this._state = 'closed';
|
|
62
|
+
}
|
|
228
63
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const initializedMsg = JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n';
|
|
248
|
-
debugLog(`SEND: ${initializedMsg.trim()}`);
|
|
249
|
-
this._proc?.stdin?.write(initializedMsg);
|
|
64
|
+
private async _ensureDaemon(): Promise<void> {
|
|
65
|
+
if (await isDaemonRunning()) return;
|
|
66
|
+
|
|
67
|
+
// Find daemon relative to this file — works for both:
|
|
68
|
+
// npx tsx src/main.ts → src/browser/mcp.ts → src/daemon.ts
|
|
69
|
+
// node dist/main.js → dist/browser/mcp.js → dist/daemon.js
|
|
70
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
71
|
+
const parentDir = path.resolve(__dirname, '..');
|
|
72
|
+
const daemonTs = path.join(parentDir, 'daemon.ts');
|
|
73
|
+
const daemonJs = path.join(parentDir, 'daemon.js');
|
|
74
|
+
const isTs = fs.existsSync(daemonTs);
|
|
75
|
+
const daemonPath = isTs ? daemonTs : daemonJs;
|
|
76
|
+
|
|
77
|
+
if (process.env.OPENCLI_VERBOSE) {
|
|
78
|
+
console.error(`[opencli] Starting daemon (${isTs ? 'ts' : 'js'})...`);
|
|
79
|
+
}
|
|
250
80
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
debugLog(`Tabs fetch error: ${err.message}`);
|
|
259
|
-
settleSuccess(page);
|
|
260
|
-
});
|
|
261
|
-
}).catch((err: Error) => {
|
|
262
|
-
debugLog(`Init promise rejected: ${err.message}`);
|
|
263
|
-
settleError('mcp-init', { rawMessage: err.message });
|
|
264
|
-
});
|
|
81
|
+
// Use the current runtime to spawn daemon — avoids slow npx resolution.
|
|
82
|
+
// If already running under tsx (dev), process.execPath is tsx's node.
|
|
83
|
+
// If running compiled (node dist/), process.execPath is node.
|
|
84
|
+
this._daemonProc = spawn(process.execPath, [daemonPath], {
|
|
85
|
+
detached: true,
|
|
86
|
+
stdio: 'ignore',
|
|
87
|
+
env: { ...process.env },
|
|
265
88
|
});
|
|
266
|
-
|
|
89
|
+
this._daemonProc.unref();
|
|
267
90
|
|
|
91
|
+
// Wait for daemon to be ready AND extension to connect
|
|
92
|
+
const deadline = Date.now() + DAEMON_SPAWN_TIMEOUT;
|
|
93
|
+
while (Date.now() < deadline) {
|
|
94
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
95
|
+
if (await isExtensionConnected()) return;
|
|
96
|
+
}
|
|
268
97
|
|
|
269
|
-
|
|
270
|
-
if (
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
try { await this._page.closeTab(index); } catch {}
|
|
283
|
-
}
|
|
284
|
-
} catch {}
|
|
285
|
-
}
|
|
286
|
-
if (this._proc && !this._proc.killed) {
|
|
287
|
-
this._proc.kill('SIGTERM');
|
|
288
|
-
const exited = await new Promise<boolean>((res) => {
|
|
289
|
-
let done = false;
|
|
290
|
-
const finish = (value: boolean) => {
|
|
291
|
-
if (done) return;
|
|
292
|
-
done = true;
|
|
293
|
-
res(value);
|
|
294
|
-
};
|
|
295
|
-
this._proc?.once('exit', () => finish(true));
|
|
296
|
-
setTimeout(() => finish(false), 3000);
|
|
297
|
-
});
|
|
298
|
-
if (!exited && this._proc && !this._proc.killed) {
|
|
299
|
-
try { this._proc.kill('SIGKILL'); } catch {}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
} finally {
|
|
303
|
-
this._rejectPendingRequests(new Error('Playwright MCP session closed'));
|
|
304
|
-
this._page = null;
|
|
305
|
-
this._proc = null;
|
|
306
|
-
this._state = 'closed';
|
|
307
|
-
PlaywrightMCP._activeInsts.delete(this);
|
|
308
|
-
}
|
|
309
|
-
})();
|
|
310
|
-
return this._closingPromise;
|
|
98
|
+
// Daemon might be up but extension not connected — give a useful error
|
|
99
|
+
if (await isDaemonRunning()) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
'Daemon is running but the Browser Extension is not connected.\n' +
|
|
102
|
+
'Please install and enable the opencli Browser Bridge extension in Chrome.',
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
throw new Error(
|
|
107
|
+
'Failed to start opencli daemon. Try running manually:\n' +
|
|
108
|
+
` node ${daemonPath}\n` +
|
|
109
|
+
'Make sure port 19825 is available.',
|
|
110
|
+
);
|
|
311
111
|
}
|
|
312
112
|
}
|