@jackwener/opencli 0.7.6 → 0.7.9
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/.agents/skills/cross-project-adapter-migration/SKILL.md +249 -0
- package/.agents/workflows/cross-project-adapter-migration.md +54 -0
- package/dist/_debug.d.ts +1 -0
- package/dist/_debug.js +7 -0
- package/dist/browser/discover.d.ts +8 -0
- package/dist/browser/discover.js +83 -0
- package/dist/browser/errors.d.ts +21 -0
- package/dist/browser/errors.js +54 -0
- package/dist/browser/index.d.ts +22 -0
- package/dist/browser/index.js +22 -0
- package/dist/browser/mcp.d.ts +33 -0
- package/dist/browser/mcp.js +304 -0
- package/dist/browser/page.d.ts +41 -0
- package/dist/browser/page.js +142 -0
- package/dist/browser/tabs.d.ts +13 -0
- package/dist/browser/tabs.js +70 -0
- package/dist/browser-tab.d.ts +2 -0
- package/dist/browser-tab.js +30 -0
- package/dist/browser.test.js +1 -1
- package/dist/cli-manifest.json +70 -3
- package/dist/clis/github/search.d.ts +1 -0
- package/dist/clis/github/search.js +20 -0
- package/dist/clis/index.d.ts +27 -0
- package/dist/clis/index.js +41 -0
- package/dist/clis/twitter/timeline.js +174 -35
- package/dist/clis/xiaohongshu/me.d.ts +1 -0
- package/dist/clis/xiaohongshu/me.js +86 -0
- package/dist/completion.js +2 -2
- package/dist/doctor.js +7 -7
- package/dist/engine.js +6 -4
- package/dist/errors.d.ts +25 -0
- package/dist/errors.js +42 -0
- package/dist/logger.d.ts +22 -0
- package/dist/logger.js +47 -0
- package/dist/main.js +8 -2
- package/dist/pipeline/_debug.d.ts +1 -0
- package/dist/pipeline/_debug.js +7 -0
- package/dist/pipeline/executor.js +8 -8
- package/dist/pipeline/steps/browser.d.ts +7 -7
- package/dist/pipeline/steps/intercept.d.ts +1 -1
- package/dist/pipeline/steps/tap.d.ts +1 -1
- package/dist/promote.d.ts +1 -0
- package/dist/promote.js +3 -0
- package/dist/register.d.ts +2 -0
- package/dist/register.js +2 -0
- package/dist/scaffold.d.ts +2 -0
- package/dist/scaffold.js +2 -0
- package/dist/setup.js +9 -3
- package/dist/smoke.d.ts +2 -0
- package/dist/smoke.js +2 -0
- package/package.json +3 -3
- package/scripts/clean-yaml.cjs +19 -0
- package/scripts/copy-yaml.cjs +21 -0
- package/scripts/postinstall.js +30 -9
- package/src/bilibili.ts +1 -1
- package/src/browser/discover.ts +90 -0
- package/src/browser/errors.ts +89 -0
- package/src/browser/index.ts +26 -0
- package/src/browser/mcp.ts +305 -0
- package/src/browser/page.ts +152 -0
- package/src/browser/tabs.ts +76 -0
- package/src/browser.test.ts +1 -1
- package/src/clis/twitter/timeline.ts +204 -36
- package/src/completion.ts +2 -2
- package/src/doctor.ts +13 -1
- package/src/engine.ts +9 -4
- package/src/errors.ts +48 -0
- package/src/logger.ts +57 -0
- package/src/main.ts +10 -3
- package/src/pipeline/executor.ts +8 -7
- package/src/pipeline/steps/browser.ts +18 -18
- package/src/pipeline/steps/intercept.ts +8 -8
- package/src/pipeline/steps/tap.ts +2 -2
- package/src/setup.ts +9 -3
- package/tsconfig.json +1 -2
- package/src/browser.ts +0 -698
package/dist/cli-manifest.json
CHANGED
|
@@ -622,6 +622,45 @@
|
|
|
622
622
|
"url"
|
|
623
623
|
]
|
|
624
624
|
},
|
|
625
|
+
{
|
|
626
|
+
"site": "github",
|
|
627
|
+
"name": "search",
|
|
628
|
+
"description": "Search GitHub repositories",
|
|
629
|
+
"strategy": "public",
|
|
630
|
+
"browser": false,
|
|
631
|
+
"args": [
|
|
632
|
+
{
|
|
633
|
+
"name": "keyword",
|
|
634
|
+
"type": "str",
|
|
635
|
+
"required": true,
|
|
636
|
+
"help": "Search keyword"
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
"name": "sort",
|
|
640
|
+
"type": "str",
|
|
641
|
+
"default": "stars",
|
|
642
|
+
"required": false,
|
|
643
|
+
"help": "Sort by: stars, forks, updated"
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
"name": "limit",
|
|
647
|
+
"type": "int",
|
|
648
|
+
"default": 20,
|
|
649
|
+
"required": false,
|
|
650
|
+
"help": "Number of results"
|
|
651
|
+
}
|
|
652
|
+
],
|
|
653
|
+
"type": "ts",
|
|
654
|
+
"modulePath": "github/search.js",
|
|
655
|
+
"domain": "github.com",
|
|
656
|
+
"columns": [
|
|
657
|
+
"rank",
|
|
658
|
+
"name",
|
|
659
|
+
"stars",
|
|
660
|
+
"language",
|
|
661
|
+
"description"
|
|
662
|
+
]
|
|
663
|
+
},
|
|
625
664
|
{
|
|
626
665
|
"site": "hackernews",
|
|
627
666
|
"name": "top",
|
|
@@ -1877,7 +1916,7 @@
|
|
|
1877
1916
|
{
|
|
1878
1917
|
"site": "twitter",
|
|
1879
1918
|
"name": "timeline",
|
|
1880
|
-
"description": "Twitter Home Timeline",
|
|
1919
|
+
"description": "Fetch Twitter Home Timeline",
|
|
1881
1920
|
"strategy": "cookie",
|
|
1882
1921
|
"browser": true,
|
|
1883
1922
|
"args": [
|
|
@@ -1893,8 +1932,15 @@
|
|
|
1893
1932
|
"modulePath": "twitter/timeline.js",
|
|
1894
1933
|
"domain": "x.com",
|
|
1895
1934
|
"columns": [
|
|
1896
|
-
"
|
|
1897
|
-
"
|
|
1935
|
+
"id",
|
|
1936
|
+
"author",
|
|
1937
|
+
"text",
|
|
1938
|
+
"likes",
|
|
1939
|
+
"retweets",
|
|
1940
|
+
"replies",
|
|
1941
|
+
"views",
|
|
1942
|
+
"created_at",
|
|
1943
|
+
"url"
|
|
1898
1944
|
]
|
|
1899
1945
|
},
|
|
1900
1946
|
{
|
|
@@ -2244,6 +2290,27 @@
|
|
|
2244
2290
|
],
|
|
2245
2291
|
"type": "yaml"
|
|
2246
2292
|
},
|
|
2293
|
+
{
|
|
2294
|
+
"site": "xiaohongshu",
|
|
2295
|
+
"name": "me",
|
|
2296
|
+
"description": "我的小红书个人信息",
|
|
2297
|
+
"strategy": "cookie",
|
|
2298
|
+
"browser": true,
|
|
2299
|
+
"args": [],
|
|
2300
|
+
"type": "ts",
|
|
2301
|
+
"modulePath": "xiaohongshu/me.js",
|
|
2302
|
+
"domain": "www.xiaohongshu.com",
|
|
2303
|
+
"columns": [
|
|
2304
|
+
"nickname",
|
|
2305
|
+
"red_id",
|
|
2306
|
+
"location",
|
|
2307
|
+
"profession",
|
|
2308
|
+
"fans",
|
|
2309
|
+
"follows",
|
|
2310
|
+
"likes_collected",
|
|
2311
|
+
"notes"
|
|
2312
|
+
]
|
|
2313
|
+
},
|
|
2247
2314
|
{
|
|
2248
2315
|
"site": "xiaohongshu",
|
|
2249
2316
|
"name": "notifications",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import all TypeScript CLI adapters so they self-register.
|
|
3
|
+
*
|
|
4
|
+
* Each TS adapter calls cli() on import, which adds itself to the global registry.
|
|
5
|
+
*/
|
|
6
|
+
import './bilibili/search.js';
|
|
7
|
+
import './bilibili/me.js';
|
|
8
|
+
import './bilibili/favorite.js';
|
|
9
|
+
import './bilibili/history.js';
|
|
10
|
+
import './bilibili/feed.js';
|
|
11
|
+
import './bilibili/user-videos.js';
|
|
12
|
+
import './bilibili/ranking.js';
|
|
13
|
+
import './bilibili/dynamic.js';
|
|
14
|
+
import './github/search.js';
|
|
15
|
+
import './zhihu/question.js';
|
|
16
|
+
import './xiaohongshu/search.js';
|
|
17
|
+
import './xiaohongshu/user.js';
|
|
18
|
+
import './bbc/news.js';
|
|
19
|
+
import './weibo/hot.js';
|
|
20
|
+
import './boss/search.js';
|
|
21
|
+
import './yahoo-finance/quote.js';
|
|
22
|
+
import './reuters/search.js';
|
|
23
|
+
import './smzdm/search.js';
|
|
24
|
+
import './ctrip/search.js';
|
|
25
|
+
import './youtube/search.js';
|
|
26
|
+
import './twitter/search.js';
|
|
27
|
+
import './twitter/profile.js';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import all TypeScript CLI adapters so they self-register.
|
|
3
|
+
*
|
|
4
|
+
* Each TS adapter calls cli() on import, which adds itself to the global registry.
|
|
5
|
+
*/
|
|
6
|
+
// bilibili
|
|
7
|
+
import './bilibili/search.js';
|
|
8
|
+
import './bilibili/me.js';
|
|
9
|
+
import './bilibili/favorite.js';
|
|
10
|
+
import './bilibili/history.js';
|
|
11
|
+
import './bilibili/feed.js';
|
|
12
|
+
import './bilibili/user-videos.js';
|
|
13
|
+
import './bilibili/ranking.js';
|
|
14
|
+
import './bilibili/dynamic.js';
|
|
15
|
+
// github
|
|
16
|
+
import './github/search.js';
|
|
17
|
+
// zhihu
|
|
18
|
+
import './zhihu/question.js';
|
|
19
|
+
// xiaohongshu
|
|
20
|
+
import './xiaohongshu/search.js';
|
|
21
|
+
import './xiaohongshu/user.js';
|
|
22
|
+
// bbc
|
|
23
|
+
import './bbc/news.js';
|
|
24
|
+
// weibo
|
|
25
|
+
import './weibo/hot.js';
|
|
26
|
+
// boss
|
|
27
|
+
import './boss/search.js';
|
|
28
|
+
// yahoo-finance
|
|
29
|
+
import './yahoo-finance/quote.js';
|
|
30
|
+
// reuters
|
|
31
|
+
import './reuters/search.js';
|
|
32
|
+
// smzdm
|
|
33
|
+
import './smzdm/search.js';
|
|
34
|
+
// ctrip
|
|
35
|
+
import './ctrip/search.js';
|
|
36
|
+
// youtube
|
|
37
|
+
import './youtube/search.js';
|
|
38
|
+
// twitter
|
|
39
|
+
import './twitter/search.js';
|
|
40
|
+
import './twitter/profile.js';
|
|
41
|
+
// reddit
|
|
@@ -1,47 +1,186 @@
|
|
|
1
1
|
import { cli, Strategy } from '../../registry.js';
|
|
2
|
+
// ── Twitter GraphQL constants ──────────────────────────────────────────
|
|
3
|
+
const BEARER_TOKEN = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
|
|
4
|
+
const HOME_TIMELINE_QUERY_ID = 'c-CzHF1LboFilMpsx4ZCrQ';
|
|
5
|
+
const FEATURES = {
|
|
6
|
+
rweb_video_screen_enabled: false,
|
|
7
|
+
profile_label_improvements_pcf_label_in_post_enabled: true,
|
|
8
|
+
rweb_tipjar_consumption_enabled: true,
|
|
9
|
+
verified_phone_label_enabled: false,
|
|
10
|
+
creator_subscriptions_tweet_preview_api_enabled: true,
|
|
11
|
+
responsive_web_graphql_timeline_navigation_enabled: true,
|
|
12
|
+
responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
|
|
13
|
+
premium_content_api_read_enabled: false,
|
|
14
|
+
communities_web_enable_tweet_community_results_fetch: true,
|
|
15
|
+
c9s_tweet_anatomy_moderator_badge_enabled: true,
|
|
16
|
+
responsive_web_grok_analyze_button_fetch_trends_enabled: false,
|
|
17
|
+
responsive_web_grok_analyze_post_followups_enabled: true,
|
|
18
|
+
responsive_web_jetfuel_frame: false,
|
|
19
|
+
responsive_web_grok_share_attachment_enabled: true,
|
|
20
|
+
articles_preview_enabled: true,
|
|
21
|
+
responsive_web_edit_tweet_api_enabled: true,
|
|
22
|
+
graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
|
|
23
|
+
view_counts_everywhere_api_enabled: true,
|
|
24
|
+
longform_notetweets_consumption_enabled: true,
|
|
25
|
+
responsive_web_twitter_article_tweet_consumption_enabled: true,
|
|
26
|
+
tweet_awards_web_tipping_enabled: false,
|
|
27
|
+
responsive_web_grok_show_grok_translated_post: false,
|
|
28
|
+
responsive_web_grok_analysis_button_from_backend: false,
|
|
29
|
+
creator_subscriptions_quote_tweet_preview_enabled: false,
|
|
30
|
+
freedom_of_speech_not_reach_fetch_enabled: true,
|
|
31
|
+
standardized_nudges_misinfo: true,
|
|
32
|
+
tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
|
|
33
|
+
longform_notetweets_rich_text_read_enabled: true,
|
|
34
|
+
longform_notetweets_inline_media_enabled: true,
|
|
35
|
+
responsive_web_grok_image_annotation_enabled: true,
|
|
36
|
+
responsive_web_enhance_cards_enabled: false,
|
|
37
|
+
};
|
|
38
|
+
function buildHomeTimelineUrl(count, cursor) {
|
|
39
|
+
const vars = {
|
|
40
|
+
count,
|
|
41
|
+
includePromotedContent: false,
|
|
42
|
+
latestControlAvailable: true,
|
|
43
|
+
requestContext: 'launch',
|
|
44
|
+
withCommunity: true,
|
|
45
|
+
};
|
|
46
|
+
if (cursor)
|
|
47
|
+
vars.cursor = cursor;
|
|
48
|
+
return `/i/api/graphql/${HOME_TIMELINE_QUERY_ID}/HomeTimeline`
|
|
49
|
+
+ `?variables=${encodeURIComponent(JSON.stringify(vars))}`
|
|
50
|
+
+ `&features=${encodeURIComponent(JSON.stringify(FEATURES))}`;
|
|
51
|
+
}
|
|
52
|
+
function extractTweet(result, seen) {
|
|
53
|
+
if (!result)
|
|
54
|
+
return null;
|
|
55
|
+
const tw = result.tweet || result;
|
|
56
|
+
const l = tw.legacy || {};
|
|
57
|
+
if (!tw.rest_id || seen.has(tw.rest_id))
|
|
58
|
+
return null;
|
|
59
|
+
seen.add(tw.rest_id);
|
|
60
|
+
const u = tw.core?.user_results?.result;
|
|
61
|
+
const screenName = u?.legacy?.screen_name || u?.core?.screen_name || 'unknown';
|
|
62
|
+
const noteText = tw.note_tweet?.note_tweet_results?.result?.text;
|
|
63
|
+
const views = tw.views?.count ? parseInt(tw.views.count, 10) : 0;
|
|
64
|
+
return {
|
|
65
|
+
id: tw.rest_id,
|
|
66
|
+
author: screenName,
|
|
67
|
+
text: noteText || l.full_text || '',
|
|
68
|
+
likes: l.favorite_count || 0,
|
|
69
|
+
retweets: l.retweet_count || 0,
|
|
70
|
+
replies: l.reply_count || 0,
|
|
71
|
+
views,
|
|
72
|
+
created_at: l.created_at || '',
|
|
73
|
+
url: `https://x.com/${screenName}/status/${tw.rest_id}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function parseHomeTimeline(data, seen) {
|
|
77
|
+
const tweets = [];
|
|
78
|
+
let nextCursor = null;
|
|
79
|
+
const instructions = data?.data?.home?.home_timeline_urt?.instructions || [];
|
|
80
|
+
for (const inst of instructions) {
|
|
81
|
+
for (const entry of inst.entries || []) {
|
|
82
|
+
const c = entry.content;
|
|
83
|
+
// Cursor entries
|
|
84
|
+
if (c?.entryType === 'TimelineTimelineCursor' || c?.__typename === 'TimelineTimelineCursor') {
|
|
85
|
+
if (c.cursorType === 'Bottom')
|
|
86
|
+
nextCursor = c.value;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (entry.entryId?.startsWith('cursor-bottom-')) {
|
|
90
|
+
nextCursor = c?.value || nextCursor;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
// Single tweet entry
|
|
94
|
+
const tweetResult = c?.itemContent?.tweet_results?.result;
|
|
95
|
+
if (tweetResult) {
|
|
96
|
+
// Skip promoted content
|
|
97
|
+
if (c?.itemContent?.promotedMetadata)
|
|
98
|
+
continue;
|
|
99
|
+
const tw = extractTweet(tweetResult, seen);
|
|
100
|
+
if (tw)
|
|
101
|
+
tweets.push(tw);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
// Conversation module (grouped tweets)
|
|
105
|
+
for (const item of c?.items || []) {
|
|
106
|
+
const nested = item.item?.itemContent?.tweet_results?.result;
|
|
107
|
+
if (nested) {
|
|
108
|
+
if (item.item?.itemContent?.promotedMetadata)
|
|
109
|
+
continue;
|
|
110
|
+
const tw = extractTweet(nested, seen);
|
|
111
|
+
if (tw)
|
|
112
|
+
tweets.push(tw);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { tweets, nextCursor };
|
|
118
|
+
}
|
|
119
|
+
// ── CLI definition ────────────────────────────────────────────────────
|
|
2
120
|
cli({
|
|
3
121
|
site: 'twitter',
|
|
4
122
|
name: 'timeline',
|
|
5
|
-
description: 'Twitter Home Timeline',
|
|
123
|
+
description: 'Fetch Twitter Home Timeline',
|
|
6
124
|
domain: 'x.com',
|
|
7
125
|
strategy: Strategy.COOKIE,
|
|
126
|
+
browser: true,
|
|
8
127
|
args: [
|
|
9
128
|
{ name: 'limit', type: 'int', default: 20 },
|
|
10
129
|
],
|
|
11
|
-
columns: ['
|
|
130
|
+
columns: ['id', 'author', 'text', 'likes', 'retweets', 'replies', 'views', 'created_at', 'url'],
|
|
12
131
|
func: async (page, kwargs) => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
await page.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}, 0);
|
|
32
|
-
return res;
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
`);
|
|
36
|
-
// trigger scroll
|
|
37
|
-
for (let i = 0; i < 3; i++) {
|
|
38
|
-
await page.evaluate('() => window.scrollTo(0, document.body.scrollHeight)');
|
|
39
|
-
await page.wait(2);
|
|
132
|
+
const limit = kwargs.limit || 20;
|
|
133
|
+
// Navigate to x.com for cookie context
|
|
134
|
+
await page.goto('https://x.com');
|
|
135
|
+
await page.wait(3);
|
|
136
|
+
// Extract CSRF token
|
|
137
|
+
const ct0 = await page.evaluate(`() => {
|
|
138
|
+
return document.cookie.split(';').map(c=>c.trim()).find(c=>c.startsWith('ct0='))?.split('=')[1] || null;
|
|
139
|
+
}`);
|
|
140
|
+
if (!ct0)
|
|
141
|
+
throw new Error('Not logged into x.com (no ct0 cookie)');
|
|
142
|
+
// Dynamically resolve queryId
|
|
143
|
+
const queryId = await page.evaluate(`async () => {
|
|
144
|
+
try {
|
|
145
|
+
const ghResp = await fetch('https://raw.githubusercontent.com/fa0311/twitter-openapi/refs/heads/main/src/config/placeholder.json');
|
|
146
|
+
if (ghResp.ok) {
|
|
147
|
+
const data = await ghResp.json();
|
|
148
|
+
const entry = data['HomeTimeline'];
|
|
149
|
+
if (entry && entry.queryId) return entry.queryId;
|
|
40
150
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
151
|
+
} catch {}
|
|
152
|
+
return null;
|
|
153
|
+
}`) || HOME_TIMELINE_QUERY_ID;
|
|
154
|
+
// Build auth headers
|
|
155
|
+
const headers = JSON.stringify({
|
|
156
|
+
'Authorization': `Bearer ${decodeURIComponent(BEARER_TOKEN)}`,
|
|
157
|
+
'X-Csrf-Token': ct0,
|
|
158
|
+
'X-Twitter-Auth-Type': 'OAuth2Session',
|
|
159
|
+
'X-Twitter-Active-User': 'yes',
|
|
160
|
+
});
|
|
161
|
+
// Paginate — fetch in browser, parse in TypeScript
|
|
162
|
+
const allTweets = [];
|
|
163
|
+
const seen = new Set();
|
|
164
|
+
let cursor = null;
|
|
165
|
+
for (let i = 0; i < 5 && allTweets.length < limit; i++) {
|
|
166
|
+
const fetchCount = Math.min(40, limit - allTweets.length + 5); // over-fetch slightly for promoted filtering
|
|
167
|
+
const apiUrl = buildHomeTimelineUrl(fetchCount, cursor)
|
|
168
|
+
.replace(HOME_TIMELINE_QUERY_ID, queryId);
|
|
169
|
+
const data = await page.evaluate(`async () => {
|
|
170
|
+
const r = await fetch("${apiUrl}", { headers: ${headers}, credentials: 'include' });
|
|
171
|
+
return r.ok ? await r.json() : { error: r.status };
|
|
172
|
+
}`);
|
|
173
|
+
if (data?.error) {
|
|
174
|
+
if (allTweets.length === 0)
|
|
175
|
+
throw new Error(`HTTP ${data.error}: Failed to fetch timeline. queryId may have expired.`);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
const { tweets, nextCursor } = parseHomeTimeline(data, seen);
|
|
179
|
+
allTweets.push(...tweets);
|
|
180
|
+
if (!nextCursor || nextCursor === cursor)
|
|
181
|
+
break;
|
|
182
|
+
cursor = nextCursor;
|
|
183
|
+
}
|
|
184
|
+
return allTweets.slice(0, limit);
|
|
185
|
+
},
|
|
47
186
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Xiaohongshu me — read self profile info.
|
|
3
|
+
*
|
|
4
|
+
* Two-step navigation:
|
|
5
|
+
* 1. /explore → get user_id from Pinia user store
|
|
6
|
+
* 2. /user/profile/{user_id} → poll until userPageData loads, read full profile
|
|
7
|
+
*/
|
|
8
|
+
import { cli, Strategy } from '../../registry.js';
|
|
9
|
+
cli({
|
|
10
|
+
site: 'xiaohongshu',
|
|
11
|
+
name: 'me',
|
|
12
|
+
description: '我的小红书个人信息',
|
|
13
|
+
domain: 'www.xiaohongshu.com',
|
|
14
|
+
strategy: Strategy.COOKIE,
|
|
15
|
+
args: [],
|
|
16
|
+
columns: ['nickname', 'red_id', 'location', 'profession', 'fans', 'follows', 'likes_collected', 'notes'],
|
|
17
|
+
func: async (page) => {
|
|
18
|
+
// Step 1: Navigate to /explore to get user_id
|
|
19
|
+
await page.goto('https://www.xiaohongshu.com/explore');
|
|
20
|
+
await page.wait(2);
|
|
21
|
+
const userId = await page.evaluate(`
|
|
22
|
+
(() => {
|
|
23
|
+
const app = document.querySelector('#app')?.__vue_app__;
|
|
24
|
+
const pinia = app?.config?.globalProperties?.$pinia;
|
|
25
|
+
const store = pinia?._s?.get('user');
|
|
26
|
+
const u = store?.$state?.userInfo || {};
|
|
27
|
+
return u.user_id || u.userId || '';
|
|
28
|
+
})()
|
|
29
|
+
`);
|
|
30
|
+
if (!userId)
|
|
31
|
+
return [{ error: 'Not logged in or user_id not found' }];
|
|
32
|
+
// Step 2: Navigate to real profile page for full data
|
|
33
|
+
await page.goto(`https://www.xiaohongshu.com/user/profile/${userId}`);
|
|
34
|
+
await page.wait(3);
|
|
35
|
+
const data = await page.evaluate(`
|
|
36
|
+
(async () => {
|
|
37
|
+
// Poll for userPageData to be populated (profile page loads async)
|
|
38
|
+
const deadline = Date.now() + 8000;
|
|
39
|
+
let pd = null;
|
|
40
|
+
while (Date.now() < deadline) {
|
|
41
|
+
const app = document.querySelector('#app')?.__vue_app__;
|
|
42
|
+
const pinia = app?.config?.globalProperties?.$pinia;
|
|
43
|
+
const store = pinia?._s?.get('user');
|
|
44
|
+
pd = store?.$state?.userPageData;
|
|
45
|
+
if (pd?.interactions?.length > 0) break;
|
|
46
|
+
await new Promise(r => setTimeout(r, 500));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!pd?.interactions?.length) {
|
|
50
|
+
return { error: 'Profile data did not load' };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const basic = pd.basicInfo || {};
|
|
54
|
+
const interactions = pd.interactions || [];
|
|
55
|
+
const tags = pd.tags || [];
|
|
56
|
+
|
|
57
|
+
const getCount = (type) => {
|
|
58
|
+
const item = interactions.find(i => i.type === type);
|
|
59
|
+
return item ? item.count : '0';
|
|
60
|
+
};
|
|
61
|
+
const getTag = (tagType) => {
|
|
62
|
+
const item = tags.find(t => t.tagType === tagType);
|
|
63
|
+
return item ? item.name : '';
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const app2 = document.querySelector('#app')?.__vue_app__;
|
|
67
|
+
const store2 = app2?.config?.globalProperties?.$pinia?._s?.get('user');
|
|
68
|
+
const noteCount = (store2?.$state?.notes || []).length;
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
nickname: basic.nickname || '',
|
|
72
|
+
red_id: basic.redId || '',
|
|
73
|
+
location: getTag('location') || basic.ipLocation || '',
|
|
74
|
+
profession: getTag('profession') || '',
|
|
75
|
+
fans: getCount('fans'),
|
|
76
|
+
follows: getCount('follows'),
|
|
77
|
+
likes_collected: getCount('interaction'),
|
|
78
|
+
notes: noteCount,
|
|
79
|
+
};
|
|
80
|
+
})()
|
|
81
|
+
`);
|
|
82
|
+
if (!data || data.error)
|
|
83
|
+
return [];
|
|
84
|
+
return [data];
|
|
85
|
+
},
|
|
86
|
+
});
|
package/dist/completion.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - Dynamic completion logic that returns candidates for the current cursor position
|
|
7
7
|
*/
|
|
8
8
|
import { getRegistry } from './registry.js';
|
|
9
|
+
import { CliError } from './errors.js';
|
|
9
10
|
// ── Dynamic completion logic ───────────────────────────────────────────────
|
|
10
11
|
/**
|
|
11
12
|
* Built-in (non-dynamic) top-level commands.
|
|
@@ -110,7 +111,6 @@ export function printCompletionScript(shell) {
|
|
|
110
111
|
process.stdout.write(fishCompletionScript());
|
|
111
112
|
break;
|
|
112
113
|
default:
|
|
113
|
-
|
|
114
|
-
process.exitCode = 1;
|
|
114
|
+
throw new CliError('UNSUPPORTED_SHELL', `Unsupported shell: ${shell}. Supported: bash, zsh, fish`);
|
|
115
115
|
}
|
|
116
116
|
}
|
package/dist/doctor.js
CHANGED
|
@@ -4,7 +4,7 @@ import * as path from 'node:path';
|
|
|
4
4
|
import { createInterface } from 'node:readline/promises';
|
|
5
5
|
import { stdin as input, stdout as output } from 'node:process';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
|
-
import { PlaywrightMCP, getTokenFingerprint } from './browser.js';
|
|
7
|
+
import { PlaywrightMCP, getTokenFingerprint } from './browser/index.js';
|
|
8
8
|
const PLAYWRIGHT_SERVER_NAME = 'playwright';
|
|
9
9
|
export const PLAYWRIGHT_TOKEN_ENV = 'PLAYWRIGHT_MCP_EXTENSION_TOKEN';
|
|
10
10
|
const PLAYWRIGHT_EXTENSION_ID = 'mmlmfjhmonkocbjadbfplnigmagldckm';
|
|
@@ -266,14 +266,14 @@ export function discoverExtensionToken() {
|
|
|
266
266
|
const platform = os.platform();
|
|
267
267
|
const bases = [];
|
|
268
268
|
if (platform === 'darwin') {
|
|
269
|
-
bases.push(path.join(home, 'Library', 'Application Support', 'Google', 'Chrome'), path.join(home, 'Library', 'Application Support', 'Google', 'Chrome Canary'), path.join(home, 'Library', 'Application Support', 'Chromium'), path.join(home, 'Library', 'Application Support', 'Microsoft Edge'));
|
|
269
|
+
bases.push(path.join(home, 'Library', 'Application Support', 'Google', 'Chrome'), path.join(home, 'Library', 'Application Support', 'Google', 'Chrome Dev'), path.join(home, 'Library', 'Application Support', 'Google', 'Chrome Beta'), path.join(home, 'Library', 'Application Support', 'Google', 'Chrome Canary'), path.join(home, 'Library', 'Application Support', 'Chromium'), path.join(home, 'Library', 'Application Support', 'Microsoft Edge'));
|
|
270
270
|
}
|
|
271
271
|
else if (platform === 'linux') {
|
|
272
|
-
bases.push(path.join(home, '.config', 'google-chrome'), path.join(home, '.config', 'chromium'), path.join(home, '.config', 'microsoft-edge'));
|
|
272
|
+
bases.push(path.join(home, '.config', 'google-chrome'), path.join(home, '.config', 'google-chrome-unstable'), path.join(home, '.config', 'google-chrome-beta'), path.join(home, '.config', 'chromium'), path.join(home, '.config', 'microsoft-edge'));
|
|
273
273
|
}
|
|
274
274
|
else if (platform === 'win32') {
|
|
275
275
|
const appData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
276
|
-
bases.push(path.join(appData, 'Google', 'Chrome', 'User Data'), path.join(appData, 'Microsoft', 'Edge', 'User Data'));
|
|
276
|
+
bases.push(path.join(appData, 'Google', 'Chrome', 'User Data'), path.join(appData, 'Google', 'Chrome Dev', 'User Data'), path.join(appData, 'Google', 'Chrome Beta', 'User Data'), path.join(appData, 'Microsoft', 'Edge', 'User Data'));
|
|
277
277
|
}
|
|
278
278
|
const profiles = enumerateProfiles(bases);
|
|
279
279
|
const tokenRe = /([A-Za-z0-9_-]{40,50})/;
|
|
@@ -388,14 +388,14 @@ export function checkExtensionInstalled() {
|
|
|
388
388
|
const platform = os.platform();
|
|
389
389
|
const browserDirs = [];
|
|
390
390
|
if (platform === 'darwin') {
|
|
391
|
-
browserDirs.push({ name: 'Chrome', base: path.join(home, 'Library', 'Application Support', 'Google', 'Chrome') }, { name: 'Chrome Canary', base: path.join(home, 'Library', 'Application Support', 'Google', 'Chrome Canary') }, { name: 'Chromium', base: path.join(home, 'Library', 'Application Support', 'Chromium') }, { name: 'Edge', base: path.join(home, 'Library', 'Application Support', 'Microsoft Edge') });
|
|
391
|
+
browserDirs.push({ name: 'Chrome', base: path.join(home, 'Library', 'Application Support', 'Google', 'Chrome') }, { name: 'Chrome Dev', base: path.join(home, 'Library', 'Application Support', 'Google', 'Chrome Dev') }, { name: 'Chrome Beta', base: path.join(home, 'Library', 'Application Support', 'Google', 'Chrome Beta') }, { name: 'Chrome Canary', base: path.join(home, 'Library', 'Application Support', 'Google', 'Chrome Canary') }, { name: 'Chromium', base: path.join(home, 'Library', 'Application Support', 'Chromium') }, { name: 'Edge', base: path.join(home, 'Library', 'Application Support', 'Microsoft Edge') });
|
|
392
392
|
}
|
|
393
393
|
else if (platform === 'linux') {
|
|
394
|
-
browserDirs.push({ name: 'Chrome', base: path.join(home, '.config', 'google-chrome') }, { name: 'Chromium', base: path.join(home, '.config', 'chromium') }, { name: 'Edge', base: path.join(home, '.config', 'microsoft-edge') });
|
|
394
|
+
browserDirs.push({ name: 'Chrome', base: path.join(home, '.config', 'google-chrome') }, { name: 'Chrome Dev', base: path.join(home, '.config', 'google-chrome-unstable') }, { name: 'Chrome Beta', base: path.join(home, '.config', 'google-chrome-beta') }, { name: 'Chromium', base: path.join(home, '.config', 'chromium') }, { name: 'Edge', base: path.join(home, '.config', 'microsoft-edge') });
|
|
395
395
|
}
|
|
396
396
|
else if (platform === 'win32') {
|
|
397
397
|
const appData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
|
|
398
|
-
browserDirs.push({ name: 'Chrome', base: path.join(appData, 'Google', 'Chrome', 'User Data') }, { name: 'Edge', base: path.join(appData, 'Microsoft', 'Edge', 'User Data') });
|
|
398
|
+
browserDirs.push({ name: 'Chrome', base: path.join(appData, 'Google', 'Chrome', 'User Data') }, { name: 'Chrome Dev', base: path.join(appData, 'Google', 'Chrome Dev', 'User Data') }, { name: 'Chrome Beta', base: path.join(appData, 'Google', 'Chrome Beta', 'User Data') }, { name: 'Edge', base: path.join(appData, 'Microsoft', 'Edge', 'User Data') });
|
|
399
399
|
}
|
|
400
400
|
const profiles = enumerateProfiles(browserDirs.map(d => d.base));
|
|
401
401
|
const foundBrowsers = [];
|
package/dist/engine.js
CHANGED
|
@@ -12,6 +12,8 @@ import * as path from 'node:path';
|
|
|
12
12
|
import yaml from 'js-yaml';
|
|
13
13
|
import { Strategy, registerCommand } from './registry.js';
|
|
14
14
|
import { executePipeline } from './pipeline.js';
|
|
15
|
+
import { log } from './logger.js';
|
|
16
|
+
import { AdapterLoadError } from './errors.js';
|
|
15
17
|
/** Set of TS module paths that have been loaded */
|
|
16
18
|
const _loadedModules = new Set();
|
|
17
19
|
/**
|
|
@@ -81,7 +83,7 @@ function loadFromManifest(manifestPath, clisDir) {
|
|
|
81
83
|
}
|
|
82
84
|
}
|
|
83
85
|
catch (err) {
|
|
84
|
-
|
|
86
|
+
log.warn(`Failed to load manifest ${manifestPath}: ${err.message}`);
|
|
85
87
|
}
|
|
86
88
|
}
|
|
87
89
|
/**
|
|
@@ -103,7 +105,7 @@ async function discoverClisFromFs(dir) {
|
|
|
103
105
|
else if ((file.endsWith('.js') && !file.endsWith('.d.js')) ||
|
|
104
106
|
(file.endsWith('.ts') && !file.endsWith('.d.ts') && !file.endsWith('.test.ts'))) {
|
|
105
107
|
promises.push(import(`file://${filePath}`).catch((err) => {
|
|
106
|
-
|
|
108
|
+
log.warn(`Failed to load module ${filePath}: ${err.message}`);
|
|
107
109
|
}));
|
|
108
110
|
}
|
|
109
111
|
}
|
|
@@ -150,7 +152,7 @@ function registerYamlCli(filePath, defaultSite) {
|
|
|
150
152
|
registerCommand(cmd);
|
|
151
153
|
}
|
|
152
154
|
catch (err) {
|
|
153
|
-
|
|
155
|
+
log.warn(`Failed to load ${filePath}: ${err.message}`);
|
|
154
156
|
}
|
|
155
157
|
}
|
|
156
158
|
/**
|
|
@@ -167,7 +169,7 @@ export async function executeCommand(cmd, page, kwargs, debug = false) {
|
|
|
167
169
|
_loadedModules.add(modulePath);
|
|
168
170
|
}
|
|
169
171
|
catch (err) {
|
|
170
|
-
throw new
|
|
172
|
+
throw new AdapterLoadError(`Failed to load adapter module ${modulePath}: ${err.message}`, 'Check that the adapter file exists and has no syntax errors.');
|
|
171
173
|
}
|
|
172
174
|
}
|
|
173
175
|
// After loading, the module's cli() call will have updated the registry
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified error types for opencli.
|
|
3
|
+
*
|
|
4
|
+
* All errors thrown by the framework should extend CliError so that
|
|
5
|
+
* the top-level handler in main.ts can render consistent, helpful output.
|
|
6
|
+
*/
|
|
7
|
+
export declare class CliError extends Error {
|
|
8
|
+
/** Machine-readable error code (e.g. 'BROWSER_CONNECT', 'ADAPTER_LOAD') */
|
|
9
|
+
readonly code: string;
|
|
10
|
+
/** Human-readable hint on how to fix the problem */
|
|
11
|
+
readonly hint?: string;
|
|
12
|
+
constructor(code: string, message: string, hint?: string);
|
|
13
|
+
}
|
|
14
|
+
export declare class BrowserConnectError extends CliError {
|
|
15
|
+
constructor(message: string, hint?: string);
|
|
16
|
+
}
|
|
17
|
+
export declare class AdapterLoadError extends CliError {
|
|
18
|
+
constructor(message: string, hint?: string);
|
|
19
|
+
}
|
|
20
|
+
export declare class CommandExecutionError extends CliError {
|
|
21
|
+
constructor(message: string, hint?: string);
|
|
22
|
+
}
|
|
23
|
+
export declare class ConfigError extends CliError {
|
|
24
|
+
constructor(message: string, hint?: string);
|
|
25
|
+
}
|