@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
@@ -54,62 +54,64 @@ export class BrowserBridge {
54
54
  const effectiveSeconds = (timeoutSeconds && timeoutSeconds > 0) ? timeoutSeconds : Math.ceil(DAEMON_SPAWN_TIMEOUT / 1000);
55
55
  const timeoutMs = effectiveSeconds * 1000;
56
56
  const health = await getDaemonHealth({ contextId });
57
+ // Detect stale daemon before any fast path. A stale daemon can still have
58
+ // the extension connected, so this cannot live only in the no-extension branch.
59
+ const daemonVersion = health.status?.daemonVersion;
60
+ const isStale = !!health.status && (!daemonVersion || daemonVersion !== PKG_VERSION);
61
+ let staleDaemonReplaced = false;
62
+ if (isStale) {
63
+ // Stale daemon — restart it so all browser commands run against the
64
+ // currently installed package code, not the old daemon binary.
65
+ const reason = daemonVersion
66
+ ? `v${daemonVersion} ≠ v${PKG_VERSION}`
67
+ : `pre-version daemon, CLI is v${PKG_VERSION}`;
68
+ if (process.env.OPENCLI_VERBOSE || process.stderr.isTTY) {
69
+ process.stderr.write(`⚠️ Stale daemon detected (${reason}). Restarting...\n`);
70
+ }
71
+ const shutdownAccepted = await requestDaemonShutdown();
72
+ const portReleased = shutdownAccepted && await waitForDaemonStop(3000);
73
+ if (!portReleased) {
74
+ // Stale daemon replacement failed — don't blindly spawn on an occupied port
75
+ throw new BrowserConnectError('Stale daemon could not be replaced', `A stale daemon (${reason}) is running but did not shut down.\n` +
76
+ ' Run manually: opencli daemon stop && opencli doctor', 'daemon-not-running');
77
+ }
78
+ // Port released — fall through to spawn a fresh daemon
79
+ staleDaemonReplaced = true;
80
+ }
57
81
  // Fast path: everything ready
58
- if (health.state === 'ready')
82
+ if (!staleDaemonReplaced && health.state === 'ready')
59
83
  return;
60
- if (health.state === 'profile-required') {
84
+ if (!staleDaemonReplaced && health.state === 'profile-required') {
61
85
  throw new BrowserConnectError('Multiple Browser Bridge profiles are connected', 'Select one with --profile <name>, OPENCLI_PROFILE=<name>, or opencli profile use <name>.\n' +
62
86
  'Run opencli profile list to see connected profiles.', 'profile-required');
63
87
  }
64
- if (health.state === 'profile-disconnected') {
88
+ if (!staleDaemonReplaced && health.state === 'profile-disconnected') {
65
89
  const label = contextId ?? health.status.contextId ?? 'unknown';
66
90
  throw new BrowserConnectError(`Browser profile "${label}" is not connected`, 'Open the matching Chrome profile and make sure the OpenCLI extension is enabled, or choose another profile with opencli profile use <name>.', 'profile-disconnected');
67
91
  }
68
92
  // Daemon running but no extension
69
- if (health.state === 'no-extension') {
70
- // Detect stale daemon: version mismatch OR missing daemonVersion (pre-version daemon)
71
- const daemonVersion = health.status?.daemonVersion;
72
- const isStale = !daemonVersion || daemonVersion !== PKG_VERSION;
73
- if (isStale) {
74
- // Stale daemon — restart it so extension gets a fresh WebSocket endpoint
75
- const reason = daemonVersion
76
- ? `v${daemonVersion} ≠ v${PKG_VERSION}`
77
- : `pre-version daemon, CLI is v${PKG_VERSION}`;
78
- if (process.env.OPENCLI_VERBOSE || process.stderr.isTTY) {
79
- process.stderr.write(`⚠️ Stale daemon detected (${reason}). Restarting...\n`);
80
- }
81
- const shutdownAccepted = await requestDaemonShutdown();
82
- const portReleased = shutdownAccepted && await waitForDaemonStop(3000);
83
- if (!portReleased) {
84
- // Stale daemon replacement failed — don't blindly spawn on an occupied port
85
- throw new BrowserConnectError('Stale daemon could not be replaced', `A stale daemon (${reason}) is running but did not shut down.\n` +
86
- ' Run manually: opencli daemon stop && opencli doctor', 'daemon-not-running');
87
- }
88
- // Port released — fall through to spawn a fresh daemon
93
+ if (!staleDaemonReplaced && health.state === 'no-extension') {
94
+ // Same version wait for extension to connect
95
+ if (process.env.OPENCLI_VERBOSE || process.stderr.isTTY) {
96
+ process.stderr.write('⏳ Waiting for Chrome/Chromium extension to connect...\n');
97
+ process.stderr.write(' Make sure Chrome or Chromium is open and the OpenCLI extension is enabled.\n');
89
98
  }
90
- else {
91
- // Same version — wait for extension to connect
92
- if (process.env.OPENCLI_VERBOSE || process.stderr.isTTY) {
93
- process.stderr.write('⏳ Waiting for Chrome/Chromium extension to connect...\n');
94
- process.stderr.write(' Make sure Chrome or Chromium is open and the OpenCLI extension is enabled.\n');
95
- }
96
- if (await this._pollUntilReady(timeoutMs, contextId))
97
- return;
98
- const finalHealth = await getDaemonHealth({ contextId });
99
- if (finalHealth.state === 'profile-required') {
100
- throw new BrowserConnectError('Multiple Browser Bridge profiles are connected', 'Select one with --profile <name>, OPENCLI_PROFILE=<name>, or opencli profile use <name>.\n' +
101
- 'Run opencli profile list to see connected profiles.', 'profile-required');
102
- }
103
- if (finalHealth.state === 'profile-disconnected') {
104
- const label = contextId ?? finalHealth.status.contextId ?? 'unknown';
105
- throw new BrowserConnectError(`Browser profile "${label}" is not connected`, 'Open the matching Chrome profile and make sure the OpenCLI extension is enabled, or choose another profile with opencli profile use <name>.', 'profile-disconnected');
106
- }
107
- throw new BrowserConnectError('Browser Bridge extension not connected', 'Make sure Chrome/Chromium is open and the extension is enabled.\n' +
108
- 'If the extension is installed, try: opencli daemon stop && opencli doctor\n' +
109
- 'If not installed:\n' +
110
- ' 1. Download: https://github.com/jackwener/opencli/releases\n' +
111
- ' 2. Open chrome://extensions → Developer Mode → Load unpacked', 'extension-not-connected');
99
+ if (await this._pollUntilReady(timeoutMs, contextId))
100
+ return;
101
+ const finalHealth = await getDaemonHealth({ contextId });
102
+ if (finalHealth.state === 'profile-required') {
103
+ throw new BrowserConnectError('Multiple Browser Bridge profiles are connected', 'Select one with --profile <name>, OPENCLI_PROFILE=<name>, or opencli profile use <name>.\n' +
104
+ 'Run opencli profile list to see connected profiles.', 'profile-required');
112
105
  }
106
+ if (finalHealth.state === 'profile-disconnected') {
107
+ const label = contextId ?? finalHealth.status.contextId ?? 'unknown';
108
+ throw new BrowserConnectError(`Browser profile "${label}" is not connected`, 'Open the matching Chrome profile and make sure the OpenCLI extension is enabled, or choose another profile with opencli profile use <name>.', 'profile-disconnected');
109
+ }
110
+ throw new BrowserConnectError('Browser Bridge extension not connected', 'Make sure Chrome/Chromium is open and the extension is enabled.\n' +
111
+ 'If the extension is installed, try: opencli daemon stop && opencli doctor\n' +
112
+ 'If not installed:\n' +
113
+ ' 1. Download: https://github.com/jackwener/opencli/releases\n' +
114
+ ' 2. Open chrome://extensions → Developer Mode → Load unpacked', 'extension-not-connected');
113
115
  }
114
116
  // No daemon — spawn one
115
117
  if (process.env.OPENCLI_VERBOSE || process.stderr.isTTY) {
@@ -26,6 +26,7 @@ export declare class CDPBridge implements IBrowserFactory {
26
26
  workspace?: string;
27
27
  cdpEndpoint?: string;
28
28
  contextId?: string;
29
+ idleTimeout?: number;
29
30
  }): Promise<IPage>;
30
31
  close(): Promise<void>;
31
32
  send(method: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<unknown>;
@@ -202,16 +202,58 @@ class CDPPage extends BasePage {
202
202
  : cookies;
203
203
  }
204
204
  async screenshot(options = {}) {
205
- const result = await this.bridge.send('Page.captureScreenshot', {
206
- format: options.format ?? 'png',
207
- quality: options.format === 'jpeg' ? (options.quality ?? 80) : undefined,
208
- captureBeyondViewport: options.fullPage ?? false,
209
- });
210
- const base64 = isRecord(result) && typeof result.data === 'string' ? result.data : '';
211
- if (options.path) {
212
- await saveBase64ToFile(base64, options.path);
205
+ const fullPage = options.fullPage === true;
206
+ const overrideWidth = options.width && options.width > 0 ? Math.ceil(options.width) : undefined;
207
+ // height is ignored under fullPage so the captureBeyondViewport path stays unchanged for users who pass --height alongside --full-page.
208
+ const overrideHeight = !fullPage && options.height && options.height > 0 ? Math.ceil(options.height) : undefined;
209
+ const needsOverride = overrideWidth !== undefined || overrideHeight !== undefined;
210
+ if (needsOverride) {
211
+ if (overrideWidth !== undefined && fullPage) {
212
+ await this.bridge.send('Emulation.setDeviceMetricsOverride', {
213
+ mobile: false,
214
+ width: overrideWidth,
215
+ height: 0,
216
+ deviceScaleFactor: 1,
217
+ });
218
+ }
219
+ let finalWidth = overrideWidth ?? 0;
220
+ let finalHeight = overrideHeight ?? 0;
221
+ if (fullPage) {
222
+ const metrics = await this.bridge.send('Page.getLayoutMetrics');
223
+ const m = isRecord(metrics) ? metrics : {};
224
+ const css = isRecord(m.cssContentSize) ? m.cssContentSize : undefined;
225
+ const fb = isRecord(m.contentSize) ? m.contentSize : undefined;
226
+ const size = css ?? fb;
227
+ if (size && typeof size.width === 'number' && typeof size.height === 'number') {
228
+ if (finalWidth === 0)
229
+ finalWidth = Math.ceil(size.width);
230
+ finalHeight = Math.ceil(size.height);
231
+ }
232
+ }
233
+ await this.bridge.send('Emulation.setDeviceMetricsOverride', {
234
+ mobile: false,
235
+ width: finalWidth,
236
+ height: finalHeight,
237
+ deviceScaleFactor: 1,
238
+ });
239
+ }
240
+ try {
241
+ const result = await this.bridge.send('Page.captureScreenshot', {
242
+ format: options.format ?? 'png',
243
+ quality: options.format === 'jpeg' ? (options.quality ?? 80) : undefined,
244
+ captureBeyondViewport: !needsOverride && fullPage,
245
+ });
246
+ const base64 = isRecord(result) && typeof result.data === 'string' ? result.data : '';
247
+ if (options.path) {
248
+ await saveBase64ToFile(base64, options.path);
249
+ }
250
+ return base64;
251
+ }
252
+ finally {
253
+ if (needsOverride) {
254
+ await this.bridge.send('Emulation.clearDeviceMetricsOverride').catch(() => { });
255
+ }
213
256
  }
214
- return base64;
215
257
  }
216
258
  async startNetworkCapture(pattern = '') {
217
259
  // Always update the filter pattern
@@ -20,6 +20,10 @@ export interface DaemonCommand {
20
20
  format?: 'png' | 'jpeg';
21
21
  quality?: number;
22
22
  fullPage?: boolean;
23
+ /** Override viewport width in CSS pixels for screenshot (0 / undefined = use current) */
24
+ width?: number;
25
+ /** Override viewport height in CSS pixels for screenshot (0 / undefined = use current; ignored when fullPage) */
26
+ height?: number;
23
27
  /** Local file paths for set-file-input action */
24
28
  files?: string[];
25
29
  /** CSS selector for file input element (set-file-input action) */
@@ -65,7 +65,7 @@ export function isTransientBrowserError(err) {
65
65
  export function formatBrowserConnectError(kind, detail) {
66
66
  switch (kind) {
67
67
  case 'daemon-not-running':
68
- return new BrowserConnectError('Cannot connect to opencli daemon.' + (detail ? `\n\n${detail}` : ''), `The daemon should auto-start. If it keeps failing, make sure port ${DEFAULT_DAEMON_PORT} is available.`, kind);
68
+ return new BrowserConnectError('Cannot connect to opencli daemon.' + (detail ? `\n\n${detail}` : ''), `Run \`opencli doctor\` to diagnose, or \`opencli daemon restart\` to force a fresh daemon. Default port is ${DEFAULT_DAEMON_PORT}.`, kind);
69
69
  case 'extension-not-connected':
70
70
  return new BrowserConnectError('Browser Bridge extension is not connected.' + (detail ? `\n\n${detail}` : ''), 'Install the extension from GitHub Releases, then reload.', kind);
71
71
  case 'command-failed':
@@ -41,7 +41,7 @@ export declare class Page extends BasePage {
41
41
  domain?: string;
42
42
  url?: string;
43
43
  }): Promise<BrowserCookie[]>;
44
- /** Close the automation window in the extension */
44
+ /** Release the current automation tab lease in the extension */
45
45
  closeWindow(): Promise<void>;
46
46
  tabs(): Promise<unknown[]>;
47
47
  newTab(url?: string): Promise<string | undefined>;
@@ -143,7 +143,7 @@ export class Page extends BasePage {
143
143
  const result = await sendCommand('cookies', { ...this._wsOpt(), ...opts });
144
144
  return Array.isArray(result) ? result : [];
145
145
  }
146
- /** Close the automation window in the extension */
146
+ /** Release the current automation tab lease in the extension */
147
147
  async closeWindow() {
148
148
  try {
149
149
  await sendCommand('close-window', { ...this._wsOpt() });
@@ -205,6 +205,8 @@ export class Page extends BasePage {
205
205
  format: options.format,
206
206
  quality: options.quality,
207
207
  fullPage: options.fullPage,
208
+ width: options.width,
209
+ height: options.height,
208
210
  });
209
211
  if (options.path) {
210
212
  await saveBase64ToFile(base64, options.path);
@@ -216,3 +216,32 @@ describe('Page active target tracking', () => {
216
216
  expect(evalCall?.[1]).not.toHaveProperty('page');
217
217
  });
218
218
  });
219
+ describe('Page.screenshot', () => {
220
+ beforeEach(() => {
221
+ sendCommandMock.mockReset();
222
+ sendCommandFullMock.mockReset();
223
+ warnMock.mockReset();
224
+ });
225
+ it('forwards width / height / fullPage options to the bridge', async () => {
226
+ sendCommandMock.mockResolvedValueOnce('BASE64');
227
+ const page = new Page('browser:default');
228
+ const data = await page.screenshot({ fullPage: true, width: 1080 });
229
+ expect(data).toBe('BASE64');
230
+ expect(sendCommandMock).toHaveBeenCalledWith('screenshot', expect.objectContaining({
231
+ workspace: 'browser:default',
232
+ fullPage: true,
233
+ width: 1080,
234
+ }));
235
+ });
236
+ it('omits viewport overrides when none are set', async () => {
237
+ sendCommandMock.mockResolvedValueOnce('BASE64');
238
+ const page = new Page('browser:default');
239
+ await page.screenshot();
240
+ const call = sendCommandMock.mock.calls.at(-1);
241
+ expect(call?.[0]).toBe('screenshot');
242
+ const args = call?.[1];
243
+ expect(args.width).toBeUndefined();
244
+ expect(args.height).toBeUndefined();
245
+ expect(args.fullPage).toBeUndefined();
246
+ });
247
+ });
@@ -15,8 +15,9 @@
15
15
  * - selector_not_found: 0 matches
16
16
  * - selector_ambiguous: >1 matches for a write op without --nth
17
17
  * - selector_nth_out_of_range: --nth beyond matches_n
18
+ * - not_editable: target exists but cannot accept text input
18
19
  */
19
- export type TargetErrorCode = 'not_found' | 'stale_ref' | 'invalid_selector' | 'selector_not_found' | 'selector_ambiguous' | 'selector_nth_out_of_range';
20
+ export type TargetErrorCode = 'not_found' | 'stale_ref' | 'invalid_selector' | 'selector_not_found' | 'selector_ambiguous' | 'selector_nth_out_of_range' | 'not_editable';
20
21
  export interface TargetErrorInfo {
21
22
  code: TargetErrorCode;
22
23
  message: string;
@@ -15,6 +15,7 @@
15
15
  * - selector_not_found: 0 matches
16
16
  * - selector_ambiguous: >1 matches for a write op without --nth
17
17
  * - selector_nth_out_of_range: --nth beyond matches_n
18
+ * - not_editable: target exists but cannot accept text input
18
19
  */
19
20
  export class TargetError extends Error {
20
21
  code;
@@ -79,6 +79,25 @@ export declare function clickResolvedJs(opts?: {
79
79
  * Generate JS for type that uses the unified resolver.
80
80
  */
81
81
  export declare function typeResolvedJs(text: string): string;
82
+ export type FillResolvedResult = {
83
+ ok: true;
84
+ actual: string;
85
+ expected: string;
86
+ length: number;
87
+ mode: 'input' | 'textarea' | 'contenteditable';
88
+ } | {
89
+ ok: false;
90
+ actual: string;
91
+ expected: string;
92
+ length: number;
93
+ mode: 'input' | 'textarea' | 'contenteditable';
94
+ } | {
95
+ ok: false;
96
+ reason: string;
97
+ tag?: string;
98
+ type?: string;
99
+ role?: string;
100
+ };
82
101
  /**
83
102
  * Prepare the resolved element for native CDP Input.insertText.
84
103
  *
@@ -90,6 +109,12 @@ export declare function prepareNativeTypeResolvedJs(opts?: {
90
109
  skipScroll?: boolean;
91
110
  skipFocus?: boolean;
92
111
  }): string;
112
+ /**
113
+ * Verify the exact value/text currently held by the resolved editable target.
114
+ * Assumes resolveTargetJs and prepareNativeTypeResolvedJs have already set
115
+ * `window.__resolved` to the normalized editable host.
116
+ */
117
+ export declare function verifyFilledResolvedJs(expected: string): string;
93
118
  /**
94
119
  * Generate JS for scrollTo that uses the unified resolver.
95
120
  * Assumes resolveTargetJs has been called and __resolved is set.
@@ -411,6 +411,49 @@ export function prepareNativeTypeResolvedJs(opts = {}) {
411
411
  })()
412
412
  `;
413
413
  }
414
+ /**
415
+ * Verify the exact value/text currently held by the resolved editable target.
416
+ * Assumes resolveTargetJs and prepareNativeTypeResolvedJs have already set
417
+ * `window.__resolved` to the normalized editable host.
418
+ */
419
+ export function verifyFilledResolvedJs(expected) {
420
+ const safeText = JSON.stringify(expected);
421
+ return `
422
+ (() => {
423
+ const el = window.__resolved;
424
+ if (!el) return { ok: false, reason: 'no_resolved_element' };
425
+
426
+ const tag = el.tagName ? el.tagName.toLowerCase() : '';
427
+ const isInput = el instanceof HTMLInputElement;
428
+ const isTextarea = el instanceof HTMLTextAreaElement;
429
+ const mode = el.isContentEditable
430
+ ? 'contenteditable'
431
+ : isTextarea
432
+ ? 'textarea'
433
+ : isInput
434
+ ? 'input'
435
+ : '';
436
+
437
+ if (!mode) {
438
+ return {
439
+ ok: false,
440
+ reason: 'not_editable',
441
+ tag,
442
+ role: el.getAttribute ? (el.getAttribute('role') || '') : '',
443
+ };
444
+ }
445
+
446
+ const actual = mode === 'contenteditable' ? (el.innerText || '') : String(el.value || '');
447
+ return {
448
+ ok: actual === ${safeText},
449
+ actual,
450
+ expected: ${safeText},
451
+ length: actual.length,
452
+ mode,
453
+ };
454
+ })()
455
+ `;
456
+ }
414
457
  /**
415
458
  * Generate JS for scrollTo that uses the unified resolver.
416
459
  * Assumes resolveTargetJs has been called and __resolved is set.
@@ -165,6 +165,24 @@ describe('BrowserBridge state', () => {
165
165
  const bridge = new BrowserBridge();
166
166
  await expect(bridge.connect({ timeout: 0.1 })).rejects.toThrow('Stale daemon could not be replaced');
167
167
  });
168
+ it('attempts stale daemon replacement even when extension is connected', async () => {
169
+ vi.spyOn(daemonClient, 'getDaemonHealth').mockResolvedValue({
170
+ state: 'ready',
171
+ status: {
172
+ ok: true,
173
+ pid: 1,
174
+ uptime: 0,
175
+ daemonVersion: '0.0.1',
176
+ extensionConnected: true,
177
+ pending: 0,
178
+ memoryMB: 0,
179
+ port: 0,
180
+ },
181
+ });
182
+ vi.spyOn(daemonClient, 'requestDaemonShutdown').mockResolvedValue(false);
183
+ const bridge = new BrowserBridge();
184
+ await expect(bridge.connect({ timeout: 0.1 })).rejects.toThrow('Stale daemon could not be replaced');
185
+ });
168
186
  });
169
187
  describe('stealth anti-detection', () => {
170
188
  it('generates non-empty JS string', () => {
@@ -32,7 +32,13 @@ const PACKAGE_ROOT = findPackageRoot(fileURLToPath(import.meta.url));
32
32
  const CLIS_DIR = path.join(PACKAGE_ROOT, 'clis');
33
33
  // Write manifest next to clis/ so both dev and installed runtime can find it.
34
34
  const OUTPUT = getCliManifestPath(CLIS_DIR);
35
- const CLI_MODULE_PATTERN = /\bcli\s*\(/;
35
+ // Module is treated as a CLI command source if it either:
36
+ // 1. Calls `cli(...)` directly (the common case), or
37
+ // 2. Calls a factory `make<Pascal>Command(...)` from clis/_shared/ that
38
+ // wraps `cli(...)`. Without (2), shared-factory adapters
39
+ // (codex/cursor/chatwise new/status/dump/screenshot) match no `cli(`
40
+ // token at the top level and silently drop out of the manifest.
41
+ const CLI_MODULE_PATTERN = /\bcli\s*\(|\bmake[A-Z]\w*Command\s*\(/;
36
42
  /**
37
43
  * Thrown by `loadManifestEntries` when an adapter file looks like a CLI
38
44
  * module (matches CLI_MODULE_PATTERN) but cannot be imported. Callers
@@ -91,13 +97,12 @@ function toManifestEntry(cmd, modulePath, sourceFile) {
91
97
  browser: cmd.browser ?? true,
92
98
  args: toManifestArgs(cmd.args),
93
99
  columns: cmd.columns,
94
- timeout: cmd.timeoutSeconds,
95
- deprecated: cmd.deprecated,
96
- replacedBy: cmd.replacedBy,
100
+ defaultFormat: cmd.defaultFormat,
97
101
  type: 'js',
98
102
  modulePath,
99
103
  sourceFile,
100
104
  navigateBefore: cmd.navigateBefore,
105
+ browserSession: cmd.browserSession,
101
106
  };
102
107
  }
103
108
  /**
@@ -46,8 +46,7 @@ describe('manifest helper rules', () => {
46
46
  ],
47
47
  domain: 'localhost',
48
48
  navigateBefore: 'https://example.com/session',
49
- deprecated: 'legacy command',
50
- replacedBy: 'opencli demo new',
49
+ defaultFormat: 'plain',
51
50
  }),
52
51
  }));
53
52
  expect(entries).toMatchObject([
@@ -74,8 +73,7 @@ describe('manifest helper rules', () => {
74
73
  type: 'js',
75
74
  modulePath: `${site}/${site}.js`,
76
75
  navigateBefore: 'https://example.com/session',
77
- deprecated: 'legacy command',
78
- replacedBy: 'opencli demo new',
76
+ defaultFormat: 'plain',
79
77
  },
80
78
  ]);
81
79
  // Verify sourceFile is included and stable for manifest consumers.
@@ -96,8 +94,6 @@ describe('manifest helper rules', () => {
96
94
  name: 'legacy',
97
95
  access: 'read',
98
96
  description: 'legacy command',
99
- deprecated: 'legacy is deprecated',
100
- replacedBy: 'opencli demo new',
101
97
  });
102
98
  return {};
103
99
  });
@@ -112,8 +108,6 @@ describe('manifest helper rules', () => {
112
108
  args: [],
113
109
  type: 'js',
114
110
  modulePath: `${site}/${site}.js`,
115
- deprecated: 'legacy is deprecated',
116
- replacedBy: 'opencli demo new',
117
111
  },
118
112
  ]);
119
113
  // Verify sourceFile is included
@@ -1,4 +1,19 @@
1
1
  import type { CliCommand } from './registry.js';
2
- /** Pipeline steps that require a live browser session. */
2
+ /**
3
+ * Pipeline steps that require a live browser session.
4
+ *
5
+ * Note: this is the *subset* of registered pipeline steps that need a page;
6
+ * non-browser steps (fetch, select, map, filter, sort, limit, download)
7
+ * deliberately stay out. The full registered-step list lives in
8
+ * `src/pipeline/registry.ts` and is re-derived elsewhere via
9
+ * `getRegisteredStepNames()` (e.g. in `validate.ts`). When you add a new
10
+ * pipeline step, decide whether it belongs here based on whether its handler
11
+ * touches the IPage object — and `src/capabilityRouting.test.ts` verifies the
12
+ * subset relationship is intact.
13
+ */
3
14
  export declare const BROWSER_ONLY_STEPS: Set<string>;
15
+ /** Internal helper: ensure BROWSER_ONLY_STEPS is a subset of registered pipeline steps. */
16
+ export declare function _validateBrowserOnlyStepsAgainstRegistry(): {
17
+ extras: string[];
18
+ };
4
19
  export declare function shouldUseBrowserSession(cmd: CliCommand): boolean;
@@ -1,8 +1,21 @@
1
- /** Pipeline steps that require a live browser session. */
1
+ import { getRegisteredStepNames } from './pipeline/registry.js';
2
+ /**
3
+ * Pipeline steps that require a live browser session.
4
+ *
5
+ * Note: this is the *subset* of registered pipeline steps that need a page;
6
+ * non-browser steps (fetch, select, map, filter, sort, limit, download)
7
+ * deliberately stay out. The full registered-step list lives in
8
+ * `src/pipeline/registry.ts` and is re-derived elsewhere via
9
+ * `getRegisteredStepNames()` (e.g. in `validate.ts`). When you add a new
10
+ * pipeline step, decide whether it belongs here based on whether its handler
11
+ * touches the IPage object — and `src/capabilityRouting.test.ts` verifies the
12
+ * subset relationship is intact.
13
+ */
2
14
  export const BROWSER_ONLY_STEPS = new Set([
3
15
  'navigate',
4
16
  'click',
5
17
  'type',
18
+ 'fill',
6
19
  'wait',
7
20
  'press',
8
21
  'snapshot',
@@ -10,6 +23,16 @@ export const BROWSER_ONLY_STEPS = new Set([
10
23
  'intercept',
11
24
  'tap',
12
25
  ]);
26
+ /** Internal helper: ensure BROWSER_ONLY_STEPS is a subset of registered pipeline steps. */
27
+ export function _validateBrowserOnlyStepsAgainstRegistry() {
28
+ const registered = new Set(getRegisteredStepNames());
29
+ const extras = [];
30
+ for (const step of BROWSER_ONLY_STEPS) {
31
+ if (!registered.has(step))
32
+ extras.push(step);
33
+ }
34
+ return { extras };
35
+ }
13
36
  function pipelineNeedsBrowserSession(pipeline) {
14
37
  return pipeline.some((step) => {
15
38
  if (!step || typeof step !== 'object')
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { Strategy } from './registry.js';
3
- import { shouldUseBrowserSession } from './capabilityRouting.js';
3
+ import { BROWSER_ONLY_STEPS, _validateBrowserOnlyStepsAgainstRegistry, shouldUseBrowserSession } from './capabilityRouting.js';
4
+ import { getRegisteredStepNames } from './pipeline/registry.js';
4
5
  function makeCmd(partial) {
5
6
  return {
6
7
  site: 'test',
@@ -42,4 +43,21 @@ describe('shouldUseBrowserSession', () => {
42
43
  func: async () => [],
43
44
  }))).toBe(true);
44
45
  });
46
+ it('routes pipelines containing the fill step into a browser session', () => {
47
+ expect(shouldUseBrowserSession(makeCmd({
48
+ browser: true,
49
+ strategy: Strategy.PUBLIC,
50
+ pipeline: [{ navigate: 'https://example.com' }, { fill: { ref: '#q', text: 'hello' } }],
51
+ }))).toBe(true);
52
+ });
53
+ });
54
+ describe('BROWSER_ONLY_STEPS / pipeline registry alignment', () => {
55
+ it('is a subset of registered pipeline step names', () => {
56
+ const { extras } = _validateBrowserOnlyStepsAgainstRegistry();
57
+ expect(extras).toEqual([]);
58
+ });
59
+ it('includes fill (DOM-touching step added in PR #1222)', () => {
60
+ expect(BROWSER_ONLY_STEPS.has('fill')).toBe(true);
61
+ expect(getRegisteredStepNames()).toContain('fill');
62
+ });
45
63
  });