@jackwener/opencli 1.7.12 → 1.7.14

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 (419) hide show
  1. package/README.md +8 -7
  2. package/README.zh-CN.md +9 -8
  3. package/cli-manifest.json +12128 -6665
  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/quote.js +139 -0
  298. package/clis/twitter/quote.test.js +106 -0
  299. package/clis/twitter/reply-dm.js +1 -1
  300. package/clis/twitter/reply.test.js +1 -29
  301. package/clis/twitter/retweet.js +99 -0
  302. package/clis/twitter/retweet.test.js +69 -0
  303. package/clis/twitter/shared.js +38 -0
  304. package/clis/twitter/shared.test.js +28 -1
  305. package/clis/twitter/unlike.js +87 -0
  306. package/clis/twitter/unlike.test.js +72 -0
  307. package/clis/twitter/unretweet.js +99 -0
  308. package/clis/twitter/unretweet.test.js +69 -0
  309. package/clis/uisdc/news.js +105 -0
  310. package/clis/uisdc/news.test.js +66 -0
  311. package/clis/wanfang/search.js +0 -1
  312. package/clis/web/read.js +47 -17
  313. package/clis/web/read.test.js +101 -1
  314. package/clis/weixin/create-draft.js +1 -1
  315. package/clis/weixin/drafts.js +1 -1
  316. package/clis/weixin/drafts.test.js +5 -1
  317. package/clis/weixin/search.js +157 -0
  318. package/clis/weixin/search.test.js +227 -0
  319. package/clis/wikidata/entity.js +60 -0
  320. package/clis/wikidata/search.js +50 -0
  321. package/clis/wikidata/utils.js +117 -0
  322. package/clis/wikidata/wikidata.test.js +83 -0
  323. package/clis/wikipedia/page.js +95 -0
  324. package/clis/wttr/current.js +63 -0
  325. package/clis/wttr/forecast.js +71 -0
  326. package/clis/wttr/utils.js +50 -0
  327. package/clis/wttr/wttr.test.js +84 -0
  328. package/clis/xianyu/chat.js +16 -4
  329. package/clis/xianyu/chat.test.js +64 -0
  330. package/clis/xianyu/publish.js +485 -0
  331. package/clis/xianyu/publish.test.js +220 -0
  332. package/clis/xiaoe/catalog.js +105 -40
  333. package/clis/xiaoe/content.js +164 -29
  334. package/clis/xiaoe/courses.js +86 -29
  335. package/clis/xiaoe/xiaoe.test.js +486 -0
  336. package/clis/xiaohongshu/creator-notes-summary.js +1 -1
  337. package/clis/xiaohongshu/publish.js +16 -3
  338. package/clis/xiaohongshu/publish.test.js +46 -1
  339. package/clis/youtube/transcript.js +13 -19
  340. package/clis/youtube/transcript.test.js +17 -0
  341. package/clis/yuanbao/ask.js +17 -66
  342. package/clis/yuanbao/ask.test.js +5 -5
  343. package/clis/yuanbao/detail.js +65 -0
  344. package/clis/yuanbao/history.js +51 -0
  345. package/clis/yuanbao/new.js +1 -0
  346. package/clis/yuanbao/read.js +38 -0
  347. package/clis/yuanbao/send.js +57 -0
  348. package/clis/yuanbao/shared.js +297 -5
  349. package/clis/yuanbao/shared.test.js +80 -0
  350. package/clis/yuanbao/status.js +44 -0
  351. package/clis/zlibrary/commands.test.js +1 -11
  352. package/dist/src/browser/base-page.d.ts +9 -0
  353. package/dist/src/browser/base-page.js +44 -1
  354. package/dist/src/browser/base-page.test.js +66 -0
  355. package/dist/src/browser/bridge.js +47 -45
  356. package/dist/src/browser/cdp.d.ts +1 -0
  357. package/dist/src/browser/cdp.js +51 -9
  358. package/dist/src/browser/daemon-client.d.ts +4 -0
  359. package/dist/src/browser/errors.js +1 -1
  360. package/dist/src/browser/page.d.ts +1 -1
  361. package/dist/src/browser/page.js +3 -1
  362. package/dist/src/browser/page.test.js +29 -0
  363. package/dist/src/browser/target-errors.d.ts +2 -1
  364. package/dist/src/browser/target-errors.js +1 -0
  365. package/dist/src/browser/target-resolver.d.ts +25 -0
  366. package/dist/src/browser/target-resolver.js +43 -0
  367. package/dist/src/browser.test.js +18 -0
  368. package/dist/src/build-manifest.js +9 -4
  369. package/dist/src/build-manifest.test.js +2 -8
  370. package/dist/src/capabilityRouting.d.ts +16 -1
  371. package/dist/src/capabilityRouting.js +24 -1
  372. package/dist/src/capabilityRouting.test.js +19 -1
  373. package/dist/src/cli.js +76 -11
  374. package/dist/src/cli.test.js +241 -1
  375. package/dist/src/commanderAdapter.js +23 -9
  376. package/dist/src/commanderAdapter.test.js +0 -1
  377. package/dist/src/discovery.js +2 -5
  378. package/dist/src/errors.js +1 -1
  379. package/dist/src/execution.d.ts +1 -1
  380. package/dist/src/execution.js +111 -27
  381. package/dist/src/execution.test.js +326 -17
  382. package/dist/src/help.d.ts +27 -2
  383. package/dist/src/help.js +196 -23
  384. package/dist/src/help.test.d.ts +1 -0
  385. package/dist/src/help.test.js +54 -0
  386. package/dist/src/main.js +14 -1
  387. package/dist/src/manifest-types.d.ts +5 -3
  388. package/dist/src/pipeline/executor.js +1 -1
  389. package/dist/src/pipeline/executor.test.js +8 -0
  390. package/dist/src/pipeline/registry.d.ts +9 -0
  391. package/dist/src/pipeline/registry.js +13 -1
  392. package/dist/src/pipeline/steps/browser.d.ts +1 -0
  393. package/dist/src/pipeline/steps/browser.js +10 -0
  394. package/dist/src/pipeline/steps/download.test.js +1 -0
  395. package/dist/src/registry-api.d.ts +1 -1
  396. package/dist/src/registry.d.ts +12 -11
  397. package/dist/src/registry.js +16 -6
  398. package/dist/src/registry.test.js +2 -2
  399. package/dist/src/runtime.d.ts +2 -1
  400. package/dist/src/runtime.js +1 -1
  401. package/dist/src/serialization.d.ts +2 -2
  402. package/dist/src/serialization.js +4 -6
  403. package/dist/src/serialization.test.js +17 -0
  404. package/dist/src/types.d.ts +17 -0
  405. package/dist/src/validate.js +15 -11
  406. package/dist/src/validate.test.d.ts +9 -0
  407. package/dist/src/validate.test.js +90 -0
  408. package/package.json +1 -1
  409. package/scripts/fetch-adapters.js +1 -1
  410. package/scripts/typed-error-lint-baseline.json +5 -77
  411. package/clis/ctrip/search.test.js +0 -64
  412. package/clis/gov-policy/commands.test.js +0 -27
  413. package/clis/linux-do/category.js +0 -37
  414. package/clis/linux-do/hot.js +0 -26
  415. package/clis/linux-do/latest.js +0 -19
  416. package/clis/pixiv/test-utils.js +0 -23
  417. package/clis/toutiao/articles.test.js +0 -30
  418. package/dist/src/analysis.d.ts +0 -40
  419. package/dist/src/analysis.js +0 -172
@@ -4,13 +4,14 @@
4
4
  * This is the single entry point for executing any CLI command. It handles:
5
5
  * 1. Argument validation and coercion
6
6
  * 2. Browser session lifecycle (if needed)
7
- * 3. Domain pre-navigation for cookie/header strategies
7
+ * 3. Domain pre-navigation for cookie strategies
8
8
  * 4. Timeout enforcement
9
9
  * 5. Lazy-loading of TS modules from manifest
10
10
  * 6. Lifecycle hooks (onBeforeExecute / onAfterExecute)
11
11
  */
12
12
  import { getRegistry, fullName, } from './registry.js';
13
13
  import { pathToFileURL } from 'node:url';
14
+ import * as crypto from 'node:crypto';
14
15
  import * as fs from 'node:fs';
15
16
  import * as os from 'node:os';
16
17
  import { executePipeline } from './pipeline/index.js';
@@ -28,6 +29,7 @@ const _loadedModules = new Map();
28
29
  /** Track mtime of loaded user adapter files for hot-reload in daemon mode. */
29
30
  const _moduleMtimes = new Map();
30
31
  const _userClisDir = `${os.homedir()}/.opencli/clis/`;
32
+ const INTERACTIVE_BROWSER_IDLE_TIMEOUT_SECONDS = 600;
31
33
  function normalizeTraceMode(raw) {
32
34
  if (raw === undefined || raw === null || raw === '' || raw === 'off')
33
35
  return 'off';
@@ -138,14 +140,37 @@ function resolvePreNav(cmd) {
138
140
  // strategy → navigateBefore expansion already happened in normalizeCommand().
139
141
  return null;
140
142
  }
141
- function ensureRequiredEnv(cmd) {
142
- const missing = (cmd.requiredEnv ?? []).find(({ name }) => {
143
- const value = process.env[name];
144
- return value === undefined || value === null || value === '';
145
- });
146
- if (!missing)
147
- return;
148
- throw new CommandExecutionError(`Command ${fullName(cmd)} requires environment variable ${missing.name}.`, missing.help ?? `Set ${missing.name} before running ${fullName(cmd)}.`);
143
+ function urlMatchesDomain(url, domain) {
144
+ if (!url || !domain)
145
+ return false;
146
+ try {
147
+ const hostname = new URL(url).hostname;
148
+ return hostname === domain || hostname.endsWith(`.${domain}`);
149
+ }
150
+ catch {
151
+ return false;
152
+ }
153
+ }
154
+ function isDomainRootPreNav(preNavUrl, domain) {
155
+ if (!domain)
156
+ return false;
157
+ try {
158
+ const parsed = new URL(preNavUrl);
159
+ const hostnameMatches = parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`);
160
+ const rootPath = parsed.pathname === '' || parsed.pathname === '/';
161
+ return hostnameMatches && rootPath && parsed.search === '' && parsed.hash === '';
162
+ }
163
+ catch {
164
+ return false;
165
+ }
166
+ }
167
+ async function shouldRunPreNav(cmd, page, reuse, preNavUrl) {
168
+ if (reuse !== 'site' || !cmd.domain)
169
+ return true;
170
+ if (!isDomainRootPreNav(preNavUrl, cmd.domain))
171
+ return true;
172
+ const currentUrl = await page.getCurrentUrl?.().catch(() => null);
173
+ return !urlMatchesDomain(currentUrl, cmd.domain);
149
174
  }
150
175
  export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
151
176
  let kwargs;
@@ -157,6 +182,7 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
157
182
  throw err;
158
183
  throw new ArgumentError(getErrorMessage(err));
159
184
  }
185
+ const userTimeoutSec = readUserTimeoutSeconds(cmd, kwargs);
160
186
  const traceMode = normalizeTraceMode(opts.trace);
161
187
  const hookCtx = {
162
188
  command: fullName(cmd),
@@ -183,17 +209,19 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
183
209
  cdpEndpoint = await resolveElectronEndpoint(cmd.site);
184
210
  }
185
211
  }
186
- ensureRequiredEnv(cmd);
187
212
  const BrowserFactory = getBrowserFactory(cmd.site);
188
213
  const contextId = resolveProfileContextId(opts.profile);
189
214
  const internal = cmd;
215
+ const browserReuse = resolveBrowserSessionReuse(cmd);
216
+ const workspace = resolveBrowserWorkspace(cmd, browserReuse);
217
+ const idleTimeout = browserReuse === 'site' ? INTERACTIVE_BROWSER_IDLE_TIMEOUT_SECONDS : undefined;
190
218
  result = await browserSession(BrowserFactory, async (page) => {
191
219
  const observation = traceMode === 'off'
192
220
  ? null
193
221
  : new ObservationSession({
194
222
  scope: {
195
223
  contextId,
196
- workspace: `site:${cmd.site}`,
224
+ workspace,
197
225
  target: page.getActivePage?.(),
198
226
  site: cmd.site,
199
227
  command: fullName(cmd),
@@ -210,7 +238,7 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
210
238
  await page.startNetworkCapture?.().catch(() => false);
211
239
  }
212
240
  const preNavUrl = resolvePreNav(cmd);
213
- if (preNavUrl) {
241
+ if (preNavUrl && await shouldRunPreNav(cmd, page, browserReuse, preNavUrl)) {
214
242
  observation?.record({
215
243
  stream: 'action',
216
244
  name: 'pre_navigate',
@@ -253,12 +281,15 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
253
281
  throw wrapped;
254
282
  }
255
283
  }
256
- // --live / OPENCLI_LIVE=1 keeps the automation window open after the
257
- // command finishes, so agents (or humans) can inspect the page state.
258
- const keepOpen = process.env.OPENCLI_LIVE === '1' || process.env.OPENCLI_LIVE === 'true';
284
+ // --live / OPENCLI_LIVE=1 keeps the current automation tab lease after
285
+ // the command finishes, so agents (or humans) can inspect the page state.
286
+ const keepOpen = browserReuse !== 'none' || process.env.OPENCLI_LIVE === '1' || process.env.OPENCLI_LIVE === 'true';
259
287
  try {
288
+ const browserTimeout = userTimeoutSec !== null
289
+ ? userTimeoutSec + RUNTIME_TIMEOUT_PADDING_SECONDS
290
+ : DEFAULT_BROWSER_COMMAND_TIMEOUT;
260
291
  const result = await runWithTimeout(runCommand(cmd, page, kwargs, debug), {
261
- timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT,
292
+ timeout: browserTimeout,
262
293
  label: fullName(cmd),
263
294
  });
264
295
  observation?.record({
@@ -270,8 +301,9 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
270
301
  await collectObservationEvidence(observation, page).catch(() => { });
271
302
  exportTraceArtifact(observation, 'success', undefined, opts.onTraceExport);
272
303
  }
273
- // Adapter commands are one-shot — close the automation window immediately
274
- // instead of waiting for the 30s idle timeout.
304
+ // Adapter commands are one-shot — release the current tab lease immediately
305
+ // instead of waiting for the 30s idle timeout. The automation container
306
+ // window stays open for reuse.
275
307
  if (!keepOpen)
276
308
  await page.closeWindow?.().catch(() => { });
277
309
  return result;
@@ -294,23 +326,26 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
294
326
  exportTraceArtifact(observation, 'failure', err, opts.onTraceExport);
295
327
  }
296
328
  }
297
- // Close the automation window on failure too — without this, the window
298
- // lingers until the extension's idle timer fires (unreliable on Windows
299
- // where MV3 service workers may be suspended before setTimeout triggers).
329
+ // Release the tab lease on failure too — without this, the lease lingers
330
+ // until the extension's idle timer fires (unreliable on Windows where
331
+ // MV3 service workers may be suspended before setTimeout triggers).
300
332
  if (!keepOpen)
301
333
  await page.closeWindow?.().catch(() => { });
302
334
  throw err;
303
335
  }
304
- }, { workspace: `site:${cmd.site}:${crypto.randomUUID()}`, cdpEndpoint, contextId });
336
+ }, { workspace, cdpEndpoint, contextId, idleTimeout });
305
337
  }
306
338
  else {
307
- // Non-browser commands: apply timeout only when explicitly configured.
308
- const timeout = cmd.timeoutSeconds;
309
- if (timeout !== undefined && timeout > 0) {
339
+ // Non-browser commands: enforce a timeout only when the command exposes
340
+ // a `--timeout` arg (and the resolved value is positive). Without that
341
+ // arg there is no meaningful default non-browser cmds are diverse
342
+ // enough that a hard cap would do more harm than good.
343
+ if (userTimeoutSec !== null) {
344
+ const ceiling = userTimeoutSec + RUNTIME_TIMEOUT_PADDING_SECONDS;
310
345
  result = await runWithTimeout(runCommand(cmd, null, kwargs, debug), {
311
- timeout,
346
+ timeout: ceiling,
312
347
  label: fullName(cmd),
313
- hint: `Increase the adapter's timeoutSeconds setting (currently ${timeout}s)`,
348
+ hint: `Pass a higher --timeout value (currently ${userTimeoutSec}s)`,
314
349
  });
315
350
  }
316
351
  else {
@@ -401,3 +436,52 @@ export function prepareCommandArgs(cmd, rawKwargs) {
401
436
  cmd.validateArgs?.(kwargs);
402
437
  return kwargs;
403
438
  }
439
+ /**
440
+ * Runtime ceiling padding (seconds) added on top of the user's `--timeout`.
441
+ * The adapter's polling loop typically uses the full user value; the padding
442
+ * gives us room for the adapter to return + closeWindow + trace export before
443
+ * the runtime kills the Promise.
444
+ */
445
+ const RUNTIME_TIMEOUT_PADDING_SECONDS = 30;
446
+ function readEnvBrowserSessionReuse() {
447
+ const raw = process.env.OPENCLI_BROWSER_REUSE;
448
+ if (raw === undefined || raw === '')
449
+ return null;
450
+ if (raw === 'none' || raw === 'site')
451
+ return raw;
452
+ throw new ArgumentError(`--reuse must be one of: none, site. Received: "${raw}"`);
453
+ }
454
+ function resolveBrowserSessionReuse(cmd) {
455
+ return readEnvBrowserSessionReuse() ?? cmd.browserSession?.reuse ?? 'none';
456
+ }
457
+ function resolveBrowserWorkspace(cmd, reuse) {
458
+ if (reuse === 'site')
459
+ return `site:${cmd.site}`;
460
+ return `site:${cmd.site}:${crypto.randomUUID()}`;
461
+ }
462
+ /**
463
+ * Resolve the user-controllable `--timeout` arg, in seconds.
464
+ *
465
+ * Convention: a command opts into runtime-enforced timeouts by declaring an
466
+ * arg named `timeout`. The arg's `default` flows through `prepareCommandArgs`
467
+ * into `kwargs.timeout`, so by the time runtime enforcement runs, the value
468
+ * is the merged user-supplied-or-default seconds.
469
+ *
470
+ * Returns the parsed positive integer (seconds), or null if the command does
471
+ * not expose a `timeout` arg. Declaring `timeout` opts into runtime timeout
472
+ * enforcement, so invalid values must fail upfront instead of silently
473
+ * disabling the runtime ceiling.
474
+ */
475
+ function readUserTimeoutSeconds(cmd, kwargs) {
476
+ if (!cmd.args.some(a => a.name === 'timeout'))
477
+ return null;
478
+ const raw = kwargs.timeout;
479
+ if (raw === undefined || raw === null || raw === '') {
480
+ throw new ArgumentError(`Argument "timeout" must be a positive integer. Received: "${String(raw)}"`);
481
+ }
482
+ const parsed = Number(raw);
483
+ if (!Number.isInteger(parsed) || parsed <= 0) {
484
+ throw new ArgumentError(`Argument "timeout" must be a positive integer. Received: "${String(raw)}"`);
485
+ }
486
+ return parsed;
487
+ }
@@ -3,44 +3,353 @@ import * as fs from 'node:fs';
3
3
  import * as os from 'node:os';
4
4
  import * as path from 'node:path';
5
5
  import { executeCommand, prepareCommandArgs } from './execution.js';
6
- import { TimeoutError, toEnvelope } from './errors.js';
6
+ import { ArgumentError, TimeoutError, toEnvelope } from './errors.js';
7
7
  import { cli, Strategy } from './registry.js';
8
- import { withTimeoutMs } from './runtime.js';
9
8
  import * as runtime from './runtime.js';
10
9
  import * as capRouting from './capabilityRouting.js';
11
10
  describe('executeCommand — non-browser timeout', () => {
12
- it('applies timeoutSeconds to non-browser commands', async () => {
11
+ it('applies the user --timeout arg as the ceiling for non-browser commands', async () => {
12
+ const runWithTimeoutSpy = vi.spyOn(runtime, 'runWithTimeout');
13
13
  const cmd = cli({
14
14
  site: 'test-execution',
15
15
  name: 'non-browser-timeout', access: 'read',
16
- description: 'test non-browser timeout',
16
+ description: 'test non-browser --timeout enforcement',
17
17
  browser: false,
18
18
  strategy: Strategy.PUBLIC,
19
- timeoutSeconds: 0.01,
19
+ args: [
20
+ { name: 'timeout', type: 'int', required: false, default: 5, help: 'Max seconds' },
21
+ ],
22
+ func: async () => [{ ok: true }],
23
+ });
24
+ await executeCommand(cmd, {});
25
+ expect(runWithTimeoutSpy).toHaveBeenCalledTimes(1);
26
+ // Ceiling = user-supplied/default timeout + 30s padding (adapter return room).
27
+ expect(runWithTimeoutSpy.mock.calls[0]?.[1]).toMatchObject({
28
+ timeout: 35,
29
+ label: 'test-execution/non-browser-timeout',
30
+ });
31
+ vi.restoreAllMocks();
32
+ });
33
+ it('fires a TimeoutError when the inner adapter exceeds the --timeout ceiling', async () => {
34
+ const cmd = cli({
35
+ site: 'test-execution',
36
+ name: 'non-browser-timeout-fires', access: 'read',
37
+ description: 'test that the ceiling actually cancels the adapter',
38
+ browser: false,
39
+ strategy: Strategy.PUBLIC,
40
+ args: [
41
+ { name: 'timeout', type: 'int', required: false, default: 1, help: 'Max seconds' },
42
+ ],
20
43
  func: () => new Promise(() => { }),
21
44
  });
22
- // Sentinel timeout at 200ms if the inner 10ms timeout fires first,
23
- // the error will be a TimeoutError with the command label, not 'sentinel'.
24
- const error = await withTimeoutMs(executeCommand(cmd, {}), 200, 'sentinel timeout')
25
- .catch((err) => err);
45
+ // Spy on runWithTimeout to intercept and pass a tiny ceiling so the test
46
+ // doesn't have to wait the real (1+30)s. We still verify the TimeoutError
47
+ // surface code, label, hint that users see.
48
+ vi.spyOn(runtime, 'runWithTimeout').mockImplementation(async (promise, opts) => {
49
+ return runtime.withTimeoutMs(promise, 50, () => new TimeoutError(opts.label ?? 'op', opts.timeout, opts.hint));
50
+ });
51
+ const error = await executeCommand(cmd, {}).catch((err) => err);
26
52
  expect(error).toBeInstanceOf(TimeoutError);
27
53
  expect(error).toMatchObject({
28
54
  code: 'TIMEOUT',
29
- message: 'test-execution/non-browser-timeout timed out after 0.01s',
55
+ hint: 'Pass a higher --timeout value (currently 1s)',
30
56
  });
57
+ vi.restoreAllMocks();
31
58
  });
32
- it('skips timeout when timeoutSeconds is 0', async () => {
59
+ it('runs non-browser commands without a ceiling when no --timeout arg is declared', async () => {
60
+ const runWithTimeoutSpy = vi.spyOn(runtime, 'runWithTimeout');
33
61
  const cmd = cli({
34
62
  site: 'test-execution',
35
- name: 'non-browser-zero-timeout', access: 'read',
36
- description: 'test zero timeout bypasses wrapping',
63
+ name: 'non-browser-no-timeout', access: 'read',
64
+ description: 'test that omitting --timeout means no ceiling',
37
65
  browser: false,
38
66
  strategy: Strategy.PUBLIC,
39
- timeoutSeconds: 0,
40
- func: () => new Promise(() => { }),
67
+ func: async () => [{ ok: true }],
68
+ });
69
+ await executeCommand(cmd, {});
70
+ expect(runWithTimeoutSpy).not.toHaveBeenCalled();
71
+ vi.restoreAllMocks();
72
+ });
73
+ it('rejects invalid --timeout values instead of silently disabling the non-browser ceiling', async () => {
74
+ const runWithTimeoutSpy = vi.spyOn(runtime, 'runWithTimeout');
75
+ const cmd = cli({
76
+ site: 'test-execution',
77
+ name: 'non-browser-invalid-timeout', access: 'read',
78
+ description: 'test invalid --timeout fails upfront',
79
+ browser: false,
80
+ strategy: Strategy.PUBLIC,
81
+ args: [
82
+ { name: 'timeout', type: 'int', required: false, default: 5, help: 'Max seconds' },
83
+ ],
84
+ func: async () => [{ ok: true }],
85
+ });
86
+ await expect(executeCommand(cmd, { timeout: 0 })).rejects.toBeInstanceOf(ArgumentError);
87
+ await expect(executeCommand(cmd, { timeout: -1 })).rejects.toBeInstanceOf(ArgumentError);
88
+ await expect(executeCommand(cmd, { timeout: 1.5 })).rejects.toBeInstanceOf(ArgumentError);
89
+ expect(runWithTimeoutSpy).not.toHaveBeenCalled();
90
+ vi.restoreAllMocks();
91
+ });
92
+ it('applies the user --timeout arg as the ceiling for browser commands (with +30s padding)', async () => {
93
+ const closeWindow = vi.fn().mockResolvedValue(undefined);
94
+ const mockPage = { closeWindow };
95
+ vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
96
+ vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn) => fn(mockPage));
97
+ const runWithTimeoutSpy = vi.spyOn(runtime, 'runWithTimeout');
98
+ const cmd = cli({
99
+ site: 'test-execution',
100
+ name: 'browser-with-timeout', access: 'read',
101
+ description: 'test browser --timeout enforcement',
102
+ browser: true,
103
+ strategy: Strategy.PUBLIC,
104
+ args: [
105
+ { name: 'timeout', type: 'int', required: false, default: 5, help: 'Max seconds' },
106
+ ],
107
+ func: async () => [{ ok: true }],
108
+ });
109
+ await executeCommand(cmd, {});
110
+ expect(runWithTimeoutSpy).toHaveBeenCalledTimes(1);
111
+ expect(runWithTimeoutSpy.mock.calls[0]?.[1]).toMatchObject({
112
+ timeout: 35,
113
+ label: 'test-execution/browser-with-timeout',
114
+ });
115
+ vi.restoreAllMocks();
116
+ });
117
+ it('falls back to DEFAULT_BROWSER_COMMAND_TIMEOUT for browser commands without a --timeout arg', async () => {
118
+ const closeWindow = vi.fn().mockResolvedValue(undefined);
119
+ const mockPage = { closeWindow };
120
+ vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
121
+ vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn) => fn(mockPage));
122
+ const runWithTimeoutSpy = vi.spyOn(runtime, 'runWithTimeout');
123
+ const cmd = cli({
124
+ site: 'test-execution',
125
+ name: 'browser-no-timeout', access: 'read',
126
+ description: 'test browser fallback to global default',
127
+ browser: true,
128
+ strategy: Strategy.PUBLIC,
129
+ func: async () => [{ ok: true }],
130
+ });
131
+ await executeCommand(cmd, {});
132
+ expect(runWithTimeoutSpy).toHaveBeenCalledTimes(1);
133
+ expect(runWithTimeoutSpy.mock.calls[0]?.[1]).toMatchObject({
134
+ timeout: runtime.DEFAULT_BROWSER_COMMAND_TIMEOUT,
135
+ label: 'test-execution/browser-no-timeout',
136
+ });
137
+ vi.restoreAllMocks();
138
+ });
139
+ it('reuses a site-scoped browser workspace and keeps the tab lease open', async () => {
140
+ const closeWindow = vi.fn().mockResolvedValue(undefined);
141
+ const mockPage = { closeWindow };
142
+ const sessionOpts = [];
143
+ vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
144
+ vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn, opts) => {
145
+ sessionOpts.push(opts ?? {});
146
+ return fn(mockPage);
147
+ });
148
+ const cmd = cli({
149
+ site: 'test-execution',
150
+ name: 'browser-reuse-site', access: 'read',
151
+ description: 'test site-scoped browser reuse',
152
+ browser: true,
153
+ strategy: Strategy.PUBLIC,
154
+ browserSession: { reuse: 'site' },
155
+ func: async () => [{ ok: true }],
41
156
  });
42
- // With timeout guard skipped, the sentinel fires instead.
43
- await expect(withTimeoutMs(executeCommand(cmd, {}), 50, 'sentinel timeout')).rejects.toThrow('sentinel timeout');
157
+ await executeCommand(cmd, {});
158
+ await executeCommand(cmd, {});
159
+ expect(sessionOpts).toHaveLength(2);
160
+ expect(sessionOpts[0]).toMatchObject({ workspace: 'site:test-execution', idleTimeout: 600 });
161
+ expect(sessionOpts[1]).toMatchObject({ workspace: 'site:test-execution', idleTimeout: 600 });
162
+ expect(closeWindow).not.toHaveBeenCalled();
163
+ vi.restoreAllMocks();
164
+ });
165
+ it('keeps default browser commands on one-shot workspaces', async () => {
166
+ const closeWindow = vi.fn().mockResolvedValue(undefined);
167
+ const mockPage = { closeWindow };
168
+ const sessionOpts = [];
169
+ vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
170
+ vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn, opts) => {
171
+ sessionOpts.push(opts ?? {});
172
+ return fn(mockPage);
173
+ });
174
+ const cmd = cli({
175
+ site: 'test-execution',
176
+ name: 'browser-reuse-default', access: 'read',
177
+ description: 'test default one-shot browser workspace',
178
+ browser: true,
179
+ strategy: Strategy.PUBLIC,
180
+ func: async () => [{ ok: true }],
181
+ });
182
+ await executeCommand(cmd, {});
183
+ await executeCommand(cmd, {});
184
+ expect(sessionOpts).toHaveLength(2);
185
+ expect(sessionOpts[0]?.workspace).toMatch(/^site:test-execution:/);
186
+ expect(sessionOpts[1]?.workspace).toMatch(/^site:test-execution:/);
187
+ expect(sessionOpts[0]?.workspace).not.toBe(sessionOpts[1]?.workspace);
188
+ expect(sessionOpts[0]?.idleTimeout).toBeUndefined();
189
+ expect(sessionOpts[1]?.idleTimeout).toBeUndefined();
190
+ expect(closeWindow).toHaveBeenCalledTimes(2);
191
+ vi.restoreAllMocks();
192
+ });
193
+ it('lets user --reuse none override adapter reuse metadata', async () => {
194
+ const closeWindow = vi.fn().mockResolvedValue(undefined);
195
+ const mockPage = { closeWindow };
196
+ const sessionOpts = [];
197
+ vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
198
+ vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn, opts) => {
199
+ sessionOpts.push(opts ?? {});
200
+ return fn(mockPage);
201
+ });
202
+ const prev = process.env.OPENCLI_BROWSER_REUSE;
203
+ process.env.OPENCLI_BROWSER_REUSE = 'none';
204
+ try {
205
+ const cmd = cli({
206
+ site: 'test-execution',
207
+ name: 'browser-reuse-override-none', access: 'read',
208
+ description: 'test user reuse override',
209
+ browser: true,
210
+ strategy: Strategy.PUBLIC,
211
+ browserSession: { reuse: 'site' },
212
+ func: async () => [{ ok: true }],
213
+ });
214
+ await executeCommand(cmd, {});
215
+ expect(sessionOpts).toHaveLength(1);
216
+ expect(sessionOpts[0]?.workspace).toMatch(/^site:test-execution:/);
217
+ expect(sessionOpts[0]?.idleTimeout).toBeUndefined();
218
+ expect(closeWindow).toHaveBeenCalledTimes(1);
219
+ }
220
+ finally {
221
+ if (prev === undefined)
222
+ delete process.env.OPENCLI_BROWSER_REUSE;
223
+ else
224
+ process.env.OPENCLI_BROWSER_REUSE = prev;
225
+ vi.restoreAllMocks();
226
+ }
227
+ });
228
+ it('skips repeated domain pre-navigation for site-reused browser sessions', async () => {
229
+ const closeWindow = vi.fn().mockResolvedValue(undefined);
230
+ const goto = vi.fn().mockResolvedValue(undefined);
231
+ const mockPage = {
232
+ closeWindow,
233
+ goto,
234
+ getCurrentUrl: vi.fn().mockResolvedValue('https://grok.com/chat/abc'),
235
+ };
236
+ vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
237
+ vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn) => fn(mockPage));
238
+ const cmd = cli({
239
+ site: 'test-execution',
240
+ name: 'browser-reuse-skip-prenav', access: 'read',
241
+ description: 'test reused same-domain tabs do not reset conversation state',
242
+ browser: true,
243
+ strategy: Strategy.COOKIE,
244
+ domain: 'grok.com',
245
+ browserSession: { reuse: 'site' },
246
+ func: async () => [{ ok: true }],
247
+ });
248
+ await executeCommand(cmd, {});
249
+ expect(goto).not.toHaveBeenCalled();
250
+ expect(closeWindow).not.toHaveBeenCalled();
251
+ vi.restoreAllMocks();
252
+ });
253
+ it('keeps explicit path pre-navigation for site-reused browser sessions', async () => {
254
+ const closeWindow = vi.fn().mockResolvedValue(undefined);
255
+ const goto = vi.fn().mockResolvedValue(undefined);
256
+ const mockPage = {
257
+ closeWindow,
258
+ goto,
259
+ getCurrentUrl: vi.fn().mockResolvedValue('https://example.com/other'),
260
+ };
261
+ vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
262
+ vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn) => fn(mockPage));
263
+ const cmd = cli({
264
+ site: 'test-execution',
265
+ name: 'browser-reuse-path-prenav', access: 'read',
266
+ description: 'test explicit path pre-navigation still runs',
267
+ browser: true,
268
+ strategy: Strategy.COOKIE,
269
+ domain: 'example.com',
270
+ navigateBefore: 'https://example.com/dashboard',
271
+ browserSession: { reuse: 'site' },
272
+ func: async () => [{ ok: true }],
273
+ });
274
+ await executeCommand(cmd, {});
275
+ expect(goto).toHaveBeenCalledWith('https://example.com/dashboard');
276
+ expect(closeWindow).not.toHaveBeenCalled();
277
+ vi.restoreAllMocks();
278
+ });
279
+ it('respects navigateBefore=false so adapter range validation fails before browser navigation', async () => {
280
+ const closeWindow = vi.fn().mockResolvedValue(undefined);
281
+ const goto = vi.fn().mockResolvedValue(undefined);
282
+ const mockPage = {
283
+ closeWindow,
284
+ goto,
285
+ getCurrentUrl: vi.fn().mockResolvedValue('about:blank'),
286
+ };
287
+ vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
288
+ vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn) => fn(mockPage));
289
+ const cmd = cli({
290
+ site: 'test-execution',
291
+ name: 'browser-invalid-limit-no-prenav', access: 'read',
292
+ description: 'test adapter range validation can fail before pre-nav',
293
+ browser: true,
294
+ strategy: Strategy.COOKIE,
295
+ domain: 'www.facebook.com',
296
+ navigateBefore: false,
297
+ args: [
298
+ { name: 'limit', type: 'int', required: false, default: 15, help: 'Limit' },
299
+ ],
300
+ func: async (_page, args) => {
301
+ const limit = Number(args.limit);
302
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
303
+ throw new ArgumentError('--limit must be a positive integer in [1, 100]');
304
+ }
305
+ return [{ ok: true }];
306
+ },
307
+ });
308
+ await expect(executeCommand(cmd, { limit: 0 })).rejects.toBeInstanceOf(ArgumentError);
309
+ expect(goto).not.toHaveBeenCalled();
310
+ vi.restoreAllMocks();
311
+ });
312
+ it('rejects invalid --timeout values instead of falling back to the browser default', async () => {
313
+ const closeWindow = vi.fn().mockResolvedValue(undefined);
314
+ const mockPage = { closeWindow };
315
+ vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
316
+ vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn) => fn(mockPage));
317
+ const runWithTimeoutSpy = vi.spyOn(runtime, 'runWithTimeout');
318
+ const cmd = cli({
319
+ site: 'test-execution',
320
+ name: 'browser-invalid-timeout', access: 'read',
321
+ description: 'test invalid browser --timeout fails upfront',
322
+ browser: true,
323
+ strategy: Strategy.PUBLIC,
324
+ args: [
325
+ { name: 'timeout', type: 'int', required: false, default: 5, help: 'Max seconds' },
326
+ ],
327
+ func: async () => [{ ok: true }],
328
+ });
329
+ await expect(executeCommand(cmd, { timeout: 0 })).rejects.toBeInstanceOf(ArgumentError);
330
+ await expect(executeCommand(cmd, { timeout: -1 })).rejects.toBeInstanceOf(ArgumentError);
331
+ await expect(executeCommand(cmd, { timeout: 1.5 })).rejects.toBeInstanceOf(ArgumentError);
332
+ expect(runWithTimeoutSpy).not.toHaveBeenCalled();
333
+ vi.restoreAllMocks();
334
+ });
335
+ it('rejects invalid browser --timeout before opening a session or pre-navigating', async () => {
336
+ vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
337
+ const browserSessionSpy = vi.spyOn(runtime, 'browserSession');
338
+ const cmd = cli({
339
+ site: 'test-execution',
340
+ name: 'browser-invalid-timeout-prenav', access: 'read',
341
+ description: 'test invalid browser --timeout fails before session setup',
342
+ browser: true,
343
+ strategy: Strategy.PUBLIC,
344
+ navigateBefore: 'https://example.com/',
345
+ args: [
346
+ { name: 'timeout', type: 'int', required: false, default: 5, help: 'Max seconds' },
347
+ ],
348
+ func: async () => [{ ok: true }],
349
+ });
350
+ await expect(executeCommand(cmd, { timeout: 0 })).rejects.toBeInstanceOf(ArgumentError);
351
+ expect(browserSessionSpy).not.toHaveBeenCalled();
352
+ vi.restoreAllMocks();
44
353
  });
45
354
  it('calls closeWindow on browser command failure', async () => {
46
355
  const closeWindow = vi.fn().mockResolvedValue(undefined);
@@ -7,9 +7,34 @@ export declare function wrapCommaList(items: readonly string[], opts?: {
7
7
  width?: number;
8
8
  indent?: string;
9
9
  }): string;
10
- export declare function formatRootAdapterHelpText(siteNames: readonly string[]): string;
11
- export declare function rootHelpData(program: Command, siteNames: readonly string[]): Record<string, unknown>;
10
+ /**
11
+ * Adapter category for help-text grouping.
12
+ *
13
+ * - `site`: web site adapter (real DNS-style domain, e.g. `www.bilibili.com`)
14
+ * - `app`: desktop app adapter (Electron/osascript, signaled by `domain: 'localhost'`
15
+ * or other non-DNS string like `'doubao-app'`)
16
+ *
17
+ * Classification is derived from the adapter's `domain` field — no new schema
18
+ * required. Adapters without a `domain` field default to `site` (most are
19
+ * public web scrapers).
20
+ */
21
+ export type AdapterKind = 'site' | 'app';
22
+ export declare function classifyAdapter(domain: string | undefined): AdapterKind;
23
+ export interface RootAdapterGroups {
24
+ /** Externally-registered CLIs (docker, gh, vercel, ...) — passthrough binaries */
25
+ external: readonly string[];
26
+ /** Desktop-app adapters (chatgpt-app, chatwise, codex, ...) */
27
+ apps: readonly string[];
28
+ /** Web-site adapters (bilibili, dianping, ...) */
29
+ sites: readonly string[];
30
+ }
31
+ export declare function formatRootAdapterHelpText(groups: RootAdapterGroups): string;
32
+ export declare function formatCommandListTerm(cmd: CliCommand): string;
33
+ export declare function rootHelpData(program: Command, groups: RootAdapterGroups): Record<string, unknown>;
12
34
  export declare function siteHelpData(site: string, commands: readonly CliCommand[]): Record<string, unknown>;
13
35
  export declare function commandHelpData(cmd: CliCommand): Record<string, unknown>;
36
+ export declare function formatCommonOptionsHelpText(): string;
37
+ export declare function formatSiteHelpText(site: string, commands: readonly CliCommand[]): string;
38
+ export declare function formatCommandHelpText(cmd: CliCommand): string;
14
39
  export declare function installStructuredHelp(command: Command, data: () => unknown, textSuffix?: string | (() => string)): void;
15
40
  export declare function formatSiteCommandDescription(cmd: CliCommand): string;