@jackwener/opencli 0.7.9 → 0.7.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/.github/workflows/pkg-pr-new.yml +30 -0
  2. package/README.md +1 -0
  3. package/README.zh-CN.md +1 -0
  4. package/dist/browser/discover.d.ts +15 -0
  5. package/dist/browser/discover.js +60 -12
  6. package/dist/browser/index.d.ts +5 -1
  7. package/dist/browser/index.js +5 -1
  8. package/dist/browser/mcp.js +7 -6
  9. package/dist/browser.test.js +135 -1
  10. package/dist/cli-manifest.json +170 -88
  11. package/dist/clis/barchart/flow.js +117 -0
  12. package/dist/clis/barchart/greeks.js +119 -0
  13. package/dist/clis/barchart/options.js +106 -0
  14. package/dist/clis/barchart/quote.js +133 -0
  15. package/dist/clis/twitter/bookmarks.d.ts +1 -0
  16. package/dist/clis/twitter/bookmarks.js +171 -0
  17. package/dist/clis/twitter/delete.js +0 -1
  18. package/dist/clis/twitter/followers.js +5 -16
  19. package/dist/clis/twitter/following.js +3 -4
  20. package/dist/clis/twitter/like.js +0 -1
  21. package/dist/clis/twitter/notifications.js +17 -7
  22. package/dist/clis/twitter/search.js +14 -6
  23. package/dist/clis/twitter/trending.yaml +8 -2
  24. package/dist/main.js +0 -0
  25. package/package.json +1 -1
  26. package/src/browser/discover.ts +73 -12
  27. package/src/browser/index.ts +5 -1
  28. package/src/browser/mcp.ts +7 -5
  29. package/src/browser.test.ts +140 -1
  30. package/src/clis/barchart/flow.ts +121 -0
  31. package/src/clis/barchart/greeks.ts +123 -0
  32. package/src/clis/barchart/options.ts +110 -0
  33. package/src/clis/barchart/quote.ts +137 -0
  34. package/src/clis/twitter/bookmarks.ts +201 -0
  35. package/src/clis/twitter/delete.ts +0 -1
  36. package/src/clis/twitter/followers.ts +5 -16
  37. package/src/clis/twitter/following.ts +3 -5
  38. package/src/clis/twitter/like.ts +0 -1
  39. package/src/clis/twitter/notifications.ts +18 -9
  40. package/src/clis/twitter/search.ts +14 -7
  41. package/src/clis/twitter/trending.yaml +8 -2
  42. package/vitest.config.ts +7 -0
  43. package/dist/_debug.js +0 -7
  44. package/dist/browser-tab.d.ts +0 -2
  45. package/dist/browser-tab.js +0 -30
  46. package/dist/browser.d.ts +0 -105
  47. package/dist/browser.js +0 -644
  48. package/dist/clis/github/search.js +0 -20
  49. package/dist/clis/index.d.ts +0 -27
  50. package/dist/clis/index.js +0 -41
  51. package/dist/clis/twitter/bookmarks.yaml +0 -85
  52. package/dist/clis/xiaohongshu/me.js +0 -86
  53. package/dist/pipeline/_debug.js +0 -7
  54. package/dist/promote.d.ts +0 -1
  55. package/dist/promote.js +0 -3
  56. package/dist/register.d.ts +0 -2
  57. package/dist/register.js +0 -2
  58. package/dist/scaffold.d.ts +0 -2
  59. package/dist/scaffold.js +0 -2
  60. package/dist/smoke.d.ts +0 -2
  61. package/dist/smoke.js +0 -2
  62. package/src/clis/twitter/bookmarks.yaml +0 -85
  63. /package/dist/{_debug.d.ts → clis/barchart/flow.d.ts} +0 -0
  64. /package/dist/clis/{github/search.d.ts → barchart/greeks.d.ts} +0 -0
  65. /package/dist/clis/{xiaohongshu/me.d.ts → barchart/options.d.ts} +0 -0
  66. /package/dist/{pipeline/_debug.d.ts → clis/barchart/quote.d.ts} +0 -0
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Barchart options chain — strike, bid/ask, volume, OI, greeks, IV.
3
+ * Auth: CSRF token from <meta name="csrf-token"> + session cookies.
4
+ */
5
+ import { cli, Strategy } from '../../registry.js';
6
+
7
+ cli({
8
+ site: 'barchart',
9
+ name: 'options',
10
+ description: 'Barchart options chain with greeks, IV, volume, and open interest',
11
+ domain: 'www.barchart.com',
12
+ strategy: Strategy.COOKIE,
13
+ args: [
14
+ { name: 'symbol', required: true, help: 'Stock ticker (e.g. AAPL)' },
15
+ { name: 'type', type: 'str', default: 'Call', help: 'Option type: Call or Put', choices: ['Call', 'Put'] },
16
+ { name: 'limit', type: 'int', default: 20, help: 'Max number of strikes to return' },
17
+ ],
18
+ columns: [
19
+ 'strike', 'bid', 'ask', 'last', 'change', 'volume', 'openInterest',
20
+ 'iv', 'delta', 'gamma', 'theta', 'vega', 'expiration',
21
+ ],
22
+ func: async (page, kwargs) => {
23
+ const symbol = kwargs.symbol.toUpperCase().trim();
24
+ const optType = kwargs.type || 'Call';
25
+ const limit = kwargs.limit ?? 20;
26
+
27
+ await page.goto(`https://www.barchart.com/stocks/quotes/${encodeURIComponent(symbol)}/options`);
28
+ await page.wait(4);
29
+
30
+ const data = await page.evaluate(`
31
+ (async () => {
32
+ const sym = '${symbol}';
33
+ const type = '${optType}';
34
+ const limit = ${limit};
35
+ const csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
36
+ const headers = { 'X-CSRF-TOKEN': csrf };
37
+
38
+ // API: options chain with greeks
39
+ try {
40
+ const fields = [
41
+ 'strikePrice','bidPrice','askPrice','lastPrice','priceChange',
42
+ 'volume','openInterest','volatility',
43
+ 'delta','gamma','theta','vega',
44
+ 'expirationDate','optionType','percentFromLast',
45
+ ].join(',');
46
+
47
+ const url = '/proxies/core-api/v1/options/chain?symbol=' + encodeURIComponent(sym)
48
+ + '&fields=' + fields + '&raw=1';
49
+ const resp = await fetch(url, { credentials: 'include', headers });
50
+ if (resp.ok) {
51
+ const d = await resp.json();
52
+ let items = d?.data || [];
53
+
54
+ // Filter by type
55
+ items = items.filter(i => {
56
+ const t = (i.raw || i).optionType || '';
57
+ return t.toLowerCase() === type.toLowerCase();
58
+ });
59
+
60
+ // Sort by closeness to current price
61
+ items.sort((a, b) => {
62
+ const aD = Math.abs((a.raw || a).percentFromLast || 999);
63
+ const bD = Math.abs((b.raw || b).percentFromLast || 999);
64
+ return aD - bD;
65
+ });
66
+
67
+ return items.slice(0, limit).map(i => {
68
+ const r = i.raw || i;
69
+ return {
70
+ strike: r.strikePrice,
71
+ bid: r.bidPrice,
72
+ ask: r.askPrice,
73
+ last: r.lastPrice,
74
+ change: r.priceChange,
75
+ volume: r.volume,
76
+ openInterest: r.openInterest,
77
+ iv: r.volatility,
78
+ delta: r.delta,
79
+ gamma: r.gamma,
80
+ theta: r.theta,
81
+ vega: r.vega,
82
+ expiration: r.expirationDate,
83
+ };
84
+ });
85
+ }
86
+ } catch(e) {}
87
+
88
+ return [];
89
+ })()
90
+ `);
91
+
92
+ if (!data || !Array.isArray(data)) return [];
93
+
94
+ return data.map(r => ({
95
+ strike: r.strike,
96
+ bid: r.bid != null ? Number(Number(r.bid).toFixed(2)) : null,
97
+ ask: r.ask != null ? Number(Number(r.ask).toFixed(2)) : null,
98
+ last: r.last != null ? Number(Number(r.last).toFixed(2)) : null,
99
+ change: r.change != null ? Number(Number(r.change).toFixed(2)) : null,
100
+ volume: r.volume,
101
+ openInterest: r.openInterest,
102
+ iv: r.iv != null ? Number(Number(r.iv).toFixed(2)) + '%' : null,
103
+ delta: r.delta != null ? Number(Number(r.delta).toFixed(4)) : null,
104
+ gamma: r.gamma != null ? Number(Number(r.gamma).toFixed(4)) : null,
105
+ theta: r.theta != null ? Number(Number(r.theta).toFixed(4)) : null,
106
+ vega: r.vega != null ? Number(Number(r.vega).toFixed(4)) : null,
107
+ expiration: r.expiration ?? null,
108
+ }));
109
+ },
110
+ });
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Barchart stock quote — price, volume, market cap, P/E, EPS, and key metrics.
3
+ * Auth: CSRF token from <meta name="csrf-token"> + session cookies.
4
+ */
5
+ import { cli, Strategy } from '../../registry.js';
6
+
7
+ cli({
8
+ site: 'barchart',
9
+ name: 'quote',
10
+ description: 'Barchart stock quote with price, volume, and key metrics',
11
+ domain: 'www.barchart.com',
12
+ strategy: Strategy.COOKIE,
13
+ args: [
14
+ { name: 'symbol', required: true, help: 'Stock ticker (e.g. AAPL, MSFT, TSLA)' },
15
+ ],
16
+ columns: [
17
+ 'symbol', 'name', 'price', 'change', 'changePct',
18
+ 'open', 'high', 'low', 'prevClose', 'volume',
19
+ 'avgVolume', 'marketCap', 'peRatio', 'eps',
20
+ ],
21
+ func: async (page, kwargs) => {
22
+ const symbol = kwargs.symbol.toUpperCase().trim();
23
+ await page.goto(`https://www.barchart.com/stocks/quotes/${encodeURIComponent(symbol)}/overview`);
24
+ await page.wait(4);
25
+
26
+ const data = await page.evaluate(`
27
+ (async () => {
28
+ const sym = '${symbol}';
29
+ const csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
30
+
31
+ // Strategy 1: internal proxy API with CSRF token
32
+ try {
33
+ const fields = [
34
+ 'symbol','symbolName','lastPrice','priceChange','percentChange',
35
+ 'highPrice','lowPrice','openPrice','previousPrice','volume','averageVolume',
36
+ 'marketCap','peRatio','earningsPerShare','tradeTime',
37
+ ].join(',');
38
+ const url = '/proxies/core-api/v1/quotes/get?symbol=' + encodeURIComponent(sym) + '&fields=' + fields;
39
+ const resp = await fetch(url, {
40
+ credentials: 'include',
41
+ headers: { 'X-CSRF-TOKEN': csrf },
42
+ });
43
+ if (resp.ok) {
44
+ const d = await resp.json();
45
+ const row = d?.data?.[0] || null;
46
+ if (row) {
47
+ return { source: 'api', row };
48
+ }
49
+ }
50
+ } catch(e) {}
51
+
52
+ // Strategy 2: parse from DOM
53
+ try {
54
+ const priceEl = document.querySelector('span.last-change');
55
+ const price = priceEl ? priceEl.textContent.trim() : null;
56
+
57
+ // Change values are sibling spans inside .pricechangerow > .last-change
58
+ const changeParent = priceEl?.parentElement;
59
+ const changeSpans = changeParent ? changeParent.querySelectorAll('span') : [];
60
+ let change = null;
61
+ let changePct = null;
62
+ for (const s of changeSpans) {
63
+ const t = s.textContent.trim();
64
+ if (s === priceEl) continue;
65
+ if (t.includes('%')) changePct = t.replace(/[()]/g, '');
66
+ else if (t.match(/^[+-]?[\\d.]+$/)) change = t;
67
+ }
68
+
69
+ // Financial data rows
70
+ const rows = document.querySelectorAll('.financial-data-row');
71
+ const fdata = {};
72
+ for (const row of rows) {
73
+ const spans = row.querySelectorAll('span');
74
+ if (spans.length >= 2) {
75
+ const label = spans[0].textContent.trim();
76
+ const valSpan = row.querySelector('span.right span:not(.ng-hide)');
77
+ fdata[label] = valSpan ? valSpan.textContent.trim() : '';
78
+ }
79
+ }
80
+
81
+ // Day high/low from row chart
82
+ const dayLow = document.querySelector('.bc-quote-row-chart .small-6:first-child .inline:not(.ng-hide)');
83
+ const dayHigh = document.querySelector('.bc-quote-row-chart .text-right .inline:not(.ng-hide)');
84
+ const openEl = document.querySelector('.mark span');
85
+ const openText = openEl ? openEl.textContent.trim().replace('Open ', '') : null;
86
+
87
+ const name = document.querySelector('h1 span.symbol');
88
+
89
+ return {
90
+ source: 'dom',
91
+ row: {
92
+ symbol: sym,
93
+ symbolName: name ? name.textContent.trim() : sym,
94
+ lastPrice: price,
95
+ priceChange: change,
96
+ percentChange: changePct,
97
+ open: openText,
98
+ highPrice: dayHigh ? dayHigh.textContent.trim() : null,
99
+ lowPrice: dayLow ? dayLow.textContent.trim() : null,
100
+ previousClose: fdata['Previous Close'] || null,
101
+ volume: fdata['Volume'] || null,
102
+ averageVolume: fdata['Average Volume'] || null,
103
+ marketCap: null,
104
+ peRatio: null,
105
+ earningsPerShare: null,
106
+ }
107
+ };
108
+ } catch(e) {
109
+ return { error: 'Could not fetch quote for ' + sym + ': ' + e.message };
110
+ }
111
+ })()
112
+ `);
113
+
114
+ if (!data || data.error) return [];
115
+
116
+ const r = data.row || {};
117
+ // API returns formatted strings like "+1.41" and "+0.56%"; use raw if available
118
+ const raw = r.raw || {};
119
+
120
+ return [{
121
+ symbol: r.symbol || symbol,
122
+ name: r.symbolName || r.name || symbol,
123
+ price: r.lastPrice ?? null,
124
+ change: r.priceChange ?? null,
125
+ changePct: r.percentChange ?? null,
126
+ open: r.openPrice ?? r.open ?? null,
127
+ high: r.highPrice ?? null,
128
+ low: r.lowPrice ?? null,
129
+ prevClose: r.previousPrice ?? r.previousClose ?? null,
130
+ volume: r.volume ?? null,
131
+ avgVolume: r.averageVolume ?? null,
132
+ marketCap: r.marketCap ?? null,
133
+ peRatio: r.peRatio ?? null,
134
+ eps: r.earningsPerShare ?? null,
135
+ }];
136
+ },
137
+ });
@@ -0,0 +1,201 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+
3
+ const BEARER_TOKEN = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
4
+ const BOOKMARKS_QUERY_ID = 'Fy0QMy4q_aZCpkO0PnyLYw';
5
+
6
+ const FEATURES = {
7
+ rweb_video_screen_enabled: false,
8
+ profile_label_improvements_pcf_label_in_post_enabled: true,
9
+ responsive_web_profile_redirect_enabled: false,
10
+ rweb_tipjar_consumption_enabled: false,
11
+ verified_phone_label_enabled: false,
12
+ creator_subscriptions_tweet_preview_api_enabled: true,
13
+ responsive_web_graphql_timeline_navigation_enabled: true,
14
+ responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
15
+ premium_content_api_read_enabled: false,
16
+ communities_web_enable_tweet_community_results_fetch: true,
17
+ c9s_tweet_anatomy_moderator_badge_enabled: true,
18
+ articles_preview_enabled: true,
19
+ responsive_web_edit_tweet_api_enabled: true,
20
+ graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
21
+ view_counts_everywhere_api_enabled: true,
22
+ longform_notetweets_consumption_enabled: true,
23
+ responsive_web_twitter_article_tweet_consumption_enabled: true,
24
+ tweet_awards_web_tipping_enabled: false,
25
+ content_disclosure_indicator_enabled: true,
26
+ content_disclosure_ai_generated_indicator_enabled: true,
27
+ freedom_of_speech_not_reach_fetch_enabled: true,
28
+ standardized_nudges_misinfo: true,
29
+ tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
30
+ longform_notetweets_rich_text_read_enabled: true,
31
+ longform_notetweets_inline_media_enabled: false,
32
+ responsive_web_enhance_cards_enabled: false,
33
+ };
34
+
35
+ interface BookmarkTweet {
36
+ id: string;
37
+ author: string;
38
+ name: string;
39
+ text: string;
40
+ likes: number;
41
+ retweets: number;
42
+ created_at: string;
43
+ url: string;
44
+ }
45
+
46
+ function buildBookmarksUrl(count: number, cursor?: string | null): string {
47
+ const vars: Record<string, any> = {
48
+ count,
49
+ includePromotedContent: false,
50
+ };
51
+ if (cursor) vars.cursor = cursor;
52
+
53
+ return `/i/api/graphql/${BOOKMARKS_QUERY_ID}/Bookmarks`
54
+ + `?variables=${encodeURIComponent(JSON.stringify(vars))}`
55
+ + `&features=${encodeURIComponent(JSON.stringify(FEATURES))}`;
56
+ }
57
+
58
+ function extractBookmarkTweet(result: any, seen: Set<string>): BookmarkTweet | null {
59
+ if (!result) return null;
60
+ const tw = result.tweet || result;
61
+ const legacy = tw.legacy || {};
62
+ if (!tw.rest_id || seen.has(tw.rest_id)) return null;
63
+ seen.add(tw.rest_id);
64
+
65
+ const user = tw.core?.user_results?.result;
66
+ const screenName = user?.legacy?.screen_name || user?.core?.screen_name || 'unknown';
67
+ const displayName = user?.legacy?.name || user?.core?.name || '';
68
+ const noteText = tw.note_tweet?.note_tweet_results?.result?.text;
69
+
70
+ return {
71
+ id: tw.rest_id,
72
+ author: screenName,
73
+ name: displayName,
74
+ text: noteText || legacy.full_text || '',
75
+ likes: legacy.favorite_count || 0,
76
+ retweets: legacy.retweet_count || 0,
77
+ created_at: legacy.created_at || '',
78
+ url: `https://x.com/${screenName}/status/${tw.rest_id}`,
79
+ };
80
+ }
81
+
82
+ function parseBookmarks(data: any, seen: Set<string>): { tweets: BookmarkTweet[]; nextCursor: string | null } {
83
+ const tweets: BookmarkTweet[] = [];
84
+ let nextCursor: string | null = null;
85
+
86
+ const instructions =
87
+ data?.data?.bookmark_timeline_v2?.timeline?.instructions
88
+ || data?.data?.bookmark_timeline?.timeline?.instructions
89
+ || [];
90
+
91
+ for (const inst of instructions) {
92
+ for (const entry of inst.entries || []) {
93
+ const content = entry.content;
94
+
95
+ if (content?.entryType === 'TimelineTimelineCursor' || content?.__typename === 'TimelineTimelineCursor') {
96
+ if (content.cursorType === 'Bottom' || content.cursorType === 'ShowMore') nextCursor = content.value;
97
+ continue;
98
+ }
99
+ if (entry.entryId?.startsWith('cursor-bottom-') || entry.entryId?.startsWith('cursor-showMore-')) {
100
+ nextCursor = content?.value || content?.itemContent?.value || nextCursor;
101
+ continue;
102
+ }
103
+
104
+ const direct = extractBookmarkTweet(content?.itemContent?.tweet_results?.result, seen);
105
+ if (direct) {
106
+ tweets.push(direct);
107
+ continue;
108
+ }
109
+
110
+ for (const item of content?.items || []) {
111
+ const nested = extractBookmarkTweet(item.item?.itemContent?.tweet_results?.result, seen);
112
+ if (nested) tweets.push(nested);
113
+ }
114
+ }
115
+ }
116
+
117
+ return { tweets, nextCursor };
118
+ }
119
+
120
+ cli({
121
+ site: 'twitter',
122
+ name: 'bookmarks',
123
+ description: 'Fetch Twitter/X bookmarks',
124
+ domain: 'x.com',
125
+ strategy: Strategy.COOKIE,
126
+ browser: true,
127
+ args: [
128
+ { name: 'limit', type: 'int', default: 20 },
129
+ ],
130
+ columns: ['author', 'text', 'likes', 'url'],
131
+ func: async (page, kwargs) => {
132
+ const limit = kwargs.limit || 20;
133
+
134
+ await page.goto('https://x.com');
135
+ await page.wait(3);
136
+
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) throw new Error('Not logged into x.com (no ct0 cookie)');
141
+
142
+ const queryId = await page.evaluate(`async () => {
143
+ try {
144
+ const ghResp = await fetch('https://raw.githubusercontent.com/fa0311/twitter-openapi/refs/heads/main/src/config/placeholder.json');
145
+ if (ghResp.ok) {
146
+ const data = await ghResp.json();
147
+ const entry = data['Bookmarks'];
148
+ if (entry && entry.queryId) return entry.queryId;
149
+ }
150
+ } catch {}
151
+ try {
152
+ const scripts = performance.getEntriesByType('resource')
153
+ .filter(r => r.name.includes('client-web') && r.name.endsWith('.js'))
154
+ .map(r => r.name);
155
+ for (const scriptUrl of scripts.slice(0, 15)) {
156
+ try {
157
+ const text = await (await fetch(scriptUrl)).text();
158
+ const re = /queryId:"([A-Za-z0-9_-]+)"[^}]{0,200}operationName:"Bookmarks"/;
159
+ const m = text.match(re);
160
+ if (m) return m[1];
161
+ } catch {}
162
+ }
163
+ } catch {}
164
+ return null;
165
+ }`) || BOOKMARKS_QUERY_ID;
166
+
167
+ const headers = JSON.stringify({
168
+ 'Authorization': `Bearer ${decodeURIComponent(BEARER_TOKEN)}`,
169
+ 'X-Csrf-Token': ct0,
170
+ 'X-Twitter-Auth-Type': 'OAuth2Session',
171
+ 'X-Twitter-Active-User': 'yes',
172
+ });
173
+
174
+ const allTweets: BookmarkTweet[] = [];
175
+ const seen = new Set<string>();
176
+ let cursor: string | null = null;
177
+
178
+ for (let i = 0; i < 5 && allTweets.length < limit; i++) {
179
+ const fetchCount = Math.min(100, limit - allTweets.length + 10);
180
+ const apiUrl = buildBookmarksUrl(fetchCount, cursor).replace(BOOKMARKS_QUERY_ID, queryId);
181
+
182
+ const data = await page.evaluate(`async () => {
183
+ const r = await fetch("${apiUrl}", { headers: ${headers}, credentials: 'include' });
184
+ return r.ok ? await r.json() : { error: r.status };
185
+ }`);
186
+
187
+ if (data?.error) {
188
+ if (allTweets.length === 0) throw new Error(`HTTP ${data.error}: Failed to fetch bookmarks. queryId may have expired.`);
189
+ break;
190
+ }
191
+
192
+ const { tweets, nextCursor } = parseBookmarks(data, seen);
193
+ allTweets.push(...tweets);
194
+
195
+ if (!nextCursor || nextCursor === cursor) break;
196
+ cursor = nextCursor;
197
+ }
198
+
199
+ return allTweets.slice(0, limit);
200
+ },
201
+ });
@@ -15,7 +15,6 @@ cli({
15
15
  func: async (page: IPage | null, kwargs: any) => {
16
16
  if (!page) throw new Error('Requires browser');
17
17
 
18
- console.log(`Navigating to tweet: ${kwargs.url}`);
19
18
  await page.goto(kwargs.url);
20
19
  await page.wait(5); // Wait for tweet to load completely
21
20
 
@@ -1,5 +1,4 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
- import * as fs from 'fs';
3
2
 
4
3
  cli({
5
4
  site: 'twitter',
@@ -37,8 +36,8 @@ cli({
37
36
  await page.goto(`https://x.com/${targetUser}`);
38
37
  await page.wait(3);
39
38
 
40
- // 2. Inject interceptor for Followers GraphQL API (or user_flow.json)
41
- await page.installInterceptor('graphql');
39
+ // 2. Inject interceptor for the followers GraphQL API
40
+ await page.installInterceptor('Followers');
42
41
 
43
42
  // 3. Click the followers link inside the profile page
44
43
  await page.evaluate(`() => {
@@ -53,24 +52,14 @@ cli({
53
52
 
54
53
  // 4. Retrieve data from opencli's registered interceptors
55
54
  const allRequests = await page.getInterceptedRequests();
55
+ const requestList = Array.isArray(allRequests) ? allRequests : [];
56
56
 
57
- // Debug: Force dump all intercepted XHRs that match followers
58
- if (!allRequests || allRequests.length === 0) {
59
- console.log('No GraphQL requests captured by the interceptor backend.');
57
+ if (requestList.length === 0) {
60
58
  return [];
61
59
  }
62
-
63
- console.log('Intercepted keys:', allRequests.map((r: any) => {
64
- try {
65
- const u = new URL(r.url); return u.pathname;
66
- } catch (e) {
67
- return r.url;
68
- }
69
- }));
70
60
 
71
- const requests = allRequests.filter((r: any) => r.url.includes('Followers'));
61
+ const requests = requestList.filter((r: any) => r?.url?.includes('Followers'));
72
62
  if (!requests || requests.length === 0) {
73
- console.log('No specific Followers requests captured. Check keys printed above.');
74
63
  return [];
75
64
  }
76
65
 
@@ -1,5 +1,4 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
- import * as fs from 'fs';
3
2
 
4
3
  cli({
5
4
  site: 'twitter',
@@ -53,15 +52,14 @@ cli({
53
52
 
54
53
  // 4. Retrieve data from opencli's registered interceptors
55
54
  const requests = await page.getInterceptedRequests();
55
+ const requestList = Array.isArray(requests) ? requests : [];
56
56
 
57
- // Debug: Force dump all intercepted XHRs that match following
58
- if (!requests || requests.length === 0) {
59
- console.log('No Following requests captured by the interceptor backend.');
57
+ if (requestList.length === 0) {
60
58
  return [];
61
59
  }
62
60
 
63
61
  let results: any[] = [];
64
- for (const req of requests) {
62
+ for (const req of requestList) {
65
63
  try {
66
64
  let instructions = req.data?.data?.user?.result?.timeline?.timeline?.instructions;
67
65
  if (!instructions) continue;
@@ -15,7 +15,6 @@ cli({
15
15
  func: async (page: IPage | null, kwargs: any) => {
16
16
  if (!page) throw new Error('Requires browser');
17
17
 
18
- console.log(`Navigating to tweet: ${kwargs.url}`);
19
18
  await page.goto(kwargs.url);
20
19
  await page.wait(5); // Wait for tweet to load completely
21
20
 
@@ -1,5 +1,4 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
- import * as fs from 'fs';
3
2
 
4
3
  cli({
5
4
  site: 'twitter',
@@ -13,13 +12,16 @@ cli({
13
12
  ],
14
13
  columns: ['id', 'action', 'author', 'text', 'url'],
15
14
  func: async (page, kwargs) => {
15
+ // Install the interceptor before loading the notifications page so we
16
+ // capture the initial timeline request triggered during page load.
17
+ await page.goto('https://x.com');
18
+ await page.wait(2);
19
+ await page.installInterceptor('NotificationsTimeline');
20
+
16
21
  // 1. Navigate to notifications
17
22
  await page.goto('https://x.com/notifications');
18
23
  await page.wait(5);
19
24
 
20
- // 2. Inject interceptor
21
- await page.installInterceptor('NotificationsTimeline');
22
-
23
25
  // 3. Trigger API by scrolling (if we need to load more)
24
26
  await page.autoScroll({ times: 2, delayMs: 2000 });
25
27
 
@@ -28,6 +30,7 @@ cli({
28
30
  if (!requests || requests.length === 0) return [];
29
31
 
30
32
  let results: any[] = [];
33
+ const seen = new Set<string>();
31
34
  for (const req of requests) {
32
35
  try {
33
36
  let instructions: any[] = [];
@@ -75,14 +78,16 @@ cli({
75
78
  if (item.__typename === 'TimelineNotification') {
76
79
  // Greet likes, retweet, mentions
77
80
  text = item.rich_message?.text || item.message?.text || '';
78
- author = item.template?.from_users?.[0]?.user_results?.result?.core?.screen_name || 'unknown';
81
+ const fromUser = item.template?.from_users?.[0]?.user_results?.result;
82
+ author = fromUser?.legacy?.screen_name || fromUser?.core?.screen_name || 'unknown';
79
83
  urlStr = item.notification_url?.url || '';
80
84
  actionText = item.notification_icon || 'Activity';
81
85
 
82
86
  // If there's an attached tweet
83
87
  const targetTweet = item.template?.target_objects?.[0]?.tweet_results?.result;
84
88
  if (targetTweet) {
85
- text += ' | ' + (targetTweet.legacy?.full_text || '');
89
+ const targetText = targetTweet.note_tweet?.note_tweet_results?.result?.text || targetTweet.legacy?.full_text || '';
90
+ text += text && targetText ? ' | ' + targetText : targetText;
86
91
  if (!urlStr) {
87
92
  urlStr = `https://x.com/i/status/${targetTweet.rest_id}`;
88
93
  }
@@ -91,18 +96,22 @@ cli({
91
96
  // Direct mention/reply
92
97
  const tweet = item.tweet_result?.result;
93
98
  author = tweet?.core?.user_results?.result?.legacy?.screen_name || 'unknown';
94
- text = tweet?.legacy?.full_text || item.message?.text || '';
99
+ text = tweet?.note_tweet?.note_tweet_results?.result?.text || tweet?.legacy?.full_text || item.message?.text || '';
95
100
  actionText = 'Mention/Reply';
96
101
  urlStr = `https://x.com/i/status/${tweet?.rest_id}`;
97
102
  } else if (item.__typename === 'Tweet') {
98
103
  author = item.core?.user_results?.result?.legacy?.screen_name || 'unknown';
99
- text = item.legacy?.full_text || '';
104
+ text = item.note_tweet?.note_tweet_results?.result?.text || item.legacy?.full_text || '';
100
105
  actionText = 'Mention';
101
106
  urlStr = `https://x.com/i/status/${item.rest_id}`;
102
107
  }
103
108
 
109
+ const id = item.id || item.rest_id || entryId;
110
+ if (seen.has(id)) return;
111
+ seen.add(id);
112
+
104
113
  results.push({
105
- id: item.id || item.rest_id || entryId,
114
+ id,
106
115
  action: actionText,
107
116
  author: author,
108
117
  text: text,
@@ -13,14 +13,17 @@ cli({
13
13
  ],
14
14
  columns: ['id', 'author', 'text', 'likes', 'views', 'url'],
15
15
  func: async (page, kwargs) => {
16
+ // Install the interceptor before opening the target page so we don't miss
17
+ // the initial SearchTimeline request fired during hydration.
18
+ await page.goto('https://x.com');
19
+ await page.wait(2);
20
+ await page.installInterceptor('SearchTimeline');
21
+
16
22
  // 1. Navigate to the search page
17
23
  const q = encodeURIComponent(kwargs.query);
18
24
  await page.goto(`https://x.com/search?q=${q}&f=top`);
19
25
  await page.wait(5);
20
26
 
21
- // 2. Inject XHR interceptor
22
- await page.installInterceptor('SearchTimeline');
23
-
24
27
  // 3. Trigger API by scrolling
25
28
  await page.autoScroll({ times: 3, delayMs: 2000 });
26
29
 
@@ -29,11 +32,13 @@ cli({
29
32
  if (!requests || requests.length === 0) return [];
30
33
 
31
34
  let results: any[] = [];
35
+ const seen = new Set<string>();
32
36
  for (const req of requests) {
33
37
  try {
34
- const insts = req.data.data.search_by_raw_query.search_timeline.timeline.instructions;
35
- const addEntries = insts.find((i: any) => i.type === 'TimelineAddEntries');
36
- if (!addEntries) continue;
38
+ const insts = req.data?.data?.search_by_raw_query?.search_timeline?.timeline?.instructions || [];
39
+ const addEntries = insts.find((i: any) => i.type === 'TimelineAddEntries')
40
+ || insts.find((i: any) => i.entries && Array.isArray(i.entries));
41
+ if (!addEntries?.entries) continue;
37
42
 
38
43
  for (const entry of addEntries.entries) {
39
44
  if (!entry.entryId.startsWith('tweet-')) continue;
@@ -45,11 +50,13 @@ cli({
45
50
  if (tweet.__typename === 'TweetWithVisibilityResults' && tweet.tweet) {
46
51
  tweet = tweet.tweet;
47
52
  }
53
+ if (!tweet.rest_id || seen.has(tweet.rest_id)) continue;
54
+ seen.add(tweet.rest_id);
48
55
 
49
56
  results.push({
50
57
  id: tweet.rest_id,
51
58
  author: tweet.core?.user_results?.result?.legacy?.screen_name || 'unknown',
52
- text: tweet.legacy?.full_text || '',
59
+ text: tweet.note_tweet?.note_tweet_results?.result?.text || tweet.legacy?.full_text || '',
53
60
  likes: tweet.legacy?.favorite_count || 0,
54
61
  views: tweet.views?.count || '0',
55
62
  url: `https://x.com/i/status/${tweet.rest_id}`