@jackwener/opencli 0.5.1 → 0.6.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/README.md +3 -2
- package/README.zh-CN.md +4 -3
- package/SKILL.md +7 -4
- package/dist/browser.d.ts +7 -3
- package/dist/browser.js +25 -92
- package/dist/browser.test.js +18 -1
- package/dist/cascade.d.ts +1 -1
- package/dist/cascade.js +42 -75
- package/dist/cli-manifest.json +80 -0
- package/dist/clis/coupang/add-to-cart.d.ts +1 -0
- package/dist/clis/coupang/add-to-cart.js +141 -0
- package/dist/clis/coupang/search.d.ts +1 -0
- package/dist/clis/coupang/search.js +453 -0
- package/dist/constants.d.ts +13 -0
- package/dist/constants.js +30 -0
- package/dist/coupang.d.ts +24 -0
- package/dist/coupang.js +262 -0
- package/dist/coupang.test.d.ts +1 -0
- package/dist/coupang.test.js +62 -0
- package/dist/doctor.d.ts +15 -0
- package/dist/doctor.js +226 -25
- package/dist/doctor.test.js +13 -6
- package/dist/engine.js +3 -3
- package/dist/engine.test.d.ts +4 -0
- package/dist/engine.test.js +67 -0
- package/dist/explore.js +1 -15
- package/dist/interceptor.d.ts +42 -0
- package/dist/interceptor.js +138 -0
- package/dist/main.js +8 -4
- package/dist/output.js +0 -5
- package/dist/pipeline/steps/intercept.js +4 -54
- package/dist/pipeline/steps/tap.js +11 -51
- package/dist/registry.d.ts +3 -1
- package/dist/registry.test.d.ts +4 -0
- package/dist/registry.test.js +90 -0
- package/dist/runtime.d.ts +15 -1
- package/dist/runtime.js +11 -6
- package/dist/setup.d.ts +4 -0
- package/dist/setup.js +145 -0
- package/dist/synthesize.js +5 -5
- package/dist/tui.d.ts +22 -0
- package/dist/tui.js +139 -0
- package/dist/validate.js +21 -0
- package/dist/verify.d.ts +7 -0
- package/dist/verify.js +7 -1
- package/dist/version.d.ts +4 -0
- package/dist/version.js +16 -0
- package/package.json +1 -1
- package/src/browser.test.ts +20 -1
- package/src/browser.ts +25 -87
- package/src/cascade.ts +47 -75
- package/src/clis/coupang/add-to-cart.ts +149 -0
- package/src/clis/coupang/search.ts +466 -0
- package/src/constants.ts +35 -0
- package/src/coupang.test.ts +78 -0
- package/src/coupang.ts +302 -0
- package/src/doctor.test.ts +15 -6
- package/src/doctor.ts +221 -25
- package/src/engine.test.ts +77 -0
- package/src/engine.ts +5 -5
- package/src/explore.ts +2 -15
- package/src/interceptor.ts +153 -0
- package/src/main.ts +9 -5
- package/src/output.ts +0 -4
- package/src/pipeline/executor.ts +15 -15
- package/src/pipeline/steps/intercept.ts +4 -55
- package/src/pipeline/steps/tap.ts +12 -51
- package/src/registry.test.ts +106 -0
- package/src/registry.ts +4 -1
- package/src/runtime.ts +22 -8
- package/src/setup.ts +169 -0
- package/src/synthesize.ts +5 -5
- package/src/tui.ts +171 -0
- package/src/validate.ts +22 -0
- package/src/verify.ts +10 -1
- package/src/version.ts +18 -0
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://nodejs.org)
|
|
10
10
|
[](./LICENSE)
|
|
11
11
|
|
|
12
|
-
A CLI tool that turns **any website** into a command-line interface. **
|
|
12
|
+
A CLI tool that turns **any website** into a command-line interface. **59 commands** across **18 sites** — bilibili, zhihu, xiaohongshu, twitter, reddit, xueqiu, github, v2ex, hackernews, bbc, weibo, boss, yahoo-finance, reuters, smzdm, ctrip, youtube, coupang — powered by browser session reuse and AI-native discovery.
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
@@ -126,13 +126,14 @@ npm install -g @jackwener/opencli@latest
|
|
|
126
126
|
| **reddit** | `hot` `frontpage` `search` `subreddit` | 🔐 Browser |
|
|
127
127
|
| **weibo** | `hot` | 🔐 Browser |
|
|
128
128
|
| **boss** | `search` | 🔐 Browser |
|
|
129
|
+
| **coupang** | `search` `add-to-cart` | 🔐 Browser |
|
|
129
130
|
| **youtube** | `search` | 🔐 Browser |
|
|
130
131
|
| **yahoo-finance** | `quote` | 🔐 Browser |
|
|
131
132
|
| **reuters** | `search` | 🔐 Browser |
|
|
132
133
|
| **smzdm** | `search` | 🔐 Browser |
|
|
133
134
|
| **ctrip** | `search` | 🔐 Browser |
|
|
134
135
|
| **github** | `search` | 🌐 Public |
|
|
135
|
-
| **v2ex** | `hot` `latest` `topic` | 🌐 Public |
|
|
136
|
+
| **v2ex** | `hot` `latest` `topic` `daily` `me` `notifications` | 🌐 Public / 🔐 Browser |
|
|
136
137
|
| **hackernews** | `top` | 🌐 Public |
|
|
137
138
|
| **bbc** | `news` | 🌐 Public |
|
|
138
139
|
|
package/README.zh-CN.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://nodejs.org)
|
|
10
10
|
[](./LICENSE)
|
|
11
11
|
|
|
12
|
-
OpenCLI 将任何网站变成命令行工具。**
|
|
12
|
+
OpenCLI 将任何网站变成命令行工具。**59 个命令**覆盖 **18 个站点** — B站、知乎、小红书、Twitter、Reddit、雪球、GitHub、V2EX、Hacker News、BBC、微博、BOSS直聘、Yahoo Finance、路透社、什么值得买、携程、YouTube、Coupang — 复用浏览器登录态,AI 驱动探索。
|
|
13
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
@@ -29,7 +29,7 @@ OpenCLI 将任何网站变成命令行工具。**57 个命令**覆盖 **17 个
|
|
|
29
29
|
|
|
30
30
|
## 亮点
|
|
31
31
|
|
|
32
|
-
- **
|
|
32
|
+
- **59 个命令,18 个站点** — B站、知乎、小红书、Twitter、Reddit、雪球(xueqiu)、GitHub、V2EX、Hacker News、BBC、微博、BOSS直聘、Yahoo Finance、路透社、什么值得买、携程、YouTube、Coupang
|
|
33
33
|
- **零风控** — 复用 Chrome 登录态,无需存储任何凭证
|
|
34
34
|
- **AI 原生** — `explore` 自动发现 API,`synthesize` 生成适配器,`cascade` 探测认证策略
|
|
35
35
|
- **动态加载引擎** — 声明式的 `.yaml` 或者底层定制的 `.ts` 适配器,放入 `clis/` 文件夹即可自动注册生效
|
|
@@ -126,13 +126,14 @@ npm install -g @jackwener/opencli@latest
|
|
|
126
126
|
| **reddit** | `hot` `frontpage` `search` `subreddit` | 🔐 浏览器 |
|
|
127
127
|
| **weibo** | `hot` | 🔐 浏览器 |
|
|
128
128
|
| **boss** | `search` | 🔐 浏览器 |
|
|
129
|
+
| **coupang** | `search` `add-to-cart` | 🔐 浏览器 |
|
|
129
130
|
| **youtube** | `search` | 🔐 浏览器 |
|
|
130
131
|
| **yahoo-finance** | `quote` | 🔐 浏览器 |
|
|
131
132
|
| **reuters** | `search` | 🔐 浏览器 |
|
|
132
133
|
| **smzdm** | `search` | 🔐 浏览器 |
|
|
133
134
|
| **ctrip** | `search` | 🔐 浏览器 |
|
|
134
135
|
| **github** | `search` | 🌐 公共 API |
|
|
135
|
-
| **v2ex** | `hot` `latest` `topic` | 🌐 公共 API |
|
|
136
|
+
| **v2ex** | `hot` `latest` `topic` `daily` `me` `notifications` | 🌐 公共 API / 🔐 浏览器 |
|
|
136
137
|
| **hackernews** | `top` | 🌐 公共 API |
|
|
137
138
|
| **bbc** | `news` | 🌐 公共 API |
|
|
138
139
|
|
package/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: opencli
|
|
3
3
|
description: "OpenCLI — Make any website your CLI. Zero risk, AI-powered, reuse Chrome login."
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.1
|
|
5
5
|
author: jackwener
|
|
6
6
|
tags: [cli, browser, web, mcp, playwright, bilibili, zhihu, twitter, github, v2ex, hackernews, reddit, xiaohongshu, xueqiu, AI, agent]
|
|
7
7
|
---
|
|
@@ -95,10 +95,13 @@ opencli reddit frontpage --limit 10 # 首页
|
|
|
95
95
|
opencli reddit search --keyword "AI" # 搜索
|
|
96
96
|
opencli reddit subreddit --name rust # 子版块浏览
|
|
97
97
|
|
|
98
|
-
# V2EX (public)
|
|
98
|
+
# V2EX (public + browser)
|
|
99
99
|
opencli v2ex hot --limit 10 # 热门话题
|
|
100
100
|
opencli v2ex latest --limit 10 # 最新话题
|
|
101
101
|
opencli v2ex topic --id 1024 # 主题详情
|
|
102
|
+
opencli v2ex daily # 每日签到 (browser)
|
|
103
|
+
opencli v2ex me # 我的信息 (browser)
|
|
104
|
+
opencli v2ex notifications --limit 10 # 通知 (browser)
|
|
102
105
|
|
|
103
106
|
# Hacker News (public)
|
|
104
107
|
opencli hackernews top --limit 10 # Top stories
|
|
@@ -156,8 +159,8 @@ opencli cascade <api-url>
|
|
|
156
159
|
# Explore with interactive fuzzing (click buttons to trigger lazy APIs)
|
|
157
160
|
opencli explore <url> --auto --click "字幕,CC,评论"
|
|
158
161
|
|
|
159
|
-
# Verify:
|
|
160
|
-
opencli verify
|
|
162
|
+
# Verify: validate adapter definitions
|
|
163
|
+
opencli verify
|
|
161
164
|
```
|
|
162
165
|
|
|
163
166
|
## Output Formats
|
package/dist/browser.d.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Browser interaction via Playwright MCP Bridge extension.
|
|
3
3
|
* Connects to an existing Chrome browser through the extension.
|
|
4
4
|
*/
|
|
5
|
+
import { withTimeoutMs } from './runtime.js';
|
|
5
6
|
type ConnectFailureKind = 'missing-token' | 'extension-timeout' | 'extension-not-installed' | 'mcp-init' | 'process-exit' | 'unknown';
|
|
6
7
|
type PlaywrightMCPState = 'idle' | 'connecting' | 'connected' | 'closing' | 'closed';
|
|
7
8
|
type ConnectFailureInput = {
|
|
@@ -29,7 +30,6 @@ export declare class Page implements IPage {
|
|
|
29
30
|
call(method: string, params?: Record<string, any>): Promise<any>;
|
|
30
31
|
goto(url: string): Promise<void>;
|
|
31
32
|
evaluate(js: string): Promise<any>;
|
|
32
|
-
private normalizeEval;
|
|
33
33
|
snapshot(opts?: {
|
|
34
34
|
interactive?: boolean;
|
|
35
35
|
compact?: boolean;
|
|
@@ -90,12 +90,16 @@ declare function diffTabIndexes(initialIdentities: string[], currentTabs: Array<
|
|
|
90
90
|
identity: string;
|
|
91
91
|
}>): number[];
|
|
92
92
|
declare function appendLimited(current: string, chunk: string, limit: number): string;
|
|
93
|
-
declare function
|
|
93
|
+
declare function buildMcpArgs(input: {
|
|
94
|
+
mcpPath: string;
|
|
95
|
+
executablePath?: string | null;
|
|
96
|
+
}): string[];
|
|
94
97
|
export declare const __test__: {
|
|
95
98
|
createJsonRpcRequest: typeof createJsonRpcRequest;
|
|
96
99
|
extractTabEntries: typeof extractTabEntries;
|
|
97
100
|
diffTabIndexes: typeof diffTabIndexes;
|
|
98
101
|
appendLimited: typeof appendLimited;
|
|
99
|
-
|
|
102
|
+
buildMcpArgs: typeof buildMcpArgs;
|
|
103
|
+
withTimeoutMs: typeof withTimeoutMs;
|
|
100
104
|
};
|
|
101
105
|
export {};
|
package/dist/browser.js
CHANGED
|
@@ -9,14 +9,10 @@ import * as fs from 'node:fs';
|
|
|
9
9
|
import * as os from 'node:os';
|
|
10
10
|
import * as path from 'node:path';
|
|
11
11
|
import { formatSnapshot } from './snapshotFormatter.js';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
return '0.0.0';
|
|
19
|
-
} })();
|
|
12
|
+
import { PKG_VERSION } from './version.js';
|
|
13
|
+
import { normalizeEvaluateSource } from './pipeline/template.js';
|
|
14
|
+
import { generateInterceptorJs, generateReadInterceptedJs } from './interceptor.js';
|
|
15
|
+
import { withTimeoutMs } from './runtime.js';
|
|
20
16
|
const CONNECT_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_CONNECT_TIMEOUT ?? '30', 10);
|
|
21
17
|
const STDERR_BUFFER_LIMIT = 16 * 1024;
|
|
22
18
|
const INITIAL_TABS_TIMEOUT_MS = 1500;
|
|
@@ -126,26 +122,9 @@ export class Page {
|
|
|
126
122
|
}
|
|
127
123
|
async evaluate(js) {
|
|
128
124
|
// Normalize IIFE format to function format expected by MCP browser_evaluate
|
|
129
|
-
const normalized =
|
|
125
|
+
const normalized = normalizeEvaluateSource(js);
|
|
130
126
|
return this.call('tools/call', { name: 'browser_evaluate', arguments: { function: normalized } });
|
|
131
127
|
}
|
|
132
|
-
normalizeEval(source) {
|
|
133
|
-
const s = source.trim();
|
|
134
|
-
if (!s)
|
|
135
|
-
return '() => undefined';
|
|
136
|
-
// IIFE: (async () => {...})() → wrap as () => (...)
|
|
137
|
-
if (s.startsWith('(') && s.endsWith(')()'))
|
|
138
|
-
return `() => (${s})`;
|
|
139
|
-
// Already a function/arrow
|
|
140
|
-
if (/^(async\s+)?\([^)]*\)\s*=>/.test(s))
|
|
141
|
-
return s;
|
|
142
|
-
if (/^(async\s+)?[A-Za-z_][A-Za-z0-9_]*\s*=>/.test(s))
|
|
143
|
-
return s;
|
|
144
|
-
if (s.startsWith('function ') || s.startsWith('async function '))
|
|
145
|
-
return s;
|
|
146
|
-
// Raw expression → wrap
|
|
147
|
-
return `() => (${s})`;
|
|
148
|
-
}
|
|
149
128
|
async snapshot(opts = {}) {
|
|
150
129
|
const raw = await this.call('tools/call', { name: 'browser_snapshot', arguments: {} });
|
|
151
130
|
if (opts.raw)
|
|
@@ -224,56 +203,14 @@ export class Page {
|
|
|
224
203
|
await this.evaluate(js);
|
|
225
204
|
}
|
|
226
205
|
async installInterceptor(pattern) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (!window.__opencli_patterns.includes('${pattern}')) {
|
|
232
|
-
window.__opencli_patterns.push('${pattern}');
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (!window.__patched_xhr) {
|
|
236
|
-
const checkMatch = (url) => window.__opencli_patterns.some(p => url.includes(p));
|
|
237
|
-
|
|
238
|
-
const XHR = XMLHttpRequest.prototype;
|
|
239
|
-
const open = XHR.open;
|
|
240
|
-
const send = XHR.send;
|
|
241
|
-
XHR.open = function(method, url) {
|
|
242
|
-
this._url = url;
|
|
243
|
-
return open.call(this, method, url, ...Array.prototype.slice.call(arguments, 2));
|
|
244
|
-
};
|
|
245
|
-
XHR.send = function() {
|
|
246
|
-
this.addEventListener('load', function() {
|
|
247
|
-
if (checkMatch(this._url)) {
|
|
248
|
-
try { window.__opencli_xhr.push({url: this._url, data: JSON.parse(this.responseText)}); } catch(e){}
|
|
249
|
-
}
|
|
250
|
-
});
|
|
251
|
-
return send.apply(this, arguments);
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
const origFetch = window.fetch;
|
|
255
|
-
window.fetch = async function(...args) {
|
|
256
|
-
let u = typeof args[0] === 'string' ? args[0] : (args[0] && args[0].url) || '';
|
|
257
|
-
const res = await origFetch.apply(this, args);
|
|
258
|
-
setTimeout(async () => {
|
|
259
|
-
try {
|
|
260
|
-
if (checkMatch(u)) {
|
|
261
|
-
const clone = res.clone();
|
|
262
|
-
const j = await clone.json();
|
|
263
|
-
window.__opencli_xhr.push({url: u, data: j});
|
|
264
|
-
}
|
|
265
|
-
} catch(e) {}
|
|
266
|
-
}, 0);
|
|
267
|
-
return res;
|
|
268
|
-
};
|
|
269
|
-
window.__patched_xhr = true;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
`;
|
|
273
|
-
await this.evaluate(js);
|
|
206
|
+
await this.evaluate(generateInterceptorJs(JSON.stringify(pattern), {
|
|
207
|
+
arrayName: '__opencli_xhr',
|
|
208
|
+
patchGuard: '__opencli_interceptor_patched',
|
|
209
|
+
}));
|
|
274
210
|
}
|
|
275
211
|
async getInterceptedRequests() {
|
|
276
|
-
|
|
212
|
+
const result = await this.evaluate(generateReadInterceptedJs('__opencli_xhr'));
|
|
213
|
+
return result || [];
|
|
277
214
|
}
|
|
278
215
|
}
|
|
279
216
|
/**
|
|
@@ -402,13 +339,13 @@ export class PlaywrightMCP {
|
|
|
402
339
|
stderr: stderrBuffer,
|
|
403
340
|
}));
|
|
404
341
|
}, timeout * 1000);
|
|
405
|
-
const mcpArgs =
|
|
342
|
+
const mcpArgs = buildMcpArgs({
|
|
343
|
+
mcpPath,
|
|
344
|
+
executablePath: process.env.OPENCLI_BROWSER_EXECUTABLE_PATH,
|
|
345
|
+
});
|
|
406
346
|
if (process.env.OPENCLI_VERBOSE) {
|
|
407
347
|
console.error(`[opencli] Extension token: ${extensionToken ? `configured (fingerprint ${tokenFingerprint})` : 'missing'}`);
|
|
408
348
|
}
|
|
409
|
-
if (process.env.OPENCLI_BROWSER_EXECUTABLE_PATH) {
|
|
410
|
-
mcpArgs.push('--executablePath', process.env.OPENCLI_BROWSER_EXECUTABLE_PATH);
|
|
411
|
-
}
|
|
412
349
|
debugLog(`Spawning node ${mcpArgs.join(' ')}`);
|
|
413
350
|
this._proc = spawn('node', mcpArgs, {
|
|
414
351
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -485,7 +422,7 @@ export class PlaywrightMCP {
|
|
|
485
422
|
this._proc?.stdin?.write(initializedMsg);
|
|
486
423
|
// Use tabs as a readiness probe and for tab cleanup bookkeeping.
|
|
487
424
|
debugLog('Fetching initial tabs count...');
|
|
488
|
-
|
|
425
|
+
withTimeoutMs(page.tabs(), INITIAL_TABS_TIMEOUT_MS, 'Timed out fetching initial tabs').then((tabs) => {
|
|
489
426
|
debugLog(`Tabs response: ${typeof tabs === 'string' ? tabs : JSON.stringify(tabs)}`);
|
|
490
427
|
this._initialTabIdentities = extractTabIdentities(tabs);
|
|
491
428
|
settleSuccess(page);
|
|
@@ -510,7 +447,7 @@ export class PlaywrightMCP {
|
|
|
510
447
|
// Extension mode opens bridge/session tabs that we can clean up best-effort.
|
|
511
448
|
if (this._page && this._proc && !this._proc.killed) {
|
|
512
449
|
try {
|
|
513
|
-
const tabs = await
|
|
450
|
+
const tabs = await withTimeoutMs(this._page.tabs(), TAB_CLEANUP_TIMEOUT_MS, 'Timed out fetching tabs during cleanup');
|
|
514
451
|
const tabEntries = extractTabEntries(tabs);
|
|
515
452
|
const tabsToClose = diffTabIndexes(this._initialTabIdentities, tabEntries);
|
|
516
453
|
for (const index of tabsToClose) {
|
|
@@ -621,24 +558,20 @@ function appendLimited(current, chunk, limit) {
|
|
|
621
558
|
return next;
|
|
622
559
|
return next.slice(-limit);
|
|
623
560
|
}
|
|
624
|
-
function
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
}, (error) => {
|
|
631
|
-
clearTimeout(timer);
|
|
632
|
-
reject(error);
|
|
633
|
-
});
|
|
634
|
-
});
|
|
561
|
+
function buildMcpArgs(input) {
|
|
562
|
+
const args = [input.mcpPath, '--extension'];
|
|
563
|
+
if (input.executablePath) {
|
|
564
|
+
args.push('--executable-path', input.executablePath);
|
|
565
|
+
}
|
|
566
|
+
return args;
|
|
635
567
|
}
|
|
636
568
|
export const __test__ = {
|
|
637
569
|
createJsonRpcRequest,
|
|
638
570
|
extractTabEntries,
|
|
639
571
|
diffTabIndexes,
|
|
640
572
|
appendLimited,
|
|
641
|
-
|
|
573
|
+
buildMcpArgs,
|
|
574
|
+
withTimeoutMs,
|
|
642
575
|
};
|
|
643
576
|
function findMcpServerPath() {
|
|
644
577
|
if (_cachedMcpServerPath !== undefined)
|
package/dist/browser.test.js
CHANGED
|
@@ -34,8 +34,25 @@ describe('browser helpers', () => {
|
|
|
34
34
|
it('keeps only the tail of stderr buffers', () => {
|
|
35
35
|
expect(__test__.appendLimited('12345', '67890', 8)).toBe('34567890');
|
|
36
36
|
});
|
|
37
|
+
it('builds Playwright MCP args with kebab-case executable path', () => {
|
|
38
|
+
expect(__test__.buildMcpArgs({
|
|
39
|
+
mcpPath: '/tmp/cli.js',
|
|
40
|
+
executablePath: '/mnt/c/Program Files/Google/Chrome/Application/chrome.exe',
|
|
41
|
+
})).toEqual([
|
|
42
|
+
'/tmp/cli.js',
|
|
43
|
+
'--extension',
|
|
44
|
+
'--executable-path',
|
|
45
|
+
'/mnt/c/Program Files/Google/Chrome/Application/chrome.exe',
|
|
46
|
+
]);
|
|
47
|
+
expect(__test__.buildMcpArgs({
|
|
48
|
+
mcpPath: '/tmp/cli.js',
|
|
49
|
+
})).toEqual([
|
|
50
|
+
'/tmp/cli.js',
|
|
51
|
+
'--extension',
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
37
54
|
it('times out slow promises', async () => {
|
|
38
|
-
await expect(__test__.
|
|
55
|
+
await expect(__test__.withTimeoutMs(new Promise(() => { }), 10, 'timeout')).rejects.toThrow('timeout');
|
|
39
56
|
});
|
|
40
57
|
});
|
|
41
58
|
describe('PlaywrightMCP state', () => {
|
package/dist/cascade.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ interface CascadeResult {
|
|
|
28
28
|
* Probe an endpoint with a specific strategy.
|
|
29
29
|
* Returns whether the probe succeeded and basic response info.
|
|
30
30
|
*/
|
|
31
|
-
export declare function probeEndpoint(page: IPage, url: string, strategy: Strategy,
|
|
31
|
+
export declare function probeEndpoint(page: IPage, url: string, strategy: Strategy, _opts?: {
|
|
32
32
|
timeout?: number;
|
|
33
33
|
}): Promise<ProbeResult>;
|
|
34
34
|
/**
|
package/dist/cascade.js
CHANGED
|
@@ -18,34 +18,54 @@ const CASCADE_ORDER = [
|
|
|
18
18
|
Strategy.INTERCEPT,
|
|
19
19
|
Strategy.UI,
|
|
20
20
|
];
|
|
21
|
+
/**
|
|
22
|
+
* Build the JavaScript source for a fetch probe.
|
|
23
|
+
* Shared logic for PUBLIC, COOKIE, and HEADER strategies.
|
|
24
|
+
*/
|
|
25
|
+
function buildFetchProbeJs(url, opts) {
|
|
26
|
+
const credentialsLine = opts.credentials ? `credentials: 'include',` : '';
|
|
27
|
+
const headerSetup = opts.extractCsrf
|
|
28
|
+
? `
|
|
29
|
+
const cookies = document.cookie.split(';').map(c => c.trim());
|
|
30
|
+
const csrf = cookies.find(c => c.startsWith('ct0=') || c.startsWith('csrf_token=') || c.startsWith('_csrf='))?.split('=').slice(1).join('=');
|
|
31
|
+
const headers = {};
|
|
32
|
+
if (csrf) { headers['X-Csrf-Token'] = csrf; headers['X-XSRF-Token'] = csrf; }
|
|
33
|
+
`
|
|
34
|
+
: 'const headers = {};';
|
|
35
|
+
return `
|
|
36
|
+
async () => {
|
|
37
|
+
try {
|
|
38
|
+
${headerSetup}
|
|
39
|
+
const resp = await fetch(${JSON.stringify(url)}, {
|
|
40
|
+
${credentialsLine}
|
|
41
|
+
headers
|
|
42
|
+
});
|
|
43
|
+
const status = resp.status;
|
|
44
|
+
if (!resp.ok) return { status, ok: false };
|
|
45
|
+
const text = await resp.text();
|
|
46
|
+
let hasData = false;
|
|
47
|
+
try {
|
|
48
|
+
const json = JSON.parse(text);
|
|
49
|
+
hasData = !!json && (Array.isArray(json) ? json.length > 0 :
|
|
50
|
+
typeof json === 'object' && Object.keys(json).length > 0);
|
|
51
|
+
// Check for API-level error codes (common in Chinese sites)
|
|
52
|
+
if (json.code !== undefined && json.code !== 0) hasData = false;
|
|
53
|
+
} catch {}
|
|
54
|
+
return { status, ok: true, hasData, preview: text.slice(0, 200) };
|
|
55
|
+
} catch (e) { return { ok: false, error: e.message }; }
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
}
|
|
21
59
|
/**
|
|
22
60
|
* Probe an endpoint with a specific strategy.
|
|
23
61
|
* Returns whether the probe succeeded and basic response info.
|
|
24
62
|
*/
|
|
25
|
-
export async function probeEndpoint(page, url, strategy,
|
|
63
|
+
export async function probeEndpoint(page, url, strategy, _opts = {}) {
|
|
26
64
|
const result = { strategy, success: false };
|
|
27
65
|
try {
|
|
28
66
|
switch (strategy) {
|
|
29
67
|
case Strategy.PUBLIC: {
|
|
30
|
-
|
|
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);
|
|
68
|
+
const resp = await page.evaluate(buildFetchProbeJs(url, {}));
|
|
49
69
|
result.statusCode = resp?.status;
|
|
50
70
|
result.success = resp?.ok && resp?.hasData;
|
|
51
71
|
result.hasData = resp?.hasData;
|
|
@@ -53,27 +73,7 @@ export async function probeEndpoint(page, url, strategy, opts = {}) {
|
|
|
53
73
|
break;
|
|
54
74
|
}
|
|
55
75
|
case Strategy.COOKIE: {
|
|
56
|
-
|
|
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);
|
|
76
|
+
const resp = await page.evaluate(buildFetchProbeJs(url, { credentials: true }));
|
|
77
77
|
result.statusCode = resp?.status;
|
|
78
78
|
result.success = resp?.ok && resp?.hasData;
|
|
79
79
|
result.hasData = resp?.hasData;
|
|
@@ -81,39 +81,7 @@ export async function probeEndpoint(page, url, strategy, opts = {}) {
|
|
|
81
81
|
break;
|
|
82
82
|
}
|
|
83
83
|
case Strategy.HEADER: {
|
|
84
|
-
|
|
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);
|
|
84
|
+
const resp = await page.evaluate(buildFetchProbeJs(url, { credentials: true, extractCsrf: true }));
|
|
117
85
|
result.statusCode = resp?.status;
|
|
118
86
|
result.success = resp?.ok && resp?.hasData;
|
|
119
87
|
result.hasData = resp?.hasData;
|
|
@@ -123,7 +91,6 @@ export async function probeEndpoint(page, url, strategy, opts = {}) {
|
|
|
123
91
|
case Strategy.INTERCEPT:
|
|
124
92
|
case Strategy.UI:
|
|
125
93
|
// These require specific implementation per-site
|
|
126
|
-
// Mark as needing manual implementation
|
|
127
94
|
result.success = false;
|
|
128
95
|
result.error = `Strategy ${strategy} requires site-specific implementation`;
|
|
129
96
|
break;
|
package/dist/cli-manifest.json
CHANGED
|
@@ -470,6 +470,86 @@
|
|
|
470
470
|
"url"
|
|
471
471
|
]
|
|
472
472
|
},
|
|
473
|
+
{
|
|
474
|
+
"site": "coupang",
|
|
475
|
+
"name": "add-to-cart",
|
|
476
|
+
"description": "Add a Coupang product to cart using logged-in browser session",
|
|
477
|
+
"strategy": "cookie",
|
|
478
|
+
"browser": true,
|
|
479
|
+
"args": [
|
|
480
|
+
{
|
|
481
|
+
"name": "productId",
|
|
482
|
+
"type": "str",
|
|
483
|
+
"required": false,
|
|
484
|
+
"help": "Coupang product ID"
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
"name": "url",
|
|
488
|
+
"type": "str",
|
|
489
|
+
"required": false,
|
|
490
|
+
"help": "Canonical product URL"
|
|
491
|
+
}
|
|
492
|
+
],
|
|
493
|
+
"type": "ts",
|
|
494
|
+
"modulePath": "coupang/add-to-cart.js",
|
|
495
|
+
"domain": "www.coupang.com",
|
|
496
|
+
"columns": [
|
|
497
|
+
"ok",
|
|
498
|
+
"product_id",
|
|
499
|
+
"url",
|
|
500
|
+
"message"
|
|
501
|
+
]
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
"site": "coupang",
|
|
505
|
+
"name": "search",
|
|
506
|
+
"description": "Search Coupang products with logged-in browser session",
|
|
507
|
+
"strategy": "cookie",
|
|
508
|
+
"browser": true,
|
|
509
|
+
"args": [
|
|
510
|
+
{
|
|
511
|
+
"name": "query",
|
|
512
|
+
"type": "str",
|
|
513
|
+
"required": true,
|
|
514
|
+
"help": "Search keyword"
|
|
515
|
+
},
|
|
516
|
+
{
|
|
517
|
+
"name": "page",
|
|
518
|
+
"type": "int",
|
|
519
|
+
"default": 1,
|
|
520
|
+
"required": false,
|
|
521
|
+
"help": "Search result page number"
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
"name": "limit",
|
|
525
|
+
"type": "int",
|
|
526
|
+
"default": 20,
|
|
527
|
+
"required": false,
|
|
528
|
+
"help": "Max results (max 50)"
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
"name": "filter",
|
|
532
|
+
"type": "str",
|
|
533
|
+
"required": false,
|
|
534
|
+
"help": "Optional search filter (currently supports: rocket)"
|
|
535
|
+
}
|
|
536
|
+
],
|
|
537
|
+
"type": "ts",
|
|
538
|
+
"modulePath": "coupang/search.js",
|
|
539
|
+
"domain": "www.coupang.com",
|
|
540
|
+
"columns": [
|
|
541
|
+
"rank",
|
|
542
|
+
"title",
|
|
543
|
+
"price",
|
|
544
|
+
"unit_price",
|
|
545
|
+
"rating",
|
|
546
|
+
"review_count",
|
|
547
|
+
"rocket",
|
|
548
|
+
"delivery_type",
|
|
549
|
+
"delivery_promise",
|
|
550
|
+
"url"
|
|
551
|
+
]
|
|
552
|
+
},
|
|
473
553
|
{
|
|
474
554
|
"site": "ctrip",
|
|
475
555
|
"name": "search",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|