@jackwener/opencli 1.7.7 → 1.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.
Files changed (280) hide show
  1. package/README.md +49 -14
  2. package/README.zh-CN.md +30 -10
  3. package/cli-manifest.json +782 -55
  4. package/clis/36kr/news.js +1 -1
  5. package/clis/amazon/discussion.js +37 -6
  6. package/clis/amazon/discussion.test.js +147 -32
  7. package/clis/apple-podcasts/commands.test.js +4 -4
  8. package/clis/apple-podcasts/episodes.js +1 -1
  9. package/clis/apple-podcasts/search.js +1 -1
  10. package/clis/apple-podcasts/top.js +1 -1
  11. package/clis/arxiv/paper.js +1 -1
  12. package/clis/arxiv/search.js +1 -1
  13. package/clis/band/mentions.js +3 -3
  14. package/clis/bbc/news.js +1 -1
  15. package/clis/bilibili/subtitle.js +2 -2
  16. package/clis/bloomberg/businessweek.js +1 -1
  17. package/clis/bloomberg/economics.js +1 -1
  18. package/clis/bloomberg/industries.js +1 -1
  19. package/clis/bloomberg/main.js +1 -1
  20. package/clis/bloomberg/markets.js +1 -1
  21. package/clis/bloomberg/opinions.js +1 -1
  22. package/clis/bloomberg/politics.js +1 -1
  23. package/clis/bloomberg/tech.js +1 -1
  24. package/clis/boss/search.js +49 -8
  25. package/clis/boss/search.test.js +78 -0
  26. package/clis/boss/send.js +3 -3
  27. package/clis/chatgpt/image.js +37 -8
  28. package/clis/chatgpt/image.test.js +92 -0
  29. package/clis/chatgpt/utils.js +39 -6
  30. package/clis/chatgpt/utils.test.js +63 -0
  31. package/clis/chatgpt-app/ask.js +4 -20
  32. package/clis/chatgpt-app/ax.js +135 -2
  33. package/clis/chatgpt-app/ax.test.js +35 -0
  34. package/clis/chatgpt-app/model.js +1 -1
  35. package/clis/chatgpt-app/new.js +1 -1
  36. package/clis/chatgpt-app/read.js +1 -1
  37. package/clis/chatgpt-app/send.js +3 -22
  38. package/clis/chatgpt-app/status.js +1 -1
  39. package/clis/chatwise/ask.js +2 -2
  40. package/clis/chatwise/model.js +2 -2
  41. package/clis/chatwise/send.js +2 -2
  42. package/clis/claude/ask.js +128 -0
  43. package/clis/claude/ask.test.js +338 -0
  44. package/clis/claude/commands.test.js +118 -0
  45. package/clis/claude/detail.js +29 -0
  46. package/clis/claude/history.js +31 -0
  47. package/clis/claude/new.js +21 -0
  48. package/clis/claude/read.js +24 -0
  49. package/clis/claude/send.js +41 -0
  50. package/clis/claude/status.js +24 -0
  51. package/clis/claude/utils.js +440 -0
  52. package/clis/claude/utils.test.js +148 -0
  53. package/clis/codex/ask.js +2 -2
  54. package/clis/codex/send.js +2 -2
  55. package/clis/ctrip/search.js +1 -1
  56. package/clis/ctrip/search.test.js +4 -4
  57. package/clis/cursor/ask.js +2 -2
  58. package/clis/cursor/composer.js +2 -2
  59. package/clis/cursor/send.js +2 -2
  60. package/clis/deepseek/ask.js +49 -10
  61. package/clis/deepseek/ask.test.js +150 -3
  62. package/clis/deepseek/utils.js +60 -22
  63. package/clis/deepseek/utils.test.js +124 -5
  64. package/clis/doubao/utils.js +53 -11
  65. package/clis/doubao/utils.test.js +22 -2
  66. package/clis/eastmoney/announcement.js +1 -1
  67. package/clis/eastmoney/convertible.js +1 -1
  68. package/clis/eastmoney/etf.js +1 -1
  69. package/clis/eastmoney/holders.js +1 -1
  70. package/clis/eastmoney/index-board.js +1 -1
  71. package/clis/eastmoney/kline.js +1 -1
  72. package/clis/eastmoney/kuaixun.js +1 -1
  73. package/clis/eastmoney/longhu.js +1 -1
  74. package/clis/eastmoney/money-flow.js +1 -1
  75. package/clis/eastmoney/northbound.js +1 -1
  76. package/clis/eastmoney/quote.js +1 -1
  77. package/clis/eastmoney/rank.js +1 -1
  78. package/clis/eastmoney/sectors.js +1 -1
  79. package/clis/facebook/marketplace-inbox.js +83 -0
  80. package/clis/facebook/marketplace-listings.js +83 -0
  81. package/clis/facebook/marketplace.test.js +91 -0
  82. package/clis/google/news.js +1 -1
  83. package/clis/google/suggest.js +1 -1
  84. package/clis/google/trends.js +1 -1
  85. package/clis/google-scholar/cite.js +74 -0
  86. package/clis/google-scholar/cite.test.js +47 -0
  87. package/clis/google-scholar/profile.js +92 -0
  88. package/clis/google-scholar/profile.test.js +49 -0
  89. package/clis/google-scholar/search.js +1 -1
  90. package/clis/google-scholar/search.test.js +15 -0
  91. package/clis/hf/top.js +1 -1
  92. package/clis/jd/item.js +679 -47
  93. package/clis/jd/item.test.js +318 -7
  94. package/clis/jd/item.test.ts +517 -0
  95. package/clis/lesswrong/comments.js +1 -1
  96. package/clis/lesswrong/curated.js +1 -1
  97. package/clis/lesswrong/frontpage.js +1 -1
  98. package/clis/lesswrong/new.js +1 -1
  99. package/clis/lesswrong/read.js +1 -1
  100. package/clis/lesswrong/sequences.js +1 -1
  101. package/clis/lesswrong/shortform.js +1 -1
  102. package/clis/lesswrong/tag.js +1 -1
  103. package/clis/lesswrong/tags.js +1 -1
  104. package/clis/lesswrong/top-month.js +1 -1
  105. package/clis/lesswrong/top-week.js +1 -1
  106. package/clis/lesswrong/top-year.js +1 -1
  107. package/clis/lesswrong/top.js +1 -1
  108. package/clis/lesswrong/user-posts.js +1 -1
  109. package/clis/lesswrong/user.js +1 -1
  110. package/clis/paperreview/commands.test.js +6 -6
  111. package/clis/paperreview/feedback.js +1 -1
  112. package/clis/paperreview/review.js +1 -1
  113. package/clis/paperreview/submit.js +1 -1
  114. package/clis/powerchina/search.js +250 -0
  115. package/clis/powerchina/search.test.js +67 -0
  116. package/clis/producthunt/posts.js +1 -1
  117. package/clis/producthunt/today.js +1 -1
  118. package/clis/sinablog/search.js +1 -1
  119. package/clis/sinafinance/news.js +1 -1
  120. package/clis/sinafinance/stock.js +6 -3
  121. package/clis/sinafinance/stock.test.js +59 -0
  122. package/clis/spotify/spotify.js +6 -6
  123. package/clis/substack/search.js +1 -1
  124. package/clis/toutiao/articles.js +80 -0
  125. package/clis/toutiao/articles.test.js +30 -0
  126. package/clis/twitter/followers.js +2 -2
  127. package/clis/twitter/following.js +224 -73
  128. package/clis/twitter/following.test.js +277 -0
  129. package/clis/twitter/post.js +184 -47
  130. package/clis/twitter/post.test.js +114 -34
  131. package/clis/uiverse/_shared.js +63 -4
  132. package/clis/uiverse/_shared.test.js +7 -0
  133. package/clis/uiverse/code.js +1 -0
  134. package/clis/uiverse/navigation.test.js +12 -0
  135. package/clis/uiverse/preview.js +1 -0
  136. package/clis/web/read.js +319 -81
  137. package/clis/web/read.test.js +221 -5
  138. package/clis/weibo/favorites.js +169 -0
  139. package/clis/weibo/favorites.test.js +114 -0
  140. package/clis/weibo/publish.js +282 -0
  141. package/clis/weibo/publish.test.js +183 -0
  142. package/clis/weixin/create-draft.js +225 -0
  143. package/clis/weixin/drafts.js +65 -0
  144. package/clis/weixin/drafts.test.js +65 -0
  145. package/clis/weread/ranking.js +1 -1
  146. package/clis/weread/search-regression.test.js +8 -8
  147. package/clis/weread/search.js +1 -1
  148. package/clis/wikipedia/random.js +1 -1
  149. package/clis/wikipedia/search.js +1 -1
  150. package/clis/wikipedia/summary.js +1 -1
  151. package/clis/wikipedia/trending.js +1 -1
  152. package/clis/xianyu/chat.js +3 -3
  153. package/clis/xianyu/item.js +2 -2
  154. package/clis/xianyu/item.test.js +3 -3
  155. package/clis/xiaohongshu/search.js +17 -2
  156. package/clis/xiaohongshu/search.test.js +37 -1
  157. package/clis/xiaoyuzhou/download.js +1 -1
  158. package/clis/xiaoyuzhou/download.test.js +3 -3
  159. package/clis/xiaoyuzhou/episode.js +1 -1
  160. package/clis/xiaoyuzhou/podcast-episodes.js +1 -1
  161. package/clis/xiaoyuzhou/podcast-episodes.test.js +2 -2
  162. package/clis/xiaoyuzhou/podcast.js +1 -1
  163. package/clis/xiaoyuzhou/transcript.js +1 -1
  164. package/clis/xiaoyuzhou/transcript.test.js +5 -5
  165. package/clis/yollomi/models.js +1 -1
  166. package/clis/youtube/channel.js +24 -1
  167. package/clis/youtube/channel.test.js +59 -0
  168. package/clis/zhihu/answer.js +21 -162
  169. package/clis/zhihu/answer.test.js +26 -53
  170. package/clis/zhihu/collection.js +197 -0
  171. package/clis/zhihu/collection.test.js +290 -0
  172. package/clis/zhihu/collections.js +127 -0
  173. package/clis/zhihu/collections.test.js +182 -0
  174. package/clis/zhihu/comment.js +24 -305
  175. package/clis/zhihu/comment.test.js +31 -35
  176. package/clis/zhihu/favorite.js +44 -182
  177. package/clis/zhihu/favorite.test.js +30 -167
  178. package/clis/zhihu/follow.js +25 -56
  179. package/clis/zhihu/follow.test.js +20 -23
  180. package/clis/zhihu/like.js +22 -67
  181. package/clis/zhihu/like.test.js +19 -42
  182. package/clis/zhihu/search.js +3 -2
  183. package/clis/zhihu/write-shared.js +8 -1
  184. package/clis/zhihu/write-shared.test.js +1 -0
  185. package/clis/zlibrary/commands.test.js +75 -0
  186. package/clis/zlibrary/info.js +47 -0
  187. package/clis/zlibrary/search.js +46 -0
  188. package/clis/zlibrary/utils.js +136 -0
  189. package/dist/src/adapter-source.d.ts +11 -0
  190. package/dist/src/adapter-source.js +24 -0
  191. package/dist/src/adapter-source.test.js +29 -0
  192. package/dist/src/browser/base-page.d.ts +3 -1
  193. package/dist/src/browser/base-page.js +76 -1
  194. package/dist/src/browser/base-page.test.d.ts +1 -0
  195. package/dist/src/browser/base-page.test.js +74 -0
  196. package/dist/src/browser/bridge.d.ts +1 -0
  197. package/dist/src/browser/bridge.js +36 -9
  198. package/dist/src/browser/cdp.d.ts +1 -0
  199. package/dist/src/browser/cdp.js +3 -3
  200. package/dist/src/browser/daemon-client.d.ts +38 -4
  201. package/dist/src/browser/daemon-client.js +24 -7
  202. package/dist/src/browser/daemon-client.test.js +49 -0
  203. package/dist/src/browser/errors.js +3 -0
  204. package/dist/src/browser/errors.test.js +3 -0
  205. package/dist/src/browser/network-cache.d.ts +1 -0
  206. package/dist/src/browser/page.d.ts +3 -1
  207. package/dist/src/browser/page.js +10 -2
  208. package/dist/src/browser/profile.d.ts +14 -0
  209. package/dist/src/browser/profile.js +85 -0
  210. package/dist/src/build-manifest.d.ts +2 -0
  211. package/dist/src/build-manifest.js +13 -3
  212. package/dist/src/build-manifest.test.js +20 -2
  213. package/dist/src/cli.d.ts +6 -0
  214. package/dist/src/cli.js +462 -32
  215. package/dist/src/cli.test.js +209 -2
  216. package/dist/src/commanderAdapter.js +29 -9
  217. package/dist/src/commanderAdapter.test.js +78 -2
  218. package/dist/src/commands/daemon.js +6 -0
  219. package/dist/src/completion-shared.js +1 -2
  220. package/dist/src/completion.test.js +3 -2
  221. package/dist/src/daemon.js +125 -41
  222. package/dist/src/doctor.d.ts +4 -6
  223. package/dist/src/doctor.js +80 -22
  224. package/dist/src/doctor.test.js +82 -0
  225. package/dist/src/engine.test.js +6 -5
  226. package/dist/src/errors.d.ts +14 -8
  227. package/dist/src/errors.js +36 -30
  228. package/dist/src/errors.test.js +5 -5
  229. package/dist/src/execution.d.ts +4 -0
  230. package/dist/src/execution.js +173 -25
  231. package/dist/src/execution.test.js +171 -1
  232. package/dist/src/main.js +10 -0
  233. package/dist/src/observation/artifact.d.ts +16 -0
  234. package/dist/src/observation/artifact.js +260 -0
  235. package/dist/src/observation/artifact.test.d.ts +1 -0
  236. package/dist/src/observation/artifact.test.js +121 -0
  237. package/dist/src/observation/events.d.ts +89 -0
  238. package/dist/src/observation/events.js +1 -0
  239. package/dist/src/observation/index.d.ts +7 -0
  240. package/dist/src/observation/index.js +7 -0
  241. package/dist/src/observation/manager.d.ts +9 -0
  242. package/dist/src/observation/manager.js +27 -0
  243. package/dist/src/observation/manager.test.d.ts +1 -0
  244. package/dist/src/observation/manager.test.js +13 -0
  245. package/dist/src/observation/redaction.d.ts +11 -0
  246. package/dist/src/observation/redaction.js +81 -0
  247. package/dist/src/observation/redaction.test.d.ts +1 -0
  248. package/dist/src/observation/redaction.test.js +32 -0
  249. package/dist/src/observation/retention.d.ts +32 -0
  250. package/dist/src/observation/retention.js +160 -0
  251. package/dist/src/observation/retention.test.d.ts +1 -0
  252. package/dist/src/observation/retention.test.js +118 -0
  253. package/dist/src/observation/ring-buffer.d.ts +22 -0
  254. package/dist/src/observation/ring-buffer.js +45 -0
  255. package/dist/src/observation/ring-buffer.test.d.ts +1 -0
  256. package/dist/src/observation/ring-buffer.test.js +22 -0
  257. package/dist/src/observation/session.d.ts +25 -0
  258. package/dist/src/observation/session.js +50 -0
  259. package/dist/src/pipeline/executor.test.js +1 -0
  260. package/dist/src/pipeline/steps/download.test.js +1 -0
  261. package/dist/src/pipeline/steps/fetch.js +1 -21
  262. package/dist/src/pipeline/steps/fetch.test.js +6 -12
  263. package/dist/src/plugin-scaffold.js +1 -1
  264. package/dist/src/plugin-scaffold.test.js +1 -1
  265. package/dist/src/registry.d.ts +40 -9
  266. package/dist/src/registry.js +3 -1
  267. package/dist/src/runtime-detect.d.ts +10 -0
  268. package/dist/src/runtime-detect.js +19 -0
  269. package/dist/src/runtime-detect.test.js +12 -1
  270. package/dist/src/runtime.d.ts +2 -0
  271. package/dist/src/runtime.js +1 -0
  272. package/dist/src/types.d.ts +22 -0
  273. package/dist/src/update-check.d.ts +31 -1
  274. package/dist/src/update-check.js +62 -16
  275. package/dist/src/update-check.test.js +86 -1
  276. package/package.json +1 -1
  277. package/dist/src/diagnostic.d.ts +0 -63
  278. package/dist/src/diagnostic.js +0 -292
  279. package/dist/src/diagnostic.test.js +0 -302
  280. /package/dist/src/{diagnostic.test.d.ts → adapter-source.test.d.ts} +0 -0
@@ -0,0 +1,80 @@
1
+ import { cli } from '@jackwener/opencli/registry';
2
+
3
+ export function parseToutiaoArticlesText(text) {
4
+ const NON_TITLE_LINES = new Set([
5
+ '展现', '阅读', '点赞', '评论',
6
+ '查看数据', '查看评论', '修改', '更多', '首发',
7
+ '已发布', '定时发布', '定时发布中', '由文章生成', '审核中',
8
+ ]);
9
+ const lines = String(text || '').split('\n').map((line) => line.trim()).filter(Boolean);
10
+ const results = [];
11
+
12
+ for (let i = 0; i < lines.length; i++) {
13
+ const line = lines[i];
14
+ if (!/^\d{2}-\d{2}\s+\d{2}:\d{2}$/.test(line)) continue;
15
+
16
+ const date = line;
17
+ let title = '';
18
+ let status = '';
19
+ let stats = null;
20
+
21
+ for (let back = 3; back >= 1; back--) {
22
+ const prev = lines[i - back] || '';
23
+ if (!prev || prev.length >= 100 || /^\d+$/.test(prev) || NON_TITLE_LINES.has(prev)) continue;
24
+ title = prev;
25
+ break;
26
+ }
27
+
28
+ for (let fwd = 1; fwd < 8; fwd++) {
29
+ const fwdLine = lines[i + fwd] || '';
30
+ if (fwdLine === '已发布' || fwdLine === '定时发布中' || fwdLine === '审核中' || fwdLine === '由文章生成') {
31
+ status = fwdLine;
32
+ }
33
+ if (fwdLine.includes('展现') && fwdLine.includes('阅读')) {
34
+ const match = fwdLine.match(/展现\s*([\d,]+)\s*阅读\s*([\d,]+)\s*点赞\s*([\d,]+)\s*评论\s*([\d,]*)/);
35
+ if (match) {
36
+ stats = {
37
+ '展现': match[1],
38
+ '阅读': match[2],
39
+ '点赞': match[3],
40
+ '评论': match[4] || '0',
41
+ };
42
+ }
43
+ }
44
+ }
45
+
46
+ if (title && stats) results.push({ title, date, status, ...stats });
47
+ }
48
+
49
+ return results;
50
+ }
51
+
52
+ cli({
53
+ site: 'toutiao',
54
+ name: 'articles',
55
+ description: '获取头条号创作者后台文章列表及数据',
56
+ domain: 'mp.toutiao.com',
57
+ args: [
58
+ { name: 'page', type: 'int', default: 1, help: '页码 (1-4)' },
59
+ ],
60
+ columns: ['title', 'date', 'status', '展现', '阅读', '点赞', '评论'],
61
+ pipeline: [
62
+ { navigate: 'https://mp.toutiao.com/profile_v4/manage/content/all?page=${{ args.page }}' },
63
+ { wait: 'networkidle' },
64
+ { wait: 3000 },
65
+ {
66
+ evaluate: `
67
+ (async () => {
68
+ // Wait for content to load
69
+ await new Promise(r => setTimeout(r, 2000));
70
+ const parse = ${parseToutiaoArticlesText.toString()};
71
+ return parse(document.body.innerText || '');
72
+ })()
73
+ `
74
+ },
75
+ ],
76
+ });
77
+
78
+ export const __test__ = {
79
+ parseToutiaoArticlesText,
80
+ };
@@ -0,0 +1,30 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { __test__ } from './articles.js';
3
+
4
+ describe('toutiao articles parser', () => {
5
+ const articleText = [
6
+ '短标题',
7
+ '04-20 20:30',
8
+ '已发布',
9
+ '展现 8 阅读 0 点赞 0 评论 0',
10
+ ].join('\n');
11
+ const parsedArticle = {
12
+ title: '短标题',
13
+ date: '04-20 20:30',
14
+ status: '已发布',
15
+ '展现': '8',
16
+ '阅读': '0',
17
+ '点赞': '0',
18
+ '评论': '0',
19
+ };
20
+
21
+ it('keeps short chinese titles instead of silently dropping the row', () => {
22
+ expect(__test__.parseToutiaoArticlesText(articleText)).toEqual([parsedArticle]);
23
+ });
24
+
25
+ it('keeps parsing when serialized into the browser evaluate context', () => {
26
+ const parse = Function(`return (${__test__.parseToutiaoArticlesText.toString()})`)();
27
+
28
+ expect(parse(articleText)).toEqual([parsedArticle]);
29
+ });
30
+ });
@@ -1,4 +1,4 @@
1
- import { AuthRequiredError, SelectorError } from '@jackwener/opencli/errors';
1
+ import { AuthRequiredError, selectorError } from '@jackwener/opencli/errors';
2
2
  import { cli, Strategy } from '@jackwener/opencli/registry';
3
3
  cli({
4
4
  site: 'twitter',
@@ -49,7 +49,7 @@ cli({
49
49
  return false;
50
50
  }`);
51
51
  if (!clicked) {
52
- throw new SelectorError('Twitter followers link', 'Twitter may have changed the layout.');
52
+ throw selectorError('Twitter followers link', 'Twitter may have changed the layout.');
53
53
  }
54
54
  await page.waitForCapture(5);
55
55
  // 4. Scroll to trigger pagination API calls
@@ -1,11 +1,142 @@
1
- import { AuthRequiredError, SelectorError } from '@jackwener/opencli/errors';
2
1
  import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
3
+ import { resolveTwitterQueryId, sanitizeQueryId } from './shared.js';
4
+
5
+ const BEARER_TOKEN = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
6
+ const FOLLOWING_QUERY_ID = 'zx6e-TLzRkeDO_a7p4b3JQ'; // Following fallback
7
+ const USER_BY_SCREEN_NAME_QUERY_ID = 'qRednkZG-rn1P6b48NINmQ';
8
+
9
+ const FEATURES = {
10
+ rweb_video_screen_enabled: false,
11
+ profile_label_improvements_pcf_label_in_post_enabled: true,
12
+ responsive_web_profile_redirect_enabled: false,
13
+ rweb_tipjar_consumption_enabled: false,
14
+ verified_phone_label_enabled: false,
15
+ creator_subscriptions_tweet_preview_api_enabled: true,
16
+ responsive_web_graphql_timeline_navigation_enabled: true,
17
+ responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
18
+ premium_content_api_read_enabled: false,
19
+ communities_web_enable_tweet_community_results_fetch: true,
20
+ c9s_tweet_anatomy_moderator_badge_enabled: true,
21
+ responsive_web_grok_analyze_button_fetch_trends_enabled: false,
22
+ responsive_web_grok_analyze_post_followups_enabled: true,
23
+ responsive_web_jetfuel_frame: true,
24
+ responsive_web_grok_share_attachment_enabled: true,
25
+ responsive_web_grok_annotations_enabled: true,
26
+ articles_preview_enabled: true,
27
+ responsive_web_edit_tweet_api_enabled: true,
28
+ graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
29
+ view_counts_everywhere_api_enabled: true,
30
+ longform_notetweets_consumption_enabled: true,
31
+ responsive_web_twitter_article_tweet_consumption_enabled: true,
32
+ tweet_awards_web_tipping_enabled: false,
33
+ content_disclosure_indicator_enabled: true,
34
+ content_disclosure_ai_generated_indicator_enabled: true,
35
+ responsive_web_grok_show_grok_translated_post: false,
36
+ responsive_web_grok_analysis_button_from_backend: true,
37
+ post_ctas_fetch_enabled: false,
38
+ freedom_of_speech_not_reach_fetch_enabled: true,
39
+ standardized_nudges_misinfo: true,
40
+ tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
41
+ longform_notetweets_rich_text_read_enabled: true,
42
+ longform_notetweets_inline_media_enabled: false,
43
+ responsive_web_grok_image_annotation_enabled: true,
44
+ responsive_web_grok_imagine_annotation_enabled: true,
45
+ responsive_web_grok_community_note_auto_translation_is_enabled: false,
46
+ responsive_web_enhance_cards_enabled: false,
47
+ };
48
+
49
+ function buildFollowingUrl(queryId, userId, count, cursor) {
50
+ const vars = {
51
+ userId,
52
+ count,
53
+ includePromotedContent: false,
54
+ withClientEventToken: false,
55
+ withBirdwatchNotes: false,
56
+ withVoice: true,
57
+ withV2Timeline: true,
58
+ };
59
+ if (cursor)
60
+ vars.cursor = cursor;
61
+ return `/i/api/graphql/${queryId}/Following`
62
+ + `?variables=${encodeURIComponent(JSON.stringify(vars))}`
63
+ + `&features=${encodeURIComponent(JSON.stringify(FEATURES))}`;
64
+ }
65
+
66
+ function buildUserByScreenNameUrl(queryId, screenName) {
67
+ const vars = JSON.stringify({ screen_name: screenName, withSafetyModeUserFields: true });
68
+ const feats = JSON.stringify({
69
+ hidden_profile_subscriptions_enabled: true,
70
+ rweb_tipjar_consumption_enabled: true,
71
+ responsive_web_graphql_exclude_directive_enabled: true,
72
+ verified_phone_label_enabled: false,
73
+ subscriptions_verification_info_is_identity_verified_enabled: true,
74
+ subscriptions_verification_info_verified_since_enabled: true,
75
+ highlights_tweets_tab_ui_enabled: true,
76
+ responsive_web_twitter_article_notes_tab_enabled: true,
77
+ subscriptions_feature_can_gift_premium: true,
78
+ creator_subscriptions_tweet_preview_api_enabled: true,
79
+ responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
80
+ responsive_web_graphql_timeline_navigation_enabled: true,
81
+ });
82
+ return `/i/api/graphql/${queryId}/UserByScreenName`
83
+ + `?variables=${encodeURIComponent(vars)}`
84
+ + `&features=${encodeURIComponent(feats)}`;
85
+ }
86
+
87
+ function extractUser(result) {
88
+ if (!result || result.__typename !== 'User')
89
+ return null;
90
+ const core = result.core || {};
91
+ const legacy = result.legacy || {};
92
+ return {
93
+ screen_name: core.screen_name || legacy.screen_name || 'unknown',
94
+ name: core.name || legacy.name || 'unknown',
95
+ bio: legacy.description || result.profile_bio?.description || '',
96
+ followers: legacy.followers_count || legacy.normal_followers_count || 0,
97
+ };
98
+ }
99
+
100
+ function parseFollowing(data) {
101
+ const users = [];
102
+ let nextCursor = null;
103
+ const instructions = data?.data?.user?.result?.timeline_v2?.timeline?.instructions
104
+ || data?.data?.user?.result?.timeline?.timeline?.instructions
105
+ || [];
106
+ for (const inst of instructions) {
107
+ for (const entry of inst.entries || []) {
108
+ const content = entry.content;
109
+ // Extract cursor
110
+ if (content?.entryType === 'TimelineTimelineCursor' || content?.__typename === 'TimelineTimelineCursor') {
111
+ if (content.cursorType === 'Bottom' || content.cursorType === 'ShowMore')
112
+ nextCursor = content.value;
113
+ continue;
114
+ }
115
+ if (entry.entryId?.startsWith('cursor-bottom-') || entry.entryId?.startsWith('cursor-showMore-')) {
116
+ nextCursor = content?.value || content?.itemContent?.value || nextCursor;
117
+ continue;
118
+ }
119
+ // Extract user
120
+ if (entry.entryId?.startsWith('user-')) {
121
+ const user = extractUser(content?.itemContent?.user_results?.result);
122
+ if (user)
123
+ users.push(user);
124
+ }
125
+ }
126
+ }
127
+ return { users, nextCursor };
128
+ }
129
+
130
+ function normalizeScreenName(value) {
131
+ return String(value || '').trim().replace(/^\/+/, '').replace(/^@+/, '');
132
+ }
133
+
3
134
  cli({
4
135
  site: 'twitter',
5
136
  name: 'following',
6
137
  description: 'Get accounts a Twitter/X user is following',
7
138
  domain: 'x.com',
8
- strategy: Strategy.INTERCEPT,
139
+ strategy: Strategy.COOKIE,
9
140
  browser: true,
10
141
  args: [
11
142
  { name: 'user', positional: true, type: 'string', required: false },
@@ -13,83 +144,103 @@ cli({
13
144
  ],
14
145
  columns: ['screen_name', 'name', 'bio', 'followers'],
15
146
  func: async (page, kwargs) => {
16
- let targetUser = kwargs.user;
17
- // If no user is specified, figure out the logged-in user's handle
147
+ const limit = kwargs.limit === undefined || kwargs.limit === null ? 50 : Number(kwargs.limit);
148
+ if (!Number.isInteger(limit) || limit <= 0) {
149
+ throw new ArgumentError('twitter following --limit must be a positive integer', 'Example: opencli twitter following @elonmusk --limit 200');
150
+ }
151
+ let targetUser = normalizeScreenName(kwargs.user);
152
+
153
+ await page.goto('https://x.com');
154
+ await page.wait(3);
155
+
156
+ const ct0 = await page.evaluate(`() => {
157
+ return document.cookie.split(';').map(c => c.trim()).find(c => c.startsWith('ct0='))?.split('=')[1] || null;
158
+ }`);
159
+ if (!ct0)
160
+ throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');
161
+
18
162
  if (!targetUser) {
19
- await page.goto('https://x.com/home');
20
- await page.wait({ selector: '[data-testid="primaryColumn"]' });
21
163
  const href = await page.evaluate(`() => {
22
- const link = document.querySelector('a[data-testid="AppTabBar_Profile_Link"]');
23
- return link ? link.getAttribute('href') : null;
24
- }`);
25
- if (!href) {
26
- throw new AuthRequiredError('x.com', 'Could not find logged-in user profile link. Are you logged in?');
27
- }
28
- targetUser = href.replace('/', '');
164
+ const link = document.querySelector('a[data-testid="AppTabBar_Profile_Link"]');
165
+ return link ? link.getAttribute('href') : null;
166
+ }`);
167
+ if (!href)
168
+ throw new AuthRequiredError('x.com', 'Could not detect logged-in user. Are you logged in?');
169
+ targetUser = normalizeScreenName(href.replace('/', ''));
29
170
  }
30
- // 1. Navigate to profile page
31
- await page.goto(`https://x.com/${targetUser}`);
32
- await page.wait(3);
33
- // 2. Install interceptor BEFORE SPA navigation.
34
- // goto() resets JS context, but SPA click preserves it.
35
- await page.installInterceptor('Following');
36
- // 3. Click the following link via SPA navigation (preserves interceptor)
37
- const safeUser = JSON.stringify(targetUser);
38
- const clicked = await page.evaluate(`() => {
39
- const target = ${safeUser};
40
- const link = document.querySelector('a[href="/' + target + '/following"]');
41
- if (link) { link.click(); return true; }
42
- return false;
43
- }`);
44
- if (!clicked) {
45
- throw new SelectorError('Twitter following link', 'Twitter may have changed the layout.');
171
+ if (!targetUser) {
172
+ throw new ArgumentError('twitter following user cannot be empty', 'Example: opencli twitter following @elonmusk --limit 200');
46
173
  }
47
- await page.waitForCapture(5);
48
- // 4. Scroll to trigger pagination API calls
49
- await page.autoScroll({ times: Math.ceil(kwargs.limit / 20), delayMs: 2000 });
50
- // 5. Retrieve intercepted data
51
- const requests = await page.getInterceptedRequests();
52
- const requestList = Array.isArray(requests) ? requests : [];
53
- if (requestList.length === 0) {
54
- return [];
174
+
175
+ const followingQueryId = await resolveTwitterQueryId(page, 'Following', FOLLOWING_QUERY_ID);
176
+ const userByScreenNameQueryId = await resolveTwitterQueryId(page, 'UserByScreenName', USER_BY_SCREEN_NAME_QUERY_ID);
177
+ const headers = JSON.stringify({
178
+ 'Authorization': `Bearer ${decodeURIComponent(BEARER_TOKEN)}`,
179
+ 'X-Csrf-Token': ct0,
180
+ 'X-Twitter-Auth-Type': 'OAuth2Session',
181
+ 'X-Twitter-Active-User': 'yes',
182
+ });
183
+
184
+ // Get userId from screen_name
185
+ const userLookup = await page.evaluate(`async () => {
186
+ const url = ${JSON.stringify(buildUserByScreenNameUrl(userByScreenNameQueryId, targetUser))};
187
+ const resp = await fetch(url, { headers: ${headers}, credentials: 'include' });
188
+ if (!resp.ok) return { error: resp.status };
189
+ const d = await resp.json();
190
+ return { userId: d.data?.user?.result?.rest_id || null };
191
+ }`);
192
+ if (userLookup?.error === 401 || userLookup?.error === 403) {
193
+ throw new AuthRequiredError('x.com', `Twitter user lookup failed (HTTP ${userLookup.error})`);
55
194
  }
56
- let results = [];
57
- for (const req of requestList) {
58
- try {
59
- // GraphQL response: { data: { user: { result: { timeline: ... } } } }
60
- let instructions = req.data?.user?.result?.timeline?.timeline?.instructions;
61
- if (!instructions)
62
- continue;
63
- let addEntries = instructions.find((i) => i.type === 'TimelineAddEntries');
64
- if (!addEntries) {
65
- addEntries = instructions.find((i) => i.entries && Array.isArray(i.entries));
66
- }
67
- if (!addEntries)
68
- continue;
69
- for (const entry of addEntries.entries) {
70
- if (!entry.entryId.startsWith('user-'))
71
- continue;
72
- const item = entry.content?.itemContent?.user_results?.result;
73
- if (!item || item.__typename !== 'User')
74
- continue;
75
- const core = item.core || {};
76
- const legacy = item.legacy || {};
77
- results.push({
78
- screen_name: core.screen_name || legacy.screen_name || 'unknown',
79
- name: core.name || legacy.name || 'unknown',
80
- bio: legacy.description || item.profile_bio?.description || '',
81
- followers: legacy.followers_count || legacy.normal_followers_count || 0
82
- });
83
- }
195
+ if (userLookup?.error) {
196
+ throw new CommandExecutionError(`HTTP ${userLookup.error}: Failed to resolve Twitter user @${targetUser}`);
197
+ }
198
+ const userId = userLookup?.userId || null;
199
+ if (!userId)
200
+ throw new CommandExecutionError(`Could not find user @${targetUser}`);
201
+
202
+ const allUsers = [];
203
+ const seen = new Set();
204
+ let cursor = null;
205
+
206
+ const maxPages = Math.ceil(limit / 50) + 2;
207
+ for (let i = 0; i < maxPages && allUsers.length < limit; i++) {
208
+ const fetchCount = Math.min(50, limit - allUsers.length + 10);
209
+ const apiUrl = buildFollowingUrl(followingQueryId, userId, fetchCount, cursor);
210
+ const data = await page.evaluate(`async () => {
211
+ const r = await fetch("${apiUrl}", { headers: ${headers}, credentials: 'include' });
212
+ return r.ok ? await r.json() : { error: r.status };
213
+ }`);
214
+ if (data?.error) {
215
+ if (data.error === 401 || data.error === 403)
216
+ throw new AuthRequiredError('x.com', `Twitter following request failed (HTTP ${data.error})`);
217
+ throw new CommandExecutionError(`HTTP ${data.error}: Failed to fetch following list. queryId may have expired.`);
84
218
  }
85
- catch (e) {
86
- // ignore parsing errors for individual payloads
219
+ const { users, nextCursor } = parseFollowing(data);
220
+ for (const u of users) {
221
+ if (!seen.has(u.screen_name)) {
222
+ seen.add(u.screen_name);
223
+ allUsers.push(u);
224
+ }
87
225
  }
226
+ if (!nextCursor || nextCursor === cursor)
227
+ break;
228
+ cursor = nextCursor;
88
229
  }
89
- // Deduplicate by screen_name
90
- const unique = new Map();
91
- results.forEach(r => unique.set(r.screen_name, r));
92
- const deduplicatedResults = Array.from(unique.values());
93
- return deduplicatedResults.slice(0, kwargs.limit);
94
- }
230
+
231
+ if (allUsers.length === 0) {
232
+ throw new EmptyResultError('twitter following', `No following accounts found for @${targetUser}`);
233
+ }
234
+
235
+ return allUsers.slice(0, limit);
236
+ },
95
237
  });
238
+
239
+ export const __test__ = {
240
+ sanitizeQueryId,
241
+ buildFollowingUrl,
242
+ buildUserByScreenNameUrl,
243
+ extractUser,
244
+ normalizeScreenName,
245
+ parseFollowing,
246
+ };