@jackwener/opencli 1.7.12 → 1.7.13

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 (407) hide show
  1. package/README.md +8 -7
  2. package/README.zh-CN.md +9 -8
  3. package/cli-manifest.json +12194 -6843
  4. package/clis/1point3acres/digest.js +35 -0
  5. package/clis/1point3acres/forum.js +51 -0
  6. package/clis/1point3acres/forums.js +44 -0
  7. package/clis/1point3acres/hot.js +35 -0
  8. package/clis/1point3acres/latest.js +35 -0
  9. package/clis/1point3acres/notifications.js +64 -0
  10. package/clis/1point3acres/search.js +71 -0
  11. package/clis/1point3acres/thread.js +117 -0
  12. package/clis/1point3acres/user.js +77 -0
  13. package/clis/1point3acres/utils.js +247 -0
  14. package/clis/_shared/desktop-commands.js +4 -0
  15. package/clis/aibase/news.js +110 -0
  16. package/clis/aibase/news.test.js +59 -0
  17. package/clis/amazon/discussion.test.js +1 -28
  18. package/clis/antigravity/watch.js +3 -2
  19. package/clis/arxiv/author.js +44 -0
  20. package/clis/baidu-scholar/search.js +0 -1
  21. package/clis/bbc/topic.js +57 -0
  22. package/clis/bbc/utils.js +79 -0
  23. package/clis/chaoxing/assignments.js +1 -1
  24. package/clis/chaoxing/exams.js +1 -1
  25. package/clis/chatgpt/ask.js +57 -0
  26. package/clis/chatgpt/commands.test.js +45 -0
  27. package/clis/chatgpt/detail.js +46 -0
  28. package/clis/chatgpt/history.js +39 -0
  29. package/clis/chatgpt/image.js +12 -11
  30. package/clis/chatgpt/image.test.js +23 -0
  31. package/clis/chatgpt/new.js +25 -0
  32. package/clis/chatgpt/read.js +43 -0
  33. package/clis/chatgpt/send.js +46 -0
  34. package/clis/chatgpt/status.js +29 -0
  35. package/clis/chatgpt/utils.js +294 -4
  36. package/clis/chatgpt/utils.test.js +13 -0
  37. package/clis/chatgpt-app/ask.js +6 -3
  38. package/clis/chatwise/ask.js +16 -43
  39. package/clis/chatwise/composer.test.js +186 -0
  40. package/clis/chatwise/send.js +2 -24
  41. package/clis/chatwise/utils.js +143 -0
  42. package/clis/claude/ask.js +1 -1
  43. package/clis/claude/detail.js +1 -0
  44. package/clis/claude/history.js +1 -0
  45. package/clis/claude/new.js +1 -0
  46. package/clis/claude/read.js +1 -0
  47. package/clis/claude/send.js +1 -0
  48. package/clis/claude/status.js +1 -0
  49. package/clis/codex/ask.js +15 -9
  50. package/clis/codex/history.js +16 -33
  51. package/clis/codex/projects.js +28 -0
  52. package/clis/codex/read.js +10 -4
  53. package/clis/codex/send.js +10 -3
  54. package/clis/codex/sidebar.js +356 -0
  55. package/clis/codex/sidebar.test.js +329 -0
  56. package/clis/coingecko/categories.js +75 -0
  57. package/clis/coingecko/coin.js +107 -0
  58. package/clis/coingecko/coingecko.test.js +109 -0
  59. package/clis/coingecko/derivatives.js +84 -0
  60. package/clis/coingecko/exchanges.js +74 -0
  61. package/clis/coingecko/global.js +71 -0
  62. package/clis/coingecko/top.js +64 -0
  63. package/clis/coingecko/trending.js +55 -0
  64. package/clis/coupang/add-to-cart.js +21 -13
  65. package/clis/coupang/coupang.test.js +159 -0
  66. package/clis/coupang/product.js +257 -0
  67. package/clis/coupang/search.js +38 -16
  68. package/clis/coupang/utils.js +55 -1
  69. package/clis/crates/crate.js +62 -0
  70. package/clis/crates/search.js +44 -0
  71. package/clis/crates/utils.js +72 -0
  72. package/clis/ctrip/ctrip.test.js +234 -0
  73. package/clis/ctrip/hotel-suggest.js +45 -0
  74. package/clis/ctrip/search.js +22 -68
  75. package/clis/ctrip/utils.js +175 -0
  76. package/clis/cursor/ask.js +6 -3
  77. package/clis/dblp/author.js +133 -0
  78. package/clis/dblp/venue.js +64 -0
  79. package/clis/deepseek/ask.js +12 -7
  80. package/clis/deepseek/ask.test.js +13 -13
  81. package/clis/deepseek/detail.js +38 -0
  82. package/clis/deepseek/detail.test.js +81 -0
  83. package/clis/deepseek/history.js +1 -0
  84. package/clis/deepseek/new.js +1 -0
  85. package/clis/deepseek/read.js +1 -0
  86. package/clis/deepseek/send.js +140 -0
  87. package/clis/deepseek/send.test.js +107 -0
  88. package/clis/deepseek/status.js +1 -0
  89. package/clis/deepseek/utils.js +66 -0
  90. package/clis/deepseek/utils.test.js +107 -1
  91. package/clis/defillama/defillama.test.js +99 -0
  92. package/clis/defillama/protocol.js +84 -0
  93. package/clis/defillama/protocols.js +55 -0
  94. package/clis/defillama/utils.js +99 -0
  95. package/clis/devto/latest.js +74 -0
  96. package/clis/dockerhub/image.js +52 -0
  97. package/clis/dockerhub/search.js +47 -0
  98. package/clis/dockerhub/utils.js +100 -0
  99. package/clis/doubao/ask.js +7 -3
  100. package/clis/doubao/detail.js +1 -0
  101. package/clis/doubao/history.js +1 -0
  102. package/clis/doubao/meeting-summary.js +1 -0
  103. package/clis/doubao/meeting-transcript.js +1 -0
  104. package/clis/doubao/new.js +1 -0
  105. package/clis/doubao/read.js +1 -0
  106. package/clis/doubao/send.js +1 -0
  107. package/clis/doubao/status.js +1 -0
  108. package/clis/douyin/draft.test.js +1 -30
  109. package/clis/endoflife/endoflife.test.js +51 -0
  110. package/clis/endoflife/product.js +55 -0
  111. package/clis/endoflife/utils.js +89 -0
  112. package/clis/facebook/__fixtures__/notifications-page.html +13 -0
  113. package/clis/facebook/notifications.js +326 -30
  114. package/clis/facebook/notifications.test.js +458 -0
  115. package/clis/flathub/app.js +71 -0
  116. package/clis/flathub/flathub.test.js +90 -0
  117. package/clis/flathub/search.js +80 -0
  118. package/clis/flathub/utils.js +114 -0
  119. package/clis/gemini/ask.js +7 -3
  120. package/clis/gemini/ask.test.js +2 -2
  121. package/clis/gemini/deep-research-result.js +6 -2
  122. package/clis/gemini/deep-research-result.test.js +15 -14
  123. package/clis/gemini/deep-research.js +8 -4
  124. package/clis/gemini/deep-research.test.js +15 -18
  125. package/clis/gemini/image.js +7 -2
  126. package/clis/gemini/new.js +1 -0
  127. package/clis/gemini/utils.js +0 -4
  128. package/clis/google-scholar/cite.js +0 -1
  129. package/clis/google-scholar/profile.js +0 -1
  130. package/clis/google-scholar/search.js +0 -1
  131. package/clis/goproxy/goproxy.test.js +103 -0
  132. package/clis/goproxy/module.js +47 -0
  133. package/clis/goproxy/utils.js +165 -0
  134. package/clis/goproxy/versions.js +59 -0
  135. package/clis/gov-law/recent.js +0 -1
  136. package/clis/gov-law/search.js +0 -1
  137. package/clis/gov-policy/__fixtures__/recent.html +16 -0
  138. package/clis/gov-policy/__fixtures__/search.html +41 -0
  139. package/clis/gov-policy/gov-policy.test.js +224 -0
  140. package/clis/gov-policy/recent.js +66 -24
  141. package/clis/gov-policy/search.js +65 -23
  142. package/clis/gov-policy/utils.js +54 -0
  143. package/clis/grok/ask.js +49 -265
  144. package/clis/grok/ask.test.js +21 -46
  145. package/clis/grok/detail.js +60 -0
  146. package/clis/grok/history.js +48 -0
  147. package/clis/grok/{image.ts → image.js} +56 -70
  148. package/clis/grok/image.test.ts +20 -0
  149. package/clis/grok/new.js +20 -0
  150. package/clis/grok/read.js +39 -0
  151. package/clis/grok/send.js +50 -0
  152. package/clis/grok/status.js +41 -0
  153. package/clis/grok/utils.js +326 -0
  154. package/clis/grok/utils.test.js +103 -0
  155. package/clis/hf/datasets.js +88 -0
  156. package/clis/hf/hf.test.js +16 -0
  157. package/clis/hf/models.js +91 -0
  158. package/clis/hf/paper.js +79 -0
  159. package/clis/hf/spaces.js +101 -0
  160. package/clis/hf/top.js +1 -0
  161. package/clis/homebrew/cask.js +39 -0
  162. package/clis/homebrew/formula.js +41 -0
  163. package/clis/homebrew/popular.js +54 -0
  164. package/clis/homebrew/utils.js +100 -0
  165. package/clis/hupu/__fixtures__/hot-home.html +64 -0
  166. package/clis/hupu/detail.js +0 -1
  167. package/clis/hupu/hot.js +156 -35
  168. package/clis/hupu/hot.test.js +224 -0
  169. package/clis/hupu/search.js +0 -1
  170. package/clis/instagram/note.js +1 -1
  171. package/clis/instagram/note.test.js +1 -29
  172. package/clis/instagram/post.js +1 -1
  173. package/clis/instagram/post.test.js +1 -1
  174. package/clis/instagram/reel.js +1 -1
  175. package/clis/instagram/story.js +1 -1
  176. package/clis/instagram/story.test.js +1 -34
  177. package/clis/jd/commands.test.js +1 -24
  178. package/clis/lichess/lichess.test.js +85 -0
  179. package/clis/lichess/top.js +46 -0
  180. package/clis/lichess/user.js +91 -0
  181. package/clis/lichess/utils.js +97 -0
  182. package/clis/linkedin/search.js +107 -10
  183. package/clis/linkedin/search.test.js +222 -0
  184. package/clis/linux-do/feed.js +2 -5
  185. package/clis/linux-do/feed.test.js +35 -0
  186. package/clis/lobsters/domain.js +92 -0
  187. package/clis/maven/artifact.js +49 -0
  188. package/clis/maven/search.js +51 -0
  189. package/clis/maven/utils.js +110 -0
  190. package/clis/mdn/search.js +97 -0
  191. package/clis/medium/tag.js +135 -0
  192. package/clis/npm/downloads.js +59 -0
  193. package/clis/npm/package.js +70 -0
  194. package/clis/npm/search.js +49 -0
  195. package/clis/npm/utils.js +76 -0
  196. package/clis/nuget/nuget.test.js +111 -0
  197. package/clis/nuget/package.js +101 -0
  198. package/clis/nuget/search.js +69 -0
  199. package/clis/nuget/utils.js +87 -0
  200. package/clis/nvd/cve.js +121 -0
  201. package/clis/oeis/oeis.test.js +88 -0
  202. package/clis/oeis/search.js +63 -0
  203. package/clis/oeis/sequence.js +71 -0
  204. package/clis/oeis/utils.js +88 -0
  205. package/clis/openalex/search.js +69 -0
  206. package/clis/openalex/utils.js +160 -0
  207. package/clis/openalex/work.js +65 -0
  208. package/clis/openfda/drug-label.js +74 -0
  209. package/clis/openfda/food-recall.js +65 -0
  210. package/clis/openfda/openfda.test.js +114 -0
  211. package/clis/openfda/utils.js +67 -0
  212. package/clis/osv/osv.test.js +97 -0
  213. package/clis/osv/query.js +72 -0
  214. package/clis/osv/utils.js +169 -0
  215. package/clis/osv/vulnerability.js +54 -0
  216. package/clis/packagist/package.js +49 -0
  217. package/clis/packagist/search.js +43 -0
  218. package/clis/packagist/utils.js +113 -0
  219. package/clis/paperreview/feedback.js +1 -1
  220. package/clis/paperreview/review.js +1 -1
  221. package/clis/paperreview/submit.js +1 -1
  222. package/clis/pixiv/download.test.js +1 -1
  223. package/clis/pixiv/illusts.test.js +1 -1
  224. package/clis/pixiv/search.test.js +1 -1
  225. package/clis/pubmed/article.js +50 -0
  226. package/clis/pubmed/author.js +64 -0
  227. package/clis/pubmed/citations.js +36 -0
  228. package/clis/pubmed/pubmed.test.js +276 -0
  229. package/clis/pubmed/related.js +45 -0
  230. package/clis/pubmed/search.js +75 -0
  231. package/clis/pubmed/utils.js +309 -0
  232. package/clis/pypi/downloads.js +66 -0
  233. package/clis/pypi/package.js +79 -0
  234. package/clis/pypi/utils.js +55 -0
  235. package/clis/quark/mv.js +1 -1
  236. package/clis/quark/save.js +1 -1
  237. package/clis/qwen/ask.js +85 -0
  238. package/clis/qwen/detail.js +62 -0
  239. package/clis/qwen/history.js +61 -0
  240. package/clis/qwen/image.js +179 -0
  241. package/clis/qwen/new.js +23 -0
  242. package/clis/qwen/read.js +41 -0
  243. package/clis/qwen/send.js +55 -0
  244. package/clis/qwen/status.js +37 -0
  245. package/clis/qwen/utils.js +409 -0
  246. package/clis/qwen/utils.test.js +45 -0
  247. package/clis/rest-countries/country.js +65 -0
  248. package/clis/rest-countries/region.js +64 -0
  249. package/clis/rest-countries/rest-countries.test.js +83 -0
  250. package/clis/rest-countries/utils.js +126 -0
  251. package/clis/reuters/article-detail.js +53 -0
  252. package/clis/reuters/reuters.test.js +299 -0
  253. package/clis/reuters/search.js +45 -34
  254. package/clis/reuters/utils.js +159 -0
  255. package/clis/rfc/rfc.js +52 -0
  256. package/clis/rfc/rfc.test.js +74 -0
  257. package/clis/rfc/utils.js +72 -0
  258. package/clis/rubygems/gem.js +42 -0
  259. package/clis/rubygems/search.js +47 -0
  260. package/clis/rubygems/utils.js +86 -0
  261. package/clis/stackoverflow/related.js +66 -0
  262. package/clis/stackoverflow/stackoverflow.test.js +58 -0
  263. package/clis/stackoverflow/tag.js +60 -0
  264. package/clis/stackoverflow/user.js +50 -0
  265. package/clis/stackoverflow/utils.js +118 -0
  266. package/clis/steam/app.js +67 -0
  267. package/clis/steam/search.js +58 -0
  268. package/clis/steam/steam.test.js +46 -0
  269. package/clis/steam/utils.js +107 -0
  270. package/clis/taobao/commands.test.js +1 -24
  271. package/clis/test-utils.js +61 -0
  272. package/clis/tieba/hot.js +0 -1
  273. package/clis/tiktok/comment.js +128 -41
  274. package/clis/tiktok/creator-videos.js +270 -0
  275. package/clis/tiktok/creator-videos.test.js +113 -0
  276. package/clis/tiktok/explore.js +137 -29
  277. package/clis/tiktok/follow.js +115 -33
  278. package/clis/tiktok/following.js +157 -36
  279. package/clis/tiktok/friends.js +139 -37
  280. package/clis/tiktok/live.js +137 -41
  281. package/clis/tiktok/notifications.js +141 -38
  282. package/clis/tiktok/refactor.test.js +389 -0
  283. package/clis/tiktok/unfollow.js +124 -38
  284. package/clis/tiktok/user.js +203 -29
  285. package/clis/tiktok/utils.js +505 -0
  286. package/clis/tiktok/write-refactor.test.js +370 -0
  287. package/clis/toutiao/articles.js +36 -62
  288. package/clis/toutiao/hot.js +63 -0
  289. package/clis/toutiao/toutiao.test.js +378 -0
  290. package/clis/toutiao/utils.js +161 -0
  291. package/clis/tvmaze/search.js +61 -0
  292. package/clis/tvmaze/show.js +60 -0
  293. package/clis/tvmaze/tvmaze.test.js +93 -0
  294. package/clis/tvmaze/utils.js +110 -0
  295. package/clis/twitter/accept.js +1 -1
  296. package/clis/twitter/followers.js +134 -69
  297. package/clis/twitter/reply-dm.js +1 -1
  298. package/clis/twitter/reply.test.js +1 -29
  299. package/clis/uisdc/news.js +105 -0
  300. package/clis/uisdc/news.test.js +66 -0
  301. package/clis/wanfang/search.js +0 -1
  302. package/clis/web/read.js +47 -17
  303. package/clis/web/read.test.js +101 -1
  304. package/clis/weixin/create-draft.js +1 -1
  305. package/clis/weixin/drafts.js +1 -1
  306. package/clis/weixin/drafts.test.js +5 -1
  307. package/clis/weixin/search.js +157 -0
  308. package/clis/weixin/search.test.js +227 -0
  309. package/clis/wikidata/entity.js +60 -0
  310. package/clis/wikidata/search.js +50 -0
  311. package/clis/wikidata/utils.js +117 -0
  312. package/clis/wikidata/wikidata.test.js +83 -0
  313. package/clis/wikipedia/page.js +95 -0
  314. package/clis/wttr/current.js +63 -0
  315. package/clis/wttr/forecast.js +71 -0
  316. package/clis/wttr/utils.js +50 -0
  317. package/clis/wttr/wttr.test.js +84 -0
  318. package/clis/xianyu/chat.js +16 -4
  319. package/clis/xianyu/chat.test.js +64 -0
  320. package/clis/xianyu/publish.js +485 -0
  321. package/clis/xianyu/publish.test.js +220 -0
  322. package/clis/xiaoe/catalog.js +105 -40
  323. package/clis/xiaoe/content.js +164 -29
  324. package/clis/xiaoe/courses.js +86 -29
  325. package/clis/xiaoe/xiaoe.test.js +486 -0
  326. package/clis/xiaohongshu/creator-notes-summary.js +1 -1
  327. package/clis/xiaohongshu/publish.js +16 -3
  328. package/clis/xiaohongshu/publish.test.js +46 -1
  329. package/clis/youtube/transcript.js +13 -19
  330. package/clis/youtube/transcript.test.js +17 -0
  331. package/clis/yuanbao/ask.js +17 -66
  332. package/clis/yuanbao/ask.test.js +5 -5
  333. package/clis/yuanbao/detail.js +65 -0
  334. package/clis/yuanbao/history.js +51 -0
  335. package/clis/yuanbao/new.js +1 -0
  336. package/clis/yuanbao/read.js +38 -0
  337. package/clis/yuanbao/send.js +57 -0
  338. package/clis/yuanbao/shared.js +297 -5
  339. package/clis/yuanbao/shared.test.js +80 -0
  340. package/clis/yuanbao/status.js +44 -0
  341. package/clis/zlibrary/commands.test.js +1 -11
  342. package/dist/src/browser/base-page.d.ts +9 -0
  343. package/dist/src/browser/base-page.js +44 -1
  344. package/dist/src/browser/base-page.test.js +66 -0
  345. package/dist/src/browser/cdp.d.ts +1 -0
  346. package/dist/src/browser/cdp.js +51 -9
  347. package/dist/src/browser/daemon-client.d.ts +4 -0
  348. package/dist/src/browser/errors.js +1 -1
  349. package/dist/src/browser/page.d.ts +1 -1
  350. package/dist/src/browser/page.js +3 -1
  351. package/dist/src/browser/page.test.js +29 -0
  352. package/dist/src/browser/target-errors.d.ts +2 -1
  353. package/dist/src/browser/target-errors.js +1 -0
  354. package/dist/src/browser/target-resolver.d.ts +25 -0
  355. package/dist/src/browser/target-resolver.js +43 -0
  356. package/dist/src/build-manifest.js +9 -4
  357. package/dist/src/build-manifest.test.js +2 -8
  358. package/dist/src/capabilityRouting.d.ts +16 -1
  359. package/dist/src/capabilityRouting.js +24 -1
  360. package/dist/src/capabilityRouting.test.js +19 -1
  361. package/dist/src/cli.js +76 -11
  362. package/dist/src/cli.test.js +150 -0
  363. package/dist/src/commanderAdapter.js +0 -5
  364. package/dist/src/commanderAdapter.test.js +0 -1
  365. package/dist/src/discovery.js +2 -5
  366. package/dist/src/errors.js +1 -1
  367. package/dist/src/execution.d.ts +1 -1
  368. package/dist/src/execution.js +111 -27
  369. package/dist/src/execution.test.js +326 -17
  370. package/dist/src/help.d.ts +23 -2
  371. package/dist/src/help.js +41 -19
  372. package/dist/src/help.test.d.ts +1 -0
  373. package/dist/src/help.test.js +54 -0
  374. package/dist/src/main.js +14 -1
  375. package/dist/src/manifest-types.d.ts +5 -3
  376. package/dist/src/pipeline/executor.js +1 -1
  377. package/dist/src/pipeline/executor.test.js +8 -0
  378. package/dist/src/pipeline/registry.d.ts +9 -0
  379. package/dist/src/pipeline/registry.js +13 -1
  380. package/dist/src/pipeline/steps/browser.d.ts +1 -0
  381. package/dist/src/pipeline/steps/browser.js +10 -0
  382. package/dist/src/pipeline/steps/download.test.js +1 -0
  383. package/dist/src/registry-api.d.ts +1 -1
  384. package/dist/src/registry.d.ts +12 -11
  385. package/dist/src/registry.js +16 -6
  386. package/dist/src/registry.test.js +2 -2
  387. package/dist/src/runtime.d.ts +2 -1
  388. package/dist/src/runtime.js +1 -1
  389. package/dist/src/serialization.d.ts +2 -2
  390. package/dist/src/serialization.js +4 -6
  391. package/dist/src/serialization.test.js +17 -0
  392. package/dist/src/types.d.ts +17 -0
  393. package/dist/src/validate.js +15 -11
  394. package/dist/src/validate.test.d.ts +9 -0
  395. package/dist/src/validate.test.js +90 -0
  396. package/package.json +1 -1
  397. package/scripts/fetch-adapters.js +1 -1
  398. package/scripts/typed-error-lint-baseline.json +5 -77
  399. package/clis/ctrip/search.test.js +0 -64
  400. package/clis/gov-policy/commands.test.js +0 -27
  401. package/clis/linux-do/category.js +0 -37
  402. package/clis/linux-do/hot.js +0 -26
  403. package/clis/linux-do/latest.js +0 -19
  404. package/clis/pixiv/test-utils.js +0 -23
  405. package/clis/toutiao/articles.test.js +0 -30
  406. package/dist/src/analysis.d.ts +0 -40
  407. package/dist/src/analysis.js +0 -172
@@ -2,44 +2,33 @@ import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import * as crypto from 'node:crypto';
4
4
  import { cli, Strategy } from '@jackwener/opencli/registry';
5
- import type { IPage } from '@jackwener/opencli/types';
5
+ import { ArgumentError, CommandExecutionError, TimeoutError } from '@jackwener/opencli/errors';
6
6
 
7
7
  const GROK_URL = 'https://grok.com/';
8
- const NO_IMAGE_PREFIX = '[NO IMAGE]';
9
- const BLOCKED_PREFIX = '[BLOCKED]';
10
8
  const SESSION_HINT = 'Likely login/auth/challenge/session issue in the existing grok.com browser session.';
11
9
 
12
- type SendResult = {
13
- ok?: boolean;
14
- msg?: string;
15
- reason?: string;
16
- detail?: string;
17
- };
18
-
19
- type BubbleImage = {
20
- src: string;
21
- w: number;
22
- h: number;
23
- };
24
-
25
- type BubbleImageSet = BubbleImage[];
26
-
27
- type FetchResult = {
28
- ok: boolean;
29
- base64?: string;
30
- contentType?: string;
31
- error?: string;
32
- };
33
-
34
- function normalizeBooleanFlag(value: unknown): boolean {
10
+ function normalizeBooleanFlag(value) {
35
11
  if (typeof value === 'boolean') return value;
36
12
  const normalized = String(value ?? '').trim().toLowerCase();
37
13
  return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';
38
14
  }
39
15
 
40
- function dedupeBySrc(images: BubbleImage[]): BubbleImage[] {
41
- const seen = new Set<string>();
42
- const out: BubbleImage[] = [];
16
+ /**
17
+ * Validate a positive-integer arg without silently flooring/clamping.
18
+ * Throws ArgumentError on `0`, negatives, non-integers, or non-numeric input.
19
+ */
20
+ function normalizePositiveInteger(value, defaultValue, label) {
21
+ const raw = value ?? defaultValue;
22
+ const n = Number(raw);
23
+ if (!Number.isInteger(n) || n <= 0) {
24
+ throw new ArgumentError(`${label} must be a positive integer`);
25
+ }
26
+ return n;
27
+ }
28
+
29
+ function dedupeBySrc(images) {
30
+ const seen = new Set();
31
+ const out = [];
43
32
  for (const img of images) {
44
33
  if (!img.src || seen.has(img.src)) continue;
45
34
  seen.add(img.src);
@@ -48,11 +37,11 @@ function dedupeBySrc(images: BubbleImage[]): BubbleImage[] {
48
37
  return out;
49
38
  }
50
39
 
51
- function imagesSignature(images: BubbleImage[]): string {
40
+ function imagesSignature(images) {
52
41
  return images.map(i => i.src).sort().join('|');
53
42
  }
54
43
 
55
- function extFromContentType(ct?: string): string {
44
+ function extFromContentType(ct) {
56
45
  if (!ct) return 'jpg';
57
46
  if (ct.includes('png')) return 'png';
58
47
  if (ct.includes('webp')) return 'webp';
@@ -60,14 +49,14 @@ function extFromContentType(ct?: string): string {
60
49
  return 'jpg';
61
50
  }
62
51
 
63
- function buildFilename(src: string, ct?: string): string {
52
+ function buildFilename(src, ct) {
64
53
  const ext = extFromContentType(ct);
65
54
  const hash = crypto.createHash('sha1').update(src).digest('hex').slice(0, 12);
66
55
  return `grok-${Date.now()}-${hash}.${ext}`;
67
56
  }
68
57
 
69
58
  /** Check whether the tab is already on grok.com (any path). */
70
- async function isOnGrok(page: IPage): Promise<boolean> {
59
+ async function isOnGrok(page) {
71
60
  const url = await page.evaluate('window.location.href').catch(() => '');
72
61
  if (typeof url !== 'string' || !url) return false;
73
62
  try {
@@ -78,7 +67,7 @@ async function isOnGrok(page: IPage): Promise<boolean> {
78
67
  }
79
68
  }
80
69
 
81
- async function tryStartFreshChat(page: IPage): Promise<void> {
70
+ async function tryStartFreshChat(page) {
82
71
  await page.evaluate(`(() => {
83
72
  const isVisible = (node) => {
84
73
  if (!(node instanceof HTMLElement)) return false;
@@ -102,7 +91,7 @@ async function tryStartFreshChat(page: IPage): Promise<void> {
102
91
  })()`);
103
92
  }
104
93
 
105
- async function sendPrompt(page: IPage, prompt: string): Promise<SendResult> {
94
+ async function sendPrompt(page, prompt) {
106
95
  const promptJson = JSON.stringify(prompt);
107
96
  return page.evaluate(`(async () => {
108
97
  try {
@@ -180,11 +169,11 @@ async function sendPrompt(page: IPage, prompt: string): Promise<SendResult> {
180
169
  box.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
181
170
  return { ok: true, msg: 'enter' };
182
171
  } catch (e) { return { ok: false, msg: e && e.toString ? e.toString() : String(e) }; }
183
- })()`) as Promise<SendResult>;
172
+ })()`);
184
173
  }
185
174
 
186
175
  /** Read <img> elements from all message bubbles so callers can filter by baseline. */
187
- async function getBubbleImageSets(page: IPage): Promise<BubbleImageSet[]> {
176
+ async function getBubbleImageSets(page) {
188
177
  const result = await page.evaluate(`(() => {
189
178
  const bubbles = document.querySelectorAll('div.message-bubble, [data-testid="message-bubble"]');
190
179
  return Array.from(bubbles).map(bubble => Array.from(bubble.querySelectorAll('img'))
@@ -196,16 +185,13 @@ async function getBubbleImageSets(page: IPage): Promise<BubbleImageSet[]> {
196
185
  .filter(i => i.src && /^https?:/.test(i.src))
197
186
  // Ignore tiny UI/avatar images that may live in the bubble chrome.
198
187
  .filter(i => (i.w === 0 || i.w >= 128) && (i.h === 0 || i.h >= 128)));
199
- })()`) as BubbleImageSet[] | undefined;
188
+ })()`);
200
189
 
201
190
  const raw = Array.isArray(result) ? result : [];
202
191
  return raw.map(dedupeBySrc);
203
192
  }
204
193
 
205
- function pickLatestImageCandidate(
206
- bubbleImageSets: BubbleImageSet[],
207
- baselineCount: number,
208
- ): BubbleImage[] {
194
+ function pickLatestImageCandidate(bubbleImageSets, baselineCount) {
209
195
  const freshSets = bubbleImageSets.slice(Math.max(0, baselineCount));
210
196
  for (let i = freshSets.length - 1; i >= 0; i -= 1) {
211
197
  if (freshSets[i].length) return freshSets[i];
@@ -216,7 +202,7 @@ function pickLatestImageCandidate(
216
202
  // Download through the browser's fetch so grok.com cookies and referer are
217
203
  // attached automatically — assets.grok.com is gated by Cloudflare and will
218
204
  // refuse direct curl/node downloads.
219
- async function fetchImageAsBase64(page: IPage, url: string): Promise<FetchResult> {
205
+ async function fetchImageAsBase64(page, url) {
220
206
  const urlJson = JSON.stringify(url);
221
207
  return page.evaluate(`(async () => {
222
208
  try {
@@ -229,21 +215,24 @@ async function fetchImageAsBase64(page: IPage, url: string): Promise<FetchResult
229
215
  for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
230
216
  return { ok: true, base64: btoa(binary), contentType: blob.type || 'image/jpeg' };
231
217
  } catch (e) { return { ok: false, error: e && e.message || String(e) }; }
232
- })()`) as Promise<FetchResult>;
218
+ })()`);
233
219
  }
234
220
 
235
- async function saveImages(
236
- page: IPage,
237
- images: BubbleImage[],
238
- outDir: string,
239
- ): Promise<Array<BubbleImage & { path: string }>> {
221
+ async function saveImages(page, images, outDir) {
240
222
  fs.mkdirSync(outDir, { recursive: true });
241
- const results: Array<BubbleImage & { path: string }> = [];
223
+ const results = [];
242
224
  for (const img of images) {
243
225
  const fetched = await fetchImageAsBase64(page, img.src);
244
226
  if (!fetched || !fetched.ok) {
245
- results.push({ ...img, path: `[DOWNLOAD FAILED] ${fetched?.error || 'unknown'}` });
246
- continue;
227
+ // Fail loudly on per-image download failure rather than emit a sentinel
228
+ // row with path = '[DOWNLOAD FAILED] ...' that downstream tools cannot
229
+ // distinguish from a real path. assets.grok.com is Cloudflare-gated, so
230
+ // a single 401/403 typically means the whole batch is unrecoverable.
231
+ const reason = fetched?.error ? `: ${fetched.error}` : '';
232
+ throw new CommandExecutionError(
233
+ `Failed to download grok image ${img.src}${reason}`,
234
+ 'assets.grok.com download requires the live grok.com browser session — verify the tab is logged in and try again.',
235
+ );
247
236
  }
248
237
  const filepath = path.join(outDir, buildFilename(img.src, fetched.contentType));
249
238
  fs.writeFileSync(filepath, Buffer.from(fetched.base64 || '', 'base64'));
@@ -252,7 +241,7 @@ async function saveImages(
252
241
  return results;
253
242
  }
254
243
 
255
- function toRow(img: BubbleImage, savedPath = '') {
244
+ function toRow(img, savedPath = '') {
256
245
  return { url: img.src, width: img.w, height: img.h, path: savedPath };
257
246
  }
258
247
 
@@ -264,6 +253,7 @@ export const imageCommand = cli({
264
253
  domain: 'grok.com',
265
254
  strategy: Strategy.COOKIE,
266
255
  browser: true,
256
+ browserSession: { reuse: 'site' },
267
257
  args: [
268
258
  { name: 'prompt', positional: true, type: 'string', required: true, help: 'Image generation prompt' },
269
259
  { name: 'timeout', type: 'int', default: 240, help: 'Max seconds to wait for the image (default: 240)' },
@@ -272,11 +262,11 @@ export const imageCommand = cli({
272
262
  { name: 'out', type: 'string', default: '', help: 'Directory to save downloaded images (uses browser session to bypass auth)' },
273
263
  ],
274
264
  columns: ['url', 'width', 'height', 'path'],
275
- func: async (page: IPage, kwargs: Record<string, any>) => {
276
- const prompt = kwargs.prompt as string;
277
- const timeoutMs = ((kwargs.timeout as number) || 240) * 1000;
265
+ func: async (page, kwargs) => {
266
+ const prompt = kwargs.prompt;
267
+ const timeoutMs = (kwargs.timeout || 240) * 1000;
278
268
  const newChat = normalizeBooleanFlag(kwargs.new);
279
- const minCount = Math.max(1, Number(kwargs.count || 1));
269
+ const minCount = normalizePositiveInteger(kwargs.count, 1, 'count');
280
270
  const outDir = (kwargs.out || '').toString().trim();
281
271
 
282
272
  if (newChat) {
@@ -292,18 +282,16 @@ export const imageCommand = cli({
292
282
  const baselineBubbleCount = (await getBubbleImageSets(page)).length;
293
283
  const sendResult = await sendPrompt(page, prompt);
294
284
  if (!sendResult || !sendResult.ok) {
295
- return [{
296
- url: `${BLOCKED_PREFIX} send failed: ${JSON.stringify(sendResult)}. ${SESSION_HINT}`,
297
- width: 0,
298
- height: 0,
299
- path: '',
300
- }];
285
+ throw new CommandExecutionError(
286
+ `Grok composer rejected the prompt: ${JSON.stringify(sendResult)}`,
287
+ SESSION_HINT,
288
+ );
301
289
  }
302
290
 
303
291
  const startTime = Date.now();
304
292
  let lastSignature = '';
305
293
  let stableCount = 0;
306
- let lastImages: BubbleImage[] = [];
294
+ let lastImages = [];
307
295
 
308
296
  while (Date.now() - startTime < timeoutMs) {
309
297
  await page.wait(3);
@@ -330,6 +318,8 @@ export const imageCommand = cli({
330
318
  }
331
319
  }
332
320
 
321
+ // Timeout — keep best-effort partial results if any image bubble showed
322
+ // up, otherwise surface the timeout instead of a sentinel row.
333
323
  if (lastImages.length) {
334
324
  if (outDir) {
335
325
  const saved = await saveImages(page, lastImages, outDir);
@@ -337,17 +327,13 @@ export const imageCommand = cli({
337
327
  }
338
328
  return lastImages.map(i => toRow(i));
339
329
  }
340
- return [{
341
- url: `${NO_IMAGE_PREFIX} No image appeared within ${Math.round(timeoutMs / 1000)}s.`,
342
- width: 0,
343
- height: 0,
344
- path: '',
345
- }];
330
+ throw new TimeoutError('grok image generation', Math.round(timeoutMs / 1000));
346
331
  },
347
332
  });
348
333
 
349
334
  export const __test__ = {
350
335
  normalizeBooleanFlag,
336
+ normalizePositiveInteger,
351
337
  isOnGrok,
352
338
  dedupeBySrc,
353
339
  imagesSignature,
@@ -1,5 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import type { IPage } from '@jackwener/opencli/types';
3
+ import { ArgumentError } from '@jackwener/opencli/errors';
3
4
  import { __test__ } from './image.js';
4
5
 
5
6
  describe('grok image helpers', () => {
@@ -95,6 +96,25 @@ describe('grok image helpers', () => {
95
96
  ]);
96
97
  });
97
98
 
99
+ describe('normalizePositiveInteger', () => {
100
+ it('returns a parsed positive integer', () => {
101
+ expect(__test__.normalizePositiveInteger(3, 1, 'count')).toBe(3);
102
+ expect(__test__.normalizePositiveInteger('5', 1, 'count')).toBe(5);
103
+ });
104
+
105
+ it('falls back to the default when the user did not pass a value', () => {
106
+ expect(__test__.normalizePositiveInteger(undefined, 1, 'count')).toBe(1);
107
+ expect(__test__.normalizePositiveInteger(null, 4, 'count')).toBe(4);
108
+ });
109
+
110
+ it('throws ArgumentError instead of silently clamping out-of-range input', () => {
111
+ expect(() => __test__.normalizePositiveInteger(0, 1, 'count')).toThrow(ArgumentError);
112
+ expect(() => __test__.normalizePositiveInteger(-1, 1, 'count')).toThrow(ArgumentError);
113
+ expect(() => __test__.normalizePositiveInteger(1.5, 1, 'count')).toThrow(ArgumentError);
114
+ expect(() => __test__.normalizePositiveInteger('not-a-number', 1, 'count')).toThrow(ArgumentError);
115
+ });
116
+ });
117
+
98
118
  it('does not reuse stale images when no new image bubble appears after baseline', () => {
99
119
  const candidate = __test__.pickLatestImageCandidate([
100
120
  [{ src: 'https://a.example/stale.jpg', w: 512, h: 512 }],
@@ -0,0 +1,20 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { GROK_DOMAIN, startNewChat } from './utils.js';
3
+
4
+ cli({
5
+ site: 'grok',
6
+ name: 'new',
7
+ access: 'write',
8
+ description: 'Start a new conversation in Grok',
9
+ domain: GROK_DOMAIN,
10
+ strategy: Strategy.COOKIE,
11
+ browser: true,
12
+ browserSession: { reuse: 'site' },
13
+ navigateBefore: false,
14
+ args: [],
15
+ columns: ['Status'],
16
+ func: async (page) => {
17
+ await startNewChat(page);
18
+ return [{ Status: 'New chat started' }];
19
+ },
20
+ });
@@ -0,0 +1,39 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import {
3
+ GROK_DOMAIN,
4
+ bubbleHtmlToMarkdown,
5
+ ensureOnGrok,
6
+ getMessageBubbles,
7
+ normalizeBooleanFlag,
8
+ } from './utils.js';
9
+
10
+ cli({
11
+ site: 'grok',
12
+ name: 'read',
13
+ access: 'read',
14
+ description: 'Read messages in the current Grok conversation',
15
+ domain: GROK_DOMAIN,
16
+ strategy: Strategy.COOKIE,
17
+ browser: true,
18
+ browserSession: { reuse: 'site' },
19
+ navigateBefore: false,
20
+ args: [
21
+ { name: 'markdown', type: 'boolean', default: false, help: 'Emit assistant replies as markdown' },
22
+ ],
23
+ columns: ['Role', 'Text'],
24
+ func: async (page, kwargs) => {
25
+ const wantMarkdown = normalizeBooleanFlag(kwargs.markdown, false);
26
+ await ensureOnGrok(page);
27
+ await page.wait(2);
28
+ const bubbles = await getMessageBubbles(page);
29
+ if (!bubbles.length) {
30
+ return [{ Role: 'system', Text: 'No visible messages in the current conversation.' }];
31
+ }
32
+ return bubbles.map((b) => ({
33
+ Role: b.role,
34
+ Text: wantMarkdown && b.role === 'Assistant' && b.html
35
+ ? (bubbleHtmlToMarkdown(b.html) || b.text)
36
+ : b.text,
37
+ }));
38
+ },
39
+ });
@@ -0,0 +1,50 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors';
3
+ import {
4
+ GROK_DOMAIN,
5
+ authRequired,
6
+ ensureOnGrok,
7
+ isLoggedIn,
8
+ normalizeBooleanFlag,
9
+ sendMessage,
10
+ startNewChat,
11
+ } from './utils.js';
12
+
13
+ cli({
14
+ site: 'grok',
15
+ name: 'send',
16
+ access: 'write',
17
+ description: 'Fire-and-forget: send a prompt to Grok without waiting for the reply',
18
+ domain: GROK_DOMAIN,
19
+ strategy: Strategy.COOKIE,
20
+ browser: true,
21
+ browserSession: { reuse: 'site' },
22
+ navigateBefore: false,
23
+ args: [
24
+ { name: 'prompt', required: true, positional: true, help: 'Prompt to send to Grok' },
25
+ { name: 'new', type: 'boolean', default: false, help: 'Start a new chat before sending' },
26
+ ],
27
+ columns: ['Status', 'Prompt'],
28
+ func: async (page, kwargs) => {
29
+ const prompt = String(kwargs.prompt || '').trim();
30
+ if (!prompt) throw new ArgumentError('prompt', 'is required');
31
+ const startFresh = normalizeBooleanFlag(kwargs.new, false);
32
+
33
+ await ensureOnGrok(page);
34
+ if (startFresh) {
35
+ await startNewChat(page);
36
+ }
37
+
38
+ const send = await sendMessage(page, prompt);
39
+ if (!send?.ok) {
40
+ // If the composer is missing, the most likely cause is that the
41
+ // signed-in session expired (Grok then renders a sign-in CTA in
42
+ // place of the composer). Surface that as AuthRequiredError so
43
+ // agents can prompt for re-auth instead of treating it as a
44
+ // generic execution failure.
45
+ if (!(await isLoggedIn(page))) throw authRequired();
46
+ throw new CommandExecutionError(send?.reason || 'Failed to send Grok prompt');
47
+ }
48
+ return [{ Status: 'sent', Prompt: prompt }];
49
+ },
50
+ });
@@ -0,0 +1,41 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import {
3
+ GROK_DOMAIN,
4
+ ensureOnGrok,
5
+ getCurrentSessionId,
6
+ getModelLabel,
7
+ isLoggedIn,
8
+ } from './utils.js';
9
+
10
+ cli({
11
+ site: 'grok',
12
+ name: 'status',
13
+ access: 'read',
14
+ description: 'Check Grok page availability, login state, current session and model',
15
+ domain: GROK_DOMAIN,
16
+ strategy: Strategy.COOKIE,
17
+ browser: true,
18
+ browserSession: { reuse: 'site' },
19
+ navigateBefore: false,
20
+ args: [],
21
+ columns: ['Status', 'Login', 'Model', 'SessionId', 'Url'],
22
+ func: async (page) => {
23
+ await ensureOnGrok(page);
24
+ await page.wait(2);
25
+ const [loggedIn, sessionId, model, url] = await Promise.all([
26
+ isLoggedIn(page),
27
+ getCurrentSessionId(page),
28
+ getModelLabel(page),
29
+ page.evaluate('window.location.href').catch(() => ''),
30
+ ]);
31
+ // Surface unknowns as `null` rather than a sentinel string so agents
32
+ // can branch cleanly.
33
+ return [{
34
+ Status: 'Connected',
35
+ Login: loggedIn ? 'Yes' : 'No',
36
+ Model: model ? model : null,
37
+ SessionId: sessionId ? sessionId : null,
38
+ Url: typeof url === 'string' ? url : '',
39
+ }];
40
+ },
41
+ });