@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,68 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import { CliError } from '../../errors.js';
6
+ import {
7
+ MAX_PDF_BYTES,
8
+ buildReviewUrl,
9
+ parseYesNo,
10
+ readPdfFile,
11
+ requestJson,
12
+ validateHelpfulness,
13
+ } from './utils.js';
14
+
15
+ describe('paperreview utils', () => {
16
+ beforeEach(() => {
17
+ vi.restoreAllMocks();
18
+ });
19
+
20
+ it('builds review URLs from the token', () => {
21
+ expect(buildReviewUrl('tok 123')).toBe('https://paperreview.ai/review?token=tok%20123');
22
+ });
23
+
24
+ it('parses yes/no flags', () => {
25
+ expect(parseYesNo('yes', 'critical-error')).toBe(true);
26
+ expect(parseYesNo('NO', 'critical-error')).toBe(false);
27
+ });
28
+
29
+ it('rejects invalid yes/no flags with CliError', () => {
30
+ expect(() => parseYesNo('maybe', 'critical-error')).toThrow(CliError);
31
+ expect(() => parseYesNo('maybe', 'critical-error')).toThrow('"critical-error" must be either "yes" or "no".');
32
+ });
33
+
34
+ it('validates helpfulness scores', () => {
35
+ expect(validateHelpfulness(5)).toBe(5);
36
+ expect(() => validateHelpfulness(0)).toThrow(CliError);
37
+ });
38
+
39
+ it('reads a valid PDF file and returns metadata', async () => {
40
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'paperreview-'));
41
+ const pdfPath = path.join(tempDir, 'sample.pdf');
42
+ const pdfBytes = Buffer.concat([Buffer.from('%PDF-1.4\n'), Buffer.alloc(256, 1)]);
43
+ await fs.writeFile(pdfPath, pdfBytes);
44
+
45
+ const result = await readPdfFile(pdfPath);
46
+
47
+ expect(result.fileName).toBe('sample.pdf');
48
+ expect(result.resolvedPath).toBe(pdfPath);
49
+ expect(result.sizeBytes).toBe(pdfBytes.length);
50
+ expect(result.buffer.equals(pdfBytes)).toBe(true);
51
+ });
52
+
53
+ it('rejects PDFs larger than the paperreview.ai size limit', async () => {
54
+ const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'paperreview-'));
55
+ const pdfPath = path.join(tempDir, 'large.pdf');
56
+ await fs.writeFile(pdfPath, Buffer.alloc(MAX_PDF_BYTES + 1, 1));
57
+
58
+ await expect(readPdfFile(pdfPath)).rejects.toThrow(CliError);
59
+ await expect(readPdfFile(pdfPath)).rejects.toThrow('The PDF is larger than paperreview.ai\'s 10MB limit.');
60
+ });
61
+
62
+ it('normalizes fetch failures into CliError', async () => {
63
+ vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('socket hang up')));
64
+
65
+ await expect(requestJson('/api/review/token')).rejects.toThrow(CliError);
66
+ await expect(requestJson('/api/review/token')).rejects.toThrow('Unable to reach paperreview.ai');
67
+ });
68
+ });
@@ -0,0 +1,276 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { CliError, getErrorMessage } from '../../errors.js';
4
+
5
+ export const PAPERREVIEW_DOMAIN = 'paperreview.ai';
6
+ export const PAPERREVIEW_BASE_URL = `https://${PAPERREVIEW_DOMAIN}`;
7
+ export const MAX_PDF_BYTES = 10 * 1024 * 1024;
8
+
9
+ export interface PaperreviewPdfFile {
10
+ buffer: Buffer;
11
+ fileName: string;
12
+ resolvedPath: string;
13
+ sizeBytes: number;
14
+ }
15
+
16
+ export interface PaperreviewRequestResult {
17
+ response: Response;
18
+ payload: any;
19
+ }
20
+
21
+ function asText(value: unknown): string {
22
+ return value == null ? '' : String(value);
23
+ }
24
+
25
+ function trimOrEmpty(value: unknown): string {
26
+ return asText(value).trim();
27
+ }
28
+
29
+ function toErrorMessage(payload: unknown, fallback: string): string {
30
+ if (payload && typeof payload === 'object') {
31
+ const detail = trimOrEmpty((payload as Record<string, unknown>).detail);
32
+ const message = trimOrEmpty((payload as Record<string, unknown>).message);
33
+ const error = trimOrEmpty((payload as Record<string, unknown>).error);
34
+ if (detail) return detail;
35
+ if (message) return message;
36
+ if (error) return error;
37
+ }
38
+ const text = trimOrEmpty(payload);
39
+ return text || fallback;
40
+ }
41
+
42
+ export function buildReviewUrl(token: string): string {
43
+ return `${PAPERREVIEW_BASE_URL}/review?token=${encodeURIComponent(token)}`;
44
+ }
45
+
46
+ export function parseYesNo(value: unknown, name: string): boolean {
47
+ const normalized = trimOrEmpty(value).toLowerCase();
48
+ if (normalized === 'yes') return true;
49
+ if (normalized === 'no') return false;
50
+ throw new CliError('ARGUMENT', `"${name}" must be either "yes" or "no".`);
51
+ }
52
+
53
+ export function normalizeVenue(value: unknown): string {
54
+ return trimOrEmpty(value);
55
+ }
56
+
57
+ export function validateHelpfulness(value: unknown): number {
58
+ const numeric = Number(value);
59
+ if (!Number.isInteger(numeric) || numeric < 1 || numeric > 5) {
60
+ throw new CliError('ARGUMENT', '"helpfulness" must be an integer from 1 to 5.');
61
+ }
62
+ return numeric;
63
+ }
64
+
65
+ export async function readPdfFile(inputPath: unknown): Promise<PaperreviewPdfFile> {
66
+ const rawPath = trimOrEmpty(inputPath);
67
+ if (!rawPath) {
68
+ throw new CliError('ARGUMENT', 'A PDF path is required.', 'Provide a local PDF file path');
69
+ }
70
+
71
+ const resolvedPath = path.resolve(rawPath);
72
+ const fileName = path.basename(resolvedPath);
73
+
74
+ if (!fileName.toLowerCase().endsWith('.pdf')) {
75
+ throw new CliError('ARGUMENT', 'The input file must end with .pdf.', 'Provide a PDF file path');
76
+ }
77
+
78
+ let fileStat;
79
+ try {
80
+ fileStat = await fs.stat(resolvedPath);
81
+ } catch (error: unknown) {
82
+ if ((error as NodeJS.ErrnoException | undefined)?.code === 'ENOENT') {
83
+ throw new CliError('FILE_NOT_FOUND', `File not found: ${resolvedPath}`, 'Provide a valid PDF file path');
84
+ }
85
+ throw new CliError('FILE_READ_ERROR', `Unable to inspect file: ${resolvedPath}`, 'Check file permissions and try again');
86
+ }
87
+
88
+ if (!fileStat.isFile()) {
89
+ throw new CliError('FILE_NOT_FOUND', `Not a file: ${resolvedPath}`, 'Provide a valid PDF file path');
90
+ }
91
+
92
+ if (fileStat.size < 100) {
93
+ throw new CliError(
94
+ 'ARGUMENT',
95
+ 'The PDF is too small. paperreview.ai requires at least 100 bytes.',
96
+ 'Provide the final paper PDF',
97
+ );
98
+ }
99
+
100
+ if (fileStat.size > MAX_PDF_BYTES) {
101
+ throw new CliError(
102
+ 'FILE_TOO_LARGE',
103
+ 'The PDF is larger than paperreview.ai\'s 10MB limit.',
104
+ 'Compress the PDF or submit a smaller file',
105
+ );
106
+ }
107
+
108
+ let buffer: Buffer;
109
+ try {
110
+ buffer = await fs.readFile(resolvedPath);
111
+ } catch {
112
+ throw new CliError('FILE_READ_ERROR', `Unable to read file: ${resolvedPath}`, 'Check file permissions and try again');
113
+ }
114
+
115
+ return {
116
+ buffer,
117
+ fileName,
118
+ resolvedPath,
119
+ sizeBytes: buffer.byteLength,
120
+ };
121
+ }
122
+
123
+ export async function requestJson(pathname: string, init: RequestInit = {}): Promise<PaperreviewRequestResult> {
124
+ let response: Response;
125
+ try {
126
+ response = await fetch(`${PAPERREVIEW_BASE_URL}${pathname}`, init);
127
+ } catch (error: unknown) {
128
+ throw new CliError(
129
+ 'FETCH_ERROR',
130
+ `Unable to reach paperreview.ai: ${getErrorMessage(error)}`,
131
+ 'Check your network connection and try again',
132
+ );
133
+ }
134
+
135
+ const rawText = await response.text();
136
+
137
+ let payload: any = rawText;
138
+ if (rawText) {
139
+ try {
140
+ payload = JSON.parse(rawText);
141
+ } catch {
142
+ payload = rawText;
143
+ }
144
+ }
145
+
146
+ return { response, payload };
147
+ }
148
+
149
+ export function ensureSuccess(response: Response, payload: unknown, fallback: string, hint?: string): void {
150
+ if (!response.ok) {
151
+ const code = response.status === 404 ? 'NOT_FOUND' : 'API_ERROR';
152
+ throw new CliError(code, toErrorMessage(payload, fallback), hint);
153
+ }
154
+ }
155
+
156
+ export function ensureApiSuccess(payload: unknown, fallback: string, hint?: string): void {
157
+ if (!payload || typeof payload !== 'object' || (payload as Record<string, unknown>).success !== true) {
158
+ throw new CliError('API_ERROR', toErrorMessage(payload, fallback), hint);
159
+ }
160
+ }
161
+
162
+ export function createUploadForm(
163
+ urlData: { presigned_fields?: Record<string, string> },
164
+ pdfFile: PaperreviewPdfFile,
165
+ ): FormData {
166
+ const form = new FormData();
167
+ for (const [key, value] of Object.entries(urlData.presigned_fields ?? {})) {
168
+ form.append(key, value);
169
+ }
170
+ form.append(
171
+ 'file',
172
+ new Blob([new Uint8Array(pdfFile.buffer)], { type: 'application/pdf' }),
173
+ pdfFile.fileName,
174
+ );
175
+ return form;
176
+ }
177
+
178
+ export async function uploadPresignedPdf(
179
+ presignedUrl: string,
180
+ pdfFile: PaperreviewPdfFile,
181
+ urlData: { presigned_fields?: Record<string, string> },
182
+ ): Promise<void> {
183
+ let response: Response;
184
+ try {
185
+ response = await fetch(presignedUrl, {
186
+ method: 'POST',
187
+ body: createUploadForm(urlData, pdfFile),
188
+ });
189
+ } catch (error: unknown) {
190
+ throw new CliError(
191
+ 'UPLOAD_ERROR',
192
+ `S3 upload failed: ${getErrorMessage(error)}`,
193
+ 'Try again in a moment',
194
+ );
195
+ }
196
+
197
+ if (!response.ok) {
198
+ const body = await response.text();
199
+ throw new CliError(
200
+ 'UPLOAD_ERROR',
201
+ body || `S3 upload failed with status ${response.status}.`,
202
+ 'Try again in a moment',
203
+ );
204
+ }
205
+ }
206
+
207
+ export function summarizeSubmission(options: {
208
+ pdfFile: PaperreviewPdfFile;
209
+ email: string;
210
+ venue: string;
211
+ token?: string;
212
+ message?: string;
213
+ s3Key?: string;
214
+ dryRun?: boolean;
215
+ status?: string;
216
+ }): Record<string, unknown> {
217
+ const { pdfFile, email, venue, token, message, s3Key, dryRun = false, status } = options;
218
+ return {
219
+ status: status ?? (dryRun ? 'dry-run' : 'submitted'),
220
+ file: pdfFile.fileName,
221
+ file_path: pdfFile.resolvedPath,
222
+ size_bytes: pdfFile.sizeBytes,
223
+ email,
224
+ venue,
225
+ token: token ?? '',
226
+ review_url: token ? buildReviewUrl(token) : '',
227
+ message: message ?? '',
228
+ s3_key: s3Key ?? '',
229
+ };
230
+ }
231
+
232
+ export function summarizeReview(token: string, payload: any, status = 'ready'): Record<string, unknown> {
233
+ const sections = payload?.sections ?? {};
234
+ const availableSections = Object.keys(sections);
235
+
236
+ return {
237
+ status,
238
+ token,
239
+ review_url: buildReviewUrl(token),
240
+ title: trimOrEmpty(payload?.title),
241
+ venue: trimOrEmpty(payload?.venue),
242
+ submission_date: trimOrEmpty(payload?.submission_date),
243
+ numerical_score: payload?.numerical_score ?? '',
244
+ has_feedback: payload?.has_feedback ?? '',
245
+ available_sections: availableSections.join(', '),
246
+ section_count: availableSections.length,
247
+ summary: trimOrEmpty(sections.summary),
248
+ strengths: trimOrEmpty(sections.strengths),
249
+ weaknesses: trimOrEmpty(sections.weaknesses),
250
+ detailed_comments: trimOrEmpty(sections.detailed_comments),
251
+ questions: trimOrEmpty(sections.questions),
252
+ assessment: trimOrEmpty(sections.assessment),
253
+ content: trimOrEmpty(payload?.content),
254
+ sections,
255
+ };
256
+ }
257
+
258
+ export function summarizeFeedback(options: {
259
+ token: string;
260
+ helpfulness: number;
261
+ criticalError: boolean;
262
+ actionableSuggestions: boolean;
263
+ comments: string;
264
+ payload: any;
265
+ }): Record<string, unknown> {
266
+ const { token, helpfulness, criticalError, actionableSuggestions, comments, payload } = options;
267
+ return {
268
+ status: 'submitted',
269
+ token,
270
+ helpfulness,
271
+ critical_error: criticalError,
272
+ actionable_suggestions: actionableSuggestions,
273
+ additional_comments: comments,
274
+ message: trimOrEmpty(payload?.message) || 'Feedback submitted.',
275
+ };
276
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Product Hunt category browse — INTERCEPT strategy.
3
+ *
4
+ * Navigates to a Product Hunt category page and scrapes the top-rated products.
5
+ * Shows all-time best products for a category (ranked by review score, not daily votes).
6
+ */
7
+ import { cli, Strategy } from '../../registry.js';
8
+ import { CliError } from '../../errors.js';
9
+ import type { IPage } from '../../types.js';
10
+ import { PRODUCTHUNT_CATEGORY_SLUGS } from './utils.js';
11
+
12
+ cli({
13
+ site: 'producthunt',
14
+ name: 'browse',
15
+ description: 'Best products in a Product Hunt category',
16
+ domain: 'www.producthunt.com',
17
+ strategy: Strategy.INTERCEPT,
18
+ args: [
19
+ {
20
+ name: 'category',
21
+ type: 'string',
22
+ positional: true,
23
+ required: true,
24
+ help: `Category slug, e.g. vibe-coding, ai-agents, developer-tools`,
25
+ },
26
+ { name: 'limit', type: 'int', default: 20, help: 'Number of results (max 50)' },
27
+ ],
28
+ columns: ['rank', 'name', 'tagline', 'reviews', 'url'],
29
+ func: async (page: IPage, args) => {
30
+ const count = Math.min(Number(args.limit) || 20, 50);
31
+ const slug = String(args.category || '').trim().toLowerCase();
32
+
33
+ await page.installInterceptor('producthunt.com');
34
+ await page.goto(`https://www.producthunt.com/categories/${slug}`);
35
+ await page.wait(5);
36
+
37
+ const domItems: any = await page.evaluate(`
38
+ (() => {
39
+ const seen = new Set();
40
+ const results = [];
41
+
42
+ // Card links: <a class="...flex-col" href="/products/<slug>"> (not review links)
43
+ const cardLinks = Array.from(document.querySelectorAll('a[href^="/products/"]')).filter(a => {
44
+ const href = a.getAttribute('href') || '';
45
+ const cls = a.className || '';
46
+ return cls.includes('flex-col') && !href.includes('/reviews');
47
+ });
48
+
49
+ for (const cardLink of cardLinks) {
50
+ const href = cardLink.getAttribute('href');
51
+ if (!href || seen.has(href)) continue;
52
+
53
+ // Child 0: div with name (strip "Launched this month/week/year" noise)
54
+ const nameDiv = cardLink.querySelector('div');
55
+ const rawName = nameDiv?.textContent?.trim() || '';
56
+ const name = rawName
57
+ .replace(/\\s*Launched\\s+this\\s+(month|week|year|day)\\s*/gi, '')
58
+ .replace(/\\s*Featured\\s*/gi, '')
59
+ .trim();
60
+
61
+ // Child 1: span.text-secondary — tagline
62
+ const taglineEl = cardLink.querySelector('span.text-secondary, span[class*="text-secondary"]');
63
+ const tagline = taglineEl?.textContent?.trim() || '';
64
+
65
+ if (!name) continue;
66
+
67
+ // Find reviews count from sibling /reviews link
68
+ let reviews = '';
69
+ let container = cardLink.parentElement;
70
+ for (let i = 0; i < 5 && container; i++) {
71
+ const reviewLink = container.querySelector('a[href="' + href + '/reviews"]');
72
+ if (reviewLink) {
73
+ reviews = (reviewLink.textContent?.trim() || '').replace(/\\s*reviews?\\s*/i, '').trim();
74
+ break;
75
+ }
76
+ container = container.parentElement;
77
+ }
78
+
79
+ seen.add(href);
80
+ results.push({
81
+ name,
82
+ tagline: tagline.slice(0, 120),
83
+ reviews: reviews || '0',
84
+ url: 'https://www.producthunt.com' + href,
85
+ });
86
+ }
87
+
88
+ return results;
89
+ })()
90
+ `);
91
+
92
+ const items = Array.isArray(domItems) ? (domItems as any[]) : [];
93
+ if (items.length === 0) {
94
+ throw new CliError(
95
+ 'NO_DATA',
96
+ `No products found for category "${slug}"`,
97
+ 'Check the category slug or try: ' + PRODUCTHUNT_CATEGORY_SLUGS.slice(0, 5).join(', '),
98
+ );
99
+ }
100
+
101
+ return items.slice(0, count).map((item: any, i: number) => ({
102
+ rank: i + 1,
103
+ name: item.name,
104
+ tagline: item.tagline,
105
+ reviews: item.reviews,
106
+ url: item.url,
107
+ }));
108
+ },
109
+ });
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Product Hunt top posts with vote counts — INTERCEPT strategy.
3
+ *
4
+ * Navigates to the Product Hunt homepage and scrapes rendered product cards.
5
+ */
6
+ import { cli, Strategy } from '../../registry.js';
7
+ import { CliError } from '../../errors.js';
8
+ import type { IPage } from '../../types.js';
9
+ import { pickVoteCount } from './utils.js';
10
+
11
+ cli({
12
+ site: 'producthunt',
13
+ name: 'hot',
14
+ description: "Today's top Product Hunt launches with vote counts",
15
+ domain: 'www.producthunt.com',
16
+ strategy: Strategy.INTERCEPT,
17
+ args: [
18
+ { name: 'limit', type: 'int', default: 20, help: 'Number of results (max 50)' },
19
+ ],
20
+ columns: ['rank', 'name', 'votes', 'url'],
21
+ func: async (page: IPage, args) => {
22
+ const count = Math.min(Number(args.limit) || 20, 50);
23
+
24
+ await page.installInterceptor('producthunt.com');
25
+ await page.goto('https://www.producthunt.com');
26
+ await page.wait(5);
27
+
28
+ const domItems: any = await page.evaluate(`
29
+ (() => {
30
+ const seen = new Set();
31
+ const results = [];
32
+
33
+ const cardLinks = Array.from(document.querySelectorAll('a[href^="/products/"]')).filter((el) => {
34
+ const href = el.getAttribute('href') || '';
35
+ const text = el.textContent?.trim() || '';
36
+ return href && !href.includes('/reviews') && text.length > 0 && text.length < 120;
37
+ });
38
+
39
+ const normalizeName = (text) => text
40
+ .replace(/^\\d+\\.\\s*/, '')
41
+ .replace(/\\s*Launched\\s+this\\s+(month|week|year|day)\\s*/gi, '')
42
+ .replace(/\\s*Featured\\s*/gi, '')
43
+ .trim();
44
+
45
+ for (const cardLink of cardLinks) {
46
+ const href = cardLink.getAttribute('href') || '';
47
+ if (!href || seen.has(href)) continue;
48
+
49
+ let card = cardLink;
50
+ let node = cardLink.parentElement;
51
+ for (let i = 0; i < 6 && node; i++) {
52
+ const hasReviewLink = !!node.querySelector('a[href="' + href + '/reviews"]');
53
+ const hasNumericNode = Array.from(node.querySelectorAll('button, [role="button"], p, span, div'))
54
+ .some((el) => /^\\d+$/.test(el.textContent?.trim() || ''));
55
+ if (hasReviewLink || hasNumericNode) {
56
+ card = node;
57
+ break;
58
+ }
59
+ node = node.parentElement;
60
+ }
61
+
62
+ const name = normalizeName(cardLink.textContent?.trim() || '');
63
+ if (!name) continue;
64
+
65
+ const voteCandidates = Array.from(card.querySelectorAll('button, [role="button"], a, p, span, div'))
66
+ .map((el) => {
67
+ const reviewLink = el.closest('a[href="' + href + '/reviews"]');
68
+ return {
69
+ text: el.textContent?.trim() || '',
70
+ tagName: el.tagName,
71
+ className: el.className || '',
72
+ role: el.getAttribute('role') || '',
73
+ inButton: !!el.closest('button, [role="button"]'),
74
+ inReviewLink: !!reviewLink,
75
+ };
76
+ })
77
+ .filter((candidate) => /^\\d+$/.test(candidate.text));
78
+
79
+ if (voteCandidates.length === 0) continue;
80
+
81
+ seen.add(href);
82
+ results.push({
83
+ name,
84
+ voteCandidates,
85
+ url: 'https://www.producthunt.com' + href,
86
+ });
87
+ }
88
+
89
+ return results;
90
+ })()
91
+ `);
92
+
93
+ const items = Array.isArray(domItems) ? (domItems as any[]) : [];
94
+ if (items.length === 0) {
95
+ throw new CliError(
96
+ 'NO_DATA',
97
+ 'Could not retrieve Product Hunt top posts',
98
+ 'Product Hunt may have changed its layout',
99
+ );
100
+ }
101
+
102
+ const rankedItems = items
103
+ .map((item: any) => ({
104
+ name: item.name,
105
+ url: item.url,
106
+ votes: pickVoteCount(Array.isArray(item.voteCandidates) ? item.voteCandidates : []),
107
+ }))
108
+ .filter((item) => item.name && item.url && item.votes);
109
+
110
+ if (rankedItems.length === 0) {
111
+ throw new CliError(
112
+ 'NO_DATA',
113
+ 'Could not retrieve Product Hunt vote counts',
114
+ 'Product Hunt may have changed its vote button structure',
115
+ );
116
+ }
117
+
118
+ rankedItems.sort((a, b) => parseInt(b.votes, 10) - parseInt(a.votes, 10));
119
+
120
+ return rankedItems.slice(0, count).map((item, i: number) => ({
121
+ rank: i + 1,
122
+ name: item.name,
123
+ votes: item.votes,
124
+ url: item.url,
125
+ }));
126
+ },
127
+ });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Product Hunt latest posts — public Atom feed, no browser needed.
3
+ */
4
+ import { cli, Strategy } from '../../registry.js';
5
+ import { fetchFeed, PRODUCTHUNT_CATEGORY_SLUGS } from './utils.js';
6
+
7
+ cli({
8
+ site: 'producthunt',
9
+ name: 'posts',
10
+ description: 'Latest Product Hunt launches (optional category filter)',
11
+ domain: 'www.producthunt.com',
12
+ strategy: Strategy.PUBLIC,
13
+ args: [
14
+ { name: 'limit', type: 'int', default: 20, help: 'Number of results (max 50)' },
15
+ {
16
+ name: 'category',
17
+ type: 'string',
18
+ default: '',
19
+ help: `Category filter: ${PRODUCTHUNT_CATEGORY_SLUGS.join(', ')}`,
20
+ },
21
+ ],
22
+ columns: ['rank', 'name', 'tagline', 'author', 'date', 'url'],
23
+ func: async (_page, args) => {
24
+ const count = Math.min(Number(args.limit) || 20, 50);
25
+ const category = String(args.category ?? '').trim() || undefined;
26
+ const posts = await fetchFeed(category);
27
+ return posts.slice(0, count);
28
+ },
29
+ });
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Product Hunt today's top launches — filtered from public Atom feed.
3
+ *
4
+ * Shows the most recently published day's products (Product Hunt runs on
5
+ * Pacific Time; the feed date may differ from UTC local date by up to 1 day).
6
+ */
7
+ import { cli, Strategy } from '../../registry.js';
8
+ import { fetchFeed } from './utils.js';
9
+
10
+ cli({
11
+ site: 'producthunt',
12
+ name: 'today',
13
+ description: "Today's Product Hunt launches (most recent day in feed)",
14
+ domain: 'www.producthunt.com',
15
+ strategy: Strategy.PUBLIC,
16
+ args: [
17
+ { name: 'limit', type: 'int', default: 20, help: 'Max results' },
18
+ ],
19
+ columns: ['rank', 'name', 'tagline', 'author', 'url'],
20
+ func: async (_page, args) => {
21
+ const count = Math.min(Number(args.limit) || 20, 50);
22
+ const posts = await fetchFeed();
23
+ if (posts.length === 0) return [];
24
+
25
+ // Use the latest date in the feed (Product Hunt is PST-based)
26
+ const latestDate = posts.map(p => p.date).sort().reverse()[0];
27
+ const todayPosts = posts.filter(p => p.date === latestDate);
28
+
29
+ return todayPosts.slice(0, count).map((p, i) => ({
30
+ rank: i + 1,
31
+ name: p.name,
32
+ tagline: p.tagline,
33
+ author: p.author,
34
+ url: p.url,
35
+ }));
36
+ },
37
+ });