@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,133 @@
1
+ /**
2
+ * @owner src/adapters/mdn/search.ts
3
+ * @does Register agent-facing MDN Web Docs search command.
4
+ * @needs MDN public search API, bounded limits, locale allowlist.
5
+ * @feeds surface coverage ledger, web-platform documentation search rows.
6
+ * @breaks MDN API drift, weak locale validation, or silent empty rows hide documentation lookup failures.
7
+ */
8
+
9
+ import { cli, Strategy } from "../../registry.js";
10
+
11
+ const API_BASE = "https://developer.mozilla.org";
12
+ const LOCALES = new Set([
13
+ "de",
14
+ "en-US",
15
+ "es",
16
+ "fr",
17
+ "ja",
18
+ "ko",
19
+ "pt-BR",
20
+ "ru",
21
+ "zh-CN",
22
+ "zh-TW",
23
+ ]);
24
+
25
+ function requireNonEmpty(value: unknown, label: string): string {
26
+ const text = String(value ?? "").trim();
27
+ if (!text) throw new Error(`mdn ${label} cannot be empty.`);
28
+ return text;
29
+ }
30
+
31
+ export function requireMdnLimit(value: unknown): number {
32
+ const raw =
33
+ value === undefined || value === null || value === "" ? 10 : value;
34
+ const limit = Number(raw);
35
+ if (!Number.isInteger(limit) || limit < 1 || limit > 50) {
36
+ throw new Error("mdn limit must be an integer in [1, 50].");
37
+ }
38
+ return limit;
39
+ }
40
+
41
+ export function requireMdnLocale(value: unknown): string {
42
+ const locale = String(value ?? "en-US").trim();
43
+ if (!LOCALES.has(locale)) {
44
+ throw new Error(`mdn locale "${String(value)}" is not supported.`);
45
+ }
46
+ return locale;
47
+ }
48
+
49
+ function stringField(value: unknown): string {
50
+ return typeof value === "string" ? value.trim() : "";
51
+ }
52
+
53
+ export function mapMdnRows(
54
+ documents: Array<Record<string, unknown>>,
55
+ limit: number,
56
+ fallbackLocale: string,
57
+ ): Array<Record<string, unknown>> {
58
+ return documents.slice(0, limit).map((doc, index) => {
59
+ const mdnUrl = stringField(doc.mdn_url);
60
+ return {
61
+ rank: index + 1,
62
+ title: stringField(doc.title),
63
+ slug: stringField(doc.slug),
64
+ locale: stringField(doc.locale) || fallbackLocale,
65
+ summary: stringField(doc.summary).replace(/\s+/g, " "),
66
+ url: mdnUrl ? `${API_BASE}${mdnUrl}` : "",
67
+ };
68
+ });
69
+ }
70
+
71
+ async function fetchJson(url: URL | string, label: string): Promise<unknown> {
72
+ const response = await fetch(url, {
73
+ headers: {
74
+ accept: "application/json",
75
+ "user-agent": "unicli-mdn (https://github.com/olo-dot-io/Uni-CLI)",
76
+ },
77
+ });
78
+ if (response.status === 404) throw new Error(`${label} returned no result.`);
79
+ if (response.status === 429) throw new Error(`${label} returned HTTP 429.`);
80
+ if (!response.ok)
81
+ throw new Error(`${label} returned HTTP ${response.status}.`);
82
+ return response.json();
83
+ }
84
+
85
+ cli({
86
+ site: "mdn",
87
+ name: "search",
88
+ description: "Search MDN Web Docs by keyword",
89
+ domain: "developer.mozilla.org",
90
+ strategy: Strategy.PUBLIC,
91
+ args: [
92
+ {
93
+ name: "query",
94
+ type: "str",
95
+ required: true,
96
+ positional: true,
97
+ description: "Search keyword",
98
+ },
99
+ { name: "limit", type: "int", default: 10, description: "Max rows" },
100
+ {
101
+ name: "locale",
102
+ type: "str",
103
+ default: "en-US",
104
+ description: "MDN locale",
105
+ },
106
+ ],
107
+ columns: ["rank", "title", "slug", "locale", "summary", "url"],
108
+ func: async (_page, kwargs) => {
109
+ const query = requireNonEmpty(kwargs.query, "query");
110
+ const limit = requireMdnLimit(kwargs.limit);
111
+ const locale = requireMdnLocale(kwargs.locale);
112
+ const url = new URL(`${API_BASE}/api/v1/search`);
113
+ url.searchParams.set("q", query);
114
+ url.searchParams.set("locale", locale);
115
+ url.searchParams.set("size", String(limit));
116
+ const body = (await fetchJson(url, "mdn search")) as {
117
+ documents?: unknown;
118
+ };
119
+ const rows = mapMdnRows(
120
+ Array.isArray(body.documents)
121
+ ? (body.documents as Array<Record<string, unknown>>)
122
+ : [],
123
+ limit,
124
+ locale,
125
+ );
126
+ if (rows.length === 0) {
127
+ throw new Error(
128
+ `mdn search returned no rows for "${query}" (${locale}).`,
129
+ );
130
+ }
131
+ return rows;
132
+ },
133
+ });
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ decodeMediumHtml,
4
+ extractMediumCategories,
5
+ mediumRssDate,
6
+ parseMediumTagRss,
7
+ requireMediumLimit,
8
+ requireMediumTag,
9
+ stripMediumHtml,
10
+ } from "./tag.js";
11
+
12
+ describe("medium agent-facing tag command", () => {
13
+ it("validates tag slugs and limits", () => {
14
+ expect(requireMediumTag(" Machine-Learning ")).toBe("machine-learning");
15
+ expect(() => requireMediumTag("bad/tag")).toThrow("not valid");
16
+ expect(requireMediumLimit(undefined)).toBe(20);
17
+ expect(requireMediumLimit("25")).toBe(25);
18
+ expect(() => requireMediumLimit("26")).toThrow("[1, 25]");
19
+ });
20
+
21
+ it("decodes and strips Medium RSS text", () => {
22
+ expect(decodeMediumHtml("A&#39;s &lt;B&gt; &amp; C")).toBe("A's <B> & C");
23
+ expect(stripMediumHtml("<p>Hello&nbsp;<strong>world</strong></p>")).toBe(
24
+ "Hello world",
25
+ );
26
+ expect(mediumRssDate("Tue, 12 May 2026 01:02:03 GMT")).toBe("2026-05-12");
27
+ expect(mediumRssDate("not a date")).toBe("");
28
+ });
29
+
30
+ it("parses RSS items into stable columns", () => {
31
+ const xml = `
32
+ <rss><channel>
33
+ <item>
34
+ <title><![CDATA[AI &amp; Agents]]></title>
35
+ <dc:creator><![CDATA[Jane Doe]]></dc:creator>
36
+ <description><![CDATA[<p>Readable <a href="https://x.test">summary</a></p>]]></description>
37
+ <category><![CDATA[AI]]></category>
38
+ <category>Programming</category>
39
+ <pubDate>Tue, 12 May 2026 01:02:03 GMT</pubDate>
40
+ <link>https://medium.com/p/example</link>
41
+ </item>
42
+ </channel></rss>
43
+ `;
44
+
45
+ expect(extractMediumCategories(xml)).toEqual(["AI", "Programming"]);
46
+ expect(parseMediumTagRss(xml, 20)).toEqual([
47
+ {
48
+ rank: 1,
49
+ title: "AI & Agents",
50
+ author: "Jane Doe",
51
+ description: "Readable summary",
52
+ categories: "AI, Programming",
53
+ published: "2026-05-12",
54
+ url: "https://medium.com/p/example",
55
+ },
56
+ ]);
57
+ });
58
+
59
+ it("rejects empty RSS feeds", () => {
60
+ expect(() => parseMediumTagRss("<rss><channel /></rss>", 20)).toThrow(
61
+ "no items",
62
+ );
63
+ });
64
+ });
@@ -0,0 +1,164 @@
1
+ /**
2
+ * @owner src/adapters/medium/tag.ts
3
+ * @does Register agent-facing Medium tag RSS reader.
4
+ * @needs Medium public tag RSS feed, strict tag slugs, bounded item parsing.
5
+ * @feeds surface coverage ledger, Medium topical article discovery, RSS reading workflows.
6
+ * @breaks Medium RSS shape drift or weak entity stripping degrades tag article rows.
7
+ */
8
+
9
+ import { cli, Strategy } from "../../registry.js";
10
+
11
+ const MEDIUM_TAG_PATTERN = /^[a-z0-9][a-z0-9-]*$/i;
12
+ const MEDIUM_TAG_LIMIT = 25;
13
+ const HTML_ENTITIES: Record<string, string> = {
14
+ "&amp;": "&",
15
+ "&lt;": "<",
16
+ "&gt;": ">",
17
+ "&quot;": '"',
18
+ "&apos;": "'",
19
+ "&#39;": "'",
20
+ "&nbsp;": " ",
21
+ };
22
+
23
+ export function requireMediumTag(value: unknown): string {
24
+ const tag = String(value ?? "")
25
+ .trim()
26
+ .toLowerCase();
27
+ if (!tag) throw new Error("medium tag is required.");
28
+ if (!MEDIUM_TAG_PATTERN.test(tag)) {
29
+ throw new Error(`medium tag "${String(value)}" is not valid.`);
30
+ }
31
+ return tag;
32
+ }
33
+
34
+ export function requireMediumLimit(value: unknown, fallback = 20): number {
35
+ const raw =
36
+ value === undefined || value === null || value === "" ? fallback : value;
37
+ const limit = Number(raw);
38
+ if (!Number.isInteger(limit) || limit < 1 || limit > MEDIUM_TAG_LIMIT) {
39
+ throw new Error(
40
+ `medium limit must be an integer in [1, ${MEDIUM_TAG_LIMIT}].`,
41
+ );
42
+ }
43
+ return limit;
44
+ }
45
+
46
+ export function decodeMediumHtml(value: unknown): string {
47
+ return String(value ?? "")
48
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, hex: string) => {
49
+ const code = Number.parseInt(hex, 16);
50
+ return Number.isFinite(code) ? String.fromCodePoint(code) : "";
51
+ })
52
+ .replace(/&#(\d+);/g, (_, dec: string) => {
53
+ const code = Number.parseInt(dec, 10);
54
+ return Number.isFinite(code) ? String.fromCodePoint(code) : "";
55
+ })
56
+ .replace(/&(amp|lt|gt|quot|apos|#39|nbsp);/g, (match) => {
57
+ return HTML_ENTITIES[match] ?? match;
58
+ });
59
+ }
60
+
61
+ function extractXmlTag(block: string, tag: string): string {
62
+ const cdata = block.match(
63
+ new RegExp(
64
+ `<${tag}[^>]*>\\s*<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>\\s*<\\/${tag}>`,
65
+ ),
66
+ );
67
+ if (cdata) return cdata[1] ?? "";
68
+ const plain = block.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`));
69
+ return plain?.[1] ?? "";
70
+ }
71
+
72
+ export function stripMediumHtml(value: unknown): string {
73
+ return decodeMediumHtml(String(value ?? "").replace(/<[^>]+>/g, " "))
74
+ .replace(/\s+/g, " ")
75
+ .trim();
76
+ }
77
+
78
+ export function mediumRssDate(value: unknown): string {
79
+ const raw = String(value ?? "").trim();
80
+ if (!raw) return "";
81
+ const date = new Date(raw);
82
+ return Number.isNaN(date.getTime()) ? "" : date.toISOString().slice(0, 10);
83
+ }
84
+
85
+ export function extractMediumCategories(block: string): string[] {
86
+ const categories: string[] = [];
87
+ const re =
88
+ /<category(?:[^>]*)>(?:<!\[CDATA\[([\s\S]*?)\]\]>|([\s\S]*?))<\/category>/g;
89
+ let match: RegExpExecArray | null;
90
+ while ((match = re.exec(block)) !== null) {
91
+ const category = decodeMediumHtml((match[1] ?? match[2] ?? "").trim());
92
+ if (category) categories.push(category);
93
+ }
94
+ return categories;
95
+ }
96
+
97
+ export function parseMediumTagRss(
98
+ xml: string,
99
+ limit: number,
100
+ ): Array<Record<string, unknown>> {
101
+ const items: string[] = [];
102
+ const re = /<item[^>]*>([\s\S]*?)<\/item>/g;
103
+ let match: RegExpExecArray | null;
104
+ while ((match = re.exec(xml)) !== null) {
105
+ if (match[1]) items.push(match[1]);
106
+ }
107
+ if (items.length === 0) throw new Error("Medium tag RSS feed has no items.");
108
+ return items.slice(0, limit).map((block, index) => ({
109
+ rank: index + 1,
110
+ title: decodeMediumHtml(extractXmlTag(block, "title")).trim(),
111
+ author: decodeMediumHtml(extractXmlTag(block, "dc:creator")).trim(),
112
+ description: stripMediumHtml(extractXmlTag(block, "description")),
113
+ categories: extractMediumCategories(block).join(", "),
114
+ published: mediumRssDate(decodeMediumHtml(extractXmlTag(block, "pubDate"))),
115
+ url: decodeMediumHtml(extractXmlTag(block, "link")).trim(),
116
+ }));
117
+ }
118
+
119
+ async function fetchMediumTagRss(tag: string): Promise<string> {
120
+ const response = await fetch(`https://medium.com/feed/tag/${tag}`, {
121
+ headers: {
122
+ Accept: "application/rss+xml, application/xml",
123
+ "User-Agent": "unicli-medium/1.0 (https://github.com/olo-dot-io/Uni-CLI)",
124
+ },
125
+ });
126
+ if (response.status === 404)
127
+ throw new Error(`Medium tag "${tag}" does not exist.`);
128
+ if (!response.ok)
129
+ throw new Error(`Medium tag returned HTTP ${response.status}.`);
130
+ return response.text();
131
+ }
132
+
133
+ cli({
134
+ site: "medium",
135
+ name: "tag",
136
+ description: "Latest Medium articles tagged with a keyword from RSS",
137
+ domain: "medium.com",
138
+ strategy: Strategy.PUBLIC,
139
+ browser: false,
140
+ args: [
141
+ {
142
+ name: "tag",
143
+ type: "str",
144
+ required: true,
145
+ positional: true,
146
+ description: "Medium tag slug",
147
+ },
148
+ { name: "limit", type: "int", default: 20, description: "Max articles" },
149
+ ],
150
+ columns: [
151
+ "rank",
152
+ "title",
153
+ "author",
154
+ "description",
155
+ "categories",
156
+ "published",
157
+ "url",
158
+ ],
159
+ func: async (_page, kwargs) => {
160
+ const tag = requireMediumTag(kwargs.tag);
161
+ const limit = requireMediumLimit(kwargs.limit);
162
+ return parseMediumTagRss(await fetchMediumTagRss(tag), limit);
163
+ },
164
+ });
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { mapNpmPackageRow, requireNpmPackageName } from "./package.js";
3
+
4
+ describe("npm agent-facing package command", () => {
5
+ it("validates npm package names", () => {
6
+ expect(requireNpmPackageName(" @vercel/og ")).toBe("@vercel/og");
7
+ expect(requireNpmPackageName("react")).toBe("react");
8
+ expect(() => requireNpmPackageName("")).toThrow("is required");
9
+ expect(() => requireNpmPackageName("../react")).toThrow("not valid");
10
+ });
11
+
12
+ it("maps latest package metadata to agent-facing columns", () => {
13
+ expect(
14
+ mapNpmPackageRow(
15
+ {
16
+ name: "react",
17
+ description: "base description",
18
+ "dist-tags": { latest: "19.0.0" },
19
+ versions: {
20
+ "19.0.0": {
21
+ description: "ui library",
22
+ license: { type: "MIT" },
23
+ homepage: "https://react.dev",
24
+ repository: { url: "git+https://github.com/facebook/react.git" },
25
+ bugs: { url: "https://github.com/facebook/react/issues" },
26
+ keywords: ["ui", "react"],
27
+ },
28
+ },
29
+ maintainers: [{ name: "core" }, { email: "ops@example.test" }],
30
+ time: {
31
+ created: "2011-10-26T00:00:00.000Z",
32
+ modified: "2026-05-01T00:00:00.000Z",
33
+ },
34
+ },
35
+ "react",
36
+ ),
37
+ ).toMatchObject({
38
+ name: "react",
39
+ latestVersion: "19.0.0",
40
+ description: "ui library",
41
+ license: "MIT",
42
+ repository: "https://github.com/facebook/react",
43
+ bugs: "https://github.com/facebook/react/issues",
44
+ maintainers: "core, ops@example.test",
45
+ keywords: "ui, react",
46
+ created: "2011-10-26",
47
+ modified: "2026-05-01",
48
+ });
49
+ expect(() => mapNpmPackageRow({}, "missing")).toThrow(
50
+ "has no latest version",
51
+ );
52
+ });
53
+ });
@@ -0,0 +1,177 @@
1
+ /**
2
+ * @owner src/adapters/npm/package.ts
3
+ * @does Register agent-facing npm single-package metadata command.
4
+ * @needs registry.npmjs.org public package documents.
5
+ * @feeds surface coverage ledger, JavaScript package inspection, registry command surface.
6
+ * @breaks npm registry document drift or weak scoped-name parsing hides package metadata failures.
7
+ */
8
+
9
+ import { cli, Strategy } from "../../registry.js";
10
+
11
+ const NPM_REGISTRY = "https://registry.npmjs.org";
12
+
13
+ interface NpmPackageVersion {
14
+ description?: unknown;
15
+ license?: unknown;
16
+ homepage?: unknown;
17
+ repository?: unknown;
18
+ bugs?: unknown;
19
+ keywords?: unknown;
20
+ }
21
+
22
+ interface NpmPackageBody {
23
+ name?: unknown;
24
+ description?: unknown;
25
+ "dist-tags"?: { latest?: unknown };
26
+ versions?: Record<string, NpmPackageVersion>;
27
+ maintainers?: unknown;
28
+ time?: { created?: unknown; modified?: unknown };
29
+ }
30
+
31
+ function stringField(value: unknown): string {
32
+ return typeof value === "string" ? value.trim() : "";
33
+ }
34
+
35
+ export function requireNpmPackageName(value: unknown): string {
36
+ const name = String(value ?? "").trim();
37
+ if (!name) throw new Error("npm package name is required.");
38
+ if (
39
+ !/^(@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/i.test(name) ||
40
+ name.includes("..")
41
+ ) {
42
+ throw new Error(`npm package name "${String(value)}" is not valid.`);
43
+ }
44
+ return name;
45
+ }
46
+
47
+ function repoUrl(repo: unknown): string {
48
+ if (typeof repo === "string")
49
+ return repo.replace(/^git\+/, "").replace(/\.git$/, "");
50
+ if (
51
+ repo &&
52
+ typeof repo === "object" &&
53
+ typeof (repo as { url?: unknown }).url === "string"
54
+ ) {
55
+ return (repo as { url: string }).url
56
+ .replace(/^git\+/, "")
57
+ .replace(/\.git$/, "");
58
+ }
59
+ return "";
60
+ }
61
+
62
+ function bugUrl(bugs: unknown): string {
63
+ if (typeof bugs === "string") return bugs;
64
+ if (
65
+ bugs &&
66
+ typeof bugs === "object" &&
67
+ typeof (bugs as { url?: unknown }).url === "string"
68
+ ) {
69
+ return (bugs as { url: string }).url;
70
+ }
71
+ return "";
72
+ }
73
+
74
+ function licenseText(license: unknown): string {
75
+ if (typeof license === "string") return license.trim();
76
+ if (
77
+ license &&
78
+ typeof license === "object" &&
79
+ typeof (license as { type?: unknown }).type === "string"
80
+ ) {
81
+ return (license as { type: string }).type.trim();
82
+ }
83
+ return "";
84
+ }
85
+
86
+ export function mapNpmPackageRow(
87
+ body: NpmPackageBody,
88
+ requested: string,
89
+ ): Record<string, unknown> {
90
+ const latest = stringField(body["dist-tags"]?.latest);
91
+ if (!latest) {
92
+ throw new Error(`npm registry has no latest version for "${requested}".`);
93
+ }
94
+ const version = body.versions?.[latest] ?? {};
95
+ const name = stringField(body.name) || requested;
96
+ const maintainers = Array.isArray(body.maintainers)
97
+ ? body.maintainers
98
+ .map((item) => {
99
+ if (item && typeof item === "object") {
100
+ const rec = item as { name?: unknown; email?: unknown };
101
+ return stringField(rec.name) || stringField(rec.email);
102
+ }
103
+ return String(item ?? "").trim();
104
+ })
105
+ .filter(Boolean)
106
+ .join(", ")
107
+ : "";
108
+ const keywords = Array.isArray(version.keywords)
109
+ ? version.keywords.map(stringField).filter(Boolean).join(", ")
110
+ : "";
111
+ return {
112
+ name,
113
+ latestVersion: latest,
114
+ description:
115
+ stringField(version.description) || stringField(body.description),
116
+ license: licenseText(version.license),
117
+ homepage: stringField(version.homepage),
118
+ repository: repoUrl(version.repository),
119
+ bugs: bugUrl(version.bugs),
120
+ maintainers,
121
+ keywords,
122
+ created: stringField(body.time?.created).slice(0, 10),
123
+ modified: stringField(body.time?.modified).slice(0, 10),
124
+ url: `https://www.npmjs.com/package/${encodeURIComponent(name)}`,
125
+ };
126
+ }
127
+
128
+ async function fetchNpmJson(name: string): Promise<unknown> {
129
+ const path = name.split("/").map(encodeURIComponent).join("/");
130
+ const response = await fetch(`${NPM_REGISTRY}/${path}`, {
131
+ headers: {
132
+ "User-Agent": "unicli (https://github.com/olo-dot-io/Uni-CLI)",
133
+ Accept: "application/json",
134
+ },
135
+ });
136
+ if (response.status === 404)
137
+ throw new Error(`npm package ${name} returned no result.`);
138
+ if (!response.ok)
139
+ throw new Error(`npm package ${name} returned HTTP ${response.status}.`);
140
+ return response.json();
141
+ }
142
+
143
+ cli({
144
+ site: "npm",
145
+ name: "package",
146
+ description: "Single npm package metadata",
147
+ domain: "registry.npmjs.org",
148
+ strategy: Strategy.PUBLIC,
149
+ args: [
150
+ {
151
+ name: "name",
152
+ type: "str",
153
+ required: true,
154
+ positional: true,
155
+ description: "npm package name",
156
+ },
157
+ ],
158
+ columns: [
159
+ "name",
160
+ "latestVersion",
161
+ "description",
162
+ "license",
163
+ "homepage",
164
+ "repository",
165
+ "bugs",
166
+ "maintainers",
167
+ "keywords",
168
+ "created",
169
+ "modified",
170
+ "url",
171
+ ],
172
+ func: async (_page, kwargs) => {
173
+ const name = requireNpmPackageName(kwargs.name);
174
+ const body = (await fetchNpmJson(name)) as NpmPackageBody;
175
+ return [mapNpmPackageRow(body, name)];
176
+ },
177
+ });
@@ -0,0 +1,102 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ collectNugetEntries,
4
+ mapNugetPackageRows,
5
+ requireNugetPackageId,
6
+ } from "./package.js";
7
+
8
+ describe("nuget agent-facing package command", () => {
9
+ it("validates package ids", () => {
10
+ expect(requireNugetPackageId(" Newtonsoft.Json ")).toBe("Newtonsoft.Json");
11
+ expect(() => requireNugetPackageId("../Newtonsoft.Json")).toThrow(
12
+ "package id must be",
13
+ );
14
+ });
15
+
16
+ it("collects inline and stub registration pages", async () => {
17
+ await expect(
18
+ collectNugetEntries(
19
+ {
20
+ items: [
21
+ {
22
+ items: [
23
+ {
24
+ catalogEntry: {
25
+ id: "Pkg",
26
+ version: "1.0.0",
27
+ published: "2020-01-01T00:00:00Z",
28
+ },
29
+ },
30
+ ],
31
+ },
32
+ { "@id": "https://example.test/page.json" },
33
+ ],
34
+ },
35
+ async () => ({
36
+ items: [
37
+ {
38
+ catalogEntry: {
39
+ id: "Pkg",
40
+ version: "2.0.0",
41
+ published: "2021-01-01T00:00:00Z",
42
+ },
43
+ },
44
+ ],
45
+ }),
46
+ ),
47
+ ).resolves.toHaveLength(2);
48
+
49
+ await expect(
50
+ collectNugetEntries({ items: [{}] }, async () => ({})),
51
+ ).rejects.toThrow("missing @id");
52
+ });
53
+
54
+ it("maps version history rows sorted by published desc", () => {
55
+ expect(
56
+ mapNugetPackageRows(
57
+ [
58
+ {
59
+ catalogEntry: {
60
+ id: "Newtonsoft.Json",
61
+ version: "1.0.0",
62
+ title: "JSON",
63
+ authors: ["A", "B"],
64
+ tags: ["json", "dotnet"],
65
+ language: "en",
66
+ licenseExpression: "MIT",
67
+ projectUrl: "https://example.test",
68
+ published: "2020-01-01T00:00:00Z",
69
+ listed: true,
70
+ },
71
+ },
72
+ {
73
+ catalogEntry: {
74
+ id: "Newtonsoft.Json",
75
+ version: "2.0.0",
76
+ published: "2021-01-01T00:00:00Z",
77
+ },
78
+ },
79
+ ],
80
+ "Newtonsoft.Json",
81
+ ),
82
+ ).toMatchObject([
83
+ {
84
+ rank: 1,
85
+ id: "Newtonsoft.Json",
86
+ version: "2.0.0",
87
+ published: "2021-01-01T00:00:00Z",
88
+ },
89
+ {
90
+ rank: 2,
91
+ id: "Newtonsoft.Json",
92
+ version: "1.0.0",
93
+ authors: "A, B",
94
+ tags: "json, dotnet",
95
+ listed: true,
96
+ },
97
+ ]);
98
+ expect(() => mapNugetPackageRows([], "missing")).toThrow(
99
+ "No published versions",
100
+ );
101
+ });
102
+ });