@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/src/synthesize.ts
CHANGED
|
@@ -1,167 +1,185 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Synthesize
|
|
3
|
-
*
|
|
4
|
-
* Takes the structured capabilities from Deep Explore and generates
|
|
5
|
-
* YAML pipeline files that can be directly registered as CLI commands.
|
|
6
|
-
*
|
|
7
|
-
* This is the bridge between discovery (explore) and usability (CLI).
|
|
2
|
+
* Synthesize candidate CLIs from explore artifacts.
|
|
3
|
+
* Generates evaluate-based YAML pipelines (matching hand-written adapter patterns).
|
|
8
4
|
*/
|
|
9
5
|
|
|
10
6
|
import * as fs from 'node:fs';
|
|
11
7
|
import * as path from 'node:path';
|
|
12
8
|
import yaml from 'js-yaml';
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
/** Volatile params to strip from generated URLs */
|
|
11
|
+
const VOLATILE_PARAMS = new Set(['w_rid', 'wts', 'callback', '_', 'timestamp', 't', 'nonce', 'sign']);
|
|
12
|
+
const SEARCH_PARAM_NAMES = new Set(['q', 'query', 'keyword', 'search', 'wd', 'kw', 'w', 'search_query']);
|
|
13
|
+
const LIMIT_PARAM_NAMES = new Set(['ps', 'page_size', 'limit', 'count', 'per_page', 'size', 'num']);
|
|
14
|
+
const PAGE_PARAM_NAMES = new Set(['pn', 'page', 'page_num', 'offset', 'cursor']);
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
export function synthesizeFromExplore(
|
|
17
|
+
target: string,
|
|
18
|
+
opts: { outDir?: string; top?: number } = {},
|
|
19
|
+
): Record<string, any> {
|
|
20
|
+
const exploreDir = resolveExploreDir(target);
|
|
21
|
+
const bundle = loadExploreBundle(exploreDir);
|
|
22
22
|
|
|
23
23
|
const targetDir = opts.outDir ?? path.join(exploreDir, 'candidates');
|
|
24
24
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
25
25
|
|
|
26
|
-
const site = manifest.site;
|
|
27
|
-
const
|
|
28
|
-
const candidates: any[] = [];
|
|
29
|
-
|
|
30
|
-
// Sort capabilities by confidence
|
|
31
|
-
const sortedCaps = [...capabilities]
|
|
26
|
+
const site = bundle.manifest.site;
|
|
27
|
+
const capabilities = (bundle.capabilities ?? [])
|
|
32
28
|
.sort((a: any, b: any) => (b.confidence ?? 0) - (a.confidence ?? 0))
|
|
33
|
-
.slice(0,
|
|
34
|
-
|
|
35
|
-
for (const cap of sortedCaps) {
|
|
36
|
-
// Find the matching endpoint for more detail
|
|
37
|
-
const endpoint = endpoints.find((ep: any) => ep.pattern === cap.endpoint) ??
|
|
38
|
-
endpoints[0];
|
|
29
|
+
.slice(0, opts.top ?? 3);
|
|
30
|
+
const candidates: any[] = [];
|
|
39
31
|
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
32
|
+
for (const cap of capabilities) {
|
|
33
|
+
const endpoint = chooseEndpoint(cap, bundle.endpoints);
|
|
34
|
+
if (!endpoint) continue;
|
|
35
|
+
const candidate = buildCandidateYaml(site, bundle.manifest, cap, endpoint);
|
|
36
|
+
const filePath = path.join(targetDir, `${candidate.name}.yaml`);
|
|
43
37
|
fs.writeFileSync(filePath, yaml.dump(candidate.yaml, { sortKeys: false, lineWidth: 120 }));
|
|
44
|
-
|
|
45
|
-
candidates.push({
|
|
46
|
-
name: cap.name,
|
|
47
|
-
path: filePath,
|
|
48
|
-
strategy: cap.strategy,
|
|
49
|
-
endpoint: cap.endpoint,
|
|
50
|
-
confidence: cap.confidence,
|
|
51
|
-
columns: candidate.yaml.columns,
|
|
52
|
-
});
|
|
38
|
+
candidates.push({ name: candidate.name, path: filePath, strategy: cap.strategy, confidence: cap.confidence });
|
|
53
39
|
}
|
|
54
40
|
|
|
55
|
-
const index = {
|
|
56
|
-
site,
|
|
57
|
-
target_url: manifest.target_url,
|
|
58
|
-
generated_from: exploreDir,
|
|
59
|
-
candidate_count: candidates.length,
|
|
60
|
-
candidates,
|
|
61
|
-
};
|
|
41
|
+
const index = { site, target_url: bundle.manifest.target_url, generated_from: exploreDir, candidate_count: candidates.length, candidates };
|
|
62
42
|
fs.writeFileSync(path.join(targetDir, 'candidates.json'), JSON.stringify(index, null, 2));
|
|
63
43
|
|
|
44
|
+
return { site, explore_dir: exploreDir, out_dir: targetDir, candidate_count: candidates.length, candidates };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function renderSynthesizeSummary(result: Record<string, any>): string {
|
|
48
|
+
const lines = ['opencli synthesize: OK', `Site: ${result.site}`, `Source: ${result.explore_dir}`, `Candidates: ${result.candidate_count}`];
|
|
49
|
+
for (const c of result.candidates ?? []) lines.push(` • ${c.name} (${c.strategy}, ${((c.confidence ?? 0) * 100).toFixed(0)}% confidence) → ${c.path}`);
|
|
50
|
+
return lines.join('\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function resolveExploreDir(target: string): string {
|
|
54
|
+
if (fs.existsSync(target)) return target;
|
|
55
|
+
const candidate = path.join('.opencli', 'explore', target);
|
|
56
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
57
|
+
throw new Error(`Explore directory not found: ${target}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function loadExploreBundle(exploreDir: string): Record<string, any> {
|
|
64
61
|
return {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
candidates,
|
|
62
|
+
manifest: JSON.parse(fs.readFileSync(path.join(exploreDir, 'manifest.json'), 'utf-8')),
|
|
63
|
+
endpoints: JSON.parse(fs.readFileSync(path.join(exploreDir, 'endpoints.json'), 'utf-8')),
|
|
64
|
+
capabilities: JSON.parse(fs.readFileSync(path.join(exploreDir, 'capabilities.json'), 'utf-8')),
|
|
65
|
+
auth: JSON.parse(fs.readFileSync(path.join(exploreDir, 'auth.json'), 'utf-8')),
|
|
70
66
|
};
|
|
71
67
|
}
|
|
72
68
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
69
|
+
function chooseEndpoint(cap: any, endpoints: any[]): any | null {
|
|
70
|
+
if (!endpoints.length) return null;
|
|
71
|
+
// Match by endpoint pattern from capability
|
|
72
|
+
if (cap.endpoint) {
|
|
73
|
+
const match = endpoints.find((e: any) => e.pattern === cap.endpoint || e.url?.includes(cap.endpoint));
|
|
74
|
+
if (match) return match;
|
|
75
|
+
}
|
|
76
|
+
return endpoints.sort((a: any, b: any) => (b.score ?? 0) - (a.score ?? 0))[0];
|
|
77
|
+
}
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
* - Templates search, limit, and pagination params
|
|
83
|
-
* - Builds URL string manually to avoid URL encoding of ${{ }} expressions
|
|
84
|
-
*/
|
|
85
|
-
function buildTemplatedUrl(rawUrl: string, cap: any, endpoint: any): string {
|
|
79
|
+
// ── URL templating ─────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
function buildTemplatedUrl(rawUrl: string, cap: any, _endpoint: any): string {
|
|
86
82
|
try {
|
|
87
83
|
const u = new URL(rawUrl);
|
|
88
84
|
const base = `${u.protocol}//${u.host}${u.pathname}`;
|
|
89
85
|
const params: Array<[string, string]> = [];
|
|
90
|
-
|
|
91
86
|
const hasKeyword = cap.recommendedArgs?.some((a: any) => a.name === 'keyword');
|
|
92
87
|
|
|
93
88
|
u.searchParams.forEach((v, k) => {
|
|
94
|
-
// Skip volatile params
|
|
95
89
|
if (VOLATILE_PARAMS.has(k)) return;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
} else if (LIMIT_PARAM_NAMES.has(k)) {
|
|
101
|
-
params.push([k, '${{ args.limit | default(20) }}']);
|
|
102
|
-
} else if (PAGE_PARAM_NAMES.has(k)) {
|
|
103
|
-
params.push([k, '${{ args.page | default(1) }}']);
|
|
104
|
-
} else {
|
|
105
|
-
params.push([k, v]);
|
|
106
|
-
}
|
|
90
|
+
if (hasKeyword && SEARCH_PARAM_NAMES.has(k)) params.push([k, '${{ args.keyword }}']);
|
|
91
|
+
else if (LIMIT_PARAM_NAMES.has(k)) params.push([k, '${{ args.limit | default(20) }}']);
|
|
92
|
+
else if (PAGE_PARAM_NAMES.has(k)) params.push([k, '${{ args.page | default(1) }}']);
|
|
93
|
+
else params.push([k, v]);
|
|
107
94
|
});
|
|
108
95
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
} catch {
|
|
112
|
-
return rawUrl;
|
|
113
|
-
}
|
|
96
|
+
return params.length ? base + '?' + params.map(([k, v]) => `${k}=${v}`).join('&') : base;
|
|
97
|
+
} catch { return rawUrl; }
|
|
114
98
|
}
|
|
115
99
|
|
|
116
100
|
/**
|
|
117
|
-
* Build
|
|
101
|
+
* Build inline evaluate script for browser-based fetch+parse.
|
|
102
|
+
* Follows patterns from bilibili/hot.yaml and twitter/trending.yaml.
|
|
118
103
|
*/
|
|
104
|
+
function buildEvaluateScript(url: string, itemPath: string, endpoint: any): string {
|
|
105
|
+
const pathChain = itemPath.split('.').map((p: string) => `?.${p}`).join('');
|
|
106
|
+
const detectedFields = endpoint?.detectedFields ?? {};
|
|
107
|
+
const hasFields = Object.keys(detectedFields).length > 0;
|
|
108
|
+
|
|
109
|
+
let mapCode = '';
|
|
110
|
+
if (hasFields) {
|
|
111
|
+
const mappings = Object.entries(detectedFields)
|
|
112
|
+
.map(([role, field]) => ` ${role}: item${String(field).split('.').map(p => `?.${p}`).join('')}`)
|
|
113
|
+
.join(',\n');
|
|
114
|
+
mapCode = `.map((item) => ({\n${mappings}\n }))`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return [
|
|
118
|
+
'(async () => {',
|
|
119
|
+
` const res = await fetch('${url}', {`,
|
|
120
|
+
` credentials: 'include'`,
|
|
121
|
+
' });',
|
|
122
|
+
' const data = await res.json();',
|
|
123
|
+
` return (data${pathChain} || [])${mapCode};`,
|
|
124
|
+
'})()\n',
|
|
125
|
+
].join('\n');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── YAML pipeline generation ───────────────────────────────────────────────
|
|
129
|
+
|
|
119
130
|
function buildCandidateYaml(site: string, manifest: any, cap: any, endpoint: any): { name: string; yaml: any } {
|
|
120
131
|
const needsBrowser = cap.strategy !== 'public';
|
|
121
132
|
const pipeline: any[] = [];
|
|
133
|
+
const templatedUrl = buildTemplatedUrl(endpoint?.url ?? manifest.target_url, cap, endpoint);
|
|
122
134
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
pipeline.push({ navigate: manifest.target_url });
|
|
126
|
-
}
|
|
135
|
+
let domain = '';
|
|
136
|
+
try { domain = new URL(manifest.target_url).hostname; } catch {}
|
|
127
137
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
138
|
+
if (cap.strategy === 'store-action' && cap.storeHint) {
|
|
139
|
+
// Store Action: navigate + wait + tap (declarative, clean)
|
|
140
|
+
pipeline.push({ navigate: manifest.target_url });
|
|
141
|
+
pipeline.push({ wait: 3 });
|
|
142
|
+
const tapStep: Record<string, any> = {
|
|
143
|
+
store: cap.storeHint.store,
|
|
144
|
+
action: cap.storeHint.action,
|
|
145
|
+
timeout: 8,
|
|
146
|
+
};
|
|
147
|
+
// Infer capture pattern from endpoint URL
|
|
148
|
+
if (endpoint?.url) {
|
|
149
|
+
try {
|
|
150
|
+
const epUrl = new URL(endpoint.url);
|
|
151
|
+
const pathParts = epUrl.pathname.split('/').filter((p: string) => p);
|
|
152
|
+
// Use last meaningful path segment as capture pattern
|
|
153
|
+
const capturePart = pathParts.filter((p: string) => !p.match(/^v\d+$/)).pop();
|
|
154
|
+
if (capturePart) tapStep.capture = capturePart;
|
|
155
|
+
} catch {}
|
|
156
|
+
}
|
|
157
|
+
if (cap.itemPath) tapStep.select = cap.itemPath;
|
|
158
|
+
pipeline.push({ tap: tapStep });
|
|
159
|
+
} else if (needsBrowser) {
|
|
160
|
+
// Browser-based: navigate + evaluate (like bilibili/hot.yaml, twitter/trending.yaml)
|
|
161
|
+
pipeline.push({ navigate: manifest.target_url });
|
|
162
|
+
const itemPath = cap.itemPath ?? 'data.data.list';
|
|
163
|
+
pipeline.push({ evaluate: buildEvaluateScript(templatedUrl, itemPath, endpoint) });
|
|
164
|
+
} else {
|
|
165
|
+
// Public API: direct fetch (like hackernews/top.yaml)
|
|
166
|
+
pipeline.push({ fetch: { url: templatedUrl } });
|
|
167
|
+
if (cap.itemPath) pipeline.push({ select: cap.itemPath });
|
|
137
168
|
}
|
|
138
169
|
|
|
139
|
-
//
|
|
170
|
+
// Map fields
|
|
140
171
|
const mapStep: Record<string, string> = {};
|
|
141
172
|
const columns = cap.recommendedColumns ?? ['title', 'url'];
|
|
142
|
-
|
|
143
|
-
// Add a rank column if not doing search
|
|
144
|
-
if (!cap.recommendedArgs?.some((a: any) => a.name === 'keyword')) {
|
|
145
|
-
mapStep['rank'] = '${{ index + 1 }}';
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Build field mappings from the endpoint's detected fields
|
|
173
|
+
if (!cap.recommendedArgs?.some((a: any) => a.name === 'keyword')) mapStep['rank'] = '${{ index + 1 }}';
|
|
149
174
|
const detectedFields = endpoint?.detectedFields ?? {};
|
|
150
175
|
for (const col of columns) {
|
|
151
176
|
const fieldPath = detectedFields[col];
|
|
152
|
-
|
|
153
|
-
mapStep[col] = `\${{ item.${fieldPath} }}`;
|
|
154
|
-
} else {
|
|
155
|
-
mapStep[col] = `\${{ item.${col} }}`;
|
|
156
|
-
}
|
|
177
|
+
mapStep[col] = fieldPath ? `\${{ item.${fieldPath} }}` : `\${{ item.${col} }}`;
|
|
157
178
|
}
|
|
158
|
-
|
|
159
179
|
pipeline.push({ map: mapStep });
|
|
160
|
-
|
|
161
|
-
// Step 5: Limit
|
|
162
180
|
pipeline.push({ limit: '${{ args.limit | default(20) }}' });
|
|
163
181
|
|
|
164
|
-
//
|
|
182
|
+
// Args
|
|
165
183
|
const argsDef: Record<string, any> = {};
|
|
166
184
|
for (const arg of cap.recommendedArgs ?? []) {
|
|
167
185
|
const def: any = { type: arg.type ?? 'str' };
|
|
@@ -172,39 +190,26 @@ function buildCandidateYaml(site: string, manifest: any, cap: any, endpoint: any
|
|
|
172
190
|
else if (arg.name === 'page') def.description = 'Page number';
|
|
173
191
|
argsDef[arg.name] = def;
|
|
174
192
|
}
|
|
175
|
-
|
|
176
|
-
// Ensure limit arg always exists
|
|
177
|
-
if (!argsDef['limit']) {
|
|
178
|
-
argsDef['limit'] = { type: 'int', default: 20, description: 'Number of items to return' };
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const allColumns = Object.keys(mapStep);
|
|
193
|
+
if (!argsDef['limit']) argsDef['limit'] = { type: 'int', default: 20, description: 'Number of items to return' };
|
|
182
194
|
|
|
183
195
|
return {
|
|
184
196
|
name: cap.name,
|
|
185
197
|
yaml: {
|
|
186
|
-
site,
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
domain: manifest.final_url ? new URL(manifest.final_url).hostname : undefined,
|
|
190
|
-
strategy: cap.strategy,
|
|
191
|
-
browser: needsBrowser,
|
|
192
|
-
args: argsDef,
|
|
193
|
-
pipeline,
|
|
194
|
-
columns: allColumns,
|
|
198
|
+
site, name: cap.name, description: `${cap.description || site + ' ' + cap.name} (auto-generated)`,
|
|
199
|
+
domain, strategy: cap.strategy, browser: needsBrowser,
|
|
200
|
+
args: argsDef, pipeline, columns: Object.keys(mapStep),
|
|
195
201
|
},
|
|
196
202
|
};
|
|
197
203
|
}
|
|
198
204
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
return lines.join('\n');
|
|
205
|
+
/** Backward-compatible export for scaffold.ts */
|
|
206
|
+
export function buildCandidate(site: string, targetUrl: string, cap: any, endpoint: any): any {
|
|
207
|
+
// Map old-style field names to new ones
|
|
208
|
+
const normalizedCap = {
|
|
209
|
+
...cap,
|
|
210
|
+
recommendedArgs: cap.recommendedArgs ?? cap.recommended_args,
|
|
211
|
+
recommendedColumns: cap.recommendedColumns ?? cap.recommended_columns,
|
|
212
|
+
};
|
|
213
|
+
const manifest = { target_url: targetUrl, final_url: targetUrl };
|
|
214
|
+
return buildCandidateYaml(site, manifest, normalizedCap, endpoint);
|
|
210
215
|
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page interface: type-safe abstraction over Playwright MCP browser page.
|
|
3
|
+
*
|
|
4
|
+
* All pipeline steps and CLI adapters should use this interface
|
|
5
|
+
* instead of `any` for browser interactions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface IPage {
|
|
9
|
+
goto(url: string): Promise<void>;
|
|
10
|
+
evaluate(js: string): Promise<any>;
|
|
11
|
+
snapshot(opts?: { interactive?: boolean; compact?: boolean; maxDepth?: number; raw?: boolean }): Promise<any>;
|
|
12
|
+
click(ref: string): Promise<void>;
|
|
13
|
+
typeText(ref: string, text: string): Promise<void>;
|
|
14
|
+
pressKey(key: string): Promise<void>;
|
|
15
|
+
wait(seconds: number): Promise<void>;
|
|
16
|
+
tabs(): Promise<any>;
|
|
17
|
+
closeTab(index?: number): Promise<void>;
|
|
18
|
+
newTab(): Promise<void>;
|
|
19
|
+
selectTab(index: number): Promise<void>;
|
|
20
|
+
networkRequests(includeStatic?: boolean): Promise<any>;
|
|
21
|
+
consoleMessages(level?: string): Promise<any>;
|
|
22
|
+
scroll(direction?: string, amount?: number): Promise<void>;
|
|
23
|
+
}
|
package/vitest.config.ts
ADDED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { cli, Strategy } from '../../registry.js';
|
|
2
|
-
cli({
|
|
3
|
-
site: 'github', name: 'search', description: 'Search GitHub repositories', domain: 'github.com', strategy: Strategy.PUBLIC, browser: false,
|
|
4
|
-
args: [
|
|
5
|
-
{ name: 'keyword', required: true, help: 'Search keyword' },
|
|
6
|
-
{ name: 'sort', default: 'stars', help: 'Sort by: stars, forks, updated' },
|
|
7
|
-
{ name: 'limit', type: 'int', default: 20, help: 'Number of results' },
|
|
8
|
-
],
|
|
9
|
-
columns: ['rank', 'name', 'stars', 'language', 'description'],
|
|
10
|
-
func: async (_page, kwargs) => {
|
|
11
|
-
const { keyword, sort = 'stars', limit = 20 } = kwargs;
|
|
12
|
-
const resp = await fetch(`https://api.github.com/search/repositories?${new URLSearchParams({ q: keyword, sort, order: 'desc', per_page: String(Math.min(Number(limit), 100)) })}`, {
|
|
13
|
-
headers: { Accept: 'application/vnd.github.v3+json', 'User-Agent': 'opencli/0.1' },
|
|
14
|
-
});
|
|
15
|
-
const data = await resp.json();
|
|
16
|
-
return (data.items ?? []).slice(0, Number(limit)).map((item, i) => ({
|
|
17
|
-
rank: i + 1, name: item.full_name, stars: item.stargazers_count, language: item.language ?? '', description: (item.description ?? '').slice(0, 80),
|
|
18
|
-
}));
|
|
19
|
-
},
|
|
20
|
-
});
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { cli, Strategy } from '../../registry.js';
|
|
2
|
-
import { fetchJson } from '../../bilibili.js';
|
|
3
|
-
cli({
|
|
4
|
-
site: 'zhihu',
|
|
5
|
-
name: 'search',
|
|
6
|
-
description: '搜索知乎问题和回答',
|
|
7
|
-
domain: 'www.zhihu.com',
|
|
8
|
-
strategy: Strategy.COOKIE,
|
|
9
|
-
args: [
|
|
10
|
-
{ name: 'keyword', required: true, help: 'Search keyword' },
|
|
11
|
-
{ name: 'type', default: 'general', help: 'general, article, video' },
|
|
12
|
-
{ name: 'limit', type: 'int', default: 20, help: 'Number of results' },
|
|
13
|
-
],
|
|
14
|
-
columns: ['rank', 'title', 'author', 'type', 'url'],
|
|
15
|
-
func: async (page, kwargs) => {
|
|
16
|
-
const { keyword, type = 'general', limit = 20 } = kwargs;
|
|
17
|
-
// Navigate to zhihu to ensure cookie context
|
|
18
|
-
await page.goto('https://www.zhihu.com');
|
|
19
|
-
const qs = new URLSearchParams({ q: keyword, type, limit: String(limit) });
|
|
20
|
-
const payload = await fetchJson(page, `https://www.zhihu.com/api/v4/search_v3?${qs}`);
|
|
21
|
-
const data = payload?.data ?? [];
|
|
22
|
-
const rows = [];
|
|
23
|
-
for (let i = 0; i < Math.min(data.length, Number(limit)); i++) {
|
|
24
|
-
const item = data[i];
|
|
25
|
-
const obj = item.object ?? item;
|
|
26
|
-
const itemType = item.type ?? obj.type ?? 'unknown';
|
|
27
|
-
let title = '';
|
|
28
|
-
let author = '';
|
|
29
|
-
let url = '';
|
|
30
|
-
if (itemType === 'search_result') {
|
|
31
|
-
const highlight = obj.highlight ?? {};
|
|
32
|
-
title = (highlight.title ?? obj.title ?? '').replace(/<[^>]+>/g, '');
|
|
33
|
-
author = obj.author?.name ?? '';
|
|
34
|
-
url = obj.url ?? '';
|
|
35
|
-
}
|
|
36
|
-
else if (obj.question) {
|
|
37
|
-
title = (obj.question.title ?? obj.title ?? '').replace(/<[^>]+>/g, '');
|
|
38
|
-
author = obj.author?.name ?? '';
|
|
39
|
-
url = obj.question.url ? `https://www.zhihu.com/question/${obj.question.id}` : '';
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
title = (obj.title ?? obj.name ?? '').replace(/<[^>]+>/g, '');
|
|
43
|
-
author = obj.author?.name ?? '';
|
|
44
|
-
url = obj.url ?? '';
|
|
45
|
-
}
|
|
46
|
-
if (!title)
|
|
47
|
-
continue;
|
|
48
|
-
rows.push({
|
|
49
|
-
rank: rows.length + 1,
|
|
50
|
-
title: title.slice(0, 60),
|
|
51
|
-
author,
|
|
52
|
-
type: itemType.replace('search_result', 'result'),
|
|
53
|
-
url,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
return rows;
|
|
57
|
-
},
|
|
58
|
-
});
|
package/dist/promote.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function promoteCandidate(opts: any): any;
|
package/dist/promote.js
DELETED
package/dist/register.d.ts
DELETED
package/dist/register.js
DELETED
package/dist/scaffold.d.ts
DELETED
package/dist/scaffold.js
DELETED
package/dist/smoke.d.ts
DELETED
package/dist/smoke.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { cli, Strategy } from '../../registry.js';
|
|
2
|
-
|
|
3
|
-
cli({
|
|
4
|
-
site: 'github', name: 'search', description: 'Search GitHub repositories', domain: 'github.com', strategy: Strategy.PUBLIC, browser: false,
|
|
5
|
-
args: [
|
|
6
|
-
{ name: 'keyword', required: true, help: 'Search keyword' },
|
|
7
|
-
{ name: 'sort', default: 'stars', help: 'Sort by: stars, forks, updated' },
|
|
8
|
-
{ name: 'limit', type: 'int', default: 20, help: 'Number of results' },
|
|
9
|
-
],
|
|
10
|
-
columns: ['rank', 'name', 'stars', 'language', 'description'],
|
|
11
|
-
func: async (_page, kwargs) => {
|
|
12
|
-
const { keyword, sort = 'stars', limit = 20 } = kwargs;
|
|
13
|
-
const resp = await fetch(`https://api.github.com/search/repositories?${new URLSearchParams({ q: keyword, sort, order: 'desc', per_page: String(Math.min(Number(limit), 100)) })}`, {
|
|
14
|
-
headers: { Accept: 'application/vnd.github.v3+json', 'User-Agent': 'opencli/0.1' },
|
|
15
|
-
});
|
|
16
|
-
const data = await resp.json() as any;
|
|
17
|
-
return (data.items ?? []).slice(0, Number(limit)).map((item: any, i: number) => ({
|
|
18
|
-
rank: i + 1, name: item.full_name, stars: item.stargazers_count, language: item.language ?? '', description: (item.description ?? '').slice(0, 80),
|
|
19
|
-
}));
|
|
20
|
-
},
|
|
21
|
-
});
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
site: github
|
|
2
|
-
name: trending
|
|
3
|
-
description: GitHub trending repositories
|
|
4
|
-
domain: github.com
|
|
5
|
-
|
|
6
|
-
args:
|
|
7
|
-
language:
|
|
8
|
-
type: str
|
|
9
|
-
default: ""
|
|
10
|
-
description: "Programming language filter (e.g. python, rust)"
|
|
11
|
-
since:
|
|
12
|
-
type: str
|
|
13
|
-
default: daily
|
|
14
|
-
description: "Time range: daily, weekly, monthly"
|
|
15
|
-
limit:
|
|
16
|
-
type: int
|
|
17
|
-
default: 20
|
|
18
|
-
description: Number of repos
|
|
19
|
-
|
|
20
|
-
pipeline:
|
|
21
|
-
- evaluate: |
|
|
22
|
-
(async () => {
|
|
23
|
-
const lang = '${{ args.language }}' ? '/${{ args.language }}' : '';
|
|
24
|
-
const res = await fetch(`https://github.com/trending${lang}?since=${{ args.since }}`, {
|
|
25
|
-
headers: { 'Accept': 'text/html' }
|
|
26
|
-
});
|
|
27
|
-
const html = await res.text();
|
|
28
|
-
const parser = new DOMParser();
|
|
29
|
-
const doc = parser.parseFromString(html, 'text/html');
|
|
30
|
-
const rows = doc.querySelectorAll('article.Box-row');
|
|
31
|
-
return Array.from(rows).map(row => {
|
|
32
|
-
const nameEl = row.querySelector('h2 a');
|
|
33
|
-
const descEl = row.querySelector('p');
|
|
34
|
-
const langEl = row.querySelector('[itemprop="programmingLanguage"]');
|
|
35
|
-
const starsEl = row.querySelectorAll('a.Link--muted');
|
|
36
|
-
const todayEl = row.querySelector('span.d-inline-block.float-sm-right');
|
|
37
|
-
return {
|
|
38
|
-
repo: nameEl?.textContent?.trim()?.replace(/\s+/g, '') || '',
|
|
39
|
-
description: descEl?.textContent?.trim() || '',
|
|
40
|
-
language: langEl?.textContent?.trim() || '',
|
|
41
|
-
stars: starsEl[0]?.textContent?.trim() || '',
|
|
42
|
-
forks: starsEl[1]?.textContent?.trim() || '',
|
|
43
|
-
today: todayEl?.textContent?.trim() || ''
|
|
44
|
-
};
|
|
45
|
-
});
|
|
46
|
-
})()
|
|
47
|
-
|
|
48
|
-
- map:
|
|
49
|
-
rank: ${{ index + 1 }}
|
|
50
|
-
repo: ${{ item.repo }}
|
|
51
|
-
description: ${{ item.description }}
|
|
52
|
-
language: ${{ item.language }}
|
|
53
|
-
stars: ${{ item.stars }}
|
|
54
|
-
today: ${{ item.today }}
|
|
55
|
-
|
|
56
|
-
- limit: ${{ args.limit }}
|
|
57
|
-
|
|
58
|
-
columns: [rank, repo, language, stars, today]
|
package/src/clis/zhihu/search.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { cli, Strategy } from '../../registry.js';
|
|
2
|
-
import { fetchJson } from '../../bilibili.js';
|
|
3
|
-
|
|
4
|
-
cli({
|
|
5
|
-
site: 'zhihu',
|
|
6
|
-
name: 'search',
|
|
7
|
-
description: '搜索知乎问题和回答',
|
|
8
|
-
domain: 'www.zhihu.com',
|
|
9
|
-
strategy: Strategy.COOKIE,
|
|
10
|
-
args: [
|
|
11
|
-
{ name: 'keyword', required: true, help: 'Search keyword' },
|
|
12
|
-
{ name: 'type', default: 'general', help: 'general, article, video' },
|
|
13
|
-
{ name: 'limit', type: 'int', default: 20, help: 'Number of results' },
|
|
14
|
-
],
|
|
15
|
-
columns: ['rank', 'title', 'author', 'type', 'url'],
|
|
16
|
-
func: async (page, kwargs) => {
|
|
17
|
-
const { keyword, type = 'general', limit = 20 } = kwargs;
|
|
18
|
-
|
|
19
|
-
// Navigate to zhihu to ensure cookie context
|
|
20
|
-
await page.goto('https://www.zhihu.com');
|
|
21
|
-
|
|
22
|
-
const qs = new URLSearchParams({ q: keyword, type, limit: String(limit) });
|
|
23
|
-
const payload = await fetchJson(page, `https://www.zhihu.com/api/v4/search_v3?${qs}`);
|
|
24
|
-
|
|
25
|
-
const data: any[] = payload?.data ?? [];
|
|
26
|
-
const rows: any[] = [];
|
|
27
|
-
|
|
28
|
-
for (let i = 0; i < Math.min(data.length, Number(limit)); i++) {
|
|
29
|
-
const item = data[i];
|
|
30
|
-
const obj = item.object ?? item;
|
|
31
|
-
const itemType = item.type ?? obj.type ?? 'unknown';
|
|
32
|
-
|
|
33
|
-
let title = '';
|
|
34
|
-
let author = '';
|
|
35
|
-
let url = '';
|
|
36
|
-
|
|
37
|
-
if (itemType === 'search_result') {
|
|
38
|
-
const highlight = obj.highlight ?? {};
|
|
39
|
-
title = (highlight.title ?? obj.title ?? '').replace(/<[^>]+>/g, '');
|
|
40
|
-
author = obj.author?.name ?? '';
|
|
41
|
-
url = obj.url ?? '';
|
|
42
|
-
} else if (obj.question) {
|
|
43
|
-
title = (obj.question.title ?? obj.title ?? '').replace(/<[^>]+>/g, '');
|
|
44
|
-
author = obj.author?.name ?? '';
|
|
45
|
-
url = obj.question.url ? `https://www.zhihu.com/question/${obj.question.id}` : '';
|
|
46
|
-
} else {
|
|
47
|
-
title = (obj.title ?? obj.name ?? '').replace(/<[^>]+>/g, '');
|
|
48
|
-
author = obj.author?.name ?? '';
|
|
49
|
-
url = obj.url ?? '';
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (!title) continue;
|
|
53
|
-
|
|
54
|
-
rows.push({
|
|
55
|
-
rank: rows.length + 1,
|
|
56
|
-
title: title.slice(0, 60),
|
|
57
|
-
author,
|
|
58
|
-
type: itemType.replace('search_result', 'result'),
|
|
59
|
-
url,
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return rows;
|
|
64
|
-
},
|
|
65
|
-
});
|
package/src/promote.ts
DELETED