@jackwener/opencli 1.3.2 → 1.4.0

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 (508) hide show
  1. package/.github/pull_request_template.md +3 -1
  2. package/.github/workflows/build-extension.yml +7 -1
  3. package/.github/workflows/ci.yml +29 -3
  4. package/.github/workflows/docs.yml +1 -1
  5. package/.github/workflows/e2e-headed.yml +20 -0
  6. package/.github/workflows/release.yml +1 -1
  7. package/.github/workflows/security.yml +0 -3
  8. package/CHANGELOG.md +55 -0
  9. package/CONTRIBUTING.md +6 -3
  10. package/README.md +37 -10
  11. package/README.zh-CN.md +37 -10
  12. package/SKILL.md +7 -2
  13. package/TESTING.md +1 -0
  14. package/chatwise-opencli.ps1 +82 -0
  15. package/dist/analysis.d.ts +38 -0
  16. package/dist/analysis.js +166 -0
  17. package/dist/browser/cdp.d.ts +0 -4
  18. package/dist/browser/cdp.js +59 -38
  19. package/dist/browser/cdp.test.d.ts +1 -0
  20. package/dist/browser/cdp.test.js +52 -0
  21. package/dist/browser/daemon-client.js +2 -1
  22. package/dist/browser/discover.js +2 -1
  23. package/dist/browser/dom-snapshot.d.ts +2 -2
  24. package/dist/browser/dom-snapshot.js +54 -1
  25. package/dist/browser/dom-snapshot.test.js +36 -0
  26. package/dist/browser/errors.js +2 -1
  27. package/dist/browser/index.d.ts +3 -2
  28. package/dist/browser/index.js +2 -1
  29. package/dist/browser/mcp.d.ts +0 -2
  30. package/dist/browser/mcp.js +2 -3
  31. package/dist/browser/page.d.ts +4 -3
  32. package/dist/browser/page.js +44 -35
  33. package/dist/browser/stealth.d.ts +16 -0
  34. package/dist/browser/stealth.js +155 -0
  35. package/dist/browser.test.js +47 -1
  36. package/dist/build-manifest.js +15 -9
  37. package/dist/build-manifest.test.js +12 -0
  38. package/dist/cascade.js +4 -2
  39. package/dist/cli-manifest.json +639 -258
  40. package/dist/cli.js +57 -29
  41. package/dist/clis/_shared/desktop-commands.d.ts +22 -0
  42. package/dist/clis/_shared/desktop-commands.js +108 -0
  43. package/dist/clis/antigravity/serve.js +5 -2
  44. package/dist/clis/arxiv/search.js +1 -1
  45. package/dist/clis/bilibili/dynamic.test.d.ts +1 -0
  46. package/dist/clis/bilibili/dynamic.test.js +68 -0
  47. package/dist/clis/bilibili/favorite.js +4 -2
  48. package/dist/clis/bilibili/following.js +3 -2
  49. package/dist/clis/bilibili/subtitle.js +8 -7
  50. package/dist/clis/bilibili/utils.js +2 -2
  51. package/dist/clis/boss/batchgreet.js +1 -1
  52. package/dist/clis/boss/chatlist.js +1 -1
  53. package/dist/clis/boss/chatmsg.js +1 -1
  54. package/dist/clis/boss/detail.js +1 -1
  55. package/dist/clis/boss/exchange.js +1 -1
  56. package/dist/clis/boss/greet.js +1 -1
  57. package/dist/clis/boss/invite.js +1 -1
  58. package/dist/clis/boss/joblist.js +1 -1
  59. package/dist/clis/boss/mark.js +4 -3
  60. package/dist/clis/boss/recommend.js +1 -1
  61. package/dist/clis/boss/resume.js +1 -1
  62. package/dist/clis/boss/search.js +1 -1
  63. package/dist/clis/boss/send.js +5 -4
  64. package/dist/clis/boss/stats.js +1 -1
  65. package/dist/clis/chatgpt/ask.js +4 -0
  66. package/dist/clis/chatgpt/new.js +5 -1
  67. package/dist/clis/chatgpt/read.js +5 -1
  68. package/dist/clis/chatgpt/send.js +2 -1
  69. package/dist/clis/chatgpt/status.js +5 -1
  70. package/dist/clis/chatwise/ask.js +8 -2
  71. package/dist/clis/chatwise/export.js +2 -0
  72. package/dist/clis/chatwise/history.js +2 -0
  73. package/dist/clis/chatwise/model.js +8 -3
  74. package/dist/clis/chatwise/new.js +3 -18
  75. package/dist/clis/chatwise/read.js +2 -0
  76. package/dist/clis/chatwise/screenshot.js +3 -27
  77. package/dist/clis/chatwise/send.js +8 -2
  78. package/dist/clis/chatwise/shared.d.ts +2 -0
  79. package/dist/clis/chatwise/shared.js +6 -0
  80. package/dist/clis/chatwise/status.js +3 -22
  81. package/dist/clis/codex/ask.js +6 -2
  82. package/dist/clis/codex/dump.js +2 -25
  83. package/dist/clis/codex/new.js +2 -25
  84. package/dist/clis/codex/screenshot.js +2 -27
  85. package/dist/clis/codex/send.js +6 -4
  86. package/dist/clis/codex/status.js +2 -22
  87. package/dist/clis/cursor/ask.js +2 -1
  88. package/dist/clis/cursor/composer.js +2 -1
  89. package/dist/clis/cursor/dump.js +2 -25
  90. package/dist/clis/cursor/new.js +2 -18
  91. package/dist/clis/cursor/read.js +2 -1
  92. package/dist/clis/cursor/screenshot.js +1 -30
  93. package/dist/clis/cursor/send.js +2 -1
  94. package/dist/clis/cursor/status.js +2 -21
  95. package/dist/clis/dictionary/examples.yaml +25 -0
  96. package/dist/clis/dictionary/search.yaml +27 -0
  97. package/dist/clis/dictionary/synonyms.yaml +25 -0
  98. package/dist/clis/douban/book-hot.js +1 -1
  99. package/dist/clis/douban/movie-hot.js +1 -1
  100. package/dist/clis/douban/search.js +1 -1
  101. package/dist/clis/douban/utils.d.ts +4 -1
  102. package/dist/clis/douban/utils.js +156 -1
  103. package/dist/clis/doubao/ask.js +1 -1
  104. package/dist/clis/doubao/new.js +1 -1
  105. package/dist/clis/doubao/read.js +1 -1
  106. package/dist/clis/doubao/send.js +1 -1
  107. package/dist/clis/doubao/status.js +1 -1
  108. package/dist/clis/doubao-app/ask.js +1 -1
  109. package/dist/clis/doubao-app/new.js +1 -1
  110. package/dist/clis/doubao-app/read.js +1 -1
  111. package/dist/clis/doubao-app/send.js +1 -1
  112. package/dist/clis/grok/ask.d.ts +4 -0
  113. package/dist/clis/grok/ask.js +28 -10
  114. package/dist/clis/grok/ask.test.js +18 -0
  115. package/dist/clis/jd/item.d.ts +1 -0
  116. package/dist/clis/jd/item.js +96 -0
  117. package/dist/clis/jd/item.test.d.ts +1 -0
  118. package/dist/clis/jd/item.test.js +28 -0
  119. package/dist/clis/jike/feed.js +1 -1
  120. package/dist/clis/jike/search.js +1 -1
  121. package/dist/clis/linkedin/search.js +5 -4
  122. package/dist/clis/linkedin/timeline.d.ts +21 -0
  123. package/dist/clis/linkedin/timeline.js +503 -0
  124. package/dist/clis/linkedin/timeline.test.d.ts +1 -0
  125. package/dist/clis/linkedin/timeline.test.js +81 -0
  126. package/dist/clis/medium/feed.js +1 -1
  127. package/dist/clis/medium/search.js +1 -1
  128. package/dist/clis/medium/user.js +1 -1
  129. package/dist/clis/medium/{shared.js → utils.js} +2 -1
  130. package/dist/clis/pixiv/detail.yaml +49 -0
  131. package/dist/clis/pixiv/download.d.ts +7 -0
  132. package/dist/clis/pixiv/download.js +78 -0
  133. package/dist/clis/pixiv/download.test.d.ts +1 -0
  134. package/dist/clis/pixiv/download.test.js +87 -0
  135. package/dist/clis/pixiv/illusts.d.ts +8 -0
  136. package/dist/clis/pixiv/illusts.js +65 -0
  137. package/dist/clis/pixiv/illusts.test.d.ts +1 -0
  138. package/dist/clis/pixiv/illusts.test.js +99 -0
  139. package/dist/clis/pixiv/ranking.yaml +53 -0
  140. package/dist/clis/pixiv/search.d.ts +6 -0
  141. package/dist/clis/pixiv/search.js +43 -0
  142. package/dist/clis/pixiv/search.test.d.ts +1 -0
  143. package/dist/clis/pixiv/search.test.js +83 -0
  144. package/dist/clis/pixiv/test-utils.d.ts +12 -0
  145. package/dist/clis/pixiv/test-utils.js +23 -0
  146. package/dist/clis/pixiv/user.yaml +46 -0
  147. package/dist/clis/pixiv/utils.d.ts +27 -0
  148. package/dist/clis/pixiv/utils.js +49 -0
  149. package/dist/clis/reddit/comment.js +2 -1
  150. package/dist/clis/reddit/read.js +4 -3
  151. package/dist/clis/reddit/read.test.d.ts +1 -0
  152. package/dist/clis/reddit/read.test.js +28 -0
  153. package/dist/clis/reddit/save.js +2 -1
  154. package/dist/clis/reddit/saved.js +7 -3
  155. package/dist/clis/reddit/subscribe.js +2 -1
  156. package/dist/clis/reddit/upvote.js +2 -1
  157. package/dist/clis/reddit/upvoted.js +7 -3
  158. package/dist/clis/sinablog/article.js +1 -1
  159. package/dist/clis/sinablog/hot.js +1 -1
  160. package/dist/clis/sinablog/user.js +1 -1
  161. package/dist/clis/substack/feed.js +1 -1
  162. package/dist/clis/substack/publication.js +1 -1
  163. package/dist/clis/substack/search.js +3 -2
  164. package/dist/clis/substack/{shared.js → utils.js} +3 -2
  165. package/dist/clis/tiktok/search.yaml +2 -1
  166. package/dist/clis/twitter/accept.js +2 -1
  167. package/dist/clis/twitter/article.js +4 -1
  168. package/dist/clis/twitter/block.js +2 -1
  169. package/dist/clis/twitter/bookmark.js +2 -1
  170. package/dist/clis/twitter/bookmarks.js +3 -2
  171. package/dist/clis/twitter/delete.js +2 -1
  172. package/dist/clis/twitter/follow.js +2 -1
  173. package/dist/clis/twitter/followers.js +3 -2
  174. package/dist/clis/twitter/following.js +3 -2
  175. package/dist/clis/twitter/hide-reply.js +2 -1
  176. package/dist/clis/twitter/like.js +2 -1
  177. package/dist/clis/twitter/notifications.js +2 -1
  178. package/dist/clis/twitter/post.js +2 -1
  179. package/dist/clis/twitter/profile.js +5 -2
  180. package/dist/clis/twitter/reply-dm.js +2 -1
  181. package/dist/clis/twitter/reply.js +2 -1
  182. package/dist/clis/twitter/search.js +30 -13
  183. package/dist/clis/twitter/search.test.d.ts +1 -0
  184. package/dist/clis/twitter/search.test.js +104 -0
  185. package/dist/clis/twitter/thread.js +2 -2
  186. package/dist/clis/twitter/timeline.js +3 -2
  187. package/dist/clis/twitter/trending.js +3 -2
  188. package/dist/clis/twitter/unblock.js +2 -1
  189. package/dist/clis/twitter/unbookmark.js +2 -1
  190. package/dist/clis/twitter/unfollow.js +2 -1
  191. package/dist/clis/v2ex/daily.js +3 -2
  192. package/dist/clis/v2ex/me.js +3 -2
  193. package/dist/clis/v2ex/notifications.js +4 -4
  194. package/dist/clis/web/read.d.ts +16 -0
  195. package/dist/clis/web/read.js +202 -0
  196. package/dist/clis/xueqiu/danjuan-utils.d.ts +55 -0
  197. package/dist/clis/xueqiu/danjuan-utils.js +126 -0
  198. package/dist/clis/xueqiu/danjuan-utils.test.d.ts +1 -0
  199. package/dist/clis/xueqiu/danjuan-utils.test.js +41 -0
  200. package/dist/clis/xueqiu/fund-holdings.d.ts +1 -0
  201. package/dist/clis/xueqiu/fund-holdings.js +28 -0
  202. package/dist/clis/xueqiu/fund-snapshot.d.ts +1 -0
  203. package/dist/clis/xueqiu/fund-snapshot.js +25 -0
  204. package/dist/clis/youtube/transcript.js +5 -4
  205. package/dist/clis/youtube/video.js +3 -2
  206. package/dist/constants.d.ts +2 -0
  207. package/dist/constants.js +2 -0
  208. package/dist/daemon.js +9 -4
  209. package/dist/discovery.js +11 -10
  210. package/dist/doctor.js +4 -2
  211. package/dist/download/index.d.ts +4 -12
  212. package/dist/download/index.js +33 -12
  213. package/dist/download/index.test.js +79 -2
  214. package/dist/download/media-download.js +4 -2
  215. package/dist/engine.test.js +76 -4
  216. package/dist/execution.d.ts +1 -9
  217. package/dist/execution.js +56 -46
  218. package/dist/explore.js +12 -111
  219. package/dist/external-clis.yaml +0 -8
  220. package/dist/external.js +7 -5
  221. package/dist/external.test.js +4 -0
  222. package/dist/generate.d.ts +0 -9
  223. package/dist/generate.js +4 -20
  224. package/dist/hooks.d.ts +46 -0
  225. package/dist/hooks.js +56 -0
  226. package/dist/hooks.test.d.ts +4 -0
  227. package/dist/hooks.test.js +92 -0
  228. package/dist/interceptor.js +70 -23
  229. package/dist/main.js +2 -0
  230. package/dist/output.js +12 -6
  231. package/dist/pipeline/executor.js +1 -1
  232. package/dist/pipeline/steps/browser.js +1 -3
  233. package/dist/pipeline/steps/download.js +42 -26
  234. package/dist/pipeline/steps/download.test.d.ts +1 -0
  235. package/dist/pipeline/steps/download.test.js +101 -0
  236. package/dist/pipeline/steps/fetch.js +40 -22
  237. package/dist/pipeline/steps/fetch.test.d.ts +1 -0
  238. package/dist/pipeline/steps/fetch.test.js +123 -0
  239. package/dist/pipeline/steps/transform.js +2 -6
  240. package/dist/pipeline/template.js +66 -52
  241. package/dist/pipeline/template.test.js +28 -0
  242. package/dist/pipeline/transform.test.js +18 -0
  243. package/dist/plugin.d.ts +40 -1
  244. package/dist/plugin.js +214 -17
  245. package/dist/plugin.test.d.ts +1 -1
  246. package/dist/plugin.test.js +219 -3
  247. package/dist/record.js +6 -98
  248. package/dist/registry-api.d.ts +2 -0
  249. package/dist/registry-api.js +1 -0
  250. package/dist/registry.d.ts +5 -2
  251. package/dist/registry.js +1 -2
  252. package/dist/runtime.d.ts +0 -1
  253. package/dist/runtime.js +14 -4
  254. package/dist/snapshotFormatter.d.ts +7 -14
  255. package/dist/snapshotFormatter.js +38 -78
  256. package/dist/utils.d.ts +9 -0
  257. package/dist/utils.js +29 -0
  258. package/dist/validate.js +3 -5
  259. package/dist/yaml-schema.d.ts +26 -0
  260. package/dist/yaml-schema.js +5 -0
  261. package/docs/.vitepress/config.mts +3 -0
  262. package/docs/adapters/browser/dictionary.md +27 -0
  263. package/docs/adapters/browser/douban.md +18 -8
  264. package/docs/adapters/browser/jd.md +27 -0
  265. package/docs/adapters/browser/linkedin.md +6 -0
  266. package/docs/adapters/browser/pixiv.md +92 -0
  267. package/docs/adapters/browser/web.md +30 -0
  268. package/docs/adapters/browser/wikipedia.md +0 -9
  269. package/docs/adapters/browser/xueqiu.md +27 -9
  270. package/docs/adapters/desktop/antigravity.md +0 -3
  271. package/docs/adapters/index.md +11 -9
  272. package/docs/comparison.md +125 -0
  273. package/docs/developer/contributing.md +21 -2
  274. package/docs/developer/testing.md +14 -8
  275. package/docs/developer/ts-adapter.md +18 -0
  276. package/docs/developer/yaml-adapter.md +16 -0
  277. package/docs/guide/plugins.md +10 -0
  278. package/docs/zh/guide/plugins.md +10 -0
  279. package/extension/dist/background.js +519 -444
  280. package/extension/manifest.json +1 -1
  281. package/extension/package.json +1 -1
  282. package/extension/src/background.test.ts +46 -1
  283. package/extension/src/background.ts +108 -33
  284. package/extension/src/cdp.ts +9 -9
  285. package/package.json +3 -2
  286. package/scripts/check-doc-coverage.sh +2 -0
  287. package/src/analysis.ts +170 -0
  288. package/src/browser/cdp.test.ts +66 -0
  289. package/src/browser/cdp.ts +64 -41
  290. package/src/browser/daemon-client.ts +4 -3
  291. package/src/browser/discover.ts +2 -1
  292. package/src/browser/dom-snapshot.test.ts +42 -0
  293. package/src/browser/dom-snapshot.ts +56 -3
  294. package/src/browser/errors.ts +2 -1
  295. package/src/browser/index.ts +3 -2
  296. package/src/browser/mcp.ts +2 -4
  297. package/src/browser/page.ts +43 -35
  298. package/src/browser/stealth.ts +156 -0
  299. package/src/browser.test.ts +51 -1
  300. package/src/build-manifest.test.ts +14 -0
  301. package/src/build-manifest.ts +13 -32
  302. package/src/cascade.ts +5 -3
  303. package/src/cli.ts +66 -34
  304. package/src/clis/_shared/desktop-commands.ts +121 -0
  305. package/src/clis/antigravity/serve.ts +6 -3
  306. package/src/clis/arxiv/search.ts +1 -1
  307. package/src/clis/bilibili/dynamic.test.ts +79 -0
  308. package/src/clis/bilibili/favorite.ts +5 -2
  309. package/src/clis/bilibili/following.ts +3 -2
  310. package/src/clis/bilibili/subtitle.ts +8 -7
  311. package/src/clis/bilibili/utils.ts +2 -2
  312. package/src/clis/boss/batchgreet.ts +1 -1
  313. package/src/clis/boss/chatlist.ts +1 -1
  314. package/src/clis/boss/chatmsg.ts +1 -1
  315. package/src/clis/boss/detail.ts +1 -1
  316. package/src/clis/boss/exchange.ts +1 -1
  317. package/src/clis/boss/greet.ts +1 -1
  318. package/src/clis/boss/invite.ts +1 -1
  319. package/src/clis/boss/joblist.ts +1 -1
  320. package/src/clis/boss/mark.ts +4 -3
  321. package/src/clis/boss/recommend.ts +1 -1
  322. package/src/clis/boss/resume.ts +1 -1
  323. package/src/clis/boss/search.ts +1 -1
  324. package/src/clis/boss/send.ts +5 -4
  325. package/src/clis/boss/stats.ts +1 -1
  326. package/src/clis/chatgpt/ask.ts +5 -0
  327. package/src/clis/chatgpt/new.ts +7 -2
  328. package/src/clis/chatgpt/read.ts +7 -2
  329. package/src/clis/chatgpt/send.ts +3 -2
  330. package/src/clis/chatgpt/status.ts +6 -1
  331. package/src/clis/chatwise/ask.ts +7 -2
  332. package/src/clis/chatwise/export.ts +2 -0
  333. package/src/clis/chatwise/history.ts +2 -0
  334. package/src/clis/chatwise/model.ts +7 -3
  335. package/src/clis/chatwise/new.ts +3 -20
  336. package/src/clis/chatwise/read.ts +2 -0
  337. package/src/clis/chatwise/screenshot.ts +3 -32
  338. package/src/clis/chatwise/send.ts +7 -2
  339. package/src/clis/chatwise/shared.ts +8 -0
  340. package/src/clis/chatwise/status.ts +3 -24
  341. package/src/clis/codex/ask.ts +5 -2
  342. package/src/clis/codex/dump.ts +2 -27
  343. package/src/clis/codex/new.ts +2 -28
  344. package/src/clis/codex/screenshot.ts +2 -32
  345. package/src/clis/codex/send.ts +5 -4
  346. package/src/clis/codex/status.ts +2 -24
  347. package/src/clis/cursor/ask.ts +2 -1
  348. package/src/clis/cursor/composer.ts +2 -1
  349. package/src/clis/cursor/dump.ts +2 -27
  350. package/src/clis/cursor/new.ts +2 -20
  351. package/src/clis/cursor/read.ts +2 -1
  352. package/src/clis/cursor/screenshot.ts +1 -36
  353. package/src/clis/cursor/send.ts +2 -1
  354. package/src/clis/cursor/status.ts +2 -22
  355. package/src/clis/dictionary/examples.yaml +25 -0
  356. package/src/clis/dictionary/search.yaml +27 -0
  357. package/src/clis/dictionary/synonyms.yaml +25 -0
  358. package/src/clis/douban/book-hot.ts +1 -1
  359. package/src/clis/douban/movie-hot.ts +1 -1
  360. package/src/clis/douban/search.ts +1 -1
  361. package/src/clis/douban/utils.ts +165 -1
  362. package/src/clis/doubao/ask.ts +1 -1
  363. package/src/clis/doubao/new.ts +1 -1
  364. package/src/clis/doubao/read.ts +1 -1
  365. package/src/clis/doubao/send.ts +1 -1
  366. package/src/clis/doubao/status.ts +1 -1
  367. package/src/clis/doubao-app/ask.ts +1 -1
  368. package/src/clis/doubao-app/new.ts +1 -1
  369. package/src/clis/doubao-app/read.ts +1 -1
  370. package/src/clis/doubao-app/send.ts +1 -1
  371. package/src/clis/grok/ask.test.ts +25 -0
  372. package/src/clis/grok/ask.ts +25 -12
  373. package/src/clis/jd/item.test.ts +35 -0
  374. package/src/clis/jd/item.ts +101 -0
  375. package/src/clis/jike/feed.ts +1 -1
  376. package/src/clis/jike/search.ts +1 -1
  377. package/src/clis/linkedin/search.ts +5 -4
  378. package/src/clis/linkedin/timeline.test.ts +99 -0
  379. package/src/clis/linkedin/timeline.ts +532 -0
  380. package/src/clis/medium/feed.ts +1 -1
  381. package/src/clis/medium/search.ts +1 -1
  382. package/src/clis/medium/user.ts +1 -1
  383. package/src/clis/medium/{shared.ts → utils.ts} +2 -1
  384. package/src/clis/pixiv/detail.yaml +49 -0
  385. package/src/clis/pixiv/download.test.ts +114 -0
  386. package/src/clis/pixiv/download.ts +91 -0
  387. package/src/clis/pixiv/illusts.test.ts +115 -0
  388. package/src/clis/pixiv/illusts.ts +78 -0
  389. package/src/clis/pixiv/ranking.yaml +53 -0
  390. package/src/clis/pixiv/search.test.ts +97 -0
  391. package/src/clis/pixiv/search.ts +53 -0
  392. package/src/clis/pixiv/test-utils.ts +29 -0
  393. package/src/clis/pixiv/user.yaml +46 -0
  394. package/src/clis/pixiv/utils.ts +62 -0
  395. package/src/clis/reddit/comment.ts +2 -1
  396. package/src/clis/reddit/read.test.ts +34 -0
  397. package/src/clis/reddit/read.ts +4 -3
  398. package/src/clis/reddit/save.ts +2 -1
  399. package/src/clis/reddit/saved.ts +6 -2
  400. package/src/clis/reddit/subscribe.ts +2 -1
  401. package/src/clis/reddit/upvote.ts +2 -1
  402. package/src/clis/reddit/upvoted.ts +6 -2
  403. package/src/clis/sinablog/article.ts +1 -1
  404. package/src/clis/sinablog/hot.ts +1 -1
  405. package/src/clis/sinablog/user.ts +1 -1
  406. package/src/clis/substack/feed.ts +1 -1
  407. package/src/clis/substack/publication.ts +1 -1
  408. package/src/clis/substack/search.ts +3 -2
  409. package/src/clis/substack/{shared.ts → utils.ts} +3 -2
  410. package/src/clis/tiktok/search.yaml +2 -1
  411. package/src/clis/twitter/accept.ts +2 -1
  412. package/src/clis/twitter/article.ts +3 -1
  413. package/src/clis/twitter/block.ts +2 -1
  414. package/src/clis/twitter/bookmark.ts +2 -1
  415. package/src/clis/twitter/bookmarks.ts +3 -2
  416. package/src/clis/twitter/delete.ts +2 -1
  417. package/src/clis/twitter/follow.ts +2 -1
  418. package/src/clis/twitter/followers.ts +3 -2
  419. package/src/clis/twitter/following.ts +3 -2
  420. package/src/clis/twitter/hide-reply.ts +2 -1
  421. package/src/clis/twitter/like.ts +2 -1
  422. package/src/clis/twitter/notifications.ts +2 -1
  423. package/src/clis/twitter/post.ts +2 -1
  424. package/src/clis/twitter/profile.ts +4 -2
  425. package/src/clis/twitter/reply-dm.ts +2 -1
  426. package/src/clis/twitter/reply.ts +2 -1
  427. package/src/clis/twitter/search.test.ts +113 -0
  428. package/src/clis/twitter/search.ts +38 -14
  429. package/src/clis/twitter/thread.ts +2 -2
  430. package/src/clis/twitter/timeline.ts +3 -2
  431. package/src/clis/twitter/trending.ts +3 -2
  432. package/src/clis/twitter/unblock.ts +2 -1
  433. package/src/clis/twitter/unbookmark.ts +2 -1
  434. package/src/clis/twitter/unfollow.ts +2 -1
  435. package/src/clis/v2ex/daily.ts +3 -2
  436. package/src/clis/v2ex/me.ts +3 -2
  437. package/src/clis/v2ex/notifications.ts +3 -4
  438. package/src/clis/web/read.ts +210 -0
  439. package/src/clis/xueqiu/danjuan-utils.test.ts +49 -0
  440. package/src/clis/xueqiu/danjuan-utils.ts +176 -0
  441. package/src/clis/xueqiu/fund-holdings.ts +32 -0
  442. package/src/clis/xueqiu/fund-snapshot.ts +27 -0
  443. package/src/clis/youtube/transcript.ts +5 -4
  444. package/src/clis/youtube/video.ts +3 -2
  445. package/src/constants.ts +3 -0
  446. package/src/daemon.ts +7 -5
  447. package/src/discovery.ts +12 -34
  448. package/src/doctor.ts +5 -3
  449. package/src/download/index.test.ts +93 -2
  450. package/src/download/index.ts +44 -23
  451. package/src/download/media-download.ts +5 -3
  452. package/src/engine.test.ts +84 -3
  453. package/src/execution.ts +62 -46
  454. package/src/explore.ts +21 -90
  455. package/src/external-clis.yaml +0 -8
  456. package/src/external.test.ts +9 -0
  457. package/src/external.ts +12 -10
  458. package/src/generate.ts +4 -41
  459. package/src/hooks.test.ts +126 -0
  460. package/src/hooks.ts +90 -0
  461. package/src/interceptor.ts +73 -23
  462. package/src/main.ts +2 -0
  463. package/src/output.ts +14 -6
  464. package/src/pipeline/executor.ts +1 -1
  465. package/src/pipeline/steps/browser.ts +1 -3
  466. package/src/pipeline/steps/download.test.ts +136 -0
  467. package/src/pipeline/steps/download.ts +47 -34
  468. package/src/pipeline/steps/fetch.test.ts +179 -0
  469. package/src/pipeline/steps/fetch.ts +39 -23
  470. package/src/pipeline/steps/transform.ts +2 -6
  471. package/src/pipeline/template.test.ts +28 -0
  472. package/src/pipeline/template.ts +67 -79
  473. package/src/pipeline/transform.test.ts +20 -0
  474. package/src/plugin.test.ts +251 -3
  475. package/src/plugin.ts +265 -21
  476. package/src/record.ts +12 -84
  477. package/src/registry-api.ts +2 -0
  478. package/src/registry.ts +7 -4
  479. package/src/runtime.ts +14 -4
  480. package/src/snapshotFormatter.ts +43 -121
  481. package/src/utils.ts +39 -0
  482. package/src/validate.ts +3 -6
  483. package/src/yaml-schema.ts +28 -0
  484. package/tests/e2e/browser-auth.test.ts +25 -0
  485. package/tests/e2e/plugin-management.test.ts +137 -0
  486. package/tests/e2e/public-commands.test.ts +34 -1
  487. package/vitest.config.ts +19 -1
  488. package/.github/workflows/pkg-pr-new.yml +0 -30
  489. package/dist/clis/douban/shared.d.ts +0 -4
  490. package/dist/clis/douban/shared.js +0 -155
  491. package/src/clis/douban/shared.ts +0 -165
  492. /package/dist/clis/boss/{common.d.ts → utils.d.ts} +0 -0
  493. /package/dist/clis/boss/{common.js → utils.js} +0 -0
  494. /package/dist/clis/doubao/{common.d.ts → utils.d.ts} +0 -0
  495. /package/dist/clis/doubao/{common.js → utils.js} +0 -0
  496. /package/dist/clis/doubao-app/{common.d.ts → utils.d.ts} +0 -0
  497. /package/dist/clis/doubao-app/{common.js → utils.js} +0 -0
  498. /package/dist/clis/jike/{shared.d.ts → utils.d.ts} +0 -0
  499. /package/dist/clis/jike/{shared.js → utils.js} +0 -0
  500. /package/dist/clis/medium/{shared.d.ts → utils.d.ts} +0 -0
  501. /package/dist/clis/sinablog/{shared.d.ts → utils.d.ts} +0 -0
  502. /package/dist/clis/sinablog/{shared.js → utils.js} +0 -0
  503. /package/dist/clis/substack/{shared.d.ts → utils.d.ts} +0 -0
  504. /package/src/clis/boss/{common.ts → utils.ts} +0 -0
  505. /package/src/clis/doubao/{common.ts → utils.ts} +0 -0
  506. /package/src/clis/doubao-app/{common.ts → utils.ts} +0 -0
  507. /package/src/clis/jike/{shared.ts → utils.ts} +0 -0
  508. /package/src/clis/sinablog/{shared.ts → utils.ts} +0 -0
@@ -11,12 +11,17 @@ import * as os from 'node:os';
11
11
  import { URL } from 'node:url';
12
12
  import type { ProgressBar } from './progress.js';
13
13
  import { isBinaryInstalled } from '../external.js';
14
+ import type { BrowserCookie } from '../types.js';
15
+ import { getErrorMessage } from '../errors.js';
16
+
17
+ export type { BrowserCookie } from '../types.js';
14
18
 
15
19
  export interface DownloadOptions {
16
20
  cookies?: string;
17
21
  headers?: Record<string, string>;
18
22
  timeout?: number;
19
23
  onProgress?: (received: number, total: number) => void;
24
+ maxRedirects?: number;
20
25
  }
21
26
 
22
27
  export interface YtdlpOptions {
@@ -27,26 +32,11 @@ export interface YtdlpOptions {
27
32
  onProgress?: (percent: number) => void;
28
33
  }
29
34
 
30
- export interface BrowserCookie {
31
- name: string;
32
- value: string;
33
- domain: string;
34
- path?: string;
35
- secure?: boolean;
36
- httpOnly?: boolean;
37
- expirationDate?: number;
38
- }
39
-
40
35
  /** Check if yt-dlp is available in PATH. */
41
36
  export function checkYtdlp(): boolean {
42
37
  return isBinaryInstalled('yt-dlp');
43
38
  }
44
39
 
45
- /** Check if ffmpeg is available in PATH. */
46
- export function checkFfmpeg(): boolean {
47
- return isBinaryInstalled('ffmpeg');
48
- }
49
-
50
40
  /** Domains that host video content and can be downloaded via yt-dlp. */
51
41
  const VIDEO_PLATFORM_DOMAINS = [
52
42
  'youtube.com', 'youtu.be', 'bilibili.com', 'twitter.com',
@@ -92,15 +82,16 @@ export async function httpDownload(
92
82
  url: string,
93
83
  destPath: string,
94
84
  options: DownloadOptions = {},
85
+ redirectCount = 0,
95
86
  ): Promise<{ success: boolean; size: number; error?: string }> {
96
- const { cookies, headers = {}, timeout = 30000, onProgress } = options;
87
+ const { cookies, headers = {}, timeout = 30000, onProgress, maxRedirects = 10 } = options;
97
88
 
98
89
  return new Promise((resolve) => {
99
90
  const parsedUrl = new URL(url);
100
91
  const protocol = parsedUrl.protocol === 'https:' ? https : http;
101
92
 
102
93
  const requestHeaders: Record<string, string> = {
103
- 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
94
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
104
95
  ...headers,
105
96
  };
106
97
 
@@ -120,7 +111,23 @@ export async function httpDownload(
120
111
  if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
121
112
  file.close();
122
113
  if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
123
- httpDownload(resolveRedirectUrl(url, response.headers.location), destPath, options).then(resolve);
114
+ if (redirectCount >= maxRedirects) {
115
+ resolve({ success: false, size: 0, error: `Too many redirects (> ${maxRedirects})` });
116
+ return;
117
+ }
118
+ const redirectUrl = resolveRedirectUrl(url, response.headers.location);
119
+ const originalHost = new URL(url).hostname;
120
+ const redirectHost = new URL(redirectUrl).hostname;
121
+ // Do not forward cookies when a redirect crosses host boundaries.
122
+ const redirectOptions = originalHost === redirectHost
123
+ ? options
124
+ : { ...options, cookies: undefined, headers: stripCookieHeaders(options.headers) };
125
+ httpDownload(
126
+ redirectUrl,
127
+ destPath,
128
+ redirectOptions,
129
+ redirectCount + 1,
130
+ ).then(resolve);
124
131
  return;
125
132
  }
126
133
 
@@ -168,6 +175,13 @@ export function resolveRedirectUrl(currentUrl: string, location: string): string
168
175
  return new URL(location, currentUrl).toString();
169
176
  }
170
177
 
178
+ function stripCookieHeaders(headers?: Record<string, string>): Record<string, string> | undefined {
179
+ if (!headers) return headers;
180
+ return Object.fromEntries(
181
+ Object.entries(headers).filter(([key]) => key.toLowerCase() !== 'cookie'),
182
+ );
183
+ }
184
+
171
185
  /**
172
186
  * Export cookies to Netscape format for yt-dlp.
173
187
  */
@@ -188,7 +202,9 @@ export function exportCookiesToNetscape(
188
202
  const cookiePath = cookie.path || '/';
189
203
  const secure = cookie.secure ? 'TRUE' : 'FALSE';
190
204
  const expiry = Math.floor(Date.now() / 1000) + 86400 * 365; // 1 year from now
191
- lines.push(`${domain}\t${includeSubdomains}\t${cookiePath}\t${secure}\t${expiry}\t${cookie.name}\t${cookie.value}`);
205
+ const safeName = cookie.name.replace(/[\t\n\r]/g, '');
206
+ const safeValue = cookie.value.replace(/[\t\n\r]/g, '');
207
+ lines.push(`${domain}\t${includeSubdomains}\t${cookiePath}\t${secure}\t${expiry}\t${safeName}\t${safeValue}`);
192
208
  }
193
209
 
194
210
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
@@ -226,8 +242,13 @@ export async function ytdlpDownload(
226
242
  '--progress',
227
243
  ];
228
244
 
229
- if (cookiesFile && fs.existsSync(cookiesFile)) {
230
- args.push('--cookies', cookiesFile);
245
+ if (cookiesFile) {
246
+ if (fs.existsSync(cookiesFile)) {
247
+ args.push('--cookies', cookiesFile);
248
+ } else {
249
+ console.error(`[download] Cookies file not found: ${cookiesFile}, falling back to browser cookies`);
250
+ args.push('--cookies-from-browser', 'chrome');
251
+ }
231
252
  } else {
232
253
  // Try to use browser cookies
233
254
  args.push('--cookies-from-browser', 'chrome');
@@ -319,8 +340,8 @@ export async function saveDocument(
319
340
 
320
341
  fs.writeFileSync(destPath, output, 'utf-8');
321
342
  return { success: true, size: Buffer.byteLength(output, 'utf-8') };
322
- } catch (err: any) {
323
- return { success: false, size: 0, error: err.message };
343
+ } catch (err) {
344
+ return { success: false, size: 0, error: getErrorMessage(err) };
324
345
  }
325
346
  }
326
347
 
@@ -9,6 +9,7 @@
9
9
 
10
10
  import * as fs from 'node:fs';
11
11
  import * as path from 'node:path';
12
+ import { getErrorMessage } from '../errors.js';
12
13
  import {
13
14
  httpDownload,
14
15
  ytdlpDownload,
@@ -153,15 +154,16 @@ export async function downloadMedia(
153
154
  status: result.success ? 'success' : 'failed',
154
155
  size: result.success ? formatBytes(result.size) : (result.error || 'unknown error'),
155
156
  });
156
- } catch (err: any) {
157
- if (progressBar) progressBar.fail(err.message);
157
+ } catch (err) {
158
+ const msg = getErrorMessage(err);
159
+ if (progressBar) progressBar.fail(msg);
158
160
  tracker.onFileComplete(false);
159
161
 
160
162
  results.push({
161
163
  index: i + 1,
162
164
  type: media.type,
163
165
  status: 'failed',
164
- size: err.message,
166
+ size: msg,
165
167
  });
166
168
  }
167
169
  }
@@ -2,17 +2,20 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
2
  import { discoverClis, discoverPlugins, PLUGINS_DIR } from './discovery.js';
3
3
  import { executeCommand } from './execution.js';
4
4
  import { getRegistry, cli, Strategy } from './registry.js';
5
+ import { clearAllHooks, onAfterExecute } from './hooks.js';
5
6
  import * as fs from 'node:fs';
7
+ import * as os from 'node:os';
6
8
  import * as path from 'node:path';
9
+ import { pathToFileURL } from 'node:url';
7
10
 
8
11
  describe('discoverClis', () => {
9
12
  it('handles non-existent directories gracefully', async () => {
10
13
  // Should not throw for missing directories
11
- await expect(discoverClis('/tmp/nonexistent-opencli-test-dir')).resolves.not.toThrow();
14
+ await expect(discoverClis(path.join(os.tmpdir(), 'nonexistent-opencli-test-dir'))).resolves.not.toThrow();
12
15
  });
13
16
 
14
17
  it('imports only CLI command modules during filesystem discovery', async () => {
15
- const tempRoot = await fs.promises.mkdtemp(path.join('/tmp', 'opencli-discovery-'));
18
+ const tempRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-discovery-'));
16
19
  const siteDir = path.join(tempRoot, 'temp-site');
17
20
  const helperPath = path.join(siteDir, 'helper.ts');
18
21
  const commandPath = path.join(siteDir, 'hello.ts');
@@ -24,7 +27,7 @@ globalThis.__opencli_helper_loaded__ = true;
24
27
  export const helper = true;
25
28
  `);
26
29
  await fs.promises.writeFile(commandPath, `
27
- import { cli, Strategy } from '${path.join(process.cwd(), 'src', 'registry.ts')}';
30
+ import { cli, Strategy } from '${pathToFileURL(path.join(process.cwd(), 'src', 'registry.ts')).href}';
28
31
  cli({
29
32
  site: 'temp-site',
30
33
  name: 'hello',
@@ -45,6 +48,36 @@ cli({
45
48
  await fs.promises.rm(tempRoot, { recursive: true, force: true });
46
49
  }
47
50
  });
51
+
52
+ it('falls back to filesystem discovery when the manifest is invalid', async () => {
53
+ const tempBuildRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-manifest-fallback-'));
54
+ const distDir = path.join(tempBuildRoot, 'dist');
55
+ const siteDir = path.join(distDir, 'fallback-site');
56
+ const commandPath = path.join(siteDir, 'hello.ts');
57
+ const manifestPath = path.join(tempBuildRoot, 'cli-manifest.json');
58
+
59
+ try {
60
+ await fs.promises.mkdir(siteDir, { recursive: true });
61
+ await fs.promises.writeFile(manifestPath, '{ invalid json');
62
+ await fs.promises.writeFile(commandPath, `
63
+ import { cli, Strategy } from '${pathToFileURL(path.join(process.cwd(), 'src', 'registry.ts')).href}';
64
+ cli({
65
+ site: 'fallback-site',
66
+ name: 'hello',
67
+ description: 'hello command',
68
+ strategy: Strategy.PUBLIC,
69
+ browser: false,
70
+ func: async () => [{ ok: true }],
71
+ });
72
+ `);
73
+
74
+ await discoverClis(distDir);
75
+
76
+ expect(getRegistry().get('fallback-site/hello')).toBeDefined();
77
+ } finally {
78
+ await fs.promises.rm(tempBuildRoot, { recursive: true, force: true });
79
+ }
80
+ });
48
81
  });
49
82
 
50
83
  describe('discoverPlugins', () => {
@@ -88,6 +121,11 @@ columns: [message]
88
121
  });
89
122
 
90
123
  describe('executeCommand', () => {
124
+ beforeEach(() => {
125
+ clearAllHooks();
126
+ vi.unstubAllEnvs();
127
+ });
128
+
91
129
  it('accepts kebab-case option names after Commander camelCases them', async () => {
92
130
  const cmd = cli({
93
131
  site: 'test-engine',
@@ -165,4 +203,47 @@ describe('executeCommand', () => {
165
203
  await executeCommand(cmd, {}, true);
166
204
  expect(receivedDebug).toBe(true);
167
205
  });
206
+
207
+ it('fires onAfterExecute even when command execution throws', async () => {
208
+ const seen: Array<{ error?: unknown; finishedAt?: number }> = [];
209
+ onAfterExecute((ctx) => {
210
+ seen.push({ error: ctx.error, finishedAt: ctx.finishedAt });
211
+ });
212
+
213
+ const cmd = cli({
214
+ site: 'test-engine',
215
+ name: 'failing-test',
216
+ description: 'failing command',
217
+ browser: false,
218
+ strategy: Strategy.PUBLIC,
219
+ func: async () => {
220
+ throw new Error('boom');
221
+ },
222
+ });
223
+
224
+ await expect(executeCommand(cmd, {})).rejects.toThrow('boom');
225
+ expect(seen).toHaveLength(1);
226
+ expect(seen[0].error).toBeInstanceOf(Error);
227
+ expect((seen[0].error as Error).message).toBe('boom');
228
+ expect(typeof seen[0].finishedAt).toBe('number');
229
+ });
230
+
231
+ it('fails fast for chatwise commands when OPENCLI_CDP_ENDPOINT is missing', async () => {
232
+ const cmd = cli({
233
+ site: 'chatwise',
234
+ name: 'status',
235
+ description: 'chatwise status',
236
+ browser: true,
237
+ strategy: Strategy.PUBLIC,
238
+ requiredEnv: [
239
+ {
240
+ name: 'OPENCLI_CDP_ENDPOINT',
241
+ help: 'Set OPENCLI_CDP_ENDPOINT before running chatwise commands.',
242
+ },
243
+ ],
244
+ func: async () => [{ ok: true }],
245
+ });
246
+
247
+ await expect(executeCommand(cmd, {})).rejects.toThrow('requires environment variable OPENCLI_CDP_ENDPOINT');
248
+ });
168
249
  });
package/src/execution.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  * 3. Domain pre-navigation for cookie/header strategies
8
8
  * 4. Timeout enforcement
9
9
  * 5. Lazy-loading of TS modules from manifest
10
+ * 6. Lifecycle hooks (onBeforeExecute / onAfterExecute)
10
11
  */
11
12
 
12
13
  import { type CliCommand, type InternalCliCommand, type Arg, Strategy, getRegistry, fullName } from './registry.js';
@@ -16,22 +17,17 @@ import { executePipeline } from './pipeline/index.js';
16
17
  import { AdapterLoadError, ArgumentError, CommandExecutionError, getErrorMessage } from './errors.js';
17
18
  import { shouldUseBrowserSession } from './capabilityRouting.js';
18
19
  import { getBrowserFactory, browserSession, runWithTimeout, DEFAULT_BROWSER_COMMAND_TIMEOUT } from './runtime.js';
20
+ import { emitHook, type HookContext } from './hooks.js';
19
21
 
20
- /** Set of TS module paths that have been loaded */
21
22
  const _loadedModules = new Set<string>();
22
23
  type CommandArgs = Record<string, unknown>;
23
24
 
24
-
25
- /**
26
- * Validates and coerces arguments based on the command's Arg definitions.
27
- */
28
25
  export function coerceAndValidateArgs(cmdArgs: Arg[], kwargs: CommandArgs): CommandArgs {
29
26
  const result: CommandArgs = { ...kwargs };
30
27
 
31
28
  for (const argDef of cmdArgs) {
32
29
  const val = result[argDef.name];
33
-
34
- // 1. Check required
30
+
35
31
  if (argDef.required && (val === undefined || val === null || val === '')) {
36
32
  throw new ArgumentError(
37
33
  `Argument "${argDef.name}" is required.`,
@@ -40,7 +36,6 @@ export function coerceAndValidateArgs(cmdArgs: Arg[], kwargs: CommandArgs): Comm
40
36
  }
41
37
 
42
38
  if (val !== undefined && val !== null) {
43
- // 2. Type coercion
44
39
  if (argDef.type === 'int' || argDef.type === 'number') {
45
40
  const num = Number(val);
46
41
  if (Number.isNaN(num)) {
@@ -58,7 +53,6 @@ export function coerceAndValidateArgs(cmdArgs: Arg[], kwargs: CommandArgs): Comm
58
53
  }
59
54
  }
60
55
 
61
- // 3. Choices validation
62
56
  const coercedVal = result[argDef.name];
63
57
  if (argDef.choices && argDef.choices.length > 0) {
64
58
  if (!argDef.choices.map(String).includes(String(coercedVal))) {
@@ -72,16 +66,12 @@ export function coerceAndValidateArgs(cmdArgs: Arg[], kwargs: CommandArgs): Comm
72
66
  return result;
73
67
  }
74
68
 
75
- /**
76
- * Run a command's func or pipeline against a page.
77
- */
78
69
  async function runCommand(
79
70
  cmd: CliCommand,
80
71
  page: IPage | null,
81
72
  kwargs: CommandArgs,
82
73
  debug: boolean,
83
74
  ): Promise<unknown> {
84
- // Lazy-load TS module on first execution (manifest fast-path)
85
75
  const internal = cmd as InternalCliCommand;
86
76
  if (internal._lazy && internal._modulePath) {
87
77
  const modulePath = internal._modulePath;
@@ -96,13 +86,18 @@ async function runCommand(
96
86
  );
97
87
  }
98
88
  }
99
- // After loading, the module's cli() call will have updated the registry.
89
+
100
90
  const updated = getRegistry().get(fullName(cmd));
101
- if (updated?.func) return updated.func(page!, kwargs, debug);
91
+ if (updated?.func) {
92
+ if (!page && updated.browser !== false) {
93
+ throw new CommandExecutionError(`Command ${fullName(cmd)} requires a browser session but none was provided`);
94
+ }
95
+ return updated.func(page as IPage, kwargs, debug);
96
+ }
102
97
  if (updated?.pipeline) return executePipeline(page, updated.pipeline, { args: kwargs, debug });
103
98
  }
104
99
 
105
- if (cmd.func) return cmd.func(page!, kwargs, debug);
100
+ if (cmd.func) return cmd.func(page as IPage, kwargs, debug);
106
101
  if (cmd.pipeline) return executePipeline(page, cmd.pipeline, { args: kwargs, debug });
107
102
  throw new CommandExecutionError(
108
103
  `Command ${fullName(cmd)} has no func or pipeline`,
@@ -110,30 +105,29 @@ async function runCommand(
110
105
  );
111
106
  }
112
107
 
113
- /**
114
- * Resolve the pre-navigation URL for a command, or null to skip.
115
- *
116
- * COOKIE/HEADER strategies need the browser on the target domain so
117
- * `fetch(url, { credentials: 'include' })` carries cookies.
118
- * Adapters that handle their own navigation set `navigateBefore: false`.
119
- */
120
108
  function resolvePreNav(cmd: CliCommand): string | null {
121
109
  if (cmd.navigateBefore === false) return null;
122
110
  if (typeof cmd.navigateBefore === 'string') return cmd.navigateBefore;
123
111
 
124
- // Default: pre-navigate for COOKIE/HEADER strategies with a domain
125
112
  if ((cmd.strategy === Strategy.COOKIE || cmd.strategy === Strategy.HEADER) && cmd.domain) {
126
113
  return `https://${cmd.domain}`;
127
114
  }
128
115
  return null;
129
116
  }
130
117
 
131
- /**
132
- * Execute a CLI command. Automatically manages browser sessions when needed.
133
- *
134
- * This is the unified entry point callers don't need to care about
135
- * whether the command requires a browser or not.
136
- */
118
+ function ensureRequiredEnv(cmd: CliCommand): void {
119
+ const missing = (cmd.requiredEnv ?? []).find(({ name }) => {
120
+ const value = process.env[name];
121
+ return value === undefined || value === null || value === '';
122
+ });
123
+ if (!missing) return;
124
+
125
+ throw new CommandExecutionError(
126
+ `Command ${fullName(cmd)} requires environment variable ${missing.name}.`,
127
+ missing.help ?? `Set ${missing.name} before running ${fullName(cmd)}.`,
128
+ );
129
+ }
130
+
137
131
  export async function executeCommand(
138
132
  cmd: CliCommand,
139
133
  rawKwargs: CommandArgs,
@@ -147,22 +141,44 @@ export async function executeCommand(
147
141
  throw new ArgumentError(getErrorMessage(err));
148
142
  }
149
143
 
150
- if (shouldUseBrowserSession(cmd)) {
151
- const BrowserFactory = getBrowserFactory();
152
- return browserSession(BrowserFactory, async (page) => {
153
- // Pre-navigate to target domain for cookie/header context if needed.
154
- // Each adapter controls this via `navigateBefore` (see CliCommand docs).
155
- const preNavUrl = resolvePreNav(cmd);
156
- if (preNavUrl) {
157
- try { await page.goto(preNavUrl); await page.wait(2); } catch {}
158
- }
159
- return runWithTimeout(runCommand(cmd, page, kwargs, debug), {
160
- timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT,
161
- label: fullName(cmd),
162
- });
163
- }, { workspace: `site:${cmd.site}` });
144
+ const hookCtx: HookContext = {
145
+ command: fullName(cmd),
146
+ args: kwargs,
147
+ startedAt: Date.now(),
148
+ };
149
+ await emitHook('onBeforeExecute', hookCtx);
150
+
151
+ let result: unknown;
152
+ try {
153
+ if (shouldUseBrowserSession(cmd)) {
154
+ ensureRequiredEnv(cmd);
155
+ const BrowserFactory = getBrowserFactory();
156
+ result = await browserSession(BrowserFactory, async (page) => {
157
+ const preNavUrl = resolvePreNav(cmd);
158
+ if (preNavUrl) {
159
+ try {
160
+ await page.goto(preNavUrl);
161
+ await page.wait(2);
162
+ } catch (err) {
163
+ if (debug) console.error(`[pre-nav] Failed to navigate to ${preNavUrl}: ${err instanceof Error ? err.message : err}`);
164
+ }
165
+ }
166
+ return runWithTimeout(runCommand(cmd, page, kwargs, debug), {
167
+ timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT,
168
+ label: fullName(cmd),
169
+ });
170
+ }, { workspace: `site:${cmd.site}` });
171
+ } else {
172
+ result = await runCommand(cmd, null, kwargs, debug);
173
+ }
174
+ } catch (err) {
175
+ hookCtx.error = err;
176
+ hookCtx.finishedAt = Date.now();
177
+ await emitHook('onAfterExecute', hookCtx);
178
+ throw err;
164
179
  }
165
180
 
166
- // Non-browser commands run directly
167
- return runCommand(cmd, null, kwargs, debug);
181
+ hookCtx.finishedAt = Date.now();
182
+ await emitHook('onAfterExecute', hookCtx, result);
183
+ return result;
168
184
  }
package/src/explore.ts CHANGED
@@ -10,12 +10,22 @@ import * as fs from 'node:fs';
10
10
  import * as path from 'node:path';
11
11
  import { DEFAULT_BROWSER_EXPLORE_TIMEOUT, browserSession, runWithTimeout } from './runtime.js';
12
12
  import type { IBrowserFactory } from './runtime.js';
13
- import { VOLATILE_PARAMS, SEARCH_PARAMS, PAGINATION_PARAMS, LIMIT_PARAMS, FIELD_ROLES } from './constants.js';
13
+ import { LIMIT_PARAMS } from './constants.js';
14
14
  import { detectFramework } from './scripts/framework.js';
15
15
  import { discoverStores } from './scripts/store.js';
16
16
  import { interactFuzz } from './scripts/interact.js';
17
17
  import type { IPage } from './types.js';
18
18
  import { log } from './logger.js';
19
+ import {
20
+ urlToPattern,
21
+ findArrayPath,
22
+ flattenFields,
23
+ detectFieldRoles,
24
+ inferCapabilityName,
25
+ inferStrategy,
26
+ detectAuthFromHeaders,
27
+ classifyQueryParams,
28
+ } from './analysis.js';
19
29
 
20
30
  // ── Site name detection ────────────────────────────────────────────────────
21
31
 
@@ -48,10 +58,6 @@ export function slugify(value: string): string {
48
58
  return value.trim().toLowerCase().replace(/[^a-zA-Z0-9]+/g, '-').replace(/^-|-$/g, '') || 'site';
49
59
  }
50
60
 
51
- // ── Field & capability inference ───────────────────────────────────────────
52
-
53
- // (constants now imported from constants.ts)
54
-
55
61
  // ── Network analysis ───────────────────────────────────────────────────────
56
62
 
57
63
  interface NetworkEntry {
@@ -160,68 +166,16 @@ function parseNetworkRequests(raw: unknown): NetworkEntry[] {
160
166
  return [];
161
167
  }
162
168
 
163
- function urlToPattern(url: string): string {
164
- try {
165
- const p = new URL(url);
166
- const pathNorm = p.pathname.replace(/\/\d+/g, '/{id}').replace(/\/[0-9a-fA-F]{8,}/g, '/{hex}').replace(/\/BV[a-zA-Z0-9]{10}/g, '/{bvid}');
167
- const params: string[] = [];
168
- p.searchParams.forEach((_v, k) => { if (!VOLATILE_PARAMS.has(k)) params.push(k); });
169
- return `${p.host}${pathNorm}${params.length ? '?' + params.sort().map(k => `${k}={}`).join('&') : ''}`;
170
- } catch { return url; }
171
- }
172
-
173
- function detectAuthIndicators(headers?: Record<string, string>): string[] {
174
- if (!headers) return [];
175
- const indicators: string[] = [];
176
- const keys = Object.keys(headers).map(k => k.toLowerCase());
177
- if (keys.some(k => k === 'authorization')) indicators.push('bearer');
178
- if (keys.some(k => k.startsWith('x-csrf') || k.startsWith('x-xsrf'))) indicators.push('csrf');
179
- if (keys.some(k => k.startsWith('x-s') || k === 'x-t' || k === 'x-s-common')) indicators.push('signature');
180
- return indicators;
181
- }
182
-
183
169
  function analyzeResponseBody(body: unknown): AnalyzedEndpoint['responseAnalysis'] {
184
170
  if (!body || typeof body !== 'object') return null;
185
- const candidates: Array<{ path: string; items: unknown[] }> = [];
186
-
187
- function findArrays(obj: unknown, path: string, depth: number) {
188
- if (depth > 4) return;
189
- if (Array.isArray(obj) && obj.length >= 2 && obj.some(item => item && typeof item === 'object' && !Array.isArray(item))) {
190
- candidates.push({ path, items: obj });
191
- }
192
- if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
193
- for (const [key, val] of Object.entries(obj)) findArrays(val, path ? `${path}.${key}` : key, depth + 1);
194
- }
195
- }
196
- findArrays(body, '', 0);
197
- if (!candidates.length) return null;
171
+ const result = findArrayPath(body);
172
+ if (!result) return null;
198
173
 
199
- candidates.sort((a, b) => b.items.length - a.items.length);
200
- const best = candidates[0];
201
- const sample = best.items[0];
174
+ const sample = result.items[0];
202
175
  const sampleFields = sample && typeof sample === 'object' ? flattenFields(sample, '', 2) : [];
176
+ const detectedFields = detectFieldRoles(sampleFields);
203
177
 
204
- const detectedFields: Record<string, string> = {};
205
- for (const [role, aliases] of Object.entries(FIELD_ROLES)) {
206
- for (const f of sampleFields) {
207
- if (aliases.includes(f.split('.').pop()?.toLowerCase() ?? '')) { detectedFields[role] = f; break; }
208
- }
209
- }
210
-
211
- return { itemPath: best.path || null, itemCount: best.items.length, detectedFields, sampleFields };
212
- }
213
-
214
- function flattenFields(obj: unknown, prefix: string, maxDepth: number): string[] {
215
- if (maxDepth <= 0 || !obj || typeof obj !== 'object') return [];
216
- const names: string[] = [];
217
- const record = obj as Record<string, unknown>;
218
- for (const key of Object.keys(record)) {
219
- const full = prefix ? `${prefix}.${key}` : key;
220
- names.push(full);
221
- const val = record[key];
222
- if (val && typeof val === 'object' && !Array.isArray(val)) names.push(...flattenFields(val, full, maxDepth - 1));
223
- }
224
- return names;
178
+ return { itemPath: result.path || null, itemCount: result.items.length, detectedFields, sampleFields };
225
179
  }
226
180
 
227
181
  function isBooleanRecord(value: unknown): value is Record<string, boolean> {
@@ -243,28 +197,6 @@ function scoreEndpoint(ep: { contentType: string; responseAnalysis: AnalyzedEndp
243
197
  return s;
244
198
  }
245
199
 
246
- function inferCapabilityName(url: string, goal?: string): string {
247
- if (goal) return goal;
248
- const u = url.toLowerCase();
249
- if (u.includes('hot') || u.includes('popular') || u.includes('ranking') || u.includes('trending')) return 'hot';
250
- if (u.includes('search')) return 'search';
251
- if (u.includes('feed') || u.includes('timeline') || u.includes('dynamic')) return 'feed';
252
- if (u.includes('comment') || u.includes('reply')) return 'comments';
253
- if (u.includes('history')) return 'history';
254
- if (u.includes('profile') || u.includes('userinfo') || u.includes('/me')) return 'me';
255
- if (u.includes('favorite') || u.includes('collect') || u.includes('bookmark')) return 'favorite';
256
- try {
257
- const segs = new URL(url).pathname.split('/').filter(s => s && !s.match(/^\d+$/) && !s.match(/^[0-9a-f]{8,}$/i));
258
- if (segs.length) return segs[segs.length - 1].replace(/[^a-z0-9]/gi, '_').toLowerCase();
259
- } catch {}
260
- return 'data';
261
- }
262
-
263
- function inferStrategy(authIndicators: string[]): string {
264
- if (authIndicators.includes('signature')) return 'intercept';
265
- if (authIndicators.includes('bearer') || authIndicators.includes('csrf')) return 'header';
266
- return 'cookie';
267
- }
268
200
 
269
201
  // ── Framework detection ────────────────────────────────────────────────────
270
202
 
@@ -300,15 +232,14 @@ function analyzeEndpoints(networkEntries: NetworkEntry[]): { analyzed: AnalyzedE
300
232
  const key = `${entry.method}:${pattern}`;
301
233
  if (seen.has(key)) continue;
302
234
 
303
- const qp: string[] = [];
304
- try { new URL(entry.url).searchParams.forEach((_v, k) => { if (!VOLATILE_PARAMS.has(k)) qp.push(k); }); } catch {}
235
+ const { params: qp, hasSearch, hasPagination, hasLimit } = classifyQueryParams(entry.url);
305
236
 
306
237
  const ep: AnalyzedEndpoint = {
307
238
  pattern, method: entry.method, url: entry.url, status: entry.status, contentType: ct,
308
- queryParams: qp, hasSearchParam: qp.some(p => SEARCH_PARAMS.has(p)),
309
- hasPaginationParam: qp.some(p => PAGINATION_PARAMS.has(p)),
310
- hasLimitParam: qp.some(p => LIMIT_PARAMS.has(p)),
311
- authIndicators: detectAuthIndicators(entry.requestHeaders),
239
+ queryParams: qp, hasSearchParam: hasSearch,
240
+ hasPaginationParam: hasPagination,
241
+ hasLimitParam: hasLimit || qp.some(p => LIMIT_PARAMS.has(p)),
242
+ authIndicators: detectAuthFromHeaders(entry.requestHeaders),
312
243
  responseAnalysis: entry.responseBody ? analyzeResponseBody(entry.responseBody) : null,
313
244
  score: 0,
314
245
  };
@@ -22,14 +22,6 @@
22
22
  install:
23
23
  default: "npm install -g @readwiseio/readwise-cli"
24
24
 
25
- - name: kubectl
26
- binary: kubectl
27
- description: "Kubernetes command-line tool"
28
- homepage: "https://kubernetes.io/docs/reference/kubectl/"
29
- tags: [kubernetes, k8s, devops]
30
- install:
31
- mac: "brew install kubectl"
32
-
33
25
  - name: docker
34
26
  binary: docker
35
27
  description: "Docker command-line interface"
@@ -33,6 +33,15 @@ describe('parseCommand', () => {
33
33
  'Install command contains unsafe shell operators',
34
34
  );
35
35
  });
36
+
37
+ it('rejects command substitution and multiline input', () => {
38
+ expect(() => parseCommand('brew install $(whoami)')).toThrow(
39
+ 'Install command contains unsafe shell operators',
40
+ );
41
+ expect(() => parseCommand('brew install gh\nrm -rf /')).toThrow(
42
+ 'Install command contains unsafe shell operators',
43
+ );
44
+ });
36
45
  });
37
46
 
38
47
  describe('installExternalCli', () => {