@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
@@ -0,0 +1,69 @@
1
+ site: xueqiu
2
+ name: earnings-date
3
+ description: 获取股票预计财报发布日期(公司大事)
4
+ domain: xueqiu.com
5
+ browser: true
6
+
7
+ args:
8
+ symbol:
9
+ positional: true
10
+ type: str
11
+ required: true
12
+ description: 股票代码,如 SH600519、SZ000858、00700
13
+ next:
14
+ type: bool
15
+ default: false
16
+ description: 仅返回最近一次未发布的财报日期
17
+ limit:
18
+ type: int
19
+ default: 10
20
+ description: 返回数量,默认 10
21
+
22
+ pipeline:
23
+ - navigate: https://xueqiu.com
24
+ - evaluate: |
25
+ (async () => {
26
+ const symbol = (${{ args.symbol | json }} || '').toUpperCase();
27
+ const onlyNext = ${{ args.next }};
28
+ if (!symbol) throw new Error('Missing argument: symbol');
29
+ const resp = await fetch(
30
+ `https://stock.xueqiu.com/v5/stock/screener/event/list.json?symbol=${encodeURIComponent(symbol)}&page=1&size=100`,
31
+ { credentials: 'include' }
32
+ );
33
+ if (!resp.ok) throw new Error('HTTP ' + resp.status + ' Hint: Not logged in?');
34
+ const d = await resp.json();
35
+ if (!d.data || !d.data.items) throw new Error('获取失败: ' + JSON.stringify(d));
36
+
37
+ // subtype 2 = 预计财报发布
38
+ let items = d.data.items.filter(item => item.subtype === 2);
39
+
40
+ const now = Date.now();
41
+ let results = items.map(item => {
42
+ const ts = item.timestamp;
43
+ const dateStr = ts ? new Date(ts).toISOString().split('T')[0] : null;
44
+ const isFuture = ts && ts > now;
45
+ return {
46
+ date: dateStr,
47
+ report: item.message,
48
+ status: isFuture ? '⏳ 未发布' : '✅ 已发布',
49
+ _ts: ts,
50
+ _future: isFuture
51
+ };
52
+ });
53
+
54
+ if (onlyNext) {
55
+ const future = results.filter(r => r._future).sort((a, b) => a._ts - b._ts);
56
+ results = future.length ? [future[0]] : [];
57
+ }
58
+
59
+ return results;
60
+ })()
61
+
62
+ - map:
63
+ date: ${{ item.date }}
64
+ report: ${{ item.report }}
65
+ status: ${{ item.status }}
66
+
67
+ - limit: ${{ args.limit }}
68
+
69
+ columns: [date, report, status]
@@ -6,7 +6,9 @@ browser: true
6
6
 
7
7
  args:
8
8
  query:
9
+ positional: true
9
10
  type: str
11
+ required: true
10
12
  description: 搜索关键词,如 茅台、AAPL、腾讯
11
13
  limit:
12
14
  type: int
@@ -19,7 +21,6 @@ pipeline:
19
21
  (async () => {
20
22
  const query = ${{ args.query | json }};
21
23
  const count = ${{ args.limit }};
22
- if (!query) throw new Error('Missing argument: query');
23
24
  const resp = await fetch(`https://xueqiu.com/stock/search.json?code=${encodeURIComponent(query)}&size=${count}`, {credentials: 'include'});
24
25
  if (!resp.ok) throw new Error('HTTP ' + resp.status + ' Hint: Not logged in?');
25
26
  const d = await resp.json();
@@ -6,7 +6,9 @@ browser: true
6
6
 
7
7
  args:
8
8
  symbol:
9
+ positional: true
9
10
  type: str
11
+ required: true
10
12
  description: 股票代码,如 SH600519、SZ000858、AAPL、00700
11
13
 
12
14
  pipeline:
@@ -11,7 +11,7 @@ cli({
11
11
  domain: 'finance.yahoo.com',
12
12
  strategy: Strategy.COOKIE,
13
13
  args: [
14
- { name: 'symbol', required: true, help: 'Stock ticker (e.g. AAPL, MSFT, TSLA)' },
14
+ { name: 'symbol', required: true, positional: true, help: 'Stock ticker (e.g. AAPL, MSFT, TSLA)' },
15
15
  ],
16
16
  columns: ['symbol', 'name', 'price', 'change', 'changePercent', 'open', 'high', 'low', 'volume', 'marketCap'],
17
17
  func: async (page, kwargs) => {
@@ -18,6 +18,10 @@ import { render as renderOutput } from './output.js';
18
18
  import { executeCommand } from './execution.js';
19
19
  import { CliError } from './errors.js';
20
20
 
21
+ function getErrorMessage(error: unknown): string {
22
+ return error instanceof Error ? error.message : String(error);
23
+ }
24
+
21
25
  /**
22
26
  * Register a single CliCommand as a Commander subcommand.
23
27
  */
@@ -46,12 +50,13 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
46
50
 
47
51
  subCmd.addHelpText('after', formatRegistryHelpText(cmd));
48
52
 
49
- subCmd.action(async (...actionArgs: any[]) => {
53
+ subCmd.action(async (...actionArgs: unknown[]) => {
50
54
  const actionOpts = actionArgs[positionalArgs.length] ?? {};
55
+ const optionsRecord = typeof actionOpts === 'object' && actionOpts !== null ? actionOpts as Record<string, unknown> : {};
51
56
  const startTime = Date.now();
52
57
 
53
58
  // ── Collect kwargs ──────────────────────────────────────────────────
54
- const kwargs: Record<string, any> = {};
59
+ const kwargs: Record<string, unknown> = {};
55
60
  for (let i = 0; i < positionalArgs.length; i++) {
56
61
  const v = actionArgs[i];
57
62
  if (v !== undefined) kwargs[positionalArgs[i].name] = v;
@@ -59,36 +64,38 @@ export function registerCommandToProgram(siteCmd: Command, cmd: CliCommand): voi
59
64
  for (const arg of cmd.args) {
60
65
  if (arg.positional) continue;
61
66
  const camelName = arg.name.replace(/-([a-z])/g, (_m, ch: string) => ch.toUpperCase());
62
- const v = actionOpts[arg.name] ?? actionOpts[camelName];
67
+ const v = optionsRecord[arg.name] ?? optionsRecord[camelName];
63
68
  if (v !== undefined) kwargs[arg.name] = v;
64
69
  }
65
70
 
66
71
  // ── Execute + render ────────────────────────────────────────────────
67
72
  try {
68
- if (actionOpts.verbose) process.env.OPENCLI_VERBOSE = '1';
73
+ const verbose = optionsRecord.verbose === true;
74
+ const format = typeof optionsRecord.format === 'string' ? optionsRecord.format : 'table';
75
+ if (verbose) process.env.OPENCLI_VERBOSE = '1';
69
76
 
70
- const result = await executeCommand(cmd, kwargs, actionOpts.verbose);
77
+ const result = await executeCommand(cmd, kwargs, verbose);
71
78
 
72
- if (actionOpts.verbose && (!result || (Array.isArray(result) && result.length === 0))) {
79
+ if (verbose && (!result || (Array.isArray(result) && result.length === 0))) {
73
80
  console.error(chalk.yellow('[Verbose] Warning: Command returned an empty result.'));
74
81
  }
75
82
  const resolved = getRegistry().get(fullName(cmd)) ?? cmd;
76
83
  renderOutput(result, {
77
- fmt: actionOpts.format,
84
+ fmt: format,
78
85
  columns: resolved.columns,
79
86
  title: `${resolved.site}/${resolved.name}`,
80
87
  elapsed: (Date.now() - startTime) / 1000,
81
88
  source: fullName(resolved),
82
89
  footerExtra: resolved.footerExtra?.(kwargs),
83
90
  });
84
- } catch (err: any) {
91
+ } catch (err) {
85
92
  if (err instanceof CliError) {
86
93
  console.error(chalk.red(`Error [${err.code}]: ${err.message}`));
87
94
  if (err.hint) console.error(chalk.yellow(`Hint: ${err.hint}`));
88
- } else if (actionOpts.verbose && err.stack) {
95
+ } else if (optionsRecord.verbose === true && err instanceof Error && err.stack) {
89
96
  console.error(chalk.red(err.stack));
90
97
  } else {
91
- console.error(chalk.red(`Error: ${err.message ?? err}`));
98
+ console.error(chalk.red(`Error: ${getErrorMessage(err)}`));
92
99
  }
93
100
  process.exitCode = 1;
94
101
  }
package/src/discovery.ts CHANGED
@@ -9,10 +9,55 @@
9
9
  */
10
10
 
11
11
  import * as fs from 'node:fs';
12
+ import * as os from 'node:os';
12
13
  import * as path from 'node:path';
14
+ import { pathToFileURL } from 'node:url';
13
15
  import yaml from 'js-yaml';
14
16
  import { type CliCommand, type InternalCliCommand, type Arg, Strategy, registerCommand } from './registry.js';
15
17
  import { log } from './logger.js';
18
+ import type { ManifestEntry } from './build-manifest.js';
19
+
20
+ /** Plugins directory: ~/.opencli/plugins/ */
21
+ export const PLUGINS_DIR = path.join(os.homedir(), '.opencli', 'plugins');
22
+ const CLI_MODULE_PATTERN = /\bcli\s*\(/;
23
+
24
+ interface YamlArgDefinition {
25
+ type?: string;
26
+ default?: unknown;
27
+ required?: boolean;
28
+ positional?: boolean;
29
+ description?: string;
30
+ help?: string;
31
+ choices?: string[];
32
+ }
33
+
34
+ interface YamlCliDefinition {
35
+ site?: string;
36
+ name?: string;
37
+ description?: string;
38
+ domain?: string;
39
+ strategy?: string;
40
+ browser?: boolean;
41
+ args?: Record<string, YamlArgDefinition>;
42
+ columns?: string[];
43
+ pipeline?: Record<string, unknown>[];
44
+ timeout?: number;
45
+ navigateBefore?: boolean | string;
46
+ }
47
+
48
+ function getErrorMessage(error: unknown): string {
49
+ return error instanceof Error ? error.message : String(error);
50
+ }
51
+
52
+ function parseStrategy(rawStrategy: string | undefined, fallback: Strategy = Strategy.COOKIE): Strategy {
53
+ if (!rawStrategy) return fallback;
54
+ const key = rawStrategy.toUpperCase() as keyof typeof Strategy;
55
+ return Strategy[key] ?? fallback;
56
+ }
57
+
58
+ function isRecord(value: unknown): value is Record<string, unknown> {
59
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
60
+ }
16
61
 
17
62
  /**
18
63
  * Discover and register CLI commands.
@@ -41,11 +86,11 @@ export async function discoverClis(...dirs: string[]): Promise<void> {
41
86
  async function loadFromManifest(manifestPath: string, clisDir: string): Promise<void> {
42
87
  try {
43
88
  const raw = await fs.promises.readFile(manifestPath, 'utf-8');
44
- const manifest = JSON.parse(raw) as any[];
89
+ const manifest = JSON.parse(raw) as ManifestEntry[];
45
90
  for (const entry of manifest) {
46
91
  if (entry.type === 'yaml') {
47
92
  // YAML pipelines fully inlined in manifest — register directly
48
- const strategy = (Strategy as any)[entry.strategy.toUpperCase()] ?? Strategy.COOKIE;
93
+ const strategy = parseStrategy(entry.strategy);
49
94
  const cmd: CliCommand = {
50
95
  site: entry.site,
51
96
  name: entry.name,
@@ -58,12 +103,13 @@ async function loadFromManifest(manifestPath: string, clisDir: string): Promise<
58
103
  pipeline: entry.pipeline,
59
104
  timeoutSeconds: entry.timeout,
60
105
  source: `manifest:${entry.site}/${entry.name}`,
106
+ navigateBefore: entry.navigateBefore,
61
107
  };
62
108
  registerCommand(cmd);
63
109
  } else if (entry.type === 'ts' && entry.modulePath) {
64
110
  // TS adapters: register a lightweight stub.
65
111
  // The actual module is loaded lazily on first executeCommand().
66
- const strategy = (Strategy as any)[(entry.strategy ?? 'cookie').toUpperCase()] ?? Strategy.COOKIE;
112
+ const strategy = parseStrategy(entry.strategy ?? 'cookie');
67
113
  const modulePath = path.resolve(clisDir, entry.modulePath);
68
114
  const cmd: InternalCliCommand = {
69
115
  site: entry.site,
@@ -76,14 +122,15 @@ async function loadFromManifest(manifestPath: string, clisDir: string): Promise<
76
122
  columns: entry.columns,
77
123
  timeoutSeconds: entry.timeout,
78
124
  source: modulePath,
125
+ navigateBefore: entry.navigateBefore,
79
126
  _lazy: true,
80
127
  _modulePath: modulePath,
81
128
  };
82
129
  registerCommand(cmd);
83
130
  }
84
131
  }
85
- } catch (err: any) {
86
- log.warn(`Failed to load manifest ${manifestPath}: ${err.message}`);
132
+ } catch (err) {
133
+ log.warn(`Failed to load manifest ${manifestPath}: ${getErrorMessage(err)}`);
87
134
  }
88
135
  }
89
136
 
@@ -92,7 +139,7 @@ async function loadFromManifest(manifestPath: string, clisDir: string): Promise<
92
139
  */
93
140
  async function discoverClisFromFs(dir: string): Promise<void> {
94
141
  try { await fs.promises.access(dir); } catch { return; }
95
- const promises: Promise<any>[] = [];
142
+ const promises: Promise<unknown>[] = [];
96
143
  const entries = await fs.promises.readdir(dir, { withFileTypes: true });
97
144
 
98
145
  for (const entry of entries) {
@@ -108,9 +155,10 @@ async function discoverClisFromFs(dir: string): Promise<void> {
108
155
  (file.endsWith('.js') && !file.endsWith('.d.js')) ||
109
156
  (file.endsWith('.ts') && !file.endsWith('.d.ts') && !file.endsWith('.test.ts'))
110
157
  ) {
158
+ if (!(await isCliModule(filePath))) continue;
111
159
  promises.push(
112
- import(`file://${filePath}`).catch((err: any) => {
113
- log.warn(`Failed to load module ${filePath}: ${err.message}`);
160
+ import(pathToFileURL(filePath).href).catch((err) => {
161
+ log.warn(`Failed to load module ${filePath}: ${getErrorMessage(err)}`);
114
162
  })
115
163
  );
116
164
  }
@@ -122,18 +170,19 @@ async function discoverClisFromFs(dir: string): Promise<void> {
122
170
  async function registerYamlCli(filePath: string, defaultSite: string): Promise<void> {
123
171
  try {
124
172
  const raw = await fs.promises.readFile(filePath, 'utf-8');
125
- const def = yaml.load(raw) as any;
126
- if (!def || typeof def !== 'object') return;
173
+ const def = yaml.load(raw) as YamlCliDefinition | null;
174
+ if (!isRecord(def)) return;
175
+ const cliDef = def as YamlCliDefinition;
127
176
 
128
- const site = def.site ?? defaultSite;
129
- const name = def.name ?? path.basename(filePath, path.extname(filePath));
130
- const strategyStr = def.strategy ?? (def.browser === false ? 'public' : 'cookie');
131
- const strategy = (Strategy as any)[strategyStr.toUpperCase()] ?? Strategy.COOKIE;
132
- const browser = def.browser ?? (strategy !== Strategy.PUBLIC);
177
+ const site = cliDef.site ?? defaultSite;
178
+ const name = cliDef.name ?? path.basename(filePath, path.extname(filePath));
179
+ const strategyStr = cliDef.strategy ?? (cliDef.browser === false ? 'public' : 'cookie');
180
+ const strategy = parseStrategy(strategyStr);
181
+ const browser = cliDef.browser ?? (strategy !== Strategy.PUBLIC);
133
182
 
134
183
  const args: Arg[] = [];
135
- if (def.args && typeof def.args === 'object') {
136
- for (const [argName, argDef] of Object.entries(def.args as Record<string, any>)) {
184
+ if (cliDef.args && typeof cliDef.args === 'object') {
185
+ for (const [argName, argDef] of Object.entries(cliDef.args)) {
137
186
  args.push({
138
187
  name: argName,
139
188
  type: argDef?.type ?? 'str',
@@ -149,19 +198,80 @@ async function registerYamlCli(filePath: string, defaultSite: string): Promise<v
149
198
  const cmd: CliCommand = {
150
199
  site,
151
200
  name,
152
- description: def.description ?? '',
153
- domain: def.domain,
201
+ description: cliDef.description ?? '',
202
+ domain: cliDef.domain,
154
203
  strategy,
155
204
  browser,
156
205
  args,
157
- columns: def.columns,
158
- pipeline: def.pipeline,
159
- timeoutSeconds: def.timeout,
206
+ columns: cliDef.columns,
207
+ pipeline: cliDef.pipeline,
208
+ timeoutSeconds: cliDef.timeout,
160
209
  source: filePath,
210
+ navigateBefore: cliDef.navigateBefore,
161
211
  };
162
212
 
163
213
  registerCommand(cmd);
164
- } catch (err: any) {
165
- log.warn(`Failed to load ${filePath}: ${err.message}`);
214
+ } catch (err) {
215
+ log.warn(`Failed to load ${filePath}: ${getErrorMessage(err)}`);
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Discover and register plugins from ~/.opencli/plugins/.
221
+ * Each subdirectory is treated as a plugin (site = directory name).
222
+ * Files inside are scanned flat (no nested site subdirs).
223
+ */
224
+ export async function discoverPlugins(): Promise<void> {
225
+ try { await fs.promises.access(PLUGINS_DIR); } catch { return; }
226
+ const entries = await fs.promises.readdir(PLUGINS_DIR, { withFileTypes: true });
227
+ for (const entry of entries) {
228
+ if (!entry.isDirectory()) continue;
229
+ await discoverPluginDir(path.join(PLUGINS_DIR, entry.name), entry.name);
230
+ }
231
+ }
232
+
233
+ /**
234
+ * Flat scan: read yaml/ts files directly in a plugin directory.
235
+ * Unlike discoverClisFromFs, this does NOT expect nested site subdirectories.
236
+ */
237
+ async function discoverPluginDir(dir: string, site: string): Promise<void> {
238
+ const files = await fs.promises.readdir(dir);
239
+ const fileSet = new Set(files);
240
+ const promises: Promise<unknown>[] = [];
241
+ for (const file of files) {
242
+ const filePath = path.join(dir, file);
243
+ if (file.endsWith('.yaml') || file.endsWith('.yml')) {
244
+ promises.push(registerYamlCli(filePath, site));
245
+ } else if (file.endsWith('.js') && !file.endsWith('.d.js')) {
246
+ if (!(await isCliModule(filePath))) continue;
247
+ promises.push(
248
+ import(pathToFileURL(filePath).href).catch((err) => {
249
+ log.warn(`Plugin ${site}/${file}: ${getErrorMessage(err)}`);
250
+ })
251
+ );
252
+ } else if (
253
+ file.endsWith('.ts') && !file.endsWith('.d.ts') && !file.endsWith('.test.ts')
254
+ ) {
255
+ // Skip .ts if a compiled .js sibling exists (production mode can't load .ts)
256
+ const jsFile = file.replace(/\.ts$/, '.js');
257
+ if (fileSet.has(jsFile)) continue;
258
+ if (!(await isCliModule(filePath))) continue;
259
+ promises.push(
260
+ import(pathToFileURL(filePath).href).catch((err) => {
261
+ log.warn(`Plugin ${site}/${file}: ${getErrorMessage(err)}`);
262
+ })
263
+ );
264
+ }
265
+ }
266
+ await Promise.all(promises);
267
+ }
268
+
269
+ async function isCliModule(filePath: string): Promise<boolean> {
270
+ try {
271
+ const source = await fs.promises.readFile(filePath, 'utf-8');
272
+ return CLI_MODULE_PATTERN.test(source);
273
+ } catch (err) {
274
+ log.warn(`Failed to inspect module ${filePath}: ${getErrorMessage(err)}`);
275
+ return false;
166
276
  }
167
277
  }
@@ -1,9 +1,36 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { renderBrowserDoctorReport } from './doctor.js';
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const { mockCheckDaemonStatus, mockListSessions, mockConnect, mockClose } = vi.hoisted(() => ({
4
+ mockCheckDaemonStatus: vi.fn(),
5
+ mockListSessions: vi.fn(),
6
+ mockConnect: vi.fn(),
7
+ mockClose: vi.fn(),
8
+ }));
9
+
10
+ vi.mock('./browser/discover.js', () => ({
11
+ checkDaemonStatus: mockCheckDaemonStatus,
12
+ }));
13
+
14
+ vi.mock('./browser/daemon-client.js', () => ({
15
+ listSessions: mockListSessions,
16
+ }));
17
+
18
+ vi.mock('./browser/index.js', () => ({
19
+ BrowserBridge: class {
20
+ connect = mockConnect;
21
+ close = mockClose;
22
+ },
23
+ }));
24
+
25
+ import { renderBrowserDoctorReport, runBrowserDoctor } from './doctor.js';
3
26
 
4
27
  describe('doctor report rendering', () => {
5
28
  const strip = (s: string) => s.replace(/\x1b\[[0-9;]*m/g, '');
6
29
 
30
+ beforeEach(() => {
31
+ vi.clearAllMocks();
32
+ });
33
+
7
34
  it('renders OK-style report when daemon and extension connected', () => {
8
35
  const text = strip(renderBrowserDoctorReport({
9
36
  daemonRunning: true,
@@ -59,4 +86,34 @@ describe('doctor report rendering', () => {
59
86
 
60
87
  expect(text).toContain('[SKIP] Connectivity: not tested (use --live)');
61
88
  });
89
+
90
+ it('reports consistent status when live check auto-starts the daemon', async () => {
91
+ // With the reordered flow, checkDaemonStatus is called only ONCE — after
92
+ // the connectivity check that may auto-start the daemon.
93
+ mockCheckDaemonStatus.mockResolvedValueOnce({ running: true, extensionConnected: false });
94
+ mockConnect.mockRejectedValueOnce(new Error(
95
+ 'Daemon is running but the Browser Extension is not connected.\n' +
96
+ 'Please install and enable the opencli Browser Bridge extension in Chrome.',
97
+ ));
98
+
99
+ const report = await runBrowserDoctor({ live: true });
100
+
101
+ // Status reflects the post-connectivity state (daemon running)
102
+ expect(report.daemonRunning).toBe(true);
103
+ expect(report.extensionConnected).toBe(false);
104
+ // checkDaemonStatus should only be called once
105
+ expect(mockCheckDaemonStatus).toHaveBeenCalledTimes(1);
106
+ // Should NOT report "daemon not running" since it IS running after live check
107
+ expect(report.issues).not.toContain(
108
+ expect.stringContaining('Daemon is not running'),
109
+ );
110
+ // Should report extension not connected
111
+ expect(report.issues).toEqual(expect.arrayContaining([
112
+ expect.stringContaining('Chrome extension is not connected'),
113
+ ]));
114
+ // Should report connectivity failure
115
+ expect(report.issues).toEqual(expect.arrayContaining([
116
+ expect.stringContaining('Browser connectivity test failed'),
117
+ ]));
118
+ });
62
119
  });
package/src/doctor.ts CHANGED
@@ -51,12 +51,14 @@ export async function checkConnectivity(opts?: { timeout?: number }): Promise<Co
51
51
  }
52
52
 
53
53
  export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<DoctorReport> {
54
- const status = await checkDaemonStatus();
55
-
54
+ // Run the live connectivity check first — it may auto-start the daemon as a
55
+ // side-effect, so we read daemon status only *after* all side-effects settle.
56
56
  let connectivity: ConnectivityResult | undefined;
57
57
  if (opts.live) {
58
58
  connectivity = await checkConnectivity();
59
59
  }
60
+
61
+ const status = await checkDaemonStatus();
60
62
  const sessions = opts.sessions && status.running && status.extensionConnected
61
63
  ? await listSessions() as Array<{ workspace: string; windowId: number; tabCount: number; idleMsRemaining: number }>
62
64
  : undefined;
@@ -1,17 +1,90 @@
1
- /**
2
- * Tests for discovery and execution modules.
3
- */
4
-
5
- import { describe, it, expect, vi, beforeEach } from 'vitest';
6
- import { discoverClis } from './discovery.js';
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { discoverClis, discoverPlugins, PLUGINS_DIR } from './discovery.js';
7
3
  import { executeCommand } from './execution.js';
8
4
  import { getRegistry, cli, Strategy } from './registry.js';
5
+ import * as fs from 'node:fs';
6
+ import * as path from 'node:path';
9
7
 
10
8
  describe('discoverClis', () => {
11
9
  it('handles non-existent directories gracefully', async () => {
12
10
  // Should not throw for missing directories
13
11
  await expect(discoverClis('/tmp/nonexistent-opencli-test-dir')).resolves.not.toThrow();
14
12
  });
13
+
14
+ it('imports only CLI command modules during filesystem discovery', async () => {
15
+ const tempRoot = await fs.promises.mkdtemp(path.join('/tmp', 'opencli-discovery-'));
16
+ const siteDir = path.join(tempRoot, 'temp-site');
17
+ const helperPath = path.join(siteDir, 'helper.ts');
18
+ const commandPath = path.join(siteDir, 'hello.ts');
19
+
20
+ try {
21
+ await fs.promises.mkdir(siteDir, { recursive: true });
22
+ await fs.promises.writeFile(helperPath, `
23
+ globalThis.__opencli_helper_loaded__ = true;
24
+ export const helper = true;
25
+ `);
26
+ await fs.promises.writeFile(commandPath, `
27
+ import { cli, Strategy } from '${path.join(process.cwd(), 'src', 'registry.ts')}';
28
+ cli({
29
+ site: 'temp-site',
30
+ name: 'hello',
31
+ description: 'hello command',
32
+ strategy: Strategy.PUBLIC,
33
+ browser: false,
34
+ func: async () => [{ ok: true }],
35
+ });
36
+ `);
37
+
38
+ delete (globalThis as any).__opencli_helper_loaded__;
39
+ await discoverClis(tempRoot);
40
+
41
+ expect((globalThis as any).__opencli_helper_loaded__).toBeUndefined();
42
+ expect(getRegistry().get('temp-site/hello')).toBeDefined();
43
+ } finally {
44
+ delete (globalThis as any).__opencli_helper_loaded__;
45
+ await fs.promises.rm(tempRoot, { recursive: true, force: true });
46
+ }
47
+ });
48
+ });
49
+
50
+ describe('discoverPlugins', () => {
51
+ const testPluginDir = path.join(PLUGINS_DIR, '__test-plugin__');
52
+ const yamlPath = path.join(testPluginDir, 'greeting.yaml');
53
+
54
+ afterEach(async () => {
55
+ try { await fs.promises.rm(testPluginDir, { recursive: true }); } catch {}
56
+ });
57
+
58
+ it('discovers YAML plugins from ~/.opencli/plugins/', async () => {
59
+ // Create a simple YAML adapter in the plugins directory
60
+ await fs.promises.mkdir(testPluginDir, { recursive: true });
61
+ await fs.promises.writeFile(yamlPath, `
62
+ site: __test-plugin__
63
+ name: greeting
64
+ description: Test plugin greeting
65
+ strategy: public
66
+ browser: false
67
+
68
+ pipeline:
69
+ - evaluate: "() => [{ message: 'hello from plugin' }]"
70
+
71
+ columns: [message]
72
+ `);
73
+
74
+ await discoverPlugins();
75
+
76
+ const registry = getRegistry();
77
+ const cmd = registry.get('__test-plugin__/greeting');
78
+ expect(cmd).toBeDefined();
79
+ expect(cmd!.site).toBe('__test-plugin__');
80
+ expect(cmd!.name).toBe('greeting');
81
+ expect(cmd!.description).toBe('Test plugin greeting');
82
+ });
83
+
84
+ it('handles non-existent plugins directory gracefully', async () => {
85
+ // discoverPlugins should not throw if ~/.opencli/plugins/ does not exist
86
+ await expect(discoverPlugins()).resolves.not.toThrow();
87
+ });
15
88
  });
16
89
 
17
90
  describe('executeCommand', () => {