@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,126 @@
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
+ export const DANJUAN_DOMAIN = 'danjuanfunds.com';
9
+ export const DANJUAN_ASSET_PAGE = `https://${DANJUAN_DOMAIN}/my-money`;
10
+ const GAIN_URL = `https://${DANJUAN_DOMAIN}/djapi/fundx/profit/assets/gain?gains=%5B%22private%22%5D`;
11
+ const SUMMARY_URL = `https://${DANJUAN_DOMAIN}/djapi/fundx/profit/assets/summary?invest_account_id=`;
12
+ // ---------------------------------------------------------------------------
13
+ // Single-evaluate fetcher
14
+ // ---------------------------------------------------------------------------
15
+ /**
16
+ * Fetch the complete Danjuan fund picture in ONE browser round-trip.
17
+ *
18
+ * Inside the browser context we:
19
+ * 1. Fetch the gain/assets overview (contains account list)
20
+ * 2. Promise.all → fetch every account's holdings in parallel
21
+ * 3. Return the combined result to Node
22
+ */
23
+ export async function fetchDanjuanAll(page) {
24
+ const raw = await page.evaluate(`
25
+ (async () => {
26
+ const f = async (u) => {
27
+ const r = await fetch(u, { credentials: 'include' });
28
+ if (!r.ok) return { _err: r.status };
29
+ try { return await r.json(); } catch { return { _err: 'parse' }; }
30
+ };
31
+ const n = (v) => { const x = Number(v); return Number.isFinite(x) ? x : null; };
32
+
33
+ const gain = await f(${JSON.stringify(GAIN_URL)});
34
+ if (gain._err) return { _httpError: gain._err };
35
+
36
+ const root = gain.data || {};
37
+ const fundSec = (root.items || []).find(i => i && i.summary_type === 'FUND');
38
+ const rawAccs = fundSec && Array.isArray(fundSec.invest_account_list)
39
+ ? fundSec.invest_account_list : [];
40
+
41
+ const accounts = rawAccs.map(a => ({
42
+ accountId: String(a.invest_account_id || ''),
43
+ accountName: a.invest_account_name || '',
44
+ accountType: a.invest_account_type || '',
45
+ accountCode: a.invest_account_code || '',
46
+ marketValue: n(a.market_value),
47
+ dailyGain: n(a.daily_gain),
48
+ mainFlag: !!a.main_flag,
49
+ }));
50
+
51
+ if (!accounts.length) {
52
+ return { _emptyAccounts: true };
53
+ }
54
+
55
+ const details = await Promise.all(
56
+ accounts.map(a => f(${JSON.stringify(SUMMARY_URL)} + encodeURIComponent(a.accountId)))
57
+ );
58
+
59
+ const holdings = [];
60
+ const detailErrors = [];
61
+ for (let i = 0; i < accounts.length; i++) {
62
+ const d = details[i];
63
+ if (d._err) {
64
+ detailErrors.push({
65
+ accountId: accounts[i].accountId,
66
+ accountName: accounts[i].accountName,
67
+ error: d._err,
68
+ });
69
+ continue;
70
+ }
71
+ const data = d.data || {};
72
+ const funds = Array.isArray(data.items) ? data.items : [];
73
+ const acc = accounts[i];
74
+ for (const fd of funds) {
75
+ holdings.push({
76
+ accountId: acc.accountId,
77
+ accountName: data.invest_account_name || acc.accountName,
78
+ accountType: data.invest_account_type || acc.accountType,
79
+ fdCode: fd.fd_code || '',
80
+ fdName: fd.fd_name || '',
81
+ category: fd.category_text || fd.category || '',
82
+ marketValue: n(fd.market_value),
83
+ volume: n(fd.volume),
84
+ usableRemainShare:n(fd.usable_remain_share),
85
+ dailyGain: n(fd.daily_gain),
86
+ holdGain: n(fd.hold_gain),
87
+ holdGainRate: n(fd.hold_gain_rate),
88
+ totalGain: n(fd.total_gain),
89
+ nav: n(fd.nav),
90
+ marketPercent: n(fd.market_percent),
91
+ });
92
+ }
93
+ }
94
+
95
+ return {
96
+ asOf: root.daily_gain_date || null,
97
+ totalAssetAmount: n(root.amount),
98
+ totalAssetDailyGain: n(root.daily_gain),
99
+ totalAssetHoldGain: n(root.hold_gain),
100
+ totalAssetTotalGain: n(root.total_gain),
101
+ totalFundMarketValue:n(fundSec && fundSec.amount),
102
+ accounts,
103
+ holdings,
104
+ detailErrors,
105
+ };
106
+ })()
107
+ `);
108
+ if (raw?._httpError) {
109
+ throw new Error(`HTTP ${raw._httpError} — Hint: not logged in to ${DANJUAN_DOMAIN}?`);
110
+ }
111
+ if (raw?._emptyAccounts) {
112
+ throw new Error(`No fund accounts found — Hint: not logged in to ${DANJUAN_DOMAIN}?`);
113
+ }
114
+ if (Array.isArray(raw?.detailErrors) && raw.detailErrors.length > 0) {
115
+ const failedAccounts = raw.detailErrors
116
+ .map((item) => {
117
+ const label = item.accountName && item.accountId
118
+ ? `${item.accountName} (${item.accountId})`
119
+ : item.accountName || item.accountId || 'unknown account';
120
+ return `${label}: ${item.error}`;
121
+ })
122
+ .join(', ');
123
+ throw new Error(`Failed to fetch Danjuan account details: ${failedAccounts}`);
124
+ }
125
+ return raw;
126
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,41 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { fetchDanjuanAll } from './danjuan-utils.js';
3
+ describe('fetchDanjuanAll', () => {
4
+ it('throws when no Danjuan accounts are visible', async () => {
5
+ const mockPage = {
6
+ evaluate: vi.fn().mockResolvedValue({ _emptyAccounts: true }),
7
+ };
8
+ await expect(fetchDanjuanAll(mockPage)).rejects.toThrow('No fund accounts found');
9
+ });
10
+ it('throws when any account detail request fails', async () => {
11
+ const mockPage = {
12
+ evaluate: vi.fn().mockResolvedValue({
13
+ detailErrors: [
14
+ { accountName: '默认账户', accountId: 'acc-1', error: 403 },
15
+ ],
16
+ }),
17
+ };
18
+ await expect(fetchDanjuanAll(mockPage)).rejects.toThrow('Failed to fetch Danjuan account details: 默认账户 (acc-1): 403');
19
+ });
20
+ it('returns the combined snapshot when all account details succeed', async () => {
21
+ const snapshot = {
22
+ asOf: '2026-03-25',
23
+ totalAssetAmount: 100,
24
+ totalAssetDailyGain: 1,
25
+ totalAssetHoldGain: 2,
26
+ totalAssetTotalGain: 3,
27
+ totalFundMarketValue: 80,
28
+ accounts: [{ accountId: 'acc-1', accountName: '默认账户' }],
29
+ holdings: [{ accountId: 'acc-1', fdCode: '000001', fdName: '示例基金' }],
30
+ detailErrors: [],
31
+ };
32
+ const mockPage = {
33
+ evaluate: vi.fn().mockResolvedValue(snapshot),
34
+ };
35
+ await expect(fetchDanjuanAll(mockPage)).resolves.toMatchObject({
36
+ asOf: '2026-03-25',
37
+ accounts: [{ accountId: 'acc-1', accountName: '默认账户' }],
38
+ holdings: [{ accountId: 'acc-1', fdCode: '000001', fdName: '示例基金' }],
39
+ });
40
+ });
41
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { fetchDanjuanAll } from './danjuan-utils.js';
3
+ cli({
4
+ site: 'xueqiu',
5
+ name: 'fund-holdings',
6
+ description: '获取蛋卷基金持仓明细(可用 --account 按子账户过滤)',
7
+ domain: 'danjuanfunds.com',
8
+ strategy: Strategy.COOKIE,
9
+ navigateBefore: 'https://danjuanfunds.com/my-money',
10
+ args: [
11
+ { name: 'account', type: 'str', default: '', help: '按子账户名称或 ID 过滤' },
12
+ ],
13
+ columns: ['accountName', 'fdCode', 'fdName', 'marketValue', 'volume', 'dailyGain', 'holdGain', 'holdGainRate', 'marketPercent'],
14
+ func: async (page, args) => {
15
+ const snapshot = await fetchDanjuanAll(page);
16
+ if (!snapshot.accounts.length) {
17
+ throw new Error('No fund accounts found — Hint: not logged in to danjuanfunds.com?');
18
+ }
19
+ const filter = String(args.account ?? '').trim();
20
+ const rows = filter
21
+ ? snapshot.holdings.filter(h => h.accountId === filter || h.accountName.includes(filter))
22
+ : snapshot.holdings;
23
+ if (!rows.length) {
24
+ throw new Error(filter ? `No holdings matched account filter: ${filter}` : 'No holdings found.');
25
+ }
26
+ return rows;
27
+ },
28
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import { fetchDanjuanAll } from './danjuan-utils.js';
3
+ cli({
4
+ site: 'xueqiu',
5
+ name: 'fund-snapshot',
6
+ description: '获取蛋卷基金快照(总资产、子账户、持仓,推荐 -f json 输出)',
7
+ domain: 'danjuanfunds.com',
8
+ strategy: Strategy.COOKIE,
9
+ navigateBefore: 'https://danjuanfunds.com/my-money',
10
+ args: [],
11
+ columns: ['asOf', 'totalAssetAmount', 'totalFundMarketValue', 'accountCount', 'holdingCount'],
12
+ func: async (page) => {
13
+ const s = await fetchDanjuanAll(page);
14
+ return [{
15
+ asOf: s.asOf,
16
+ totalAssetAmount: s.totalAssetAmount,
17
+ totalAssetDailyGain: s.totalAssetDailyGain,
18
+ totalFundMarketValue: s.totalFundMarketValue,
19
+ accountCount: s.accounts.length,
20
+ holdingCount: s.holdings.length,
21
+ accounts: s.accounts,
22
+ holdings: s.holdings,
23
+ }];
24
+ },
25
+ });
@@ -12,6 +12,7 @@
12
12
  import { cli, Strategy } from '../../registry.js';
13
13
  import { parseVideoId } from './utils.js';
14
14
  import { groupTranscriptSegments, formatGroupedTranscript, } from './transcript-group.js';
15
+ import { CommandExecutionError, EmptyResultError } from '../../errors.js';
15
16
  cli({
16
17
  site: 'youtube',
17
18
  name: 'transcript',
@@ -82,10 +83,10 @@ cli({
82
83
  })()
83
84
  `);
84
85
  if (!captionData || typeof captionData === 'string') {
85
- throw new Error(`Failed to get caption info: ${typeof captionData === 'string' ? captionData : 'null response'}`);
86
+ throw new CommandExecutionError(`Failed to get caption info: ${typeof captionData === 'string' ? captionData : 'null response'}`);
86
87
  }
87
88
  if (captionData.error) {
88
- throw new Error(`${captionData.error}${captionData.available ? ' (available: ' + captionData.available.join(', ') + ')' : ''}`);
89
+ throw new CommandExecutionError(`${captionData.error}${captionData.available ? ' (available: ' + captionData.available.join(', ') + ')' : ''}`);
89
90
  }
90
91
  // Warn if --lang was specified but not matched
91
92
  if (captionData.requestedLang && !captionData.langMatched && !captionData.langPrefixMatched) {
@@ -164,10 +165,10 @@ cli({
164
165
  })()
165
166
  `);
166
167
  if (!Array.isArray(segments)) {
167
- throw new Error(segments?.error || 'Failed to parse caption segments');
168
+ throw new CommandExecutionError(segments?.error || 'Failed to parse caption segments');
168
169
  }
169
170
  if (segments.length === 0) {
170
- throw new Error('No caption segments found');
171
+ throw new EmptyResultError('youtube transcript');
171
172
  }
172
173
  // Step 3: Fetch chapters (for grouped mode)
173
174
  let chapters = [];
@@ -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
  cli({
7
8
  site: 'youtube',
8
9
  name: 'video',
@@ -102,9 +103,9 @@ cli({
102
103
  })()
103
104
  `);
104
105
  if (!data || typeof data !== 'object')
105
- throw new Error('Failed to extract video metadata from page');
106
+ throw new CommandExecutionError('Failed to extract video metadata from page');
106
107
  if (data.error)
107
- throw new Error(data.error);
108
+ throw new CommandExecutionError(data.error);
108
109
  // Return as field/value pairs for table display
109
110
  return Object.entries(data).map(([field, value]) => ({
110
111
  field,
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * Shared constants used across explore, synthesize, and pipeline modules.
3
3
  */
4
+ /** Default daemon port for HTTP/WebSocket communication with browser extension */
5
+ export declare const DEFAULT_DAEMON_PORT = 19825;
4
6
  /** URL query params that are volatile/ephemeral and should be stripped from patterns */
5
7
  export declare const VOLATILE_PARAMS: Set<string>;
6
8
  /** Search-related query parameter names */
package/dist/constants.js CHANGED
@@ -1,6 +1,8 @@
1
1
  /**
2
2
  * Shared constants used across explore, synthesize, and pipeline modules.
3
3
  */
4
+ /** Default daemon port for HTTP/WebSocket communication with browser extension */
5
+ export const DEFAULT_DAEMON_PORT = 19825;
4
6
  /** URL query params that are volatile/ephemeral and should be stripped from patterns */
5
7
  export const VOLATILE_PARAMS = new Set([
6
8
  'w_rid', 'wts', '_', 'callback', 'timestamp', 't', 'nonce', 'sign',
package/dist/daemon.js CHANGED
@@ -20,7 +20,8 @@
20
20
  */
21
21
  import { createServer } from 'node:http';
22
22
  import { WebSocketServer, WebSocket } from 'ws';
23
- const PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? '19825', 10);
23
+ import { DEFAULT_DAEMON_PORT } from './constants.js';
24
+ const PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
24
25
  const IDLE_TIMEOUT = 5 * 60 * 1000; // 5 minutes
25
26
  // ─── State ───────────────────────────────────────────────────────────
26
27
  let extensionWs = null;
@@ -48,17 +49,21 @@ function readBody(req) {
48
49
  return new Promise((resolve, reject) => {
49
50
  const chunks = [];
50
51
  let size = 0;
52
+ let aborted = false;
51
53
  req.on('data', (c) => {
52
54
  size += c.length;
53
55
  if (size > MAX_BODY) {
56
+ aborted = true;
54
57
  req.destroy();
55
58
  reject(new Error('Body too large'));
56
59
  return;
57
60
  }
58
61
  chunks.push(c);
59
62
  });
60
- req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
61
- req.on('error', reject);
63
+ req.on('end', () => { if (!aborted)
64
+ resolve(Buffer.concat(chunks).toString('utf-8')); });
65
+ req.on('error', (err) => { if (!aborted)
66
+ reject(err); });
62
67
  });
63
68
  }
64
69
  function jsonResponse(res, status, data) {
@@ -236,7 +241,7 @@ httpServer.listen(PORT, '127.0.0.1', () => {
236
241
  httpServer.on('error', (err) => {
237
242
  if (err.code === 'EADDRINUSE') {
238
243
  console.error(`[daemon] Port ${PORT} already in use — another daemon is likely running. Exiting.`);
239
- process.exit(0);
244
+ process.exit(1);
240
245
  }
241
246
  console.error('[daemon] Server error:', err.message);
242
247
  process.exit(1);
package/dist/discovery.js CHANGED
@@ -17,16 +17,15 @@ import { getErrorMessage } from './errors.js';
17
17
  import { log } from './logger.js';
18
18
  /** Plugins directory: ~/.opencli/plugins/ */
19
19
  export const PLUGINS_DIR = path.join(os.homedir(), '.opencli', 'plugins');
20
- const CLI_MODULE_PATTERN = /\bcli\s*\(/;
20
+ /** Matches files that register commands via cli() or lifecycle hooks */
21
+ const PLUGIN_MODULE_PATTERN = /\b(?:cli|onStartup|onBeforeExecute|onAfterExecute)\s*\(/;
21
22
  function parseStrategy(rawStrategy, fallback = Strategy.COOKIE) {
22
23
  if (!rawStrategy)
23
24
  return fallback;
24
25
  const key = rawStrategy.toUpperCase();
25
26
  return Strategy[key] ?? fallback;
26
27
  }
27
- function isRecord(value) {
28
- return typeof value === 'object' && value !== null && !Array.isArray(value);
29
- }
28
+ import { isRecord } from './utils.js';
30
29
  /**
31
30
  * Discover and register CLI commands.
32
31
  * Uses pre-compiled manifest when available for instant startup.
@@ -37,13 +36,14 @@ export async function discoverClis(...dirs) {
37
36
  const manifestPath = path.resolve(dir, '..', 'cli-manifest.json');
38
37
  try {
39
38
  await fs.promises.access(manifestPath);
40
- await loadFromManifest(manifestPath, dir);
41
- continue; // Skip filesystem scan for this directory
39
+ const loaded = await loadFromManifest(manifestPath, dir);
40
+ if (loaded)
41
+ continue; // Skip filesystem scan only when manifest is usable
42
42
  }
43
43
  catch {
44
- // Fallback: runtime filesystem scan (development)
45
- await discoverClisFromFs(dir);
44
+ // Fall through to filesystem scan
46
45
  }
46
+ await discoverClisFromFs(dir);
47
47
  }
48
48
  }
49
49
  /**
@@ -98,9 +98,11 @@ async function loadFromManifest(manifestPath, clisDir) {
98
98
  registerCommand(cmd);
99
99
  }
100
100
  }
101
+ return true;
101
102
  }
102
103
  catch (err) {
103
104
  log.warn(`Failed to load manifest ${manifestPath}: ${getErrorMessage(err)}`);
105
+ return false;
104
106
  }
105
107
  }
106
108
  /**
@@ -113,7 +115,6 @@ async function discoverClisFromFs(dir) {
113
115
  catch {
114
116
  return;
115
117
  }
116
- const promises = [];
117
118
  const entries = await fs.promises.readdir(dir, { withFileTypes: true });
118
119
  const sitePromises = entries
119
120
  .filter(entry => entry.isDirectory())
@@ -242,7 +243,7 @@ async function discoverPluginDir(dir, site) {
242
243
  async function isCliModule(filePath) {
243
244
  try {
244
245
  const source = await fs.promises.readFile(filePath, 'utf-8');
245
- return CLI_MODULE_PATTERN.test(source);
246
+ return PLUGIN_MODULE_PATTERN.test(source);
246
247
  }
247
248
  catch (err) {
248
249
  log.warn(`Failed to inspect module ${filePath}: ${getErrorMessage(err)}`);
package/dist/doctor.js CHANGED
@@ -5,9 +5,11 @@
5
5
  * MCP path discovery, or config file scanning.
6
6
  */
7
7
  import chalk from 'chalk';
8
+ import { DEFAULT_DAEMON_PORT } from './constants.js';
8
9
  import { checkDaemonStatus } from './browser/discover.js';
9
10
  import { BrowserBridge } from './browser/index.js';
10
11
  import { listSessions } from './browser/daemon-client.js';
12
+ import { getErrorMessage } from './errors.js';
11
13
  /**
12
14
  * Test connectivity by attempting a real browser command.
13
15
  */
@@ -22,7 +24,7 @@ export async function checkConnectivity(opts) {
22
24
  return { ok: true, durationMs: Date.now() - start };
23
25
  }
24
26
  catch (err) {
25
- return { ok: false, error: err?.message ?? String(err), durationMs: Date.now() - start };
27
+ return { ok: false, error: getErrorMessage(err), durationMs: Date.now() - start };
26
28
  }
27
29
  }
28
30
  export async function runBrowserDoctor(opts = {}) {
@@ -75,7 +77,7 @@ export function renderBrowserDoctorReport(report) {
75
77
  const lines = [chalk.bold(`opencli v${report.cliVersion ?? 'unknown'} doctor`), ''];
76
78
  // Daemon status
77
79
  const daemonIcon = report.daemonRunning ? chalk.green('[OK]') : chalk.red('[MISSING]');
78
- lines.push(`${daemonIcon} Daemon: ${report.daemonRunning ? 'running on port 19825' : 'not running'}`);
80
+ lines.push(`${daemonIcon} Daemon: ${report.daemonRunning ? `running on port ${DEFAULT_DAEMON_PORT}` : 'not running'}`);
79
81
  // Extension status
80
82
  const extIcon = report.extensionConnected ? chalk.green('[OK]') : chalk.yellow('[MISSING]');
81
83
  lines.push(`${extIcon} Extension: ${report.extensionConnected ? 'connected' : 'not connected'}`);
@@ -1,11 +1,14 @@
1
1
  /**
2
2
  * Download utilities: HTTP downloads, yt-dlp wrapper, format conversion.
3
3
  */
4
+ import type { BrowserCookie } from '../types.js';
5
+ export type { BrowserCookie } from '../types.js';
4
6
  export interface DownloadOptions {
5
7
  cookies?: string;
6
8
  headers?: Record<string, string>;
7
9
  timeout?: number;
8
10
  onProgress?: (received: number, total: number) => void;
11
+ maxRedirects?: number;
9
12
  }
10
13
  export interface YtdlpOptions {
11
14
  cookies?: string;
@@ -14,19 +17,8 @@ export interface YtdlpOptions {
14
17
  extraArgs?: string[];
15
18
  onProgress?: (percent: number) => void;
16
19
  }
17
- export interface BrowserCookie {
18
- name: string;
19
- value: string;
20
- domain: string;
21
- path?: string;
22
- secure?: boolean;
23
- httpOnly?: boolean;
24
- expirationDate?: number;
25
- }
26
20
  /** Check if yt-dlp is available in PATH. */
27
21
  export declare function checkYtdlp(): boolean;
28
- /** Check if ffmpeg is available in PATH. */
29
- export declare function checkFfmpeg(): boolean;
30
22
  /**
31
23
  * Detect content type from URL and optional headers.
32
24
  */
@@ -38,7 +30,7 @@ export declare function requiresYtdlp(url: string): boolean;
38
30
  /**
39
31
  * HTTP download with progress callback.
40
32
  */
41
- export declare function httpDownload(url: string, destPath: string, options?: DownloadOptions): Promise<{
33
+ export declare function httpDownload(url: string, destPath: string, options?: DownloadOptions, redirectCount?: number): Promise<{
42
34
  success: boolean;
43
35
  size: number;
44
36
  error?: string;
@@ -9,14 +9,11 @@ import * as http from 'node:http';
9
9
  import * as os from 'node:os';
10
10
  import { URL } from 'node:url';
11
11
  import { isBinaryInstalled } from '../external.js';
12
+ import { getErrorMessage } from '../errors.js';
12
13
  /** Check if yt-dlp is available in PATH. */
13
14
  export function checkYtdlp() {
14
15
  return isBinaryInstalled('yt-dlp');
15
16
  }
16
- /** Check if ffmpeg is available in PATH. */
17
- export function checkFfmpeg() {
18
- return isBinaryInstalled('ffmpeg');
19
- }
20
17
  /** Domains that host video content and can be downloaded via yt-dlp. */
21
18
  const VIDEO_PLATFORM_DOMAINS = [
22
19
  'youtube.com', 'youtu.be', 'bilibili.com', 'twitter.com',
@@ -59,13 +56,13 @@ export function requiresYtdlp(url) {
59
56
  /**
60
57
  * HTTP download with progress callback.
61
58
  */
62
- export async function httpDownload(url, destPath, options = {}) {
63
- const { cookies, headers = {}, timeout = 30000, onProgress } = options;
59
+ export async function httpDownload(url, destPath, options = {}, redirectCount = 0) {
60
+ const { cookies, headers = {}, timeout = 30000, onProgress, maxRedirects = 10 } = options;
64
61
  return new Promise((resolve) => {
65
62
  const parsedUrl = new URL(url);
66
63
  const protocol = parsedUrl.protocol === 'https:' ? https : http;
67
64
  const requestHeaders = {
68
- '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',
65
+ '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',
69
66
  ...headers,
70
67
  };
71
68
  if (cookies) {
@@ -82,7 +79,18 @@ export async function httpDownload(url, destPath, options = {}) {
82
79
  file.close();
83
80
  if (fs.existsSync(tempPath))
84
81
  fs.unlinkSync(tempPath);
85
- httpDownload(resolveRedirectUrl(url, response.headers.location), destPath, options).then(resolve);
82
+ if (redirectCount >= maxRedirects) {
83
+ resolve({ success: false, size: 0, error: `Too many redirects (> ${maxRedirects})` });
84
+ return;
85
+ }
86
+ const redirectUrl = resolveRedirectUrl(url, response.headers.location);
87
+ const originalHost = new URL(url).hostname;
88
+ const redirectHost = new URL(redirectUrl).hostname;
89
+ // Do not forward cookies when a redirect crosses host boundaries.
90
+ const redirectOptions = originalHost === redirectHost
91
+ ? options
92
+ : { ...options, cookies: undefined, headers: stripCookieHeaders(options.headers) };
93
+ httpDownload(redirectUrl, destPath, redirectOptions, redirectCount + 1).then(resolve);
86
94
  return;
87
95
  }
88
96
  if (response.statusCode !== 200) {
@@ -125,6 +133,11 @@ export async function httpDownload(url, destPath, options = {}) {
125
133
  export function resolveRedirectUrl(currentUrl, location) {
126
134
  return new URL(location, currentUrl).toString();
127
135
  }
136
+ function stripCookieHeaders(headers) {
137
+ if (!headers)
138
+ return headers;
139
+ return Object.fromEntries(Object.entries(headers).filter(([key]) => key.toLowerCase() !== 'cookie'));
140
+ }
128
141
  /**
129
142
  * Export cookies to Netscape format for yt-dlp.
130
143
  */
@@ -141,7 +154,9 @@ export function exportCookiesToNetscape(cookies, filePath) {
141
154
  const cookiePath = cookie.path || '/';
142
155
  const secure = cookie.secure ? 'TRUE' : 'FALSE';
143
156
  const expiry = Math.floor(Date.now() / 1000) + 86400 * 365; // 1 year from now
144
- lines.push(`${domain}\t${includeSubdomains}\t${cookiePath}\t${secure}\t${expiry}\t${cookie.name}\t${cookie.value}`);
157
+ const safeName = cookie.name.replace(/[\t\n\r]/g, '');
158
+ const safeValue = cookie.value.replace(/[\t\n\r]/g, '');
159
+ lines.push(`${domain}\t${includeSubdomains}\t${cookiePath}\t${secure}\t${expiry}\t${safeName}\t${safeValue}`);
145
160
  }
146
161
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
147
162
  fs.writeFileSync(filePath, lines.join('\n'));
@@ -168,8 +183,14 @@ export async function ytdlpDownload(url, destPath, options = {}) {
168
183
  '--no-playlist',
169
184
  '--progress',
170
185
  ];
171
- if (cookiesFile && fs.existsSync(cookiesFile)) {
172
- args.push('--cookies', cookiesFile);
186
+ if (cookiesFile) {
187
+ if (fs.existsSync(cookiesFile)) {
188
+ args.push('--cookies', cookiesFile);
189
+ }
190
+ else {
191
+ console.error(`[download] Cookies file not found: ${cookiesFile}, falling back to browser cookies`);
192
+ args.push('--cookies-from-browser', 'chrome');
193
+ }
173
194
  }
174
195
  else {
175
196
  // Try to use browser cookies
@@ -251,7 +272,7 @@ export async function saveDocument(content, destPath, format = 'markdown', metad
251
272
  return { success: true, size: Buffer.byteLength(output, 'utf-8') };
252
273
  }
253
274
  catch (err) {
254
- return { success: false, size: 0, error: err.message };
275
+ return { success: false, size: 0, error: getErrorMessage(err) };
255
276
  }
256
277
  }
257
278
  /**