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