@jackwener/opencli 1.6.0 → 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 (390) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +27 -45
  4. package/README.zh-CN.md +32 -34
  5. package/autoresearch/browse-tasks.json +18 -20
  6. package/autoresearch/commands/debug.ts +163 -0
  7. package/autoresearch/commands/fix.ts +145 -0
  8. package/autoresearch/commands/plan.ts +88 -0
  9. package/autoresearch/commands/run.ts +138 -0
  10. package/autoresearch/config.ts +82 -0
  11. package/autoresearch/engine.ts +359 -0
  12. package/autoresearch/eval-all.ts +127 -0
  13. package/autoresearch/eval-browse.ts +1 -1
  14. package/autoresearch/eval-publish.ts +238 -0
  15. package/autoresearch/eval-save.ts +249 -0
  16. package/autoresearch/eval-skill.ts +14 -8
  17. package/autoresearch/eval-v2ex.ts +220 -0
  18. package/autoresearch/eval-zhihu.ts +230 -0
  19. package/autoresearch/logger.ts +69 -0
  20. package/autoresearch/presets/combined-reliability.ts +27 -0
  21. package/autoresearch/presets/index.ts +23 -0
  22. package/autoresearch/presets/operate-reliability.ts +24 -0
  23. package/autoresearch/presets/save-reliability.ts +26 -0
  24. package/autoresearch/presets/skill-quality.ts +20 -0
  25. package/autoresearch/presets/v2ex-reliability.ts +24 -0
  26. package/autoresearch/presets/zhihu-reliability.ts +25 -0
  27. package/autoresearch/publish-tasks.json +345 -0
  28. package/autoresearch/run-save.sh +11 -0
  29. package/autoresearch/save-adapters/xhs-explore-deep.ts +64 -0
  30. package/autoresearch/save-adapters/xhs-note-comments.ts +61 -0
  31. package/autoresearch/save-adapters/xhs-search-full.ts +62 -0
  32. package/autoresearch/save-adapters/zhihu-hot-detail.ts +52 -0
  33. package/autoresearch/save-adapters/zhihu-question-full.ts +57 -0
  34. package/autoresearch/save-adapters/zhihu-search-detail.ts +53 -0
  35. package/autoresearch/save-tasks.json +281 -0
  36. package/autoresearch/v2ex-tasks.json +899 -0
  37. package/autoresearch/zhihu-tasks.json +848 -0
  38. package/bun.lock +615 -0
  39. package/dist/browser/base-page.d.ts +4 -2
  40. package/dist/browser/base-page.js +37 -4
  41. package/dist/browser/bridge.js +10 -8
  42. package/dist/browser/cdp.js +2 -6
  43. package/dist/browser/daemon-client.d.ts +11 -1
  44. package/dist/browser/daemon-client.js +3 -0
  45. package/dist/browser/dom-helpers.d.ts +4 -2
  46. package/dist/browser/dom-helpers.js +42 -31
  47. package/dist/browser/dom-snapshot.js +23 -1
  48. package/dist/browser/page.d.ts +7 -2
  49. package/dist/browser/page.js +112 -30
  50. package/dist/browser.test.js +1 -1
  51. package/dist/build-manifest.d.ts +1 -0
  52. package/dist/build-manifest.js +1 -0
  53. package/dist/cli-manifest.json +1133 -182
  54. package/dist/cli.d.ts +2 -0
  55. package/dist/cli.js +48 -7
  56. package/dist/cli.test.d.ts +1 -0
  57. package/dist/cli.test.js +88 -0
  58. package/dist/clis/1688/item.d.ts +70 -0
  59. package/dist/clis/1688/item.js +187 -0
  60. package/dist/clis/1688/item.test.d.ts +1 -0
  61. package/dist/clis/1688/item.test.js +67 -0
  62. package/dist/clis/1688/search.d.ts +56 -0
  63. package/dist/clis/1688/search.js +309 -0
  64. package/dist/clis/1688/search.test.d.ts +1 -0
  65. package/dist/clis/1688/search.test.js +75 -0
  66. package/dist/clis/1688/shared.d.ts +112 -0
  67. package/dist/clis/1688/shared.js +514 -0
  68. package/dist/clis/1688/shared.test.d.ts +1 -0
  69. package/dist/clis/1688/shared.test.js +57 -0
  70. package/dist/clis/1688/store.d.ts +45 -0
  71. package/dist/clis/1688/store.js +226 -0
  72. package/dist/clis/1688/store.test.d.ts +1 -0
  73. package/dist/clis/1688/store.test.js +62 -0
  74. package/dist/clis/amazon/bestsellers.d.ts +0 -20
  75. package/dist/clis/amazon/bestsellers.js +6 -129
  76. package/dist/clis/amazon/bestsellers.test.js +12 -3
  77. package/dist/clis/amazon/movers-shakers.d.ts +1 -0
  78. package/dist/clis/amazon/movers-shakers.js +7 -0
  79. package/dist/clis/amazon/new-releases.d.ts +1 -0
  80. package/dist/clis/amazon/new-releases.js +7 -0
  81. package/dist/clis/amazon/rankings.d.ts +59 -0
  82. package/dist/clis/amazon/rankings.js +226 -0
  83. package/dist/clis/amazon/rankings.test.d.ts +1 -0
  84. package/dist/clis/amazon/rankings.test.js +41 -0
  85. package/dist/clis/amazon/shared.d.ts +11 -0
  86. package/dist/clis/amazon/shared.js +121 -11
  87. package/dist/clis/amazon/shared.test.js +11 -0
  88. package/dist/clis/bilibili/comments.js +2 -2
  89. package/dist/clis/bilibili/comments.test.js +3 -2
  90. package/dist/clis/bilibili/download.js +2 -1
  91. package/dist/clis/bilibili/subtitle.js +4 -3
  92. package/dist/clis/bilibili/subtitle.test.js +2 -1
  93. package/dist/clis/bilibili/utils.d.ts +5 -0
  94. package/dist/clis/bilibili/utils.js +30 -0
  95. package/dist/clis/bilibili/utils.test.d.ts +1 -0
  96. package/dist/clis/bilibili/utils.test.js +17 -0
  97. package/dist/clis/douban/marks.js +1 -1
  98. package/dist/clis/douban/subject.yaml +50 -19
  99. package/dist/clis/doubao/utils.js +32 -12
  100. package/dist/clis/douyin/_shared/browser-fetch.test.js +0 -1
  101. package/dist/clis/douyin/_shared/transcode.test.js +0 -2
  102. package/dist/clis/douyin/draft.test.js +0 -2
  103. package/dist/clis/facebook/search.test.js +0 -2
  104. package/dist/clis/gemini/ask.js +9 -3
  105. package/dist/clis/gemini/ask.test.d.ts +1 -0
  106. package/dist/clis/gemini/ask.test.js +100 -0
  107. package/dist/clis/gemini/reply-state.test.d.ts +1 -0
  108. package/dist/clis/gemini/reply-state.test.js +641 -0
  109. package/dist/clis/gemini/utils.d.ts +44 -1
  110. package/dist/clis/gemini/utils.js +528 -61
  111. package/dist/clis/gemini/utils.test.js +149 -2
  112. package/dist/clis/hupu/detail.d.ts +1 -0
  113. package/dist/clis/hupu/detail.js +72 -0
  114. package/dist/clis/hupu/hot.yaml +43 -0
  115. package/dist/clis/hupu/like.d.ts +1 -0
  116. package/dist/clis/hupu/like.js +75 -0
  117. package/dist/clis/hupu/reply.d.ts +1 -0
  118. package/dist/clis/hupu/reply.js +71 -0
  119. package/dist/clis/hupu/search.d.ts +1 -0
  120. package/dist/clis/hupu/search.js +59 -0
  121. package/dist/clis/hupu/unlike.d.ts +1 -0
  122. package/dist/clis/hupu/unlike.js +75 -0
  123. package/dist/clis/hupu/utils.d.ts +20 -0
  124. package/dist/clis/hupu/utils.js +319 -0
  125. package/dist/clis/instagram/_shared/private-publish.d.ts +138 -0
  126. package/dist/clis/instagram/_shared/private-publish.js +1030 -0
  127. package/dist/clis/instagram/_shared/private-publish.test.d.ts +1 -0
  128. package/dist/clis/instagram/_shared/private-publish.test.js +705 -0
  129. package/dist/clis/instagram/_shared/protocol-capture.d.ts +26 -0
  130. package/dist/clis/instagram/_shared/protocol-capture.js +282 -0
  131. package/dist/clis/instagram/_shared/protocol-capture.test.d.ts +1 -0
  132. package/dist/clis/instagram/_shared/protocol-capture.test.js +114 -0
  133. package/dist/clis/instagram/_shared/runtime-info.d.ts +9 -0
  134. package/dist/clis/instagram/_shared/runtime-info.js +81 -0
  135. package/dist/clis/instagram/note.d.ts +1 -0
  136. package/dist/clis/instagram/note.js +222 -0
  137. package/dist/clis/instagram/note.test.d.ts +1 -0
  138. package/dist/clis/instagram/note.test.js +81 -0
  139. package/dist/clis/instagram/post.d.ts +4 -0
  140. package/dist/clis/instagram/post.js +1496 -0
  141. package/dist/clis/instagram/post.test.d.ts +1 -0
  142. package/dist/clis/instagram/post.test.js +1647 -0
  143. package/dist/clis/instagram/reel.d.ts +1 -0
  144. package/dist/clis/instagram/reel.js +826 -0
  145. package/dist/clis/instagram/reel.test.d.ts +1 -0
  146. package/dist/clis/instagram/reel.test.js +167 -0
  147. package/dist/clis/instagram/story.d.ts +1 -0
  148. package/dist/clis/instagram/story.js +115 -0
  149. package/dist/clis/instagram/story.test.d.ts +1 -0
  150. package/dist/clis/instagram/story.test.js +167 -0
  151. package/dist/clis/sinafinance/stock-rank.d.ts +4 -0
  152. package/dist/clis/sinafinance/stock-rank.js +65 -0
  153. package/dist/clis/substack/utils.test.js +0 -2
  154. package/dist/clis/twitter/post.js +72 -45
  155. package/dist/clis/twitter/post.test.d.ts +1 -0
  156. package/dist/clis/twitter/post.test.js +116 -0
  157. package/dist/clis/twitter/reply.d.ts +12 -0
  158. package/dist/clis/twitter/reply.js +257 -35
  159. package/dist/clis/twitter/reply.test.d.ts +1 -0
  160. package/dist/clis/twitter/reply.test.js +151 -0
  161. package/dist/clis/twitter/search.js +67 -5
  162. package/dist/clis/twitter/search.test.js +83 -5
  163. package/dist/clis/xianyu/chat.d.ts +7 -0
  164. package/dist/clis/xianyu/chat.js +146 -0
  165. package/dist/clis/xianyu/chat.test.d.ts +1 -0
  166. package/dist/clis/xianyu/chat.test.js +15 -0
  167. package/dist/clis/xianyu/item.d.ts +7 -0
  168. package/dist/clis/xianyu/item.js +152 -0
  169. package/dist/clis/xianyu/item.test.d.ts +1 -0
  170. package/dist/clis/xianyu/item.test.js +56 -0
  171. package/dist/clis/xianyu/search.d.ts +10 -0
  172. package/dist/clis/xianyu/search.js +134 -0
  173. package/dist/clis/xianyu/search.test.d.ts +1 -0
  174. package/dist/clis/xianyu/search.test.js +17 -0
  175. package/dist/clis/xianyu/utils.d.ts +1 -0
  176. package/dist/clis/xianyu/utils.js +8 -0
  177. package/dist/clis/xiaoe/catalog.yaml +129 -0
  178. package/dist/clis/xiaoe/content.yaml +43 -0
  179. package/dist/clis/xiaoe/courses.yaml +73 -0
  180. package/dist/clis/xiaoe/detail.yaml +39 -0
  181. package/dist/clis/xiaoe/play-url.yaml +124 -0
  182. package/dist/clis/xiaohongshu/comments.test.js +0 -2
  183. package/dist/clis/xiaohongshu/creator-note-detail.test.js +0 -2
  184. package/dist/clis/xiaohongshu/creator-notes.test.js +0 -2
  185. package/dist/clis/xiaohongshu/download.test.js +0 -2
  186. package/dist/clis/xiaohongshu/note.test.js +0 -2
  187. package/dist/clis/xiaohongshu/publish.test.js +0 -2
  188. package/dist/clis/xiaohongshu/search.js +29 -20
  189. package/dist/clis/xiaohongshu/search.test.js +56 -48
  190. package/dist/clis/yuanbao/ask.d.ts +21 -0
  191. package/dist/clis/yuanbao/ask.js +427 -0
  192. package/dist/clis/yuanbao/ask.test.d.ts +1 -0
  193. package/dist/clis/yuanbao/ask.test.js +124 -0
  194. package/dist/clis/yuanbao/new.d.ts +1 -0
  195. package/dist/clis/yuanbao/new.js +70 -0
  196. package/dist/clis/yuanbao/new.test.d.ts +1 -0
  197. package/dist/clis/yuanbao/new.test.js +30 -0
  198. package/dist/clis/yuanbao/shared.d.ts +13 -0
  199. package/dist/clis/yuanbao/shared.js +49 -0
  200. package/dist/clis/zhihu/question.js +30 -19
  201. package/dist/clis/zhihu/question.test.js +34 -16
  202. package/dist/commanderAdapter.js +8 -4
  203. package/dist/commanderAdapter.test.js +42 -0
  204. package/dist/completion.js +3 -1
  205. package/dist/completion.test.d.ts +1 -0
  206. package/dist/completion.test.js +23 -0
  207. package/dist/doctor.js +1 -1
  208. package/dist/electron-apps.d.ts +2 -0
  209. package/dist/electron-apps.js +7 -1
  210. package/dist/errors.js +1 -1
  211. package/dist/execution.js +25 -35
  212. package/dist/explore.js +1 -1
  213. package/dist/launcher.d.ts +4 -0
  214. package/dist/launcher.js +64 -8
  215. package/dist/launcher.test.js +88 -7
  216. package/dist/output.d.ts +2 -0
  217. package/dist/output.js +10 -1
  218. package/dist/output.test.d.ts +0 -3
  219. package/dist/output.test.js +59 -92
  220. package/dist/pipeline/executor.test.js +0 -2
  221. package/dist/pipeline/steps/download.test.js +0 -2
  222. package/dist/registry.d.ts +2 -0
  223. package/dist/serialization.d.ts +1 -0
  224. package/dist/serialization.js +1 -0
  225. package/dist/types.d.ts +9 -2
  226. package/docs/.vitepress/config.mts +4 -0
  227. package/docs/adapters/browser/1688.md +52 -0
  228. package/docs/adapters/browser/36kr.md +2 -1
  229. package/docs/adapters/browser/doubao.md +5 -1
  230. package/docs/adapters/browser/hupu.md +53 -0
  231. package/docs/adapters/browser/sinafinance.md +32 -2
  232. package/docs/adapters/browser/weibo.md +6 -1
  233. package/docs/adapters/browser/wikipedia.md +2 -0
  234. package/docs/adapters/browser/xianyu.md +42 -0
  235. package/docs/adapters/browser/xiaoe.md +44 -0
  236. package/docs/adapters/browser/yuanbao.md +64 -0
  237. package/docs/adapters/index.md +14 -5
  238. package/docs/comparison.md +1 -1
  239. package/docs/developer/ai-workflow.md +2 -2
  240. package/docs/developer/contributing.md +1 -1
  241. package/docs/developer/testing.md +2 -0
  242. package/docs/guide/plugins.md +1 -0
  243. package/docs/guide/troubleshooting.md +11 -0
  244. package/docs/superpowers/specs/2026-04-03-v2ex-autoresearch-design.md +41 -0
  245. package/docs/zh/guide/plugins.md +1 -0
  246. package/extension/dist/background.js +1127 -0
  247. package/extension/src/background.test.ts +39 -0
  248. package/extension/src/background.ts +223 -34
  249. package/extension/src/cdp.ts +194 -4
  250. package/extension/src/protocol.ts +22 -1
  251. package/package.json +3 -2
  252. package/scripts/postinstall.js +1 -1
  253. package/skills/opencli-explorer/SKILL.md +1 -1
  254. package/skills/opencli-oneshot/SKILL.md +2 -2
  255. package/skills/opencli-operate/SKILL.md +120 -27
  256. package/skills/opencli-usage/SKILL.md +31 -20
  257. package/skills/opencli-usage/browser.md +114 -16
  258. package/skills/opencli-usage/public-api.md +32 -3
  259. package/skills/smart-search/SKILL.md +156 -0
  260. package/skills/smart-search/references/sources-ai.md +74 -0
  261. package/skills/smart-search/references/sources-info.md +43 -0
  262. package/skills/smart-search/references/sources-media.md +50 -0
  263. package/skills/smart-search/references/sources-other.md +42 -0
  264. package/skills/smart-search/references/sources-shopping.md +31 -0
  265. package/skills/smart-search/references/sources-social.md +51 -0
  266. package/skills/smart-search/references/sources-tech.md +42 -0
  267. package/skills/smart-search/references/sources-travel.md +20 -0
  268. package/src/browser/base-page.ts +41 -6
  269. package/src/browser/bridge.ts +11 -8
  270. package/src/browser/cdp.ts +1 -8
  271. package/src/browser/daemon-client.ts +11 -1
  272. package/src/browser/dom-helpers.ts +43 -31
  273. package/src/browser/dom-snapshot.ts +23 -1
  274. package/src/browser/page.ts +115 -31
  275. package/src/browser.test.ts +1 -1
  276. package/src/build-manifest.ts +2 -0
  277. package/src/cli.test.ts +133 -0
  278. package/src/cli.ts +73 -11
  279. package/src/clis/1688/item.test.ts +69 -0
  280. package/src/clis/1688/item.ts +282 -0
  281. package/src/clis/1688/search.test.ts +81 -0
  282. package/src/clis/1688/search.ts +402 -0
  283. package/src/clis/1688/shared.test.ts +75 -0
  284. package/src/clis/1688/shared.ts +623 -0
  285. package/src/clis/1688/store.test.ts +69 -0
  286. package/src/clis/1688/store.ts +300 -0
  287. package/src/clis/amazon/bestsellers.test.ts +12 -3
  288. package/src/clis/amazon/bestsellers.ts +6 -178
  289. package/src/clis/amazon/movers-shakers.ts +8 -0
  290. package/src/clis/amazon/new-releases.ts +8 -0
  291. package/src/clis/amazon/rankings.test.ts +47 -0
  292. package/src/clis/amazon/rankings.ts +312 -0
  293. package/src/clis/amazon/shared.test.ts +16 -0
  294. package/src/clis/amazon/shared.ts +134 -12
  295. package/src/clis/bilibili/comments.test.ts +4 -3
  296. package/src/clis/bilibili/comments.ts +2 -2
  297. package/src/clis/bilibili/download.ts +2 -1
  298. package/src/clis/bilibili/subtitle.test.ts +2 -1
  299. package/src/clis/bilibili/subtitle.ts +4 -3
  300. package/src/clis/bilibili/utils.test.ts +21 -0
  301. package/src/clis/bilibili/utils.ts +27 -0
  302. package/src/clis/douban/marks.ts +1 -1
  303. package/src/clis/douban/subject.yaml +50 -19
  304. package/src/clis/doubao/utils.ts +32 -12
  305. package/src/clis/douyin/_shared/browser-fetch.test.ts +0 -1
  306. package/src/clis/douyin/_shared/transcode.test.ts +0 -2
  307. package/src/clis/douyin/draft.test.ts +0 -2
  308. package/src/clis/facebook/search.test.ts +0 -2
  309. package/src/clis/gemini/ask.test.ts +116 -0
  310. package/src/clis/gemini/ask.ts +10 -3
  311. package/src/clis/gemini/reply-state.test.ts +708 -0
  312. package/src/clis/gemini/utils.test.ts +184 -2
  313. package/src/clis/gemini/utils.ts +588 -60
  314. package/src/clis/hupu/detail.ts +126 -0
  315. package/src/clis/hupu/hot.yaml +43 -0
  316. package/src/clis/hupu/like.ts +76 -0
  317. package/src/clis/hupu/reply.ts +76 -0
  318. package/src/clis/hupu/search.ts +95 -0
  319. package/src/clis/hupu/unlike.ts +76 -0
  320. package/src/clis/hupu/utils.ts +381 -0
  321. package/src/clis/instagram/_shared/private-publish.test.ts +827 -0
  322. package/src/clis/instagram/_shared/private-publish.ts +1303 -0
  323. package/src/clis/instagram/_shared/protocol-capture.test.ts +148 -0
  324. package/src/clis/instagram/_shared/protocol-capture.ts +321 -0
  325. package/src/clis/instagram/_shared/runtime-info.ts +91 -0
  326. package/src/clis/instagram/note.test.ts +96 -0
  327. package/src/clis/instagram/note.ts +254 -0
  328. package/src/clis/instagram/post.test.ts +1716 -0
  329. package/src/clis/instagram/post.ts +1620 -0
  330. package/src/clis/instagram/reel.test.ts +191 -0
  331. package/src/clis/instagram/reel.ts +886 -0
  332. package/src/clis/instagram/story.test.ts +191 -0
  333. package/src/clis/instagram/story.ts +151 -0
  334. package/src/clis/sinafinance/stock-rank.ts +68 -0
  335. package/src/clis/substack/utils.test.ts +0 -2
  336. package/src/clis/twitter/post.test.ts +157 -0
  337. package/src/clis/twitter/post.ts +82 -48
  338. package/src/clis/twitter/reply.test.ts +177 -0
  339. package/src/clis/twitter/reply.ts +285 -39
  340. package/src/clis/twitter/search.test.ts +88 -5
  341. package/src/clis/twitter/search.ts +68 -5
  342. package/src/clis/xianyu/chat.test.ts +20 -0
  343. package/src/clis/xianyu/chat.ts +175 -0
  344. package/src/clis/xianyu/item.test.ts +67 -0
  345. package/src/clis/xianyu/item.ts +172 -0
  346. package/src/clis/xianyu/search.test.ts +22 -0
  347. package/src/clis/xianyu/search.ts +151 -0
  348. package/src/clis/xianyu/utils.ts +9 -0
  349. package/src/clis/xiaoe/catalog.yaml +129 -0
  350. package/src/clis/xiaoe/content.yaml +43 -0
  351. package/src/clis/xiaoe/courses.yaml +73 -0
  352. package/src/clis/xiaoe/detail.yaml +39 -0
  353. package/src/clis/xiaoe/play-url.yaml +124 -0
  354. package/src/clis/xiaohongshu/comments.test.ts +0 -2
  355. package/src/clis/xiaohongshu/creator-note-detail.test.ts +0 -2
  356. package/src/clis/xiaohongshu/creator-notes.test.ts +0 -2
  357. package/src/clis/xiaohongshu/download.test.ts +0 -2
  358. package/src/clis/xiaohongshu/note.test.ts +0 -2
  359. package/src/clis/xiaohongshu/publish.test.ts +0 -2
  360. package/src/clis/xiaohongshu/search.test.ts +59 -48
  361. package/src/clis/xiaohongshu/search.ts +31 -21
  362. package/src/clis/yuanbao/ask.test.ts +156 -0
  363. package/src/clis/yuanbao/ask.ts +522 -0
  364. package/src/clis/yuanbao/new.test.ts +36 -0
  365. package/src/clis/yuanbao/new.ts +81 -0
  366. package/src/clis/yuanbao/shared.ts +57 -0
  367. package/src/clis/zhihu/question.test.ts +42 -17
  368. package/src/clis/zhihu/question.ts +31 -26
  369. package/src/commanderAdapter.test.ts +51 -0
  370. package/src/commanderAdapter.ts +8 -4
  371. package/src/completion.test.ts +30 -0
  372. package/src/completion.ts +3 -1
  373. package/src/doctor.ts +1 -1
  374. package/src/electron-apps.ts +9 -1
  375. package/src/errors.ts +1 -1
  376. package/src/execution.ts +26 -30
  377. package/src/explore.ts +1 -1
  378. package/src/launcher.test.ts +121 -7
  379. package/src/launcher.ts +87 -9
  380. package/src/output.test.ts +50 -90
  381. package/src/output.ts +10 -1
  382. package/src/pipeline/executor.test.ts +0 -2
  383. package/src/pipeline/steps/download.test.ts +0 -2
  384. package/src/registry.ts +2 -0
  385. package/src/serialization.ts +2 -0
  386. package/src/types.ts +9 -2
  387. package/tests/e2e/browser-auth.test.ts +9 -0
  388. package/CLI-EXPLORER.md +0 -724
  389. package/CLI-ONESHOT.md +0 -216
  390. package/SKILL.md +0 -59
@@ -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
  }
package/dist/execution.js CHANGED
@@ -19,7 +19,7 @@ import { emitHook } from './hooks.js';
19
19
  import { checkDaemonStatus } from './browser/discover.js';
20
20
  import { log } from './logger.js';
21
21
  import { isElectronApp } from './electron-apps.js';
22
- import { resolveElectronEndpoint } from './launcher.js';
22
+ import { probeCDP, resolveElectronEndpoint } from './launcher.js';
23
23
  const _loadedModules = new Set();
24
24
  export function coerceAndValidateArgs(cmdArgs, kwargs) {
25
25
  const result = { ...kwargs };
@@ -111,29 +111,11 @@ function ensureRequiredEnv(cmd) {
111
111
  return;
112
112
  throw new CommandExecutionError(`Command ${fullName(cmd)} requires environment variable ${missing.name}.`, missing.help ?? `Set ${missing.name} before running ${fullName(cmd)}.`);
113
113
  }
114
- /**
115
- * Check if the browser is already on the target domain, avoiding redundant navigation.
116
- * Returns true if current page hostname matches the pre-nav URL hostname.
117
- */
118
- async function isAlreadyOnDomain(page, targetUrl) {
119
- if (!page.getCurrentUrl)
120
- return false;
121
- try {
122
- const currentUrl = await page.getCurrentUrl();
123
- if (!currentUrl)
124
- return false;
125
- const currentHost = new URL(currentUrl).hostname;
126
- const targetHost = new URL(targetUrl).hostname;
127
- return currentHost === targetHost;
128
- }
129
- catch {
130
- return false;
131
- }
132
- }
133
114
  export async function executeCommand(cmd, rawKwargs, debug = false) {
134
115
  let kwargs;
135
116
  try {
136
117
  kwargs = coerceAndValidateArgs(cmd.args, rawKwargs);
118
+ cmd.validateArgs?.(kwargs);
137
119
  }
138
120
  catch (err) {
139
121
  if (err instanceof ArgumentError)
@@ -152,8 +134,18 @@ export async function executeCommand(cmd, rawKwargs, debug = false) {
152
134
  const electron = isElectronApp(cmd.site);
153
135
  let cdpEndpoint;
154
136
  if (electron) {
155
- // Electron apps: auto-detect, prompt restart if needed, launch with CDP
156
- cdpEndpoint = await resolveElectronEndpoint(cmd.site);
137
+ // Electron apps: respect manual endpoint override, then try auto-detect
138
+ const manualEndpoint = process.env.OPENCLI_CDP_ENDPOINT;
139
+ if (manualEndpoint) {
140
+ const port = Number(new URL(manualEndpoint).port);
141
+ if (!await probeCDP(port)) {
142
+ throw new CommandExecutionError(`CDP not reachable at ${manualEndpoint}`, 'Check that the app is running with --remote-debugging-port and the endpoint is correct.');
143
+ }
144
+ cdpEndpoint = manualEndpoint;
145
+ }
146
+ else {
147
+ cdpEndpoint = await resolveElectronEndpoint(cmd.site);
148
+ }
157
149
  }
158
150
  else {
159
151
  // Browser Bridge: fail-fast when daemon is up but extension is missing.
@@ -162,7 +154,7 @@ export async function executeCommand(cmd, rawKwargs, debug = false) {
162
154
  if (status.running && !status.extensionConnected) {
163
155
  throw new BrowserConnectError('Browser Bridge extension not connected', 'Install the Browser Bridge:\n' +
164
156
  ' 1. Download: https://github.com/jackwener/opencli/releases\n' +
165
- ' 2. chrome://extensions → Developer Mode → Load unpacked\n' +
157
+ ' 2. In Chrome or Chromium, open chrome://extensions → Developer Mode → Load unpacked\n' +
166
158
  ' Then run: opencli doctor');
167
159
  }
168
160
  }
@@ -171,19 +163,17 @@ export async function executeCommand(cmd, rawKwargs, debug = false) {
171
163
  result = await browserSession(BrowserFactory, async (page) => {
172
164
  const preNavUrl = resolvePreNav(cmd);
173
165
  if (preNavUrl) {
174
- const skip = await isAlreadyOnDomain(page, preNavUrl);
175
- if (skip) {
176
- if (debug)
177
- log.debug('[pre-nav] Already on target domain, skipping navigation');
166
+ // Navigate directly the extension's handleNavigate already has a fast-path
167
+ // that skips navigation if the tab is already at the target URL.
168
+ // This avoids an extra exec round-trip (getCurrentUrl) on first command and
169
+ // lets the extension create the automation window with the target URL directly
170
+ // instead of about:blank.
171
+ try {
172
+ await page.goto(preNavUrl);
178
173
  }
179
- else {
180
- try {
181
- await page.goto(preNavUrl);
182
- }
183
- catch (err) {
184
- if (debug)
185
- log.debug(`[pre-nav] Failed to navigate to ${preNavUrl}: ${err instanceof Error ? err.message : err}`);
186
- }
174
+ catch (err) {
175
+ if (debug)
176
+ log.debug(`[pre-nav] Failed to navigate to ${preNavUrl}: ${err instanceof Error ? err.message : err}`);
187
177
  }
188
178
  }
189
179
  return runWithTimeout(runCommand(cmd, page, kwargs, debug), {
package/dist/explore.js CHANGED
@@ -263,7 +263,7 @@ export async function exploreUrl(url, opts) {
263
263
  await page.wait(2); // wait for XHRs to settle
264
264
  }
265
265
  catch (e) {
266
- log.debug(`Interactive fuzzing skipped: ${e instanceof Error ? e.message : String(e)}`);
266
+ log.verbose(`Interactive fuzzing skipped: ${e instanceof Error ? e.message : String(e)}`);
267
267
  }
268
268
  }
269
269
  // Step 3: Read page metadata
@@ -8,6 +8,7 @@
8
8
  * 4. Launch with --remote-debugging-port
9
9
  * 5. Poll /json until ready
10
10
  */
11
+ import type { ElectronAppEntry } from './electron-apps.js';
11
12
  /**
12
13
  * Probe whether a CDP endpoint is listening on the given port.
13
14
  * Returns true if http://127.0.0.1:{port}/json responds successfully.
@@ -28,6 +29,9 @@ export declare function killProcess(processName: string): void;
28
29
  * Returns null if the app is not installed.
29
30
  */
30
31
  export declare function discoverAppPath(displayName: string): string | null;
32
+ export declare function resolveExecutableCandidates(appPath: string, app: ElectronAppEntry): string[];
33
+ export declare function launchDetachedApp(executable: string, args: string[], label: string): Promise<void>;
34
+ export declare function launchElectronApp(appPath: string, app: ElectronAppEntry, args: string[], label: string): Promise<void>;
31
35
  /**
32
36
  * Main entry point: resolve an Electron app to a CDP endpoint URL.
33
37
  *
package/dist/launcher.js CHANGED
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { execFileSync, spawn } from 'node:child_process';
12
12
  import { request as httpRequest } from 'node:http';
13
+ import * as path from 'node:path';
13
14
  import { getElectronApp } from './electron-apps.js';
14
15
  import { confirmPrompt } from './tui.js';
15
16
  import { CommandExecutionError } from './errors.js';
@@ -38,6 +39,8 @@ export function probeCDP(port, timeoutMs = PROBE_TIMEOUT_MS) {
38
39
  * Uses pgrep on macOS/Linux.
39
40
  */
40
41
  export function detectProcess(processName) {
42
+ if (process.platform === 'win32')
43
+ return false; // pgrep not available on Windows
41
44
  try {
42
45
  execFileSync('pgrep', ['-x', processName], { encoding: 'utf-8', stdio: 'pipe' });
43
46
  return true;
@@ -50,6 +53,8 @@ export function detectProcess(processName) {
50
53
  * Kill a process by name. Sends SIGTERM first, then SIGKILL after grace period.
51
54
  */
52
55
  export function killProcess(processName) {
56
+ if (process.platform === 'win32')
57
+ return; // pkill not available on Windows
53
58
  try {
54
59
  execFileSync('pkill', ['-x', processName], { stdio: 'pipe' });
55
60
  }
@@ -91,6 +96,57 @@ export function discoverAppPath(displayName) {
91
96
  function resolveExecutable(appPath, processName) {
92
97
  return `${appPath}/Contents/MacOS/${processName}`;
93
98
  }
99
+ function isMissingExecutableError(err, label) {
100
+ return err instanceof CommandExecutionError
101
+ && err.message.startsWith(`Could not launch ${label}: executable not found at `);
102
+ }
103
+ export function resolveExecutableCandidates(appPath, app) {
104
+ const executableNames = app.executableNames?.length ? app.executableNames : [app.processName];
105
+ return [...new Set(executableNames)].map((name) => resolveExecutable(appPath, name));
106
+ }
107
+ export async function launchDetachedApp(executable, args, label) {
108
+ await new Promise((resolve, reject) => {
109
+ const child = spawn(executable, args, {
110
+ detached: true,
111
+ stdio: 'ignore',
112
+ });
113
+ const onError = (err) => {
114
+ if (err.code === 'ENOENT') {
115
+ reject(new CommandExecutionError(`Could not launch ${label}: executable not found at ${executable}`, `Install ${label}, reinstall it, or register a custom app path in ~/.opencli/apps.yaml`));
116
+ return;
117
+ }
118
+ reject(new CommandExecutionError(`Failed to launch ${label}`, err.message));
119
+ };
120
+ child.once('error', onError);
121
+ child.once('spawn', () => {
122
+ child.off('error', onError);
123
+ child.unref();
124
+ resolve();
125
+ });
126
+ });
127
+ }
128
+ export async function launchElectronApp(appPath, app, args, label) {
129
+ const executables = resolveExecutableCandidates(appPath, app);
130
+ let lastMissingExecutableError;
131
+ for (const executable of executables) {
132
+ log.debug(`[launcher] Launching: ${executable} ${args.join(' ')}`);
133
+ try {
134
+ await launchDetachedApp(executable, args, label);
135
+ return;
136
+ }
137
+ catch (err) {
138
+ if (isMissingExecutableError(err, label)) {
139
+ lastMissingExecutableError = err;
140
+ continue;
141
+ }
142
+ throw err;
143
+ }
144
+ }
145
+ if (executables.length > 1) {
146
+ throw new CommandExecutionError(`Could not launch ${label}: no compatible executable found in ${path.join(appPath, 'Contents', 'MacOS')}`, `Tried: ${executables.map((executable) => path.basename(executable)).join(', ')}. Install ${label}, reinstall it, or register a custom app path in ~/.opencli/apps.yaml`);
147
+ }
148
+ throw lastMissingExecutableError ?? new CommandExecutionError(`Could not launch ${label}`, `Install ${label}, reinstall it, or register a custom app path in ~/.opencli/apps.yaml`);
149
+ }
94
150
  async function pollForReady(port) {
95
151
  const deadline = Date.now() + POLL_TIMEOUT_MS;
96
152
  while (Date.now() < deadline) {
@@ -119,7 +175,13 @@ export async function resolveElectronEndpoint(site) {
119
175
  log.debug(`[launcher] CDP already available on port ${port}`);
120
176
  return endpoint;
121
177
  }
122
- // Step 2: Running without CDP?
178
+ // Step 2: Running without CDP? (process detection requires Unix tools)
179
+ if (process.platform !== 'darwin' && process.platform !== 'linux') {
180
+ throw new CommandExecutionError(`${label} is not reachable on CDP port ${port}.`, `Auto-launch is not yet supported on ${process.platform}.\n` +
181
+ `Start ${label} manually with --remote-debugging-port=${port}, then either:\n` +
182
+ ` • Set OPENCLI_CDP_ENDPOINT=http://127.0.0.1:${port}\n` +
183
+ ` • Or just re-run the command once ${label} is listening on port ${port}.`);
184
+ }
123
185
  const isRunning = detectProcess(processName);
124
186
  if (isRunning) {
125
187
  log.debug(`[launcher] ${label} is running but CDP not available`);
@@ -136,14 +198,8 @@ export async function resolveElectronEndpoint(site) {
136
198
  throw new CommandExecutionError(`Could not find ${label} on this machine.`, `Install ${label} or register a custom path in ~/.opencli/apps.yaml`);
137
199
  }
138
200
  // Step 4: Launch
139
- const executable = resolveExecutable(appPath, processName);
140
201
  const args = [`--remote-debugging-port=${port}`, ...(app.extraArgs ?? [])];
141
- log.debug(`[launcher] Launching: ${executable} ${args.join(' ')}`);
142
- const child = spawn(executable, args, {
143
- detached: true,
144
- stdio: 'ignore',
145
- });
146
- child.unref();
202
+ await launchElectronApp(appPath, app, args, label);
147
203
  // Step 5: Poll for readiness
148
204
  process.stderr.write(` Waiting for ${label} on port ${port}...\n`);
149
205
  await pollForReady(port);