@jackwener/opencli 1.0.5 → 1.1.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/CHANGELOG.md +8 -0
- package/README.md +36 -10
- package/README.zh-CN.md +3 -0
- package/SKILL.md +7 -2
- package/dist/bilibili.js +4 -2
- package/dist/cli-manifest.json +506 -6
- package/dist/cli.js +51 -1
- package/dist/clis/antigravity/serve.js +296 -47
- package/dist/clis/arxiv/paper.d.ts +1 -0
- package/dist/clis/arxiv/paper.js +21 -0
- package/dist/clis/arxiv/search.d.ts +1 -0
- package/dist/clis/arxiv/search.js +24 -0
- package/dist/clis/arxiv/utils.d.ts +18 -0
- package/dist/clis/arxiv/utils.js +49 -0
- package/dist/clis/boss/batchgreet.d.ts +1 -0
- package/dist/clis/boss/batchgreet.js +147 -0
- package/dist/clis/boss/exchange.d.ts +1 -0
- package/dist/clis/boss/exchange.js +111 -0
- package/dist/clis/boss/greet.d.ts +1 -0
- package/dist/clis/boss/greet.js +175 -0
- package/dist/clis/boss/invite.d.ts +1 -0
- package/dist/clis/boss/invite.js +158 -0
- package/dist/clis/boss/joblist.d.ts +1 -0
- package/dist/clis/boss/joblist.js +55 -0
- package/dist/clis/boss/mark.d.ts +1 -0
- package/dist/clis/boss/mark.js +141 -0
- package/dist/clis/boss/recommend.d.ts +1 -0
- package/dist/clis/boss/recommend.js +83 -0
- package/dist/clis/boss/stats.d.ts +1 -0
- package/dist/clis/boss/stats.js +116 -0
- package/dist/clis/sinafinance/news.d.ts +7 -0
- package/dist/clis/sinafinance/news.js +61 -0
- package/dist/clis/wikipedia/search.d.ts +1 -0
- package/dist/clis/wikipedia/search.js +30 -0
- package/dist/clis/wikipedia/summary.d.ts +1 -0
- package/dist/clis/wikipedia/summary.js +28 -0
- package/dist/clis/wikipedia/utils.d.ts +8 -0
- package/dist/clis/wikipedia/utils.js +18 -0
- package/dist/clis/xiaohongshu/creator-note-detail.d.ts +64 -5
- package/dist/clis/xiaohongshu/creator-note-detail.js +258 -69
- package/dist/clis/xiaohongshu/creator-note-detail.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/creator-note-detail.test.js +211 -0
- package/dist/clis/xiaohongshu/creator-notes-summary.d.ts +28 -0
- package/dist/clis/xiaohongshu/creator-notes-summary.js +92 -0
- package/dist/clis/xiaohongshu/creator-notes-summary.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/creator-notes-summary.test.js +49 -0
- package/dist/clis/xiaohongshu/creator-notes.d.ts +18 -5
- package/dist/clis/xiaohongshu/creator-notes.js +159 -71
- package/dist/clis/xiaohongshu/creator-notes.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/creator-notes.test.js +162 -0
- package/dist/external.d.ts +20 -0
- package/dist/external.js +159 -0
- package/docs/.vitepress/config.mts +1 -0
- package/docs/public/CNAME +1 -0
- package/package.json +1 -1
- package/src/bilibili.ts +4 -2
- package/src/browser/cdp.ts +3 -3
- package/src/cli.ts +56 -1
- package/src/clis/antigravity/README.md +3 -46
- package/src/clis/antigravity/serve.ts +323 -50
- package/src/clis/arxiv/paper.ts +21 -0
- package/src/clis/arxiv/search.ts +24 -0
- package/src/clis/arxiv/utils.ts +63 -0
- package/src/clis/boss/batchgreet.ts +167 -0
- package/src/clis/boss/exchange.ts +126 -0
- package/src/clis/boss/greet.ts +198 -0
- package/src/clis/boss/invite.ts +177 -0
- package/src/clis/boss/joblist.ts +63 -0
- package/src/clis/boss/mark.ts +155 -0
- package/src/clis/boss/recommend.ts +94 -0
- package/src/clis/boss/stats.ts +130 -0
- package/src/clis/chaoxing/README.md +2 -24
- package/src/clis/chatgpt/README.md +3 -42
- package/src/clis/chatwise/README.md +3 -36
- package/src/clis/codex/README.md +3 -32
- package/src/clis/cursor/README.md +3 -31
- package/src/clis/discord-app/README.md +2 -25
- package/src/clis/feishu/README.md +2 -17
- package/src/clis/neteasemusic/README.md +3 -29
- package/src/clis/notion/README.md +2 -26
- package/src/clis/sinafinance/news.ts +76 -0
- package/src/clis/wechat/README.md +2 -25
- package/src/clis/wikipedia/search.ts +32 -0
- package/src/clis/wikipedia/summary.ts +28 -0
- package/src/clis/wikipedia/utils.ts +20 -0
- package/src/clis/xiaohongshu/creator-note-detail.test.ts +223 -0
- package/src/clis/xiaohongshu/creator-note-detail.ts +340 -72
- package/src/clis/xiaohongshu/creator-notes-summary.test.ts +54 -0
- package/src/clis/xiaohongshu/creator-notes-summary.ts +120 -0
- package/src/clis/xiaohongshu/creator-notes.test.ts +178 -0
- package/src/clis/xiaohongshu/creator-notes.ts +215 -75
- package/src/daemon.ts +3 -3
- package/src/external-clis.yaml +39 -0
- package/src/external.ts +182 -0
- package/CDP.md +0 -103
- package/CDP.zh-CN.md +0 -103
- package/CLI-ELECTRON.md +0 -125
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
- name: gh
|
|
2
|
+
binary: gh
|
|
3
|
+
description: "GitHub CLI — repos, PRs, issues, releases, gists"
|
|
4
|
+
homepage: "https://cli.github.com"
|
|
5
|
+
tags: [github, git, dev]
|
|
6
|
+
install:
|
|
7
|
+
mac: "brew install gh"
|
|
8
|
+
|
|
9
|
+
- name: obsidian
|
|
10
|
+
binary: obsidian
|
|
11
|
+
description: "Obsidian vault management — notes, search, tags, tasks, sync"
|
|
12
|
+
homepage: "https://obsidian.md/help/cli"
|
|
13
|
+
tags: [notes, knowledge, markdown]
|
|
14
|
+
install:
|
|
15
|
+
mac: "brew install --cask obsidian"
|
|
16
|
+
|
|
17
|
+
- name: readwise
|
|
18
|
+
binary: readwise
|
|
19
|
+
description: "Readwise & Reader CLI — highlights, annotations, reading list"
|
|
20
|
+
homepage: "https://github.com/readwiseio/readwise-cli"
|
|
21
|
+
tags: [reading, highlights]
|
|
22
|
+
install:
|
|
23
|
+
default: "npm install -g @readwiseio/readwise-cli"
|
|
24
|
+
|
|
25
|
+
- name: kubectl
|
|
26
|
+
binary: kubectl
|
|
27
|
+
description: "Kubernetes command-line tool"
|
|
28
|
+
homepage: "https://kubernetes.io/docs/reference/kubectl/"
|
|
29
|
+
tags: [kubernetes, k8s, devops]
|
|
30
|
+
install:
|
|
31
|
+
mac: "brew install kubectl"
|
|
32
|
+
|
|
33
|
+
- name: docker
|
|
34
|
+
binary: docker
|
|
35
|
+
description: "Docker command-line interface"
|
|
36
|
+
homepage: "https://docs.docker.com/engine/reference/commandline/cli/"
|
|
37
|
+
tags: [docker, containers, devops]
|
|
38
|
+
install:
|
|
39
|
+
mac: "brew install --cask docker"
|
package/src/external.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { spawnSync, execSync } from 'node:child_process';
|
|
6
|
+
import yaml from 'js-yaml';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { log } from './logger.js';
|
|
9
|
+
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
|
|
12
|
+
export interface ExternalCliInstall {
|
|
13
|
+
mac?: string;
|
|
14
|
+
linux?: string;
|
|
15
|
+
windows?: string;
|
|
16
|
+
default?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ExternalCliConfig {
|
|
20
|
+
name: string;
|
|
21
|
+
binary: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
homepage?: string;
|
|
24
|
+
tags?: string[];
|
|
25
|
+
install?: ExternalCliInstall;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getUserRegistryPath(): string {
|
|
29
|
+
const home = os.homedir();
|
|
30
|
+
return path.join(home, '.opencli', 'external-clis.yaml');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function loadExternalClis(): ExternalCliConfig[] {
|
|
34
|
+
const configs = new Map<string, ExternalCliConfig>();
|
|
35
|
+
|
|
36
|
+
// 1. Load built-in
|
|
37
|
+
const builtinPath = path.resolve(__dirname, 'external-clis.yaml');
|
|
38
|
+
try {
|
|
39
|
+
if (fs.existsSync(builtinPath)) {
|
|
40
|
+
const raw = fs.readFileSync(builtinPath, 'utf8');
|
|
41
|
+
const parsed = (yaml.load(raw) || []) as ExternalCliConfig[];
|
|
42
|
+
for (const item of parsed) configs.set(item.name, item);
|
|
43
|
+
}
|
|
44
|
+
} catch (err: any) {
|
|
45
|
+
log.warn(`Failed to parse built-in external-clis.yaml: ${err.message}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 2. Load user custom
|
|
49
|
+
const userPath = getUserRegistryPath();
|
|
50
|
+
try {
|
|
51
|
+
if (fs.existsSync(userPath)) {
|
|
52
|
+
const raw = fs.readFileSync(userPath, 'utf8');
|
|
53
|
+
const parsed = (yaml.load(raw) || []) as ExternalCliConfig[];
|
|
54
|
+
for (const item of parsed) {
|
|
55
|
+
configs.set(item.name, item); // Overwrite built-in if duplicated
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch (err: any) {
|
|
59
|
+
log.warn(`Failed to parse user external-clis.yaml: ${err.message}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return Array.from(configs.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function isBinaryInstalled(binary: string): boolean {
|
|
66
|
+
try {
|
|
67
|
+
const isWindows = os.platform() === 'win32';
|
|
68
|
+
const cmd = isWindows ? 'where' : 'command -v';
|
|
69
|
+
execSync(`${cmd} ${binary}`, { stdio: 'ignore' });
|
|
70
|
+
return true;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getInstallCmd(installConfig?: ExternalCliInstall): string | null {
|
|
77
|
+
if (!installConfig) return null;
|
|
78
|
+
const platform = os.platform();
|
|
79
|
+
if (platform === 'darwin' && installConfig.mac) return installConfig.mac;
|
|
80
|
+
if (platform === 'linux' && installConfig.linux) return installConfig.linux;
|
|
81
|
+
if (platform === 'win32' && installConfig.windows) return installConfig.windows;
|
|
82
|
+
if (installConfig.default) return installConfig.default;
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function installExternalCli(cli: ExternalCliConfig): boolean {
|
|
87
|
+
if (!cli.install) {
|
|
88
|
+
console.error(chalk.red(`No auto-install command configured for '${cli.name}'.`));
|
|
89
|
+
console.error(`Please install '${cli.binary}' manually.`);
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const cmd = getInstallCmd(cli.install);
|
|
94
|
+
if (!cmd) {
|
|
95
|
+
console.error(chalk.red(`No install command for your platform (${os.platform()}) for '${cli.name}'.`));
|
|
96
|
+
if (cli.homepage) console.error(`See: ${cli.homepage}`);
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log(chalk.cyan(`🔹 '${cli.name}' is not installed. Auto-installing...`));
|
|
101
|
+
console.log(chalk.dim(`$ ${cmd}`));
|
|
102
|
+
try {
|
|
103
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
104
|
+
console.log(chalk.green(`✅ Installed '${cli.name}' successfully.\n`));
|
|
105
|
+
return true;
|
|
106
|
+
} catch (err: any) {
|
|
107
|
+
console.error(chalk.red(`❌ Failed to install '${cli.name}': ${err.message}`));
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function executeExternalCli(name: string, args: string[]): Promise<void> {
|
|
113
|
+
const configs = loadExternalClis();
|
|
114
|
+
const cli = configs.find((c) => c.name === name);
|
|
115
|
+
if (!cli) {
|
|
116
|
+
throw new Error(`External CLI '${name}' not found in registry.`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 1. Check if installed
|
|
120
|
+
if (!isBinaryInstalled(cli.binary)) {
|
|
121
|
+
// 2. Try to auto install
|
|
122
|
+
const success = installExternalCli(cli);
|
|
123
|
+
if (!success) {
|
|
124
|
+
process.exitCode = 1;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 3. Passthrough execution
|
|
130
|
+
// We use spawnSync to properly inherit stdio and block until completion
|
|
131
|
+
const result = spawnSync(cli.binary, args, { stdio: 'inherit' });
|
|
132
|
+
if (result.error) {
|
|
133
|
+
console.error(chalk.red(`Failed to execute '${cli.binary}': ${result.error.message}`));
|
|
134
|
+
process.exitCode = 1;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (result.status !== null) {
|
|
139
|
+
process.exitCode = result.status;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function registerExternalCli(name: string, binary?: string, install?: string, description?: string): void {
|
|
144
|
+
const userPath = getUserRegistryPath();
|
|
145
|
+
const configDir = path.dirname(userPath);
|
|
146
|
+
|
|
147
|
+
if (!fs.existsSync(configDir)) {
|
|
148
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let items: ExternalCliConfig[] = [];
|
|
152
|
+
if (fs.existsSync(userPath)) {
|
|
153
|
+
try {
|
|
154
|
+
const raw = fs.readFileSync(userPath, 'utf8');
|
|
155
|
+
items = (yaml.load(raw) || []) as ExternalCliConfig[];
|
|
156
|
+
} catch {
|
|
157
|
+
// Ignore
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const existingIndex = items.findIndex((c) => c.name === name);
|
|
162
|
+
|
|
163
|
+
const newItem: ExternalCliConfig = {
|
|
164
|
+
name,
|
|
165
|
+
binary: binary || name,
|
|
166
|
+
};
|
|
167
|
+
if (description) newItem.description = description;
|
|
168
|
+
if (install) newItem.install = { default: install };
|
|
169
|
+
|
|
170
|
+
if (existingIndex >= 0) {
|
|
171
|
+
// Merge
|
|
172
|
+
items[existingIndex] = { ...items[existingIndex], ...newItem };
|
|
173
|
+
console.log(chalk.green(`Updated '${name}' in user registry.`));
|
|
174
|
+
} else {
|
|
175
|
+
items.push(newItem);
|
|
176
|
+
console.log(chalk.green(`Registered '${name}' in user registry.`));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const dump = yaml.dump(items, { indent: 2, sortKeys: true });
|
|
180
|
+
fs.writeFileSync(userPath, dump, 'utf8');
|
|
181
|
+
console.log(chalk.dim(userPath));
|
|
182
|
+
}
|
package/CDP.md
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
# Connecting OpenCLI via CDP (Remote/Headless Servers)
|
|
2
|
-
|
|
3
|
-
If you cannot use the opencli Browser Bridge extension (e.g., in a remote headless server environment without a UI), OpenCLI provides an alternative: connecting directly to Chrome via **CDP (Chrome DevTools Protocol)**.
|
|
4
|
-
|
|
5
|
-
Because CDP binds to `localhost` by default for security reasons, accessing it from a remote server requires an additional networking tunnel.
|
|
6
|
-
|
|
7
|
-
This guide is broken down into three phases:
|
|
8
|
-
1. **Preparation**: Start Chrome with CDP enabled locally.
|
|
9
|
-
2. **Network Tunnels**: Expose that CDP port to your remote server using either **SSH Tunnels** or **Reverse Proxies**.
|
|
10
|
-
3. **Execution**: Run OpenCLI on your server.
|
|
11
|
-
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
## Phase 1: Preparation (Local Machine)
|
|
15
|
-
|
|
16
|
-
First, you need to start a Chrome browser on your local machine with remote debugging enabled.
|
|
17
|
-
|
|
18
|
-
**macOS:**
|
|
19
|
-
```bash
|
|
20
|
-
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
|
|
21
|
-
--remote-debugging-port=9222 \
|
|
22
|
-
--user-data-dir="$HOME/chrome-debug-profile" \
|
|
23
|
-
--remote-allow-origins="*"
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
**Linux:**
|
|
27
|
-
```bash
|
|
28
|
-
google-chrome \
|
|
29
|
-
--remote-debugging-port=9222 \
|
|
30
|
-
--user-data-dir="$HOME/chrome-debug-profile" \
|
|
31
|
-
--remote-allow-origins="*"
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
**Windows:**
|
|
35
|
-
```cmd
|
|
36
|
-
"C:\Program Files\Google\Chrome\Application\chrome.exe" ^
|
|
37
|
-
--remote-debugging-port=9222 ^
|
|
38
|
-
--user-data-dir="%USERPROFILE%\chrome-debug-profile" ^
|
|
39
|
-
--remote-allow-origins="*"
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
> **Note**: The `--remote-allow-origins="*"` flag is often required for modern Chrome versions to accept cross-origin CDP WebSocket connections (e.g. from reverse proxies like ngrok).
|
|
43
|
-
|
|
44
|
-
Once this browser instance opens, **log into the target websites you want to use** (e.g., bilibili.com, zhihu.com) so that the session contains the correct cookies.
|
|
45
|
-
|
|
46
|
-
---
|
|
47
|
-
|
|
48
|
-
## Phase 2: Remote Access Methods
|
|
49
|
-
|
|
50
|
-
Once CDP is running locally on port `9222`, you must securely expose this port to your remote server. Choose one of the two methods below depending on your network conditions.
|
|
51
|
-
|
|
52
|
-
### Method A: SSH Tunnel (Recommended)
|
|
53
|
-
|
|
54
|
-
If your local machine has SSH access to the remote server, this is the most secure and straightforward method.
|
|
55
|
-
|
|
56
|
-
Run this command on your **Local Machine** to forward the remote server's port `9222` back to your local port `9222`:
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
ssh -R 9222:localhost:9222 your-server-user@your-server-ip
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
Leave this SSH session running in the background.
|
|
63
|
-
|
|
64
|
-
### Method B: Reverse Proxy (ngrok / frp / socat)
|
|
65
|
-
|
|
66
|
-
If you cannot establish a direct SSH connection (e.g., due to NAT or firewalls), you can use an intranet penetration tool like `ngrok`.
|
|
67
|
-
|
|
68
|
-
Run this command on your **Local Machine** to expose your local port `9222` to the public internet securely via ngrok:
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
ngrok http 9222
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
This will print a forwarding URL, such as `https://abcdef.ngrok.app`. **Copy this URL**.
|
|
75
|
-
|
|
76
|
-
---
|
|
77
|
-
|
|
78
|
-
## Phase 3: Execution (Remote Server)
|
|
79
|
-
|
|
80
|
-
Now switch to your **Remote Server** where OpenCLI is installed.
|
|
81
|
-
|
|
82
|
-
Depending on the network tunnel method you chose in Phase 2, set the `OPENCLI_CDP_ENDPOINT` environment variable and run your commands.
|
|
83
|
-
|
|
84
|
-
### If you used Method A (SSH Tunnel):
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
export OPENCLI_CDP_ENDPOINT="http://localhost:9222"
|
|
88
|
-
opencli doctor # Verify connection
|
|
89
|
-
opencli bilibili hot --limit 5 # Test a command
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
### If you used Method B (Reverse Proxy like ngrok):
|
|
93
|
-
|
|
94
|
-
```bash
|
|
95
|
-
# Use the URL you copied from ngrok earlier
|
|
96
|
-
export OPENCLI_CDP_ENDPOINT="https://abcdef.ngrok.app"
|
|
97
|
-
opencli doctor # Verify connection
|
|
98
|
-
opencli bilibili hot --limit 5 # Test a command
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
> *Tip: If you provide a standard HTTP/HTTPS CDP endpoint, OpenCLI requests the `/json` target list and picks the most likely inspectable app/page target automatically. If multiple app targets exist, you can further narrow selection with `OPENCLI_CDP_TARGET` (for example `antigravity` or `codex`).*
|
|
102
|
-
|
|
103
|
-
If you plan to use this setup frequently, you can persist the environment variable by adding the `export` line to your `~/.bashrc` or `~/.zshrc` on the server.
|
package/CDP.zh-CN.md
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
# 通过 CDP 远程连接 OpenCLI (服务器/无头环境)
|
|
2
|
-
|
|
3
|
-
如果你无法使用 opencli Browser Bridge 浏览器扩展(例如:在无界面的远程服务器上运行 OpenCLI 时),OpenCLI 提供了备选方案:通过连接 **CDP (Chrome DevTools Protocol,即 Chrome 开发者工具协议)** 来直接控制本地 Chrome。
|
|
4
|
-
|
|
5
|
-
出于安全考虑,CDP 默认仅绑定在 `localhost` 的本地端口。所以,若是想让**远程服务器**调用本地的 CDP 服务,我们需要依靠一层额外的网络隧道。
|
|
6
|
-
|
|
7
|
-
本指南将整个过程拆分为三个阶段:
|
|
8
|
-
1. **阶段一:准备工作**(在本地启动允许 CDP 调试的 Chrome)。
|
|
9
|
-
2. **阶段二:建立网络隧道**(通过 **SSH反向隧道** 或 **反向代理工具**,将本地的 CDP 端口暴露给服务器)。
|
|
10
|
-
3. **阶段三:执行命令**(在服务器端运行 OpenCLI)。
|
|
11
|
-
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
## 阶段一:准备工作 (本地电脑)
|
|
15
|
-
|
|
16
|
-
首先,你需要在你的本地电脑上,通过命令行参数启动一个开启了远程调试端口的 Chrome 实例。
|
|
17
|
-
|
|
18
|
-
**macOS:**
|
|
19
|
-
```bash
|
|
20
|
-
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
|
|
21
|
-
--remote-debugging-port=9222 \
|
|
22
|
-
--user-data-dir="$HOME/chrome-debug-profile" \
|
|
23
|
-
--remote-allow-origins="*"
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
**Linux:**
|
|
27
|
-
```bash
|
|
28
|
-
google-chrome \
|
|
29
|
-
--remote-debugging-port=9222 \
|
|
30
|
-
--user-data-dir="$HOME/chrome-debug-profile" \
|
|
31
|
-
--remote-allow-origins="*"
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
**Windows:**
|
|
35
|
-
```cmd
|
|
36
|
-
"C:\Program Files\Google\Chrome\Application\chrome.exe" ^
|
|
37
|
-
--remote-debugging-port=9222 ^
|
|
38
|
-
--user-data-dir="%USERPROFILE%\chrome-debug-profile" ^
|
|
39
|
-
--remote-allow-origins="*"
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
> **注意**:此处增加的 `--remote-allow-origins="*"` 参数对于较新版本的 Chrome 来说通常是[必需的],以允许来自反向代理(如 ngrok)的跨域 WebSocket 连接请求。
|
|
43
|
-
|
|
44
|
-
待这个新的浏览器实例打开后,**手工登录那些你打算使用的网站**(如 bilibili.com、zhihu.com 等),这可以让该浏览器的运行资料(Profile)保留上这些网站登录用的 Cookie。
|
|
45
|
-
|
|
46
|
-
---
|
|
47
|
-
|
|
48
|
-
## 阶段二:建立网络隧道
|
|
49
|
-
|
|
50
|
-
现在你的本地已经有了一个监听在 `9222` 端口的 CDP 服务,接下来,选择以下任意一种方式将其实际暴露给你的远端服务器。
|
|
51
|
-
|
|
52
|
-
### 方法 A:SSH 反向端口转发 (推荐)
|
|
53
|
-
|
|
54
|
-
如果你的本地电脑可以直连远程服务器的 SSH,那么这是最简单且最安全的做法。
|
|
55
|
-
|
|
56
|
-
在你的 **本地电脑** 终端上直接运行这条 ssh 命令,将远程服务器的 `9222` 端口反向映射回本地的 `9222` 端口:
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
ssh -R 9222:localhost:9222 your-server-user@your-server-ip
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
保持此 SSH 会话在后台运行即可。
|
|
63
|
-
|
|
64
|
-
### 方法 B:反向代理 / 内网穿透 (ngrok / frp / socat)
|
|
65
|
-
|
|
66
|
-
如果因为 NAT 或防火墙等因素导致无法直连 SSH 服务器,你可以使用 `ngrok` 等内网穿透工具。
|
|
67
|
-
|
|
68
|
-
在 **本地电脑** 运行 ngrok 将本地的 `9222` 端口暴露到公网:
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
ngrok http 9222
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
此时终端里会打印出一段专属的转发 URL 地址(如:`https://abcdef.ngrok.app`)。**复制这一段 URL 地址备用**。
|
|
75
|
-
|
|
76
|
-
---
|
|
77
|
-
|
|
78
|
-
## 阶段三:执行命令 (远程服务器)
|
|
79
|
-
|
|
80
|
-
现在,所有的准备工作已结束。请切换到你已安装好 OpenCLI 的 **远程服务器** 终端上。
|
|
81
|
-
|
|
82
|
-
根据你在上方阶段二所选择的隧道方案,在终端中配置对应的 `OPENCLI_CDP_ENDPOINT` 环境变量:
|
|
83
|
-
|
|
84
|
-
### 若使用 方法 A (SSH 反向隧道):
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
export OPENCLI_CDP_ENDPOINT="http://localhost:9222"
|
|
88
|
-
opencli doctor # 查看并验证连接是否通畅
|
|
89
|
-
opencli bilibili hot --limit 5 # 执行目标命令
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
### 若使用 方法 B (Ngrok 等反向代理):
|
|
93
|
-
|
|
94
|
-
```bash
|
|
95
|
-
# 将刚刚使用 ngrok 得到的地址填入这里
|
|
96
|
-
export OPENCLI_CDP_ENDPOINT="https://abcdef.ngrok.app"
|
|
97
|
-
opencli doctor # 查看并验证连接是否通畅
|
|
98
|
-
opencli bilibili hot --limit 5 # 执行目标命令
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
> *Tip: 如果你填写的是一个普通 HTTP/HTTPS 的 CDP 地址,OpenCLI 会自动请求 `/json` target 列表,并挑选最可能的 app/page target;如果同一个端口下暴露了多个应用 target,还可以通过 `OPENCLI_CDP_TARGET`(例如 `antigravity`、`codex`)进一步缩小匹配范围。*
|
|
102
|
-
|
|
103
|
-
如果你想在此服务器上永久启用该配置,可以将对应的 `export` 语句追加进入你的 `~/.bashrc` 或 `~/.zshrc` 配置文件中。
|
package/CLI-ELECTRON.md
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: How to CLI-ify and automate any Electron Desktop Application via CDP
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# CLI-ifying Electron Applications (Skill Guide)
|
|
6
|
-
|
|
7
|
-
Based on the successful automation of **Cursor**, **Codex**, **Antigravity**, **ChatWise**, **Notion**, and **Discord** desktop apps, this guide serves as the standard operating procedure (SOP) for adapting ANY Electron-based application into an OpenCLI adapter.
|
|
8
|
-
|
|
9
|
-
## Core Concept
|
|
10
|
-
|
|
11
|
-
Electron apps are essentially local Chromium browser instances. By exposing a debugging port (CDP — Chrome DevTools Protocol) at launch time, we can use the Browser Bridge to pierce through the UI layer, accessing and controlling all underlying state including React/Vue components and Shadow DOM.
|
|
12
|
-
|
|
13
|
-
> **Note:** Not all desktop apps are Electron. WeChat (native Cocoa) and Feishu/Lark (custom Lark Framework) embed Chromium but do NOT expose CDP. For those apps, use the AppleScript + clipboard approach instead (see [Non-Electron Pattern](#non-electron-pattern-applescript)).
|
|
14
|
-
|
|
15
|
-
### Launching the Target App
|
|
16
|
-
```bash
|
|
17
|
-
/Applications/AppName.app/Contents/MacOS/AppName --remote-debugging-port=9222
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
### Verifying Electron
|
|
21
|
-
```bash
|
|
22
|
-
# Check for Electron Framework in the app bundle
|
|
23
|
-
ls /Applications/AppName.app/Contents/Frameworks/Electron\ Framework.framework
|
|
24
|
-
# If this directory exists → Electron → CDP works
|
|
25
|
-
# If not → check for libEGL.dylib (embedded Chromium/CEF, CDP may not work)
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## The 5-Command Pattern (CDP / Electron)
|
|
29
|
-
|
|
30
|
-
Every new Electron adapter should implement these 5 commands in `src/clis/<app_name>/`:
|
|
31
|
-
|
|
32
|
-
### 1. `status.ts` — Connection Test
|
|
33
|
-
```typescript
|
|
34
|
-
export const statusCommand = cli({
|
|
35
|
-
site: 'myapp',
|
|
36
|
-
name: 'status',
|
|
37
|
-
domain: 'localhost',
|
|
38
|
-
strategy: Strategy.UI,
|
|
39
|
-
browser: true, // Requires CDP connection
|
|
40
|
-
args: [],
|
|
41
|
-
columns: ['Status', 'Url', 'Title'],
|
|
42
|
-
func: async (page: IPage) => {
|
|
43
|
-
const url = await page.evaluate('window.location.href');
|
|
44
|
-
const title = await page.evaluate('document.title');
|
|
45
|
-
return [{ Status: 'Connected', Url: url, Title: title }];
|
|
46
|
-
},
|
|
47
|
-
});
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
### 2. `dump.ts` — Reverse Engineering Core
|
|
51
|
-
Modern app DOMs are huge and obfuscated. **Never guess selectors.** Dump first, then extract precise class names with AI or `grep`:
|
|
52
|
-
```typescript
|
|
53
|
-
const dom = await page.evaluate('document.body.innerHTML');
|
|
54
|
-
fs.writeFileSync('/tmp/app-dom.html', dom);
|
|
55
|
-
const snap = await page.snapshot({ interactive: false });
|
|
56
|
-
fs.writeFileSync('/tmp/app-snapshot.json', JSON.stringify(snap, null, 2));
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
### 3. `send.ts` — Advanced Text Injection
|
|
60
|
-
Electron apps often use complex rich-text editors (Monaco, Lexical, ProseMirror). Setting `.value` directly is ignored by React state.
|
|
61
|
-
|
|
62
|
-
**Best practice:** Use `document.execCommand('insertText')` to perfectly simulate real user input, fully piercing React state:
|
|
63
|
-
```javascript
|
|
64
|
-
const composer = document.querySelector('[contenteditable="true"]');
|
|
65
|
-
composer.focus();
|
|
66
|
-
document.execCommand('insertText', false, 'Hello');
|
|
67
|
-
```
|
|
68
|
-
Then submit with `await page.pressKey('Enter')`.
|
|
69
|
-
|
|
70
|
-
### 4. `read.ts` — Context Extraction
|
|
71
|
-
Don't extract the entire page text. Use `dump.ts` output to find the real "conversation container":
|
|
72
|
-
- Look for semantic selectors: `[role="log"]`, `[data-testid="conversation"]`, `[data-content-search-turn-key]`
|
|
73
|
-
- Format output as Markdown — readable by both humans and LLMs
|
|
74
|
-
|
|
75
|
-
### 5. `new.ts` — Keyboard Shortcuts
|
|
76
|
-
Many GUI actions respond to native shortcuts rather than button clicks:
|
|
77
|
-
```typescript
|
|
78
|
-
const isMac = process.platform === 'darwin';
|
|
79
|
-
await page.pressKey(isMac ? 'Meta+N' : 'Control+N');
|
|
80
|
-
await page.wait(1); // Wait for re-render
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
## Environment Variable
|
|
84
|
-
```bash
|
|
85
|
-
export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9222"
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
## Non-Electron Pattern (AppleScript)
|
|
89
|
-
|
|
90
|
-
For native macOS apps (WeChat, Feishu) that don't expose CDP:
|
|
91
|
-
```typescript
|
|
92
|
-
export const statusCommand = cli({
|
|
93
|
-
site: 'myapp',
|
|
94
|
-
strategy: Strategy.PUBLIC,
|
|
95
|
-
browser: false, // No browser needed
|
|
96
|
-
func: async (page: IPage | null) => {
|
|
97
|
-
const output = execSync("osascript -e 'application \"MyApp\" is running'", { encoding: 'utf-8' }).trim();
|
|
98
|
-
return [{ Status: output === 'true' ? 'Running' : 'Stopped' }];
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
Core techniques:
|
|
104
|
-
- **status**: `osascript -e 'application "AppName" is running'`
|
|
105
|
-
- **send**: `pbcopy` → activate window → `Cmd+V` → `Enter`
|
|
106
|
-
- **read**: `Cmd+A` → `Cmd+C` → `pbpaste`
|
|
107
|
-
- **search**: Activate → `Cmd+F`/`Cmd+K` → `keystroke "query"`
|
|
108
|
-
|
|
109
|
-
## Pitfalls & Gotchas
|
|
110
|
-
|
|
111
|
-
1. **Port conflicts (EADDRINUSE)**: Only one app per port. Use unique ports: Codex=9222, ChatGPT=9224, Cursor=9226, ChatWise=9228, Notion=9230, Discord=9232
|
|
112
|
-
2. **IPage abstraction**: OpenCLI wraps the browser page as `IPage` (`src/types.ts`). Use `page.pressKey()` and `page.evaluate()`, NOT direct DOM APIs
|
|
113
|
-
3. **Timing**: Always add `await page.wait(0.5)` to `1.0` after DOM mutations. Returning too early disconnects prematurely
|
|
114
|
-
4. **AppleScript requires Accessibility**: Terminal app must be granted permission in System Settings → Privacy & Security → Accessibility
|
|
115
|
-
|
|
116
|
-
## Port Assignment Table
|
|
117
|
-
|
|
118
|
-
| App | Port | Mode |
|
|
119
|
-
|-----|------|------|
|
|
120
|
-
| Codex | 9222 | CDP |
|
|
121
|
-
| ChatGPT | 9224 | CDP / AppleScript |
|
|
122
|
-
| Cursor | 9226 | CDP |
|
|
123
|
-
| ChatWise | 9228 | CDP |
|
|
124
|
-
| Notion | 9230 | CDP |
|
|
125
|
-
| Discord App | 9232 | CDP |
|