@jackwener/opencli 1.0.6 → 1.1.1

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 (400) hide show
  1. package/.agents/skills/cross-project-adapter-migration/SKILL.md +2 -2
  2. package/.github/pull_request_template.md +7 -0
  3. package/.github/workflows/doc-check.yml +36 -0
  4. package/.github/workflows/docs.yml +7 -42
  5. package/CHANGELOG.md +23 -0
  6. package/CLI-EXPLORER.md +9 -8
  7. package/README.md +51 -10
  8. package/README.zh-CN.md +29 -11
  9. package/SKILL.md +102 -33
  10. package/dist/browser/cdp.js +6 -1
  11. package/dist/browser/page.d.ts +4 -1
  12. package/dist/browser/page.js +7 -1
  13. package/dist/build-manifest.js +23 -16
  14. package/dist/cli-manifest.json +951 -296
  15. package/dist/cli.d.ts +6 -0
  16. package/dist/cli.js +225 -148
  17. package/dist/clis/antigravity/serve.js +296 -47
  18. package/dist/clis/apple-podcasts/commands.test.d.ts +2 -0
  19. package/dist/clis/apple-podcasts/commands.test.js +76 -0
  20. package/dist/clis/apple-podcasts/search.js +2 -2
  21. package/dist/clis/apple-podcasts/top.js +9 -2
  22. package/dist/clis/arxiv/paper.js +21 -0
  23. package/dist/clis/arxiv/search.js +24 -0
  24. package/dist/clis/arxiv/utils.d.ts +18 -0
  25. package/dist/clis/arxiv/utils.js +49 -0
  26. package/dist/clis/bilibili/dynamic.js +1 -1
  27. package/dist/clis/bilibili/favorite.js +1 -1
  28. package/dist/clis/bilibili/feed.js +1 -1
  29. package/dist/clis/bilibili/following.js +1 -1
  30. package/dist/clis/bilibili/history.js +1 -1
  31. package/dist/clis/bilibili/me.js +1 -1
  32. package/dist/clis/bilibili/ranking.js +1 -1
  33. package/dist/clis/bilibili/search.js +3 -3
  34. package/dist/clis/bilibili/subtitle.js +1 -1
  35. package/dist/clis/bilibili/user-videos.js +1 -1
  36. package/dist/{bilibili.d.ts → clis/bilibili/utils.d.ts} +1 -1
  37. package/dist/clis/bloomberg/businessweek.d.ts +1 -0
  38. package/dist/clis/bloomberg/businessweek.js +17 -0
  39. package/dist/clis/bloomberg/economics.d.ts +1 -0
  40. package/dist/clis/bloomberg/economics.js +17 -0
  41. package/dist/clis/bloomberg/feeds.d.ts +1 -0
  42. package/dist/clis/bloomberg/feeds.js +15 -0
  43. package/dist/clis/bloomberg/industries.d.ts +1 -0
  44. package/dist/clis/bloomberg/industries.js +17 -0
  45. package/dist/clis/bloomberg/main.d.ts +1 -0
  46. package/dist/clis/bloomberg/main.js +17 -0
  47. package/dist/clis/bloomberg/markets.d.ts +1 -0
  48. package/dist/clis/bloomberg/markets.js +17 -0
  49. package/dist/clis/bloomberg/news.d.ts +1 -0
  50. package/dist/clis/bloomberg/news.js +105 -0
  51. package/dist/clis/bloomberg/opinions.d.ts +1 -0
  52. package/dist/clis/bloomberg/opinions.js +17 -0
  53. package/dist/clis/bloomberg/politics.d.ts +1 -0
  54. package/dist/clis/bloomberg/politics.js +17 -0
  55. package/dist/clis/bloomberg/tech.d.ts +1 -0
  56. package/dist/clis/bloomberg/tech.js +17 -0
  57. package/dist/clis/bloomberg/utils.d.ts +34 -0
  58. package/dist/clis/bloomberg/utils.js +364 -0
  59. package/dist/clis/bloomberg/utils.test.d.ts +1 -0
  60. package/dist/clis/bloomberg/utils.test.js +129 -0
  61. package/dist/clis/boss/batchgreet.d.ts +1 -0
  62. package/dist/clis/boss/batchgreet.js +147 -0
  63. package/dist/clis/boss/chatlist.js +2 -2
  64. package/dist/clis/boss/detail.js +2 -2
  65. package/dist/clis/boss/exchange.d.ts +1 -0
  66. package/dist/clis/boss/exchange.js +111 -0
  67. package/dist/clis/boss/greet.d.ts +1 -0
  68. package/dist/clis/boss/greet.js +175 -0
  69. package/dist/clis/boss/invite.d.ts +1 -0
  70. package/dist/clis/boss/invite.js +158 -0
  71. package/dist/clis/boss/joblist.d.ts +1 -0
  72. package/dist/clis/boss/joblist.js +55 -0
  73. package/dist/clis/boss/mark.d.ts +1 -0
  74. package/dist/clis/boss/mark.js +141 -0
  75. package/dist/clis/boss/recommend.d.ts +1 -0
  76. package/dist/clis/boss/recommend.js +83 -0
  77. package/dist/clis/boss/search.js +1 -1
  78. package/dist/clis/boss/send.js +1 -1
  79. package/dist/clis/boss/stats.d.ts +1 -0
  80. package/dist/clis/boss/stats.js +116 -0
  81. package/dist/clis/chaoxing/assignments.js +1 -1
  82. package/dist/clis/chaoxing/exams.js +1 -1
  83. package/dist/{chaoxing.d.ts → clis/chaoxing/utils.d.ts} +1 -1
  84. package/dist/{chaoxing.js → clis/chaoxing/utils.js} +0 -2
  85. package/dist/clis/chaoxing/utils.test.d.ts +1 -0
  86. package/dist/{chaoxing.test.js → clis/chaoxing/utils.test.js} +1 -1
  87. package/dist/clis/chatgpt/read.js +1 -1
  88. package/dist/clis/chatwise/export.js +1 -1
  89. package/dist/clis/chatwise/model.js +2 -2
  90. package/dist/clis/chatwise/screenshot.js +1 -1
  91. package/dist/clis/codex/export.js +1 -1
  92. package/dist/clis/codex/model.js +2 -2
  93. package/dist/clis/codex/screenshot.js +1 -1
  94. package/dist/clis/coupang/add-to-cart.js +3 -4
  95. package/dist/clis/coupang/search.js +2 -4
  96. package/dist/clis/coupang/utils.test.d.ts +1 -0
  97. package/dist/{coupang.test.js → clis/coupang/utils.test.js} +1 -1
  98. package/dist/clis/ctrip/search.js +1 -1
  99. package/dist/clis/cursor/export.js +1 -1
  100. package/dist/clis/cursor/model.js +2 -2
  101. package/dist/clis/cursor/screenshot.js +1 -1
  102. package/dist/clis/jike/comment.js +2 -3
  103. package/dist/clis/jike/create.js +1 -2
  104. package/dist/clis/jike/feed.js +0 -1
  105. package/dist/clis/jike/like.js +1 -2
  106. package/dist/clis/jike/notifications.js +0 -1
  107. package/dist/clis/jike/post.yaml +1 -0
  108. package/dist/clis/jike/repost.js +1 -2
  109. package/dist/clis/jike/search.js +2 -3
  110. package/dist/clis/jike/topic.yaml +1 -0
  111. package/dist/clis/jike/user.yaml +1 -0
  112. package/dist/clis/jimeng/history.yaml +0 -1
  113. package/dist/clis/linkedin/search.js +7 -7
  114. package/dist/clis/linux-do/category.yaml +1 -0
  115. package/dist/clis/linux-do/search.yaml +4 -3
  116. package/dist/clis/linux-do/topic.yaml +1 -0
  117. package/dist/clis/notion/export.js +1 -1
  118. package/dist/clis/reddit/comment.js +3 -4
  119. package/dist/clis/reddit/read.js +4 -5
  120. package/dist/clis/reddit/save.js +2 -3
  121. package/dist/clis/reddit/saved.js +0 -1
  122. package/dist/clis/reddit/search.yaml +1 -0
  123. package/dist/clis/reddit/subscribe.js +0 -1
  124. package/dist/clis/reddit/upvote.js +2 -3
  125. package/dist/clis/reddit/upvoted.js +0 -1
  126. package/dist/clis/reddit/user-comments.yaml +1 -0
  127. package/dist/clis/reddit/user-posts.yaml +1 -0
  128. package/dist/clis/reddit/user.yaml +1 -0
  129. package/dist/clis/reuters/search.js +1 -1
  130. package/dist/clis/sinafinance/news.d.ts +7 -0
  131. package/dist/clis/sinafinance/news.js +61 -0
  132. package/dist/clis/smzdm/search.js +2 -3
  133. package/dist/clis/stackoverflow/search.yaml +1 -0
  134. package/dist/clis/steam/top-sellers.yaml +29 -0
  135. package/dist/clis/twitter/accept.js +2 -2
  136. package/dist/clis/twitter/article.js +2 -2
  137. package/dist/clis/twitter/block.d.ts +1 -0
  138. package/dist/clis/twitter/block.js +88 -0
  139. package/dist/clis/twitter/delete.js +1 -1
  140. package/dist/clis/twitter/hide-reply.d.ts +1 -0
  141. package/dist/clis/twitter/hide-reply.js +66 -0
  142. package/dist/clis/twitter/like.js +1 -1
  143. package/dist/clis/twitter/post.js +1 -1
  144. package/dist/clis/twitter/reply-dm.js +1 -1
  145. package/dist/clis/twitter/reply.js +2 -2
  146. package/dist/clis/twitter/search.js +1 -1
  147. package/dist/clis/twitter/thread.js +2 -2
  148. package/dist/clis/twitter/trending.d.ts +1 -0
  149. package/dist/clis/twitter/trending.js +91 -0
  150. package/dist/clis/twitter/unblock.d.ts +1 -0
  151. package/dist/clis/twitter/unblock.js +71 -0
  152. package/dist/clis/v2ex/topic.yaml +1 -0
  153. package/dist/clis/weibo/hot.js +0 -1
  154. package/dist/clis/weread/book.js +1 -1
  155. package/dist/clis/weread/highlights.js +1 -1
  156. package/dist/clis/weread/notes.js +1 -1
  157. package/dist/clis/weread/search.js +1 -1
  158. package/dist/clis/wikipedia/search.d.ts +1 -0
  159. package/dist/clis/wikipedia/search.js +30 -0
  160. package/dist/clis/wikipedia/summary.d.ts +1 -0
  161. package/dist/clis/wikipedia/summary.js +28 -0
  162. package/dist/clis/wikipedia/utils.d.ts +8 -0
  163. package/dist/clis/wikipedia/utils.js +18 -0
  164. package/dist/clis/xiaohongshu/creator-note-detail.d.ts +79 -5
  165. package/dist/clis/xiaohongshu/creator-note-detail.js +323 -70
  166. package/dist/clis/xiaohongshu/creator-note-detail.test.d.ts +1 -0
  167. package/dist/clis/xiaohongshu/creator-note-detail.test.js +258 -0
  168. package/dist/clis/xiaohongshu/creator-notes-summary.d.ts +28 -0
  169. package/dist/clis/xiaohongshu/creator-notes-summary.js +92 -0
  170. package/dist/clis/xiaohongshu/creator-notes-summary.test.d.ts +1 -0
  171. package/dist/clis/xiaohongshu/creator-notes-summary.test.js +49 -0
  172. package/dist/clis/xiaohongshu/creator-notes.d.ts +18 -5
  173. package/dist/clis/xiaohongshu/creator-notes.js +189 -71
  174. package/dist/clis/xiaohongshu/creator-notes.test.d.ts +1 -0
  175. package/dist/clis/xiaohongshu/creator-notes.test.js +191 -0
  176. package/dist/clis/xiaohongshu/creator-profile.js +0 -1
  177. package/dist/clis/xiaohongshu/creator-stats.js +0 -1
  178. package/dist/clis/xiaohongshu/download.js +2 -3
  179. package/dist/clis/xiaohongshu/feed.yaml +0 -1
  180. package/dist/clis/xiaohongshu/notifications.yaml +0 -1
  181. package/dist/clis/xiaohongshu/search.js +2 -2
  182. package/dist/clis/xiaohongshu/user.js +1 -2
  183. package/dist/clis/yahoo-finance/quote.js +0 -1
  184. package/dist/clis/youtube/search.js +1 -1
  185. package/dist/clis/youtube/transcript.js +1 -1
  186. package/dist/clis/youtube/video.js +1 -1
  187. package/dist/clis/zhihu/download.js +1 -2
  188. package/dist/clis/zhihu/question.js +1 -1
  189. package/dist/clis/zhihu/search.yaml +4 -3
  190. package/dist/commanderAdapter.d.ts +21 -0
  191. package/dist/commanderAdapter.js +111 -0
  192. package/dist/{engine.d.ts → discovery.d.ts} +0 -6
  193. package/dist/{engine.js → discovery.js} +1 -98
  194. package/dist/download/index.d.ts +2 -6
  195. package/dist/download/index.js +19 -46
  196. package/dist/engine.test.d.ts +1 -1
  197. package/dist/engine.test.js +8 -7
  198. package/dist/execution.d.ts +22 -0
  199. package/dist/execution.js +129 -0
  200. package/dist/explore.js +121 -107
  201. package/dist/external-clis.yaml +48 -0
  202. package/dist/external.d.ts +25 -0
  203. package/dist/external.js +156 -0
  204. package/dist/main.js +1 -1
  205. package/dist/pipeline/steps/browser.js +8 -2
  206. package/dist/registry.d.ts +2 -0
  207. package/dist/registry.js +2 -0
  208. package/dist/runtime.d.ts +5 -0
  209. package/dist/runtime.js +8 -0
  210. package/dist/serialization.d.ts +34 -0
  211. package/dist/serialization.js +63 -0
  212. package/dist/types.d.ts +4 -1
  213. package/docs/.vitepress/config.mts +14 -3
  214. package/docs/adapters/browser/arxiv.md +27 -0
  215. package/docs/adapters/browser/barchart.md +32 -0
  216. package/docs/adapters/browser/bloomberg.md +70 -0
  217. package/docs/adapters/browser/chaoxing.md +39 -0
  218. package/docs/adapters/browser/grok.md +35 -0
  219. package/docs/adapters/browser/hf.md +42 -0
  220. package/docs/adapters/browser/jike.md +45 -0
  221. package/docs/adapters/browser/jimeng.md +39 -0
  222. package/docs/adapters/browser/linux-do.md +45 -0
  223. package/docs/adapters/browser/sinafinance.md +35 -0
  224. package/docs/adapters/browser/stackoverflow.md +35 -0
  225. package/docs/adapters/browser/steam.md +26 -0
  226. package/docs/adapters/browser/twitter.md +3 -0
  227. package/docs/adapters/browser/weread.md +48 -0
  228. package/docs/adapters/browser/wikipedia.md +30 -0
  229. package/docs/adapters/browser/xiaohongshu.md +5 -1
  230. package/docs/adapters/desktop/chatgpt.md +3 -3
  231. package/docs/adapters/index.md +13 -0
  232. package/docs/advanced/download.md +4 -4
  233. package/docs/developer/architecture.md +17 -4
  234. package/package.json +1 -1
  235. package/scripts/check-doc-coverage.sh +69 -0
  236. package/scripts/copy-yaml.cjs +7 -0
  237. package/src/browser/cdp.ts +9 -4
  238. package/src/browser/page.ts +7 -1
  239. package/src/build-manifest.ts +25 -19
  240. package/src/cli.ts +253 -119
  241. package/src/clis/antigravity/serve.ts +323 -50
  242. package/src/clis/apple-podcasts/commands.test.ts +95 -0
  243. package/src/clis/apple-podcasts/search.ts +2 -2
  244. package/src/clis/apple-podcasts/top.ts +12 -2
  245. package/src/clis/arxiv/paper.ts +21 -0
  246. package/src/clis/arxiv/search.ts +24 -0
  247. package/src/clis/arxiv/utils.ts +63 -0
  248. package/src/clis/bilibili/dynamic.ts +1 -1
  249. package/src/clis/bilibili/favorite.ts +1 -1
  250. package/src/clis/bilibili/feed.ts +1 -1
  251. package/src/clis/bilibili/following.ts +1 -1
  252. package/src/clis/bilibili/history.ts +1 -1
  253. package/src/clis/bilibili/me.ts +1 -1
  254. package/src/clis/bilibili/ranking.ts +1 -1
  255. package/src/clis/bilibili/search.ts +3 -3
  256. package/src/clis/bilibili/subtitle.ts +1 -1
  257. package/src/clis/bilibili/user-videos.ts +1 -1
  258. package/src/{bilibili.ts → clis/bilibili/utils.ts} +1 -1
  259. package/src/clis/bloomberg/businessweek.ts +18 -0
  260. package/src/clis/bloomberg/economics.ts +18 -0
  261. package/src/clis/bloomberg/feeds.ts +16 -0
  262. package/src/clis/bloomberg/industries.ts +18 -0
  263. package/src/clis/bloomberg/main.ts +18 -0
  264. package/src/clis/bloomberg/markets.ts +18 -0
  265. package/src/clis/bloomberg/news.ts +136 -0
  266. package/src/clis/bloomberg/opinions.ts +18 -0
  267. package/src/clis/bloomberg/politics.ts +18 -0
  268. package/src/clis/bloomberg/tech.ts +18 -0
  269. package/src/clis/bloomberg/utils.test.ts +135 -0
  270. package/src/clis/bloomberg/utils.ts +429 -0
  271. package/src/clis/boss/batchgreet.ts +167 -0
  272. package/src/clis/boss/chatlist.ts +2 -2
  273. package/src/clis/boss/detail.ts +2 -2
  274. package/src/clis/boss/exchange.ts +126 -0
  275. package/src/clis/boss/greet.ts +198 -0
  276. package/src/clis/boss/invite.ts +177 -0
  277. package/src/clis/boss/joblist.ts +63 -0
  278. package/src/clis/boss/mark.ts +155 -0
  279. package/src/clis/boss/recommend.ts +94 -0
  280. package/src/clis/boss/search.ts +1 -1
  281. package/src/clis/boss/send.ts +1 -1
  282. package/src/clis/boss/stats.ts +130 -0
  283. package/src/clis/chaoxing/assignments.ts +1 -1
  284. package/src/clis/chaoxing/exams.ts +1 -1
  285. package/src/{chaoxing.test.ts → clis/chaoxing/utils.test.ts} +1 -1
  286. package/src/{chaoxing.ts → clis/chaoxing/utils.ts} +1 -3
  287. package/src/clis/chatgpt/README.zh-CN.md +3 -3
  288. package/src/clis/chatgpt/read.ts +1 -1
  289. package/src/clis/chatwise/export.ts +1 -1
  290. package/src/clis/chatwise/model.ts +2 -2
  291. package/src/clis/chatwise/screenshot.ts +1 -1
  292. package/src/clis/codex/export.ts +1 -1
  293. package/src/clis/codex/model.ts +2 -2
  294. package/src/clis/codex/screenshot.ts +1 -1
  295. package/src/clis/coupang/add-to-cart.ts +3 -4
  296. package/src/clis/coupang/search.ts +2 -4
  297. package/src/{coupang.test.ts → clis/coupang/utils.test.ts} +1 -1
  298. package/src/clis/ctrip/search.ts +1 -1
  299. package/src/clis/cursor/export.ts +1 -1
  300. package/src/clis/cursor/model.ts +2 -2
  301. package/src/clis/cursor/screenshot.ts +1 -1
  302. package/src/clis/jike/comment.ts +2 -3
  303. package/src/clis/jike/create.ts +1 -2
  304. package/src/clis/jike/feed.ts +0 -1
  305. package/src/clis/jike/like.ts +1 -2
  306. package/src/clis/jike/notifications.ts +0 -1
  307. package/src/clis/jike/post.yaml +1 -0
  308. package/src/clis/jike/repost.ts +1 -2
  309. package/src/clis/jike/search.ts +2 -3
  310. package/src/clis/jike/topic.yaml +1 -0
  311. package/src/clis/jike/user.yaml +1 -0
  312. package/src/clis/jimeng/history.yaml +0 -1
  313. package/src/clis/linkedin/search.ts +7 -7
  314. package/src/clis/linux-do/category.yaml +1 -0
  315. package/src/clis/linux-do/search.yaml +4 -3
  316. package/src/clis/linux-do/topic.yaml +1 -0
  317. package/src/clis/notion/export.ts +1 -1
  318. package/src/clis/reddit/comment.ts +3 -4
  319. package/src/clis/reddit/read.ts +4 -5
  320. package/src/clis/reddit/save.ts +2 -3
  321. package/src/clis/reddit/saved.ts +0 -1
  322. package/src/clis/reddit/search.yaml +1 -0
  323. package/src/clis/reddit/subscribe.ts +0 -1
  324. package/src/clis/reddit/upvote.ts +2 -3
  325. package/src/clis/reddit/upvoted.ts +0 -1
  326. package/src/clis/reddit/user-comments.yaml +1 -0
  327. package/src/clis/reddit/user-posts.yaml +1 -0
  328. package/src/clis/reddit/user.yaml +1 -0
  329. package/src/clis/reuters/search.ts +1 -1
  330. package/src/clis/sinafinance/news.ts +76 -0
  331. package/src/clis/smzdm/search.ts +2 -3
  332. package/src/clis/stackoverflow/search.yaml +1 -0
  333. package/src/clis/steam/top-sellers.yaml +29 -0
  334. package/src/clis/twitter/accept.ts +2 -2
  335. package/src/clis/twitter/article.ts +2 -2
  336. package/src/clis/twitter/block.ts +92 -0
  337. package/src/clis/twitter/delete.ts +1 -1
  338. package/src/clis/twitter/hide-reply.ts +70 -0
  339. package/src/clis/twitter/like.ts +1 -1
  340. package/src/clis/twitter/post.ts +1 -1
  341. package/src/clis/twitter/reply-dm.ts +1 -1
  342. package/src/clis/twitter/reply.ts +2 -2
  343. package/src/clis/twitter/search.ts +1 -1
  344. package/src/clis/twitter/thread.ts +2 -2
  345. package/src/clis/twitter/trending.ts +113 -0
  346. package/src/clis/twitter/unblock.ts +75 -0
  347. package/src/clis/v2ex/topic.yaml +1 -0
  348. package/src/clis/weibo/hot.ts +0 -1
  349. package/src/clis/weread/book.ts +1 -1
  350. package/src/clis/weread/highlights.ts +1 -1
  351. package/src/clis/weread/notes.ts +1 -1
  352. package/src/clis/weread/search.ts +1 -1
  353. package/src/clis/wikipedia/search.ts +32 -0
  354. package/src/clis/wikipedia/summary.ts +28 -0
  355. package/src/clis/wikipedia/utils.ts +20 -0
  356. package/src/clis/xiaohongshu/creator-note-detail.test.ts +272 -0
  357. package/src/clis/xiaohongshu/creator-note-detail.ts +425 -73
  358. package/src/clis/xiaohongshu/creator-notes-summary.test.ts +54 -0
  359. package/src/clis/xiaohongshu/creator-notes-summary.ts +120 -0
  360. package/src/clis/xiaohongshu/creator-notes.test.ts +211 -0
  361. package/src/clis/xiaohongshu/creator-notes.ts +254 -75
  362. package/src/clis/xiaohongshu/creator-profile.ts +0 -1
  363. package/src/clis/xiaohongshu/creator-stats.ts +0 -1
  364. package/src/clis/xiaohongshu/download.ts +2 -3
  365. package/src/clis/xiaohongshu/feed.yaml +0 -1
  366. package/src/clis/xiaohongshu/notifications.yaml +0 -1
  367. package/src/clis/xiaohongshu/search.ts +2 -2
  368. package/src/clis/xiaohongshu/user.ts +1 -2
  369. package/src/clis/yahoo-finance/quote.ts +0 -1
  370. package/src/clis/youtube/search.ts +1 -1
  371. package/src/clis/youtube/transcript.ts +1 -1
  372. package/src/clis/youtube/video.ts +1 -1
  373. package/src/clis/zhihu/download.ts +1 -2
  374. package/src/clis/zhihu/question.ts +1 -1
  375. package/src/clis/zhihu/search.yaml +4 -3
  376. package/src/commanderAdapter.ts +113 -0
  377. package/src/daemon.ts +3 -3
  378. package/src/{engine.ts → discovery.ts} +1 -108
  379. package/src/download/index.ts +21 -54
  380. package/src/engine.test.ts +8 -7
  381. package/src/execution.ts +138 -0
  382. package/src/explore.ts +135 -109
  383. package/src/external-clis.yaml +48 -0
  384. package/src/external.ts +185 -0
  385. package/src/main.ts +1 -1
  386. package/src/pipeline/steps/browser.ts +7 -2
  387. package/src/registry.ts +5 -0
  388. package/src/runtime.ts +9 -0
  389. package/src/serialization.ts +79 -0
  390. package/src/types.ts +1 -1
  391. package/tests/e2e/browser-public.test.ts +25 -0
  392. package/tests/e2e/public-commands.test.ts +55 -1
  393. package/dist/clis/twitter/trending.yaml +0 -46
  394. package/src/clis/twitter/trending.yaml +0 -46
  395. /package/dist/{chaoxing.test.d.ts → clis/arxiv/paper.d.ts} +0 -0
  396. /package/dist/{coupang.test.d.ts → clis/arxiv/search.d.ts} +0 -0
  397. /package/dist/{bilibili.js → clis/bilibili/utils.js} +0 -0
  398. /package/dist/{coupang.d.ts → clis/coupang/utils.d.ts} +0 -0
  399. /package/dist/{coupang.js → clis/coupang/utils.js} +0 -0
  400. /package/src/{coupang.ts → clis/coupang/utils.ts} +0 -0
@@ -81,76 +81,274 @@ function sleep(ms: number): Promise<void> {
81
81
  return new Promise(resolve => setTimeout(resolve, ms));
82
82
  }
83
83
 
84
+ // ─── DOM helpers ─────────────────────────────────────────────────────
85
+
86
+ /**
87
+ * Click the 'New Conversation' button to reset context.
88
+ */
89
+ async function startNewConversation(page: IPage): Promise<void> {
90
+ await page.evaluate(`
91
+ (() => {
92
+ const btn = document.querySelector('[data-tooltip-id="new-conversation-tooltip"]');
93
+ if (btn) btn.click();
94
+ })()
95
+ `);
96
+ await sleep(1000); // Give UI time to clear
97
+ }
98
+
99
+ /**
100
+ * Switch the active model in Antigravity UI.
101
+ */
102
+ async function switchModel(page: IPage, anthropicModelId: string): Promise<void> {
103
+ // Map standard model IDs to Antigravity UI names based on actual UI
104
+ let targetName = 'claude sonnet 4.6'; // Default fallback
105
+ const id = anthropicModelId.toLowerCase();
106
+
107
+ if (id.includes('sonnet')) {
108
+ targetName = 'claude sonnet 4.6';
109
+ } else if (id.includes('opus')) {
110
+ targetName = 'claude opus 4.6';
111
+ } else if (id.includes('gemini') && id.includes('pro')) {
112
+ targetName = 'gemini 3.1 pro (high)';
113
+ } else if (id.includes('gemini') && id.includes('flash')) {
114
+ targetName = 'gemini 3 flash';
115
+ } else if (id.includes('gpt')) {
116
+ targetName = 'gpt-oss 120b';
117
+ }
118
+
119
+ try {
120
+ await page.evaluate(`
121
+ async () => {
122
+ const targetModelName = ${JSON.stringify(targetName)};
123
+ const trigger = document.querySelector('div[aria-haspopup="dialog"] > div[tabindex="0"]');
124
+ if (!trigger) return; // Silent fail if UI changed
125
+
126
+ // Open dropdown only if not already selected
127
+ if (trigger.innerText.toLowerCase().includes(targetModelName)) return;
128
+
129
+ trigger.click();
130
+ await new Promise(r => setTimeout(r, 200));
131
+
132
+ const spans = Array.from(document.querySelectorAll('[role="dialog"] span'));
133
+ const target = spans.find(s => s.innerText.toLowerCase().includes(targetModelName));
134
+ if (target) {
135
+ const optionNode = target.closest('.cursor-pointer') || target;
136
+ optionNode.click();
137
+ } else {
138
+ // Close if not found
139
+ trigger.click();
140
+ }
141
+ }
142
+ `);
143
+ await sleep(500); // Wait for switch
144
+ } catch (err) {
145
+ console.error(`[serve] Warning: Could not switch to model ${targetName}:`, err);
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Check if the Antigravity UI is currently generating a response
151
+ * by looking for Stop/Cancel buttons or loading indicators.
152
+ */
153
+ async function isGenerating(page: IPage): Promise<boolean> {
154
+ const result = await page.evaluate(`
155
+ (() => {
156
+ // Look for a cancel/stop button in the UI
157
+ const cancelBtn = document.querySelector('button[aria-label*="cancel" i], button[aria-label*="stop" i], button[title*="cancel" i], button[title*="stop" i]');
158
+ return !!cancelBtn;
159
+ })()
160
+ `);
161
+ return Boolean(result);
162
+ }
163
+
164
+ /**
165
+ * Walk from the scroll container and find the deepest element that
166
+ * has multiple non-empty children (our message container).
167
+ */
168
+ function findMessageContainer(root: Element | null, depth = 0): Element | null {
169
+ if (!root || depth > 12) return null;
170
+ const nonEmpty = Array.from(root.children).filter(
171
+ c => (c as HTMLElement).innerText?.trim().length > 5
172
+ );
173
+ if (nonEmpty.length >= 2) return root;
174
+ if (nonEmpty.length === 1) return findMessageContainer(nonEmpty[0], depth + 1);
175
+ return root;
176
+ }
177
+
84
178
  // ─── Antigravity CDP Operations ──────────────────────────────────────
85
179
 
180
+ /**
181
+ * Get the full chat text for change-detection polling.
182
+ */
86
183
  async function getConversationText(page: IPage): Promise<string> {
87
184
  const text = await page.evaluate(`
88
185
  (() => {
89
186
  const container = document.getElementById('conversation');
90
- return container ? container.innerText : '';
187
+ if (!container) return '';
188
+ // Read only the first child div (actual chat content),
189
+ // skipping UI chrome like file change panels, model selectors, etc.
190
+ const chatContent = container.children[0];
191
+ return chatContent ? chatContent.innerText : container.innerText;
91
192
  })()
92
193
  `);
93
194
  return String(text ?? '');
94
195
  }
95
196
 
96
- async function sendMessage(page: IPage, message: string): Promise<void> {
97
- await page.evaluate(`
98
- (async () => {
197
+ /**
198
+ * Get the text of the last assistant reply by navigating to the message container
199
+ * and extracting the last non-empty message block.
200
+ */
201
+ async function getLastAssistantReply(page: IPage, userText?: string): Promise<string> {
202
+ const text = await page.evaluate(`
203
+ (() => {
204
+ const conv = document.getElementById('conversation')?.children[0];
205
+ const scroll = conv?.querySelector('.overflow-y-auto');
206
+
207
+ // Walk down until we find a container with multiple message siblings
208
+ function findMsgContainer(el, depth) {
209
+ if (!el || depth > 12) return null;
210
+ const nonEmpty = Array.from(el.children).filter(c => c.innerText && c.innerText.trim().length > 5);
211
+ if (nonEmpty.length >= 2) return el;
212
+ if (nonEmpty.length === 1) return findMsgContainer(nonEmpty[0], depth + 1);
213
+ return null;
214
+ }
215
+
216
+ const container = findMsgContainer(scroll || conv, 0);
217
+ if (!container) return '';
218
+
219
+ // Get all non-empty children (skip trailing empty UI divs)
220
+ const msgs = Array.from(container.children).filter(
221
+ c => c.innerText && c.innerText.trim().length > 5
222
+ );
223
+
224
+ if (msgs.length === 0) return '';
225
+
226
+ // The last element is the last assistant reply
227
+ const last = msgs[msgs.length - 1];
228
+ return last.innerText || '';
229
+ })()
230
+ `);
231
+ let reply = String(text ?? '').trim();
232
+
233
+ // Strip echoed user message from the top (Antigravity sometimes includes it)
234
+ if (userText && reply.startsWith(userText)) {
235
+ reply = reply.slice(userText.length).trim();
236
+ }
237
+
238
+ // Strip thinking block: "Thought for Xs\n..." at the start
239
+ reply = reply.replace(/^Thought for[^\n]*\n+/i, '').trim();
240
+
241
+ // Strip "Copy" button text at the end
242
+ reply = reply.replace(/\s*\bCopy\b\s*$/m, '').trim();
243
+
244
+ // De-duplicate trailing repeated content (e.g., "OK\n\nOK" → "OK")
245
+ const half = Math.floor(reply.length / 2);
246
+ const firstHalf = reply.slice(0, half).trim();
247
+ const secondHalf = reply.slice(half).trim();
248
+ if (firstHalf && firstHalf === secondHalf) {
249
+ reply = firstHalf;
250
+ }
251
+
252
+ return reply;
253
+ }
254
+
255
+ async function sendMessage(page: IPage, message: string, bridge?: CDPBridge): Promise<void> {
256
+ if (!bridge) {
257
+ // Fallback: use JS-based approach
258
+ await page.evaluate(`
259
+ (() => {
260
+ const container = document.getElementById('antigravity.agentSidePanelInputBox');
261
+ const editor = container?.querySelector('[data-lexical-editor="true"]');
262
+ if (!editor) throw new Error('Could not find input box');
263
+ editor.focus();
264
+ document.execCommand('insertText', false, ${JSON.stringify(message)});
265
+ })()
266
+ `);
267
+ await sleep(500);
268
+ await page.pressKey('Enter');
269
+ return;
270
+ }
271
+
272
+ // Get the bounding box of the Lexical editor for a physical mouse click
273
+ const rect = await page.evaluate(`
274
+ (() => {
99
275
  const container = document.getElementById('antigravity.agentSidePanelInputBox');
100
276
  if (!container) throw new Error('Could not find antigravity.agentSidePanelInputBox');
101
277
  const editor = container.querySelector('[data-lexical-editor="true"]');
102
278
  if (!editor) throw new Error('Could not find Antigravity input box');
103
-
104
- editor.focus();
105
- document.execCommand('insertText', false, ${JSON.stringify(message)});
279
+ const r = editor.getBoundingClientRect();
280
+ return JSON.stringify({ x: r.left + r.width / 2, y: r.top + r.height / 2 });
106
281
  })()
107
282
  `);
108
- await sleep(500);
109
- await page.pressKey('Enter');
283
+ const { x, y } = JSON.parse(String(rect));
284
+
285
+ // Physical mouse click to give the element real browser focus
286
+ await bridge.send('Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button: 'left', clickCount: 1 });
287
+ await sleep(50);
288
+ await bridge.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 });
289
+ await sleep(200);
290
+
291
+ // Inject text at the CDP level (no deprecated execCommand)
292
+ await bridge.send('Input.insertText', { text: message });
293
+ await sleep(300);
294
+
295
+ // Send Enter via native CDP key event
296
+ await bridge.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13 });
297
+ await sleep(50);
298
+ await bridge.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Enter', code: 'Enter', windowsVirtualKeyCode: 13, nativeVirtualKeyCode: 13 });
110
299
  }
111
300
 
112
301
  async function waitForReply(
113
302
  page: IPage,
114
303
  beforeText: string,
115
- opts: { timeout?: number; pollInterval?: number; stableThreshold?: number } = {},
116
- ): Promise<string> {
304
+ opts: { timeout?: number; pollInterval?: number } = {},
305
+ ): Promise<void> {
117
306
  const timeout = opts.timeout ?? 120_000; // 2 minutes max
118
307
  const pollInterval = opts.pollInterval ?? 500; // 500ms polling
119
- const stableThreshold = opts.stableThreshold ?? 6; // 6 × 500ms = 3s stable
120
308
 
121
309
  const deadline = Date.now() + timeout;
122
- let lastText = beforeText;
123
- let stableCount = 0;
124
310
 
125
- // Wait a bit for the model to start generating
311
+ // Wait a bit to ensure the UI transitions to "generating" state after we hit Enter
126
312
  await sleep(1000);
127
313
 
314
+ let hasStartedGenerating = false;
315
+ let lastText = beforeText;
316
+ let stableCount = 0;
317
+ const stableThreshold = 4; // 4 * 500ms = 2s of stability fallback
318
+
128
319
  while (Date.now() < deadline) {
129
- const current = await getConversationText(page);
130
-
131
- if (current.length > beforeText.length) {
132
- // New content appeared
133
- if (current === lastText) {
134
- stableCount++;
135
- if (stableCount >= stableThreshold) {
136
- // Text has been stable — reply is complete
137
- return current.slice(beforeText.length).trim();
320
+ const generating = await isGenerating(page);
321
+ const currentText = await getConversationText(page);
322
+ const textChanged = currentText !== beforeText && currentText.length > 0;
323
+
324
+ if (generating) {
325
+ hasStartedGenerating = true;
326
+ stableCount = 0; // Reset stability while generating
327
+ } else {
328
+ if (hasStartedGenerating) {
329
+ // It actively generated and now it stopped -> DONE
330
+ // Provide a small buffer to let React render the final message fully
331
+ await sleep(500);
332
+ return;
333
+ }
334
+
335
+ // Fallback: If it never showed "Generating/Cancel", but text changed and is stable
336
+ if (textChanged) {
337
+ if (currentText === lastText) {
338
+ stableCount++;
339
+ if (stableCount >= stableThreshold) {
340
+ return; // Text has been stable for 2 seconds -> DONE
341
+ }
342
+ } else {
343
+ stableCount = 0;
344
+ lastText = currentText;
138
345
  }
139
- } else {
140
- // Still generating
141
- stableCount = 0;
142
- lastText = current;
143
346
  }
144
347
  }
145
348
 
146
349
  await sleep(pollInterval);
147
350
  }
148
351
 
149
- // Timeout — return whatever we have
150
- const finalText = await getConversationText(page);
151
- if (finalText.length > beforeText.length) {
152
- return finalText.slice(beforeText.length).trim();
153
- }
154
352
  throw new Error('Timeout waiting for Antigravity reply');
155
353
  }
156
354
 
@@ -159,6 +357,7 @@ async function waitForReply(
159
357
  async function handleMessages(
160
358
  body: AnthropicRequest,
161
359
  page: IPage,
360
+ bridge?: CDPBridge,
162
361
  ): Promise<AnthropicResponse> {
163
362
  // Extract the last user message
164
363
  const userMessages = body.messages.filter(m => m.role === 'user');
@@ -172,16 +371,30 @@ async function handleMessages(
172
371
  throw new Error('Empty user message');
173
372
  }
174
373
 
374
+ // Optimization 1: New conversation if this is the first message in the session
375
+ if (body.messages.length === 1) {
376
+ console.error(`[serve] New session detected (1 message). Starting new conversation in UI.`);
377
+ await startNewConversation(page);
378
+ }
379
+
380
+ // Optimization 3: Switch model if requested
381
+ if (body.model) {
382
+ await switchModel(page, body.model);
383
+ }
384
+
175
385
  // Get conversation state before sending
176
386
  const beforeText = await getConversationText(page);
177
387
 
178
388
  // Send the message
179
389
  console.error(`[serve] Sending: "${userText.slice(0, 80)}${userText.length > 80 ? '...' : ''}"`);
180
- await sendMessage(page, userText);
390
+ await sendMessage(page, userText, bridge);
181
391
 
182
- // Poll for reply
392
+ // Poll for reply (change detection)
183
393
  console.error('[serve] Waiting for reply...');
184
- const replyText = await waitForReply(page, beforeText);
394
+ await waitForReply(page, beforeText);
395
+
396
+ // Extract the actual reply text precisely from the DOM
397
+ const replyText = await getLastAssistantReply(page, userText);
185
398
  console.error(`[serve] Got reply: "${replyText.slice(0, 80)}${replyText.length > 80 ? '...' : ''}"`);
186
399
 
187
400
  return {
@@ -204,17 +417,74 @@ async function handleMessages(
204
417
  export async function startServe(opts: { port?: number } = {}): Promise<void> {
205
418
  const port = opts.port ?? 8082;
206
419
 
207
- // Establish persistent CDP connection
208
- console.error('[serve] Connecting to Antigravity via CDP...');
209
- const cdp = new CDPBridge();
210
- const page = await cdp.connect({ timeout: 15_000 });
211
- console.error('[serve] CDP connected successfully.');
420
+ // Lazy CDP connection — connect when first request comes in
421
+ let cdp: CDPBridge | null = null;
422
+ let page: IPage | null = null;
423
+ let requestInFlight = false;
212
424
 
213
- // Verify we can read conversation
214
- const testText = await getConversationText(page);
215
- console.error(`[serve] Conversation element found (${testText.length} chars).`);
425
+ async function ensureConnected(): Promise<IPage> {
426
+ if (page) {
427
+ try {
428
+ await page.evaluate('1+1');
429
+ return page;
430
+ } catch {
431
+ console.error('[serve] CDP connection lost, reconnecting...');
432
+ cdp?.close().catch(() => {});
433
+ cdp = null;
434
+ page = null;
435
+ }
436
+ }
216
437
 
217
- let requestInFlight = false;
438
+ const endpoint = process.env.OPENCLI_CDP_ENDPOINT;
439
+ if (!endpoint) {
440
+ throw new Error(
441
+ 'OPENCLI_CDP_ENDPOINT is not set.\n' +
442
+ 'Usage: OPENCLI_CDP_ENDPOINT=http://127.0.0.1:9224 opencli antigravity serve'
443
+ );
444
+ }
445
+
446
+ // Note: Antigravity chat panel lives inside editor windows, not in Launchpad.
447
+ // If multiple editor windows are open, set OPENCLI_CDP_TARGET to the window title.
448
+ if (process.env.OPENCLI_CDP_TARGET) {
449
+ console.error(`[serve] Using OPENCLI_CDP_TARGET=${process.env.OPENCLI_CDP_TARGET}`);
450
+ }
451
+
452
+ // List available targets for debugging
453
+ try {
454
+ const res = await fetch(`${endpoint.replace(/\/$/, '')}/json`);
455
+ const targets = await res.json() as Array<{ title?: string; type?: string }>;
456
+ const pages = targets.filter(t => t.type === 'page');
457
+ console.error(`[serve] Available targets: ${pages.map(t => `"${t.title}"`).join(', ')}`);
458
+ } catch { /* ignore */ }
459
+
460
+ console.error(`[serve] Connecting via CDP (target pattern: "${process.env.OPENCLI_CDP_TARGET}")...`);
461
+ cdp = new CDPBridge();
462
+ try {
463
+ page = await cdp.connect({ timeout: 15_000 });
464
+ } catch (err: any) {
465
+ cdp = null;
466
+ const isRefused = err?.cause?.code === 'ECONNREFUSED' || err?.message?.includes('ECONNREFUSED');
467
+ throw new Error(
468
+ isRefused
469
+ ? `Cannot connect to Antigravity at ${endpoint}.\n` +
470
+ ' 1. Make sure Antigravity is running\n' +
471
+ ' 2. Launch with: --remote-debugging-port=9224'
472
+ : `CDP connection failed: ${err.message}`
473
+ );
474
+ }
475
+
476
+ console.error('[serve] ✅ CDP connected.');
477
+
478
+ // Quick verification
479
+ const hasUI = await page.evaluate(`
480
+ (() => !!document.getElementById('conversation') || !!document.getElementById('antigravity.agentSidePanelInputBox'))()
481
+ `);
482
+ if (!hasUI) {
483
+ console.error('[serve] ⚠️ Warning: chat UI elements not found in this target. Try setting OPENCLI_CDP_TARGET to the correct window title.');
484
+ }
485
+
486
+ return page;
487
+ }
218
488
 
219
489
  const server = createServer(async (req, res) => {
220
490
  // CORS preflight
@@ -266,7 +536,6 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
266
536
  const body = JSON.parse(rawBody) as AnthropicRequest;
267
537
 
268
538
  if (body.stream) {
269
- // We don't support streaming — return error
270
539
  jsonResponse(res, 400, {
271
540
  type: 'error',
272
541
  error: {
@@ -277,7 +546,9 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
277
546
  return;
278
547
  }
279
548
 
280
- const response = await handleMessages(body, page);
549
+ // Lazy connect on first request
550
+ const activePage = await ensureConnected();
551
+ const response = await handleMessages(body, activePage, cdp ?? undefined);
281
552
  jsonResponse(res, 200, response);
282
553
  } finally {
283
554
  requestInFlight = false;
@@ -287,7 +558,7 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
287
558
 
288
559
  // Health check
289
560
  if (req.method === 'GET' && (pathname === '/' || pathname === '/health')) {
290
- jsonResponse(res, 200, { ok: true, status: 'connected' });
561
+ jsonResponse(res, 200, { ok: true, cdpConnected: page !== null });
291
562
  return;
292
563
  }
293
564
 
@@ -296,7 +567,7 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
296
567
  error: { type: 'not_found_error', message: `Not found: ${pathname}` },
297
568
  });
298
569
  } catch (err) {
299
- console.error('[serve] Error:', err);
570
+ console.error('[serve] Error:', err instanceof Error ? err.message : err);
300
571
  jsonResponse(res, 500, {
301
572
  type: 'error',
302
573
  error: {
@@ -310,6 +581,7 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
310
581
  server.listen(port, '127.0.0.1', () => {
311
582
  console.error(`\n[serve] ✅ Antigravity API proxy running at http://127.0.0.1:${port}`);
312
583
  console.error(`[serve] Compatible with Anthropic /v1/messages API`);
584
+ console.error(`[serve] CDP connection will be established on first request.`);
313
585
  console.error(`\n[serve] Usage with Claude Code:`);
314
586
  console.error(` ANTHROPIC_BASE_URL=http://localhost:${port} claude\n`);
315
587
  });
@@ -317,7 +589,7 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
317
589
  // Graceful shutdown
318
590
  const shutdown = () => {
319
591
  console.error('\n[serve] Shutting down...');
320
- cdp.close().catch(() => {});
592
+ cdp?.close().catch(() => {});
321
593
  server.close();
322
594
  process.exit(0);
323
595
  };
@@ -327,3 +599,4 @@ export async function startServe(opts: { port?: number } = {}): Promise<void> {
327
599
  // Keep alive
328
600
  await new Promise(() => {});
329
601
  }
602
+
@@ -0,0 +1,95 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { getRegistry } from '../../registry.js';
3
+ import './search.js';
4
+ import './top.js';
5
+
6
+ describe('apple-podcasts search command', () => {
7
+ beforeEach(() => {
8
+ vi.restoreAllMocks();
9
+ });
10
+
11
+ it('uses the positional query argument for the iTunes search request', async () => {
12
+ const cmd = getRegistry().get('apple-podcasts/search');
13
+ expect(cmd?.func).toBeTypeOf('function');
14
+
15
+ const fetchMock = vi.fn().mockResolvedValue({
16
+ ok: true,
17
+ json: () => Promise.resolve({
18
+ results: [
19
+ {
20
+ collectionId: 42,
21
+ collectionName: 'Machine Learning Guide',
22
+ artistName: 'OpenCLI',
23
+ trackCount: 12,
24
+ primaryGenreName: 'Technology',
25
+ },
26
+ ],
27
+ }),
28
+ });
29
+ vi.stubGlobal('fetch', fetchMock);
30
+
31
+ const result = await cmd!.func!(null as any, {
32
+ query: 'machine learning',
33
+ keyword: 'sports',
34
+ limit: 5,
35
+ });
36
+
37
+ expect(fetchMock).toHaveBeenCalledWith(
38
+ 'https://itunes.apple.com/search?term=machine%20learning&media=podcast&limit=5',
39
+ );
40
+ expect(result).toEqual([
41
+ {
42
+ id: 42,
43
+ title: 'Machine Learning Guide',
44
+ author: 'OpenCLI',
45
+ episodes: 12,
46
+ genre: 'Technology',
47
+ },
48
+ ]);
49
+ });
50
+ });
51
+
52
+ describe('apple-podcasts top command', () => {
53
+ beforeEach(() => {
54
+ vi.restoreAllMocks();
55
+ });
56
+
57
+ it('uses the canonical Apple charts host and maps ranked results', async () => {
58
+ const cmd = getRegistry().get('apple-podcasts/top');
59
+ expect(cmd?.func).toBeTypeOf('function');
60
+
61
+ const fetchMock = vi.fn().mockResolvedValue({
62
+ ok: true,
63
+ json: () => Promise.resolve({
64
+ feed: {
65
+ results: [
66
+ { id: '100', name: 'Top Show', artistName: 'Host A' },
67
+ { id: '101', name: 'Second Show', artistName: 'Host B' },
68
+ ],
69
+ },
70
+ }),
71
+ });
72
+ vi.stubGlobal('fetch', fetchMock);
73
+
74
+ const result = await cmd!.func!(null as any, { country: 'US', limit: 2 });
75
+
76
+ expect(fetchMock).toHaveBeenCalledWith(
77
+ 'https://rss.marketingtools.apple.com/api/v2/us/podcasts/top/2/podcasts.json',
78
+ );
79
+ expect(result).toEqual([
80
+ { rank: 1, title: 'Top Show', author: 'Host A', id: '100' },
81
+ { rank: 2, title: 'Second Show', author: 'Host B', id: '101' },
82
+ ]);
83
+ });
84
+
85
+ it('normalizes network failures into CliError output', async () => {
86
+ const cmd = getRegistry().get('apple-podcasts/top');
87
+ expect(cmd?.func).toBeTypeOf('function');
88
+
89
+ vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('socket hang up')));
90
+
91
+ await expect(cmd!.func!(null as any, { country: 'us', limit: 3 })).rejects.toThrow(
92
+ 'Unable to reach Apple Podcasts charts for US',
93
+ );
94
+ });
95
+ });
@@ -9,12 +9,12 @@ cli({
9
9
  strategy: Strategy.PUBLIC,
10
10
  browser: false,
11
11
  args: [
12
- { name: 'keyword', positional: true, required: true, help: 'Search keyword' },
12
+ { name: 'query', positional: true, required: true, help: 'Search keyword' },
13
13
  { name: 'limit', type: 'int', default: 10, help: 'Max results' },
14
14
  ],
15
15
  columns: ['id', 'title', 'author', 'episodes', 'genre'],
16
16
  func: async (_page, args) => {
17
- const term = encodeURIComponent(args.keyword);
17
+ const term = encodeURIComponent(args.query);
18
18
  const limit = Math.max(1, Math.min(Number(args.limit), 25));
19
19
  const data = await itunesFetch(`/search?term=${term}&media=podcast&limit=${limit}`);
20
20
  if (!data.results?.length) throw new CliError('NOT_FOUND', 'No podcasts found', `Try a different keyword`);
@@ -2,7 +2,7 @@ import { cli, Strategy } from '../../registry.js';
2
2
  import { CliError } from '../../errors.js';
3
3
 
4
4
  // Apple Marketing Tools RSS API — public, no key required
5
- const CHARTS_URL = 'https://rss.applemarketingtools.com/api/v2';
5
+ const CHARTS_URL = 'https://rss.marketingtools.apple.com/api/v2';
6
6
 
7
7
  cli({
8
8
  site: 'apple-podcasts',
@@ -19,7 +19,17 @@ cli({
19
19
  const limit = Math.max(1, Math.min(Number(args.limit), 100));
20
20
  const country = String(args.country || 'us').trim().toLowerCase();
21
21
  const url = `${CHARTS_URL}/${country}/podcasts/top/${limit}/podcasts.json`;
22
- const resp = await fetch(url);
22
+ let resp: Response;
23
+ try {
24
+ resp = await fetch(url);
25
+ } catch (error: any) {
26
+ const reason = error?.cause?.code ?? error?.message ?? 'unknown network error';
27
+ throw new CliError(
28
+ 'FETCH_ERROR',
29
+ `Unable to reach Apple Podcasts charts for ${country.toUpperCase()}`,
30
+ `Apple charts may be temporarily unavailable (${reason}). Try again later.`,
31
+ );
32
+ }
23
33
  if (!resp.ok) throw new CliError('FETCH_ERROR', `Charts API HTTP ${resp.status}`, `Check country code: ${country}`);
24
34
  const data = await resp.json();
25
35
  const results = data?.feed?.results;
@@ -0,0 +1,21 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { CliError } from '../../errors.js';
3
+ import { arxivFetch, parseEntries } from './utils.js';
4
+
5
+ cli({
6
+ site: 'arxiv',
7
+ name: 'paper',
8
+ description: 'Get arXiv paper details by ID',
9
+ strategy: Strategy.PUBLIC,
10
+ browser: false,
11
+ args: [
12
+ { name: 'id', positional: true, required: true, help: 'arXiv paper ID (e.g. 1706.03762)' },
13
+ ],
14
+ columns: ['id', 'title', 'authors', 'published', 'abstract', 'url'],
15
+ func: async (_page, args) => {
16
+ const xml = await arxivFetch(`id_list=${encodeURIComponent(args.id)}`);
17
+ const entries = parseEntries(xml);
18
+ if (!entries.length) throw new CliError('NOT_FOUND', `Paper ${args.id} not found`, 'Check the arXiv ID format, e.g. 1706.03762');
19
+ return entries;
20
+ },
21
+ });
@@ -0,0 +1,24 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { CliError } from '../../errors.js';
3
+ import { arxivFetch, parseEntries } from './utils.js';
4
+
5
+ cli({
6
+ site: 'arxiv',
7
+ name: 'search',
8
+ description: 'Search arXiv papers',
9
+ strategy: Strategy.PUBLIC,
10
+ browser: false,
11
+ args: [
12
+ { name: 'query', positional: true, required: true, help: 'Search keyword (e.g. "attention is all you need")' },
13
+ { name: 'limit', type: 'int', default: 10, help: 'Max results (max 25)' },
14
+ ],
15
+ columns: ['id', 'title', 'authors', 'published'],
16
+ func: async (_page, args) => {
17
+ const limit = Math.max(1, Math.min(Number(args.limit), 25));
18
+ const query = encodeURIComponent(`all:${args.keyword}`);
19
+ const xml = await arxivFetch(`search_query=${query}&max_results=${limit}&sortBy=relevance`);
20
+ const entries = parseEntries(xml);
21
+ if (!entries.length) throw new CliError('NOT_FOUND', 'No papers found', 'Try a different keyword');
22
+ return entries.map(e => ({ id: e.id, title: e.title, authors: e.authors, published: e.published }));
23
+ },
24
+ });