@jackwener/opencli 1.1.1 → 1.2.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 (509) hide show
  1. package/CONTRIBUTING.md +39 -1
  2. package/README.md +9 -10
  3. package/README.zh-CN.md +39 -17
  4. package/SKILL.md +10 -5
  5. package/dist/browser/cdp.d.ts +4 -4
  6. package/dist/browser/cdp.js +39 -16
  7. package/dist/browser/daemon-client.d.ts +2 -1
  8. package/dist/browser/dom-helpers.js +38 -7
  9. package/dist/browser/dom-snapshot.d.ts +86 -0
  10. package/dist/browser/dom-snapshot.js +729 -0
  11. package/dist/browser/dom-snapshot.test.d.ts +11 -0
  12. package/dist/browser/dom-snapshot.test.js +212 -0
  13. package/dist/browser/index.d.ts +2 -0
  14. package/dist/browser/index.js +1 -0
  15. package/dist/browser/page.d.ts +14 -24
  16. package/dist/browser/page.js +37 -4
  17. package/dist/build-manifest.d.ts +11 -4
  18. package/dist/build-manifest.js +59 -21
  19. package/dist/build-manifest.test.js +58 -2
  20. package/dist/cli-manifest.json +3856 -1509
  21. package/dist/cli.js +66 -0
  22. package/dist/clis/barchart/greeks.js +1 -1
  23. package/dist/clis/barchart/options.js +1 -1
  24. package/dist/clis/barchart/quote.js +1 -1
  25. package/dist/clis/bilibili/download.js +1 -1
  26. package/dist/clis/bilibili/following.js +1 -1
  27. package/dist/clis/bilibili/subtitle.js +1 -1
  28. package/dist/clis/bilibili/user-videos.js +1 -1
  29. package/dist/clis/boss/batchgreet.js +10 -97
  30. package/dist/clis/boss/chatlist.js +8 -25
  31. package/dist/clis/boss/chatmsg.js +11 -42
  32. package/dist/clis/boss/common.d.ts +92 -0
  33. package/dist/clis/boss/common.js +223 -0
  34. package/dist/clis/boss/detail.js +7 -49
  35. package/dist/clis/boss/exchange.js +13 -79
  36. package/dist/clis/boss/greet.js +18 -145
  37. package/dist/clis/boss/invite.js +26 -121
  38. package/dist/clis/boss/joblist.js +6 -31
  39. package/dist/clis/boss/mark.js +12 -85
  40. package/dist/clis/boss/recommend.js +10 -49
  41. package/dist/clis/boss/resume.js +18 -118
  42. package/dist/clis/boss/search.js +12 -60
  43. package/dist/clis/boss/send.js +17 -151
  44. package/dist/clis/boss/stats.js +18 -69
  45. package/dist/clis/coupang/add-to-cart.js +1 -1
  46. package/dist/clis/devto/tag.yaml +34 -0
  47. package/dist/clis/devto/top.yaml +29 -0
  48. package/dist/clis/devto/user.yaml +33 -0
  49. package/dist/clis/douban/book-hot.d.ts +1 -0
  50. package/dist/clis/douban/book-hot.js +14 -0
  51. package/dist/clis/douban/marks.d.ts +1 -0
  52. package/dist/clis/douban/marks.js +115 -0
  53. package/dist/clis/douban/movie-hot.d.ts +1 -0
  54. package/dist/clis/douban/movie-hot.js +14 -0
  55. package/dist/clis/douban/reviews.d.ts +1 -0
  56. package/dist/clis/douban/reviews.js +106 -0
  57. package/dist/clis/douban/search.d.ts +1 -0
  58. package/dist/clis/douban/search.js +16 -0
  59. package/dist/clis/douban/shared.d.ts +4 -0
  60. package/dist/clis/douban/shared.js +155 -0
  61. package/dist/clis/douban/subject.yaml +76 -0
  62. package/dist/clis/douban/top250.yaml +70 -0
  63. package/dist/clis/douban/utils.d.ts +35 -0
  64. package/dist/clis/douban/utils.js +48 -0
  65. package/dist/clis/facebook/add-friend.yaml +43 -0
  66. package/dist/clis/facebook/events.yaml +44 -0
  67. package/dist/clis/facebook/feed.yaml +63 -0
  68. package/dist/clis/facebook/friends.yaml +42 -0
  69. package/dist/clis/facebook/groups.yaml +50 -0
  70. package/dist/clis/facebook/join-group.yaml +44 -0
  71. package/dist/clis/facebook/memories.yaml +39 -0
  72. package/dist/clis/facebook/notifications.yaml +40 -0
  73. package/dist/clis/facebook/profile.yaml +37 -0
  74. package/dist/clis/facebook/search.yaml +46 -0
  75. package/dist/clis/google/news.d.ts +5 -0
  76. package/dist/clis/google/news.js +58 -0
  77. package/dist/clis/google/search.d.ts +10 -0
  78. package/dist/clis/google/search.js +127 -0
  79. package/dist/clis/google/suggest.d.ts +5 -0
  80. package/dist/clis/google/suggest.js +34 -0
  81. package/dist/clis/google/trends.d.ts +5 -0
  82. package/dist/clis/google/trends.js +38 -0
  83. package/dist/clis/google/utils.d.ts +9 -0
  84. package/dist/clis/google/utils.js +23 -0
  85. package/dist/clis/google/utils.test.d.ts +1 -0
  86. package/dist/clis/google/utils.test.js +75 -0
  87. package/dist/clis/grok/ask.d.ts +14 -0
  88. package/dist/clis/grok/ask.js +257 -65
  89. package/dist/clis/grok/ask.test.d.ts +1 -0
  90. package/dist/clis/grok/ask.test.js +36 -0
  91. package/dist/clis/instagram/comment.yaml +52 -0
  92. package/dist/clis/instagram/explore.yaml +43 -0
  93. package/dist/clis/instagram/follow.yaml +41 -0
  94. package/dist/clis/instagram/followers.yaml +51 -0
  95. package/dist/clis/instagram/following.yaml +51 -0
  96. package/dist/clis/instagram/like.yaml +46 -0
  97. package/dist/clis/instagram/profile.yaml +42 -0
  98. package/dist/clis/instagram/save.yaml +46 -0
  99. package/dist/clis/instagram/saved.yaml +40 -0
  100. package/dist/clis/instagram/search.yaml +43 -0
  101. package/dist/clis/instagram/unfollow.yaml +38 -0
  102. package/dist/clis/instagram/unlike.yaml +46 -0
  103. package/dist/clis/instagram/unsave.yaml +46 -0
  104. package/dist/clis/instagram/user.yaml +54 -0
  105. package/dist/clis/jike/repost.js +1 -1
  106. package/dist/clis/jimeng/generate.yaml +1 -0
  107. package/dist/clis/linux-do/category.yaml +1 -0
  108. package/dist/clis/lobsters/active.yaml +29 -0
  109. package/dist/clis/lobsters/hot.yaml +29 -0
  110. package/dist/clis/lobsters/newest.yaml +29 -0
  111. package/dist/clis/lobsters/tag.yaml +34 -0
  112. package/dist/clis/medium/feed.d.ts +1 -0
  113. package/dist/clis/medium/feed.js +15 -0
  114. package/dist/clis/medium/search.d.ts +1 -0
  115. package/dist/clis/medium/search.js +15 -0
  116. package/dist/clis/medium/shared.d.ts +5 -0
  117. package/dist/clis/medium/shared.js +78 -0
  118. package/dist/clis/medium/user.d.ts +1 -0
  119. package/dist/clis/medium/user.js +15 -0
  120. package/dist/clis/reddit/comment.js +1 -1
  121. package/dist/clis/reddit/read.js +1 -1
  122. package/dist/clis/reddit/save.js +1 -1
  123. package/dist/clis/reddit/subreddit.yaml +1 -0
  124. package/dist/clis/reddit/subscribe.js +1 -1
  125. package/dist/clis/reddit/upvote.js +1 -1
  126. package/dist/clis/sinablog/article.d.ts +1 -0
  127. package/dist/clis/sinablog/article.js +14 -0
  128. package/dist/clis/sinablog/hot.d.ts +1 -0
  129. package/dist/clis/sinablog/hot.js +14 -0
  130. package/dist/clis/sinablog/search.d.ts +1 -0
  131. package/dist/clis/sinablog/search.js +51 -0
  132. package/dist/clis/sinablog/shared.d.ts +7 -0
  133. package/dist/clis/sinablog/shared.js +187 -0
  134. package/dist/clis/sinablog/user.d.ts +1 -0
  135. package/dist/clis/sinablog/user.js +15 -0
  136. package/dist/clis/substack/feed.d.ts +1 -0
  137. package/dist/clis/substack/feed.js +15 -0
  138. package/dist/clis/substack/publication.d.ts +1 -0
  139. package/dist/clis/substack/publication.js +15 -0
  140. package/dist/clis/substack/search.d.ts +1 -0
  141. package/dist/clis/substack/search.js +77 -0
  142. package/dist/clis/substack/shared.d.ts +4 -0
  143. package/dist/clis/substack/shared.js +129 -0
  144. package/dist/clis/tiktok/comment.yaml +66 -0
  145. package/dist/clis/tiktok/explore.yaml +39 -0
  146. package/dist/clis/tiktok/follow.yaml +39 -0
  147. package/dist/clis/tiktok/following.yaml +46 -0
  148. package/dist/clis/tiktok/friends.yaml +47 -0
  149. package/dist/clis/tiktok/like.yaml +38 -0
  150. package/dist/clis/tiktok/live.yaml +51 -0
  151. package/dist/clis/tiktok/notifications.yaml +52 -0
  152. package/dist/clis/tiktok/profile.yaml +45 -0
  153. package/dist/clis/tiktok/save.yaml +34 -0
  154. package/dist/clis/tiktok/search.yaml +46 -0
  155. package/dist/clis/tiktok/unfollow.yaml +44 -0
  156. package/dist/clis/tiktok/unlike.yaml +38 -0
  157. package/dist/clis/tiktok/unsave.yaml +36 -0
  158. package/dist/clis/tiktok/user.yaml +44 -0
  159. package/dist/clis/twitter/download.d.ts +1 -1
  160. package/dist/clis/twitter/download.js +3 -3
  161. package/dist/clis/twitter/followers.js +1 -1
  162. package/dist/clis/twitter/following.js +1 -1
  163. package/dist/clis/twitter/thread.js +1 -1
  164. package/dist/clis/twitter/timeline.d.ts +23 -0
  165. package/dist/clis/twitter/timeline.js +42 -14
  166. package/dist/clis/twitter/timeline.test.d.ts +1 -0
  167. package/dist/clis/twitter/timeline.test.js +102 -0
  168. package/dist/clis/wikipedia/random.d.ts +1 -0
  169. package/dist/clis/wikipedia/random.js +19 -0
  170. package/dist/clis/wikipedia/search.js +3 -3
  171. package/dist/clis/wikipedia/summary.js +4 -9
  172. package/dist/clis/wikipedia/trending.d.ts +1 -0
  173. package/dist/clis/wikipedia/trending.js +35 -0
  174. package/dist/clis/wikipedia/utils.d.ts +28 -0
  175. package/dist/clis/wikipedia/utils.js +13 -0
  176. package/dist/clis/xiaohongshu/creator-note-detail.js +1 -1
  177. package/dist/clis/xiaohongshu/creator-note-detail.test.js +2 -0
  178. package/dist/clis/xiaohongshu/creator-notes.test.js +2 -0
  179. package/dist/clis/xiaohongshu/download.js +1 -1
  180. package/dist/clis/xueqiu/earnings-date.yaml +69 -0
  181. package/dist/clis/xueqiu/search.yaml +2 -1
  182. package/dist/clis/xueqiu/stock.yaml +2 -0
  183. package/dist/clis/yahoo-finance/quote.js +1 -1
  184. package/dist/commanderAdapter.js +13 -7
  185. package/dist/discovery.d.ts +8 -0
  186. package/dist/discovery.js +105 -19
  187. package/dist/doctor.js +3 -1
  188. package/dist/doctor.test.js +46 -2
  189. package/dist/engine.test.d.ts +0 -3
  190. package/dist/engine.test.js +74 -6
  191. package/dist/execution.d.ts +4 -2
  192. package/dist/execution.js +31 -7
  193. package/dist/explore.d.ts +76 -3
  194. package/dist/explore.js +11 -4
  195. package/dist/generate.d.ts +41 -2
  196. package/dist/generate.js +5 -4
  197. package/dist/main.js +2 -1
  198. package/dist/pipeline/executor.d.ts +2 -2
  199. package/dist/pipeline/executor.js +2 -2
  200. package/dist/pipeline/executor.test.js +33 -6
  201. package/dist/pipeline/registry.d.ts +1 -1
  202. package/dist/pipeline/steps/browser.d.ts +7 -7
  203. package/dist/pipeline/steps/browser.js +15 -7
  204. package/dist/pipeline/steps/fetch.d.ts +1 -1
  205. package/dist/pipeline/steps/fetch.js +11 -7
  206. package/dist/pipeline/steps/transform.d.ts +6 -5
  207. package/dist/pipeline/steps/transform.js +30 -9
  208. package/dist/pipeline/template.d.ts +6 -6
  209. package/dist/pipeline/template.js +43 -5
  210. package/dist/pipeline/template.test.js +18 -0
  211. package/dist/pipeline/transform.test.js +11 -0
  212. package/dist/plugin.d.ts +31 -0
  213. package/dist/plugin.js +216 -0
  214. package/dist/plugin.test.d.ts +4 -0
  215. package/dist/plugin.test.js +76 -0
  216. package/dist/registry-api.d.ts +11 -0
  217. package/dist/registry-api.js +9 -0
  218. package/dist/registry.d.ts +11 -0
  219. package/dist/registry.js +6 -1
  220. package/dist/synthesize.d.ts +94 -4
  221. package/dist/synthesize.js +5 -4
  222. package/dist/types.d.ts +39 -26
  223. package/dist/validate.js +8 -2
  224. package/docs/.vitepress/config.mts +6 -4
  225. package/docs/adapters/browser/barchart.md +6 -5
  226. package/docs/adapters/browser/bilibili.md +9 -0
  227. package/docs/adapters/browser/devto.md +35 -0
  228. package/docs/adapters/browser/douban.md +38 -0
  229. package/docs/adapters/browser/facebook.md +36 -0
  230. package/docs/adapters/browser/google.md +62 -0
  231. package/docs/adapters/browser/grok.md +26 -8
  232. package/docs/adapters/browser/instagram.md +46 -0
  233. package/docs/adapters/browser/lobsters.md +32 -0
  234. package/docs/adapters/browser/medium.md +32 -0
  235. package/docs/adapters/browser/reddit.md +9 -0
  236. package/docs/adapters/browser/sinablog.md +36 -0
  237. package/docs/adapters/browser/substack.md +38 -0
  238. package/docs/adapters/browser/tiktok.md +68 -0
  239. package/docs/adapters/browser/wikipedia.md +11 -2
  240. package/docs/adapters/browser/xueqiu.md +10 -0
  241. package/docs/adapters/browser/yahoo-finance.md +6 -5
  242. package/docs/adapters/desktop/antigravity.md +6 -0
  243. package/docs/adapters/desktop/chatgpt.md +2 -1
  244. package/docs/adapters/desktop/codex.md +5 -1
  245. package/docs/adapters/desktop/cursor.md +4 -0
  246. package/docs/adapters/desktop/discord.md +7 -7
  247. package/docs/adapters/index.md +1 -4
  248. package/docs/guide/getting-started.md +1 -0
  249. package/docs/guide/plugins.md +153 -0
  250. package/docs/zh/guide/plugins.md +107 -0
  251. package/extension/src/background.ts +18 -11
  252. package/package.json +10 -5
  253. package/scripts/clean-dist.cjs +13 -0
  254. package/src/browser/cdp.ts +71 -31
  255. package/src/browser/daemon-client.ts +2 -1
  256. package/src/browser/dom-helpers.ts +38 -7
  257. package/src/browser/dom-snapshot.test.ts +249 -0
  258. package/src/browser/dom-snapshot.ts +770 -0
  259. package/src/browser/index.ts +2 -0
  260. package/src/browser/page.ts +50 -19
  261. package/src/build-manifest.test.ts +70 -2
  262. package/src/build-manifest.ts +94 -26
  263. package/src/cli.ts +71 -2
  264. package/src/clis/barchart/greeks.ts +1 -1
  265. package/src/clis/barchart/options.ts +1 -1
  266. package/src/clis/barchart/quote.ts +1 -1
  267. package/src/clis/bilibili/download.ts +1 -1
  268. package/src/clis/bilibili/following.ts +1 -1
  269. package/src/clis/bilibili/subtitle.ts +1 -1
  270. package/src/clis/bilibili/user-videos.ts +1 -1
  271. package/src/clis/boss/batchgreet.ts +14 -106
  272. package/src/clis/boss/chatlist.ts +12 -26
  273. package/src/clis/boss/chatmsg.ts +16 -40
  274. package/src/clis/boss/common.ts +287 -0
  275. package/src/clis/boss/detail.ts +8 -54
  276. package/src/clis/boss/exchange.ts +15 -89
  277. package/src/clis/boss/greet.ts +23 -160
  278. package/src/clis/boss/invite.ts +36 -133
  279. package/src/clis/boss/joblist.ts +7 -36
  280. package/src/clis/boss/mark.ts +13 -94
  281. package/src/clis/boss/recommend.ts +12 -57
  282. package/src/clis/boss/resume.ts +19 -124
  283. package/src/clis/boss/search.ts +13 -66
  284. package/src/clis/boss/send.ts +21 -161
  285. package/src/clis/boss/stats.ts +19 -74
  286. package/src/clis/coupang/add-to-cart.ts +1 -1
  287. package/src/clis/devto/tag.yaml +34 -0
  288. package/src/clis/devto/top.yaml +29 -0
  289. package/src/clis/devto/user.yaml +33 -0
  290. package/src/clis/douban/book-hot.ts +15 -0
  291. package/src/clis/douban/marks.ts +135 -0
  292. package/src/clis/douban/movie-hot.ts +15 -0
  293. package/src/clis/douban/reviews.ts +127 -0
  294. package/src/clis/douban/search.ts +17 -0
  295. package/src/clis/douban/shared.ts +165 -0
  296. package/src/clis/douban/subject.yaml +76 -0
  297. package/src/clis/douban/top250.yaml +70 -0
  298. package/src/clis/douban/utils.ts +81 -0
  299. package/src/clis/facebook/add-friend.yaml +43 -0
  300. package/src/clis/facebook/events.yaml +44 -0
  301. package/src/clis/facebook/feed.yaml +63 -0
  302. package/src/clis/facebook/friends.yaml +42 -0
  303. package/src/clis/facebook/groups.yaml +50 -0
  304. package/src/clis/facebook/join-group.yaml +44 -0
  305. package/src/clis/facebook/memories.yaml +39 -0
  306. package/src/clis/facebook/notifications.yaml +40 -0
  307. package/src/clis/facebook/profile.yaml +37 -0
  308. package/src/clis/facebook/search.yaml +46 -0
  309. package/src/clis/google/news.ts +66 -0
  310. package/src/clis/google/search.ts +133 -0
  311. package/src/clis/google/suggest.ts +40 -0
  312. package/src/clis/google/trends.ts +44 -0
  313. package/src/clis/google/utils.test.ts +82 -0
  314. package/src/clis/google/utils.ts +24 -0
  315. package/src/clis/grok/ask.test.ts +53 -0
  316. package/src/clis/grok/ask.ts +300 -69
  317. package/src/clis/instagram/comment.yaml +52 -0
  318. package/src/clis/instagram/explore.yaml +43 -0
  319. package/src/clis/instagram/follow.yaml +41 -0
  320. package/src/clis/instagram/followers.yaml +51 -0
  321. package/src/clis/instagram/following.yaml +51 -0
  322. package/src/clis/instagram/like.yaml +46 -0
  323. package/src/clis/instagram/profile.yaml +42 -0
  324. package/src/clis/instagram/save.yaml +46 -0
  325. package/src/clis/instagram/saved.yaml +40 -0
  326. package/src/clis/instagram/search.yaml +43 -0
  327. package/src/clis/instagram/unfollow.yaml +38 -0
  328. package/src/clis/instagram/unlike.yaml +46 -0
  329. package/src/clis/instagram/unsave.yaml +46 -0
  330. package/src/clis/instagram/user.yaml +54 -0
  331. package/src/clis/jike/repost.ts +1 -1
  332. package/src/clis/jimeng/generate.yaml +1 -0
  333. package/src/clis/linux-do/category.yaml +1 -0
  334. package/src/clis/lobsters/active.yaml +29 -0
  335. package/src/clis/lobsters/hot.yaml +29 -0
  336. package/src/clis/lobsters/newest.yaml +29 -0
  337. package/src/clis/lobsters/tag.yaml +34 -0
  338. package/src/clis/medium/feed.ts +16 -0
  339. package/src/clis/medium/search.ts +16 -0
  340. package/src/clis/medium/shared.ts +83 -0
  341. package/src/clis/medium/user.ts +16 -0
  342. package/src/clis/reddit/comment.ts +1 -1
  343. package/src/clis/reddit/read.ts +1 -1
  344. package/src/clis/reddit/save.ts +1 -1
  345. package/src/clis/reddit/subreddit.yaml +1 -0
  346. package/src/clis/reddit/subscribe.ts +1 -1
  347. package/src/clis/reddit/upvote.ts +1 -1
  348. package/src/clis/sinablog/article.ts +15 -0
  349. package/src/clis/sinablog/hot.ts +15 -0
  350. package/src/clis/sinablog/search.ts +56 -0
  351. package/src/clis/sinablog/shared.ts +198 -0
  352. package/src/clis/sinablog/user.ts +16 -0
  353. package/src/clis/substack/feed.ts +16 -0
  354. package/src/clis/substack/publication.ts +16 -0
  355. package/src/clis/substack/search.ts +91 -0
  356. package/src/clis/substack/shared.ts +132 -0
  357. package/src/clis/tiktok/comment.yaml +66 -0
  358. package/src/clis/tiktok/explore.yaml +39 -0
  359. package/src/clis/tiktok/follow.yaml +39 -0
  360. package/src/clis/tiktok/following.yaml +46 -0
  361. package/src/clis/tiktok/friends.yaml +47 -0
  362. package/src/clis/tiktok/like.yaml +38 -0
  363. package/src/clis/tiktok/live.yaml +51 -0
  364. package/src/clis/tiktok/notifications.yaml +52 -0
  365. package/src/clis/tiktok/profile.yaml +45 -0
  366. package/src/clis/tiktok/save.yaml +34 -0
  367. package/src/clis/tiktok/search.yaml +46 -0
  368. package/src/clis/tiktok/unfollow.yaml +44 -0
  369. package/src/clis/tiktok/unlike.yaml +38 -0
  370. package/src/clis/tiktok/unsave.yaml +36 -0
  371. package/src/clis/tiktok/user.yaml +44 -0
  372. package/src/clis/twitter/download.ts +3 -3
  373. package/src/clis/twitter/followers.ts +1 -1
  374. package/src/clis/twitter/following.ts +1 -1
  375. package/src/clis/twitter/thread.ts +1 -1
  376. package/src/clis/twitter/timeline.test.ts +109 -0
  377. package/src/clis/twitter/timeline.ts +59 -19
  378. package/src/clis/wikipedia/random.ts +19 -0
  379. package/src/clis/wikipedia/search.ts +10 -4
  380. package/src/clis/wikipedia/summary.ts +4 -9
  381. package/src/clis/wikipedia/trending.ts +41 -0
  382. package/src/clis/wikipedia/utils.ts +31 -0
  383. package/src/clis/xiaohongshu/creator-note-detail.test.ts +2 -0
  384. package/src/clis/xiaohongshu/creator-note-detail.ts +1 -1
  385. package/src/clis/xiaohongshu/creator-notes.test.ts +2 -0
  386. package/src/clis/xiaohongshu/download.ts +1 -1
  387. package/src/clis/xueqiu/earnings-date.yaml +69 -0
  388. package/src/clis/xueqiu/search.yaml +2 -1
  389. package/src/clis/xueqiu/stock.yaml +2 -0
  390. package/src/clis/yahoo-finance/quote.ts +1 -1
  391. package/src/commanderAdapter.ts +17 -10
  392. package/src/discovery.ts +134 -24
  393. package/src/doctor.test.ts +59 -2
  394. package/src/doctor.ts +4 -2
  395. package/src/engine.test.ts +79 -6
  396. package/src/execution.ts +42 -16
  397. package/src/explore.ts +77 -9
  398. package/src/generate.ts +58 -9
  399. package/src/main.ts +2 -1
  400. package/src/pipeline/executor.test.ts +35 -6
  401. package/src/pipeline/executor.ts +11 -7
  402. package/src/pipeline/registry.ts +3 -3
  403. package/src/pipeline/steps/browser.ts +24 -15
  404. package/src/pipeline/steps/fetch.ts +18 -13
  405. package/src/pipeline/steps/transform.ts +40 -15
  406. package/src/pipeline/template.test.ts +18 -0
  407. package/src/pipeline/template.ts +86 -13
  408. package/src/pipeline/transform.test.ts +15 -2
  409. package/src/plugin.test.ts +86 -0
  410. package/src/plugin.ts +254 -0
  411. package/src/registry-api.ts +12 -0
  412. package/src/registry.ts +19 -1
  413. package/src/synthesize.ts +102 -21
  414. package/src/types.ts +44 -12
  415. package/src/validate.ts +19 -4
  416. package/tests/e2e/browser-public.test.ts +11 -0
  417. package/tests/e2e/public-commands.test.ts +64 -0
  418. package/dist/clis/feishu/new.d.ts +0 -1
  419. package/dist/clis/feishu/new.js +0 -27
  420. package/dist/clis/feishu/read.d.ts +0 -1
  421. package/dist/clis/feishu/read.js +0 -40
  422. package/dist/clis/feishu/search.d.ts +0 -1
  423. package/dist/clis/feishu/search.js +0 -30
  424. package/dist/clis/feishu/send.d.ts +0 -1
  425. package/dist/clis/feishu/send.js +0 -39
  426. package/dist/clis/feishu/status.d.ts +0 -1
  427. package/dist/clis/feishu/status.js +0 -28
  428. package/dist/clis/neteasemusic/like.d.ts +0 -1
  429. package/dist/clis/neteasemusic/like.js +0 -25
  430. package/dist/clis/neteasemusic/lyrics.d.ts +0 -1
  431. package/dist/clis/neteasemusic/lyrics.js +0 -47
  432. package/dist/clis/neteasemusic/next.d.ts +0 -1
  433. package/dist/clis/neteasemusic/next.js +0 -26
  434. package/dist/clis/neteasemusic/play.d.ts +0 -1
  435. package/dist/clis/neteasemusic/play.js +0 -26
  436. package/dist/clis/neteasemusic/playing.d.ts +0 -1
  437. package/dist/clis/neteasemusic/playing.js +0 -59
  438. package/dist/clis/neteasemusic/playlist.d.ts +0 -1
  439. package/dist/clis/neteasemusic/playlist.js +0 -46
  440. package/dist/clis/neteasemusic/prev.d.ts +0 -1
  441. package/dist/clis/neteasemusic/prev.js +0 -25
  442. package/dist/clis/neteasemusic/search.d.ts +0 -1
  443. package/dist/clis/neteasemusic/search.js +0 -52
  444. package/dist/clis/neteasemusic/status.d.ts +0 -1
  445. package/dist/clis/neteasemusic/status.js +0 -16
  446. package/dist/clis/neteasemusic/volume.d.ts +0 -1
  447. package/dist/clis/neteasemusic/volume.js +0 -54
  448. package/dist/clis/wechat/chats.d.ts +0 -1
  449. package/dist/clis/wechat/chats.js +0 -28
  450. package/dist/clis/wechat/contacts.d.ts +0 -1
  451. package/dist/clis/wechat/contacts.js +0 -28
  452. package/dist/clis/wechat/read.d.ts +0 -1
  453. package/dist/clis/wechat/read.js +0 -58
  454. package/dist/clis/wechat/search.d.ts +0 -1
  455. package/dist/clis/wechat/search.js +0 -31
  456. package/dist/clis/wechat/send.d.ts +0 -1
  457. package/dist/clis/wechat/send.js +0 -42
  458. package/dist/clis/wechat/status.d.ts +0 -1
  459. package/dist/clis/wechat/status.js +0 -29
  460. package/dist/pipeline.d.ts +0 -7
  461. package/dist/pipeline.js +0 -7
  462. package/docs/adapters/browser/github.md +0 -26
  463. package/docs/adapters/desktop/feishu.md +0 -20
  464. package/docs/adapters/desktop/neteasemusic.md +0 -31
  465. package/docs/adapters/desktop/wechat.md +0 -28
  466. package/src/clis/antigravity/README.md +0 -5
  467. package/src/clis/antigravity/README.zh-CN.md +0 -51
  468. package/src/clis/chaoxing/README.md +0 -14
  469. package/src/clis/chaoxing/README.zh-CN.md +0 -35
  470. package/src/clis/chatgpt/README.md +0 -5
  471. package/src/clis/chatgpt/README.zh-CN.md +0 -44
  472. package/src/clis/chatwise/README.md +0 -5
  473. package/src/clis/chatwise/README.zh-CN.md +0 -38
  474. package/src/clis/codex/README.md +0 -5
  475. package/src/clis/codex/README.zh-CN.md +0 -33
  476. package/src/clis/cursor/README.md +0 -5
  477. package/src/clis/cursor/README.zh-CN.md +0 -33
  478. package/src/clis/discord-app/README.md +0 -5
  479. package/src/clis/discord-app/README.zh-CN.md +0 -28
  480. package/src/clis/feishu/README.md +0 -5
  481. package/src/clis/feishu/README.zh-CN.md +0 -20
  482. package/src/clis/feishu/new.ts +0 -32
  483. package/src/clis/feishu/read.ts +0 -48
  484. package/src/clis/feishu/search.ts +0 -35
  485. package/src/clis/feishu/send.ts +0 -46
  486. package/src/clis/feishu/status.ts +0 -34
  487. package/src/clis/neteasemusic/README.md +0 -5
  488. package/src/clis/neteasemusic/README.zh-CN.md +0 -31
  489. package/src/clis/neteasemusic/like.ts +0 -28
  490. package/src/clis/neteasemusic/lyrics.ts +0 -53
  491. package/src/clis/neteasemusic/next.ts +0 -30
  492. package/src/clis/neteasemusic/play.ts +0 -30
  493. package/src/clis/neteasemusic/playing.ts +0 -62
  494. package/src/clis/neteasemusic/playlist.ts +0 -51
  495. package/src/clis/neteasemusic/prev.ts +0 -29
  496. package/src/clis/neteasemusic/search.ts +0 -58
  497. package/src/clis/neteasemusic/status.ts +0 -18
  498. package/src/clis/neteasemusic/volume.ts +0 -61
  499. package/src/clis/notion/README.md +0 -5
  500. package/src/clis/notion/README.zh-CN.md +0 -29
  501. package/src/clis/wechat/README.md +0 -5
  502. package/src/clis/wechat/README.zh-CN.md +0 -28
  503. package/src/clis/wechat/chats.ts +0 -33
  504. package/src/clis/wechat/contacts.ts +0 -33
  505. package/src/clis/wechat/read.ts +0 -72
  506. package/src/clis/wechat/search.ts +0 -36
  507. package/src/clis/wechat/send.ts +0 -49
  508. package/src/clis/wechat/status.ts +0 -35
  509. package/src/pipeline.ts +0 -8
@@ -5,6 +5,10 @@
5
5
  import type { IPage } from '../../types.js';
6
6
  import { render } from '../template.js';
7
7
 
8
+ function isRecord(value: unknown): value is Record<string, unknown> {
9
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
10
+ }
11
+
8
12
  /** Simple async concurrency limiter */
9
13
  async function mapConcurrent<T, R>(items: T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]> {
10
14
  const results: R[] = new Array(items.length);
@@ -25,9 +29,9 @@ async function mapConcurrent<T, R>(items: T[], limit: number, fn: (item: T, inde
25
29
  /** Single URL fetch helper */
26
30
  async function fetchSingle(
27
31
  page: IPage | null, url: string, method: string,
28
- queryParams: Record<string, any>, headers: Record<string, any>,
29
- args: Record<string, any>, data: any,
30
- ): Promise<any> {
32
+ queryParams: Record<string, unknown>, headers: Record<string, unknown>,
33
+ args: Record<string, unknown>, data: unknown,
34
+ ): Promise<unknown> {
31
35
  const renderedParams: Record<string, string> = {};
32
36
  for (const [k, v] of Object.entries(queryParams)) renderedParams[k] = String(render(v, { args, data }));
33
37
  const renderedHeaders: Record<string, string> = {};
@@ -65,10 +69,10 @@ async function fetchSingle(
65
69
  async function fetchBatchInBrowser(
66
70
  page: IPage, urls: string[], method: string,
67
71
  headers: Record<string, string>, concurrency: number,
68
- ): Promise<any[]> {
72
+ ): Promise<unknown[]> {
69
73
  const headersJs = JSON.stringify(headers);
70
74
  const urlsJs = JSON.stringify(urls);
71
- return page.evaluate(`
75
+ return (await page.evaluate(`
72
76
  async () => {
73
77
  const urls = ${urlsJs};
74
78
  const method = "${method}";
@@ -94,19 +98,20 @@ async function fetchBatchInBrowser(
94
98
  await Promise.all(workers);
95
99
  return results;
96
100
  }
97
- `);
101
+ `)) as unknown[];
98
102
  }
99
103
 
100
- export async function stepFetch(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any> {
101
- const urlOrObj = typeof params === 'string' ? params : (params?.url ?? '');
102
- const method = params?.method ?? 'GET';
103
- const queryParams: Record<string, any> = params?.params ?? {};
104
- const headers: Record<string, any> = params?.headers ?? {};
104
+ export async function stepFetch(page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
105
+ const paramObject = isRecord(params) ? params : {};
106
+ const urlOrObj = typeof params === 'string' ? params : (paramObject.url ?? '');
107
+ const method = typeof paramObject.method === 'string' ? paramObject.method : 'GET';
108
+ const queryParams = isRecord(paramObject.params) ? paramObject.params : {};
109
+ const headers = isRecord(paramObject.headers) ? paramObject.headers : {};
105
110
  const urlTemplate = String(urlOrObj);
106
111
 
107
112
  // Per-item fetch when data is array and URL references item
108
113
  if (Array.isArray(data) && urlTemplate.includes('item')) {
109
- const concurrency = typeof params?.concurrency === 'number' ? params.concurrency : 5;
114
+ const concurrency = typeof paramObject.concurrency === 'number' ? paramObject.concurrency : 5;
110
115
 
111
116
  // Render all URLs upfront
112
117
  const renderedHeaders: Record<string, string> = {};
@@ -114,7 +119,7 @@ export async function stepFetch(page: IPage | null, params: any, data: any, args
114
119
  const renderedParams: Record<string, string> = {};
115
120
  for (const [k, v] of Object.entries(queryParams)) renderedParams[k] = String(render(v, { args, data }));
116
121
 
117
- const urls = data.map((item: any, index: number) => {
122
+ const urls = data.map((item, index) => {
118
123
  let url = String(render(urlTemplate, { args, data, item, index }));
119
124
  if (Object.keys(renderedParams).length > 0) {
120
125
  const qs = new URLSearchParams(renderedParams).toString();
@@ -2,14 +2,19 @@
2
2
  * Pipeline steps: data transforms — select, map, filter, sort, limit.
3
3
  */
4
4
 
5
+ import type { IPage } from '../../types.js';
5
6
  import { render, evalExpr } from '../template.js';
6
7
 
7
- export async function stepSelect(_page: any, params: any, data: any, args: Record<string, any>): Promise<any> {
8
+ function isRecord(value: unknown): value is Record<string, unknown> {
9
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
10
+ }
11
+
12
+ export async function stepSelect(_page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
8
13
  const pathStr = String(render(params, { args, data }));
9
14
  if (data && typeof data === 'object') {
10
- let current = data;
15
+ let current: unknown = data;
11
16
  for (const part of pathStr.split('.')) {
12
- if (current && typeof current === 'object' && !Array.isArray(current)) current = (current as any)[part];
17
+ if (isRecord(current)) current = current[part];
13
18
  else if (Array.isArray(current) && /^\d+$/.test(part)) current = current[parseInt(part, 10)];
14
19
  else return null;
15
20
  }
@@ -18,33 +23,53 @@ export async function stepSelect(_page: any, params: any, data: any, args: Recor
18
23
  return data;
19
24
  }
20
25
 
21
- export async function stepMap(_page: any, params: any, data: any, args: Record<string, any>): Promise<any> {
26
+ export async function stepMap(_page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
22
27
  if (!data || typeof data !== 'object') return data;
23
- let items: any[] = Array.isArray(data) ? data : [data];
24
- if (!Array.isArray(data) && typeof data === 'object' && 'data' in data) items = data.data;
25
- const result: any[] = [];
28
+ let source: unknown = data;
29
+
30
+ // Support inline select: { map: { select: 'path', key: '${{ item.x }}' } }
31
+ if (isRecord(params) && 'select' in params) {
32
+ source = await stepSelect(null, params.select, data, args);
33
+ }
34
+
35
+ if (!source || typeof source !== 'object') return source;
36
+
37
+ let items: unknown[] = Array.isArray(source) ? source : [source];
38
+ if (isRecord(source) && Array.isArray(source.data)) items = source.data;
39
+ const result: Array<Record<string, unknown>> = [];
40
+ const templateParams = isRecord(params) ? params : {};
26
41
  for (let i = 0; i < items.length; i++) {
27
42
  const item = items[i];
28
- const row: Record<string, any> = {};
29
- for (const [key, template] of Object.entries(params)) row[key] = render(template, { args, data, item, index: i });
43
+ const row: Record<string, unknown> = {};
44
+ for (const [key, template] of Object.entries(templateParams)) {
45
+ if (key === 'select') continue;
46
+ row[key] = render(template, { args, data: source, item, index: i });
47
+ }
30
48
  result.push(row);
31
49
  }
32
50
  return result;
33
51
  }
34
52
 
35
- export async function stepFilter(_page: any, params: any, data: any, args: Record<string, any>): Promise<any> {
53
+ export async function stepFilter(_page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
36
54
  if (!Array.isArray(data)) return data;
37
55
  return data.filter((item, i) => evalExpr(String(params), { args, item, index: i }));
38
56
  }
39
57
 
40
- export async function stepSort(_page: any, params: any, data: any, _args: Record<string, any>): Promise<any> {
58
+ export async function stepSort(_page: IPage | null, params: unknown, data: unknown, _args: Record<string, unknown>): Promise<unknown> {
41
59
  if (!Array.isArray(data)) return data;
42
- const key = typeof params === 'object' ? (params.by ?? '') : String(params);
43
- const reverse = typeof params === 'object' ? params.order === 'desc' : false;
44
- return [...data].sort((a, b) => { const va = a[key] ?? ''; const vb = b[key] ?? ''; const cmp = va < vb ? -1 : va > vb ? 1 : 0; return reverse ? -cmp : cmp; });
60
+ const key = isRecord(params) ? String(params.by ?? '') : String(params);
61
+ const reverse = isRecord(params) ? params.order === 'desc' : false;
62
+ return [...data].sort((a, b) => {
63
+ const left = isRecord(a) ? a[key] : undefined;
64
+ const right = isRecord(b) ? b[key] : undefined;
65
+ const va = left ?? '';
66
+ const vb = right ?? '';
67
+ const cmp = va < vb ? -1 : va > vb ? 1 : 0;
68
+ return reverse ? -cmp : cmp;
69
+ });
45
70
  }
46
71
 
47
- export async function stepLimit(_page: any, params: any, data: any, args: Record<string, any>): Promise<any> {
72
+ export async function stepLimit(_page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
48
73
  if (!Array.isArray(data)) return data;
49
74
  return data.slice(0, Number(render(params, { args, data })));
50
75
  }
@@ -57,6 +57,15 @@ describe('evalExpr', () => {
57
57
  it('resolves simple path', () => {
58
58
  expect(evalExpr('item.title', { item: { title: 'Test' } })).toBe('Test');
59
59
  });
60
+ it('evaluates JS helper expressions', () => {
61
+ expect(evalExpr('encodeURIComponent(args.keyword)', { args: { keyword: 'hello world' } })).toBe('hello%20world');
62
+ });
63
+ it('evaluates ternary expressions', () => {
64
+ expect(evalExpr("args.kind === 'tech' ? 'technology' : args.kind", { args: { kind: 'tech' } })).toBe('technology');
65
+ });
66
+ it('evaluates method calls on values', () => {
67
+ expect(evalExpr("args.username.startsWith('@') ? args.username : '@' + args.username", { args: { username: 'alice' } })).toBe('@alice');
68
+ });
60
69
  it('applies join filter', () => {
61
70
  expect(evalExpr('item.tags | join(,)', { item: { tags: ['a', 'b', 'c'] } })).toBe('a,b,c');
62
71
  });
@@ -104,6 +113,15 @@ describe('render', () => {
104
113
  it('renders URL template', () => {
105
114
  expect(render('https://api.example.com/search?q=${{ args.keyword }}', { args: { keyword: 'test' } })).toBe('https://api.example.com/search?q=test');
106
115
  });
116
+ it('renders inline helper expressions', () => {
117
+ expect(render('https://example.com/search?q=${{ encodeURIComponent(args.keyword) }}', { args: { keyword: 'hello world' } })).toBe('https://example.com/search?q=hello%20world');
118
+ });
119
+ it('renders full multiline expressions', () => {
120
+ expect(render("${{\n args.topic ? `https://medium.com/tag/${args.topic}` : 'https://medium.com/tag/technology'\n}}", { args: { topic: 'ai' } })).toBe('https://medium.com/tag/ai');
121
+ });
122
+ it('renders block expressions with surrounding whitespace', () => {
123
+ expect(render("\n ${{ args.kind === 'tech' ? 'technology' : args.kind }}\n", { args: { kind: 'tech' } })).toBe('technology');
124
+ });
107
125
  });
108
126
 
109
127
  describe('normalizeEvaluateSource', () => {
@@ -3,20 +3,25 @@
3
3
  */
4
4
 
5
5
  export interface RenderContext {
6
- args?: Record<string, any>;
7
- data?: any;
8
- item?: any;
6
+ args?: Record<string, unknown>;
7
+ data?: unknown;
8
+ item?: unknown;
9
9
  index?: number;
10
10
  }
11
11
 
12
- export function render(template: any, ctx: RenderContext): any {
12
+ function isRecord(value: unknown): value is Record<string, unknown> {
13
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
14
+ }
15
+
16
+ export function render(template: unknown, ctx: RenderContext): unknown {
13
17
  if (typeof template !== 'string') return template;
18
+ const trimmed = template.trim();
14
19
  // Full expression: entire string is a single ${{ ... }}
15
20
  // Use [^}] to prevent matching across }} boundaries (e.g. "${{ a }}-${{ b }}")
16
- const fullMatch = template.match(/^\$\{\{\s*([^}]*(?:\}[^}][^}]*)*)\s*\}\}$/);
17
- if (fullMatch && !template.includes('}}-') && !template.includes('}}${{')) return evalExpr(fullMatch[1].trim(), ctx);
21
+ const fullMatch = trimmed.match(/^\$\{\{\s*([^}]*(?:\}[^}][^}]*)*)\s*\}\}$/);
22
+ if (fullMatch && !trimmed.includes('}}-') && !trimmed.includes('}}${{')) return evalExpr(fullMatch[1].trim(), ctx);
18
23
  // Check if the entire string is a single expression (no other text around it)
19
- const singleExpr = template.match(/^\$\{\{\s*([\s\S]*?)\s*\}\}$/);
24
+ const singleExpr = trimmed.match(/^\$\{\{\s*([\s\S]*?)\s*\}\}$/);
20
25
  if (singleExpr) {
21
26
  // Verify it's truly a single expression (no other ${{ inside)
22
27
  const inner = singleExpr[1];
@@ -25,7 +30,7 @@ export function render(template: any, ctx: RenderContext): any {
25
30
  return template.replace(/\$\{\{\s*(.*?)\s*\}\}/g, (_m, expr) => String(evalExpr(expr.trim(), ctx)));
26
31
  }
27
32
 
28
- export function evalExpr(expr: string, ctx: RenderContext): any {
33
+ export function evalExpr(expr: string, ctx: RenderContext): unknown {
29
34
  const args = ctx.args ?? {};
30
35
  const item = ctx.item ?? {};
31
36
  const data = ctx.data;
@@ -68,7 +73,10 @@ export function evalExpr(expr: string, ctx: RenderContext): any {
68
73
  return right.replace(/^['"]|['"]$/g, '');
69
74
  }
70
75
 
71
- return resolvePath(expr, { args, item, data, index });
76
+ const resolved = resolvePath(expr, { args, item, data, index });
77
+ if (resolved !== null && resolved !== undefined) return resolved;
78
+
79
+ return evalJsExpr(expr, { args, item, data, index });
72
80
  }
73
81
 
74
82
  /**
@@ -77,7 +85,7 @@ export function evalExpr(expr: string, ctx: RenderContext): any {
77
85
  * default(val), join(sep), upper, lower, truncate(n), trim,
78
86
  * replace(old,new), keys, length, first, last, json
79
87
  */
80
- function applyFilter(filterExpr: string, value: any): any {
88
+ function applyFilter(filterExpr: string, value: unknown): unknown {
81
89
  const match = filterExpr.match(/^(\w+)(?:\((.+)\))?$/);
82
90
  if (!match) return value;
83
91
  const [, name, rawArgs] = match;
@@ -145,32 +153,97 @@ function applyFilter(filterExpr: string, value: any): any {
145
153
  const parts = value.split(/[/\\]/);
146
154
  return parts[parts.length - 1] || value;
147
155
  }
156
+ case 'urlencode':
157
+ return typeof value === 'string' ? encodeURIComponent(value) : value;
158
+ case 'urldecode':
159
+ return typeof value === 'string' ? decodeURIComponent(value) : value;
148
160
  default:
149
161
  return value;
150
162
  }
151
163
  }
152
164
 
153
- export function resolvePath(pathStr: string, ctx: RenderContext): any {
165
+ export function resolvePath(pathStr: string, ctx: RenderContext): unknown {
154
166
  const args = ctx.args ?? {};
155
167
  const item = ctx.item ?? {};
156
168
  const data = ctx.data;
157
169
  const index = ctx.index ?? 0;
158
170
  const parts = pathStr.split('.');
159
171
  const rootName = parts[0];
160
- let obj: any; let rest: string[];
172
+ let obj: unknown;
173
+ let rest: string[];
161
174
  if (rootName === 'args') { obj = args; rest = parts.slice(1); }
162
175
  else if (rootName === 'item') { obj = item; rest = parts.slice(1); }
163
176
  else if (rootName === 'data') { obj = data; rest = parts.slice(1); }
164
177
  else if (rootName === 'index') return index;
165
178
  else { obj = item; rest = parts; }
166
179
  for (const part of rest) {
167
- if (obj && typeof obj === 'object' && !Array.isArray(obj)) obj = obj[part];
180
+ if (isRecord(obj)) obj = obj[part];
168
181
  else if (Array.isArray(obj) && /^\d+$/.test(part)) obj = obj[parseInt(part, 10)];
169
182
  else return null;
170
183
  }
171
184
  return obj;
172
185
  }
173
186
 
187
+ /**
188
+ * Evaluate arbitrary JS expressions as a last-resort fallback.
189
+ *
190
+ * ⚠️ SECURITY NOTE: Uses `new Function()` to execute the expression.
191
+ * This is acceptable here because:
192
+ * 1. YAML adapters are authored by trusted repo contributors only.
193
+ * 2. The expression runs in the same Node.js process (no sandbox).
194
+ * 3. Only a curated set of globals is exposed (no require/import/process/fs).
195
+ * If opencli ever loads untrusted third-party adapters, this MUST be replaced
196
+ * with a proper sandboxed evaluator.
197
+ */
198
+ function evalJsExpr(expr: string, ctx: RenderContext): unknown {
199
+ // Guard against absurdly long expressions that could indicate injection.
200
+ if (expr.length > 2000) return undefined;
201
+
202
+ const args = ctx.args ?? {};
203
+ const item = ctx.item ?? {};
204
+ const data = ctx.data;
205
+ const index = ctx.index ?? 0;
206
+
207
+ try {
208
+ const fn = new Function(
209
+ 'args',
210
+ 'item',
211
+ 'data',
212
+ 'index',
213
+ 'encodeURIComponent',
214
+ 'decodeURIComponent',
215
+ 'JSON',
216
+ 'Math',
217
+ 'Number',
218
+ 'String',
219
+ 'Boolean',
220
+ 'Array',
221
+ 'Object',
222
+ 'Date',
223
+ `"use strict"; return (${expr});`,
224
+ );
225
+
226
+ return fn(
227
+ args,
228
+ item,
229
+ data,
230
+ index,
231
+ encodeURIComponent,
232
+ decodeURIComponent,
233
+ JSON,
234
+ Math,
235
+ Number,
236
+ String,
237
+ Boolean,
238
+ Array,
239
+ Object,
240
+ Date,
241
+ );
242
+ } catch {
243
+ return undefined;
244
+ }
245
+ }
246
+
174
247
  /**
175
248
  * Normalize JavaScript source for browser evaluate() calls.
176
249
  */
@@ -58,6 +58,19 @@ describe('stepMap', () => {
58
58
  it('returns null/undefined as-is', async () => {
59
59
  expect(await stepMap(null, { x: '${{ item.x }}' }, null, {})).toBeNull();
60
60
  });
61
+
62
+ it('supports inline select before mapping', async () => {
63
+ const result = await stepMap(null, {
64
+ select: 'posts',
65
+ title: '${{ item.title }}',
66
+ rank: '${{ index + 1 }}',
67
+ }, { posts: [{ title: 'One' }, { title: 'Two' }] }, {});
68
+
69
+ expect(result).toEqual([
70
+ { title: 'One', rank: 1 },
71
+ { title: 'Two', rank: 2 },
72
+ ]);
73
+ });
61
74
  });
62
75
 
63
76
  describe('stepFilter', () => {
@@ -75,12 +88,12 @@ describe('stepFilter', () => {
75
88
  describe('stepSort', () => {
76
89
  it('sorts ascending by key', async () => {
77
90
  const result = await stepSort(null, 'score', SAMPLE_DATA, {});
78
- expect(result.map((r: any) => r.title)).toEqual(['Alpha', 'Gamma', 'Beta']);
91
+ expect((result as typeof SAMPLE_DATA).map((r) => r.title)).toEqual(['Alpha', 'Gamma', 'Beta']);
79
92
  });
80
93
 
81
94
  it('sorts descending', async () => {
82
95
  const result = await stepSort(null, { by: 'score', order: 'desc' }, SAMPLE_DATA, {});
83
- expect(result.map((r: any) => r.title)).toEqual(['Beta', 'Gamma', 'Alpha']);
96
+ expect((result as typeof SAMPLE_DATA).map((r) => r.title)).toEqual(['Beta', 'Gamma', 'Alpha']);
84
97
  });
85
98
 
86
99
  it('does not mutate original', async () => {
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Tests for plugin management: install, uninstall, list.
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import * as fs from 'node:fs';
7
+ import * as path from 'node:path';
8
+ import { PLUGINS_DIR } from './discovery.js';
9
+ import { listPlugins, uninstallPlugin, _parseSource } from './plugin.js';
10
+
11
+ describe('parseSource', () => {
12
+ it('parses github:user/repo format', () => {
13
+ const result = _parseSource('github:ByteYue/opencli-plugin-github-trending');
14
+ expect(result).toEqual({
15
+ cloneUrl: 'https://github.com/ByteYue/opencli-plugin-github-trending.git',
16
+ name: 'github-trending',
17
+ });
18
+ });
19
+
20
+ it('parses https URL format', () => {
21
+ const result = _parseSource('https://github.com/ByteYue/opencli-plugin-hot-digest');
22
+ expect(result).toEqual({
23
+ cloneUrl: 'https://github.com/ByteYue/opencli-plugin-hot-digest.git',
24
+ name: 'hot-digest',
25
+ });
26
+ });
27
+
28
+ it('strips opencli-plugin- prefix from name', () => {
29
+ const result = _parseSource('github:user/opencli-plugin-my-tool');
30
+ expect(result!.name).toBe('my-tool');
31
+ });
32
+
33
+ it('keeps name without prefix', () => {
34
+ const result = _parseSource('github:user/awesome-cli');
35
+ expect(result!.name).toBe('awesome-cli');
36
+ });
37
+
38
+ it('returns null for invalid source', () => {
39
+ expect(_parseSource('invalid')).toBeNull();
40
+ expect(_parseSource('npm:some-package')).toBeNull();
41
+ });
42
+ });
43
+
44
+ describe('listPlugins', () => {
45
+ const testDir = path.join(PLUGINS_DIR, '__test-list-plugin__');
46
+
47
+ afterEach(() => {
48
+ try { fs.rmSync(testDir, { recursive: true }); } catch {}
49
+ });
50
+
51
+ it('lists installed plugins', () => {
52
+ fs.mkdirSync(testDir, { recursive: true });
53
+ fs.writeFileSync(path.join(testDir, 'hello.yaml'), 'site: test\nname: hello\n');
54
+
55
+ const plugins = listPlugins();
56
+ const found = plugins.find(p => p.name === '__test-list-plugin__');
57
+ expect(found).toBeDefined();
58
+ expect(found!.commands).toContain('hello');
59
+ });
60
+
61
+ it('returns empty array when no plugins dir', () => {
62
+ // listPlugins should handle missing dir gracefully
63
+ const plugins = listPlugins();
64
+ expect(Array.isArray(plugins)).toBe(true);
65
+ });
66
+ });
67
+
68
+ describe('uninstallPlugin', () => {
69
+ const testDir = path.join(PLUGINS_DIR, '__test-uninstall__');
70
+
71
+ afterEach(() => {
72
+ try { fs.rmSync(testDir, { recursive: true }); } catch {}
73
+ });
74
+
75
+ it('removes plugin directory', () => {
76
+ fs.mkdirSync(testDir, { recursive: true });
77
+ fs.writeFileSync(path.join(testDir, 'test.yaml'), 'site: test');
78
+
79
+ uninstallPlugin('__test-uninstall__');
80
+ expect(fs.existsSync(testDir)).toBe(false);
81
+ });
82
+
83
+ it('throws for non-existent plugin', () => {
84
+ expect(() => uninstallPlugin('__nonexistent__')).toThrow('not installed');
85
+ });
86
+ });