@jackwener/opencli 1.7.12 → 1.7.14

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 (419) hide show
  1. package/README.md +8 -7
  2. package/README.zh-CN.md +9 -8
  3. package/cli-manifest.json +12128 -6665
  4. package/clis/1point3acres/digest.js +35 -0
  5. package/clis/1point3acres/forum.js +51 -0
  6. package/clis/1point3acres/forums.js +44 -0
  7. package/clis/1point3acres/hot.js +35 -0
  8. package/clis/1point3acres/latest.js +35 -0
  9. package/clis/1point3acres/notifications.js +64 -0
  10. package/clis/1point3acres/search.js +71 -0
  11. package/clis/1point3acres/thread.js +117 -0
  12. package/clis/1point3acres/user.js +77 -0
  13. package/clis/1point3acres/utils.js +247 -0
  14. package/clis/_shared/desktop-commands.js +4 -0
  15. package/clis/aibase/news.js +110 -0
  16. package/clis/aibase/news.test.js +59 -0
  17. package/clis/amazon/discussion.test.js +1 -28
  18. package/clis/antigravity/watch.js +3 -2
  19. package/clis/arxiv/author.js +44 -0
  20. package/clis/baidu-scholar/search.js +0 -1
  21. package/clis/bbc/topic.js +57 -0
  22. package/clis/bbc/utils.js +79 -0
  23. package/clis/chaoxing/assignments.js +1 -1
  24. package/clis/chaoxing/exams.js +1 -1
  25. package/clis/chatgpt/ask.js +57 -0
  26. package/clis/chatgpt/commands.test.js +45 -0
  27. package/clis/chatgpt/detail.js +46 -0
  28. package/clis/chatgpt/history.js +39 -0
  29. package/clis/chatgpt/image.js +12 -11
  30. package/clis/chatgpt/image.test.js +23 -0
  31. package/clis/chatgpt/new.js +25 -0
  32. package/clis/chatgpt/read.js +43 -0
  33. package/clis/chatgpt/send.js +46 -0
  34. package/clis/chatgpt/status.js +29 -0
  35. package/clis/chatgpt/utils.js +294 -4
  36. package/clis/chatgpt/utils.test.js +13 -0
  37. package/clis/chatgpt-app/ask.js +6 -3
  38. package/clis/chatwise/ask.js +16 -43
  39. package/clis/chatwise/composer.test.js +186 -0
  40. package/clis/chatwise/send.js +2 -24
  41. package/clis/chatwise/utils.js +143 -0
  42. package/clis/claude/ask.js +1 -1
  43. package/clis/claude/detail.js +1 -0
  44. package/clis/claude/history.js +1 -0
  45. package/clis/claude/new.js +1 -0
  46. package/clis/claude/read.js +1 -0
  47. package/clis/claude/send.js +1 -0
  48. package/clis/claude/status.js +1 -0
  49. package/clis/codex/ask.js +15 -9
  50. package/clis/codex/history.js +16 -33
  51. package/clis/codex/projects.js +28 -0
  52. package/clis/codex/read.js +10 -4
  53. package/clis/codex/send.js +10 -3
  54. package/clis/codex/sidebar.js +356 -0
  55. package/clis/codex/sidebar.test.js +329 -0
  56. package/clis/coingecko/categories.js +75 -0
  57. package/clis/coingecko/coin.js +107 -0
  58. package/clis/coingecko/coingecko.test.js +109 -0
  59. package/clis/coingecko/derivatives.js +84 -0
  60. package/clis/coingecko/exchanges.js +74 -0
  61. package/clis/coingecko/global.js +71 -0
  62. package/clis/coingecko/top.js +64 -0
  63. package/clis/coingecko/trending.js +55 -0
  64. package/clis/coupang/add-to-cart.js +21 -13
  65. package/clis/coupang/coupang.test.js +159 -0
  66. package/clis/coupang/product.js +257 -0
  67. package/clis/coupang/search.js +38 -16
  68. package/clis/coupang/utils.js +55 -1
  69. package/clis/crates/crate.js +62 -0
  70. package/clis/crates/search.js +44 -0
  71. package/clis/crates/utils.js +72 -0
  72. package/clis/ctrip/ctrip.test.js +234 -0
  73. package/clis/ctrip/hotel-suggest.js +45 -0
  74. package/clis/ctrip/search.js +22 -68
  75. package/clis/ctrip/utils.js +175 -0
  76. package/clis/cursor/ask.js +6 -3
  77. package/clis/dblp/author.js +133 -0
  78. package/clis/dblp/venue.js +64 -0
  79. package/clis/deepseek/ask.js +12 -7
  80. package/clis/deepseek/ask.test.js +13 -13
  81. package/clis/deepseek/detail.js +38 -0
  82. package/clis/deepseek/detail.test.js +81 -0
  83. package/clis/deepseek/history.js +1 -0
  84. package/clis/deepseek/new.js +1 -0
  85. package/clis/deepseek/read.js +1 -0
  86. package/clis/deepseek/send.js +140 -0
  87. package/clis/deepseek/send.test.js +107 -0
  88. package/clis/deepseek/status.js +1 -0
  89. package/clis/deepseek/utils.js +66 -0
  90. package/clis/deepseek/utils.test.js +107 -1
  91. package/clis/defillama/defillama.test.js +99 -0
  92. package/clis/defillama/protocol.js +84 -0
  93. package/clis/defillama/protocols.js +55 -0
  94. package/clis/defillama/utils.js +99 -0
  95. package/clis/devto/latest.js +74 -0
  96. package/clis/dockerhub/image.js +52 -0
  97. package/clis/dockerhub/search.js +47 -0
  98. package/clis/dockerhub/utils.js +100 -0
  99. package/clis/doubao/ask.js +7 -3
  100. package/clis/doubao/detail.js +1 -0
  101. package/clis/doubao/history.js +1 -0
  102. package/clis/doubao/meeting-summary.js +1 -0
  103. package/clis/doubao/meeting-transcript.js +1 -0
  104. package/clis/doubao/new.js +1 -0
  105. package/clis/doubao/read.js +1 -0
  106. package/clis/doubao/send.js +1 -0
  107. package/clis/doubao/status.js +1 -0
  108. package/clis/douyin/draft.test.js +1 -30
  109. package/clis/endoflife/endoflife.test.js +51 -0
  110. package/clis/endoflife/product.js +55 -0
  111. package/clis/endoflife/utils.js +89 -0
  112. package/clis/facebook/__fixtures__/notifications-page.html +13 -0
  113. package/clis/facebook/notifications.js +326 -30
  114. package/clis/facebook/notifications.test.js +458 -0
  115. package/clis/flathub/app.js +71 -0
  116. package/clis/flathub/flathub.test.js +90 -0
  117. package/clis/flathub/search.js +80 -0
  118. package/clis/flathub/utils.js +114 -0
  119. package/clis/gemini/ask.js +7 -3
  120. package/clis/gemini/ask.test.js +2 -2
  121. package/clis/gemini/deep-research-result.js +6 -2
  122. package/clis/gemini/deep-research-result.test.js +15 -14
  123. package/clis/gemini/deep-research.js +8 -4
  124. package/clis/gemini/deep-research.test.js +15 -18
  125. package/clis/gemini/image.js +7 -2
  126. package/clis/gemini/new.js +1 -0
  127. package/clis/gemini/utils.js +0 -4
  128. package/clis/google-scholar/cite.js +0 -1
  129. package/clis/google-scholar/profile.js +0 -1
  130. package/clis/google-scholar/search.js +0 -1
  131. package/clis/goproxy/goproxy.test.js +103 -0
  132. package/clis/goproxy/module.js +47 -0
  133. package/clis/goproxy/utils.js +165 -0
  134. package/clis/goproxy/versions.js +59 -0
  135. package/clis/gov-law/recent.js +0 -1
  136. package/clis/gov-law/search.js +0 -1
  137. package/clis/gov-policy/__fixtures__/recent.html +16 -0
  138. package/clis/gov-policy/__fixtures__/search.html +41 -0
  139. package/clis/gov-policy/gov-policy.test.js +224 -0
  140. package/clis/gov-policy/recent.js +66 -24
  141. package/clis/gov-policy/search.js +65 -23
  142. package/clis/gov-policy/utils.js +54 -0
  143. package/clis/grok/ask.js +49 -265
  144. package/clis/grok/ask.test.js +21 -46
  145. package/clis/grok/detail.js +60 -0
  146. package/clis/grok/history.js +48 -0
  147. package/clis/grok/{image.ts → image.js} +56 -70
  148. package/clis/grok/image.test.ts +20 -0
  149. package/clis/grok/new.js +20 -0
  150. package/clis/grok/read.js +39 -0
  151. package/clis/grok/send.js +50 -0
  152. package/clis/grok/status.js +41 -0
  153. package/clis/grok/utils.js +326 -0
  154. package/clis/grok/utils.test.js +103 -0
  155. package/clis/hf/datasets.js +88 -0
  156. package/clis/hf/hf.test.js +16 -0
  157. package/clis/hf/models.js +91 -0
  158. package/clis/hf/paper.js +79 -0
  159. package/clis/hf/spaces.js +101 -0
  160. package/clis/hf/top.js +1 -0
  161. package/clis/homebrew/cask.js +39 -0
  162. package/clis/homebrew/formula.js +41 -0
  163. package/clis/homebrew/popular.js +54 -0
  164. package/clis/homebrew/utils.js +100 -0
  165. package/clis/hupu/__fixtures__/hot-home.html +64 -0
  166. package/clis/hupu/detail.js +0 -1
  167. package/clis/hupu/hot.js +156 -35
  168. package/clis/hupu/hot.test.js +224 -0
  169. package/clis/hupu/search.js +0 -1
  170. package/clis/instagram/note.js +1 -1
  171. package/clis/instagram/note.test.js +1 -29
  172. package/clis/instagram/post.js +1 -1
  173. package/clis/instagram/post.test.js +1 -1
  174. package/clis/instagram/reel.js +1 -1
  175. package/clis/instagram/story.js +1 -1
  176. package/clis/instagram/story.test.js +1 -34
  177. package/clis/jd/commands.test.js +1 -24
  178. package/clis/lichess/lichess.test.js +85 -0
  179. package/clis/lichess/top.js +46 -0
  180. package/clis/lichess/user.js +91 -0
  181. package/clis/lichess/utils.js +97 -0
  182. package/clis/linkedin/search.js +107 -10
  183. package/clis/linkedin/search.test.js +222 -0
  184. package/clis/linux-do/feed.js +2 -5
  185. package/clis/linux-do/feed.test.js +35 -0
  186. package/clis/lobsters/domain.js +92 -0
  187. package/clis/maven/artifact.js +49 -0
  188. package/clis/maven/search.js +51 -0
  189. package/clis/maven/utils.js +110 -0
  190. package/clis/mdn/search.js +97 -0
  191. package/clis/medium/tag.js +135 -0
  192. package/clis/npm/downloads.js +59 -0
  193. package/clis/npm/package.js +70 -0
  194. package/clis/npm/search.js +49 -0
  195. package/clis/npm/utils.js +76 -0
  196. package/clis/nuget/nuget.test.js +111 -0
  197. package/clis/nuget/package.js +101 -0
  198. package/clis/nuget/search.js +69 -0
  199. package/clis/nuget/utils.js +87 -0
  200. package/clis/nvd/cve.js +121 -0
  201. package/clis/oeis/oeis.test.js +88 -0
  202. package/clis/oeis/search.js +63 -0
  203. package/clis/oeis/sequence.js +71 -0
  204. package/clis/oeis/utils.js +88 -0
  205. package/clis/openalex/search.js +69 -0
  206. package/clis/openalex/utils.js +160 -0
  207. package/clis/openalex/work.js +65 -0
  208. package/clis/openfda/drug-label.js +74 -0
  209. package/clis/openfda/food-recall.js +65 -0
  210. package/clis/openfda/openfda.test.js +114 -0
  211. package/clis/openfda/utils.js +67 -0
  212. package/clis/osv/osv.test.js +97 -0
  213. package/clis/osv/query.js +72 -0
  214. package/clis/osv/utils.js +169 -0
  215. package/clis/osv/vulnerability.js +54 -0
  216. package/clis/packagist/package.js +49 -0
  217. package/clis/packagist/search.js +43 -0
  218. package/clis/packagist/utils.js +113 -0
  219. package/clis/paperreview/feedback.js +1 -1
  220. package/clis/paperreview/review.js +1 -1
  221. package/clis/paperreview/submit.js +1 -1
  222. package/clis/pixiv/download.test.js +1 -1
  223. package/clis/pixiv/illusts.test.js +1 -1
  224. package/clis/pixiv/search.test.js +1 -1
  225. package/clis/pubmed/article.js +50 -0
  226. package/clis/pubmed/author.js +64 -0
  227. package/clis/pubmed/citations.js +36 -0
  228. package/clis/pubmed/pubmed.test.js +276 -0
  229. package/clis/pubmed/related.js +45 -0
  230. package/clis/pubmed/search.js +75 -0
  231. package/clis/pubmed/utils.js +309 -0
  232. package/clis/pypi/downloads.js +66 -0
  233. package/clis/pypi/package.js +79 -0
  234. package/clis/pypi/utils.js +55 -0
  235. package/clis/quark/mv.js +1 -1
  236. package/clis/quark/save.js +1 -1
  237. package/clis/qwen/ask.js +85 -0
  238. package/clis/qwen/detail.js +62 -0
  239. package/clis/qwen/history.js +61 -0
  240. package/clis/qwen/image.js +179 -0
  241. package/clis/qwen/new.js +23 -0
  242. package/clis/qwen/read.js +41 -0
  243. package/clis/qwen/send.js +55 -0
  244. package/clis/qwen/status.js +37 -0
  245. package/clis/qwen/utils.js +409 -0
  246. package/clis/qwen/utils.test.js +45 -0
  247. package/clis/rest-countries/country.js +65 -0
  248. package/clis/rest-countries/region.js +64 -0
  249. package/clis/rest-countries/rest-countries.test.js +83 -0
  250. package/clis/rest-countries/utils.js +126 -0
  251. package/clis/reuters/article-detail.js +53 -0
  252. package/clis/reuters/reuters.test.js +299 -0
  253. package/clis/reuters/search.js +45 -34
  254. package/clis/reuters/utils.js +159 -0
  255. package/clis/rfc/rfc.js +52 -0
  256. package/clis/rfc/rfc.test.js +74 -0
  257. package/clis/rfc/utils.js +72 -0
  258. package/clis/rubygems/gem.js +42 -0
  259. package/clis/rubygems/search.js +47 -0
  260. package/clis/rubygems/utils.js +86 -0
  261. package/clis/stackoverflow/related.js +66 -0
  262. package/clis/stackoverflow/stackoverflow.test.js +58 -0
  263. package/clis/stackoverflow/tag.js +60 -0
  264. package/clis/stackoverflow/user.js +50 -0
  265. package/clis/stackoverflow/utils.js +118 -0
  266. package/clis/steam/app.js +67 -0
  267. package/clis/steam/search.js +58 -0
  268. package/clis/steam/steam.test.js +46 -0
  269. package/clis/steam/utils.js +107 -0
  270. package/clis/taobao/commands.test.js +1 -24
  271. package/clis/test-utils.js +61 -0
  272. package/clis/tieba/hot.js +0 -1
  273. package/clis/tiktok/comment.js +128 -41
  274. package/clis/tiktok/creator-videos.js +270 -0
  275. package/clis/tiktok/creator-videos.test.js +113 -0
  276. package/clis/tiktok/explore.js +137 -29
  277. package/clis/tiktok/follow.js +115 -33
  278. package/clis/tiktok/following.js +157 -36
  279. package/clis/tiktok/friends.js +139 -37
  280. package/clis/tiktok/live.js +137 -41
  281. package/clis/tiktok/notifications.js +141 -38
  282. package/clis/tiktok/refactor.test.js +389 -0
  283. package/clis/tiktok/unfollow.js +124 -38
  284. package/clis/tiktok/user.js +203 -29
  285. package/clis/tiktok/utils.js +505 -0
  286. package/clis/tiktok/write-refactor.test.js +370 -0
  287. package/clis/toutiao/articles.js +36 -62
  288. package/clis/toutiao/hot.js +63 -0
  289. package/clis/toutiao/toutiao.test.js +378 -0
  290. package/clis/toutiao/utils.js +161 -0
  291. package/clis/tvmaze/search.js +61 -0
  292. package/clis/tvmaze/show.js +60 -0
  293. package/clis/tvmaze/tvmaze.test.js +93 -0
  294. package/clis/tvmaze/utils.js +110 -0
  295. package/clis/twitter/accept.js +1 -1
  296. package/clis/twitter/followers.js +134 -69
  297. package/clis/twitter/quote.js +139 -0
  298. package/clis/twitter/quote.test.js +106 -0
  299. package/clis/twitter/reply-dm.js +1 -1
  300. package/clis/twitter/reply.test.js +1 -29
  301. package/clis/twitter/retweet.js +99 -0
  302. package/clis/twitter/retweet.test.js +69 -0
  303. package/clis/twitter/shared.js +38 -0
  304. package/clis/twitter/shared.test.js +28 -1
  305. package/clis/twitter/unlike.js +87 -0
  306. package/clis/twitter/unlike.test.js +72 -0
  307. package/clis/twitter/unretweet.js +99 -0
  308. package/clis/twitter/unretweet.test.js +69 -0
  309. package/clis/uisdc/news.js +105 -0
  310. package/clis/uisdc/news.test.js +66 -0
  311. package/clis/wanfang/search.js +0 -1
  312. package/clis/web/read.js +47 -17
  313. package/clis/web/read.test.js +101 -1
  314. package/clis/weixin/create-draft.js +1 -1
  315. package/clis/weixin/drafts.js +1 -1
  316. package/clis/weixin/drafts.test.js +5 -1
  317. package/clis/weixin/search.js +157 -0
  318. package/clis/weixin/search.test.js +227 -0
  319. package/clis/wikidata/entity.js +60 -0
  320. package/clis/wikidata/search.js +50 -0
  321. package/clis/wikidata/utils.js +117 -0
  322. package/clis/wikidata/wikidata.test.js +83 -0
  323. package/clis/wikipedia/page.js +95 -0
  324. package/clis/wttr/current.js +63 -0
  325. package/clis/wttr/forecast.js +71 -0
  326. package/clis/wttr/utils.js +50 -0
  327. package/clis/wttr/wttr.test.js +84 -0
  328. package/clis/xianyu/chat.js +16 -4
  329. package/clis/xianyu/chat.test.js +64 -0
  330. package/clis/xianyu/publish.js +485 -0
  331. package/clis/xianyu/publish.test.js +220 -0
  332. package/clis/xiaoe/catalog.js +105 -40
  333. package/clis/xiaoe/content.js +164 -29
  334. package/clis/xiaoe/courses.js +86 -29
  335. package/clis/xiaoe/xiaoe.test.js +486 -0
  336. package/clis/xiaohongshu/creator-notes-summary.js +1 -1
  337. package/clis/xiaohongshu/publish.js +16 -3
  338. package/clis/xiaohongshu/publish.test.js +46 -1
  339. package/clis/youtube/transcript.js +13 -19
  340. package/clis/youtube/transcript.test.js +17 -0
  341. package/clis/yuanbao/ask.js +17 -66
  342. package/clis/yuanbao/ask.test.js +5 -5
  343. package/clis/yuanbao/detail.js +65 -0
  344. package/clis/yuanbao/history.js +51 -0
  345. package/clis/yuanbao/new.js +1 -0
  346. package/clis/yuanbao/read.js +38 -0
  347. package/clis/yuanbao/send.js +57 -0
  348. package/clis/yuanbao/shared.js +297 -5
  349. package/clis/yuanbao/shared.test.js +80 -0
  350. package/clis/yuanbao/status.js +44 -0
  351. package/clis/zlibrary/commands.test.js +1 -11
  352. package/dist/src/browser/base-page.d.ts +9 -0
  353. package/dist/src/browser/base-page.js +44 -1
  354. package/dist/src/browser/base-page.test.js +66 -0
  355. package/dist/src/browser/bridge.js +47 -45
  356. package/dist/src/browser/cdp.d.ts +1 -0
  357. package/dist/src/browser/cdp.js +51 -9
  358. package/dist/src/browser/daemon-client.d.ts +4 -0
  359. package/dist/src/browser/errors.js +1 -1
  360. package/dist/src/browser/page.d.ts +1 -1
  361. package/dist/src/browser/page.js +3 -1
  362. package/dist/src/browser/page.test.js +29 -0
  363. package/dist/src/browser/target-errors.d.ts +2 -1
  364. package/dist/src/browser/target-errors.js +1 -0
  365. package/dist/src/browser/target-resolver.d.ts +25 -0
  366. package/dist/src/browser/target-resolver.js +43 -0
  367. package/dist/src/browser.test.js +18 -0
  368. package/dist/src/build-manifest.js +9 -4
  369. package/dist/src/build-manifest.test.js +2 -8
  370. package/dist/src/capabilityRouting.d.ts +16 -1
  371. package/dist/src/capabilityRouting.js +24 -1
  372. package/dist/src/capabilityRouting.test.js +19 -1
  373. package/dist/src/cli.js +76 -11
  374. package/dist/src/cli.test.js +241 -1
  375. package/dist/src/commanderAdapter.js +23 -9
  376. package/dist/src/commanderAdapter.test.js +0 -1
  377. package/dist/src/discovery.js +2 -5
  378. package/dist/src/errors.js +1 -1
  379. package/dist/src/execution.d.ts +1 -1
  380. package/dist/src/execution.js +111 -27
  381. package/dist/src/execution.test.js +326 -17
  382. package/dist/src/help.d.ts +27 -2
  383. package/dist/src/help.js +196 -23
  384. package/dist/src/help.test.d.ts +1 -0
  385. package/dist/src/help.test.js +54 -0
  386. package/dist/src/main.js +14 -1
  387. package/dist/src/manifest-types.d.ts +5 -3
  388. package/dist/src/pipeline/executor.js +1 -1
  389. package/dist/src/pipeline/executor.test.js +8 -0
  390. package/dist/src/pipeline/registry.d.ts +9 -0
  391. package/dist/src/pipeline/registry.js +13 -1
  392. package/dist/src/pipeline/steps/browser.d.ts +1 -0
  393. package/dist/src/pipeline/steps/browser.js +10 -0
  394. package/dist/src/pipeline/steps/download.test.js +1 -0
  395. package/dist/src/registry-api.d.ts +1 -1
  396. package/dist/src/registry.d.ts +12 -11
  397. package/dist/src/registry.js +16 -6
  398. package/dist/src/registry.test.js +2 -2
  399. package/dist/src/runtime.d.ts +2 -1
  400. package/dist/src/runtime.js +1 -1
  401. package/dist/src/serialization.d.ts +2 -2
  402. package/dist/src/serialization.js +4 -6
  403. package/dist/src/serialization.test.js +17 -0
  404. package/dist/src/types.d.ts +17 -0
  405. package/dist/src/validate.js +15 -11
  406. package/dist/src/validate.test.d.ts +9 -0
  407. package/dist/src/validate.test.js +90 -0
  408. package/package.json +1 -1
  409. package/scripts/fetch-adapters.js +1 -1
  410. package/scripts/typed-error-lint-baseline.json +5 -77
  411. package/clis/ctrip/search.test.js +0 -64
  412. package/clis/gov-policy/commands.test.js +0 -27
  413. package/clis/linux-do/category.js +0 -37
  414. package/clis/linux-do/hot.js +0 -26
  415. package/clis/linux-do/latest.js +0 -19
  416. package/clis/pixiv/test-utils.js +0 -23
  417. package/clis/toutiao/articles.test.js +0 -30
  418. package/dist/src/analysis.d.ts +0 -40
  419. package/dist/src/analysis.js +0 -172
@@ -0,0 +1,126 @@
1
+ // Shared helpers for the REST Countries adapter (https://restcountries.com).
2
+ //
3
+ // REST Countries is a free public country-metadata API, no API key required.
4
+ // We hit v3.1 only. The `fields=` query param is mandatory in v3.1 to keep
5
+ // payloads small; we always specify the agent-useful projection.
6
+ import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
7
+
8
+ export const REST_COUNTRIES_BASE = 'https://restcountries.com/v3.1';
9
+ const UA = 'opencli-rest-countries-adapter/1.0 (+https://github.com/jackwener/opencli; mailto:opencli@example.com)';
10
+
11
+ // REST Countries valid region values; subregions are validated server-side.
12
+ export const REST_COUNTRIES_REGIONS = new Set(['africa', 'americas', 'asia', 'europe', 'oceania', 'antarctic']);
13
+
14
+ // Fields the adapter always requests; keep this list aligned with `columns` so
15
+ // rows never have null-where-absent silent drops.
16
+ export const COUNTRY_FIELDS = [
17
+ 'name', 'cca2', 'cca3', 'ccn3', 'capital', 'region', 'subregion',
18
+ 'population', 'area', 'languages', 'currencies', 'flag', 'latlng', 'timezones',
19
+ 'independent', 'unMember', 'landlocked',
20
+ ].join(',');
21
+
22
+ export function requireString(value, label) {
23
+ const s = String(value ?? '').trim();
24
+ if (!s) throw new ArgumentError(`rest-countries ${label} cannot be empty`);
25
+ return s;
26
+ }
27
+
28
+ export function requireBoundedInt(value, defaultValue, maxValue, label = 'limit') {
29
+ const raw = value ?? defaultValue;
30
+ const n = typeof raw === 'number' ? raw : Number(raw);
31
+ if (!Number.isInteger(n) || n <= 0) {
32
+ throw new ArgumentError(`rest-countries ${label} must be a positive integer`);
33
+ }
34
+ if (n > maxValue) {
35
+ throw new ArgumentError(`rest-countries ${label} must be <= ${maxValue}`);
36
+ }
37
+ return n;
38
+ }
39
+
40
+ export function requireRegion(value) {
41
+ const raw = String(value ?? '').trim().toLowerCase();
42
+ if (!raw) throw new ArgumentError('rest-countries region is required (e.g. "europe", "asia")');
43
+ if (!REST_COUNTRIES_REGIONS.has(raw)) {
44
+ throw new ArgumentError(
45
+ `rest-countries region "${value}" is not recognised`,
46
+ `Allowed regions: ${[...REST_COUNTRIES_REGIONS].join(', ')}.`,
47
+ );
48
+ }
49
+ return raw;
50
+ }
51
+
52
+ export async function restCountriesFetch(url, label) {
53
+ let resp;
54
+ try {
55
+ resp = await fetch(url, { headers: { 'user-agent': UA, accept: 'application/json' } });
56
+ }
57
+ catch (err) {
58
+ throw new CommandExecutionError(
59
+ `${label} request failed: ${err?.message ?? err}`,
60
+ 'Check that restcountries.com is reachable from this network.',
61
+ );
62
+ }
63
+ if (resp.status === 404) {
64
+ throw new EmptyResultError(label, `REST Countries returned 404 for ${url}.`);
65
+ }
66
+ if (resp.status === 429) {
67
+ throw new CommandExecutionError(`${label} returned HTTP 429 (rate limited)`);
68
+ }
69
+ if (!resp.ok) {
70
+ throw new CommandExecutionError(`${label} returned HTTP ${resp.status}`);
71
+ }
72
+ let body;
73
+ try {
74
+ body = await resp.json();
75
+ }
76
+ catch (err) {
77
+ throw new CommandExecutionError(`${label} returned malformed JSON: ${err?.message ?? err}`);
78
+ }
79
+ return body;
80
+ }
81
+
82
+ /** Convert REST Countries' `{cur: {name, symbol}}` map to a comma-joined list. */
83
+ export function joinCurrencies(currencies) {
84
+ if (!currencies || typeof currencies !== 'object') return '';
85
+ return Object.entries(currencies)
86
+ .map(([code, info]) => {
87
+ const name = info && typeof info.name === 'string' ? info.name : '';
88
+ return name ? `${code} (${name})` : code;
89
+ })
90
+ .join(', ');
91
+ }
92
+
93
+ /** Convert `{eng: 'English', fra: 'French'}` map to a comma-joined list of language names. */
94
+ export function joinLanguages(languages) {
95
+ if (!languages || typeof languages !== 'object') return '';
96
+ return Object.values(languages).filter((v) => typeof v === 'string' && v.trim()).join(', ');
97
+ }
98
+
99
+ /** Project a REST Countries v3.1 country object into a row matching the adapter columns. */
100
+ export function projectCountry(c) {
101
+ const common = c?.name?.common ?? null;
102
+ const official = c?.name?.official ?? null;
103
+ const cca3 = typeof c?.cca3 === 'string' ? c.cca3 : null;
104
+ return {
105
+ commonName: typeof common === 'string' ? common : null,
106
+ officialName: typeof official === 'string' ? official : null,
107
+ cca2: typeof c?.cca2 === 'string' ? c.cca2 : null,
108
+ cca3,
109
+ ccn3: typeof c?.ccn3 === 'string' ? c.ccn3 : null,
110
+ capital: Array.isArray(c?.capital) ? c.capital.join(', ') : null,
111
+ region: typeof c?.region === 'string' ? c.region : null,
112
+ subregion: typeof c?.subregion === 'string' ? c.subregion : null,
113
+ population: typeof c?.population === 'number' ? c.population : null,
114
+ area: typeof c?.area === 'number' ? c.area : null,
115
+ languages: joinLanguages(c?.languages),
116
+ currencies: joinCurrencies(c?.currencies),
117
+ latitude: Array.isArray(c?.latlng) && typeof c.latlng[0] === 'number' ? c.latlng[0] : null,
118
+ longitude: Array.isArray(c?.latlng) && typeof c.latlng[1] === 'number' ? c.latlng[1] : null,
119
+ timezones: Array.isArray(c?.timezones) ? c.timezones.join(', ') : null,
120
+ independent: typeof c?.independent === 'boolean' ? c.independent : null,
121
+ unMember: typeof c?.unMember === 'boolean' ? c.unMember : null,
122
+ landlocked: typeof c?.landlocked === 'boolean' ? c.landlocked : null,
123
+ flag: typeof c?.flag === 'string' ? c.flag : null,
124
+ url: cca3 ? `https://restcountries.com/v3.1/alpha/${cca3.toLowerCase()}` : '',
125
+ };
126
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Reuters article-detail — full article body + canonical metadata.
3
+ *
4
+ * Pairs with `reuters search` (use the `url` column to round-trip into
5
+ * detail). Reads the in-page Fusion globalContent payload + paragraph DOM.
6
+ */
7
+ import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
8
+ import { cli, Strategy } from '@jackwener/opencli/registry';
9
+ import { buildArticleDetailScript, mapArticleDetail } from './utils.js';
10
+
11
+ const REUTERS_HOST = /^https?:\/\/(?:www\.)?reuters\.com\//i;
12
+
13
+ cli({
14
+ site: 'reuters',
15
+ name: 'article-detail',
16
+ access: 'read',
17
+ description: 'Reuters 路透社文章详情:标题/作者/正文文本',
18
+ domain: 'www.reuters.com',
19
+ strategy: Strategy.COOKIE,
20
+ args: [
21
+ { name: 'url', required: true, positional: true, help: 'Reuters article URL (must be on reuters.com)' },
22
+ ],
23
+ columns: ['title', 'date', 'section', 'section_path', 'authors', 'description', 'word_count', 'url', 'body'],
24
+ func: async (page, kwargs) => {
25
+ const url = String(kwargs.url || '').trim();
26
+ if (!url) {
27
+ throw new ArgumentError('Article URL cannot be empty');
28
+ }
29
+ if (!REUTERS_HOST.test(url)) {
30
+ throw new ArgumentError(`URL must be on reuters.com, got ${url}`);
31
+ }
32
+ await page.goto(url);
33
+ await page.wait(2);
34
+ const result = await page.evaluate(buildArticleDetailScript());
35
+ if (result?.error) {
36
+ throw new CommandExecutionError(`Reuters article-detail failed inside the page: ${result.error}`);
37
+ }
38
+ if (result?.authRequired) {
39
+ throw new AuthRequiredError('www.reuters.com', 'Reuters article-detail is gated by login, subscription, or human verification');
40
+ }
41
+ if (!result || result.ok !== true) {
42
+ throw new CommandExecutionError(
43
+ 'Reuters article-detail returned no payload',
44
+ 'Check that the URL points to a Reuters article and that the page loaded',
45
+ );
46
+ }
47
+ const detail = mapArticleDetail(result.body?.article, result.body?.bodyText, url);
48
+ if (!detail || (!detail.title && !detail.body)) {
49
+ throw new EmptyResultError('reuters article-detail', 'Page rendered no article body — likely paywalled or a non-article URL');
50
+ }
51
+ return [detail];
52
+ },
53
+ });
@@ -0,0 +1,299 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
3
+ import { getRegistry } from '@jackwener/opencli/registry';
4
+ import './search.js';
5
+ import './article-detail.js';
6
+ import { buildArticleDetailScript, buildSearchScript, isAuthStatus, looksAuthWallText, mapArticleDetail, mapSearchArticles, parseLimit } from './utils.js';
7
+
8
+ function makePage(evaluateResult) {
9
+ return {
10
+ goto: vi.fn().mockResolvedValue(undefined),
11
+ wait: vi.fn().mockResolvedValue(undefined),
12
+ evaluate: vi.fn().mockResolvedValue(evaluateResult),
13
+ };
14
+ }
15
+
16
+ describe('reuters parseLimit', () => {
17
+ it('returns fallback for undefined / empty', () => {
18
+ expect(parseLimit(undefined)).toBe(10);
19
+ expect(parseLimit('')).toBe(10);
20
+ });
21
+ it('accepts integers in [1, 40]', () => {
22
+ expect(parseLimit(1)).toBe(1);
23
+ expect(parseLimit(40)).toBe(40);
24
+ expect(parseLimit('25')).toBe(25);
25
+ });
26
+ it('rejects non-integer', () => {
27
+ expect(() => parseLimit('abc')).toThrow('--limit must be an integer');
28
+ expect(() => parseLimit(3.5)).toThrow('--limit must be an integer');
29
+ });
30
+ it('rejects out-of-range without silent clamp', () => {
31
+ expect(() => parseLimit(0)).toThrow('--limit must be between 1 and 40, got 0');
32
+ expect(() => parseLimit(41)).toThrow('--limit must be between 1 and 40, got 41');
33
+ });
34
+ });
35
+
36
+ describe('reuters buildSearchScript', () => {
37
+ it('embeds the user query as JSON-safe literal', () => {
38
+ const script = buildSearchScript('hello "world"', 5);
39
+ expect(script).toContain('"hello \\"world\\""');
40
+ expect(script).toContain('size: 5');
41
+ });
42
+ });
43
+
44
+ describe('reuters auth-wall helpers', () => {
45
+ it('detects auth statuses and Reuters challenge/paywall text', () => {
46
+ expect(isAuthStatus(401)).toBe(true);
47
+ expect(isAuthStatus(403)).toBe(true);
48
+ expect(isAuthStatus(500)).toBe(false);
49
+ expect(looksAuthWallText('DataDome verify you are human')).toBe(true);
50
+ expect(looksAuthWallText('Subscribe to continue reading')).toBe(true);
51
+ expect(looksAuthWallText('ordinary article body')).toBe(false);
52
+ });
53
+ });
54
+
55
+ describe('reuters mapSearchArticles', () => {
56
+ const sampleBody = {
57
+ result: {
58
+ articles: [
59
+ {
60
+ title: 'Tariff war heats up',
61
+ display_date: '2026-05-06T14:30:00.000Z',
62
+ taxonomy: { section: { name: 'World', path: '/world' } },
63
+ authors: [{ name: 'Jane Doe' }, { name: 'Bob Smith' }],
64
+ canonical_url: '/world/tariff-war-2026-05-06/',
65
+ },
66
+ {
67
+ headlines: { basic: 'Markets rally' },
68
+ published_time: '2026-05-05T09:00:00.000Z',
69
+ canonical_url: '/markets/rally-2026-05-05/',
70
+ },
71
+ ],
72
+ },
73
+ };
74
+
75
+ it('projects to declared shape', () => {
76
+ const rows = mapSearchArticles(sampleBody, 5);
77
+ expect(rows).toEqual([
78
+ {
79
+ rank: 1,
80
+ title: 'Tariff war heats up',
81
+ date: '2026-05-06',
82
+ section: 'World',
83
+ section_path: '/world',
84
+ authors: 'Jane Doe, Bob Smith',
85
+ url: 'https://www.reuters.com/world/tariff-war-2026-05-06/',
86
+ },
87
+ {
88
+ rank: 2,
89
+ title: 'Markets rally',
90
+ date: '2026-05-05',
91
+ section: null,
92
+ section_path: null,
93
+ authors: null,
94
+ url: 'https://www.reuters.com/markets/rally-2026-05-05/',
95
+ },
96
+ ]);
97
+ });
98
+
99
+ it('honors limit slice', () => {
100
+ const rows = mapSearchArticles(sampleBody, 1);
101
+ expect(rows).toHaveLength(1);
102
+ });
103
+
104
+ it('falls back to top-level articles array', () => {
105
+ const rows = mapSearchArticles({ articles: [{ title: 'X', canonical_url: '/x/' }] }, 5);
106
+ expect(rows).toHaveLength(1);
107
+ expect(rows[0].url).toBe('https://www.reuters.com/x/');
108
+ });
109
+
110
+ it('returns empty array when no articles', () => {
111
+ expect(mapSearchArticles({}, 5)).toEqual([]);
112
+ });
113
+
114
+ it('drops rows that have no title (no silent empty-title row)', () => {
115
+ const rows = mapSearchArticles({ articles: [{ canonical_url: '/x/' }] }, 5);
116
+ expect(rows).toEqual([]);
117
+ });
118
+
119
+ it('drops rows without url because search.url is the article-detail id', () => {
120
+ const rows = mapSearchArticles({ articles: [{ title: 'No URL' }, { title: 'Has URL', canonical_url: '/world/has-url/' }] }, 5);
121
+ expect(rows).toEqual([
122
+ {
123
+ rank: 2,
124
+ title: 'Has URL',
125
+ date: null,
126
+ section: null,
127
+ section_path: null,
128
+ authors: null,
129
+ url: 'https://www.reuters.com/world/has-url/',
130
+ },
131
+ ]);
132
+ });
133
+ });
134
+
135
+ describe('reuters mapArticleDetail', () => {
136
+ it('projects fusion globalContent + body text', () => {
137
+ const detail = mapArticleDetail(
138
+ {
139
+ title: 'Headline',
140
+ display_date: '2026-05-06T14:30:00.000Z',
141
+ taxonomy: { section: { name: 'Tech', path: '/tech' } },
142
+ authors: [{ name: 'Alice' }],
143
+ description: { basic: 'A summary.' },
144
+ word_count: 312,
145
+ canonical_url: '/tech/headline-2026-05-06/',
146
+ },
147
+ 'Para 1\n\nPara 2',
148
+ );
149
+ expect(detail).toEqual({
150
+ title: 'Headline',
151
+ date: '2026-05-06',
152
+ section: 'Tech',
153
+ section_path: '/tech',
154
+ authors: 'Alice',
155
+ description: 'A summary.',
156
+ word_count: 312,
157
+ url: 'https://www.reuters.com/tech/headline-2026-05-06/',
158
+ body: 'Para 1\n\nPara 2',
159
+ });
160
+ });
161
+
162
+ it('returns null when neither article nor body text', () => {
163
+ expect(mapArticleDetail(null, '')).toBeNull();
164
+ });
165
+
166
+ it('uses input URL fallback when fusion metadata has no canonical URL', () => {
167
+ const detail = mapArticleDetail({ title: 'Body-only' }, 'Body text', 'https://www.reuters.com/world/body-only/');
168
+ expect(detail.url).toBe('https://www.reuters.com/world/body-only/');
169
+ });
170
+ });
171
+
172
+ describe('reuters search command (registry-level)', () => {
173
+ const cmd = getRegistry().get('reuters/search');
174
+
175
+ it('declares Strategy.COOKIE + access:read', () => {
176
+ expect(cmd.access).toBe('read');
177
+ expect(String(cmd.strategy)).toContain('cookie');
178
+ });
179
+
180
+ it('rejects --limit out-of-range before browser navigation', async () => {
181
+ const page = makePage(null);
182
+ await expect(cmd.func(page, { query: 'x', limit: 0 })).rejects.toBeInstanceOf(ArgumentError);
183
+ await expect(cmd.func(page, { query: 'x', limit: 41 })).rejects.toBeInstanceOf(ArgumentError);
184
+ expect(page.goto).not.toHaveBeenCalled();
185
+ });
186
+
187
+ it('rejects empty query as ArgumentError', async () => {
188
+ const page = makePage(null);
189
+ await expect(cmd.func(page, { query: ' ', limit: 5 })).rejects.toBeInstanceOf(ArgumentError);
190
+ });
191
+
192
+ it('throws CommandExecutionError when in-page fetch errored', async () => {
193
+ const page = makePage({ ok: false, status: 0, body: null, error: 'NetworkError' });
194
+ await expect(cmd.func(page, { query: 'x', limit: 5 })).rejects.toBeInstanceOf(CommandExecutionError);
195
+ });
196
+
197
+ it('throws AuthRequiredError on Reuters auth/challenge status', async () => {
198
+ const page = makePage({ ok: false, status: 403, body: { html: '<captcha/>' } });
199
+ await expect(cmd.func(page, { query: 'x', limit: 5 })).rejects.toBeInstanceOf(AuthRequiredError);
200
+ });
201
+
202
+ it('throws CommandExecutionError on non-auth upstream failure', async () => {
203
+ const page = makePage({ ok: false, status: 500, statusText: 'Server Error', body: { error: 'upstream' } });
204
+ await expect(cmd.func(page, { query: 'x', limit: 5 })).rejects.toBeInstanceOf(CommandExecutionError);
205
+ });
206
+
207
+ it('throws AuthRequiredError when 200 body is Reuters challenge HTML', async () => {
208
+ const page = makePage({ ok: true, status: 200, body: null, textPreview: 'DataDome verify you are human', parseError: 'Unexpected token <' });
209
+ await expect(cmd.func(page, { query: 'x', limit: 5 })).rejects.toBeInstanceOf(AuthRequiredError);
210
+ });
211
+
212
+ it('throws CommandExecutionError when 200 but body is null (captcha HTML)', async () => {
213
+ const page = makePage({ ok: true, status: 200, body: null });
214
+ await expect(cmd.func(page, { query: 'x', limit: 5 })).rejects.toBeInstanceOf(CommandExecutionError);
215
+ });
216
+
217
+ it('throws EmptyResultError when API returned no articles', async () => {
218
+ const page = makePage({ ok: true, status: 200, body: { result: { articles: [] } } });
219
+ await expect(cmd.func(page, { query: 'x', limit: 5 })).rejects.toBeInstanceOf(EmptyResultError);
220
+ });
221
+
222
+ it('returns mapped rows on success', async () => {
223
+ const page = makePage({
224
+ ok: true,
225
+ status: 200,
226
+ body: {
227
+ result: {
228
+ articles: [
229
+ { title: 'A', display_date: '2026-05-06T00:00:00Z', canonical_url: '/a/' },
230
+ ],
231
+ },
232
+ },
233
+ });
234
+ const rows = await cmd.func(page, { query: 'tariff', limit: 5 });
235
+ expect(rows).toHaveLength(1);
236
+ expect(rows[0].title).toBe('A');
237
+ });
238
+ });
239
+
240
+ describe('reuters article-detail command (registry-level)', () => {
241
+ const cmd = getRegistry().get('reuters/article-detail');
242
+
243
+ it('rejects empty url', async () => {
244
+ const page = makePage(null);
245
+ await expect(cmd.func(page, { url: ' ' })).rejects.toBeInstanceOf(ArgumentError);
246
+ });
247
+
248
+ it('rejects non-reuters URL', async () => {
249
+ const page = makePage(null);
250
+ await expect(cmd.func(page, { url: 'https://example.com/article' })).rejects.toThrow('must be on reuters.com');
251
+ });
252
+
253
+ it('throws CommandExecutionError when in-page returns nothing', async () => {
254
+ const page = makePage(null);
255
+ await expect(cmd.func(page, { url: 'https://www.reuters.com/world/x/' })).rejects.toBeInstanceOf(CommandExecutionError);
256
+ });
257
+
258
+ it('throws CommandExecutionError when in-page errored', async () => {
259
+ const page = makePage({ ok: false, error: 'boom' });
260
+ await expect(cmd.func(page, { url: 'https://www.reuters.com/world/x/' })).rejects.toBeInstanceOf(CommandExecutionError);
261
+ });
262
+
263
+ it('throws AuthRequiredError when page reports a paywall/challenge', async () => {
264
+ const page = makePage({ ok: true, authRequired: true, body: { article: null, bodyText: null } });
265
+ await expect(cmd.func(page, { url: 'https://www.reuters.com/world/x/' })).rejects.toBeInstanceOf(AuthRequiredError);
266
+ });
267
+
268
+ it('throws EmptyResultError when page rendered no article body', async () => {
269
+ const page = makePage({ ok: true, body: { article: null, bodyText: null } });
270
+ await expect(cmd.func(page, { url: 'https://www.reuters.com/world/x/' })).rejects.toBeInstanceOf(EmptyResultError);
271
+ });
272
+
273
+ it('returns single-row detail on success', async () => {
274
+ const page = makePage({
275
+ ok: true,
276
+ body: {
277
+ article: {
278
+ title: 'H',
279
+ display_date: '2026-05-06T00:00:00Z',
280
+ taxonomy: { section: { name: 'World', path: '/world' } },
281
+ canonical_url: '/world/h/',
282
+ },
283
+ bodyText: 'Body here',
284
+ },
285
+ });
286
+ const rows = await cmd.func(page, { url: 'https://www.reuters.com/world/h/' });
287
+ expect(rows).toHaveLength(1);
288
+ expect(rows[0].body).toBe('Body here');
289
+ expect(rows[0].url).toBe('https://www.reuters.com/world/h/');
290
+ });
291
+ });
292
+
293
+ describe('reuters buildArticleDetailScript', () => {
294
+ it('returns a valid JS string referencing fusion-metadata + paragraph selectors', () => {
295
+ const s = buildArticleDetailScript();
296
+ expect(s).toContain('fusion-metadata');
297
+ expect(s).toContain("data-testid^=\"paragraph-\"");
298
+ });
299
+ });
@@ -1,7 +1,13 @@
1
1
  /**
2
- * Reuters news search — API with HTML fallback.
2
+ * Reuters news search — uses the in-page articles-by-search-v2 API.
3
+ *
4
+ * The endpoint sits behind a Datadome anti-bot challenge for direct fetches,
5
+ * so we run inside an authenticated reuters.com tab via Strategy.COOKIE.
3
6
  */
7
+ import { ArgumentError, AuthRequiredError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
4
8
  import { cli, Strategy } from '@jackwener/opencli/registry';
9
+ import { buildSearchScript, isAuthStatus, looksAuthWallText, mapSearchArticles, parseLimit } from './utils.js';
10
+
5
11
  cli({
6
12
  site: 'reuters',
7
13
  name: 'search',
@@ -11,42 +17,47 @@ cli({
11
17
  strategy: Strategy.COOKIE,
12
18
  args: [
13
19
  { name: 'query', required: true, positional: true, help: 'Search query' },
14
- { name: 'limit', type: 'int', default: 10, help: 'Number of results (max 40)' },
20
+ { name: 'limit', type: 'int', default: 10, help: 'Number of results (1-40)' },
15
21
  ],
16
- columns: ['rank', 'title', 'date', 'section', 'url'],
22
+ columns: ['rank', 'title', 'date', 'section', 'section_path', 'authors', 'url'],
17
23
  func: async (page, kwargs) => {
18
- const count = Math.min(kwargs.limit || 10, 40);
24
+ const limit = parseLimit(kwargs.limit);
25
+ const query = String(kwargs.query || '').trim();
26
+ if (!query) {
27
+ throw new ArgumentError('Search query cannot be empty', 'Provide a non-empty keyword');
28
+ }
19
29
  await page.goto('https://www.reuters.com');
20
30
  await page.wait(2);
21
- const data = await page.evaluate(`
22
- (async () => {
23
- const count = ${count};
24
- const apiQuery = JSON.stringify({
25
- keyword: ${JSON.stringify(kwargs.query)},
26
- offset: 0, orderby: 'display_date:desc', size: count, website: 'reuters'
27
- });
28
- const apiUrl = 'https://www.reuters.com/pf/api/v3/content/fetch/articles-by-search-v2?query=' + encodeURIComponent(apiQuery);
29
- try {
30
- const resp = await fetch(apiUrl, {credentials: 'include'});
31
- if (resp.ok) {
32
- const data = await resp.json();
33
- const articles = data.result?.articles || data.articles || [];
34
- if (articles.length > 0) {
35
- return articles.slice(0, count).map((a, i) => ({
36
- rank: i + 1,
37
- title: a.title || a.headlines?.basic || '',
38
- date: (a.display_date || a.published_time || '').split('T')[0],
39
- section: a.taxonomy?.section?.name || '',
40
- url: a.canonical_url ? 'https://www.reuters.com' + a.canonical_url : '',
41
- }));
42
- }
43
- }
44
- } catch(e) {}
45
- return {error: 'Reuters API unavailable'};
46
- })()
47
- `);
48
- if (!Array.isArray(data))
49
- return [];
50
- return data;
31
+ const result = await page.evaluate(buildSearchScript(query, limit));
32
+ if (result?.error) {
33
+ throw new CommandExecutionError(`Reuters search failed inside the page: ${result.error}`);
34
+ }
35
+ if (!result || typeof result !== 'object') {
36
+ throw new CommandExecutionError('Reuters search API returned an unreadable response');
37
+ }
38
+ if (isAuthStatus(result.status) || looksAuthWallText(result.textPreview)) {
39
+ throw new AuthRequiredError(
40
+ 'www.reuters.com',
41
+ `Reuters search requires an accessible Reuters browser session or completed human verification${result.status ? ` (HTTP ${result.status})` : ''}`,
42
+ );
43
+ }
44
+ if (result.ok !== true) {
45
+ const status = Number.isFinite(result.status) && result.status > 0
46
+ ? `HTTP ${result.status}${result.statusText ? ` ${result.statusText}` : ''}`
47
+ : 'no upstream response';
48
+ throw new CommandExecutionError(`Reuters search API failed (${status})`);
49
+ }
50
+ if (!result.body) {
51
+ const detail = result.parseError ? `: ${result.parseError}` : '';
52
+ throw new CommandExecutionError(
53
+ `Reuters search returned a non-JSON body${detail}`,
54
+ 'Open www.reuters.com in your browser and clear any challenge before retrying.',
55
+ );
56
+ }
57
+ const rows = mapSearchArticles(result.body, limit);
58
+ if (!rows.length) {
59
+ throw new EmptyResultError('reuters search', `No articles matched "${query}". Try broadening the query.`);
60
+ }
61
+ return rows;
51
62
  },
52
63
  });