@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
@@ -202,16 +202,58 @@ class CDPPage extends BasePage {
202
202
  : cookies;
203
203
  }
204
204
  async screenshot(options = {}) {
205
- const result = await this.bridge.send('Page.captureScreenshot', {
206
- format: options.format ?? 'png',
207
- quality: options.format === 'jpeg' ? (options.quality ?? 80) : undefined,
208
- captureBeyondViewport: options.fullPage ?? false,
209
- });
210
- const base64 = isRecord(result) && typeof result.data === 'string' ? result.data : '';
211
- if (options.path) {
212
- await saveBase64ToFile(base64, options.path);
205
+ const fullPage = options.fullPage === true;
206
+ const overrideWidth = options.width && options.width > 0 ? Math.ceil(options.width) : undefined;
207
+ // height is ignored under fullPage so the captureBeyondViewport path stays unchanged for users who pass --height alongside --full-page.
208
+ const overrideHeight = !fullPage && options.height && options.height > 0 ? Math.ceil(options.height) : undefined;
209
+ const needsOverride = overrideWidth !== undefined || overrideHeight !== undefined;
210
+ if (needsOverride) {
211
+ if (overrideWidth !== undefined && fullPage) {
212
+ await this.bridge.send('Emulation.setDeviceMetricsOverride', {
213
+ mobile: false,
214
+ width: overrideWidth,
215
+ height: 0,
216
+ deviceScaleFactor: 1,
217
+ });
218
+ }
219
+ let finalWidth = overrideWidth ?? 0;
220
+ let finalHeight = overrideHeight ?? 0;
221
+ if (fullPage) {
222
+ const metrics = await this.bridge.send('Page.getLayoutMetrics');
223
+ const m = isRecord(metrics) ? metrics : {};
224
+ const css = isRecord(m.cssContentSize) ? m.cssContentSize : undefined;
225
+ const fb = isRecord(m.contentSize) ? m.contentSize : undefined;
226
+ const size = css ?? fb;
227
+ if (size && typeof size.width === 'number' && typeof size.height === 'number') {
228
+ if (finalWidth === 0)
229
+ finalWidth = Math.ceil(size.width);
230
+ finalHeight = Math.ceil(size.height);
231
+ }
232
+ }
233
+ await this.bridge.send('Emulation.setDeviceMetricsOverride', {
234
+ mobile: false,
235
+ width: finalWidth,
236
+ height: finalHeight,
237
+ deviceScaleFactor: 1,
238
+ });
239
+ }
240
+ try {
241
+ const result = await this.bridge.send('Page.captureScreenshot', {
242
+ format: options.format ?? 'png',
243
+ quality: options.format === 'jpeg' ? (options.quality ?? 80) : undefined,
244
+ captureBeyondViewport: !needsOverride && fullPage,
245
+ });
246
+ const base64 = isRecord(result) && typeof result.data === 'string' ? result.data : '';
247
+ if (options.path) {
248
+ await saveBase64ToFile(base64, options.path);
249
+ }
250
+ return base64;
251
+ }
252
+ finally {
253
+ if (needsOverride) {
254
+ await this.bridge.send('Emulation.clearDeviceMetricsOverride').catch(() => { });
255
+ }
213
256
  }
214
- return base64;
215
257
  }
216
258
  async startNetworkCapture(pattern = '') {
217
259
  // Always update the filter pattern
@@ -20,6 +20,10 @@ export interface DaemonCommand {
20
20
  format?: 'png' | 'jpeg';
21
21
  quality?: number;
22
22
  fullPage?: boolean;
23
+ /** Override viewport width in CSS pixels for screenshot (0 / undefined = use current) */
24
+ width?: number;
25
+ /** Override viewport height in CSS pixels for screenshot (0 / undefined = use current; ignored when fullPage) */
26
+ height?: number;
23
27
  /** Local file paths for set-file-input action */
24
28
  files?: string[];
25
29
  /** CSS selector for file input element (set-file-input action) */
@@ -65,7 +65,7 @@ export function isTransientBrowserError(err) {
65
65
  export function formatBrowserConnectError(kind, detail) {
66
66
  switch (kind) {
67
67
  case 'daemon-not-running':
68
- return new BrowserConnectError('Cannot connect to opencli daemon.' + (detail ? `\n\n${detail}` : ''), `The daemon should auto-start. If it keeps failing, make sure port ${DEFAULT_DAEMON_PORT} is available.`, kind);
68
+ return new BrowserConnectError('Cannot connect to opencli daemon.' + (detail ? `\n\n${detail}` : ''), `Run \`opencli doctor\` to diagnose, or \`opencli daemon restart\` to force a fresh daemon. Default port is ${DEFAULT_DAEMON_PORT}.`, kind);
69
69
  case 'extension-not-connected':
70
70
  return new BrowserConnectError('Browser Bridge extension is not connected.' + (detail ? `\n\n${detail}` : ''), 'Install the extension from GitHub Releases, then reload.', kind);
71
71
  case 'command-failed':
@@ -41,7 +41,7 @@ export declare class Page extends BasePage {
41
41
  domain?: string;
42
42
  url?: string;
43
43
  }): Promise<BrowserCookie[]>;
44
- /** Close the automation window in the extension */
44
+ /** Release the current automation tab lease in the extension */
45
45
  closeWindow(): Promise<void>;
46
46
  tabs(): Promise<unknown[]>;
47
47
  newTab(url?: string): Promise<string | undefined>;
@@ -143,7 +143,7 @@ export class Page extends BasePage {
143
143
  const result = await sendCommand('cookies', { ...this._wsOpt(), ...opts });
144
144
  return Array.isArray(result) ? result : [];
145
145
  }
146
- /** Close the automation window in the extension */
146
+ /** Release the current automation tab lease in the extension */
147
147
  async closeWindow() {
148
148
  try {
149
149
  await sendCommand('close-window', { ...this._wsOpt() });
@@ -205,6 +205,8 @@ export class Page extends BasePage {
205
205
  format: options.format,
206
206
  quality: options.quality,
207
207
  fullPage: options.fullPage,
208
+ width: options.width,
209
+ height: options.height,
208
210
  });
209
211
  if (options.path) {
210
212
  await saveBase64ToFile(base64, options.path);
@@ -216,3 +216,32 @@ describe('Page active target tracking', () => {
216
216
  expect(evalCall?.[1]).not.toHaveProperty('page');
217
217
  });
218
218
  });
219
+ describe('Page.screenshot', () => {
220
+ beforeEach(() => {
221
+ sendCommandMock.mockReset();
222
+ sendCommandFullMock.mockReset();
223
+ warnMock.mockReset();
224
+ });
225
+ it('forwards width / height / fullPage options to the bridge', async () => {
226
+ sendCommandMock.mockResolvedValueOnce('BASE64');
227
+ const page = new Page('browser:default');
228
+ const data = await page.screenshot({ fullPage: true, width: 1080 });
229
+ expect(data).toBe('BASE64');
230
+ expect(sendCommandMock).toHaveBeenCalledWith('screenshot', expect.objectContaining({
231
+ workspace: 'browser:default',
232
+ fullPage: true,
233
+ width: 1080,
234
+ }));
235
+ });
236
+ it('omits viewport overrides when none are set', async () => {
237
+ sendCommandMock.mockResolvedValueOnce('BASE64');
238
+ const page = new Page('browser:default');
239
+ await page.screenshot();
240
+ const call = sendCommandMock.mock.calls.at(-1);
241
+ expect(call?.[0]).toBe('screenshot');
242
+ const args = call?.[1];
243
+ expect(args.width).toBeUndefined();
244
+ expect(args.height).toBeUndefined();
245
+ expect(args.fullPage).toBeUndefined();
246
+ });
247
+ });
@@ -15,8 +15,9 @@
15
15
  * - selector_not_found: 0 matches
16
16
  * - selector_ambiguous: >1 matches for a write op without --nth
17
17
  * - selector_nth_out_of_range: --nth beyond matches_n
18
+ * - not_editable: target exists but cannot accept text input
18
19
  */
19
- export type TargetErrorCode = 'not_found' | 'stale_ref' | 'invalid_selector' | 'selector_not_found' | 'selector_ambiguous' | 'selector_nth_out_of_range';
20
+ export type TargetErrorCode = 'not_found' | 'stale_ref' | 'invalid_selector' | 'selector_not_found' | 'selector_ambiguous' | 'selector_nth_out_of_range' | 'not_editable';
20
21
  export interface TargetErrorInfo {
21
22
  code: TargetErrorCode;
22
23
  message: string;
@@ -15,6 +15,7 @@
15
15
  * - selector_not_found: 0 matches
16
16
  * - selector_ambiguous: >1 matches for a write op without --nth
17
17
  * - selector_nth_out_of_range: --nth beyond matches_n
18
+ * - not_editable: target exists but cannot accept text input
18
19
  */
19
20
  export class TargetError extends Error {
20
21
  code;
@@ -79,6 +79,25 @@ export declare function clickResolvedJs(opts?: {
79
79
  * Generate JS for type that uses the unified resolver.
80
80
  */
81
81
  export declare function typeResolvedJs(text: string): string;
82
+ export type FillResolvedResult = {
83
+ ok: true;
84
+ actual: string;
85
+ expected: string;
86
+ length: number;
87
+ mode: 'input' | 'textarea' | 'contenteditable';
88
+ } | {
89
+ ok: false;
90
+ actual: string;
91
+ expected: string;
92
+ length: number;
93
+ mode: 'input' | 'textarea' | 'contenteditable';
94
+ } | {
95
+ ok: false;
96
+ reason: string;
97
+ tag?: string;
98
+ type?: string;
99
+ role?: string;
100
+ };
82
101
  /**
83
102
  * Prepare the resolved element for native CDP Input.insertText.
84
103
  *
@@ -90,6 +109,12 @@ export declare function prepareNativeTypeResolvedJs(opts?: {
90
109
  skipScroll?: boolean;
91
110
  skipFocus?: boolean;
92
111
  }): string;
112
+ /**
113
+ * Verify the exact value/text currently held by the resolved editable target.
114
+ * Assumes resolveTargetJs and prepareNativeTypeResolvedJs have already set
115
+ * `window.__resolved` to the normalized editable host.
116
+ */
117
+ export declare function verifyFilledResolvedJs(expected: string): string;
93
118
  /**
94
119
  * Generate JS for scrollTo that uses the unified resolver.
95
120
  * Assumes resolveTargetJs has been called and __resolved is set.
@@ -411,6 +411,49 @@ export function prepareNativeTypeResolvedJs(opts = {}) {
411
411
  })()
412
412
  `;
413
413
  }
414
+ /**
415
+ * Verify the exact value/text currently held by the resolved editable target.
416
+ * Assumes resolveTargetJs and prepareNativeTypeResolvedJs have already set
417
+ * `window.__resolved` to the normalized editable host.
418
+ */
419
+ export function verifyFilledResolvedJs(expected) {
420
+ const safeText = JSON.stringify(expected);
421
+ return `
422
+ (() => {
423
+ const el = window.__resolved;
424
+ if (!el) return { ok: false, reason: 'no_resolved_element' };
425
+
426
+ const tag = el.tagName ? el.tagName.toLowerCase() : '';
427
+ const isInput = el instanceof HTMLInputElement;
428
+ const isTextarea = el instanceof HTMLTextAreaElement;
429
+ const mode = el.isContentEditable
430
+ ? 'contenteditable'
431
+ : isTextarea
432
+ ? 'textarea'
433
+ : isInput
434
+ ? 'input'
435
+ : '';
436
+
437
+ if (!mode) {
438
+ return {
439
+ ok: false,
440
+ reason: 'not_editable',
441
+ tag,
442
+ role: el.getAttribute ? (el.getAttribute('role') || '') : '',
443
+ };
444
+ }
445
+
446
+ const actual = mode === 'contenteditable' ? (el.innerText || '') : String(el.value || '');
447
+ return {
448
+ ok: actual === ${safeText},
449
+ actual,
450
+ expected: ${safeText},
451
+ length: actual.length,
452
+ mode,
453
+ };
454
+ })()
455
+ `;
456
+ }
414
457
  /**
415
458
  * Generate JS for scrollTo that uses the unified resolver.
416
459
  * Assumes resolveTargetJs has been called and __resolved is set.
@@ -32,7 +32,13 @@ const PACKAGE_ROOT = findPackageRoot(fileURLToPath(import.meta.url));
32
32
  const CLIS_DIR = path.join(PACKAGE_ROOT, 'clis');
33
33
  // Write manifest next to clis/ so both dev and installed runtime can find it.
34
34
  const OUTPUT = getCliManifestPath(CLIS_DIR);
35
- const CLI_MODULE_PATTERN = /\bcli\s*\(/;
35
+ // Module is treated as a CLI command source if it either:
36
+ // 1. Calls `cli(...)` directly (the common case), or
37
+ // 2. Calls a factory `make<Pascal>Command(...)` from clis/_shared/ that
38
+ // wraps `cli(...)`. Without (2), shared-factory adapters
39
+ // (codex/cursor/chatwise new/status/dump/screenshot) match no `cli(`
40
+ // token at the top level and silently drop out of the manifest.
41
+ const CLI_MODULE_PATTERN = /\bcli\s*\(|\bmake[A-Z]\w*Command\s*\(/;
36
42
  /**
37
43
  * Thrown by `loadManifestEntries` when an adapter file looks like a CLI
38
44
  * module (matches CLI_MODULE_PATTERN) but cannot be imported. Callers
@@ -91,13 +97,12 @@ function toManifestEntry(cmd, modulePath, sourceFile) {
91
97
  browser: cmd.browser ?? true,
92
98
  args: toManifestArgs(cmd.args),
93
99
  columns: cmd.columns,
94
- timeout: cmd.timeoutSeconds,
95
- deprecated: cmd.deprecated,
96
- replacedBy: cmd.replacedBy,
100
+ defaultFormat: cmd.defaultFormat,
97
101
  type: 'js',
98
102
  modulePath,
99
103
  sourceFile,
100
104
  navigateBefore: cmd.navigateBefore,
105
+ browserSession: cmd.browserSession,
101
106
  };
102
107
  }
103
108
  /**
@@ -46,8 +46,7 @@ describe('manifest helper rules', () => {
46
46
  ],
47
47
  domain: 'localhost',
48
48
  navigateBefore: 'https://example.com/session',
49
- deprecated: 'legacy command',
50
- replacedBy: 'opencli demo new',
49
+ defaultFormat: 'plain',
51
50
  }),
52
51
  }));
53
52
  expect(entries).toMatchObject([
@@ -74,8 +73,7 @@ describe('manifest helper rules', () => {
74
73
  type: 'js',
75
74
  modulePath: `${site}/${site}.js`,
76
75
  navigateBefore: 'https://example.com/session',
77
- deprecated: 'legacy command',
78
- replacedBy: 'opencli demo new',
76
+ defaultFormat: 'plain',
79
77
  },
80
78
  ]);
81
79
  // Verify sourceFile is included and stable for manifest consumers.
@@ -96,8 +94,6 @@ describe('manifest helper rules', () => {
96
94
  name: 'legacy',
97
95
  access: 'read',
98
96
  description: 'legacy command',
99
- deprecated: 'legacy is deprecated',
100
- replacedBy: 'opencli demo new',
101
97
  });
102
98
  return {};
103
99
  });
@@ -112,8 +108,6 @@ describe('manifest helper rules', () => {
112
108
  args: [],
113
109
  type: 'js',
114
110
  modulePath: `${site}/${site}.js`,
115
- deprecated: 'legacy is deprecated',
116
- replacedBy: 'opencli demo new',
117
111
  },
118
112
  ]);
119
113
  // Verify sourceFile is included
@@ -1,4 +1,19 @@
1
1
  import type { CliCommand } from './registry.js';
2
- /** Pipeline steps that require a live browser session. */
2
+ /**
3
+ * Pipeline steps that require a live browser session.
4
+ *
5
+ * Note: this is the *subset* of registered pipeline steps that need a page;
6
+ * non-browser steps (fetch, select, map, filter, sort, limit, download)
7
+ * deliberately stay out. The full registered-step list lives in
8
+ * `src/pipeline/registry.ts` and is re-derived elsewhere via
9
+ * `getRegisteredStepNames()` (e.g. in `validate.ts`). When you add a new
10
+ * pipeline step, decide whether it belongs here based on whether its handler
11
+ * touches the IPage object — and `src/capabilityRouting.test.ts` verifies the
12
+ * subset relationship is intact.
13
+ */
3
14
  export declare const BROWSER_ONLY_STEPS: Set<string>;
15
+ /** Internal helper: ensure BROWSER_ONLY_STEPS is a subset of registered pipeline steps. */
16
+ export declare function _validateBrowserOnlyStepsAgainstRegistry(): {
17
+ extras: string[];
18
+ };
4
19
  export declare function shouldUseBrowserSession(cmd: CliCommand): boolean;
@@ -1,8 +1,21 @@
1
- /** Pipeline steps that require a live browser session. */
1
+ import { getRegisteredStepNames } from './pipeline/registry.js';
2
+ /**
3
+ * Pipeline steps that require a live browser session.
4
+ *
5
+ * Note: this is the *subset* of registered pipeline steps that need a page;
6
+ * non-browser steps (fetch, select, map, filter, sort, limit, download)
7
+ * deliberately stay out. The full registered-step list lives in
8
+ * `src/pipeline/registry.ts` and is re-derived elsewhere via
9
+ * `getRegisteredStepNames()` (e.g. in `validate.ts`). When you add a new
10
+ * pipeline step, decide whether it belongs here based on whether its handler
11
+ * touches the IPage object — and `src/capabilityRouting.test.ts` verifies the
12
+ * subset relationship is intact.
13
+ */
2
14
  export const BROWSER_ONLY_STEPS = new Set([
3
15
  'navigate',
4
16
  'click',
5
17
  'type',
18
+ 'fill',
6
19
  'wait',
7
20
  'press',
8
21
  'snapshot',
@@ -10,6 +23,16 @@ export const BROWSER_ONLY_STEPS = new Set([
10
23
  'intercept',
11
24
  'tap',
12
25
  ]);
26
+ /** Internal helper: ensure BROWSER_ONLY_STEPS is a subset of registered pipeline steps. */
27
+ export function _validateBrowserOnlyStepsAgainstRegistry() {
28
+ const registered = new Set(getRegisteredStepNames());
29
+ const extras = [];
30
+ for (const step of BROWSER_ONLY_STEPS) {
31
+ if (!registered.has(step))
32
+ extras.push(step);
33
+ }
34
+ return { extras };
35
+ }
13
36
  function pipelineNeedsBrowserSession(pipeline) {
14
37
  return pipeline.some((step) => {
15
38
  if (!step || typeof step !== 'object')
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { Strategy } from './registry.js';
3
- import { shouldUseBrowserSession } from './capabilityRouting.js';
3
+ import { BROWSER_ONLY_STEPS, _validateBrowserOnlyStepsAgainstRegistry, shouldUseBrowserSession } from './capabilityRouting.js';
4
+ import { getRegisteredStepNames } from './pipeline/registry.js';
4
5
  function makeCmd(partial) {
5
6
  return {
6
7
  site: 'test',
@@ -42,4 +43,21 @@ describe('shouldUseBrowserSession', () => {
42
43
  func: async () => [],
43
44
  }))).toBe(true);
44
45
  });
46
+ it('routes pipelines containing the fill step into a browser session', () => {
47
+ expect(shouldUseBrowserSession(makeCmd({
48
+ browser: true,
49
+ strategy: Strategy.PUBLIC,
50
+ pipeline: [{ navigate: 'https://example.com' }, { fill: { ref: '#q', text: 'hello' } }],
51
+ }))).toBe(true);
52
+ });
53
+ });
54
+ describe('BROWSER_ONLY_STEPS / pipeline registry alignment', () => {
55
+ it('is a subset of registered pipeline step names', () => {
56
+ const { extras } = _validateBrowserOnlyStepsAgainstRegistry();
57
+ expect(extras).toEqual([]);
58
+ });
59
+ it('includes fill (DOM-touching step added in PR #1222)', () => {
60
+ expect(BROWSER_ONLY_STEPS.has('fill')).toBe(true);
61
+ expect(getRegisteredStepNames()).toContain('fill');
62
+ });
45
63
  });
package/dist/src/cli.js CHANGED
@@ -8,7 +8,7 @@ import * as fs from 'node:fs';
8
8
  import * as os from 'node:os';
9
9
  import * as path from 'node:path';
10
10
  import { fileURLToPath } from 'node:url';
11
- import { Command } from 'commander';
11
+ import { Command, InvalidArgumentError } from 'commander';
12
12
  import { styleText } from 'node:util';
13
13
  import { findPackageRoot, getBuiltEntryCandidates } from './package-paths.js';
14
14
  import { fullName, getRegistry, strategyLabel } from './registry.js';
@@ -18,7 +18,7 @@ import { PKG_VERSION } from './version.js';
18
18
  import { printCompletionScript } from './completion.js';
19
19
  import { loadExternalClis, executeExternalCli, installExternalCli, registerExternalCli, isBinaryInstalled } from './external.js';
20
20
  import { registerAllCommands } from './commanderAdapter.js';
21
- import { formatRootAdapterHelpText, installStructuredHelp, rootHelpData } from './help.js';
21
+ import { classifyAdapter, formatRootAdapterHelpText, installStructuredHelp, rootHelpData } from './help.js';
22
22
  import { EXIT_CODES, getErrorMessage, BrowserConnectError } from './errors.js';
23
23
  import { TargetError } from './browser/target-errors.js';
24
24
  import { resolveTargetJs, getTextResolvedJs, getValueResolvedJs, getAttributesResolvedJs, selectResolvedJs, isAutocompleteResolvedJs } from './browser/target-resolver.js';
@@ -316,7 +316,9 @@ async function resolveStoredBrowserTarget(page, scope = DEFAULT_BROWSER_WORKSPAC
316
316
  async function getBrowserPage(targetPage, workspace = DEFAULT_BROWSER_WORKSPACE, contextId) {
317
317
  const { BrowserBridge } = await import('./browser/index.js');
318
318
  const bridge = new BrowserBridge();
319
- const envTimeout = process.env.OPENCLI_BROWSER_TIMEOUT;
319
+ // Idle timeout: how long the browser workspace lease stays alive between commands
320
+ // (controls when the automation tab is released). Not the per-command runtime timeout.
321
+ const envTimeout = process.env.OPENCLI_BROWSER_IDLE_TIMEOUT;
320
322
  const idleTimeout = envTimeout ? parseInt(envTimeout, 10) : undefined;
321
323
  const page = await bridge.connect({
322
324
  timeout: 30,
@@ -389,6 +391,16 @@ function parsePositiveIntOption(val, label, fallback) {
389
391
  }
390
392
  return parsed;
391
393
  }
394
+ function parseScreenshotDim(val, label) {
395
+ if (!/^\d+$/.test(val)) {
396
+ throw new InvalidArgumentError(`--${label} must be a positive integer (got "${val}")`);
397
+ }
398
+ const parsed = parseInt(val, 10);
399
+ if (parsed <= 0) {
400
+ throw new InvalidArgumentError(`--${label} must be a positive integer (got "${val}")`);
401
+ }
402
+ return parsed;
403
+ }
392
404
  function applyVerbose(opts) {
393
405
  if (opts.verbose)
394
406
  process.env.OPENCLI_VERBOSE = '1';
@@ -888,14 +900,22 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
888
900
  console.log(JSON.stringify(frames, null, 2));
889
901
  }));
890
902
  addBrowserTabOption(browser.command('screenshot').argument('[path]', 'Save to file (base64 if omitted)'))
903
+ .option('--full-page', 'Capture the full scrollable page, not just the viewport', false)
904
+ .option('--width <n>', 'Override viewport width in CSS pixels for this screenshot only', (v) => parseScreenshotDim(v, 'width'))
905
+ .option('--height <n>', 'Override viewport height in CSS pixels for this screenshot only (ignored with --full-page)', (v) => parseScreenshotDim(v, 'height'))
891
906
  .description('Take screenshot')
892
- .action(browserAction(async (page, path) => {
907
+ .action(browserAction(async (page, path, opts) => {
908
+ const shotOpts = {
909
+ fullPage: opts.fullPage === true,
910
+ width: opts.width,
911
+ height: opts.height,
912
+ };
893
913
  if (path) {
894
- await page.screenshot({ path });
914
+ await page.screenshot({ ...shotOpts, path });
895
915
  console.log(`Screenshot saved to: ${path}`);
896
916
  }
897
917
  else {
898
- console.log(await page.screenshot({ format: 'png' }));
918
+ console.log(await page.screenshot({ ...shotOpts, format: 'png' }));
899
919
  }
900
920
  }));
901
921
  addBrowserTabOption(browser.command('console'))
@@ -1313,6 +1333,33 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
1313
1333
  autocomplete: !!isAutocomplete,
1314
1334
  }, null, 2));
1315
1335
  }));
1336
+ addBrowserTabOption(browser.command('fill')
1337
+ .argument('<target>', 'Numeric ref (from browser state / find) or CSS selector')
1338
+ .argument('<text>', 'Text to set exactly')
1339
+ .option('--nth <n>', 'When <target> is a multi-match CSS selector, pick the nth match (0-based)')
1340
+ .description('Set input/textarea/contenteditable text exactly and verify the value — JSON envelope {filled, verified, text, actual}'))
1341
+ .action(browserAction(async (page, target, text, opts) => {
1342
+ const parsed = nthToResolveOpts(opts?.nth);
1343
+ if ('error' in parsed) {
1344
+ console.log(JSON.stringify({ error: { code: 'usage_error', message: parsed.error } }, null, 2));
1345
+ process.exitCode = EXIT_CODES.USAGE_ERROR;
1346
+ return;
1347
+ }
1348
+ const result = await page.fillText(String(target), String(text), parsed.opts);
1349
+ if (!result.verified)
1350
+ process.exitCode = EXIT_CODES.GENERIC_ERROR;
1351
+ console.log(JSON.stringify({
1352
+ filled: result.filled,
1353
+ verified: result.verified,
1354
+ target: String(target),
1355
+ text: String(text),
1356
+ actual: result.actual,
1357
+ length: result.length,
1358
+ matches_n: result.matches_n,
1359
+ match_level: result.match_level,
1360
+ ...(result.mode ? { mode: result.mode } : {}),
1361
+ }, null, 2));
1362
+ }));
1316
1363
  addBrowserTabOption(browser.command('select')
1317
1364
  .argument('<target>', 'Numeric ref (from browser state / find) or CSS selector of a <select> element')
1318
1365
  .argument('<option>', 'Option text (or value) to select')
@@ -2036,10 +2083,10 @@ cli({
2036
2083
  }
2037
2084
  });
2038
2085
  // ── Session ──
2039
- browser.command('close').description('Close the automation window')
2086
+ browser.command('close').description('Release the current automation tab lease')
2040
2087
  .action(browserAction(async (page) => {
2041
2088
  await page.closeWindow?.();
2042
- console.log('Automation window closed');
2089
+ console.log('Automation tab lease released');
2043
2090
  }));
2044
2091
  // ── Built-in: doctor / completion ──────────────────────────────────────────
2045
2092
  program
@@ -2539,11 +2586,29 @@ cli({
2539
2586
  siteGroups.set('antigravity', antigravityCmd);
2540
2587
  const siteNames = registerAllCommands(program, siteGroups);
2541
2588
  applyRootSubcommandSummaries(program);
2542
- const siteNameSet = new Set(siteNames);
2589
+ // ── Help-text grouping: External CLIs / App adapters / Site adapters ──
2590
+ // Classification derives from each adapter's `domain` field — see classifyAdapter.
2591
+ // External CLIs are taken from the externalClis registry (passthrough binaries).
2592
+ const externalNames = externalClis.map(ext => ext.name);
2593
+ const siteDomains = new Map();
2594
+ for (const [, cmd] of getRegistry()) {
2595
+ if (!siteDomains.has(cmd.site))
2596
+ siteDomains.set(cmd.site, cmd.domain);
2597
+ }
2598
+ const apps = [];
2599
+ const sites = [];
2600
+ for (const site of siteNames) {
2601
+ if (classifyAdapter(siteDomains.get(site)) === 'app')
2602
+ apps.push(site);
2603
+ else
2604
+ sites.push(site);
2605
+ }
2606
+ const adapterGroups = { external: externalNames, apps, sites };
2607
+ const adapterNameSet = new Set([...externalNames, ...siteNames]);
2543
2608
  program.configureHelp({
2544
- visibleCommands: (command) => command.commands.filter(child => command !== program || !siteNameSet.has(child.name())),
2609
+ visibleCommands: (command) => command.commands.filter(child => command !== program || !adapterNameSet.has(child.name())),
2545
2610
  });
2546
- installStructuredHelp(program, () => rootHelpData(program, siteNames), () => formatRootAdapterHelpText(siteNames));
2611
+ installStructuredHelp(program, () => rootHelpData(program, adapterGroups), () => formatRootAdapterHelpText(adapterGroups));
2547
2612
  // ── Unknown command fallback ──────────────────────────────────────────────
2548
2613
  // Security: do NOT auto-discover and register arbitrary system binaries.
2549
2614
  // Only explicitly registered external CLIs are allowed.