@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
@@ -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,8 +7,29 @@ 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 rootHelpData(program: Command, groups: RootAdapterGroups): Record<string, unknown>;
12
33
  export declare function siteHelpData(site: string, commands: readonly CliCommand[]): Record<string, unknown>;
13
34
  export declare function commandHelpData(cmd: CliCommand): Record<string, unknown>;
14
35
  export declare function installStructuredHelp(command: Command, data: () => unknown, textSuffix?: string | (() => string)): void;
package/dist/src/help.js CHANGED
@@ -50,18 +50,32 @@ export function wrapCommaList(items, opts = {}) {
50
50
  lines.push(line);
51
51
  return lines.join('\n');
52
52
  }
53
- export function formatRootAdapterHelpText(siteNames) {
54
- if (siteNames.length === 0)
55
- return '';
53
+ export function classifyAdapter(domain) {
54
+ if (!domain)
55
+ return 'site';
56
+ return domain.includes('.') ? 'site' : 'app';
57
+ }
58
+ function formatGroupSection(label, names) {
59
+ if (names.length === 0)
60
+ return [];
56
61
  return [
62
+ `${label} (${names.length}):`,
63
+ wrapCommaList(names),
57
64
  '',
58
- `Site adapters (${siteNames.length}):`,
59
- wrapCommaList(siteNames),
60
- '',
61
- "Run 'opencli list' for full command details, or 'opencli <site> --help' to inspect one site.",
62
- "Agent tip: use 'opencli <site> --help -f yaml' for structured commands, args, access, and examples.",
63
- '',
64
- ].join('\n');
65
+ ];
66
+ }
67
+ export function formatRootAdapterHelpText(groups) {
68
+ const total = groups.external.length + groups.apps.length + groups.sites.length;
69
+ if (total === 0)
70
+ return '';
71
+ const lines = [''];
72
+ lines.push(...formatGroupSection('External CLIs', groups.external));
73
+ lines.push(...formatGroupSection('App adapters', groups.apps));
74
+ lines.push(...formatGroupSection('Site adapters', groups.sites));
75
+ lines.push("Run 'opencli list' for full command details, or 'opencli <site> --help' to inspect one site.");
76
+ lines.push("Agent tip: use 'opencli <site> --help -f yaml' for structured commands, args, access, and examples.");
77
+ lines.push('');
78
+ return lines.join('\n');
65
79
  }
66
80
  function compactArg(arg) {
67
81
  return {
@@ -84,26 +98,35 @@ function compactCommand(cmd, opts = {}) {
84
98
  ...(cmd.aliases?.length ? { aliases: cmd.aliases } : {}),
85
99
  args: cmd.args.map(compactArg),
86
100
  example: formatCommandExample(cmd),
101
+ ...(cmd.browserSession ? { browserSession: cmd.browserSession } : {}),
102
+ ...(cmd.defaultFormat ? { defaultFormat: cmd.defaultFormat } : {}),
87
103
  ...(opts.includeColumns && cmd.columns?.length ? { columns: cmd.columns } : {}),
88
- ...(cmd.deprecated ? { deprecated: cmd.deprecated } : {}),
89
- ...(cmd.replacedBy ? { replacedBy: cmd.replacedBy } : {}),
90
104
  };
91
105
  }
92
- export function rootHelpData(program, siteNames) {
93
- const siteSet = new Set(siteNames);
106
+ export function rootHelpData(program, groups) {
107
+ const adapterNames = new Set([...groups.external, ...groups.apps, ...groups.sites]);
94
108
  const commands = program.commands
95
- .filter(command => !siteSet.has(command.name()))
109
+ .filter(command => !adapterNames.has(command.name()))
96
110
  .map(command => ({
97
111
  name: command.name(),
98
112
  description: command.description(),
99
113
  }));
114
+ const sortLocale = (a, b) => a.localeCompare(b);
100
115
  return {
101
116
  name: program.name(),
102
117
  description: program.description(),
103
118
  commands,
119
+ external_clis: {
120
+ count: groups.external.length,
121
+ clis: [...groups.external].sort(sortLocale),
122
+ },
123
+ app_adapters: {
124
+ count: groups.apps.length,
125
+ apps: [...groups.apps].sort(sortLocale),
126
+ },
104
127
  site_adapters: {
105
- count: siteNames.length,
106
- sites: [...siteNames].sort((a, b) => a.localeCompare(b)),
128
+ count: groups.sites.length,
129
+ sites: [...groups.sites].sort(sortLocale),
107
130
  },
108
131
  next: [
109
132
  'opencli <site> --help -f yaml',
@@ -144,6 +167,5 @@ export function installStructuredHelp(command, data, textSuffix) {
144
167
  }
145
168
  export function formatSiteCommandDescription(cmd) {
146
169
  const access = cmd.access === 'write' ? '[write]' : '[read]';
147
- const deprecatedSuffix = cmd.deprecated ? ' [deprecated]' : '';
148
- return `${access} ${cmd.description}${deprecatedSuffix}`;
170
+ return `${access} ${cmd.description}`;
149
171
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,54 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { classifyAdapter, formatRootAdapterHelpText } from './help.js';
3
+ describe('classifyAdapter', () => {
4
+ it('classifies DNS-style domains as site', () => {
5
+ expect(classifyAdapter('www.bilibili.com')).toBe('site');
6
+ expect(classifyAdapter('chatgpt.com')).toBe('site');
7
+ expect(classifyAdapter('claude.ai')).toBe('site');
8
+ expect(classifyAdapter('grok.com')).toBe('site');
9
+ });
10
+ it('classifies localhost as app (Electron / osascript desktop integrations)', () => {
11
+ expect(classifyAdapter('localhost')).toBe('app');
12
+ });
13
+ it('classifies non-DNS domain strings as app (e.g. literal "doubao-app")', () => {
14
+ expect(classifyAdapter('doubao-app')).toBe('app');
15
+ });
16
+ it('defaults missing domain to site (most adapters without explicit domain are public web scrapers)', () => {
17
+ expect(classifyAdapter(undefined)).toBe('site');
18
+ });
19
+ });
20
+ describe('formatRootAdapterHelpText', () => {
21
+ it('renders all three sections in External / App / Site order when populated', () => {
22
+ const text = formatRootAdapterHelpText({
23
+ external: ['gh', 'docker'],
24
+ apps: ['chatwise', 'codex'],
25
+ sites: ['bilibili'],
26
+ });
27
+ expect(text).toContain('External CLIs (2):');
28
+ expect(text).toContain('App adapters (2):');
29
+ expect(text).toContain('Site adapters (1):');
30
+ expect(text.indexOf('External CLIs')).toBeLessThan(text.indexOf('App adapters'));
31
+ expect(text.indexOf('App adapters')).toBeLessThan(text.indexOf('Site adapters'));
32
+ });
33
+ it('omits empty sections instead of rendering a (0) header', () => {
34
+ const text = formatRootAdapterHelpText({
35
+ external: [],
36
+ apps: [],
37
+ sites: ['bilibili'],
38
+ });
39
+ expect(text).not.toContain('External CLIs');
40
+ expect(text).not.toContain('App adapters');
41
+ expect(text).toContain('Site adapters (1):');
42
+ });
43
+ it('returns empty string when all groups are empty', () => {
44
+ expect(formatRootAdapterHelpText({ external: [], apps: [], sites: [] })).toBe('');
45
+ });
46
+ it('always renders the agent discovery hint when any section is populated', () => {
47
+ const text = formatRootAdapterHelpText({
48
+ external: [],
49
+ apps: [],
50
+ sites: ['bilibili'],
51
+ });
52
+ expect(text).toContain("'opencli <site> --help -f yaml'");
53
+ });
54
+ });
package/dist/src/main.js CHANGED
@@ -28,7 +28,7 @@ const __dirname = path.dirname(__filename);
28
28
  const BUILTIN_CLIS = path.join(findPackageRoot(__filename), 'clis');
29
29
  const USER_CLIS = path.join(os.homedir(), '.opencli', 'clis');
30
30
  // ── Session lifecycle flags ──────────────────────────────────────────────
31
- // `--live` / `--focus` are top-level-ish toggles that tweak the automation
31
+ // `--live` / `--focus` / `--reuse` are top-level-ish toggles that tweak the automation
32
32
  // window's lifecycle. We strip them from argv before Commander runs so they
33
33
  // can be placed anywhere and work on any subcommand (adapter or browser).
34
34
  {
@@ -42,6 +42,19 @@ const USER_CLIS = path.join(os.homedir(), '.opencli', 'clis');
42
42
  process.env.OPENCLI_WINDOW_FOCUSED = '1';
43
43
  process.argv.splice(focusIdx, 1);
44
44
  }
45
+ const reuseIdx = process.argv.findIndex(arg => arg === '--reuse' || arg.startsWith('--reuse='));
46
+ if (reuseIdx !== -1) {
47
+ const arg = process.argv[reuseIdx];
48
+ const value = arg.startsWith('--reuse=')
49
+ ? arg.slice('--reuse='.length)
50
+ : process.argv[reuseIdx + 1];
51
+ if (value !== 'none' && value !== 'site') {
52
+ process.stderr.write(`--reuse must be one of: none, site. Received: "${value ?? ''}"\n`);
53
+ process.exit(EXIT_CODES.USAGE_ERROR);
54
+ }
55
+ process.env.OPENCLI_BROWSER_REUSE = value;
56
+ process.argv.splice(reuseIdx, arg.startsWith('--reuse=') ? 1 : 2);
57
+ }
45
58
  }
46
59
  // ── Ultra-fast path: lightweight commands bypass full discovery ──────────
47
60
  // These are high-frequency or trivial paths that must not pay the startup tax.
@@ -28,9 +28,7 @@ export interface ManifestEntry {
28
28
  }>;
29
29
  columns?: string[];
30
30
  pipeline?: Record<string, unknown>[];
31
- timeout?: number;
32
- deprecated?: boolean | string;
33
- replacedBy?: string;
31
+ defaultFormat?: 'table' | 'plain' | 'json' | 'yaml' | 'yml' | 'md' | 'markdown' | 'csv';
34
32
  type: 'js';
35
33
  /** Relative path from clis/ dir, e.g. 'bilibili/search.js' */
36
34
  modulePath?: string;
@@ -38,4 +36,8 @@ export interface ManifestEntry {
38
36
  sourceFile?: string;
39
37
  /** Pre-navigation control — see CliCommand.navigateBefore */
40
38
  navigateBefore?: boolean | string;
39
+ /** Browser session lifecycle defaults — see CliCommand.browserSession */
40
+ browserSession?: {
41
+ reuse?: 'none' | 'site';
42
+ };
41
43
  }
@@ -32,7 +32,7 @@ export async function executePipeline(page, pipeline, ctx = {}) {
32
32
  }
33
33
  }
34
34
  catch (err) {
35
- // Attempt cleanup: close automation window on pipeline failure
35
+ // Attempt cleanup: release automation tab lease on pipeline failure.
36
36
  if (page?.closeWindow) {
37
37
  try {
38
38
  await page.closeWindow();
@@ -14,6 +14,7 @@ function createMockPage(overrides = {}) {
14
14
  snapshot: vi.fn().mockResolvedValue(''),
15
15
  click: vi.fn(),
16
16
  typeText: vi.fn(),
17
+ fillText: vi.fn(),
17
18
  pressKey: vi.fn(),
18
19
  getFormState: vi.fn().mockResolvedValue({}),
19
20
  wait: vi.fn(),
@@ -159,6 +160,13 @@ describe('executePipeline', () => {
159
160
  ]);
160
161
  expect(page.click).toHaveBeenCalledWith('5');
161
162
  });
163
+ it('fill step calls page.fillText with raw rendered text', async () => {
164
+ const page = createMockPage();
165
+ await executePipeline(page, [
166
+ { fill: { ref: '@5', text: 'line1\\n/ / ${{ args.tail }}' } },
167
+ ], { args: { tail: 'raw' } });
168
+ expect(page.fillText).toHaveBeenCalledWith('5', 'line1\\n/ / raw');
169
+ });
162
170
  it('navigate preserves existing data through pipeline', async () => {
163
171
  const page = createMockPage({
164
172
  evaluate: vi.fn().mockResolvedValue([{ a: 1 }]),
@@ -13,6 +13,15 @@ export type StepHandler<TData = unknown, TResult = unknown, TParams = unknown> =
13
13
  * Get a registered step handler by name.
14
14
  */
15
15
  export declare function getStep(name: string): StepHandler | undefined;
16
+ /**
17
+ * List all currently registered step names. Used by `validate.ts` to allowlist
18
+ * step names without maintaining a parallel hand-coded list.
19
+ *
20
+ * Note: this depends on registerStep() side effects below already having run.
21
+ * Importing this module triggers all core registrations at the bottom of the
22
+ * file, so the returned array reflects every core + plugin step at call time.
23
+ */
24
+ export declare function getRegisteredStepNames(): string[];
16
25
  /**
17
26
  * Register a new custom step handler for the YAML pipeline.
18
27
  */