publishport-opencli 1.8.4-pp.1

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 (2171) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +300 -0
  3. package/README.zh-CN.md +341 -0
  4. package/cli-manifest.json +44212 -0
  5. package/clis/12306/auth.js +59 -0
  6. package/clis/12306/me.js +73 -0
  7. package/clis/12306/orders.js +96 -0
  8. package/clis/12306/passengers.js +90 -0
  9. package/clis/12306/price.js +166 -0
  10. package/clis/12306/stations.js +66 -0
  11. package/clis/12306/train.js +91 -0
  12. package/clis/12306/trains.js +166 -0
  13. package/clis/12306/utils.js +272 -0
  14. package/clis/12306/utils.test.js +424 -0
  15. package/clis/1688/assets.js +205 -0
  16. package/clis/1688/assets.test.js +39 -0
  17. package/clis/1688/auth.js +46 -0
  18. package/clis/1688/download.js +77 -0
  19. package/clis/1688/download.test.js +31 -0
  20. package/clis/1688/item.js +188 -0
  21. package/clis/1688/item.test.js +67 -0
  22. package/clis/1688/search.js +310 -0
  23. package/clis/1688/search.test.js +75 -0
  24. package/clis/1688/shared.js +557 -0
  25. package/clis/1688/shared.test.js +57 -0
  26. package/clis/1688/store.js +227 -0
  27. package/clis/1688/store.test.js +62 -0
  28. package/clis/1point3acres/auth.js +52 -0
  29. package/clis/1point3acres/digest.js +35 -0
  30. package/clis/1point3acres/forum.js +51 -0
  31. package/clis/1point3acres/forums.js +44 -0
  32. package/clis/1point3acres/hot.js +35 -0
  33. package/clis/1point3acres/latest.js +35 -0
  34. package/clis/1point3acres/notifications.js +64 -0
  35. package/clis/1point3acres/search.js +71 -0
  36. package/clis/1point3acres/thread.js +117 -0
  37. package/clis/1point3acres/user.js +77 -0
  38. package/clis/1point3acres/utils.js +247 -0
  39. package/clis/36kr/article.js +66 -0
  40. package/clis/36kr/article.test.js +46 -0
  41. package/clis/36kr/hot.js +87 -0
  42. package/clis/36kr/hot.test.js +15 -0
  43. package/clis/36kr/news.js +52 -0
  44. package/clis/36kr/news.test.js +85 -0
  45. package/clis/36kr/search.js +79 -0
  46. package/clis/51job/company.js +126 -0
  47. package/clis/51job/detail.js +109 -0
  48. package/clis/51job/hot.js +56 -0
  49. package/clis/51job/search.js +80 -0
  50. package/clis/51job/utils.js +302 -0
  51. package/clis/51job/utils.test.js +69 -0
  52. package/clis/_atlassian/shared.js +577 -0
  53. package/clis/_atlassian/shared.test.js +170 -0
  54. package/clis/_shared/common.js +32 -0
  55. package/clis/_shared/desktop-commands.js +112 -0
  56. package/clis/_shared/search-adapter.js +70 -0
  57. package/clis/_shared/site-auth.js +118 -0
  58. package/clis/_shared/site-auth.test.js +98 -0
  59. package/clis/aibase/news.js +110 -0
  60. package/clis/aibase/news.test.js +59 -0
  61. package/clis/amazon/auth.js +53 -0
  62. package/clis/amazon/bestsellers.js +8 -0
  63. package/clis/amazon/bestsellers.test.js +29 -0
  64. package/clis/amazon/discussion.js +123 -0
  65. package/clis/amazon/discussion.test.js +124 -0
  66. package/clis/amazon/movers-shakers.js +8 -0
  67. package/clis/amazon/new-releases.js +8 -0
  68. package/clis/amazon/offer.js +141 -0
  69. package/clis/amazon/offer.test.js +29 -0
  70. package/clis/amazon/product.js +93 -0
  71. package/clis/amazon/product.test.js +24 -0
  72. package/clis/amazon/rankings.js +227 -0
  73. package/clis/amazon/rankings.test.js +41 -0
  74. package/clis/amazon/search.js +88 -0
  75. package/clis/amazon/search.test.js +22 -0
  76. package/clis/amazon/shared.js +365 -0
  77. package/clis/amazon/shared.test.js +44 -0
  78. package/clis/antigravity/SKILL.md +38 -0
  79. package/clis/antigravity/_actions.js +318 -0
  80. package/clis/antigravity/antigravity.test.js +172 -0
  81. package/clis/antigravity/audit-extras.js +341 -0
  82. package/clis/antigravity/delete.js +60 -0
  83. package/clis/antigravity/dump.js +29 -0
  84. package/clis/antigravity/extract-code.js +33 -0
  85. package/clis/antigravity/history.js +26 -0
  86. package/clis/antigravity/mark-read.js +52 -0
  87. package/clis/antigravity/model.js +161 -0
  88. package/clis/antigravity/new.js +26 -0
  89. package/clis/antigravity/read.js +35 -0
  90. package/clis/antigravity/rename.js +33 -0
  91. package/clis/antigravity/send.js +36 -0
  92. package/clis/antigravity/serve.js +558 -0
  93. package/clis/antigravity/status.js +19 -0
  94. package/clis/antigravity/storage.js +366 -0
  95. package/clis/antigravity/watch.js +43 -0
  96. package/clis/apple-podcasts/commands.test.js +119 -0
  97. package/clis/apple-podcasts/episodes.js +29 -0
  98. package/clis/apple-podcasts/search.js +31 -0
  99. package/clis/apple-podcasts/top.js +45 -0
  100. package/clis/apple-podcasts/utils.js +30 -0
  101. package/clis/apple-podcasts/utils.test.js +57 -0
  102. package/clis/archive/archive.test.js +262 -0
  103. package/clis/archive/item.js +92 -0
  104. package/clis/archive/search.js +115 -0
  105. package/clis/archive/snapshots.js +129 -0
  106. package/clis/archive/wayback.js +83 -0
  107. package/clis/arxiv/arxiv.test.js +112 -0
  108. package/clis/arxiv/author.js +44 -0
  109. package/clis/arxiv/paper.js +22 -0
  110. package/clis/arxiv/recent.js +33 -0
  111. package/clis/arxiv/search.js +36 -0
  112. package/clis/arxiv/utils.js +112 -0
  113. package/clis/autohome/__fixtures__/catalog.html +11 -0
  114. package/clis/autohome/__fixtures__/koubei.json +116 -0
  115. package/clis/autohome/autohome.test.js +115 -0
  116. package/clis/autohome/brand.js +108 -0
  117. package/clis/autohome/score.js +103 -0
  118. package/clis/autohome/utils.js +157 -0
  119. package/clis/baidu-scholar/search.js +87 -0
  120. package/clis/baidu-scholar/search.test.js +23 -0
  121. package/clis/band/auth.js +62 -0
  122. package/clis/band/bands.js +73 -0
  123. package/clis/band/mentions.js +128 -0
  124. package/clis/band/post.js +176 -0
  125. package/clis/band/posts.js +95 -0
  126. package/clis/barchart/flow.js +116 -0
  127. package/clis/barchart/greeks.js +208 -0
  128. package/clis/barchart/greeks.test.js +138 -0
  129. package/clis/barchart/options.js +107 -0
  130. package/clis/barchart/quote.js +135 -0
  131. package/clis/bbc/news.js +42 -0
  132. package/clis/bbc/topic.js +57 -0
  133. package/clis/bbc/utils.js +79 -0
  134. package/clis/bilibili/auth.js +36 -0
  135. package/clis/bilibili/comment.js +107 -0
  136. package/clis/bilibili/comment.test.js +153 -0
  137. package/clis/bilibili/comments.js +136 -0
  138. package/clis/bilibili/comments.test.js +142 -0
  139. package/clis/bilibili/download.js +178 -0
  140. package/clis/bilibili/download.test.js +173 -0
  141. package/clis/bilibili/dynamic.js +34 -0
  142. package/clis/bilibili/dynamic.test.js +68 -0
  143. package/clis/bilibili/favorite.js +47 -0
  144. package/clis/bilibili/feed.js +220 -0
  145. package/clis/bilibili/follow.js +140 -0
  146. package/clis/bilibili/follow.test.js +203 -0
  147. package/clis/bilibili/following.js +44 -0
  148. package/clis/bilibili/history.js +45 -0
  149. package/clis/bilibili/hot.js +40 -0
  150. package/clis/bilibili/hot.test.js +17 -0
  151. package/clis/bilibili/me.js +13 -0
  152. package/clis/bilibili/ranking.js +25 -0
  153. package/clis/bilibili/search.js +24 -0
  154. package/clis/bilibili/subtitle.js +141 -0
  155. package/clis/bilibili/subtitle.test.js +240 -0
  156. package/clis/bilibili/summary.js +167 -0
  157. package/clis/bilibili/summary.test.js +210 -0
  158. package/clis/bilibili/unfollow.js +121 -0
  159. package/clis/bilibili/user-videos.js +39 -0
  160. package/clis/bilibili/utils.js +273 -0
  161. package/clis/bilibili/utils.test.js +97 -0
  162. package/clis/bilibili/video.js +155 -0
  163. package/clis/bilibili/video.test.js +308 -0
  164. package/clis/binance/asks.js +22 -0
  165. package/clis/binance/commands.test.js +70 -0
  166. package/clis/binance/depth.js +21 -0
  167. package/clis/binance/gainers.js +23 -0
  168. package/clis/binance/klines.js +22 -0
  169. package/clis/binance/losers.js +23 -0
  170. package/clis/binance/pairs.js +22 -0
  171. package/clis/binance/price.js +19 -0
  172. package/clis/binance/prices.js +20 -0
  173. package/clis/binance/ticker.js +22 -0
  174. package/clis/binance/top.js +22 -0
  175. package/clis/binance/trades.js +21 -0
  176. package/clis/bloomberg/businessweek.js +125 -0
  177. package/clis/bloomberg/businessweek.test.js +149 -0
  178. package/clis/bloomberg/crypto.js +18 -0
  179. package/clis/bloomberg/economics.js +18 -0
  180. package/clis/bloomberg/feeds.js +16 -0
  181. package/clis/bloomberg/green.js +18 -0
  182. package/clis/bloomberg/industries.js +18 -0
  183. package/clis/bloomberg/main.js +18 -0
  184. package/clis/bloomberg/markets.js +18 -0
  185. package/clis/bloomberg/news.js +106 -0
  186. package/clis/bloomberg/opinions.js +18 -0
  187. package/clis/bloomberg/politics.js +18 -0
  188. package/clis/bloomberg/pursuits.js +18 -0
  189. package/clis/bloomberg/tech.js +18 -0
  190. package/clis/bloomberg/utils.js +380 -0
  191. package/clis/bloomberg/utils.test.js +129 -0
  192. package/clis/bluesky/feeds.js +28 -0
  193. package/clis/bluesky/followers.js +28 -0
  194. package/clis/bluesky/following.js +28 -0
  195. package/clis/bluesky/profile.js +30 -0
  196. package/clis/bluesky/search.js +29 -0
  197. package/clis/bluesky/starter-packs.js +29 -0
  198. package/clis/bluesky/thread.js +31 -0
  199. package/clis/bluesky/trending.js +20 -0
  200. package/clis/bluesky/user.js +35 -0
  201. package/clis/booking/booking.test.js +356 -0
  202. package/clis/booking/search.js +351 -0
  203. package/clis/boss/auth.js +47 -0
  204. package/clis/boss/batchgreet.js +61 -0
  205. package/clis/boss/chatlist.js +116 -0
  206. package/clis/boss/chatlist.test.js +211 -0
  207. package/clis/boss/chatmsg.js +117 -0
  208. package/clis/boss/chatmsg.test.js +230 -0
  209. package/clis/boss/detail.js +63 -0
  210. package/clis/boss/exchange.js +46 -0
  211. package/clis/boss/greet.js +49 -0
  212. package/clis/boss/invite.js +64 -0
  213. package/clis/boss/joblist.js +31 -0
  214. package/clis/boss/mark.js +70 -0
  215. package/clis/boss/recommend.js +45 -0
  216. package/clis/boss/resume.js +150 -0
  217. package/clis/boss/search.js +197 -0
  218. package/clis/boss/search.test.js +78 -0
  219. package/clis/boss/send.js +44 -0
  220. package/clis/boss/stats.js +66 -0
  221. package/clis/boss/utils.js +468 -0
  222. package/clis/boss/utils.test.js +34 -0
  223. package/clis/brave/search.js +80 -0
  224. package/clis/brave/search.test.js +76 -0
  225. package/clis/chaoxing/assignments.js +76 -0
  226. package/clis/chaoxing/auth.js +54 -0
  227. package/clis/chaoxing/exams.js +75 -0
  228. package/clis/chaoxing/utils.js +223 -0
  229. package/clis/chaoxing/utils.test.js +45 -0
  230. package/clis/chatgpt/ask.js +129 -0
  231. package/clis/chatgpt/auth.js +52 -0
  232. package/clis/chatgpt/commands.test.js +204 -0
  233. package/clis/chatgpt/detail.js +63 -0
  234. package/clis/chatgpt/envelope.test.js +108 -0
  235. package/clis/chatgpt/history.js +39 -0
  236. package/clis/chatgpt/image.js +167 -0
  237. package/clis/chatgpt/image.test.js +207 -0
  238. package/clis/chatgpt/model.js +31 -0
  239. package/clis/chatgpt/model.test.js +36 -0
  240. package/clis/chatgpt/new.js +32 -0
  241. package/clis/chatgpt/project-file-add.js +74 -0
  242. package/clis/chatgpt/project-list.js +37 -0
  243. package/clis/chatgpt/read.js +44 -0
  244. package/clis/chatgpt/send.js +68 -0
  245. package/clis/chatgpt/status.js +29 -0
  246. package/clis/chatgpt/utils.js +2226 -0
  247. package/clis/chatgpt/utils.test.js +1453 -0
  248. package/clis/chatgpt-app/ask.js +87 -0
  249. package/clis/chatgpt-app/ax.js +603 -0
  250. package/clis/chatgpt-app/ax.test.js +95 -0
  251. package/clis/chatgpt-app/commands.test.js +45 -0
  252. package/clis/chatgpt-app/model.js +25 -0
  253. package/clis/chatgpt-app/new.js +61 -0
  254. package/clis/chatgpt-app/read.js +32 -0
  255. package/clis/chatgpt-app/send.js +37 -0
  256. package/clis/chatgpt-app/status.js +26 -0
  257. package/clis/chatwise/ask.js +54 -0
  258. package/clis/chatwise/composer.test.js +186 -0
  259. package/clis/chatwise/export.js +47 -0
  260. package/clis/chatwise/history.js +61 -0
  261. package/clis/chatwise/model.js +85 -0
  262. package/clis/chatwise/new.js +2 -0
  263. package/clis/chatwise/read.js +40 -0
  264. package/clis/chatwise/screenshot.js +2 -0
  265. package/clis/chatwise/send.js +28 -0
  266. package/clis/chatwise/status.js +2 -0
  267. package/clis/chatwise/utils.js +143 -0
  268. package/clis/chess/analyze.js +35 -0
  269. package/clis/chess/analyze.test.js +79 -0
  270. package/clis/chess/game.js +114 -0
  271. package/clis/chess/game.test.js +178 -0
  272. package/clis/chess/games.js +67 -0
  273. package/clis/chess/games.test.js +164 -0
  274. package/clis/chess/stats.js +32 -0
  275. package/clis/chess/stats.test.js +79 -0
  276. package/clis/chess/utils.js +170 -0
  277. package/clis/chess/utils.test.js +230 -0
  278. package/clis/claude/ask.js +144 -0
  279. package/clis/claude/ask.test.js +338 -0
  280. package/clis/claude/auth.js +55 -0
  281. package/clis/claude/commands.test.js +118 -0
  282. package/clis/claude/detail.js +38 -0
  283. package/clis/claude/history.js +33 -0
  284. package/clis/claude/new.js +29 -0
  285. package/clis/claude/read.js +27 -0
  286. package/clis/claude/send.js +48 -0
  287. package/clis/claude/status.js +26 -0
  288. package/clis/claude/utils.js +463 -0
  289. package/clis/claude/utils.test.js +148 -0
  290. package/clis/cnki/search.js +61 -0
  291. package/clis/cnki/search.test.js +18 -0
  292. package/clis/codex/_actions.js +270 -0
  293. package/clis/codex/archive.js +37 -0
  294. package/clis/codex/ask.js +78 -0
  295. package/clis/codex/dump.js +2 -0
  296. package/clis/codex/export.js +38 -0
  297. package/clis/codex/extract-diff.js +45 -0
  298. package/clis/codex/history.js +27 -0
  299. package/clis/codex/model.js +264 -0
  300. package/clis/codex/new.js +2 -0
  301. package/clis/codex/pin.js +30 -0
  302. package/clis/codex/projects.js +28 -0
  303. package/clis/codex/read.js +41 -0
  304. package/clis/codex/rename.js +86 -0
  305. package/clis/codex/screenshot.js +2 -0
  306. package/clis/codex/send.js +51 -0
  307. package/clis/codex/sidebar.js +356 -0
  308. package/clis/codex/sidebar.test.js +388 -0
  309. package/clis/codex/status.js +2 -0
  310. package/clis/coingecko/categories.js +75 -0
  311. package/clis/coingecko/coin.js +107 -0
  312. package/clis/coingecko/coingecko.test.js +109 -0
  313. package/clis/coingecko/derivatives.js +84 -0
  314. package/clis/coingecko/exchanges.js +74 -0
  315. package/clis/coingecko/global.js +71 -0
  316. package/clis/coingecko/top.js +64 -0
  317. package/clis/coingecko/trending.js +55 -0
  318. package/clis/confluence/commands.test.js +195 -0
  319. package/clis/confluence/create.js +39 -0
  320. package/clis/confluence/page.js +23 -0
  321. package/clis/confluence/search.js +34 -0
  322. package/clis/confluence/shared.js +173 -0
  323. package/clis/confluence/update.js +38 -0
  324. package/clis/coupang/add-to-cart.js +149 -0
  325. package/clis/coupang/auth.js +48 -0
  326. package/clis/coupang/coupang.test.js +159 -0
  327. package/clis/coupang/product.js +257 -0
  328. package/clis/coupang/search.js +474 -0
  329. package/clis/coupang/utils.js +316 -0
  330. package/clis/coupang/utils.test.js +62 -0
  331. package/clis/crates/crate.js +62 -0
  332. package/clis/crates/search.js +44 -0
  333. package/clis/crates/utils.js +72 -0
  334. package/clis/ctrip/auth.js +50 -0
  335. package/clis/ctrip/ctrip.test.js +719 -0
  336. package/clis/ctrip/flight.js +136 -0
  337. package/clis/ctrip/hotel-search.js +132 -0
  338. package/clis/ctrip/hotel-suggest.js +45 -0
  339. package/clis/ctrip/search.js +41 -0
  340. package/clis/ctrip/utils.js +473 -0
  341. package/clis/cursor/ask.js +74 -0
  342. package/clis/cursor/composer.js +43 -0
  343. package/clis/cursor/dump.js +2 -0
  344. package/clis/cursor/export.js +52 -0
  345. package/clis/cursor/extract-code.js +36 -0
  346. package/clis/cursor/history.js +44 -0
  347. package/clis/cursor/model.js +54 -0
  348. package/clis/cursor/new.js +2 -0
  349. package/clis/cursor/read.js +45 -0
  350. package/clis/cursor/screenshot.js +2 -0
  351. package/clis/cursor/send.js +41 -0
  352. package/clis/cursor/status.js +2 -0
  353. package/clis/dblp/author.js +133 -0
  354. package/clis/dblp/dblp.test.js +397 -0
  355. package/clis/dblp/paper.js +40 -0
  356. package/clis/dblp/search.js +45 -0
  357. package/clis/dblp/utils.js +290 -0
  358. package/clis/dblp/venue.js +64 -0
  359. package/clis/deepseek/ask.js +158 -0
  360. package/clis/deepseek/ask.test.js +399 -0
  361. package/clis/deepseek/auth.js +54 -0
  362. package/clis/deepseek/detail.js +46 -0
  363. package/clis/deepseek/detail.test.js +81 -0
  364. package/clis/deepseek/history.js +27 -0
  365. package/clis/deepseek/new.js +33 -0
  366. package/clis/deepseek/read.js +25 -0
  367. package/clis/deepseek/send.js +140 -0
  368. package/clis/deepseek/send.test.js +107 -0
  369. package/clis/deepseek/status.js +26 -0
  370. package/clis/deepseek/utils.js +487 -0
  371. package/clis/deepseek/utils.test.js +370 -0
  372. package/clis/defillama/defillama.test.js +99 -0
  373. package/clis/defillama/protocol.js +84 -0
  374. package/clis/defillama/protocols.js +55 -0
  375. package/clis/defillama/utils.js +99 -0
  376. package/clis/devto/devto.test.js +236 -0
  377. package/clis/devto/latest.js +74 -0
  378. package/clis/devto/read.js +103 -0
  379. package/clis/devto/tag.js +36 -0
  380. package/clis/devto/top.js +30 -0
  381. package/clis/devto/user.js +35 -0
  382. package/clis/dianping/__fixtures__/search.html +168 -0
  383. package/clis/dianping/__fixtures__/shop.html +6 -0
  384. package/clis/dianping/auth.js +49 -0
  385. package/clis/dianping/cityResolver.js +185 -0
  386. package/clis/dianping/dianping.test.js +578 -0
  387. package/clis/dianping/search.js +157 -0
  388. package/clis/dianping/shop.js +173 -0
  389. package/clis/dianping/utils.js +157 -0
  390. package/clis/dictionary/examples.js +28 -0
  391. package/clis/dictionary/search.js +30 -0
  392. package/clis/dictionary/synonyms.js +28 -0
  393. package/clis/discord-app/channels.js +26 -0
  394. package/clis/discord-app/commands.test.js +395 -0
  395. package/clis/discord-app/delete.js +115 -0
  396. package/clis/discord-app/goto.js +38 -0
  397. package/clis/discord-app/members.js +39 -0
  398. package/clis/discord-app/read.js +42 -0
  399. package/clis/discord-app/search.js +67 -0
  400. package/clis/discord-app/send.js +28 -0
  401. package/clis/discord-app/servers.js +26 -0
  402. package/clis/discord-app/status.js +17 -0
  403. package/clis/discord-app/thread-read.js +45 -0
  404. package/clis/discord-app/threads.js +37 -0
  405. package/clis/discord-app/utils.js +610 -0
  406. package/clis/dockerhub/image.js +52 -0
  407. package/clis/dockerhub/search.js +47 -0
  408. package/clis/dockerhub/utils.js +100 -0
  409. package/clis/dongchedi/__fixtures__/search.json +60 -0
  410. package/clis/dongchedi/__fixtures__/series-detail.json +911 -0
  411. package/clis/dongchedi/__fixtures__/series-score.json +1396 -0
  412. package/clis/dongchedi/dongchedi.test.js +191 -0
  413. package/clis/dongchedi/koubei.js +85 -0
  414. package/clis/dongchedi/models.js +91 -0
  415. package/clis/dongchedi/score.js +83 -0
  416. package/clis/dongchedi/search.js +83 -0
  417. package/clis/dongchedi/series.js +87 -0
  418. package/clis/dongchedi/specs.js +115 -0
  419. package/clis/dongchedi/utils.js +180 -0
  420. package/clis/douban/auth.js +50 -0
  421. package/clis/douban/book-hot.js +15 -0
  422. package/clis/douban/download.js +68 -0
  423. package/clis/douban/download.test.js +170 -0
  424. package/clis/douban/marks.js +116 -0
  425. package/clis/douban/movie-hot.js +15 -0
  426. package/clis/douban/movie-hot.test.js +14 -0
  427. package/clis/douban/photos.js +35 -0
  428. package/clis/douban/reviews.js +107 -0
  429. package/clis/douban/search.js +18 -0
  430. package/clis/douban/search.test.js +11 -0
  431. package/clis/douban/subject.js +46 -0
  432. package/clis/douban/subject.test.js +11 -0
  433. package/clis/douban/top250.js +68 -0
  434. package/clis/douban/utils.js +657 -0
  435. package/clis/douban/utils.test.js +488 -0
  436. package/clis/doubao/ask.js +40 -0
  437. package/clis/doubao/auth.js +66 -0
  438. package/clis/doubao/detail.js +35 -0
  439. package/clis/doubao/detail.test.js +42 -0
  440. package/clis/doubao/history.js +30 -0
  441. package/clis/doubao/history.test.js +37 -0
  442. package/clis/doubao/meeting-summary.js +41 -0
  443. package/clis/doubao/meeting-transcript.js +38 -0
  444. package/clis/doubao/new.js +22 -0
  445. package/clis/doubao/read.js +21 -0
  446. package/clis/doubao/send.js +24 -0
  447. package/clis/doubao/status.js +26 -0
  448. package/clis/doubao/utils.js +1156 -0
  449. package/clis/doubao/utils.test.js +396 -0
  450. package/clis/doubao-app/ask.js +54 -0
  451. package/clis/doubao-app/dump.js +25 -0
  452. package/clis/doubao-app/new.js +21 -0
  453. package/clis/doubao-app/read.js +19 -0
  454. package/clis/doubao-app/screenshot.js +19 -0
  455. package/clis/doubao-app/send.js +28 -0
  456. package/clis/doubao-app/status.js +17 -0
  457. package/clis/doubao-app/utils.js +110 -0
  458. package/clis/douyin/_shared/browser-fetch.js +66 -0
  459. package/clis/douyin/_shared/browser-fetch.test.js +64 -0
  460. package/clis/douyin/_shared/creation-id.js +5 -0
  461. package/clis/douyin/_shared/creation-id.test.js +22 -0
  462. package/clis/douyin/_shared/evaluate-result.js +16 -0
  463. package/clis/douyin/_shared/imagex-upload.js +53 -0
  464. package/clis/douyin/_shared/imagex-upload.test.js +87 -0
  465. package/clis/douyin/_shared/public-api.js +29 -0
  466. package/clis/douyin/_shared/sts2.js +21 -0
  467. package/clis/douyin/_shared/sts2.test.js +27 -0
  468. package/clis/douyin/_shared/text-extra.js +15 -0
  469. package/clis/douyin/_shared/text-extra.test.js +37 -0
  470. package/clis/douyin/_shared/timing.js +22 -0
  471. package/clis/douyin/_shared/timing.test.js +28 -0
  472. package/clis/douyin/_shared/tos-upload-short-read.test.js +83 -0
  473. package/clis/douyin/_shared/tos-upload.js +331 -0
  474. package/clis/douyin/_shared/tos-upload.test.js +229 -0
  475. package/clis/douyin/_shared/transcode.js +45 -0
  476. package/clis/douyin/_shared/transcode.test.js +91 -0
  477. package/clis/douyin/_shared/types.js +1 -0
  478. package/clis/douyin/_shared/vod-upload.js +212 -0
  479. package/clis/douyin/_shared/vod-upload.test.js +38 -0
  480. package/clis/douyin/activities.js +23 -0
  481. package/clis/douyin/activities.test.js +55 -0
  482. package/clis/douyin/auth.js +39 -0
  483. package/clis/douyin/collections.js +23 -0
  484. package/clis/douyin/collections.test.js +45 -0
  485. package/clis/douyin/delete.js +152 -0
  486. package/clis/douyin/delete.test.js +100 -0
  487. package/clis/douyin/draft.js +355 -0
  488. package/clis/douyin/draft.test.js +335 -0
  489. package/clis/douyin/drafts.js +24 -0
  490. package/clis/douyin/drafts.test.js +11 -0
  491. package/clis/douyin/hashtag.js +114 -0
  492. package/clis/douyin/hashtag.test.js +171 -0
  493. package/clis/douyin/location.js +25 -0
  494. package/clis/douyin/location.test.js +23 -0
  495. package/clis/douyin/profile.js +29 -0
  496. package/clis/douyin/profile.test.js +46 -0
  497. package/clis/douyin/publish-upload-id.test.js +206 -0
  498. package/clis/douyin/publish.js +341 -0
  499. package/clis/douyin/publish.test.js +38 -0
  500. package/clis/douyin/search.js +308 -0
  501. package/clis/douyin/search.test.js +307 -0
  502. package/clis/douyin/stats.js +28 -0
  503. package/clis/douyin/stats.test.js +22 -0
  504. package/clis/douyin/update.js +32 -0
  505. package/clis/douyin/update.test.js +11 -0
  506. package/clis/douyin/user-videos.js +82 -0
  507. package/clis/douyin/user-videos.test.js +151 -0
  508. package/clis/douyin/videos.js +52 -0
  509. package/clis/douyin/videos.test.js +54 -0
  510. package/clis/duckduckgo/search.js +131 -0
  511. package/clis/duckduckgo/search.test.js +128 -0
  512. package/clis/duckduckgo/suggest.js +45 -0
  513. package/clis/duckduckgo/suggest.test.js +66 -0
  514. package/clis/eastmoney/_secid.js +78 -0
  515. package/clis/eastmoney/announcement.js +53 -0
  516. package/clis/eastmoney/convertible.js +74 -0
  517. package/clis/eastmoney/etf.js +66 -0
  518. package/clis/eastmoney/holders.js +79 -0
  519. package/clis/eastmoney/hot-rank.js +51 -0
  520. package/clis/eastmoney/hot-rank.test.js +59 -0
  521. package/clis/eastmoney/index-board.js +97 -0
  522. package/clis/eastmoney/kline.js +88 -0
  523. package/clis/eastmoney/kuaixun.js +55 -0
  524. package/clis/eastmoney/longhu.js +68 -0
  525. package/clis/eastmoney/money-flow.js +79 -0
  526. package/clis/eastmoney/northbound.js +58 -0
  527. package/clis/eastmoney/quote.js +108 -0
  528. package/clis/eastmoney/rank.js +95 -0
  529. package/clis/eastmoney/sectors.js +77 -0
  530. package/clis/endoflife/endoflife.test.js +51 -0
  531. package/clis/endoflife/product.js +55 -0
  532. package/clis/endoflife/utils.js +89 -0
  533. package/clis/facebook/__fixtures__/notifications-page.html +13 -0
  534. package/clis/facebook/add-friend.js +44 -0
  535. package/clis/facebook/auth.js +43 -0
  536. package/clis/facebook/events.js +41 -0
  537. package/clis/facebook/feed.js +305 -0
  538. package/clis/facebook/feed.test.js +169 -0
  539. package/clis/facebook/friends.js +39 -0
  540. package/clis/facebook/groups.js +47 -0
  541. package/clis/facebook/join-group.js +45 -0
  542. package/clis/facebook/marketplace-inbox.js +84 -0
  543. package/clis/facebook/marketplace-listings.js +84 -0
  544. package/clis/facebook/marketplace.test.js +91 -0
  545. package/clis/facebook/memories.js +36 -0
  546. package/clis/facebook/notifications.js +333 -0
  547. package/clis/facebook/notifications.test.js +458 -0
  548. package/clis/facebook/profile.js +38 -0
  549. package/clis/facebook/search.js +39 -0
  550. package/clis/facebook/search.test.js +55 -0
  551. package/clis/flathub/app.js +71 -0
  552. package/clis/flathub/flathub.test.js +90 -0
  553. package/clis/flathub/search.js +80 -0
  554. package/clis/flathub/utils.js +114 -0
  555. package/clis/flomo/auth.js +55 -0
  556. package/clis/flomo/memos.js +228 -0
  557. package/clis/flomo/memos.test.js +144 -0
  558. package/clis/gemini/ask.js +51 -0
  559. package/clis/gemini/ask.test.js +100 -0
  560. package/clis/gemini/auth.js +48 -0
  561. package/clis/gemini/commands.test.js +212 -0
  562. package/clis/gemini/deep-research-result.js +96 -0
  563. package/clis/gemini/deep-research-result.test.js +155 -0
  564. package/clis/gemini/deep-research.js +103 -0
  565. package/clis/gemini/deep-research.test.js +181 -0
  566. package/clis/gemini/detail.js +82 -0
  567. package/clis/gemini/history.js +70 -0
  568. package/clis/gemini/image.js +111 -0
  569. package/clis/gemini/new.js +22 -0
  570. package/clis/gemini/read.js +36 -0
  571. package/clis/gemini/reply-state.test.js +671 -0
  572. package/clis/gemini/status.js +32 -0
  573. package/clis/gemini/utils.js +2056 -0
  574. package/clis/gemini/utils.test.js +392 -0
  575. package/clis/geogebra/add-circle.js +46 -0
  576. package/clis/geogebra/add-line.js +35 -0
  577. package/clis/geogebra/add-point.js +27 -0
  578. package/clis/geogebra/add-polygon.js +25 -0
  579. package/clis/geogebra/eval.js +35 -0
  580. package/clis/geogebra/geogebra.test.js +175 -0
  581. package/clis/geogebra/hexagon.js +62 -0
  582. package/clis/geogebra/info.js +72 -0
  583. package/clis/geogebra/list.js +35 -0
  584. package/clis/geogebra/triangle.js +60 -0
  585. package/clis/geogebra/utils.js +271 -0
  586. package/clis/gitee/auth.js +41 -0
  587. package/clis/gitee/index.js +3 -0
  588. package/clis/gitee/search.js +137 -0
  589. package/clis/gitee/search.test.js +65 -0
  590. package/clis/gitee/trending.js +568 -0
  591. package/clis/gitee/user.js +200 -0
  592. package/clis/gitee/user.test.js +63 -0
  593. package/clis/github/auth.js +44 -0
  594. package/clis/github-trending/repos.js +168 -0
  595. package/clis/github-trending/repos.test.js +149 -0
  596. package/clis/google/news.js +59 -0
  597. package/clis/google/search.js +138 -0
  598. package/clis/google/suggest.js +35 -0
  599. package/clis/google/trends.js +39 -0
  600. package/clis/google/utils.js +23 -0
  601. package/clis/google/utils.test.js +75 -0
  602. package/clis/google-scholar/cite.js +74 -0
  603. package/clis/google-scholar/cite.test.js +47 -0
  604. package/clis/google-scholar/profile.js +92 -0
  605. package/clis/google-scholar/profile.test.js +49 -0
  606. package/clis/google-scholar/search.js +73 -0
  607. package/clis/google-scholar/search.test.js +71 -0
  608. package/clis/goproxy/goproxy.test.js +103 -0
  609. package/clis/goproxy/module.js +47 -0
  610. package/clis/goproxy/utils.js +165 -0
  611. package/clis/goproxy/versions.js +59 -0
  612. package/clis/gov-law/commands.test.js +39 -0
  613. package/clis/gov-law/recent.js +22 -0
  614. package/clis/gov-law/search.js +41 -0
  615. package/clis/gov-law/shared.js +51 -0
  616. package/clis/gov-policy/__fixtures__/recent.html +16 -0
  617. package/clis/gov-policy/__fixtures__/search.html +41 -0
  618. package/clis/gov-policy/gov-policy.test.js +224 -0
  619. package/clis/gov-policy/recent.js +90 -0
  620. package/clis/gov-policy/search.js +91 -0
  621. package/clis/gov-policy/utils.js +54 -0
  622. package/clis/grok/ask.js +77 -0
  623. package/clis/grok/ask.test.js +29 -0
  624. package/clis/grok/auth.js +52 -0
  625. package/clis/grok/delete.js +54 -0
  626. package/clis/grok/detail.js +60 -0
  627. package/clis/grok/export-all.js +409 -0
  628. package/clis/grok/export-utils.js +94 -0
  629. package/clis/grok/export.js +189 -0
  630. package/clis/grok/export.test.js +210 -0
  631. package/clis/grok/history.js +48 -0
  632. package/clis/grok/image.js +343 -0
  633. package/clis/grok/image.test.ts +127 -0
  634. package/clis/grok/new.js +20 -0
  635. package/clis/grok/pin.js +68 -0
  636. package/clis/grok/read.js +39 -0
  637. package/clis/grok/send.js +50 -0
  638. package/clis/grok/status.js +41 -0
  639. package/clis/grok/utils.js +640 -0
  640. package/clis/grok/utils.test.js +141 -0
  641. package/clis/guazi/__fixtures__/detail.html +7 -0
  642. package/clis/guazi/__fixtures__/list.html +5 -0
  643. package/clis/guazi/browse.js +95 -0
  644. package/clis/guazi/car.js +110 -0
  645. package/clis/guazi/guazi.test.js +126 -0
  646. package/clis/guazi/utils.js +133 -0
  647. package/clis/hackernews/ask.js +31 -0
  648. package/clis/hackernews/best.js +31 -0
  649. package/clis/hackernews/hackernews.test.js +132 -0
  650. package/clis/hackernews/jobs.js +29 -0
  651. package/clis/hackernews/new.js +31 -0
  652. package/clis/hackernews/read.js +188 -0
  653. package/clis/hackernews/search.js +38 -0
  654. package/clis/hackernews/show.js +31 -0
  655. package/clis/hackernews/top.js +31 -0
  656. package/clis/hackernews/user.js +23 -0
  657. package/clis/hf/auth.js +41 -0
  658. package/clis/hf/datasets.js +88 -0
  659. package/clis/hf/hf.test.js +16 -0
  660. package/clis/hf/models.js +91 -0
  661. package/clis/hf/paper.js +79 -0
  662. package/clis/hf/spaces.js +101 -0
  663. package/clis/hf/top.js +121 -0
  664. package/clis/homebrew/cask.js +39 -0
  665. package/clis/homebrew/formula.js +41 -0
  666. package/clis/homebrew/popular.js +54 -0
  667. package/clis/homebrew/utils.js +100 -0
  668. package/clis/huodongxing/events.js +322 -0
  669. package/clis/huodongxing/events.test.js +296 -0
  670. package/clis/hupu/__fixtures__/hot-home.html +64 -0
  671. package/clis/hupu/auth.js +47 -0
  672. package/clis/hupu/detail.js +72 -0
  673. package/clis/hupu/hot.js +163 -0
  674. package/clis/hupu/hot.test.js +224 -0
  675. package/clis/hupu/like.js +76 -0
  676. package/clis/hupu/mentions.js +160 -0
  677. package/clis/hupu/reply.js +72 -0
  678. package/clis/hupu/search.js +60 -0
  679. package/clis/hupu/unlike.js +76 -0
  680. package/clis/hupu/utils.js +319 -0
  681. package/clis/imdb/person.js +204 -0
  682. package/clis/imdb/reviews.js +89 -0
  683. package/clis/imdb/search.js +162 -0
  684. package/clis/imdb/title.js +94 -0
  685. package/clis/imdb/top.js +54 -0
  686. package/clis/imdb/trending.js +53 -0
  687. package/clis/imdb/utils.js +285 -0
  688. package/clis/imdb/utils.test.js +88 -0
  689. package/clis/indeed/indeed.test.js +375 -0
  690. package/clis/indeed/job.js +86 -0
  691. package/clis/indeed/search.js +110 -0
  692. package/clis/indeed/utils.js +152 -0
  693. package/clis/instagram/_shared/private-publish.js +1030 -0
  694. package/clis/instagram/_shared/private-publish.test.js +705 -0
  695. package/clis/instagram/_shared/protocol-capture.js +282 -0
  696. package/clis/instagram/_shared/protocol-capture.test.js +114 -0
  697. package/clis/instagram/_shared/runtime-info.js +81 -0
  698. package/clis/instagram/auth.js +57 -0
  699. package/clis/instagram/collection-create.js +58 -0
  700. package/clis/instagram/collection-delete.js +92 -0
  701. package/clis/instagram/comment.js +48 -0
  702. package/clis/instagram/download.js +226 -0
  703. package/clis/instagram/download.test.js +118 -0
  704. package/clis/instagram/explore.js +42 -0
  705. package/clis/instagram/follow.js +44 -0
  706. package/clis/instagram/followers.js +46 -0
  707. package/clis/instagram/following.js +90 -0
  708. package/clis/instagram/instagram.test.js +381 -0
  709. package/clis/instagram/like.js +46 -0
  710. package/clis/instagram/note.js +223 -0
  711. package/clis/instagram/note.test.js +53 -0
  712. package/clis/instagram/post.js +1497 -0
  713. package/clis/instagram/post.test.js +1647 -0
  714. package/clis/instagram/profile.js +40 -0
  715. package/clis/instagram/reel.js +827 -0
  716. package/clis/instagram/reel.test.js +167 -0
  717. package/clis/instagram/save.js +46 -0
  718. package/clis/instagram/saved.js +53 -0
  719. package/clis/instagram/search.js +39 -0
  720. package/clis/instagram/story.js +116 -0
  721. package/clis/instagram/story.test.js +134 -0
  722. package/clis/instagram/unfollow.js +41 -0
  723. package/clis/instagram/unlike.js +46 -0
  724. package/clis/instagram/unsave.js +46 -0
  725. package/clis/instagram/user.js +49 -0
  726. package/clis/jd/add-cart.js +72 -0
  727. package/clis/jd/auth.js +46 -0
  728. package/clis/jd/cart.js +80 -0
  729. package/clis/jd/commands.test.js +41 -0
  730. package/clis/jd/detail.js +63 -0
  731. package/clis/jd/item.js +730 -0
  732. package/clis/jd/item.test.js +354 -0
  733. package/clis/jd/item.test.ts +517 -0
  734. package/clis/jd/reviews.js +55 -0
  735. package/clis/jd/search.js +66 -0
  736. package/clis/jianyu/auth.js +62 -0
  737. package/clis/jianyu/detail.js +21 -0
  738. package/clis/jianyu/search.js +634 -0
  739. package/clis/jianyu/search.test.js +153 -0
  740. package/clis/jianyu/shared/china-bid-search.js +165 -0
  741. package/clis/jianyu/shared/procurement-contract.js +324 -0
  742. package/clis/jianyu/shared/procurement-contract.test.js +72 -0
  743. package/clis/jianyu/shared/procurement-detail.js +107 -0
  744. package/clis/jianyu/shared/procurement-detail.test.js +84 -0
  745. package/clis/jike/auth.js +48 -0
  746. package/clis/jike/comment.js +107 -0
  747. package/clis/jike/create.js +106 -0
  748. package/clis/jike/feed.js +68 -0
  749. package/clis/jike/like.js +61 -0
  750. package/clis/jike/notifications.js +169 -0
  751. package/clis/jike/post.js +72 -0
  752. package/clis/jike/read.test.js +86 -0
  753. package/clis/jike/repost.js +103 -0
  754. package/clis/jike/search.js +68 -0
  755. package/clis/jike/topic.js +65 -0
  756. package/clis/jike/user.js +65 -0
  757. package/clis/jike/utils.js +25 -0
  758. package/clis/jimeng/auth.js +46 -0
  759. package/clis/jimeng/generate.js +84 -0
  760. package/clis/jimeng/history.js +48 -0
  761. package/clis/jimeng/new.js +44 -0
  762. package/clis/jimeng/workspaces.js +42 -0
  763. package/clis/jira/attachments.js +28 -0
  764. package/clis/jira/commands.test.js +287 -0
  765. package/clis/jira/comments.js +28 -0
  766. package/clis/jira/issue.js +28 -0
  767. package/clis/jira/links.js +28 -0
  768. package/clis/jira/search.js +47 -0
  769. package/clis/jira/shared.js +256 -0
  770. package/clis/juejin/hot.js +37 -0
  771. package/clis/juejin/juejin.test.js +241 -0
  772. package/clis/juejin/recommend.js +61 -0
  773. package/clis/juejin/utils.js +199 -0
  774. package/clis/ke/auth.js +46 -0
  775. package/clis/ke/chengjiao.js +78 -0
  776. package/clis/ke/ershoufang.js +101 -0
  777. package/clis/ke/utils.js +104 -0
  778. package/clis/ke/xiaoqu.js +78 -0
  779. package/clis/ke/zufang.js +95 -0
  780. package/clis/kimi/_utils.js +110 -0
  781. package/clis/kimi/audit-extras.js +268 -0
  782. package/clis/kimi/auth.js +57 -0
  783. package/clis/kimi/chat.js +470 -0
  784. package/clis/kimi/kimi.test.js +187 -0
  785. package/clis/kimi/storage.js +169 -0
  786. package/clis/kimi/ui.js +314 -0
  787. package/clis/kimi/usage.js +144 -0
  788. package/clis/lesswrong/_helpers.js +75 -0
  789. package/clis/lesswrong/comments.js +65 -0
  790. package/clis/lesswrong/curated.js +31 -0
  791. package/clis/lesswrong/frontpage.js +31 -0
  792. package/clis/lesswrong/frontpage.test.js +37 -0
  793. package/clis/lesswrong/new.js +31 -0
  794. package/clis/lesswrong/read.js +46 -0
  795. package/clis/lesswrong/sequences.js +28 -0
  796. package/clis/lesswrong/shortform.js +31 -0
  797. package/clis/lesswrong/tag.js +46 -0
  798. package/clis/lesswrong/tags.js +28 -0
  799. package/clis/lesswrong/top-month.js +31 -0
  800. package/clis/lesswrong/top-week.js +31 -0
  801. package/clis/lesswrong/top-year.js +31 -0
  802. package/clis/lesswrong/top.js +31 -0
  803. package/clis/lesswrong/user-posts.js +42 -0
  804. package/clis/lesswrong/user.js +45 -0
  805. package/clis/lichess/lichess.test.js +85 -0
  806. package/clis/lichess/top.js +46 -0
  807. package/clis/lichess/user.js +91 -0
  808. package/clis/lichess/utils.js +97 -0
  809. package/clis/linkedin/auth.js +62 -0
  810. package/clis/linkedin/connect.js +524 -0
  811. package/clis/linkedin/connect.test.js +328 -0
  812. package/clis/linkedin/inbox.js +234 -0
  813. package/clis/linkedin/inbox.test.js +152 -0
  814. package/clis/linkedin/job-detail.js +167 -0
  815. package/clis/linkedin/job-detail.test.js +38 -0
  816. package/clis/linkedin/jobs-preferences.js +113 -0
  817. package/clis/linkedin/jobs-preferences.test.js +43 -0
  818. package/clis/linkedin/people-search.js +262 -0
  819. package/clis/linkedin/people-search.test.js +216 -0
  820. package/clis/linkedin/post-analytics.js +74 -0
  821. package/clis/linkedin/post-analytics.test.js +40 -0
  822. package/clis/linkedin/posts-core.js +241 -0
  823. package/clis/linkedin/posts.js +22 -0
  824. package/clis/linkedin/posts.test.js +40 -0
  825. package/clis/linkedin/profile-analytics.js +104 -0
  826. package/clis/linkedin/profile-analytics.test.js +67 -0
  827. package/clis/linkedin/profile-experience.js +671 -0
  828. package/clis/linkedin/profile-experience.test.js +152 -0
  829. package/clis/linkedin/profile-projects.js +311 -0
  830. package/clis/linkedin/profile-projects.test.js +111 -0
  831. package/clis/linkedin/profile-read.js +148 -0
  832. package/clis/linkedin/profile-read.test.js +77 -0
  833. package/clis/linkedin/safe-send.js +357 -0
  834. package/clis/linkedin/safe-send.test.js +204 -0
  835. package/clis/linkedin/salesnav-inbox.js +210 -0
  836. package/clis/linkedin/salesnav-inbox.test.js +113 -0
  837. package/clis/linkedin/salesnav-message.js +360 -0
  838. package/clis/linkedin/salesnav-message.test.js +172 -0
  839. package/clis/linkedin/salesnav-search.js +186 -0
  840. package/clis/linkedin/salesnav-search.test.js +76 -0
  841. package/clis/linkedin/salesnav-thread.js +212 -0
  842. package/clis/linkedin/salesnav-thread.test.js +79 -0
  843. package/clis/linkedin/search.js +462 -0
  844. package/clis/linkedin/search.test.js +222 -0
  845. package/clis/linkedin/sent-invitations.js +92 -0
  846. package/clis/linkedin/sent-invitations.test.js +62 -0
  847. package/clis/linkedin/services-read.js +213 -0
  848. package/clis/linkedin/services-read.test.js +105 -0
  849. package/clis/linkedin/shared.js +124 -0
  850. package/clis/linkedin/thread-snapshot.js +214 -0
  851. package/clis/linkedin/thread-snapshot.test.js +89 -0
  852. package/clis/linkedin/timeline.js +511 -0
  853. package/clis/linkedin/timeline.test.js +81 -0
  854. package/clis/linkedin-learning/auth.js +62 -0
  855. package/clis/linkedin-learning/course.js +138 -0
  856. package/clis/linkedin-learning/course.test.js +114 -0
  857. package/clis/linkedin-learning/search.js +155 -0
  858. package/clis/linkedin-learning/search.test.js +144 -0
  859. package/clis/linkedin-learning/trending.js +133 -0
  860. package/clis/linkedin-learning/trending.test.js +123 -0
  861. package/clis/linux-do/auth.js +55 -0
  862. package/clis/linux-do/categories.js +50 -0
  863. package/clis/linux-do/feed.js +395 -0
  864. package/clis/linux-do/feed.test.js +153 -0
  865. package/clis/linux-do/search.js +29 -0
  866. package/clis/linux-do/tags.js +28 -0
  867. package/clis/linux-do/topic-content.js +155 -0
  868. package/clis/linux-do/topic-content.test.js +59 -0
  869. package/clis/linux-do/topic.js +53 -0
  870. package/clis/linux-do/user-posts.js +57 -0
  871. package/clis/linux-do/user-topics.js +37 -0
  872. package/clis/lobsters/active.js +29 -0
  873. package/clis/lobsters/domain.js +92 -0
  874. package/clis/lobsters/hot.js +29 -0
  875. package/clis/lobsters/lobsters.test.js +169 -0
  876. package/clis/lobsters/newest.js +29 -0
  877. package/clis/lobsters/read.js +196 -0
  878. package/clis/lobsters/tag.js +35 -0
  879. package/clis/maimai/auth.js +47 -0
  880. package/clis/maimai/search-talents.js +192 -0
  881. package/clis/maimai/search-talents.test.js +29 -0
  882. package/clis/manus/_utils.js +133 -0
  883. package/clis/manus/auth.js +54 -0
  884. package/clis/manus/connectors.js +40 -0
  885. package/clis/manus/credits.js +39 -0
  886. package/clis/manus/list.js +66 -0
  887. package/clis/manus/manus.test.js +382 -0
  888. package/clis/manus/read.js +63 -0
  889. package/clis/manus/skills.js +61 -0
  890. package/clis/manus/status.js +43 -0
  891. package/clis/maven/artifact.js +49 -0
  892. package/clis/maven/search.js +51 -0
  893. package/clis/maven/utils.js +110 -0
  894. package/clis/mdn/search.js +97 -0
  895. package/clis/medium/feed.js +16 -0
  896. package/clis/medium/search.js +16 -0
  897. package/clis/medium/tag.js +135 -0
  898. package/clis/medium/user.js +16 -0
  899. package/clis/medium/utils.js +79 -0
  900. package/clis/mubu/doc.js +41 -0
  901. package/clis/mubu/docs.js +44 -0
  902. package/clis/mubu/notes.js +245 -0
  903. package/clis/mubu/recent.js +28 -0
  904. package/clis/mubu/search.js +63 -0
  905. package/clis/mubu/utils.js +304 -0
  906. package/clis/notebooklm/add-source.js +269 -0
  907. package/clis/notebooklm/add-source.test.js +97 -0
  908. package/clis/notebooklm/auth.js +55 -0
  909. package/clis/notebooklm/compat.test.js +16 -0
  910. package/clis/notebooklm/create.js +76 -0
  911. package/clis/notebooklm/create.test.js +58 -0
  912. package/clis/notebooklm/current.js +28 -0
  913. package/clis/notebooklm/generate-audio.js +91 -0
  914. package/clis/notebooklm/generate-audio.test.js +63 -0
  915. package/clis/notebooklm/generate-slides.js +106 -0
  916. package/clis/notebooklm/generate-slides.test.js +75 -0
  917. package/clis/notebooklm/get.js +37 -0
  918. package/clis/notebooklm/history.js +25 -0
  919. package/clis/notebooklm/history.test.js +58 -0
  920. package/clis/notebooklm/list.js +36 -0
  921. package/clis/notebooklm/note-list.js +28 -0
  922. package/clis/notebooklm/note-list.test.js +56 -0
  923. package/clis/notebooklm/notes-get.js +47 -0
  924. package/clis/notebooklm/notes-get.test.js +72 -0
  925. package/clis/notebooklm/open.js +42 -0
  926. package/clis/notebooklm/open.test.js +63 -0
  927. package/clis/notebooklm/rpc.js +203 -0
  928. package/clis/notebooklm/rpc.test.js +131 -0
  929. package/clis/notebooklm/shared.js +3 -0
  930. package/clis/notebooklm/source-fulltext.js +44 -0
  931. package/clis/notebooklm/source-fulltext.test.js +106 -0
  932. package/clis/notebooklm/source-get.js +40 -0
  933. package/clis/notebooklm/source-get.test.js +84 -0
  934. package/clis/notebooklm/source-guide.js +44 -0
  935. package/clis/notebooklm/source-guide.test.js +104 -0
  936. package/clis/notebooklm/source-list.js +30 -0
  937. package/clis/notebooklm/status.js +31 -0
  938. package/clis/notebooklm/summary.js +30 -0
  939. package/clis/notebooklm/summary.test.js +78 -0
  940. package/clis/notebooklm/utils.js +814 -0
  941. package/clis/notebooklm/utils.test.js +449 -0
  942. package/clis/notebooklm/write-note.js +103 -0
  943. package/clis/notebooklm/write-note.test.js +70 -0
  944. package/clis/nowcoder/auth.js +65 -0
  945. package/clis/nowcoder/companies.js +24 -0
  946. package/clis/nowcoder/creators.js +28 -0
  947. package/clis/nowcoder/detail.js +62 -0
  948. package/clis/nowcoder/experience.js +37 -0
  949. package/clis/nowcoder/hot.js +25 -0
  950. package/clis/nowcoder/jobs.js +22 -0
  951. package/clis/nowcoder/notifications.js +30 -0
  952. package/clis/nowcoder/papers.js +41 -0
  953. package/clis/nowcoder/practice.js +38 -0
  954. package/clis/nowcoder/recommend.js +31 -0
  955. package/clis/nowcoder/referral.js +40 -0
  956. package/clis/nowcoder/salary.js +41 -0
  957. package/clis/nowcoder/search.js +50 -0
  958. package/clis/nowcoder/suggest.js +34 -0
  959. package/clis/nowcoder/topics.js +28 -0
  960. package/clis/nowcoder/trending.js +26 -0
  961. package/clis/npm/downloads.js +59 -0
  962. package/clis/npm/package.js +70 -0
  963. package/clis/npm/search.js +49 -0
  964. package/clis/npm/utils.js +76 -0
  965. package/clis/nuget/nuget.test.js +111 -0
  966. package/clis/nuget/package.js +101 -0
  967. package/clis/nuget/search.js +69 -0
  968. package/clis/nuget/utils.js +87 -0
  969. package/clis/nvd/cve.js +121 -0
  970. package/clis/oeis/oeis.test.js +88 -0
  971. package/clis/oeis/search.js +63 -0
  972. package/clis/oeis/sequence.js +71 -0
  973. package/clis/oeis/utils.js +88 -0
  974. package/clis/ones/common.js +144 -0
  975. package/clis/ones/enrich-tasks.js +37 -0
  976. package/clis/ones/login.js +81 -0
  977. package/clis/ones/logout.js +18 -0
  978. package/clis/ones/me.js +31 -0
  979. package/clis/ones/my-tasks.js +121 -0
  980. package/clis/ones/resolve-labels.js +64 -0
  981. package/clis/ones/task-helpers.js +212 -0
  982. package/clis/ones/task-helpers.test.js +12 -0
  983. package/clis/ones/task.js +67 -0
  984. package/clis/ones/tasks.js +80 -0
  985. package/clis/ones/token-info.js +43 -0
  986. package/clis/ones/worklog.js +268 -0
  987. package/clis/ones/worklog.test.js +20 -0
  988. package/clis/openalex/search.js +69 -0
  989. package/clis/openalex/utils.js +160 -0
  990. package/clis/openalex/work.js +65 -0
  991. package/clis/openfda/drug-label.js +74 -0
  992. package/clis/openfda/food-recall.js +65 -0
  993. package/clis/openfda/openfda.test.js +114 -0
  994. package/clis/openfda/utils.js +67 -0
  995. package/clis/openreview/author.js +58 -0
  996. package/clis/openreview/openreview.test.js +427 -0
  997. package/clis/openreview/paper.js +43 -0
  998. package/clis/openreview/reviews.js +131 -0
  999. package/clis/openreview/search.js +46 -0
  1000. package/clis/openreview/utils.js +172 -0
  1001. package/clis/openreview/venue.js +63 -0
  1002. package/clis/osv/osv.test.js +97 -0
  1003. package/clis/osv/query.js +72 -0
  1004. package/clis/osv/utils.js +169 -0
  1005. package/clis/osv/vulnerability.js +54 -0
  1006. package/clis/packagist/package.js +49 -0
  1007. package/clis/packagist/search.js +43 -0
  1008. package/clis/packagist/utils.js +113 -0
  1009. package/clis/paperreview/commands.test.js +243 -0
  1010. package/clis/paperreview/feedback.js +53 -0
  1011. package/clis/paperreview/review.js +38 -0
  1012. package/clis/paperreview/submit.js +86 -0
  1013. package/clis/paperreview/utils.js +197 -0
  1014. package/clis/paperreview/utils.test.js +49 -0
  1015. package/clis/pixiv/auth.js +64 -0
  1016. package/clis/pixiv/detail.js +66 -0
  1017. package/clis/pixiv/detail.test.js +93 -0
  1018. package/clis/pixiv/download.js +82 -0
  1019. package/clis/pixiv/download.test.js +97 -0
  1020. package/clis/pixiv/illusts.js +66 -0
  1021. package/clis/pixiv/illusts.test.js +99 -0
  1022. package/clis/pixiv/ranking.js +60 -0
  1023. package/clis/pixiv/search.js +44 -0
  1024. package/clis/pixiv/search.test.js +83 -0
  1025. package/clis/pixiv/user.js +59 -0
  1026. package/clis/pixiv/user.test.js +100 -0
  1027. package/clis/pixiv/utils.js +98 -0
  1028. package/clis/powerchina/auth.js +52 -0
  1029. package/clis/powerchina/search.js +251 -0
  1030. package/clis/powerchina/search.test.js +93 -0
  1031. package/clis/producthunt/browse.js +100 -0
  1032. package/clis/producthunt/hot.js +111 -0
  1033. package/clis/producthunt/posts.js +29 -0
  1034. package/clis/producthunt/today.js +36 -0
  1035. package/clis/producthunt/utils.js +99 -0
  1036. package/clis/producthunt/utils.test.js +64 -0
  1037. package/clis/pubmed/article.js +55 -0
  1038. package/clis/pubmed/author.js +64 -0
  1039. package/clis/pubmed/citations.js +36 -0
  1040. package/clis/pubmed/clinical-trial.js +59 -0
  1041. package/clis/pubmed/journal.js +59 -0
  1042. package/clis/pubmed/mesh.js +47 -0
  1043. package/clis/pubmed/pubmed.test.js +464 -0
  1044. package/clis/pubmed/related.js +45 -0
  1045. package/clis/pubmed/review.js +58 -0
  1046. package/clis/pubmed/search.js +75 -0
  1047. package/clis/pubmed/utils.js +325 -0
  1048. package/clis/pypi/downloads.js +66 -0
  1049. package/clis/pypi/package.js +79 -0
  1050. package/clis/pypi/utils.js +55 -0
  1051. package/clis/qoder/_utils.js +191 -0
  1052. package/clis/qoder/composer.js +45 -0
  1053. package/clis/qoder/history.js +49 -0
  1054. package/clis/qoder/qoder.test.js +125 -0
  1055. package/clis/qoder/quest.js +147 -0
  1056. package/clis/qoder/read.js +29 -0
  1057. package/clis/qoder/status.js +21 -0
  1058. package/clis/qoder/ui.js +332 -0
  1059. package/clis/quark/auth.js +47 -0
  1060. package/clis/quark/ls.js +64 -0
  1061. package/clis/quark/mkdir.js +37 -0
  1062. package/clis/quark/mv.js +54 -0
  1063. package/clis/quark/rename.js +27 -0
  1064. package/clis/quark/rm.js +25 -0
  1065. package/clis/quark/save.js +81 -0
  1066. package/clis/quark/share-tree.js +46 -0
  1067. package/clis/quark/utils.js +146 -0
  1068. package/clis/quark/utils.test.js +58 -0
  1069. package/clis/qwen/ask.js +104 -0
  1070. package/clis/qwen/ask.test.js +36 -0
  1071. package/clis/qwen/auth.js +56 -0
  1072. package/clis/qwen/detail.js +62 -0
  1073. package/clis/qwen/history.js +61 -0
  1074. package/clis/qwen/image.js +179 -0
  1075. package/clis/qwen/new.js +23 -0
  1076. package/clis/qwen/read.js +41 -0
  1077. package/clis/qwen/send.js +55 -0
  1078. package/clis/qwen/status.js +37 -0
  1079. package/clis/qwen/utils.js +461 -0
  1080. package/clis/qwen/utils.test.js +128 -0
  1081. package/clis/reddit/auth.js +52 -0
  1082. package/clis/reddit/comment.js +58 -0
  1083. package/clis/reddit/extract-media.test.js +149 -0
  1084. package/clis/reddit/frontpage.js +70 -0
  1085. package/clis/reddit/frontpage.test.js +34 -0
  1086. package/clis/reddit/home.js +146 -0
  1087. package/clis/reddit/home.test.js +170 -0
  1088. package/clis/reddit/hot.js +81 -0
  1089. package/clis/reddit/hot.test.js +32 -0
  1090. package/clis/reddit/popular.js +79 -0
  1091. package/clis/reddit/popular.test.js +26 -0
  1092. package/clis/reddit/read.js +588 -0
  1093. package/clis/reddit/read.test.js +402 -0
  1094. package/clis/reddit/reply.js +182 -0
  1095. package/clis/reddit/reply.test.js +89 -0
  1096. package/clis/reddit/save.js +52 -0
  1097. package/clis/reddit/saved.js +50 -0
  1098. package/clis/reddit/search.js +102 -0
  1099. package/clis/reddit/search.test.js +26 -0
  1100. package/clis/reddit/subreddit-info.js +116 -0
  1101. package/clis/reddit/subreddit-info.test.js +163 -0
  1102. package/clis/reddit/subreddit.js +97 -0
  1103. package/clis/reddit/subreddit.test.js +31 -0
  1104. package/clis/reddit/subscribe.js +51 -0
  1105. package/clis/reddit/subscribed.js +165 -0
  1106. package/clis/reddit/subscribed.test.js +168 -0
  1107. package/clis/reddit/upvote.js +65 -0
  1108. package/clis/reddit/upvoted.js +50 -0
  1109. package/clis/reddit/user-comments.js +45 -0
  1110. package/clis/reddit/user-posts.js +43 -0
  1111. package/clis/reddit/user.js +38 -0
  1112. package/clis/reddit/whoami.js +83 -0
  1113. package/clis/reddit/whoami.test.js +105 -0
  1114. package/clis/rednote/auth.js +52 -0
  1115. package/clis/rednote/comments.js +85 -0
  1116. package/clis/rednote/download.js +59 -0
  1117. package/clis/rednote/feed.js +27 -0
  1118. package/clis/rednote/navigation.test.js +26 -0
  1119. package/clis/rednote/note.js +68 -0
  1120. package/clis/rednote/notifications.js +139 -0
  1121. package/clis/rednote/rednote.test.js +240 -0
  1122. package/clis/rednote/search.js +107 -0
  1123. package/clis/rednote/user.js +55 -0
  1124. package/clis/rest-countries/country.js +65 -0
  1125. package/clis/rest-countries/region.js +64 -0
  1126. package/clis/rest-countries/rest-countries.test.js +83 -0
  1127. package/clis/rest-countries/utils.js +126 -0
  1128. package/clis/reuters/article-detail.js +53 -0
  1129. package/clis/reuters/auth.js +67 -0
  1130. package/clis/reuters/reuters.test.js +299 -0
  1131. package/clis/reuters/search.js +63 -0
  1132. package/clis/reuters/utils.js +159 -0
  1133. package/clis/rfc/rfc.js +52 -0
  1134. package/clis/rfc/rfc.test.js +74 -0
  1135. package/clis/rfc/utils.js +72 -0
  1136. package/clis/rubygems/gem.js +42 -0
  1137. package/clis/rubygems/search.js +47 -0
  1138. package/clis/rubygems/utils.js +86 -0
  1139. package/clis/semanticscholar/citations.js +61 -0
  1140. package/clis/semanticscholar/paper.js +52 -0
  1141. package/clis/semanticscholar/recommendations.js +47 -0
  1142. package/clis/semanticscholar/search.js +48 -0
  1143. package/clis/semanticscholar/semanticscholar.test.js +279 -0
  1144. package/clis/semanticscholar/utils.js +196 -0
  1145. package/clis/sinablog/article.js +15 -0
  1146. package/clis/sinablog/hot.js +15 -0
  1147. package/clis/sinablog/search.js +52 -0
  1148. package/clis/sinablog/user.js +16 -0
  1149. package/clis/sinablog/utils.js +186 -0
  1150. package/clis/sinafinance/news.js +62 -0
  1151. package/clis/sinafinance/rolling-news.js +41 -0
  1152. package/clis/sinafinance/stock-rank.js +66 -0
  1153. package/clis/sinafinance/stock.js +121 -0
  1154. package/clis/sinafinance/stock.test.js +59 -0
  1155. package/clis/slock/api-base-canary.test.js +62 -0
  1156. package/clis/slock/attachment-download.js +72 -0
  1157. package/clis/slock/attachment-download.test.js +102 -0
  1158. package/clis/slock/attachment-upload.js +112 -0
  1159. package/clis/slock/attachment-upload.test.js +131 -0
  1160. package/clis/slock/attachment-url.js +53 -0
  1161. package/clis/slock/attachment-url.test.js +41 -0
  1162. package/clis/slock/auth-verify.js +19 -0
  1163. package/clis/slock/bookmark-add.js +43 -0
  1164. package/clis/slock/bookmark-add.test.js +38 -0
  1165. package/clis/slock/bookmark-list.js +50 -0
  1166. package/clis/slock/bookmark-list.test.js +47 -0
  1167. package/clis/slock/bookmark-remove.js +42 -0
  1168. package/clis/slock/bookmark-remove.test.js +24 -0
  1169. package/clis/slock/channel-action.js +63 -0
  1170. package/clis/slock/channel-action.test.js +65 -0
  1171. package/clis/slock/channel-archive.js +9 -0
  1172. package/clis/slock/channel-create.js +48 -0
  1173. package/clis/slock/channel-create.test.js +40 -0
  1174. package/clis/slock/channel-files.js +51 -0
  1175. package/clis/slock/channel-files.test.js +32 -0
  1176. package/clis/slock/channel-info.js +42 -0
  1177. package/clis/slock/channel-info.test.js +25 -0
  1178. package/clis/slock/channel-join.js +9 -0
  1179. package/clis/slock/channel-leave.js +9 -0
  1180. package/clis/slock/channel-list.js +40 -0
  1181. package/clis/slock/channel-list.test.js +37 -0
  1182. package/clis/slock/channel-mark.js +71 -0
  1183. package/clis/slock/channel-mark.test.js +49 -0
  1184. package/clis/slock/channel-members.js +64 -0
  1185. package/clis/slock/channel-members.test.js +44 -0
  1186. package/clis/slock/channel-unarchive.js +11 -0
  1187. package/clis/slock/cross-command.test.js +21 -0
  1188. package/clis/slock/dm-list.js +41 -0
  1189. package/clis/slock/dm-list.test.js +26 -0
  1190. package/clis/slock/error-detail-canary.test.js +144 -0
  1191. package/clis/slock/errors.js +30 -0
  1192. package/clis/slock/errors.test.js +29 -0
  1193. package/clis/slock/in-page.js +213 -0
  1194. package/clis/slock/in-page.test.js +362 -0
  1195. package/clis/slock/inbox-done.js +40 -0
  1196. package/clis/slock/inbox-done.test.js +21 -0
  1197. package/clis/slock/inbox-read-all.js +32 -0
  1198. package/clis/slock/inbox-read-all.test.js +20 -0
  1199. package/clis/slock/inbox.js +74 -0
  1200. package/clis/slock/inbox.test.js +46 -0
  1201. package/clis/slock/login.js +3 -0
  1202. package/clis/slock/message-read.js +168 -0
  1203. package/clis/slock/message-read.test.js +83 -0
  1204. package/clis/slock/message-search.js +72 -0
  1205. package/clis/slock/message-search.test.js +61 -0
  1206. package/clis/slock/message-send.js +149 -0
  1207. package/clis/slock/message-send.test.js +127 -0
  1208. package/clis/slock/reaction-add.js +42 -0
  1209. package/clis/slock/reaction-add.test.js +36 -0
  1210. package/clis/slock/reaction-remove.js +42 -0
  1211. package/clis/slock/reaction-remove.test.js +29 -0
  1212. package/clis/slock/resolve.js +74 -0
  1213. package/clis/slock/resolve.test.js +102 -0
  1214. package/clis/slock/server-list.js +38 -0
  1215. package/clis/slock/server-list.test.js +24 -0
  1216. package/clis/slock/server-override-canary.test.js +87 -0
  1217. package/clis/slock/server-use.js +53 -0
  1218. package/clis/slock/server-use.test.js +48 -0
  1219. package/clis/slock/shared.js +14 -0
  1220. package/clis/slock/short-id-canary.test.js +86 -0
  1221. package/clis/slock/site-session-canary.test.js +49 -0
  1222. package/clis/slock/task-claim.js +72 -0
  1223. package/clis/slock/task-claim.test.js +81 -0
  1224. package/clis/slock/task-convert.js +106 -0
  1225. package/clis/slock/task-convert.test.js +82 -0
  1226. package/clis/slock/task-create.js +82 -0
  1227. package/clis/slock/task-create.test.js +65 -0
  1228. package/clis/slock/task-delete.js +55 -0
  1229. package/clis/slock/task-delete.test.js +52 -0
  1230. package/clis/slock/task-get.js +59 -0
  1231. package/clis/slock/task-get.test.js +45 -0
  1232. package/clis/slock/task-list-server.js +63 -0
  1233. package/clis/slock/task-list-server.test.js +48 -0
  1234. package/clis/slock/task-list.js +70 -0
  1235. package/clis/slock/task-list.test.js +59 -0
  1236. package/clis/slock/task-status.js +93 -0
  1237. package/clis/slock/task-status.test.js +76 -0
  1238. package/clis/slock/task-unclaim.js +66 -0
  1239. package/clis/slock/task-unclaim.test.js +49 -0
  1240. package/clis/slock/thread-done.js +9 -0
  1241. package/clis/slock/thread-follow.js +46 -0
  1242. package/clis/slock/thread-follow.test.js +34 -0
  1243. package/clis/slock/thread-list.js +44 -0
  1244. package/clis/slock/thread-list.test.js +24 -0
  1245. package/clis/slock/thread-state.js +45 -0
  1246. package/clis/slock/thread-state.test.js +38 -0
  1247. package/clis/slock/thread-undone.js +9 -0
  1248. package/clis/slock/thread-unfollow.js +9 -0
  1249. package/clis/slock/unread-summary.js +49 -0
  1250. package/clis/slock/unread-summary.test.js +31 -0
  1251. package/clis/slock/whoami.js +12 -0
  1252. package/clis/slock/whoami.test.js +23 -0
  1253. package/clis/smzdm/search.js +149 -0
  1254. package/clis/smzdm/search.test.js +114 -0
  1255. package/clis/spotify/spotify.js +327 -0
  1256. package/clis/spotify/utils.js +66 -0
  1257. package/clis/spotify/utils.test.js +67 -0
  1258. package/clis/stackoverflow/bounties.js +35 -0
  1259. package/clis/stackoverflow/hot.js +32 -0
  1260. package/clis/stackoverflow/read.js +314 -0
  1261. package/clis/stackoverflow/related.js +66 -0
  1262. package/clis/stackoverflow/search.js +35 -0
  1263. package/clis/stackoverflow/stackoverflow.test.js +404 -0
  1264. package/clis/stackoverflow/tag.js +60 -0
  1265. package/clis/stackoverflow/unanswered.js +33 -0
  1266. package/clis/stackoverflow/user.js +50 -0
  1267. package/clis/stackoverflow/utils.js +118 -0
  1268. package/clis/steam/app.js +67 -0
  1269. package/clis/steam/search.js +58 -0
  1270. package/clis/steam/steam.test.js +46 -0
  1271. package/clis/steam/top-sellers.js +26 -0
  1272. package/clis/steam/utils.js +107 -0
  1273. package/clis/substack/feed.js +16 -0
  1274. package/clis/substack/publication.js +16 -0
  1275. package/clis/substack/search.js +79 -0
  1276. package/clis/substack/utils.js +136 -0
  1277. package/clis/substack/utils.test.js +44 -0
  1278. package/clis/suno/auth.js +63 -0
  1279. package/clis/suno/commands.test.js +188 -0
  1280. package/clis/suno/download.js +140 -0
  1281. package/clis/suno/download.test.js +151 -0
  1282. package/clis/suno/generate.js +231 -0
  1283. package/clis/suno/generate.test.js +252 -0
  1284. package/clis/suno/list.js +79 -0
  1285. package/clis/suno/status.js +63 -0
  1286. package/clis/suno/utils.js +549 -0
  1287. package/clis/suno/utils.test.js +329 -0
  1288. package/clis/taobao/add-cart.js +150 -0
  1289. package/clis/taobao/auth.js +58 -0
  1290. package/clis/taobao/cart.js +96 -0
  1291. package/clis/taobao/commands.test.js +41 -0
  1292. package/clis/taobao/detail.js +71 -0
  1293. package/clis/taobao/reviews.js +77 -0
  1294. package/clis/taobao/search.js +97 -0
  1295. package/clis/tdx/hot-rank.js +48 -0
  1296. package/clis/tdx/hot-rank.test.js +59 -0
  1297. package/clis/test-utils.js +61 -0
  1298. package/clis/ths/hot-rank.js +50 -0
  1299. package/clis/ths/hot-rank.test.js +64 -0
  1300. package/clis/tieba/commands.test.js +79 -0
  1301. package/clis/tieba/hot.js +48 -0
  1302. package/clis/tieba/posts.js +86 -0
  1303. package/clis/tieba/read.js +141 -0
  1304. package/clis/tieba/search.js +109 -0
  1305. package/clis/tieba/utils.js +240 -0
  1306. package/clis/tieba/utils.test.js +290 -0
  1307. package/clis/tiktok/auth.js +65 -0
  1308. package/clis/tiktok/comment.js +145 -0
  1309. package/clis/tiktok/creator-videos.js +270 -0
  1310. package/clis/tiktok/creator-videos.test.js +113 -0
  1311. package/clis/tiktok/explore.js +144 -0
  1312. package/clis/tiktok/follow.js +122 -0
  1313. package/clis/tiktok/following.js +164 -0
  1314. package/clis/tiktok/friends.js +146 -0
  1315. package/clis/tiktok/like.js +34 -0
  1316. package/clis/tiktok/live.js +144 -0
  1317. package/clis/tiktok/notifications.js +153 -0
  1318. package/clis/tiktok/profile.js +55 -0
  1319. package/clis/tiktok/refactor.test.js +389 -0
  1320. package/clis/tiktok/save.js +30 -0
  1321. package/clis/tiktok/search.js +40 -0
  1322. package/clis/tiktok/unfollow.js +131 -0
  1323. package/clis/tiktok/unlike.js +34 -0
  1324. package/clis/tiktok/unsave.js +32 -0
  1325. package/clis/tiktok/user.js +216 -0
  1326. package/clis/tiktok/utils.js +505 -0
  1327. package/clis/tiktok/write-refactor.test.js +370 -0
  1328. package/clis/toutiao/articles.js +55 -0
  1329. package/clis/toutiao/auth.js +70 -0
  1330. package/clis/toutiao/hot.js +63 -0
  1331. package/clis/toutiao/toutiao.test.js +378 -0
  1332. package/clis/toutiao/utils.js +161 -0
  1333. package/clis/trae-cn/activity.js +22 -0
  1334. package/clis/trae-cn/approve.js +52 -0
  1335. package/clis/trae-cn/ask.js +99 -0
  1336. package/clis/trae-cn/dump.js +5 -0
  1337. package/clis/trae-cn/export.js +40 -0
  1338. package/clis/trae-cn/model.js +22 -0
  1339. package/clis/trae-cn/new.js +63 -0
  1340. package/clis/trae-cn/read.js +23 -0
  1341. package/clis/trae-cn/screenshot.js +5 -0
  1342. package/clis/trae-cn/select-model.js +25 -0
  1343. package/clis/trae-cn/send.js +23 -0
  1344. package/clis/trae-cn/setup.js +75 -0
  1345. package/clis/trae-cn/status.js +27 -0
  1346. package/clis/trae-cn/targets.js +96 -0
  1347. package/clis/trae-cn/trae-cn.test.js +412 -0
  1348. package/clis/trae-cn/utils.js +750 -0
  1349. package/clis/trae-cn/watch.js +119 -0
  1350. package/clis/trae-solo/_actions.js +60 -0
  1351. package/clis/trae-solo/_fs.js +118 -0
  1352. package/clis/trae-solo/_state.js +111 -0
  1353. package/clis/trae-solo/automation.js +97 -0
  1354. package/clis/trae-solo/history.js +54 -0
  1355. package/clis/trae-solo/mode.js +81 -0
  1356. package/clis/trae-solo/model.js +134 -0
  1357. package/clis/trae-solo/renderer-storage.js +170 -0
  1358. package/clis/trae-solo/settings.js +54 -0
  1359. package/clis/trae-solo/skill-fs.js +123 -0
  1360. package/clis/trae-solo/skill.js +220 -0
  1361. package/clis/trae-solo/state-fs.js +156 -0
  1362. package/clis/trae-solo/status.js +2 -0
  1363. package/clis/trae-solo/task-fs.js +181 -0
  1364. package/clis/trae-solo/trae-solo.test.js +67 -0
  1365. package/clis/trae-solo/user-rules.js +29 -0
  1366. package/clis/trae-solo/workspaces-fs.js +113 -0
  1367. package/clis/tvmaze/search.js +61 -0
  1368. package/clis/tvmaze/show.js +60 -0
  1369. package/clis/tvmaze/tvmaze.test.js +93 -0
  1370. package/clis/tvmaze/utils.js +110 -0
  1371. package/clis/twitter/accept.js +204 -0
  1372. package/clis/twitter/article.js +170 -0
  1373. package/clis/twitter/auth.js +41 -0
  1374. package/clis/twitter/block.js +90 -0
  1375. package/clis/twitter/bookmark-folder.js +190 -0
  1376. package/clis/twitter/bookmark-folder.test.js +396 -0
  1377. package/clis/twitter/bookmark-folders.js +114 -0
  1378. package/clis/twitter/bookmark-folders.test.js +152 -0
  1379. package/clis/twitter/bookmark.js +74 -0
  1380. package/clis/twitter/bookmark.test.js +74 -0
  1381. package/clis/twitter/bookmarks.js +181 -0
  1382. package/clis/twitter/bookmarks.test.js +206 -0
  1383. package/clis/twitter/delete.js +85 -0
  1384. package/clis/twitter/delete.test.js +85 -0
  1385. package/clis/twitter/device-follow.js +194 -0
  1386. package/clis/twitter/device-follow.test.js +287 -0
  1387. package/clis/twitter/download.js +481 -0
  1388. package/clis/twitter/download.test.js +457 -0
  1389. package/clis/twitter/follow-batch.js +162 -0
  1390. package/clis/twitter/follow-batch.test.js +137 -0
  1391. package/clis/twitter/follow.js +67 -0
  1392. package/clis/twitter/followers.js +193 -0
  1393. package/clis/twitter/followers.test.js +62 -0
  1394. package/clis/twitter/following.js +274 -0
  1395. package/clis/twitter/following.test.js +359 -0
  1396. package/clis/twitter/hide-reply.js +87 -0
  1397. package/clis/twitter/hide-reply.test.js +76 -0
  1398. package/clis/twitter/like.js +80 -0
  1399. package/clis/twitter/like.test.js +73 -0
  1400. package/clis/twitter/likes.js +241 -0
  1401. package/clis/twitter/likes.test.js +220 -0
  1402. package/clis/twitter/list-add-batch.js +32 -0
  1403. package/clis/twitter/list-add-core.js +245 -0
  1404. package/clis/twitter/list-add.js +20 -0
  1405. package/clis/twitter/list-add.test.js +133 -0
  1406. package/clis/twitter/list-batch-utils.js +95 -0
  1407. package/clis/twitter/list-batch.test.js +113 -0
  1408. package/clis/twitter/list-create.js +155 -0
  1409. package/clis/twitter/list-create.test.js +169 -0
  1410. package/clis/twitter/list-delete.js +158 -0
  1411. package/clis/twitter/list-delete.test.js +102 -0
  1412. package/clis/twitter/list-remove-batch.js +32 -0
  1413. package/clis/twitter/list-remove-core.js +291 -0
  1414. package/clis/twitter/list-remove.js +20 -0
  1415. package/clis/twitter/list-remove.test.js +147 -0
  1416. package/clis/twitter/list-tweets.js +199 -0
  1417. package/clis/twitter/list-tweets.test.js +198 -0
  1418. package/clis/twitter/lists.js +178 -0
  1419. package/clis/twitter/lists.test.js +253 -0
  1420. package/clis/twitter/notifications.js +128 -0
  1421. package/clis/twitter/post.js +316 -0
  1422. package/clis/twitter/post.test.js +275 -0
  1423. package/clis/twitter/profile.js +174 -0
  1424. package/clis/twitter/profile.test.js +150 -0
  1425. package/clis/twitter/quote.js +172 -0
  1426. package/clis/twitter/quote.test.js +194 -0
  1427. package/clis/twitter/reply-dm.js +183 -0
  1428. package/clis/twitter/reply.js +248 -0
  1429. package/clis/twitter/reply.test.js +237 -0
  1430. package/clis/twitter/retweet.js +94 -0
  1431. package/clis/twitter/retweet.test.js +73 -0
  1432. package/clis/twitter/search.js +346 -0
  1433. package/clis/twitter/search.test.js +400 -0
  1434. package/clis/twitter/shared.js +564 -0
  1435. package/clis/twitter/shared.test.js +1012 -0
  1436. package/clis/twitter/thread.js +164 -0
  1437. package/clis/twitter/thread.test.js +58 -0
  1438. package/clis/twitter/timeline.js +211 -0
  1439. package/clis/twitter/timeline.test.js +104 -0
  1440. package/clis/twitter/trending.js +58 -0
  1441. package/clis/twitter/trending.test.js +15 -0
  1442. package/clis/twitter/tweets.js +314 -0
  1443. package/clis/twitter/tweets.test.js +362 -0
  1444. package/clis/twitter/unblock.js +73 -0
  1445. package/clis/twitter/unbookmark.js +71 -0
  1446. package/clis/twitter/unbookmark.test.js +73 -0
  1447. package/clis/twitter/unfollow.js +73 -0
  1448. package/clis/twitter/unlike.js +80 -0
  1449. package/clis/twitter/unlike.test.js +75 -0
  1450. package/clis/twitter/unretweet.js +94 -0
  1451. package/clis/twitter/unretweet.test.js +73 -0
  1452. package/clis/twitter/utils.js +323 -0
  1453. package/clis/twitter/utils.test.js +169 -0
  1454. package/clis/uisdc/news.js +105 -0
  1455. package/clis/uisdc/news.test.js +66 -0
  1456. package/clis/uiverse/_shared.js +427 -0
  1457. package/clis/uiverse/_shared.test.js +62 -0
  1458. package/clis/uiverse/code.js +49 -0
  1459. package/clis/uiverse/navigation.test.js +12 -0
  1460. package/clis/uiverse/preview.js +73 -0
  1461. package/clis/upwork/auth.js +53 -0
  1462. package/clis/upwork/detail.js +132 -0
  1463. package/clis/upwork/feed.js +109 -0
  1464. package/clis/upwork/search.js +115 -0
  1465. package/clis/upwork/upwork.test.js +566 -0
  1466. package/clis/upwork/utils.js +323 -0
  1467. package/clis/v2ex/auth.js +44 -0
  1468. package/clis/v2ex/daily.js +99 -0
  1469. package/clis/v2ex/hot.js +26 -0
  1470. package/clis/v2ex/latest.js +26 -0
  1471. package/clis/v2ex/me.js +100 -0
  1472. package/clis/v2ex/member.js +28 -0
  1473. package/clis/v2ex/node.js +39 -0
  1474. package/clis/v2ex/nodes.js +26 -0
  1475. package/clis/v2ex/notifications.js +72 -0
  1476. package/clis/v2ex/replies.js +27 -0
  1477. package/clis/v2ex/topic.js +31 -0
  1478. package/clis/v2ex/user.js +34 -0
  1479. package/clis/wanfang/search.js +66 -0
  1480. package/clis/wanfang/search.test.js +23 -0
  1481. package/clis/web/read.js +491 -0
  1482. package/clis/web/read.test.js +392 -0
  1483. package/clis/wechat-channels/auth.js +62 -0
  1484. package/clis/wechat-channels/publish.js +732 -0
  1485. package/clis/wechat-channels/publish.test.js +144 -0
  1486. package/clis/weibo/auth.js +53 -0
  1487. package/clis/weibo/comments.js +53 -0
  1488. package/clis/weibo/delete.js +172 -0
  1489. package/clis/weibo/delete.test.js +94 -0
  1490. package/clis/weibo/envelope.test.js +85 -0
  1491. package/clis/weibo/favorites.js +170 -0
  1492. package/clis/weibo/favorites.test.js +114 -0
  1493. package/clis/weibo/feed.js +69 -0
  1494. package/clis/weibo/hot.js +39 -0
  1495. package/clis/weibo/me.js +75 -0
  1496. package/clis/weibo/post.js +75 -0
  1497. package/clis/weibo/publish.js +306 -0
  1498. package/clis/weibo/publish.test.js +192 -0
  1499. package/clis/weibo/search.js +80 -0
  1500. package/clis/weibo/user-posts.js +234 -0
  1501. package/clis/weibo/user-posts.test.js +92 -0
  1502. package/clis/weibo/user.js +63 -0
  1503. package/clis/weibo/utils.js +59 -0
  1504. package/clis/weibo/utils.test.js +36 -0
  1505. package/clis/weixin/create-draft.js +226 -0
  1506. package/clis/weixin/download.js +371 -0
  1507. package/clis/weixin/download.test.js +106 -0
  1508. package/clis/weixin/drafts.js +66 -0
  1509. package/clis/weixin/drafts.test.js +69 -0
  1510. package/clis/weixin/search.js +157 -0
  1511. package/clis/weixin/search.test.js +227 -0
  1512. package/clis/weread/ai-outline.js +171 -0
  1513. package/clis/weread/ai-outline.test.js +83 -0
  1514. package/clis/weread/auth.js +59 -0
  1515. package/clis/weread/book-search.js +438 -0
  1516. package/clis/weread/book-search.test.js +242 -0
  1517. package/clis/weread/book.js +233 -0
  1518. package/clis/weread/commands.test.js +398 -0
  1519. package/clis/weread/highlights.js +24 -0
  1520. package/clis/weread/notebooks.js +22 -0
  1521. package/clis/weread/notes.js +30 -0
  1522. package/clis/weread/private-api-regression.test.js +369 -0
  1523. package/clis/weread/ranking.js +29 -0
  1524. package/clis/weread/search-regression.test.js +494 -0
  1525. package/clis/weread/search.js +186 -0
  1526. package/clis/weread/shelf.js +66 -0
  1527. package/clis/weread/utils.js +321 -0
  1528. package/clis/weread/utils.test.js +128 -0
  1529. package/clis/weread-official/book.js +135 -0
  1530. package/clis/weread-official/commands.test.js +385 -0
  1531. package/clis/weread-official/discover.js +107 -0
  1532. package/clis/weread-official/list-apis.js +95 -0
  1533. package/clis/weread-official/notes.js +171 -0
  1534. package/clis/weread-official/readdata.js +158 -0
  1535. package/clis/weread-official/review.js +93 -0
  1536. package/clis/weread-official/search.js +106 -0
  1537. package/clis/weread-official/shelf.js +97 -0
  1538. package/clis/weread-official/utils.js +293 -0
  1539. package/clis/weread-official/utils.test.js +242 -0
  1540. package/clis/wikidata/entity.js +60 -0
  1541. package/clis/wikidata/search.js +50 -0
  1542. package/clis/wikidata/utils.js +117 -0
  1543. package/clis/wikidata/wikidata.test.js +83 -0
  1544. package/clis/wikipedia/page.js +95 -0
  1545. package/clis/wikipedia/random.js +20 -0
  1546. package/clis/wikipedia/search.js +31 -0
  1547. package/clis/wikipedia/summary.js +24 -0
  1548. package/clis/wikipedia/trending.js +40 -0
  1549. package/clis/wikipedia/trending.test.js +57 -0
  1550. package/clis/wikipedia/utils.js +31 -0
  1551. package/clis/wttr/current.js +63 -0
  1552. package/clis/wttr/forecast.js +71 -0
  1553. package/clis/wttr/utils.js +50 -0
  1554. package/clis/wttr/wttr.test.js +84 -0
  1555. package/clis/xianyu/auth.js +68 -0
  1556. package/clis/xianyu/chat.js +74 -0
  1557. package/clis/xianyu/chat.test.js +84 -0
  1558. package/clis/xianyu/im.js +322 -0
  1559. package/clis/xianyu/im.test.js +253 -0
  1560. package/clis/xianyu/inbox.js +96 -0
  1561. package/clis/xianyu/item.js +153 -0
  1562. package/clis/xianyu/item.test.js +56 -0
  1563. package/clis/xianyu/messages.js +91 -0
  1564. package/clis/xianyu/publish.js +485 -0
  1565. package/clis/xianyu/publish.test.js +220 -0
  1566. package/clis/xianyu/reply.js +82 -0
  1567. package/clis/xianyu/search.js +135 -0
  1568. package/clis/xianyu/search.test.js +17 -0
  1569. package/clis/xianyu/utils.js +8 -0
  1570. package/clis/xiaoe/auth.js +55 -0
  1571. package/clis/xiaoe/catalog.js +227 -0
  1572. package/clis/xiaoe/content.js +175 -0
  1573. package/clis/xiaoe/courses.js +127 -0
  1574. package/clis/xiaoe/detail.js +36 -0
  1575. package/clis/xiaoe/play-url.js +121 -0
  1576. package/clis/xiaoe/xiaoe.test.js +486 -0
  1577. package/clis/xiaohongshu/ask.js +426 -0
  1578. package/clis/xiaohongshu/ask.test.js +271 -0
  1579. package/clis/xiaohongshu/auth.js +53 -0
  1580. package/clis/xiaohongshu/collection-helpers.js +283 -0
  1581. package/clis/xiaohongshu/collection-helpers.test.js +125 -0
  1582. package/clis/xiaohongshu/comments.js +228 -0
  1583. package/clis/xiaohongshu/comments.test.js +320 -0
  1584. package/clis/xiaohongshu/creator-note-detail.js +482 -0
  1585. package/clis/xiaohongshu/creator-note-detail.test.js +451 -0
  1586. package/clis/xiaohongshu/creator-notes-summary.js +98 -0
  1587. package/clis/xiaohongshu/creator-notes-summary.test.js +94 -0
  1588. package/clis/xiaohongshu/creator-notes.js +479 -0
  1589. package/clis/xiaohongshu/creator-notes.test.js +281 -0
  1590. package/clis/xiaohongshu/creator-profile.js +55 -0
  1591. package/clis/xiaohongshu/creator-stats.js +76 -0
  1592. package/clis/xiaohongshu/creator-stats.test.js +24 -0
  1593. package/clis/xiaohongshu/delete-note.js +260 -0
  1594. package/clis/xiaohongshu/delete-note.test.js +172 -0
  1595. package/clis/xiaohongshu/download.js +249 -0
  1596. package/clis/xiaohongshu/download.test.js +339 -0
  1597. package/clis/xiaohongshu/draft-clear.js +91 -0
  1598. package/clis/xiaohongshu/draft-delete.js +103 -0
  1599. package/clis/xiaohongshu/draft-open.js +43 -0
  1600. package/clis/xiaohongshu/draft-utils.js +142 -0
  1601. package/clis/xiaohongshu/drafts.js +28 -0
  1602. package/clis/xiaohongshu/drafts.test.js +172 -0
  1603. package/clis/xiaohongshu/feed.js +163 -0
  1604. package/clis/xiaohongshu/feed.test.js +126 -0
  1605. package/clis/xiaohongshu/follow.js +226 -0
  1606. package/clis/xiaohongshu/follow.test.js +211 -0
  1607. package/clis/xiaohongshu/liked.js +29 -0
  1608. package/clis/xiaohongshu/liked.test.js +82 -0
  1609. package/clis/xiaohongshu/navigation.test.js +38 -0
  1610. package/clis/xiaohongshu/note-helpers.js +69 -0
  1611. package/clis/xiaohongshu/note.js +104 -0
  1612. package/clis/xiaohongshu/note.test.js +249 -0
  1613. package/clis/xiaohongshu/notifications.js +46 -0
  1614. package/clis/xiaohongshu/publish.js +1423 -0
  1615. package/clis/xiaohongshu/publish.test.js +1141 -0
  1616. package/clis/xiaohongshu/saved.js +29 -0
  1617. package/clis/xiaohongshu/saved.test.js +143 -0
  1618. package/clis/xiaohongshu/search.js +330 -0
  1619. package/clis/xiaohongshu/search.test.js +325 -0
  1620. package/clis/xiaohongshu/unfollow.js +285 -0
  1621. package/clis/xiaohongshu/user-helpers.js +78 -0
  1622. package/clis/xiaohongshu/user-helpers.test.js +160 -0
  1623. package/clis/xiaohongshu/user.js +140 -0
  1624. package/clis/xiaohongshu/user.test.js +141 -0
  1625. package/clis/xiaoyuzhou/auth.js +303 -0
  1626. package/clis/xiaoyuzhou/auth.test.js +124 -0
  1627. package/clis/xiaoyuzhou/download.js +54 -0
  1628. package/clis/xiaoyuzhou/download.test.js +135 -0
  1629. package/clis/xiaoyuzhou/episode.js +34 -0
  1630. package/clis/xiaoyuzhou/podcast-episodes.js +41 -0
  1631. package/clis/xiaoyuzhou/podcast-episodes.test.js +78 -0
  1632. package/clis/xiaoyuzhou/podcast.js +33 -0
  1633. package/clis/xiaoyuzhou/transcript.js +77 -0
  1634. package/clis/xiaoyuzhou/transcript.test.js +195 -0
  1635. package/clis/xiaoyuzhou/utils.js +15 -0
  1636. package/clis/xiaoyuzhou/utils.test.js +39 -0
  1637. package/clis/xueqiu/auth.js +63 -0
  1638. package/clis/xueqiu/comments.js +355 -0
  1639. package/clis/xueqiu/comments.test.js +696 -0
  1640. package/clis/xueqiu/danjuan-utils.js +126 -0
  1641. package/clis/xueqiu/danjuan-utils.test.js +41 -0
  1642. package/clis/xueqiu/earnings-date.js +45 -0
  1643. package/clis/xueqiu/feed.js +39 -0
  1644. package/clis/xueqiu/fund-holdings.js +29 -0
  1645. package/clis/xueqiu/fund-snapshot.js +26 -0
  1646. package/clis/xueqiu/groups.js +23 -0
  1647. package/clis/xueqiu/hot-stock.js +32 -0
  1648. package/clis/xueqiu/hot.js +37 -0
  1649. package/clis/xueqiu/kline.js +44 -0
  1650. package/clis/xueqiu/search.js +37 -0
  1651. package/clis/xueqiu/stock.js +65 -0
  1652. package/clis/xueqiu/utils.js +48 -0
  1653. package/clis/xueqiu/utils.test.js +26 -0
  1654. package/clis/xueqiu/watchlist.js +37 -0
  1655. package/clis/yahoo/search.js +92 -0
  1656. package/clis/yahoo/search.test.js +94 -0
  1657. package/clis/yahoo-finance/quote.js +74 -0
  1658. package/clis/yollomi/background.js +46 -0
  1659. package/clis/yollomi/edit.js +57 -0
  1660. package/clis/yollomi/face-swap.js +44 -0
  1661. package/clis/yollomi/generate.js +101 -0
  1662. package/clis/yollomi/models.js +34 -0
  1663. package/clis/yollomi/object-remover.js +43 -0
  1664. package/clis/yollomi/remove-bg.js +39 -0
  1665. package/clis/yollomi/restore.js +39 -0
  1666. package/clis/yollomi/try-on.js +47 -0
  1667. package/clis/yollomi/upload.js +72 -0
  1668. package/clis/yollomi/upscale.js +54 -0
  1669. package/clis/yollomi/utils.js +180 -0
  1670. package/clis/yollomi/video.js +57 -0
  1671. package/clis/youdao/note.js +258 -0
  1672. package/clis/youdao/note.test.js +99 -0
  1673. package/clis/youtube/auth.js +54 -0
  1674. package/clis/youtube/channel.js +227 -0
  1675. package/clis/youtube/channel.test.js +283 -0
  1676. package/clis/youtube/comments.js +96 -0
  1677. package/clis/youtube/feed.js +121 -0
  1678. package/clis/youtube/feed.test.js +131 -0
  1679. package/clis/youtube/history.js +119 -0
  1680. package/clis/youtube/like.js +67 -0
  1681. package/clis/youtube/playlist.js +98 -0
  1682. package/clis/youtube/search.js +100 -0
  1683. package/clis/youtube/subscribe.js +76 -0
  1684. package/clis/youtube/subscriptions.js +58 -0
  1685. package/clis/youtube/transcript-group.js +226 -0
  1686. package/clis/youtube/transcript-group.test.js +99 -0
  1687. package/clis/youtube/transcript.js +656 -0
  1688. package/clis/youtube/transcript.test.js +296 -0
  1689. package/clis/youtube/unlike.js +67 -0
  1690. package/clis/youtube/unsubscribe.js +76 -0
  1691. package/clis/youtube/utils.js +220 -0
  1692. package/clis/youtube/utils.test.js +84 -0
  1693. package/clis/youtube/video.js +152 -0
  1694. package/clis/youtube/video.test.js +111 -0
  1695. package/clis/youtube/watch-later.js +77 -0
  1696. package/clis/yuanbao/ask.js +362 -0
  1697. package/clis/yuanbao/ask.test.js +124 -0
  1698. package/clis/yuanbao/auth.js +59 -0
  1699. package/clis/yuanbao/detail.js +65 -0
  1700. package/clis/yuanbao/history.js +51 -0
  1701. package/clis/yuanbao/new.js +72 -0
  1702. package/clis/yuanbao/new.test.js +30 -0
  1703. package/clis/yuanbao/read.js +38 -0
  1704. package/clis/yuanbao/send.js +57 -0
  1705. package/clis/yuanbao/shared.js +341 -0
  1706. package/clis/yuanbao/shared.test.js +80 -0
  1707. package/clis/yuanbao/status.js +44 -0
  1708. package/clis/zhihu/answer-comments.js +280 -0
  1709. package/clis/zhihu/answer-comments.test.js +287 -0
  1710. package/clis/zhihu/answer-detail.js +216 -0
  1711. package/clis/zhihu/answer-detail.test.js +338 -0
  1712. package/clis/zhihu/answer.js +54 -0
  1713. package/clis/zhihu/answer.test.js +54 -0
  1714. package/clis/zhihu/auth.js +59 -0
  1715. package/clis/zhihu/collection.js +199 -0
  1716. package/clis/zhihu/collection.test.js +337 -0
  1717. package/clis/zhihu/collections.js +128 -0
  1718. package/clis/zhihu/collections.test.js +182 -0
  1719. package/clis/zhihu/comment.js +55 -0
  1720. package/clis/zhihu/comment.test.js +50 -0
  1721. package/clis/zhihu/download.js +80 -0
  1722. package/clis/zhihu/download.test.js +12 -0
  1723. package/clis/zhihu/favorite.js +87 -0
  1724. package/clis/zhihu/favorite.test.js +59 -0
  1725. package/clis/zhihu/follow.js +50 -0
  1726. package/clis/zhihu/follow.test.js +42 -0
  1727. package/clis/zhihu/followers.js +40 -0
  1728. package/clis/zhihu/followers.test.js +42 -0
  1729. package/clis/zhihu/following.js +40 -0
  1730. package/clis/zhihu/following.test.js +44 -0
  1731. package/clis/zhihu/hot.js +44 -0
  1732. package/clis/zhihu/like.js +47 -0
  1733. package/clis/zhihu/like.test.js +41 -0
  1734. package/clis/zhihu/paginate.js +128 -0
  1735. package/clis/zhihu/pins.js +41 -0
  1736. package/clis/zhihu/pins.test.js +44 -0
  1737. package/clis/zhihu/question.js +113 -0
  1738. package/clis/zhihu/question.test.js +251 -0
  1739. package/clis/zhihu/recommend.js +103 -0
  1740. package/clis/zhihu/recommend.test.js +143 -0
  1741. package/clis/zhihu/search.js +206 -0
  1742. package/clis/zhihu/search.test.js +198 -0
  1743. package/clis/zhihu/target.js +91 -0
  1744. package/clis/zhihu/target.test.js +77 -0
  1745. package/clis/zhihu/text.js +29 -0
  1746. package/clis/zhihu/text.test.js +24 -0
  1747. package/clis/zhihu/user-answers.js +41 -0
  1748. package/clis/zhihu/user-answers.test.js +69 -0
  1749. package/clis/zhihu/user-arg.js +34 -0
  1750. package/clis/zhihu/user-articles.js +40 -0
  1751. package/clis/zhihu/user-articles.test.js +44 -0
  1752. package/clis/zhihu/user.js +59 -0
  1753. package/clis/zhihu/user.test.js +73 -0
  1754. package/clis/zhihu/write-shared.js +228 -0
  1755. package/clis/zhihu/write-shared.test.js +176 -0
  1756. package/clis/zlibrary/commands.test.js +65 -0
  1757. package/clis/zlibrary/info.js +48 -0
  1758. package/clis/zlibrary/search.js +47 -0
  1759. package/clis/zlibrary/utils.js +136 -0
  1760. package/clis/zsxq/auth.js +68 -0
  1761. package/clis/zsxq/dynamics.js +48 -0
  1762. package/clis/zsxq/groups.js +33 -0
  1763. package/clis/zsxq/search.js +44 -0
  1764. package/clis/zsxq/search.test.js +24 -0
  1765. package/clis/zsxq/topic.js +50 -0
  1766. package/clis/zsxq/topic.test.js +30 -0
  1767. package/clis/zsxq/topics.js +26 -0
  1768. package/clis/zsxq/topics.test.js +24 -0
  1769. package/clis/zsxq/utils.js +233 -0
  1770. package/clis/zsxq/utils.test.js +31 -0
  1771. package/dist/src/adapter-shadow.d.ts +11 -0
  1772. package/dist/src/adapter-shadow.js +72 -0
  1773. package/dist/src/adapter-shadow.test.d.ts +1 -0
  1774. package/dist/src/adapter-shadow.test.js +49 -0
  1775. package/dist/src/adapter-source.d.ts +11 -0
  1776. package/dist/src/adapter-source.js +24 -0
  1777. package/dist/src/adapter-source.test.d.ts +1 -0
  1778. package/dist/src/adapter-source.test.js +29 -0
  1779. package/dist/src/browser/analyze.d.ts +118 -0
  1780. package/dist/src/browser/analyze.js +405 -0
  1781. package/dist/src/browser/analyze.test.d.ts +1 -0
  1782. package/dist/src/browser/analyze.test.js +209 -0
  1783. package/dist/src/browser/article-extract.d.ts +57 -0
  1784. package/dist/src/browser/article-extract.e2e.test.d.ts +1 -0
  1785. package/dist/src/browser/article-extract.e2e.test.js +105 -0
  1786. package/dist/src/browser/article-extract.js +169 -0
  1787. package/dist/src/browser/article-extract.test.d.ts +1 -0
  1788. package/dist/src/browser/article-extract.test.js +94 -0
  1789. package/dist/src/browser/ax-snapshot.d.ts +37 -0
  1790. package/dist/src/browser/ax-snapshot.js +217 -0
  1791. package/dist/src/browser/ax-snapshot.test.d.ts +1 -0
  1792. package/dist/src/browser/ax-snapshot.test.js +91 -0
  1793. package/dist/src/browser/base-page.d.ts +151 -0
  1794. package/dist/src/browser/base-page.js +1060 -0
  1795. package/dist/src/browser/base-page.test.d.ts +1 -0
  1796. package/dist/src/browser/base-page.test.js +803 -0
  1797. package/dist/src/browser/bridge.d.ts +28 -0
  1798. package/dist/src/browser/bridge.js +172 -0
  1799. package/dist/src/browser/cdp-click-fixture.test.d.ts +1 -0
  1800. package/dist/src/browser/cdp-click-fixture.test.js +87 -0
  1801. package/dist/src/browser/cdp.d.ts +46 -0
  1802. package/dist/src/browser/cdp.js +536 -0
  1803. package/dist/src/browser/cdp.test.d.ts +1 -0
  1804. package/dist/src/browser/cdp.test.js +79 -0
  1805. package/dist/src/browser/compound.d.ts +59 -0
  1806. package/dist/src/browser/compound.js +112 -0
  1807. package/dist/src/browser/compound.test.d.ts +1 -0
  1808. package/dist/src/browser/compound.test.js +175 -0
  1809. package/dist/src/browser/daemon-client.d.ts +133 -0
  1810. package/dist/src/browser/daemon-client.js +154 -0
  1811. package/dist/src/browser/daemon-client.test.d.ts +1 -0
  1812. package/dist/src/browser/daemon-client.test.js +199 -0
  1813. package/dist/src/browser/daemon-lifecycle.d.ts +23 -0
  1814. package/dist/src/browser/daemon-lifecycle.js +67 -0
  1815. package/dist/src/browser/daemon-version.d.ts +4 -0
  1816. package/dist/src/browser/daemon-version.js +12 -0
  1817. package/dist/src/browser/dom-helpers.d.ts +41 -0
  1818. package/dist/src/browser/dom-helpers.js +239 -0
  1819. package/dist/src/browser/dom-helpers.test.d.ts +1 -0
  1820. package/dist/src/browser/dom-helpers.test.js +106 -0
  1821. package/dist/src/browser/dom-snapshot.d.ts +93 -0
  1822. package/dist/src/browser/dom-snapshot.js +894 -0
  1823. package/dist/src/browser/dom-snapshot.test.d.ts +11 -0
  1824. package/dist/src/browser/dom-snapshot.test.js +313 -0
  1825. package/dist/src/browser/errors.d.ts +38 -0
  1826. package/dist/src/browser/errors.js +78 -0
  1827. package/dist/src/browser/errors.test.d.ts +1 -0
  1828. package/dist/src/browser/errors.test.js +60 -0
  1829. package/dist/src/browser/extract.d.ts +69 -0
  1830. package/dist/src/browser/extract.js +132 -0
  1831. package/dist/src/browser/extract.test.d.ts +1 -0
  1832. package/dist/src/browser/extract.test.js +129 -0
  1833. package/dist/src/browser/find.d.ts +84 -0
  1834. package/dist/src/browser/find.js +398 -0
  1835. package/dist/src/browser/find.test.d.ts +1 -0
  1836. package/dist/src/browser/find.test.js +180 -0
  1837. package/dist/src/browser/html-tree.d.ts +75 -0
  1838. package/dist/src/browser/html-tree.js +112 -0
  1839. package/dist/src/browser/html-tree.test.d.ts +1 -0
  1840. package/dist/src/browser/html-tree.test.js +181 -0
  1841. package/dist/src/browser/index.d.ts +14 -0
  1842. package/dist/src/browser/index.js +12 -0
  1843. package/dist/src/browser/network-cache.d.ts +49 -0
  1844. package/dist/src/browser/network-cache.js +78 -0
  1845. package/dist/src/browser/network-cache.test.d.ts +1 -0
  1846. package/dist/src/browser/network-cache.test.js +75 -0
  1847. package/dist/src/browser/network-key.d.ts +22 -0
  1848. package/dist/src/browser/network-key.js +66 -0
  1849. package/dist/src/browser/network-key.test.d.ts +1 -0
  1850. package/dist/src/browser/network-key.test.js +49 -0
  1851. package/dist/src/browser/page.d.ts +83 -0
  1852. package/dist/src/browser/page.js +453 -0
  1853. package/dist/src/browser/page.test.d.ts +1 -0
  1854. package/dist/src/browser/page.test.js +406 -0
  1855. package/dist/src/browser/profile.d.ts +14 -0
  1856. package/dist/src/browser/profile.js +85 -0
  1857. package/dist/src/browser/shape-filter.d.ts +52 -0
  1858. package/dist/src/browser/shape-filter.js +101 -0
  1859. package/dist/src/browser/shape-filter.test.d.ts +1 -0
  1860. package/dist/src/browser/shape-filter.test.js +101 -0
  1861. package/dist/src/browser/shape.d.ts +23 -0
  1862. package/dist/src/browser/shape.js +95 -0
  1863. package/dist/src/browser/shape.test.d.ts +1 -0
  1864. package/dist/src/browser/shape.test.js +82 -0
  1865. package/dist/src/browser/stealth.d.ts +11 -0
  1866. package/dist/src/browser/stealth.js +359 -0
  1867. package/dist/src/browser/stealth.test.d.ts +1 -0
  1868. package/dist/src/browser/stealth.test.js +134 -0
  1869. package/dist/src/browser/tabs.d.ts +13 -0
  1870. package/dist/src/browser/tabs.js +70 -0
  1871. package/dist/src/browser/target-errors.d.ts +39 -0
  1872. package/dist/src/browser/target-errors.js +45 -0
  1873. package/dist/src/browser/target-errors.test.d.ts +1 -0
  1874. package/dist/src/browser/target-errors.test.js +94 -0
  1875. package/dist/src/browser/target-resolver.d.ts +158 -0
  1876. package/dist/src/browser/target-resolver.js +570 -0
  1877. package/dist/src/browser/target-resolver.test.d.ts +1 -0
  1878. package/dist/src/browser/target-resolver.test.js +118 -0
  1879. package/dist/src/browser/utils.d.ts +20 -0
  1880. package/dist/src/browser/utils.js +64 -0
  1881. package/dist/src/browser/utils.test.d.ts +1 -0
  1882. package/dist/src/browser/utils.test.js +29 -0
  1883. package/dist/src/browser/verify-fixture.d.ts +65 -0
  1884. package/dist/src/browser/verify-fixture.js +318 -0
  1885. package/dist/src/browser/verify-fixture.test.d.ts +1 -0
  1886. package/dist/src/browser/verify-fixture.test.js +219 -0
  1887. package/dist/src/browser/visual-refs.d.ts +11 -0
  1888. package/dist/src/browser/visual-refs.js +108 -0
  1889. package/dist/src/browser.test.d.ts +1 -0
  1890. package/dist/src/browser.test.js +287 -0
  1891. package/dist/src/build-manifest.d.ts +103 -0
  1892. package/dist/src/build-manifest.js +363 -0
  1893. package/dist/src/build-manifest.test.d.ts +1 -0
  1894. package/dist/src/build-manifest.test.js +341 -0
  1895. package/dist/src/capabilityRouting.d.ts +19 -0
  1896. package/dist/src/capabilityRouting.js +56 -0
  1897. package/dist/src/capabilityRouting.test.d.ts +1 -0
  1898. package/dist/src/capabilityRouting.test.js +63 -0
  1899. package/dist/src/cli-argv-preprocess.d.ts +63 -0
  1900. package/dist/src/cli-argv-preprocess.js +303 -0
  1901. package/dist/src/cli-argv-preprocess.test.d.ts +1 -0
  1902. package/dist/src/cli-argv-preprocess.test.js +304 -0
  1903. package/dist/src/cli.d.ts +79 -0
  1904. package/dist/src/cli.js +3357 -0
  1905. package/dist/src/cli.test.d.ts +1 -0
  1906. package/dist/src/cli.test.js +3003 -0
  1907. package/dist/src/commanderAdapter.d.ts +21 -0
  1908. package/dist/src/commanderAdapter.js +208 -0
  1909. package/dist/src/commanderAdapter.test.d.ts +1 -0
  1910. package/dist/src/commanderAdapter.test.js +352 -0
  1911. package/dist/src/commands/auth.d.ts +40 -0
  1912. package/dist/src/commands/auth.js +434 -0
  1913. package/dist/src/commands/auth.test.d.ts +1 -0
  1914. package/dist/src/commands/auth.test.js +252 -0
  1915. package/dist/src/commands/daemon.d.ts +9 -0
  1916. package/dist/src/commands/daemon.js +98 -0
  1917. package/dist/src/commands/daemon.test.d.ts +1 -0
  1918. package/dist/src/commands/daemon.test.js +283 -0
  1919. package/dist/src/completion-fast.d.ts +22 -0
  1920. package/dist/src/completion-fast.js +88 -0
  1921. package/dist/src/completion-shared.d.ts +13 -0
  1922. package/dist/src/completion-shared.js +60 -0
  1923. package/dist/src/completion.d.ts +21 -0
  1924. package/dist/src/completion.js +68 -0
  1925. package/dist/src/completion.test.d.ts +1 -0
  1926. package/dist/src/completion.test.js +24 -0
  1927. package/dist/src/constants.d.ts +15 -0
  1928. package/dist/src/constants.js +32 -0
  1929. package/dist/src/convention-audit.d.ts +50 -0
  1930. package/dist/src/convention-audit.js +553 -0
  1931. package/dist/src/convention-audit.test.d.ts +1 -0
  1932. package/dist/src/convention-audit.test.js +247 -0
  1933. package/dist/src/daemon-utils.d.ts +18 -0
  1934. package/dist/src/daemon-utils.js +37 -0
  1935. package/dist/src/daemon.d.ts +22 -0
  1936. package/dist/src/daemon.js +437 -0
  1937. package/dist/src/daemon.test.d.ts +1 -0
  1938. package/dist/src/daemon.test.js +60 -0
  1939. package/dist/src/discovery.d.ts +43 -0
  1940. package/dist/src/discovery.js +266 -0
  1941. package/dist/src/doctor.d.ts +39 -0
  1942. package/dist/src/doctor.js +230 -0
  1943. package/dist/src/doctor.test.d.ts +1 -0
  1944. package/dist/src/doctor.test.js +263 -0
  1945. package/dist/src/download/article-download.d.ts +72 -0
  1946. package/dist/src/download/article-download.js +305 -0
  1947. package/dist/src/download/article-download.test.d.ts +1 -0
  1948. package/dist/src/download/article-download.test.js +235 -0
  1949. package/dist/src/download/index.d.ts +71 -0
  1950. package/dist/src/download/index.js +363 -0
  1951. package/dist/src/download/index.test.d.ts +1 -0
  1952. package/dist/src/download/index.test.js +174 -0
  1953. package/dist/src/download/media-download.d.ts +49 -0
  1954. package/dist/src/download/media-download.js +127 -0
  1955. package/dist/src/download/media-download.test.d.ts +1 -0
  1956. package/dist/src/download/media-download.test.js +112 -0
  1957. package/dist/src/download/progress.d.ts +36 -0
  1958. package/dist/src/download/progress.js +120 -0
  1959. package/dist/src/download/progress.test.d.ts +1 -0
  1960. package/dist/src/download/progress.test.js +36 -0
  1961. package/dist/src/electron-apps.d.ts +31 -0
  1962. package/dist/src/electron-apps.js +91 -0
  1963. package/dist/src/electron-apps.test.d.ts +1 -0
  1964. package/dist/src/electron-apps.test.js +76 -0
  1965. package/dist/src/engine.test.d.ts +1 -0
  1966. package/dist/src/engine.test.js +312 -0
  1967. package/dist/src/errors.d.ts +114 -0
  1968. package/dist/src/errors.js +174 -0
  1969. package/dist/src/errors.test.d.ts +1 -0
  1970. package/dist/src/errors.test.js +109 -0
  1971. package/dist/src/execution.d.ts +24 -0
  1972. package/dist/src/execution.js +509 -0
  1973. package/dist/src/execution.test.d.ts +1 -0
  1974. package/dist/src/execution.test.js +645 -0
  1975. package/dist/src/extension-manifest-regression.test.d.ts +1 -0
  1976. package/dist/src/extension-manifest-regression.test.js +12 -0
  1977. package/dist/src/external-clis.yaml +109 -0
  1978. package/dist/src/external.d.ts +49 -0
  1979. package/dist/src/external.js +211 -0
  1980. package/dist/src/external.test.d.ts +1 -0
  1981. package/dist/src/external.test.js +110 -0
  1982. package/dist/src/help.d.ts +88 -0
  1983. package/dist/src/help.js +555 -0
  1984. package/dist/src/help.test.d.ts +1 -0
  1985. package/dist/src/help.test.js +58 -0
  1986. package/dist/src/hooks.d.ts +46 -0
  1987. package/dist/src/hooks.js +58 -0
  1988. package/dist/src/hooks.test.d.ts +4 -0
  1989. package/dist/src/hooks.test.js +92 -0
  1990. package/dist/src/interceptor.d.ts +44 -0
  1991. package/dist/src/interceptor.js +183 -0
  1992. package/dist/src/interceptor.test.d.ts +4 -0
  1993. package/dist/src/interceptor.test.js +81 -0
  1994. package/dist/src/launcher.d.ts +41 -0
  1995. package/dist/src/launcher.js +226 -0
  1996. package/dist/src/launcher.test.d.ts +1 -0
  1997. package/dist/src/launcher.test.js +153 -0
  1998. package/dist/src/logger.d.ts +26 -0
  1999. package/dist/src/logger.js +49 -0
  2000. package/dist/src/main.d.ts +5 -0
  2001. package/dist/src/main.js +171 -0
  2002. package/dist/src/manifest-types.d.ts +43 -0
  2003. package/dist/src/manifest-types.js +9 -0
  2004. package/dist/src/node-network.d.ts +10 -0
  2005. package/dist/src/node-network.js +174 -0
  2006. package/dist/src/node-network.test.d.ts +1 -0
  2007. package/dist/src/node-network.test.js +55 -0
  2008. package/dist/src/observation/artifact.d.ts +16 -0
  2009. package/dist/src/observation/artifact.js +260 -0
  2010. package/dist/src/observation/artifact.test.d.ts +1 -0
  2011. package/dist/src/observation/artifact.test.js +121 -0
  2012. package/dist/src/observation/events.d.ts +89 -0
  2013. package/dist/src/observation/events.js +1 -0
  2014. package/dist/src/observation/index.d.ts +7 -0
  2015. package/dist/src/observation/index.js +7 -0
  2016. package/dist/src/observation/manager.d.ts +9 -0
  2017. package/dist/src/observation/manager.js +27 -0
  2018. package/dist/src/observation/manager.test.d.ts +1 -0
  2019. package/dist/src/observation/manager.test.js +13 -0
  2020. package/dist/src/observation/redaction.d.ts +11 -0
  2021. package/dist/src/observation/redaction.js +81 -0
  2022. package/dist/src/observation/redaction.test.d.ts +1 -0
  2023. package/dist/src/observation/redaction.test.js +32 -0
  2024. package/dist/src/observation/retention.d.ts +32 -0
  2025. package/dist/src/observation/retention.js +160 -0
  2026. package/dist/src/observation/retention.test.d.ts +1 -0
  2027. package/dist/src/observation/retention.test.js +118 -0
  2028. package/dist/src/observation/ring-buffer.d.ts +22 -0
  2029. package/dist/src/observation/ring-buffer.js +45 -0
  2030. package/dist/src/observation/ring-buffer.test.d.ts +1 -0
  2031. package/dist/src/observation/ring-buffer.test.js +22 -0
  2032. package/dist/src/observation/session.d.ts +25 -0
  2033. package/dist/src/observation/session.js +50 -0
  2034. package/dist/src/output.d.ts +14 -0
  2035. package/dist/src/output.js +152 -0
  2036. package/dist/src/output.test.d.ts +1 -0
  2037. package/dist/src/output.test.js +53 -0
  2038. package/dist/src/package-exports.test.d.ts +1 -0
  2039. package/dist/src/package-exports.test.js +112 -0
  2040. package/dist/src/package-paths.d.ts +8 -0
  2041. package/dist/src/package-paths.js +41 -0
  2042. package/dist/src/pipeline/executor.d.ts +11 -0
  2043. package/dist/src/pipeline/executor.js +93 -0
  2044. package/dist/src/pipeline/executor.test.d.ts +4 -0
  2045. package/dist/src/pipeline/executor.test.js +182 -0
  2046. package/dist/src/pipeline/index.d.ts +5 -0
  2047. package/dist/src/pipeline/index.js +5 -0
  2048. package/dist/src/pipeline/registry.d.ts +28 -0
  2049. package/dist/src/pipeline/registry.js +55 -0
  2050. package/dist/src/pipeline/steps/browser.d.ts +13 -0
  2051. package/dist/src/pipeline/steps/browser.js +86 -0
  2052. package/dist/src/pipeline/steps/download.d.ts +18 -0
  2053. package/dist/src/pipeline/steps/download.js +252 -0
  2054. package/dist/src/pipeline/steps/download.test.d.ts +1 -0
  2055. package/dist/src/pipeline/steps/download.test.js +102 -0
  2056. package/dist/src/pipeline/steps/fetch.d.ts +5 -0
  2057. package/dist/src/pipeline/steps/fetch.js +122 -0
  2058. package/dist/src/pipeline/steps/fetch.test.d.ts +1 -0
  2059. package/dist/src/pipeline/steps/fetch.test.js +117 -0
  2060. package/dist/src/pipeline/steps/intercept.d.ts +5 -0
  2061. package/dist/src/pipeline/steps/intercept.js +50 -0
  2062. package/dist/src/pipeline/steps/tap.d.ts +12 -0
  2063. package/dist/src/pipeline/steps/tap.js +93 -0
  2064. package/dist/src/pipeline/steps/transform.d.ts +9 -0
  2065. package/dist/src/pipeline/steps/transform.js +70 -0
  2066. package/dist/src/pipeline/template.d.ts +17 -0
  2067. package/dist/src/pipeline/template.js +338 -0
  2068. package/dist/src/pipeline/template.test.d.ts +4 -0
  2069. package/dist/src/pipeline/template.test.js +175 -0
  2070. package/dist/src/pipeline/transform.test.d.ts +4 -0
  2071. package/dist/src/pipeline/transform.test.js +133 -0
  2072. package/dist/src/plugin-manifest.d.ts +70 -0
  2073. package/dist/src/plugin-manifest.js +160 -0
  2074. package/dist/src/plugin-manifest.test.d.ts +4 -0
  2075. package/dist/src/plugin-manifest.test.js +179 -0
  2076. package/dist/src/plugin-scaffold.d.ts +28 -0
  2077. package/dist/src/plugin-scaffold.js +143 -0
  2078. package/dist/src/plugin-scaffold.test.d.ts +4 -0
  2079. package/dist/src/plugin-scaffold.test.js +83 -0
  2080. package/dist/src/plugin.d.ts +145 -0
  2081. package/dist/src/plugin.js +1245 -0
  2082. package/dist/src/plugin.test.d.ts +4 -0
  2083. package/dist/src/plugin.test.js +1324 -0
  2084. package/dist/src/registry-api.d.ts +13 -0
  2085. package/dist/src/registry-api.js +10 -0
  2086. package/dist/src/registry.d.ts +123 -0
  2087. package/dist/src/registry.js +125 -0
  2088. package/dist/src/registry.test.d.ts +4 -0
  2089. package/dist/src/registry.test.js +209 -0
  2090. package/dist/src/runtime-detect.d.ts +31 -0
  2091. package/dist/src/runtime-detect.js +47 -0
  2092. package/dist/src/runtime-detect.test.d.ts +1 -0
  2093. package/dist/src/runtime-detect.test.js +39 -0
  2094. package/dist/src/runtime.d.ts +47 -0
  2095. package/dist/src/runtime.js +66 -0
  2096. package/dist/src/scripts/framework.d.ts +7 -0
  2097. package/dist/src/scripts/framework.js +25 -0
  2098. package/dist/src/scripts/interact.d.ts +4 -0
  2099. package/dist/src/scripts/interact.js +20 -0
  2100. package/dist/src/scripts/store.d.ts +13 -0
  2101. package/dist/src/scripts/store.js +48 -0
  2102. package/dist/src/serialization.d.ts +42 -0
  2103. package/dist/src/serialization.js +102 -0
  2104. package/dist/src/serialization.test.d.ts +1 -0
  2105. package/dist/src/serialization.test.js +78 -0
  2106. package/dist/src/skills.d.ts +14 -0
  2107. package/dist/src/skills.js +127 -0
  2108. package/dist/src/skills.test.d.ts +1 -0
  2109. package/dist/src/skills.test.js +71 -0
  2110. package/dist/src/snapshotFormatter.d.ts +11 -0
  2111. package/dist/src/snapshotFormatter.js +338 -0
  2112. package/dist/src/snapshotFormatter.test.d.ts +7 -0
  2113. package/dist/src/snapshotFormatter.test.js +521 -0
  2114. package/dist/src/tui.d.ts +28 -0
  2115. package/dist/src/tui.js +179 -0
  2116. package/dist/src/tui.test.d.ts +1 -0
  2117. package/dist/src/tui.test.js +19 -0
  2118. package/dist/src/types.d.ts +236 -0
  2119. package/dist/src/types.js +7 -0
  2120. package/dist/src/update-check.d.ts +66 -0
  2121. package/dist/src/update-check.js +202 -0
  2122. package/dist/src/update-check.test.d.ts +1 -0
  2123. package/dist/src/update-check.test.js +116 -0
  2124. package/dist/src/utils.d.ts +57 -0
  2125. package/dist/src/utils.js +152 -0
  2126. package/dist/src/utils.test.d.ts +1 -0
  2127. package/dist/src/utils.test.js +155 -0
  2128. package/dist/src/validate.d.ts +21 -0
  2129. package/dist/src/validate.js +129 -0
  2130. package/dist/src/validate.test.d.ts +9 -0
  2131. package/dist/src/validate.test.js +90 -0
  2132. package/dist/src/verify.d.ts +26 -0
  2133. package/dist/src/verify.js +67 -0
  2134. package/dist/src/version.d.ts +4 -0
  2135. package/dist/src/version.js +22 -0
  2136. package/dist/src/weixin-download.test.d.ts +1 -0
  2137. package/dist/src/weixin-download.test.js +45 -0
  2138. package/package.json +109 -0
  2139. package/scripts/check-doc-coverage.sh +81 -0
  2140. package/scripts/check-listing-id-pairing.mjs +193 -0
  2141. package/scripts/check-silent-column-drop.mjs +105 -0
  2142. package/scripts/check-typed-error-lint.mjs +118 -0
  2143. package/scripts/clean-dist.cjs +13 -0
  2144. package/scripts/copy-yaml.cjs +12 -0
  2145. package/scripts/fetch-adapters.js +293 -0
  2146. package/scripts/postinstall.js +174 -0
  2147. package/scripts/silent-column-drop-baseline.json +910 -0
  2148. package/scripts/typed-error-lint-baseline.json +1162 -0
  2149. package/skills/opencli-adapter-author/SKILL.md +256 -0
  2150. package/skills/opencli-adapter-author/references/adapter-template.md +471 -0
  2151. package/skills/opencli-adapter-author/references/api-discovery.md +295 -0
  2152. package/skills/opencli-adapter-author/references/coverage-matrix.md +81 -0
  2153. package/skills/opencli-adapter-author/references/field-conventions.md +172 -0
  2154. package/skills/opencli-adapter-author/references/field-decode-playbook.md +181 -0
  2155. package/skills/opencli-adapter-author/references/jsdom-fixture-pattern.md +196 -0
  2156. package/skills/opencli-adapter-author/references/output-design.md +149 -0
  2157. package/skills/opencli-adapter-author/references/site-memory/bilibili.md +70 -0
  2158. package/skills/opencli-adapter-author/references/site-memory/eastmoney.md +70 -0
  2159. package/skills/opencli-adapter-author/references/site-memory/tonghuashun.md +51 -0
  2160. package/skills/opencli-adapter-author/references/site-memory/xueqiu.md +58 -0
  2161. package/skills/opencli-adapter-author/references/site-memory.md +231 -0
  2162. package/skills/opencli-adapter-author/references/site-recon.md +134 -0
  2163. package/skills/opencli-adapter-author/references/strategy-selection.md +169 -0
  2164. package/skills/opencli-adapter-author/references/success-rate-pitfalls.md +162 -0
  2165. package/skills/opencli-adapter-author/references/typed-errors.md +234 -0
  2166. package/skills/opencli-autofix/SKILL.md +297 -0
  2167. package/skills/opencli-browser/SKILL.md +444 -0
  2168. package/skills/opencli-browser-sitemap/SKILL.md +96 -0
  2169. package/skills/opencli-sitemap-author/SKILL.md +161 -0
  2170. package/skills/opencli-sitemap-author/references/sitemap-schema.md +660 -0
  2171. package/skills/opencli-usage/SKILL.md +169 -0
@@ -0,0 +1,3003 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import * as fs from 'node:fs';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ import yaml from 'js-yaml';
6
+ import { cli, getRegistry, Strategy } from './registry.js';
7
+ import { BrowserCommandError } from './browser/daemon-client.js';
8
+ import { TargetError } from './browser/target-errors.js';
9
+ import { PKG_VERSION } from './version.js';
10
+ import { classifyAdapter } from './help.js';
11
+ const { mockBrowserConnect, mockBrowserClose, mockBindTab, mockSendCommand, mockExecFileSync, browserState, } = vi.hoisted(() => ({
12
+ mockBrowserConnect: vi.fn(),
13
+ mockBrowserClose: vi.fn(),
14
+ mockBindTab: vi.fn(),
15
+ mockSendCommand: vi.fn(),
16
+ mockExecFileSync: vi.fn(),
17
+ browserState: { page: null },
18
+ }));
19
+ vi.mock('./browser/index.js', () => {
20
+ mockBrowserConnect.mockImplementation(async () => browserState.page);
21
+ return {
22
+ BrowserBridge: class {
23
+ connect = mockBrowserConnect;
24
+ close = mockBrowserClose;
25
+ },
26
+ };
27
+ });
28
+ vi.mock('./browser/daemon-client.js', async () => {
29
+ const actual = await vi.importActual('./browser/daemon-client.js');
30
+ return {
31
+ ...actual,
32
+ bindTab: mockBindTab,
33
+ sendCommand: mockSendCommand,
34
+ };
35
+ });
36
+ vi.mock('node:child_process', async () => {
37
+ const actual = await vi.importActual('node:child_process');
38
+ return {
39
+ ...actual,
40
+ execFileSync: mockExecFileSync,
41
+ };
42
+ });
43
+ import { createProgram, findPackageRoot, normalizeVerifyRows, renderVerifyPreview, resolveBrowserVerifyInvocation, resolveSitemapAvailabilityForUrl, selectFreshByTimestamp } from './cli.js';
44
+ describe('createProgram root help descriptions', () => {
45
+ function descriptionFor(program, name) {
46
+ return program.commands.find(cmd => cmd.name() === name)?.description();
47
+ }
48
+ it('summarizes built-in command groups with their subcommands', () => {
49
+ const program = createProgram('', '');
50
+ expect(descriptionFor(program, 'browser')).toContain('open');
51
+ expect(descriptionFor(program, 'browser')).toContain('type');
52
+ expect(descriptionFor(program, 'browser')).toContain('verify');
53
+ expect(descriptionFor(program, 'browser')).not.toContain('Browser control');
54
+ expect(descriptionFor(program, 'auth')).toBe('refresh, status');
55
+ expect(descriptionFor(program, 'plugin')).toBe('create, install, list, uninstall, update');
56
+ expect(descriptionFor(program, 'adapter')).toBe('eject, reset, status');
57
+ expect(descriptionFor(program, 'profile')).toBe('list, rename, use');
58
+ expect(descriptionFor(program, 'daemon')).toBe('restart, status, stop');
59
+ expect(descriptionFor(program, 'external')).toBe('install, list, register');
60
+ });
61
+ it('renders auth namespace structured help', () => {
62
+ const argv = process.argv;
63
+ try {
64
+ const program = createProgram('', '');
65
+ const auth = program.commands.find(cmd => cmd.name() === 'auth');
66
+ expect(auth).toBeTruthy();
67
+ process.argv = ['node', 'opencli', 'auth', '--help', '-f', 'yaml'];
68
+ const data = yaml.load(auth.helpInformation());
69
+ expect(data).toMatchObject({
70
+ namespace: 'auth',
71
+ description: 'Inspect website login status',
72
+ command_count: 2,
73
+ });
74
+ expect(data.commands.map((cmd) => cmd.name)).toEqual(['refresh', 'status']);
75
+ const status = auth.commands.find(cmd => cmd.name() === 'status');
76
+ process.argv = ['node', 'opencli', 'auth', 'status', '--help', '-f', 'yaml'];
77
+ const statusData = yaml.load(status.helpInformation());
78
+ expect(statusData.command).toBe('opencli auth status');
79
+ expect(statusData.command_options.map((option) => option.name)).toEqual(expect.arrayContaining([
80
+ 'site',
81
+ 'full',
82
+ 'concurrency',
83
+ 'timeout',
84
+ 'only',
85
+ 'format',
86
+ ]));
87
+ }
88
+ finally {
89
+ process.argv = argv;
90
+ }
91
+ });
92
+ it('keeps leaf command descriptions unchanged', () => {
93
+ const program = createProgram('', '');
94
+ expect(descriptionFor(program, 'list')).toBe('List all available CLI commands');
95
+ expect(descriptionFor(program, 'doctor')).toBe('Diagnose opencli browser bridge connectivity');
96
+ });
97
+ it('keeps site adapters out of root commands and lists sites in the root help tail', () => {
98
+ const registry = getRegistry();
99
+ const snapshot = new Map(registry);
100
+ registry.clear();
101
+ try {
102
+ cli({
103
+ site: 'bilibili',
104
+ name: 'hot',
105
+ access: 'read',
106
+ description: 'Bilibili hot videos',
107
+ strategy: Strategy.PUBLIC,
108
+ browser: false,
109
+ });
110
+ cli({
111
+ site: 'youtube',
112
+ name: 'search',
113
+ access: 'read',
114
+ description: 'Search YouTube',
115
+ strategy: Strategy.PUBLIC,
116
+ browser: false,
117
+ });
118
+ const program = createProgram('', '');
119
+ const help = program.helpInformation();
120
+ expect(help).toContain('Site adapters (2):');
121
+ expect(help).toContain('bilibili, youtube');
122
+ expect(help).toContain("opencli <site> --help -f yaml");
123
+ expect(help).not.toMatch(/\n bilibili\s+hot/);
124
+ expect(help).not.toMatch(/\n youtube\s+search/);
125
+ }
126
+ finally {
127
+ registry.clear();
128
+ for (const [key, value] of snapshot)
129
+ registry.set(key, value);
130
+ }
131
+ });
132
+ it('groups adapters into App / Site buckets by domain field', () => {
133
+ const registry = getRegistry();
134
+ const snapshot = new Map(registry);
135
+ registry.clear();
136
+ try {
137
+ cli({
138
+ site: 'bilibili',
139
+ name: 'hot',
140
+ access: 'read',
141
+ description: 'Bilibili hot videos',
142
+ domain: 'www.bilibili.com',
143
+ strategy: Strategy.PUBLIC,
144
+ browser: false,
145
+ });
146
+ cli({
147
+ site: 'chatwise',
148
+ name: 'ask',
149
+ access: 'write',
150
+ description: 'Ask Chatwise desktop app',
151
+ domain: 'localhost',
152
+ strategy: Strategy.UI,
153
+ browser: true,
154
+ });
155
+ const program = createProgram('', '');
156
+ const help = program.helpInformation();
157
+ // Two separate sections, each with own count
158
+ expect(help).toContain('App adapters (1):');
159
+ expect(help).toMatch(/App adapters \(1\):\n {2}chatwise/);
160
+ expect(help).toContain('Site adapters (1):');
161
+ expect(help).toMatch(/Site adapters \(1\):\n {2}bilibili/);
162
+ // App adapters appear before Site adapters (External CLIs are absent here)
163
+ expect(help.indexOf('App adapters')).toBeLessThan(help.indexOf('Site adapters'));
164
+ }
165
+ finally {
166
+ registry.clear();
167
+ for (const [key, value] of snapshot)
168
+ registry.set(key, value);
169
+ }
170
+ });
171
+ it('classifies local IP domains as app adapters', () => {
172
+ expect(classifyAdapter('localhost')).toBe('app');
173
+ expect(classifyAdapter('127.0.0.1')).toBe('app');
174
+ expect(classifyAdapter('::1')).toBe('app');
175
+ expect(classifyAdapter('www.bilibili.com')).toBe('site');
176
+ });
177
+ it('splits list table output into App and Site sections without changing per-site rows', async () => {
178
+ const registry = getRegistry();
179
+ const snapshot = new Map(registry);
180
+ const stdoutSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
181
+ const restoreStdoutSpy = () => stdoutSpy.mockImplementation(() => { });
182
+ registry.clear();
183
+ try {
184
+ cli({
185
+ site: 'antigravity',
186
+ name: 'history',
187
+ access: 'read',
188
+ description: 'Read Antigravity history',
189
+ domain: '127.0.0.1',
190
+ strategy: Strategy.UI,
191
+ browser: true,
192
+ });
193
+ cli({
194
+ site: 'chatwise',
195
+ name: 'ask',
196
+ access: 'write',
197
+ description: 'Ask Chatwise desktop app',
198
+ domain: 'localhost',
199
+ strategy: Strategy.UI,
200
+ browser: true,
201
+ });
202
+ cli({
203
+ site: 'bilibili',
204
+ name: 'hot',
205
+ access: 'read',
206
+ description: 'Bilibili hot videos',
207
+ domain: 'www.bilibili.com',
208
+ strategy: Strategy.PUBLIC,
209
+ browser: false,
210
+ });
211
+ const program = createProgram('', '');
212
+ await program.parseAsync(['node', 'opencli', 'list']);
213
+ const output = stdoutSpy.mock.calls.flat().join('\n');
214
+ expect(output).toContain('App adapters');
215
+ expect(output).toContain('Site adapters');
216
+ expect(output.indexOf('App adapters')).toBeLessThan(output.indexOf('Site adapters'));
217
+ expect(output).toMatch(/App adapters[\s\S]*antigravity[\s\S]*history \[ui\] — Read Antigravity history/);
218
+ expect(output).toMatch(/App adapters[\s\S]*chatwise[\s\S]*ask \[ui\] — Ask Chatwise desktop app/);
219
+ expect(output).toMatch(/Site adapters[\s\S]*bilibili[\s\S]*hot \[public\] — Bilibili hot videos/);
220
+ expect(output).toContain('3 built-in commands across 2 apps + 1 sites,');
221
+ }
222
+ finally {
223
+ restoreStdoutSpy();
224
+ stdoutSpy.mockClear();
225
+ registry.clear();
226
+ for (const [key, value] of snapshot)
227
+ registry.set(key, value);
228
+ }
229
+ });
230
+ it('omits empty list table sections and leaves structured list rows unchanged', async () => {
231
+ const registry = getRegistry();
232
+ const snapshot = new Map(registry);
233
+ const stdoutSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
234
+ const restoreStdoutSpy = () => stdoutSpy.mockImplementation(() => { });
235
+ registry.clear();
236
+ try {
237
+ cli({
238
+ site: 'bilibili',
239
+ name: 'hot',
240
+ access: 'read',
241
+ description: 'Bilibili hot videos',
242
+ domain: 'www.bilibili.com',
243
+ strategy: Strategy.PUBLIC,
244
+ browser: false,
245
+ columns: ['title', 'url'],
246
+ });
247
+ const tableProgram = createProgram('', '');
248
+ await tableProgram.parseAsync(['node', 'opencli', 'list']);
249
+ const tableOutput = stdoutSpy.mock.calls.flat().join('\n');
250
+ expect(tableOutput).not.toContain('App adapters');
251
+ expect(tableOutput).toContain('Site adapters');
252
+ expect(tableOutput).toContain('1 built-in commands across 0 apps + 1 sites,');
253
+ stdoutSpy.mockClear();
254
+ const jsonProgram = createProgram('', '');
255
+ await jsonProgram.parseAsync(['node', 'opencli', 'list', '-f', 'json']);
256
+ const jsonOutput = stdoutSpy.mock.calls.flat().join('\n');
257
+ const rows = JSON.parse(jsonOutput);
258
+ expect(rows).toMatchObject([
259
+ {
260
+ site: 'bilibili',
261
+ name: 'hot',
262
+ domain: 'www.bilibili.com',
263
+ columns: ['title', 'url'],
264
+ },
265
+ ]);
266
+ expect(rows[0]).not.toHaveProperty('adapterKind');
267
+ expect(rows[0]).not.toHaveProperty('section');
268
+ }
269
+ finally {
270
+ restoreStdoutSpy();
271
+ stdoutSpy.mockClear();
272
+ registry.clear();
273
+ for (const [key, value] of snapshot)
274
+ registry.set(key, value);
275
+ }
276
+ });
277
+ it('exposes external_clis / app_adapters / site_adapters in structured help', () => {
278
+ const registry = getRegistry();
279
+ const snapshot = new Map(registry);
280
+ const argv = process.argv;
281
+ registry.clear();
282
+ try {
283
+ cli({
284
+ site: 'bilibili',
285
+ name: 'hot',
286
+ access: 'read',
287
+ description: 'Bilibili hot videos',
288
+ domain: 'www.bilibili.com',
289
+ strategy: Strategy.PUBLIC,
290
+ browser: false,
291
+ });
292
+ cli({
293
+ site: 'chatwise',
294
+ name: 'ask',
295
+ access: 'write',
296
+ description: 'Ask Chatwise desktop app',
297
+ domain: 'localhost',
298
+ strategy: Strategy.UI,
299
+ browser: true,
300
+ });
301
+ const program = createProgram('', '');
302
+ process.argv = ['node', 'opencli', '--help', '-f', 'yaml'];
303
+ const data = yaml.load(program.helpInformation());
304
+ expect(data.app_adapters.count).toBe(1);
305
+ expect(data.app_adapters.apps).toEqual(['chatwise']);
306
+ expect(data.site_adapters.count).toBe(1);
307
+ expect(data.site_adapters.sites).toEqual(['bilibili']);
308
+ expect(data.external_clis.count).toBeGreaterThanOrEqual(0);
309
+ expect(Array.isArray(data.external_clis.clis)).toBe(true);
310
+ expect(Array.isArray(data.external_clis.display)).toBe(true);
311
+ // Adapters must NOT leak into the core commands list
312
+ const commandNames = data.commands.map((cmd) => cmd.name);
313
+ expect(commandNames).not.toContain('bilibili');
314
+ expect(commandNames).not.toContain('chatwise');
315
+ }
316
+ finally {
317
+ process.argv = argv;
318
+ registry.clear();
319
+ for (const [key, value] of snapshot)
320
+ registry.set(key, value);
321
+ }
322
+ });
323
+ it('renders root structured help with built-ins and site adapter names', () => {
324
+ const registry = getRegistry();
325
+ const snapshot = new Map(registry);
326
+ const argv = process.argv;
327
+ registry.clear();
328
+ try {
329
+ cli({
330
+ site: 'bilibili',
331
+ name: 'hot',
332
+ access: 'read',
333
+ description: 'Bilibili hot videos',
334
+ strategy: Strategy.PUBLIC,
335
+ browser: false,
336
+ });
337
+ const program = createProgram('', '');
338
+ process.argv = ['node', 'opencli', '--help', '-f', 'yaml'];
339
+ const data = yaml.load(program.helpInformation());
340
+ expect(data.site_adapters.count).toBe(1);
341
+ expect(data.site_adapters.sites).toEqual(['bilibili']);
342
+ expect(data.commands.map((cmd) => cmd.name)).toContain('list');
343
+ expect(data.commands.map((cmd) => cmd.name)).not.toContain('bilibili');
344
+ }
345
+ finally {
346
+ process.argv = argv;
347
+ registry.clear();
348
+ for (const [key, value] of snapshot)
349
+ registry.set(key, value);
350
+ }
351
+ });
352
+ it('renders per-site structured help with all commands, access, args, and examples', () => {
353
+ const registry = getRegistry();
354
+ const snapshot = new Map(registry);
355
+ const argv = process.argv;
356
+ registry.clear();
357
+ try {
358
+ cli({
359
+ site: 'bilibili',
360
+ name: 'hot',
361
+ access: 'read',
362
+ description: 'Bilibili hot videos',
363
+ strategy: Strategy.PUBLIC,
364
+ browser: false,
365
+ args: [{ name: 'limit', type: 'int', default: 20, help: 'Number of videos' }],
366
+ columns: ['title', 'url'],
367
+ });
368
+ const program = createProgram('', '');
369
+ const site = program.commands.find(cmd => cmd.name() === 'bilibili');
370
+ expect(site).toBeTruthy();
371
+ process.argv = ['node', 'opencli', 'bilibili', '--help', '-f', 'yaml'];
372
+ const data = yaml.load(site.helpInformation());
373
+ expect(data.site).toBe('bilibili');
374
+ expect(data.commands).toMatchObject([
375
+ {
376
+ name: 'hot',
377
+ access: 'read',
378
+ description: 'Bilibili hot videos',
379
+ browser: false,
380
+ example: 'opencli bilibili hot -f yaml',
381
+ command_options: [{ name: 'limit', type: 'int', default: 20 }],
382
+ columns: ['title', 'url'],
383
+ },
384
+ ]);
385
+ expect(data.commands[0]).not.toHaveProperty('args');
386
+ }
387
+ finally {
388
+ process.argv = argv;
389
+ registry.clear();
390
+ for (const [key, value] of snapshot)
391
+ registry.set(key, value);
392
+ }
393
+ });
394
+ it('renders per-site text help without per-command common option noise', () => {
395
+ const registry = getRegistry();
396
+ const snapshot = new Map(registry);
397
+ registry.clear();
398
+ try {
399
+ cli({
400
+ site: 'bilibili',
401
+ name: 'hot',
402
+ access: 'read',
403
+ description: 'Bilibili hot videos',
404
+ strategy: Strategy.PUBLIC,
405
+ browser: false,
406
+ args: [{ name: 'limit', type: 'int', default: 20, help: 'Number of videos' }],
407
+ });
408
+ cli({
409
+ site: 'bilibili',
410
+ name: 'video',
411
+ access: 'read',
412
+ description: 'Read one video',
413
+ domain: 'www.bilibili.com',
414
+ strategy: Strategy.PUBLIC,
415
+ browser: true,
416
+ args: [{ name: 'bvid', positional: true, required: true, help: 'Video id' }],
417
+ });
418
+ const program = createProgram('', '');
419
+ const site = program.commands.find(cmd => cmd.name() === 'bilibili');
420
+ expect(site).toBeTruthy();
421
+ const help = site.helpInformation();
422
+ expect(help).toContain('hot [options] [read] Bilibili hot videos');
423
+ expect(help).toContain('video <bvid> [read] Read one video');
424
+ expect(help).toContain('hot [options]');
425
+ expect(help).not.toContain('video <bvid> [options]');
426
+ expect(help).not.toContain('\nOptions:');
427
+ expect(help).toContain('Common options:');
428
+ expect(help).toContain('-f, --format <fmt>');
429
+ expect(help).toContain('--trace <mode>');
430
+ expect(help).toContain('get all command args/options in one structured response');
431
+ }
432
+ finally {
433
+ registry.clear();
434
+ for (const [key, value] of snapshot)
435
+ registry.set(key, value);
436
+ }
437
+ });
438
+ it('separates command args from common options in structured help', () => {
439
+ const registry = getRegistry();
440
+ const snapshot = new Map(registry);
441
+ const argv = process.argv;
442
+ registry.clear();
443
+ try {
444
+ cli({
445
+ site: 'bilibili',
446
+ name: 'video',
447
+ access: 'read',
448
+ description: 'Read one video',
449
+ strategy: Strategy.PUBLIC,
450
+ domain: 'www.bilibili.com',
451
+ browser: true,
452
+ args: [
453
+ { name: 'bvid', positional: true, required: true, help: 'Video id' },
454
+ { name: 'with-comments', type: 'boolean', default: false, help: 'Include comments' },
455
+ ],
456
+ columns: ['title', 'url'],
457
+ });
458
+ const program = createProgram('', '');
459
+ const site = program.commands.find(cmd => cmd.name() === 'bilibili');
460
+ const command = site.commands.find(cmd => cmd.name() === 'video');
461
+ expect(command).toBeTruthy();
462
+ process.argv = ['node', 'opencli', 'bilibili', 'video', '--help', '-f', 'yaml'];
463
+ const data = yaml.load(command.helpInformation());
464
+ expect(data.usage).toBe('opencli bilibili video <bvid> [options]');
465
+ expect(data.browser).toBe(true);
466
+ expect(data.domain).toBe('www.bilibili.com');
467
+ expect(data.positionals).toMatchObject([{ name: 'bvid', positional: true, required: true }]);
468
+ expect(data.command_options).toMatchObject([{ name: 'with-comments', default: false }]);
469
+ expect(data.common_options.map((option) => option.name)).toEqual(['format', 'trace', 'verbose', 'help']);
470
+ expect(data.columns).toEqual(['title', 'url']);
471
+ expect(data).not.toHaveProperty('args');
472
+ }
473
+ finally {
474
+ process.argv = argv;
475
+ registry.clear();
476
+ for (const [key, value] of snapshot)
477
+ registry.set(key, value);
478
+ }
479
+ });
480
+ it('renders browser namespace structured help from Commander metadata', () => {
481
+ const argv = process.argv;
482
+ try {
483
+ const program = createProgram('', '');
484
+ const browser = program.commands.find(cmd => cmd.name() === 'browser');
485
+ expect(browser).toBeTruthy();
486
+ process.argv = ['node', 'opencli', 'browser', '--session', 'test', '--help', '-f', 'yaml'];
487
+ const data = yaml.load(browser.helpInformation());
488
+ expect(data.namespace).toBe('browser');
489
+ expect(data.command).toBe('opencli browser');
490
+ expect(data.description).toBe('Browser control — navigate, click, type, extract, wait (no LLM needed)');
491
+ expect(data.command_count).toBeGreaterThan(20);
492
+ // `--session` is now a hidden internal option; user-facing surface is the
493
+ // <session> positional declared via `.usage()`. Structured help drops
494
+ // hidden options, so namespace_options shouldn't expose it.
495
+ expect(data.namespace_options).not.toEqual(expect.arrayContaining([
496
+ expect.objectContaining({ name: 'session' }),
497
+ ]));
498
+ expect(data.namespace_options).toEqual(expect.arrayContaining([
499
+ expect.objectContaining({
500
+ name: 'window',
501
+ flags: '--window <mode>',
502
+ takes_value: 'required',
503
+ }),
504
+ ]));
505
+ expect(data.usage).toBe('opencli browser <session> <command> [options]');
506
+ expect(data.global_options).toEqual(expect.arrayContaining([
507
+ expect.objectContaining({
508
+ name: 'version',
509
+ flags: '-V, --version',
510
+ }),
511
+ expect.objectContaining({
512
+ name: 'profile',
513
+ flags: '--profile <name>',
514
+ takes_value: 'required',
515
+ }),
516
+ ]));
517
+ const click = data.commands.find((cmd) => cmd.name === 'click');
518
+ // Structured help command/usage paths include the <session> positional so
519
+ // agents construct the correct full invocation. `name` is the leaf
520
+ // identifier (placeholder positionals are stripped).
521
+ expect(click).toMatchObject({
522
+ command: 'opencli browser <session> click',
523
+ usage: 'opencli browser <session> click [target] [options]',
524
+ positionals: [{ name: 'target' }],
525
+ });
526
+ expect(click.command_options.map((option) => option.name)).toEqual(['role', 'name', 'label', 'text', 'testid', 'nth', 'tab']);
527
+ const tabList = data.commands.find((cmd) => cmd.name === 'tab list');
528
+ expect(tabList).toMatchObject({
529
+ command: 'opencli browser <session> tab list',
530
+ usage: 'opencli browser <session> tab list [options]',
531
+ command_options: [],
532
+ });
533
+ const getText = data.commands.find((cmd) => cmd.name === 'get text');
534
+ expect(getText).toMatchObject({
535
+ command: 'opencli browser <session> get text',
536
+ positionals: [{ name: 'target' }],
537
+ });
538
+ expect(data.structured_help).toMatchObject({
539
+ formats: ['yaml', 'json'],
540
+ usage: 'opencli browser --help -f yaml',
541
+ });
542
+ }
543
+ finally {
544
+ process.argv = argv;
545
+ }
546
+ });
547
+ it('renders nested browser parent structured help for a subtree', () => {
548
+ const argv = process.argv;
549
+ try {
550
+ const program = createProgram('', '');
551
+ const browser = program.commands.find(cmd => cmd.name() === 'browser');
552
+ const tab = browser.commands.find(cmd => cmd.name() === 'tab');
553
+ expect(tab).toBeTruthy();
554
+ process.argv = ['node', 'opencli', 'browser', '--session', 'test', 'tab', '--help', '-f', 'yaml'];
555
+ const data = yaml.load(tab.helpInformation());
556
+ expect(data).toMatchObject({
557
+ namespace: 'browser',
558
+ group: 'tab',
559
+ command: 'opencli browser <session> tab',
560
+ usage: 'opencli browser <session> tab <command> [args] [options]',
561
+ command_count: 4,
562
+ });
563
+ expect(data.commands.map((cmd) => cmd.name)).toEqual([
564
+ 'tab close',
565
+ 'tab list',
566
+ 'tab new',
567
+ 'tab select',
568
+ ]);
569
+ expect(data.commands.find((cmd) => cmd.name === 'tab close')).toMatchObject({
570
+ command: 'opencli browser <session> tab close',
571
+ usage: 'opencli browser <session> tab close [targetId] [options]',
572
+ positionals: [{ name: 'targetId', help: 'Target tab/page identity returned by "browser open", "browser tab new", or "browser tab list"' }],
573
+ });
574
+ // session is now a hidden internal option (consumed from the <session> positional).
575
+ // namespace_options should only list user-facing options.
576
+ expect(data.namespace_options.map((option) => option.name)).toEqual(['window']);
577
+ expect(data.structured_help).toMatchObject({
578
+ usage: 'opencli browser <session> tab --help -f yaml',
579
+ });
580
+ }
581
+ finally {
582
+ process.argv = argv;
583
+ }
584
+ });
585
+ it('renders browser command structured help without needing the full namespace dump', () => {
586
+ const argv = process.argv;
587
+ try {
588
+ const program = createProgram('', '');
589
+ const browser = program.commands.find(cmd => cmd.name() === 'browser');
590
+ const click = browser.commands.find(cmd => cmd.name() === 'click');
591
+ expect(click).toBeTruthy();
592
+ process.argv = ['node', 'opencli', 'browser', '--session', 'test', 'click', '--help', '-f', 'yaml'];
593
+ const data = yaml.load(click.helpInformation());
594
+ expect(data).toMatchObject({
595
+ namespace: 'browser',
596
+ name: 'click',
597
+ command: 'opencli browser <session> click',
598
+ usage: 'opencli browser <session> click [target] [options]',
599
+ positionals: [{ name: 'target' }],
600
+ structured_help: {
601
+ usage: 'opencli browser <session> click --help -f yaml',
602
+ },
603
+ });
604
+ expect(data.command_options.map((option) => option.name)).toEqual(['role', 'name', 'label', 'text', 'testid', 'nth', 'tab']);
605
+ // session is hidden; only `window` surfaces as a namespace option.
606
+ expect(data.namespace_options.map((option) => option.name)).toEqual(['window']);
607
+ expect(data.global_options.map((option) => option.name)).toContain('profile');
608
+ }
609
+ finally {
610
+ process.argv = argv;
611
+ }
612
+ });
613
+ it('renders daemon namespace structured help with leaves and global options', () => {
614
+ const argv = process.argv;
615
+ try {
616
+ const program = createProgram('', '');
617
+ const daemon = program.commands.find(cmd => cmd.name() === 'daemon');
618
+ expect(daemon).toBeTruthy();
619
+ process.argv = ['node', 'opencli', 'daemon', '--help', '-f', 'yaml'];
620
+ const data = yaml.load(daemon.helpInformation());
621
+ expect(data).toMatchObject({
622
+ namespace: 'daemon',
623
+ command: 'opencli daemon',
624
+ usage: 'opencli daemon <command> [args] [options]',
625
+ description: 'Manage the opencli daemon',
626
+ command_count: 3,
627
+ namespace_options: [],
628
+ structured_help: { usage: 'opencli daemon --help -f yaml' },
629
+ });
630
+ expect(data.commands.map((cmd) => cmd.name)).toEqual(['restart', 'status', 'stop']);
631
+ expect(data.global_options.map((option) => option.name)).toEqual(expect.arrayContaining(['version', 'profile']));
632
+ }
633
+ finally {
634
+ process.argv = argv;
635
+ }
636
+ });
637
+ it('renders plugin namespace structured help with positional + option leaves', () => {
638
+ const argv = process.argv;
639
+ try {
640
+ const program = createProgram('', '');
641
+ const plugin = program.commands.find(cmd => cmd.name() === 'plugin');
642
+ expect(plugin).toBeTruthy();
643
+ process.argv = ['node', 'opencli', 'plugin', '--help', '-f', 'yaml'];
644
+ const data = yaml.load(plugin.helpInformation());
645
+ expect(data).toMatchObject({
646
+ namespace: 'plugin',
647
+ command: 'opencli plugin',
648
+ description: 'Manage opencli plugins',
649
+ namespace_options: [],
650
+ });
651
+ expect(data.commands.map((cmd) => cmd.name)).toEqual(['create', 'install', 'list', 'uninstall', 'update']);
652
+ const update = data.commands.find((cmd) => cmd.name === 'update');
653
+ expect(update).toMatchObject({
654
+ usage: 'opencli plugin update [name] [options]',
655
+ positionals: [{ name: 'name' }],
656
+ });
657
+ expect(update.command_options.map((option) => option.name)).toEqual(['all']);
658
+ }
659
+ finally {
660
+ process.argv = argv;
661
+ }
662
+ });
663
+ it('renders adapter namespace structured help preserving original description after applyRootSubcommandSummaries', () => {
664
+ const argv = process.argv;
665
+ try {
666
+ const program = createProgram('', '');
667
+ const adapter = program.commands.find(cmd => cmd.name() === 'adapter');
668
+ expect(adapter).toBeTruthy();
669
+ process.argv = ['node', 'opencli', 'adapter', '--help', '-f', 'yaml'];
670
+ const data = yaml.load(adapter.helpInformation());
671
+ // applyRootSubcommandSummaries() rewrites .description() to a child-name listing;
672
+ // structured help must surface the original product description via the snapshot.
673
+ expect(data.description).toBe('Manage CLI adapters');
674
+ expect(data.commands.map((cmd) => cmd.name)).toEqual(['eject', 'reset', 'status']);
675
+ const reset = data.commands.find((cmd) => cmd.name === 'reset');
676
+ expect(reset).toMatchObject({
677
+ usage: 'opencli adapter reset [site] [options]',
678
+ positionals: [{ name: 'site' }],
679
+ });
680
+ expect(reset.command_options.map((option) => option.name)).toEqual(['all']);
681
+ }
682
+ finally {
683
+ process.argv = argv;
684
+ }
685
+ });
686
+ it('renders profile namespace structured help including required positionals', () => {
687
+ const argv = process.argv;
688
+ try {
689
+ const program = createProgram('', '');
690
+ const profile = program.commands.find(cmd => cmd.name() === 'profile');
691
+ expect(profile).toBeTruthy();
692
+ process.argv = ['node', 'opencli', 'profile', '--help', '-f', 'yaml'];
693
+ const data = yaml.load(profile.helpInformation());
694
+ expect(data).toMatchObject({
695
+ namespace: 'profile',
696
+ description: 'Manage Browser Bridge Chrome profiles',
697
+ command_count: 3,
698
+ });
699
+ expect(data.commands.map((cmd) => cmd.name)).toEqual(['list', 'rename', 'use']);
700
+ const rename = data.commands.find((cmd) => cmd.name === 'rename');
701
+ expect(rename).toMatchObject({
702
+ usage: 'opencli profile rename <contextId> <alias> [options]',
703
+ positionals: [
704
+ { name: 'contextId', required: true },
705
+ { name: 'alias', required: true },
706
+ ],
707
+ });
708
+ }
709
+ finally {
710
+ process.argv = argv;
711
+ }
712
+ });
713
+ });
714
+ describe('resolveBrowserVerifyInvocation', () => {
715
+ it('prefers the built entry declared in package metadata', () => {
716
+ const projectRoot = path.join('repo-root');
717
+ const exists = new Set([
718
+ path.join(projectRoot, 'dist', 'src', 'main.js'),
719
+ ]);
720
+ expect(resolveBrowserVerifyInvocation({
721
+ projectRoot,
722
+ readFile: () => JSON.stringify({ bin: { opencli: 'dist/src/main.js' } }),
723
+ fileExists: (candidate) => exists.has(candidate),
724
+ })).toEqual({
725
+ binary: process.execPath,
726
+ args: [path.join(projectRoot, 'dist', 'src', 'main.js')],
727
+ cwd: projectRoot,
728
+ });
729
+ });
730
+ it('falls back to compatibility built-entry candidates when package metadata is unavailable', () => {
731
+ const projectRoot = path.join('repo-root');
732
+ const exists = new Set([
733
+ path.join(projectRoot, 'dist', 'src', 'main.js'),
734
+ ]);
735
+ expect(resolveBrowserVerifyInvocation({
736
+ projectRoot,
737
+ readFile: () => { throw new Error('no package json'); },
738
+ fileExists: (candidate) => exists.has(candidate),
739
+ })).toEqual({
740
+ binary: process.execPath,
741
+ args: [path.join(projectRoot, 'dist', 'src', 'main.js')],
742
+ cwd: projectRoot,
743
+ });
744
+ });
745
+ it('falls back to the local tsx binary in source checkouts on Windows', () => {
746
+ const projectRoot = path.join('repo-root');
747
+ const exists = new Set([
748
+ path.join(projectRoot, 'src', 'main.ts'),
749
+ path.join(projectRoot, 'node_modules', '.bin', 'tsx.cmd'),
750
+ ]);
751
+ expect(resolveBrowserVerifyInvocation({
752
+ projectRoot,
753
+ platform: 'win32',
754
+ fileExists: (candidate) => exists.has(candidate),
755
+ })).toEqual({
756
+ binary: path.join(projectRoot, 'node_modules', '.bin', 'tsx.cmd'),
757
+ args: [path.join(projectRoot, 'src', 'main.ts')],
758
+ cwd: projectRoot,
759
+ shell: true,
760
+ });
761
+ });
762
+ it('falls back to npx tsx when local tsx is unavailable', () => {
763
+ const projectRoot = path.join('repo-root');
764
+ const exists = new Set([
765
+ path.join(projectRoot, 'src', 'main.ts'),
766
+ ]);
767
+ expect(resolveBrowserVerifyInvocation({
768
+ projectRoot,
769
+ platform: 'linux',
770
+ fileExists: (candidate) => exists.has(candidate),
771
+ })).toEqual({
772
+ binary: 'npx',
773
+ args: ['tsx', path.join(projectRoot, 'src', 'main.ts')],
774
+ cwd: projectRoot,
775
+ });
776
+ });
777
+ });
778
+ describe('selectFreshByTimestamp', () => {
779
+ it('uses timestamp watermarks so rolled buffers still emit new messages', () => {
780
+ const first = selectFreshByTimestamp([
781
+ { timestamp: 1, text: 'a' },
782
+ { timestamp: 2, text: 'b' },
783
+ ], 0);
784
+ expect(first.fresh.map((item) => item.text)).toEqual(['a', 'b']);
785
+ expect(first.lastSeenTs).toBe(2);
786
+ const rolled = selectFreshByTimestamp([
787
+ { timestamp: 2, text: 'b' },
788
+ { timestamp: 3, text: 'c' },
789
+ ], first.lastSeenTs);
790
+ expect(rolled.fresh.map((item) => item.text)).toEqual(['c']);
791
+ expect(rolled.lastSeenTs).toBe(3);
792
+ });
793
+ });
794
+ describe('resolveSitemapAvailabilityForUrl', () => {
795
+ function registryFor(site, domain) {
796
+ return new Map([[`${site}:read`, {
797
+ site,
798
+ name: 'read',
799
+ access: 'read',
800
+ description: 'read',
801
+ domain,
802
+ browser: false,
803
+ args: [],
804
+ }]]);
805
+ }
806
+ it('detects local sitemap overlays using adapter registry domain matches', () => {
807
+ const homeDir = path.join(os.tmpdir(), 'opencli-sitemap-home');
808
+ const packageRoot = path.join(os.tmpdir(), 'opencli-sitemap-package');
809
+ const localSitemap = path.join(homeDir, '.opencli', 'sites', 'hackernews', 'sitemap');
810
+ const exists = new Set([localSitemap]);
811
+ const report = resolveSitemapAvailabilityForUrl('https://news.ycombinator.com/item?id=1', {
812
+ homeDir,
813
+ packageRoot,
814
+ registry: registryFor('hackernews', 'news.ycombinator.com'),
815
+ fileExists: (candidate) => exists.has(candidate),
816
+ });
817
+ expect(report).toMatchObject({
818
+ site: 'hackernews',
819
+ available: true,
820
+ source: 'local',
821
+ paths: { local: localSitemap },
822
+ });
823
+ expect(report?.hint).toContain('opencli-browser-sitemap');
824
+ });
825
+ it('reports global+local when both sitemap layers exist', () => {
826
+ const homeDir = path.join(os.tmpdir(), 'opencli-sitemap-home');
827
+ const packageRoot = path.join(os.tmpdir(), 'opencli-sitemap-package');
828
+ const localSitemap = path.join(homeDir, '.opencli', 'sites', 'twitter', 'sitemap.md');
829
+ const globalSitemap = path.join(packageRoot, 'sitemaps', 'twitter');
830
+ const exists = new Set([localSitemap, globalSitemap]);
831
+ const report = resolveSitemapAvailabilityForUrl('https://x.com/opencli', {
832
+ homeDir,
833
+ packageRoot,
834
+ registry: registryFor('twitter', 'x.com'),
835
+ fileExists: (candidate) => exists.has(candidate),
836
+ });
837
+ expect(report).toMatchObject({
838
+ site: 'twitter',
839
+ source: 'local+global',
840
+ paths: { local: localSitemap, global: globalSitemap },
841
+ });
842
+ });
843
+ it('returns null when no sitemap layer exists', () => {
844
+ const report = resolveSitemapAvailabilityForUrl('https://example.com/', {
845
+ homeDir: path.join(os.tmpdir(), 'opencli-sitemap-home'),
846
+ packageRoot: path.join(os.tmpdir(), 'opencli-sitemap-package'),
847
+ registry: new Map(),
848
+ fileExists: () => false,
849
+ });
850
+ expect(report).toBeNull();
851
+ });
852
+ });
853
+ describe('browser verify', () => {
854
+ beforeEach(() => {
855
+ process.exitCode = undefined;
856
+ mockExecFileSync.mockReset().mockReturnValue('[]');
857
+ });
858
+ it('passes --trace through to the adapter subprocess', async () => {
859
+ const originalHome = process.env.HOME;
860
+ const originalUserProfile = process.env.USERPROFILE;
861
+ const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-browser-verify-trace-'));
862
+ process.env.HOME = fakeHome;
863
+ process.env.USERPROFILE = fakeHome;
864
+ try {
865
+ const adapterDir = path.join(fakeHome, '.opencli', 'clis', 'hn');
866
+ fs.mkdirSync(adapterDir, { recursive: true });
867
+ fs.writeFileSync(path.join(adapterDir, 'top.js'), 'export default {};\n', 'utf-8');
868
+ const program = createProgram('', '');
869
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'verify', 'hn/top', '--no-fixture', '--trace', 'retain-on-failure']);
870
+ expect(mockExecFileSync).toHaveBeenCalledTimes(1);
871
+ const [, execArgs] = mockExecFileSync.mock.calls[0];
872
+ expect(execArgs.slice(-6)).toEqual(['hn', 'top', '--trace', 'retain-on-failure', '--format', 'json']);
873
+ }
874
+ finally {
875
+ if (originalHome === undefined)
876
+ delete process.env.HOME;
877
+ else
878
+ process.env.HOME = originalHome;
879
+ if (originalUserProfile === undefined)
880
+ delete process.env.USERPROFILE;
881
+ else
882
+ process.env.USERPROFILE = originalUserProfile;
883
+ fs.rmSync(fakeHome, { recursive: true, force: true });
884
+ }
885
+ });
886
+ it('uses --seed-args when no fixture args exist', async () => {
887
+ const originalHome = process.env.HOME;
888
+ const originalUserProfile = process.env.USERPROFILE;
889
+ const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-browser-verify-seed-'));
890
+ process.env.HOME = fakeHome;
891
+ process.env.USERPROFILE = fakeHome;
892
+ try {
893
+ const adapterDir = path.join(fakeHome, '.opencli', 'clis', 'hn');
894
+ fs.mkdirSync(adapterDir, { recursive: true });
895
+ fs.writeFileSync(path.join(adapterDir, 'top.js'), 'export default {};\n', 'utf-8');
896
+ const program = createProgram('', '');
897
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'verify', 'hn/top', '--no-fixture', '--seed-args', 'opencli-verify']);
898
+ expect(mockExecFileSync).toHaveBeenCalledTimes(1);
899
+ const [, execArgs] = mockExecFileSync.mock.calls[0];
900
+ expect(execArgs.slice(-5)).toEqual(['hn', 'top', 'opencli-verify', '--format', 'json']);
901
+ }
902
+ finally {
903
+ if (originalHome === undefined)
904
+ delete process.env.HOME;
905
+ else
906
+ process.env.HOME = originalHome;
907
+ if (originalUserProfile === undefined)
908
+ delete process.env.USERPROFILE;
909
+ else
910
+ process.env.USERPROFILE = originalUserProfile;
911
+ fs.rmSync(fakeHome, { recursive: true, force: true });
912
+ }
913
+ });
914
+ it('writes --seed-args into a starter fixture', async () => {
915
+ const originalHome = process.env.HOME;
916
+ const originalUserProfile = process.env.USERPROFILE;
917
+ const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-browser-verify-write-seed-'));
918
+ process.env.HOME = fakeHome;
919
+ process.env.USERPROFILE = fakeHome;
920
+ mockExecFileSync.mockReturnValue(JSON.stringify([{ title: 'ok' }]));
921
+ try {
922
+ const adapterDir = path.join(fakeHome, '.opencli', 'clis', 'hn');
923
+ fs.mkdirSync(adapterDir, { recursive: true });
924
+ fs.writeFileSync(path.join(adapterDir, 'top.js'), 'export default {};\n', 'utf-8');
925
+ const program = createProgram('', '');
926
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'verify', 'hn/top', '--write-fixture', '--seed-args', 'opencli-verify']);
927
+ const fixtureFile = path.join(fakeHome, '.opencli', 'sites', 'hn', 'verify', 'top.json');
928
+ const fixture = JSON.parse(fs.readFileSync(fixtureFile, 'utf-8'));
929
+ expect(fixture.args).toEqual(['opencli-verify']);
930
+ expect(fixture.expect.columns).toEqual(['title']);
931
+ }
932
+ finally {
933
+ if (originalHome === undefined)
934
+ delete process.env.HOME;
935
+ else
936
+ process.env.HOME = originalHome;
937
+ if (originalUserProfile === undefined)
938
+ delete process.env.USERPROFILE;
939
+ else
940
+ process.env.USERPROFILE = originalUserProfile;
941
+ fs.rmSync(fakeHome, { recursive: true, force: true });
942
+ }
943
+ });
944
+ it('fails before fixture handling when output row shape is not agent-native', async () => {
945
+ const originalHome = process.env.HOME;
946
+ const originalUserProfile = process.env.USERPROFILE;
947
+ const fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-browser-verify-shape-'));
948
+ process.env.HOME = fakeHome;
949
+ process.env.USERPROFILE = fakeHome;
950
+ mockExecFileSync.mockReturnValue(JSON.stringify([{ title: 'ok', author: { user_id: 'u1' } }]));
951
+ const consoleLogSpy = vi.mocked(console.log);
952
+ consoleLogSpy.mockClear();
953
+ try {
954
+ const adapterDir = path.join(fakeHome, '.opencli', 'clis', 'hn');
955
+ fs.mkdirSync(adapterDir, { recursive: true });
956
+ fs.writeFileSync(path.join(adapterDir, 'top.js'), 'export default {};\n', 'utf-8');
957
+ const program = createProgram('', '');
958
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'verify', 'hn/top', '--no-fixture']);
959
+ expect(process.exitCode).toBe(1);
960
+ const output = consoleLogSpy.mock.calls.map((args) => args.join(' ')).join('\n');
961
+ expect(output).toContain('Adapter output violates row shape conventions');
962
+ expect(output).toContain('author.user_id');
963
+ }
964
+ finally {
965
+ consoleLogSpy.mockClear();
966
+ if (originalHome === undefined)
967
+ delete process.env.HOME;
968
+ else
969
+ process.env.HOME = originalHome;
970
+ if (originalUserProfile === undefined)
971
+ delete process.env.USERPROFILE;
972
+ else
973
+ process.env.USERPROFILE = originalUserProfile;
974
+ fs.rmSync(fakeHome, { recursive: true, force: true });
975
+ }
976
+ });
977
+ });
978
+ describe('profile list', () => {
979
+ const stdoutSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
980
+ beforeEach(() => {
981
+ process.exitCode = undefined;
982
+ stdoutSpy.mockClear();
983
+ vi.stubGlobal('fetch', vi.fn());
984
+ });
985
+ it('reports stale daemon instead of no profiles when status lacks profile support', async () => {
986
+ vi.mocked(fetch).mockResolvedValue({
987
+ ok: true,
988
+ json: async () => ({
989
+ ok: true,
990
+ pid: 123,
991
+ uptime: 1,
992
+ daemonVersion: '1.7.6',
993
+ extensionConnected: true,
994
+ extensionVersion: '1.0.3',
995
+ pending: 0,
996
+ memoryMB: 20,
997
+ port: 19825,
998
+ }),
999
+ });
1000
+ const program = createProgram('', '');
1001
+ await program.parseAsync(['node', 'opencli', 'profile', 'list']);
1002
+ const output = stdoutSpy.mock.calls.flat().join('\n');
1003
+ expect(output).toContain('stale');
1004
+ expect(output).toContain('opencli daemon restart');
1005
+ expect(output).not.toContain('No Browser Bridge profiles connected');
1006
+ });
1007
+ it('keeps the empty profile message for current daemon status with no profiles', async () => {
1008
+ vi.mocked(fetch).mockResolvedValue({
1009
+ ok: true,
1010
+ json: async () => ({
1011
+ ok: true,
1012
+ pid: 123,
1013
+ uptime: 1,
1014
+ daemonVersion: PKG_VERSION,
1015
+ extensionConnected: false,
1016
+ profiles: [],
1017
+ pending: 0,
1018
+ memoryMB: 20,
1019
+ port: 19825,
1020
+ }),
1021
+ });
1022
+ const program = createProgram('', '');
1023
+ await program.parseAsync(['node', 'opencli', 'profile', 'list']);
1024
+ const output = stdoutSpy.mock.calls.flat().join('\n');
1025
+ expect(output).toContain('No Browser Bridge profiles connected');
1026
+ expect(output).not.toContain('opencli daemon restart');
1027
+ });
1028
+ });
1029
+ describe('browser tab targeting commands', () => {
1030
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
1031
+ const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
1032
+ function getBrowserStateFile(cacheDir, session = 'test') {
1033
+ return path.join(cacheDir, 'browser-state', `${session}.json`);
1034
+ }
1035
+ beforeEach(() => {
1036
+ process.exitCode = undefined;
1037
+ process.env.OPENCLI_CACHE_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-browser-tab-state-'));
1038
+ consoleLogSpy.mockClear();
1039
+ stderrSpy.mockClear();
1040
+ mockBrowserConnect.mockClear();
1041
+ mockBrowserClose.mockReset().mockResolvedValue(undefined);
1042
+ delete process.env.OPENCLI_WINDOW;
1043
+ mockBindTab.mockReset().mockResolvedValue({
1044
+ session: 'test',
1045
+ page: 'tab-2',
1046
+ url: 'https://user.example/inbox',
1047
+ title: 'Inbox',
1048
+ });
1049
+ mockSendCommand.mockReset().mockResolvedValue({ closed: true });
1050
+ browserState.page = {
1051
+ goto: vi.fn().mockResolvedValue(undefined),
1052
+ wait: vi.fn().mockResolvedValue(undefined),
1053
+ setActivePage: vi.fn(),
1054
+ getActivePage: vi.fn().mockReturnValue('tab-1'),
1055
+ getCurrentUrl: vi.fn().mockResolvedValue('https://one.example'),
1056
+ startNetworkCapture: vi.fn().mockResolvedValue(true),
1057
+ getCookies: vi.fn().mockResolvedValue([]),
1058
+ evaluate: vi.fn().mockResolvedValue({ ok: true }),
1059
+ snapshot: vi.fn().mockResolvedValue('snapshot'),
1060
+ tabs: vi.fn().mockResolvedValue([
1061
+ { index: 0, page: 'tab-1', url: 'https://one.example', title: 'one', active: true },
1062
+ { index: 1, page: 'tab-2', url: 'https://two.example', title: 'two', active: false },
1063
+ ]),
1064
+ selectTab: vi.fn().mockResolvedValue(undefined),
1065
+ newTab: vi.fn().mockResolvedValue('tab-3'),
1066
+ closeTab: vi.fn().mockResolvedValue(undefined),
1067
+ handleJavaScriptDialog: vi.fn().mockResolvedValue(undefined),
1068
+ frames: vi.fn().mockResolvedValue([
1069
+ { index: 0, frameId: 'frame-1', url: 'https://x.example/embed', name: 'x-embed' },
1070
+ ]),
1071
+ evaluateInFrame: vi.fn().mockResolvedValue('inside frame'),
1072
+ screenshot: vi.fn().mockResolvedValue('base64-shot'),
1073
+ annotatedScreenshot: vi.fn().mockResolvedValue('annotated-base64-shot'),
1074
+ readNetworkCapture: vi.fn().mockResolvedValue([]),
1075
+ closeWindow: vi.fn().mockResolvedValue(undefined),
1076
+ waitForDownload: vi.fn().mockResolvedValue({
1077
+ downloaded: true,
1078
+ filename: '/tmp/receipt.pdf',
1079
+ url: 'https://app.example/receipt.pdf',
1080
+ state: 'complete',
1081
+ elapsedMs: 10,
1082
+ }),
1083
+ session: 'test',
1084
+ };
1085
+ });
1086
+ function lastJsonLog() {
1087
+ const calls = consoleLogSpy.mock.calls;
1088
+ if (calls.length === 0)
1089
+ throw new Error('Expected at least one console.log call');
1090
+ const last = calls[calls.length - 1][0];
1091
+ if (typeof last !== 'string')
1092
+ throw new Error(`Expected string arg to console.log, got ${typeof last}`);
1093
+ return JSON.parse(last);
1094
+ }
1095
+ it('binds the current Chrome tab into a browser session', async () => {
1096
+ const program = createProgram('', '');
1097
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'bind']);
1098
+ expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, session: 'test', surface: 'browser' });
1099
+ expect(mockBindTab).toHaveBeenCalledWith('test', {});
1100
+ const out = lastJsonLog();
1101
+ expect(out.session).toBe('test');
1102
+ expect(out.url).toBe('https://user.example/inbox');
1103
+ });
1104
+ it('requires an explicit session for browser commands', async () => {
1105
+ const program = createProgram('', '');
1106
+ // --session is now a hidden internal flag; commander no longer guards it.
1107
+ // The action body throws via getBrowserSession(), surfacing the
1108
+ // <session> positional in the error message.
1109
+ await program.parseAsync(['node', 'opencli', 'browser', 'state']);
1110
+ expect(mockBrowserConnect).not.toHaveBeenCalled();
1111
+ expect(stderrSpy.mock.calls.flat().join('')).toContain('<session> is a required positional argument');
1112
+ });
1113
+ it('runs browser commands against an explicit session', async () => {
1114
+ const program = createProgram('', '');
1115
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'state']);
1116
+ expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, session: 'test', surface: 'browser', windowMode: 'foreground' });
1117
+ expect(browserState.page?.snapshot).toHaveBeenCalled();
1118
+ });
1119
+ it('passes browser --window through Commander options without relying on env pre-processing', async () => {
1120
+ const program = createProgram('', '');
1121
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', '--window', 'background', 'state']);
1122
+ expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, session: 'test', surface: 'browser', windowMode: 'background' });
1123
+ expect(browserState.page?.snapshot).toHaveBeenCalled();
1124
+ });
1125
+ it('passes the opt-in AX source to browser state', async () => {
1126
+ const program = createProgram('', '');
1127
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'state', '--source', 'ax']);
1128
+ expect(browserState.page?.snapshot).toHaveBeenCalledWith({ viewportExpand: 2000, source: 'ax' });
1129
+ });
1130
+ it('prints DOM vs AX snapshot metrics without changing default state output', async () => {
1131
+ browserState.page = {
1132
+ ...browserState.page,
1133
+ snapshot: vi.fn(async (opts) => {
1134
+ if (opts?.source === 'ax') {
1135
+ return 'source: ax\n---\n[1]button "Save"\nframe "https://app.example/embed":\n [2]button "Frame Save"\n---\ninteractive: 2';
1136
+ }
1137
+ return 'URL: https://app.example\n[1] button "Save"';
1138
+ }),
1139
+ };
1140
+ const program = createProgram('', '');
1141
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'state', '--compare-sources']);
1142
+ expect(browserState.page?.snapshot).toHaveBeenCalledWith({ viewportExpand: 2000, source: 'dom' });
1143
+ expect(browserState.page?.snapshot).toHaveBeenCalledWith({ viewportExpand: 2000, source: 'ax' });
1144
+ const out = lastJsonLog();
1145
+ expect(out.url).toBe('https://one.example');
1146
+ expect(out.sources.dom).toMatchObject({ ok: true, refs: 1, frame_sections: 0 });
1147
+ expect(out.sources.ax).toMatchObject({ ok: true, refs: 2, frame_sections: 1, interactive: 2 });
1148
+ });
1149
+ it('keeps compare-sources usable when one observation backend fails', async () => {
1150
+ browserState.page = {
1151
+ ...browserState.page,
1152
+ snapshot: vi.fn(async (opts) => {
1153
+ if (opts?.source === 'ax')
1154
+ throw new Error('AX unavailable');
1155
+ return '[1] button "Save"';
1156
+ }),
1157
+ };
1158
+ const program = createProgram('', '');
1159
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'state', '--compare-sources']);
1160
+ const out = lastJsonLog();
1161
+ expect(out.sources.dom).toMatchObject({ ok: true, refs: 1 });
1162
+ expect(out.sources.ax).toMatchObject({
1163
+ ok: false,
1164
+ error: { message: 'AX unavailable' },
1165
+ });
1166
+ });
1167
+ it('rejects unknown browser state sources before touching the page', async () => {
1168
+ const program = createProgram('', '');
1169
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'state', '--source', 'magic']);
1170
+ expect(browserState.page?.snapshot).not.toHaveBeenCalled();
1171
+ const out = lastJsonLog();
1172
+ expect(out.error.code).toBe('invalid_source');
1173
+ expect(process.exitCode).toBeDefined();
1174
+ });
1175
+ it('captures annotated screenshots through the visual ref overlay path', async () => {
1176
+ const program = createProgram('', '');
1177
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'screenshot', '--annotate']);
1178
+ expect(browserState.page?.annotatedScreenshot).toHaveBeenCalledWith({
1179
+ fullPage: false,
1180
+ annotate: true,
1181
+ width: undefined,
1182
+ height: undefined,
1183
+ format: 'png',
1184
+ });
1185
+ expect(browserState.page?.screenshot).not.toHaveBeenCalled();
1186
+ expect(consoleLogSpy).toHaveBeenLastCalledWith('annotated-base64-shot');
1187
+ });
1188
+ it('allows history navigation in a bound session', async () => {
1189
+ browserState.page = {
1190
+ ...browserState.page,
1191
+ evaluate: vi.fn(),
1192
+ wait: vi.fn(),
1193
+ session: 'test',
1194
+ };
1195
+ const program = createProgram('', '');
1196
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'back']);
1197
+ expect(browserState.page?.evaluate).toHaveBeenCalledWith('history.back()');
1198
+ });
1199
+ it('unbinds a session through the daemon close-window command', async () => {
1200
+ const program = createProgram('', '');
1201
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'unbind']);
1202
+ expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, session: 'test', surface: 'browser' });
1203
+ expect(mockSendCommand).toHaveBeenCalledWith('close-window', { session: 'test', surface: 'browser' });
1204
+ const out = lastJsonLog();
1205
+ expect(out).toEqual({ unbound: true, session: 'test' });
1206
+ });
1207
+ it('does not print false success when unbind fails', async () => {
1208
+ mockSendCommand.mockRejectedValueOnce(new BrowserCommandError('Session "test" is not attached to a tab.', 'bound_session_missing', 'Run bind again, then retry the browser command.'));
1209
+ const program = createProgram('', '');
1210
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'unbind']);
1211
+ const out = lastJsonLog();
1212
+ expect(out.error.code).toBe('bound_session_missing');
1213
+ expect(process.exitCode).toBeDefined();
1214
+ });
1215
+ it('accepts JavaScript dialogs through the browser dialog command', async () => {
1216
+ const program = createProgram('', '');
1217
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'dialog', 'accept', '--text', 'ok']);
1218
+ expect(browserState.page?.handleJavaScriptDialog).toHaveBeenCalledWith(true, 'ok');
1219
+ const out = lastJsonLog();
1220
+ expect(out).toEqual({ handled: true, action: 'accept', text: 'ok' });
1221
+ });
1222
+ it('emits a structured error when a browser action is blocked by a JavaScript dialog', async () => {
1223
+ browserState.page = {
1224
+ ...browserState.page,
1225
+ evaluate: vi.fn().mockRejectedValue(new Error('JavaScript dialog showing')),
1226
+ };
1227
+ const program = createProgram('', '');
1228
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'eval', 'document.title']);
1229
+ const out = lastJsonLog();
1230
+ expect(out.error.code).toBe('javascript_dialog_open');
1231
+ expect(out.error.hint).toContain('browser dialog accept');
1232
+ expect(process.exitCode).toBeDefined();
1233
+ });
1234
+ it('binds browser commands to an explicit target tab via --tab', async () => {
1235
+ const program = createProgram('', '');
1236
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'eval', '--tab', 'tab-2', 'document.title']);
1237
+ expect(browserState.page?.setActivePage).toHaveBeenCalledWith('tab-2');
1238
+ expect(browserState.page?.evaluate).toHaveBeenCalledWith('document.title');
1239
+ });
1240
+ it('rejects an explicit --tab target that is no longer in the current session', async () => {
1241
+ browserState.page = {
1242
+ setActivePage: vi.fn(),
1243
+ getActivePage: vi.fn(),
1244
+ tabs: vi.fn().mockResolvedValue([]),
1245
+ evaluate: vi.fn(),
1246
+ };
1247
+ const program = createProgram('', '');
1248
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'eval', '--tab', 'tab-stale', 'document.title']);
1249
+ expect(process.exitCode).toBeDefined();
1250
+ expect(browserState.page?.setActivePage).not.toHaveBeenCalled();
1251
+ expect(browserState.page?.evaluate).not.toHaveBeenCalled();
1252
+ expect(stderrSpy.mock.calls.flat().join('\n')).toContain('Target tab tab-stale is not part of the current browser session');
1253
+ });
1254
+ it('lists tabs with target IDs via browser tab list', async () => {
1255
+ const program = createProgram('', '');
1256
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'tab', 'list']);
1257
+ expect(browserState.page?.tabs).toHaveBeenCalledTimes(1);
1258
+ expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"page": "tab-1"');
1259
+ expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"page": "tab-2"');
1260
+ });
1261
+ it('creates a new tab and prints its target ID', async () => {
1262
+ const program = createProgram('', '');
1263
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'tab', 'new', 'https://three.example']);
1264
+ expect(browserState.page?.newTab).toHaveBeenCalledWith('https://three.example');
1265
+ expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"page": "tab-3"');
1266
+ });
1267
+ it('prints the resolved target ID when browser open creates or navigates a tab', async () => {
1268
+ const program = createProgram('', '');
1269
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'open', 'https://example.com']);
1270
+ expect(browserState.page?.goto).toHaveBeenCalledWith('https://example.com');
1271
+ expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"url": "https://one.example"');
1272
+ expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"page": "tab-1"');
1273
+ });
1274
+ it('lists cross-origin frames via browser frames', async () => {
1275
+ const program = createProgram('', '');
1276
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'frames']);
1277
+ expect(browserState.page?.frames).toHaveBeenCalledTimes(1);
1278
+ expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"frameId": "frame-1"');
1279
+ expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"url": "https://x.example/embed"');
1280
+ });
1281
+ it('routes browser eval --frame through frame-targeted evaluation', async () => {
1282
+ const program = createProgram('', '');
1283
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'eval', '--frame', '0', 'document.title']);
1284
+ expect(browserState.page?.evaluateInFrame).toHaveBeenCalledWith('document.title', 0);
1285
+ expect(browserState.page?.evaluate).not.toHaveBeenCalled();
1286
+ expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('inside frame');
1287
+ });
1288
+ it('does not promote a newly created tab to the persisted default target', async () => {
1289
+ const program = createProgram('', '');
1290
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'tab', 'new', 'https://three.example']);
1291
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'eval', 'document.title']);
1292
+ expect(browserState.page?.newTab).toHaveBeenCalledWith('https://three.example');
1293
+ expect(browserState.page?.setActivePage).not.toHaveBeenCalled();
1294
+ expect(browserState.page?.evaluate).toHaveBeenCalledWith('document.title');
1295
+ });
1296
+ it('persists an explicitly selected tab as the default target for later untargeted commands', async () => {
1297
+ const program = createProgram('', '');
1298
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'tab', 'select', 'tab-2']);
1299
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'eval', 'document.title']);
1300
+ expect(browserState.page?.selectTab).toHaveBeenCalledWith('tab-2');
1301
+ expect(browserState.page?.setActivePage).toHaveBeenCalledWith('tab-2');
1302
+ expect(browserState.page?.evaluate).toHaveBeenCalledWith('document.title');
1303
+ expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"selected": "tab-2"');
1304
+ });
1305
+ it('clears a saved default target when it is no longer present in the current session', async () => {
1306
+ const cacheDir = String(process.env.OPENCLI_CACHE_DIR);
1307
+ const program = createProgram('', '');
1308
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'tab', 'select', 'tab-2']);
1309
+ expect(fs.existsSync(getBrowserStateFile(cacheDir))).toBe(true);
1310
+ browserState.page = {
1311
+ setActivePage: vi.fn(),
1312
+ getActivePage: vi.fn(),
1313
+ tabs: vi.fn().mockResolvedValue([]),
1314
+ evaluate: vi.fn().mockResolvedValue({ ok: true }),
1315
+ readNetworkCapture: vi.fn().mockResolvedValue([]),
1316
+ };
1317
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'eval', 'document.title']);
1318
+ expect(browserState.page?.setActivePage).not.toHaveBeenCalled();
1319
+ expect(browserState.page?.evaluate).toHaveBeenCalledWith('document.title');
1320
+ expect(fs.existsSync(getBrowserStateFile(cacheDir))).toBe(false);
1321
+ });
1322
+ it('clears the persisted default target when that tab is closed', async () => {
1323
+ const program = createProgram('', '');
1324
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'tab', 'select', 'tab-2']);
1325
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'tab', 'close', 'tab-2']);
1326
+ vi.mocked(browserState.page?.setActivePage).mockClear();
1327
+ vi.mocked(browserState.page?.evaluate).mockClear();
1328
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'eval', 'document.title']);
1329
+ expect(browserState.page?.closeTab).toHaveBeenCalledWith('tab-2');
1330
+ expect(browserState.page?.setActivePage).not.toHaveBeenCalled();
1331
+ expect(browserState.page?.evaluate).toHaveBeenCalledWith('document.title');
1332
+ });
1333
+ it('closes a tab by target ID', async () => {
1334
+ const program = createProgram('', '');
1335
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'tab', 'close', 'tab-2']);
1336
+ expect(browserState.page?.closeTab).toHaveBeenCalledWith('tab-2');
1337
+ expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"closed": "tab-2"');
1338
+ });
1339
+ it('rejects closing a stale tab target ID that is no longer in the current session', async () => {
1340
+ browserState.page = {
1341
+ session: 'test',
1342
+ tabs: vi.fn().mockResolvedValue([]),
1343
+ closeTab: vi.fn(),
1344
+ };
1345
+ const program = createProgram('', '');
1346
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'tab', 'close', 'tab-stale']);
1347
+ expect(process.exitCode).toBeDefined();
1348
+ expect(browserState.page?.closeTab).not.toHaveBeenCalled();
1349
+ expect(stderrSpy.mock.calls.flat().join('\n')).toContain('Target tab tab-stale is not part of the current browser session');
1350
+ });
1351
+ it('browser analyze merges HttpOnly cookie names from page.getCookies and drains stale capture before verdict', async () => {
1352
+ browserState.page = {
1353
+ goto: vi.fn().mockResolvedValue(undefined),
1354
+ wait: vi.fn().mockResolvedValue(undefined),
1355
+ setActivePage: vi.fn(),
1356
+ getActivePage: vi.fn().mockReturnValue('tab-1'),
1357
+ getCurrentUrl: vi.fn().mockResolvedValue('https://target.example'),
1358
+ startNetworkCapture: vi.fn().mockResolvedValue(true),
1359
+ getCookies: vi.fn().mockResolvedValue([{ name: 'cf_clearance', value: 'x', domain: '.target.example' }]),
1360
+ evaluate: vi.fn().mockResolvedValue({
1361
+ cookieNames: [],
1362
+ initialState: {
1363
+ __INITIAL_STATE__: false,
1364
+ __NUXT__: false,
1365
+ __NEXT_DATA__: false,
1366
+ __APOLLO_STATE__: false,
1367
+ },
1368
+ title: 'Target',
1369
+ finalUrl: 'https://target.example/',
1370
+ }),
1371
+ tabs: vi.fn().mockResolvedValue([{ index: 0, page: 'tab-1', url: 'https://target.example', title: 'Target', active: true }]),
1372
+ readNetworkCapture: vi.fn()
1373
+ .mockResolvedValueOnce([
1374
+ {
1375
+ url: 'https://stale.example/api/old',
1376
+ method: 'GET',
1377
+ responseStatus: 200,
1378
+ responseContentType: 'application/json',
1379
+ responsePreview: '{"stale":true}',
1380
+ },
1381
+ ])
1382
+ .mockResolvedValueOnce([
1383
+ {
1384
+ url: 'https://target.example/api/items',
1385
+ method: 'GET',
1386
+ responseStatus: 200,
1387
+ responseContentType: 'application/json',
1388
+ responsePreview: '{"items":[{"title":"A","id":"1"}]}',
1389
+ },
1390
+ ]),
1391
+ };
1392
+ const program = createProgram('', '');
1393
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'analyze', 'https://target.example/']);
1394
+ const out = lastJsonLog();
1395
+ expect(browserState.page?.readNetworkCapture).toHaveBeenCalledTimes(2);
1396
+ expect(out.pattern.pattern).toBe('A');
1397
+ expect(out.api_candidates[0].url).toBe('https://target.example/api/items');
1398
+ expect(out.api_candidates[0].verdict).toBe('likely_data');
1399
+ expect(out.anti_bot.evidence).toContain('cookie:cf_clearance');
1400
+ });
1401
+ it('browser analyze falls back to interceptor buffer when network capture is unsupported', async () => {
1402
+ let bufferReads = 0;
1403
+ browserState.page = {
1404
+ goto: vi.fn().mockResolvedValue(undefined),
1405
+ wait: vi.fn().mockResolvedValue(undefined),
1406
+ setActivePage: vi.fn(),
1407
+ getActivePage: vi.fn().mockReturnValue('tab-1'),
1408
+ getCurrentUrl: vi.fn().mockResolvedValue('https://target.example'),
1409
+ startNetworkCapture: vi.fn().mockResolvedValue(false),
1410
+ getCookies: vi.fn().mockResolvedValue([{ name: 'cf_clearance', value: 'x', domain: '.target.example' }]),
1411
+ evaluate: vi.fn().mockImplementation(async (arg) => {
1412
+ if (typeof arg === 'string' && arg.includes('document.cookie')) {
1413
+ return {
1414
+ cookieNames: [],
1415
+ initialState: {
1416
+ __INITIAL_STATE__: false,
1417
+ __NUXT__: false,
1418
+ __NEXT_DATA__: false,
1419
+ __APOLLO_STATE__: false,
1420
+ },
1421
+ title: 'Target',
1422
+ finalUrl: 'https://target.example/',
1423
+ };
1424
+ }
1425
+ if (typeof arg === 'string' && arg.includes('window.__opencli_net = []')) {
1426
+ bufferReads += 1;
1427
+ if (bufferReads === 1) {
1428
+ return JSON.stringify([
1429
+ {
1430
+ url: 'https://stale.example/api/old',
1431
+ method: 'GET',
1432
+ status: 200,
1433
+ size: 12,
1434
+ ct: 'application/json',
1435
+ body: { stale: true },
1436
+ },
1437
+ ]);
1438
+ }
1439
+ return JSON.stringify([
1440
+ {
1441
+ url: 'https://target.example/waf',
1442
+ method: 'GET',
1443
+ status: 403,
1444
+ size: 17,
1445
+ ct: 'text/html',
1446
+ body: 'Cloudflare Ray ID',
1447
+ },
1448
+ ]);
1449
+ }
1450
+ return undefined;
1451
+ }),
1452
+ tabs: vi.fn().mockResolvedValue([{ index: 0, page: 'tab-1', url: 'https://target.example', title: 'Target', active: true }]),
1453
+ readNetworkCapture: vi.fn().mockResolvedValue([]),
1454
+ };
1455
+ const program = createProgram('', '');
1456
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'analyze', 'https://target.example/']);
1457
+ const out = lastJsonLog();
1458
+ expect(browserState.page?.readNetworkCapture).toHaveBeenCalledTimes(2);
1459
+ expect(bufferReads).toBe(2);
1460
+ expect(out.anti_bot.vendor).toBe('cloudflare');
1461
+ expect(out.anti_bot.evidence).toContain('cookie:cf_clearance');
1462
+ expect(out.anti_bot.evidence).toContain('body:https://target.example/waf');
1463
+ });
1464
+ it('browser wait xhr starts capture, injects interceptor on fallback, and ignores stale ring entries', async () => {
1465
+ browserState.page = {
1466
+ goto: vi.fn().mockResolvedValue(undefined),
1467
+ wait: vi.fn().mockResolvedValue(undefined),
1468
+ setActivePage: vi.fn(),
1469
+ getActivePage: vi.fn().mockReturnValue('tab-1'),
1470
+ getCurrentUrl: vi.fn().mockResolvedValue('https://target.example'),
1471
+ startNetworkCapture: vi.fn().mockResolvedValue(false),
1472
+ evaluate: vi.fn().mockResolvedValue(undefined),
1473
+ tabs: vi.fn().mockResolvedValue([{ index: 0, page: 'tab-1', url: 'https://target.example', title: 'Target', active: true }]),
1474
+ readNetworkCapture: vi.fn()
1475
+ .mockResolvedValueOnce([
1476
+ {
1477
+ url: 'https://stale.example/api/old',
1478
+ method: 'GET',
1479
+ responseStatus: 200,
1480
+ responseContentType: 'application/json',
1481
+ responsePreview: '{"stale":true}',
1482
+ },
1483
+ ])
1484
+ .mockResolvedValueOnce([
1485
+ {
1486
+ url: 'https://target.example/api/target',
1487
+ method: 'GET',
1488
+ responseStatus: 200,
1489
+ responseContentType: 'application/json',
1490
+ responsePreview: '{"ok":true}',
1491
+ },
1492
+ ]),
1493
+ };
1494
+ const program = createProgram('', '');
1495
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'wait', 'xhr', '/api/target', '--timeout', '900']);
1496
+ const out = lastJsonLog();
1497
+ expect(browserState.page?.startNetworkCapture).toHaveBeenCalledTimes(1);
1498
+ expect(browserState.page?.evaluate).toHaveBeenCalledWith(expect.stringContaining('window.__opencli_net'));
1499
+ expect(browserState.page?.readNetworkCapture).toHaveBeenCalledTimes(2);
1500
+ expect(out.matched.url).toBe('https://target.example/api/target');
1501
+ });
1502
+ it('browser wait xhr reads interceptor buffer when network capture is unsupported', async () => {
1503
+ let bufferReads = 0;
1504
+ browserState.page = {
1505
+ goto: vi.fn().mockResolvedValue(undefined),
1506
+ wait: vi.fn().mockResolvedValue(undefined),
1507
+ setActivePage: vi.fn(),
1508
+ getActivePage: vi.fn().mockReturnValue('tab-1'),
1509
+ getCurrentUrl: vi.fn().mockResolvedValue('https://target.example'),
1510
+ startNetworkCapture: vi.fn().mockResolvedValue(false),
1511
+ evaluate: vi.fn().mockImplementation(async (arg) => {
1512
+ if (typeof arg === 'string' && arg.includes('window.__opencli_net = []')) {
1513
+ bufferReads += 1;
1514
+ if (bufferReads === 1) {
1515
+ return JSON.stringify([
1516
+ {
1517
+ url: 'https://stale.example/api/old',
1518
+ method: 'GET',
1519
+ status: 200,
1520
+ size: 12,
1521
+ ct: 'application/json',
1522
+ body: { stale: true },
1523
+ },
1524
+ ]);
1525
+ }
1526
+ return JSON.stringify([
1527
+ {
1528
+ url: 'https://target.example/api/target',
1529
+ method: 'GET',
1530
+ status: 200,
1531
+ size: 11,
1532
+ ct: 'application/json',
1533
+ body: { ok: true },
1534
+ },
1535
+ ]);
1536
+ }
1537
+ return undefined;
1538
+ }),
1539
+ tabs: vi.fn().mockResolvedValue([{ index: 0, page: 'tab-1', url: 'https://target.example', title: 'Target', active: true }]),
1540
+ readNetworkCapture: vi.fn().mockResolvedValue([]),
1541
+ };
1542
+ const program = createProgram('', '');
1543
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'wait', 'xhr', '/api/target', '--timeout', '900']);
1544
+ const out = lastJsonLog();
1545
+ expect(browserState.page?.startNetworkCapture).toHaveBeenCalledTimes(1);
1546
+ expect(browserState.page?.readNetworkCapture).toHaveBeenCalledTimes(2);
1547
+ expect(bufferReads).toBe(2);
1548
+ expect(out.matched.url).toBe('https://target.example/api/target');
1549
+ });
1550
+ it('browser wait download delegates to the Browser Bridge download observer', async () => {
1551
+ browserState.page = {
1552
+ goto: vi.fn().mockResolvedValue(undefined),
1553
+ wait: vi.fn().mockResolvedValue(undefined),
1554
+ waitForDownload: vi.fn().mockResolvedValue({
1555
+ downloaded: true,
1556
+ filename: '/tmp/receipt.pdf',
1557
+ url: 'https://app.example/receipt.pdf',
1558
+ state: 'complete',
1559
+ elapsedMs: 10,
1560
+ }),
1561
+ setActivePage: vi.fn(),
1562
+ getActivePage: vi.fn().mockReturnValue('tab-1'),
1563
+ getCurrentUrl: vi.fn().mockResolvedValue('https://target.example'),
1564
+ tabs: vi.fn().mockResolvedValue([{ index: 0, page: 'tab-1', url: 'https://target.example', title: 'Target', active: true }]),
1565
+ };
1566
+ const program = createProgram('', '');
1567
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'wait', 'download', 'receipt', '--timeout', '900']);
1568
+ expect(browserState.page?.waitForDownload).toHaveBeenCalledWith('receipt', 900);
1569
+ expect(lastJsonLog()).toEqual({
1570
+ downloaded: true,
1571
+ filename: '/tmp/receipt.pdf',
1572
+ url: 'https://app.example/receipt.pdf',
1573
+ state: 'complete',
1574
+ elapsedMs: 10,
1575
+ });
1576
+ });
1577
+ it('browser wait download reports an error envelope when no matching download completes', async () => {
1578
+ browserState.page = {
1579
+ goto: vi.fn().mockResolvedValue(undefined),
1580
+ wait: vi.fn().mockResolvedValue(undefined),
1581
+ waitForDownload: vi.fn().mockResolvedValue({
1582
+ downloaded: false,
1583
+ state: 'interrupted',
1584
+ error: 'No download matched "receipt" within 900ms',
1585
+ elapsedMs: 900,
1586
+ }),
1587
+ setActivePage: vi.fn(),
1588
+ getActivePage: vi.fn().mockReturnValue('tab-1'),
1589
+ getCurrentUrl: vi.fn().mockResolvedValue('https://target.example'),
1590
+ tabs: vi.fn().mockResolvedValue([{ index: 0, page: 'tab-1', url: 'https://target.example', title: 'Target', active: true }]),
1591
+ };
1592
+ const program = createProgram('', '');
1593
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'wait', 'download', 'receipt', '--timeout', '900']);
1594
+ const out = lastJsonLog();
1595
+ expect(out.error.code).toBe('download_not_seen');
1596
+ expect(out.download.elapsedMs).toBe(900);
1597
+ expect(process.exitCode).toBeDefined();
1598
+ });
1599
+ });
1600
+ describe('browser network command', () => {
1601
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
1602
+ function getNetworkCachePath(cacheDir) {
1603
+ return path.join(cacheDir, 'browser-network', 'test.json');
1604
+ }
1605
+ function getCustomNetworkCachePath(cacheDir) {
1606
+ return path.join(cacheDir, 'browser-network', 'custom.json');
1607
+ }
1608
+ function lastJsonLog() {
1609
+ const calls = consoleLogSpy.mock.calls;
1610
+ if (calls.length === 0)
1611
+ throw new Error('Expected at least one console.log call');
1612
+ const last = calls[calls.length - 1][0];
1613
+ if (typeof last !== 'string')
1614
+ throw new Error(`Expected string arg to console.log, got ${typeof last}`);
1615
+ return JSON.parse(last);
1616
+ }
1617
+ beforeEach(() => {
1618
+ process.exitCode = undefined;
1619
+ process.env.OPENCLI_CACHE_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-browser-net-'));
1620
+ consoleLogSpy.mockClear();
1621
+ mockBrowserConnect.mockClear();
1622
+ mockBrowserClose.mockReset().mockResolvedValue(undefined);
1623
+ browserState.page = {
1624
+ session: 'test',
1625
+ setActivePage: vi.fn(),
1626
+ getActivePage: vi.fn().mockReturnValue('tab-1'),
1627
+ tabs: vi.fn().mockResolvedValue([{ page: 'tab-1', active: true }]),
1628
+ evaluate: vi.fn().mockResolvedValue(''),
1629
+ readNetworkCapture: vi.fn().mockResolvedValue([
1630
+ {
1631
+ url: 'https://x.com/i/api/graphql/qid/UserTweets?v=1',
1632
+ method: 'GET',
1633
+ responseStatus: 200,
1634
+ responseContentType: 'application/json',
1635
+ responsePreview: JSON.stringify({ data: { user: { rest_id: '42' } } }),
1636
+ timestamp: Date.now(),
1637
+ },
1638
+ {
1639
+ url: 'https://cdn.example.com/app.js',
1640
+ method: 'GET',
1641
+ responseStatus: 200,
1642
+ responseContentType: 'application/javascript',
1643
+ responsePreview: '// js',
1644
+ },
1645
+ ]),
1646
+ };
1647
+ });
1648
+ it('emits JSON with shape previews and persists the capture to disk', async () => {
1649
+ const cacheDir = String(process.env.OPENCLI_CACHE_DIR);
1650
+ const program = createProgram('', '');
1651
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1652
+ const out = lastJsonLog();
1653
+ expect(out.count).toBe(1);
1654
+ expect(out.filtered_out).toBe(1);
1655
+ expect(out.entries[0].key).toBe('UserTweets');
1656
+ expect(out.entries[0].shape['$.data.user.rest_id']).toBe('string');
1657
+ expect(out.entries[0]).not.toHaveProperty('body');
1658
+ expect(fs.existsSync(getNetworkCachePath(cacheDir))).toBe(true);
1659
+ });
1660
+ it('uses the selected browser session for network cache scope', async () => {
1661
+ const cacheDir = String(process.env.OPENCLI_CACHE_DIR);
1662
+ browserState.page = {
1663
+ ...browserState.page,
1664
+ session: 'custom',
1665
+ };
1666
+ const program = createProgram('', '');
1667
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'custom', 'network']);
1668
+ const out = lastJsonLog();
1669
+ expect(out.session).toBe('custom');
1670
+ expect(fs.existsSync(getCustomNetworkCachePath(cacheDir))).toBe(true);
1671
+ expect(fs.existsSync(getNetworkCachePath(cacheDir))).toBe(false);
1672
+ });
1673
+ it('--all includes static resources that the default filter drops', async () => {
1674
+ const program = createProgram('', '');
1675
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--all']);
1676
+ const out = lastJsonLog();
1677
+ expect(out.count).toBe(2);
1678
+ expect(out.entries.map((e) => e.key)).toContain('UserTweets');
1679
+ expect(out.entries.map((e) => e.key)).toContain('GET cdn.example.com/app.js');
1680
+ });
1681
+ it('--failed and --since filter captured entries by status and time window', async () => {
1682
+ const now = Date.now();
1683
+ browserState.page.readNetworkCapture = vi.fn().mockResolvedValue([
1684
+ {
1685
+ url: 'https://api.example.com/new-fail',
1686
+ method: 'GET',
1687
+ responseStatus: 500,
1688
+ responseContentType: 'application/json',
1689
+ responsePreview: JSON.stringify({ error: true }),
1690
+ timestamp: now,
1691
+ },
1692
+ {
1693
+ url: 'https://api.example.com/old-fail',
1694
+ method: 'GET',
1695
+ responseStatus: 500,
1696
+ responseContentType: 'application/json',
1697
+ responsePreview: JSON.stringify({ error: true }),
1698
+ timestamp: now - 180_000,
1699
+ },
1700
+ {
1701
+ url: 'https://api.example.com/new-ok',
1702
+ method: 'GET',
1703
+ responseStatus: 200,
1704
+ responseContentType: 'application/json',
1705
+ responsePreview: JSON.stringify({ ok: true }),
1706
+ timestamp: now,
1707
+ },
1708
+ ]);
1709
+ const program = createProgram('', '');
1710
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--since', '120s', '--failed']);
1711
+ const out = lastJsonLog();
1712
+ expect(out.count).toBe(1);
1713
+ expect(out.entries[0].url).toBe('https://api.example.com/new-fail');
1714
+ expect(out.entries[0].timestamp).toMatch(/T/);
1715
+ });
1716
+ it('default output keeps text/javascript API responses while dropping static JS files', async () => {
1717
+ browserState.page.readNetworkCapture = vi.fn().mockResolvedValue([
1718
+ {
1719
+ url: 'https://hw.mail.163.com/js6/s?sid=abc&func=mbox:listMessages',
1720
+ method: 'POST',
1721
+ responseStatus: 200,
1722
+ responseContentType: 'text/javascript',
1723
+ responsePreview: JSON.stringify({ messages: [{ id: 'm1', subject: 'hello' }] }),
1724
+ },
1725
+ {
1726
+ url: 'https://cdn.example.com/app.js',
1727
+ method: 'GET',
1728
+ responseStatus: 200,
1729
+ responseContentType: 'application/javascript',
1730
+ responsePreview: '// js',
1731
+ },
1732
+ ]);
1733
+ const program = createProgram('', '');
1734
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1735
+ const out = lastJsonLog();
1736
+ expect(out.count).toBe(1);
1737
+ expect(out.filtered_out).toBe(1);
1738
+ expect(out.entries[0].key).toBe('POST hw.mail.163.com/js6/s');
1739
+ expect(out.entries[0].ct).toBe('text/javascript');
1740
+ expect(out.entries[0].shape['$.messages']).toBe('array(1)');
1741
+ });
1742
+ it('--raw emits full bodies inline for every entry', async () => {
1743
+ const program = createProgram('', '');
1744
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--raw']);
1745
+ const out = lastJsonLog();
1746
+ expect(out.entries[0].body).toEqual({ data: { user: { rest_id: '42' } } });
1747
+ expect(out.entries[0].timestamp).toMatch(/T/);
1748
+ });
1749
+ it('--detail <key> returns the full body for the requested entry', async () => {
1750
+ const program = createProgram('', '');
1751
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1752
+ consoleLogSpy.mockClear();
1753
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--detail', 'UserTweets']);
1754
+ const out = lastJsonLog();
1755
+ expect(out.key).toBe('UserTweets');
1756
+ expect(out.body).toEqual({ data: { user: { rest_id: '42' } } });
1757
+ expect(out.shape['$.data.user.rest_id']).toBe('string');
1758
+ expect(out.timestamp).toMatch(/T/);
1759
+ });
1760
+ it('--detail reports key_not_found with the list of available keys', async () => {
1761
+ const program = createProgram('', '');
1762
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1763
+ consoleLogSpy.mockClear();
1764
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--detail', 'NopeOp']);
1765
+ const out = lastJsonLog();
1766
+ expect(out.error.code).toBe('key_not_found');
1767
+ expect(out.error.available_keys).toContain('UserTweets');
1768
+ expect(process.exitCode).toBeDefined();
1769
+ });
1770
+ it('--detail reports cache_missing when no capture has been persisted yet', async () => {
1771
+ const program = createProgram('', '');
1772
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--detail', 'UserTweets']);
1773
+ const out = lastJsonLog();
1774
+ expect(out.error.code).toBe('cache_missing');
1775
+ expect(process.exitCode).toBeDefined();
1776
+ });
1777
+ it('emits capture_failed when readNetworkCapture throws', async () => {
1778
+ browserState.page.readNetworkCapture = vi.fn().mockRejectedValue(new Error('CDP disconnected'));
1779
+ const program = createProgram('', '');
1780
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1781
+ const out = lastJsonLog();
1782
+ expect(out.error.code).toBe('capture_failed');
1783
+ expect(out.error.message).toContain('CDP disconnected');
1784
+ expect(process.exitCode).toBeDefined();
1785
+ });
1786
+ it('surfaces cache_warning in the envelope when persistence fails', async () => {
1787
+ const cacheDir = String(process.env.OPENCLI_CACHE_DIR);
1788
+ // Pre-create the target path as a file where a directory is expected,
1789
+ // forcing the mkdir inside saveNetworkCache to throw.
1790
+ const clashDir = path.join(cacheDir, 'browser-network');
1791
+ fs.writeFileSync(clashDir, 'not-a-directory');
1792
+ const program = createProgram('', '');
1793
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1794
+ const out = lastJsonLog();
1795
+ expect(out.cache_warning).toMatch(/Could not persist capture cache/);
1796
+ expect(out.count).toBe(1);
1797
+ expect(process.exitCode).toBeUndefined();
1798
+ });
1799
+ describe('--filter', () => {
1800
+ function apiResponse(url, body) {
1801
+ return {
1802
+ url,
1803
+ method: 'GET',
1804
+ responseStatus: 200,
1805
+ responseContentType: 'application/json',
1806
+ responsePreview: JSON.stringify(body),
1807
+ };
1808
+ }
1809
+ beforeEach(() => {
1810
+ browserState.page.readNetworkCapture = vi.fn().mockResolvedValue([
1811
+ apiResponse('https://x.com/i/api/graphql/qid/UserTweets?v=1', { data: { items: [{ author: 'a', text: 't', likes: 1 }] } }),
1812
+ apiResponse('https://x.com/i/api/graphql/qid/UserProfile?v=1', { data: { user: { id: 'u1', followers: 10 } } }),
1813
+ apiResponse('https://x.com/i/api/graphql/qid/Settings?v=1', { config: { theme: 'dark' } }),
1814
+ ]);
1815
+ });
1816
+ it('narrows entries to those whose shape has ALL named fields', async () => {
1817
+ const program = createProgram('', '');
1818
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--filter', 'author,text,likes']);
1819
+ const out = lastJsonLog();
1820
+ expect(out.count).toBe(1);
1821
+ expect(out.filter).toEqual(['author', 'text', 'likes']);
1822
+ expect(out.filter_dropped).toBe(2);
1823
+ expect(out.entries[0].key).toBe('UserTweets');
1824
+ });
1825
+ it('matches container segments too, not just leaf names (any-segment rule)', async () => {
1826
+ const program = createProgram('', '');
1827
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--filter', 'data,items']);
1828
+ const out = lastJsonLog();
1829
+ expect(out.count).toBe(1);
1830
+ expect(out.entries[0].key).toBe('UserTweets');
1831
+ });
1832
+ it('drops entries that are missing any required field (AND semantics)', async () => {
1833
+ const program = createProgram('', '');
1834
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--filter', 'author,followers']);
1835
+ const out = lastJsonLog();
1836
+ expect(out.count).toBe(0);
1837
+ expect(out.entries).toEqual([]);
1838
+ expect(out.filter).toEqual(['author', 'followers']);
1839
+ expect(out.filter_dropped).toBe(3);
1840
+ });
1841
+ it('returns empty entries (not an error) when nothing matches', async () => {
1842
+ const program = createProgram('', '');
1843
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--filter', 'nonexistent_field']);
1844
+ const out = lastJsonLog();
1845
+ expect(out.count).toBe(0);
1846
+ expect(out.entries).toEqual([]);
1847
+ expect(out).not.toHaveProperty('error');
1848
+ expect(process.exitCode).toBeUndefined();
1849
+ });
1850
+ it('is case-sensitive so agents do not conflate `Id` with `id`', async () => {
1851
+ const program = createProgram('', '');
1852
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--filter', 'Data']);
1853
+ const out = lastJsonLog();
1854
+ expect(out.count).toBe(0);
1855
+ });
1856
+ it('persists the full (unfiltered) capture so --detail lookups still find filtered-out keys', async () => {
1857
+ const program = createProgram('', '');
1858
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--filter', 'author,text,likes']);
1859
+ consoleLogSpy.mockClear();
1860
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--detail', 'UserProfile']);
1861
+ const out = lastJsonLog();
1862
+ expect(out.key).toBe('UserProfile');
1863
+ expect(out.body).toEqual({ data: { user: { id: 'u1', followers: 10 } } });
1864
+ });
1865
+ it('composes with --raw: entries keep full bodies, filter still narrows', async () => {
1866
+ const program = createProgram('', '');
1867
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--filter', 'author', '--raw']);
1868
+ const out = lastJsonLog();
1869
+ expect(out.count).toBe(1);
1870
+ expect(out.entries[0].body).toEqual({ data: { items: [{ author: 'a', text: 't', likes: 1 }] } });
1871
+ });
1872
+ it('reports invalid_filter for empty value', async () => {
1873
+ const program = createProgram('', '');
1874
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--filter', '']);
1875
+ const out = lastJsonLog();
1876
+ expect(out.error.code).toBe('invalid_filter');
1877
+ expect(process.exitCode).toBeDefined();
1878
+ });
1879
+ it('reports invalid_filter for commas-only value', async () => {
1880
+ const program = createProgram('', '');
1881
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--filter', ',,,']);
1882
+ const out = lastJsonLog();
1883
+ expect(out.error.code).toBe('invalid_filter');
1884
+ expect(process.exitCode).toBeDefined();
1885
+ });
1886
+ it('rejects --filter combined with --detail as invalid_args', async () => {
1887
+ const program = createProgram('', '');
1888
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--filter', 'author', '--detail', 'UserTweets']);
1889
+ const out = lastJsonLog();
1890
+ expect(out.error.code).toBe('invalid_args');
1891
+ expect(out.error.message).toContain('--filter');
1892
+ expect(out.error.message).toContain('--detail');
1893
+ expect(process.exitCode).toBeDefined();
1894
+ });
1895
+ });
1896
+ describe('body truncation signals', () => {
1897
+ it('flags body_truncated in list view when the capture layer capped the body', async () => {
1898
+ browserState.page.readNetworkCapture = vi.fn().mockResolvedValue([
1899
+ {
1900
+ url: 'https://api.example.com/huge',
1901
+ method: 'GET',
1902
+ responseStatus: 200,
1903
+ responseContentType: 'application/json',
1904
+ responsePreview: '{"data":"x"}',
1905
+ responseBodyFullSize: 99_999_999,
1906
+ responseBodyTruncated: true,
1907
+ },
1908
+ ]);
1909
+ const program = createProgram('', '');
1910
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1911
+ const out = lastJsonLog();
1912
+ expect(out.body_truncated_count).toBe(1);
1913
+ expect(out.entries[0].body_truncated).toBe(true);
1914
+ expect(out.entries[0].size).toBe(99_999_999);
1915
+ });
1916
+ it('--detail surfaces body_truncated + body_full_size when capture had to cap the body', async () => {
1917
+ browserState.page.readNetworkCapture = vi.fn().mockResolvedValue([
1918
+ {
1919
+ url: 'https://api.example.com/huge',
1920
+ method: 'GET',
1921
+ responseStatus: 200,
1922
+ responseContentType: 'application/json',
1923
+ responsePreview: 'truncated-prefix-not-valid-json',
1924
+ responseBodyFullSize: 50_000_000,
1925
+ responseBodyTruncated: true,
1926
+ },
1927
+ ]);
1928
+ const program = createProgram('', '');
1929
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1930
+ consoleLogSpy.mockClear();
1931
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--detail', 'GET api.example.com/huge']);
1932
+ const out = lastJsonLog();
1933
+ expect(out.body_truncated).toBe(true);
1934
+ expect(out.body_full_size).toBe(50_000_000);
1935
+ expect(out.body_truncation_reason).toBe('capture-limit');
1936
+ });
1937
+ it('--max-body caps the emitted body and marks body_truncation_reason = max-body', async () => {
1938
+ const longString = 'x'.repeat(5000);
1939
+ browserState.page.readNetworkCapture = vi.fn().mockResolvedValue([
1940
+ {
1941
+ url: 'https://api.example.com/plain',
1942
+ method: 'GET',
1943
+ responseStatus: 200,
1944
+ responseContentType: 'text/plain',
1945
+ responsePreview: longString,
1946
+ },
1947
+ ]);
1948
+ const program = createProgram('', '');
1949
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1950
+ consoleLogSpy.mockClear();
1951
+ await program.parseAsync([
1952
+ 'node', 'opencli', 'browser', '--session', 'test', 'network',
1953
+ '--detail', 'GET api.example.com/plain',
1954
+ '--max-body', '100',
1955
+ ]);
1956
+ const out = lastJsonLog();
1957
+ expect(typeof out.body).toBe('string');
1958
+ expect(out.body).toHaveLength(100);
1959
+ expect(out.body_truncated).toBe(true);
1960
+ expect(out.body_truncation_reason).toBe('max-body');
1961
+ expect(out.body_full_size).toBe(5000);
1962
+ });
1963
+ it('--max-body leaves parsed JSON bodies untouched (no mid-object cut)', async () => {
1964
+ browserState.page.readNetworkCapture = vi.fn().mockResolvedValue([
1965
+ {
1966
+ url: 'https://api.example.com/json',
1967
+ method: 'GET',
1968
+ responseStatus: 200,
1969
+ responseContentType: 'application/json',
1970
+ responsePreview: JSON.stringify({ data: { user: { rest_id: 'u1' } } }),
1971
+ },
1972
+ ]);
1973
+ const program = createProgram('', '');
1974
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1975
+ consoleLogSpy.mockClear();
1976
+ await program.parseAsync([
1977
+ 'node', 'opencli', 'browser', '--session', 'test', 'network',
1978
+ '--detail', 'GET api.example.com/json',
1979
+ '--max-body', '10',
1980
+ ]);
1981
+ const out = lastJsonLog();
1982
+ // JSON body already parsed at capture time — --max-body only applies to
1983
+ // string bodies (which is where the agent-visible hazard lives).
1984
+ expect(out.body).toEqual({ data: { user: { rest_id: 'u1' } } });
1985
+ expect(out).not.toHaveProperty('body_truncated');
1986
+ });
1987
+ it('rejects non-numeric --max-body with invalid_max_body', async () => {
1988
+ browserState.page.readNetworkCapture = vi.fn().mockResolvedValue([
1989
+ {
1990
+ url: 'https://api.example.com/x',
1991
+ method: 'GET',
1992
+ responseStatus: 200,
1993
+ responseContentType: 'application/json',
1994
+ responsePreview: '{"a":1}',
1995
+ },
1996
+ ]);
1997
+ const program = createProgram('', '');
1998
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1999
+ consoleLogSpy.mockClear();
2000
+ await program.parseAsync([
2001
+ 'node', 'opencli', 'browser', '--session', 'test', 'network',
2002
+ '--detail', 'GET api.example.com/x',
2003
+ '--max-body', 'abc',
2004
+ ]);
2005
+ expect(lastJsonLog().error.code).toBe('invalid_max_body');
2006
+ expect(process.exitCode).toBeDefined();
2007
+ });
2008
+ it('--raw emits snake_case body_truncated / body_full_size, matching non-raw + detail', async () => {
2009
+ browserState.page.readNetworkCapture = vi.fn().mockResolvedValue([
2010
+ {
2011
+ url: 'https://api.example.com/huge',
2012
+ method: 'GET',
2013
+ responseStatus: 200,
2014
+ responseContentType: 'application/json',
2015
+ responsePreview: 'truncated-prefix',
2016
+ responseBodyFullSize: 20_000_000,
2017
+ responseBodyTruncated: true,
2018
+ },
2019
+ ]);
2020
+ const program = createProgram('', '');
2021
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--raw']);
2022
+ const out = lastJsonLog();
2023
+ expect(out.entries).toHaveLength(1);
2024
+ const entry = out.entries[0];
2025
+ expect(entry.body_truncated).toBe(true);
2026
+ expect(entry.body_full_size).toBe(20_000_000);
2027
+ // Internal camelCase must not leak into the agent-facing envelope.
2028
+ expect(entry).not.toHaveProperty('bodyTruncated');
2029
+ expect(entry).not.toHaveProperty('bodyFullSize');
2030
+ });
2031
+ });
2032
+ });
2033
+ describe('browser console command', () => {
2034
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
2035
+ beforeEach(() => {
2036
+ process.exitCode = undefined;
2037
+ consoleLogSpy.mockClear();
2038
+ mockBrowserConnect.mockClear();
2039
+ mockBrowserClose.mockReset().mockResolvedValue(undefined);
2040
+ const now = Date.now();
2041
+ browserState.page = {
2042
+ session: 'test',
2043
+ setActivePage: vi.fn(),
2044
+ getActivePage: vi.fn().mockReturnValue('tab-1'),
2045
+ tabs: vi.fn().mockResolvedValue([{ page: 'tab-1', active: true }]),
2046
+ consoleMessages: vi.fn().mockResolvedValue([
2047
+ { type: 'error', text: 'boom', timestamp: now },
2048
+ { type: 'log', text: 'ok', timestamp: now },
2049
+ { type: 'warning', text: 'old warning', timestamp: now - 180_000 },
2050
+ ]),
2051
+ };
2052
+ });
2053
+ function lastJsonLog() {
2054
+ const calls = consoleLogSpy.mock.calls;
2055
+ if (calls.length === 0)
2056
+ throw new Error('Expected at least one console.log call');
2057
+ const last = calls[calls.length - 1][0];
2058
+ if (typeof last !== 'string')
2059
+ throw new Error(`Expected string arg to console.log, got ${typeof last}`);
2060
+ return JSON.parse(last);
2061
+ }
2062
+ it('filters console messages by level and time window', async () => {
2063
+ const program = createProgram('', '');
2064
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'console', '--level', 'error', '--since', '120s']);
2065
+ const out = lastJsonLog();
2066
+ expect(out.count).toBe(1);
2067
+ expect(out.messages[0]).toMatchObject({ type: 'error', text: 'boom' });
2068
+ });
2069
+ });
2070
+ describe('browser get html command', () => {
2071
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
2072
+ function lastLogArg() {
2073
+ const calls = consoleLogSpy.mock.calls;
2074
+ if (calls.length === 0)
2075
+ throw new Error('expected console.log call');
2076
+ return calls[calls.length - 1][0];
2077
+ }
2078
+ function lastJsonLog() {
2079
+ const arg = lastLogArg();
2080
+ if (typeof arg !== 'string')
2081
+ throw new Error(`expected string arg, got ${typeof arg}`);
2082
+ return JSON.parse(arg);
2083
+ }
2084
+ beforeEach(() => {
2085
+ process.exitCode = undefined;
2086
+ process.env.OPENCLI_CACHE_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-html-'));
2087
+ consoleLogSpy.mockClear();
2088
+ mockBrowserConnect.mockClear();
2089
+ mockBrowserClose.mockReset().mockResolvedValue(undefined);
2090
+ browserState.page = {
2091
+ setActivePage: vi.fn(),
2092
+ getActivePage: vi.fn().mockReturnValue('tab-1'),
2093
+ tabs: vi.fn().mockResolvedValue([{ page: 'tab-1', active: true }]),
2094
+ evaluate: vi.fn(),
2095
+ };
2096
+ });
2097
+ it('returns full outerHTML by default with no truncation', async () => {
2098
+ const big = '<div>' + 'x'.repeat(100_000) + '</div>';
2099
+ browserState.page.evaluate.mockResolvedValueOnce({ kind: 'ok', html: big });
2100
+ const program = createProgram('', '');
2101
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html']);
2102
+ expect(lastLogArg()).toBe(big);
2103
+ });
2104
+ it('caps output with --max and prepends a visible truncation marker', async () => {
2105
+ const big = '<div>' + 'x'.repeat(500) + '</div>';
2106
+ browserState.page.evaluate.mockResolvedValueOnce({ kind: 'ok', html: big });
2107
+ const program = createProgram('', '');
2108
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html', '--max', '100']);
2109
+ const out = String(lastLogArg());
2110
+ expect(out.startsWith('<!-- opencli: truncated 100 of')).toBe(true);
2111
+ expect(out.length).toBeGreaterThan(100);
2112
+ expect(out.length).toBeLessThan(big.length);
2113
+ });
2114
+ it('rejects negative --max with invalid_max error', async () => {
2115
+ const program = createProgram('', '');
2116
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html', '--max', '-1']);
2117
+ expect(lastJsonLog().error.code).toBe('invalid_max');
2118
+ expect(process.exitCode).toBeDefined();
2119
+ expect(browserState.page.evaluate).not.toHaveBeenCalled();
2120
+ });
2121
+ it('rejects fractional --max with invalid_max error', async () => {
2122
+ const program = createProgram('', '');
2123
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html', '--max', '1.5']);
2124
+ expect(lastJsonLog().error.code).toBe('invalid_max');
2125
+ expect(process.exitCode).toBeDefined();
2126
+ expect(browserState.page.evaluate).not.toHaveBeenCalled();
2127
+ });
2128
+ it('rejects non-numeric --max (e.g. "10abc") with invalid_max error', async () => {
2129
+ const program = createProgram('', '');
2130
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html', '--max', '10abc']);
2131
+ expect(lastJsonLog().error.code).toBe('invalid_max');
2132
+ expect(process.exitCode).toBeDefined();
2133
+ expect(browserState.page.evaluate).not.toHaveBeenCalled();
2134
+ });
2135
+ it('--as json returns structured tree envelope', async () => {
2136
+ browserState.page.evaluate.mockResolvedValueOnce({
2137
+ selector: '.hero',
2138
+ matched: 1,
2139
+ tree: { tag: 'div', attrs: { class: 'hero' }, text: 'Hi', children: [] },
2140
+ });
2141
+ const program = createProgram('', '');
2142
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html', '--selector', '.hero', '--as', 'json']);
2143
+ const out = lastJsonLog();
2144
+ expect(out.matched).toBe(1);
2145
+ expect(out.tree.tag).toBe('div');
2146
+ expect(out.tree.attrs.class).toBe('hero');
2147
+ });
2148
+ it('--as json emits selector_not_found when matched is 0', async () => {
2149
+ browserState.page.evaluate.mockResolvedValueOnce({ selector: '.missing', matched: 0, tree: null });
2150
+ const program = createProgram('', '');
2151
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html', '--selector', '.missing', '--as', 'json']);
2152
+ expect(lastJsonLog().error.code).toBe('selector_not_found');
2153
+ expect(process.exitCode).toBeDefined();
2154
+ });
2155
+ it('raw mode emits selector_not_found when the selector matches nothing', async () => {
2156
+ browserState.page.evaluate.mockResolvedValueOnce({ kind: 'ok', html: null });
2157
+ const program = createProgram('', '');
2158
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html', '--selector', '.missing']);
2159
+ expect(lastJsonLog().error.code).toBe('selector_not_found');
2160
+ expect(process.exitCode).toBeDefined();
2161
+ });
2162
+ it('raw mode emits invalid_selector when the page rejects the selector syntax', async () => {
2163
+ browserState.page.evaluate.mockResolvedValueOnce({
2164
+ kind: 'invalid_selector',
2165
+ reason: "'##$@@' is not a valid selector",
2166
+ });
2167
+ const program = createProgram('', '');
2168
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html', '--selector', '##$@@']);
2169
+ const err = lastJsonLog().error;
2170
+ expect(err.code).toBe('invalid_selector');
2171
+ expect(err.message).toContain('##$@@');
2172
+ expect(err.message).toContain('not a valid selector');
2173
+ expect(process.exitCode).toBeDefined();
2174
+ });
2175
+ it('--as json emits invalid_selector when the page rejects the selector syntax', async () => {
2176
+ browserState.page.evaluate.mockResolvedValueOnce({
2177
+ selector: '##$@@',
2178
+ invalidSelector: true,
2179
+ reason: "'##$@@' is not a valid selector",
2180
+ });
2181
+ const program = createProgram('', '');
2182
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html', '--selector', '##$@@', '--as', 'json']);
2183
+ const err = lastJsonLog().error;
2184
+ expect(err.code).toBe('invalid_selector');
2185
+ expect(err.message).toContain('##$@@');
2186
+ expect(process.exitCode).toBeDefined();
2187
+ });
2188
+ it('rejects unknown --as format with invalid_format error', async () => {
2189
+ const program = createProgram('', '');
2190
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html', '--as', 'yaml']);
2191
+ expect(lastJsonLog().error.code).toBe('invalid_format');
2192
+ expect(process.exitCode).toBeDefined();
2193
+ });
2194
+ });
2195
+ // Shared helper for the selector-first describe blocks below.
2196
+ // Each block spies console.log, mocks the IPage surface it touches, and
2197
+ // parses the last stringified call to inspect the JSON envelope — the
2198
+ // canonical agent-facing contract for the selector-first commands.
2199
+ function installSelectorFirstTestHarness(label, pageOverrides) {
2200
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
2201
+ function lastLogArg() {
2202
+ const calls = consoleLogSpy.mock.calls;
2203
+ if (calls.length === 0)
2204
+ throw new Error('expected console.log call');
2205
+ return calls[calls.length - 1][0];
2206
+ }
2207
+ function lastJsonLog() {
2208
+ const arg = lastLogArg();
2209
+ if (typeof arg !== 'string')
2210
+ throw new Error(`expected string arg, got ${typeof arg}`);
2211
+ return JSON.parse(arg);
2212
+ }
2213
+ beforeEach(() => {
2214
+ process.exitCode = undefined;
2215
+ process.env.OPENCLI_CACHE_DIR = fs.mkdtempSync(path.join(os.tmpdir(), `opencli-${label}-`));
2216
+ consoleLogSpy.mockClear();
2217
+ mockBrowserConnect.mockClear();
2218
+ mockBrowserClose.mockReset().mockResolvedValue(undefined);
2219
+ browserState.page = {
2220
+ setActivePage: vi.fn(),
2221
+ getActivePage: vi.fn().mockReturnValue('tab-1'),
2222
+ tabs: vi.fn().mockResolvedValue([{ page: 'tab-1', active: true }]),
2223
+ session: 'test',
2224
+ ...pageOverrides(),
2225
+ };
2226
+ });
2227
+ return { lastJsonLog };
2228
+ }
2229
+ describe('browser find command', () => {
2230
+ const { lastJsonLog } = installSelectorFirstTestHarness('find', () => ({
2231
+ evaluate: vi.fn(),
2232
+ }));
2233
+ it('returns a {matches_n, entries} envelope for a matching selector', async () => {
2234
+ // `find` always returns numeric refs (existing on snapshot-tagged elements,
2235
+ // allocated on the spot for fresh matches) — see reviewer contract in
2236
+ // #opencli-browser msg 52c51eb6.
2237
+ browserState.page.evaluate.mockResolvedValueOnce({
2238
+ matches_n: 2,
2239
+ entries: [
2240
+ { nth: 0, ref: 5, tag: 'button', role: '', text: 'OK', attrs: { class: 'btn' }, visible: true },
2241
+ { nth: 1, ref: 17, tag: 'button', role: '', text: 'Cancel', attrs: { class: 'btn' }, visible: true },
2242
+ ],
2243
+ });
2244
+ const program = createProgram('', '');
2245
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'find', '--css', '.btn']);
2246
+ const out = lastJsonLog();
2247
+ expect(out.matches_n).toBe(2);
2248
+ expect(out.entries).toHaveLength(2);
2249
+ expect(out.entries[0].ref).toBe(5);
2250
+ expect(out.entries[1].ref).toBe(17);
2251
+ expect(process.exitCode).toBeUndefined();
2252
+ });
2253
+ it('finds elements by semantic role/name without requiring CSS', async () => {
2254
+ browserState.page.evaluate.mockResolvedValueOnce({
2255
+ matches_n: 1,
2256
+ entries: [
2257
+ { nth: 0, ref: 9, tag: 'button', role: 'button', text: 'Save', attrs: {}, visible: true },
2258
+ ],
2259
+ });
2260
+ const program = createProgram('', '');
2261
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'find', '--role', 'button', '--name', 'Save']);
2262
+ const js = browserState.page.evaluate.mock.calls[0][0];
2263
+ expect(js).toContain('CRITERIA');
2264
+ expect(js).toContain('function accessibleName');
2265
+ expect(lastJsonLog()).toEqual({
2266
+ matches_n: 1,
2267
+ entries: [
2268
+ { nth: 0, ref: 9, tag: 'button', role: 'button', text: 'Save', attrs: {}, visible: true },
2269
+ ],
2270
+ });
2271
+ });
2272
+ it('forwards --limit / --text-max into the generated JS', async () => {
2273
+ browserState.page.evaluate.mockResolvedValueOnce({ matches_n: 0, entries: [] });
2274
+ const program = createProgram('', '');
2275
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'find', '--css', '.btn', '--limit', '3', '--text-max', '20']);
2276
+ const js = browserState.page.evaluate.mock.calls[0][0];
2277
+ expect(js).toContain('LIMIT = 3');
2278
+ expect(js).toContain('TEXT_MAX = 20');
2279
+ });
2280
+ it('emits invalid_selector envelope when the page rejects selector syntax', async () => {
2281
+ browserState.page.evaluate.mockResolvedValueOnce({
2282
+ error: { code: 'invalid_selector', message: 'Invalid CSS selector: ">>>"', hint: 'Check the selector syntax.' },
2283
+ });
2284
+ const program = createProgram('', '');
2285
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'find', '--css', '>>>']);
2286
+ expect(lastJsonLog().error.code).toBe('invalid_selector');
2287
+ expect(process.exitCode).toBeDefined();
2288
+ });
2289
+ it('emits selector_not_found envelope when the selector matches nothing', async () => {
2290
+ browserState.page.evaluate.mockResolvedValueOnce({
2291
+ error: { code: 'selector_not_found', message: 'CSS selector ".missing" matched 0 elements', hint: 'Use browser state to inspect the page.' },
2292
+ });
2293
+ const program = createProgram('', '');
2294
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'find', '--css', '.missing']);
2295
+ expect(lastJsonLog().error.code).toBe('selector_not_found');
2296
+ expect(process.exitCode).toBeDefined();
2297
+ });
2298
+ it('rejects missing --css with usage_error (no evaluate call)', async () => {
2299
+ const program = createProgram('', '');
2300
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'find']);
2301
+ expect(lastJsonLog().error.code).toBe('usage_error');
2302
+ expect(browserState.page.evaluate).not.toHaveBeenCalled();
2303
+ expect(process.exitCode).toBeDefined();
2304
+ });
2305
+ it('rejects malformed --limit with usage_error (no evaluate call)', async () => {
2306
+ const program = createProgram('', '');
2307
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'find', '--css', '.btn', '--limit', 'abc']);
2308
+ expect(lastJsonLog().error.code).toBe('usage_error');
2309
+ expect(browserState.page.evaluate).not.toHaveBeenCalled();
2310
+ expect(process.exitCode).toBeDefined();
2311
+ });
2312
+ });
2313
+ describe('browser get text/value/attributes commands', () => {
2314
+ const { lastJsonLog } = installSelectorFirstTestHarness('get-sel', () => ({
2315
+ evaluate: vi.fn(),
2316
+ }));
2317
+ it('emits {value, matches_n, match_level} envelope for a numeric ref', async () => {
2318
+ const evalMock = browserState.page.evaluate;
2319
+ // 1st call: resolveTargetJs -> { ok: true, matches_n: 1, match_level: 'exact' }
2320
+ evalMock.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' });
2321
+ // 2nd call: getTextResolvedJs -> the element's text
2322
+ evalMock.mockResolvedValueOnce('Hello world');
2323
+ const program = createProgram('', '');
2324
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'text', '7']);
2325
+ expect(lastJsonLog()).toEqual({ value: 'Hello world', matches_n: 1, match_level: 'exact' });
2326
+ });
2327
+ it('resolves a semantic locator to a ref before get text', async () => {
2328
+ const evalMock = browserState.page.evaluate;
2329
+ evalMock.mockResolvedValueOnce({
2330
+ matches_n: 1,
2331
+ entries: [
2332
+ { nth: 0, ref: 12, tag: 'button', role: 'button', text: 'Save', attrs: {}, visible: true },
2333
+ ],
2334
+ });
2335
+ evalMock.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' });
2336
+ evalMock.mockResolvedValueOnce('Save');
2337
+ const program = createProgram('', '');
2338
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'text', '--role', 'button', '--name', 'Save']);
2339
+ expect(evalMock.mock.calls[0][0]).toContain('function accessibleName');
2340
+ expect(evalMock.mock.calls[1][0]).toContain('const ref = "12"');
2341
+ expect(lastJsonLog()).toEqual({ value: 'Save', matches_n: 1, match_level: 'exact' });
2342
+ });
2343
+ it('reports total_matches when semantic get reads the first of multiple matches', async () => {
2344
+ const evalMock = browserState.page.evaluate;
2345
+ evalMock.mockResolvedValueOnce({
2346
+ matches_n: 3,
2347
+ entries: [
2348
+ { nth: 0, ref: 12, tag: 'button', role: 'button', text: 'Save', attrs: {}, visible: true },
2349
+ { nth: 1, ref: 13, tag: 'button', role: 'button', text: 'Save draft', attrs: {}, visible: true },
2350
+ { nth: 2, ref: 14, tag: 'button', role: 'button', text: 'Save copy', attrs: {}, visible: true },
2351
+ ],
2352
+ });
2353
+ evalMock.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' });
2354
+ evalMock.mockResolvedValueOnce('Save');
2355
+ const program = createProgram('', '');
2356
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'text', '--role', 'button', '--name', 'Save']);
2357
+ expect(evalMock.mock.calls[0][0]).toContain('const LIMIT = 6');
2358
+ expect(evalMock.mock.calls[1][0]).toContain('const ref = "12"');
2359
+ expect(lastJsonLog()).toEqual({ value: 'Save', matches_n: 1, match_level: 'exact', total_matches: 3 });
2360
+ });
2361
+ it('reports matches_n on multi-match CSS (read path: first match wins)', async () => {
2362
+ const evalMock = browserState.page.evaluate;
2363
+ evalMock.mockResolvedValueOnce({ ok: true, matches_n: 3, match_level: 'exact' });
2364
+ evalMock.mockResolvedValueOnce('first');
2365
+ const program = createProgram('', '');
2366
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'text', '.btn']);
2367
+ expect(lastJsonLog()).toEqual({ value: 'first', matches_n: 3, match_level: 'exact' });
2368
+ });
2369
+ it('parses the attributes payload back into a real object', async () => {
2370
+ const evalMock = browserState.page.evaluate;
2371
+ evalMock.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' });
2372
+ // getAttributesResolvedJs returns a JSON-encoded string — the CLI must parse it
2373
+ evalMock.mockResolvedValueOnce(JSON.stringify({ id: 'nav', class: 'hero' }));
2374
+ const program = createProgram('', '');
2375
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'attributes', '#nav']);
2376
+ const out = lastJsonLog();
2377
+ expect(out.matches_n).toBe(1);
2378
+ expect(out.match_level).toBe('exact');
2379
+ expect(out.value).toEqual({ id: 'nav', class: 'hero' });
2380
+ });
2381
+ it('propagates selector_not_found from the resolver as an error envelope', async () => {
2382
+ browserState.page.evaluate.mockResolvedValueOnce({
2383
+ ok: false,
2384
+ code: 'selector_not_found',
2385
+ message: 'CSS selector ".missing" matched 0 elements',
2386
+ hint: 'Try a less specific selector.',
2387
+ });
2388
+ const program = createProgram('', '');
2389
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'text', '.missing']);
2390
+ expect(lastJsonLog().error.code).toBe('selector_not_found');
2391
+ expect(process.exitCode).toBeDefined();
2392
+ });
2393
+ it('forwards --nth into the resolver opts and reports matches_n', async () => {
2394
+ const evalMock = browserState.page.evaluate;
2395
+ evalMock.mockResolvedValueOnce({ ok: true, matches_n: 4, match_level: 'exact' });
2396
+ evalMock.mockResolvedValueOnce('second');
2397
+ const program = createProgram('', '');
2398
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'value', '.btn', '--nth', '1']);
2399
+ const resolveJs = evalMock.mock.calls[0][0];
2400
+ // resolveTargetJs embeds nth as a raw number literal; look for the binding
2401
+ expect(resolveJs).toContain('const nth = 1');
2402
+ expect(lastJsonLog()).toEqual({ value: 'second', matches_n: 4, match_level: 'exact' });
2403
+ });
2404
+ it('rejects malformed --nth with usage_error before touching the page', async () => {
2405
+ const program = createProgram('', '');
2406
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'text', '.btn', '--nth', 'abc']);
2407
+ expect(lastJsonLog().error.code).toBe('usage_error');
2408
+ expect(browserState.page.evaluate).not.toHaveBeenCalled();
2409
+ expect(process.exitCode).toBeDefined();
2410
+ });
2411
+ });
2412
+ describe('browser click/type commands', () => {
2413
+ const { lastJsonLog } = installSelectorFirstTestHarness('click-type', () => ({
2414
+ evaluate: vi.fn().mockResolvedValue(false),
2415
+ click: vi.fn().mockResolvedValue({ matches_n: 1, match_level: 'exact' }),
2416
+ dblClick: vi.fn().mockResolvedValue({ matches_n: 1, match_level: 'exact' }),
2417
+ hover: vi.fn().mockResolvedValue({ matches_n: 1, match_level: 'exact' }),
2418
+ focus: vi.fn().mockResolvedValue({ focused: true, matches_n: 1, match_level: 'exact' }),
2419
+ setChecked: vi.fn().mockResolvedValue({ checked: true, changed: true, matches_n: 1, match_level: 'exact', kind: 'checkbox' }),
2420
+ uploadFiles: vi.fn().mockResolvedValue({
2421
+ uploaded: true,
2422
+ files: 1,
2423
+ file_names: ['receipt.pdf'],
2424
+ target: '#file',
2425
+ matches_n: 1,
2426
+ match_level: 'exact',
2427
+ multiple: false,
2428
+ }),
2429
+ drag: vi.fn().mockResolvedValue({
2430
+ dragged: true,
2431
+ source: '#card',
2432
+ target: '#lane',
2433
+ source_matches_n: 1,
2434
+ target_matches_n: 1,
2435
+ source_match_level: 'exact',
2436
+ target_match_level: 'exact',
2437
+ }),
2438
+ typeText: vi.fn().mockResolvedValue({ matches_n: 1, match_level: 'exact' }),
2439
+ fillText: vi.fn().mockResolvedValue({
2440
+ filled: true,
2441
+ verified: true,
2442
+ expected: '',
2443
+ actual: '',
2444
+ length: 0,
2445
+ matches_n: 1,
2446
+ match_level: 'exact',
2447
+ mode: 'input',
2448
+ }),
2449
+ wait: vi.fn().mockResolvedValue(undefined),
2450
+ }));
2451
+ it('emits {clicked, target, matches_n, match_level} on success', async () => {
2452
+ browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2453
+ const program = createProgram('', '');
2454
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'click', '#save']);
2455
+ expect(browserState.page.click).toHaveBeenCalledWith('#save', {});
2456
+ expect(lastJsonLog()).toEqual({ clicked: true, target: '#save', matches_n: 1, match_level: 'exact' });
2457
+ });
2458
+ it('clicks a unique semantic locator without a prior state call', async () => {
2459
+ browserState.page.evaluate.mockResolvedValueOnce({
2460
+ matches_n: 1,
2461
+ entries: [
2462
+ { nth: 0, ref: 23, tag: 'button', role: 'button', text: 'Submit', attrs: {}, visible: true },
2463
+ ],
2464
+ });
2465
+ browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2466
+ const program = createProgram('', '');
2467
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'click', '--role', 'button', '--name', 'Submit']);
2468
+ expect(browserState.page.click).toHaveBeenCalledWith('23', {});
2469
+ expect(lastJsonLog()).toEqual({ clicked: true, target: '23', matches_n: 1, match_level: 'exact' });
2470
+ });
2471
+ it('rejects ambiguous semantic locators before write actions', async () => {
2472
+ browserState.page.evaluate.mockResolvedValueOnce({
2473
+ matches_n: 2,
2474
+ entries: [
2475
+ { nth: 0, ref: 1, tag: 'button', role: 'button', text: 'Save', attrs: {}, visible: true },
2476
+ { nth: 1, ref: 2, tag: 'button', role: 'button', text: 'Save draft', attrs: {}, visible: true },
2477
+ ],
2478
+ });
2479
+ const program = createProgram('', '');
2480
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'click', '--role', 'button', '--name', 'Save']);
2481
+ const err = lastJsonLog().error;
2482
+ expect(err.code).toBe('semantic_ambiguous');
2483
+ expect(err.matches_n).toBe(2);
2484
+ expect(browserState.page.click).not.toHaveBeenCalled();
2485
+ expect(process.exitCode).toBeDefined();
2486
+ });
2487
+ it('hover: resolves a semantic locator before moving the mouse', async () => {
2488
+ browserState.page.evaluate.mockResolvedValueOnce({
2489
+ matches_n: 1,
2490
+ entries: [
2491
+ { nth: 0, ref: 31, tag: 'button', role: 'button', text: 'Settings', attrs: {}, visible: true },
2492
+ ],
2493
+ });
2494
+ const program = createProgram('', '');
2495
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'hover', '--role', 'button', '--name', 'Settings']);
2496
+ expect(browserState.page.hover).toHaveBeenCalledWith('31', {});
2497
+ expect(lastJsonLog()).toEqual({ hovered: true, target: '31', matches_n: 1, match_level: 'exact' });
2498
+ });
2499
+ it('check: resolves a semantic locator before setting checked state', async () => {
2500
+ browserState.page.evaluate.mockResolvedValueOnce({
2501
+ matches_n: 1,
2502
+ entries: [
2503
+ { nth: 0, ref: 32, tag: 'input', role: 'checkbox', text: 'Accept', attrs: {}, visible: true },
2504
+ ],
2505
+ });
2506
+ browserState.page.setChecked.mockResolvedValueOnce({ checked: true, changed: false, matches_n: 1, match_level: 'exact', kind: 'checkbox' });
2507
+ const program = createProgram('', '');
2508
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'check', '--role', 'checkbox', '--name', 'Accept']);
2509
+ expect(browserState.page.setChecked).toHaveBeenCalledWith('32', true, {});
2510
+ expect(lastJsonLog()).toEqual({ checked: true, changed: false, target: '32', matches_n: 1, match_level: 'exact', kind: 'checkbox' });
2511
+ });
2512
+ it('upload: treats the first positional as a file when using semantic locator flags', async () => {
2513
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-upload-semantic-'));
2514
+ const file = path.join(dir, 'receipt.pdf');
2515
+ fs.writeFileSync(file, 'pdf');
2516
+ browserState.page.evaluate.mockResolvedValueOnce({
2517
+ matches_n: 1,
2518
+ entries: [
2519
+ { nth: 0, ref: 33, tag: 'input', role: 'button', text: 'Upload receipt', attrs: {}, visible: true },
2520
+ ],
2521
+ });
2522
+ browserState.page.uploadFiles.mockResolvedValueOnce({
2523
+ uploaded: true,
2524
+ files: 1,
2525
+ file_names: ['receipt.pdf'],
2526
+ target: '33',
2527
+ matches_n: 1,
2528
+ match_level: 'exact',
2529
+ multiple: false,
2530
+ });
2531
+ const program = createProgram('', '');
2532
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'upload', '--role', 'button', '--name', 'Upload receipt', file]);
2533
+ expect(browserState.page.uploadFiles).toHaveBeenCalledWith('33', [file], {});
2534
+ expect(lastJsonLog()).toMatchObject({ uploaded: true, target: '33', files: 1 });
2535
+ });
2536
+ it('type: treats the first positional as text when using semantic locator flags', async () => {
2537
+ browserState.page.evaluate
2538
+ .mockResolvedValueOnce({
2539
+ matches_n: 1,
2540
+ entries: [
2541
+ { nth: 0, ref: 34, tag: 'input', role: 'textbox', text: '', attrs: {}, visible: true },
2542
+ ],
2543
+ })
2544
+ .mockResolvedValueOnce(false);
2545
+ browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2546
+ browserState.page.typeText.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2547
+ const program = createProgram('', '');
2548
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'type', '--label', 'Email', 'me@example.com']);
2549
+ expect(browserState.page.click).toHaveBeenCalledWith('34', {});
2550
+ expect(browserState.page.typeText).toHaveBeenCalledWith('34', 'me@example.com', {});
2551
+ expect(lastJsonLog()).toMatchObject({ typed: true, target: '34', text: 'me@example.com' });
2552
+ });
2553
+ it('fill: treats the first positional as text when using semantic locator flags', async () => {
2554
+ browserState.page.evaluate.mockResolvedValueOnce({
2555
+ matches_n: 1,
2556
+ entries: [
2557
+ { nth: 0, ref: 35, tag: 'input', role: 'textbox', text: '', attrs: {}, visible: true },
2558
+ ],
2559
+ });
2560
+ browserState.page.fillText.mockResolvedValueOnce({
2561
+ filled: true,
2562
+ verified: true,
2563
+ expected: 'me@example.com',
2564
+ actual: 'me@example.com',
2565
+ length: 14,
2566
+ matches_n: 1,
2567
+ match_level: 'exact',
2568
+ });
2569
+ const program = createProgram('', '');
2570
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'fill', '--label', 'Email', 'me@example.com']);
2571
+ expect(browserState.page.fillText).toHaveBeenCalledWith('35', 'me@example.com', {});
2572
+ expect(lastJsonLog()).toMatchObject({ filled: true, verified: true, target: '35', text: 'me@example.com' });
2573
+ });
2574
+ it('drag: resolves source and target from prefixed semantic locators', async () => {
2575
+ browserState.page.evaluate
2576
+ .mockResolvedValueOnce({
2577
+ matches_n: 1,
2578
+ entries: [
2579
+ { nth: 0, ref: 40, tag: 'div', role: 'button', text: 'Card A', attrs: {}, visible: true },
2580
+ ],
2581
+ })
2582
+ .mockResolvedValueOnce({
2583
+ matches_n: 1,
2584
+ entries: [
2585
+ { nth: 0, ref: 41, tag: 'div', role: 'region', text: 'Done', attrs: {}, visible: true },
2586
+ ],
2587
+ });
2588
+ browserState.page.drag.mockResolvedValueOnce({
2589
+ dragged: true,
2590
+ source: '40',
2591
+ target: '41',
2592
+ source_matches_n: 1,
2593
+ target_matches_n: 1,
2594
+ source_match_level: 'exact',
2595
+ target_match_level: 'exact',
2596
+ });
2597
+ const program = createProgram('', '');
2598
+ await program.parseAsync([
2599
+ 'node',
2600
+ 'opencli',
2601
+ 'browser',
2602
+ '--session',
2603
+ 'test',
2604
+ 'drag',
2605
+ '--from-role',
2606
+ 'button',
2607
+ '--from-name',
2608
+ 'Card A',
2609
+ '--to-role',
2610
+ 'region',
2611
+ '--to-name',
2612
+ 'Done',
2613
+ ]);
2614
+ expect(browserState.page.drag).toHaveBeenCalledWith('40', '41', { from: {}, to: {} });
2615
+ expect(lastJsonLog()).toMatchObject({ dragged: true, source: '40', target: '41' });
2616
+ });
2617
+ it('surfaces match_level=stable when resolver falls back to fingerprint match', async () => {
2618
+ browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'stable' });
2619
+ const program = createProgram('', '');
2620
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'click', '7']);
2621
+ expect(lastJsonLog()).toEqual({ clicked: true, target: '7', matches_n: 1, match_level: 'stable' });
2622
+ });
2623
+ it('forwards --nth as ResolveOptions.nth to page.click', async () => {
2624
+ browserState.page.click.mockResolvedValueOnce({ matches_n: 3, match_level: 'exact' });
2625
+ const program = createProgram('', '');
2626
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'click', '.btn', '--nth', '2']);
2627
+ expect(browserState.page.click).toHaveBeenCalledWith('.btn', { nth: 2 });
2628
+ expect(lastJsonLog()).toEqual({ clicked: true, target: '.btn', matches_n: 3, match_level: 'exact' });
2629
+ });
2630
+ it('surfaces selector_ambiguous from page.click as an error envelope', async () => {
2631
+ browserState.page.click.mockRejectedValueOnce(new TargetError({
2632
+ code: 'selector_ambiguous',
2633
+ message: 'CSS selector ".btn" matched 3 elements; clicks require a unique target.',
2634
+ hint: 'Pass --nth <n> to pick one (0-based).',
2635
+ matches_n: 3,
2636
+ }));
2637
+ const program = createProgram('', '');
2638
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'click', '.btn']);
2639
+ const err = lastJsonLog().error;
2640
+ expect(err.code).toBe('selector_ambiguous');
2641
+ expect(err.matches_n).toBe(3);
2642
+ expect(process.exitCode).toBeDefined();
2643
+ });
2644
+ it('surfaces selector_nth_out_of_range from page.click as an error envelope', async () => {
2645
+ browserState.page.click.mockRejectedValueOnce(new TargetError({
2646
+ code: 'selector_nth_out_of_range',
2647
+ message: '--nth 99 is out of range for CSS selector ".btn" (matches_n=3).',
2648
+ hint: 'Pick an index in [0, 2].',
2649
+ matches_n: 3,
2650
+ }));
2651
+ const program = createProgram('', '');
2652
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'click', '.btn', '--nth', '99']);
2653
+ expect(lastJsonLog().error.code).toBe('selector_nth_out_of_range');
2654
+ expect(process.exitCode).toBeDefined();
2655
+ });
2656
+ it('rejects malformed --nth on click with usage_error before touching the page', async () => {
2657
+ const program = createProgram('', '');
2658
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'click', '.btn', '--nth', 'abc']);
2659
+ expect(lastJsonLog().error.code).toBe('usage_error');
2660
+ expect(browserState.page.click).not.toHaveBeenCalled();
2661
+ expect(process.exitCode).toBeDefined();
2662
+ });
2663
+ it('hover: delegates to page.hover and emits a structured envelope', async () => {
2664
+ browserState.page.hover.mockResolvedValueOnce({ matches_n: 2, match_level: 'exact' });
2665
+ const program = createProgram('', '');
2666
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'hover', '.menu', '--nth', '1']);
2667
+ expect(browserState.page.hover).toHaveBeenCalledWith('.menu', { nth: 1 });
2668
+ expect(lastJsonLog()).toEqual({ hovered: true, target: '.menu', matches_n: 2, match_level: 'exact' });
2669
+ });
2670
+ it('focus: delegates to page.focus and reports whether the element took focus', async () => {
2671
+ browserState.page.focus.mockResolvedValueOnce({ focused: true, matches_n: 1, match_level: 'stable' });
2672
+ const program = createProgram('', '');
2673
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'focus', '7']);
2674
+ expect(browserState.page.focus).toHaveBeenCalledWith('7', {});
2675
+ expect(lastJsonLog()).toEqual({ focused: true, target: '7', matches_n: 1, match_level: 'stable' });
2676
+ });
2677
+ it('dblclick: delegates to page.dblClick and emits a structured envelope', async () => {
2678
+ browserState.page.dblClick.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2679
+ const program = createProgram('', '');
2680
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'dblclick', '#row']);
2681
+ expect(browserState.page.dblClick).toHaveBeenCalledWith('#row', {});
2682
+ expect(lastJsonLog()).toEqual({ dblclicked: true, target: '#row', matches_n: 1, match_level: 'exact' });
2683
+ });
2684
+ it('check: ensures target is checked through page.setChecked', async () => {
2685
+ browserState.page.setChecked.mockResolvedValueOnce({
2686
+ checked: true,
2687
+ changed: true,
2688
+ matches_n: 2,
2689
+ match_level: 'exact',
2690
+ kind: 'checkbox',
2691
+ });
2692
+ const program = createProgram('', '');
2693
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'check', '.todo', '--nth', '1']);
2694
+ expect(browserState.page.setChecked).toHaveBeenCalledWith('.todo', true, { nth: 1 });
2695
+ expect(lastJsonLog()).toEqual({ checked: true, changed: true, target: '.todo', matches_n: 2, match_level: 'exact', kind: 'checkbox' });
2696
+ });
2697
+ it('uncheck: ensures target is unchecked through page.setChecked', async () => {
2698
+ browserState.page.setChecked.mockResolvedValueOnce({
2699
+ checked: false,
2700
+ changed: false,
2701
+ matches_n: 1,
2702
+ match_level: 'stable',
2703
+ kind: 'checkbox',
2704
+ });
2705
+ const program = createProgram('', '');
2706
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'uncheck', '#subscribe']);
2707
+ expect(browserState.page.setChecked).toHaveBeenCalledWith('#subscribe', false, {});
2708
+ expect(lastJsonLog()).toEqual({ checked: false, changed: false, target: '#subscribe', matches_n: 1, match_level: 'stable', kind: 'checkbox' });
2709
+ });
2710
+ it('upload: validates local files and delegates to page.uploadFiles', async () => {
2711
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-upload-'));
2712
+ const file = path.join(dir, 'receipt.pdf');
2713
+ fs.writeFileSync(file, 'pdf');
2714
+ browserState.page.uploadFiles.mockResolvedValueOnce({
2715
+ uploaded: true,
2716
+ files: 1,
2717
+ file_names: ['receipt.pdf'],
2718
+ target: '#file',
2719
+ matches_n: 1,
2720
+ match_level: 'exact',
2721
+ multiple: false,
2722
+ });
2723
+ const program = createProgram('', '');
2724
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'upload', '#file', file]);
2725
+ expect(browserState.page.uploadFiles).toHaveBeenCalledWith('#file', [file], {});
2726
+ expect(lastJsonLog()).toEqual({
2727
+ uploaded: true,
2728
+ files: 1,
2729
+ file_names: ['receipt.pdf'],
2730
+ target: '#file',
2731
+ matches_n: 1,
2732
+ match_level: 'exact',
2733
+ multiple: false,
2734
+ });
2735
+ });
2736
+ it('upload: rejects missing files before touching the page', async () => {
2737
+ const program = createProgram('', '');
2738
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'upload', '#file', '/tmp/opencli-missing-file']);
2739
+ expect(lastJsonLog().error.code).toBe('file_not_found');
2740
+ expect(browserState.page.uploadFiles).not.toHaveBeenCalled();
2741
+ expect(process.exitCode).toBeDefined();
2742
+ });
2743
+ it('drag: delegates to page.drag and forwards source/target nth options', async () => {
2744
+ browserState.page.drag.mockResolvedValueOnce({
2745
+ dragged: true,
2746
+ source: '.card',
2747
+ target: '.lane',
2748
+ source_matches_n: 3,
2749
+ target_matches_n: 2,
2750
+ source_match_level: 'exact',
2751
+ target_match_level: 'stable',
2752
+ });
2753
+ const program = createProgram('', '');
2754
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'drag', '.card', '.lane', '--from-nth', '2', '--to-nth', '1']);
2755
+ expect(browserState.page.drag).toHaveBeenCalledWith('.card', '.lane', { from: { nth: 2 }, to: { nth: 1 } });
2756
+ expect(lastJsonLog()).toEqual({
2757
+ dragged: true,
2758
+ source: '.card',
2759
+ target: '.lane',
2760
+ source_matches_n: 3,
2761
+ target_matches_n: 2,
2762
+ source_match_level: 'exact',
2763
+ target_match_level: 'stable',
2764
+ });
2765
+ });
2766
+ it('drag: rejects malformed --from-nth before touching the page', async () => {
2767
+ const program = createProgram('', '');
2768
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'drag', '.card', '.lane', '--from-nth', 'abc']);
2769
+ expect(lastJsonLog().error.code).toBe('usage_error');
2770
+ expect(browserState.page.drag).not.toHaveBeenCalled();
2771
+ expect(process.exitCode).toBeDefined();
2772
+ });
2773
+ it('type: clicks, waits, then typeText — emits {typed, text, target, matches_n, match_level, autocomplete}', async () => {
2774
+ browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2775
+ browserState.page.typeText.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2776
+ browserState.page.evaluate.mockResolvedValueOnce(false); // isAutocomplete
2777
+ const program = createProgram('', '');
2778
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'type', '#q', 'hello']);
2779
+ expect(browserState.page.click).toHaveBeenCalledWith('#q', {});
2780
+ expect(browserState.page.wait).toHaveBeenCalledWith(0.3);
2781
+ expect(browserState.page.typeText).toHaveBeenCalledWith('#q', 'hello', {});
2782
+ expect(lastJsonLog()).toEqual({
2783
+ typed: true, text: 'hello', target: '#q', matches_n: 1, match_level: 'exact', autocomplete: false,
2784
+ });
2785
+ });
2786
+ it('type: waits an extra 0.4s when the input reports autocomplete=true', async () => {
2787
+ browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2788
+ browserState.page.typeText.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2789
+ browserState.page.evaluate.mockResolvedValueOnce(true);
2790
+ const program = createProgram('', '');
2791
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'type', '#q', 'hi']);
2792
+ const waitCalls = browserState.page.wait.mock.calls;
2793
+ expect(waitCalls).toContainEqual([0.3]);
2794
+ expect(waitCalls).toContainEqual([0.4]);
2795
+ expect(lastJsonLog().autocomplete).toBe(true);
2796
+ expect(lastJsonLog().match_level).toBe('exact');
2797
+ });
2798
+ it('type: surfaces match_level=reidentified when ref had to be reidentified by fingerprint', async () => {
2799
+ browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'reidentified' });
2800
+ browserState.page.typeText.mockResolvedValueOnce({ matches_n: 1, match_level: 'reidentified' });
2801
+ browserState.page.evaluate.mockResolvedValueOnce(false);
2802
+ const program = createProgram('', '');
2803
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'type', '9', 'hi']);
2804
+ // The typeText call is the authoritative match_level source for the `type` envelope.
2805
+ expect(lastJsonLog().match_level).toBe('reidentified');
2806
+ });
2807
+ it('type: forwards --nth to both click and typeText', async () => {
2808
+ browserState.page.click.mockResolvedValueOnce({ matches_n: 5, match_level: 'exact' });
2809
+ browserState.page.typeText.mockResolvedValueOnce({ matches_n: 5, match_level: 'exact' });
2810
+ const program = createProgram('', '');
2811
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'type', '.field', 'x', '--nth', '3']);
2812
+ expect(browserState.page.click).toHaveBeenCalledWith('.field', { nth: 3 });
2813
+ expect(browserState.page.typeText).toHaveBeenCalledWith('.field', 'x', { nth: 3 });
2814
+ });
2815
+ it('fill: delegates exact raw text to page.fillText and emits verification details', async () => {
2816
+ browserState.page.fillText.mockResolvedValueOnce({
2817
+ filled: true,
2818
+ verified: true,
2819
+ expected: 'line1\\n/ / raw',
2820
+ actual: 'line1\\n/ / raw',
2821
+ length: 14,
2822
+ matches_n: 1,
2823
+ match_level: 'exact',
2824
+ mode: 'textarea',
2825
+ });
2826
+ const program = createProgram('', '');
2827
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'fill', '#msg', 'line1\\n/ / raw']);
2828
+ expect(browserState.page.fillText).toHaveBeenCalledWith('#msg', 'line1\\n/ / raw', {});
2829
+ expect(lastJsonLog()).toEqual({
2830
+ filled: true,
2831
+ verified: true,
2832
+ target: '#msg',
2833
+ text: 'line1\\n/ / raw',
2834
+ actual: 'line1\\n/ / raw',
2835
+ length: 14,
2836
+ matches_n: 1,
2837
+ match_level: 'exact',
2838
+ mode: 'textarea',
2839
+ });
2840
+ expect(process.exitCode).toBeUndefined();
2841
+ });
2842
+ it('fill: sets a non-zero exit code when verification fails', async () => {
2843
+ browserState.page.fillText.mockResolvedValueOnce({
2844
+ filled: true,
2845
+ verified: false,
2846
+ expected: 'expected',
2847
+ actual: 'actual',
2848
+ length: 6,
2849
+ matches_n: 1,
2850
+ match_level: 'exact',
2851
+ });
2852
+ const program = createProgram('', '');
2853
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'fill', '#msg', 'expected']);
2854
+ expect(lastJsonLog()).toEqual({
2855
+ filled: true,
2856
+ verified: false,
2857
+ target: '#msg',
2858
+ text: 'expected',
2859
+ actual: 'actual',
2860
+ length: 6,
2861
+ matches_n: 1,
2862
+ match_level: 'exact',
2863
+ });
2864
+ expect(process.exitCode).toBeDefined();
2865
+ });
2866
+ it('fill: forwards --nth to page.fillText', async () => {
2867
+ const program = createProgram('', '');
2868
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'fill', '.field', 'x', '--nth', '2']);
2869
+ expect(browserState.page.fillText).toHaveBeenCalledWith('.field', 'x', { nth: 2 });
2870
+ });
2871
+ });
2872
+ describe('browser select command', () => {
2873
+ const { lastJsonLog } = installSelectorFirstTestHarness('select', () => ({
2874
+ evaluate: vi.fn(),
2875
+ }));
2876
+ it('emits {selected, target, matches_n, match_level} on success', async () => {
2877
+ const evalMock = browserState.page.evaluate;
2878
+ evalMock.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' });
2879
+ evalMock.mockResolvedValueOnce({ selected: 'US' });
2880
+ const program = createProgram('', '');
2881
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'select', '#country', 'US']);
2882
+ expect(lastJsonLog()).toEqual({ selected: 'US', target: '#country', matches_n: 1, match_level: 'exact' });
2883
+ });
2884
+ it('maps "Not a <select>" to a not_a_select error envelope', async () => {
2885
+ const evalMock = browserState.page.evaluate;
2886
+ evalMock.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' });
2887
+ evalMock.mockResolvedValueOnce({ error: 'Not a <select>' });
2888
+ const program = createProgram('', '');
2889
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'select', '#not-select', 'US']);
2890
+ const err = lastJsonLog().error;
2891
+ expect(err.code).toBe('not_a_select');
2892
+ expect(err.matches_n).toBe(1);
2893
+ expect(process.exitCode).toBeDefined();
2894
+ });
2895
+ it('maps missing-option failures to an option_not_found envelope with available list', async () => {
2896
+ const evalMock = browserState.page.evaluate;
2897
+ evalMock.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' });
2898
+ evalMock.mockResolvedValueOnce({ error: 'Option "XX" not found', available: ['US', 'CA'] });
2899
+ const program = createProgram('', '');
2900
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'select', '#country', 'XX']);
2901
+ const err = lastJsonLog().error;
2902
+ expect(err.code).toBe('option_not_found');
2903
+ expect(err.available).toEqual(['US', 'CA']);
2904
+ expect(process.exitCode).toBeDefined();
2905
+ });
2906
+ it('select: treats the first positional as option when using semantic locator flags', async () => {
2907
+ const evalMock = browserState.page.evaluate;
2908
+ evalMock
2909
+ .mockResolvedValueOnce({
2910
+ matches_n: 1,
2911
+ entries: [
2912
+ { nth: 0, ref: 36, tag: 'select', role: 'combobox', text: 'Country', attrs: {}, visible: true },
2913
+ ],
2914
+ })
2915
+ .mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' })
2916
+ .mockResolvedValueOnce({ selected: 'Uruguay' });
2917
+ const program = createProgram('', '');
2918
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'select', '--label', 'Country', 'Uruguay']);
2919
+ expect(lastJsonLog()).toEqual({ selected: 'Uruguay', target: '36', matches_n: 1, match_level: 'exact' });
2920
+ });
2921
+ it('surfaces selector_ambiguous from the resolver before calling selectResolvedJs', async () => {
2922
+ browserState.page.evaluate.mockResolvedValueOnce({
2923
+ ok: false,
2924
+ code: 'selector_ambiguous',
2925
+ message: 'CSS selector ".dropdown" matched 2 elements.',
2926
+ hint: 'Pass --nth <n>.',
2927
+ matches_n: 2,
2928
+ });
2929
+ const program = createProgram('', '');
2930
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'select', '.dropdown', 'US']);
2931
+ expect(lastJsonLog().error.code).toBe('selector_ambiguous');
2932
+ // The select payload JS must not fire when resolution fails
2933
+ expect(browserState.page.evaluate.mock.calls).toHaveLength(1);
2934
+ expect(process.exitCode).toBeDefined();
2935
+ });
2936
+ });
2937
+ describe('findPackageRoot', () => {
2938
+ it('walks up from dist/src to the package root', () => {
2939
+ const packageRoot = path.join('repo-root');
2940
+ const cliFile = path.join(packageRoot, 'dist', 'src', 'cli.js');
2941
+ const exists = new Set([
2942
+ path.join(packageRoot, 'package.json'),
2943
+ ]);
2944
+ expect(findPackageRoot(cliFile, (candidate) => exists.has(candidate))).toBe(packageRoot);
2945
+ });
2946
+ it('walks up from src to the package root', () => {
2947
+ const packageRoot = path.join('repo-root');
2948
+ const cliFile = path.join(packageRoot, 'src', 'cli.ts');
2949
+ const exists = new Set([
2950
+ path.join(packageRoot, 'package.json'),
2951
+ ]);
2952
+ expect(findPackageRoot(cliFile, (candidate) => exists.has(candidate))).toBe(packageRoot);
2953
+ });
2954
+ });
2955
+ describe('normalizeVerifyRows', () => {
2956
+ it('returns an empty array for null / primitives', () => {
2957
+ expect(normalizeVerifyRows(null)).toEqual([]);
2958
+ expect(normalizeVerifyRows(undefined)).toEqual([]);
2959
+ expect(normalizeVerifyRows('hello')).toEqual([]);
2960
+ });
2961
+ it('passes through array-of-objects', () => {
2962
+ const rows = [{ a: 1 }, { a: 2 }];
2963
+ expect(normalizeVerifyRows(rows)).toEqual(rows);
2964
+ });
2965
+ it('wraps array-of-primitives as { value } rows', () => {
2966
+ expect(normalizeVerifyRows([1, 'two', null])).toEqual([
2967
+ { value: 1 }, { value: 'two' }, { value: null },
2968
+ ]);
2969
+ });
2970
+ it('unwraps common envelope shapes', () => {
2971
+ expect(normalizeVerifyRows({ rows: [{ a: 1 }] })).toEqual([{ a: 1 }]);
2972
+ expect(normalizeVerifyRows({ items: [{ b: 2 }] })).toEqual([{ b: 2 }]);
2973
+ expect(normalizeVerifyRows({ data: [{ c: 3 }] })).toEqual([{ c: 3 }]);
2974
+ expect(normalizeVerifyRows({ results: [{ d: 4 }] })).toEqual([{ d: 4 }]);
2975
+ });
2976
+ it('wraps a single object as a one-row array', () => {
2977
+ expect(normalizeVerifyRows({ ok: true })).toEqual([{ ok: true }]);
2978
+ });
2979
+ });
2980
+ describe('renderVerifyPreview', () => {
2981
+ it('emits a placeholder for empty rows', () => {
2982
+ expect(renderVerifyPreview([])).toContain('no rows');
2983
+ });
2984
+ it('prints column headers followed by row cells', () => {
2985
+ const out = renderVerifyPreview([{ a: 'x', b: 1 }, { a: 'y', b: 2 }]);
2986
+ const lines = out.split('\n');
2987
+ expect(lines[0]).toContain('a');
2988
+ expect(lines[0]).toContain('b');
2989
+ expect(lines.some((l) => l.includes('x') && l.includes('1'))).toBe(true);
2990
+ expect(lines.some((l) => l.includes('y') && l.includes('2'))).toBe(true);
2991
+ });
2992
+ it('truncates long cells and reports hidden rows / columns', () => {
2993
+ const rows = Array.from({ length: 15 }, (_, i) => ({
2994
+ a: i, b: 'x'.repeat(100), c: i, d: i, e: i, f: i, g: i, h: i,
2995
+ }));
2996
+ const out = renderVerifyPreview(rows, { maxRows: 5, maxCols: 3, cellMax: 10 });
2997
+ expect(out).toContain('and 10 more row');
2998
+ expect(out).toContain('more column');
2999
+ // cell gets truncated
3000
+ expect(out).toContain('xxxxxxxxxx');
3001
+ expect(out).not.toContain('xxxxxxxxxxx'); // never 11 consecutive
3002
+ });
3003
+ });