@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
@@ -14,6 +14,8 @@ import {
14
14
 
15
15
  const HOME = "https://chat.deepseek.com";
16
16
  const CHAT = `${HOME}/`;
17
+ const CONVERSATION_ID_RE =
18
+ /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
17
19
  const INPUT = "textarea, [contenteditable='true'], .chat-input textarea";
18
20
  const FILE_INPUT = 'input[type="file"]';
19
21
  const MESSAGES =
@@ -37,6 +39,13 @@ async function readConversation(
37
39
  page: IPage,
38
40
  ): Promise<Record<string, unknown>[]> {
39
41
  await openChat(page);
42
+ return readVisibleConversation(page, true);
43
+ }
44
+
45
+ async function readVisibleConversation(
46
+ page: IPage,
47
+ withFallback: boolean,
48
+ ): Promise<Record<string, unknown>[]> {
40
49
  const items = (await page.evaluate(`(() => {
41
50
  const nodes = [...document.querySelectorAll(${js(MESSAGES)})];
42
51
  const rows = nodes.map((node, index) => ({
@@ -44,11 +53,28 @@ async function readConversation(
44
53
  text: (node.textContent || '').trim(),
45
54
  role: /user|human|question/i.test(node.getAttribute('class') || '') ? 'user' : 'assistant'
46
55
  })).filter((row) => row.text.length > 0);
47
- return rows.length ? rows : [{ index: 1, role: 'page', text: (document.body?.innerText || '').trim().slice(0, 4000) }];
56
+ return rows.length ? rows : (${withFallback} ? [{ index: 1, role: 'page', text: (document.body?.innerText || '').trim().slice(0, 4000) }] : []);
48
57
  })()`)) as Record<string, unknown>[];
49
58
  return items;
50
59
  }
51
60
 
61
+ export function parseDeepSeekConversationId(value: unknown): string {
62
+ const raw = str(value).trim();
63
+ if (!raw) throw new Error("DeepSeek conversation ID cannot be empty.");
64
+ const urlMatch = raw.match(/\/a\/chat\/s\/([a-f0-9-]+)/i);
65
+ const candidate = urlMatch ? urlMatch[1] : raw;
66
+ if (!CONVERSATION_ID_RE.test(candidate)) {
67
+ throw new Error(
68
+ `Invalid DeepSeek conversation ID: ${raw}. Expected a UUID or /a/chat/s/<id> URL.`,
69
+ );
70
+ }
71
+ return candidate.toLowerCase();
72
+ }
73
+
74
+ function deepSeekConversationUrl(id: string): string {
75
+ return `${HOME}/a/chat/s/${id}`;
76
+ }
77
+
52
78
  function resolveUploadFiles(value: unknown): string[] {
53
79
  const raw = str(value).trim();
54
80
  if (!raw) return [];
@@ -201,3 +227,54 @@ cli({
201
227
  return rows.slice(0, limit);
202
228
  },
203
229
  });
230
+
231
+ cli({
232
+ site: "deepseek",
233
+ name: "detail",
234
+ description: "Read a specific DeepSeek conversation by ID",
235
+ domain: "chat.deepseek.com",
236
+ strategy: Strategy.COOKIE,
237
+ browser: true,
238
+ args: [{ name: "id", type: "str", required: true, positional: true }],
239
+ columns: ["index", "role", "text"],
240
+ func: async (page, kwargs) => {
241
+ const p = page as IPage;
242
+ const id = parseDeepSeekConversationId(kwargs.id);
243
+ await p.goto(deepSeekConversationUrl(id), { settleMs: 1800 });
244
+ await p.wait(1);
245
+ const rows = await readVisibleConversation(p, false);
246
+ if (!rows.length) {
247
+ throw new Error(
248
+ `No visible DeepSeek messages found for conversation ${id}.`,
249
+ );
250
+ }
251
+ return rows;
252
+ },
253
+ });
254
+
255
+ cli({
256
+ site: "deepseek",
257
+ name: "send",
258
+ description:
259
+ "Send a prompt to a specific DeepSeek conversation without waiting for a response",
260
+ domain: "chat.deepseek.com",
261
+ strategy: Strategy.COOKIE,
262
+ browser: true,
263
+ args: [
264
+ { name: "id", type: "str", required: true, positional: true },
265
+ { name: "prompt", type: "str", required: true, positional: true },
266
+ ],
267
+ columns: ["status", "injectedText"],
268
+ func: async (page, kwargs) => {
269
+ const p = page as IPage;
270
+ const id = parseDeepSeekConversationId(kwargs.id);
271
+ const prompt = str(kwargs.prompt).trim();
272
+ if (!prompt) throw new Error("DeepSeek prompt cannot be empty.");
273
+ await p.goto(deepSeekConversationUrl(id), { settleMs: 1800 });
274
+ await p.click(INPUT);
275
+ await p.insertText(prompt);
276
+ await p.press("Enter");
277
+ await p.wait(0.6);
278
+ return [{ status: "success", injectedText: prompt }];
279
+ },
280
+ });
@@ -0,0 +1,75 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ mapDefillamaDetailRow,
4
+ mapDefillamaProtocolRows,
5
+ requireDefillamaLimit,
6
+ requireDefillamaSlug,
7
+ unixToDate,
8
+ } from "./protocols.js";
9
+
10
+ describe("defillama agent-facing protocol commands", () => {
11
+ it("validates limits and slugs", () => {
12
+ expect(requireDefillamaLimit(undefined)).toBe(30);
13
+ expect(requireDefillamaLimit("500")).toBe(500);
14
+ expect(() => requireDefillamaLimit("501")).toThrow("defillama limit must");
15
+ expect(requireDefillamaSlug("aave-v3")).toBe("aave-v3");
16
+ expect(() => requireDefillamaSlug("../aave")).toThrow("not valid");
17
+ });
18
+
19
+ it("normalizes unix timestamps", () => {
20
+ expect(unixToDate(1)).toBe("1970-01-01");
21
+ expect(unixToDate(null)).toBeNull();
22
+ });
23
+
24
+ it("maps protocol list rows sorted by TVL", () => {
25
+ expect(
26
+ mapDefillamaProtocolRows(
27
+ [
28
+ { slug: "small", name: "Small", tvl: 1 },
29
+ {
30
+ slug: "large",
31
+ name: "Large",
32
+ category: "Lending",
33
+ tvl: "10",
34
+ chains: ["Ethereum"],
35
+ },
36
+ ],
37
+ 2,
38
+ ),
39
+ ).toMatchObject([
40
+ {
41
+ rank: 1,
42
+ slug: "large",
43
+ name: "Large",
44
+ category: "Lending",
45
+ tvl: 10,
46
+ chains: "Ethereum",
47
+ },
48
+ { rank: 2, slug: "small", tvl: 1 },
49
+ ]);
50
+ });
51
+
52
+ it("maps protocol detail rows with list metadata", () => {
53
+ expect(
54
+ mapDefillamaDetailRow(
55
+ "aave",
56
+ {
57
+ name: "Aave",
58
+ tvl: [{ date: 1, totalLiquidityUSD: "100" }],
59
+ chains: ["Ethereum"],
60
+ github: ["aave/protocol-v2"],
61
+ twitter: "aave",
62
+ url: "https://aave.com",
63
+ },
64
+ [{ slug: "aave", category: "Lending", chains: ["Polygon"] }],
65
+ ),
66
+ ).toMatchObject({
67
+ slug: "aave",
68
+ name: "Aave",
69
+ category: "Lending",
70
+ tvl: 100,
71
+ chains: "Ethereum, Polygon",
72
+ github: "aave/protocol-v2",
73
+ });
74
+ });
75
+ });
@@ -0,0 +1,253 @@
1
+ /**
2
+ * @owner src/adapters/defillama/protocols.ts
3
+ * @does Register agent-facing DefiLlama protocol list and detail commands.
4
+ * @needs DefiLlama public API, bounded result limits, validated protocol slugs.
5
+ * @feeds surface coverage ledger, DeFi protocol TVL rows, protocol metadata detail.
6
+ * @breaks DefiLlama API drift, weak slug validation, or silent empty rows hide DeFi data failures.
7
+ */
8
+
9
+ import { cli, Strategy } from "../../registry.js";
10
+
11
+ const API_BASE = "https://api.llama.fi";
12
+ const SLUG_RE = /^[a-z0-9][a-z0-9._-]{0,99}$/;
13
+
14
+ export function requireDefillamaLimit(value: unknown): number {
15
+ const raw =
16
+ value === undefined || value === null || value === "" ? 30 : value;
17
+ const limit = Number(raw);
18
+ if (!Number.isInteger(limit) || limit < 1 || limit > 500) {
19
+ throw new Error("defillama limit must be an integer in [1, 500].");
20
+ }
21
+ return limit;
22
+ }
23
+
24
+ export function requireDefillamaSlug(value: unknown): string {
25
+ const slug = String(value ?? "").trim();
26
+ if (!slug) throw new Error("defillama slug cannot be empty.");
27
+ if (!SLUG_RE.test(slug)) {
28
+ throw new Error(`defillama slug "${String(value)}" is not valid.`);
29
+ }
30
+ return slug;
31
+ }
32
+
33
+ export function unixToDate(value: unknown): string | null {
34
+ if (value == null) return null;
35
+ const n = Number(value);
36
+ if (!Number.isFinite(n) || n <= 0) return null;
37
+ const date = new Date(n > 1e12 ? n : n * 1000);
38
+ return Number.isNaN(date.getTime()) ? null : date.toISOString().slice(0, 10);
39
+ }
40
+
41
+ function stringField(value: unknown): string {
42
+ return typeof value === "string" ? value.trim() : "";
43
+ }
44
+
45
+ function numberField(value: unknown): number | null {
46
+ const n =
47
+ typeof value === "number"
48
+ ? value
49
+ : typeof value === "string" && value.trim()
50
+ ? Number(value)
51
+ : NaN;
52
+ return Number.isFinite(n) ? n : null;
53
+ }
54
+
55
+ function joinStrings(value: unknown): string {
56
+ return Array.isArray(value)
57
+ ? value.filter((item) => typeof item === "string" && item.trim()).join(", ")
58
+ : "";
59
+ }
60
+
61
+ function objectField(value: unknown): Record<string, unknown> {
62
+ return value && typeof value === "object"
63
+ ? (value as Record<string, unknown>)
64
+ : {};
65
+ }
66
+
67
+ export function mapDefillamaProtocolRows(
68
+ rows: Array<Record<string, unknown>>,
69
+ limit: number,
70
+ ): Array<Record<string, unknown>> {
71
+ return rows
72
+ .filter((row) => (row.slug || row.name) && numberField(row.tvl) != null)
73
+ .sort((a, b) => (numberField(b.tvl) ?? 0) - (numberField(a.tvl) ?? 0))
74
+ .slice(0, limit)
75
+ .map((row, index) => {
76
+ const slug = stringField(row.slug);
77
+ return {
78
+ rank: index + 1,
79
+ slug,
80
+ name: stringField(row.name),
81
+ category: stringField(row.category),
82
+ tvl: numberField(row.tvl),
83
+ mcap: numberField(row.mcap),
84
+ change_1d: numberField(row.change_1d),
85
+ change_7d: numberField(row.change_7d),
86
+ chains: joinStrings(row.chains),
87
+ listedAt: unixToDate(row.listedAt),
88
+ url: slug ? `https://defillama.com/protocol/${slug}` : "",
89
+ };
90
+ });
91
+ }
92
+
93
+ export function mapDefillamaDetailRow(
94
+ slug: string,
95
+ detail: Record<string, unknown>,
96
+ protocols: Array<Record<string, unknown>>,
97
+ ): Record<string, unknown> {
98
+ const tvlSeries = Array.isArray(detail.tvl)
99
+ ? (detail.tvl as Array<Record<string, unknown>>)
100
+ : [];
101
+ const lastPoint = tvlSeries[tvlSeries.length - 1] ?? {};
102
+ const isParent = detail.isParentProtocol === true;
103
+ const chains = new Set(
104
+ Array.isArray(detail.chains) ? (detail.chains as Array<string>) : [],
105
+ );
106
+ let category = "";
107
+ if (isParent) {
108
+ for (const protocol of protocols) {
109
+ if (
110
+ protocol.parentProtocol === detail.id &&
111
+ Array.isArray(protocol.chains)
112
+ ) {
113
+ for (const chain of protocol.chains) {
114
+ if (typeof chain === "string") chains.add(chain);
115
+ }
116
+ }
117
+ }
118
+ } else {
119
+ const match = protocols.find((protocol) => protocol.slug === slug);
120
+ category = stringField(match?.category);
121
+ if (Array.isArray(match?.chains)) {
122
+ for (const chain of match.chains) {
123
+ if (typeof chain === "string") chains.add(chain);
124
+ }
125
+ }
126
+ }
127
+ return {
128
+ slug,
129
+ name: stringField(detail.name),
130
+ category,
131
+ isParent,
132
+ tvl: numberField(lastPoint.totalLiquidityUSD),
133
+ tvlAt: unixToDate(lastPoint.date),
134
+ mcap: numberField(detail.mcap),
135
+ chains: [...chains].join(", "),
136
+ twitter: stringField(detail.twitter),
137
+ github: joinStrings(detail.github),
138
+ audits: stringField(detail.audits),
139
+ listedAt: unixToDate(detail.listedAt),
140
+ description: stringField(detail.description),
141
+ website: stringField(detail.url),
142
+ url: `https://defillama.com/protocol/${slug}`,
143
+ };
144
+ }
145
+
146
+ async function fetchJson(url: URL | string, label: string): Promise<unknown> {
147
+ const response = await fetch(url, {
148
+ headers: {
149
+ accept: "application/json",
150
+ "user-agent": "unicli-defillama (https://github.com/olo-dot-io/Uni-CLI)",
151
+ },
152
+ });
153
+ if (response.status === 404) throw new Error(`${label} returned no result.`);
154
+ if (response.status === 400) throw new Error(`${label} returned HTTP 400.`);
155
+ if (response.status === 429) throw new Error(`${label} returned HTTP 429.`);
156
+ if (!response.ok)
157
+ throw new Error(`${label} returned HTTP ${response.status}.`);
158
+ return response.json();
159
+ }
160
+
161
+ cli({
162
+ site: "defillama",
163
+ name: "protocols",
164
+ description: "Top DeFi protocols by current TVL",
165
+ domain: "defillama.com",
166
+ strategy: Strategy.PUBLIC,
167
+ args: [{ name: "limit", type: "int", default: 30, description: "Max rows" }],
168
+ columns: [
169
+ "rank",
170
+ "slug",
171
+ "name",
172
+ "category",
173
+ "tvl",
174
+ "mcap",
175
+ "change_1d",
176
+ "change_7d",
177
+ "chains",
178
+ "listedAt",
179
+ "url",
180
+ ],
181
+ func: async (_page, kwargs) => {
182
+ const limit = requireDefillamaLimit(kwargs.limit);
183
+ const body = await fetchJson(
184
+ `${API_BASE}/protocols`,
185
+ "defillama protocols",
186
+ );
187
+ const rows = mapDefillamaProtocolRows(
188
+ Array.isArray(body) ? (body as Array<Record<string, unknown>>) : [],
189
+ limit,
190
+ );
191
+ if (rows.length === 0)
192
+ throw new Error("defillama protocols returned no rows.");
193
+ return rows;
194
+ },
195
+ });
196
+
197
+ cli({
198
+ site: "defillama",
199
+ name: "protocol",
200
+ description: "Fetch DefiLlama protocol detail by slug",
201
+ domain: "defillama.com",
202
+ strategy: Strategy.PUBLIC,
203
+ args: [
204
+ {
205
+ name: "slug",
206
+ type: "str",
207
+ required: true,
208
+ positional: true,
209
+ description: "Protocol slug",
210
+ },
211
+ ],
212
+ columns: [
213
+ "slug",
214
+ "name",
215
+ "category",
216
+ "isParent",
217
+ "tvl",
218
+ "tvlAt",
219
+ "mcap",
220
+ "chains",
221
+ "twitter",
222
+ "github",
223
+ "audits",
224
+ "listedAt",
225
+ "description",
226
+ "website",
227
+ "url",
228
+ ],
229
+ func: async (_page, kwargs) => {
230
+ const slug = requireDefillamaSlug(kwargs.slug);
231
+ const detail = objectField(
232
+ await fetchJson(
233
+ `${API_BASE}/protocol/${encodeURIComponent(slug)}`,
234
+ "defillama protocol",
235
+ ),
236
+ );
237
+ if (!detail.name)
238
+ throw new Error(`defillama protocol returned no row for "${slug}".`);
239
+ const protocols = await fetchJson(
240
+ `${API_BASE}/protocols`,
241
+ "defillama protocol list",
242
+ );
243
+ return [
244
+ mapDefillamaDetailRow(
245
+ slug,
246
+ detail,
247
+ Array.isArray(protocols)
248
+ ? (protocols as Array<Record<string, unknown>>)
249
+ : [],
250
+ ),
251
+ ];
252
+ },
253
+ });
@@ -0,0 +1,49 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ mapDevtoArticleRow,
4
+ requireDevtoArticleId,
5
+ requireDevtoMaxLength,
6
+ } from "./read.js";
7
+
8
+ describe("devto agent-facing read command", () => {
9
+ it("validates ids and max-length", () => {
10
+ expect(requireDevtoArticleId(" 3605688 ")).toBe("3605688");
11
+ expect(() => requireDevtoArticleId("slug")).toThrow("Invalid DEV.to");
12
+ expect(requireDevtoMaxLength(undefined)).toBe(20_000);
13
+ expect(requireDevtoMaxLength("100")).toBe(100);
14
+ expect(() => requireDevtoMaxLength("99")).toThrow("max-length");
15
+ });
16
+
17
+ it("maps article body, tags, and explicit truncation", () => {
18
+ expect(
19
+ mapDevtoArticleRow(
20
+ {
21
+ id: 3605688,
22
+ title: "Readable",
23
+ body_markdown: `${"x".repeat(105)}`,
24
+ user: { username: "author" },
25
+ public_reactions_count: 7,
26
+ reading_time_minutes: 3,
27
+ tag_list: ["ai", "cli"],
28
+ published_at: "2026-05-12T00:00:00Z",
29
+ url: "https://dev.to/a/readable",
30
+ },
31
+ "3605688",
32
+ 100,
33
+ ),
34
+ ).toEqual({
35
+ id: 3605688,
36
+ title: "Readable",
37
+ author: "author",
38
+ reactions: 7,
39
+ reading_time: 3,
40
+ tags: "ai, cli",
41
+ published_at: "2026-05-12T00:00:00Z",
42
+ body: `${"x".repeat(100)}\n\n... [truncated]`,
43
+ url: "https://dev.to/a/readable",
44
+ });
45
+ expect(() => mapDevtoArticleRow({ id: 1 }, "1", 100)).toThrow(
46
+ "body_markdown",
47
+ );
48
+ });
49
+ });
@@ -0,0 +1,145 @@
1
+ /**
2
+ * @owner src/adapters/devto/read.ts
3
+ * @does Register agent-facing DEV.to article reader.
4
+ * @needs Public dev.to articles API, numeric article ids, explicit body truncation.
5
+ * @feeds surface coverage ledger, developer article reading workflow.
6
+ * @breaks DEV.to API shape drift or missing body_markdown would silently hide article content.
7
+ */
8
+
9
+ import { cli, Strategy } from "../../registry.js";
10
+
11
+ const DEVTO_ARTICLE_BASE = "https://dev.to/api/articles";
12
+
13
+ interface DevtoArticle {
14
+ id?: unknown;
15
+ title?: unknown;
16
+ body_markdown?: unknown;
17
+ user?: { username?: unknown };
18
+ public_reactions_count?: unknown;
19
+ reading_time_minutes?: unknown;
20
+ tag_list?: unknown;
21
+ tags?: unknown;
22
+ published_at?: unknown;
23
+ url?: unknown;
24
+ }
25
+
26
+ function stringField(value: unknown): string {
27
+ return typeof value === "string" ? value : "";
28
+ }
29
+
30
+ function numberField(value: unknown): number {
31
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
32
+ }
33
+
34
+ export function requireDevtoArticleId(value: unknown): string {
35
+ const id = String(value ?? "").trim();
36
+ if (!/^\d+$/.test(id)) {
37
+ throw new Error(`Invalid DEV.to article id: ${String(value)}.`);
38
+ }
39
+ return id;
40
+ }
41
+
42
+ export function requireDevtoMaxLength(
43
+ value: unknown,
44
+ fallback = 20_000,
45
+ ): number {
46
+ if (value === undefined || value === null || value === "") return fallback;
47
+ const n = Number(value);
48
+ if (!Number.isInteger(n) || n < 100) {
49
+ throw new Error("devto read max-length must be an integer >= 100.");
50
+ }
51
+ return n;
52
+ }
53
+
54
+ function normalizeTags(article: DevtoArticle): string {
55
+ const tags = article.tag_list ?? article.tags ?? "";
56
+ return Array.isArray(tags) ? tags.map(String).join(", ") : String(tags);
57
+ }
58
+
59
+ export function mapDevtoArticleRow(
60
+ article: DevtoArticle,
61
+ requestedId: string,
62
+ maxLength: number,
63
+ ): Record<string, unknown> {
64
+ if (!article || !article.id) {
65
+ throw new Error(`DEV.to article ${requestedId} returned no article.`);
66
+ }
67
+ const body = stringField(article.body_markdown);
68
+ if (!body.trim()) {
69
+ throw new Error(
70
+ `DEV.to article ${requestedId} did not include body_markdown.`,
71
+ );
72
+ }
73
+ return {
74
+ id: article.id,
75
+ title: stringField(article.title),
76
+ author: stringField(article.user?.username) || "[deleted]",
77
+ reactions: numberField(article.public_reactions_count),
78
+ reading_time: numberField(article.reading_time_minutes),
79
+ tags: normalizeTags(article),
80
+ published_at: stringField(article.published_at),
81
+ body:
82
+ body.length > maxLength
83
+ ? `${body.slice(0, maxLength)}\n\n... [truncated]`
84
+ : body,
85
+ url: stringField(article.url),
86
+ };
87
+ }
88
+
89
+ async function fetchDevtoArticle(id: string): Promise<DevtoArticle> {
90
+ const response = await fetch(`${DEVTO_ARTICLE_BASE}/${id}`, {
91
+ headers: {
92
+ "User-Agent": "unicli-devto/1.0 (https://github.com/olo-dot-io/Uni-CLI)",
93
+ Accept: "application/json",
94
+ },
95
+ });
96
+ if (response.status === 404)
97
+ throw new Error(`DEV.to article ${id} not found.`);
98
+ if (!response.ok) {
99
+ throw new Error(
100
+ `DEV.to API returned HTTP ${response.status} for article ${id}.`,
101
+ );
102
+ }
103
+ return (await response.json()) as DevtoArticle;
104
+ }
105
+
106
+ cli({
107
+ site: "devto",
108
+ name: "read",
109
+ description: "Read a DEV.to article body by id",
110
+ domain: "dev.to",
111
+ strategy: Strategy.PUBLIC,
112
+ args: [
113
+ {
114
+ name: "id",
115
+ type: "str",
116
+ required: true,
117
+ positional: true,
118
+ description: "DEV.to numeric article id",
119
+ },
120
+ {
121
+ name: "max-length",
122
+ type: "int",
123
+ default: 20_000,
124
+ description: "Max body characters",
125
+ },
126
+ ],
127
+ columns: [
128
+ "id",
129
+ "title",
130
+ "author",
131
+ "reactions",
132
+ "reading_time",
133
+ "tags",
134
+ "published_at",
135
+ "body",
136
+ "url",
137
+ ],
138
+ func: async (_page, kwargs) => {
139
+ const id = requireDevtoArticleId(kwargs.id);
140
+ const maxLength = requireDevtoMaxLength(
141
+ kwargs["max-length"] ?? kwargs.maxLength,
142
+ );
143
+ return [mapDevtoArticleRow(await fetchDevtoArticle(id), id, maxLength)];
144
+ },
145
+ });