@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
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDP execution via chrome.debugger API.
|
|
3
|
+
*
|
|
4
|
+
* chrome.debugger only needs the "debugger" permission — no host_permissions.
|
|
5
|
+
* It can attach to any http/https tab. Avoid chrome:// and chrome-extension://
|
|
6
|
+
* tabs (resolveTabId in background.ts filters them).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const attached = new Set<number>();
|
|
10
|
+
|
|
11
|
+
async function ensureAttached(tabId: number): Promise<void> {
|
|
12
|
+
if (attached.has(tabId)) return;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
await chrome.debugger.attach({ tabId }, '1.3');
|
|
16
|
+
} catch (e: unknown) {
|
|
17
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
18
|
+
if (msg.includes('Another debugger is already attached')) {
|
|
19
|
+
try { await chrome.debugger.detach({ tabId }); } catch { /* ignore */ }
|
|
20
|
+
try {
|
|
21
|
+
await chrome.debugger.attach({ tabId }, '1.3');
|
|
22
|
+
} catch {
|
|
23
|
+
throw new Error(`attach failed: ${msg}`);
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
throw new Error(`attach failed: ${msg}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
attached.add(tabId);
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
await chrome.debugger.sendCommand({ tabId }, 'Runtime.enable');
|
|
33
|
+
} catch {
|
|
34
|
+
// Some pages may not need explicit enable
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function evaluate(tabId: number, expression: string): Promise<unknown> {
|
|
39
|
+
await ensureAttached(tabId);
|
|
40
|
+
|
|
41
|
+
const result = await chrome.debugger.sendCommand({ tabId }, 'Runtime.evaluate', {
|
|
42
|
+
expression,
|
|
43
|
+
returnByValue: true,
|
|
44
|
+
awaitPromise: true,
|
|
45
|
+
}) as {
|
|
46
|
+
result?: { type: string; value?: unknown; description?: string; subtype?: string };
|
|
47
|
+
exceptionDetails?: { exception?: { description?: string }; text?: string };
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
if (result.exceptionDetails) {
|
|
51
|
+
const errMsg = result.exceptionDetails.exception?.description
|
|
52
|
+
|| result.exceptionDetails.text
|
|
53
|
+
|| 'Eval error';
|
|
54
|
+
throw new Error(errMsg);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return result.result?.value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const evaluateAsync = evaluate;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Capture a screenshot via CDP Page.captureScreenshot.
|
|
64
|
+
* Returns base64-encoded image data.
|
|
65
|
+
*/
|
|
66
|
+
export async function screenshot(
|
|
67
|
+
tabId: number,
|
|
68
|
+
options: { format?: 'png' | 'jpeg'; quality?: number; fullPage?: boolean } = {},
|
|
69
|
+
): Promise<string> {
|
|
70
|
+
await ensureAttached(tabId);
|
|
71
|
+
|
|
72
|
+
const format = options.format ?? 'png';
|
|
73
|
+
|
|
74
|
+
// For full-page screenshots, get the full page dimensions first
|
|
75
|
+
if (options.fullPage) {
|
|
76
|
+
// Get full page metrics
|
|
77
|
+
const metrics = await chrome.debugger.sendCommand({ tabId }, 'Page.getLayoutMetrics') as {
|
|
78
|
+
contentSize?: { width: number; height: number };
|
|
79
|
+
cssContentSize?: { width: number; height: number };
|
|
80
|
+
};
|
|
81
|
+
const size = metrics.cssContentSize || metrics.contentSize;
|
|
82
|
+
if (size) {
|
|
83
|
+
// Set device metrics to full page size
|
|
84
|
+
await chrome.debugger.sendCommand({ tabId }, 'Emulation.setDeviceMetricsOverride', {
|
|
85
|
+
mobile: false,
|
|
86
|
+
width: Math.ceil(size.width),
|
|
87
|
+
height: Math.ceil(size.height),
|
|
88
|
+
deviceScaleFactor: 1,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const params: Record<string, unknown> = { format };
|
|
95
|
+
if (format === 'jpeg' && options.quality !== undefined) {
|
|
96
|
+
params.quality = Math.max(0, Math.min(100, options.quality));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const result = await chrome.debugger.sendCommand({ tabId }, 'Page.captureScreenshot', params) as {
|
|
100
|
+
data: string; // base64-encoded
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
return result.data;
|
|
104
|
+
} finally {
|
|
105
|
+
// Reset device metrics if we changed them for full-page
|
|
106
|
+
if (options.fullPage) {
|
|
107
|
+
await chrome.debugger.sendCommand({ tabId }, 'Emulation.clearDeviceMetricsOverride').catch(() => {});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function detach(tabId: number): void {
|
|
113
|
+
if (!attached.has(tabId)) return;
|
|
114
|
+
attached.delete(tabId);
|
|
115
|
+
try { chrome.debugger.detach({ tabId }); } catch { /* ignore */ }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function registerListeners(): void {
|
|
119
|
+
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
120
|
+
attached.delete(tabId);
|
|
121
|
+
});
|
|
122
|
+
chrome.debugger.onDetach.addListener((source) => {
|
|
123
|
+
if (source.tabId) attached.delete(source.tabId);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opencli browser protocol — shared types between daemon, extension, and CLI.
|
|
3
|
+
*
|
|
4
|
+
* 5 actions: exec, navigate, tabs, cookies, screenshot.
|
|
5
|
+
* Everything else is just JS code sent via 'exec'.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type Action = 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot';
|
|
9
|
+
|
|
10
|
+
export interface Command {
|
|
11
|
+
/** Unique request ID */
|
|
12
|
+
id: string;
|
|
13
|
+
/** Action type */
|
|
14
|
+
action: Action;
|
|
15
|
+
/** Target tab ID (omit for active tab) */
|
|
16
|
+
tabId?: number;
|
|
17
|
+
/** JS code to evaluate in page context (exec action) */
|
|
18
|
+
code?: string;
|
|
19
|
+
/** URL to navigate to (navigate action) */
|
|
20
|
+
url?: string;
|
|
21
|
+
/** Sub-operation for tabs: list, new, close, select */
|
|
22
|
+
op?: 'list' | 'new' | 'close' | 'select';
|
|
23
|
+
/** Tab index for tabs select/close */
|
|
24
|
+
index?: number;
|
|
25
|
+
/** Cookie domain filter */
|
|
26
|
+
domain?: string;
|
|
27
|
+
/** Screenshot format: png (default) or jpeg */
|
|
28
|
+
format?: 'png' | 'jpeg';
|
|
29
|
+
/** JPEG quality (0-100), only for jpeg format */
|
|
30
|
+
quality?: number;
|
|
31
|
+
/** Whether to capture full page (not just viewport) */
|
|
32
|
+
fullPage?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface Result {
|
|
36
|
+
/** Matching request ID */
|
|
37
|
+
id: string;
|
|
38
|
+
/** Whether the command succeeded */
|
|
39
|
+
ok: boolean;
|
|
40
|
+
/** Result data on success */
|
|
41
|
+
data?: unknown;
|
|
42
|
+
/** Error message on failure */
|
|
43
|
+
error?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Default daemon port */
|
|
47
|
+
export const DAEMON_PORT = 19825;
|
|
48
|
+
export const DAEMON_HOST = 'localhost';
|
|
49
|
+
export const DAEMON_WS_URL = `ws://${DAEMON_HOST}:${DAEMON_PORT}/ext`;
|
|
50
|
+
export const DAEMON_HTTP_URL = `http://${DAEMON_HOST}:${DAEMON_PORT}`;
|
|
51
|
+
|
|
52
|
+
/** Base reconnect delay for extension WebSocket (ms) */
|
|
53
|
+
export const WS_RECONNECT_BASE_DELAY = 2000;
|
|
54
|
+
/** Max reconnect delay (ms) */
|
|
55
|
+
export const WS_RECONNECT_MAX_DELAY = 60000;
|
|
56
|
+
/** Idle timeout before daemon auto-exits (ms) */
|
|
57
|
+
export const DAEMON_IDLE_TIMEOUT = 5 * 60 * 1000;
|
|
Binary file
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"outDir": "dist",
|
|
10
|
+
"rootDir": "src",
|
|
11
|
+
"declaration": false,
|
|
12
|
+
"types": ["chrome"]
|
|
13
|
+
},
|
|
14
|
+
"include": ["src"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
build: {
|
|
6
|
+
outDir: 'dist',
|
|
7
|
+
emptyOutDir: true,
|
|
8
|
+
rollupOptions: {
|
|
9
|
+
input: resolve(__dirname, 'src/background.ts'),
|
|
10
|
+
output: {
|
|
11
|
+
entryFileNames: 'background.js',
|
|
12
|
+
format: 'es',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
target: 'esnext',
|
|
16
|
+
minify: false,
|
|
17
|
+
},
|
|
18
|
+
});
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jackwener/opencli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
7
|
"description": "Make any website your CLI. AI-powered.",
|
|
8
8
|
"engines": {
|
|
9
|
-
"node": ">=
|
|
9
|
+
"node": ">=20.0.0"
|
|
10
10
|
},
|
|
11
11
|
"type": "module",
|
|
12
12
|
"main": "dist/main.js",
|
|
@@ -25,14 +25,14 @@
|
|
|
25
25
|
"lint": "tsc --noEmit",
|
|
26
26
|
"prepublishOnly": "npm run build",
|
|
27
27
|
"test": "vitest run",
|
|
28
|
+
"test:site": "node scripts/test-site.mjs",
|
|
28
29
|
"test:watch": "vitest"
|
|
29
30
|
},
|
|
30
31
|
"keywords": [
|
|
31
32
|
"cli",
|
|
32
33
|
"browser",
|
|
33
34
|
"web",
|
|
34
|
-
"ai"
|
|
35
|
-
"playwright"
|
|
35
|
+
"ai"
|
|
36
36
|
],
|
|
37
37
|
"author": "jackwener",
|
|
38
38
|
"license": "Apache-2.0",
|
|
@@ -43,12 +43,13 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"chalk": "^5.3.0",
|
|
45
45
|
"cli-table3": "^0.6.5",
|
|
46
|
-
"commander": "^
|
|
47
|
-
"js-yaml": "^4.1.0"
|
|
46
|
+
"commander": "^14.0.3",
|
|
47
|
+
"js-yaml": "^4.1.0",
|
|
48
|
+
"ws": "^8.18.0"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
50
|
-
"@playwright/mcp": "^0.0.68",
|
|
51
51
|
"@types/js-yaml": "^4.0.9",
|
|
52
|
+
"@types/ws": "^8.5.13",
|
|
52
53
|
"@types/node": "^22.13.10",
|
|
53
54
|
"tsx": "^4.19.3",
|
|
54
55
|
"typescript": "^5.8.2",
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import * as fs from 'node:fs';
|
|
5
|
+
import * as path from 'node:path';
|
|
6
|
+
|
|
7
|
+
const site = process.argv[2]?.trim();
|
|
8
|
+
|
|
9
|
+
if (!site) {
|
|
10
|
+
console.error('Usage: npm run test:site -- <site>');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const repoRoot = path.resolve(new URL('..', import.meta.url).pathname);
|
|
15
|
+
const srcDir = path.join(repoRoot, 'src');
|
|
16
|
+
|
|
17
|
+
function runStep(label, command, args) {
|
|
18
|
+
console.log(`\n==> ${label}`);
|
|
19
|
+
const result = spawnSync(command, args, {
|
|
20
|
+
cwd: repoRoot,
|
|
21
|
+
stdio: 'inherit',
|
|
22
|
+
env: process.env,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (result.status !== 0) {
|
|
26
|
+
process.exit(result.status ?? 1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function walk(dir) {
|
|
31
|
+
const files = [];
|
|
32
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
33
|
+
const fullPath = path.join(dir, entry.name);
|
|
34
|
+
if (entry.isDirectory()) {
|
|
35
|
+
files.push(...walk(fullPath));
|
|
36
|
+
} else {
|
|
37
|
+
files.push(fullPath);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return files;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function toPosix(filePath) {
|
|
44
|
+
return filePath.split(path.sep).join('/');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function findSiteTests() {
|
|
48
|
+
return walk(srcDir)
|
|
49
|
+
.filter(filePath => filePath.endsWith('.test.ts'))
|
|
50
|
+
.filter(filePath => {
|
|
51
|
+
const normalized = toPosix(path.relative(repoRoot, filePath));
|
|
52
|
+
return normalized.includes(`/clis/${site}/`) || normalized.includes(`/${site}.test.ts`);
|
|
53
|
+
})
|
|
54
|
+
.sort();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
runStep('Typecheck', 'npm', ['run', 'typecheck']);
|
|
58
|
+
runStep('Targeted verify', 'npx', ['tsx', 'src/main.ts', 'verify', site]);
|
|
59
|
+
|
|
60
|
+
const testFiles = findSiteTests();
|
|
61
|
+
if (testFiles.length === 0) {
|
|
62
|
+
console.log(`\nNo site-specific vitest files found for "${site}". Skipping full vitest run.`);
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
runStep(
|
|
67
|
+
`Site tests (${site})`,
|
|
68
|
+
'npx',
|
|
69
|
+
['vitest', 'run', ...testFiles.map(filePath => path.relative(repoRoot, filePath))],
|
|
70
|
+
);
|
|
@@ -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';
|
|
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,225 +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
|
-
export function resetMcpServerPathCache(): void {
|
|
16
|
-
_cachedMcpServerPath = undefined;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function setMcpDiscoveryTestHooks(input?: {
|
|
20
|
-
existsSync?: typeof fs.existsSync;
|
|
21
|
-
execSync?: typeof execSync;
|
|
22
|
-
}): void {
|
|
23
|
-
_existsSync = input?.existsSync ?? fs.existsSync;
|
|
24
|
-
_execSync = input?.execSync ?? execSync;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function findMcpServerPath(): string | null {
|
|
28
|
-
if (_cachedMcpServerPath !== undefined) return _cachedMcpServerPath;
|
|
8
|
+
import { isDaemonRunning } from './daemon-client.js';
|
|
29
9
|
|
|
30
|
-
|
|
31
|
-
if (envMcp && _existsSync(envMcp)) {
|
|
32
|
-
_cachedMcpServerPath = envMcp;
|
|
33
|
-
return _cachedMcpServerPath;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Check local node_modules first (@playwright/mcp is the modern package)
|
|
37
|
-
const localMcp = path.resolve('node_modules', '@playwright', 'mcp', 'cli.js');
|
|
38
|
-
if (_existsSync(localMcp)) {
|
|
39
|
-
_cachedMcpServerPath = localMcp;
|
|
40
|
-
return _cachedMcpServerPath;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Check project-relative path
|
|
44
|
-
const __dirname2 = path.dirname(fileURLToPath(import.meta.url));
|
|
45
|
-
const projectMcp = path.resolve(__dirname2, '..', '..', 'node_modules', '@playwright', 'mcp', 'cli.js');
|
|
46
|
-
if (_existsSync(projectMcp)) {
|
|
47
|
-
_cachedMcpServerPath = projectMcp;
|
|
48
|
-
return _cachedMcpServerPath;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Check global npm/yarn locations derived from current Node runtime.
|
|
52
|
-
const nodePrefix = path.resolve(path.dirname(process.execPath), '..');
|
|
53
|
-
const globalNodeModules = path.join(nodePrefix, 'lib', 'node_modules');
|
|
54
|
-
const globalMcp = path.join(globalNodeModules, '@playwright', 'mcp', 'cli.js');
|
|
55
|
-
if (_existsSync(globalMcp)) {
|
|
56
|
-
_cachedMcpServerPath = globalMcp;
|
|
57
|
-
return _cachedMcpServerPath;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Check npm global root directly.
|
|
61
|
-
try {
|
|
62
|
-
const npmRootGlobal = _execSync('npm root -g 2>/dev/null', {
|
|
63
|
-
encoding: 'utf-8',
|
|
64
|
-
timeout: 5000,
|
|
65
|
-
}).trim();
|
|
66
|
-
const npmGlobalMcp = path.join(npmRootGlobal, '@playwright', 'mcp', 'cli.js');
|
|
67
|
-
if (npmRootGlobal && _existsSync(npmGlobalMcp)) {
|
|
68
|
-
_cachedMcpServerPath = npmGlobalMcp;
|
|
69
|
-
return _cachedMcpServerPath;
|
|
70
|
-
}
|
|
71
|
-
} catch {}
|
|
72
|
-
|
|
73
|
-
// Check common locations
|
|
74
|
-
const candidates = [
|
|
75
|
-
path.join(os.homedir(), '.npm', '_npx'),
|
|
76
|
-
path.join(os.homedir(), 'node_modules', '.bin'),
|
|
77
|
-
'/usr/local/lib/node_modules',
|
|
78
|
-
];
|
|
79
|
-
|
|
80
|
-
// Try npx resolution (legacy package name)
|
|
81
|
-
try {
|
|
82
|
-
const result = _execSync('npx -y --package=@playwright/mcp which mcp-server-playwright 2>/dev/null', { encoding: 'utf-8', timeout: 10000 }).trim();
|
|
83
|
-
if (result && _existsSync(result)) {
|
|
84
|
-
_cachedMcpServerPath = result;
|
|
85
|
-
return _cachedMcpServerPath;
|
|
86
|
-
}
|
|
87
|
-
} catch {}
|
|
88
|
-
|
|
89
|
-
// Try which
|
|
90
|
-
try {
|
|
91
|
-
const result = _execSync('which mcp-server-playwright 2>/dev/null', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
92
|
-
if (result && _existsSync(result)) {
|
|
93
|
-
_cachedMcpServerPath = result;
|
|
94
|
-
return _cachedMcpServerPath;
|
|
95
|
-
}
|
|
96
|
-
} catch {}
|
|
97
|
-
|
|
98
|
-
// Search in common npx cache
|
|
99
|
-
for (const base of candidates) {
|
|
100
|
-
if (!_existsSync(base)) continue;
|
|
101
|
-
try {
|
|
102
|
-
const found = _execSync(`find "${base}" -name "cli.js" -path "*playwright*mcp*" 2>/dev/null | head -1`, { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
103
|
-
if (found) {
|
|
104
|
-
_cachedMcpServerPath = found;
|
|
105
|
-
return _cachedMcpServerPath;
|
|
106
|
-
}
|
|
107
|
-
} catch {}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
_cachedMcpServerPath = null;
|
|
111
|
-
return _cachedMcpServerPath;
|
|
112
|
-
}
|
|
10
|
+
export { isDaemonRunning };
|
|
113
11
|
|
|
114
12
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* Starting with Chrome 144, users can enable remote debugging from
|
|
118
|
-
* chrome://inspect#remote-debugging without any command-line flags.
|
|
119
|
-
* Chrome writes the active port and browser GUID to a DevToolsActivePort file
|
|
120
|
-
* in the user data directory, which we read to construct the WebSocket endpoint.
|
|
13
|
+
* Check daemon status and return connection info.
|
|
121
14
|
*/
|
|
122
|
-
export function
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
candidates.push(path.join(localAppData, 'Google', 'Chrome', 'User Data', 'DevToolsActivePort'));
|
|
134
|
-
candidates.push(path.join(localAppData, 'Microsoft', 'Edge', 'User Data', 'DevToolsActivePort'));
|
|
135
|
-
} else if (process.platform === 'darwin') {
|
|
136
|
-
candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'Google', 'Chrome', 'DevToolsActivePort'));
|
|
137
|
-
candidates.push(path.join(os.homedir(), 'Library', 'Application Support', 'Microsoft Edge', 'DevToolsActivePort'));
|
|
138
|
-
} else {
|
|
139
|
-
candidates.push(path.join(os.homedir(), '.config', 'google-chrome', 'DevToolsActivePort'));
|
|
140
|
-
candidates.push(path.join(os.homedir(), '.config', 'chromium', 'DevToolsActivePort'));
|
|
141
|
-
candidates.push(path.join(os.homedir(), '.config', 'microsoft-edge', 'DevToolsActivePort'));
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
for (const filePath of candidates) {
|
|
145
|
-
try {
|
|
146
|
-
const content = fs.readFileSync(filePath, 'utf-8').trim();
|
|
147
|
-
const lines = content.split('\n');
|
|
148
|
-
if (lines.length >= 2) {
|
|
149
|
-
const port = parseInt(lines[0], 10);
|
|
150
|
-
const browserPath = lines[1]; // e.g. /devtools/browser/<GUID>
|
|
151
|
-
if (port > 0 && browserPath.startsWith('/devtools/browser/')) {
|
|
152
|
-
return `ws://127.0.0.1:${port}${browserPath}`;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
} catch {}
|
|
156
|
-
}
|
|
157
|
-
return null;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
export function resolveCdpEndpoint(): { endpoint?: string; requestedCdp: boolean } {
|
|
161
|
-
const envVal = process.env.OPENCLI_CDP_ENDPOINT;
|
|
162
|
-
if (envVal === '1' || envVal?.toLowerCase() === 'true') {
|
|
163
|
-
const autoDiscovered = discoverChromeEndpoint();
|
|
164
|
-
return { endpoint: autoDiscovered ?? envVal, requestedCdp: true };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (envVal) {
|
|
168
|
-
return { endpoint: envVal, requestedCdp: true };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Fallback to auto-discovery if not explicitly set
|
|
172
|
-
const autoDiscovered = discoverChromeEndpoint();
|
|
173
|
-
if (autoDiscovered) {
|
|
174
|
-
return { endpoint: autoDiscovered, requestedCdp: true };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return { requestedCdp: false };
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function buildRuntimeArgs(input?: { executablePath?: string | null; cdpEndpoint?: string }): string[] {
|
|
181
|
-
const args: string[] = [];
|
|
182
|
-
|
|
183
|
-
// Priority 1: CDP endpoint (remote Chrome debugging or local Auto-Discovery)
|
|
184
|
-
if (input?.cdpEndpoint) {
|
|
185
|
-
args.push('--cdp-endpoint', input.cdpEndpoint);
|
|
186
|
-
return args;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Priority 2: Extension mode (local Chrome with MCP Bridge extension)
|
|
190
|
-
if (!process.env.CI) {
|
|
191
|
-
args.push('--extension');
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// CI/standalone mode: @playwright/mcp launches its own browser (headed by default).
|
|
195
|
-
// xvfb provides a virtual display for headed mode in GitHub Actions.
|
|
196
|
-
if (input?.executablePath) {
|
|
197
|
-
args.push('--executable-path', input.executablePath);
|
|
198
|
-
}
|
|
199
|
-
return args;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
export function buildMcpArgs(input: { mcpPath: string; executablePath?: string | null; cdpEndpoint?: string }): string[] {
|
|
203
|
-
return [input.mcpPath, ...buildRuntimeArgs(input)];
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export function buildMcpLaunchSpec(input: { mcpPath?: string | null; executablePath?: string | null; cdpEndpoint?: string }): {
|
|
207
|
-
command: string;
|
|
208
|
-
args: string[];
|
|
209
|
-
usedNpxFallback: boolean;
|
|
210
|
-
} {
|
|
211
|
-
const runtimeArgs = buildRuntimeArgs(input);
|
|
212
|
-
if (input.mcpPath) {
|
|
213
|
-
return {
|
|
214
|
-
command: 'node',
|
|
215
|
-
args: [input.mcpPath, ...runtimeArgs],
|
|
216
|
-
usedNpxFallback: false,
|
|
217
|
-
};
|
|
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 };
|
|
218
26
|
}
|
|
219
|
-
|
|
220
|
-
return {
|
|
221
|
-
command: 'npx',
|
|
222
|
-
args: ['-y', '@playwright/mcp@latest', ...runtimeArgs],
|
|
223
|
-
usedNpxFallback: true,
|
|
224
|
-
};
|
|
225
27
|
}
|