@jackwener/opencli 1.6.1 → 1.6.2

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 (384) hide show
  1. package/CONTRIBUTING.md +1 -1
  2. package/README.md +27 -45
  3. package/README.zh-CN.md +32 -34
  4. package/autoresearch/browse-tasks.json +18 -20
  5. package/autoresearch/commands/debug.ts +163 -0
  6. package/autoresearch/commands/fix.ts +145 -0
  7. package/autoresearch/commands/plan.ts +88 -0
  8. package/autoresearch/commands/run.ts +138 -0
  9. package/autoresearch/config.ts +82 -0
  10. package/autoresearch/engine.ts +359 -0
  11. package/autoresearch/eval-all.ts +127 -0
  12. package/autoresearch/eval-browse.ts +1 -1
  13. package/autoresearch/eval-publish.ts +238 -0
  14. package/autoresearch/eval-save.ts +249 -0
  15. package/autoresearch/eval-skill.ts +14 -8
  16. package/autoresearch/eval-v2ex.ts +220 -0
  17. package/autoresearch/eval-zhihu.ts +230 -0
  18. package/autoresearch/logger.ts +69 -0
  19. package/autoresearch/presets/combined-reliability.ts +27 -0
  20. package/autoresearch/presets/index.ts +23 -0
  21. package/autoresearch/presets/operate-reliability.ts +24 -0
  22. package/autoresearch/presets/save-reliability.ts +26 -0
  23. package/autoresearch/presets/skill-quality.ts +20 -0
  24. package/autoresearch/presets/v2ex-reliability.ts +24 -0
  25. package/autoresearch/presets/zhihu-reliability.ts +25 -0
  26. package/autoresearch/publish-tasks.json +345 -0
  27. package/autoresearch/run-save.sh +11 -0
  28. package/autoresearch/save-adapters/xhs-explore-deep.ts +64 -0
  29. package/autoresearch/save-adapters/xhs-note-comments.ts +61 -0
  30. package/autoresearch/save-adapters/xhs-search-full.ts +62 -0
  31. package/autoresearch/save-adapters/zhihu-hot-detail.ts +52 -0
  32. package/autoresearch/save-adapters/zhihu-question-full.ts +57 -0
  33. package/autoresearch/save-adapters/zhihu-search-detail.ts +53 -0
  34. package/autoresearch/save-tasks.json +281 -0
  35. package/autoresearch/v2ex-tasks.json +899 -0
  36. package/autoresearch/zhihu-tasks.json +848 -0
  37. package/dist/browser/base-page.d.ts +4 -2
  38. package/dist/browser/base-page.js +37 -4
  39. package/dist/browser/bridge.js +10 -8
  40. package/dist/browser/cdp.js +2 -6
  41. package/dist/browser/daemon-client.d.ts +11 -1
  42. package/dist/browser/daemon-client.js +3 -0
  43. package/dist/browser/dom-helpers.d.ts +4 -2
  44. package/dist/browser/dom-helpers.js +42 -31
  45. package/dist/browser/dom-snapshot.js +23 -1
  46. package/dist/browser/page.d.ts +7 -2
  47. package/dist/browser/page.js +112 -30
  48. package/dist/browser.test.js +1 -1
  49. package/dist/build-manifest.d.ts +1 -0
  50. package/dist/build-manifest.js +1 -0
  51. package/dist/cli-manifest.json +1135 -184
  52. package/dist/cli.d.ts +2 -0
  53. package/dist/cli.js +48 -7
  54. package/dist/cli.test.d.ts +1 -0
  55. package/dist/cli.test.js +88 -0
  56. package/dist/clis/1688/item.d.ts +70 -0
  57. package/dist/clis/1688/item.js +187 -0
  58. package/dist/clis/1688/item.test.d.ts +1 -0
  59. package/dist/clis/1688/item.test.js +67 -0
  60. package/dist/clis/1688/search.d.ts +56 -0
  61. package/dist/clis/1688/search.js +309 -0
  62. package/dist/clis/1688/search.test.d.ts +1 -0
  63. package/dist/clis/1688/search.test.js +75 -0
  64. package/dist/clis/1688/shared.d.ts +112 -0
  65. package/dist/clis/1688/shared.js +514 -0
  66. package/dist/clis/1688/shared.test.d.ts +1 -0
  67. package/dist/clis/1688/shared.test.js +57 -0
  68. package/dist/clis/1688/store.d.ts +45 -0
  69. package/dist/clis/1688/store.js +226 -0
  70. package/dist/clis/1688/store.test.d.ts +1 -0
  71. package/dist/clis/1688/store.test.js +62 -0
  72. package/dist/clis/amazon/bestsellers.d.ts +0 -20
  73. package/dist/clis/amazon/bestsellers.js +6 -129
  74. package/dist/clis/amazon/bestsellers.test.js +12 -3
  75. package/dist/clis/amazon/movers-shakers.d.ts +1 -0
  76. package/dist/clis/amazon/movers-shakers.js +7 -0
  77. package/dist/clis/amazon/new-releases.d.ts +1 -0
  78. package/dist/clis/amazon/new-releases.js +7 -0
  79. package/dist/clis/amazon/rankings.d.ts +59 -0
  80. package/dist/clis/amazon/rankings.js +226 -0
  81. package/dist/clis/amazon/rankings.test.d.ts +1 -0
  82. package/dist/clis/amazon/rankings.test.js +41 -0
  83. package/dist/clis/amazon/shared.d.ts +11 -0
  84. package/dist/clis/amazon/shared.js +121 -11
  85. package/dist/clis/amazon/shared.test.js +11 -0
  86. package/dist/clis/bilibili/comments.js +2 -2
  87. package/dist/clis/bilibili/comments.test.js +3 -2
  88. package/dist/clis/bilibili/download.js +2 -1
  89. package/dist/clis/bilibili/subtitle.js +4 -3
  90. package/dist/clis/bilibili/subtitle.test.js +2 -1
  91. package/dist/clis/bilibili/utils.d.ts +5 -0
  92. package/dist/clis/bilibili/utils.js +30 -0
  93. package/dist/clis/bilibili/utils.test.d.ts +1 -0
  94. package/dist/clis/bilibili/utils.test.js +17 -0
  95. package/dist/clis/douban/marks.js +1 -1
  96. package/dist/clis/douban/subject.yaml +50 -19
  97. package/dist/clis/doubao/utils.js +32 -12
  98. package/dist/clis/douyin/_shared/browser-fetch.test.js +0 -1
  99. package/dist/clis/douyin/_shared/transcode.test.js +0 -2
  100. package/dist/clis/douyin/draft.test.js +0 -2
  101. package/dist/clis/facebook/search.test.js +0 -2
  102. package/dist/clis/gemini/ask.js +9 -3
  103. package/dist/clis/gemini/ask.test.d.ts +1 -0
  104. package/dist/clis/gemini/ask.test.js +100 -0
  105. package/dist/clis/gemini/reply-state.test.d.ts +1 -0
  106. package/dist/clis/gemini/reply-state.test.js +641 -0
  107. package/dist/clis/gemini/utils.d.ts +44 -1
  108. package/dist/clis/gemini/utils.js +528 -61
  109. package/dist/clis/gemini/utils.test.js +149 -2
  110. package/dist/clis/hupu/detail.d.ts +1 -0
  111. package/dist/clis/hupu/detail.js +72 -0
  112. package/dist/clis/hupu/hot.yaml +43 -0
  113. package/dist/clis/hupu/like.d.ts +1 -0
  114. package/dist/clis/hupu/like.js +75 -0
  115. package/dist/clis/hupu/reply.d.ts +1 -0
  116. package/dist/clis/hupu/reply.js +71 -0
  117. package/dist/clis/hupu/search.d.ts +1 -0
  118. package/dist/clis/hupu/search.js +59 -0
  119. package/dist/clis/hupu/unlike.d.ts +1 -0
  120. package/dist/clis/hupu/unlike.js +75 -0
  121. package/dist/clis/hupu/utils.d.ts +20 -0
  122. package/dist/clis/hupu/utils.js +319 -0
  123. package/dist/clis/instagram/_shared/private-publish.d.ts +138 -0
  124. package/dist/clis/instagram/_shared/private-publish.js +1030 -0
  125. package/dist/clis/instagram/_shared/private-publish.test.d.ts +1 -0
  126. package/dist/clis/instagram/_shared/private-publish.test.js +705 -0
  127. package/dist/clis/instagram/_shared/protocol-capture.d.ts +26 -0
  128. package/dist/clis/instagram/_shared/protocol-capture.js +282 -0
  129. package/dist/clis/instagram/_shared/protocol-capture.test.d.ts +1 -0
  130. package/dist/clis/instagram/_shared/protocol-capture.test.js +114 -0
  131. package/dist/clis/instagram/_shared/runtime-info.d.ts +9 -0
  132. package/dist/clis/instagram/_shared/runtime-info.js +81 -0
  133. package/dist/clis/instagram/note.d.ts +1 -0
  134. package/dist/clis/instagram/note.js +222 -0
  135. package/dist/clis/instagram/note.test.d.ts +1 -0
  136. package/dist/clis/instagram/note.test.js +81 -0
  137. package/dist/clis/instagram/post.d.ts +4 -0
  138. package/dist/clis/instagram/post.js +1496 -0
  139. package/dist/clis/instagram/post.test.d.ts +1 -0
  140. package/dist/clis/instagram/post.test.js +1647 -0
  141. package/dist/clis/instagram/reel.d.ts +1 -0
  142. package/dist/clis/instagram/reel.js +826 -0
  143. package/dist/clis/instagram/reel.test.d.ts +1 -0
  144. package/dist/clis/instagram/reel.test.js +167 -0
  145. package/dist/clis/instagram/story.d.ts +1 -0
  146. package/dist/clis/instagram/story.js +115 -0
  147. package/dist/clis/instagram/story.test.d.ts +1 -0
  148. package/dist/clis/instagram/story.test.js +167 -0
  149. package/dist/clis/sinafinance/stock-rank.d.ts +4 -0
  150. package/dist/clis/sinafinance/stock-rank.js +65 -0
  151. package/dist/clis/substack/utils.test.js +0 -2
  152. package/dist/clis/twitter/post.js +72 -45
  153. package/dist/clis/twitter/post.test.d.ts +1 -0
  154. package/dist/clis/twitter/post.test.js +116 -0
  155. package/dist/clis/twitter/reply.d.ts +12 -0
  156. package/dist/clis/twitter/reply.js +257 -35
  157. package/dist/clis/twitter/reply.test.d.ts +1 -0
  158. package/dist/clis/twitter/reply.test.js +151 -0
  159. package/dist/clis/xianyu/chat.d.ts +7 -0
  160. package/dist/clis/xianyu/chat.js +146 -0
  161. package/dist/clis/xianyu/chat.test.d.ts +1 -0
  162. package/dist/clis/xianyu/chat.test.js +15 -0
  163. package/dist/clis/xianyu/item.d.ts +7 -0
  164. package/dist/clis/xianyu/item.js +152 -0
  165. package/dist/clis/xianyu/item.test.d.ts +1 -0
  166. package/dist/clis/xianyu/item.test.js +56 -0
  167. package/dist/clis/xianyu/search.d.ts +10 -0
  168. package/dist/clis/xianyu/search.js +134 -0
  169. package/dist/clis/xianyu/search.test.d.ts +1 -0
  170. package/dist/clis/xianyu/search.test.js +17 -0
  171. package/dist/clis/xianyu/utils.d.ts +1 -0
  172. package/dist/clis/xianyu/utils.js +8 -0
  173. package/dist/clis/xiaoe/catalog.yaml +129 -0
  174. package/dist/clis/xiaoe/content.yaml +43 -0
  175. package/dist/clis/xiaoe/courses.yaml +73 -0
  176. package/dist/clis/xiaoe/detail.yaml +39 -0
  177. package/dist/clis/xiaoe/play-url.yaml +124 -0
  178. package/dist/clis/xiaohongshu/comments.test.js +0 -2
  179. package/dist/clis/xiaohongshu/creator-note-detail.test.js +0 -2
  180. package/dist/clis/xiaohongshu/creator-notes.test.js +0 -2
  181. package/dist/clis/xiaohongshu/download.test.js +0 -2
  182. package/dist/clis/xiaohongshu/note.test.js +0 -2
  183. package/dist/clis/xiaohongshu/publish.test.js +0 -2
  184. package/dist/clis/xiaohongshu/search.js +29 -20
  185. package/dist/clis/xiaohongshu/search.test.js +56 -48
  186. package/dist/clis/yuanbao/ask.d.ts +21 -0
  187. package/dist/clis/yuanbao/ask.js +427 -0
  188. package/dist/clis/yuanbao/ask.test.d.ts +1 -0
  189. package/dist/clis/yuanbao/ask.test.js +124 -0
  190. package/dist/clis/yuanbao/new.d.ts +1 -0
  191. package/dist/clis/yuanbao/new.js +70 -0
  192. package/dist/clis/yuanbao/new.test.d.ts +1 -0
  193. package/dist/clis/yuanbao/new.test.js +30 -0
  194. package/dist/clis/yuanbao/shared.d.ts +13 -0
  195. package/dist/clis/yuanbao/shared.js +49 -0
  196. package/dist/clis/zhihu/question.js +30 -19
  197. package/dist/clis/zhihu/question.test.js +34 -16
  198. package/dist/commanderAdapter.js +8 -4
  199. package/dist/commanderAdapter.test.js +42 -0
  200. package/dist/completion.js +3 -1
  201. package/dist/completion.test.d.ts +1 -0
  202. package/dist/completion.test.js +23 -0
  203. package/dist/doctor.js +1 -1
  204. package/dist/electron-apps.d.ts +2 -0
  205. package/dist/electron-apps.js +7 -1
  206. package/dist/errors.js +1 -1
  207. package/dist/execution.js +25 -35
  208. package/dist/explore.js +1 -1
  209. package/dist/launcher.d.ts +4 -0
  210. package/dist/launcher.js +64 -8
  211. package/dist/launcher.test.js +88 -7
  212. package/dist/output.d.ts +2 -0
  213. package/dist/output.js +10 -1
  214. package/dist/output.test.d.ts +0 -3
  215. package/dist/output.test.js +59 -92
  216. package/dist/pipeline/executor.test.js +0 -2
  217. package/dist/pipeline/steps/download.test.js +0 -2
  218. package/dist/registry.d.ts +2 -0
  219. package/dist/serialization.d.ts +1 -0
  220. package/dist/serialization.js +1 -0
  221. package/dist/types.d.ts +9 -2
  222. package/docs/.vitepress/config.mts +4 -0
  223. package/docs/adapters/browser/1688.md +52 -0
  224. package/docs/adapters/browser/36kr.md +2 -1
  225. package/docs/adapters/browser/doubao.md +5 -1
  226. package/docs/adapters/browser/hupu.md +53 -0
  227. package/docs/adapters/browser/sinafinance.md +32 -2
  228. package/docs/adapters/browser/weibo.md +6 -1
  229. package/docs/adapters/browser/wikipedia.md +2 -0
  230. package/docs/adapters/browser/xianyu.md +42 -0
  231. package/docs/adapters/browser/xiaoe.md +44 -0
  232. package/docs/adapters/browser/yuanbao.md +64 -0
  233. package/docs/adapters/index.md +14 -5
  234. package/docs/comparison.md +1 -1
  235. package/docs/developer/ai-workflow.md +2 -2
  236. package/docs/developer/contributing.md +1 -1
  237. package/docs/developer/testing.md +2 -0
  238. package/docs/guide/plugins.md +1 -0
  239. package/docs/guide/troubleshooting.md +11 -0
  240. package/docs/superpowers/specs/2026-04-03-v2ex-autoresearch-design.md +41 -0
  241. package/docs/zh/guide/plugins.md +1 -0
  242. package/extension/dist/background.js +1127 -0
  243. package/extension/src/background.test.ts +39 -0
  244. package/extension/src/background.ts +223 -34
  245. package/extension/src/cdp.ts +194 -4
  246. package/extension/src/protocol.ts +22 -1
  247. package/package.json +3 -2
  248. package/scripts/postinstall.js +1 -1
  249. package/skills/opencli-explorer/SKILL.md +1 -1
  250. package/skills/opencli-oneshot/SKILL.md +2 -2
  251. package/skills/opencli-operate/SKILL.md +120 -27
  252. package/skills/opencli-usage/SKILL.md +31 -20
  253. package/skills/opencli-usage/browser.md +114 -16
  254. package/skills/opencli-usage/public-api.md +32 -3
  255. package/skills/smart-search/SKILL.md +156 -0
  256. package/skills/smart-search/references/sources-ai.md +74 -0
  257. package/skills/smart-search/references/sources-info.md +43 -0
  258. package/skills/smart-search/references/sources-media.md +50 -0
  259. package/skills/smart-search/references/sources-other.md +42 -0
  260. package/skills/smart-search/references/sources-shopping.md +31 -0
  261. package/skills/smart-search/references/sources-social.md +51 -0
  262. package/skills/smart-search/references/sources-tech.md +42 -0
  263. package/skills/smart-search/references/sources-travel.md +20 -0
  264. package/src/browser/base-page.ts +41 -6
  265. package/src/browser/bridge.ts +11 -8
  266. package/src/browser/cdp.ts +1 -8
  267. package/src/browser/daemon-client.ts +11 -1
  268. package/src/browser/dom-helpers.ts +43 -31
  269. package/src/browser/dom-snapshot.ts +23 -1
  270. package/src/browser/page.ts +115 -31
  271. package/src/browser.test.ts +1 -1
  272. package/src/build-manifest.ts +2 -0
  273. package/src/cli.test.ts +133 -0
  274. package/src/cli.ts +73 -11
  275. package/src/clis/1688/item.test.ts +69 -0
  276. package/src/clis/1688/item.ts +282 -0
  277. package/src/clis/1688/search.test.ts +81 -0
  278. package/src/clis/1688/search.ts +402 -0
  279. package/src/clis/1688/shared.test.ts +75 -0
  280. package/src/clis/1688/shared.ts +623 -0
  281. package/src/clis/1688/store.test.ts +69 -0
  282. package/src/clis/1688/store.ts +300 -0
  283. package/src/clis/amazon/bestsellers.test.ts +12 -3
  284. package/src/clis/amazon/bestsellers.ts +6 -178
  285. package/src/clis/amazon/movers-shakers.ts +8 -0
  286. package/src/clis/amazon/new-releases.ts +8 -0
  287. package/src/clis/amazon/rankings.test.ts +47 -0
  288. package/src/clis/amazon/rankings.ts +312 -0
  289. package/src/clis/amazon/shared.test.ts +16 -0
  290. package/src/clis/amazon/shared.ts +134 -12
  291. package/src/clis/bilibili/comments.test.ts +4 -3
  292. package/src/clis/bilibili/comments.ts +2 -2
  293. package/src/clis/bilibili/download.ts +2 -1
  294. package/src/clis/bilibili/subtitle.test.ts +2 -1
  295. package/src/clis/bilibili/subtitle.ts +4 -3
  296. package/src/clis/bilibili/utils.test.ts +21 -0
  297. package/src/clis/bilibili/utils.ts +27 -0
  298. package/src/clis/douban/marks.ts +1 -1
  299. package/src/clis/douban/subject.yaml +50 -19
  300. package/src/clis/doubao/utils.ts +32 -12
  301. package/src/clis/douyin/_shared/browser-fetch.test.ts +0 -1
  302. package/src/clis/douyin/_shared/transcode.test.ts +0 -2
  303. package/src/clis/douyin/draft.test.ts +0 -2
  304. package/src/clis/facebook/search.test.ts +0 -2
  305. package/src/clis/gemini/ask.test.ts +116 -0
  306. package/src/clis/gemini/ask.ts +10 -3
  307. package/src/clis/gemini/reply-state.test.ts +708 -0
  308. package/src/clis/gemini/utils.test.ts +184 -2
  309. package/src/clis/gemini/utils.ts +588 -60
  310. package/src/clis/hupu/detail.ts +126 -0
  311. package/src/clis/hupu/hot.yaml +43 -0
  312. package/src/clis/hupu/like.ts +76 -0
  313. package/src/clis/hupu/reply.ts +76 -0
  314. package/src/clis/hupu/search.ts +95 -0
  315. package/src/clis/hupu/unlike.ts +76 -0
  316. package/src/clis/hupu/utils.ts +381 -0
  317. package/src/clis/instagram/_shared/private-publish.test.ts +827 -0
  318. package/src/clis/instagram/_shared/private-publish.ts +1303 -0
  319. package/src/clis/instagram/_shared/protocol-capture.test.ts +148 -0
  320. package/src/clis/instagram/_shared/protocol-capture.ts +321 -0
  321. package/src/clis/instagram/_shared/runtime-info.ts +91 -0
  322. package/src/clis/instagram/note.test.ts +96 -0
  323. package/src/clis/instagram/note.ts +254 -0
  324. package/src/clis/instagram/post.test.ts +1716 -0
  325. package/src/clis/instagram/post.ts +1620 -0
  326. package/src/clis/instagram/reel.test.ts +191 -0
  327. package/src/clis/instagram/reel.ts +886 -0
  328. package/src/clis/instagram/story.test.ts +191 -0
  329. package/src/clis/instagram/story.ts +151 -0
  330. package/src/clis/sinafinance/stock-rank.ts +68 -0
  331. package/src/clis/substack/utils.test.ts +0 -2
  332. package/src/clis/twitter/post.test.ts +157 -0
  333. package/src/clis/twitter/post.ts +82 -48
  334. package/src/clis/twitter/reply.test.ts +177 -0
  335. package/src/clis/twitter/reply.ts +285 -39
  336. package/src/clis/xianyu/chat.test.ts +20 -0
  337. package/src/clis/xianyu/chat.ts +175 -0
  338. package/src/clis/xianyu/item.test.ts +67 -0
  339. package/src/clis/xianyu/item.ts +172 -0
  340. package/src/clis/xianyu/search.test.ts +22 -0
  341. package/src/clis/xianyu/search.ts +151 -0
  342. package/src/clis/xianyu/utils.ts +9 -0
  343. package/src/clis/xiaoe/catalog.yaml +129 -0
  344. package/src/clis/xiaoe/content.yaml +43 -0
  345. package/src/clis/xiaoe/courses.yaml +73 -0
  346. package/src/clis/xiaoe/detail.yaml +39 -0
  347. package/src/clis/xiaoe/play-url.yaml +124 -0
  348. package/src/clis/xiaohongshu/comments.test.ts +0 -2
  349. package/src/clis/xiaohongshu/creator-note-detail.test.ts +0 -2
  350. package/src/clis/xiaohongshu/creator-notes.test.ts +0 -2
  351. package/src/clis/xiaohongshu/download.test.ts +0 -2
  352. package/src/clis/xiaohongshu/note.test.ts +0 -2
  353. package/src/clis/xiaohongshu/publish.test.ts +0 -2
  354. package/src/clis/xiaohongshu/search.test.ts +59 -48
  355. package/src/clis/xiaohongshu/search.ts +31 -21
  356. package/src/clis/yuanbao/ask.test.ts +156 -0
  357. package/src/clis/yuanbao/ask.ts +522 -0
  358. package/src/clis/yuanbao/new.test.ts +36 -0
  359. package/src/clis/yuanbao/new.ts +81 -0
  360. package/src/clis/yuanbao/shared.ts +57 -0
  361. package/src/clis/zhihu/question.test.ts +42 -17
  362. package/src/clis/zhihu/question.ts +31 -26
  363. package/src/commanderAdapter.test.ts +51 -0
  364. package/src/commanderAdapter.ts +8 -4
  365. package/src/completion.test.ts +30 -0
  366. package/src/completion.ts +3 -1
  367. package/src/doctor.ts +1 -1
  368. package/src/electron-apps.ts +9 -1
  369. package/src/errors.ts +1 -1
  370. package/src/execution.ts +26 -30
  371. package/src/explore.ts +1 -1
  372. package/src/launcher.test.ts +121 -7
  373. package/src/launcher.ts +87 -9
  374. package/src/output.test.ts +50 -90
  375. package/src/output.ts +10 -1
  376. package/src/pipeline/executor.test.ts +0 -2
  377. package/src/pipeline/steps/download.test.ts +0 -2
  378. package/src/registry.ts +2 -0
  379. package/src/serialization.ts +2 -0
  380. package/src/types.ts +9 -2
  381. package/tests/e2e/browser-auth.test.ts +9 -0
  382. package/CLI-EXPLORER.md +0 -724
  383. package/CLI-ONESHOT.md +0 -216
  384. package/SKILL.md +0 -59
@@ -0,0 +1,124 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { AuthRequiredError, CommandExecutionError, TimeoutError } from '../../errors.js';
3
+ import { __test__ } from './ask.js';
4
+ import { askCommand } from './ask.js';
5
+ describe('yuanbao ask helpers', () => {
6
+ describe('isOnYuanbao', () => {
7
+ const fakePage = (url) => ({ evaluate: () => url instanceof Error ? Promise.reject(url) : Promise.resolve(url) });
8
+ it('returns true for yuanbao.tencent.com URLs', async () => {
9
+ expect(await __test__.isOnYuanbao(fakePage('https://yuanbao.tencent.com/'))).toBe(true);
10
+ expect(await __test__.isOnYuanbao(fakePage('https://yuanbao.tencent.com/chat/abc'))).toBe(true);
11
+ });
12
+ it('returns false for non-yuanbao domains', async () => {
13
+ expect(await __test__.isOnYuanbao(fakePage('https://example.com/?next=yuanbao.tencent.com'))).toBe(false);
14
+ expect(await __test__.isOnYuanbao(fakePage('about:blank'))).toBe(false);
15
+ });
16
+ it('returns false when evaluate throws', async () => {
17
+ expect(await __test__.isOnYuanbao(fakePage(new Error('detached')))).toBe(false);
18
+ });
19
+ });
20
+ it('removes echoed prompt prefixes from transcript additions', () => {
21
+ expect(__test__.sanitizeYuanbaoResponseText('你好\n你好,我是元宝。', '你好')).toBe('你好,我是元宝。');
22
+ });
23
+ it('filters transient in-progress assistant placeholders', () => {
24
+ expect(__test__.sanitizeYuanbaoResponseText('正在搜索资料', '张雪机车相关的股票有哪些?')).toBe('');
25
+ });
26
+ it('normalizes boolean flags with explicit defaults', () => {
27
+ expect(__test__.normalizeBooleanFlag(undefined, true)).toBe(true);
28
+ expect(__test__.normalizeBooleanFlag(undefined, false)).toBe(false);
29
+ expect(__test__.normalizeBooleanFlag('true', false)).toBe(true);
30
+ expect(__test__.normalizeBooleanFlag('1', false)).toBe(true);
31
+ expect(__test__.normalizeBooleanFlag('yes', false)).toBe(true);
32
+ expect(__test__.normalizeBooleanFlag('false', true)).toBe(false);
33
+ });
34
+ it('ignores baseline lines and echoed prompts when collecting additions', () => {
35
+ const response = __test__.collectYuanbaoTranscriptAdditions(['旧消息'], ['旧消息', '你好', '你好\n你好,我是元宝。'], '你好');
36
+ expect(response).toBe('你好,我是元宝。');
37
+ });
38
+ it('prefers fresh assistant messages over echoed prompts and older messages', () => {
39
+ const response = __test__.pickLatestYuanbaoAssistantCandidate(['旧回复', '你好', '你好!我是元宝,由腾讯推出的AI助手。'], 1, '你好');
40
+ expect(response).toBe('你好!我是元宝,由腾讯推出的AI助手。');
41
+ });
42
+ it('converts assistant html tables to markdown tables via turndown', () => {
43
+ const markdown = __test__.convertYuanbaoHtmlToMarkdown(`
44
+ <h3>核心产业链概念股一览</h3>
45
+ <table>
46
+ <thead>
47
+ <tr><th>细分赛道</th><th>核心标的</th></tr>
48
+ </thead>
49
+ <tbody>
50
+ <tr><td>光模块</td><td>中际旭创</td></tr>
51
+ </tbody>
52
+ </table>
53
+ `);
54
+ expect(markdown).toContain('### 核心产业链概念股一览');
55
+ expect(markdown).toContain('| 细分赛道 | 核心标的 |');
56
+ expect(markdown).toContain('| --- | --- |');
57
+ expect(markdown).toContain('| 光模块 | 中际旭创 |');
58
+ });
59
+ it('tracks stabilization by incrementing repeats and resetting on changes', () => {
60
+ expect(__test__.updateStableState('', 0, '第一段')).toEqual({
61
+ previousText: '第一段',
62
+ stableCount: 0,
63
+ });
64
+ expect(__test__.updateStableState('第一段', 0, '第一段')).toEqual({
65
+ previousText: '第一段',
66
+ stableCount: 1,
67
+ });
68
+ expect(__test__.updateStableState('第一段', 1, '第二段')).toEqual({
69
+ previousText: '第二段',
70
+ stableCount: 0,
71
+ });
72
+ });
73
+ });
74
+ function createAskPageMock(overrides = {}) {
75
+ const currentUrl = overrides.currentUrl ?? 'https://yuanbao.tencent.com/';
76
+ const hasLoginGate = overrides.hasLoginGate ?? false;
77
+ const sendResult = overrides.sendResult;
78
+ return {
79
+ goto: vi.fn().mockResolvedValue(undefined),
80
+ wait: vi.fn().mockResolvedValue(undefined),
81
+ evaluate: vi.fn().mockImplementation(async (script) => {
82
+ if (script === 'window.location.href')
83
+ return currentUrl;
84
+ if (script.includes('微信扫码登录'))
85
+ return hasLoginGate;
86
+ if (script.includes('[dt-button-id="internet_search"]'))
87
+ return { found: false, enabled: false };
88
+ if (script.includes('[dt-button-id="deep_think"]'))
89
+ return { found: false, enabled: false };
90
+ if (script.includes('.agent-chat__list__item--ai'))
91
+ return [];
92
+ if (script.includes('const stopLines = new Set(['))
93
+ return [];
94
+ if (script.includes('Failed to insert the prompt into the Yuanbao composer.')) {
95
+ return sendResult ?? { ok: true, action: 'click' };
96
+ }
97
+ throw new Error(`Unexpected evaluate script in test: ${script.slice(0, 80)}`);
98
+ }),
99
+ };
100
+ }
101
+ describe('yuanbao ask command', () => {
102
+ it('throws AuthRequiredError when Yuanbao shows a login gate before sending', async () => {
103
+ const page = createAskPageMock({ hasLoginGate: true });
104
+ await expect(askCommand.func(page, { prompt: '你好', timeout: '60', search: true, think: false }))
105
+ .rejects.toBeInstanceOf(AuthRequiredError);
106
+ });
107
+ it('throws CommandExecutionError when the prompt cannot be sent', async () => {
108
+ const page = createAskPageMock({
109
+ sendResult: {
110
+ ok: false,
111
+ reason: 'Yuanbao composer was not found.',
112
+ },
113
+ });
114
+ await expect(askCommand.func(page, { prompt: '你好', timeout: '60', search: true, think: false }))
115
+ .rejects.toBeInstanceOf(CommandExecutionError);
116
+ });
117
+ it('throws TimeoutError when no response arrives before timeout', async () => {
118
+ const page = createAskPageMock({
119
+ sendResult: { ok: true, action: 'click' },
120
+ });
121
+ await expect(askCommand.func(page, { prompt: '你好', timeout: '-1', search: true, think: false }))
122
+ .rejects.toBeInstanceOf(TimeoutError);
123
+ });
124
+ });
@@ -0,0 +1 @@
1
+ export declare const newCommand: import("../../registry.js").CliCommand;
@@ -0,0 +1,70 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { YUANBAO_DOMAIN, YUANBAO_URL, IS_VISIBLE_JS, authRequired, ensureYuanbaoPage, hasLoginGate } from './shared.js';
3
+ async function getCurrentUrl(page) {
4
+ const result = await page.evaluate('window.location.href').catch(() => '');
5
+ return typeof result === 'string' ? result : '';
6
+ }
7
+ async function getComposerText(page) {
8
+ const result = await page.evaluate(`(() => {
9
+ const composer = document.querySelector('.ql-editor, [contenteditable="true"]');
10
+ return composer ? (composer.textContent || '').trim() : '';
11
+ })()`);
12
+ return typeof result === 'string' ? result.trim() : '';
13
+ }
14
+ async function startNewYuanbaoChat(page) {
15
+ await ensureYuanbaoPage(page);
16
+ if (await hasLoginGate(page))
17
+ return 'blocked';
18
+ const beforeUrl = await getCurrentUrl(page);
19
+ const action = await page.evaluate(`(() => {
20
+ ${IS_VISIBLE_JS}
21
+
22
+ const trigger = Array.from(document.querySelectorAll('.yb-common-nav__trigger[data-desc="new-chat"]'))
23
+ .find((node) => isVisible(node));
24
+
25
+ if (trigger instanceof HTMLElement) {
26
+ trigger.click();
27
+ return 'clicked';
28
+ }
29
+
30
+ return 'navigate';
31
+ })()`);
32
+ if (action === 'navigate') {
33
+ await page.goto(YUANBAO_URL, { waitUntil: 'load', settleMs: 2500 });
34
+ await page.wait(1);
35
+ if (await hasLoginGate(page))
36
+ return 'blocked';
37
+ return 'navigate';
38
+ }
39
+ await page.wait(1);
40
+ if (await hasLoginGate(page))
41
+ return 'blocked';
42
+ const afterUrl = await getCurrentUrl(page);
43
+ const composerText = await getComposerText(page);
44
+ if (afterUrl !== beforeUrl || !composerText)
45
+ return 'clicked';
46
+ await page.goto(YUANBAO_URL, { waitUntil: 'load', settleMs: 2500 });
47
+ await page.wait(1);
48
+ return 'navigate';
49
+ }
50
+ export const newCommand = cli({
51
+ site: 'yuanbao',
52
+ name: 'new',
53
+ description: 'Start a new conversation in Yuanbao web chat',
54
+ domain: YUANBAO_DOMAIN,
55
+ strategy: Strategy.COOKIE,
56
+ browser: true,
57
+ navigateBefore: false,
58
+ args: [],
59
+ columns: ['Status', 'Action'],
60
+ func: async (page) => {
61
+ const action = await startNewYuanbaoChat(page);
62
+ if (action === 'blocked') {
63
+ throw authRequired('Yuanbao opened a login gate instead of starting a new chat.');
64
+ }
65
+ return [{
66
+ Status: 'Success',
67
+ Action: action === 'navigate' ? 'Reloaded Yuanbao homepage as fallback' : 'Clicked New chat',
68
+ }];
69
+ },
70
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { AuthRequiredError } from '../../errors.js';
3
+ import { newCommand } from './new.js';
4
+ function createNewPageMock(overrides = {}) {
5
+ const currentUrl = overrides.currentUrl ?? 'https://yuanbao.tencent.com/';
6
+ const triggerAction = overrides.triggerAction ?? 'clicked';
7
+ const hasLoginGate = overrides.hasLoginGate ?? false;
8
+ const composerText = overrides.composerText ?? '';
9
+ return {
10
+ goto: vi.fn().mockResolvedValue(undefined),
11
+ wait: vi.fn().mockResolvedValue(undefined),
12
+ evaluate: vi.fn().mockImplementation(async (script) => {
13
+ if (script === 'window.location.href')
14
+ return currentUrl;
15
+ if (script.includes('微信扫码登录'))
16
+ return hasLoginGate;
17
+ if (script.includes('.ql-editor, [contenteditable="true"]'))
18
+ return composerText;
19
+ if (script.includes('const trigger = Array.from(document.querySelectorAll'))
20
+ return triggerAction;
21
+ throw new Error(`Unexpected evaluate script in test: ${script.slice(0, 80)}`);
22
+ }),
23
+ };
24
+ }
25
+ describe('yuanbao new command', () => {
26
+ it('throws AuthRequiredError when Yuanbao shows a login gate', async () => {
27
+ const page = createNewPageMock({ hasLoginGate: true });
28
+ await expect(newCommand.func(page, {})).rejects.toBeInstanceOf(AuthRequiredError);
29
+ });
30
+ });
@@ -0,0 +1,13 @@
1
+ import type { IPage } from '../../types.js';
2
+ import { AuthRequiredError } from '../../errors.js';
3
+ export declare const YUANBAO_DOMAIN = "yuanbao.tencent.com";
4
+ export declare const YUANBAO_URL = "https://yuanbao.tencent.com/";
5
+ /**
6
+ * Reusable visibility check for injected browser scripts.
7
+ * Embed in page.evaluate strings via `${IS_VISIBLE_JS}`.
8
+ */
9
+ export declare const IS_VISIBLE_JS = "const isVisible = (node) => {\n if (!(node instanceof HTMLElement)) return false;\n const rect = node.getBoundingClientRect();\n const style = window.getComputedStyle(node);\n return rect.width > 0\n && rect.height > 0\n && style.display !== 'none'\n && style.visibility !== 'hidden';\n};";
10
+ export declare function authRequired(message: string): AuthRequiredError;
11
+ export declare function isOnYuanbao(page: IPage): Promise<boolean>;
12
+ export declare function ensureYuanbaoPage(page: IPage): Promise<void>;
13
+ export declare function hasLoginGate(page: IPage): Promise<boolean>;
@@ -0,0 +1,49 @@
1
+ import { AuthRequiredError } from '../../errors.js';
2
+ export const YUANBAO_DOMAIN = 'yuanbao.tencent.com';
3
+ export const YUANBAO_URL = 'https://yuanbao.tencent.com/';
4
+ const SESSION_HINT = 'Likely login/auth/challenge/session issue in the existing yuanbao.tencent.com browser session.';
5
+ /**
6
+ * Reusable visibility check for injected browser scripts.
7
+ * Embed in page.evaluate strings via `${IS_VISIBLE_JS}`.
8
+ */
9
+ export const IS_VISIBLE_JS = `const isVisible = (node) => {
10
+ if (!(node instanceof HTMLElement)) return false;
11
+ const rect = node.getBoundingClientRect();
12
+ const style = window.getComputedStyle(node);
13
+ return rect.width > 0
14
+ && rect.height > 0
15
+ && style.display !== 'none'
16
+ && style.visibility !== 'hidden';
17
+ };`;
18
+ export function authRequired(message) {
19
+ return new AuthRequiredError(YUANBAO_DOMAIN, `${message} ${SESSION_HINT}`);
20
+ }
21
+ export async function isOnYuanbao(page) {
22
+ const url = await page.evaluate('window.location.href').catch(() => '');
23
+ if (typeof url !== 'string' || !url)
24
+ return false;
25
+ try {
26
+ const hostname = new URL(url).hostname;
27
+ return hostname === YUANBAO_DOMAIN || hostname.endsWith(`.${YUANBAO_DOMAIN}`);
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ export async function ensureYuanbaoPage(page) {
34
+ if (!(await isOnYuanbao(page))) {
35
+ await page.goto(YUANBAO_URL, { waitUntil: 'load', settleMs: 2500 });
36
+ await page.wait(1);
37
+ }
38
+ }
39
+ export async function hasLoginGate(page) {
40
+ const result = await page.evaluate(`(() => {
41
+ const bodyText = document.body.innerText || '';
42
+ const hasWechatLoginText = bodyText.includes('微信扫码登录');
43
+ const hasWechatIframe = Array.from(document.querySelectorAll('iframe'))
44
+ .some((frame) => (frame.getAttribute('src') || '').includes('open.weixin.qq.com/connect/qrconnect'));
45
+
46
+ return hasWechatLoginText || hasWechatIframe;
47
+ })()`);
48
+ return Boolean(result);
49
+ }
@@ -1,5 +1,14 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
2
  import { AuthRequiredError, CliError } from '../../errors.js';
3
+ function stripHtml(html) {
4
+ return html
5
+ .replace(/<[^>]+>/g, '')
6
+ .replace(/&nbsp;/g, ' ')
7
+ .replace(/&lt;/g, '<')
8
+ .replace(/&gt;/g, '>')
9
+ .replace(/&amp;/g, '&')
10
+ .trim();
11
+ }
3
12
  cli({
4
13
  site: 'zhihu',
5
14
  name: 'question',
@@ -13,30 +22,32 @@ cli({
13
22
  columns: ['rank', 'author', 'votes', 'content'],
14
23
  func: async (page, kwargs) => {
15
24
  const { id, limit = 5 } = kwargs;
25
+ const questionId = String(id);
26
+ if (!/^\d+$/.test(questionId)) {
27
+ throw new CliError('INVALID_INPUT', 'Question ID must be numeric', 'Example: opencli zhihu question 123456789');
28
+ }
16
29
  const answerLimit = Number(limit);
17
- const stripHtml = (html) => (html || '').replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&').trim();
18
- // Only fetch answers here. The question detail endpoint is not used by the
19
- // current CLI output and can fail independently, which would incorrectly
20
- // turn a successful answers response into a login error.
21
- const result = await page.evaluate(async ({ questionId, answerLimit }) => {
22
- const aResp = await fetch(`https://www.zhihu.com/api/v4/questions/${questionId}/answers?limit=${answerLimit}&offset=0&sort_by=default&include=data[*].content,voteup_count,comment_count,author`, { credentials: 'include' });
23
- if (!aResp.ok)
24
- return { ok: false, status: aResp.status };
25
- const a = await aResp.json();
26
- return { ok: true, answers: Array.isArray(a?.data) ? a.data : [] };
27
- }, { questionId: String(id), answerLimit });
28
- if (!result?.ok) {
29
- if (result?.status === 401 || result?.status === 403) {
30
+ await page.goto(`https://www.zhihu.com/question/${questionId}`);
31
+ const url = `https://www.zhihu.com/api/v4/questions/${questionId}/answers?limit=${answerLimit}&offset=0&sort_by=default&include=data[*].content,voteup_count,comment_count,author`;
32
+ const data = await page.evaluate(`
33
+ (async () => {
34
+ const r = await fetch(${JSON.stringify(url)}, { credentials: 'include' });
35
+ if (!r.ok) return { __httpError: r.status };
36
+ return await r.json();
37
+ })()
38
+ `);
39
+ if (!data || data.__httpError) {
40
+ const status = data?.__httpError;
41
+ if (status === 401 || status === 403) {
30
42
  throw new AuthRequiredError('www.zhihu.com', 'Failed to fetch question data from Zhihu');
31
43
  }
32
- throw new CliError('FETCH_ERROR', `Zhihu question answers request failed with HTTP ${result?.status ?? 'unknown'}`, 'Try again later or rerun with -v for more detail');
44
+ throw new CliError('FETCH_ERROR', status ? `Zhihu question answers request failed (HTTP ${status})` : 'Zhihu question answers request failed', 'Try again later or rerun with -v for more detail');
33
45
  }
34
- const answers = result.answers.slice(0, answerLimit).map((a, i) => ({
46
+ return (data.data || []).map((item, i) => ({
35
47
  rank: i + 1,
36
- author: a.author?.name ?? 'anonymous',
37
- votes: a.voteup_count ?? 0,
38
- content: stripHtml(a.content ?? '').slice(0, 200),
48
+ author: item.author?.name || 'anonymous',
49
+ votes: item.voteup_count || 0,
50
+ content: stripHtml(item.content || '').substring(0, 200),
39
51
  }));
40
- return answers;
41
52
  },
42
53
  });
@@ -1,27 +1,26 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
2
  import { getRegistry } from '../../registry.js';
3
- import { AuthRequiredError } from '../../errors.js';
3
+ import { AuthRequiredError, CliError } from '../../errors.js';
4
4
  import './question.js';
5
5
  describe('zhihu question', () => {
6
- it('returns answers even when the unused question detail request fails', async () => {
6
+ it('returns answers from the Zhihu API', async () => {
7
7
  const cmd = getRegistry().get('zhihu/question');
8
8
  expect(cmd?.func).toBeTypeOf('function');
9
- const evaluate = vi.fn().mockImplementation(async (_fn, args) => {
10
- expect(args).toEqual({ questionId: '2021881398772981878', answerLimit: 3 });
9
+ const goto = vi.fn().mockResolvedValue(undefined);
10
+ const evaluate = vi.fn().mockImplementation(async (js) => {
11
+ expect(js).toContain('questions/2021881398772981878/answers?limit=3');
12
+ expect(js).toContain("credentials: 'include'");
11
13
  return {
12
- ok: true,
13
- answers: [
14
+ data: [
14
15
  {
15
16
  author: { name: 'alice' },
16
17
  voteup_count: 12,
17
- content: '<p>Hello <b>Zhihu</b></p>',
18
+ content: 'Hello Zhihu',
18
19
  },
19
20
  ],
20
21
  };
21
22
  });
22
- const page = {
23
- evaluate,
24
- };
23
+ const page = { goto, evaluate };
25
24
  await expect(cmd.func(page, { id: '2021881398772981878', limit: 3 })).resolves.toEqual([
26
25
  {
27
26
  rank: 1,
@@ -30,25 +29,44 @@ describe('zhihu question', () => {
30
29
  content: 'Hello Zhihu',
31
30
  },
32
31
  ]);
32
+ expect(goto).toHaveBeenCalledWith('https://www.zhihu.com/question/2021881398772981878');
33
33
  expect(evaluate).toHaveBeenCalledTimes(1);
34
34
  });
35
35
  it('maps auth-like answer failures to AuthRequiredError', async () => {
36
36
  const cmd = getRegistry().get('zhihu/question');
37
- expect(cmd?.func).toBeTypeOf('function');
38
37
  const page = {
39
- evaluate: vi.fn().mockResolvedValue({ ok: false, status: 403 }),
38
+ goto: vi.fn().mockResolvedValue(undefined),
39
+ evaluate: vi.fn().mockResolvedValue({ __httpError: 403 }),
40
40
  };
41
41
  await expect(cmd.func(page, { id: '2021881398772981878', limit: 3 })).rejects.toBeInstanceOf(AuthRequiredError);
42
42
  });
43
- it('preserves non-auth fetch failures as CliError instead of login errors', async () => {
43
+ it('preserves non-auth fetch failures as CliError', async () => {
44
44
  const cmd = getRegistry().get('zhihu/question');
45
- expect(cmd?.func).toBeTypeOf('function');
46
45
  const page = {
47
- evaluate: vi.fn().mockResolvedValue({ ok: false, status: 500 }),
46
+ goto: vi.fn().mockResolvedValue(undefined),
47
+ evaluate: vi.fn().mockResolvedValue({ __httpError: 500 }),
48
48
  };
49
49
  await expect(cmd.func(page, { id: '2021881398772981878', limit: 3 })).rejects.toMatchObject({
50
50
  code: 'FETCH_ERROR',
51
- message: 'Zhihu question answers request failed with HTTP 500',
51
+ message: 'Zhihu question answers request failed (HTTP 500)',
52
52
  });
53
53
  });
54
+ it('handles null evaluate response as fetch error', async () => {
55
+ const cmd = getRegistry().get('zhihu/question');
56
+ const page = {
57
+ goto: vi.fn().mockResolvedValue(undefined),
58
+ evaluate: vi.fn().mockResolvedValue(null),
59
+ };
60
+ await expect(cmd.func(page, { id: '2021881398772981878', limit: 3 })).rejects.toMatchObject({
61
+ code: 'FETCH_ERROR',
62
+ message: 'Zhihu question answers request failed',
63
+ });
64
+ });
65
+ it('rejects non-numeric question IDs', async () => {
66
+ const cmd = getRegistry().get('zhihu/question');
67
+ const page = { goto: vi.fn(), evaluate: vi.fn() };
68
+ await expect(cmd.func(page, { id: "abc'; alert(1); //", limit: 1 })).rejects.toBeInstanceOf(CliError);
69
+ expect(page.goto).not.toHaveBeenCalled();
70
+ expect(page.evaluate).not.toHaveBeenCalled();
71
+ });
54
72
  });
@@ -49,7 +49,8 @@ export function registerCommandToProgram(siteCmd, cmd) {
49
49
  positionalArgs.push(arg);
50
50
  }
51
51
  else {
52
- const flag = arg.required ? `--${arg.name} <value>` : `--${arg.name} [value]`;
52
+ const expectsValue = arg.required || arg.valueRequired;
53
+ const flag = expectsValue ? `--${arg.name} <value>` : `--${arg.name} [value]`;
53
54
  if (arg.required)
54
55
  subCmd.requiredOption(flag, arg.help ?? '');
55
56
  else if (arg.default != null)
@@ -83,8 +84,10 @@ export function registerCommandToProgram(siteCmd, cmd) {
83
84
  if (v !== undefined)
84
85
  kwargs[arg.name] = normalizeArgValue(arg.type, v, arg.name);
85
86
  }
87
+ cmd.validateArgs?.(kwargs);
86
88
  const verbose = optionsRecord.verbose === true;
87
89
  let format = typeof optionsRecord.format === 'string' ? optionsRecord.format : 'table';
90
+ const formatExplicit = subCmd.getOptionValueSource('format') === 'cli';
88
91
  if (verbose)
89
92
  process.env.OPENCLI_VERBOSE = '1';
90
93
  if (cmd.deprecated) {
@@ -97,7 +100,7 @@ export function registerCommandToProgram(siteCmd, cmd) {
97
100
  return;
98
101
  }
99
102
  const resolved = getRegistry().get(fullName(cmd)) ?? cmd;
100
- if (format === 'table' && resolved.defaultFormat) {
103
+ if (!formatExplicit && format === 'table' && resolved.defaultFormat) {
101
104
  format = resolved.defaultFormat;
102
105
  }
103
106
  if (verbose && (!result || (Array.isArray(result) && result.length === 0))) {
@@ -105,6 +108,7 @@ export function registerCommandToProgram(siteCmd, cmd) {
105
108
  }
106
109
  renderOutput(result, {
107
110
  fmt: format,
111
+ fmtExplicit: formatExplicit,
108
112
  columns: resolved.columns,
109
113
  title: `${resolved.site}/${resolved.name}`,
110
114
  elapsed: (Date.now() - startTime) / 1000,
@@ -198,7 +202,7 @@ async function renderError(err, cmdName, verbose) {
198
202
  if (err instanceof AuthRequiredError) {
199
203
  console.error(chalk.red(`🔒 Not logged in to ${err.domain}`));
200
204
  // Respect custom hints set by the adapter; fall back to generic guidance.
201
- console.error(chalk.yellow(`→ ${err.hint ?? `Open Chrome and log in to https://${err.domain}, then retry.`}`));
205
+ console.error(chalk.yellow(`→ ${err.hint ?? `Open Chrome or Chromium and log in to https://${err.domain}, then retry.`}`));
202
206
  return;
203
207
  }
204
208
  // ── TimeoutError ──────────────────────────────────────────────────────
@@ -255,7 +259,7 @@ async function renderError(err, cmdName, verbose) {
255
259
  const kind = classifyGenericError(msg);
256
260
  if (kind === 'auth') {
257
261
  console.error(chalk.red(`🔒 ${msg}`));
258
- console.error(chalk.yellow('→ Open Chrome, log in to the target site, then retry.'));
262
+ console.error(chalk.yellow('→ Open Chrome or Chromium, log in to the target site, then retry.'));
259
263
  return;
260
264
  }
261
265
  if (kind === 'http') {
@@ -100,6 +100,48 @@ describe('commanderAdapter boolean alias support', () => {
100
100
  expect(kwargs.undo).toBe(false);
101
101
  });
102
102
  });
103
+ describe('commanderAdapter value-required optional options', () => {
104
+ const cmd = {
105
+ site: 'instagram',
106
+ name: 'post',
107
+ description: 'Post to Instagram',
108
+ browser: true,
109
+ args: [
110
+ { name: 'image', valueRequired: true, help: 'Single image path' },
111
+ { name: 'images', valueRequired: true, help: 'Comma-separated image paths' },
112
+ { name: 'content', positional: true, required: false, help: 'Caption text' },
113
+ ],
114
+ validateArgs: (kwargs) => {
115
+ if (!kwargs.image && !kwargs.images) {
116
+ throw new Error('media required');
117
+ }
118
+ },
119
+ func: vi.fn(),
120
+ };
121
+ beforeEach(() => {
122
+ mockExecuteCommand.mockReset();
123
+ mockExecuteCommand.mockResolvedValue([]);
124
+ mockRenderOutput.mockReset();
125
+ delete process.env.OPENCLI_VERBOSE;
126
+ process.exitCode = undefined;
127
+ });
128
+ it('requires a value when --image is present', async () => {
129
+ const program = new Command();
130
+ program.exitOverride();
131
+ const siteCmd = program.command('instagram');
132
+ registerCommandToProgram(siteCmd, cmd);
133
+ await expect(program.parseAsync(['node', 'opencli', 'instagram', 'post', '--image'])).rejects.toMatchObject({ code: 'commander.optionMissingArgument' });
134
+ expect(mockExecuteCommand).not.toHaveBeenCalled();
135
+ });
136
+ it('runs validateArgs before executeCommand so missing media does not dispatch the browser command', async () => {
137
+ const program = new Command();
138
+ const siteCmd = program.command('instagram');
139
+ registerCommandToProgram(siteCmd, cmd);
140
+ await program.parseAsync(['node', 'opencli', 'instagram', 'post', 'caption only']);
141
+ expect(mockExecuteCommand).not.toHaveBeenCalled();
142
+ expect(process.exitCode).toBeDefined();
143
+ });
144
+ });
103
145
  describe('commanderAdapter command aliases', () => {
104
146
  const cmd = {
105
147
  site: 'notebooklm',
@@ -21,7 +21,9 @@ const BUILTIN_COMMANDS = [
21
21
  'generate',
22
22
  'cascade',
23
23
  'doctor',
24
- 'setup',
24
+ 'plugin',
25
+ 'install',
26
+ 'register',
25
27
  'completion',
26
28
  ];
27
29
  /**
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ const { mockGetRegistry } = vi.hoisted(() => ({
3
+ mockGetRegistry: vi.fn(() => new Map([
4
+ ['github/issues', { site: 'github', name: 'issues' }],
5
+ ])),
6
+ }));
7
+ vi.mock('./registry.js', () => ({
8
+ getRegistry: mockGetRegistry,
9
+ }));
10
+ import { getCompletions } from './completion.js';
11
+ describe('getCompletions', () => {
12
+ it('includes top-level built-ins that are registered outside the site registry', () => {
13
+ const completions = getCompletions([], 1);
14
+ expect(completions).toContain('plugin');
15
+ expect(completions).toContain('install');
16
+ expect(completions).toContain('register');
17
+ expect(completions).not.toContain('setup');
18
+ });
19
+ it('still includes discovered site names', () => {
20
+ const completions = getCompletions([], 1);
21
+ expect(completions).toContain('github');
22
+ });
23
+ });
package/dist/doctor.js CHANGED
@@ -55,7 +55,7 @@ export async function runBrowserDoctor(opts = {}) {
55
55
  issues.push('Daemon is not running. It should start automatically when you run an opencli browser command.');
56
56
  }
57
57
  if (status.running && !status.extensionConnected) {
58
- issues.push('Daemon is running but the Chrome extension is not connected.\n' +
58
+ issues.push('Daemon is running but the Chrome/Chromium extension is not connected.\n' +
59
59
  'Please install the opencli Browser Bridge extension:\n' +
60
60
  ' 1. Download from https://github.com/jackwener/opencli/releases\n' +
61
61
  ' 2. Open chrome://extensions/ → Enable Developer Mode\n' +
@@ -9,6 +9,8 @@ export interface ElectronAppEntry {
9
9
  port: number;
10
10
  /** macOS process name for detection via pgrep */
11
11
  processName: string;
12
+ /** Candidate executable names inside Contents/MacOS/, tried in order */
13
+ executableNames?: string[];
12
14
  /** macOS bundle ID for path discovery */
13
15
  bundleId?: string;
14
16
  /** Human-readable name for prompts */
@@ -15,7 +15,13 @@ export const builtinApps = {
15
15
  notion: { port: 9230, processName: 'Notion', bundleId: 'notion.id', displayName: 'Notion' },
16
16
  'discord-app': { port: 9232, processName: 'Discord', bundleId: 'com.discord.app', displayName: 'Discord' },
17
17
  'doubao-app': { port: 9225, processName: 'Doubao', bundleId: 'com.volcengine.doubao', displayName: 'Doubao' },
18
- antigravity: { port: 9234, processName: 'Antigravity', bundleId: 'dev.antigravity.app', displayName: 'Antigravity' },
18
+ antigravity: {
19
+ port: 9234,
20
+ processName: 'Antigravity',
21
+ executableNames: ['Electron', 'Antigravity'],
22
+ bundleId: 'dev.antigravity.app',
23
+ displayName: 'Antigravity',
24
+ },
19
25
  chatgpt: { port: 9236, processName: 'ChatGPT', bundleId: 'com.openai.chat', displayName: 'ChatGPT' },
20
26
  };
21
27
  /** Merge builtin + user-defined apps. User entries are additive only. */
package/dist/errors.js CHANGED
@@ -72,7 +72,7 @@ export class ConfigError extends CliError {
72
72
  export class AuthRequiredError extends CliError {
73
73
  domain;
74
74
  constructor(domain, message) {
75
- super('AUTH_REQUIRED', message ?? `Not logged in to ${domain}`, `Please open Chrome and log in to https://${domain}`, EXIT_CODES.NOPERM);
75
+ super('AUTH_REQUIRED', message ?? `Not logged in to ${domain}`, `Please open Chrome or Chromium and log in to https://${domain}`, EXIT_CODES.NOPERM);
76
76
  this.domain = domain;
77
77
  }
78
78
  }