@jackwener/opencli 0.1.0 → 0.1.1

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 (52) hide show
  1. package/CLI-CREATOR.md +594 -0
  2. package/README.md +116 -38
  3. package/README.zh-CN.md +143 -0
  4. package/SKILL.md +154 -102
  5. package/dist/browser.d.ts +1 -0
  6. package/dist/browser.js +35 -1
  7. package/dist/cascade.d.ts +45 -0
  8. package/dist/cascade.js +180 -0
  9. package/dist/clis/bilibili/hot.yaml +38 -0
  10. package/dist/clis/github/trending.yaml +58 -0
  11. package/dist/clis/hackernews/top.yaml +36 -0
  12. package/dist/clis/index.d.ts +2 -1
  13. package/dist/clis/index.js +3 -1
  14. package/dist/clis/reddit/hot.yaml +46 -0
  15. package/dist/clis/twitter/trending.yaml +40 -0
  16. package/dist/clis/v2ex/hot.yaml +25 -0
  17. package/dist/clis/v2ex/latest.yaml +25 -0
  18. package/dist/clis/v2ex/topic.yaml +27 -0
  19. package/dist/clis/xiaohongshu/feed.yaml +32 -0
  20. package/dist/clis/xiaohongshu/notifications.yaml +38 -0
  21. package/dist/clis/xiaohongshu/search.d.ts +5 -0
  22. package/dist/clis/xiaohongshu/search.js +68 -0
  23. package/dist/clis/zhihu/hot.yaml +42 -0
  24. package/dist/clis/zhihu/question.js +39 -0
  25. package/dist/clis/zhihu/search.yaml +55 -0
  26. package/dist/explore.d.ts +23 -13
  27. package/dist/explore.js +293 -422
  28. package/dist/main.js +17 -0
  29. package/dist/pipeline.js +238 -2
  30. package/dist/synthesize.d.ts +11 -8
  31. package/dist/synthesize.js +142 -118
  32. package/package.json +4 -2
  33. package/src/browser.ts +33 -1
  34. package/src/cascade.ts +217 -0
  35. package/src/clis/index.ts +4 -1
  36. package/src/clis/reddit/hot.yaml +46 -0
  37. package/src/clis/v2ex/hot.yaml +5 -9
  38. package/src/clis/v2ex/latest.yaml +5 -8
  39. package/src/clis/v2ex/topic.yaml +27 -0
  40. package/src/clis/xiaohongshu/feed.yaml +32 -0
  41. package/src/clis/xiaohongshu/notifications.yaml +38 -0
  42. package/src/clis/xiaohongshu/search.ts +71 -0
  43. package/src/clis/zhihu/hot.yaml +22 -8
  44. package/src/clis/zhihu/question.ts +45 -0
  45. package/src/clis/zhihu/search.yaml +55 -0
  46. package/src/explore.ts +303 -465
  47. package/src/main.ts +14 -0
  48. package/src/pipeline.ts +239 -2
  49. package/src/synthesize.ts +142 -137
  50. package/dist/clis/zhihu/search.js +0 -58
  51. package/src/clis/zhihu/search.ts +0 -65
  52. /package/dist/clis/zhihu/{search.d.ts → question.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
29
+
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
31
34
 
32
- 浏览器命令需要:
33
- 1. Chrome 浏览器正在运行
34
- 2. 安装 [Playwright MCP Bridge](https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm) 扩展
35
- 3. 点击扩展图标批准连接
35
+ > **Note**: You must be logged into the target website in Chrome before running commands. Tabs opened during command execution are auto-closed afterwards.
36
36
 
37
- 公共 API 命令(hackernewsgithub search)无需浏览器。
37
+ Public API commands (`hackernews`, `github search`, `v2ex`) need no browser.
38
38
 
39
- ## 内置命令
39
+ ## Commands Reference
40
40
 
41
- ### 数据查询
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,91 @@ 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 # 问题详情和回答
56
57
 
57
- # GitHub
58
+ # 小红书 (browser)
59
+ opencli xiaohongshu search --keyword "美食" # 搜索笔记
60
+ opencli xiaohongshu notifications # 通知(mentions/likes/connections)
61
+ opencli xiaohongshu feed --limit 10 # 推荐 Feed
62
+
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 # 主题详情
67
78
 
68
- # Hacker News
69
- opencli hackernews top --limit 10 # 热门故事(无需浏览器)
79
+ # Hacker News (public)
80
+ opencli hackernews top --limit 10 # Top stories
70
81
  ```
71
82
 
72
- ### 管理命令
83
+ ### Management Commands
73
84
 
74
85
  ```bash
75
- opencli list # 列出所有可用命令
76
- opencli list --json # JSON 格式输出
77
- opencli validate # 验证所有 CLI 定义
78
- opencli validate bilibili # 验证指定站点
86
+ opencli list # List all commands
87
+ opencli list --json # JSON output
88
+ opencli validate # Validate all CLI definitions
89
+ opencli validate bilibili # Validate specific site
79
90
  ```
80
91
 
81
- ### AI 工作流(为 AI Agent 设计)
92
+ ### AI Agent Workflow
82
93
 
83
94
  ```bash
84
- opencli explore <url> # 探索网站,生成 API 发现成果物
85
- opencli synthesize <site> # 从探索成果物合成候选 CLI
86
- opencli generate <url> --goal "hot" # 一键:探索 → 合成 → 注册
87
- opencli verify <site/name> --smoke # 验证 + Smoke 测试
95
+ # Deep Explore: network intercept → response analysis → capability inference
96
+ opencli explore <url> --site <name>
97
+
98
+ # Synthesize: generate evaluate-based YAML pipelines from explore artifacts
99
+ opencli synthesize <site>
100
+
101
+ # Generate: one-shot explore → synthesize → register
102
+ opencli generate <url> --goal "hot"
103
+
104
+ # Strategy Cascade: auto-probe PUBLIC → COOKIE → HEADER
105
+ opencli cascade <api-url>
106
+
107
+ # Verify: smoke-test a generated adapter
108
+ opencli verify <site/name> --smoke
88
109
  ```
89
110
 
90
- ## 输出格式
111
+ ## Output Formats
91
112
 
92
- 所有命令支持 `--format` / `-f` 选项:
113
+ All commands support `--format` / `-f`:
93
114
 
94
115
  ```bash
95
- opencli bilibili hot -f table # 默认表格
96
- opencli bilibili hot -f json # JSON
116
+ opencli bilibili hot -f table # Default: rich table
117
+ opencli bilibili hot -f json # JSON (pipe to jq, feed to AI agent)
97
118
  opencli bilibili hot -f md # Markdown
98
119
  opencli bilibili hot -f csv # CSV
99
120
  ```
100
121
 
101
- ## 调试
122
+ ## Verbose Mode
102
123
 
103
124
  ```bash
104
- opencli bilibili hot -v # 显示 pipeline 每步详情
125
+ opencli bilibili hot -v # Show each pipeline step and data flow
105
126
  ```
106
127
 
107
- ## 创建新的 CLI 适配器
128
+ ## Creating Adapters
108
129
 
109
- ### YAML 方式(声明式,推荐)
130
+ ### YAML Pipeline (declarative, recommended)
110
131
 
111
- `src/clis/<site>/<name>.yaml` 创建文件:
132
+ Create `src/clis/<site>/<name>.yaml`:
112
133
 
113
134
  ```yaml
114
135
  site: mysite
115
136
  name: hot
116
- description: Hot topics on mysite
137
+ description: Hot topics
117
138
  domain: www.mysite.com
118
139
  strategy: cookie # public | cookie | header | intercept | ui
119
140
  browser: true
@@ -130,11 +151,13 @@ pipeline:
130
151
  - evaluate: |
131
152
  (async () => {
132
153
  const res = await fetch('/api/hot', { credentials: 'include' });
133
- return await res.json();
154
+ const d = await res.json();
155
+ return d.data.items.map(item => ({
156
+ title: item.title,
157
+ score: item.score,
158
+ }));
134
159
  })()
135
160
 
136
- - select: data.items
137
-
138
161
  - map:
139
162
  rank: ${{ index + 1 }}
140
163
  title: ${{ item.title }}
@@ -145,9 +168,24 @@ pipeline:
145
168
  columns: [rank, title, score]
146
169
  ```
147
170
 
148
- ### TypeScript 方式(编程式,更灵活)
171
+ For public APIs (no browser):
149
172
 
150
- 在 `src/clis/<site>/<name>.ts` 创建并在 `clis/index.ts` 中 import:
173
+ ```yaml
174
+ strategy: public
175
+ browser: false
176
+
177
+ pipeline:
178
+ - fetch:
179
+ url: https://api.example.com/hot.json
180
+ - select: data.items
181
+ - map:
182
+ title: ${{ item.title }}
183
+ - limit: ${{ args.limit }}
184
+ ```
185
+
186
+ ### TypeScript Adapter (programmatic)
187
+
188
+ Create `src/clis/<site>/<name>.ts` and import in `clis/index.ts`:
151
189
 
152
190
  ```typescript
153
191
  import { cli, Strategy } from '../../registry.js';
@@ -159,72 +197,86 @@ cli({
159
197
  args: [{ name: 'keyword', required: true }],
160
198
  columns: ['rank', 'title', 'url'],
161
199
  func: async (page, kwargs) => {
200
+ await page.goto('https://www.mysite.com');
162
201
  const data = await page.evaluate(`
163
- async () => {
164
- const res = await fetch('/api/search?q=${kwargs.keyword}', { credentials: 'include' });
202
+ (async () => {
203
+ const res = await fetch('/api/search?q=${kwargs.keyword}', {
204
+ credentials: 'include'
205
+ });
165
206
  return await res.json();
166
- }
207
+ })()
167
208
  `);
168
209
  return data.items.map((item, i) => ({
169
- rank: i + 1,
170
- title: item.title,
171
- url: item.url,
210
+ rank: i + 1, title: item.title, url: item.url,
172
211
  }));
173
212
  },
174
213
  });
175
214
  ```
176
215
 
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 }}` 进行模板替换:
216
+ **When to use TS**: XHR interception (小红书), cookie extraction (Twitter ct0), Wbi signing (Bilibili), auto-pagination, complex data transforms.
217
+
218
+ ## Pipeline Steps
219
+
220
+ | Step | Description | Example |
221
+ |------|-------------|---------|
222
+ | `navigate` | Go to URL | `navigate: https://example.com` |
223
+ | `fetch` | HTTP request (browser cookies) | `fetch: { url: "...", params: { q: "..." } }` |
224
+ | `evaluate` | Run JavaScript in page | `evaluate: \| (async () => { ... })()` |
225
+ | `select` | Extract JSON path | `select: data.items` |
226
+ | `map` | Map fields | `map: { title: "${{ item.title }}" }` |
227
+ | `filter` | Filter items | `filter: item.score > 100` |
228
+ | `sort` | Sort items | `sort: { by: score, order: desc }` |
229
+ | `limit` | Cap result count | `limit: ${{ args.limit }}` |
230
+ | `intercept` | Declarative XHR capture | `intercept: { trigger: "navigate:...", capture: "api/hot" }` |
231
+ | `tap` | Store action + XHR capture | `tap: { store: "feed", action: "fetchFeeds", capture: "homefeed" }` |
232
+ | `snapshot` | Page accessibility tree | `snapshot: { interactive: true }` |
233
+ | `click` | Click element | `click: ${{ ref }}` |
234
+ | `type` | Type text | `type: { ref: "@1", text: "hello" }` |
235
+ | `wait` | Wait for time/text | `wait: 2` or `wait: { text: "loaded" }` |
236
+ | `press` | Press key | `press: Enter` |
237
+
238
+ ## Template Syntax
198
239
 
199
240
  ```yaml
200
- # 引用参数
241
+ # Arguments with defaults
201
242
  ${{ args.keyword }}
202
243
  ${{ args.limit | default(20) }}
203
244
 
204
- # 引用当前 item(在 map/filter 中)
245
+ # Current item (in map/filter)
205
246
  ${{ item.title }}
206
247
  ${{ item.data.nested.field }}
207
248
 
208
- # 索引(从 0 开始)
249
+ # Index (0-based)
209
250
  ${{ index }}
210
251
  ${{ index + 1 }}
211
252
  ```
212
253
 
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` 或检查网络 |
254
+ ## 5-Tier Authentication Strategy
255
+
256
+ | Tier | Name | Method | Example |
257
+ |------|------|--------|---------|
258
+ | 1 | `public` | No auth, Node.js fetch | Hacker News, V2EX |
259
+ | 2 | `cookie` | Browser fetch with `credentials: include` | Bilibili, Zhihu |
260
+ | 3 | `header` | Custom headers (ct0, Bearer) | Twitter GraphQL |
261
+ | 4 | `intercept` | XHR interception + store mutation | 小红书 Pinia |
262
+ | 5 | `ui` | Full UI automation (click/type/scroll) | Last resort |
263
+
264
+ ## Environment Variables
265
+
266
+ | Variable | Default | Description |
267
+ |----------|---------|-------------|
268
+ | `OPENCLI_BROWSER_CONNECT_TIMEOUT` | 30 | Browser connection timeout (sec) |
269
+ | `OPENCLI_BROWSER_COMMAND_TIMEOUT` | 45 | Command execution timeout (sec) |
270
+ | `OPENCLI_BROWSER_EXPLORE_TIMEOUT` | 120 | Explore timeout (sec) |
271
+ | `OPENCLI_EXTENSION_LOCK_TIMEOUT` | 120 | Extension lock timeout (sec) |
272
+ | `PLAYWRIGHT_MCP_EXTENSION_TOKEN` | — | Auto-approve extension connection |
273
+
274
+ ## Troubleshooting
275
+
276
+ | Issue | Solution |
277
+ |-------|----------|
278
+ | `npx not found` | Install Node.js: `brew install node` |
279
+ | `Timed out connecting to browser` | 1) Chrome must be open 2) Install MCP Bridge extension 3) Click to approve |
280
+ | `Extension lock timed out` | Another opencli command is running; browser commands run serially |
281
+ | `Target page context` error | Add `navigate:` step before `evaluate:` in YAML |
282
+ | Empty table data | Check if evaluate returns JSON string (MCP parsing) or data path is wrong |
package/dist/browser.d.ts CHANGED
@@ -39,6 +39,7 @@ export declare class PlaywrightMCP {
39
39
  private _waiters;
40
40
  private _lockAcquired;
41
41
  private _initialTabCount;
42
+ private _page;
42
43
  connect(opts?: {
43
44
  timeout?: number;
44
45
  }): Promise<Page>;
package/dist/browser.js CHANGED
@@ -37,7 +37,18 @@ export class Page {
37
37
  if (result?.content) {
38
38
  const textParts = result.content.filter((c) => c.type === 'text');
39
39
  if (textParts.length === 1) {
40
- const text = textParts[0].text;
40
+ let text = textParts[0].text;
41
+ // MCP browser_evaluate returns: "[JSON]\n### Ran Playwright code\n```js\n...\n```"
42
+ // Strip the "### Ran Playwright code" suffix to get clean JSON
43
+ const codeMarker = text.indexOf('### Ran Playwright code');
44
+ if (codeMarker !== -1) {
45
+ text = text.slice(0, codeMarker).trim();
46
+ }
47
+ // Also handle "### Result\n[JSON]" format (some MCP versions)
48
+ const resultMarker = text.indexOf('### Result\n');
49
+ if (resultMarker !== -1) {
50
+ text = text.slice(resultMarker + '### Result\n'.length).trim();
51
+ }
41
52
  try {
42
53
  return JSON.parse(text);
43
54
  }
@@ -106,6 +117,7 @@ export class PlaywrightMCP {
106
117
  _waiters = [];
107
118
  _lockAcquired = false;
108
119
  _initialTabCount = 0;
120
+ _page = null;
109
121
  async connect(opts = {}) {
110
122
  await this._acquireLock();
111
123
  const timeout = opts.timeout ?? CONNECT_TIMEOUT;
@@ -124,6 +136,7 @@ export class PlaywrightMCP {
124
136
  this._proc.stdout.setMaxListeners(20);
125
137
  const page = new Page((msg) => { if (this._proc?.stdin?.writable)
126
138
  this._proc.stdin.write(msg); }, () => new Promise((res) => { this._waiters.push(res); }));
139
+ this._page = page;
127
140
  this._proc.stdout?.on('data', (chunk) => {
128
141
  this._buffer += chunk.toString();
129
142
  const lines = this._buffer.split('\n');
@@ -174,12 +187,33 @@ export class PlaywrightMCP {
174
187
  }
175
188
  async close() {
176
189
  try {
190
+ // Close tabs opened during this session (site tabs + extension tabs)
191
+ if (this._page && this._proc && !this._proc.killed) {
192
+ try {
193
+ const tabs = await this._page.tabs();
194
+ const tabStr = typeof tabs === 'string' ? tabs : JSON.stringify(tabs);
195
+ const allTabs = tabStr.match(/Tab (\d+)/g) || [];
196
+ const currentTabCount = allTabs.length;
197
+ // Close tabs in reverse order to avoid index shifting issues
198
+ // Keep the original tabs that existed before the command started
199
+ if (currentTabCount > this._initialTabCount && this._initialTabCount > 0) {
200
+ for (let i = currentTabCount - 1; i >= this._initialTabCount; i--) {
201
+ try {
202
+ await this._page.closeTab(i);
203
+ }
204
+ catch { }
205
+ }
206
+ }
207
+ }
208
+ catch { }
209
+ }
177
210
  if (this._proc && !this._proc.killed) {
178
211
  this._proc.kill('SIGTERM');
179
212
  await new Promise((res) => { this._proc?.on('exit', () => res()); setTimeout(res, 3000); });
180
213
  }
181
214
  }
182
215
  finally {
216
+ this._page = null;
183
217
  this._releaseLock();
184
218
  }
185
219
  }
@@ -0,0 +1,45 @@
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
+ interface ProbeResult {
14
+ strategy: Strategy;
15
+ success: boolean;
16
+ statusCode?: number;
17
+ hasData?: boolean;
18
+ error?: string;
19
+ responsePreview?: string;
20
+ }
21
+ interface CascadeResult {
22
+ bestStrategy: Strategy;
23
+ probes: ProbeResult[];
24
+ confidence: number;
25
+ }
26
+ /**
27
+ * Probe an endpoint with a specific strategy.
28
+ * Returns whether the probe succeeded and basic response info.
29
+ */
30
+ export declare function probeEndpoint(page: any, url: string, strategy: Strategy, opts?: {
31
+ timeout?: number;
32
+ }): Promise<ProbeResult>;
33
+ /**
34
+ * Run the cascade: try each strategy in order until one works.
35
+ * Returns the simplest working strategy.
36
+ */
37
+ export declare function cascadeProbe(page: any, url: string, opts?: {
38
+ maxStrategy?: Strategy;
39
+ timeout?: number;
40
+ }): Promise<CascadeResult>;
41
+ /**
42
+ * Render cascade results for display.
43
+ */
44
+ export declare function renderCascadeResult(result: CascadeResult): string;
45
+ export {};
@@ -0,0 +1,180 @@
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
+ /** Strategy cascade order (simplest → most complex) */
14
+ const CASCADE_ORDER = [
15
+ Strategy.PUBLIC,
16
+ Strategy.COOKIE,
17
+ Strategy.HEADER,
18
+ Strategy.INTERCEPT,
19
+ Strategy.UI,
20
+ ];
21
+ /**
22
+ * Probe an endpoint with a specific strategy.
23
+ * Returns whether the probe succeeded and basic response info.
24
+ */
25
+ export async function probeEndpoint(page, url, strategy, opts = {}) {
26
+ const result = { strategy, success: false };
27
+ try {
28
+ switch (strategy) {
29
+ case Strategy.PUBLIC: {
30
+ // Try direct fetch without browser (no credentials)
31
+ const js = `
32
+ async () => {
33
+ try {
34
+ const resp = await fetch(${JSON.stringify(url)});
35
+ const status = resp.status;
36
+ if (!resp.ok) return { status, ok: false };
37
+ const text = await resp.text();
38
+ let hasData = false;
39
+ try {
40
+ const json = JSON.parse(text);
41
+ hasData = !!json && (Array.isArray(json) ? json.length > 0 :
42
+ typeof json === 'object' && Object.keys(json).length > 0);
43
+ } catch {}
44
+ return { status, ok: true, hasData, preview: text.slice(0, 200) };
45
+ } catch (e) { return { ok: false, error: e.message }; }
46
+ }
47
+ `;
48
+ const resp = await page.evaluate(js);
49
+ result.statusCode = resp?.status;
50
+ result.success = resp?.ok && resp?.hasData;
51
+ result.hasData = resp?.hasData;
52
+ result.responsePreview = resp?.preview;
53
+ break;
54
+ }
55
+ case Strategy.COOKIE: {
56
+ // Fetch with credentials: 'include' (uses browser cookies)
57
+ const js = `
58
+ async () => {
59
+ try {
60
+ const resp = await fetch(${JSON.stringify(url)}, { credentials: 'include' });
61
+ const status = resp.status;
62
+ if (!resp.ok) return { status, ok: false };
63
+ const text = await resp.text();
64
+ let hasData = false;
65
+ try {
66
+ const json = JSON.parse(text);
67
+ hasData = !!json && (Array.isArray(json) ? json.length > 0 :
68
+ typeof json === 'object' && Object.keys(json).length > 0);
69
+ // Check for API-level error codes (common in Chinese sites)
70
+ if (json.code !== undefined && json.code !== 0) hasData = false;
71
+ } catch {}
72
+ return { status, ok: true, hasData, preview: text.slice(0, 200) };
73
+ } catch (e) { return { ok: false, error: e.message }; }
74
+ }
75
+ `;
76
+ const resp = await page.evaluate(js);
77
+ result.statusCode = resp?.status;
78
+ result.success = resp?.ok && resp?.hasData;
79
+ result.hasData = resp?.hasData;
80
+ result.responsePreview = resp?.preview;
81
+ break;
82
+ }
83
+ case Strategy.HEADER: {
84
+ // Fetch with credentials + try to extract common auth headers
85
+ const js = `
86
+ async () => {
87
+ try {
88
+ // Try to extract CSRF tokens from cookies
89
+ const cookies = document.cookie.split(';').map(c => c.trim());
90
+ const csrf = cookies.find(c => c.startsWith('ct0=') || c.startsWith('csrf_token=') || c.startsWith('_csrf='))?.split('=').slice(1).join('=');
91
+
92
+ const headers = {};
93
+ if (csrf) {
94
+ headers['X-Csrf-Token'] = csrf;
95
+ headers['X-XSRF-Token'] = csrf;
96
+ }
97
+
98
+ const resp = await fetch(${JSON.stringify(url)}, {
99
+ credentials: 'include',
100
+ headers
101
+ });
102
+ const status = resp.status;
103
+ if (!resp.ok) return { status, ok: false };
104
+ const text = await resp.text();
105
+ let hasData = false;
106
+ try {
107
+ const json = JSON.parse(text);
108
+ hasData = !!json && (Array.isArray(json) ? json.length > 0 :
109
+ typeof json === 'object' && Object.keys(json).length > 0);
110
+ if (json.code !== undefined && json.code !== 0) hasData = false;
111
+ } catch {}
112
+ return { status, ok: true, hasData, preview: text.slice(0, 200) };
113
+ } catch (e) { return { ok: false, error: e.message }; }
114
+ }
115
+ `;
116
+ const resp = await page.evaluate(js);
117
+ result.statusCode = resp?.status;
118
+ result.success = resp?.ok && resp?.hasData;
119
+ result.hasData = resp?.hasData;
120
+ result.responsePreview = resp?.preview;
121
+ break;
122
+ }
123
+ case Strategy.INTERCEPT:
124
+ case Strategy.UI:
125
+ // These require specific implementation per-site
126
+ // Mark as needing manual implementation
127
+ result.success = false;
128
+ result.error = `Strategy ${strategy} requires site-specific implementation`;
129
+ break;
130
+ }
131
+ }
132
+ catch (err) {
133
+ result.success = false;
134
+ result.error = err.message ?? String(err);
135
+ }
136
+ return result;
137
+ }
138
+ /**
139
+ * Run the cascade: try each strategy in order until one works.
140
+ * Returns the simplest working strategy.
141
+ */
142
+ export async function cascadeProbe(page, url, opts = {}) {
143
+ const maxIdx = opts.maxStrategy
144
+ ? CASCADE_ORDER.indexOf(opts.maxStrategy)
145
+ : CASCADE_ORDER.indexOf(Strategy.HEADER); // Don't auto-try INTERCEPT/UI
146
+ const probes = [];
147
+ for (let i = 0; i <= Math.min(maxIdx, CASCADE_ORDER.length - 1); i++) {
148
+ const strategy = CASCADE_ORDER[i];
149
+ const probe = await probeEndpoint(page, url, strategy, opts);
150
+ probes.push(probe);
151
+ if (probe.success) {
152
+ return {
153
+ bestStrategy: strategy,
154
+ probes,
155
+ confidence: 1.0 - (i * 0.1), // Higher confidence for simpler strategies
156
+ };
157
+ }
158
+ }
159
+ // None worked — default to COOKIE (most common for logged-in sites)
160
+ return {
161
+ bestStrategy: Strategy.COOKIE,
162
+ probes,
163
+ confidence: 0.3,
164
+ };
165
+ }
166
+ /**
167
+ * Render cascade results for display.
168
+ */
169
+ export function renderCascadeResult(result) {
170
+ const lines = [
171
+ `Strategy Cascade: ${result.bestStrategy} (${(result.confidence * 100).toFixed(0)}% confidence)`,
172
+ ];
173
+ for (const probe of result.probes) {
174
+ const icon = probe.success ? '✅' : '❌';
175
+ const status = probe.statusCode ? ` [${probe.statusCode}]` : '';
176
+ const err = probe.error ? ` — ${probe.error}` : '';
177
+ lines.push(` ${icon} ${probe.strategy}${status}${err}`);
178
+ }
179
+ return lines.join('\n');
180
+ }