@jackwener/opencli 1.1.0 → 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 (769) hide show
  1. package/.agents/skills/cross-project-adapter-migration/SKILL.md +2 -2
  2. package/.github/pull_request_template.md +7 -0
  3. package/.github/workflows/doc-check.yml +36 -0
  4. package/.github/workflows/docs.yml +7 -42
  5. package/CHANGELOG.md +23 -0
  6. package/CLI-EXPLORER.md +9 -8
  7. package/CONTRIBUTING.md +39 -1
  8. package/README.md +33 -19
  9. package/README.zh-CN.md +64 -27
  10. package/SKILL.md +102 -33
  11. package/dist/browser/cdp.d.ts +4 -4
  12. package/dist/browser/cdp.js +45 -17
  13. package/dist/browser/daemon-client.d.ts +2 -1
  14. package/dist/browser/dom-helpers.js +38 -7
  15. package/dist/browser/dom-snapshot.d.ts +86 -0
  16. package/dist/browser/dom-snapshot.js +729 -0
  17. package/dist/browser/dom-snapshot.test.d.ts +11 -0
  18. package/dist/browser/dom-snapshot.test.js +212 -0
  19. package/dist/browser/index.d.ts +2 -0
  20. package/dist/browser/index.js +1 -0
  21. package/dist/browser/page.d.ts +18 -25
  22. package/dist/browser/page.js +44 -5
  23. package/dist/build-manifest.d.ts +11 -4
  24. package/dist/build-manifest.js +79 -34
  25. package/dist/build-manifest.test.js +58 -2
  26. package/dist/cli-manifest.json +4273 -1771
  27. package/dist/cli.d.ts +6 -0
  28. package/dist/cli.js +255 -162
  29. package/dist/clis/apple-podcasts/commands.test.d.ts +2 -0
  30. package/dist/clis/apple-podcasts/commands.test.js +76 -0
  31. package/dist/clis/apple-podcasts/search.js +2 -2
  32. package/dist/clis/apple-podcasts/top.js +9 -2
  33. package/dist/clis/arxiv/search.js +1 -1
  34. package/dist/clis/barchart/greeks.js +1 -1
  35. package/dist/clis/barchart/options.js +1 -1
  36. package/dist/clis/barchart/quote.js +1 -1
  37. package/dist/clis/bilibili/download.js +1 -1
  38. package/dist/clis/bilibili/dynamic.js +1 -1
  39. package/dist/clis/bilibili/favorite.js +1 -1
  40. package/dist/clis/bilibili/feed.js +1 -1
  41. package/dist/clis/bilibili/following.js +2 -2
  42. package/dist/clis/bilibili/history.js +1 -1
  43. package/dist/clis/bilibili/me.js +1 -1
  44. package/dist/clis/bilibili/ranking.js +1 -1
  45. package/dist/clis/bilibili/search.js +3 -3
  46. package/dist/clis/bilibili/subtitle.js +2 -2
  47. package/dist/clis/bilibili/user-videos.js +2 -2
  48. package/dist/{bilibili.d.ts → clis/bilibili/utils.d.ts} +1 -1
  49. package/dist/clis/bloomberg/businessweek.js +17 -0
  50. package/dist/clis/bloomberg/economics.js +17 -0
  51. package/dist/clis/bloomberg/feeds.d.ts +1 -0
  52. package/dist/clis/bloomberg/feeds.js +15 -0
  53. package/dist/clis/bloomberg/industries.d.ts +1 -0
  54. package/dist/clis/bloomberg/industries.js +17 -0
  55. package/dist/clis/bloomberg/main.d.ts +1 -0
  56. package/dist/clis/bloomberg/main.js +17 -0
  57. package/dist/clis/bloomberg/markets.d.ts +1 -0
  58. package/dist/clis/bloomberg/markets.js +17 -0
  59. package/dist/clis/bloomberg/news.d.ts +1 -0
  60. package/dist/clis/bloomberg/news.js +105 -0
  61. package/dist/clis/bloomberg/opinions.d.ts +1 -0
  62. package/dist/clis/bloomberg/opinions.js +17 -0
  63. package/dist/clis/bloomberg/politics.d.ts +1 -0
  64. package/dist/clis/bloomberg/politics.js +17 -0
  65. package/dist/clis/bloomberg/tech.d.ts +1 -0
  66. package/dist/clis/bloomberg/tech.js +17 -0
  67. package/dist/clis/bloomberg/utils.d.ts +34 -0
  68. package/dist/clis/bloomberg/utils.js +364 -0
  69. package/dist/clis/bloomberg/utils.test.d.ts +1 -0
  70. package/dist/clis/bloomberg/utils.test.js +129 -0
  71. package/dist/clis/boss/batchgreet.js +12 -99
  72. package/dist/clis/boss/chatlist.js +9 -26
  73. package/dist/clis/boss/chatmsg.js +11 -42
  74. package/dist/clis/boss/common.d.ts +92 -0
  75. package/dist/clis/boss/common.js +223 -0
  76. package/dist/clis/boss/detail.js +8 -50
  77. package/dist/clis/boss/exchange.js +13 -79
  78. package/dist/clis/boss/greet.js +20 -147
  79. package/dist/clis/boss/invite.js +26 -121
  80. package/dist/clis/boss/joblist.js +6 -31
  81. package/dist/clis/boss/mark.js +12 -85
  82. package/dist/clis/boss/recommend.js +10 -49
  83. package/dist/clis/boss/resume.js +18 -118
  84. package/dist/clis/boss/search.js +13 -61
  85. package/dist/clis/boss/send.js +18 -152
  86. package/dist/clis/boss/stats.js +20 -71
  87. package/dist/clis/chaoxing/assignments.js +1 -1
  88. package/dist/clis/chaoxing/exams.js +1 -1
  89. package/dist/{chaoxing.d.ts → clis/chaoxing/utils.d.ts} +1 -1
  90. package/dist/{chaoxing.js → clis/chaoxing/utils.js} +0 -2
  91. package/dist/clis/chaoxing/utils.test.d.ts +1 -0
  92. package/dist/{chaoxing.test.js → clis/chaoxing/utils.test.js} +1 -1
  93. package/dist/clis/chatgpt/read.js +1 -1
  94. package/dist/clis/chatwise/export.js +1 -1
  95. package/dist/clis/chatwise/model.js +2 -2
  96. package/dist/clis/chatwise/screenshot.js +1 -1
  97. package/dist/clis/codex/export.js +1 -1
  98. package/dist/clis/codex/model.js +2 -2
  99. package/dist/clis/codex/screenshot.js +1 -1
  100. package/dist/clis/coupang/add-to-cart.js +3 -4
  101. package/dist/clis/coupang/search.js +2 -4
  102. package/dist/clis/coupang/utils.test.d.ts +1 -0
  103. package/dist/{coupang.test.js → clis/coupang/utils.test.js} +1 -1
  104. package/dist/clis/ctrip/search.js +1 -1
  105. package/dist/clis/cursor/export.js +1 -1
  106. package/dist/clis/cursor/model.js +2 -2
  107. package/dist/clis/cursor/screenshot.js +1 -1
  108. package/dist/clis/devto/tag.yaml +34 -0
  109. package/dist/clis/devto/top.yaml +29 -0
  110. package/dist/clis/devto/user.yaml +33 -0
  111. package/dist/clis/douban/book-hot.d.ts +1 -0
  112. package/dist/clis/douban/book-hot.js +14 -0
  113. package/dist/clis/douban/marks.d.ts +1 -0
  114. package/dist/clis/douban/marks.js +115 -0
  115. package/dist/clis/douban/movie-hot.d.ts +1 -0
  116. package/dist/clis/douban/movie-hot.js +14 -0
  117. package/dist/clis/douban/reviews.d.ts +1 -0
  118. package/dist/clis/douban/reviews.js +106 -0
  119. package/dist/clis/douban/search.d.ts +1 -0
  120. package/dist/clis/douban/search.js +16 -0
  121. package/dist/clis/douban/shared.d.ts +4 -0
  122. package/dist/clis/douban/shared.js +155 -0
  123. package/dist/clis/douban/subject.yaml +76 -0
  124. package/dist/clis/douban/top250.yaml +70 -0
  125. package/dist/clis/douban/utils.d.ts +35 -0
  126. package/dist/clis/douban/utils.js +48 -0
  127. package/dist/clis/facebook/add-friend.yaml +43 -0
  128. package/dist/clis/facebook/events.yaml +44 -0
  129. package/dist/clis/facebook/feed.yaml +63 -0
  130. package/dist/clis/facebook/friends.yaml +42 -0
  131. package/dist/clis/facebook/groups.yaml +50 -0
  132. package/dist/clis/facebook/join-group.yaml +44 -0
  133. package/dist/clis/facebook/memories.yaml +39 -0
  134. package/dist/clis/facebook/notifications.yaml +40 -0
  135. package/dist/clis/facebook/profile.yaml +37 -0
  136. package/dist/clis/facebook/search.yaml +46 -0
  137. package/dist/clis/google/news.d.ts +5 -0
  138. package/dist/clis/google/news.js +58 -0
  139. package/dist/clis/google/search.d.ts +10 -0
  140. package/dist/clis/google/search.js +127 -0
  141. package/dist/clis/google/suggest.d.ts +5 -0
  142. package/dist/clis/google/suggest.js +34 -0
  143. package/dist/clis/google/trends.d.ts +5 -0
  144. package/dist/clis/google/trends.js +38 -0
  145. package/dist/clis/google/utils.d.ts +9 -0
  146. package/dist/clis/google/utils.js +23 -0
  147. package/dist/clis/google/utils.test.d.ts +1 -0
  148. package/dist/clis/google/utils.test.js +75 -0
  149. package/dist/clis/grok/ask.d.ts +14 -0
  150. package/dist/clis/grok/ask.js +257 -65
  151. package/dist/clis/grok/ask.test.d.ts +1 -0
  152. package/dist/clis/grok/ask.test.js +36 -0
  153. package/dist/clis/instagram/comment.yaml +52 -0
  154. package/dist/clis/instagram/explore.yaml +43 -0
  155. package/dist/clis/instagram/follow.yaml +41 -0
  156. package/dist/clis/instagram/followers.yaml +51 -0
  157. package/dist/clis/instagram/following.yaml +51 -0
  158. package/dist/clis/instagram/like.yaml +46 -0
  159. package/dist/clis/instagram/profile.yaml +42 -0
  160. package/dist/clis/instagram/save.yaml +46 -0
  161. package/dist/clis/instagram/saved.yaml +40 -0
  162. package/dist/clis/instagram/search.yaml +43 -0
  163. package/dist/clis/instagram/unfollow.yaml +38 -0
  164. package/dist/clis/instagram/unlike.yaml +46 -0
  165. package/dist/clis/instagram/unsave.yaml +46 -0
  166. package/dist/clis/instagram/user.yaml +54 -0
  167. package/dist/clis/jike/comment.js +2 -3
  168. package/dist/clis/jike/create.js +1 -2
  169. package/dist/clis/jike/feed.js +0 -1
  170. package/dist/clis/jike/like.js +1 -2
  171. package/dist/clis/jike/notifications.js +0 -1
  172. package/dist/clis/jike/post.yaml +1 -0
  173. package/dist/clis/jike/repost.js +2 -3
  174. package/dist/clis/jike/search.js +2 -3
  175. package/dist/clis/jike/topic.yaml +1 -0
  176. package/dist/clis/jike/user.yaml +1 -0
  177. package/dist/clis/jimeng/generate.yaml +1 -0
  178. package/dist/clis/jimeng/history.yaml +0 -1
  179. package/dist/clis/linkedin/search.js +7 -7
  180. package/dist/clis/linux-do/category.yaml +2 -0
  181. package/dist/clis/linux-do/search.yaml +4 -3
  182. package/dist/clis/linux-do/topic.yaml +1 -0
  183. package/dist/clis/lobsters/active.yaml +29 -0
  184. package/dist/clis/lobsters/hot.yaml +29 -0
  185. package/dist/clis/lobsters/newest.yaml +29 -0
  186. package/dist/clis/lobsters/tag.yaml +34 -0
  187. package/dist/clis/medium/feed.d.ts +1 -0
  188. package/dist/clis/medium/feed.js +15 -0
  189. package/dist/clis/medium/search.d.ts +1 -0
  190. package/dist/clis/medium/search.js +15 -0
  191. package/dist/clis/medium/shared.d.ts +5 -0
  192. package/dist/clis/medium/shared.js +78 -0
  193. package/dist/clis/medium/user.d.ts +1 -0
  194. package/dist/clis/medium/user.js +15 -0
  195. package/dist/clis/notion/export.js +1 -1
  196. package/dist/clis/reddit/comment.js +3 -4
  197. package/dist/clis/reddit/read.js +4 -5
  198. package/dist/clis/reddit/save.js +2 -3
  199. package/dist/clis/reddit/saved.js +0 -1
  200. package/dist/clis/reddit/search.yaml +1 -0
  201. package/dist/clis/reddit/subreddit.yaml +1 -0
  202. package/dist/clis/reddit/subscribe.js +1 -2
  203. package/dist/clis/reddit/upvote.js +2 -3
  204. package/dist/clis/reddit/upvoted.js +0 -1
  205. package/dist/clis/reddit/user-comments.yaml +1 -0
  206. package/dist/clis/reddit/user-posts.yaml +1 -0
  207. package/dist/clis/reddit/user.yaml +1 -0
  208. package/dist/clis/reuters/search.js +1 -1
  209. package/dist/clis/sinablog/article.d.ts +1 -0
  210. package/dist/clis/sinablog/article.js +14 -0
  211. package/dist/clis/sinablog/hot.d.ts +1 -0
  212. package/dist/clis/sinablog/hot.js +14 -0
  213. package/dist/clis/sinablog/search.d.ts +1 -0
  214. package/dist/clis/sinablog/search.js +51 -0
  215. package/dist/clis/sinablog/shared.d.ts +7 -0
  216. package/dist/clis/sinablog/shared.js +187 -0
  217. package/dist/clis/sinablog/user.d.ts +1 -0
  218. package/dist/clis/sinablog/user.js +15 -0
  219. package/dist/clis/smzdm/search.js +2 -3
  220. package/dist/clis/stackoverflow/search.yaml +1 -0
  221. package/dist/clis/steam/top-sellers.yaml +29 -0
  222. package/dist/clis/substack/feed.d.ts +1 -0
  223. package/dist/clis/substack/feed.js +15 -0
  224. package/dist/clis/substack/publication.d.ts +1 -0
  225. package/dist/clis/substack/publication.js +15 -0
  226. package/dist/clis/substack/search.d.ts +1 -0
  227. package/dist/clis/substack/search.js +77 -0
  228. package/dist/clis/substack/shared.d.ts +4 -0
  229. package/dist/clis/substack/shared.js +129 -0
  230. package/dist/clis/tiktok/comment.yaml +66 -0
  231. package/dist/clis/tiktok/explore.yaml +39 -0
  232. package/dist/clis/tiktok/follow.yaml +39 -0
  233. package/dist/clis/tiktok/following.yaml +46 -0
  234. package/dist/clis/tiktok/friends.yaml +47 -0
  235. package/dist/clis/tiktok/like.yaml +38 -0
  236. package/dist/clis/tiktok/live.yaml +51 -0
  237. package/dist/clis/tiktok/notifications.yaml +52 -0
  238. package/dist/clis/tiktok/profile.yaml +45 -0
  239. package/dist/clis/tiktok/save.yaml +34 -0
  240. package/dist/clis/tiktok/search.yaml +46 -0
  241. package/dist/clis/tiktok/unfollow.yaml +44 -0
  242. package/dist/clis/tiktok/unlike.yaml +38 -0
  243. package/dist/clis/tiktok/unsave.yaml +36 -0
  244. package/dist/clis/tiktok/user.yaml +44 -0
  245. package/dist/clis/twitter/accept.js +2 -2
  246. package/dist/clis/twitter/article.js +2 -2
  247. package/dist/clis/twitter/block.d.ts +1 -0
  248. package/dist/clis/twitter/block.js +88 -0
  249. package/dist/clis/twitter/delete.js +1 -1
  250. package/dist/clis/twitter/download.d.ts +1 -1
  251. package/dist/clis/twitter/download.js +3 -3
  252. package/dist/clis/twitter/followers.js +1 -1
  253. package/dist/clis/twitter/following.js +1 -1
  254. package/dist/clis/twitter/hide-reply.d.ts +1 -0
  255. package/dist/clis/twitter/hide-reply.js +66 -0
  256. package/dist/clis/twitter/like.js +1 -1
  257. package/dist/clis/twitter/post.js +1 -1
  258. package/dist/clis/twitter/reply-dm.js +1 -1
  259. package/dist/clis/twitter/reply.js +2 -2
  260. package/dist/clis/twitter/search.js +1 -1
  261. package/dist/clis/twitter/thread.js +2 -2
  262. package/dist/clis/twitter/timeline.d.ts +23 -0
  263. package/dist/clis/twitter/timeline.js +42 -14
  264. package/dist/clis/twitter/timeline.test.d.ts +1 -0
  265. package/dist/clis/twitter/timeline.test.js +102 -0
  266. package/dist/clis/twitter/trending.d.ts +1 -0
  267. package/dist/clis/twitter/trending.js +91 -0
  268. package/dist/clis/twitter/unblock.d.ts +1 -0
  269. package/dist/clis/twitter/unblock.js +71 -0
  270. package/dist/clis/v2ex/topic.yaml +1 -0
  271. package/dist/clis/weibo/hot.js +0 -1
  272. package/dist/clis/weread/book.js +1 -1
  273. package/dist/clis/weread/highlights.js +1 -1
  274. package/dist/clis/weread/notes.js +1 -1
  275. package/dist/clis/weread/search.js +1 -1
  276. package/dist/clis/wikipedia/random.d.ts +1 -0
  277. package/dist/clis/wikipedia/random.js +19 -0
  278. package/dist/clis/wikipedia/search.js +4 -4
  279. package/dist/clis/wikipedia/summary.js +4 -9
  280. package/dist/clis/wikipedia/trending.d.ts +1 -0
  281. package/dist/clis/wikipedia/trending.js +35 -0
  282. package/dist/clis/wikipedia/utils.d.ts +28 -0
  283. package/dist/clis/wikipedia/utils.js +13 -0
  284. package/dist/clis/xiaohongshu/creator-note-detail.d.ts +15 -0
  285. package/dist/clis/xiaohongshu/creator-note-detail.js +69 -5
  286. package/dist/clis/xiaohongshu/creator-note-detail.test.js +82 -33
  287. package/dist/clis/xiaohongshu/creator-notes.js +35 -5
  288. package/dist/clis/xiaohongshu/creator-notes.test.js +37 -6
  289. package/dist/clis/xiaohongshu/creator-profile.js +0 -1
  290. package/dist/clis/xiaohongshu/creator-stats.js +0 -1
  291. package/dist/clis/xiaohongshu/download.js +2 -3
  292. package/dist/clis/xiaohongshu/feed.yaml +0 -1
  293. package/dist/clis/xiaohongshu/notifications.yaml +0 -1
  294. package/dist/clis/xiaohongshu/search.js +2 -2
  295. package/dist/clis/xiaohongshu/user.js +1 -2
  296. package/dist/clis/xueqiu/earnings-date.yaml +69 -0
  297. package/dist/clis/xueqiu/search.yaml +2 -1
  298. package/dist/clis/xueqiu/stock.yaml +2 -0
  299. package/dist/clis/yahoo-finance/quote.js +1 -2
  300. package/dist/clis/youtube/search.js +1 -1
  301. package/dist/clis/youtube/transcript.js +1 -1
  302. package/dist/clis/youtube/video.js +1 -1
  303. package/dist/clis/zhihu/download.js +1 -2
  304. package/dist/clis/zhihu/question.js +1 -1
  305. package/dist/clis/zhihu/search.yaml +4 -3
  306. package/dist/commanderAdapter.d.ts +21 -0
  307. package/dist/commanderAdapter.js +117 -0
  308. package/dist/{engine.d.ts → discovery.d.ts} +6 -4
  309. package/dist/{engine.js → discovery.js} +93 -104
  310. package/dist/doctor.js +3 -1
  311. package/dist/doctor.test.js +46 -2
  312. package/dist/download/index.d.ts +2 -6
  313. package/dist/download/index.js +19 -46
  314. package/dist/engine.test.d.ts +0 -3
  315. package/dist/engine.test.js +80 -11
  316. package/dist/execution.d.ts +24 -0
  317. package/dist/execution.js +153 -0
  318. package/dist/explore.d.ts +76 -3
  319. package/dist/explore.js +132 -111
  320. package/dist/external-clis.yaml +48 -0
  321. package/dist/external.d.ts +7 -2
  322. package/dist/external.js +11 -14
  323. package/dist/generate.d.ts +41 -2
  324. package/dist/generate.js +5 -4
  325. package/dist/main.js +2 -1
  326. package/dist/pipeline/executor.d.ts +2 -2
  327. package/dist/pipeline/executor.js +2 -2
  328. package/dist/pipeline/executor.test.js +33 -6
  329. package/dist/pipeline/registry.d.ts +1 -1
  330. package/dist/pipeline/steps/browser.d.ts +7 -7
  331. package/dist/pipeline/steps/browser.js +21 -7
  332. package/dist/pipeline/steps/fetch.d.ts +1 -1
  333. package/dist/pipeline/steps/fetch.js +11 -7
  334. package/dist/pipeline/steps/transform.d.ts +6 -5
  335. package/dist/pipeline/steps/transform.js +30 -9
  336. package/dist/pipeline/template.d.ts +6 -6
  337. package/dist/pipeline/template.js +43 -5
  338. package/dist/pipeline/template.test.js +18 -0
  339. package/dist/pipeline/transform.test.js +11 -0
  340. package/dist/plugin.d.ts +31 -0
  341. package/dist/plugin.js +216 -0
  342. package/dist/plugin.test.d.ts +4 -0
  343. package/dist/plugin.test.js +76 -0
  344. package/dist/registry-api.d.ts +11 -0
  345. package/dist/registry-api.js +9 -0
  346. package/dist/registry.d.ts +13 -0
  347. package/dist/registry.js +8 -1
  348. package/dist/runtime.d.ts +5 -0
  349. package/dist/runtime.js +8 -0
  350. package/dist/serialization.d.ts +34 -0
  351. package/dist/serialization.js +63 -0
  352. package/dist/synthesize.d.ts +94 -4
  353. package/dist/synthesize.js +5 -4
  354. package/dist/types.d.ts +43 -27
  355. package/dist/validate.js +8 -2
  356. package/docs/.vitepress/config.mts +20 -7
  357. package/docs/adapters/browser/arxiv.md +27 -0
  358. package/docs/adapters/browser/barchart.md +33 -0
  359. package/docs/adapters/browser/bilibili.md +9 -0
  360. package/docs/adapters/browser/bloomberg.md +70 -0
  361. package/docs/adapters/browser/chaoxing.md +39 -0
  362. package/docs/adapters/browser/devto.md +35 -0
  363. package/docs/adapters/browser/douban.md +38 -0
  364. package/docs/adapters/browser/facebook.md +36 -0
  365. package/docs/adapters/browser/google.md +62 -0
  366. package/docs/adapters/browser/grok.md +53 -0
  367. package/docs/adapters/browser/hf.md +42 -0
  368. package/docs/adapters/browser/instagram.md +46 -0
  369. package/docs/adapters/browser/jike.md +45 -0
  370. package/docs/adapters/browser/jimeng.md +39 -0
  371. package/docs/adapters/browser/linux-do.md +45 -0
  372. package/docs/adapters/browser/lobsters.md +32 -0
  373. package/docs/adapters/browser/medium.md +32 -0
  374. package/docs/adapters/browser/reddit.md +9 -0
  375. package/docs/adapters/browser/sinablog.md +36 -0
  376. package/docs/adapters/browser/sinafinance.md +35 -0
  377. package/docs/adapters/browser/stackoverflow.md +35 -0
  378. package/docs/adapters/browser/steam.md +26 -0
  379. package/docs/adapters/browser/substack.md +38 -0
  380. package/docs/adapters/browser/tiktok.md +68 -0
  381. package/docs/adapters/browser/twitter.md +3 -0
  382. package/docs/adapters/browser/weread.md +48 -0
  383. package/docs/adapters/browser/wikipedia.md +39 -0
  384. package/docs/adapters/browser/xiaohongshu.md +5 -1
  385. package/docs/adapters/browser/xueqiu.md +10 -0
  386. package/docs/adapters/browser/yahoo-finance.md +6 -5
  387. package/docs/adapters/desktop/antigravity.md +6 -0
  388. package/docs/adapters/desktop/chatgpt.md +5 -4
  389. package/docs/adapters/desktop/codex.md +5 -1
  390. package/docs/adapters/desktop/cursor.md +4 -0
  391. package/docs/adapters/desktop/discord.md +7 -7
  392. package/docs/adapters/index.md +14 -4
  393. package/docs/advanced/download.md +4 -4
  394. package/docs/developer/architecture.md +17 -4
  395. package/docs/guide/getting-started.md +1 -0
  396. package/docs/guide/plugins.md +153 -0
  397. package/docs/zh/guide/plugins.md +107 -0
  398. package/extension/src/background.ts +18 -11
  399. package/package.json +10 -5
  400. package/scripts/check-doc-coverage.sh +69 -0
  401. package/scripts/clean-dist.cjs +13 -0
  402. package/scripts/copy-yaml.cjs +7 -0
  403. package/src/browser/cdp.ts +77 -32
  404. package/src/browser/daemon-client.ts +2 -1
  405. package/src/browser/dom-helpers.ts +38 -7
  406. package/src/browser/dom-snapshot.test.ts +249 -0
  407. package/src/browser/dom-snapshot.ts +770 -0
  408. package/src/browser/index.ts +2 -0
  409. package/src/browser/page.ts +57 -20
  410. package/src/build-manifest.test.ts +70 -2
  411. package/src/build-manifest.ts +114 -40
  412. package/src/cli.ts +287 -139
  413. package/src/clis/apple-podcasts/commands.test.ts +95 -0
  414. package/src/clis/apple-podcasts/search.ts +2 -2
  415. package/src/clis/apple-podcasts/top.ts +12 -2
  416. package/src/clis/arxiv/search.ts +1 -1
  417. package/src/clis/barchart/greeks.ts +1 -1
  418. package/src/clis/barchart/options.ts +1 -1
  419. package/src/clis/barchart/quote.ts +1 -1
  420. package/src/clis/bilibili/download.ts +1 -1
  421. package/src/clis/bilibili/dynamic.ts +1 -1
  422. package/src/clis/bilibili/favorite.ts +1 -1
  423. package/src/clis/bilibili/feed.ts +1 -1
  424. package/src/clis/bilibili/following.ts +2 -2
  425. package/src/clis/bilibili/history.ts +1 -1
  426. package/src/clis/bilibili/me.ts +1 -1
  427. package/src/clis/bilibili/ranking.ts +1 -1
  428. package/src/clis/bilibili/search.ts +3 -3
  429. package/src/clis/bilibili/subtitle.ts +2 -2
  430. package/src/clis/bilibili/user-videos.ts +2 -2
  431. package/src/{bilibili.ts → clis/bilibili/utils.ts} +1 -1
  432. package/src/clis/bloomberg/businessweek.ts +18 -0
  433. package/src/clis/bloomberg/economics.ts +18 -0
  434. package/src/clis/bloomberg/feeds.ts +16 -0
  435. package/src/clis/bloomberg/industries.ts +18 -0
  436. package/src/clis/bloomberg/main.ts +18 -0
  437. package/src/clis/bloomberg/markets.ts +18 -0
  438. package/src/clis/bloomberg/news.ts +136 -0
  439. package/src/clis/bloomberg/opinions.ts +18 -0
  440. package/src/clis/bloomberg/politics.ts +18 -0
  441. package/src/clis/bloomberg/tech.ts +18 -0
  442. package/src/clis/bloomberg/utils.test.ts +135 -0
  443. package/src/clis/bloomberg/utils.ts +429 -0
  444. package/src/clis/boss/batchgreet.ts +16 -108
  445. package/src/clis/boss/chatlist.ts +13 -27
  446. package/src/clis/boss/chatmsg.ts +16 -40
  447. package/src/clis/boss/common.ts +287 -0
  448. package/src/clis/boss/detail.ts +9 -55
  449. package/src/clis/boss/exchange.ts +15 -89
  450. package/src/clis/boss/greet.ts +25 -162
  451. package/src/clis/boss/invite.ts +36 -133
  452. package/src/clis/boss/joblist.ts +7 -36
  453. package/src/clis/boss/mark.ts +13 -94
  454. package/src/clis/boss/recommend.ts +12 -57
  455. package/src/clis/boss/resume.ts +19 -124
  456. package/src/clis/boss/search.ts +14 -67
  457. package/src/clis/boss/send.ts +22 -162
  458. package/src/clis/boss/stats.ts +21 -76
  459. package/src/clis/chaoxing/assignments.ts +1 -1
  460. package/src/clis/chaoxing/exams.ts +1 -1
  461. package/src/{chaoxing.test.ts → clis/chaoxing/utils.test.ts} +1 -1
  462. package/src/{chaoxing.ts → clis/chaoxing/utils.ts} +1 -3
  463. package/src/clis/chatgpt/read.ts +1 -1
  464. package/src/clis/chatwise/export.ts +1 -1
  465. package/src/clis/chatwise/model.ts +2 -2
  466. package/src/clis/chatwise/screenshot.ts +1 -1
  467. package/src/clis/codex/export.ts +1 -1
  468. package/src/clis/codex/model.ts +2 -2
  469. package/src/clis/codex/screenshot.ts +1 -1
  470. package/src/clis/coupang/add-to-cart.ts +3 -4
  471. package/src/clis/coupang/search.ts +2 -4
  472. package/src/{coupang.test.ts → clis/coupang/utils.test.ts} +1 -1
  473. package/src/clis/ctrip/search.ts +1 -1
  474. package/src/clis/cursor/export.ts +1 -1
  475. package/src/clis/cursor/model.ts +2 -2
  476. package/src/clis/cursor/screenshot.ts +1 -1
  477. package/src/clis/devto/tag.yaml +34 -0
  478. package/src/clis/devto/top.yaml +29 -0
  479. package/src/clis/devto/user.yaml +33 -0
  480. package/src/clis/douban/book-hot.ts +15 -0
  481. package/src/clis/douban/marks.ts +135 -0
  482. package/src/clis/douban/movie-hot.ts +15 -0
  483. package/src/clis/douban/reviews.ts +127 -0
  484. package/src/clis/douban/search.ts +17 -0
  485. package/src/clis/douban/shared.ts +165 -0
  486. package/src/clis/douban/subject.yaml +76 -0
  487. package/src/clis/douban/top250.yaml +70 -0
  488. package/src/clis/douban/utils.ts +81 -0
  489. package/src/clis/facebook/add-friend.yaml +43 -0
  490. package/src/clis/facebook/events.yaml +44 -0
  491. package/src/clis/facebook/feed.yaml +63 -0
  492. package/src/clis/facebook/friends.yaml +42 -0
  493. package/src/clis/facebook/groups.yaml +50 -0
  494. package/src/clis/facebook/join-group.yaml +44 -0
  495. package/src/clis/facebook/memories.yaml +39 -0
  496. package/src/clis/facebook/notifications.yaml +40 -0
  497. package/src/clis/facebook/profile.yaml +37 -0
  498. package/src/clis/facebook/search.yaml +46 -0
  499. package/src/clis/google/news.ts +66 -0
  500. package/src/clis/google/search.ts +133 -0
  501. package/src/clis/google/suggest.ts +40 -0
  502. package/src/clis/google/trends.ts +44 -0
  503. package/src/clis/google/utils.test.ts +82 -0
  504. package/src/clis/google/utils.ts +24 -0
  505. package/src/clis/grok/ask.test.ts +53 -0
  506. package/src/clis/grok/ask.ts +300 -69
  507. package/src/clis/instagram/comment.yaml +52 -0
  508. package/src/clis/instagram/explore.yaml +43 -0
  509. package/src/clis/instagram/follow.yaml +41 -0
  510. package/src/clis/instagram/followers.yaml +51 -0
  511. package/src/clis/instagram/following.yaml +51 -0
  512. package/src/clis/instagram/like.yaml +46 -0
  513. package/src/clis/instagram/profile.yaml +42 -0
  514. package/src/clis/instagram/save.yaml +46 -0
  515. package/src/clis/instagram/saved.yaml +40 -0
  516. package/src/clis/instagram/search.yaml +43 -0
  517. package/src/clis/instagram/unfollow.yaml +38 -0
  518. package/src/clis/instagram/unlike.yaml +46 -0
  519. package/src/clis/instagram/unsave.yaml +46 -0
  520. package/src/clis/instagram/user.yaml +54 -0
  521. package/src/clis/jike/comment.ts +2 -3
  522. package/src/clis/jike/create.ts +1 -2
  523. package/src/clis/jike/feed.ts +0 -1
  524. package/src/clis/jike/like.ts +1 -2
  525. package/src/clis/jike/notifications.ts +0 -1
  526. package/src/clis/jike/post.yaml +1 -0
  527. package/src/clis/jike/repost.ts +2 -3
  528. package/src/clis/jike/search.ts +2 -3
  529. package/src/clis/jike/topic.yaml +1 -0
  530. package/src/clis/jike/user.yaml +1 -0
  531. package/src/clis/jimeng/generate.yaml +1 -0
  532. package/src/clis/jimeng/history.yaml +0 -1
  533. package/src/clis/linkedin/search.ts +7 -7
  534. package/src/clis/linux-do/category.yaml +2 -0
  535. package/src/clis/linux-do/search.yaml +4 -3
  536. package/src/clis/linux-do/topic.yaml +1 -0
  537. package/src/clis/lobsters/active.yaml +29 -0
  538. package/src/clis/lobsters/hot.yaml +29 -0
  539. package/src/clis/lobsters/newest.yaml +29 -0
  540. package/src/clis/lobsters/tag.yaml +34 -0
  541. package/src/clis/medium/feed.ts +16 -0
  542. package/src/clis/medium/search.ts +16 -0
  543. package/src/clis/medium/shared.ts +83 -0
  544. package/src/clis/medium/user.ts +16 -0
  545. package/src/clis/notion/export.ts +1 -1
  546. package/src/clis/reddit/comment.ts +3 -4
  547. package/src/clis/reddit/read.ts +4 -5
  548. package/src/clis/reddit/save.ts +2 -3
  549. package/src/clis/reddit/saved.ts +0 -1
  550. package/src/clis/reddit/search.yaml +1 -0
  551. package/src/clis/reddit/subreddit.yaml +1 -0
  552. package/src/clis/reddit/subscribe.ts +1 -2
  553. package/src/clis/reddit/upvote.ts +2 -3
  554. package/src/clis/reddit/upvoted.ts +0 -1
  555. package/src/clis/reddit/user-comments.yaml +1 -0
  556. package/src/clis/reddit/user-posts.yaml +1 -0
  557. package/src/clis/reddit/user.yaml +1 -0
  558. package/src/clis/reuters/search.ts +1 -1
  559. package/src/clis/sinablog/article.ts +15 -0
  560. package/src/clis/sinablog/hot.ts +15 -0
  561. package/src/clis/sinablog/search.ts +56 -0
  562. package/src/clis/sinablog/shared.ts +198 -0
  563. package/src/clis/sinablog/user.ts +16 -0
  564. package/src/clis/smzdm/search.ts +2 -3
  565. package/src/clis/stackoverflow/search.yaml +1 -0
  566. package/src/clis/steam/top-sellers.yaml +29 -0
  567. package/src/clis/substack/feed.ts +16 -0
  568. package/src/clis/substack/publication.ts +16 -0
  569. package/src/clis/substack/search.ts +91 -0
  570. package/src/clis/substack/shared.ts +132 -0
  571. package/src/clis/tiktok/comment.yaml +66 -0
  572. package/src/clis/tiktok/explore.yaml +39 -0
  573. package/src/clis/tiktok/follow.yaml +39 -0
  574. package/src/clis/tiktok/following.yaml +46 -0
  575. package/src/clis/tiktok/friends.yaml +47 -0
  576. package/src/clis/tiktok/like.yaml +38 -0
  577. package/src/clis/tiktok/live.yaml +51 -0
  578. package/src/clis/tiktok/notifications.yaml +52 -0
  579. package/src/clis/tiktok/profile.yaml +45 -0
  580. package/src/clis/tiktok/save.yaml +34 -0
  581. package/src/clis/tiktok/search.yaml +46 -0
  582. package/src/clis/tiktok/unfollow.yaml +44 -0
  583. package/src/clis/tiktok/unlike.yaml +38 -0
  584. package/src/clis/tiktok/unsave.yaml +36 -0
  585. package/src/clis/tiktok/user.yaml +44 -0
  586. package/src/clis/twitter/accept.ts +2 -2
  587. package/src/clis/twitter/article.ts +2 -2
  588. package/src/clis/twitter/block.ts +92 -0
  589. package/src/clis/twitter/delete.ts +1 -1
  590. package/src/clis/twitter/download.ts +3 -3
  591. package/src/clis/twitter/followers.ts +1 -1
  592. package/src/clis/twitter/following.ts +1 -1
  593. package/src/clis/twitter/hide-reply.ts +70 -0
  594. package/src/clis/twitter/like.ts +1 -1
  595. package/src/clis/twitter/post.ts +1 -1
  596. package/src/clis/twitter/reply-dm.ts +1 -1
  597. package/src/clis/twitter/reply.ts +2 -2
  598. package/src/clis/twitter/search.ts +1 -1
  599. package/src/clis/twitter/thread.ts +2 -2
  600. package/src/clis/twitter/timeline.test.ts +109 -0
  601. package/src/clis/twitter/timeline.ts +59 -19
  602. package/src/clis/twitter/trending.ts +113 -0
  603. package/src/clis/twitter/unblock.ts +75 -0
  604. package/src/clis/v2ex/topic.yaml +1 -0
  605. package/src/clis/weibo/hot.ts +0 -1
  606. package/src/clis/weread/book.ts +1 -1
  607. package/src/clis/weread/highlights.ts +1 -1
  608. package/src/clis/weread/notes.ts +1 -1
  609. package/src/clis/weread/search.ts +1 -1
  610. package/src/clis/wikipedia/random.ts +19 -0
  611. package/src/clis/wikipedia/search.ts +11 -5
  612. package/src/clis/wikipedia/summary.ts +4 -9
  613. package/src/clis/wikipedia/trending.ts +41 -0
  614. package/src/clis/wikipedia/utils.ts +31 -0
  615. package/src/clis/xiaohongshu/creator-note-detail.test.ts +84 -33
  616. package/src/clis/xiaohongshu/creator-note-detail.ts +89 -5
  617. package/src/clis/xiaohongshu/creator-notes.test.ts +41 -6
  618. package/src/clis/xiaohongshu/creator-notes.ts +44 -5
  619. package/src/clis/xiaohongshu/creator-profile.ts +0 -1
  620. package/src/clis/xiaohongshu/creator-stats.ts +0 -1
  621. package/src/clis/xiaohongshu/download.ts +2 -3
  622. package/src/clis/xiaohongshu/feed.yaml +0 -1
  623. package/src/clis/xiaohongshu/notifications.yaml +0 -1
  624. package/src/clis/xiaohongshu/search.ts +2 -2
  625. package/src/clis/xiaohongshu/user.ts +1 -2
  626. package/src/clis/xueqiu/earnings-date.yaml +69 -0
  627. package/src/clis/xueqiu/search.yaml +2 -1
  628. package/src/clis/xueqiu/stock.yaml +2 -0
  629. package/src/clis/yahoo-finance/quote.ts +1 -2
  630. package/src/clis/youtube/search.ts +1 -1
  631. package/src/clis/youtube/transcript.ts +1 -1
  632. package/src/clis/youtube/video.ts +1 -1
  633. package/src/clis/zhihu/download.ts +1 -2
  634. package/src/clis/zhihu/question.ts +1 -1
  635. package/src/clis/zhihu/search.yaml +4 -3
  636. package/src/commanderAdapter.ts +120 -0
  637. package/src/discovery.ts +277 -0
  638. package/src/doctor.test.ts +59 -2
  639. package/src/doctor.ts +4 -2
  640. package/src/download/index.ts +21 -54
  641. package/src/engine.test.ts +85 -11
  642. package/src/execution.ts +164 -0
  643. package/src/explore.ts +211 -117
  644. package/src/external-clis.yaml +9 -0
  645. package/src/external.ts +15 -12
  646. package/src/generate.ts +58 -9
  647. package/src/main.ts +2 -1
  648. package/src/pipeline/executor.test.ts +35 -6
  649. package/src/pipeline/executor.ts +11 -7
  650. package/src/pipeline/registry.ts +3 -3
  651. package/src/pipeline/steps/browser.ts +29 -15
  652. package/src/pipeline/steps/fetch.ts +18 -13
  653. package/src/pipeline/steps/transform.ts +40 -15
  654. package/src/pipeline/template.test.ts +18 -0
  655. package/src/pipeline/template.ts +86 -13
  656. package/src/pipeline/transform.test.ts +15 -2
  657. package/src/plugin.test.ts +86 -0
  658. package/src/plugin.ts +254 -0
  659. package/src/registry-api.ts +12 -0
  660. package/src/registry.ts +24 -1
  661. package/src/runtime.ts +9 -0
  662. package/src/serialization.ts +79 -0
  663. package/src/synthesize.ts +102 -21
  664. package/src/types.ts +45 -13
  665. package/src/validate.ts +19 -4
  666. package/tests/e2e/browser-public.test.ts +36 -0
  667. package/tests/e2e/public-commands.test.ts +119 -1
  668. package/dist/clis/feishu/new.d.ts +0 -1
  669. package/dist/clis/feishu/new.js +0 -27
  670. package/dist/clis/feishu/read.d.ts +0 -1
  671. package/dist/clis/feishu/read.js +0 -40
  672. package/dist/clis/feishu/search.d.ts +0 -1
  673. package/dist/clis/feishu/search.js +0 -30
  674. package/dist/clis/feishu/send.d.ts +0 -1
  675. package/dist/clis/feishu/send.js +0 -39
  676. package/dist/clis/feishu/status.d.ts +0 -1
  677. package/dist/clis/feishu/status.js +0 -28
  678. package/dist/clis/neteasemusic/like.d.ts +0 -1
  679. package/dist/clis/neteasemusic/like.js +0 -25
  680. package/dist/clis/neteasemusic/lyrics.d.ts +0 -1
  681. package/dist/clis/neteasemusic/lyrics.js +0 -47
  682. package/dist/clis/neteasemusic/next.d.ts +0 -1
  683. package/dist/clis/neteasemusic/next.js +0 -26
  684. package/dist/clis/neteasemusic/play.d.ts +0 -1
  685. package/dist/clis/neteasemusic/play.js +0 -26
  686. package/dist/clis/neteasemusic/playing.d.ts +0 -1
  687. package/dist/clis/neteasemusic/playing.js +0 -59
  688. package/dist/clis/neteasemusic/playlist.d.ts +0 -1
  689. package/dist/clis/neteasemusic/playlist.js +0 -46
  690. package/dist/clis/neteasemusic/prev.d.ts +0 -1
  691. package/dist/clis/neteasemusic/prev.js +0 -25
  692. package/dist/clis/neteasemusic/search.d.ts +0 -1
  693. package/dist/clis/neteasemusic/search.js +0 -52
  694. package/dist/clis/neteasemusic/status.d.ts +0 -1
  695. package/dist/clis/neteasemusic/status.js +0 -16
  696. package/dist/clis/neteasemusic/volume.d.ts +0 -1
  697. package/dist/clis/neteasemusic/volume.js +0 -54
  698. package/dist/clis/twitter/trending.yaml +0 -46
  699. package/dist/clis/wechat/chats.d.ts +0 -1
  700. package/dist/clis/wechat/chats.js +0 -28
  701. package/dist/clis/wechat/contacts.d.ts +0 -1
  702. package/dist/clis/wechat/contacts.js +0 -28
  703. package/dist/clis/wechat/read.d.ts +0 -1
  704. package/dist/clis/wechat/read.js +0 -58
  705. package/dist/clis/wechat/search.d.ts +0 -1
  706. package/dist/clis/wechat/search.js +0 -31
  707. package/dist/clis/wechat/send.d.ts +0 -1
  708. package/dist/clis/wechat/send.js +0 -42
  709. package/dist/clis/wechat/status.d.ts +0 -1
  710. package/dist/clis/wechat/status.js +0 -29
  711. package/dist/pipeline.d.ts +0 -7
  712. package/dist/pipeline.js +0 -7
  713. package/docs/adapters/browser/github.md +0 -26
  714. package/docs/adapters/desktop/feishu.md +0 -20
  715. package/docs/adapters/desktop/neteasemusic.md +0 -31
  716. package/docs/adapters/desktop/wechat.md +0 -28
  717. package/docs/public/CNAME +0 -1
  718. package/src/clis/antigravity/README.md +0 -5
  719. package/src/clis/antigravity/README.zh-CN.md +0 -51
  720. package/src/clis/chaoxing/README.md +0 -14
  721. package/src/clis/chaoxing/README.zh-CN.md +0 -35
  722. package/src/clis/chatgpt/README.md +0 -5
  723. package/src/clis/chatgpt/README.zh-CN.md +0 -44
  724. package/src/clis/chatwise/README.md +0 -5
  725. package/src/clis/chatwise/README.zh-CN.md +0 -38
  726. package/src/clis/codex/README.md +0 -5
  727. package/src/clis/codex/README.zh-CN.md +0 -33
  728. package/src/clis/cursor/README.md +0 -5
  729. package/src/clis/cursor/README.zh-CN.md +0 -33
  730. package/src/clis/discord-app/README.md +0 -5
  731. package/src/clis/discord-app/README.zh-CN.md +0 -28
  732. package/src/clis/feishu/README.md +0 -5
  733. package/src/clis/feishu/README.zh-CN.md +0 -20
  734. package/src/clis/feishu/new.ts +0 -32
  735. package/src/clis/feishu/read.ts +0 -48
  736. package/src/clis/feishu/search.ts +0 -35
  737. package/src/clis/feishu/send.ts +0 -46
  738. package/src/clis/feishu/status.ts +0 -34
  739. package/src/clis/neteasemusic/README.md +0 -5
  740. package/src/clis/neteasemusic/README.zh-CN.md +0 -31
  741. package/src/clis/neteasemusic/like.ts +0 -28
  742. package/src/clis/neteasemusic/lyrics.ts +0 -53
  743. package/src/clis/neteasemusic/next.ts +0 -30
  744. package/src/clis/neteasemusic/play.ts +0 -30
  745. package/src/clis/neteasemusic/playing.ts +0 -62
  746. package/src/clis/neteasemusic/playlist.ts +0 -51
  747. package/src/clis/neteasemusic/prev.ts +0 -29
  748. package/src/clis/neteasemusic/search.ts +0 -58
  749. package/src/clis/neteasemusic/status.ts +0 -18
  750. package/src/clis/neteasemusic/volume.ts +0 -61
  751. package/src/clis/notion/README.md +0 -5
  752. package/src/clis/notion/README.zh-CN.md +0 -29
  753. package/src/clis/twitter/trending.yaml +0 -46
  754. package/src/clis/wechat/README.md +0 -5
  755. package/src/clis/wechat/README.zh-CN.md +0 -28
  756. package/src/clis/wechat/chats.ts +0 -33
  757. package/src/clis/wechat/contacts.ts +0 -33
  758. package/src/clis/wechat/read.ts +0 -72
  759. package/src/clis/wechat/search.ts +0 -36
  760. package/src/clis/wechat/send.ts +0 -49
  761. package/src/clis/wechat/status.ts +0 -35
  762. package/src/engine.ts +0 -274
  763. package/src/pipeline.ts +0 -8
  764. /package/dist/{bilibili.js → clis/bilibili/utils.js} +0 -0
  765. /package/dist/{chaoxing.test.d.ts → clis/bloomberg/businessweek.d.ts} +0 -0
  766. /package/dist/{coupang.test.d.ts → clis/bloomberg/economics.d.ts} +0 -0
  767. /package/dist/{coupang.d.ts → clis/coupang/utils.d.ts} +0 -0
  768. /package/dist/{coupang.js → clis/coupang/utils.js} +0 -0
  769. /package/src/{coupang.ts → clis/coupang/utils.ts} +0 -0
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { describe, it, expect, vi } from 'vitest';
6
6
  import { executePipeline } from './index.js';
7
+ import { ConfigError } from '../errors.js';
7
8
  import type { IPage } from '../types.js';
8
9
 
9
10
  /** Create a minimal mock page for testing */
@@ -16,6 +17,7 @@ function createMockPage(overrides: Partial<IPage> = {}): IPage {
16
17
  click: vi.fn(),
17
18
  typeText: vi.fn(),
18
19
  pressKey: vi.fn(),
20
+ getFormState: vi.fn().mockResolvedValue({}),
19
21
  wait: vi.fn(),
20
22
  tabs: vi.fn().mockResolvedValue([]),
21
23
  closeTab: vi.fn(),
@@ -24,6 +26,7 @@ function createMockPage(overrides: Partial<IPage> = {}): IPage {
24
26
  networkRequests: vi.fn().mockResolvedValue([]),
25
27
  consoleMessages: vi.fn().mockResolvedValue(''),
26
28
  scroll: vi.fn(),
29
+ scrollTo: vi.fn(),
27
30
  autoScroll: vi.fn(),
28
31
  installInterceptor: vi.fn(),
29
32
  getInterceptedRequests: vi.fn().mockResolvedValue([]),
@@ -79,6 +82,32 @@ describe('executePipeline', () => {
79
82
  ]);
80
83
  });
81
84
 
85
+ it('runs inline select inside map step', async () => {
86
+ const page = createMockPage({
87
+ evaluate: vi.fn().mockResolvedValue({
88
+ posts: [
89
+ { title: 'First', rank: 1 },
90
+ { title: 'Second', rank: 2 },
91
+ ],
92
+ }),
93
+ });
94
+ const result = await executePipeline(page, [
95
+ { evaluate: 'test' },
96
+ {
97
+ map: {
98
+ select: 'posts',
99
+ title: '${{ item.title }}',
100
+ rank: '${{ item.rank }}',
101
+ },
102
+ },
103
+ ]);
104
+
105
+ expect(result).toEqual([
106
+ { title: 'First', rank: 1 },
107
+ { title: 'Second', rank: 2 },
108
+ ]);
109
+ });
110
+
82
111
  it('executes limit step', async () => {
83
112
  const page = createMockPage({
84
113
  evaluate: vi.fn().mockResolvedValue([1, 2, 3, 4, 5]),
@@ -120,13 +149,13 @@ describe('executePipeline', () => {
120
149
  expect(page.wait).toHaveBeenCalledWith(2);
121
150
  });
122
151
 
123
- it('handles unknown steps gracefully in debug mode', async () => {
124
- const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
125
- await executePipeline(null, [
152
+ it('fails fast on unknown steps', async () => {
153
+ await expect(executePipeline(null, [
154
+ { unknownStep: 'test' },
155
+ ], { debug: true })).rejects.toBeInstanceOf(ConfigError);
156
+ await expect(executePipeline(null, [
126
157
  { unknownStep: 'test' },
127
- ], { debug: true });
128
- expect(stderr).toHaveBeenCalledWith(expect.stringContaining('Unknown step'));
129
- stderr.mockRestore();
158
+ ], { debug: true })).rejects.toThrow('Unknown pipeline step "unknownStep"');
130
159
  });
131
160
 
132
161
  it('passes args through template rendering', async () => {
@@ -6,20 +6,21 @@ import chalk from 'chalk';
6
6
  import type { IPage } from '../types.js';
7
7
  import { getStep, type StepHandler } from './registry.js';
8
8
  import { log } from '../logger.js';
9
+ import { ConfigError } from '../errors.js';
9
10
 
10
11
  export interface PipelineContext {
11
- args?: Record<string, any>;
12
+ args?: Record<string, unknown>;
12
13
  debug?: boolean;
13
14
  }
14
15
 
15
16
  export async function executePipeline(
16
17
  page: IPage | null,
17
- pipeline: any[],
18
+ pipeline: unknown[],
18
19
  ctx: PipelineContext = {},
19
- ): Promise<any> {
20
+ ): Promise<unknown> {
20
21
  const args = ctx.args ?? {};
21
22
  const debug = ctx.debug ?? false;
22
- let data: any = null;
23
+ let data: unknown = null;
23
24
  const total = pipeline.length;
24
25
 
25
26
  for (let i = 0; i < pipeline.length; i++) {
@@ -32,7 +33,10 @@ export async function executePipeline(
32
33
  if (handler) {
33
34
  data = await handler(page, params, data, args);
34
35
  } else {
35
- if (debug) log.warn(`Unknown step: ${op}`);
36
+ throw new ConfigError(
37
+ `Unknown pipeline step "${op}" at index ${i}.`,
38
+ 'Check the YAML pipeline step name or register the custom step before execution.',
39
+ );
36
40
  }
37
41
 
38
42
  if (debug) debugStepResult(op, data);
@@ -41,7 +45,7 @@ export async function executePipeline(
41
45
  return data;
42
46
  }
43
47
 
44
- function debugStepStart(stepNum: number, total: number, op: string, params: any): void {
48
+ function debugStepStart(stepNum: number, total: number, op: string, params: unknown): void {
45
49
  let preview = '';
46
50
  if (typeof params === 'string') {
47
51
  preview = params.length <= 80 ? ` → ${params}` : ` → ${params.slice(0, 77)}...`;
@@ -51,7 +55,7 @@ function debugStepStart(stepNum: number, total: number, op: string, params: any)
51
55
  log.step(stepNum, total, op, preview);
52
56
  }
53
57
 
54
- function debugStepResult(op: string, data: any): void {
58
+ function debugStepResult(op: string, data: unknown): void {
55
59
  if (data === null || data === undefined) {
56
60
  log.stepResult('(no data)');
57
61
  } else if (Array.isArray(data)) {
@@ -18,11 +18,11 @@ import { stepDownload } from './steps/download.js';
18
18
  * TData is the type of the `data` state flowing into the step.
19
19
  * TResult is the expected return type.
20
20
  */
21
- export type StepHandler<TData = any, TResult = any> = (
21
+ export type StepHandler<TData = unknown, TResult = unknown, TParams = unknown> = (
22
22
  page: IPage | null,
23
- params: any,
23
+ params: TParams,
24
24
  data: TData,
25
- args: Record<string, any>
25
+ args: Record<string, unknown>
26
26
  ) => Promise<TResult>;
27
27
 
28
28
  const _stepRegistry = new Map<string, StepHandler>();
@@ -6,19 +6,28 @@
6
6
  import type { IPage } from '../../types.js';
7
7
  import { render } from '../template.js';
8
8
 
9
- export async function stepNavigate(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any> {
10
- const url = render(params, { args, data });
11
- await page!.goto(String(url));
9
+ function isRecord(value: unknown): value is Record<string, unknown> {
10
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
11
+ }
12
+
13
+ export async function stepNavigate(page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
14
+ if (isRecord(params) && 'url' in params) {
15
+ const url = String(render(params.url, { args, data }));
16
+ await page!.goto(url, { waitUntil: params.waitUntil as 'load' | 'none' | undefined, settleMs: typeof params.settleMs === 'number' ? params.settleMs : undefined });
17
+ } else {
18
+ const url = render(params, { args, data });
19
+ await page!.goto(String(url));
20
+ }
12
21
  return data;
13
22
  }
14
23
 
15
- export async function stepClick(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any> {
24
+ export async function stepClick(page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
16
25
  await page!.click(String(render(params, { args, data })).replace(/^@/, ''));
17
26
  return data;
18
27
  }
19
28
 
20
- export async function stepType(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any> {
21
- if (typeof params === 'object' && params) {
29
+ export async function stepType(page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
30
+ if (isRecord(params)) {
22
31
  const ref = String(render(params.ref ?? '', { args, data })).replace(/^@/, '');
23
32
  const text = String(render(params.text ?? '', { args, data }));
24
33
  await page!.typeText(ref, text);
@@ -27,32 +36,37 @@ export async function stepType(page: IPage | null, params: any, data: any, args:
27
36
  return data;
28
37
  }
29
38
 
30
- export async function stepWait(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any> {
39
+ export async function stepWait(page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
31
40
  if (typeof params === 'number') await page!.wait(params);
32
- else if (typeof params === 'object' && params) {
41
+ else if (isRecord(params)) {
33
42
  if ('text' in params) {
34
43
  await page!.wait({
35
44
  text: String(render(params.text, { args, data })),
36
- timeout: params.timeout
45
+ timeout: typeof params.timeout === 'number' ? params.timeout : undefined,
37
46
  });
38
47
  } else if ('time' in params) await page!.wait(Number(params.time));
39
48
  } else if (typeof params === 'string') await page!.wait(Number(render(params, { args, data })));
40
49
  return data;
41
50
  }
42
51
 
43
- export async function stepPress(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any> {
52
+ export async function stepPress(page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
44
53
  await page!.pressKey(String(render(params, { args, data })));
45
54
  return data;
46
55
  }
47
56
 
48
- export async function stepSnapshot(page: IPage | null, params: any, _data: any, _args: Record<string, any>): Promise<any> {
49
- const opts = (typeof params === 'object' && params) ? params : {};
50
- return page!.snapshot({ interactive: opts.interactive ?? false, compact: opts.compact ?? false, maxDepth: opts.max_depth, raw: opts.raw ?? false });
57
+ export async function stepSnapshot(page: IPage | null, params: unknown, _data: unknown, _args: Record<string, unknown>): Promise<unknown> {
58
+ const opts = isRecord(params) ? params : {};
59
+ return page!.snapshot({
60
+ interactive: typeof opts.interactive === 'boolean' ? opts.interactive : false,
61
+ compact: typeof opts.compact === 'boolean' ? opts.compact : false,
62
+ maxDepth: typeof opts.max_depth === 'number' ? opts.max_depth : undefined,
63
+ raw: typeof opts.raw === 'boolean' ? opts.raw : false,
64
+ });
51
65
  }
52
66
 
53
- export async function stepEvaluate(page: IPage | null, params: any, data: any, args: Record<string, any>): Promise<any> {
67
+ export async function stepEvaluate(page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
54
68
  const js = String(render(params, { args, data }));
55
- let result = await page!.evaluate(js);
69
+ let result: unknown = await page!.evaluate(js);
56
70
  // MCP may return JSON as a string — auto-parse it
57
71
  if (typeof result === 'string') {
58
72
  const trimmed = result.trim();
@@ -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 () => {