@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
package/clis/grok/ask.js CHANGED
@@ -1,263 +1,26 @@
1
1
  import { cli, Strategy } from '@jackwener/opencli/registry';
2
- const GROK_URL = 'https://grok.com/';
3
- const RESPONSE_SELECTOR = 'div.message-bubble, [data-testid="message-bubble"]';
4
- const BLOCKED_PREFIX = '[BLOCKED]';
5
- const NO_RESPONSE_PREFIX = '[NO RESPONSE]';
2
+ import { CommandExecutionError, TimeoutError } from '@jackwener/opencli/errors';
3
+ import {
4
+ authRequired,
5
+ ensureOnGrok,
6
+ getMessageBubbles,
7
+ isLoggedIn,
8
+ normalizeBooleanFlag,
9
+ sendMessage,
10
+ startNewChat,
11
+ waitForAnswer,
12
+ } from './utils.js';
13
+
6
14
  const SESSION_HINT = 'Likely login/auth/challenge/session issue in the existing grok.com browser session.';
7
- function blocked(message) {
8
- return [{ response: `${BLOCKED_PREFIX} ${message} ${SESSION_HINT}` }];
9
- }
10
- function normalizeBubbleText(value) {
11
- return typeof value === 'string' ? value.trim() : '';
12
- }
13
- function normalizeBooleanFlag(value) {
14
- if (typeof value === 'boolean')
15
- return value;
16
- const normalized = String(value ?? '').trim().toLowerCase();
17
- return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';
18
- }
19
- function pickLatestAssistantCandidate(bubbles, baselineCount, prompt) {
20
- const normalizedPrompt = prompt.trim();
21
- const freshBubbles = bubbles
22
- .slice(Math.max(0, baselineCount))
23
- .map(normalizeBubbleText)
24
- .filter(Boolean);
25
- for (let i = freshBubbles.length - 1; i >= 0; i -= 1) {
26
- if (freshBubbles[i] !== normalizedPrompt)
27
- return freshBubbles[i];
15
+
16
+ async function getBaselineLastAssistantId(page) {
17
+ const bubbles = await getMessageBubbles(page);
18
+ for (let i = bubbles.length - 1; i >= 0; i -= 1) {
19
+ if (bubbles[i].role === 'Assistant') return bubbles[i].id;
28
20
  }
29
21
  return '';
30
22
  }
31
- function updateStableState(previousText, stableCount, nextText) {
32
- if (!nextText)
33
- return { previousText: '', stableCount: 0 };
34
- if (nextText === previousText)
35
- return { previousText, stableCount: stableCount + 1 };
36
- return { previousText: nextText, stableCount: 0 };
37
- }
38
- /** Check whether the tab is already on grok.com (any path). */
39
- async function isOnGrok(page) {
40
- // catch handles blank tabs (about:blank) or detached pages
41
- const url = await page.evaluate('window.location.href').catch(() => '');
42
- if (typeof url !== 'string' || !url)
43
- return false;
44
- try {
45
- const hostname = new URL(url).hostname;
46
- return hostname === 'grok.com' || hostname.endsWith('.grok.com');
47
- }
48
- catch {
49
- return false;
50
- }
51
- }
52
- async function runDefaultAsk(page, prompt, timeoutMs, newChat) {
53
- if (newChat) {
54
- // Explicitly start a fresh conversation via the homepage
55
- await page.goto(GROK_URL);
56
- await page.wait(2);
57
- await tryStartFreshChat(page);
58
- await page.wait(2);
59
- }
60
- else if (!(await isOnGrok(page))) {
61
- // First invocation or tab was recycled — navigate to Grok
62
- await page.goto(GROK_URL);
63
- await page.wait(3);
64
- }
65
- const promptJson = JSON.stringify(prompt);
66
- const sendResult = await page.evaluate(`(async () => {
67
- try {
68
- const box = document.querySelector('textarea');
69
- if (!box) return { ok: false, msg: 'no textarea' };
70
- box.focus(); box.value = '';
71
- document.execCommand('selectAll');
72
- document.execCommand('insertText', false, ${promptJson});
73
- await new Promise(r => setTimeout(r, 1500));
74
- const btn = document.querySelector('button[aria-label="\\u63d0\\u4ea4"]');
75
- if (btn && !btn.disabled) { btn.click(); return { ok: true, msg: 'clicked' }; }
76
- const sub = [...document.querySelectorAll('button[type="submit"]')].find(b => !b.disabled);
77
- if (sub) { sub.click(); return { ok: true, msg: 'clicked-submit' }; }
78
- box.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
79
- return { ok: true, msg: 'enter' };
80
- } catch (e) { return { ok: false, msg: e.toString() }; }
81
- })()`);
82
- if (!sendResult || !sendResult.ok) {
83
- return [{ response: '[SEND FAILED] ' + JSON.stringify(sendResult) }];
84
- }
85
- const startTime = Date.now();
86
- let lastText = '';
87
- let stableCount = 0;
88
- while (Date.now() - startTime < timeoutMs) {
89
- await page.wait(3);
90
- const response = await page.evaluate(`(() => {
91
- const bubbles = document.querySelectorAll('div.message-bubble, [data-testid="message-bubble"]');
92
- if (bubbles.length < 2) return '';
93
- const last = bubbles[bubbles.length - 1];
94
- const text = (last.innerText || '').trim();
95
- if (!text || text.length < 2) return '';
96
- return text;
97
- })()`);
98
- if (response && response.length > 2) {
99
- if (response === lastText) {
100
- stableCount++;
101
- if (stableCount >= 2)
102
- return [{ response }];
103
- }
104
- else {
105
- stableCount = 0;
106
- }
107
- }
108
- lastText = response || '';
109
- }
110
- if (lastText)
111
- return [{ response: lastText }];
112
- return [{ response: NO_RESPONSE_PREFIX }];
113
- }
114
- async function getBubbleTexts(page) {
115
- const result = await page.evaluate(`(() => {
116
- return Array.from(document.querySelectorAll(${JSON.stringify(RESPONSE_SELECTOR)}))
117
- .map(node => (node instanceof HTMLElement ? node.innerText : node?.textContent || ''))
118
- .map(text => (typeof text === 'string' ? text.trim() : ''))
119
- .filter(Boolean);
120
- })()`);
121
- return Array.isArray(result) ? result.map(normalizeBubbleText).filter(Boolean) : [];
122
- }
123
- async function tryStartFreshChat(page) {
124
- await page.evaluate(`(() => {
125
- const isVisible = (node) => {
126
- if (!(node instanceof HTMLElement)) return false;
127
- const rect = node.getBoundingClientRect();
128
- const style = window.getComputedStyle(node);
129
- return rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none';
130
- };
131
-
132
- const candidates = Array.from(document.querySelectorAll('a, button')).filter(node => {
133
- if (!isVisible(node)) return false;
134
- const text = (node.textContent || '').trim().toLowerCase();
135
- const aria = (node.getAttribute('aria-label') || '').trim().toLowerCase();
136
- const href = node.getAttribute('href') || '';
137
- return text.includes('new chat')
138
- || text.includes('new conversation')
139
- || aria.includes('new chat')
140
- || aria.includes('new conversation')
141
- || href === '/';
142
- });
143
-
144
- const target = candidates[0];
145
- if (target instanceof HTMLElement) target.click();
146
- })()`);
147
- }
148
- async function sendPromptViaExplicitWeb(page, prompt) {
149
- return page.evaluate(`(async () => {
150
- const waitFor = (ms) => new Promise(resolve => setTimeout(resolve, ms));
151
- const composerSelector = '.ProseMirror[contenteditable="true"]';
152
- let composer = null;
153
-
154
- for (let attempt = 0; attempt < 12; attempt += 1) {
155
- const candidate = document.querySelector(composerSelector);
156
- if (candidate instanceof HTMLElement) {
157
- composer = candidate;
158
- break;
159
- }
160
-
161
- await waitFor(1000);
162
- }
163
-
164
- if (!(composer instanceof HTMLElement)) {
165
- return {
166
- ok: false,
167
- reason: 'Grok composer was not found on grok.com.',
168
- };
169
- }
170
-
171
- const editor = composer.editor;
172
- if (!editor?.commands?.focus || !editor?.commands?.insertContent) {
173
- return {
174
- ok: false,
175
- reason: 'Grok composer editor API was unavailable.',
176
- };
177
- }
178
23
 
179
- const isVisibleEnabledSubmit = (node) => {
180
- if (!(node instanceof HTMLButtonElement)) return false;
181
- const rect = node.getBoundingClientRect();
182
- const style = window.getComputedStyle(node);
183
- return !node.disabled
184
- && rect.width > 0
185
- && rect.height > 0
186
- && style.visibility !== 'hidden'
187
- && style.display !== 'none';
188
- };
189
-
190
- try {
191
- if (editor.commands.clearContent) editor.commands.clearContent();
192
- editor.commands.focus();
193
- editor.commands.insertContent(${JSON.stringify(prompt)});
194
- } catch (error) {
195
- return {
196
- ok: false,
197
- reason: 'Failed to insert the prompt into the Grok composer.',
198
- detail: error instanceof Error ? error.message : String(error),
199
- };
200
- }
201
-
202
- let submit = null;
203
- for (let attempt = 0; attempt < 6; attempt += 1) {
204
- const candidate = Array.from(document.querySelectorAll('button[aria-label="Submit"]'))
205
- .find(isVisibleEnabledSubmit);
206
-
207
- if (candidate instanceof HTMLButtonElement) {
208
- submit = candidate;
209
- break;
210
- }
211
-
212
- await waitFor(500);
213
- }
214
-
215
- if (!(submit instanceof HTMLButtonElement)) {
216
- return {
217
- ok: false,
218
- reason: 'Grok submit button did not reach a clickable ready state after prompt insertion.',
219
- };
220
- }
221
-
222
- submit.click();
223
- return { ok: true };
224
- })()`);
225
- }
226
- async function runExplicitWebAsk(page, prompt, timeoutMs, newChat) {
227
- if (newChat) {
228
- // Navigate to homepage and start a fresh conversation
229
- await page.goto(GROK_URL, { settleMs: 2000 });
230
- await tryStartFreshChat(page);
231
- await page.wait(2);
232
- }
233
- else if (!(await isOnGrok(page))) {
234
- // First invocation or tab was recycled — navigate to Grok
235
- await page.goto(GROK_URL, { settleMs: 2000 });
236
- }
237
- const baselineBubbles = await getBubbleTexts(page);
238
- const sendResult = await sendPromptViaExplicitWeb(page, prompt);
239
- if (!sendResult?.ok) {
240
- const details = sendResult?.detail ? ` ${sendResult.detail}` : '';
241
- return blocked(`${sendResult?.reason || 'Unable to send the prompt to Grok.'}${details}`);
242
- }
243
- const startTime = Date.now();
244
- let lastText = '';
245
- let stableCount = 0;
246
- while (Date.now() - startTime < timeoutMs) {
247
- await page.wait(2);
248
- const bubbleTexts = await getBubbleTexts(page);
249
- const candidate = pickLatestAssistantCandidate(bubbleTexts, baselineBubbles.length, prompt);
250
- const nextState = updateStableState(lastText, stableCount, candidate);
251
- lastText = nextState.previousText;
252
- stableCount = nextState.stableCount;
253
- if (candidate && stableCount >= 2) {
254
- return [{ response: candidate }];
255
- }
256
- }
257
- if (lastText)
258
- return [{ response: lastText }];
259
- return [{ response: `${NO_RESPONSE_PREFIX} No new assistant message bubble appeared within ${Math.round(timeoutMs / 1000)}s.` }];
260
- }
261
24
  export const askCommand = cli({
262
25
  site: 'grok',
263
26
  name: 'ask',
@@ -266,28 +29,49 @@ export const askCommand = cli({
266
29
  domain: 'grok.com',
267
30
  strategy: Strategy.COOKIE,
268
31
  browser: true,
32
+ browserSession: { reuse: 'site' },
269
33
  args: [
270
34
  { name: 'prompt', positional: true, type: 'string', required: true, help: 'Prompt to send to Grok' },
271
35
  { name: 'timeout', type: 'int', default: 120, help: 'Max seconds to wait for response (default: 120)' },
272
36
  { name: 'new', type: 'boolean', default: false, help: 'Start a new chat before sending (default: false)' },
273
- { name: 'web', type: 'boolean', default: false, help: 'Use the explicit grok.com consumer web flow (default: false)' },
274
37
  ],
275
38
  columns: ['response'],
276
39
  func: async (page, kwargs) => {
277
40
  const prompt = kwargs.prompt;
278
- const timeoutMs = (kwargs.timeout || 120) * 1000;
41
+ const timeoutSeconds = kwargs.timeout || 120;
279
42
  const newChat = normalizeBooleanFlag(kwargs.new);
280
- const useExplicitWeb = normalizeBooleanFlag(kwargs.web);
281
- if (useExplicitWeb) {
282
- return runExplicitWebAsk(page, prompt, timeoutMs, newChat);
43
+
44
+ if (newChat) {
45
+ await startNewChat(page);
46
+ } else {
47
+ await ensureOnGrok(page);
283
48
  }
284
- return runDefaultAsk(page, prompt, timeoutMs, newChat);
49
+
50
+ if (!(await isLoggedIn(page))) {
51
+ throw authRequired();
52
+ }
53
+
54
+ const baselineLastAssistantId = await getBaselineLastAssistantId(page);
55
+ const sendResult = await sendMessage(page, prompt);
56
+ if (!sendResult || !sendResult.ok) {
57
+ const reason = sendResult?.reason || 'Unable to send the prompt to Grok.';
58
+ const detail = sendResult?.detail ? ` ${sendResult.detail}` : '';
59
+ throw new CommandExecutionError(`${reason}${detail}`, SESSION_HINT);
60
+ }
61
+
62
+ const result = await waitForAnswer(page, prompt, timeoutSeconds, baselineLastAssistantId);
63
+ if (result.status === 'ok') {
64
+ return [{ response: result.assistant.text }];
65
+ }
66
+ // Partial: streaming was seen but did not stabilize; keep the best-effort
67
+ // text rather than throwing — the caller asked us to wait, not to discard.
68
+ if (result.status === 'partial' && result.assistant) {
69
+ return [{ response: result.assistant.text }];
70
+ }
71
+ throw new TimeoutError('grok ask response', timeoutSeconds);
285
72
  },
286
73
  });
74
+
287
75
  export const __test__ = {
288
- pickLatestAssistantCandidate,
289
- updateStableState,
290
- normalizeBooleanFlag,
291
- normalizeBubbleText,
292
- isOnGrok,
76
+ getBaselineLastAssistantId,
293
77
  };
@@ -1,54 +1,29 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { __test__ } from './ask.js';
3
+
3
4
  describe('grok ask helpers', () => {
4
- describe('isOnGrok', () => {
5
- const fakePage = (url) => ({ evaluate: () => url instanceof Error ? Promise.reject(url) : Promise.resolve(url) });
6
- it('returns true for grok.com URLs', async () => {
7
- expect(await __test__.isOnGrok(fakePage('https://grok.com/'))).toBe(true);
8
- expect(await __test__.isOnGrok(fakePage('https://grok.com/chat/abc123'))).toBe(true);
5
+ describe('getBaselineLastAssistantId', () => {
6
+ const fakePage = (bubbles) => ({
7
+ // getMessageBubbles in utils.js drives a page.evaluate; in tests we
8
+ // route directly to the in-memory bubble array.
9
+ evaluate: () => Promise.resolve(bubbles),
9
10
  });
10
- it('returns true for grok.com subdomains', async () => {
11
- expect(await __test__.isOnGrok(fakePage('https://api.grok.com/v1'))).toBe(true);
11
+
12
+ it('returns the id of the most recent Assistant bubble, ignoring later User turns', async () => {
13
+ const bubbles = [
14
+ { id: 'a1', role: 'Assistant', text: 'first answer', html: '' },
15
+ { id: 'u1', role: 'User', text: 'follow-up', html: '' },
16
+ { id: 'a2', role: 'Assistant', text: 'second answer', html: '' },
17
+ { id: 'u2', role: 'User', text: 'newest user turn', html: '' },
18
+ ];
19
+ expect(await __test__.getBaselineLastAssistantId(fakePage(bubbles))).toBe('a2');
12
20
  });
13
- it('returns false for non-grok domains', async () => {
14
- expect(await __test__.isOnGrok(fakePage('https://fakegrok.com/'))).toBe(false);
15
- expect(await __test__.isOnGrok(fakePage('https://example.com/?next=grok.com'))).toBe(false);
16
- expect(await __test__.isOnGrok(fakePage('about:blank'))).toBe(false);
17
- });
18
- it('returns false when evaluate throws (detached tab)', async () => {
19
- expect(await __test__.isOnGrok(fakePage(new Error('detached')))).toBe(false);
20
- });
21
- });
22
- it('normalizes boolean flags for explicit web routing', () => {
23
- expect(__test__.normalizeBooleanFlag(true)).toBe(true);
24
- expect(__test__.normalizeBooleanFlag('true')).toBe(true);
25
- expect(__test__.normalizeBooleanFlag('1')).toBe(true);
26
- expect(__test__.normalizeBooleanFlag('yes')).toBe(true);
27
- expect(__test__.normalizeBooleanFlag('on')).toBe(true);
28
- expect(__test__.normalizeBooleanFlag(false)).toBe(false);
29
- expect(__test__.normalizeBooleanFlag('false')).toBe(false);
30
- expect(__test__.normalizeBooleanFlag(undefined)).toBe(false);
31
- });
32
- it('ignores baseline bubbles and the echoed prompt when choosing the latest assistant candidate', () => {
33
- const candidate = __test__.pickLatestAssistantCandidate(['older assistant answer', 'Prompt text', 'Assistant draft', 'Assistant final'], 1, 'Prompt text');
34
- expect(candidate).toBe('Assistant final');
35
- });
36
- it('returns empty when only the echoed prompt appeared after send', () => {
37
- const candidate = __test__.pickLatestAssistantCandidate(['older assistant answer', 'Prompt text'], 1, 'Prompt text');
38
- expect(candidate).toBe('');
39
- });
40
- it('tracks stabilization by incrementing repeats and resetting on changes', () => {
41
- expect(__test__.updateStableState('', 0, 'First chunk')).toEqual({
42
- previousText: 'First chunk',
43
- stableCount: 0,
44
- });
45
- expect(__test__.updateStableState('First chunk', 0, 'First chunk')).toEqual({
46
- previousText: 'First chunk',
47
- stableCount: 1,
48
- });
49
- expect(__test__.updateStableState('First chunk', 1, 'Second chunk')).toEqual({
50
- previousText: 'Second chunk',
51
- stableCount: 0,
21
+
22
+ it('returns empty string when no Assistant bubble exists yet (fresh chat)', async () => {
23
+ expect(await __test__.getBaselineLastAssistantId(fakePage([]))).toBe('');
24
+ expect(await __test__.getBaselineLastAssistantId(fakePage([
25
+ { id: 'u1', role: 'User', text: 'hello', html: '' },
26
+ ]))).toBe('');
52
27
  });
53
28
  });
54
29
  });
@@ -0,0 +1,60 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { EmptyResultError } from '@jackwener/opencli/errors';
3
+ import {
4
+ GROK_DOMAIN,
5
+ bubbleHtmlToMarkdown,
6
+ getMessageBubbles,
7
+ normalizeBooleanFlag,
8
+ parseGrokSessionId,
9
+ } from './utils.js';
10
+
11
+ cli({
12
+ site: 'grok',
13
+ name: 'detail',
14
+ access: 'read',
15
+ description: 'Open a Grok conversation by ID and read its messages',
16
+ domain: GROK_DOMAIN,
17
+ strategy: Strategy.COOKIE,
18
+ browser: true,
19
+ navigateBefore: false,
20
+ browserSession: { reuse: 'site' },
21
+ args: [
22
+ { name: 'id', positional: true, required: true, help: 'Session ID (UUID) or full https://grok.com/c/<id> URL' },
23
+ { name: 'markdown', type: 'boolean', default: false, help: 'Emit assistant replies as markdown' },
24
+ ],
25
+ columns: ['Role', 'Text'],
26
+ func: async (page, kwargs) => {
27
+ const sessionId = parseGrokSessionId(kwargs.id);
28
+ const wantMarkdown = normalizeBooleanFlag(kwargs.markdown, false);
29
+
30
+ await page.goto(`https://grok.com/c/${sessionId}`);
31
+ await page.wait(2);
32
+
33
+ // Poll for the conversation transcript to load. Grok renders the chat
34
+ // page shell before fetching message history, so a fixed wait can race
35
+ // the initial empty render. Cap at ~20s so genuinely missing IDs surface
36
+ // an EmptyResultError rather than hanging.
37
+ let bubbles = [];
38
+ const POLL_DEADLINE_MS = 20_000;
39
+ const POLL_INTERVAL_S = 1;
40
+ const startedAt = Date.now();
41
+ while (Date.now() - startedAt < POLL_DEADLINE_MS) {
42
+ bubbles = await getMessageBubbles(page);
43
+ if (bubbles.length > 0) break;
44
+ await page.wait(POLL_INTERVAL_S);
45
+ }
46
+
47
+ if (!bubbles.length) {
48
+ throw new EmptyResultError(
49
+ 'grok detail',
50
+ `No visible messages found for conversation ${sessionId}. Verify the ID is correct and that the conversation belongs to the signed-in account.`,
51
+ );
52
+ }
53
+ return bubbles.map((b) => ({
54
+ Role: b.role,
55
+ Text: wantMarkdown && b.role === 'Assistant' && b.html
56
+ ? (bubbleHtmlToMarkdown(b.html) || b.text)
57
+ : b.text,
58
+ }));
59
+ },
60
+ });
@@ -0,0 +1,48 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { ArgumentError, EmptyResultError } from '@jackwener/opencli/errors';
3
+ import {
4
+ GROK_DOMAIN,
5
+ authRequired,
6
+ ensureOnGrok,
7
+ getHistoryFromSidebar,
8
+ isLoggedIn,
9
+ } from './utils.js';
10
+
11
+ cli({
12
+ site: 'grok',
13
+ name: 'history',
14
+ access: 'read',
15
+ description: 'List recent Grok conversations from the sidebar (requires login)',
16
+ domain: GROK_DOMAIN,
17
+ strategy: Strategy.COOKIE,
18
+ browser: true,
19
+ browserSession: { reuse: 'site' },
20
+ navigateBefore: false,
21
+ args: [
22
+ { name: 'limit', type: 'int', default: 20, help: 'Max conversations to show (default 20, max 100)' },
23
+ ],
24
+ columns: ['Index', 'Title', 'Url'],
25
+ func: async (page, kwargs) => {
26
+ const limit = Number(kwargs.limit ?? 20);
27
+ if (!Number.isInteger(limit) || limit <= 0) {
28
+ throw new ArgumentError('limit', 'must be a positive integer');
29
+ }
30
+ if (limit > 100) {
31
+ throw new ArgumentError('limit', 'must be <= 100');
32
+ }
33
+ await ensureOnGrok(page);
34
+ await page.wait(2);
35
+ if (!(await isLoggedIn(page))) {
36
+ throw authRequired();
37
+ }
38
+ const sessions = await getHistoryFromSidebar(page, limit);
39
+ if (!sessions.length) {
40
+ throw new EmptyResultError('grok history', 'No Grok conversations found in the sidebar for the signed-in account.');
41
+ }
42
+ return sessions.slice(0, limit).map((s, i) => ({
43
+ Index: i + 1,
44
+ Title: s.title || '(untitled)',
45
+ Url: `https://grok.com/c/${s.id}`,
46
+ }));
47
+ },
48
+ });