@jackwener/opencli 1.4.0 → 1.5.0

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 (514) hide show
  1. package/.github/actions/setup-chrome/action.yml +5 -4
  2. package/.github/workflows/build-extension.yml +2 -6
  3. package/.github/workflows/ci.yml +37 -3
  4. package/.github/workflows/e2e-headed.yml +16 -3
  5. package/CHANGELOG.md +23 -0
  6. package/PRIVACY.md +57 -0
  7. package/README.md +36 -7
  8. package/README.zh-CN.md +13 -6
  9. package/SKILL.md +103 -2
  10. package/dist/browser/cdp.d.ts +2 -1
  11. package/dist/browser/discover.d.ts +4 -1
  12. package/dist/browser/discover.js +6 -2
  13. package/dist/browser/errors.d.ts +2 -2
  14. package/dist/browser/errors.js +4 -12
  15. package/dist/browser/mcp.d.ts +2 -1
  16. package/dist/build-manifest.d.ts +2 -0
  17. package/dist/build-manifest.js +39 -14
  18. package/dist/build-manifest.test.js +21 -0
  19. package/dist/capabilityRouting.d.ts +2 -0
  20. package/dist/capabilityRouting.js +2 -1
  21. package/dist/cli-manifest.json +1838 -151
  22. package/dist/cli.js +34 -3
  23. package/dist/clis/36kr/article.d.ts +1 -0
  24. package/dist/clis/36kr/article.js +62 -0
  25. package/dist/clis/36kr/hot.d.ts +3 -0
  26. package/dist/clis/36kr/hot.js +80 -0
  27. package/dist/clis/36kr/hot.test.d.ts +1 -0
  28. package/dist/clis/36kr/hot.test.js +15 -0
  29. package/dist/clis/36kr/news.d.ts +1 -0
  30. package/dist/clis/36kr/news.js +51 -0
  31. package/dist/clis/36kr/news.test.d.ts +1 -0
  32. package/dist/clis/36kr/news.test.js +85 -0
  33. package/dist/clis/36kr/search.d.ts +1 -0
  34. package/dist/clis/36kr/search.js +72 -0
  35. package/dist/clis/apple-podcasts/search.js +2 -1
  36. package/dist/clis/arxiv/search.js +2 -2
  37. package/dist/clis/bbc/news.js +0 -1
  38. package/dist/clis/bilibili/comments.d.ts +5 -0
  39. package/dist/clis/bilibili/comments.js +40 -0
  40. package/dist/clis/bilibili/comments.test.d.ts +1 -0
  41. package/dist/clis/bilibili/comments.test.js +82 -0
  42. package/dist/clis/chatgpt/ask.js +29 -14
  43. package/dist/clis/chatgpt/ax.d.ts +6 -0
  44. package/dist/clis/chatgpt/ax.js +172 -1
  45. package/dist/clis/chatgpt/model.d.ts +1 -0
  46. package/dist/clis/chatgpt/model.js +24 -0
  47. package/dist/clis/chatgpt/send.js +12 -3
  48. package/dist/clis/ctrip/search.js +0 -1
  49. package/dist/clis/douban/download.d.ts +1 -0
  50. package/dist/clis/douban/download.js +67 -0
  51. package/dist/clis/douban/download.test.d.ts +1 -0
  52. package/dist/clis/douban/download.test.js +170 -0
  53. package/dist/clis/douban/photos.d.ts +1 -0
  54. package/dist/clis/douban/photos.js +34 -0
  55. package/dist/clis/douban/utils.d.ts +25 -0
  56. package/dist/clis/douban/utils.js +190 -1
  57. package/dist/clis/douban/utils.test.d.ts +1 -0
  58. package/dist/clis/douban/utils.test.js +64 -0
  59. package/dist/clis/douyin/_shared/browser-fetch.d.ts +10 -0
  60. package/dist/clis/douyin/_shared/browser-fetch.js +30 -0
  61. package/dist/clis/douyin/_shared/browser-fetch.test.d.ts +1 -0
  62. package/dist/clis/douyin/_shared/browser-fetch.test.js +31 -0
  63. package/dist/clis/douyin/_shared/creation-id.d.ts +1 -0
  64. package/dist/clis/douyin/_shared/creation-id.js +5 -0
  65. package/dist/clis/douyin/_shared/creation-id.test.d.ts +1 -0
  66. package/dist/clis/douyin/_shared/creation-id.test.js +22 -0
  67. package/dist/clis/douyin/_shared/imagex-upload.d.ts +20 -0
  68. package/dist/clis/douyin/_shared/imagex-upload.js +53 -0
  69. package/dist/clis/douyin/_shared/imagex-upload.test.d.ts +1 -0
  70. package/dist/clis/douyin/_shared/imagex-upload.test.js +87 -0
  71. package/dist/clis/douyin/_shared/sts2.d.ts +8 -0
  72. package/dist/clis/douyin/_shared/sts2.js +15 -0
  73. package/dist/clis/douyin/_shared/text-extra.d.ts +18 -0
  74. package/dist/clis/douyin/_shared/text-extra.js +15 -0
  75. package/dist/clis/douyin/_shared/text-extra.test.d.ts +1 -0
  76. package/dist/clis/douyin/_shared/text-extra.test.js +37 -0
  77. package/dist/clis/douyin/_shared/timing.d.ts +2 -0
  78. package/dist/clis/douyin/_shared/timing.js +22 -0
  79. package/dist/clis/douyin/_shared/timing.test.d.ts +1 -0
  80. package/dist/clis/douyin/_shared/timing.test.js +28 -0
  81. package/dist/clis/douyin/_shared/tos-upload-short-read.test.d.ts +11 -0
  82. package/dist/clis/douyin/_shared/tos-upload-short-read.test.js +83 -0
  83. package/dist/clis/douyin/_shared/tos-upload.d.ts +53 -0
  84. package/dist/clis/douyin/_shared/tos-upload.js +295 -0
  85. package/dist/clis/douyin/_shared/tos-upload.test.d.ts +1 -0
  86. package/dist/clis/douyin/_shared/tos-upload.test.js +229 -0
  87. package/dist/clis/douyin/_shared/transcode.d.ts +27 -0
  88. package/dist/clis/douyin/_shared/transcode.js +45 -0
  89. package/dist/clis/douyin/_shared/transcode.test.d.ts +1 -0
  90. package/dist/clis/douyin/_shared/transcode.test.js +93 -0
  91. package/dist/clis/douyin/_shared/types.d.ts +26 -0
  92. package/dist/clis/douyin/_shared/types.js +1 -0
  93. package/dist/clis/douyin/activities.d.ts +1 -0
  94. package/dist/clis/douyin/activities.js +20 -0
  95. package/dist/clis/douyin/activities.test.d.ts +1 -0
  96. package/dist/clis/douyin/activities.test.js +22 -0
  97. package/dist/clis/douyin/collections.d.ts +1 -0
  98. package/dist/clis/douyin/collections.js +22 -0
  99. package/dist/clis/douyin/collections.test.d.ts +1 -0
  100. package/dist/clis/douyin/collections.test.js +23 -0
  101. package/dist/clis/douyin/delete.d.ts +1 -0
  102. package/dist/clis/douyin/delete.js +18 -0
  103. package/dist/clis/douyin/delete.test.d.ts +1 -0
  104. package/dist/clis/douyin/delete.test.js +11 -0
  105. package/dist/clis/douyin/draft.d.ts +14 -0
  106. package/dist/clis/douyin/draft.js +237 -0
  107. package/dist/clis/douyin/draft.test.d.ts +1 -0
  108. package/dist/clis/douyin/draft.test.js +11 -0
  109. package/dist/clis/douyin/drafts.d.ts +1 -0
  110. package/dist/clis/douyin/drafts.js +23 -0
  111. package/dist/clis/douyin/drafts.test.d.ts +1 -0
  112. package/dist/clis/douyin/drafts.test.js +11 -0
  113. package/dist/clis/douyin/hashtag.d.ts +1 -0
  114. package/dist/clis/douyin/hashtag.js +45 -0
  115. package/dist/clis/douyin/hashtag.test.d.ts +1 -0
  116. package/dist/clis/douyin/hashtag.test.js +25 -0
  117. package/dist/clis/douyin/location.d.ts +1 -0
  118. package/dist/clis/douyin/location.js +24 -0
  119. package/dist/clis/douyin/location.test.d.ts +1 -0
  120. package/dist/clis/douyin/location.test.js +23 -0
  121. package/dist/clis/douyin/profile.d.ts +1 -0
  122. package/dist/clis/douyin/profile.js +28 -0
  123. package/dist/clis/douyin/profile.test.d.ts +1 -0
  124. package/dist/clis/douyin/profile.test.js +11 -0
  125. package/dist/clis/douyin/publish.d.ts +14 -0
  126. package/dist/clis/douyin/publish.js +288 -0
  127. package/dist/clis/douyin/publish.test.d.ts +1 -0
  128. package/dist/clis/douyin/publish.test.js +38 -0
  129. package/dist/clis/douyin/stats.d.ts +1 -0
  130. package/dist/clis/douyin/stats.js +27 -0
  131. package/dist/clis/douyin/stats.test.d.ts +1 -0
  132. package/dist/clis/douyin/stats.test.js +22 -0
  133. package/dist/clis/douyin/update.d.ts +1 -0
  134. package/dist/clis/douyin/update.js +31 -0
  135. package/dist/clis/douyin/update.test.d.ts +1 -0
  136. package/dist/clis/douyin/update.test.js +11 -0
  137. package/dist/clis/douyin/videos.d.ts +1 -0
  138. package/dist/clis/douyin/videos.js +34 -0
  139. package/dist/clis/douyin/videos.test.d.ts +1 -0
  140. package/dist/clis/douyin/videos.test.js +11 -0
  141. package/dist/clis/hackernews/search.yaml +1 -1
  142. package/dist/clis/imdb/person.d.ts +1 -0
  143. package/dist/clis/imdb/person.js +203 -0
  144. package/dist/clis/imdb/reviews.d.ts +1 -0
  145. package/dist/clis/imdb/reviews.js +88 -0
  146. package/dist/clis/imdb/search.d.ts +1 -0
  147. package/dist/clis/imdb/search.js +161 -0
  148. package/dist/clis/imdb/title.d.ts +1 -0
  149. package/dist/clis/imdb/title.js +93 -0
  150. package/dist/clis/imdb/top.d.ts +1 -0
  151. package/dist/clis/imdb/top.js +53 -0
  152. package/dist/clis/imdb/trending.d.ts +1 -0
  153. package/dist/clis/imdb/trending.js +52 -0
  154. package/dist/clis/imdb/utils.d.ts +46 -0
  155. package/dist/clis/imdb/utils.js +285 -0
  156. package/dist/clis/imdb/utils.test.d.ts +1 -0
  157. package/dist/clis/imdb/utils.test.js +88 -0
  158. package/dist/clis/instagram/search.yaml +2 -1
  159. package/dist/clis/jd/item.d.ts +4 -0
  160. package/dist/clis/jd/item.js +16 -15
  161. package/dist/clis/jd/item.test.js +16 -1
  162. package/dist/clis/linux-do/categories.yaml +38 -9
  163. package/dist/clis/linux-do/category.d.ts +1 -0
  164. package/dist/clis/linux-do/category.js +36 -0
  165. package/dist/clis/linux-do/feed.d.ts +45 -0
  166. package/dist/clis/linux-do/feed.js +397 -0
  167. package/dist/clis/linux-do/feed.test.d.ts +1 -0
  168. package/dist/clis/linux-do/feed.test.js +118 -0
  169. package/dist/clis/linux-do/hot.d.ts +1 -0
  170. package/dist/clis/linux-do/hot.js +25 -0
  171. package/dist/clis/linux-do/latest.d.ts +1 -0
  172. package/dist/clis/linux-do/latest.js +18 -0
  173. package/dist/clis/linux-do/search.yaml +3 -1
  174. package/dist/clis/linux-do/tags.yaml +41 -0
  175. package/dist/clis/linux-do/topic.yaml +41 -3
  176. package/dist/clis/linux-do/user-posts.yaml +67 -0
  177. package/dist/clis/linux-do/user-topics.yaml +54 -0
  178. package/dist/clis/medium/search.js +1 -1
  179. package/dist/clis/paperreview/commands.test.d.ts +3 -0
  180. package/dist/clis/paperreview/commands.test.js +243 -0
  181. package/dist/clis/paperreview/feedback.d.ts +1 -0
  182. package/dist/clis/paperreview/feedback.js +52 -0
  183. package/dist/clis/paperreview/review.d.ts +1 -0
  184. package/dist/clis/paperreview/review.js +37 -0
  185. package/dist/clis/paperreview/submit.d.ts +1 -0
  186. package/dist/clis/paperreview/submit.js +85 -0
  187. package/dist/clis/paperreview/utils.d.ts +46 -0
  188. package/dist/clis/paperreview/utils.js +197 -0
  189. package/dist/clis/paperreview/utils.test.d.ts +1 -0
  190. package/dist/clis/paperreview/utils.test.js +49 -0
  191. package/dist/clis/producthunt/browse.d.ts +1 -0
  192. package/dist/clis/producthunt/browse.js +99 -0
  193. package/dist/clis/producthunt/hot.d.ts +1 -0
  194. package/dist/clis/producthunt/hot.js +110 -0
  195. package/dist/clis/producthunt/posts.d.ts +1 -0
  196. package/dist/clis/producthunt/posts.js +28 -0
  197. package/dist/clis/producthunt/today.d.ts +1 -0
  198. package/dist/clis/producthunt/today.js +35 -0
  199. package/dist/clis/producthunt/utils.d.ts +29 -0
  200. package/dist/clis/producthunt/utils.js +99 -0
  201. package/dist/clis/producthunt/utils.test.d.ts +1 -0
  202. package/dist/clis/producthunt/utils.test.js +64 -0
  203. package/dist/clis/reuters/search.js +0 -1
  204. package/dist/clis/twitter/article.js +4 -28
  205. package/dist/clis/twitter/likes.d.ts +24 -0
  206. package/dist/clis/twitter/likes.js +217 -0
  207. package/dist/clis/twitter/likes.test.d.ts +1 -0
  208. package/dist/clis/twitter/likes.test.js +85 -0
  209. package/dist/clis/twitter/profile.js +4 -28
  210. package/dist/clis/twitter/search.js +7 -4
  211. package/dist/clis/twitter/search.test.js +56 -2
  212. package/dist/clis/twitter/shared.d.ts +6 -0
  213. package/dist/clis/twitter/shared.js +35 -0
  214. package/dist/clis/twitter/timeline.js +2 -13
  215. package/dist/clis/weibo/comments.d.ts +1 -0
  216. package/dist/clis/weibo/comments.js +53 -0
  217. package/dist/clis/weibo/feed.d.ts +1 -0
  218. package/dist/clis/weibo/feed.js +56 -0
  219. package/dist/clis/weibo/hot.js +0 -1
  220. package/dist/clis/weibo/me.d.ts +1 -0
  221. package/dist/clis/weibo/me.js +76 -0
  222. package/dist/clis/weibo/post.d.ts +1 -0
  223. package/dist/clis/weibo/post.js +75 -0
  224. package/dist/clis/weibo/user.d.ts +1 -0
  225. package/dist/clis/weibo/user.js +63 -0
  226. package/dist/clis/weibo/utils.d.ts +6 -0
  227. package/dist/clis/weibo/utils.js +30 -0
  228. package/dist/clis/weixin/download.d.ts +17 -0
  229. package/dist/clis/weixin/download.js +88 -20
  230. package/dist/clis/weread/book.js +2 -2
  231. package/dist/clis/weread/commands.test.d.ts +3 -0
  232. package/dist/clis/weread/commands.test.js +43 -0
  233. package/dist/clis/weread/highlights.js +2 -2
  234. package/dist/clis/weread/notebooks.js +2 -2
  235. package/dist/clis/weread/notes.js +3 -3
  236. package/dist/clis/weread/search.js +3 -2
  237. package/dist/clis/weread/shelf.js +2 -2
  238. package/dist/clis/weread/utils.d.ts +4 -4
  239. package/dist/clis/weread/utils.js +32 -14
  240. package/dist/clis/weread/utils.test.js +1 -28
  241. package/dist/clis/xiaohongshu/comments.d.ts +5 -0
  242. package/dist/clis/xiaohongshu/comments.js +74 -0
  243. package/dist/clis/xiaohongshu/comments.test.d.ts +1 -0
  244. package/dist/clis/xiaohongshu/comments.test.js +79 -0
  245. package/dist/clis/xiaohongshu/publish.js +114 -18
  246. package/dist/clis/xiaohongshu/publish.test.d.ts +1 -0
  247. package/dist/clis/xiaohongshu/publish.test.js +119 -0
  248. package/dist/clis/xueqiu/search.yaml +2 -1
  249. package/dist/clis/yahoo-finance/quote.js +0 -1
  250. package/dist/clis/youtube/channel.d.ts +1 -0
  251. package/dist/clis/youtube/channel.js +150 -0
  252. package/dist/clis/youtube/comments.d.ts +1 -0
  253. package/dist/clis/youtube/comments.js +95 -0
  254. package/dist/clis/youtube/search.js +0 -1
  255. package/dist/clis/zhihu/search.yaml +2 -1
  256. package/dist/commanderAdapter.d.ts +1 -0
  257. package/dist/commanderAdapter.js +176 -29
  258. package/dist/commanderAdapter.test.d.ts +1 -0
  259. package/dist/commanderAdapter.test.js +62 -0
  260. package/dist/daemon.js +17 -1
  261. package/dist/discovery.js +8 -14
  262. package/dist/doctor.d.ts +1 -0
  263. package/dist/doctor.js +9 -2
  264. package/dist/download/index.js +63 -51
  265. package/dist/download/index.test.js +17 -4
  266. package/dist/errors.d.ts +3 -1
  267. package/dist/errors.js +15 -32
  268. package/dist/execution.d.ts +1 -3
  269. package/dist/execution.js +21 -1
  270. package/dist/external-clis.yaml +0 -17
  271. package/dist/hooks.js +2 -0
  272. package/dist/main.js +5 -0
  273. package/dist/output.js +5 -1
  274. package/dist/pipeline/executor.js +3 -4
  275. package/dist/plugin-manifest.d.ts +70 -0
  276. package/dist/plugin-manifest.js +160 -0
  277. package/dist/plugin-manifest.test.d.ts +4 -0
  278. package/dist/plugin-manifest.test.js +179 -0
  279. package/dist/plugin.d.ts +38 -5
  280. package/dist/plugin.js +267 -33
  281. package/dist/plugin.test.js +220 -3
  282. package/dist/registry.d.ts +4 -0
  283. package/dist/registry.js +2 -0
  284. package/dist/runtime-detect.d.ts +21 -0
  285. package/dist/runtime-detect.js +32 -0
  286. package/dist/runtime-detect.test.d.ts +1 -0
  287. package/dist/runtime-detect.test.js +27 -0
  288. package/dist/runtime.js +1 -1
  289. package/dist/serialization.d.ts +2 -0
  290. package/dist/serialization.js +6 -0
  291. package/dist/types.d.ts +1 -0
  292. package/dist/update-check.d.ts +22 -0
  293. package/dist/update-check.js +112 -0
  294. package/dist/weixin-download.test.d.ts +1 -0
  295. package/dist/weixin-download.test.js +30 -0
  296. package/dist/weread-private-api-regression.test.d.ts +1 -0
  297. package/dist/weread-private-api-regression.test.js +122 -0
  298. package/dist/weread-search-regression.test.d.ts +1 -0
  299. package/dist/weread-search-regression.test.js +39 -0
  300. package/dist/yaml-schema.d.ts +3 -0
  301. package/dist/yaml-schema.js +18 -1
  302. package/docs/.vitepress/config.mts +17 -0
  303. package/docs/adapters/browser/36kr.md +47 -0
  304. package/docs/adapters/browser/douban.md +14 -0
  305. package/docs/adapters/browser/douyin.md +75 -0
  306. package/docs/adapters/browser/imdb.md +47 -0
  307. package/docs/adapters/browser/jd.md +2 -2
  308. package/docs/adapters/browser/linux-do.md +181 -20
  309. package/docs/adapters/browser/paperreview.md +43 -0
  310. package/docs/adapters/browser/producthunt.md +49 -0
  311. package/docs/adapters/browser/twitter.md +6 -0
  312. package/docs/adapters/desktop/chatgpt.md +5 -0
  313. package/docs/adapters/index.md +12 -3
  314. package/docs/advanced/download.md +4 -0
  315. package/docs/advanced/rate-limiter-plugin.md +99 -0
  316. package/docs/guide/electron-app-cli.md +200 -0
  317. package/docs/guide/getting-started.md +1 -0
  318. package/docs/guide/plugins.md +87 -0
  319. package/docs/zh/guide/electron-app-cli.md +188 -0
  320. package/docs/zh/guide/getting-started.md +1 -0
  321. package/docs/zh/guide/plugins.md +65 -0
  322. package/extension/dist/background.js +508 -518
  323. package/extension/manifest.json +6 -2
  324. package/extension/package.json +2 -1
  325. package/extension/popup.html +84 -0
  326. package/extension/popup.js +25 -0
  327. package/extension/scripts/package-release.mjs +179 -0
  328. package/extension/src/background.ts +22 -1
  329. package/package.json +4 -1
  330. package/scripts/postinstall.js +10 -0
  331. package/src/browser/cdp.ts +2 -1
  332. package/src/browser/discover.ts +8 -3
  333. package/src/browser/errors.ts +13 -14
  334. package/src/browser/mcp.ts +2 -1
  335. package/src/build-manifest.test.ts +23 -0
  336. package/src/build-manifest.ts +40 -15
  337. package/src/capabilityRouting.ts +2 -1
  338. package/src/cli.ts +35 -3
  339. package/src/clis/36kr/article.ts +69 -0
  340. package/src/clis/36kr/hot.test.ts +19 -0
  341. package/src/clis/36kr/hot.ts +100 -0
  342. package/src/clis/36kr/news.test.ts +90 -0
  343. package/src/clis/36kr/news.ts +54 -0
  344. package/src/clis/36kr/search.ts +78 -0
  345. package/src/clis/apple-podcasts/search.ts +2 -1
  346. package/src/clis/arxiv/search.ts +2 -2
  347. package/src/clis/bbc/news.ts +0 -1
  348. package/src/clis/bilibili/comments.test.ts +102 -0
  349. package/src/clis/bilibili/comments.ts +44 -0
  350. package/src/clis/chatgpt/ask.ts +28 -14
  351. package/src/clis/chatgpt/ax.ts +180 -1
  352. package/src/clis/chatgpt/model.ts +27 -0
  353. package/src/clis/chatgpt/send.ts +16 -6
  354. package/src/clis/ctrip/search.ts +0 -1
  355. package/src/clis/douban/download.test.ts +196 -0
  356. package/src/clis/douban/download.ts +78 -0
  357. package/src/clis/douban/photos.ts +36 -0
  358. package/src/clis/douban/utils.test.ts +97 -0
  359. package/src/clis/douban/utils.ts +232 -1
  360. package/src/clis/douyin/_shared/browser-fetch.test.ts +38 -0
  361. package/src/clis/douyin/_shared/browser-fetch.ts +45 -0
  362. package/src/clis/douyin/_shared/creation-id.test.ts +26 -0
  363. package/src/clis/douyin/_shared/creation-id.ts +8 -0
  364. package/src/clis/douyin/_shared/imagex-upload.test.ts +113 -0
  365. package/src/clis/douyin/_shared/imagex-upload.ts +76 -0
  366. package/src/clis/douyin/_shared/sts2.ts +20 -0
  367. package/src/clis/douyin/_shared/text-extra.test.ts +42 -0
  368. package/src/clis/douyin/_shared/text-extra.ts +33 -0
  369. package/src/clis/douyin/_shared/timing.test.ts +38 -0
  370. package/src/clis/douyin/_shared/timing.ts +22 -0
  371. package/src/clis/douyin/_shared/tos-upload-short-read.test.ts +102 -0
  372. package/src/clis/douyin/_shared/tos-upload.test.ts +281 -0
  373. package/src/clis/douyin/_shared/tos-upload.ts +444 -0
  374. package/src/clis/douyin/_shared/transcode.test.ts +117 -0
  375. package/src/clis/douyin/_shared/transcode.ts +78 -0
  376. package/src/clis/douyin/_shared/types.ts +29 -0
  377. package/src/clis/douyin/activities.test.ts +25 -0
  378. package/src/clis/douyin/activities.ts +23 -0
  379. package/src/clis/douyin/collections.test.ts +26 -0
  380. package/src/clis/douyin/collections.ts +25 -0
  381. package/src/clis/douyin/delete.test.ts +12 -0
  382. package/src/clis/douyin/delete.ts +20 -0
  383. package/src/clis/douyin/draft.test.ts +12 -0
  384. package/src/clis/douyin/draft.ts +282 -0
  385. package/src/clis/douyin/drafts.test.ts +12 -0
  386. package/src/clis/douyin/drafts.ts +27 -0
  387. package/src/clis/douyin/hashtag.test.ts +28 -0
  388. package/src/clis/douyin/hashtag.ts +56 -0
  389. package/src/clis/douyin/location.test.ts +26 -0
  390. package/src/clis/douyin/location.ts +27 -0
  391. package/src/clis/douyin/profile.test.ts +12 -0
  392. package/src/clis/douyin/profile.ts +37 -0
  393. package/src/clis/douyin/publish.test.ts +45 -0
  394. package/src/clis/douyin/publish.ts +340 -0
  395. package/src/clis/douyin/stats.test.ts +25 -0
  396. package/src/clis/douyin/stats.ts +30 -0
  397. package/src/clis/douyin/update.test.ts +12 -0
  398. package/src/clis/douyin/update.ts +43 -0
  399. package/src/clis/douyin/videos.test.ts +12 -0
  400. package/src/clis/douyin/videos.ts +49 -0
  401. package/src/clis/hackernews/search.yaml +1 -1
  402. package/src/clis/imdb/person.ts +232 -0
  403. package/src/clis/imdb/reviews.ts +111 -0
  404. package/src/clis/imdb/search.ts +179 -0
  405. package/src/clis/imdb/title.ts +121 -0
  406. package/src/clis/imdb/top.ts +67 -0
  407. package/src/clis/imdb/trending.ts +66 -0
  408. package/src/clis/imdb/utils.test.ts +117 -0
  409. package/src/clis/imdb/utils.ts +305 -0
  410. package/src/clis/instagram/search.yaml +2 -1
  411. package/src/clis/jd/item.test.ts +18 -1
  412. package/src/clis/jd/item.ts +18 -15
  413. package/src/clis/linux-do/categories.yaml +38 -9
  414. package/src/clis/linux-do/category.ts +37 -0
  415. package/src/clis/linux-do/feed.test.ts +132 -0
  416. package/src/clis/linux-do/feed.ts +501 -0
  417. package/src/clis/linux-do/hot.ts +26 -0
  418. package/src/clis/linux-do/latest.ts +19 -0
  419. package/src/clis/linux-do/search.yaml +3 -1
  420. package/src/clis/linux-do/tags.yaml +41 -0
  421. package/src/clis/linux-do/topic.yaml +41 -3
  422. package/src/clis/linux-do/user-posts.yaml +67 -0
  423. package/src/clis/linux-do/user-topics.yaml +54 -0
  424. package/src/clis/medium/search.ts +1 -1
  425. package/src/clis/paperreview/commands.test.ts +283 -0
  426. package/src/clis/paperreview/feedback.ts +64 -0
  427. package/src/clis/paperreview/review.ts +47 -0
  428. package/src/clis/paperreview/submit.ts +119 -0
  429. package/src/clis/paperreview/utils.test.ts +68 -0
  430. package/src/clis/paperreview/utils.ts +276 -0
  431. package/src/clis/producthunt/browse.ts +109 -0
  432. package/src/clis/producthunt/hot.ts +127 -0
  433. package/src/clis/producthunt/posts.ts +29 -0
  434. package/src/clis/producthunt/today.ts +37 -0
  435. package/src/clis/producthunt/utils.test.ts +72 -0
  436. package/src/clis/producthunt/utils.ts +122 -0
  437. package/src/clis/reuters/search.ts +0 -1
  438. package/src/clis/twitter/article.ts +5 -28
  439. package/src/clis/twitter/likes.test.ts +91 -0
  440. package/src/clis/twitter/likes.ts +256 -0
  441. package/src/clis/twitter/profile.ts +5 -28
  442. package/src/clis/twitter/search.test.ts +71 -2
  443. package/src/clis/twitter/search.ts +8 -4
  444. package/src/clis/twitter/shared.ts +45 -0
  445. package/src/clis/twitter/timeline.ts +2 -13
  446. package/src/clis/weibo/comments.ts +54 -0
  447. package/src/clis/weibo/feed.ts +57 -0
  448. package/src/clis/weibo/hot.ts +0 -1
  449. package/src/clis/weibo/me.ts +77 -0
  450. package/src/clis/weibo/post.ts +77 -0
  451. package/src/clis/weibo/user.ts +64 -0
  452. package/src/clis/weibo/utils.ts +32 -0
  453. package/src/clis/weixin/download.ts +114 -20
  454. package/src/clis/weread/book.ts +2 -2
  455. package/src/clis/weread/commands.test.ts +57 -0
  456. package/src/clis/weread/highlights.ts +2 -2
  457. package/src/clis/weread/notebooks.ts +2 -2
  458. package/src/clis/weread/notes.ts +3 -3
  459. package/src/clis/weread/search.ts +3 -2
  460. package/src/clis/weread/shelf.ts +2 -2
  461. package/src/clis/weread/utils.test.ts +1 -32
  462. package/src/clis/weread/utils.ts +41 -16
  463. package/src/clis/xiaohongshu/comments.test.ts +96 -0
  464. package/src/clis/xiaohongshu/comments.ts +81 -0
  465. package/src/clis/xiaohongshu/publish.test.ts +137 -0
  466. package/src/clis/xiaohongshu/publish.ts +129 -18
  467. package/src/clis/xueqiu/search.yaml +2 -1
  468. package/src/clis/yahoo-finance/quote.ts +0 -1
  469. package/src/clis/youtube/channel.ts +155 -0
  470. package/src/clis/youtube/comments.ts +97 -0
  471. package/src/clis/youtube/search.ts +0 -1
  472. package/src/clis/zhihu/search.yaml +2 -1
  473. package/src/commanderAdapter.test.ts +78 -0
  474. package/src/commanderAdapter.ts +188 -24
  475. package/src/daemon.ts +19 -1
  476. package/src/discovery.ts +8 -15
  477. package/src/doctor.ts +13 -2
  478. package/src/download/index.test.ts +14 -4
  479. package/src/download/index.ts +67 -55
  480. package/src/errors.ts +25 -66
  481. package/src/execution.ts +28 -3
  482. package/src/external-clis.yaml +0 -17
  483. package/src/hooks.ts +1 -0
  484. package/src/main.ts +6 -0
  485. package/src/output.ts +3 -1
  486. package/src/pipeline/executor.ts +4 -6
  487. package/src/plugin-manifest.test.ts +223 -0
  488. package/src/plugin-manifest.ts +206 -0
  489. package/src/plugin.test.ts +246 -2
  490. package/src/plugin.ts +338 -36
  491. package/src/registry.ts +6 -1
  492. package/src/runtime-detect.test.ts +30 -0
  493. package/src/runtime-detect.ts +36 -0
  494. package/src/runtime.ts +1 -1
  495. package/src/serialization.ts +4 -0
  496. package/src/types.ts +1 -0
  497. package/src/update-check.ts +114 -0
  498. package/src/weixin-download.test.ts +64 -0
  499. package/src/weread-private-api-regression.test.ts +150 -0
  500. package/src/weread-search-regression.test.ts +44 -0
  501. package/src/yaml-schema.ts +20 -0
  502. package/tests/e2e/browser-auth.test.ts +13 -9
  503. package/tests/e2e/browser-public-extended.test.ts +162 -0
  504. package/tests/e2e/browser-public.test.ts +55 -136
  505. package/tests/e2e/helpers.ts +2 -1
  506. package/tests/e2e/public-commands.test.ts +37 -3
  507. package/tests/smoke/api-health.test.ts +1 -1
  508. package/vitest.config.ts +34 -17
  509. package/dist/clis/linux-do/category.yaml +0 -51
  510. package/dist/clis/linux-do/hot.yaml +0 -50
  511. package/dist/clis/linux-do/latest.yaml +0 -40
  512. package/src/clis/linux-do/category.yaml +0 -51
  513. package/src/clis/linux-do/hot.yaml +0 -50
  514. package/src/clis/linux-do/latest.yaml +0 -40
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Product Hunt shared helpers.
3
+ */
4
+ export const PRODUCTHUNT_CATEGORY_SLUGS = [
5
+ 'ai-agents',
6
+ 'ai-coding-agents',
7
+ 'ai-code-editors',
8
+ 'ai-chatbots',
9
+ 'ai-workflow-automation',
10
+ 'vibe-coding',
11
+ 'developer-tools',
12
+ 'productivity',
13
+ 'design-creative',
14
+ 'marketing-sales',
15
+ 'no-code-platforms',
16
+ 'llms',
17
+ 'finance',
18
+ 'social-community',
19
+ 'engineering-development',
20
+ ];
21
+ const UA = 'Mozilla/5.0 (compatible; opencli/1.0)';
22
+ /**
23
+ * Fetch Product Hunt Atom RSS feed.
24
+ * @param category Optional category slug (e.g. "ai", "developer-tools")
25
+ */
26
+ export async function fetchFeed(category) {
27
+ const url = category
28
+ ? `https://www.producthunt.com/feed?category=${encodeURIComponent(category)}`
29
+ : 'https://www.producthunt.com/feed';
30
+ const resp = await fetch(url, { headers: { 'User-Agent': UA } });
31
+ if (!resp.ok)
32
+ return [];
33
+ const xml = await resp.text();
34
+ return parseFeed(xml);
35
+ }
36
+ export function parseFeed(xml) {
37
+ const posts = [];
38
+ const entryRegex = /<entry>([\s\S]*?)<\/entry>/g;
39
+ let match;
40
+ let rank = 1;
41
+ while ((match = entryRegex.exec(xml))) {
42
+ const block = match[1];
43
+ const name = block.match(/<title>([\s\S]*?)<\/title>/)?.[1]?.trim() ?? '';
44
+ const author = block.match(/<name>([\s\S]*?)<\/name>/)?.[1]?.trim() ?? '';
45
+ const pubRaw = block.match(/<published>(.*?)<\/published>/)?.[1]?.trim() ?? '';
46
+ const date = pubRaw.slice(0, 10);
47
+ const link = block.match(/<link[^>]*href="([^"]+)"/)?.[1]?.trim() ?? '';
48
+ // Extract tagline from HTML content (first <p> text)
49
+ const contentRaw = block.match(/<content[^>]*>([\s\S]*?)<\/content>/)?.[1] ?? '';
50
+ const contentDecoded = contentRaw
51
+ .replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&').replace(/&quot;/g, '"');
52
+ const tagline = contentDecoded
53
+ .replace(/<[^>]+>/g, ' ')
54
+ .replace(/\s+/g, ' ')
55
+ .replace(/\s*Discussion\s*\|?\s*/gi, '')
56
+ .replace(/\s*\|?\s*Link\s*$/gi, '')
57
+ .trim()
58
+ .slice(0, 120);
59
+ if (name) {
60
+ posts.push({ rank: rank++, name, tagline, author, date, url: link });
61
+ }
62
+ }
63
+ return posts;
64
+ }
65
+ export function pickVoteCount(candidates) {
66
+ const scored = candidates
67
+ .map((candidate) => {
68
+ const text = String(candidate.text ?? '').trim();
69
+ if (!/^\d+$/.test(text))
70
+ return null;
71
+ if (candidate.inReviewLink)
72
+ return null;
73
+ const value = parseInt(text, 10);
74
+ if (!Number.isFinite(value) || value <= 0)
75
+ return null;
76
+ const signal = `${candidate.tagName ?? ''} ${candidate.className ?? ''} ${candidate.role ?? ''}`.toLowerCase();
77
+ let score = 0;
78
+ if (candidate.inButton)
79
+ score += 4;
80
+ if (signal.includes('vote') || signal.includes('upvote'))
81
+ score += 3;
82
+ if (signal.includes('button'))
83
+ score += 1;
84
+ return { text, score, value };
85
+ })
86
+ .filter((candidate) => Boolean(candidate))
87
+ .sort((a, b) => {
88
+ if (b.score !== a.score)
89
+ return b.score - a.score;
90
+ if (b.value !== a.value)
91
+ return b.value - a.value;
92
+ return a.text.localeCompare(b.text);
93
+ });
94
+ return scored[0]?.text ?? '';
95
+ }
96
+ /** Format ISO date string to YYYY-MM-DD */
97
+ export function toDate(iso) {
98
+ return iso.slice(0, 10);
99
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,64 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { parseFeed, pickVoteCount, PRODUCTHUNT_CATEGORY_SLUGS } from './utils.js';
3
+ const SAMPLE_ATOM = `<?xml version="1.0" encoding="UTF-8"?>
4
+ <feed xmlns="http://www.w3.org/2005/Atom">
5
+ <title>Product Hunt</title>
6
+ <entry>
7
+ <id>tag:www.producthunt.com,2005:Post/1001</id>
8
+ <published>2026-03-26T10:00:00-07:00</published>
9
+ <title>Awesome AI Tool</title>
10
+ <content type="html">&lt;p&gt;The best AI tool ever made&lt;/p&gt;&lt;p&gt;&lt;a href="..."&gt;Discussion&lt;/a&gt;&lt;/p&gt;</content>
11
+ <author><name>Jane Doe</name></author>
12
+ <link rel="alternate" type="text/html" href="https://www.producthunt.com/products/awesome-ai-tool"/>
13
+ </entry>
14
+ <entry>
15
+ <id>tag:www.producthunt.com,2005:Post/1002</id>
16
+ <published>2026-03-25T08:00:00-07:00</published>
17
+ <title>Dev Helper</title>
18
+ <content type="html">&lt;p&gt;Speeds up your workflow&lt;/p&gt;</content>
19
+ <author><name>John Smith</name></author>
20
+ <link rel="alternate" type="text/html" href="https://www.producthunt.com/products/dev-helper"/>
21
+ </entry>
22
+ </feed>`;
23
+ describe('parseFeed', () => {
24
+ it('parses entries into ranked posts', () => {
25
+ const posts = parseFeed(SAMPLE_ATOM);
26
+ expect(posts).toHaveLength(2);
27
+ expect(posts[0].rank).toBe(1);
28
+ expect(posts[0].name).toBe('Awesome AI Tool');
29
+ expect(posts[0].author).toBe('Jane Doe');
30
+ expect(posts[0].date).toBe('2026-03-26');
31
+ expect(posts[0].url).toBe('https://www.producthunt.com/products/awesome-ai-tool');
32
+ expect(posts[0].tagline).toContain('best AI tool');
33
+ });
34
+ it('strips HTML and Discussion link from tagline', () => {
35
+ const posts = parseFeed(SAMPLE_ATOM);
36
+ expect(posts[0].tagline).not.toContain('<p>');
37
+ expect(posts[0].tagline).not.toContain('Discussion');
38
+ });
39
+ it('returns empty array for empty feed', () => {
40
+ expect(parseFeed('<feed></feed>')).toHaveLength(0);
41
+ });
42
+ it('assigns sequential ranks', () => {
43
+ const posts = parseFeed(SAMPLE_ATOM);
44
+ expect(posts.map(p => p.rank)).toEqual([1, 2]);
45
+ });
46
+ it('prefers vote-like candidates over unrelated numeric badges', () => {
47
+ const votes = pickVoteCount([
48
+ { text: '12', className: 'font-semibold', inButton: false, inReviewLink: false },
49
+ { text: '98', className: 'vote-button', inButton: true, inReviewLink: false },
50
+ ]);
51
+ expect(votes).toBe('98');
52
+ });
53
+ it('ignores numbers inside review links', () => {
54
+ const votes = pickVoteCount([
55
+ { text: '120', className: 'text-secondary', inButton: false, inReviewLink: true },
56
+ { text: '45', className: 'vote-button', inButton: true, inReviewLink: false },
57
+ ]);
58
+ expect(votes).toBe('45');
59
+ });
60
+ it('shares category slugs across commands', () => {
61
+ expect(PRODUCTHUNT_CATEGORY_SLUGS).toContain('developer-tools');
62
+ expect(PRODUCTHUNT_CATEGORY_SLUGS).toContain('ai-agents');
63
+ });
64
+ });
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * Reuters news search — API with HTML fallback.
3
- * Source: bb-sites/reuters/search.js
4
3
  */
5
4
  import { cli, Strategy } from '../../registry.js';
6
5
  cli({
@@ -1,5 +1,7 @@
1
1
  import { AuthRequiredError, CommandExecutionError } from '../../errors.js';
2
2
  import { cli, Strategy } from '../../registry.js';
3
+ import { resolveTwitterQueryId } from './shared.js';
4
+ const TWEET_RESULT_BY_REST_ID_QUERY_ID = '7xflPyRiUxGVbJd4uWmbfg';
3
5
  cli({
4
6
  site: 'twitter',
5
7
  name: 'article',
@@ -20,6 +22,7 @@ cli({
20
22
  // Navigate to the tweet page for cookie context
21
23
  await page.goto(`https://x.com/i/status/${tweetId}`);
22
24
  await page.wait(3);
25
+ const queryId = await resolveTwitterQueryId(page, 'TweetResultByRestId', TWEET_RESULT_BY_REST_ID_QUERY_ID);
23
26
  const result = await page.evaluate(`
24
27
  async () => {
25
28
  const tweetId = "${tweetId}";
@@ -54,34 +57,7 @@ cli({
54
57
  withArticlePlainText: true,
55
58
  });
56
59
 
57
- // Dynamically resolve queryId: GitHub community source → JS bundle scan → hardcoded fallback
58
- async function resolveQueryId(operationName, fallbackId) {
59
- try {
60
- const ghResp = await fetch('https://raw.githubusercontent.com/fa0311/twitter-openapi/refs/heads/main/src/config/placeholder.json');
61
- if (ghResp.ok) {
62
- const data = await ghResp.json();
63
- const entry = data[operationName];
64
- if (entry && entry.queryId) return entry.queryId;
65
- }
66
- } catch {}
67
- try {
68
- const scripts = performance.getEntriesByType('resource')
69
- .filter(r => r.name.includes('client-web') && r.name.endsWith('.js'))
70
- .map(r => r.name);
71
- for (const scriptUrl of scripts.slice(0, 15)) {
72
- try {
73
- const text = await (await fetch(scriptUrl)).text();
74
- const re = new RegExp('queryId:"([A-Za-z0-9_-]+)"[^}]{0,200}operationName:"' + operationName + '"');
75
- const m = text.match(re);
76
- if (m) return m[1];
77
- } catch {}
78
- }
79
- } catch {}
80
- return fallbackId;
81
- }
82
-
83
- const queryId = await resolveQueryId('TweetResultByRestId', '7xflPyRiUxGVbJd4uWmbfg');
84
- const url = '/i/api/graphql/' + queryId + '/TweetResultByRestId?variables='
60
+ const url = '/i/api/graphql/' + ${JSON.stringify(queryId)} + '/TweetResultByRestId?variables='
85
61
  + encodeURIComponent(variables)
86
62
  + '&features=' + encodeURIComponent(features)
87
63
  + '&fieldToggles=' + encodeURIComponent(fieldToggles);
@@ -0,0 +1,24 @@
1
+ import { sanitizeQueryId } from './shared.js';
2
+ interface LikedTweet {
3
+ id: string;
4
+ author: string;
5
+ name: string;
6
+ text: string;
7
+ likes: number;
8
+ retweets: number;
9
+ created_at: string;
10
+ url: string;
11
+ }
12
+ declare function buildLikesUrl(queryId: string, userId: string, count: number, cursor?: string | null): string;
13
+ declare function buildUserByScreenNameUrl(queryId: string, screenName: string): string;
14
+ declare function parseLikes(data: any, seen: Set<string>): {
15
+ tweets: LikedTweet[];
16
+ nextCursor: string | null;
17
+ };
18
+ export declare const __test__: {
19
+ sanitizeQueryId: typeof sanitizeQueryId;
20
+ buildLikesUrl: typeof buildLikesUrl;
21
+ buildUserByScreenNameUrl: typeof buildUserByScreenNameUrl;
22
+ parseLikes: typeof parseLikes;
23
+ };
24
+ export {};
@@ -0,0 +1,217 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { AuthRequiredError, CommandExecutionError } from '../../errors.js';
3
+ import { resolveTwitterQueryId, sanitizeQueryId } from './shared.js';
4
+ const BEARER_TOKEN = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
5
+ const LIKES_QUERY_ID = 'RozQdCp4CilQzrcuU0NY5w';
6
+ const USER_BY_SCREEN_NAME_QUERY_ID = 'qRednkZG-rn1P6b48NINmQ';
7
+ const FEATURES = {
8
+ rweb_video_screen_enabled: false,
9
+ profile_label_improvements_pcf_label_in_post_enabled: true,
10
+ responsive_web_profile_redirect_enabled: false,
11
+ rweb_tipjar_consumption_enabled: false,
12
+ verified_phone_label_enabled: false,
13
+ creator_subscriptions_tweet_preview_api_enabled: true,
14
+ responsive_web_graphql_timeline_navigation_enabled: true,
15
+ responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
16
+ premium_content_api_read_enabled: false,
17
+ communities_web_enable_tweet_community_results_fetch: true,
18
+ c9s_tweet_anatomy_moderator_badge_enabled: true,
19
+ responsive_web_grok_analyze_button_fetch_trends_enabled: false,
20
+ responsive_web_grok_analyze_post_followups_enabled: true,
21
+ responsive_web_jetfuel_frame: true,
22
+ responsive_web_grok_share_attachment_enabled: true,
23
+ responsive_web_grok_annotations_enabled: true,
24
+ articles_preview_enabled: true,
25
+ responsive_web_edit_tweet_api_enabled: true,
26
+ graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
27
+ view_counts_everywhere_api_enabled: true,
28
+ longform_notetweets_consumption_enabled: true,
29
+ responsive_web_twitter_article_tweet_consumption_enabled: true,
30
+ tweet_awards_web_tipping_enabled: false,
31
+ content_disclosure_indicator_enabled: true,
32
+ content_disclosure_ai_generated_indicator_enabled: true,
33
+ responsive_web_grok_show_grok_translated_post: false,
34
+ responsive_web_grok_analysis_button_from_backend: true,
35
+ post_ctas_fetch_enabled: false,
36
+ freedom_of_speech_not_reach_fetch_enabled: true,
37
+ standardized_nudges_misinfo: true,
38
+ tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled: true,
39
+ longform_notetweets_rich_text_read_enabled: true,
40
+ longform_notetweets_inline_media_enabled: false,
41
+ responsive_web_grok_image_annotation_enabled: true,
42
+ responsive_web_grok_imagine_annotation_enabled: true,
43
+ responsive_web_grok_community_note_auto_translation_is_enabled: false,
44
+ responsive_web_enhance_cards_enabled: false
45
+ };
46
+ function buildLikesUrl(queryId, userId, count, cursor) {
47
+ const vars = {
48
+ userId,
49
+ count,
50
+ includePromotedContent: false,
51
+ withClientEventToken: false,
52
+ withBirdwatchNotes: false,
53
+ withVoice: true
54
+ };
55
+ if (cursor)
56
+ vars.cursor = cursor;
57
+ return `/i/api/graphql/${queryId}/Likes`
58
+ + `?variables=${encodeURIComponent(JSON.stringify(vars))}`
59
+ + `&features=${encodeURIComponent(JSON.stringify(FEATURES))}`;
60
+ }
61
+ function buildUserByScreenNameUrl(queryId, screenName) {
62
+ const vars = JSON.stringify({ screen_name: screenName, withSafetyModeUserFields: true });
63
+ const feats = JSON.stringify({
64
+ hidden_profile_subscriptions_enabled: true,
65
+ rweb_tipjar_consumption_enabled: true,
66
+ responsive_web_graphql_exclude_directive_enabled: true,
67
+ verified_phone_label_enabled: false,
68
+ subscriptions_verification_info_is_identity_verified_enabled: true,
69
+ subscriptions_verification_info_verified_since_enabled: true,
70
+ highlights_tweets_tab_ui_enabled: true,
71
+ responsive_web_twitter_article_notes_tab_enabled: true,
72
+ subscriptions_feature_can_gift_premium: true,
73
+ creator_subscriptions_tweet_preview_api_enabled: true,
74
+ responsive_web_graphql_skip_user_profile_image_extensions_enabled: false,
75
+ responsive_web_graphql_timeline_navigation_enabled: true,
76
+ });
77
+ return `/i/api/graphql/${queryId}/UserByScreenName`
78
+ + `?variables=${encodeURIComponent(vars)}`
79
+ + `&features=${encodeURIComponent(feats)}`;
80
+ }
81
+ function extractLikedTweet(result, seen) {
82
+ if (!result)
83
+ return null;
84
+ const tw = result.tweet || result;
85
+ const legacy = tw.legacy || {};
86
+ if (!tw.rest_id || seen.has(tw.rest_id))
87
+ return null;
88
+ seen.add(tw.rest_id);
89
+ const user = tw.core?.user_results?.result;
90
+ const screenName = user?.legacy?.screen_name || user?.core?.screen_name || 'unknown';
91
+ const displayName = user?.legacy?.name || user?.core?.name || '';
92
+ const noteText = tw.note_tweet?.note_tweet_results?.result?.text;
93
+ return {
94
+ id: tw.rest_id,
95
+ author: screenName,
96
+ name: displayName,
97
+ text: noteText || legacy.full_text || '',
98
+ likes: legacy.favorite_count || 0,
99
+ retweets: legacy.retweet_count || 0,
100
+ created_at: legacy.created_at || '',
101
+ url: `https://x.com/${screenName}/status/${tw.rest_id}`,
102
+ };
103
+ }
104
+ function parseLikes(data, seen) {
105
+ const tweets = [];
106
+ let nextCursor = null;
107
+ const instructions = data?.data?.user?.result?.timeline_v2?.timeline?.instructions
108
+ || data?.data?.user?.result?.timeline?.timeline?.instructions
109
+ || [];
110
+ for (const inst of instructions) {
111
+ for (const entry of inst.entries || []) {
112
+ const content = entry.content;
113
+ if (content?.entryType === 'TimelineTimelineCursor' || content?.__typename === 'TimelineTimelineCursor') {
114
+ if (content.cursorType === 'Bottom' || content.cursorType === 'ShowMore')
115
+ nextCursor = content.value;
116
+ continue;
117
+ }
118
+ if (entry.entryId?.startsWith('cursor-bottom-') || entry.entryId?.startsWith('cursor-showMore-')) {
119
+ nextCursor = content?.value || content?.itemContent?.value || nextCursor;
120
+ continue;
121
+ }
122
+ const direct = extractLikedTweet(content?.itemContent?.tweet_results?.result, seen);
123
+ if (direct) {
124
+ tweets.push(direct);
125
+ continue;
126
+ }
127
+ for (const item of content?.items || []) {
128
+ const nested = extractLikedTweet(item.item?.itemContent?.tweet_results?.result, seen);
129
+ if (nested)
130
+ tweets.push(nested);
131
+ }
132
+ }
133
+ }
134
+ return { tweets, nextCursor };
135
+ }
136
+ cli({
137
+ site: 'twitter',
138
+ name: 'likes',
139
+ description: 'Fetch liked tweets of a Twitter user',
140
+ domain: 'x.com',
141
+ strategy: Strategy.COOKIE,
142
+ browser: true,
143
+ args: [
144
+ { name: 'username', type: 'string', positional: true, help: 'Twitter screen name (without @). Defaults to logged-in user.' },
145
+ { name: 'limit', type: 'int', default: 20 },
146
+ ],
147
+ columns: ['author', 'name', 'text', 'likes', 'url'],
148
+ func: async (page, kwargs) => {
149
+ const limit = kwargs.limit || 20;
150
+ let username = (kwargs.username || '').replace(/^@/, '');
151
+ await page.goto('https://x.com');
152
+ await page.wait(3);
153
+ const ct0 = await page.evaluate(`() => {
154
+ return document.cookie.split(';').map(c => c.trim()).find(c => c.startsWith('ct0='))?.split('=')[1] || null;
155
+ }`);
156
+ if (!ct0)
157
+ throw new AuthRequiredError('x.com', 'Not logged into x.com (no ct0 cookie)');
158
+ // If no username provided, detect the logged-in user
159
+ if (!username) {
160
+ const href = await page.evaluate(`() => {
161
+ const link = document.querySelector('a[data-testid="AppTabBar_Profile_Link"]');
162
+ return link ? link.getAttribute('href') : null;
163
+ }`);
164
+ if (!href)
165
+ throw new AuthRequiredError('x.com', 'Could not detect logged-in user. Are you logged in?');
166
+ username = href.replace('/', '');
167
+ }
168
+ const likesQueryId = await resolveTwitterQueryId(page, 'Likes', LIKES_QUERY_ID);
169
+ const userByScreenNameQueryId = await resolveTwitterQueryId(page, 'UserByScreenName', USER_BY_SCREEN_NAME_QUERY_ID);
170
+ const headers = JSON.stringify({
171
+ 'Authorization': `Bearer ${decodeURIComponent(BEARER_TOKEN)}`,
172
+ 'X-Csrf-Token': ct0,
173
+ 'X-Twitter-Auth-Type': 'OAuth2Session',
174
+ 'X-Twitter-Active-User': 'yes',
175
+ });
176
+ // Get userId from screen_name
177
+ const userId = await page.evaluate(`async () => {
178
+ const screenName = ${JSON.stringify(username)};
179
+ const url = ${JSON.stringify(buildUserByScreenNameUrl(userByScreenNameQueryId, username))};
180
+ const resp = await fetch(url, { headers: ${headers}, credentials: 'include' });
181
+ if (!resp.ok) return null;
182
+ const d = await resp.json();
183
+ return d.data?.user?.result?.rest_id || null;
184
+ }`);
185
+ if (!userId) {
186
+ throw new CommandExecutionError(`Could not find user @${username}`);
187
+ }
188
+ const allTweets = [];
189
+ const seen = new Set();
190
+ let cursor = null;
191
+ for (let i = 0; i < 5 && allTweets.length < limit; i++) {
192
+ const fetchCount = Math.min(100, limit - allTweets.length + 10);
193
+ const apiUrl = buildLikesUrl(likesQueryId, userId, fetchCount, cursor);
194
+ const data = await page.evaluate(`async () => {
195
+ const r = await fetch("${apiUrl}", { headers: ${headers}, credentials: 'include' });
196
+ return r.ok ? await r.json() : { error: r.status };
197
+ }`);
198
+ if (data?.error) {
199
+ if (allTweets.length === 0)
200
+ throw new CommandExecutionError(`HTTP ${data.error}: Failed to fetch likes. queryId may have expired.`);
201
+ break;
202
+ }
203
+ const { tweets, nextCursor } = parseLikes(data, seen);
204
+ allTweets.push(...tweets);
205
+ if (!nextCursor || nextCursor === cursor)
206
+ break;
207
+ cursor = nextCursor;
208
+ }
209
+ return allTweets.slice(0, limit);
210
+ },
211
+ });
212
+ export const __test__ = {
213
+ sanitizeQueryId,
214
+ buildLikesUrl,
215
+ buildUserByScreenNameUrl,
216
+ parseLikes,
217
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,85 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { __test__ } from './likes.js';
3
+ describe('twitter likes helpers', () => {
4
+ it('falls back when queryId contains unsafe characters', () => {
5
+ expect(__test__.sanitizeQueryId('safe_Query-123', 'fallback')).toBe('safe_Query-123');
6
+ expect(__test__.sanitizeQueryId('bad"id', 'fallback')).toBe('fallback');
7
+ expect(__test__.sanitizeQueryId('bad/id', 'fallback')).toBe('fallback');
8
+ expect(__test__.sanitizeQueryId(null, 'fallback')).toBe('fallback');
9
+ });
10
+ it('builds likes url with the provided queryId', () => {
11
+ const url = __test__.buildLikesUrl('query123', '42', 20, 'cursor-1');
12
+ expect(url).toContain('/i/api/graphql/query123/Likes');
13
+ expect(decodeURIComponent(url)).toContain('"userId":"42"');
14
+ expect(decodeURIComponent(url)).toContain('"cursor":"cursor-1"');
15
+ });
16
+ it('parses likes timeline entries and bottom cursor', () => {
17
+ const payload = {
18
+ data: {
19
+ user: {
20
+ result: {
21
+ timeline_v2: {
22
+ timeline: {
23
+ instructions: [
24
+ {
25
+ entries: [
26
+ {
27
+ entryId: 'tweet-1',
28
+ content: {
29
+ itemContent: {
30
+ tweet_results: {
31
+ result: {
32
+ rest_id: '1',
33
+ legacy: {
34
+ full_text: 'liked post',
35
+ favorite_count: 7,
36
+ retweet_count: 2,
37
+ created_at: 'now',
38
+ },
39
+ core: {
40
+ user_results: {
41
+ result: {
42
+ legacy: {
43
+ screen_name: 'alice',
44
+ name: 'Alice',
45
+ },
46
+ },
47
+ },
48
+ },
49
+ },
50
+ },
51
+ },
52
+ },
53
+ },
54
+ {
55
+ entryId: 'cursor-bottom-1',
56
+ content: {
57
+ entryType: 'TimelineTimelineCursor',
58
+ cursorType: 'Bottom',
59
+ value: 'cursor-next',
60
+ },
61
+ },
62
+ ],
63
+ },
64
+ ],
65
+ },
66
+ },
67
+ },
68
+ },
69
+ },
70
+ };
71
+ const result = __test__.parseLikes(payload, new Set());
72
+ expect(result.nextCursor).toBe('cursor-next');
73
+ expect(result.tweets).toHaveLength(1);
74
+ expect(result.tweets[0]).toMatchObject({
75
+ id: '1',
76
+ author: 'alice',
77
+ name: 'Alice',
78
+ text: 'liked post',
79
+ likes: 7,
80
+ retweets: 2,
81
+ created_at: 'now',
82
+ url: 'https://x.com/alice/status/1',
83
+ });
84
+ });
85
+ });
@@ -1,5 +1,7 @@
1
1
  import { AuthRequiredError, CommandExecutionError } from '../../errors.js';
2
2
  import { cli, Strategy } from '../../registry.js';
3
+ import { resolveTwitterQueryId } from './shared.js';
4
+ const USER_BY_SCREEN_NAME_QUERY_ID = 'qRednkZG-rn1P6b48NINmQ';
3
5
  cli({
4
6
  site: 'twitter',
5
7
  name: 'profile',
@@ -28,6 +30,7 @@ cli({
28
30
  // Navigate directly to the user's profile page (gives us cookie context)
29
31
  await page.goto(`https://x.com/${username}`);
30
32
  await page.wait(3);
33
+ const queryId = await resolveTwitterQueryId(page, 'UserByScreenName', USER_BY_SCREEN_NAME_QUERY_ID);
31
34
  const result = await page.evaluate(`
32
35
  async () => {
33
36
  const screenName = "${username}";
@@ -61,34 +64,7 @@ cli({
61
64
  responsive_web_graphql_timeline_navigation_enabled: true,
62
65
  });
63
66
 
64
- // Dynamically resolve queryId: GitHub community source → JS bundle scan → hardcoded fallback
65
- async function resolveQueryId(operationName, fallbackId) {
66
- try {
67
- const ghResp = await fetch('https://raw.githubusercontent.com/fa0311/twitter-openapi/refs/heads/main/src/config/placeholder.json');
68
- if (ghResp.ok) {
69
- const data = await ghResp.json();
70
- const entry = data[operationName];
71
- if (entry && entry.queryId) return entry.queryId;
72
- }
73
- } catch {}
74
- try {
75
- const scripts = performance.getEntriesByType('resource')
76
- .filter(r => r.name.includes('client-web') && r.name.endsWith('.js'))
77
- .map(r => r.name);
78
- for (const scriptUrl of scripts.slice(0, 15)) {
79
- try {
80
- const text = await (await fetch(scriptUrl)).text();
81
- const re = new RegExp('queryId:"([A-Za-z0-9_-]+)"[^}]{0,200}operationName:"' + operationName + '"');
82
- const m = text.match(re);
83
- if (m) return m[1];
84
- } catch {}
85
- }
86
- } catch {}
87
- return fallbackId;
88
- }
89
-
90
- const queryId = await resolveQueryId('UserByScreenName', 'qRednkZG-rn1P6b48NINmQ');
91
- const url = '/i/api/graphql/' + queryId + '/UserByScreenName?variables='
67
+ const url = '/i/api/graphql/' + ${JSON.stringify(queryId)} + '/UserByScreenName?variables='
92
68
  + encodeURIComponent(variables)
93
69
  + '&features=' + encodeURIComponent(features);
94
70
 
@@ -7,8 +7,8 @@ import { cli, Strategy } from '../../registry.js';
7
7
  * pushState + popstate. A second attempt is enough for the intermittent cases
8
8
  * reported in issue #353 while keeping the flow narrowly scoped.
9
9
  */
10
- async function navigateToSearch(page, query) {
11
- const searchUrl = JSON.stringify(`/search?q=${encodeURIComponent(query)}&f=top`);
10
+ async function navigateToSearch(page, query, filter) {
11
+ const searchUrl = JSON.stringify(`/search?q=${encodeURIComponent(query)}&f=${filter}`);
12
12
  let lastPath = '';
13
13
  for (let attempt = 1; attempt <= 2; attempt++) {
14
14
  await page.evaluate(`
@@ -37,11 +37,13 @@ cli({
37
37
  browser: true,
38
38
  args: [
39
39
  { name: 'query', type: 'string', required: true, positional: true },
40
+ { name: 'filter', type: 'string', default: 'top', choices: ['top', 'live'] },
40
41
  { name: 'limit', type: 'int', default: 15 },
41
42
  ],
42
- columns: ['id', 'author', 'text', 'likes', 'views', 'url'],
43
+ columns: ['id', 'author', 'text', 'created_at', 'likes', 'views', 'url'],
43
44
  func: async (page, kwargs) => {
44
45
  const query = kwargs.query;
46
+ const filter = kwargs.filter === 'live' ? 'live' : 'top';
45
47
  // 1. Navigate to x.com/explore (has a search input at the top)
46
48
  await page.goto('https://x.com/explore');
47
49
  await page.wait(3);
@@ -54,7 +56,7 @@ cli({
54
56
  // a full page reload, so the interceptor stays alive.
55
57
  // Note: the previous approach (nativeSetter + Enter keydown on the
56
58
  // search input) does not reliably trigger Twitter's form submission.
57
- await navigateToSearch(page, query);
59
+ await navigateToSearch(page, query, filter);
58
60
  // 4. Scroll to trigger additional pagination
59
61
  await page.autoScroll({ times: 3, delayMs: 2000 });
60
62
  // 6. Retrieve captured data
@@ -89,6 +91,7 @@ cli({
89
91
  id: tweet.rest_id,
90
92
  author: tweetUser?.core?.screen_name || tweetUser?.legacy?.screen_name || 'unknown',
91
93
  text: tweet.note_tweet?.note_tweet_results?.result?.text || tweet.legacy?.full_text || '',
94
+ created_at: tweet.legacy?.created_at || '',
92
95
  likes: tweet.legacy?.favorite_count || 0,
93
96
  views: tweet.views?.count || '0',
94
97
  url: `https://x.com/i/status/${tweet.rest_id}`