@jackwener/opencli 0.1.0 → 0.1.2
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/CLI-CREATOR.md +594 -0
- package/README.md +124 -39
- package/README.zh-CN.md +151 -0
- package/SKILL.md +178 -102
- package/dist/bilibili.d.ts +6 -5
- package/dist/browser.d.ts +3 -1
- package/dist/browser.js +44 -2
- package/dist/cascade.d.ts +46 -0
- package/dist/cascade.js +180 -0
- package/dist/clis/bbc/news.js +42 -0
- package/dist/clis/bilibili/hot.yaml +38 -0
- package/dist/clis/boss/search.js +47 -0
- package/dist/clis/ctrip/search.d.ts +1 -0
- package/dist/clis/ctrip/search.js +62 -0
- package/dist/clis/hackernews/top.yaml +36 -0
- package/dist/clis/index.d.ts +10 -1
- package/dist/clis/index.js +19 -1
- package/dist/clis/reddit/hot.yaml +46 -0
- package/dist/clis/reuters/search.d.ts +1 -0
- package/dist/clis/reuters/search.js +52 -0
- package/dist/clis/smzdm/search.d.ts +1 -0
- package/dist/clis/smzdm/search.js +66 -0
- package/dist/clis/twitter/trending.yaml +40 -0
- package/dist/clis/v2ex/hot.yaml +25 -0
- package/dist/clis/v2ex/latest.yaml +25 -0
- package/dist/clis/v2ex/topic.yaml +27 -0
- package/dist/clis/weibo/hot.d.ts +1 -0
- package/dist/clis/weibo/hot.js +41 -0
- package/dist/clis/xiaohongshu/feed.yaml +32 -0
- package/dist/clis/xiaohongshu/notifications.yaml +38 -0
- package/dist/clis/xiaohongshu/search.d.ts +5 -0
- package/dist/clis/xiaohongshu/search.js +68 -0
- package/dist/clis/yahoo-finance/quote.d.ts +1 -0
- package/dist/clis/yahoo-finance/quote.js +74 -0
- package/dist/clis/youtube/search.d.ts +1 -0
- package/dist/clis/youtube/search.js +60 -0
- package/dist/clis/zhihu/hot.yaml +42 -0
- package/dist/clis/zhihu/question.d.ts +1 -0
- package/dist/clis/zhihu/question.js +39 -0
- package/dist/clis/zhihu/search.yaml +55 -0
- package/dist/engine.d.ts +2 -1
- package/dist/explore.d.ts +23 -13
- package/dist/explore.js +293 -422
- package/dist/generate.js +2 -1
- package/dist/main.js +21 -2
- package/dist/pipeline/executor.d.ts +9 -0
- package/dist/pipeline/executor.js +88 -0
- package/dist/pipeline/index.d.ts +5 -0
- package/dist/pipeline/index.js +5 -0
- package/dist/pipeline/steps/browser.d.ts +12 -0
- package/dist/pipeline/steps/browser.js +68 -0
- package/dist/pipeline/steps/fetch.d.ts +5 -0
- package/dist/pipeline/steps/fetch.js +50 -0
- package/dist/pipeline/steps/intercept.d.ts +5 -0
- package/dist/pipeline/steps/intercept.js +75 -0
- package/dist/pipeline/steps/tap.d.ts +12 -0
- package/dist/pipeline/steps/tap.js +130 -0
- package/dist/pipeline/steps/transform.d.ts +8 -0
- package/dist/pipeline/steps/transform.js +53 -0
- package/dist/pipeline/template.d.ts +16 -0
- package/dist/pipeline/template.js +115 -0
- package/dist/pipeline/template.test.d.ts +4 -0
- package/dist/pipeline/template.test.js +102 -0
- package/dist/pipeline/transform.test.d.ts +4 -0
- package/dist/pipeline/transform.test.js +90 -0
- package/dist/pipeline.d.ts +5 -7
- package/dist/pipeline.js +5 -313
- package/dist/registry.d.ts +3 -2
- package/dist/runtime.d.ts +2 -1
- package/dist/synthesize.d.ts +11 -8
- package/dist/synthesize.js +142 -118
- package/dist/types.d.ts +27 -0
- package/dist/types.js +7 -0
- package/package.json +9 -4
- package/src/bilibili.ts +9 -7
- package/src/browser.ts +41 -3
- package/src/cascade.ts +218 -0
- package/src/clis/bbc/news.ts +42 -0
- package/src/clis/boss/search.ts +47 -0
- package/src/clis/ctrip/search.ts +62 -0
- package/src/clis/index.ts +28 -1
- package/src/clis/reddit/hot.yaml +46 -0
- package/src/clis/reuters/search.ts +52 -0
- package/src/clis/smzdm/search.ts +66 -0
- package/src/clis/v2ex/hot.yaml +5 -9
- package/src/clis/v2ex/latest.yaml +5 -8
- package/src/clis/v2ex/topic.yaml +27 -0
- package/src/clis/weibo/hot.ts +41 -0
- package/src/clis/xiaohongshu/feed.yaml +32 -0
- package/src/clis/xiaohongshu/notifications.yaml +38 -0
- package/src/clis/xiaohongshu/search.ts +71 -0
- package/src/clis/yahoo-finance/quote.ts +74 -0
- package/src/clis/youtube/search.ts +60 -0
- package/src/clis/zhihu/hot.yaml +22 -8
- package/src/clis/zhihu/question.ts +45 -0
- package/src/clis/zhihu/search.yaml +55 -0
- package/src/engine.ts +2 -1
- package/src/explore.ts +303 -465
- package/src/generate.ts +3 -1
- package/src/main.ts +18 -2
- package/src/pipeline/executor.ts +98 -0
- package/src/pipeline/index.ts +6 -0
- package/src/pipeline/steps/browser.ts +67 -0
- package/src/pipeline/steps/fetch.ts +60 -0
- package/src/pipeline/steps/intercept.ts +78 -0
- package/src/pipeline/steps/tap.ts +137 -0
- package/src/pipeline/steps/transform.ts +50 -0
- package/src/pipeline/template.test.ts +107 -0
- package/src/pipeline/template.ts +101 -0
- package/src/pipeline/transform.test.ts +107 -0
- package/src/pipeline.ts +5 -292
- package/src/registry.ts +4 -2
- package/src/runtime.ts +3 -1
- package/src/synthesize.ts +142 -137
- package/src/types.ts +23 -0
- package/vitest.config.ts +7 -0
- package/dist/clis/github/search.js +0 -20
- package/dist/clis/zhihu/search.js +0 -58
- package/dist/promote.d.ts +0 -1
- package/dist/promote.js +0 -3
- package/dist/register.d.ts +0 -2
- package/dist/register.js +0 -2
- package/dist/scaffold.d.ts +0 -2
- package/dist/scaffold.js +0 -2
- package/dist/smoke.d.ts +0 -2
- package/dist/smoke.js +0 -2
- package/src/clis/github/search.ts +0 -21
- package/src/clis/github/trending.yaml +0 -58
- package/src/clis/zhihu/search.ts +0 -65
- package/src/promote.ts +0 -3
- package/src/register.ts +0 -2
- package/src/scaffold.ts +0 -2
- package/src/smoke.ts +0 -2
- /package/dist/clis/{github/search.d.ts → bbc/news.d.ts} +0 -0
- /package/dist/clis/{zhihu → boss}/search.d.ts +0 -0
package/SKILL.md
CHANGED
|
@@ -1,47 +1,47 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: opencli
|
|
3
|
-
description: "OpenCLI — Make any website your CLI. Zero
|
|
3
|
+
description: "OpenCLI — Make any website your CLI. Zero risk, AI-powered, reuse Chrome login."
|
|
4
4
|
version: 0.1.0
|
|
5
5
|
author: jackwener
|
|
6
|
-
tags: [cli, browser, web, mcp, playwright, bilibili, zhihu, twitter, github, v2ex, hackernews,
|
|
6
|
+
tags: [cli, browser, web, mcp, playwright, bilibili, zhihu, twitter, github, v2ex, hackernews, reddit, xiaohongshu, AI, agent]
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# OpenCLI
|
|
10
10
|
|
|
11
|
-
> Make any website your CLI.
|
|
11
|
+
> Make any website your CLI. Reuse Chrome login, zero risk, AI-powered discovery.
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## Install & Run
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
|
|
17
|
-
npm install
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
## 使用方式
|
|
16
|
+
# npm global install (recommended)
|
|
17
|
+
npm install -g @jackwener/opencli
|
|
18
|
+
opencli <command>
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
# Or from source
|
|
21
|
+
cd ~/code/opencli && npm install
|
|
24
22
|
npx tsx src/main.ts <command>
|
|
25
23
|
|
|
26
|
-
#
|
|
27
|
-
npm
|
|
24
|
+
# Update to latest
|
|
25
|
+
npm update -g @jackwener/opencli
|
|
28
26
|
```
|
|
29
27
|
|
|
30
|
-
##
|
|
28
|
+
## Prerequisites
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
1. Chrome
|
|
34
|
-
2.
|
|
35
|
-
3.
|
|
30
|
+
Browser commands require:
|
|
31
|
+
1. Chrome browser running **(logged into target sites)**
|
|
32
|
+
2. [Playwright MCP Bridge](https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm) extension
|
|
33
|
+
3. Configure `PLAYWRIGHT_MCP_EXTENSION_TOKEN` (from extension settings) in your MCP config
|
|
36
34
|
|
|
37
|
-
|
|
35
|
+
> **Note**: You must be logged into the target website in Chrome before running commands. Tabs opened during command execution are auto-closed afterwards.
|
|
38
36
|
|
|
39
|
-
|
|
37
|
+
Public API commands (`hackernews`, `github search`, `v2ex`) need no browser.
|
|
40
38
|
|
|
41
|
-
|
|
39
|
+
## Commands Reference
|
|
40
|
+
|
|
41
|
+
### Data Commands
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
# Bilibili
|
|
44
|
+
# Bilibili (browser)
|
|
45
45
|
opencli bilibili hot --limit 10 # B站热门视频
|
|
46
46
|
opencli bilibili search --keyword "rust" # 搜索视频
|
|
47
47
|
opencli bilibili me # 我的信息
|
|
@@ -50,70 +50,115 @@ opencli bilibili history --limit 20 # 观看历史
|
|
|
50
50
|
opencli bilibili feed --limit 10 # 动态时间线
|
|
51
51
|
opencli bilibili user-videos --uid 12345 # 用户投稿
|
|
52
52
|
|
|
53
|
-
# 知乎
|
|
53
|
+
# 知乎 (browser)
|
|
54
54
|
opencli zhihu hot --limit 10 # 知乎热榜
|
|
55
55
|
opencli zhihu search --keyword "AI" # 搜索
|
|
56
|
+
opencli zhihu question --id 34816524 # 问题详情和回答
|
|
57
|
+
|
|
58
|
+
# 小红书 (browser)
|
|
59
|
+
opencli xiaohongshu search --keyword "美食" # 搜索笔记
|
|
60
|
+
opencli xiaohongshu notifications # 通知(mentions/likes/connections)
|
|
61
|
+
opencli xiaohongshu feed --limit 10 # 推荐 Feed
|
|
56
62
|
|
|
57
|
-
# GitHub
|
|
63
|
+
# GitHub (trending=browser, search=public)
|
|
58
64
|
opencli github trending --limit 10 # GitHub Trending
|
|
59
|
-
opencli github search --keyword "cli" #
|
|
65
|
+
opencli github search --keyword "cli" # 搜索仓库
|
|
60
66
|
|
|
61
|
-
# Twitter/X
|
|
67
|
+
# Twitter/X (browser)
|
|
62
68
|
opencli twitter trending --limit 10 # 热门话题
|
|
63
69
|
|
|
64
|
-
#
|
|
70
|
+
# Reddit (browser)
|
|
71
|
+
opencli reddit hot --limit 10 # 热门帖子
|
|
72
|
+
opencli reddit hot --subreddit programming # 指定子版块
|
|
73
|
+
|
|
74
|
+
# V2EX (public)
|
|
65
75
|
opencli v2ex hot --limit 10 # 热门话题
|
|
66
76
|
opencli v2ex latest --limit 10 # 最新话题
|
|
77
|
+
opencli v2ex topic --id 1024 # 主题详情
|
|
78
|
+
|
|
79
|
+
# Hacker News (public)
|
|
80
|
+
opencli hackernews top --limit 10 # Top stories
|
|
81
|
+
|
|
82
|
+
# BBC (public)
|
|
83
|
+
opencli bbc news --limit 10 # BBC News RSS headlines
|
|
84
|
+
|
|
85
|
+
# 微博 (browser)
|
|
86
|
+
opencli weibo hot --limit 10 # 微博热搜
|
|
87
|
+
|
|
88
|
+
# BOSS直聘 (browser)
|
|
89
|
+
opencli boss search --query "AI agent" # 搜索职位
|
|
67
90
|
|
|
68
|
-
#
|
|
69
|
-
opencli
|
|
91
|
+
# YouTube (browser)
|
|
92
|
+
opencli youtube search --query "rust" # 搜索视频
|
|
93
|
+
|
|
94
|
+
# Yahoo Finance (browser)
|
|
95
|
+
opencli yahoo-finance quote --symbol AAPL # 股票行情
|
|
96
|
+
|
|
97
|
+
# Reuters (browser)
|
|
98
|
+
opencli reuters search --query "AI" # 路透社搜索
|
|
99
|
+
|
|
100
|
+
# 什么值得买 (browser)
|
|
101
|
+
opencli smzdm search --keyword "耳机" # 搜索好价
|
|
102
|
+
|
|
103
|
+
# 携程 (browser)
|
|
104
|
+
opencli ctrip search --query "三亚" # 搜索目的地
|
|
70
105
|
```
|
|
71
106
|
|
|
72
|
-
###
|
|
107
|
+
### Management Commands
|
|
73
108
|
|
|
74
109
|
```bash
|
|
75
|
-
opencli list #
|
|
76
|
-
opencli list --json # JSON
|
|
77
|
-
opencli validate #
|
|
78
|
-
opencli validate bilibili #
|
|
110
|
+
opencli list # List all commands
|
|
111
|
+
opencli list --json # JSON output
|
|
112
|
+
opencli validate # Validate all CLI definitions
|
|
113
|
+
opencli validate bilibili # Validate specific site
|
|
79
114
|
```
|
|
80
115
|
|
|
81
|
-
### AI
|
|
116
|
+
### AI Agent Workflow
|
|
82
117
|
|
|
83
118
|
```bash
|
|
84
|
-
|
|
85
|
-
opencli
|
|
86
|
-
|
|
87
|
-
|
|
119
|
+
# Deep Explore: network intercept → response analysis → capability inference
|
|
120
|
+
opencli explore <url> --site <name>
|
|
121
|
+
|
|
122
|
+
# Synthesize: generate evaluate-based YAML pipelines from explore artifacts
|
|
123
|
+
opencli synthesize <site>
|
|
124
|
+
|
|
125
|
+
# Generate: one-shot explore → synthesize → register
|
|
126
|
+
opencli generate <url> --goal "hot"
|
|
127
|
+
|
|
128
|
+
# Strategy Cascade: auto-probe PUBLIC → COOKIE → HEADER
|
|
129
|
+
opencli cascade <api-url>
|
|
130
|
+
|
|
131
|
+
# Verify: smoke-test a generated adapter
|
|
132
|
+
opencli verify <site/name> --smoke
|
|
88
133
|
```
|
|
89
134
|
|
|
90
|
-
##
|
|
135
|
+
## Output Formats
|
|
91
136
|
|
|
92
|
-
|
|
137
|
+
All commands support `--format` / `-f`:
|
|
93
138
|
|
|
94
139
|
```bash
|
|
95
|
-
opencli bilibili hot -f table #
|
|
96
|
-
opencli bilibili hot -f json # JSON
|
|
140
|
+
opencli bilibili hot -f table # Default: rich table
|
|
141
|
+
opencli bilibili hot -f json # JSON (pipe to jq, feed to AI agent)
|
|
97
142
|
opencli bilibili hot -f md # Markdown
|
|
98
143
|
opencli bilibili hot -f csv # CSV
|
|
99
144
|
```
|
|
100
145
|
|
|
101
|
-
##
|
|
146
|
+
## Verbose Mode
|
|
102
147
|
|
|
103
148
|
```bash
|
|
104
|
-
opencli bilibili hot -v #
|
|
149
|
+
opencli bilibili hot -v # Show each pipeline step and data flow
|
|
105
150
|
```
|
|
106
151
|
|
|
107
|
-
##
|
|
152
|
+
## Creating Adapters
|
|
108
153
|
|
|
109
|
-
### YAML
|
|
154
|
+
### YAML Pipeline (declarative, recommended)
|
|
110
155
|
|
|
111
|
-
|
|
156
|
+
Create `src/clis/<site>/<name>.yaml`:
|
|
112
157
|
|
|
113
158
|
```yaml
|
|
114
159
|
site: mysite
|
|
115
160
|
name: hot
|
|
116
|
-
description: Hot topics
|
|
161
|
+
description: Hot topics
|
|
117
162
|
domain: www.mysite.com
|
|
118
163
|
strategy: cookie # public | cookie | header | intercept | ui
|
|
119
164
|
browser: true
|
|
@@ -130,11 +175,13 @@ pipeline:
|
|
|
130
175
|
- evaluate: |
|
|
131
176
|
(async () => {
|
|
132
177
|
const res = await fetch('/api/hot', { credentials: 'include' });
|
|
133
|
-
|
|
178
|
+
const d = await res.json();
|
|
179
|
+
return d.data.items.map(item => ({
|
|
180
|
+
title: item.title,
|
|
181
|
+
score: item.score,
|
|
182
|
+
}));
|
|
134
183
|
})()
|
|
135
184
|
|
|
136
|
-
- select: data.items
|
|
137
|
-
|
|
138
185
|
- map:
|
|
139
186
|
rank: ${{ index + 1 }}
|
|
140
187
|
title: ${{ item.title }}
|
|
@@ -145,9 +192,24 @@ pipeline:
|
|
|
145
192
|
columns: [rank, title, score]
|
|
146
193
|
```
|
|
147
194
|
|
|
148
|
-
|
|
195
|
+
For public APIs (no browser):
|
|
196
|
+
|
|
197
|
+
```yaml
|
|
198
|
+
strategy: public
|
|
199
|
+
browser: false
|
|
200
|
+
|
|
201
|
+
pipeline:
|
|
202
|
+
- fetch:
|
|
203
|
+
url: https://api.example.com/hot.json
|
|
204
|
+
- select: data.items
|
|
205
|
+
- map:
|
|
206
|
+
title: ${{ item.title }}
|
|
207
|
+
- limit: ${{ args.limit }}
|
|
208
|
+
```
|
|
149
209
|
|
|
150
|
-
|
|
210
|
+
### TypeScript Adapter (programmatic)
|
|
211
|
+
|
|
212
|
+
Create `src/clis/<site>/<name>.ts` and import in `clis/index.ts`:
|
|
151
213
|
|
|
152
214
|
```typescript
|
|
153
215
|
import { cli, Strategy } from '../../registry.js';
|
|
@@ -159,72 +221,86 @@ cli({
|
|
|
159
221
|
args: [{ name: 'keyword', required: true }],
|
|
160
222
|
columns: ['rank', 'title', 'url'],
|
|
161
223
|
func: async (page, kwargs) => {
|
|
224
|
+
await page.goto('https://www.mysite.com');
|
|
162
225
|
const data = await page.evaluate(`
|
|
163
|
-
async () => {
|
|
164
|
-
const res = await fetch('/api/search?q=${kwargs.keyword}', {
|
|
226
|
+
(async () => {
|
|
227
|
+
const res = await fetch('/api/search?q=${kwargs.keyword}', {
|
|
228
|
+
credentials: 'include'
|
|
229
|
+
});
|
|
165
230
|
return await res.json();
|
|
166
|
-
}
|
|
231
|
+
})()
|
|
167
232
|
`);
|
|
168
233
|
return data.items.map((item, i) => ({
|
|
169
|
-
rank: i + 1,
|
|
170
|
-
title: item.title,
|
|
171
|
-
url: item.url,
|
|
234
|
+
rank: i + 1, title: item.title, url: item.url,
|
|
172
235
|
}));
|
|
173
236
|
},
|
|
174
237
|
});
|
|
175
238
|
```
|
|
176
239
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
|
182
|
-
|
|
183
|
-
| `
|
|
184
|
-
| `
|
|
185
|
-
| `
|
|
186
|
-
| `
|
|
187
|
-
| `
|
|
188
|
-
| `
|
|
189
|
-
| `
|
|
190
|
-
| `
|
|
191
|
-
| `
|
|
192
|
-
| `
|
|
193
|
-
| `
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
240
|
+
**When to use TS**: XHR interception (小红书), cookie extraction (Twitter ct0), Wbi signing (Bilibili), auto-pagination, complex data transforms.
|
|
241
|
+
|
|
242
|
+
## Pipeline Steps
|
|
243
|
+
|
|
244
|
+
| Step | Description | Example |
|
|
245
|
+
|------|-------------|---------|
|
|
246
|
+
| `navigate` | Go to URL | `navigate: https://example.com` |
|
|
247
|
+
| `fetch` | HTTP request (browser cookies) | `fetch: { url: "...", params: { q: "..." } }` |
|
|
248
|
+
| `evaluate` | Run JavaScript in page | `evaluate: \| (async () => { ... })()` |
|
|
249
|
+
| `select` | Extract JSON path | `select: data.items` |
|
|
250
|
+
| `map` | Map fields | `map: { title: "${{ item.title }}" }` |
|
|
251
|
+
| `filter` | Filter items | `filter: item.score > 100` |
|
|
252
|
+
| `sort` | Sort items | `sort: { by: score, order: desc }` |
|
|
253
|
+
| `limit` | Cap result count | `limit: ${{ args.limit }}` |
|
|
254
|
+
| `intercept` | Declarative XHR capture | `intercept: { trigger: "navigate:...", capture: "api/hot" }` |
|
|
255
|
+
| `tap` | Store action + XHR capture | `tap: { store: "feed", action: "fetchFeeds", capture: "homefeed" }` |
|
|
256
|
+
| `snapshot` | Page accessibility tree | `snapshot: { interactive: true }` |
|
|
257
|
+
| `click` | Click element | `click: ${{ ref }}` |
|
|
258
|
+
| `type` | Type text | `type: { ref: "@1", text: "hello" }` |
|
|
259
|
+
| `wait` | Wait for time/text | `wait: 2` or `wait: { text: "loaded" }` |
|
|
260
|
+
| `press` | Press key | `press: Enter` |
|
|
261
|
+
|
|
262
|
+
## Template Syntax
|
|
198
263
|
|
|
199
264
|
```yaml
|
|
200
|
-
#
|
|
265
|
+
# Arguments with defaults
|
|
201
266
|
${{ args.keyword }}
|
|
202
267
|
${{ args.limit | default(20) }}
|
|
203
268
|
|
|
204
|
-
#
|
|
269
|
+
# Current item (in map/filter)
|
|
205
270
|
${{ item.title }}
|
|
206
271
|
${{ item.data.nested.field }}
|
|
207
272
|
|
|
208
|
-
#
|
|
273
|
+
# Index (0-based)
|
|
209
274
|
${{ index }}
|
|
210
275
|
${{ index + 1 }}
|
|
211
276
|
```
|
|
212
277
|
|
|
213
|
-
##
|
|
214
|
-
|
|
215
|
-
|
|
|
216
|
-
|
|
217
|
-
| `
|
|
218
|
-
| `
|
|
219
|
-
| `
|
|
220
|
-
| `
|
|
221
|
-
| `
|
|
222
|
-
|
|
223
|
-
##
|
|
224
|
-
|
|
225
|
-
|
|
|
226
|
-
|
|
227
|
-
| `
|
|
228
|
-
| `
|
|
229
|
-
| `
|
|
230
|
-
| `
|
|
278
|
+
## 5-Tier Authentication Strategy
|
|
279
|
+
|
|
280
|
+
| Tier | Name | Method | Example |
|
|
281
|
+
|------|------|--------|---------|
|
|
282
|
+
| 1 | `public` | No auth, Node.js fetch | Hacker News, V2EX |
|
|
283
|
+
| 2 | `cookie` | Browser fetch with `credentials: include` | Bilibili, Zhihu |
|
|
284
|
+
| 3 | `header` | Custom headers (ct0, Bearer) | Twitter GraphQL |
|
|
285
|
+
| 4 | `intercept` | XHR interception + store mutation | 小红书 Pinia |
|
|
286
|
+
| 5 | `ui` | Full UI automation (click/type/scroll) | Last resort |
|
|
287
|
+
|
|
288
|
+
## Environment Variables
|
|
289
|
+
|
|
290
|
+
| Variable | Default | Description |
|
|
291
|
+
|----------|---------|-------------|
|
|
292
|
+
| `OPENCLI_BROWSER_CONNECT_TIMEOUT` | 30 | Browser connection timeout (sec) |
|
|
293
|
+
| `OPENCLI_BROWSER_COMMAND_TIMEOUT` | 45 | Command execution timeout (sec) |
|
|
294
|
+
| `OPENCLI_BROWSER_EXPLORE_TIMEOUT` | 120 | Explore timeout (sec) |
|
|
295
|
+
| `OPENCLI_EXTENSION_LOCK_TIMEOUT` | 120 | Extension lock timeout (sec) |
|
|
296
|
+
| `PLAYWRIGHT_MCP_EXTENSION_TOKEN` | — | Auto-approve extension connection |
|
|
297
|
+
|
|
298
|
+
## Troubleshooting
|
|
299
|
+
|
|
300
|
+
| Issue | Solution |
|
|
301
|
+
|-------|----------|
|
|
302
|
+
| `npx not found` | Install Node.js: `brew install node` |
|
|
303
|
+
| `Timed out connecting to browser` | 1) Chrome must be open 2) Install MCP Bridge extension 3) Click to approve |
|
|
304
|
+
| `Extension lock timed out` | Another opencli command is running; browser commands run serially |
|
|
305
|
+
| `Target page context` error | Add `navigate:` step before `evaluate:` in YAML |
|
|
306
|
+
| Empty table data | Check if evaluate returns JSON string (MCP parsing) or data path is wrong |
|
package/dist/bilibili.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bilibili shared helpers: WBI signing, authenticated fetch, nav data, UID resolution.
|
|
3
3
|
*/
|
|
4
|
+
import type { IPage } from './types.js';
|
|
4
5
|
export declare function stripHtml(s: string): string;
|
|
5
6
|
export declare function payloadData(payload: any): any;
|
|
6
|
-
export declare function wbiSign(page:
|
|
7
|
-
export declare function apiGet(page:
|
|
7
|
+
export declare function wbiSign(page: IPage, params: Record<string, any>): Promise<Record<string, string>>;
|
|
8
|
+
export declare function apiGet(page: IPage, path: string, opts?: {
|
|
8
9
|
params?: Record<string, any>;
|
|
9
10
|
signed?: boolean;
|
|
10
11
|
}): Promise<any>;
|
|
11
|
-
export declare function fetchJson(page:
|
|
12
|
-
export declare function getSelfUid(page:
|
|
13
|
-
export declare function resolveUid(page:
|
|
12
|
+
export declare function fetchJson(page: IPage, url: string): Promise<any>;
|
|
13
|
+
export declare function getSelfUid(page: IPage): Promise<string>;
|
|
14
|
+
export declare function resolveUid(page: IPage, input: string): Promise<string>;
|
package/dist/browser.d.ts
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* Browser interaction via Playwright MCP Bridge extension.
|
|
3
3
|
* Connects to an existing Chrome browser through the extension's stdio JSON-RPC.
|
|
4
4
|
*/
|
|
5
|
+
import type { IPage } from './types.js';
|
|
5
6
|
/**
|
|
6
7
|
* Page abstraction wrapping JSON-RPC calls to Playwright MCP.
|
|
7
8
|
*/
|
|
8
|
-
export declare class Page {
|
|
9
|
+
export declare class Page implements IPage {
|
|
9
10
|
private _send;
|
|
10
11
|
private _recv;
|
|
11
12
|
constructor(_send: (msg: string) => void, _recv: () => Promise<any>);
|
|
@@ -39,6 +40,7 @@ export declare class PlaywrightMCP {
|
|
|
39
40
|
private _waiters;
|
|
40
41
|
private _lockAcquired;
|
|
41
42
|
private _initialTabCount;
|
|
43
|
+
private _page;
|
|
42
44
|
connect(opts?: {
|
|
43
45
|
timeout?: number;
|
|
44
46
|
}): Promise<Page>;
|
package/dist/browser.js
CHANGED
|
@@ -8,6 +8,14 @@ import * as fs from 'node:fs';
|
|
|
8
8
|
import * as os from 'node:os';
|
|
9
9
|
import * as path from 'node:path';
|
|
10
10
|
import { formatSnapshot } from './snapshotFormatter.js';
|
|
11
|
+
// Read version from package.json (single source of truth)
|
|
12
|
+
const __browser_dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const PKG_VERSION = (() => { try {
|
|
14
|
+
return JSON.parse(fs.readFileSync(path.resolve(__browser_dirname, '..', 'package.json'), 'utf-8')).version;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return '0.0.0';
|
|
18
|
+
} })();
|
|
11
19
|
const EXTENSION_LOCK_TIMEOUT = parseInt(process.env.OPENCLI_EXTENSION_LOCK_TIMEOUT ?? '120', 10);
|
|
12
20
|
const EXTENSION_LOCK_POLL = parseInt(process.env.OPENCLI_EXTENSION_LOCK_POLL_INTERVAL ?? '1', 10);
|
|
13
21
|
const CONNECT_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_CONNECT_TIMEOUT ?? '30', 10);
|
|
@@ -37,7 +45,18 @@ export class Page {
|
|
|
37
45
|
if (result?.content) {
|
|
38
46
|
const textParts = result.content.filter((c) => c.type === 'text');
|
|
39
47
|
if (textParts.length === 1) {
|
|
40
|
-
|
|
48
|
+
let text = textParts[0].text;
|
|
49
|
+
// MCP browser_evaluate returns: "[JSON]\n### Ran Playwright code\n```js\n...\n```"
|
|
50
|
+
// Strip the "### Ran Playwright code" suffix to get clean JSON
|
|
51
|
+
const codeMarker = text.indexOf('### Ran Playwright code');
|
|
52
|
+
if (codeMarker !== -1) {
|
|
53
|
+
text = text.slice(0, codeMarker).trim();
|
|
54
|
+
}
|
|
55
|
+
// Also handle "### Result\n[JSON]" format (some MCP versions)
|
|
56
|
+
const resultMarker = text.indexOf('### Result\n');
|
|
57
|
+
if (resultMarker !== -1) {
|
|
58
|
+
text = text.slice(resultMarker + '### Result\n'.length).trim();
|
|
59
|
+
}
|
|
41
60
|
try {
|
|
42
61
|
return JSON.parse(text);
|
|
43
62
|
}
|
|
@@ -106,6 +125,7 @@ export class PlaywrightMCP {
|
|
|
106
125
|
_waiters = [];
|
|
107
126
|
_lockAcquired = false;
|
|
108
127
|
_initialTabCount = 0;
|
|
128
|
+
_page = null;
|
|
109
129
|
async connect(opts = {}) {
|
|
110
130
|
await this._acquireLock();
|
|
111
131
|
const timeout = opts.timeout ?? CONNECT_TIMEOUT;
|
|
@@ -124,6 +144,7 @@ export class PlaywrightMCP {
|
|
|
124
144
|
this._proc.stdout.setMaxListeners(20);
|
|
125
145
|
const page = new Page((msg) => { if (this._proc?.stdin?.writable)
|
|
126
146
|
this._proc.stdin.write(msg); }, () => new Promise((res) => { this._waiters.push(res); }));
|
|
147
|
+
this._page = page;
|
|
127
148
|
this._proc.stdout?.on('data', (chunk) => {
|
|
128
149
|
this._buffer += chunk.toString();
|
|
129
150
|
const lines = this._buffer.split('\n');
|
|
@@ -146,7 +167,7 @@ export class PlaywrightMCP {
|
|
|
146
167
|
const initMsg = jsonRpcRequest('initialize', {
|
|
147
168
|
protocolVersion: '2024-11-05',
|
|
148
169
|
capabilities: {},
|
|
149
|
-
clientInfo: { name: 'opencli', version:
|
|
170
|
+
clientInfo: { name: 'opencli', version: PKG_VERSION },
|
|
150
171
|
});
|
|
151
172
|
this._proc.stdin?.write(initMsg);
|
|
152
173
|
// Wait for initialize response, then send initialized notification
|
|
@@ -174,12 +195,33 @@ export class PlaywrightMCP {
|
|
|
174
195
|
}
|
|
175
196
|
async close() {
|
|
176
197
|
try {
|
|
198
|
+
// Close tabs opened during this session (site tabs + extension tabs)
|
|
199
|
+
if (this._page && this._proc && !this._proc.killed) {
|
|
200
|
+
try {
|
|
201
|
+
const tabs = await this._page.tabs();
|
|
202
|
+
const tabStr = typeof tabs === 'string' ? tabs : JSON.stringify(tabs);
|
|
203
|
+
const allTabs = tabStr.match(/Tab (\d+)/g) || [];
|
|
204
|
+
const currentTabCount = allTabs.length;
|
|
205
|
+
// Close tabs in reverse order to avoid index shifting issues
|
|
206
|
+
// Keep the original tabs that existed before the command started
|
|
207
|
+
if (currentTabCount > this._initialTabCount && this._initialTabCount > 0) {
|
|
208
|
+
for (let i = currentTabCount - 1; i >= this._initialTabCount; i--) {
|
|
209
|
+
try {
|
|
210
|
+
await this._page.closeTab(i);
|
|
211
|
+
}
|
|
212
|
+
catch { }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch { }
|
|
217
|
+
}
|
|
177
218
|
if (this._proc && !this._proc.killed) {
|
|
178
219
|
this._proc.kill('SIGTERM');
|
|
179
220
|
await new Promise((res) => { this._proc?.on('exit', () => res()); setTimeout(res, 3000); });
|
|
180
221
|
}
|
|
181
222
|
}
|
|
182
223
|
finally {
|
|
224
|
+
this._page = null;
|
|
183
225
|
this._releaseLock();
|
|
184
226
|
}
|
|
185
227
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strategy Cascade: automatic strategy downgrade chain.
|
|
3
|
+
*
|
|
4
|
+
* Probes an API endpoint starting from the simplest strategy (PUBLIC)
|
|
5
|
+
* and automatically downgrades through the strategy tiers until one works:
|
|
6
|
+
*
|
|
7
|
+
* PUBLIC → COOKIE → HEADER → INTERCEPT → UI
|
|
8
|
+
*
|
|
9
|
+
* This eliminates the need for manual strategy selection — the system
|
|
10
|
+
* automatically finds the minimum-privilege strategy that works.
|
|
11
|
+
*/
|
|
12
|
+
import { Strategy } from './registry.js';
|
|
13
|
+
import type { IPage } from './types.js';
|
|
14
|
+
interface ProbeResult {
|
|
15
|
+
strategy: Strategy;
|
|
16
|
+
success: boolean;
|
|
17
|
+
statusCode?: number;
|
|
18
|
+
hasData?: boolean;
|
|
19
|
+
error?: string;
|
|
20
|
+
responsePreview?: string;
|
|
21
|
+
}
|
|
22
|
+
interface CascadeResult {
|
|
23
|
+
bestStrategy: Strategy;
|
|
24
|
+
probes: ProbeResult[];
|
|
25
|
+
confidence: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Probe an endpoint with a specific strategy.
|
|
29
|
+
* Returns whether the probe succeeded and basic response info.
|
|
30
|
+
*/
|
|
31
|
+
export declare function probeEndpoint(page: IPage, url: string, strategy: Strategy, opts?: {
|
|
32
|
+
timeout?: number;
|
|
33
|
+
}): Promise<ProbeResult>;
|
|
34
|
+
/**
|
|
35
|
+
* Run the cascade: try each strategy in order until one works.
|
|
36
|
+
* Returns the simplest working strategy.
|
|
37
|
+
*/
|
|
38
|
+
export declare function cascadeProbe(page: IPage, url: string, opts?: {
|
|
39
|
+
maxStrategy?: Strategy;
|
|
40
|
+
timeout?: number;
|
|
41
|
+
}): Promise<CascadeResult>;
|
|
42
|
+
/**
|
|
43
|
+
* Render cascade results for display.
|
|
44
|
+
*/
|
|
45
|
+
export declare function renderCascadeResult(result: CascadeResult): string;
|
|
46
|
+
export {};
|