@jackwener/opencli 1.0.1 → 1.0.4
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/workflows/build-extension.yml +80 -0
- package/.github/workflows/ci.yml +6 -6
- package/.github/workflows/docs.yml +52 -0
- package/.github/workflows/e2e-headed.yml +2 -2
- package/.github/workflows/pkg-pr-new.yml +2 -2
- package/.github/workflows/release.yml +2 -5
- package/.github/workflows/security.yml +2 -2
- package/CDP.md +1 -1
- package/CDP.zh-CN.md +1 -1
- package/README.md +42 -34
- package/README.zh-CN.md +42 -34
- package/SKILL.md +3 -5
- package/dist/browser/cdp.d.ts +42 -0
- package/dist/browser/cdp.js +339 -0
- package/dist/browser/daemon-client.d.ts +3 -1
- package/dist/browser/daemon-client.js +4 -0
- package/dist/browser/dom-helpers.d.ts +20 -0
- package/dist/browser/dom-helpers.js +109 -0
- package/dist/browser/index.d.ts +3 -0
- package/dist/browser/index.js +4 -0
- package/dist/browser/mcp.d.ts +1 -0
- package/dist/browser/mcp.js +10 -5
- package/dist/browser/page.d.ts +7 -0
- package/dist/browser/page.js +39 -123
- package/dist/browser/utils.d.ts +10 -0
- package/dist/browser/utils.js +27 -0
- package/dist/browser.test.js +49 -1
- package/dist/build-manifest.js +3 -1
- package/dist/build-manifest.test.js +34 -0
- package/dist/capabilityRouting.d.ts +2 -0
- package/dist/capabilityRouting.js +30 -0
- package/dist/capabilityRouting.test.d.ts +1 -0
- package/dist/capabilityRouting.test.js +42 -0
- package/dist/chaoxing.d.ts +58 -0
- package/dist/chaoxing.js +225 -0
- package/dist/chaoxing.test.d.ts +1 -0
- package/dist/chaoxing.test.js +45 -0
- package/dist/cli-manifest.json +885 -48
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +234 -0
- package/dist/clis/antigravity/serve.d.ts +14 -0
- package/dist/clis/antigravity/serve.js +263 -0
- package/dist/clis/bilibili/download.js +4 -14
- package/dist/clis/boss/chatlist.d.ts +1 -0
- package/dist/clis/boss/chatlist.js +50 -0
- package/dist/clis/boss/chatmsg.d.ts +1 -0
- package/dist/clis/boss/chatmsg.js +73 -0
- package/dist/clis/boss/resume.d.ts +1 -0
- package/dist/clis/boss/resume.js +249 -0
- package/dist/clis/boss/send.d.ts +1 -0
- package/dist/clis/boss/send.js +176 -0
- package/dist/clis/chaoxing/assignments.d.ts +1 -0
- package/dist/clis/chaoxing/assignments.js +74 -0
- package/dist/clis/chaoxing/exams.d.ts +1 -0
- package/dist/clis/chaoxing/exams.js +74 -0
- package/dist/clis/chatgpt/ask.js +15 -14
- package/dist/clis/chatgpt/ax.d.ts +1 -0
- package/dist/clis/chatgpt/ax.js +78 -0
- package/dist/clis/chatgpt/read.js +5 -6
- package/dist/clis/hf/top.d.ts +1 -0
- package/dist/clis/hf/top.js +119 -0
- package/dist/clis/jike/comment.d.ts +1 -0
- package/dist/clis/jike/comment.js +107 -0
- package/dist/clis/jike/create.d.ts +1 -0
- package/dist/clis/jike/create.js +106 -0
- package/dist/clis/jike/feed.d.ts +1 -0
- package/dist/clis/jike/feed.js +67 -0
- package/dist/clis/jike/like.d.ts +1 -0
- package/dist/clis/jike/like.js +61 -0
- package/dist/clis/jike/notifications.d.ts +1 -0
- package/dist/clis/jike/notifications.js +169 -0
- package/dist/clis/jike/post.yaml +58 -0
- package/dist/clis/jike/repost.d.ts +1 -0
- package/dist/clis/jike/repost.js +103 -0
- package/dist/clis/jike/search.d.ts +1 -0
- package/dist/clis/jike/search.js +67 -0
- package/dist/clis/jike/shared.d.ts +19 -0
- package/dist/clis/jike/shared.js +25 -0
- package/dist/clis/jike/topic.yaml +52 -0
- package/dist/clis/jike/user.yaml +51 -0
- package/dist/clis/smzdm/search.js +28 -39
- package/dist/clis/stackoverflow/bounties.yaml +29 -0
- package/dist/clis/stackoverflow/hot.yaml +28 -0
- package/dist/clis/stackoverflow/search.yaml +32 -0
- package/dist/clis/stackoverflow/unanswered.yaml +28 -0
- package/dist/clis/twitter/download.js +6 -16
- package/dist/clis/twitter/post.js +9 -2
- package/dist/clis/twitter/search.js +14 -33
- package/dist/clis/xiaohongshu/download.d.ts +1 -1
- package/dist/clis/xiaohongshu/download.js +4 -4
- package/dist/clis/zhihu/download.js +3 -3
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +16 -0
- package/dist/download/index.d.ts +12 -8
- package/dist/download/index.js +11 -3
- package/dist/download/index.test.d.ts +1 -0
- package/dist/download/index.test.js +14 -0
- package/dist/engine.js +25 -14
- package/dist/explore.d.ts +1 -0
- package/dist/explore.js +48 -103
- package/dist/generate.js +1 -0
- package/dist/interceptor.js +3 -2
- package/dist/main.js +4 -193
- package/dist/output.d.ts +2 -1
- package/dist/output.js +3 -1
- package/dist/pipeline/executor.test.js +1 -0
- package/dist/pipeline/steps/download.js +14 -18
- package/dist/registry.d.ts +4 -3
- package/dist/registry.js +5 -2
- package/dist/runtime.d.ts +4 -1
- package/dist/runtime.js +2 -2
- package/dist/scripts/framework.d.ts +4 -0
- package/dist/scripts/framework.js +21 -0
- package/dist/scripts/interact.d.ts +4 -0
- package/dist/scripts/interact.js +20 -0
- package/dist/scripts/store.d.ts +9 -0
- package/dist/scripts/store.js +44 -0
- package/dist/synthesize.js +1 -1
- package/dist/types.d.ts +12 -0
- package/dist/verify.d.ts +6 -1
- package/dist/verify.js +54 -2
- package/docs/.vitepress/config.mts +193 -0
- package/docs/adapters/browser/apple-podcasts.md +28 -0
- package/docs/adapters/browser/bbc.md +26 -0
- package/docs/adapters/browser/bilibili.md +38 -0
- package/docs/adapters/browser/boss.md +28 -0
- package/docs/adapters/browser/coupang.md +28 -0
- package/docs/adapters/browser/ctrip.md +27 -0
- package/docs/adapters/browser/github.md +26 -0
- package/docs/adapters/browser/hackernews.md +26 -0
- package/docs/adapters/browser/linkedin.md +27 -0
- package/docs/adapters/browser/reddit.md +41 -0
- package/docs/adapters/browser/reuters.md +27 -0
- package/docs/adapters/browser/smzdm.md +27 -0
- package/docs/adapters/browser/twitter.md +47 -0
- package/docs/adapters/browser/v2ex.md +32 -0
- package/docs/adapters/browser/weibo.md +27 -0
- package/docs/adapters/browser/xiaohongshu.md +32 -0
- package/docs/adapters/browser/xiaoyuzhou.md +28 -0
- package/docs/adapters/browser/xueqiu.md +32 -0
- package/docs/adapters/browser/yahoo-finance.md +26 -0
- package/docs/adapters/browser/youtube.md +29 -0
- package/docs/adapters/browser/zhihu.md +30 -0
- package/docs/adapters/desktop/antigravity.md +46 -0
- package/docs/adapters/desktop/chatgpt.md +43 -0
- package/docs/adapters/desktop/chatwise.md +38 -0
- package/docs/adapters/desktop/codex.md +32 -0
- package/docs/adapters/desktop/cursor.md +33 -0
- package/docs/adapters/desktop/discord.md +28 -0
- package/docs/adapters/desktop/feishu.md +20 -0
- package/docs/adapters/desktop/neteasemusic.md +31 -0
- package/docs/adapters/desktop/notion.md +29 -0
- package/docs/adapters/desktop/wechat.md +28 -0
- package/docs/adapters/index.md +49 -0
- package/docs/advanced/cdp.md +103 -0
- package/docs/advanced/download.md +63 -0
- package/docs/advanced/electron.md +125 -0
- package/docs/advanced/remote-chrome.md +72 -0
- package/docs/developer/ai-workflow.md +66 -0
- package/docs/developer/architecture.md +90 -0
- package/docs/developer/contributing.md +136 -0
- package/docs/developer/testing.md +237 -0
- package/docs/developer/ts-adapter.md +87 -0
- package/docs/developer/yaml-adapter.md +108 -0
- package/docs/guide/browser-bridge.md +38 -0
- package/docs/guide/getting-started.md +56 -0
- package/docs/guide/installation.md +37 -0
- package/docs/guide/troubleshooting.md +56 -0
- package/docs/index.md +35 -0
- package/docs/zh/adapters/index.md +5 -0
- package/docs/zh/advanced/cdp.md +3 -0
- package/docs/zh/developer/contributing.md +24 -0
- package/docs/zh/guide/browser-bridge.md +25 -0
- package/docs/zh/guide/getting-started.md +40 -0
- package/docs/zh/guide/installation.md +37 -0
- package/docs/zh/index.md +29 -0
- package/extension/dist/background.js +386 -438
- package/extension/manifest.json +2 -2
- package/extension/package-lock.json +1156 -0
- package/extension/src/background.test.ts +151 -0
- package/extension/src/background.ts +124 -53
- package/extension/src/protocol.ts +3 -1
- package/package.json +7 -3
- package/src/browser/cdp.ts +367 -0
- package/src/browser/daemon-client.ts +7 -1
- package/src/browser/dom-helpers.ts +116 -0
- package/src/browser/index.ts +4 -0
- package/src/browser/mcp.ts +14 -6
- package/src/browser/page.ts +47 -124
- package/src/browser/utils.ts +27 -0
- package/src/browser.test.ts +56 -0
- package/src/build-manifest.test.ts +36 -0
- package/src/build-manifest.ts +2 -1
- package/src/capabilityRouting.test.ts +47 -0
- package/src/capabilityRouting.ts +28 -0
- package/src/chaoxing.test.ts +53 -0
- package/src/chaoxing.ts +268 -0
- package/src/cli.ts +205 -0
- package/src/clis/antigravity/SKILL.md +5 -0
- package/src/clis/antigravity/serve.ts +329 -0
- package/src/clis/bilibili/download.ts +4 -15
- package/src/clis/boss/chatlist.ts +50 -0
- package/src/clis/boss/chatmsg.ts +70 -0
- package/src/clis/boss/resume.ts +262 -0
- package/src/clis/boss/send.ts +193 -0
- package/src/clis/chaoxing/README.md +36 -0
- package/src/clis/chaoxing/README.zh-CN.md +35 -0
- package/src/clis/chaoxing/assignments.ts +88 -0
- package/src/clis/chaoxing/exams.ts +88 -0
- package/src/clis/chatgpt/ask.ts +14 -15
- package/src/clis/chatgpt/ax.ts +81 -0
- package/src/clis/chatgpt/read.ts +5 -7
- package/src/clis/hf/top.ts +141 -0
- package/src/clis/jike/comment.ts +113 -0
- package/src/clis/jike/create.ts +113 -0
- package/src/clis/jike/feed.ts +74 -0
- package/src/clis/jike/like.ts +65 -0
- package/src/clis/jike/notifications.ts +185 -0
- package/src/clis/jike/post.yaml +58 -0
- package/src/clis/jike/repost.ts +114 -0
- package/src/clis/jike/search.ts +74 -0
- package/src/clis/jike/shared.ts +36 -0
- package/src/clis/jike/topic.yaml +52 -0
- package/src/clis/jike/user.yaml +51 -0
- package/src/clis/smzdm/search.ts +30 -39
- package/src/clis/stackoverflow/bounties.yaml +29 -0
- package/src/clis/stackoverflow/hot.yaml +28 -0
- package/src/clis/stackoverflow/search.yaml +32 -0
- package/src/clis/stackoverflow/unanswered.yaml +28 -0
- package/src/clis/twitter/download.ts +6 -17
- package/src/clis/twitter/post.ts +9 -2
- package/src/clis/twitter/search.ts +15 -33
- package/src/clis/xiaohongshu/download.ts +4 -4
- package/src/clis/zhihu/download.ts +3 -3
- package/src/doctor.ts +18 -2
- package/src/download/index.test.ts +16 -0
- package/src/download/index.ts +22 -4
- package/src/engine.ts +20 -13
- package/src/explore.ts +54 -103
- package/src/generate.ts +1 -0
- package/src/interceptor.ts +3 -2
- package/src/main.ts +4 -180
- package/src/output.ts +15 -13
- package/src/pipeline/executor.test.ts +1 -0
- package/src/pipeline/steps/download.ts +14 -17
- package/src/registry.ts +9 -5
- package/src/runtime.ts +3 -2
- package/src/scripts/framework.ts +20 -0
- package/src/scripts/interact.ts +22 -0
- package/src/scripts/store.ts +40 -0
- package/src/synthesize.ts +1 -1
- package/src/types.ts +9 -0
- package/src/verify.ts +64 -3
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CDP client — implements IPage by connecting directly to a Chrome/Electron CDP WebSocket.
|
|
3
|
+
*
|
|
4
|
+
* Fixes applied:
|
|
5
|
+
* - send() now has a 30s timeout guard (P0 #4)
|
|
6
|
+
* - goto() waits for Page.loadEventFired instead of hardcoded 1s sleep (P1 #3)
|
|
7
|
+
* - Implemented scroll, autoScroll, screenshot, networkRequests (P1 #2)
|
|
8
|
+
* - Shared DOM helper methods extracted to reduce duplication with Page (P1 #5)
|
|
9
|
+
*/
|
|
10
|
+
import { WebSocket } from 'ws';
|
|
11
|
+
import { wrapForEval } from './utils.js';
|
|
12
|
+
import { clickJs, typeTextJs, pressKeyJs, waitForTextJs, scrollJs, autoScrollJs, networkRequestsJs, } from './dom-helpers.js';
|
|
13
|
+
const CDP_SEND_TIMEOUT = 30_000; // 30s per command
|
|
14
|
+
export class CDPBridge {
|
|
15
|
+
_ws = null;
|
|
16
|
+
_idCounter = 0;
|
|
17
|
+
_pending = new Map();
|
|
18
|
+
_eventListeners = new Map();
|
|
19
|
+
async connect(opts) {
|
|
20
|
+
const endpoint = process.env.OPENCLI_CDP_ENDPOINT;
|
|
21
|
+
if (!endpoint)
|
|
22
|
+
throw new Error('OPENCLI_CDP_ENDPOINT is not set');
|
|
23
|
+
// If it's a direct ws:// URL, use it. Otherwise, fetch the /json endpoint to find a page.
|
|
24
|
+
let wsUrl = endpoint;
|
|
25
|
+
if (endpoint.startsWith('http')) {
|
|
26
|
+
const res = await fetch(`${endpoint.replace(/\/$/, '')}/json`);
|
|
27
|
+
if (!res.ok)
|
|
28
|
+
throw new Error(`Failed to fetch CDP targets: ${res.statusText}`);
|
|
29
|
+
const targets = await res.json();
|
|
30
|
+
const target = selectCDPTarget(targets);
|
|
31
|
+
if (!target || !target.webSocketDebuggerUrl) {
|
|
32
|
+
throw new Error('No inspectable targets found at CDP endpoint');
|
|
33
|
+
}
|
|
34
|
+
wsUrl = target.webSocketDebuggerUrl;
|
|
35
|
+
}
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const ws = new WebSocket(wsUrl);
|
|
38
|
+
const timeout = setTimeout(() => reject(new Error('CDP connect timeout')), opts?.timeout ?? 10000);
|
|
39
|
+
ws.on('open', () => {
|
|
40
|
+
clearTimeout(timeout);
|
|
41
|
+
this._ws = ws;
|
|
42
|
+
resolve(new CDPPage(this));
|
|
43
|
+
});
|
|
44
|
+
ws.on('error', (err) => {
|
|
45
|
+
clearTimeout(timeout);
|
|
46
|
+
reject(err);
|
|
47
|
+
});
|
|
48
|
+
ws.on('message', (data) => {
|
|
49
|
+
try {
|
|
50
|
+
const msg = JSON.parse(data.toString());
|
|
51
|
+
// Handle command responses
|
|
52
|
+
if (msg.id && this._pending.has(msg.id)) {
|
|
53
|
+
const entry = this._pending.get(msg.id);
|
|
54
|
+
clearTimeout(entry.timer);
|
|
55
|
+
this._pending.delete(msg.id);
|
|
56
|
+
if (msg.error) {
|
|
57
|
+
entry.reject(new Error(msg.error.message));
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
entry.resolve(msg.result);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Handle CDP events
|
|
64
|
+
if (msg.method) {
|
|
65
|
+
const listeners = this._eventListeners.get(msg.method);
|
|
66
|
+
if (listeners) {
|
|
67
|
+
for (const fn of listeners)
|
|
68
|
+
fn(msg.params);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// ignore parsing errors
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async close() {
|
|
79
|
+
if (this._ws) {
|
|
80
|
+
this._ws.close();
|
|
81
|
+
this._ws = null;
|
|
82
|
+
}
|
|
83
|
+
for (const p of this._pending.values()) {
|
|
84
|
+
clearTimeout(p.timer);
|
|
85
|
+
p.reject(new Error('CDP connection closed'));
|
|
86
|
+
}
|
|
87
|
+
this._pending.clear();
|
|
88
|
+
this._eventListeners.clear();
|
|
89
|
+
}
|
|
90
|
+
/** Send a CDP command with timeout guard (P0 fix #4) */
|
|
91
|
+
async send(method, params = {}, timeoutMs = CDP_SEND_TIMEOUT) {
|
|
92
|
+
if (!this._ws || this._ws.readyState !== WebSocket.OPEN) {
|
|
93
|
+
throw new Error('CDP connection is not open');
|
|
94
|
+
}
|
|
95
|
+
const id = ++this._idCounter;
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
const timer = setTimeout(() => {
|
|
98
|
+
this._pending.delete(id);
|
|
99
|
+
reject(new Error(`CDP command '${method}' timed out after ${timeoutMs / 1000}s`));
|
|
100
|
+
}, timeoutMs);
|
|
101
|
+
this._pending.set(id, { resolve, reject, timer });
|
|
102
|
+
this._ws.send(JSON.stringify({ id, method, params }));
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/** Listen for a CDP event */
|
|
106
|
+
on(event, handler) {
|
|
107
|
+
let set = this._eventListeners.get(event);
|
|
108
|
+
if (!set) {
|
|
109
|
+
set = new Set();
|
|
110
|
+
this._eventListeners.set(event, set);
|
|
111
|
+
}
|
|
112
|
+
set.add(handler);
|
|
113
|
+
}
|
|
114
|
+
/** Remove a CDP event listener */
|
|
115
|
+
off(event, handler) {
|
|
116
|
+
this._eventListeners.get(event)?.delete(handler);
|
|
117
|
+
}
|
|
118
|
+
/** Wait for a CDP event to fire (one-shot) */
|
|
119
|
+
waitForEvent(event, timeoutMs = 15_000) {
|
|
120
|
+
return new Promise((resolve, reject) => {
|
|
121
|
+
const timer = setTimeout(() => {
|
|
122
|
+
this.off(event, handler);
|
|
123
|
+
reject(new Error(`Timed out waiting for CDP event '${event}'`));
|
|
124
|
+
}, timeoutMs);
|
|
125
|
+
const handler = (params) => {
|
|
126
|
+
clearTimeout(timer);
|
|
127
|
+
this.off(event, handler);
|
|
128
|
+
resolve(params);
|
|
129
|
+
};
|
|
130
|
+
this.on(event, handler);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
class CDPPage {
|
|
135
|
+
bridge;
|
|
136
|
+
constructor(bridge) {
|
|
137
|
+
this.bridge = bridge;
|
|
138
|
+
}
|
|
139
|
+
/** Navigate with proper load event waiting (P1 fix #3) */
|
|
140
|
+
async goto(url) {
|
|
141
|
+
await this.bridge.send('Page.enable');
|
|
142
|
+
const loadPromise = this.bridge.waitForEvent('Page.loadEventFired', 30_000)
|
|
143
|
+
.catch(() => { }); // Don't fail if event times out
|
|
144
|
+
await this.bridge.send('Page.navigate', { url });
|
|
145
|
+
await loadPromise;
|
|
146
|
+
}
|
|
147
|
+
async evaluate(js) {
|
|
148
|
+
const expression = wrapForEval(js);
|
|
149
|
+
const result = await this.bridge.send('Runtime.evaluate', {
|
|
150
|
+
expression,
|
|
151
|
+
returnByValue: true,
|
|
152
|
+
awaitPromise: true
|
|
153
|
+
});
|
|
154
|
+
if (result.exceptionDetails) {
|
|
155
|
+
throw new Error('Evaluate error: ' + (result.exceptionDetails.exception?.description || 'Unknown exception'));
|
|
156
|
+
}
|
|
157
|
+
return result.result?.value;
|
|
158
|
+
}
|
|
159
|
+
async getCookies(opts = {}) {
|
|
160
|
+
const result = await this.bridge.send('Network.getCookies', opts.url ? { urls: [opts.url] } : {});
|
|
161
|
+
const cookies = Array.isArray(result?.cookies) ? result.cookies : [];
|
|
162
|
+
return opts.domain
|
|
163
|
+
? cookies.filter((cookie) => typeof cookie.domain === 'string' && cookie.domain.includes(opts.domain))
|
|
164
|
+
: cookies;
|
|
165
|
+
}
|
|
166
|
+
async snapshot(_opts) {
|
|
167
|
+
// CDP doesn't have a built-in accessibility tree equivalent without additional setup
|
|
168
|
+
return '(snapshot not available in CDP mode)';
|
|
169
|
+
}
|
|
170
|
+
// ── Shared DOM operations (P1 fix #5 — using dom-helpers.ts) ──
|
|
171
|
+
async click(ref) {
|
|
172
|
+
await this.evaluate(clickJs(ref));
|
|
173
|
+
}
|
|
174
|
+
async typeText(ref, text) {
|
|
175
|
+
await this.evaluate(typeTextJs(ref, text));
|
|
176
|
+
}
|
|
177
|
+
async pressKey(key) {
|
|
178
|
+
await this.evaluate(pressKeyJs(key));
|
|
179
|
+
}
|
|
180
|
+
async wait(options) {
|
|
181
|
+
if (typeof options === 'number') {
|
|
182
|
+
await new Promise(resolve => setTimeout(resolve, options * 1000));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (options.time) {
|
|
186
|
+
await new Promise(resolve => setTimeout(resolve, options.time * 1000));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (options.text) {
|
|
190
|
+
const timeout = (options.timeout ?? 30) * 1000;
|
|
191
|
+
await this.evaluate(waitForTextJs(options.text, timeout));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// ── Implemented methods (P1 fix #2) ──
|
|
195
|
+
async scroll(direction = 'down', amount = 500) {
|
|
196
|
+
await this.evaluate(scrollJs(direction, amount));
|
|
197
|
+
}
|
|
198
|
+
async autoScroll(options) {
|
|
199
|
+
const times = options?.times ?? 3;
|
|
200
|
+
const delayMs = options?.delayMs ?? 2000;
|
|
201
|
+
await this.evaluate(autoScrollJs(times, delayMs));
|
|
202
|
+
}
|
|
203
|
+
async screenshot(options = {}) {
|
|
204
|
+
const result = await this.bridge.send('Page.captureScreenshot', {
|
|
205
|
+
format: options.format ?? 'png',
|
|
206
|
+
quality: options.format === 'jpeg' ? (options.quality ?? 80) : undefined,
|
|
207
|
+
captureBeyondViewport: options.fullPage ?? false,
|
|
208
|
+
});
|
|
209
|
+
const base64 = result.data;
|
|
210
|
+
if (options.path) {
|
|
211
|
+
const fs = await import('node:fs');
|
|
212
|
+
const path = await import('node:path');
|
|
213
|
+
const dir = path.dirname(options.path);
|
|
214
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
215
|
+
await fs.promises.writeFile(options.path, Buffer.from(base64, 'base64'));
|
|
216
|
+
}
|
|
217
|
+
return base64;
|
|
218
|
+
}
|
|
219
|
+
async networkRequests(includeStatic = false) {
|
|
220
|
+
return this.evaluate(networkRequestsJs(includeStatic));
|
|
221
|
+
}
|
|
222
|
+
async tabs() {
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
async closeTab(_index) {
|
|
226
|
+
// Not supported in direct CDP mode
|
|
227
|
+
}
|
|
228
|
+
async newTab() {
|
|
229
|
+
await this.bridge.send('Target.createTarget', { url: 'about:blank' });
|
|
230
|
+
}
|
|
231
|
+
async selectTab(_index) {
|
|
232
|
+
// Not supported in direct CDP mode
|
|
233
|
+
}
|
|
234
|
+
async consoleMessages(_level) {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
async installInterceptor(pattern) {
|
|
238
|
+
const { generateInterceptorJs } = await import('../interceptor.js');
|
|
239
|
+
await this.evaluate(generateInterceptorJs(JSON.stringify(pattern), {
|
|
240
|
+
arrayName: '__opencli_xhr',
|
|
241
|
+
patchGuard: '__opencli_interceptor_patched',
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
async getInterceptedRequests() {
|
|
245
|
+
const { generateReadInterceptedJs } = await import('../interceptor.js');
|
|
246
|
+
const result = await this.evaluate(generateReadInterceptedJs('__opencli_xhr'));
|
|
247
|
+
return result || [];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// ── CDP target selection (unchanged) ──
|
|
251
|
+
function selectCDPTarget(targets) {
|
|
252
|
+
const preferredPattern = compilePreferredPattern(process.env.OPENCLI_CDP_TARGET);
|
|
253
|
+
const ranked = targets
|
|
254
|
+
.map((target, index) => ({ target, index, score: scoreCDPTarget(target, preferredPattern) }))
|
|
255
|
+
.filter(({ score }) => Number.isFinite(score))
|
|
256
|
+
.sort((a, b) => {
|
|
257
|
+
if (b.score !== a.score)
|
|
258
|
+
return b.score - a.score;
|
|
259
|
+
return a.index - b.index;
|
|
260
|
+
});
|
|
261
|
+
return ranked[0]?.target;
|
|
262
|
+
}
|
|
263
|
+
function scoreCDPTarget(target, preferredPattern) {
|
|
264
|
+
if (!target.webSocketDebuggerUrl)
|
|
265
|
+
return Number.NEGATIVE_INFINITY;
|
|
266
|
+
const type = (target.type ?? '').toLowerCase();
|
|
267
|
+
const url = (target.url ?? '').toLowerCase();
|
|
268
|
+
const title = (target.title ?? '').toLowerCase();
|
|
269
|
+
const haystack = `${title} ${url}`;
|
|
270
|
+
if (!haystack.trim() && !type)
|
|
271
|
+
return Number.NEGATIVE_INFINITY;
|
|
272
|
+
if (haystack.includes('devtools'))
|
|
273
|
+
return Number.NEGATIVE_INFINITY;
|
|
274
|
+
let score = 0;
|
|
275
|
+
if (preferredPattern && preferredPattern.test(haystack))
|
|
276
|
+
score += 1000;
|
|
277
|
+
if (type === 'app')
|
|
278
|
+
score += 120;
|
|
279
|
+
else if (type === 'webview')
|
|
280
|
+
score += 100;
|
|
281
|
+
else if (type === 'page')
|
|
282
|
+
score += 80;
|
|
283
|
+
else if (type === 'iframe')
|
|
284
|
+
score += 20;
|
|
285
|
+
if (url.startsWith('http://localhost') || url.startsWith('https://localhost'))
|
|
286
|
+
score += 90;
|
|
287
|
+
if (url.startsWith('file://'))
|
|
288
|
+
score += 60;
|
|
289
|
+
if (url.startsWith('http://127.0.0.1') || url.startsWith('https://127.0.0.1'))
|
|
290
|
+
score += 50;
|
|
291
|
+
if (url.startsWith('about:blank'))
|
|
292
|
+
score -= 120;
|
|
293
|
+
if (url === '' || url === 'about:blank')
|
|
294
|
+
score -= 40;
|
|
295
|
+
if (title && title !== 'devtools')
|
|
296
|
+
score += 25;
|
|
297
|
+
if (title.includes('antigravity'))
|
|
298
|
+
score += 120;
|
|
299
|
+
if (title.includes('codex'))
|
|
300
|
+
score += 120;
|
|
301
|
+
if (title.includes('cursor'))
|
|
302
|
+
score += 120;
|
|
303
|
+
if (title.includes('chatwise'))
|
|
304
|
+
score += 120;
|
|
305
|
+
if (title.includes('notion'))
|
|
306
|
+
score += 120;
|
|
307
|
+
if (title.includes('discord'))
|
|
308
|
+
score += 120;
|
|
309
|
+
if (title.includes('netease'))
|
|
310
|
+
score += 120;
|
|
311
|
+
if (url.includes('antigravity'))
|
|
312
|
+
score += 100;
|
|
313
|
+
if (url.includes('codex'))
|
|
314
|
+
score += 100;
|
|
315
|
+
if (url.includes('cursor'))
|
|
316
|
+
score += 100;
|
|
317
|
+
if (url.includes('chatwise'))
|
|
318
|
+
score += 100;
|
|
319
|
+
if (url.includes('notion'))
|
|
320
|
+
score += 100;
|
|
321
|
+
if (url.includes('discord'))
|
|
322
|
+
score += 100;
|
|
323
|
+
if (url.includes('netease'))
|
|
324
|
+
score += 100;
|
|
325
|
+
return score;
|
|
326
|
+
}
|
|
327
|
+
function compilePreferredPattern(raw) {
|
|
328
|
+
const value = raw?.trim();
|
|
329
|
+
if (!value)
|
|
330
|
+
return undefined;
|
|
331
|
+
return new RegExp(escapeRegExp(value.toLowerCase()));
|
|
332
|
+
}
|
|
333
|
+
function escapeRegExp(value) {
|
|
334
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
335
|
+
}
|
|
336
|
+
export const __test__ = {
|
|
337
|
+
selectCDPTarget,
|
|
338
|
+
scoreCDPTarget,
|
|
339
|
+
};
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export interface DaemonCommand {
|
|
7
7
|
id: string;
|
|
8
|
-
action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window';
|
|
8
|
+
action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions';
|
|
9
9
|
tabId?: number;
|
|
10
10
|
code?: string;
|
|
11
|
+
workspace?: string;
|
|
11
12
|
url?: string;
|
|
12
13
|
op?: string;
|
|
13
14
|
index?: number;
|
|
@@ -35,3 +36,4 @@ export declare function isExtensionConnected(): Promise<boolean>;
|
|
|
35
36
|
* Retries up to 3 times with 500ms delay for transient failures.
|
|
36
37
|
*/
|
|
37
38
|
export declare function sendCommand(action: DaemonCommand['action'], params?: Omit<DaemonCommand, 'id' | 'action'>): Promise<unknown>;
|
|
39
|
+
export declare function listSessions(): Promise<any[]>;
|
|
@@ -80,3 +80,7 @@ export async function sendCommand(action, params = {}) {
|
|
|
80
80
|
// Unreachable — the loop always returns or throws
|
|
81
81
|
throw new Error('sendCommand: max retries exhausted');
|
|
82
82
|
}
|
|
83
|
+
export async function listSessions() {
|
|
84
|
+
const result = await sendCommand('sessions');
|
|
85
|
+
return Array.isArray(result) ? result : [];
|
|
86
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared DOM operation JS generators.
|
|
3
|
+
*
|
|
4
|
+
* Used by both Page (daemon mode) and CDPPage (direct CDP mode)
|
|
5
|
+
* to eliminate code duplication for click, type, press, wait, scroll, etc.
|
|
6
|
+
*/
|
|
7
|
+
/** Generate JS to click an element by ref */
|
|
8
|
+
export declare function clickJs(ref: string): string;
|
|
9
|
+
/** Generate JS to type text into an element by ref */
|
|
10
|
+
export declare function typeTextJs(ref: string, text: string): string;
|
|
11
|
+
/** Generate JS to press a keyboard key */
|
|
12
|
+
export declare function pressKeyJs(key: string): string;
|
|
13
|
+
/** Generate JS to wait for text to appear in the page */
|
|
14
|
+
export declare function waitForTextJs(text: string, timeoutMs: number): string;
|
|
15
|
+
/** Generate JS for scroll */
|
|
16
|
+
export declare function scrollJs(direction: string, amount: number): string;
|
|
17
|
+
/** Generate JS for auto-scroll with lazy-load detection */
|
|
18
|
+
export declare function autoScrollJs(times: number, delayMs: number): string;
|
|
19
|
+
/** Generate JS to read performance resource entries as network requests */
|
|
20
|
+
export declare function networkRequestsJs(includeStatic: boolean): string;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared DOM operation JS generators.
|
|
3
|
+
*
|
|
4
|
+
* Used by both Page (daemon mode) and CDPPage (direct CDP mode)
|
|
5
|
+
* to eliminate code duplication for click, type, press, wait, scroll, etc.
|
|
6
|
+
*/
|
|
7
|
+
/** Generate JS to click an element by ref */
|
|
8
|
+
export function clickJs(ref) {
|
|
9
|
+
const safeRef = JSON.stringify(ref);
|
|
10
|
+
return `
|
|
11
|
+
(() => {
|
|
12
|
+
const ref = ${safeRef};
|
|
13
|
+
const el = document.querySelector('[data-ref="' + ref + '"]')
|
|
14
|
+
|| document.querySelectorAll('a, button, input, [role="button"], [tabindex]')[parseInt(ref, 10) || 0];
|
|
15
|
+
if (!el) throw new Error('Element not found: ' + ref);
|
|
16
|
+
el.scrollIntoView({ behavior: 'instant', block: 'center' });
|
|
17
|
+
el.click();
|
|
18
|
+
return 'clicked';
|
|
19
|
+
})()
|
|
20
|
+
`;
|
|
21
|
+
}
|
|
22
|
+
/** Generate JS to type text into an element by ref */
|
|
23
|
+
export function typeTextJs(ref, text) {
|
|
24
|
+
const safeRef = JSON.stringify(ref);
|
|
25
|
+
const safeText = JSON.stringify(text);
|
|
26
|
+
return `
|
|
27
|
+
(() => {
|
|
28
|
+
const ref = ${safeRef};
|
|
29
|
+
const el = document.querySelector('[data-ref="' + ref + '"]')
|
|
30
|
+
|| document.querySelectorAll('input, textarea, [contenteditable]')[parseInt(ref, 10) || 0];
|
|
31
|
+
if (!el) throw new Error('Element not found: ' + ref);
|
|
32
|
+
el.focus();
|
|
33
|
+
el.value = ${safeText};
|
|
34
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
35
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
36
|
+
return 'typed';
|
|
37
|
+
})()
|
|
38
|
+
`;
|
|
39
|
+
}
|
|
40
|
+
/** Generate JS to press a keyboard key */
|
|
41
|
+
export function pressKeyJs(key) {
|
|
42
|
+
return `
|
|
43
|
+
(() => {
|
|
44
|
+
const el = document.activeElement || document.body;
|
|
45
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: ${JSON.stringify(key)}, bubbles: true }));
|
|
46
|
+
el.dispatchEvent(new KeyboardEvent('keyup', { key: ${JSON.stringify(key)}, bubbles: true }));
|
|
47
|
+
return 'pressed';
|
|
48
|
+
})()
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
51
|
+
/** Generate JS to wait for text to appear in the page */
|
|
52
|
+
export function waitForTextJs(text, timeoutMs) {
|
|
53
|
+
return `
|
|
54
|
+
new Promise((resolve, reject) => {
|
|
55
|
+
const deadline = Date.now() + ${timeoutMs};
|
|
56
|
+
const check = () => {
|
|
57
|
+
if (document.body.innerText.includes(${JSON.stringify(text)})) return resolve('found');
|
|
58
|
+
if (Date.now() > deadline) return reject(new Error('Text not found: ' + ${JSON.stringify(text)}));
|
|
59
|
+
setTimeout(check, 200);
|
|
60
|
+
};
|
|
61
|
+
check();
|
|
62
|
+
})
|
|
63
|
+
`;
|
|
64
|
+
}
|
|
65
|
+
/** Generate JS for scroll */
|
|
66
|
+
export function scrollJs(direction, amount) {
|
|
67
|
+
const dx = direction === 'left' ? -amount : direction === 'right' ? amount : 0;
|
|
68
|
+
const dy = direction === 'up' ? -amount : direction === 'down' ? amount : 0;
|
|
69
|
+
return `window.scrollBy(${dx}, ${dy})`;
|
|
70
|
+
}
|
|
71
|
+
/** Generate JS for auto-scroll with lazy-load detection */
|
|
72
|
+
export function autoScrollJs(times, delayMs) {
|
|
73
|
+
return `
|
|
74
|
+
(async () => {
|
|
75
|
+
for (let i = 0; i < ${times}; i++) {
|
|
76
|
+
const lastHeight = document.body.scrollHeight;
|
|
77
|
+
window.scrollTo(0, lastHeight);
|
|
78
|
+
await new Promise(resolve => {
|
|
79
|
+
let timeoutId;
|
|
80
|
+
const observer = new MutationObserver(() => {
|
|
81
|
+
if (document.body.scrollHeight > lastHeight) {
|
|
82
|
+
clearTimeout(timeoutId);
|
|
83
|
+
observer.disconnect();
|
|
84
|
+
setTimeout(resolve, 100);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
88
|
+
timeoutId = setTimeout(() => { observer.disconnect(); resolve(null); }, ${delayMs});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
})()
|
|
92
|
+
`;
|
|
93
|
+
}
|
|
94
|
+
/** Generate JS to read performance resource entries as network requests */
|
|
95
|
+
export function networkRequestsJs(includeStatic) {
|
|
96
|
+
return `
|
|
97
|
+
(() => {
|
|
98
|
+
const entries = performance.getEntriesByType('resource');
|
|
99
|
+
return entries
|
|
100
|
+
${includeStatic ? '' : '.filter(e => !["img", "font", "css", "script"].some(t => e.initiatorType === t))'}
|
|
101
|
+
.map(e => ({
|
|
102
|
+
url: e.name,
|
|
103
|
+
type: e.initiatorType,
|
|
104
|
+
duration: Math.round(e.duration),
|
|
105
|
+
size: e.transferSize || 0,
|
|
106
|
+
}));
|
|
107
|
+
})()
|
|
108
|
+
`;
|
|
109
|
+
}
|
package/dist/browser/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { Page } from './page.js';
|
|
8
8
|
export { BrowserBridge, BrowserBridge as PlaywrightMCP } from './mcp.js';
|
|
9
|
+
export { CDPBridge } from './cdp.js';
|
|
9
10
|
export { isDaemonRunning } from './daemon-client.js';
|
|
10
11
|
import { extractTabEntries, diffTabIndexes, appendLimited } from './tabs.js';
|
|
11
12
|
import { withTimeoutMs } from '../runtime.js';
|
|
@@ -14,4 +15,6 @@ export declare const __test__: {
|
|
|
14
15
|
diffTabIndexes: typeof diffTabIndexes;
|
|
15
16
|
appendLimited: typeof appendLimited;
|
|
16
17
|
withTimeoutMs: typeof withTimeoutMs;
|
|
18
|
+
selectCDPTarget: (targets: import("./cdp.js").CDPTarget[]) => import("./cdp.js").CDPTarget | undefined;
|
|
19
|
+
scoreCDPTarget: (target: import("./cdp.js").CDPTarget, preferredPattern?: RegExp) => number;
|
|
17
20
|
};
|
package/dist/browser/index.js
CHANGED
|
@@ -6,12 +6,16 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export { Page } from './page.js';
|
|
8
8
|
export { BrowserBridge, BrowserBridge as PlaywrightMCP } from './mcp.js';
|
|
9
|
+
export { CDPBridge } from './cdp.js';
|
|
9
10
|
export { isDaemonRunning } from './daemon-client.js';
|
|
10
11
|
import { extractTabEntries, diffTabIndexes, appendLimited } from './tabs.js';
|
|
12
|
+
import { __test__ as cdpTest } from './cdp.js';
|
|
11
13
|
import { withTimeoutMs } from '../runtime.js';
|
|
12
14
|
export const __test__ = {
|
|
13
15
|
extractTabEntries,
|
|
14
16
|
diffTabIndexes,
|
|
15
17
|
appendLimited,
|
|
16
18
|
withTimeoutMs,
|
|
19
|
+
selectCDPTarget: cdpTest.selectCDPTarget,
|
|
20
|
+
scoreCDPTarget: cdpTest.scoreCDPTarget,
|
|
17
21
|
};
|
package/dist/browser/mcp.d.ts
CHANGED
package/dist/browser/mcp.js
CHANGED
|
@@ -29,8 +29,8 @@ export class BrowserBridge {
|
|
|
29
29
|
throw new Error('Session is closed');
|
|
30
30
|
this._state = 'connecting';
|
|
31
31
|
try {
|
|
32
|
-
await this._ensureDaemon();
|
|
33
|
-
this._page = new Page();
|
|
32
|
+
await this._ensureDaemon(opts.timeout);
|
|
33
|
+
this._page = new Page(opts.workspace);
|
|
34
34
|
this._state = 'connected';
|
|
35
35
|
return this._page;
|
|
36
36
|
}
|
|
@@ -48,9 +48,14 @@ export class BrowserBridge {
|
|
|
48
48
|
this._page = null;
|
|
49
49
|
this._state = 'closed';
|
|
50
50
|
}
|
|
51
|
-
async _ensureDaemon() {
|
|
52
|
-
|
|
51
|
+
async _ensureDaemon(timeoutSeconds) {
|
|
52
|
+
const timeoutMs = Math.max(1, timeoutSeconds ?? Math.ceil(DAEMON_SPAWN_TIMEOUT / 1000)) * 1000;
|
|
53
|
+
if (await isExtensionConnected())
|
|
53
54
|
return;
|
|
55
|
+
if (await isDaemonRunning()) {
|
|
56
|
+
throw new Error('Daemon is running but the Browser Extension is not connected.\n' +
|
|
57
|
+
'Please install and enable the opencli Browser Bridge extension in Chrome.');
|
|
58
|
+
}
|
|
54
59
|
// Find daemon relative to this file — works for both:
|
|
55
60
|
// npx tsx src/main.ts → src/browser/mcp.ts → src/daemon.ts
|
|
56
61
|
// node dist/main.js → dist/browser/mcp.js → dist/daemon.js
|
|
@@ -75,7 +80,7 @@ export class BrowserBridge {
|
|
|
75
80
|
});
|
|
76
81
|
this._daemonProc.unref();
|
|
77
82
|
// Wait for daemon to be ready AND extension to connect
|
|
78
|
-
const deadline = Date.now() +
|
|
83
|
+
const deadline = Date.now() + timeoutMs;
|
|
79
84
|
while (Date.now() < deadline) {
|
|
80
85
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
81
86
|
if (await isExtensionConnected())
|
package/dist/browser/page.d.ts
CHANGED
|
@@ -14,14 +14,21 @@ import type { IPage } from '../types.js';
|
|
|
14
14
|
* Page — implements IPage by talking to the daemon via HTTP.
|
|
15
15
|
*/
|
|
16
16
|
export declare class Page implements IPage {
|
|
17
|
+
private readonly workspace;
|
|
18
|
+
constructor(workspace?: string);
|
|
17
19
|
/** Active tab ID, set after navigate and used in all subsequent commands */
|
|
18
20
|
private _tabId;
|
|
19
21
|
/** Helper: spread tabId into command params if we have one */
|
|
20
22
|
private _tabOpt;
|
|
23
|
+
private _workspaceOpt;
|
|
21
24
|
goto(url: string): Promise<void>;
|
|
22
25
|
/** Close the automation window in the extension */
|
|
23
26
|
closeWindow(): Promise<void>;
|
|
24
27
|
evaluate(js: string): Promise<any>;
|
|
28
|
+
getCookies(opts?: {
|
|
29
|
+
domain?: string;
|
|
30
|
+
url?: string;
|
|
31
|
+
}): Promise<any[]>;
|
|
25
32
|
snapshot(opts?: {
|
|
26
33
|
interactive?: boolean;
|
|
27
34
|
compact?: boolean;
|