@zenalexa/unicli 0.219.0 → 0.220.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 (430) hide show
  1. package/AGENTS.md +9 -9
  2. package/README.md +55 -249
  3. package/README.zh-CN.md +55 -249
  4. package/dist/adapters/1point3acres/forum.d.ts +58 -0
  5. package/dist/adapters/1point3acres/forum.d.ts.map +1 -0
  6. package/dist/adapters/1point3acres/forum.js +708 -0
  7. package/dist/adapters/1point3acres/forum.js.map +1 -0
  8. package/dist/adapters/aibase/news.d.ts +21 -0
  9. package/dist/adapters/aibase/news.d.ts.map +1 -0
  10. package/dist/adapters/aibase/news.js +96 -0
  11. package/dist/adapters/aibase/news.js.map +1 -0
  12. package/dist/adapters/arxiv/papers.d.ts +27 -0
  13. package/dist/adapters/arxiv/papers.d.ts.map +1 -0
  14. package/dist/adapters/arxiv/papers.js +193 -0
  15. package/dist/adapters/arxiv/papers.js.map +1 -0
  16. package/dist/adapters/bbc/topic.d.ts +24 -0
  17. package/dist/adapters/bbc/topic.d.ts.map +1 -0
  18. package/dist/adapters/bbc/topic.js +122 -0
  19. package/dist/adapters/bbc/topic.js.map +1 -0
  20. package/dist/adapters/chatgpt/web.d.ts +24 -0
  21. package/dist/adapters/chatgpt/web.d.ts.map +1 -0
  22. package/dist/adapters/chatgpt/web.js +242 -0
  23. package/dist/adapters/chatgpt/web.js.map +1 -0
  24. package/dist/adapters/claude/web.d.ts +24 -0
  25. package/dist/adapters/claude/web.d.ts.map +1 -0
  26. package/dist/adapters/claude/web.js +575 -0
  27. package/dist/adapters/claude/web.js.map +1 -0
  28. package/dist/adapters/codex/projects.d.ts +27 -0
  29. package/dist/adapters/codex/projects.d.ts.map +1 -0
  30. package/dist/adapters/codex/projects.js +147 -0
  31. package/dist/adapters/codex/projects.js.map +1 -0
  32. package/dist/adapters/coingecko/markets.d.ts +19 -0
  33. package/dist/adapters/coingecko/markets.d.ts.map +1 -0
  34. package/dist/adapters/coingecko/markets.js +474 -0
  35. package/dist/adapters/coingecko/markets.js.map +1 -0
  36. package/dist/adapters/coupang/product.d.ts +27 -0
  37. package/dist/adapters/coupang/product.d.ts.map +1 -0
  38. package/dist/adapters/coupang/product.js +211 -0
  39. package/dist/adapters/coupang/product.js.map +1 -0
  40. package/dist/adapters/crates/registry.d.ts +44 -0
  41. package/dist/adapters/crates/registry.d.ts.map +1 -0
  42. package/dist/adapters/crates/registry.js +186 -0
  43. package/dist/adapters/crates/registry.js.map +1 -0
  44. package/dist/adapters/ctrip/travel.d.ts +83 -0
  45. package/dist/adapters/ctrip/travel.d.ts.map +1 -0
  46. package/dist/adapters/ctrip/travel.js +630 -0
  47. package/dist/adapters/ctrip/travel.js.map +1 -0
  48. package/dist/adapters/dblp/publications.d.ts +41 -0
  49. package/dist/adapters/dblp/publications.d.ts.map +1 -0
  50. package/dist/adapters/dblp/publications.js +409 -0
  51. package/dist/adapters/dblp/publications.js.map +1 -0
  52. package/dist/adapters/deepseek/web.d.ts +1 -1
  53. package/dist/adapters/deepseek/web.d.ts.map +1 -1
  54. package/dist/adapters/deepseek/web.js +66 -1
  55. package/dist/adapters/deepseek/web.js.map +1 -1
  56. package/dist/adapters/defillama/protocols.d.ts +13 -0
  57. package/dist/adapters/defillama/protocols.d.ts.map +1 -0
  58. package/dist/adapters/defillama/protocols.js +218 -0
  59. package/dist/adapters/defillama/protocols.js.map +1 -0
  60. package/dist/adapters/devto/read.d.ts +26 -0
  61. package/dist/adapters/devto/read.d.ts.map +1 -0
  62. package/dist/adapters/devto/read.js +110 -0
  63. package/dist/adapters/devto/read.js.map +1 -0
  64. package/dist/adapters/dianping/shop.d.ts +38 -0
  65. package/dist/adapters/dianping/shop.d.ts.map +1 -0
  66. package/dist/adapters/dianping/shop.js +194 -0
  67. package/dist/adapters/dianping/shop.js.map +1 -0
  68. package/dist/adapters/dockerhub/registry.d.ts +36 -0
  69. package/dist/adapters/dockerhub/registry.d.ts.map +1 -0
  70. package/dist/adapters/dockerhub/registry.js +172 -0
  71. package/dist/adapters/dockerhub/registry.js.map +1 -0
  72. package/dist/adapters/endoflife/product.d.ts +11 -0
  73. package/dist/adapters/endoflife/product.d.ts.map +1 -0
  74. package/dist/adapters/endoflife/product.js +113 -0
  75. package/dist/adapters/endoflife/product.js.map +1 -0
  76. package/dist/adapters/facebook/marketplace-extra.d.ts +9 -0
  77. package/dist/adapters/facebook/marketplace-extra.d.ts.map +1 -0
  78. package/dist/adapters/facebook/marketplace-extra.js +170 -0
  79. package/dist/adapters/facebook/marketplace-extra.js.map +1 -0
  80. package/dist/adapters/flathub/apps.d.ts +17 -0
  81. package/dist/adapters/flathub/apps.d.ts.map +1 -0
  82. package/dist/adapters/flathub/apps.js +220 -0
  83. package/dist/adapters/flathub/apps.js.map +1 -0
  84. package/dist/adapters/goproxy/module.d.ts +24 -0
  85. package/dist/adapters/goproxy/module.d.ts.map +1 -0
  86. package/dist/adapters/goproxy/module.js +221 -0
  87. package/dist/adapters/goproxy/module.js.map +1 -0
  88. package/dist/adapters/grok/web.d.ts +29 -0
  89. package/dist/adapters/grok/web.d.ts.map +1 -0
  90. package/dist/adapters/grok/web.js +553 -0
  91. package/dist/adapters/grok/web.js.map +1 -0
  92. package/dist/adapters/hackernews/read.d.ts +31 -0
  93. package/dist/adapters/hackernews/read.d.ts.map +1 -0
  94. package/dist/adapters/hackernews/read.js +201 -0
  95. package/dist/adapters/hackernews/read.js.map +1 -0
  96. package/dist/adapters/hf/paper.d.ts +22 -0
  97. package/dist/adapters/hf/paper.d.ts.map +1 -0
  98. package/dist/adapters/hf/paper.js +112 -0
  99. package/dist/adapters/hf/paper.js.map +1 -0
  100. package/dist/adapters/homebrew/packages.d.ts +52 -0
  101. package/dist/adapters/homebrew/packages.d.ts.map +1 -0
  102. package/dist/adapters/homebrew/packages.js +240 -0
  103. package/dist/adapters/homebrew/packages.js.map +1 -0
  104. package/dist/adapters/indeed/jobs.d.ts +38 -0
  105. package/dist/adapters/indeed/jobs.d.ts.map +1 -0
  106. package/dist/adapters/indeed/jobs.js +300 -0
  107. package/dist/adapters/indeed/jobs.js.map +1 -0
  108. package/dist/adapters/instagram/collections.d.ts +9 -0
  109. package/dist/adapters/instagram/collections.d.ts.map +1 -0
  110. package/dist/adapters/instagram/collections.js +174 -0
  111. package/dist/adapters/instagram/collections.js.map +1 -0
  112. package/dist/adapters/lichess/players.d.ts +46 -0
  113. package/dist/adapters/lichess/players.d.ts.map +1 -0
  114. package/dist/adapters/lichess/players.js +221 -0
  115. package/dist/adapters/lichess/players.js.map +1 -0
  116. package/dist/adapters/lobsters/read-domain.d.ts +35 -0
  117. package/dist/adapters/lobsters/read-domain.d.ts.map +1 -0
  118. package/dist/adapters/lobsters/read-domain.js +306 -0
  119. package/dist/adapters/lobsters/read-domain.js.map +1 -0
  120. package/dist/adapters/maven/artifact.d.ts +30 -0
  121. package/dist/adapters/maven/artifact.d.ts.map +1 -0
  122. package/dist/adapters/maven/artifact.js +121 -0
  123. package/dist/adapters/maven/artifact.js.map +1 -0
  124. package/dist/adapters/mdn/search.d.ts +11 -0
  125. package/dist/adapters/mdn/search.d.ts.map +1 -0
  126. package/dist/adapters/mdn/search.js +115 -0
  127. package/dist/adapters/mdn/search.js.map +1 -0
  128. package/dist/adapters/medium/tag.d.ts +15 -0
  129. package/dist/adapters/medium/tag.d.ts.map +1 -0
  130. package/dist/adapters/medium/tag.js +148 -0
  131. package/dist/adapters/medium/tag.js.map +1 -0
  132. package/dist/adapters/npm/package.d.ts +32 -0
  133. package/dist/adapters/npm/package.d.ts.map +1 -0
  134. package/dist/adapters/npm/package.js +141 -0
  135. package/dist/adapters/npm/package.js.map +1 -0
  136. package/dist/adapters/nuget/package.d.ts +34 -0
  137. package/dist/adapters/nuget/package.d.ts.map +1 -0
  138. package/dist/adapters/nuget/package.js +135 -0
  139. package/dist/adapters/nuget/package.js.map +1 -0
  140. package/dist/adapters/nvd/cve.d.ts +42 -0
  141. package/dist/adapters/nvd/cve.d.ts.map +1 -0
  142. package/dist/adapters/nvd/cve.js +132 -0
  143. package/dist/adapters/nvd/cve.js.map +1 -0
  144. package/dist/adapters/oeis/sequences.d.ts +14 -0
  145. package/dist/adapters/oeis/sequences.d.ts.map +1 -0
  146. package/dist/adapters/oeis/sequences.js +219 -0
  147. package/dist/adapters/oeis/sequences.js.map +1 -0
  148. package/dist/adapters/openalex/works.d.ts +43 -0
  149. package/dist/adapters/openalex/works.d.ts.map +1 -0
  150. package/dist/adapters/openalex/works.js +267 -0
  151. package/dist/adapters/openalex/works.js.map +1 -0
  152. package/dist/adapters/openfda/records.d.ts +18 -0
  153. package/dist/adapters/openfda/records.d.ts.map +1 -0
  154. package/dist/adapters/openfda/records.js +209 -0
  155. package/dist/adapters/openfda/records.js.map +1 -0
  156. package/dist/adapters/openreview/papers.d.ts +34 -0
  157. package/dist/adapters/openreview/papers.d.ts.map +1 -0
  158. package/dist/adapters/openreview/papers.js +463 -0
  159. package/dist/adapters/openreview/papers.js.map +1 -0
  160. package/dist/adapters/osv/security.d.ts +36 -0
  161. package/dist/adapters/osv/security.d.ts.map +1 -0
  162. package/dist/adapters/osv/security.js +247 -0
  163. package/dist/adapters/osv/security.js.map +1 -0
  164. package/dist/adapters/packagist/package.d.ts +31 -0
  165. package/dist/adapters/packagist/package.d.ts.map +1 -0
  166. package/dist/adapters/packagist/package.js +108 -0
  167. package/dist/adapters/packagist/package.js.map +1 -0
  168. package/dist/adapters/pubmed/articles.d.ts +31 -0
  169. package/dist/adapters/pubmed/articles.d.ts.map +1 -0
  170. package/dist/adapters/pubmed/articles.js +385 -0
  171. package/dist/adapters/pubmed/articles.js.map +1 -0
  172. package/dist/adapters/pypi/package.d.ts +38 -0
  173. package/dist/adapters/pypi/package.d.ts.map +1 -0
  174. package/dist/adapters/pypi/package.js +235 -0
  175. package/dist/adapters/pypi/package.js.map +1 -0
  176. package/dist/adapters/qwen/web.d.ts +26 -0
  177. package/dist/adapters/qwen/web.d.ts.map +1 -0
  178. package/dist/adapters/qwen/web.js +672 -0
  179. package/dist/adapters/qwen/web.js.map +1 -0
  180. package/dist/adapters/reddit/account.d.ts +12 -0
  181. package/dist/adapters/reddit/account.d.ts.map +1 -0
  182. package/dist/adapters/reddit/account.js +409 -0
  183. package/dist/adapters/reddit/account.js.map +1 -0
  184. package/dist/adapters/rednote/web.d.ts +30 -0
  185. package/dist/adapters/rednote/web.d.ts.map +1 -0
  186. package/dist/adapters/rednote/web.js +858 -0
  187. package/dist/adapters/rednote/web.js.map +1 -0
  188. package/dist/adapters/rest-countries/countries.d.ts +14 -0
  189. package/dist/adapters/rest-countries/countries.d.ts.map +1 -0
  190. package/dist/adapters/rest-countries/countries.js +231 -0
  191. package/dist/adapters/rest-countries/countries.js.map +1 -0
  192. package/dist/adapters/reuters/article-detail.d.ts +37 -0
  193. package/dist/adapters/reuters/article-detail.d.ts.map +1 -0
  194. package/dist/adapters/reuters/article-detail.js +139 -0
  195. package/dist/adapters/reuters/article-detail.js.map +1 -0
  196. package/dist/adapters/rfc/rfc.d.ts +11 -0
  197. package/dist/adapters/rfc/rfc.d.ts.map +1 -0
  198. package/dist/adapters/rfc/rfc.js +121 -0
  199. package/dist/adapters/rfc/rfc.js.map +1 -0
  200. package/dist/adapters/rubygems/gem.d.ts +26 -0
  201. package/dist/adapters/rubygems/gem.d.ts.map +1 -0
  202. package/dist/adapters/rubygems/gem.js +96 -0
  203. package/dist/adapters/rubygems/gem.js.map +1 -0
  204. package/dist/adapters/stackoverflow/questions.d.ts +79 -0
  205. package/dist/adapters/stackoverflow/questions.d.ts.map +1 -0
  206. package/dist/adapters/stackoverflow/questions.js +504 -0
  207. package/dist/adapters/stackoverflow/questions.js.map +1 -0
  208. package/dist/adapters/steam/app.d.ts +39 -0
  209. package/dist/adapters/steam/app.d.ts.map +1 -0
  210. package/dist/adapters/steam/app.js +165 -0
  211. package/dist/adapters/steam/app.js.map +1 -0
  212. package/dist/adapters/tiktok/creator-videos.d.ts +52 -0
  213. package/dist/adapters/tiktok/creator-videos.d.ts.map +1 -0
  214. package/dist/adapters/tiktok/creator-videos.js +267 -0
  215. package/dist/adapters/tiktok/creator-videos.js.map +1 -0
  216. package/dist/adapters/tvmaze/shows.d.ts +13 -0
  217. package/dist/adapters/tvmaze/shows.d.ts.map +1 -0
  218. package/dist/adapters/tvmaze/shows.js +240 -0
  219. package/dist/adapters/tvmaze/shows.js.map +1 -0
  220. package/dist/adapters/twitter/bookmark-folders.d.ts +33 -0
  221. package/dist/adapters/twitter/bookmark-folders.d.ts.map +1 -0
  222. package/dist/adapters/twitter/bookmark-folders.js +290 -0
  223. package/dist/adapters/twitter/bookmark-folders.js.map +1 -0
  224. package/dist/adapters/twitter/quote.d.ts +18 -0
  225. package/dist/adapters/twitter/quote.d.ts.map +1 -0
  226. package/dist/adapters/twitter/quote.js +285 -0
  227. package/dist/adapters/twitter/quote.js.map +1 -0
  228. package/dist/adapters/twitter/tweet-actions.d.ts +12 -0
  229. package/dist/adapters/twitter/tweet-actions.d.ts.map +1 -0
  230. package/dist/adapters/twitter/tweet-actions.js +145 -0
  231. package/dist/adapters/twitter/tweet-actions.js.map +1 -0
  232. package/dist/adapters/twitter/tweet-url.d.ts +15 -0
  233. package/dist/adapters/twitter/tweet-url.d.ts.map +1 -0
  234. package/dist/adapters/twitter/tweet-url.js +57 -0
  235. package/dist/adapters/twitter/tweet-url.js.map +1 -0
  236. package/dist/adapters/uisdc/news.d.ts +22 -0
  237. package/dist/adapters/uisdc/news.d.ts.map +1 -0
  238. package/dist/adapters/uisdc/news.js +91 -0
  239. package/dist/adapters/uisdc/news.js.map +1 -0
  240. package/dist/adapters/weibo/favorites-publish.d.ts +28 -0
  241. package/dist/adapters/weibo/favorites-publish.d.ts.map +1 -0
  242. package/dist/adapters/weibo/favorites-publish.js +356 -0
  243. package/dist/adapters/weibo/favorites-publish.js.map +1 -0
  244. package/dist/adapters/wikidata/entities.d.ts +15 -0
  245. package/dist/adapters/wikidata/entities.d.ts.map +1 -0
  246. package/dist/adapters/wikidata/entities.js +219 -0
  247. package/dist/adapters/wikidata/entities.js.map +1 -0
  248. package/dist/adapters/wikipedia/page.d.ts +21 -0
  249. package/dist/adapters/wikipedia/page.d.ts.map +1 -0
  250. package/dist/adapters/wikipedia/page.js +116 -0
  251. package/dist/adapters/wikipedia/page.js.map +1 -0
  252. package/dist/adapters/wttr/weather.d.ts +12 -0
  253. package/dist/adapters/wttr/weather.d.ts.map +1 -0
  254. package/dist/adapters/wttr/weather.js +207 -0
  255. package/dist/adapters/wttr/weather.js.map +1 -0
  256. package/dist/adapters/xianyu/publish.d.ts +31 -0
  257. package/dist/adapters/xianyu/publish.d.ts.map +1 -0
  258. package/dist/adapters/xianyu/publish.js +349 -0
  259. package/dist/adapters/xianyu/publish.js.map +1 -0
  260. package/dist/adapters/xiaohongshu/user-helpers.d.ts +2 -2
  261. package/dist/adapters/xiaohongshu/user-helpers.d.ts.map +1 -1
  262. package/dist/adapters/xiaohongshu/user-helpers.js +5 -4
  263. package/dist/adapters/xiaohongshu/user-helpers.js.map +1 -1
  264. package/dist/adapters/yuanbao/web.d.ts +27 -0
  265. package/dist/adapters/yuanbao/web.d.ts.map +1 -0
  266. package/dist/adapters/yuanbao/web.js +365 -0
  267. package/dist/adapters/yuanbao/web.js.map +1 -0
  268. package/dist/adapters/zhihu/collection.d.ts +33 -0
  269. package/dist/adapters/zhihu/collection.d.ts.map +1 -0
  270. package/dist/adapters/zhihu/collection.js +185 -0
  271. package/dist/adapters/zhihu/collection.js.map +1 -0
  272. package/dist/adapters/zlibrary/web.d.ts +19 -0
  273. package/dist/adapters/zlibrary/web.d.ts.map +1 -0
  274. package/dist/adapters/zlibrary/web.js +153 -0
  275. package/dist/adapters/zlibrary/web.js.map +1 -0
  276. package/dist/browser/daemon-client.js +2 -2
  277. package/dist/browser/daemon-client.js.map +1 -1
  278. package/dist/discovery/macos-dynamic.d.ts.map +1 -1
  279. package/dist/discovery/macos-dynamic.js +17 -3
  280. package/dist/discovery/macos-dynamic.js.map +1 -1
  281. package/dist/manifest-compact.txt +10 -10
  282. package/dist/manifest-search.json +1 -1
  283. package/dist/manifest.json +5130 -112
  284. package/dist/transport/refs.d.ts +5 -3
  285. package/dist/transport/refs.d.ts.map +1 -1
  286. package/dist/transport/refs.js +8 -1
  287. package/dist/transport/refs.js.map +1 -1
  288. package/dist/transport/sidecar-binary.d.ts +7 -0
  289. package/dist/transport/sidecar-binary.d.ts.map +1 -1
  290. package/dist/transport/sidecar-binary.js +28 -8
  291. package/dist/transport/sidecar-binary.js.map +1 -1
  292. package/package.json +4 -3
  293. package/server.json +3 -3
  294. package/skills/unicli/SKILL.md +1 -1
  295. package/skills/unicli-claude-code/SKILL.md +1 -1
  296. package/skills/unicli-hermes/SKILL.md +1 -1
  297. package/src/adapters/1point3acres/forum.test.ts +300 -0
  298. package/src/adapters/1point3acres/forum.ts +852 -0
  299. package/src/adapters/aibase/news.test.ts +42 -0
  300. package/src/adapters/aibase/news.ts +118 -0
  301. package/src/adapters/arxiv/papers.test.ts +59 -0
  302. package/src/adapters/arxiv/papers.ts +226 -0
  303. package/src/adapters/bbc/topic.test.ts +52 -0
  304. package/src/adapters/bbc/topic.ts +149 -0
  305. package/src/adapters/chatgpt/web.test.ts +121 -0
  306. package/src/adapters/chatgpt/web.ts +286 -0
  307. package/src/adapters/claude/web.test.ts +206 -0
  308. package/src/adapters/claude/web.ts +684 -0
  309. package/src/adapters/codex/projects.test.ts +77 -0
  310. package/src/adapters/codex/projects.ts +178 -0
  311. package/src/adapters/coingecko/markets.test.ts +156 -0
  312. package/src/adapters/coingecko/markets.ts +574 -0
  313. package/src/adapters/coupang/product.test.ts +111 -0
  314. package/src/adapters/coupang/product.ts +256 -0
  315. package/src/adapters/crates/registry.test.ts +89 -0
  316. package/src/adapters/crates/registry.ts +247 -0
  317. package/src/adapters/ctrip/travel.test.ts +359 -0
  318. package/src/adapters/ctrip/travel.ts +792 -0
  319. package/src/adapters/dblp/publications.test.ts +123 -0
  320. package/src/adapters/dblp/publications.ts +494 -0
  321. package/src/adapters/deepseek/web.test.ts +69 -0
  322. package/src/adapters/deepseek/web.ts +78 -1
  323. package/src/adapters/defillama/protocols.test.ts +75 -0
  324. package/src/adapters/defillama/protocols.ts +253 -0
  325. package/src/adapters/devto/read.test.ts +49 -0
  326. package/src/adapters/devto/read.ts +145 -0
  327. package/src/adapters/dianping/shop.test.ts +134 -0
  328. package/src/adapters/dianping/shop.ts +261 -0
  329. package/src/adapters/dockerhub/registry.test.ts +97 -0
  330. package/src/adapters/dockerhub/registry.ts +223 -0
  331. package/src/adapters/endoflife/product.test.ts +50 -0
  332. package/src/adapters/endoflife/product.ts +128 -0
  333. package/src/adapters/facebook/marketplace-extra.test.ts +132 -0
  334. package/src/adapters/facebook/marketplace-extra.ts +213 -0
  335. package/src/adapters/flathub/apps.test.ts +85 -0
  336. package/src/adapters/flathub/apps.ts +254 -0
  337. package/src/adapters/goproxy/module.test.ts +72 -0
  338. package/src/adapters/goproxy/module.ts +258 -0
  339. package/src/adapters/grok/web.test.ts +181 -0
  340. package/src/adapters/grok/web.ts +640 -0
  341. package/src/adapters/hackernews/read.test.ts +68 -0
  342. package/src/adapters/hackernews/read.ts +265 -0
  343. package/src/adapters/hf/paper.test.ts +48 -0
  344. package/src/adapters/hf/paper.ts +138 -0
  345. package/src/adapters/homebrew/packages.test.ts +109 -0
  346. package/src/adapters/homebrew/packages.ts +304 -0
  347. package/src/adapters/indeed/jobs.test.ts +230 -0
  348. package/src/adapters/indeed/jobs.ts +375 -0
  349. package/src/adapters/instagram/collections.test.ts +94 -0
  350. package/src/adapters/instagram/collections.ts +206 -0
  351. package/src/adapters/lichess/players.test.ts +99 -0
  352. package/src/adapters/lichess/players.ts +277 -0
  353. package/src/adapters/lobsters/read-domain.test.ts +121 -0
  354. package/src/adapters/lobsters/read-domain.ts +400 -0
  355. package/src/adapters/maven/artifact.test.ts +67 -0
  356. package/src/adapters/maven/artifact.ts +155 -0
  357. package/src/adapters/mdn/search.test.ts +39 -0
  358. package/src/adapters/mdn/search.ts +133 -0
  359. package/src/adapters/medium/tag.test.ts +64 -0
  360. package/src/adapters/medium/tag.ts +164 -0
  361. package/src/adapters/npm/package.test.ts +53 -0
  362. package/src/adapters/npm/package.ts +177 -0
  363. package/src/adapters/nuget/package.test.ts +102 -0
  364. package/src/adapters/nuget/package.ts +193 -0
  365. package/src/adapters/nvd/cve.test.ts +66 -0
  366. package/src/adapters/nvd/cve.ts +182 -0
  367. package/src/adapters/oeis/sequences.test.ts +71 -0
  368. package/src/adapters/oeis/sequences.ts +234 -0
  369. package/src/adapters/openalex/works.test.ts +99 -0
  370. package/src/adapters/openalex/works.ts +319 -0
  371. package/src/adapters/openfda/records.test.ts +90 -0
  372. package/src/adapters/openfda/records.ts +239 -0
  373. package/src/adapters/openreview/papers.test.ts +139 -0
  374. package/src/adapters/openreview/papers.ts +560 -0
  375. package/src/adapters/osv/security.test.ts +91 -0
  376. package/src/adapters/osv/security.ts +298 -0
  377. package/src/adapters/packagist/package.test.ts +62 -0
  378. package/src/adapters/packagist/package.ts +146 -0
  379. package/src/adapters/pubmed/articles.test.ts +96 -0
  380. package/src/adapters/pubmed/articles.ts +497 -0
  381. package/src/adapters/pypi/package.test.ts +131 -0
  382. package/src/adapters/pypi/package.ts +297 -0
  383. package/src/adapters/qwen/web.test.ts +176 -0
  384. package/src/adapters/qwen/web.ts +758 -0
  385. package/src/adapters/reddit/account.test.ts +56 -0
  386. package/src/adapters/reddit/account.ts +493 -0
  387. package/src/adapters/rednote/web.test.ts +354 -0
  388. package/src/adapters/rednote/web.ts +968 -0
  389. package/src/adapters/rest-countries/countries.test.ts +80 -0
  390. package/src/adapters/rest-countries/countries.ts +271 -0
  391. package/src/adapters/reuters/article-detail.test.ts +65 -0
  392. package/src/adapters/reuters/article-detail.ts +186 -0
  393. package/src/adapters/rfc/rfc.test.ts +37 -0
  394. package/src/adapters/rfc/rfc.ts +133 -0
  395. package/src/adapters/rubygems/gem.test.ts +43 -0
  396. package/src/adapters/rubygems/gem.ts +126 -0
  397. package/src/adapters/stackoverflow/questions.test.ts +207 -0
  398. package/src/adapters/stackoverflow/questions.ts +765 -0
  399. package/src/adapters/steam/app.test.ts +68 -0
  400. package/src/adapters/steam/app.ts +218 -0
  401. package/src/adapters/tiktok/creator-videos.test.ts +158 -0
  402. package/src/adapters/tiktok/creator-videos.ts +370 -0
  403. package/src/adapters/tvmaze/shows.test.ts +93 -0
  404. package/src/adapters/tvmaze/shows.ts +271 -0
  405. package/src/adapters/twitter/bookmark-folders.test.ts +164 -0
  406. package/src/adapters/twitter/bookmark-folders.ts +366 -0
  407. package/src/adapters/twitter/quote.test.ts +157 -0
  408. package/src/adapters/twitter/quote.ts +332 -0
  409. package/src/adapters/twitter/tweet-actions.test.ts +51 -0
  410. package/src/adapters/twitter/tweet-actions.ts +187 -0
  411. package/src/adapters/twitter/tweet-url.ts +65 -0
  412. package/src/adapters/uisdc/news.test.ts +46 -0
  413. package/src/adapters/uisdc/news.ts +111 -0
  414. package/src/adapters/weibo/favorites-publish.test.ts +177 -0
  415. package/src/adapters/weibo/favorites-publish.ts +426 -0
  416. package/src/adapters/wikidata/entities.test.ts +103 -0
  417. package/src/adapters/wikidata/entities.ts +253 -0
  418. package/src/adapters/wikipedia/page.test.ts +49 -0
  419. package/src/adapters/wikipedia/page.ts +158 -0
  420. package/src/adapters/wttr/weather.test.ts +99 -0
  421. package/src/adapters/wttr/weather.ts +239 -0
  422. package/src/adapters/xianyu/publish.test.ts +210 -0
  423. package/src/adapters/xianyu/publish.ts +420 -0
  424. package/src/adapters/xiaohongshu/user-helpers.ts +5 -2
  425. package/src/adapters/yuanbao/web.test.ts +144 -0
  426. package/src/adapters/yuanbao/web.ts +435 -0
  427. package/src/adapters/zhihu/collection.test.ts +96 -0
  428. package/src/adapters/zhihu/collection.ts +239 -0
  429. package/src/adapters/zlibrary/web.test.ts +104 -0
  430. package/src/adapters/zlibrary/web.ts +192 -0
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ buildExtractAibaseNewsJs,
4
+ mapAibaseNewsPayload,
5
+ requireNewsLimit,
6
+ } from "./news.js";
7
+
8
+ describe("aibase agent-facing news command", () => {
9
+ it("validates limits and exposes selector extraction code", () => {
10
+ expect(requireNewsLimit(undefined)).toBe(20);
11
+ expect(requireNewsLimit("50")).toBe(50);
12
+ expect(() => requireNewsLimit("0")).toThrow("limit must be");
13
+ expect(buildExtractAibaseNewsJs()).toContain("/zh/daily/");
14
+ });
15
+
16
+ it("maps extracted rows and rejects selector drift", () => {
17
+ expect(
18
+ mapAibaseNewsPayload(
19
+ {
20
+ ok: true,
21
+ rows: [
22
+ { title: " AI news ", url: " https://www.aibase.com/zh/daily/1 " },
23
+ { title: "", url: "https://example.test/empty" },
24
+ ],
25
+ },
26
+ 10,
27
+ ),
28
+ ).toEqual([
29
+ {
30
+ rank: 1,
31
+ title: "AI news",
32
+ url: "https://www.aibase.com/zh/daily/1",
33
+ },
34
+ ]);
35
+ expect(() =>
36
+ mapAibaseNewsPayload({ ok: false, reason: "selector-missing" }, 10),
37
+ ).toThrow("selector drift");
38
+ expect(() => mapAibaseNewsPayload({ ok: true, rows: [] }, 10)).toThrow(
39
+ "no article rows",
40
+ );
41
+ });
42
+ });
@@ -0,0 +1,118 @@
1
+ /**
2
+ * @owner src/adapters/aibase/news.ts
3
+ * @does Register agent-facing AIbase daily-news browser extraction command.
4
+ * @needs Public AIbase daily page, browser runtime, selector-drift error reporting.
5
+ * @feeds surface coverage ledger, AI news discovery, Chinese AI daily brief surface.
6
+ * @breaks AIbase DOM drift or silent empty extraction hides news coverage failures.
7
+ */
8
+
9
+ import { cli, Strategy } from "../../registry.js";
10
+ import type { IPage } from "../../types.js";
11
+
12
+ const AIBASE_DAILY_URL = "https://www.aibase.com/zh/daily";
13
+
14
+ function normalizeText(value: unknown): string {
15
+ return String(value ?? "")
16
+ .replace(/\s+/g, " ")
17
+ .trim();
18
+ }
19
+
20
+ export function requireNewsLimit(value: unknown, fallback = 20): number {
21
+ if (value === undefined || value === null || value === "") return fallback;
22
+ const n = Number(value);
23
+ if (!Number.isInteger(n) || n < 1 || n > 50) {
24
+ throw new Error("limit must be an integer in [1, 50].");
25
+ }
26
+ return n;
27
+ }
28
+
29
+ export function buildExtractAibaseNewsJs(): string {
30
+ return `
31
+ (() => {
32
+ const anchors = Array.from(document.querySelectorAll('.bg-white .grid a[href], a[href*="/zh/daily/"]'))
33
+ .filter((anchor) => {
34
+ const href = anchor.getAttribute('href') || '';
35
+ const text = (anchor.innerText || anchor.textContent || '').trim();
36
+ return text && href && !href.endsWith('/zh/daily') && !href.endsWith('/zh/daily/');
37
+ });
38
+ if (anchors.length === 0) {
39
+ return {
40
+ ok: false,
41
+ reason: 'selector-missing',
42
+ title: document.title || '',
43
+ bodyText: (document.body?.innerText || document.body?.textContent || '').slice(0, 500),
44
+ };
45
+ }
46
+ const seen = new Set();
47
+ const rows = [];
48
+ for (const anchor of anchors) {
49
+ const url = new URL(anchor.getAttribute('href'), location.href).href;
50
+ if (seen.has(url)) continue;
51
+ seen.add(url);
52
+ rows.push({
53
+ title: anchor.innerText || anchor.textContent || '',
54
+ url,
55
+ });
56
+ }
57
+ return { ok: true, rows };
58
+ })()
59
+ `;
60
+ }
61
+
62
+ interface BrowserExtractPayload {
63
+ ok?: unknown;
64
+ reason?: unknown;
65
+ title?: unknown;
66
+ rows?: Array<{ title?: unknown; url?: unknown }>;
67
+ }
68
+
69
+ export function mapAibaseNewsPayload(
70
+ payload: BrowserExtractPayload,
71
+ limit: number,
72
+ ): Array<Record<string, unknown>> {
73
+ if (!payload || typeof payload !== "object") {
74
+ throw new Error("AIbase daily page returned an unreadable payload.");
75
+ }
76
+ if (payload.ok !== true) {
77
+ const reason = normalizeText(payload.reason) || "selector-drift";
78
+ const title = normalizeText(payload.title);
79
+ throw new Error(
80
+ `AIbase daily selector drift: ${reason}${title ? ` (${title})` : ""}`,
81
+ );
82
+ }
83
+ const rows = (Array.isArray(payload.rows) ? payload.rows : [])
84
+ .map((row, index) => ({
85
+ rank: index + 1,
86
+ title: normalizeText(row.title),
87
+ url: normalizeText(row.url),
88
+ }))
89
+ .filter((row) => row.title && row.url);
90
+ if (rows.length === 0) {
91
+ throw new Error(
92
+ "AIbase daily page loaded, but no article rows were extracted.",
93
+ );
94
+ }
95
+ return rows
96
+ .slice(0, limit)
97
+ .map((row, index) => ({ ...row, rank: index + 1 }));
98
+ }
99
+
100
+ cli({
101
+ site: "aibase",
102
+ name: "news",
103
+ description: "AIbase daily AI industry news",
104
+ domain: "www.aibase.com",
105
+ strategy: Strategy.PUBLIC,
106
+ browser: true,
107
+ args: [{ name: "limit", type: "int", default: 20, description: "Max rows" }],
108
+ columns: ["rank", "title", "url"],
109
+ func: async (page, kwargs) => {
110
+ const p = page as IPage;
111
+ const limit = requireNewsLimit(kwargs.limit);
112
+ await p.goto(AIBASE_DAILY_URL, { waitUntil: "load", settleMs: 3000 });
113
+ const payload = (await p.evaluate(
114
+ buildExtractAibaseNewsJs(),
115
+ )) as BrowserExtractPayload;
116
+ return mapAibaseNewsPayload(payload, limit);
117
+ },
118
+ });
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ decodeArxivEntities,
4
+ parseArxivEntries,
5
+ requireArxivAuthor,
6
+ requireArxivCategory,
7
+ requireArxivLimit,
8
+ } from "./papers.js";
9
+
10
+ describe("arxiv agent-facing author and recent commands", () => {
11
+ it("validates author, category, and limit inputs", () => {
12
+ expect(requireArxivAuthor(" Yoshua Bengio ")).toBe("Yoshua Bengio");
13
+ expect(() => requireArxivAuthor("")).toThrow("cannot be empty");
14
+ expect(requireArxivCategory("cs.CL")).toBe("cs.CL");
15
+ expect(requireArxivCategory("q-bio.NC")).toBe("q-bio.NC");
16
+ expect(() => requireArxivCategory("../cs.CL")).toThrow("Invalid arXiv");
17
+ expect(requireArxivLimit(undefined, 20)).toBe(20);
18
+ expect(requireArxivLimit("50", 20)).toBe(50);
19
+ expect(() => requireArxivLimit("51", 20)).toThrow("arxiv limit");
20
+ });
21
+
22
+ it("decodes entities and parses Atom entries", () => {
23
+ expect(decodeArxivEntities("A &amp; B &lt; C")).toBe("A & B < C");
24
+ expect(
25
+ parseArxivEntries(`
26
+ <feed>
27
+ <entry>
28
+ <id>http://arxiv.org/abs/1706.03762v7</id>
29
+ <title> Attention Is All You Need </title>
30
+ <summary> We present transformers. </summary>
31
+ <published>2017-06-12T17:57:34Z</published>
32
+ <updated>2023-08-02T00:00:00Z</updated>
33
+ <author><name>Alice &amp; Bob</name></author>
34
+ <author><name>Carol</name></author>
35
+ <arxiv:primary_category term="cs.CL" />
36
+ <category term="cs.CL" />
37
+ <category term="cs.LG" />
38
+ <arxiv:comment>15 pages</arxiv:comment>
39
+ <link title="pdf" href="https://arxiv.org/pdf/1706.03762" rel="related" />
40
+ </entry>
41
+ </feed>
42
+ `),
43
+ ).toEqual([
44
+ {
45
+ id: "1706.03762",
46
+ title: "Attention Is All You Need",
47
+ authors: "Alice & Bob, Carol",
48
+ abstract: "We present transformers.",
49
+ published: "2017-06-12",
50
+ updated: "2023-08-02",
51
+ primary_category: "cs.CL",
52
+ categories: "cs.CL, cs.LG",
53
+ comment: "15 pages",
54
+ pdf: "https://arxiv.org/pdf/1706.03762",
55
+ url: "https://arxiv.org/abs/1706.03762",
56
+ },
57
+ ]);
58
+ });
59
+ });
@@ -0,0 +1,226 @@
1
+ /**
2
+ * @owner src/adapters/arxiv/papers.ts
3
+ * @does Register agent-facing arXiv author and recent category commands.
4
+ * @needs export.arxiv.org Atom API, category validation, conservative XML parsing.
5
+ * @feeds surface coverage ledger, scholarly search workflow, arXiv category monitoring.
6
+ * @breaks arXiv Atom shape drift, weak category parsing, or silent empty feeds hide paper discovery failures.
7
+ */
8
+
9
+ import { cli, Strategy } from "../../registry.js";
10
+
11
+ const ARXIV_BASE = "https://export.arxiv.org/api/query";
12
+ const CATEGORY_RE = /^[a-z]+(?:-[a-z]+)*(?:\.[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)?$/;
13
+
14
+ interface ArxivEntry {
15
+ id: string;
16
+ title: string;
17
+ authors: string;
18
+ abstract: string;
19
+ published: string;
20
+ updated: string;
21
+ primary_category: string;
22
+ categories: string;
23
+ comment: string;
24
+ pdf: string;
25
+ url: string;
26
+ }
27
+
28
+ export function requireArxivLimit(
29
+ value: unknown,
30
+ fallback: number,
31
+ max = 50,
32
+ ): number {
33
+ if (value === undefined || value === null || value === "") return fallback;
34
+ const n = Number(value);
35
+ if (!Number.isInteger(n) || n < 1 || n > max) {
36
+ throw new Error(`arxiv limit must be an integer in [1, ${max}].`);
37
+ }
38
+ return n;
39
+ }
40
+
41
+ export function requireArxivAuthor(value: unknown): string {
42
+ const author = String(value ?? "").trim();
43
+ if (!author) throw new Error("arxiv author cannot be empty.");
44
+ return author;
45
+ }
46
+
47
+ export function requireArxivCategory(value: unknown): string {
48
+ const category = String(value ?? "").trim();
49
+ if (!CATEGORY_RE.test(category)) {
50
+ throw new Error(`Invalid arXiv category "${String(value)}".`);
51
+ }
52
+ return category;
53
+ }
54
+
55
+ export function decodeArxivEntities(value: string): string {
56
+ return value
57
+ .replace(/&amp;/g, "&")
58
+ .replace(/&lt;/g, "<")
59
+ .replace(/&gt;/g, ">")
60
+ .replace(/&quot;/g, '"')
61
+ .replace(/&apos;/g, "'")
62
+ .replace(/&#39;/g, "'");
63
+ }
64
+
65
+ function extractFirst(xml: string, tag: string): string {
66
+ const match = xml.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`));
67
+ return match ? match[1].trim() : "";
68
+ }
69
+
70
+ function extractAll(xml: string, tag: string): string[] {
71
+ const out: string[] = [];
72
+ const re = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, "g");
73
+ let match: RegExpExecArray | null;
74
+ while ((match = re.exec(xml)) !== null) out.push(match[1].trim());
75
+ return out;
76
+ }
77
+
78
+ function extractAttr(xml: string, tag: string, attr: string): string {
79
+ const match = xml.match(new RegExp(`<${tag}\\b[^>]*?\\b${attr}="([^"]*)"`));
80
+ return match ? match[1] : "";
81
+ }
82
+
83
+ function extractAllAttr(xml: string, tag: string, attr: string): string[] {
84
+ const out: string[] = [];
85
+ const re = new RegExp(`<${tag}\\b[^>]*?\\b${attr}="([^"]*)"`, "g");
86
+ let match: RegExpExecArray | null;
87
+ while ((match = re.exec(xml)) !== null) out.push(match[1]);
88
+ return out;
89
+ }
90
+
91
+ function findLinkHref(xml: string, rel: string): string {
92
+ const re = /<link\b([^>]*)\/?>/g;
93
+ let match: RegExpExecArray | null;
94
+ while ((match = re.exec(xml)) !== null) {
95
+ const attrs = match[1];
96
+ if (!new RegExp(`\\brel="${rel}"`).test(attrs)) continue;
97
+ const href = attrs.match(/\bhref="([^"]*)"/);
98
+ if (href) return href[1];
99
+ }
100
+ return "";
101
+ }
102
+
103
+ export function parseArxivEntries(xml: string): ArxivEntry[] {
104
+ const out: ArxivEntry[] = [];
105
+ const re = /<entry>([\s\S]*?)<\/entry>/g;
106
+ let match: RegExpExecArray | null;
107
+ while ((match = re.exec(xml)) !== null) {
108
+ const entry = match[1];
109
+ const rawId = extractFirst(entry, "id");
110
+ const id = rawId
111
+ .replace(/^https?:\/\/arxiv\.org\/abs\//, "")
112
+ .replace(/v\d+$/, "");
113
+ const title = decodeArxivEntities(
114
+ extractFirst(entry, "title").replace(/\s+/g, " "),
115
+ ).trim();
116
+ out.push({
117
+ id,
118
+ title,
119
+ authors: decodeArxivEntities(extractAll(entry, "name").join(", ")),
120
+ abstract: decodeArxivEntities(
121
+ extractFirst(entry, "summary").replace(/\s+/g, " "),
122
+ ).trim(),
123
+ published: extractFirst(entry, "published").slice(0, 10),
124
+ updated: extractFirst(entry, "updated").slice(0, 10),
125
+ primary_category: extractAttr(entry, "arxiv:primary_category", "term"),
126
+ categories: extractAllAttr(entry, "category", "term").join(", "),
127
+ comment: decodeArxivEntities(
128
+ extractFirst(entry, "arxiv:comment").replace(/\s+/g, " "),
129
+ ).trim(),
130
+ pdf: findLinkHref(entry, "related") || `https://arxiv.org/pdf/${id}`,
131
+ url: `https://arxiv.org/abs/${id}`,
132
+ });
133
+ }
134
+ return out;
135
+ }
136
+
137
+ async function fetchArxiv(params: URLSearchParams): Promise<string> {
138
+ const response = await fetch(`${ARXIV_BASE}?${params.toString()}`, {
139
+ headers: {
140
+ "User-Agent": "unicli-arxiv/1.0 (https://github.com/olo-dot-io/Uni-CLI)",
141
+ Accept: "application/atom+xml, application/xml, text/xml",
142
+ },
143
+ });
144
+ if (!response.ok)
145
+ throw new Error(`arXiv API returned HTTP ${response.status}.`);
146
+ return response.text();
147
+ }
148
+
149
+ function compactRows(entries: ArxivEntry[]): Array<Record<string, unknown>> {
150
+ return entries.map((entry) => ({
151
+ id: entry.id,
152
+ title: entry.title,
153
+ authors: entry.authors,
154
+ published: entry.published,
155
+ primary_category: entry.primary_category,
156
+ url: entry.url,
157
+ }));
158
+ }
159
+
160
+ cli({
161
+ site: "arxiv",
162
+ name: "author",
163
+ description: "List arXiv papers by a given author",
164
+ domain: "export.arxiv.org",
165
+ strategy: Strategy.PUBLIC,
166
+ args: [
167
+ {
168
+ name: "author",
169
+ type: "str",
170
+ required: true,
171
+ positional: true,
172
+ description: "Author name",
173
+ },
174
+ { name: "limit", type: "int", default: 20, description: "Max papers" },
175
+ ],
176
+ columns: ["id", "title", "authors", "published", "primary_category", "url"],
177
+ func: async (_page, kwargs) => {
178
+ const author = requireArxivAuthor(kwargs.author);
179
+ const limit = requireArxivLimit(kwargs.limit, 20);
180
+ const params = new URLSearchParams({
181
+ search_query: `au:"${author}"`,
182
+ max_results: String(limit),
183
+ sortBy: "submittedDate",
184
+ sortOrder: "descending",
185
+ });
186
+ const rows = compactRows(parseArxivEntries(await fetchArxiv(params)));
187
+ if (rows.length === 0) {
188
+ throw new Error(`No arXiv papers found for author "${author}".`);
189
+ }
190
+ return rows;
191
+ },
192
+ });
193
+
194
+ cli({
195
+ site: "arxiv",
196
+ name: "recent",
197
+ description: "List recent arXiv submissions in a category",
198
+ domain: "export.arxiv.org",
199
+ strategy: Strategy.PUBLIC,
200
+ args: [
201
+ {
202
+ name: "category",
203
+ type: "str",
204
+ required: true,
205
+ positional: true,
206
+ description: "arXiv category",
207
+ },
208
+ { name: "limit", type: "int", default: 10, description: "Max papers" },
209
+ ],
210
+ columns: ["id", "title", "authors", "published", "primary_category", "url"],
211
+ func: async (_page, kwargs) => {
212
+ const category = requireArxivCategory(kwargs.category);
213
+ const limit = requireArxivLimit(kwargs.limit, 10);
214
+ const params = new URLSearchParams({
215
+ search_query: `cat:${category}`,
216
+ max_results: String(limit),
217
+ sortBy: "submittedDate",
218
+ sortOrder: "descending",
219
+ });
220
+ const rows = compactRows(parseArxivEntries(await fetchArxiv(params)));
221
+ if (rows.length === 0) {
222
+ throw new Error(`No recent arXiv papers found in ${category}.`);
223
+ }
224
+ return rows;
225
+ },
226
+ });
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ bbcPubDateToIso,
4
+ decodeBbcEntities,
5
+ extractRssTag,
6
+ parseBbcRssItems,
7
+ requireBbcLimit,
8
+ requireBbcTopic,
9
+ } from "./topic.js";
10
+
11
+ describe("bbc agent-facing topic command", () => {
12
+ it("validates topics and limits", () => {
13
+ expect(requireBbcTopic("science-and-environment")).toBe(
14
+ "science_and_environment",
15
+ );
16
+ expect(requireBbcTopic(" Entertainment and Arts ")).toBe(
17
+ "entertainment_and_arts",
18
+ );
19
+ expect(() => requireBbcTopic("sport")).toThrow("not supported");
20
+ expect(requireBbcLimit(undefined)).toBe(20);
21
+ expect(requireBbcLimit("50")).toBe(50);
22
+ expect(() => requireBbcLimit("51")).toThrow("bbc limit");
23
+ });
24
+
25
+ it("parses RSS items with CDATA and entities", () => {
26
+ expect(decodeBbcEntities("A &amp; B &#39;C&#39;")).toBe("A & B 'C'");
27
+ const block = "<title><![CDATA[World &amp; News]]></title>";
28
+ expect(extractRssTag(block, "title")).toBe("World &amp; News");
29
+ expect(
30
+ parseBbcRssItems(`
31
+ <rss><channel>
32
+ <item>
33
+ <title><![CDATA[World &amp; News]]></title>
34
+ <description>Summary &lt;one&gt;</description>
35
+ <link>https://www.bbc.com/news/world-1</link>
36
+ <pubDate>Tue, 12 May 2026 10:00:00 GMT</pubDate>
37
+ <guid>guid-1</guid>
38
+ </item>
39
+ </channel></rss>
40
+ `),
41
+ ).toEqual([
42
+ {
43
+ title: "World & News",
44
+ description: "Summary <one>",
45
+ link: "https://www.bbc.com/news/world-1",
46
+ pubDate: "Tue, 12 May 2026 10:00:00 GMT",
47
+ guid: "guid-1",
48
+ },
49
+ ]);
50
+ expect(bbcPubDateToIso("Tue, 12 May 2026 10:00:00 GMT")).toBe("2026-05-12");
51
+ });
52
+ });
@@ -0,0 +1,149 @@
1
+ /**
2
+ * @owner src/adapters/bbc/topic.ts
3
+ * @does Register agent-facing BBC topic RSS command.
4
+ * @needs feeds.bbci.co.uk public RSS feeds, topic whitelist, conservative RSS parsing.
5
+ * @feeds surface coverage ledger, BBC section headline discovery.
6
+ * @breaks BBC RSS envelope drift, loose topic parsing, or silent empty feeds hide news coverage failures.
7
+ */
8
+
9
+ import { cli, Strategy } from "../../registry.js";
10
+
11
+ const BBC_FEED_BASE = "https://feeds.bbci.co.uk/news";
12
+ const TOPICS = [
13
+ "world",
14
+ "business",
15
+ "politics",
16
+ "health",
17
+ "education",
18
+ "science_and_environment",
19
+ "technology",
20
+ "entertainment_and_arts",
21
+ ] as const;
22
+
23
+ type BbcTopic = (typeof TOPICS)[number];
24
+
25
+ interface RssItem {
26
+ title: string;
27
+ description: string;
28
+ link: string;
29
+ pubDate: string;
30
+ guid: string;
31
+ }
32
+
33
+ export function requireBbcLimit(value: unknown, fallback = 20): number {
34
+ if (value === undefined || value === null || value === "") return fallback;
35
+ const n = Number(value);
36
+ if (!Number.isInteger(n) || n < 1 || n > 50) {
37
+ throw new Error("bbc limit must be an integer in [1, 50].");
38
+ }
39
+ return n;
40
+ }
41
+
42
+ export function requireBbcTopic(value: unknown): BbcTopic {
43
+ const topic = String(value ?? "")
44
+ .trim()
45
+ .toLowerCase()
46
+ .replace(/[\s-]+/g, "_");
47
+ if (!TOPICS.includes(topic as BbcTopic)) {
48
+ throw new Error(`bbc topic "${String(value)}" is not supported.`);
49
+ }
50
+ return topic as BbcTopic;
51
+ }
52
+
53
+ export function decodeBbcEntities(value: unknown): string {
54
+ return String(value ?? "")
55
+ .replace(/&#x([0-9a-fA-F]+);/g, (_m, hex: string) =>
56
+ String.fromCodePoint(Number.parseInt(hex, 16)),
57
+ )
58
+ .replace(/&#(\d+);/g, (_m, dec: string) =>
59
+ String.fromCodePoint(Number.parseInt(dec, 10)),
60
+ )
61
+ .replace(/&amp;/g, "&")
62
+ .replace(/&lt;/g, "<")
63
+ .replace(/&gt;/g, ">")
64
+ .replace(/&quot;/g, '"')
65
+ .replace(/&apos;/g, "'")
66
+ .replace(/&#39;/g, "'")
67
+ .replace(/&nbsp;/g, " ");
68
+ }
69
+
70
+ export function extractRssTag(block: string, tag: string): string {
71
+ const cdata = block.match(
72
+ new RegExp(
73
+ `<${tag}[^>]*>\\s*<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>\\s*<\\/${tag}>`,
74
+ ),
75
+ );
76
+ if (cdata) return cdata[1];
77
+ const plain = block.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`));
78
+ return plain ? plain[1] : "";
79
+ }
80
+
81
+ export function parseBbcRssItems(xml: string): RssItem[] {
82
+ const items: RssItem[] = [];
83
+ const re = /<item[^>]*>([\s\S]*?)<\/item>/g;
84
+ let match: RegExpExecArray | null;
85
+ while ((match = re.exec(xml)) !== null) {
86
+ const block = match[1];
87
+ items.push({
88
+ title: decodeBbcEntities(extractRssTag(block, "title")).trim(),
89
+ description: decodeBbcEntities(
90
+ extractRssTag(block, "description"),
91
+ ).trim(),
92
+ link: decodeBbcEntities(extractRssTag(block, "link")).trim(),
93
+ pubDate: decodeBbcEntities(extractRssTag(block, "pubDate")).trim(),
94
+ guid: decodeBbcEntities(extractRssTag(block, "guid")).trim(),
95
+ });
96
+ }
97
+ return items;
98
+ }
99
+
100
+ export function bbcPubDateToIso(value: unknown): string {
101
+ const date = new Date(String(value ?? ""));
102
+ return Number.isNaN(date.getTime()) ? "" : date.toISOString().slice(0, 10);
103
+ }
104
+
105
+ async function fetchBbcRss(topic: BbcTopic): Promise<string> {
106
+ const response = await fetch(`${BBC_FEED_BASE}/${topic}/rss.xml`, {
107
+ headers: {
108
+ "User-Agent": "unicli-bbc/1.0 (https://github.com/olo-dot-io/Uni-CLI)",
109
+ Accept: "application/rss+xml, application/xml",
110
+ },
111
+ });
112
+ if (!response.ok)
113
+ throw new Error(`bbc topic ${topic} returned HTTP ${response.status}.`);
114
+ return response.text();
115
+ }
116
+
117
+ cli({
118
+ site: "bbc",
119
+ name: "topic",
120
+ description: "BBC News headlines for a specific section",
121
+ domain: "feeds.bbci.co.uk",
122
+ strategy: Strategy.PUBLIC,
123
+ args: [
124
+ {
125
+ name: "topic",
126
+ type: "str",
127
+ required: true,
128
+ positional: true,
129
+ choices: [...TOPICS],
130
+ description: "BBC section name",
131
+ },
132
+ { name: "limit", type: "int", default: 20, description: "Max headlines" },
133
+ ],
134
+ columns: ["rank", "title", "description", "pubDate", "url"],
135
+ func: async (_page, kwargs) => {
136
+ const topic = requireBbcTopic(kwargs.topic);
137
+ const limit = requireBbcLimit(kwargs.limit);
138
+ const items = parseBbcRssItems(await fetchBbcRss(topic));
139
+ if (items.length === 0)
140
+ throw new Error(`BBC ${topic} feed returned no items.`);
141
+ return items.slice(0, limit).map((item, index) => ({
142
+ rank: index + 1,
143
+ title: item.title,
144
+ description: item.description,
145
+ pubDate: bbcPubDateToIso(item.pubDate),
146
+ url: item.link,
147
+ }));
148
+ },
149
+ });