@jackwener/opencli 1.7.8 → 1.7.10

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 (281) hide show
  1. package/README.md +49 -14
  2. package/README.zh-CN.md +30 -10
  3. package/cli-manifest.json +646 -30
  4. package/clis/36kr/news.js +1 -1
  5. package/clis/apple-podcasts/commands.test.js +4 -4
  6. package/clis/apple-podcasts/episodes.js +1 -1
  7. package/clis/apple-podcasts/search.js +1 -1
  8. package/clis/apple-podcasts/top.js +1 -1
  9. package/clis/arxiv/paper.js +1 -1
  10. package/clis/arxiv/search.js +1 -1
  11. package/clis/band/mentions.js +3 -3
  12. package/clis/bbc/news.js +1 -1
  13. package/clis/bilibili/subtitle.js +2 -2
  14. package/clis/bloomberg/businessweek.js +1 -1
  15. package/clis/bloomberg/economics.js +1 -1
  16. package/clis/bloomberg/industries.js +1 -1
  17. package/clis/bloomberg/main.js +1 -1
  18. package/clis/bloomberg/markets.js +1 -1
  19. package/clis/bloomberg/opinions.js +1 -1
  20. package/clis/bloomberg/politics.js +1 -1
  21. package/clis/bloomberg/tech.js +1 -1
  22. package/clis/boss/search.js +49 -8
  23. package/clis/boss/search.test.js +78 -0
  24. package/clis/boss/send.js +3 -3
  25. package/clis/chatgpt/image.js +37 -8
  26. package/clis/chatgpt/image.test.js +92 -0
  27. package/clis/chatgpt/utils.js +39 -6
  28. package/clis/chatgpt/utils.test.js +63 -0
  29. package/clis/chatgpt-app/ask.js +1 -1
  30. package/clis/chatgpt-app/ax.js +4 -2
  31. package/clis/chatgpt-app/ax.test.js +12 -0
  32. package/clis/chatgpt-app/model.js +1 -1
  33. package/clis/chatgpt-app/new.js +1 -1
  34. package/clis/chatgpt-app/read.js +1 -1
  35. package/clis/chatgpt-app/send.js +1 -1
  36. package/clis/chatgpt-app/status.js +1 -1
  37. package/clis/chatwise/ask.js +2 -2
  38. package/clis/chatwise/model.js +2 -2
  39. package/clis/chatwise/send.js +2 -2
  40. package/clis/claude/ask.js +128 -0
  41. package/clis/claude/ask.test.js +338 -0
  42. package/clis/claude/commands.test.js +118 -0
  43. package/clis/claude/detail.js +29 -0
  44. package/clis/claude/history.js +31 -0
  45. package/clis/claude/new.js +21 -0
  46. package/clis/claude/read.js +24 -0
  47. package/clis/claude/send.js +41 -0
  48. package/clis/claude/status.js +24 -0
  49. package/clis/claude/utils.js +440 -0
  50. package/clis/claude/utils.test.js +148 -0
  51. package/clis/codex/ask.js +2 -2
  52. package/clis/codex/send.js +2 -2
  53. package/clis/ctrip/search.js +1 -1
  54. package/clis/ctrip/search.test.js +4 -4
  55. package/clis/cursor/ask.js +2 -2
  56. package/clis/cursor/composer.js +2 -2
  57. package/clis/cursor/send.js +2 -2
  58. package/clis/deepseek/ask.js +17 -4
  59. package/clis/deepseek/ask.test.js +46 -0
  60. package/clis/deepseek/utils.js +55 -16
  61. package/clis/deepseek/utils.test.js +124 -5
  62. package/clis/doubao/utils.js +53 -11
  63. package/clis/doubao/utils.test.js +22 -2
  64. package/clis/eastmoney/announcement.js +1 -1
  65. package/clis/eastmoney/convertible.js +1 -1
  66. package/clis/eastmoney/etf.js +1 -1
  67. package/clis/eastmoney/holders.js +1 -1
  68. package/clis/eastmoney/index-board.js +1 -1
  69. package/clis/eastmoney/kline.js +1 -1
  70. package/clis/eastmoney/kuaixun.js +1 -1
  71. package/clis/eastmoney/longhu.js +1 -1
  72. package/clis/eastmoney/money-flow.js +1 -1
  73. package/clis/eastmoney/northbound.js +1 -1
  74. package/clis/eastmoney/quote.js +1 -1
  75. package/clis/eastmoney/rank.js +1 -1
  76. package/clis/eastmoney/sectors.js +1 -1
  77. package/clis/facebook/marketplace-inbox.js +83 -0
  78. package/clis/facebook/marketplace-listings.js +83 -0
  79. package/clis/facebook/marketplace.test.js +91 -0
  80. package/clis/google/news.js +1 -1
  81. package/clis/google/suggest.js +1 -1
  82. package/clis/google/trends.js +1 -1
  83. package/clis/google-scholar/cite.js +74 -0
  84. package/clis/google-scholar/cite.test.js +47 -0
  85. package/clis/google-scholar/profile.js +92 -0
  86. package/clis/google-scholar/profile.test.js +49 -0
  87. package/clis/google-scholar/search.js +1 -1
  88. package/clis/google-scholar/search.test.js +15 -0
  89. package/clis/hf/top.js +1 -1
  90. package/clis/instagram/collection-create.js +57 -0
  91. package/clis/instagram/saved.js +21 -7
  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/producthunt/posts.js +1 -1
  115. package/clis/producthunt/today.js +1 -1
  116. package/clis/sinablog/search.js +1 -1
  117. package/clis/sinafinance/news.js +1 -1
  118. package/clis/sinafinance/stock.js +1 -1
  119. package/clis/sinafinance/stock.test.js +2 -2
  120. package/clis/spotify/spotify.js +6 -6
  121. package/clis/substack/search.js +1 -1
  122. package/clis/toutiao/articles.js +5 -6
  123. package/clis/toutiao/articles.test.js +22 -15
  124. package/clis/twitter/followers.js +2 -2
  125. package/clis/twitter/following.js +224 -73
  126. package/clis/twitter/following.test.js +277 -0
  127. package/clis/twitter/post.js +184 -47
  128. package/clis/twitter/post.test.js +114 -34
  129. package/clis/uiverse/_shared.js +63 -4
  130. package/clis/uiverse/_shared.test.js +7 -0
  131. package/clis/uiverse/code.js +1 -0
  132. package/clis/uiverse/navigation.test.js +12 -0
  133. package/clis/uiverse/preview.js +1 -0
  134. package/clis/web/read.js +319 -81
  135. package/clis/web/read.test.js +221 -5
  136. package/clis/weibo/favorites.js +169 -0
  137. package/clis/weibo/favorites.test.js +114 -0
  138. package/clis/weibo/publish.js +282 -0
  139. package/clis/weibo/publish.test.js +183 -0
  140. package/clis/weread/ranking.js +1 -1
  141. package/clis/weread/search-regression.test.js +8 -8
  142. package/clis/weread/search.js +1 -1
  143. package/clis/wikipedia/random.js +1 -1
  144. package/clis/wikipedia/search.js +1 -1
  145. package/clis/wikipedia/summary.js +1 -1
  146. package/clis/wikipedia/trending.js +1 -1
  147. package/clis/xianyu/chat.js +3 -3
  148. package/clis/xianyu/item.js +2 -2
  149. package/clis/xianyu/item.test.js +3 -3
  150. package/clis/xiaohongshu/search.js +17 -2
  151. package/clis/xiaohongshu/search.test.js +37 -1
  152. package/clis/xiaoyuzhou/download.js +1 -1
  153. package/clis/xiaoyuzhou/download.test.js +3 -3
  154. package/clis/xiaoyuzhou/episode.js +1 -1
  155. package/clis/xiaoyuzhou/podcast-episodes.js +1 -1
  156. package/clis/xiaoyuzhou/podcast-episodes.test.js +2 -2
  157. package/clis/xiaoyuzhou/podcast.js +1 -1
  158. package/clis/xiaoyuzhou/transcript.js +1 -1
  159. package/clis/xiaoyuzhou/transcript.test.js +5 -5
  160. package/clis/yollomi/models.js +1 -1
  161. package/clis/youtube/channel.js +24 -1
  162. package/clis/youtube/channel.test.js +59 -0
  163. package/clis/zhihu/answer.js +21 -162
  164. package/clis/zhihu/answer.test.js +26 -53
  165. package/clis/zhihu/collection.js +197 -0
  166. package/clis/zhihu/collection.test.js +290 -0
  167. package/clis/zhihu/collections.js +127 -0
  168. package/clis/zhihu/collections.test.js +182 -0
  169. package/clis/zhihu/comment.js +24 -305
  170. package/clis/zhihu/comment.test.js +31 -35
  171. package/clis/zhihu/favorite.js +44 -182
  172. package/clis/zhihu/favorite.test.js +30 -167
  173. package/clis/zhihu/follow.js +25 -56
  174. package/clis/zhihu/follow.test.js +20 -23
  175. package/clis/zhihu/like.js +22 -67
  176. package/clis/zhihu/like.test.js +19 -42
  177. package/clis/zhihu/search.js +3 -2
  178. package/clis/zhihu/write-shared.js +8 -1
  179. package/clis/zhihu/write-shared.test.js +1 -0
  180. package/clis/zlibrary/commands.test.js +75 -0
  181. package/clis/zlibrary/info.js +47 -0
  182. package/clis/zlibrary/search.js +46 -0
  183. package/clis/zlibrary/utils.js +136 -0
  184. package/dist/src/adapter-source.d.ts +11 -0
  185. package/dist/src/adapter-source.js +24 -0
  186. package/dist/src/adapter-source.test.js +29 -0
  187. package/dist/src/browser/base-page.d.ts +3 -1
  188. package/dist/src/browser/base-page.js +76 -1
  189. package/dist/src/browser/base-page.test.d.ts +1 -0
  190. package/dist/src/browser/base-page.test.js +74 -0
  191. package/dist/src/browser/bridge.d.ts +1 -2
  192. package/dist/src/browser/bridge.js +40 -41
  193. package/dist/src/browser/cdp.d.ts +1 -0
  194. package/dist/src/browser/cdp.js +3 -3
  195. package/dist/src/browser/daemon-client.d.ts +38 -4
  196. package/dist/src/browser/daemon-client.js +24 -7
  197. package/dist/src/browser/daemon-client.test.js +49 -0
  198. package/dist/src/browser/daemon-lifecycle.d.ts +23 -0
  199. package/dist/src/browser/daemon-lifecycle.js +67 -0
  200. package/dist/src/browser/daemon-version.d.ts +4 -0
  201. package/dist/src/browser/daemon-version.js +12 -0
  202. package/dist/src/browser/errors.js +3 -0
  203. package/dist/src/browser/errors.test.js +3 -0
  204. package/dist/src/browser/network-cache.d.ts +1 -0
  205. package/dist/src/browser/page.d.ts +3 -1
  206. package/dist/src/browser/page.js +10 -2
  207. package/dist/src/browser/profile.d.ts +14 -0
  208. package/dist/src/browser/profile.js +85 -0
  209. package/dist/src/build-manifest.d.ts +2 -0
  210. package/dist/src/build-manifest.js +13 -3
  211. package/dist/src/build-manifest.test.js +20 -2
  212. package/dist/src/cli.d.ts +6 -0
  213. package/dist/src/cli.js +477 -35
  214. package/dist/src/cli.test.js +303 -2
  215. package/dist/src/commanderAdapter.js +17 -9
  216. package/dist/src/commanderAdapter.test.js +67 -2
  217. package/dist/src/commands/daemon.d.ts +2 -0
  218. package/dist/src/commands/daemon.js +42 -1
  219. package/dist/src/commands/daemon.test.js +103 -2
  220. package/dist/src/completion-shared.js +1 -2
  221. package/dist/src/completion.test.js +3 -2
  222. package/dist/src/daemon.js +125 -41
  223. package/dist/src/doctor.d.ts +5 -6
  224. package/dist/src/doctor.js +77 -19
  225. package/dist/src/doctor.test.js +117 -0
  226. package/dist/src/engine.test.js +6 -5
  227. package/dist/src/errors.d.ts +14 -8
  228. package/dist/src/errors.js +36 -30
  229. package/dist/src/errors.test.js +5 -5
  230. package/dist/src/execution.d.ts +4 -0
  231. package/dist/src/execution.js +173 -25
  232. package/dist/src/execution.test.js +171 -1
  233. package/dist/src/main.js +10 -0
  234. package/dist/src/observation/artifact.d.ts +16 -0
  235. package/dist/src/observation/artifact.js +260 -0
  236. package/dist/src/observation/artifact.test.d.ts +1 -0
  237. package/dist/src/observation/artifact.test.js +121 -0
  238. package/dist/src/observation/events.d.ts +89 -0
  239. package/dist/src/observation/events.js +1 -0
  240. package/dist/src/observation/index.d.ts +7 -0
  241. package/dist/src/observation/index.js +7 -0
  242. package/dist/src/observation/manager.d.ts +9 -0
  243. package/dist/src/observation/manager.js +27 -0
  244. package/dist/src/observation/manager.test.d.ts +1 -0
  245. package/dist/src/observation/manager.test.js +13 -0
  246. package/dist/src/observation/redaction.d.ts +11 -0
  247. package/dist/src/observation/redaction.js +81 -0
  248. package/dist/src/observation/redaction.test.d.ts +1 -0
  249. package/dist/src/observation/redaction.test.js +32 -0
  250. package/dist/src/observation/retention.d.ts +32 -0
  251. package/dist/src/observation/retention.js +160 -0
  252. package/dist/src/observation/retention.test.d.ts +1 -0
  253. package/dist/src/observation/retention.test.js +118 -0
  254. package/dist/src/observation/ring-buffer.d.ts +22 -0
  255. package/dist/src/observation/ring-buffer.js +45 -0
  256. package/dist/src/observation/ring-buffer.test.d.ts +1 -0
  257. package/dist/src/observation/ring-buffer.test.js +22 -0
  258. package/dist/src/observation/session.d.ts +25 -0
  259. package/dist/src/observation/session.js +50 -0
  260. package/dist/src/pipeline/executor.test.js +1 -0
  261. package/dist/src/pipeline/steps/download.test.js +1 -0
  262. package/dist/src/pipeline/steps/fetch.js +1 -21
  263. package/dist/src/pipeline/steps/fetch.test.js +6 -12
  264. package/dist/src/plugin-scaffold.js +1 -1
  265. package/dist/src/plugin-scaffold.test.js +1 -1
  266. package/dist/src/registry.d.ts +40 -9
  267. package/dist/src/registry.js +3 -1
  268. package/dist/src/runtime-detect.d.ts +10 -0
  269. package/dist/src/runtime-detect.js +19 -0
  270. package/dist/src/runtime-detect.test.js +12 -1
  271. package/dist/src/runtime.d.ts +2 -0
  272. package/dist/src/runtime.js +1 -0
  273. package/dist/src/types.d.ts +22 -0
  274. package/dist/src/update-check.d.ts +31 -1
  275. package/dist/src/update-check.js +62 -16
  276. package/dist/src/update-check.test.js +86 -1
  277. package/package.json +1 -1
  278. package/dist/src/diagnostic.d.ts +0 -63
  279. package/dist/src/diagnostic.js +0 -292
  280. package/dist/src/diagnostic.test.js +0 -302
  281. /package/dist/src/{diagnostic.test.d.ts → adapter-source.test.d.ts} +0 -0
@@ -18,7 +18,7 @@ cli({
18
18
  },
19
19
  ],
20
20
  columns: ['field', 'value'],
21
- func: async (_page, kwargs) => {
21
+ func: async (kwargs) => {
22
22
  const slug = gqlEscape(String(kwargs.username).toLowerCase());
23
23
  const query = `query UserProfile {
24
24
  user(input: {selector: {slug: "${slug}"}}) {
@@ -38,7 +38,7 @@ describe('paperreview submit command', () => {
38
38
  resolvedPath: '/tmp/paper.pdf',
39
39
  sizeBytes: 4096,
40
40
  });
41
- const result = await cmd.func(null, {
41
+ const result = await cmd.func({
42
42
  pdf: './paper.pdf',
43
43
  email: 'wang2629651228@gmail.com',
44
44
  venue: 'RAL',
@@ -80,7 +80,7 @@ describe('paperreview submit command', () => {
80
80
  message: 'Submission accepted',
81
81
  },
82
82
  });
83
- const result = await cmd.func(null, {
83
+ const result = await cmd.func({
84
84
  pdf: './paper.pdf',
85
85
  email: 'wang2629651228@gmail.com',
86
86
  venue: 'RAL',
@@ -112,7 +112,7 @@ describe('paperreview submit command', () => {
112
112
  s3_key: 'uploads/paper.pdf',
113
113
  },
114
114
  });
115
- const result = await cmd.func(null, {
115
+ const result = await cmd.func({
116
116
  pdf: './paper.pdf',
117
117
  email: 'wang2629651228@gmail.com',
118
118
  venue: 'RAL',
@@ -153,7 +153,7 @@ describe('paperreview submit command', () => {
153
153
  message: 'Submission accepted',
154
154
  },
155
155
  });
156
- const result = await cmd.func(null, {
156
+ const result = await cmd.func({
157
157
  pdf: './paper.pdf',
158
158
  email: 'wang2629651228@gmail.com',
159
159
  venue: 'RAL',
@@ -190,7 +190,7 @@ describe('paperreview review command', () => {
190
190
  response: { status: 202 },
191
191
  payload: { detail: 'Review is still processing.' },
192
192
  });
193
- const result = await cmd.func(null, { token: 'tok_123' });
193
+ const result = await cmd.func({ token: 'tok_123' });
194
194
  expect(result).toMatchObject({
195
195
  status: 'processing',
196
196
  token: 'tok_123',
@@ -214,7 +214,7 @@ describe('paperreview feedback command', () => {
214
214
  response: { ok: true, status: 200 },
215
215
  payload: { message: 'Thanks for the feedback.' },
216
216
  });
217
- const result = await cmd.func(null, {
217
+ const result = await cmd.func({
218
218
  token: 'tok_123',
219
219
  helpfulness: 4,
220
220
  'critical-error': 'yes',
@@ -17,7 +17,7 @@ cli({
17
17
  { name: 'additional-comments', help: 'Optional free-text feedback' },
18
18
  ],
19
19
  columns: ['status', 'token', 'helpfulness', 'critical_error', 'actionable_suggestions', 'message'],
20
- func: async (_page, kwargs) => {
20
+ func: async (kwargs) => {
21
21
  const token = String(kwargs.token ?? '').trim();
22
22
  if (!token) {
23
23
  throw new CliError('ARGUMENT', 'A review token is required.');
@@ -13,7 +13,7 @@ cli({
13
13
  { name: 'token', positional: true, required: true, help: 'Review token returned by paperreview.ai' },
14
14
  ],
15
15
  columns: ['status', 'title', 'venue', 'numerical_score', 'has_feedback', 'review_url'],
16
- func: async (_page, kwargs) => {
16
+ func: async (kwargs) => {
17
17
  const token = String(kwargs.token ?? '').trim();
18
18
  if (!token) {
19
19
  throw new CliError('ARGUMENT', 'A review token is required.');
@@ -24,7 +24,7 @@ cli({
24
24
  return 'prepared only';
25
25
  return undefined;
26
26
  },
27
- func: async (_page, kwargs) => {
27
+ func: async (kwargs) => {
28
28
  const pdfFile = await readPdfFile(kwargs.pdf);
29
29
  const email = String(kwargs.email ?? '').trim();
30
30
  const venue = normalizeVenue(kwargs.venue);
@@ -19,7 +19,7 @@ cli({
19
19
  },
20
20
  ],
21
21
  columns: ['rank', 'name', 'tagline', 'author', 'date', 'url'],
22
- func: async (_page, args) => {
22
+ func: async (args) => {
23
23
  const count = Math.min(Number(args.limit) || 20, 50);
24
24
  const category = String(args.category ?? '').trim() || undefined;
25
25
  const posts = await fetchFeed(category);
@@ -16,7 +16,7 @@ cli({
16
16
  { name: 'limit', type: 'int', default: 20, help: 'Max results' },
17
17
  ],
18
18
  columns: ['rank', 'name', 'tagline', 'author', 'url'],
19
- func: async (_page, args) => {
19
+ func: async (args) => {
20
20
  const count = Math.min(Number(args.limit) || 20, 50);
21
21
  const posts = await fetchFeed();
22
22
  if (posts.length === 0)
@@ -47,5 +47,5 @@ cli({
47
47
  { name: 'limit', type: 'int', default: 20, help: '返回的文章数量' },
48
48
  ],
49
49
  columns: ['rank', 'title', 'author', 'date', 'description', 'url'],
50
- func: async (_page, args) => searchSinaBlog(args.keyword, Math.max(1, Math.min(Number(args.limit) || 20, 50))),
50
+ func: async (args) => searchSinaBlog(args.keyword, Math.max(1, Math.min(Number(args.limit) || 20, 50))),
51
51
  });
@@ -34,7 +34,7 @@ cli({
34
34
  { name: 'type', type: 'int', default: 0, help: 'News type: 0=全部 1=A股 2=宏观 3=公司 4=数据 5=市场 6=国际 7=观点 8=央行 9=其它' },
35
35
  ],
36
36
  columns: ['id', 'time', 'content', 'views'],
37
- func: async (_page, args) => {
37
+ func: async (args) => {
38
38
  const limit = Math.max(1, Math.min(Number(args.limit), 50));
39
39
  const apiTag = TYPE_MAP[args.type] ?? 0;
40
40
  const params = new URLSearchParams({
@@ -62,7 +62,7 @@ cli({
62
62
  { name: 'market', type: 'string', default: 'auto', help: 'Market: cn, hk, us, auto (default: auto searches cn → hk → us)' },
63
63
  ],
64
64
  columns: ['Symbol', 'Name', 'Price', 'Change', 'ChangePercent', 'Open', 'High', 'Low', 'Volume', 'MarketCap'],
65
- func: async (_page, args) => {
65
+ func: async (args) => {
66
66
  const key = String(args.key);
67
67
  const market = String(args.market);
68
68
  const marketMap = {
@@ -28,7 +28,7 @@ describe('sinafinance stock command', () => {
28
28
  .mockResolvedValueOnce(textResponse('var hq_str_gb_AAPL="Apple Inc,189.98,1.23,0,1.56,0,188.50,180.00,195.00,175.00,1200000,0,3000000000000";'));
29
29
  vi.stubGlobal('fetch', fetchMock);
30
30
 
31
- const result = await cmd.func(null, { key: 'AAPL', market: 'auto' });
31
+ const result = await cmd.func({ key: 'AAPL', market: 'auto' });
32
32
 
33
33
  expect(fetchMock).toHaveBeenNthCalledWith(1, 'https://suggest3.sinajs.cn/suggest/type=11,31,41&key=AAPL', expect.any(Object));
34
34
  expect(fetchMock).toHaveBeenNthCalledWith(2, 'https://hq.sinajs.cn/list=gb_AAPL', expect.any(Object));
@@ -48,7 +48,7 @@ describe('sinafinance stock command', () => {
48
48
  .mockResolvedValueOnce(textResponse('var hq_str_gb_AAPL="苹果公司,189.98,1.23,0,1.56,0,188.50,180.00,195.00,175.00,1200000,0,3000000000000";'));
49
49
  vi.stubGlobal('fetch', fetchMock);
50
50
 
51
- const result = await cmd.func(null, { key: '苹果', market: 'auto' });
51
+ const result = await cmd.func({ key: '苹果', market: 'auto' });
52
52
 
53
53
  expect(fetchMock).toHaveBeenNthCalledWith(2, 'https://hq.sinajs.cn/list=gb_AAPL', expect.any(Object));
54
54
  expect(result[0]).toMatchObject({
@@ -198,7 +198,7 @@ cli({
198
198
  browser: false,
199
199
  args: [{ name: 'query', type: 'str', default: '', positional: true, help: 'Track or artist to play (optional)' }],
200
200
  columns: ['track', 'artist', 'status'],
201
- func: async (_page, kwargs) => {
201
+ func: async (kwargs) => {
202
202
  if (kwargs.query) {
203
203
  const { uri, name, artist } = await findTrackUri(kwargs.query);
204
204
  await api('PUT', '/me/player/play', { uris: [uri] });
@@ -246,7 +246,7 @@ cli({
246
246
  browser: false,
247
247
  args: [{ name: 'level', type: 'int', default: 50, positional: true, required: true, help: 'Volume 0–100' }],
248
248
  columns: ['volume'],
249
- func: async (_page, kwargs) => {
249
+ func: async (kwargs) => {
250
250
  const level = Math.round(kwargs.level);
251
251
  if (level < 0 || level > 100)
252
252
  throw new CliError('INVALID_ARGS', 'Volume must be between 0 and 100');
@@ -265,7 +265,7 @@ cli({
265
265
  { name: 'limit', type: 'int', default: 10, help: 'Number of results (default: 10)' },
266
266
  ],
267
267
  columns: ['track', 'artist', 'album', 'uri'],
268
- func: async (_page, kwargs) => {
268
+ func: async (kwargs) => {
269
269
  const limit = Math.min(50, Math.max(1, Math.round(kwargs.limit)));
270
270
  const data = await api('GET', `/search?q=${encodeURIComponent(kwargs.query)}&type=track&limit=${limit}`);
271
271
  const results = mapSpotifyTrackResults(data);
@@ -282,7 +282,7 @@ cli({
282
282
  browser: false,
283
283
  args: [{ name: 'query', type: 'str', required: true, positional: true, help: 'Track to add to queue' }],
284
284
  columns: ['track', 'artist', 'status'],
285
- func: async (_page, kwargs) => {
285
+ func: async (kwargs) => {
286
286
  const { uri, name, artist } = await findTrackUri(kwargs.query);
287
287
  await api('POST', `/me/player/queue?uri=${encodeURIComponent(uri)}`);
288
288
  return [{ track: name, artist, status: 'added to queue' }];
@@ -296,7 +296,7 @@ cli({
296
296
  browser: false,
297
297
  args: [{ name: 'state', type: 'str', default: 'on', positional: true, choices: ['on', 'off'], help: 'on or off' }],
298
298
  columns: ['shuffle'],
299
- func: async (_page, kwargs) => {
299
+ func: async (kwargs) => {
300
300
  await api('PUT', `/me/player/shuffle?state=${kwargs.state === 'on'}`);
301
301
  return [{ shuffle: kwargs.state }];
302
302
  },
@@ -309,7 +309,7 @@ cli({
309
309
  browser: false,
310
310
  args: [{ name: 'mode', type: 'str', default: 'context', positional: true, choices: ['off', 'track', 'context'], help: 'off / track / context' }],
311
311
  columns: ['repeat'],
312
- func: async (_page, kwargs) => {
312
+ func: async (kwargs) => {
313
313
  await api('PUT', `/me/player/repeat?state=${kwargs.mode}`);
314
314
  return [{ repeat: kwargs.mode }];
315
315
  },
@@ -69,7 +69,7 @@ cli({
69
69
  { name: 'limit', type: 'int', default: 20, help: '返回结果数量' },
70
70
  ],
71
71
  columns: ['rank', 'title', 'author', 'date', 'description', 'url'],
72
- func: async (_page, args) => {
72
+ func: async (args) => {
73
73
  const limit = Math.max(1, Math.min(Number(args.limit) || 20, 50));
74
74
  return args.type === 'publications'
75
75
  ? searchPublications(args.keyword, limit)
@@ -1,12 +1,11 @@
1
1
  import { cli } from '@jackwener/opencli/registry';
2
2
 
3
- const NON_TITLE_LINES = new Set([
4
- '展现', '阅读', '点赞', '评论',
5
- '查看数据', '查看评论', '修改', '更多', '首发',
6
- '已发布', '定时发布', '定时发布中', '由文章生成', '审核中',
7
- ]);
8
-
9
3
  export function parseToutiaoArticlesText(text) {
4
+ const NON_TITLE_LINES = new Set([
5
+ '展现', '阅读', '点赞', '评论',
6
+ '查看数据', '查看评论', '修改', '更多', '首发',
7
+ '已发布', '定时发布', '定时发布中', '由文章生成', '审核中',
8
+ ]);
10
9
  const lines = String(text || '').split('\n').map((line) => line.trim()).filter(Boolean);
11
10
  const results = [];
12
11
 
@@ -2,22 +2,29 @@ import { describe, expect, it } from 'vitest';
2
2
  import { __test__ } from './articles.js';
3
3
 
4
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
+
5
21
  it('keeps short chinese titles instead of silently dropping the row', () => {
6
- const text = [
7
- '短标题',
8
- '04-20 20:30',
9
- '已发布',
10
- '展现 8 阅读 0 点赞 0 评论 0',
11
- ].join('\n');
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()})`)();
12
27
 
13
- expect(__test__.parseToutiaoArticlesText(text)).toEqual([{
14
- title: '短标题',
15
- date: '04-20 20:30',
16
- status: '已发布',
17
- '展现': '8',
18
- '阅读': '0',
19
- '点赞': '0',
20
- '评论': '0',
21
- }]);
28
+ expect(parse(articleText)).toEqual([parsedArticle]);
22
29
  });
23
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
+ };