@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
@@ -58,25 +58,17 @@ export class Page extends BasePage {
58
58
  this._tabId = result.tabId;
59
59
  }
60
60
  this._lastUrl = url;
61
- // Inject stealth anti-detection patches (guard flag prevents double-injection).
62
- try {
63
- await sendCommand('exec', {
64
- code: generateStealthJs(),
65
- ...this._cmdOpts(),
66
- });
67
- } catch {
68
- // Non-fatal: stealth is best-effort
69
- }
70
- // Smart settle: use DOM stability detection instead of fixed sleep.
71
- // settleMs is now a timeout cap (default 1000ms), not a fixed wait.
61
+ // Inject stealth + settle in a single round-trip instead of two sequential exec calls.
62
+ // The stealth guard flag prevents double-injection; settle uses DOM stability detection.
72
63
  if (options?.waitUntil !== 'none') {
73
64
  const maxMs = options?.settleMs ?? 1000;
74
- const settleOpts = {
75
- code: waitForDomStableJs(maxMs, Math.min(500, maxMs)),
65
+ const combinedCode = `${generateStealthJs()};\n${waitForDomStableJs(maxMs, Math.min(500, maxMs))}`;
66
+ const combinedOpts = {
67
+ code: combinedCode,
76
68
  ...this._cmdOpts(),
77
69
  };
78
70
  try {
79
- await sendCommand('exec', settleOpts);
71
+ await sendCommand('exec', combinedOpts);
80
72
  } catch (err) {
81
73
  if (!isRetryableSettleError(err)) throw err;
82
74
  // SPA client-side redirects can invalidate the CDP target after
@@ -84,14 +76,21 @@ export class Page extends BasePage {
84
76
  // to load, then retry the settle probe once.
85
77
  try {
86
78
  await new Promise((r) => setTimeout(r, 200));
87
- await sendCommand('exec', settleOpts);
79
+ await sendCommand('exec', combinedOpts);
88
80
  } catch (retryErr) {
89
81
  if (!isRetryableSettleError(retryErr)) throw retryErr;
90
- // Retry also failed — give up silently. Settle is best-effort
91
- // after successful navigation; the next real command will surface
92
- // any persistent target error immediately.
93
82
  }
94
83
  }
84
+ } else {
85
+ // Even with waitUntil='none', still inject stealth (best-effort)
86
+ try {
87
+ await sendCommand('exec', {
88
+ code: generateStealthJs(),
89
+ ...this._cmdOpts(),
90
+ });
91
+ } catch {
92
+ // Non-fatal: stealth is best-effort
93
+ }
95
94
  }
96
95
  }
97
96
 
@@ -121,6 +120,9 @@ export class Page extends BasePage {
121
120
  await sendCommand('close-window', { ...this._wsOpt() });
122
121
  } catch {
123
122
  // Window may already be closed or daemon may be down
123
+ } finally {
124
+ this._tabId = undefined;
125
+ this._lastUrl = null;
124
126
  }
125
127
  }
126
128
 
@@ -129,18 +131,6 @@ export class Page extends BasePage {
129
131
  return Array.isArray(result) ? result : [];
130
132
  }
131
133
 
132
- async closeTab(index?: number): Promise<void> {
133
- await sendCommand('tabs', { op: 'close', ...this._wsOpt(), ...(index !== undefined ? { index } : {}) });
134
- // Invalidate cached tabId — the closed tab might have been our active one.
135
- // We can't know for sure (close-by-index doesn't return tabId), so reset.
136
- this._tabId = undefined;
137
- }
138
-
139
- async newTab(): Promise<void> {
140
- const result = await sendCommand('tabs', { op: 'new', ...this._wsOpt() }) as { tabId?: number };
141
- if (result?.tabId) this._tabId = result.tabId;
142
- }
143
-
144
134
  async selectTab(index: number): Promise<void> {
145
135
  const result = await sendCommand('tabs', { op: 'select', index, ...this._wsOpt() }) as { selected?: number };
146
136
  if (result?.selected) this._tabId = result.selected;
@@ -164,6 +154,19 @@ export class Page extends BasePage {
164
154
  return base64;
165
155
  }
166
156
 
157
+ async startNetworkCapture(pattern: string = ''): Promise<void> {
158
+ await sendCommand('network-capture-start', {
159
+ pattern,
160
+ ...this._cmdOpts(),
161
+ });
162
+ }
163
+
164
+ async readNetworkCapture(): Promise<unknown[]> {
165
+ const result = await sendCommand('network-capture-read', {
166
+ ...this._cmdOpts(),
167
+ });
168
+ return Array.isArray(result) ? result : [];
169
+ }
167
170
  /**
168
171
  * Set local file paths on a file input element via CDP DOM.setFileInputFiles.
169
172
  * Chrome reads the files directly from the local filesystem, avoiding the
@@ -180,6 +183,16 @@ export class Page extends BasePage {
180
183
  }
181
184
  }
182
185
 
186
+ async insertText(text: string): Promise<void> {
187
+ const result = await sendCommand('insert-text', {
188
+ text,
189
+ ...this._cmdOpts(),
190
+ }) as { inserted?: boolean };
191
+ if (!result?.inserted) {
192
+ throw new Error('insertText returned no inserted flag — command may not be supported by the extension');
193
+ }
194
+ }
195
+
183
196
  async cdp(method: string, params: Record<string, unknown> = {}): Promise<unknown> {
184
197
  return sendCommand('cdp', {
185
198
  cdpMethod: method,
@@ -188,6 +201,78 @@ export class Page extends BasePage {
188
201
  });
189
202
  }
190
203
 
204
+ /** CDP native click fallback — called when JS el.click() fails */
205
+ protected override async tryNativeClick(x: number, y: number): Promise<boolean> {
206
+ try {
207
+ await this.nativeClick(x, y);
208
+ return true;
209
+ } catch {
210
+ return false;
211
+ }
212
+ }
213
+
214
+ /** Precise click using DOM.getContentQuads/getBoxModel for inline elements */
215
+ async clickWithQuads(ref: string): Promise<void> {
216
+ const safeRef = JSON.stringify(ref);
217
+ const cssSelector = `[data-opencli-ref="${ref.replace(/"/g, '\\"')}"]`;
218
+
219
+ // Scroll element into view first
220
+ await this.evaluate(`
221
+ (() => {
222
+ const el = document.querySelector('[data-opencli-ref="' + ${safeRef} + '"]');
223
+ if (el) el.scrollIntoView({ behavior: 'instant', block: 'center' });
224
+ return !!el;
225
+ })()
226
+ `);
227
+
228
+ try {
229
+ // Find DOM node via CDP
230
+ const doc = await this.cdp('DOM.getDocument', {}) as { root: { nodeId: number } };
231
+ const result = await this.cdp('DOM.querySelectorAll', {
232
+ nodeId: doc.root.nodeId,
233
+ selector: cssSelector,
234
+ }) as { nodeIds: number[] };
235
+
236
+ if (!result.nodeIds?.length) throw new Error('DOM node not found');
237
+
238
+ const nodeId = result.nodeIds[0];
239
+
240
+ // Try getContentQuads first (precise for inline elements)
241
+ try {
242
+ const quads = await this.cdp('DOM.getContentQuads', { nodeId }) as { quads: number[][] };
243
+ if (quads.quads?.length) {
244
+ const q = quads.quads[0];
245
+ const cx = (q[0] + q[2] + q[4] + q[6]) / 4;
246
+ const cy = (q[1] + q[3] + q[5] + q[7]) / 4;
247
+ await this.nativeClick(Math.round(cx), Math.round(cy));
248
+ return;
249
+ }
250
+ } catch { /* fallthrough */ }
251
+
252
+ // Try getBoxModel
253
+ try {
254
+ const box = await this.cdp('DOM.getBoxModel', { nodeId }) as { model: { content: number[] } };
255
+ if (box.model?.content) {
256
+ const c = box.model.content;
257
+ const cx = (c[0] + c[2] + c[4] + c[6]) / 4;
258
+ const cy = (c[1] + c[3] + c[5] + c[7]) / 4;
259
+ await this.nativeClick(Math.round(cx), Math.round(cy));
260
+ return;
261
+ }
262
+ } catch { /* fallthrough */ }
263
+ } catch { /* fallthrough */ }
264
+
265
+ // Final fallback: regular click
266
+ await this.evaluate(`
267
+ (() => {
268
+ const el = document.querySelector('[data-opencli-ref="' + ${safeRef} + '"]');
269
+ if (!el) throw new Error('Element not found: ' + ${safeRef});
270
+ el.click();
271
+ return 'clicked';
272
+ })()
273
+ `);
274
+ }
275
+
191
276
  async nativeClick(x: number, y: number): Promise<void> {
192
277
  await this.cdp('Input.dispatchMouseEvent', {
193
278
  type: 'mousePressed',
@@ -228,4 +313,3 @@ export class Page extends BasePage {
228
313
  });
229
314
  }
230
315
  }
231
-
@@ -136,7 +136,7 @@ describe('BrowserBridge state', () => {
136
136
 
137
137
  it('fails fast when daemon is running but extension is disconnected', async () => {
138
138
  vi.spyOn(daemonClient, 'isExtensionConnected').mockResolvedValue(false);
139
- vi.spyOn(daemonClient, 'isDaemonRunning').mockResolvedValue(true);
139
+ vi.spyOn(daemonClient, 'fetchDaemonStatus').mockResolvedValue({ extensionConnected: false } as any);
140
140
 
141
141
  const bridge = new BrowserBridge();
142
142
 
@@ -33,6 +33,7 @@ export interface ManifestEntry {
33
33
  type?: string;
34
34
  default?: unknown;
35
35
  required?: boolean;
36
+ valueRequired?: boolean;
36
37
  positional?: boolean;
37
38
  help?: string;
38
39
  choices?: string[];
@@ -62,6 +63,7 @@ function toManifestArgs(args: CliCommand['args']): ManifestEntry['args'] {
62
63
  type: arg.type ?? 'str',
63
64
  default: arg.default,
64
65
  required: !!arg.required,
66
+ valueRequired: !!arg.valueRequired || undefined,
65
67
  positional: arg.positional || undefined,
66
68
  help: arg.help ?? '',
67
69
  choices: arg.choices,
@@ -0,0 +1,133 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import type { IPage } from './types.js';
3
+
4
+ const {
5
+ mockExploreUrl,
6
+ mockRenderExploreSummary,
7
+ mockGenerateCliFromUrl,
8
+ mockRenderGenerateSummary,
9
+ mockRecordSession,
10
+ mockRenderRecordSummary,
11
+ mockCascadeProbe,
12
+ mockRenderCascadeResult,
13
+ mockGetBrowserFactory,
14
+ mockBrowserSession,
15
+ } = vi.hoisted(() => ({
16
+ mockExploreUrl: vi.fn(),
17
+ mockRenderExploreSummary: vi.fn(),
18
+ mockGenerateCliFromUrl: vi.fn(),
19
+ mockRenderGenerateSummary: vi.fn(),
20
+ mockRecordSession: vi.fn(),
21
+ mockRenderRecordSummary: vi.fn(),
22
+ mockCascadeProbe: vi.fn(),
23
+ mockRenderCascadeResult: vi.fn(),
24
+ mockGetBrowserFactory: vi.fn(() => ({ name: 'BrowserFactory' })),
25
+ mockBrowserSession: vi.fn(),
26
+ }));
27
+
28
+ vi.mock('./explore.js', () => ({
29
+ exploreUrl: mockExploreUrl,
30
+ renderExploreSummary: mockRenderExploreSummary,
31
+ }));
32
+
33
+ vi.mock('./generate.js', () => ({
34
+ generateCliFromUrl: mockGenerateCliFromUrl,
35
+ renderGenerateSummary: mockRenderGenerateSummary,
36
+ }));
37
+
38
+ vi.mock('./record.js', () => ({
39
+ recordSession: mockRecordSession,
40
+ renderRecordSummary: mockRenderRecordSummary,
41
+ }));
42
+
43
+ vi.mock('./cascade.js', () => ({
44
+ cascadeProbe: mockCascadeProbe,
45
+ renderCascadeResult: mockRenderCascadeResult,
46
+ }));
47
+
48
+ vi.mock('./runtime.js', () => ({
49
+ getBrowserFactory: mockGetBrowserFactory,
50
+ browserSession: mockBrowserSession,
51
+ }));
52
+
53
+ import { createProgram } from './cli.js';
54
+
55
+ describe('built-in browser commands verbose wiring', () => {
56
+ const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
57
+
58
+ beforeEach(() => {
59
+ delete process.env.OPENCLI_VERBOSE;
60
+ process.exitCode = undefined;
61
+
62
+ mockExploreUrl.mockReset().mockResolvedValue({ ok: true });
63
+ mockRenderExploreSummary.mockReset().mockReturnValue('explore-summary');
64
+ mockGenerateCliFromUrl.mockReset().mockResolvedValue({ ok: true });
65
+ mockRenderGenerateSummary.mockReset().mockReturnValue('generate-summary');
66
+ mockRecordSession.mockReset().mockResolvedValue({ candidateCount: 1 });
67
+ mockRenderRecordSummary.mockReset().mockReturnValue('record-summary');
68
+ mockCascadeProbe.mockReset().mockResolvedValue({ ok: true });
69
+ mockRenderCascadeResult.mockReset().mockReturnValue('cascade-summary');
70
+ mockGetBrowserFactory.mockClear();
71
+ mockBrowserSession.mockReset().mockImplementation(async (_factory, fn) => {
72
+ const page = {
73
+ goto: vi.fn(),
74
+ wait: vi.fn(),
75
+ } as unknown as IPage;
76
+ return fn(page);
77
+ });
78
+ });
79
+
80
+ it('enables OPENCLI_VERBOSE for explore via the real CLI command', async () => {
81
+ const program = createProgram('', '');
82
+
83
+ await program.parseAsync(['node', 'opencli', 'explore', 'https://example.com', '-v']);
84
+
85
+ expect(process.env.OPENCLI_VERBOSE).toBe('1');
86
+ expect(mockExploreUrl).toHaveBeenCalledWith(
87
+ 'https://example.com',
88
+ expect.objectContaining({ workspace: 'explore:example.com' }),
89
+ );
90
+ });
91
+
92
+ it('enables OPENCLI_VERBOSE for generate via the real CLI command', async () => {
93
+ const program = createProgram('', '');
94
+
95
+ await program.parseAsync(['node', 'opencli', 'generate', 'https://example.com', '-v']);
96
+
97
+ expect(process.env.OPENCLI_VERBOSE).toBe('1');
98
+ expect(mockGenerateCliFromUrl).toHaveBeenCalledWith(
99
+ expect.objectContaining({ url: 'https://example.com', workspace: 'generate:example.com' }),
100
+ );
101
+ });
102
+
103
+ it('enables OPENCLI_VERBOSE for record via the real CLI command', async () => {
104
+ const program = createProgram('', '');
105
+
106
+ await program.parseAsync(['node', 'opencli', 'record', 'https://example.com', '-v']);
107
+
108
+ expect(process.env.OPENCLI_VERBOSE).toBe('1');
109
+ expect(mockRecordSession).toHaveBeenCalledWith(
110
+ expect.objectContaining({ url: 'https://example.com' }),
111
+ );
112
+ });
113
+
114
+ it('enables OPENCLI_VERBOSE for cascade via the real CLI command', async () => {
115
+ const program = createProgram('', '');
116
+
117
+ await program.parseAsync(['node', 'opencli', 'cascade', 'https://example.com', '-v']);
118
+
119
+ expect(process.env.OPENCLI_VERBOSE).toBe('1');
120
+ expect(mockBrowserSession).toHaveBeenCalled();
121
+ expect(mockCascadeProbe).toHaveBeenCalledWith(expect.any(Object), 'https://example.com');
122
+ });
123
+
124
+ it('leaves OPENCLI_VERBOSE unset when verbose is omitted', async () => {
125
+ const program = createProgram('', '');
126
+
127
+ await program.parseAsync(['node', 'opencli', 'explore', 'https://example.com']);
128
+
129
+ expect(process.env.OPENCLI_VERBOSE).toBeUndefined();
130
+ });
131
+
132
+ consoleLogSpy.mockClear();
133
+ });
package/src/cli.ts CHANGED
@@ -25,7 +25,11 @@ async function getOperatePage(): Promise<import('./types.js').IPage> {
25
25
  return bridge.connect({ timeout: 30, workspace: 'operate:default' });
26
26
  }
27
27
 
28
- export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
28
+ function applyVerbose(opts: { verbose?: boolean }): void {
29
+ if (opts.verbose) process.env.OPENCLI_VERBOSE = '1';
30
+ }
31
+
32
+ export function createProgram(BUILTIN_CLIS: string, USER_CLIS: string): Command {
29
33
  const program = new Command();
30
34
  // enablePositionalOptions: prevents parent from consuming flags meant for subcommands;
31
35
  // prerequisite for passThroughOptions to forward --help/--version to external binaries
@@ -145,7 +149,16 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
145
149
  .option('--wait <s>', '', '3')
146
150
  .option('--auto', 'Enable interactive fuzzing')
147
151
  .option('--click <labels>', 'Comma-separated labels to click before fuzzing')
148
- .action(async (url, opts) => {
152
+ .option('-v, --verbose', 'Debug output')
153
+ .action(async (url: string, opts: {
154
+ site?: string;
155
+ goal?: string;
156
+ wait: string;
157
+ auto?: boolean;
158
+ click?: string;
159
+ verbose?: boolean;
160
+ }) => {
161
+ applyVerbose(opts);
149
162
  const { exploreUrl, renderExploreSummary } = await import('./explore.js');
150
163
  const clickLabels = opts.click
151
164
  ? opts.click.split(',').map((s: string) => s.trim())
@@ -168,7 +181,9 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
168
181
  .description('Synthesize CLIs from explore')
169
182
  .argument('<target>')
170
183
  .option('--top <n>', '', '3')
184
+ .option('-v, --verbose', 'Debug output')
171
185
  .action(async (target, opts) => {
186
+ applyVerbose(opts);
172
187
  const { synthesizeFromExplore, renderSynthesizeSummary } = await import('./synthesize.js');
173
188
  console.log(renderSynthesizeSummary(synthesizeFromExplore(target, { top: parseInt(opts.top) })));
174
189
  });
@@ -179,7 +194,13 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
179
194
  .argument('<url>')
180
195
  .option('--goal <text>')
181
196
  .option('--site <name>')
182
- .action(async (url, opts) => {
197
+ .option('-v, --verbose', 'Debug output')
198
+ .action(async (url: string, opts: {
199
+ goal?: string;
200
+ site?: string;
201
+ verbose?: boolean;
202
+ }) => {
203
+ applyVerbose(opts);
183
204
  const { generateCliFromUrl, renderGenerateSummary } = await import('./generate.js');
184
205
  const workspace = `generate:${inferHost(url, opts.site)}`;
185
206
  const r = await generateCliFromUrl({
@@ -203,7 +224,15 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
203
224
  .option('--out <dir>', 'Output directory for candidates')
204
225
  .option('--poll <ms>', 'Poll interval in milliseconds', '2000')
205
226
  .option('--timeout <ms>', 'Auto-stop after N milliseconds (default: 60000)', '60000')
206
- .action(async (url, opts) => {
227
+ .option('-v, --verbose', 'Debug output')
228
+ .action(async (url: string, opts: {
229
+ site?: string;
230
+ out?: string;
231
+ poll: string;
232
+ timeout: string;
233
+ verbose?: boolean;
234
+ }) => {
235
+ applyVerbose(opts);
207
236
  const { recordSession, renderRecordSummary } = await import('./record.js');
208
237
  const result = await recordSession({
209
238
  BrowserFactory: getBrowserFactory(),
@@ -222,7 +251,12 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
222
251
  .description('Strategy cascade: find simplest working strategy')
223
252
  .argument('<url>')
224
253
  .option('--site <name>')
225
- .action(async (url, opts) => {
254
+ .option('-v, --verbose', 'Debug output')
255
+ .action(async (url: string, opts: {
256
+ site?: string;
257
+ verbose?: boolean;
258
+ }) => {
259
+ applyVerbose(opts);
226
260
  const { cascadeProbe, renderCascadeResult } = await import('./cascade.js');
227
261
  const workspace = `cascade:${inferHost(url, opts.site)}`;
228
262
  const result = await browserSession(getBrowserFactory(), async (page) => {
@@ -302,7 +336,7 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
302
336
 
303
337
  operate.command('state').description('Page state: URL, title, interactive elements with [N] indices')
304
338
  .action(operateAction(async (page) => {
305
- const snapshot = await page.snapshot({ viewportExpand: 800 });
339
+ const snapshot = await page.snapshot({ viewportExpand: 2000 });
306
340
  const url = await page.getCurrentUrl?.() ?? '';
307
341
  console.log(`URL: ${url}\n`);
308
342
  console.log(typeof snapshot === 'string' ? snapshot : JSON.stringify(snapshot, null, 2));
@@ -372,7 +406,23 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
372
406
  await page.click(index);
373
407
  await page.wait(0.3);
374
408
  await page.typeText(index, text);
375
- console.log(`Typed "${text}" into element [${index}]`);
409
+ // Detect autocomplete/combobox fields and wait for dropdown suggestions
410
+ const isAutocomplete = await page.evaluate(`
411
+ (() => {
412
+ const el = document.querySelector('[data-opencli-ref="${index}"]');
413
+ if (!el) return false;
414
+ const role = el.getAttribute('role');
415
+ const ac = el.getAttribute('aria-autocomplete');
416
+ const list = el.getAttribute('list');
417
+ return role === 'combobox' || ac === 'list' || ac === 'both' || !!list;
418
+ })()
419
+ `);
420
+ if (isAutocomplete) {
421
+ await page.wait(0.4);
422
+ console.log(`Typed "${text}" into autocomplete [${index}] — use state to see suggestions`);
423
+ } else {
424
+ console.log(`Typed "${text}" into element [${index}]`);
425
+ }
376
426
  }));
377
427
 
378
428
  operate.command('select').argument('<index>', 'Element index of <select>').argument('<option>', 'Option text')
@@ -589,19 +639,25 @@ cli({
589
639
  console.log(`🔍 Verifying ${name}...\n`);
590
640
  console.log(` Loading: ${filePath}`);
591
641
 
642
+ // Read adapter to check if it defines a 'limit' arg
643
+ const adapterSrc = fs.readFileSync(filePath, 'utf-8');
644
+ const hasLimitArg = /['"]limit['"]/.test(adapterSrc);
645
+ const limitFlag = hasLimitArg ? ' --limit 3' : '';
646
+ const verifyCmd = `node dist/main.js ${site} ${command}${limitFlag}`;
647
+
592
648
  try {
593
- const output = execSync(`node dist/main.js ${site} ${command} --limit 3`, {
649
+ const output = execSync(verifyCmd, {
594
650
  cwd: path.join(path.dirname(import.meta.url.replace('file://', '')), '..'),
595
651
  timeout: 30000,
596
652
  encoding: 'utf-8',
597
653
  env: process.env,
598
654
  stdio: ['pipe', 'pipe', 'pipe'],
599
655
  });
600
- console.log(` Executing: opencli ${site} ${command} --limit 3\n`);
656
+ console.log(` Executing: opencli ${site} ${command}${limitFlag}\n`);
601
657
  console.log(output);
602
658
  console.log(`\n ✓ Adapter works!`);
603
659
  } catch (err: any) {
604
- console.log(` Executing: opencli ${site} ${command} --limit 3\n`);
660
+ console.log(` Executing: opencli ${site} ${command}${limitFlag}\n`);
605
661
  if (err.stdout) console.log(err.stdout);
606
662
  if (err.stderr) console.error(err.stderr.slice(0, 500));
607
663
  console.log(`\n ✗ Adapter failed. Fix the code and try again.`);
@@ -628,7 +684,9 @@ cli({
628
684
  .description('Diagnose opencli browser bridge connectivity')
629
685
  .option('--no-live', 'Skip live browser connectivity test')
630
686
  .option('--sessions', 'Show active automation sessions', false)
687
+ .option('-v, --verbose', 'Debug output')
631
688
  .action(async (opts) => {
689
+ applyVerbose(opts);
632
690
  const { runBrowserDoctor, renderBrowserDoctorReport } = await import('./doctor.js');
633
691
  const report = await runBrowserDoctor({ live: opts.live, sessions: opts.sessions, cliVersion: PKG_VERSION });
634
692
  console.log(renderBrowserDoctorReport(report));
@@ -938,7 +996,11 @@ cli({
938
996
  process.exitCode = EXIT_CODES.USAGE_ERROR;
939
997
  });
940
998
 
941
- program.parse();
999
+ return program;
1000
+ }
1001
+
1002
+ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
1003
+ createProgram(BUILTIN_CLIS, USER_CLIS).parse();
942
1004
  }
943
1005
 
944
1006
  // ── Helpers ─────────────────────────────────────────────────────────────────
@@ -0,0 +1,69 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { __test__ } from './item.js';
3
+
4
+ describe('1688 item normalization', () => {
5
+ it('normalizes public item payload into contract fields', () => {
6
+ const result = __test__.normalizeItemPayload({
7
+ href: 'https://detail.1688.com/offer/887904326744.html',
8
+ title: '法式春季长袖开衫连衣裙女新款大码女装碎花吊带裙套装142077 - 阿里巴巴',
9
+ bodyText: `
10
+ 青岛沁澜衣品服装有限公司
11
+ 入驻13年
12
+ 主营:大码女装
13
+ 店铺回头率
14
+ 87%
15
+ 山东青岛
16
+ 3套起批
17
+ 已售1600+套
18
+ 支持定制logo
19
+ `,
20
+ offerTitle: '法式春季长袖开衫连衣裙女新款大码女装碎花吊带裙套装142077',
21
+ offerId: 887904326744,
22
+ seller: {
23
+ companyName: '青岛沁澜衣品服装有限公司',
24
+ memberId: 'b2b-1641351767',
25
+ winportUrl: 'https://yinuoweierfushi.1688.com/page/index.html?spm=a1',
26
+ },
27
+ trade: {
28
+ beginAmount: 3,
29
+ priceDisplay: '96.00-98.00',
30
+ unit: '套',
31
+ saleCount: 1655,
32
+ offerIDatacenterSellInfo: {
33
+ 面料名称: '莫代尔',
34
+ 主面料成分: '莫代尔纤维',
35
+ sellPointModel: '{"ignore":true}',
36
+ },
37
+ offerPriceModel: {
38
+ currentPrices: [
39
+ { beginAmount: 3, price: '98.00' },
40
+ { beginAmount: 50, price: '97.00' },
41
+ ],
42
+ },
43
+ },
44
+ gallery: {
45
+ mainImage: ['https://example.com/1.jpg'],
46
+ offerImgList: ['https://example.com/2.jpg'],
47
+ wlImageInfos: [{ fullPathImageURI: 'https://example.com/3.jpg' }],
48
+ },
49
+ services: [
50
+ { serviceName: '延期必赔', agreeDeliveryHours: 360 },
51
+ { serviceName: '品质保障' },
52
+ ],
53
+ });
54
+
55
+ expect(result.offer_id).toBe('887904326744');
56
+ expect(result.member_id).toBe('b2b-1641351767');
57
+ expect(result.shop_id).toBe('yinuoweierfushi');
58
+ expect(result.seller_url).toBe('https://yinuoweierfushi.1688.com');
59
+ expect(result.price_text).toBe('¥96.00-98.00');
60
+ expect(result.moq_text).toBe('3套起批');
61
+ expect(result.origin_place).toBe('山东青岛');
62
+ expect(result.delivery_days_text).toBe('360小时内发货');
63
+ expect(result.private_label_text).toBe('支持定制logo');
64
+ expect(result.visible_attributes).toEqual([
65
+ { key: '面料名称', value: '莫代尔' },
66
+ { key: '主面料成分', value: '莫代尔纤维' },
67
+ ]);
68
+ });
69
+ });