@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
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Shared helpers for Danjuan (蛋卷基金) adapters.
3
+ *
4
+ * Core design: a single page.evaluate call fetches the gain overview AND
5
+ * all per-account holdings in parallel (Promise.all), minimising Node↔Browser
6
+ * round-trips to exactly one.
7
+ */
8
+
9
+ import type { IPage } from '../../types.js';
10
+
11
+ export const DANJUAN_DOMAIN = 'danjuanfunds.com';
12
+ export const DANJUAN_ASSET_PAGE = `https://${DANJUAN_DOMAIN}/my-money`;
13
+
14
+ const GAIN_URL = `https://${DANJUAN_DOMAIN}/djapi/fundx/profit/assets/gain?gains=%5B%22private%22%5D`;
15
+ const SUMMARY_URL = `https://${DANJUAN_DOMAIN}/djapi/fundx/profit/assets/summary?invest_account_id=`;
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Types — keep everything explicit so TS consumers get autocomplete.
19
+ // ---------------------------------------------------------------------------
20
+
21
+ export interface DanjuanAccount {
22
+ accountId: string;
23
+ accountName: string;
24
+ accountType: string;
25
+ accountCode: string;
26
+ marketValue: number | null;
27
+ dailyGain: number | null;
28
+ mainFlag: boolean;
29
+ }
30
+
31
+ export interface DanjuanHolding {
32
+ accountId: string;
33
+ accountName: string;
34
+ accountType: string;
35
+ fdCode: string;
36
+ fdName: string;
37
+ category: string;
38
+ marketValue: number | null;
39
+ volume: number | null;
40
+ usableRemainShare: number | null;
41
+ dailyGain: number | null;
42
+ holdGain: number | null;
43
+ holdGainRate: number | null;
44
+ totalGain: number | null;
45
+ nav: number | null;
46
+ marketPercent: number | null;
47
+ }
48
+
49
+ export interface DanjuanSnapshot {
50
+ asOf: string | null;
51
+ totalAssetAmount: number | null;
52
+ totalAssetDailyGain: number | null;
53
+ totalAssetHoldGain: number | null;
54
+ totalAssetTotalGain: number | null;
55
+ totalFundMarketValue: number | null;
56
+ accounts: DanjuanAccount[];
57
+ holdings: DanjuanHolding[];
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Single-evaluate fetcher
62
+ // ---------------------------------------------------------------------------
63
+
64
+ /**
65
+ * Fetch the complete Danjuan fund picture in ONE browser round-trip.
66
+ *
67
+ * Inside the browser context we:
68
+ * 1. Fetch the gain/assets overview (contains account list)
69
+ * 2. Promise.all → fetch every account's holdings in parallel
70
+ * 3. Return the combined result to Node
71
+ */
72
+ export async function fetchDanjuanAll(page: IPage): Promise<DanjuanSnapshot> {
73
+ const raw: any = await page.evaluate(`
74
+ (async () => {
75
+ const f = async (u) => {
76
+ const r = await fetch(u, { credentials: 'include' });
77
+ if (!r.ok) return { _err: r.status };
78
+ try { return await r.json(); } catch { return { _err: 'parse' }; }
79
+ };
80
+ const n = (v) => { const x = Number(v); return Number.isFinite(x) ? x : null; };
81
+
82
+ const gain = await f(${JSON.stringify(GAIN_URL)});
83
+ if (gain._err) return { _httpError: gain._err };
84
+
85
+ const root = gain.data || {};
86
+ const fundSec = (root.items || []).find(i => i && i.summary_type === 'FUND');
87
+ const rawAccs = fundSec && Array.isArray(fundSec.invest_account_list)
88
+ ? fundSec.invest_account_list : [];
89
+
90
+ const accounts = rawAccs.map(a => ({
91
+ accountId: String(a.invest_account_id || ''),
92
+ accountName: a.invest_account_name || '',
93
+ accountType: a.invest_account_type || '',
94
+ accountCode: a.invest_account_code || '',
95
+ marketValue: n(a.market_value),
96
+ dailyGain: n(a.daily_gain),
97
+ mainFlag: !!a.main_flag,
98
+ }));
99
+
100
+ if (!accounts.length) {
101
+ return { _emptyAccounts: true };
102
+ }
103
+
104
+ const details = await Promise.all(
105
+ accounts.map(a => f(${JSON.stringify(SUMMARY_URL)} + encodeURIComponent(a.accountId)))
106
+ );
107
+
108
+ const holdings = [];
109
+ const detailErrors = [];
110
+ for (let i = 0; i < accounts.length; i++) {
111
+ const d = details[i];
112
+ if (d._err) {
113
+ detailErrors.push({
114
+ accountId: accounts[i].accountId,
115
+ accountName: accounts[i].accountName,
116
+ error: d._err,
117
+ });
118
+ continue;
119
+ }
120
+ const data = d.data || {};
121
+ const funds = Array.isArray(data.items) ? data.items : [];
122
+ const acc = accounts[i];
123
+ for (const fd of funds) {
124
+ holdings.push({
125
+ accountId: acc.accountId,
126
+ accountName: data.invest_account_name || acc.accountName,
127
+ accountType: data.invest_account_type || acc.accountType,
128
+ fdCode: fd.fd_code || '',
129
+ fdName: fd.fd_name || '',
130
+ category: fd.category_text || fd.category || '',
131
+ marketValue: n(fd.market_value),
132
+ volume: n(fd.volume),
133
+ usableRemainShare:n(fd.usable_remain_share),
134
+ dailyGain: n(fd.daily_gain),
135
+ holdGain: n(fd.hold_gain),
136
+ holdGainRate: n(fd.hold_gain_rate),
137
+ totalGain: n(fd.total_gain),
138
+ nav: n(fd.nav),
139
+ marketPercent: n(fd.market_percent),
140
+ });
141
+ }
142
+ }
143
+
144
+ return {
145
+ asOf: root.daily_gain_date || null,
146
+ totalAssetAmount: n(root.amount),
147
+ totalAssetDailyGain: n(root.daily_gain),
148
+ totalAssetHoldGain: n(root.hold_gain),
149
+ totalAssetTotalGain: n(root.total_gain),
150
+ totalFundMarketValue:n(fundSec && fundSec.amount),
151
+ accounts,
152
+ holdings,
153
+ detailErrors,
154
+ };
155
+ })()
156
+ `);
157
+
158
+ if (raw?._httpError) {
159
+ throw new Error(`HTTP ${raw._httpError} — Hint: not logged in to ${DANJUAN_DOMAIN}?`);
160
+ }
161
+ if (raw?._emptyAccounts) {
162
+ throw new Error(`No fund accounts found — Hint: not logged in to ${DANJUAN_DOMAIN}?`);
163
+ }
164
+ if (Array.isArray(raw?.detailErrors) && raw.detailErrors.length > 0) {
165
+ const failedAccounts = raw.detailErrors
166
+ .map((item: { accountName?: string; accountId?: string; error?: string | number }) => {
167
+ const label = item.accountName && item.accountId
168
+ ? `${item.accountName} (${item.accountId})`
169
+ : item.accountName || item.accountId || 'unknown account';
170
+ return `${label}: ${item.error}`;
171
+ })
172
+ .join(', ');
173
+ throw new Error(`Failed to fetch Danjuan account details: ${failedAccounts}`);
174
+ }
175
+ return raw as DanjuanSnapshot;
176
+ }
@@ -0,0 +1,32 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+ import { fetchDanjuanAll } from './danjuan-utils.js';
4
+
5
+ cli({
6
+ site: 'xueqiu',
7
+ name: 'fund-holdings',
8
+ description: '获取蛋卷基金持仓明细(可用 --account 按子账户过滤)',
9
+ domain: 'danjuanfunds.com',
10
+ strategy: Strategy.COOKIE,
11
+ navigateBefore: 'https://danjuanfunds.com/my-money',
12
+ args: [
13
+ { name: 'account', type: 'str', default: '', help: '按子账户名称或 ID 过滤' },
14
+ ],
15
+ columns: ['accountName', 'fdCode', 'fdName', 'marketValue', 'volume', 'dailyGain', 'holdGain', 'holdGainRate', 'marketPercent'],
16
+ func: async (page: IPage, args) => {
17
+ const snapshot = await fetchDanjuanAll(page);
18
+ if (!snapshot.accounts.length) {
19
+ throw new Error('No fund accounts found — Hint: not logged in to danjuanfunds.com?');
20
+ }
21
+
22
+ const filter = String(args.account ?? '').trim();
23
+ const rows = filter
24
+ ? snapshot.holdings.filter(h => h.accountId === filter || h.accountName.includes(filter))
25
+ : snapshot.holdings;
26
+
27
+ if (!rows.length) {
28
+ throw new Error(filter ? `No holdings matched account filter: ${filter}` : 'No holdings found.');
29
+ }
30
+ return rows;
31
+ },
32
+ });
@@ -0,0 +1,27 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+ import { fetchDanjuanAll } from './danjuan-utils.js';
4
+
5
+ cli({
6
+ site: 'xueqiu',
7
+ name: 'fund-snapshot',
8
+ description: '获取蛋卷基金快照(总资产、子账户、持仓,推荐 -f json 输出)',
9
+ domain: 'danjuanfunds.com',
10
+ strategy: Strategy.COOKIE,
11
+ navigateBefore: 'https://danjuanfunds.com/my-money',
12
+ args: [],
13
+ columns: ['asOf', 'totalAssetAmount', 'totalFundMarketValue', 'accountCount', 'holdingCount'],
14
+ func: async (page: IPage) => {
15
+ const s = await fetchDanjuanAll(page);
16
+ return [{
17
+ asOf: s.asOf,
18
+ totalAssetAmount: s.totalAssetAmount,
19
+ totalAssetDailyGain: s.totalAssetDailyGain,
20
+ totalFundMarketValue: s.totalFundMarketValue,
21
+ accountCount: s.accounts.length,
22
+ holdingCount: s.holdings.length,
23
+ accounts: s.accounts,
24
+ holdings: s.holdings,
25
+ }];
26
+ },
27
+ });
@@ -17,6 +17,7 @@ import {
17
17
  type RawSegment,
18
18
  type Chapter,
19
19
  } from './transcript-group.js';
20
+ import { CommandExecutionError, EmptyResultError } from '../../errors.js';
20
21
 
21
22
  cli({
22
23
  site: 'youtube',
@@ -91,10 +92,10 @@ cli({
91
92
  `);
92
93
 
93
94
  if (!captionData || typeof captionData === 'string') {
94
- throw new Error(`Failed to get caption info: ${typeof captionData === 'string' ? captionData : 'null response'}`);
95
+ throw new CommandExecutionError(`Failed to get caption info: ${typeof captionData === 'string' ? captionData : 'null response'}`);
95
96
  }
96
97
  if (captionData.error) {
97
- throw new Error(`${captionData.error}${captionData.available ? ' (available: ' + captionData.available.join(', ') + ')' : ''}`);
98
+ throw new CommandExecutionError(`${captionData.error}${captionData.available ? ' (available: ' + captionData.available.join(', ') + ')' : ''}`);
98
99
  }
99
100
 
100
101
  // Warn if --lang was specified but not matched
@@ -176,10 +177,10 @@ cli({
176
177
  `);
177
178
 
178
179
  if (!Array.isArray(segments)) {
179
- throw new Error((segments as any)?.error || 'Failed to parse caption segments');
180
+ throw new CommandExecutionError((segments as any)?.error || 'Failed to parse caption segments');
180
181
  }
181
182
  if (segments.length === 0) {
182
- throw new Error('No caption segments found');
183
+ throw new EmptyResultError('youtube transcript');
183
184
  }
184
185
 
185
186
  // Step 3: Fetch chapters (for grouped mode)
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { cli, Strategy } from '../../registry.js';
5
5
  import { parseVideoId } from './utils.js';
6
+ import { CommandExecutionError } from '../../errors.js';
6
7
 
7
8
  cli({
8
9
  site: 'youtube',
@@ -104,8 +105,8 @@ cli({
104
105
  })()
105
106
  `);
106
107
 
107
- if (!data || typeof data !== 'object') throw new Error('Failed to extract video metadata from page');
108
- if (data.error) throw new Error(data.error);
108
+ if (!data || typeof data !== 'object') throw new CommandExecutionError('Failed to extract video metadata from page');
109
+ if (data.error) throw new CommandExecutionError(data.error);
109
110
 
110
111
  // Return as field/value pairs for table display
111
112
  return Object.entries(data).map(([field, value]) => ({
package/src/constants.ts CHANGED
@@ -2,6 +2,9 @@
2
2
  * Shared constants used across explore, synthesize, and pipeline modules.
3
3
  */
4
4
 
5
+ /** Default daemon port for HTTP/WebSocket communication with browser extension */
6
+ export const DEFAULT_DAEMON_PORT = 19825;
7
+
5
8
  /** URL query params that are volatile/ephemeral and should be stripped from patterns */
6
9
  export const VOLATILE_PARAMS = new Set([
7
10
  'w_rid', 'wts', '_', 'callback', 'timestamp', 't', 'nonce', 'sign',
package/src/daemon.ts CHANGED
@@ -21,8 +21,9 @@
21
21
 
22
22
  import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
23
23
  import { WebSocketServer, WebSocket, type RawData } from 'ws';
24
+ import { DEFAULT_DAEMON_PORT } from './constants.js';
24
25
 
25
- const PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? '19825', 10);
26
+ const PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
26
27
  const IDLE_TIMEOUT = 5 * 60 * 1000; // 5 minutes
27
28
 
28
29
  // ─── State ───────────────────────────────────────────────────────────
@@ -63,13 +64,14 @@ function readBody(req: IncomingMessage): Promise<string> {
63
64
  return new Promise((resolve, reject) => {
64
65
  const chunks: Buffer[] = [];
65
66
  let size = 0;
67
+ let aborted = false;
66
68
  req.on('data', (c: Buffer) => {
67
69
  size += c.length;
68
- if (size > MAX_BODY) { req.destroy(); reject(new Error('Body too large')); return; }
70
+ if (size > MAX_BODY) { aborted = true; req.destroy(); reject(new Error('Body too large')); return; }
69
71
  chunks.push(c);
70
72
  });
71
- req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
72
- req.on('error', reject);
73
+ req.on('end', () => { if (!aborted) resolve(Buffer.concat(chunks).toString('utf-8')); });
74
+ req.on('error', (err) => { if (!aborted) reject(err); });
73
75
  });
74
76
  }
75
77
 
@@ -270,7 +272,7 @@ httpServer.listen(PORT, '127.0.0.1', () => {
270
272
  httpServer.on('error', (err: NodeJS.ErrnoException) => {
271
273
  if (err.code === 'EADDRINUSE') {
272
274
  console.error(`[daemon] Port ${PORT} already in use — another daemon is likely running. Exiting.`);
273
- process.exit(0);
275
+ process.exit(1);
274
276
  }
275
277
  console.error('[daemon] Server error:', err.message);
276
278
  process.exit(1);
package/src/discovery.ts CHANGED
@@ -20,31 +20,10 @@ import type { ManifestEntry } from './build-manifest.js';
20
20
 
21
21
  /** Plugins directory: ~/.opencli/plugins/ */
22
22
  export const PLUGINS_DIR = path.join(os.homedir(), '.opencli', 'plugins');
23
- const CLI_MODULE_PATTERN = /\bcli\s*\(/;
23
+ /** Matches files that register commands via cli() or lifecycle hooks */
24
+ const PLUGIN_MODULE_PATTERN = /\b(?:cli|onStartup|onBeforeExecute|onAfterExecute)\s*\(/;
24
25
 
25
- interface YamlArgDefinition {
26
- type?: string;
27
- default?: unknown;
28
- required?: boolean;
29
- positional?: boolean;
30
- description?: string;
31
- help?: string;
32
- choices?: string[];
33
- }
34
-
35
- interface YamlCliDefinition {
36
- site?: string;
37
- name?: string;
38
- description?: string;
39
- domain?: string;
40
- strategy?: string;
41
- browser?: boolean;
42
- args?: Record<string, YamlArgDefinition>;
43
- columns?: string[];
44
- pipeline?: Record<string, unknown>[];
45
- timeout?: number;
46
- navigateBefore?: boolean | string;
47
- }
26
+ import type { YamlCliDefinition } from './yaml-schema.js';
48
27
 
49
28
  function parseStrategy(rawStrategy: string | undefined, fallback: Strategy = Strategy.COOKIE): Strategy {
50
29
  if (!rawStrategy) return fallback;
@@ -52,9 +31,7 @@ function parseStrategy(rawStrategy: string | undefined, fallback: Strategy = Str
52
31
  return Strategy[key] ?? fallback;
53
32
  }
54
33
 
55
- function isRecord(value: unknown): value is Record<string, unknown> {
56
- return typeof value === 'object' && value !== null && !Array.isArray(value);
57
- }
34
+ import { isRecord } from './utils.js';
58
35
 
59
36
  /**
60
37
  * Discover and register CLI commands.
@@ -66,12 +43,12 @@ export async function discoverClis(...dirs: string[]): Promise<void> {
66
43
  const manifestPath = path.resolve(dir, '..', 'cli-manifest.json');
67
44
  try {
68
45
  await fs.promises.access(manifestPath);
69
- await loadFromManifest(manifestPath, dir);
70
- continue; // Skip filesystem scan for this directory
46
+ const loaded = await loadFromManifest(manifestPath, dir);
47
+ if (loaded) continue; // Skip filesystem scan only when manifest is usable
71
48
  } catch {
72
- // Fallback: runtime filesystem scan (development)
73
- await discoverClisFromFs(dir);
49
+ // Fall through to filesystem scan
74
50
  }
51
+ await discoverClisFromFs(dir);
75
52
  }
76
53
  }
77
54
 
@@ -80,7 +57,7 @@ export async function discoverClis(...dirs: string[]): Promise<void> {
80
57
  * YAML pipelines are inlined — zero YAML parsing at runtime.
81
58
  * TS modules are deferred — loaded lazily on first execution.
82
59
  */
83
- async function loadFromManifest(manifestPath: string, clisDir: string): Promise<void> {
60
+ async function loadFromManifest(manifestPath: string, clisDir: string): Promise<boolean> {
84
61
  try {
85
62
  const raw = await fs.promises.readFile(manifestPath, 'utf-8');
86
63
  const manifest = JSON.parse(raw) as ManifestEntry[];
@@ -126,8 +103,10 @@ async function loadFromManifest(manifestPath: string, clisDir: string): Promise<
126
103
  registerCommand(cmd);
127
104
  }
128
105
  }
106
+ return true;
129
107
  } catch (err) {
130
108
  log.warn(`Failed to load manifest ${manifestPath}: ${getErrorMessage(err)}`);
109
+ return false;
131
110
  }
132
111
  }
133
112
 
@@ -136,7 +115,6 @@ async function loadFromManifest(manifestPath: string, clisDir: string): Promise<
136
115
  */
137
116
  async function discoverClisFromFs(dir: string): Promise<void> {
138
117
  try { await fs.promises.access(dir); } catch { return; }
139
- const promises: Promise<unknown>[] = [];
140
118
  const entries = await fs.promises.readdir(dir, { withFileTypes: true });
141
119
 
142
120
  const sitePromises = entries
@@ -269,7 +247,7 @@ async function discoverPluginDir(dir: string, site: string): Promise<void> {
269
247
  async function isCliModule(filePath: string): Promise<boolean> {
270
248
  try {
271
249
  const source = await fs.promises.readFile(filePath, 'utf-8');
272
- return CLI_MODULE_PATTERN.test(source);
250
+ return PLUGIN_MODULE_PATTERN.test(source);
273
251
  } catch (err) {
274
252
  log.warn(`Failed to inspect module ${filePath}: ${getErrorMessage(err)}`);
275
253
  return false;
package/src/doctor.ts CHANGED
@@ -6,9 +6,11 @@
6
6
  */
7
7
 
8
8
  import chalk from 'chalk';
9
+ import { DEFAULT_DAEMON_PORT } from './constants.js';
9
10
  import { checkDaemonStatus } from './browser/discover.js';
10
11
  import { BrowserBridge } from './browser/index.js';
11
12
  import { listSessions } from './browser/daemon-client.js';
13
+ import { getErrorMessage } from './errors.js';
12
14
 
13
15
  export type DoctorOptions = {
14
16
  fix?: boolean;
@@ -45,8 +47,8 @@ export async function checkConnectivity(opts?: { timeout?: number }): Promise<Co
45
47
  await page.evaluate('1 + 1');
46
48
  await mcp.close();
47
49
  return { ok: true, durationMs: Date.now() - start };
48
- } catch (err: any) {
49
- return { ok: false, error: err?.message ?? String(err), durationMs: Date.now() - start };
50
+ } catch (err) {
51
+ return { ok: false, error: getErrorMessage(err), durationMs: Date.now() - start };
50
52
  }
51
53
  }
52
54
 
@@ -107,7 +109,7 @@ export function renderBrowserDoctorReport(report: DoctorReport): string {
107
109
 
108
110
  // Daemon status
109
111
  const daemonIcon = report.daemonRunning ? chalk.green('[OK]') : chalk.red('[MISSING]');
110
- lines.push(`${daemonIcon} Daemon: ${report.daemonRunning ? 'running on port 19825' : 'not running'}`);
112
+ lines.push(`${daemonIcon} Daemon: ${report.daemonRunning ? `running on port ${DEFAULT_DAEMON_PORT}` : 'not running'}`);
111
113
 
112
114
  // Extension status
113
115
  const extIcon = report.extensionConnected ? chalk.green('[OK]') : chalk.yellow('[MISSING]');
@@ -1,5 +1,29 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { formatCookieHeader, resolveRedirectUrl } from './index.js';
1
+ import * as fs from 'node:fs';
2
+ import * as http from 'node:http';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ import { afterEach, describe, expect, it } from 'vitest';
6
+ import { formatCookieHeader, httpDownload, resolveRedirectUrl } from './index.js';
7
+
8
+ const servers: http.Server[] = [];
9
+
10
+ afterEach(async () => {
11
+ await Promise.all(servers.map((server) => new Promise<void>((resolve, reject) => {
12
+ server.close((err) => (err ? reject(err) : resolve()));
13
+ })));
14
+ servers.length = 0;
15
+ });
16
+
17
+ async function startServer(handler: http.RequestListener, hostname = '127.0.0.1'): Promise<string> {
18
+ const server = http.createServer(handler);
19
+ servers.push(server);
20
+ await new Promise<void>((resolve) => server.listen(0, hostname, resolve));
21
+ const address = server.address();
22
+ if (!address || typeof address === 'string') {
23
+ throw new Error('Failed to start test server');
24
+ }
25
+ return `http://${hostname}:${address.port}`;
26
+ }
3
27
 
4
28
  describe('download helpers', () => {
5
29
  it('resolves relative redirects against the original URL', () => {
@@ -13,4 +37,71 @@ describe('download helpers', () => {
13
37
  { name: 'ct0', value: 'def', domain: 'example.com' },
14
38
  ])).toBe('sid=abc; ct0=def');
15
39
  });
40
+
41
+ it('fails after exceeding the redirect limit', async () => {
42
+ const baseUrl = await startServer((_req, res) => {
43
+ res.statusCode = 302;
44
+ res.setHeader('Location', '/loop');
45
+ res.end();
46
+ });
47
+
48
+ const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-download-'));
49
+ const destPath = path.join(tempDir, 'file.txt');
50
+ const result = await httpDownload(`${baseUrl}/loop`, destPath, { maxRedirects: 2 });
51
+
52
+ expect(result).toEqual({
53
+ success: false,
54
+ size: 0,
55
+ error: 'Too many redirects (> 2)',
56
+ });
57
+ expect(fs.existsSync(destPath)).toBe(false);
58
+ });
59
+
60
+ it('does not forward cookies across cross-domain redirects', async () => {
61
+ let forwardedCookie: string | undefined;
62
+ const targetUrl = await startServer((req, res) => {
63
+ forwardedCookie = req.headers.cookie;
64
+ res.statusCode = 200;
65
+ res.end('ok');
66
+ }, 'localhost');
67
+
68
+ const redirectUrl = await startServer((_req, res) => {
69
+ res.statusCode = 302;
70
+ res.setHeader('Location', targetUrl);
71
+ res.end();
72
+ });
73
+
74
+ const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-download-'));
75
+ const destPath = path.join(tempDir, 'redirect.txt');
76
+ const result = await httpDownload(`${redirectUrl}/start`, destPath, { cookies: 'sid=abc' });
77
+
78
+ expect(result).toEqual({ success: true, size: 2 });
79
+ expect(forwardedCookie).toBeUndefined();
80
+ expect(fs.readFileSync(destPath, 'utf8')).toBe('ok');
81
+ });
82
+
83
+ it('does not forward cookie headers across cross-domain redirects', async () => {
84
+ let forwardedCookie: string | undefined;
85
+ const targetUrl = await startServer((req, res) => {
86
+ forwardedCookie = req.headers.cookie;
87
+ res.statusCode = 200;
88
+ res.end('ok');
89
+ }, 'localhost');
90
+
91
+ const redirectUrl = await startServer((_req, res) => {
92
+ res.statusCode = 302;
93
+ res.setHeader('Location', targetUrl);
94
+ res.end();
95
+ });
96
+
97
+ const tempDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'opencli-download-'));
98
+ const destPath = path.join(tempDir, 'redirect-header.txt');
99
+ const result = await httpDownload(`${redirectUrl}/start`, destPath, {
100
+ headers: { Cookie: 'sid=header-cookie' },
101
+ });
102
+
103
+ expect(result).toEqual({ success: true, size: 2 });
104
+ expect(forwardedCookie).toBeUndefined();
105
+ expect(fs.readFileSync(destPath, 'utf8')).toBe('ok');
106
+ });
16
107
  });