@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,137 @@
1
+ import * as fs from 'node:fs';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+
5
+ import { describe, expect, it, vi } from 'vitest';
6
+
7
+ import { getRegistry } from '../../registry.js';
8
+ import type { IPage } from '../../types.js';
9
+ import './publish.js';
10
+
11
+ function createPageMock(evaluateResults: any[]): IPage {
12
+ const evaluate = vi.fn();
13
+ for (const result of evaluateResults) {
14
+ evaluate.mockResolvedValueOnce(result);
15
+ }
16
+
17
+ return {
18
+ goto: vi.fn().mockResolvedValue(undefined),
19
+ evaluate,
20
+ snapshot: vi.fn().mockResolvedValue(undefined),
21
+ click: vi.fn().mockResolvedValue(undefined),
22
+ typeText: vi.fn().mockResolvedValue(undefined),
23
+ pressKey: vi.fn().mockResolvedValue(undefined),
24
+ scrollTo: vi.fn().mockResolvedValue(undefined),
25
+ getFormState: vi.fn().mockResolvedValue({ forms: [], orphanFields: [] }),
26
+ wait: vi.fn().mockResolvedValue(undefined),
27
+ tabs: vi.fn().mockResolvedValue([]),
28
+ closeTab: vi.fn().mockResolvedValue(undefined),
29
+ newTab: vi.fn().mockResolvedValue(undefined),
30
+ selectTab: vi.fn().mockResolvedValue(undefined),
31
+ networkRequests: vi.fn().mockResolvedValue([]),
32
+ consoleMessages: vi.fn().mockResolvedValue([]),
33
+ scroll: vi.fn().mockResolvedValue(undefined),
34
+ autoScroll: vi.fn().mockResolvedValue(undefined),
35
+ installInterceptor: vi.fn().mockResolvedValue(undefined),
36
+ getInterceptedRequests: vi.fn().mockResolvedValue([]),
37
+ getCookies: vi.fn().mockResolvedValue([]),
38
+ screenshot: vi.fn().mockResolvedValue(''),
39
+ };
40
+ }
41
+
42
+ describe('xiaohongshu publish', () => {
43
+ it('selects the image-text tab and publishes successfully', async () => {
44
+ const cmd = getRegistry().get('xiaohongshu/publish');
45
+ expect(cmd?.func).toBeTypeOf('function');
46
+
47
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-xhs-publish-'));
48
+ const imagePath = path.join(tempDir, 'demo.jpg');
49
+ fs.writeFileSync(imagePath, Buffer.from([0xff, 0xd8, 0xff, 0xd9]));
50
+
51
+ const page = createPageMock([
52
+ 'https://creator.xiaohongshu.com/publish/publish?from=menu_left',
53
+ { ok: true, target: '上传图文', text: '上传图文' },
54
+ { hasTitleInput: true, hasImageInput: true, hasVideoSurface: false },
55
+ { ok: true, count: 1 },
56
+ false,
57
+ { ok: true, sel: 'input[maxlength="20"]' },
58
+ { ok: true, sel: '[contenteditable="true"][class*="content"]' },
59
+ true,
60
+ 'https://creator.xiaohongshu.com/publish/success',
61
+ '发布成功',
62
+ ]);
63
+
64
+ const result = await cmd!.func!(page, {
65
+ title: 'DeepSeek别乱问',
66
+ content: '一篇真实一点的小红书正文',
67
+ images: imagePath,
68
+ topics: '',
69
+ draft: false,
70
+ });
71
+
72
+ const evaluateCalls = (page.evaluate as any).mock.calls.map((args: any[]) => String(args[0]));
73
+ expect(evaluateCalls.some((code: string) => code.includes("const targets = ['上传图文', '图文', '图片']"))).toBe(true);
74
+ expect(evaluateCalls.some((code: string) => code.includes("No image file input found on page"))).toBe(true);
75
+ expect(result).toEqual([
76
+ {
77
+ status: '✅ 发布成功',
78
+ detail: '"DeepSeek别乱问" · 1张图片 · 发布成功',
79
+ },
80
+ ]);
81
+ });
82
+
83
+ it('fails early with a clear error when still on the video page', async () => {
84
+ const cmd = getRegistry().get('xiaohongshu/publish');
85
+ expect(cmd?.func).toBeTypeOf('function');
86
+
87
+ const page = createPageMock([
88
+ 'https://creator.xiaohongshu.com/publish/publish?from=menu_left',
89
+ { ok: false, visibleTexts: ['上传视频', '上传图文'] },
90
+ { hasTitleInput: false, hasImageInput: false, hasVideoSurface: true },
91
+ { hasTitleInput: false, hasImageInput: false, hasVideoSurface: true },
92
+ { hasTitleInput: false, hasImageInput: false, hasVideoSurface: true },
93
+ { hasTitleInput: false, hasImageInput: false, hasVideoSurface: true },
94
+ ]);
95
+
96
+ await expect(cmd!.func!(page, {
97
+ title: 'DeepSeek别乱问',
98
+ content: '一篇真实一点的小红书正文',
99
+ topics: '',
100
+ draft: false,
101
+ })).rejects.toThrow('Still on the video publish page after trying to select 图文');
102
+
103
+ expect(page.screenshot).toHaveBeenCalledWith({ path: '/tmp/xhs_publish_tab_debug.png' });
104
+ });
105
+
106
+ it('waits for the image-text surface to appear after clicking the tab', async () => {
107
+ const cmd = getRegistry().get('xiaohongshu/publish');
108
+ expect(cmd?.func).toBeTypeOf('function');
109
+
110
+ const page = createPageMock([
111
+ 'https://creator.xiaohongshu.com/publish/publish?from=menu_left',
112
+ { ok: true, target: '上传图文', text: '上传图文' },
113
+ { hasTitleInput: false, hasImageInput: false, hasVideoSurface: true },
114
+ { hasTitleInput: true, hasImageInput: true, hasVideoSurface: false },
115
+ { ok: true, sel: 'input[maxlength="20"]' },
116
+ { ok: true, sel: '[contenteditable="true"][class*="content"]' },
117
+ true,
118
+ 'https://creator.xiaohongshu.com/publish/success',
119
+ '发布成功',
120
+ ]);
121
+
122
+ const result = await cmd!.func!(page, {
123
+ title: '延迟切换也能过',
124
+ content: '图文页切换慢一点也继续等',
125
+ topics: '',
126
+ draft: false,
127
+ });
128
+
129
+ expect((page.wait as any).mock.calls).toContainEqual([{ time: 0.5 }]);
130
+ expect(result).toEqual([
131
+ {
132
+ status: '✅ 发布成功',
133
+ detail: '"延迟切换也能过" · 无图 · 发布成功',
134
+ },
135
+ ]);
136
+ });
137
+ });
@@ -63,14 +63,22 @@ async function injectImages(page: IPage, images: ImagePayload[]): Promise<{ ok:
63
63
  (async () => {
64
64
  const images = ${payload};
65
65
 
66
- // Prefer image/* file inputs; fall back to the first available input.
66
+ // Only use image-capable file inputs. Do not fall back to a generic uploader,
67
+ // otherwise we can accidentally feed images into the video upload flow.
67
68
  const inputs = Array.from(document.querySelectorAll('input[type="file"]'));
68
69
  const input = inputs.find(el => {
69
70
  const accept = el.getAttribute('accept') || '';
70
- return accept.includes('image') || accept.includes('.jpg') || accept.includes('.png');
71
- }) || inputs[0];
71
+ return (
72
+ accept.includes('image') ||
73
+ accept.includes('.jpg') ||
74
+ accept.includes('.jpeg') ||
75
+ accept.includes('.png') ||
76
+ accept.includes('.gif') ||
77
+ accept.includes('.webp')
78
+ );
79
+ });
72
80
 
73
- if (!input) return { ok: false, count: 0, error: 'No file input found on page' };
81
+ if (!input) return { ok: false, count: 0, error: 'No image file input found on page' };
74
82
 
75
83
  const dt = new DataTransfer();
76
84
  for (const img of images) {
@@ -151,6 +159,111 @@ async function fillField(page: IPage, selectors: string[], text: string, fieldNa
151
159
  }
152
160
  }
153
161
 
162
+ async function selectImageTextTab(
163
+ page: IPage,
164
+ ): Promise<{ ok: boolean; target?: string; text?: string; visibleTexts?: string[] }> {
165
+ const result = await page.evaluate(`
166
+ () => {
167
+ const isVisible = (el) => {
168
+ if (!el || el.offsetParent === null) return false;
169
+ const rect = el.getBoundingClientRect();
170
+ return rect.width > 0 && rect.height > 0;
171
+ };
172
+
173
+ const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
174
+ const selector = 'button, [role="tab"], [role="button"], a, label, div, span, li';
175
+ const nodes = Array.from(document.querySelectorAll(selector));
176
+ const targets = ['上传图文', '图文', '图片'];
177
+
178
+ for (const target of targets) {
179
+ for (const node of nodes) {
180
+ if (!isVisible(node)) continue;
181
+ const text = normalize(node.innerText || node.textContent || '');
182
+ if (!text || text.includes('视频')) continue;
183
+ if (text === target || text.startsWith(target) || text.includes(target)) {
184
+ const clickable = node.closest('button, [role="tab"], [role="button"], a, label') || node;
185
+ clickable.click();
186
+ return { ok: true, target, text };
187
+ }
188
+ }
189
+ }
190
+
191
+ const visibleTexts = [];
192
+ for (const node of nodes) {
193
+ if (!isVisible(node)) continue;
194
+ const text = normalize(node.innerText || node.textContent || '');
195
+ if (!text || text.length > 20) continue;
196
+ visibleTexts.push(text);
197
+ if (visibleTexts.length >= 20) break;
198
+ }
199
+ return { ok: false, visibleTexts };
200
+ }
201
+ `);
202
+ if (result?.ok) {
203
+ await page.wait({ time: 1 });
204
+ }
205
+ return result;
206
+ }
207
+
208
+ async function inspectPublishSurface(
209
+ page: IPage,
210
+ ): Promise<{ hasTitleInput: boolean; hasImageInput: boolean; hasVideoSurface: boolean }> {
211
+ return page.evaluate(`
212
+ () => {
213
+ const text = (document.body?.innerText || '').replace(/\\s+/g, ' ').trim();
214
+ const hasTitleInput = !!Array.from(document.querySelectorAll('input, textarea')).find((el) => {
215
+ if (!el || el.offsetParent === null) return false;
216
+ const placeholder = (el.getAttribute('placeholder') || '').trim();
217
+ const cls = el.className ? String(el.className) : '';
218
+ const maxLength = Number(el.getAttribute('maxlength') || 0);
219
+ return (
220
+ placeholder.includes('标题') ||
221
+ /title/i.test(placeholder) ||
222
+ /title/i.test(cls) ||
223
+ maxLength === 20
224
+ );
225
+ });
226
+ const hasImageInput = !!Array.from(document.querySelectorAll('input[type="file"]')).find((el) => {
227
+ const accept = el.getAttribute('accept') || '';
228
+ return (
229
+ accept.includes('image') ||
230
+ accept.includes('.jpg') ||
231
+ accept.includes('.jpeg') ||
232
+ accept.includes('.png') ||
233
+ accept.includes('.gif') ||
234
+ accept.includes('.webp')
235
+ );
236
+ });
237
+ return {
238
+ hasTitleInput,
239
+ hasImageInput,
240
+ hasVideoSurface: text.includes('拖拽视频到此处点击上传') || text.includes('上传视频'),
241
+ };
242
+ }
243
+ `);
244
+ }
245
+
246
+ async function waitForImageTextSurface(
247
+ page: IPage,
248
+ maxWaitMs = 5_000,
249
+ ): Promise<{ hasTitleInput: boolean; hasImageInput: boolean; hasVideoSurface: boolean }> {
250
+ const pollMs = 500;
251
+ const maxAttempts = Math.max(1, Math.ceil(maxWaitMs / pollMs));
252
+ let surface = await inspectPublishSurface(page);
253
+
254
+ for (let i = 0; i < maxAttempts; i++) {
255
+ if (surface.hasTitleInput || surface.hasImageInput || !surface.hasVideoSurface) {
256
+ return surface;
257
+ }
258
+ if (i < maxAttempts - 1) {
259
+ await page.wait({ time: pollMs / 1_000 });
260
+ surface = await inspectPublishSurface(page);
261
+ }
262
+ }
263
+
264
+ return surface;
265
+ }
266
+
154
267
  cli({
155
268
  site: 'xiaohongshu',
156
269
  name: 'publish',
@@ -204,20 +317,18 @@ cli({
204
317
  }
205
318
 
206
319
  // ── Step 2: Select 图文 (image+text) note type if tabs are present ─────────
207
- const tabClicked: boolean = await page.evaluate(`
208
- () => {
209
- const allEls = document.querySelectorAll('[class*="tab"], [class*="note-type"], [class*="type-item"]');
210
- for (const el of allEls) {
211
- const text = el.innerText || el.textContent || '';
212
- if ((text.includes('图文') || text.includes('图片')) && el.offsetParent !== null) {
213
- el.click();
214
- return true;
215
- }
216
- }
217
- return false;
218
- }
219
- `);
220
- if (tabClicked) await page.wait({ time: 1 });
320
+ const tabResult = await selectImageTextTab(page);
321
+ const surface = await waitForImageTextSurface(page, tabResult?.ok ? 5_000 : 2_000);
322
+ if (!surface.hasTitleInput && !surface.hasImageInput && surface.hasVideoSurface) {
323
+ await page.screenshot({ path: '/tmp/xhs_publish_tab_debug.png' });
324
+ const detail = tabResult?.ok
325
+ ? `clicked "${tabResult.text}"`
326
+ : `visible candidates: ${(tabResult?.visibleTexts || []).join(' | ') || 'none'}`;
327
+ throw new Error(
328
+ 'Still on the video publish page after trying to select 图文. ' +
329
+ `Details: ${detail}. Debug screenshot: /tmp/xhs_publish_tab_debug.png`
330
+ );
331
+ }
221
332
 
222
333
  // ── Step 3: Upload images ──────────────────────────────────────────────────
223
334
  if (imageData.length > 0) {
@@ -48,7 +48,8 @@ pipeline:
48
48
  exchange: ${{ item.exchange }}
49
49
  price: ${{ item.price }}
50
50
  changePercent: ${{ item.changePercent }}
51
+ url: ${{ item.url }}
51
52
 
52
53
  - limit: ${{ args.limit }}
53
54
 
54
- columns: [symbol, name, exchange, price, changePercent]
55
+ columns: [symbol, name, exchange, price, changePercent, url]
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * Yahoo Finance stock quote — multi-strategy API fallback.
3
- * Source: bb-sites/yahoo-finance/quote.js
4
3
  */
5
4
  import { cli, Strategy } from '../../registry.js';
6
5
 
@@ -0,0 +1,155 @@
1
+ /**
2
+ * YouTube channel — get channel info and recent videos via InnerTube API.
3
+ */
4
+ import { cli, Strategy } from '../../registry.js';
5
+ import { CommandExecutionError } from '../../errors.js';
6
+
7
+ cli({
8
+ site: 'youtube',
9
+ name: 'channel',
10
+ description: 'Get YouTube channel info and recent videos',
11
+ domain: 'www.youtube.com',
12
+ strategy: Strategy.COOKIE,
13
+ args: [
14
+ { name: 'id', required: true, positional: true, help: 'Channel ID (UCxxxx) or handle (@name)' },
15
+ { name: 'limit', type: 'int', default: 10, help: 'Max recent videos (max 30)' },
16
+ ],
17
+ columns: ['field', 'value'],
18
+ func: async (page, kwargs) => {
19
+ const channelId = String(kwargs.id);
20
+ const limit = Math.min(kwargs.limit || 10, 30);
21
+ await page.goto('https://www.youtube.com');
22
+ await page.wait(2);
23
+
24
+ const data = await page.evaluate(`
25
+ (async () => {
26
+ const channelId = ${JSON.stringify(channelId)};
27
+ const limit = ${limit};
28
+ const cfg = window.ytcfg?.data_ || {};
29
+ const apiKey = cfg.INNERTUBE_API_KEY;
30
+ const context = cfg.INNERTUBE_CONTEXT;
31
+ if (!apiKey || !context) return {error: 'YouTube config not found'};
32
+
33
+ // Resolve handle to browseId if needed
34
+ let browseId = channelId;
35
+ if (channelId.startsWith('@')) {
36
+ const resolveResp = await fetch('/youtubei/v1/navigation/resolve_url?key=' + apiKey + '&prettyPrint=false', {
37
+ method: 'POST', credentials: 'include',
38
+ headers: {'Content-Type': 'application/json'},
39
+ body: JSON.stringify({context, url: 'https://www.youtube.com/' + channelId})
40
+ });
41
+ if (resolveResp.ok) {
42
+ const resolveData = await resolveResp.json();
43
+ browseId = resolveData.endpoint?.browseEndpoint?.browseId || channelId;
44
+ }
45
+ }
46
+
47
+ // Fetch channel data
48
+ const resp = await fetch('/youtubei/v1/browse?key=' + apiKey + '&prettyPrint=false', {
49
+ method: 'POST', credentials: 'include',
50
+ headers: {'Content-Type': 'application/json'},
51
+ body: JSON.stringify({context, browseId})
52
+ });
53
+ if (!resp.ok) return {error: 'Channel API returned HTTP ' + resp.status};
54
+ const data = await resp.json();
55
+
56
+ // Channel metadata
57
+ const metadata = data.metadata?.channelMetadataRenderer || {};
58
+ const header = data.header?.pageHeaderRenderer || data.header?.c4TabbedHeaderRenderer || {};
59
+
60
+ // Subscriber count from header
61
+ let subscriberCount = '';
62
+ try {
63
+ const rows = header.content?.pageHeaderViewModel?.metadata?.contentMetadataViewModel?.metadataRows || [];
64
+ for (const row of rows) {
65
+ for (const part of (row.metadataParts || [])) {
66
+ const text = part.text?.content || '';
67
+ if (text.includes('subscriber')) subscriberCount = text;
68
+ }
69
+ }
70
+ } catch {}
71
+ // Fallback for old c4TabbedHeaderRenderer format
72
+ if (!subscriberCount && header.subscriberCountText?.simpleText) {
73
+ subscriberCount = header.subscriberCountText.simpleText;
74
+ }
75
+
76
+ // Extract recent videos from Home tab
77
+ const tabs = data.contents?.twoColumnBrowseResultsRenderer?.tabs || [];
78
+ const homeTab = tabs.find(t => t.tabRenderer?.selected);
79
+ const recentVideos = [];
80
+
81
+ if (homeTab) {
82
+ const sections = homeTab.tabRenderer?.content?.sectionListRenderer?.contents || [];
83
+ for (const section of sections) {
84
+ for (const shelf of (section.itemSectionRenderer?.contents || [])) {
85
+ for (const item of (shelf.shelfRenderer?.content?.horizontalListRenderer?.items || [])) {
86
+ // New lockupViewModel format
87
+ const lvm = item.lockupViewModel;
88
+ if (lvm && lvm.contentType === 'LOCKUP_CONTENT_TYPE_VIDEO' && recentVideos.length < limit) {
89
+ const meta = lvm.metadata?.lockupMetadataViewModel;
90
+ const rows = meta?.metadata?.contentMetadataViewModel?.metadataRows || [];
91
+ const viewsAndTime = (rows[0]?.metadataParts || []).map(p => p.text?.content).filter(Boolean).join(' | ');
92
+ let duration = '';
93
+ for (const ov of (lvm.contentImage?.thumbnailViewModel?.overlays || [])) {
94
+ for (const b of (ov.thumbnailBottomOverlayViewModel?.badges || [])) {
95
+ if (b.thumbnailBadgeViewModel?.text) duration = b.thumbnailBadgeViewModel.text;
96
+ }
97
+ }
98
+ recentVideos.push({
99
+ title: meta?.title?.content || '',
100
+ duration,
101
+ views: viewsAndTime,
102
+ url: 'https://www.youtube.com/watch?v=' + lvm.contentId,
103
+ });
104
+ }
105
+ // Legacy gridVideoRenderer format
106
+ if (item.gridVideoRenderer && recentVideos.length < limit) {
107
+ const v = item.gridVideoRenderer;
108
+ recentVideos.push({
109
+ title: v.title?.runs?.[0]?.text || v.title?.simpleText || '',
110
+ duration: v.thumbnailOverlays?.[0]?.thumbnailOverlayTimeStatusRenderer?.text?.simpleText || '',
111
+ views: (v.shortViewCountText?.simpleText || '') + (v.publishedTimeText?.simpleText ? ' | ' + v.publishedTimeText.simpleText : ''),
112
+ url: 'https://www.youtube.com/watch?v=' + v.videoId,
113
+ });
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
119
+
120
+ return {
121
+ name: metadata.title || '',
122
+ channelId: metadata.externalId || browseId,
123
+ handle: metadata.vanityChannelUrl?.split('/').pop() || '',
124
+ description: (metadata.description || '').substring(0, 500),
125
+ subscribers: subscriberCount,
126
+ url: metadata.channelUrl || 'https://www.youtube.com/channel/' + browseId,
127
+ keywords: metadata.keywords || '',
128
+ recentVideos,
129
+ };
130
+ })()
131
+ `);
132
+
133
+ if (!data || typeof data !== 'object') throw new CommandExecutionError('Failed to fetch channel data');
134
+ if ((data as Record<string, unknown>).error) throw new CommandExecutionError(String((data as Record<string, unknown>).error));
135
+
136
+ const result = data as Record<string, unknown>;
137
+ const videos = result.recentVideos as Array<Record<string, string>> | undefined;
138
+ delete result.recentVideos;
139
+
140
+ // Channel info as field/value pairs + recent videos as table
141
+ const rows = Object.entries(result).map(([field, value]) => ({
142
+ field,
143
+ value: String(value),
144
+ }));
145
+
146
+ if (videos && videos.length > 0) {
147
+ rows.push({ field: '---', value: '--- Recent Videos ---' });
148
+ for (const v of videos) {
149
+ rows.push({ field: v.title, value: `${v.duration} | ${v.views} | ${v.url}` });
150
+ }
151
+ }
152
+
153
+ return rows;
154
+ },
155
+ });
@@ -0,0 +1,97 @@
1
+ /**
2
+ * YouTube comments — get video comments via InnerTube API.
3
+ */
4
+ import { cli, Strategy } from '../../registry.js';
5
+ import { CommandExecutionError } from '../../errors.js';
6
+ import { parseVideoId } from './utils.js';
7
+
8
+ cli({
9
+ site: 'youtube',
10
+ name: 'comments',
11
+ description: 'Get YouTube video comments',
12
+ domain: 'www.youtube.com',
13
+ strategy: Strategy.COOKIE,
14
+ args: [
15
+ { name: 'url', required: true, positional: true, help: 'YouTube video URL or video ID' },
16
+ { name: 'limit', type: 'int', default: 20, help: 'Max comments (max 100)' },
17
+ ],
18
+ columns: ['rank', 'author', 'text', 'likes', 'replies', 'time'],
19
+ func: async (page, kwargs) => {
20
+ const videoId = parseVideoId(kwargs.url);
21
+ const limit = Math.min(kwargs.limit || 20, 100);
22
+ await page.goto(`https://www.youtube.com/watch?v=${videoId}`);
23
+ await page.wait(3);
24
+
25
+ const data = await page.evaluate(`
26
+ (async () => {
27
+ const videoId = ${JSON.stringify(videoId)};
28
+ const limit = ${limit};
29
+ const cfg = window.ytcfg?.data_ || {};
30
+ const apiKey = cfg.INNERTUBE_API_KEY;
31
+ const context = cfg.INNERTUBE_CONTEXT;
32
+ if (!apiKey || !context) return {error: 'YouTube config not found'};
33
+
34
+ // Step 1: Get comment continuation token
35
+ let continuationToken = null;
36
+
37
+ // Try from current page ytInitialData
38
+ if (window.ytInitialData) {
39
+ const results = window.ytInitialData.contents?.twoColumnWatchNextResults?.results?.results?.contents || [];
40
+ const commentSection = results.find(i => i.itemSectionRenderer?.targetId === 'comments-section');
41
+ continuationToken = commentSection?.itemSectionRenderer?.contents?.[0]?.continuationItemRenderer?.continuationEndpoint?.continuationCommand?.token;
42
+ }
43
+
44
+ // Fallback: fetch via next API
45
+ if (!continuationToken) {
46
+ const nextResp = await fetch('/youtubei/v1/next?key=' + apiKey + '&prettyPrint=false', {
47
+ method: 'POST', credentials: 'include',
48
+ headers: {'Content-Type': 'application/json'},
49
+ body: JSON.stringify({context, videoId})
50
+ });
51
+ if (!nextResp.ok) return {error: 'Failed to get video data: HTTP ' + nextResp.status};
52
+ const nextData = await nextResp.json();
53
+ const results = nextData.contents?.twoColumnWatchNextResults?.results?.results?.contents || [];
54
+ const commentSection = results.find(i => i.itemSectionRenderer?.targetId === 'comments-section');
55
+ continuationToken = commentSection?.itemSectionRenderer?.contents?.[0]?.continuationItemRenderer?.continuationEndpoint?.continuationCommand?.token;
56
+ }
57
+
58
+ if (!continuationToken) return {error: 'No comment section found — comments may be disabled'};
59
+
60
+ // Step 2: Fetch comments
61
+ const commentResp = await fetch('/youtubei/v1/next?key=' + apiKey + '&prettyPrint=false', {
62
+ method: 'POST', credentials: 'include',
63
+ headers: {'Content-Type': 'application/json'},
64
+ body: JSON.stringify({context, continuation: continuationToken})
65
+ });
66
+ if (!commentResp.ok) return {error: 'Failed to fetch comments: HTTP ' + commentResp.status};
67
+ const commentData = await commentResp.json();
68
+
69
+ // Parse from frameworkUpdates (new ViewModel format)
70
+ const mutations = commentData.frameworkUpdates?.entityBatchUpdate?.mutations || [];
71
+ const commentEntities = mutations.filter(m => m.payload?.commentEntityPayload);
72
+
73
+ return commentEntities.slice(0, limit).map((m, i) => {
74
+ const p = m.payload.commentEntityPayload;
75
+ const props = p.properties || {};
76
+ const author = p.author || {};
77
+ const toolbar = p.toolbar || {};
78
+ return {
79
+ rank: i + 1,
80
+ author: author.displayName || '',
81
+ text: (props.content?.content || '').substring(0, 300),
82
+ likes: toolbar.likeCountNotliked || '0',
83
+ replies: toolbar.replyCount || '0',
84
+ time: props.publishedTime || '',
85
+ };
86
+ });
87
+ })()
88
+ `);
89
+
90
+ if (!Array.isArray(data)) {
91
+ const errMsg = data && typeof data === 'object' ? String((data as Record<string, unknown>).error || '') : '';
92
+ if (errMsg) throw new CommandExecutionError(errMsg);
93
+ return [];
94
+ }
95
+ return data;
96
+ },
97
+ });
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * YouTube search — innertube API via browser session.
3
- * Source: bb-sites/youtube/search.js
4
3
  */
5
4
  import { cli, Strategy } from '../../registry.js';
6
5
 
@@ -52,7 +52,8 @@ pipeline:
52
52
  type: ${{ item.type }}
53
53
  author: ${{ item.author }}
54
54
  votes: ${{ item.votes }}
55
+ url: ${{ item.url }}
55
56
 
56
57
  - limit: ${{ args.limit }}
57
58
 
58
- columns: [rank, title, type, author, votes]
59
+ columns: [rank, title, type, author, votes, url]
@@ -0,0 +1,78 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { Command } from 'commander';
3
+ import type { CliCommand } from './registry.js';
4
+
5
+ const { mockExecuteCommand, mockRenderOutput } = vi.hoisted(() => ({
6
+ mockExecuteCommand: vi.fn(),
7
+ mockRenderOutput: vi.fn(),
8
+ }));
9
+
10
+ vi.mock('./execution.js', () => ({
11
+ executeCommand: mockExecuteCommand,
12
+ }));
13
+
14
+ vi.mock('./output.js', () => ({
15
+ render: mockRenderOutput,
16
+ }));
17
+
18
+ import { registerCommandToProgram } from './commanderAdapter.js';
19
+
20
+ describe('commanderAdapter arg passing', () => {
21
+ const cmd: CliCommand = {
22
+ site: 'paperreview',
23
+ name: 'submit',
24
+ description: 'Submit a PDF',
25
+ browser: false,
26
+ args: [
27
+ { name: 'pdf', positional: true, required: true, help: 'Path to the paper PDF' },
28
+ { name: 'dry-run', type: 'bool', default: false, help: 'Validate only' },
29
+ { name: 'prepare-only', type: 'bool', default: false, help: 'Prepare only' },
30
+ ],
31
+ func: vi.fn(),
32
+ };
33
+
34
+ beforeEach(() => {
35
+ mockExecuteCommand.mockReset();
36
+ mockExecuteCommand.mockResolvedValue([]);
37
+ mockRenderOutput.mockReset();
38
+ delete process.env.OPENCLI_VERBOSE;
39
+ process.exitCode = undefined;
40
+ });
41
+
42
+ it('passes bool flag values through to executeCommand for coercion', async () => {
43
+ const program = new Command();
44
+ const siteCmd = program.command('paperreview');
45
+ registerCommandToProgram(siteCmd, cmd);
46
+
47
+ await program.parseAsync(['node', 'opencli', 'paperreview', 'submit', './paper.pdf', '--dry-run', 'false']);
48
+
49
+ expect(mockExecuteCommand).toHaveBeenCalled();
50
+ const kwargs = mockExecuteCommand.mock.calls[0][1];
51
+ expect(kwargs.pdf).toBe('./paper.pdf');
52
+ expect(kwargs).toHaveProperty('dry-run');
53
+ });
54
+
55
+ it('passes valueless bool flags as true to executeCommand', async () => {
56
+ const program = new Command();
57
+ const siteCmd = program.command('paperreview');
58
+ registerCommandToProgram(siteCmd, cmd);
59
+
60
+ await program.parseAsync(['node', 'opencli', 'paperreview', 'submit', './paper.pdf', '--prepare-only']);
61
+
62
+ expect(mockExecuteCommand).toHaveBeenCalled();
63
+ const kwargs = mockExecuteCommand.mock.calls[0][1];
64
+ expect(kwargs.pdf).toBe('./paper.pdf');
65
+ expect(kwargs['prepare-only']).toBe(true);
66
+ });
67
+
68
+ it('rejects invalid bool values before calling executeCommand', async () => {
69
+ const program = new Command();
70
+ const siteCmd = program.command('paperreview');
71
+ registerCommandToProgram(siteCmd, cmd);
72
+
73
+ await program.parseAsync(['node', 'opencli', 'paperreview', 'submit', './paper.pdf', '--dry-run', 'maybe']);
74
+
75
+ // normalizeArgValue validates bools eagerly; executeCommand should not be reached
76
+ expect(mockExecuteCommand).not.toHaveBeenCalled();
77
+ });
78
+ });