@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.
Files changed (135) hide show
  1. package/CLI-CREATOR.md +594 -0
  2. package/README.md +124 -39
  3. package/README.zh-CN.md +151 -0
  4. package/SKILL.md +178 -102
  5. package/dist/bilibili.d.ts +6 -5
  6. package/dist/browser.d.ts +3 -1
  7. package/dist/browser.js +44 -2
  8. package/dist/cascade.d.ts +46 -0
  9. package/dist/cascade.js +180 -0
  10. package/dist/clis/bbc/news.js +42 -0
  11. package/dist/clis/bilibili/hot.yaml +38 -0
  12. package/dist/clis/boss/search.js +47 -0
  13. package/dist/clis/ctrip/search.d.ts +1 -0
  14. package/dist/clis/ctrip/search.js +62 -0
  15. package/dist/clis/hackernews/top.yaml +36 -0
  16. package/dist/clis/index.d.ts +10 -1
  17. package/dist/clis/index.js +19 -1
  18. package/dist/clis/reddit/hot.yaml +46 -0
  19. package/dist/clis/reuters/search.d.ts +1 -0
  20. package/dist/clis/reuters/search.js +52 -0
  21. package/dist/clis/smzdm/search.d.ts +1 -0
  22. package/dist/clis/smzdm/search.js +66 -0
  23. package/dist/clis/twitter/trending.yaml +40 -0
  24. package/dist/clis/v2ex/hot.yaml +25 -0
  25. package/dist/clis/v2ex/latest.yaml +25 -0
  26. package/dist/clis/v2ex/topic.yaml +27 -0
  27. package/dist/clis/weibo/hot.d.ts +1 -0
  28. package/dist/clis/weibo/hot.js +41 -0
  29. package/dist/clis/xiaohongshu/feed.yaml +32 -0
  30. package/dist/clis/xiaohongshu/notifications.yaml +38 -0
  31. package/dist/clis/xiaohongshu/search.d.ts +5 -0
  32. package/dist/clis/xiaohongshu/search.js +68 -0
  33. package/dist/clis/yahoo-finance/quote.d.ts +1 -0
  34. package/dist/clis/yahoo-finance/quote.js +74 -0
  35. package/dist/clis/youtube/search.d.ts +1 -0
  36. package/dist/clis/youtube/search.js +60 -0
  37. package/dist/clis/zhihu/hot.yaml +42 -0
  38. package/dist/clis/zhihu/question.d.ts +1 -0
  39. package/dist/clis/zhihu/question.js +39 -0
  40. package/dist/clis/zhihu/search.yaml +55 -0
  41. package/dist/engine.d.ts +2 -1
  42. package/dist/explore.d.ts +23 -13
  43. package/dist/explore.js +293 -422
  44. package/dist/generate.js +2 -1
  45. package/dist/main.js +21 -2
  46. package/dist/pipeline/executor.d.ts +9 -0
  47. package/dist/pipeline/executor.js +88 -0
  48. package/dist/pipeline/index.d.ts +5 -0
  49. package/dist/pipeline/index.js +5 -0
  50. package/dist/pipeline/steps/browser.d.ts +12 -0
  51. package/dist/pipeline/steps/browser.js +68 -0
  52. package/dist/pipeline/steps/fetch.d.ts +5 -0
  53. package/dist/pipeline/steps/fetch.js +50 -0
  54. package/dist/pipeline/steps/intercept.d.ts +5 -0
  55. package/dist/pipeline/steps/intercept.js +75 -0
  56. package/dist/pipeline/steps/tap.d.ts +12 -0
  57. package/dist/pipeline/steps/tap.js +130 -0
  58. package/dist/pipeline/steps/transform.d.ts +8 -0
  59. package/dist/pipeline/steps/transform.js +53 -0
  60. package/dist/pipeline/template.d.ts +16 -0
  61. package/dist/pipeline/template.js +115 -0
  62. package/dist/pipeline/template.test.d.ts +4 -0
  63. package/dist/pipeline/template.test.js +102 -0
  64. package/dist/pipeline/transform.test.d.ts +4 -0
  65. package/dist/pipeline/transform.test.js +90 -0
  66. package/dist/pipeline.d.ts +5 -7
  67. package/dist/pipeline.js +5 -313
  68. package/dist/registry.d.ts +3 -2
  69. package/dist/runtime.d.ts +2 -1
  70. package/dist/synthesize.d.ts +11 -8
  71. package/dist/synthesize.js +142 -118
  72. package/dist/types.d.ts +27 -0
  73. package/dist/types.js +7 -0
  74. package/package.json +9 -4
  75. package/src/bilibili.ts +9 -7
  76. package/src/browser.ts +41 -3
  77. package/src/cascade.ts +218 -0
  78. package/src/clis/bbc/news.ts +42 -0
  79. package/src/clis/boss/search.ts +47 -0
  80. package/src/clis/ctrip/search.ts +62 -0
  81. package/src/clis/index.ts +28 -1
  82. package/src/clis/reddit/hot.yaml +46 -0
  83. package/src/clis/reuters/search.ts +52 -0
  84. package/src/clis/smzdm/search.ts +66 -0
  85. package/src/clis/v2ex/hot.yaml +5 -9
  86. package/src/clis/v2ex/latest.yaml +5 -8
  87. package/src/clis/v2ex/topic.yaml +27 -0
  88. package/src/clis/weibo/hot.ts +41 -0
  89. package/src/clis/xiaohongshu/feed.yaml +32 -0
  90. package/src/clis/xiaohongshu/notifications.yaml +38 -0
  91. package/src/clis/xiaohongshu/search.ts +71 -0
  92. package/src/clis/yahoo-finance/quote.ts +74 -0
  93. package/src/clis/youtube/search.ts +60 -0
  94. package/src/clis/zhihu/hot.yaml +22 -8
  95. package/src/clis/zhihu/question.ts +45 -0
  96. package/src/clis/zhihu/search.yaml +55 -0
  97. package/src/engine.ts +2 -1
  98. package/src/explore.ts +303 -465
  99. package/src/generate.ts +3 -1
  100. package/src/main.ts +18 -2
  101. package/src/pipeline/executor.ts +98 -0
  102. package/src/pipeline/index.ts +6 -0
  103. package/src/pipeline/steps/browser.ts +67 -0
  104. package/src/pipeline/steps/fetch.ts +60 -0
  105. package/src/pipeline/steps/intercept.ts +78 -0
  106. package/src/pipeline/steps/tap.ts +137 -0
  107. package/src/pipeline/steps/transform.ts +50 -0
  108. package/src/pipeline/template.test.ts +107 -0
  109. package/src/pipeline/template.ts +101 -0
  110. package/src/pipeline/transform.test.ts +107 -0
  111. package/src/pipeline.ts +5 -292
  112. package/src/registry.ts +4 -2
  113. package/src/runtime.ts +3 -1
  114. package/src/synthesize.ts +142 -137
  115. package/src/types.ts +23 -0
  116. package/vitest.config.ts +7 -0
  117. package/dist/clis/github/search.js +0 -20
  118. package/dist/clis/zhihu/search.js +0 -58
  119. package/dist/promote.d.ts +0 -1
  120. package/dist/promote.js +0 -3
  121. package/dist/register.d.ts +0 -2
  122. package/dist/register.js +0 -2
  123. package/dist/scaffold.d.ts +0 -2
  124. package/dist/scaffold.js +0 -2
  125. package/dist/smoke.d.ts +0 -2
  126. package/dist/smoke.js +0 -2
  127. package/src/clis/github/search.ts +0 -21
  128. package/src/clis/github/trending.yaml +0 -58
  129. package/src/clis/zhihu/search.ts +0 -65
  130. package/src/promote.ts +0 -3
  131. package/src/register.ts +0 -2
  132. package/src/scaffold.ts +0 -2
  133. package/src/smoke.ts +0 -2
  134. /package/dist/clis/{github/search.d.ts → bbc/news.d.ts} +0 -0
  135. /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 setup, AI-powered. Turn any website into CLI commands via Chrome browser."
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, 哔哩哔哩, 知乎, AI, agent]
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. 操控 Chrome 无风控风险,复用登录,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
- cd ~/code/ai-native-cli
17
- npm install
18
- ```
19
-
20
- ## 使用方式
16
+ # npm global install (recommended)
17
+ npm install -g @jackwener/opencli
18
+ opencli <command>
21
19
 
22
- ```bash
23
- # 通过 npx 运行(推荐)
20
+ # Or from source
21
+ cd ~/code/opencli && npm install
24
22
  npx tsx src/main.ts <command>
25
23
 
26
- # 或者构建后运行
27
- npm run build && node dist/main.js <command>
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. 安装 [Playwright MCP Bridge](https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm) 扩展
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
- 公共 API 命令(hackernews、github search)无需浏览器。
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
- # V2EX
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
- # Hacker News
69
- opencli hackernews top --limit 10 # 热门故事(无需浏览器)
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 # 验证所有 CLI 定义
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 工作流(为 AI Agent 设计)
116
+ ### AI Agent Workflow
82
117
 
83
118
  ```bash
84
- opencli explore <url> # 探索网站,生成 API 发现成果物
85
- opencli synthesize <site> # 从探索成果物合成候选 CLI
86
- opencli generate <url> --goal "hot" # 一键:探索 → 合成 → 注册
87
- opencli verify <site/name> --smoke # 验证 + Smoke 测试
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
- 所有命令支持 `--format` / `-f` 选项:
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 # 显示 pipeline 每步详情
149
+ opencli bilibili hot -v # Show each pipeline step and data flow
105
150
  ```
106
151
 
107
- ## 创建新的 CLI 适配器
152
+ ## Creating Adapters
108
153
 
109
- ### YAML 方式(声明式,推荐)
154
+ ### YAML Pipeline (declarative, recommended)
110
155
 
111
- `src/clis/<site>/<name>.yaml` 创建文件:
156
+ Create `src/clis/<site>/<name>.yaml`:
112
157
 
113
158
  ```yaml
114
159
  site: mysite
115
160
  name: hot
116
- description: Hot topics on mysite
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
- return await res.json();
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
- ### TypeScript 方式(编程式,更灵活)
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
- `src/clis/<site>/<name>.ts` 创建并在 `clis/index.ts` 中 import:
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}', { credentials: 'include' });
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
- ## Pipeline 步骤参考
178
-
179
- | 步骤 | 说明 | 示例 |
180
- |------|------|------|
181
- | `navigate` | 导航到 URL | `navigate: https://example.com` |
182
- | `fetch` | HTTP 请求(使用浏览器 cookie) | `fetch: { url: "...", params: { q: "${{ args.keyword }}" } }` |
183
- | `evaluate` | 执行 JavaScript | `evaluate: \| (async () => { ... })()` |
184
- | `select` | 选取 JSON 路径 | `select: data.items` |
185
- | `map` | 映射字段 | `map: { title: "${{ item.title }}" }` |
186
- | `filter` | 过滤 | `filter: item.score > 100` |
187
- | `sort` | 排序 | `sort: { by: score, order: desc }` |
188
- | `limit` | 限制数量 | `limit: ${{ args.limit }}` |
189
- | `snapshot` | 获取页面快照 | `snapshot: { interactive: true }` |
190
- | `click` | 点击元素 | `click: ${{ ref }}` |
191
- | `type` | 输入文本 | `type: { ref: "@1", text: "hello" }` |
192
- | `wait` | 等待 | `wait: 2` `wait: { text: "loaded" }` |
193
- | `press` | 按键 | `press: Enter` |
194
-
195
- ## 模板语法
196
-
197
- 使用 `${{ expression }}` 进行模板替换:
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
- # 引用当前 item(在 map/filter 中)
269
+ # Current item (in map/filter)
205
270
  ${{ item.title }}
206
271
  ${{ item.data.nested.field }}
207
272
 
208
- # 索引(从 0 开始)
273
+ # Index (0-based)
209
274
  ${{ index }}
210
275
  ${{ index + 1 }}
211
276
  ```
212
277
 
213
- ## 环境变量
214
-
215
- | 变量 | 默认值 | 说明 |
216
- |------|--------|------|
217
- | `OPENCLI_BROWSER_CONNECT_TIMEOUT` | 30 | 浏览器连接超时(秒) |
218
- | `OPENCLI_BROWSER_COMMAND_TIMEOUT` | 45 | 命令执行超时(秒) |
219
- | `OPENCLI_BROWSER_EXPLORE_TIMEOUT` | 120 | Explore 超时(秒) |
220
- | `OPENCLI_EXTENSION_LOCK_TIMEOUT` | 120 | 扩展锁超时(秒) |
221
- | `PLAYWRIGHT_MCP_EXTENSION_TOKEN` | | 自动批准扩展连接 |
222
-
223
- ## 错误排查
224
-
225
- | 错误 | 解决方案 |
226
- |------|----------|
227
- | `npx not found` | 安装 Node.js: `brew install node` |
228
- | `Timed out connecting to browser` | 1) 确认 Chrome 已打开 2) 安装 Playwright MCP Bridge 扩展 3) 点击扩展图标批准 |
229
- | `Extension lock timed out` | 等待其他 opencli 命令完成,浏览器命令需串行运行 |
230
- | `Request timed out` | 增大 `OPENCLI_BROWSER_COMMAND_TIMEOUT` 或检查网络 |
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 |
@@ -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: any, params: Record<string, any>): Promise<Record<string, string>>;
7
- export declare function apiGet(page: any, path: string, opts?: {
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: any, url: string): Promise<any>;
12
- export declare function getSelfUid(page: any): Promise<string>;
13
- export declare function resolveUid(page: any, input: string): Promise<string>;
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
- const text = textParts[0].text;
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: '0.1.0' },
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 {};