@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,640 @@
1
+ /**
2
+ * @owner src/adapters/grok/web.ts
3
+ * @does Register agent-facing Grok web commands implemented with site-specific safety checks.
4
+ * @needs Logged-in grok.com browser session, Grok chat DOM, optional local image output directory.
5
+ * @feeds surface coverage ledger and Grok read/history/detail/send/new/status/image workflows.
6
+ * @breaks Grok DOM drift or weak UUID validation can miss or target the wrong conversation.
7
+ */
8
+
9
+ import { createHash } from "node:crypto";
10
+ import { mkdirSync, writeFileSync } from "node:fs";
11
+ import { homedir } from "node:os";
12
+ import { join } from "node:path";
13
+ import { cli, Strategy } from "../../registry.js";
14
+ import { boolArg, js, str } from "../_shared/browser-tools.js";
15
+
16
+ const GROK_HOME = "https://grok.com/";
17
+ const GROK_SESSION_ID_RE =
18
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
19
+ const VISIBLE_JS = `const isVisible = (node) => {
20
+ if (!(node instanceof Element)) return false;
21
+ const style = window.getComputedStyle(node);
22
+ if (style.visibility === 'hidden' || style.display === 'none') return false;
23
+ const rect = node.getBoundingClientRect();
24
+ return rect.width > 0 && rect.height > 0;
25
+ };`;
26
+
27
+ interface GrokBrowserPage {
28
+ goto: (url: string, opts?: Record<string, unknown>) => Promise<unknown>;
29
+ wait: (seconds: number) => Promise<unknown>;
30
+ evaluate: (script: string) => Promise<unknown>;
31
+ }
32
+
33
+ interface GrokBubble {
34
+ id?: unknown;
35
+ role?: unknown;
36
+ text?: unknown;
37
+ html?: unknown;
38
+ }
39
+
40
+ interface GrokImage {
41
+ src: string;
42
+ w: number;
43
+ h: number;
44
+ }
45
+
46
+ export function normalizeGrokBoolean(
47
+ value: unknown,
48
+ fallback = false,
49
+ ): boolean {
50
+ if (typeof value === "boolean") return value;
51
+ if (value === undefined || value === null || value === "") return fallback;
52
+ return /^(true|1|yes|on)$/i.test(str(value).trim());
53
+ }
54
+
55
+ export function parseGrokSessionId(value: unknown): string {
56
+ const raw = str(value).trim();
57
+ if (!raw) throw new Error("Grok session ID cannot be empty.");
58
+ let candidate = raw;
59
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) {
60
+ let parsed: URL;
61
+ try {
62
+ parsed = new URL(raw);
63
+ } catch {
64
+ throw new Error(`Invalid Grok URL: ${raw}.`);
65
+ }
66
+ const host = parsed.hostname.toLowerCase();
67
+ const match = parsed.pathname.match(
68
+ /^\/c\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\/?$/i,
69
+ );
70
+ if (
71
+ parsed.protocol !== "https:" ||
72
+ (host !== "grok.com" && !host.endsWith(".grok.com")) ||
73
+ !match
74
+ ) {
75
+ throw new Error(
76
+ `Invalid Grok conversation URL: ${raw}. Expected https://grok.com/c/<uuid>.`,
77
+ );
78
+ }
79
+ candidate = match[1];
80
+ }
81
+ if (!GROK_SESSION_ID_RE.test(candidate)) {
82
+ throw new Error(
83
+ `Invalid Grok session ID: ${raw}. Expected a UUID or https://grok.com/c/<uuid>.`,
84
+ );
85
+ }
86
+ return candidate.toLowerCase();
87
+ }
88
+
89
+ export function grokHtmlToMarkdown(value: unknown): string {
90
+ return str(value)
91
+ .replace(/<br\s*\/?>/gi, "\n")
92
+ .replace(/<\/(p|div|li|h[1-6]|pre)>/gi, "\n")
93
+ .replace(/<[^>]+>/g, " ")
94
+ .replace(/&nbsp;/g, " ")
95
+ .replace(/&lt;/g, "<")
96
+ .replace(/&gt;/g, ">")
97
+ .replace(/&amp;/g, "&")
98
+ .replace(/&quot;/g, '"')
99
+ .replace(/&#39;/g, "'")
100
+ .replace(/[ \t]+\n/g, "\n")
101
+ .replace(/\n{3,}/g, "\n\n")
102
+ .replace(/[ \t]{2,}/g, " ")
103
+ .trim();
104
+ }
105
+
106
+ export function mapGrokBubbles(
107
+ bubbles: GrokBubble[],
108
+ wantMarkdown: boolean,
109
+ ): Record<string, unknown>[] {
110
+ return bubbles
111
+ .map((bubble) => {
112
+ const role = str(bubble.role) === "Assistant" ? "Assistant" : "User";
113
+ const text =
114
+ wantMarkdown && role === "Assistant" && str(bubble.html).trim()
115
+ ? grokHtmlToMarkdown(bubble.html) || str(bubble.text)
116
+ : str(bubble.text);
117
+ return { Role: role, Text: text.trim() };
118
+ })
119
+ .filter((row) => row.Text);
120
+ }
121
+
122
+ export function mapGrokSessions(
123
+ sessions: { id?: unknown; title?: unknown }[],
124
+ ): Record<string, unknown>[] {
125
+ return sessions
126
+ .map((session, index) => {
127
+ const id = str(session.id).toLowerCase();
128
+ return {
129
+ Index: index + 1,
130
+ Title: str(session.title, "(untitled)").trim() || "(untitled)",
131
+ Url: `${GROK_HOME}c/${id}`,
132
+ };
133
+ })
134
+ .filter((row) => /\/c\/[0-9a-f-]{36}$/i.test(str(row.Url)));
135
+ }
136
+
137
+ export function pickLatestGrokImages(
138
+ bubbleImageSets: GrokImage[][],
139
+ baselineCount: number,
140
+ ): GrokImage[] {
141
+ const freshSets = bubbleImageSets.slice(Math.max(0, baselineCount));
142
+ for (let i = freshSets.length - 1; i >= 0; i -= 1) {
143
+ if (freshSets[i]?.length) return freshSets[i];
144
+ }
145
+ return [];
146
+ }
147
+
148
+ function outputDir(value: unknown): string {
149
+ return str(value).replace(/^~(?=$|\/)/, homedir());
150
+ }
151
+
152
+ function grokBoundedInt(
153
+ value: unknown,
154
+ fallback: number,
155
+ max: number,
156
+ label: string,
157
+ ): number {
158
+ const raw = value ?? fallback;
159
+ const n = Number(raw);
160
+ if (!Number.isInteger(n) || n <= 0) {
161
+ throw new Error(`Grok ${label} must be a positive integer.`);
162
+ }
163
+ if (n > max) {
164
+ throw new Error(`Grok ${label} must be <= ${max}.`);
165
+ }
166
+ return n;
167
+ }
168
+
169
+ function imageExt(mime: unknown): string {
170
+ const value = str(mime).toLowerCase();
171
+ if (value.includes("png")) return "png";
172
+ if (value.includes("webp")) return "webp";
173
+ if (value.includes("gif")) return "gif";
174
+ return "jpg";
175
+ }
176
+
177
+ function imageFileName(url: string, mime: string): string {
178
+ const hash = createHash("sha1").update(url).digest("hex").slice(0, 12);
179
+ return `grok-${Date.now()}-${hash}.${imageExt(mime)}`;
180
+ }
181
+
182
+ function imageSignature(images: GrokImage[]): string {
183
+ return images
184
+ .map((image) => image.src)
185
+ .sort()
186
+ .join("|");
187
+ }
188
+
189
+ async function currentUrl(page: GrokBrowserPage): Promise<string> {
190
+ const value = await page.evaluate("window.location.href").catch(() => "");
191
+ return typeof value === "string" ? value : "";
192
+ }
193
+
194
+ async function ensureOnGrok(page: GrokBrowserPage): Promise<void> {
195
+ const url = await currentUrl(page);
196
+ try {
197
+ const host = new URL(url).hostname.toLowerCase();
198
+ if (host === "grok.com" || host.endsWith(".grok.com")) return;
199
+ } catch {
200
+ await page.goto(GROK_HOME, { waitUntil: "load", settleMs: 2500 });
201
+ await page.wait(2);
202
+ return;
203
+ }
204
+ await page.goto(GROK_HOME, { waitUntil: "load", settleMs: 2500 });
205
+ await page.wait(2);
206
+ }
207
+
208
+ async function isLoggedIn(page: GrokBrowserPage): Promise<boolean> {
209
+ const result = await page.evaluate(`(() => {
210
+ ${VISIBLE_JS}
211
+ const composer = document.querySelector('.ProseMirror[contenteditable="true"]');
212
+ if (composer && isVisible(composer)) {
213
+ const signInCta = Array.from(document.querySelectorAll('button, a'))
214
+ .some((node) => isVisible(node) && /^(sign in|log in)$/i.test((node.textContent || '').trim()));
215
+ return !signInCta;
216
+ }
217
+ return false;
218
+ })()`);
219
+ return Boolean(result);
220
+ }
221
+
222
+ async function getCurrentSessionId(page: GrokBrowserPage): Promise<string> {
223
+ const url = await currentUrl(page);
224
+ const match = url.match(
225
+ /\/c\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i,
226
+ );
227
+ return match ? match[1].toLowerCase() : "";
228
+ }
229
+
230
+ async function getModelLabel(page: GrokBrowserPage): Promise<string> {
231
+ const result = await page.evaluate(`(() => {
232
+ ${VISIBLE_JS}
233
+ const trigger = Array.from(document.querySelectorAll('button[aria-label="Model select"]'))
234
+ .find((node) => isVisible(node));
235
+ return trigger ? (trigger.innerText || trigger.textContent || '').trim().split('\\n')[0].trim() : '';
236
+ })()`);
237
+ return typeof result === "string" ? result : "";
238
+ }
239
+
240
+ async function getMessageBubbles(page: GrokBrowserPage): Promise<GrokBubble[]> {
241
+ const result = await page.evaluate(`(() => {
242
+ ${VISIBLE_JS}
243
+ const findResponseId = (node) => {
244
+ let parent = node.parentElement;
245
+ while (parent && parent !== document.body) {
246
+ const id = parent.getAttribute('id') || '';
247
+ if (id.startsWith('response-')) return id.slice('response-'.length);
248
+ parent = parent.parentElement;
249
+ }
250
+ return '';
251
+ };
252
+ const bubbles = Array.from(document.querySelectorAll('[data-testid="user-message"], [data-testid="assistant-message"]'))
253
+ .filter((node) => node instanceof HTMLElement && isVisible(node));
254
+ return bubbles.map((node, index) => {
255
+ const isAssistant = node.getAttribute('data-testid') === 'assistant-message';
256
+ const responseId = findResponseId(node) || ('pos-' + index);
257
+ const html = node.innerHTML || '';
258
+ const text = (node.innerText || node.textContent || '').replace(/\\s+/g, ' ').trim();
259
+ return {
260
+ id: responseId + (isAssistant ? '-assistant' : '-user'),
261
+ role: isAssistant ? 'Assistant' : 'User',
262
+ text,
263
+ html,
264
+ };
265
+ });
266
+ })()`);
267
+ return Array.isArray(result)
268
+ ? (result as GrokBubble[]).filter(
269
+ (bubble) => str(bubble.id) && (str(bubble.text) || str(bubble.html)),
270
+ )
271
+ : [];
272
+ }
273
+
274
+ async function getHistoryFromSidebar(
275
+ page: GrokBrowserPage,
276
+ limit: number,
277
+ ): Promise<Record<string, unknown>[]> {
278
+ const result = await page.evaluate(`(() => {
279
+ ${VISIBLE_JS}
280
+ const re = /^\\/c\\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
281
+ const entries = [];
282
+ const seen = new Set();
283
+ const anchors = Array.from(document.querySelectorAll('a[href^="/c/"]'));
284
+ for (const a of anchors) {
285
+ if (!(a instanceof HTMLElement) || !isVisible(a)) continue;
286
+ const href = a.getAttribute('href') || '';
287
+ const match = href.match(re);
288
+ if (!match) continue;
289
+ const id = match[1].toLowerCase();
290
+ const title = (a.innerText || a.textContent || '').trim();
291
+ if (!seen.has(id)) {
292
+ seen.add(id);
293
+ entries.push({ id, title });
294
+ } else if (title) {
295
+ const existing = entries.find((entry) => entry.id === id);
296
+ if (existing && !existing.title) existing.title = title;
297
+ }
298
+ }
299
+ return entries;
300
+ })()`);
301
+ const sessions = Array.isArray(result)
302
+ ? (result as { id?: unknown; title?: unknown }[])
303
+ : [];
304
+ const rows = mapGrokSessions(sessions).slice(0, limit);
305
+ if (!rows.length)
306
+ throw new Error("No Grok conversations found in the sidebar.");
307
+ return rows;
308
+ }
309
+
310
+ async function startNewChat(page: GrokBrowserPage): Promise<void> {
311
+ await page.goto(GROK_HOME, { waitUntil: "load", settleMs: 2500 });
312
+ await page.wait(2);
313
+ }
314
+
315
+ async function sendGrokMessage(
316
+ page: GrokBrowserPage,
317
+ prompt: string,
318
+ ): Promise<void> {
319
+ const result = (await page.evaluate(`(async () => {
320
+ const waitFor = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
321
+ const composerSelector = '.ProseMirror[contenteditable="true"]';
322
+ let composer = null;
323
+ for (let attempt = 0; attempt < 12; attempt += 1) {
324
+ const candidate = document.querySelector(composerSelector);
325
+ if (candidate instanceof HTMLElement) {
326
+ composer = candidate;
327
+ break;
328
+ }
329
+ await waitFor(500);
330
+ }
331
+ if (!(composer instanceof HTMLElement)) {
332
+ return { ok: false, reason: 'Grok composer (.ProseMirror) was not found on grok.com.' };
333
+ }
334
+ const editor = composer.editor;
335
+ if (!editor?.commands?.focus || !editor?.commands?.insertContent) {
336
+ return { ok: false, reason: 'Grok composer editor API was unavailable.' };
337
+ }
338
+ if (typeof editor.commands.clearContent === 'function') editor.commands.clearContent();
339
+ editor.commands.focus();
340
+ editor.commands.insertContent(${js(prompt)});
341
+ const isClickableSubmit = (node) => {
342
+ if (!(node instanceof HTMLButtonElement)) return false;
343
+ if (node.disabled) return false;
344
+ const style = window.getComputedStyle(node);
345
+ if (style.visibility === 'hidden' || style.display === 'none') return false;
346
+ const rect = node.getBoundingClientRect();
347
+ return rect.width > 0 && rect.height > 0;
348
+ };
349
+ for (let attempt = 0; attempt < 12; attempt += 1) {
350
+ const submit = Array.from(document.querySelectorAll('button[aria-label="Submit"]')).find(isClickableSubmit);
351
+ if (submit instanceof HTMLButtonElement) {
352
+ submit.click();
353
+ return { ok: true };
354
+ }
355
+ await waitFor(500);
356
+ }
357
+ return { ok: false, reason: 'Grok submit button did not reach a clickable state.' };
358
+ })()`)) as { ok?: boolean; reason?: string };
359
+ if (!result?.ok)
360
+ throw new Error(result?.reason || "Failed to send Grok prompt.");
361
+ }
362
+
363
+ async function getBubbleImageSets(
364
+ page: GrokBrowserPage,
365
+ ): Promise<GrokImage[][]> {
366
+ const result = await page.evaluate(`(() => {
367
+ const bubbles = document.querySelectorAll('div.message-bubble, [data-testid="message-bubble"], [data-testid="assistant-message"]');
368
+ const dedupe = (images) => {
369
+ const seen = new Set();
370
+ const out = [];
371
+ for (const image of images) {
372
+ if (!image.src || seen.has(image.src)) continue;
373
+ seen.add(image.src);
374
+ out.push(image);
375
+ }
376
+ return out;
377
+ };
378
+ return Array.from(bubbles).map((bubble) => dedupe(Array.from(bubble.querySelectorAll('img'))
379
+ .map((img) => ({
380
+ src: img.currentSrc || img.src || '',
381
+ w: img.naturalWidth || img.width || 0,
382
+ h: img.naturalHeight || img.height || 0,
383
+ }))
384
+ .filter((image) => image.src && /^https?:/.test(image.src))
385
+ .filter((image) => (image.w === 0 || image.w >= 128) && (image.h === 0 || image.h >= 128))));
386
+ })()`);
387
+ if (!Array.isArray(result)) return [];
388
+ return result.map((set) =>
389
+ Array.isArray(set)
390
+ ? set
391
+ .map((image) => ({
392
+ src: str((image as GrokImage).src),
393
+ w: Number((image as GrokImage).w) || 0,
394
+ h: Number((image as GrokImage).h) || 0,
395
+ }))
396
+ .filter((image) => image.src)
397
+ : [],
398
+ );
399
+ }
400
+
401
+ async function fetchImageAsset(
402
+ page: GrokBrowserPage,
403
+ url: string,
404
+ ): Promise<{ mime: string; base64: string }> {
405
+ const result = (await page.evaluate(`(async () => {
406
+ const response = await fetch(${js(url)}, { credentials: 'include', referrer: 'https://grok.com/' });
407
+ if (!response.ok) return { ok: false, status: response.status };
408
+ const blob = await response.blob();
409
+ const bytes = new Uint8Array(await blob.arrayBuffer());
410
+ let binary = '';
411
+ const chunk = 0x8000;
412
+ for (let i = 0; i < bytes.length; i += chunk) {
413
+ binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
414
+ }
415
+ return { ok: true, mime: blob.type || 'image/jpeg', base64: btoa(binary) };
416
+ })()`)) as { ok?: boolean; status?: number; mime?: string; base64?: string };
417
+ if (!result?.ok || !result.base64) {
418
+ throw new Error(
419
+ `Failed to download Grok image ${url}: status=${result?.status ?? "?"}.`,
420
+ );
421
+ }
422
+ return { mime: str(result.mime), base64: result.base64 };
423
+ }
424
+
425
+ cli({
426
+ site: "grok",
427
+ name: "read",
428
+ description: "Read messages in the current Grok conversation",
429
+ domain: "grok.com",
430
+ strategy: Strategy.COOKIE,
431
+ browser: true,
432
+ args: [{ name: "markdown", type: "bool", default: false }],
433
+ columns: ["Role", "Text"],
434
+ func: async (page, kwargs) => {
435
+ const p = page as GrokBrowserPage;
436
+ await ensureOnGrok(p);
437
+ await p.wait(2);
438
+ const rows = mapGrokBubbles(
439
+ await getMessageBubbles(p),
440
+ normalizeGrokBoolean(kwargs.markdown),
441
+ );
442
+ if (!rows.length)
443
+ throw new Error("No visible Grok messages in the current conversation.");
444
+ return rows;
445
+ },
446
+ });
447
+
448
+ cli({
449
+ site: "grok",
450
+ name: "history",
451
+ description: "List recent Grok conversations from the sidebar",
452
+ domain: "grok.com",
453
+ strategy: Strategy.COOKIE,
454
+ browser: true,
455
+ args: [{ name: "limit", type: "int", default: 20 }],
456
+ columns: ["Index", "Title", "Url"],
457
+ func: async (page, kwargs) => {
458
+ const p = page as GrokBrowserPage;
459
+ const limit = grokBoundedInt(kwargs.limit, 20, 100, "history limit");
460
+ await ensureOnGrok(p);
461
+ await p.wait(2);
462
+ if (!(await isLoggedIn(p))) throw new Error("Grok login is required.");
463
+ return getHistoryFromSidebar(p, limit);
464
+ },
465
+ });
466
+
467
+ cli({
468
+ site: "grok",
469
+ name: "detail",
470
+ description: "Open a Grok conversation by ID and read its messages",
471
+ domain: "grok.com",
472
+ strategy: Strategy.COOKIE,
473
+ browser: true,
474
+ args: [
475
+ { name: "id", type: "str", required: true, positional: true },
476
+ { name: "markdown", type: "bool", default: false },
477
+ ],
478
+ columns: ["Role", "Text"],
479
+ func: async (page, kwargs) => {
480
+ const p = page as GrokBrowserPage;
481
+ const sessionId = parseGrokSessionId(kwargs.id);
482
+ await p.goto(`${GROK_HOME}c/${sessionId}`, {
483
+ waitUntil: "load",
484
+ settleMs: 2500,
485
+ });
486
+ const startedAt = Date.now();
487
+ while (Date.now() - startedAt < 20_000) {
488
+ const rows = mapGrokBubbles(
489
+ await getMessageBubbles(p),
490
+ normalizeGrokBoolean(kwargs.markdown),
491
+ );
492
+ if (rows.length) return rows;
493
+ await p.wait(1);
494
+ }
495
+ throw new Error(
496
+ `No visible Grok messages found for conversation ${sessionId}.`,
497
+ );
498
+ },
499
+ });
500
+
501
+ cli({
502
+ site: "grok",
503
+ name: "send",
504
+ description: "Send a prompt to Grok without waiting for the reply",
505
+ domain: "grok.com",
506
+ strategy: Strategy.COOKIE,
507
+ browser: true,
508
+ args: [
509
+ { name: "prompt", type: "str", required: true, positional: true },
510
+ { name: "new", type: "bool", default: false },
511
+ ],
512
+ columns: ["Status", "Prompt"],
513
+ func: async (page, kwargs) => {
514
+ const prompt = str(kwargs.prompt).trim();
515
+ if (!prompt) throw new Error("Grok prompt cannot be empty.");
516
+ const p = page as GrokBrowserPage;
517
+ await ensureOnGrok(p);
518
+ if (boolArg(kwargs.new)) await startNewChat(p);
519
+ await sendGrokMessage(p, prompt);
520
+ return [{ Status: "sent", Prompt: prompt }];
521
+ },
522
+ });
523
+
524
+ cli({
525
+ site: "grok",
526
+ name: "new",
527
+ description: "Start a new conversation in Grok",
528
+ domain: "grok.com",
529
+ strategy: Strategy.COOKIE,
530
+ browser: true,
531
+ columns: ["Status"],
532
+ func: async (page) => {
533
+ await startNewChat(page as GrokBrowserPage);
534
+ return [{ Status: "New chat started" }];
535
+ },
536
+ });
537
+
538
+ cli({
539
+ site: "grok",
540
+ name: "status",
541
+ description:
542
+ "Check Grok page availability, login state, current session and model",
543
+ domain: "grok.com",
544
+ strategy: Strategy.COOKIE,
545
+ browser: true,
546
+ columns: ["Status", "Login", "Model", "SessionId", "Url"],
547
+ func: async (page) => {
548
+ const p = page as GrokBrowserPage;
549
+ await ensureOnGrok(p);
550
+ await p.wait(2);
551
+ const [loggedIn, sessionId, model, url] = await Promise.all([
552
+ isLoggedIn(p),
553
+ getCurrentSessionId(p),
554
+ getModelLabel(p),
555
+ currentUrl(p),
556
+ ]);
557
+ return [
558
+ {
559
+ Status: "Connected",
560
+ Login: loggedIn ? "Yes" : "No",
561
+ Model: model || null,
562
+ SessionId: sessionId || null,
563
+ Url: url,
564
+ },
565
+ ];
566
+ },
567
+ });
568
+
569
+ cli({
570
+ site: "grok",
571
+ name: "image",
572
+ description: "Generate images with Grok and return or save image assets",
573
+ domain: "grok.com",
574
+ strategy: Strategy.COOKIE,
575
+ browser: true,
576
+ args: [
577
+ { name: "prompt", type: "str", required: true, positional: true },
578
+ { name: "timeout", type: "int", default: 240 },
579
+ { name: "new", type: "bool", default: false },
580
+ { name: "count", type: "int", default: 1 },
581
+ { name: "out", type: "str", default: "" },
582
+ ],
583
+ columns: ["url", "width", "height", "path"],
584
+ func: async (page, kwargs) => {
585
+ const prompt = str(kwargs.prompt).trim();
586
+ if (!prompt) throw new Error("Grok image prompt cannot be empty.");
587
+ const p = page as GrokBrowserPage;
588
+ const timeout = grokBoundedInt(kwargs.timeout, 240, 600, "image timeout");
589
+ const minCount = grokBoundedInt(kwargs.count, 1, 100, "image count");
590
+ await ensureOnGrok(p);
591
+ if (normalizeGrokBoolean(kwargs.new)) await startNewChat(p);
592
+ const baselineCount = (await getBubbleImageSets(p)).length;
593
+ await sendGrokMessage(p, prompt);
594
+ const startedAt = Date.now();
595
+ let lastSignature = "";
596
+ let stableCount = 0;
597
+ let lastImages: GrokImage[] = [];
598
+ while (Date.now() - startedAt < timeout * 1000) {
599
+ await p.wait(3);
600
+ const images = pickLatestGrokImages(
601
+ await getBubbleImageSets(p),
602
+ baselineCount,
603
+ );
604
+ if (images.length >= minCount) {
605
+ const signature = imageSignature(images);
606
+ if (signature === lastSignature) {
607
+ stableCount += 1;
608
+ if (stableCount >= 2) {
609
+ lastImages = images;
610
+ break;
611
+ }
612
+ } else {
613
+ stableCount = 0;
614
+ lastSignature = signature;
615
+ lastImages = images;
616
+ }
617
+ }
618
+ }
619
+ if (!lastImages.length)
620
+ throw new Error("No generated Grok images observed before timeout.");
621
+ const dir = str(kwargs.out).trim() ? outputDir(kwargs.out) : "";
622
+ if (dir) mkdirSync(dir, { recursive: true });
623
+ const rows: Record<string, unknown>[] = [];
624
+ for (const image of lastImages) {
625
+ let filePath = "";
626
+ if (dir) {
627
+ const asset = await fetchImageAsset(p, image.src);
628
+ filePath = join(dir, imageFileName(image.src, asset.mime));
629
+ writeFileSync(filePath, Buffer.from(asset.base64, "base64"));
630
+ }
631
+ rows.push({
632
+ url: image.src,
633
+ width: image.w,
634
+ height: image.h,
635
+ path: filePath,
636
+ });
637
+ }
638
+ return rows;
639
+ },
640
+ });
@@ -0,0 +1,68 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ buildHnReadRows,
4
+ hnHtmlToText,
5
+ requireHnItemId,
6
+ requireMinInt,
7
+ requirePositiveInt,
8
+ } from "./read.js";
9
+
10
+ describe("hackernews agent-facing read command", () => {
11
+ it("validates item ids, positive ints, and HTML text conversion", () => {
12
+ expect(requireHnItemId(" 39847301 ")).toBe("39847301");
13
+ expect(() => requireHnItemId("abc")).toThrow("Invalid HN");
14
+ expect(requirePositiveInt(undefined, 25, "limit")).toBe(25);
15
+ expect(() => requirePositiveInt("0", 25, "limit")).toThrow("positive");
16
+ expect(requireMinInt(undefined, 2000, 100, "max")).toBe(2000);
17
+ expect(() => requireMinInt("99", 2000, 100, "max")).toThrow(">= 100");
18
+ expect(
19
+ hnHtmlToText(
20
+ '<p>Hello &amp; <i>world</i><p><a href="https://x.test">link</a>',
21
+ ),
22
+ ).toBe("Hello & world\n\nlink (https://x.test)");
23
+ });
24
+
25
+ it("builds story and bounded comment tree rows", async () => {
26
+ const fixtures = new Map([
27
+ [
28
+ 2,
29
+ {
30
+ id: 2,
31
+ type: "comment",
32
+ by: "alice",
33
+ text: "First",
34
+ kids: [4, 5],
35
+ },
36
+ ],
37
+ [3, { id: 3, type: "comment", by: "bob", text: "Second" }],
38
+ [4, { id: 4, type: "comment", by: "carol", text: "Reply" }],
39
+ [5, { id: 5, type: "comment", by: "dan", text: "Hidden" }],
40
+ ]);
41
+ const rows = await buildHnReadRows(
42
+ {
43
+ id: 1,
44
+ type: "story",
45
+ by: "pg",
46
+ title: "Launch",
47
+ url: "https://example.test",
48
+ score: 42,
49
+ kids: [2, 3, 99],
50
+ },
51
+ async (id) => fixtures.get(id) ?? null,
52
+ { limit: 2, maxDepth: 2, maxReplies: 1, maxLength: 100 },
53
+ );
54
+ expect(rows).toEqual([
55
+ {
56
+ type: "POST",
57
+ author: "pg",
58
+ score: 42,
59
+ text: "Launch\nhttps://example.test",
60
+ },
61
+ { type: "L0", author: "alice", score: "", text: "First" },
62
+ { type: "L1", author: "carol", score: "", text: " > Reply" },
63
+ { type: "L1", author: "", score: "", text: " [+1 more replies]" },
64
+ { type: "L0", author: "bob", score: "", text: "Second" },
65
+ { type: "", author: "", score: "", text: "[+1 more top-level comments]" },
66
+ ]);
67
+ });
68
+ });