@jackwener/opencli 1.1.0 → 1.1.1

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 (354) 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/README.md +25 -10
  8. package/README.zh-CN.md +26 -11
  9. package/SKILL.md +95 -31
  10. package/dist/browser/cdp.js +6 -1
  11. package/dist/browser/page.d.ts +4 -1
  12. package/dist/browser/page.js +7 -1
  13. package/dist/build-manifest.js +23 -16
  14. package/dist/cli-manifest.json +431 -276
  15. package/dist/cli.d.ts +6 -0
  16. package/dist/cli.js +189 -162
  17. package/dist/clis/apple-podcasts/commands.test.d.ts +2 -0
  18. package/dist/clis/apple-podcasts/commands.test.js +76 -0
  19. package/dist/clis/apple-podcasts/search.js +2 -2
  20. package/dist/clis/apple-podcasts/top.js +9 -2
  21. package/dist/clis/arxiv/search.js +1 -1
  22. package/dist/clis/bilibili/dynamic.js +1 -1
  23. package/dist/clis/bilibili/favorite.js +1 -1
  24. package/dist/clis/bilibili/feed.js +1 -1
  25. package/dist/clis/bilibili/following.js +1 -1
  26. package/dist/clis/bilibili/history.js +1 -1
  27. package/dist/clis/bilibili/me.js +1 -1
  28. package/dist/clis/bilibili/ranking.js +1 -1
  29. package/dist/clis/bilibili/search.js +3 -3
  30. package/dist/clis/bilibili/subtitle.js +1 -1
  31. package/dist/clis/bilibili/user-videos.js +1 -1
  32. package/dist/{bilibili.d.ts → clis/bilibili/utils.d.ts} +1 -1
  33. package/dist/clis/bloomberg/businessweek.js +17 -0
  34. package/dist/clis/bloomberg/economics.js +17 -0
  35. package/dist/clis/bloomberg/feeds.d.ts +1 -0
  36. package/dist/clis/bloomberg/feeds.js +15 -0
  37. package/dist/clis/bloomberg/industries.d.ts +1 -0
  38. package/dist/clis/bloomberg/industries.js +17 -0
  39. package/dist/clis/bloomberg/main.d.ts +1 -0
  40. package/dist/clis/bloomberg/main.js +17 -0
  41. package/dist/clis/bloomberg/markets.d.ts +1 -0
  42. package/dist/clis/bloomberg/markets.js +17 -0
  43. package/dist/clis/bloomberg/news.d.ts +1 -0
  44. package/dist/clis/bloomberg/news.js +105 -0
  45. package/dist/clis/bloomberg/opinions.d.ts +1 -0
  46. package/dist/clis/bloomberg/opinions.js +17 -0
  47. package/dist/clis/bloomberg/politics.d.ts +1 -0
  48. package/dist/clis/bloomberg/politics.js +17 -0
  49. package/dist/clis/bloomberg/tech.d.ts +1 -0
  50. package/dist/clis/bloomberg/tech.js +17 -0
  51. package/dist/clis/bloomberg/utils.d.ts +34 -0
  52. package/dist/clis/bloomberg/utils.js +364 -0
  53. package/dist/clis/bloomberg/utils.test.d.ts +1 -0
  54. package/dist/clis/bloomberg/utils.test.js +129 -0
  55. package/dist/clis/boss/batchgreet.js +2 -2
  56. package/dist/clis/boss/chatlist.js +2 -2
  57. package/dist/clis/boss/detail.js +2 -2
  58. package/dist/clis/boss/greet.js +4 -4
  59. package/dist/clis/boss/search.js +1 -1
  60. package/dist/clis/boss/send.js +1 -1
  61. package/dist/clis/boss/stats.js +2 -2
  62. package/dist/clis/chaoxing/assignments.js +1 -1
  63. package/dist/clis/chaoxing/exams.js +1 -1
  64. package/dist/{chaoxing.d.ts → clis/chaoxing/utils.d.ts} +1 -1
  65. package/dist/{chaoxing.js → clis/chaoxing/utils.js} +0 -2
  66. package/dist/clis/chaoxing/utils.test.d.ts +1 -0
  67. package/dist/{chaoxing.test.js → clis/chaoxing/utils.test.js} +1 -1
  68. package/dist/clis/chatgpt/read.js +1 -1
  69. package/dist/clis/chatwise/export.js +1 -1
  70. package/dist/clis/chatwise/model.js +2 -2
  71. package/dist/clis/chatwise/screenshot.js +1 -1
  72. package/dist/clis/codex/export.js +1 -1
  73. package/dist/clis/codex/model.js +2 -2
  74. package/dist/clis/codex/screenshot.js +1 -1
  75. package/dist/clis/coupang/add-to-cart.js +3 -4
  76. package/dist/clis/coupang/search.js +2 -4
  77. package/dist/clis/coupang/utils.test.d.ts +1 -0
  78. package/dist/{coupang.test.js → clis/coupang/utils.test.js} +1 -1
  79. package/dist/clis/ctrip/search.js +1 -1
  80. package/dist/clis/cursor/export.js +1 -1
  81. package/dist/clis/cursor/model.js +2 -2
  82. package/dist/clis/cursor/screenshot.js +1 -1
  83. package/dist/clis/jike/comment.js +2 -3
  84. package/dist/clis/jike/create.js +1 -2
  85. package/dist/clis/jike/feed.js +0 -1
  86. package/dist/clis/jike/like.js +1 -2
  87. package/dist/clis/jike/notifications.js +0 -1
  88. package/dist/clis/jike/post.yaml +1 -0
  89. package/dist/clis/jike/repost.js +1 -2
  90. package/dist/clis/jike/search.js +2 -3
  91. package/dist/clis/jike/topic.yaml +1 -0
  92. package/dist/clis/jike/user.yaml +1 -0
  93. package/dist/clis/jimeng/history.yaml +0 -1
  94. package/dist/clis/linkedin/search.js +7 -7
  95. package/dist/clis/linux-do/category.yaml +1 -0
  96. package/dist/clis/linux-do/search.yaml +4 -3
  97. package/dist/clis/linux-do/topic.yaml +1 -0
  98. package/dist/clis/notion/export.js +1 -1
  99. package/dist/clis/reddit/comment.js +3 -4
  100. package/dist/clis/reddit/read.js +4 -5
  101. package/dist/clis/reddit/save.js +2 -3
  102. package/dist/clis/reddit/saved.js +0 -1
  103. package/dist/clis/reddit/search.yaml +1 -0
  104. package/dist/clis/reddit/subscribe.js +0 -1
  105. package/dist/clis/reddit/upvote.js +2 -3
  106. package/dist/clis/reddit/upvoted.js +0 -1
  107. package/dist/clis/reddit/user-comments.yaml +1 -0
  108. package/dist/clis/reddit/user-posts.yaml +1 -0
  109. package/dist/clis/reddit/user.yaml +1 -0
  110. package/dist/clis/reuters/search.js +1 -1
  111. package/dist/clis/smzdm/search.js +2 -3
  112. package/dist/clis/stackoverflow/search.yaml +1 -0
  113. package/dist/clis/steam/top-sellers.yaml +29 -0
  114. package/dist/clis/twitter/accept.js +2 -2
  115. package/dist/clis/twitter/article.js +2 -2
  116. package/dist/clis/twitter/block.d.ts +1 -0
  117. package/dist/clis/twitter/block.js +88 -0
  118. package/dist/clis/twitter/delete.js +1 -1
  119. package/dist/clis/twitter/hide-reply.d.ts +1 -0
  120. package/dist/clis/twitter/hide-reply.js +66 -0
  121. package/dist/clis/twitter/like.js +1 -1
  122. package/dist/clis/twitter/post.js +1 -1
  123. package/dist/clis/twitter/reply-dm.js +1 -1
  124. package/dist/clis/twitter/reply.js +2 -2
  125. package/dist/clis/twitter/search.js +1 -1
  126. package/dist/clis/twitter/thread.js +2 -2
  127. package/dist/clis/twitter/trending.d.ts +1 -0
  128. package/dist/clis/twitter/trending.js +91 -0
  129. package/dist/clis/twitter/unblock.d.ts +1 -0
  130. package/dist/clis/twitter/unblock.js +71 -0
  131. package/dist/clis/v2ex/topic.yaml +1 -0
  132. package/dist/clis/weibo/hot.js +0 -1
  133. package/dist/clis/weread/book.js +1 -1
  134. package/dist/clis/weread/highlights.js +1 -1
  135. package/dist/clis/weread/notes.js +1 -1
  136. package/dist/clis/weread/search.js +1 -1
  137. package/dist/clis/wikipedia/search.js +1 -1
  138. package/dist/clis/xiaohongshu/creator-note-detail.d.ts +15 -0
  139. package/dist/clis/xiaohongshu/creator-note-detail.js +69 -5
  140. package/dist/clis/xiaohongshu/creator-note-detail.test.js +80 -33
  141. package/dist/clis/xiaohongshu/creator-notes.js +35 -5
  142. package/dist/clis/xiaohongshu/creator-notes.test.js +35 -6
  143. package/dist/clis/xiaohongshu/creator-profile.js +0 -1
  144. package/dist/clis/xiaohongshu/creator-stats.js +0 -1
  145. package/dist/clis/xiaohongshu/download.js +2 -3
  146. package/dist/clis/xiaohongshu/feed.yaml +0 -1
  147. package/dist/clis/xiaohongshu/notifications.yaml +0 -1
  148. package/dist/clis/xiaohongshu/search.js +2 -2
  149. package/dist/clis/xiaohongshu/user.js +1 -2
  150. package/dist/clis/yahoo-finance/quote.js +0 -1
  151. package/dist/clis/youtube/search.js +1 -1
  152. package/dist/clis/youtube/transcript.js +1 -1
  153. package/dist/clis/youtube/video.js +1 -1
  154. package/dist/clis/zhihu/download.js +1 -2
  155. package/dist/clis/zhihu/question.js +1 -1
  156. package/dist/clis/zhihu/search.yaml +4 -3
  157. package/dist/commanderAdapter.d.ts +21 -0
  158. package/dist/commanderAdapter.js +111 -0
  159. package/dist/{engine.d.ts → discovery.d.ts} +0 -6
  160. package/dist/{engine.js → discovery.js} +1 -98
  161. package/dist/download/index.d.ts +2 -6
  162. package/dist/download/index.js +19 -46
  163. package/dist/engine.test.d.ts +1 -1
  164. package/dist/engine.test.js +8 -7
  165. package/dist/execution.d.ts +22 -0
  166. package/dist/execution.js +129 -0
  167. package/dist/explore.js +121 -107
  168. package/dist/external-clis.yaml +48 -0
  169. package/dist/external.d.ts +7 -2
  170. package/dist/external.js +11 -14
  171. package/dist/main.js +1 -1
  172. package/dist/pipeline/steps/browser.js +8 -2
  173. package/dist/registry.d.ts +2 -0
  174. package/dist/registry.js +2 -0
  175. package/dist/runtime.d.ts +5 -0
  176. package/dist/runtime.js +8 -0
  177. package/dist/serialization.d.ts +34 -0
  178. package/dist/serialization.js +63 -0
  179. package/dist/types.d.ts +4 -1
  180. package/docs/.vitepress/config.mts +14 -3
  181. package/docs/adapters/browser/arxiv.md +27 -0
  182. package/docs/adapters/browser/barchart.md +32 -0
  183. package/docs/adapters/browser/bloomberg.md +70 -0
  184. package/docs/adapters/browser/chaoxing.md +39 -0
  185. package/docs/adapters/browser/grok.md +35 -0
  186. package/docs/adapters/browser/hf.md +42 -0
  187. package/docs/adapters/browser/jike.md +45 -0
  188. package/docs/adapters/browser/jimeng.md +39 -0
  189. package/docs/adapters/browser/linux-do.md +45 -0
  190. package/docs/adapters/browser/sinafinance.md +35 -0
  191. package/docs/adapters/browser/stackoverflow.md +35 -0
  192. package/docs/adapters/browser/steam.md +26 -0
  193. package/docs/adapters/browser/twitter.md +3 -0
  194. package/docs/adapters/browser/weread.md +48 -0
  195. package/docs/adapters/browser/wikipedia.md +30 -0
  196. package/docs/adapters/browser/xiaohongshu.md +5 -1
  197. package/docs/adapters/desktop/chatgpt.md +3 -3
  198. package/docs/adapters/index.md +13 -0
  199. package/docs/advanced/download.md +4 -4
  200. package/docs/developer/architecture.md +17 -4
  201. package/package.json +1 -1
  202. package/scripts/check-doc-coverage.sh +69 -0
  203. package/scripts/copy-yaml.cjs +7 -0
  204. package/src/browser/cdp.ts +6 -1
  205. package/src/browser/page.ts +7 -1
  206. package/src/build-manifest.ts +25 -19
  207. package/src/cli.ts +218 -139
  208. package/src/clis/apple-podcasts/commands.test.ts +95 -0
  209. package/src/clis/apple-podcasts/search.ts +2 -2
  210. package/src/clis/apple-podcasts/top.ts +12 -2
  211. package/src/clis/arxiv/search.ts +1 -1
  212. package/src/clis/bilibili/dynamic.ts +1 -1
  213. package/src/clis/bilibili/favorite.ts +1 -1
  214. package/src/clis/bilibili/feed.ts +1 -1
  215. package/src/clis/bilibili/following.ts +1 -1
  216. package/src/clis/bilibili/history.ts +1 -1
  217. package/src/clis/bilibili/me.ts +1 -1
  218. package/src/clis/bilibili/ranking.ts +1 -1
  219. package/src/clis/bilibili/search.ts +3 -3
  220. package/src/clis/bilibili/subtitle.ts +1 -1
  221. package/src/clis/bilibili/user-videos.ts +1 -1
  222. package/src/{bilibili.ts → clis/bilibili/utils.ts} +1 -1
  223. package/src/clis/bloomberg/businessweek.ts +18 -0
  224. package/src/clis/bloomberg/economics.ts +18 -0
  225. package/src/clis/bloomberg/feeds.ts +16 -0
  226. package/src/clis/bloomberg/industries.ts +18 -0
  227. package/src/clis/bloomberg/main.ts +18 -0
  228. package/src/clis/bloomberg/markets.ts +18 -0
  229. package/src/clis/bloomberg/news.ts +136 -0
  230. package/src/clis/bloomberg/opinions.ts +18 -0
  231. package/src/clis/bloomberg/politics.ts +18 -0
  232. package/src/clis/bloomberg/tech.ts +18 -0
  233. package/src/clis/bloomberg/utils.test.ts +135 -0
  234. package/src/clis/bloomberg/utils.ts +429 -0
  235. package/src/clis/boss/batchgreet.ts +2 -2
  236. package/src/clis/boss/chatlist.ts +2 -2
  237. package/src/clis/boss/detail.ts +2 -2
  238. package/src/clis/boss/greet.ts +4 -4
  239. package/src/clis/boss/search.ts +1 -1
  240. package/src/clis/boss/send.ts +1 -1
  241. package/src/clis/boss/stats.ts +2 -2
  242. package/src/clis/chaoxing/assignments.ts +1 -1
  243. package/src/clis/chaoxing/exams.ts +1 -1
  244. package/src/{chaoxing.test.ts → clis/chaoxing/utils.test.ts} +1 -1
  245. package/src/{chaoxing.ts → clis/chaoxing/utils.ts} +1 -3
  246. package/src/clis/chatgpt/README.zh-CN.md +3 -3
  247. package/src/clis/chatgpt/read.ts +1 -1
  248. package/src/clis/chatwise/export.ts +1 -1
  249. package/src/clis/chatwise/model.ts +2 -2
  250. package/src/clis/chatwise/screenshot.ts +1 -1
  251. package/src/clis/codex/export.ts +1 -1
  252. package/src/clis/codex/model.ts +2 -2
  253. package/src/clis/codex/screenshot.ts +1 -1
  254. package/src/clis/coupang/add-to-cart.ts +3 -4
  255. package/src/clis/coupang/search.ts +2 -4
  256. package/src/{coupang.test.ts → clis/coupang/utils.test.ts} +1 -1
  257. package/src/clis/ctrip/search.ts +1 -1
  258. package/src/clis/cursor/export.ts +1 -1
  259. package/src/clis/cursor/model.ts +2 -2
  260. package/src/clis/cursor/screenshot.ts +1 -1
  261. package/src/clis/jike/comment.ts +2 -3
  262. package/src/clis/jike/create.ts +1 -2
  263. package/src/clis/jike/feed.ts +0 -1
  264. package/src/clis/jike/like.ts +1 -2
  265. package/src/clis/jike/notifications.ts +0 -1
  266. package/src/clis/jike/post.yaml +1 -0
  267. package/src/clis/jike/repost.ts +1 -2
  268. package/src/clis/jike/search.ts +2 -3
  269. package/src/clis/jike/topic.yaml +1 -0
  270. package/src/clis/jike/user.yaml +1 -0
  271. package/src/clis/jimeng/history.yaml +0 -1
  272. package/src/clis/linkedin/search.ts +7 -7
  273. package/src/clis/linux-do/category.yaml +1 -0
  274. package/src/clis/linux-do/search.yaml +4 -3
  275. package/src/clis/linux-do/topic.yaml +1 -0
  276. package/src/clis/notion/export.ts +1 -1
  277. package/src/clis/reddit/comment.ts +3 -4
  278. package/src/clis/reddit/read.ts +4 -5
  279. package/src/clis/reddit/save.ts +2 -3
  280. package/src/clis/reddit/saved.ts +0 -1
  281. package/src/clis/reddit/search.yaml +1 -0
  282. package/src/clis/reddit/subscribe.ts +0 -1
  283. package/src/clis/reddit/upvote.ts +2 -3
  284. package/src/clis/reddit/upvoted.ts +0 -1
  285. package/src/clis/reddit/user-comments.yaml +1 -0
  286. package/src/clis/reddit/user-posts.yaml +1 -0
  287. package/src/clis/reddit/user.yaml +1 -0
  288. package/src/clis/reuters/search.ts +1 -1
  289. package/src/clis/smzdm/search.ts +2 -3
  290. package/src/clis/stackoverflow/search.yaml +1 -0
  291. package/src/clis/steam/top-sellers.yaml +29 -0
  292. package/src/clis/twitter/accept.ts +2 -2
  293. package/src/clis/twitter/article.ts +2 -2
  294. package/src/clis/twitter/block.ts +92 -0
  295. package/src/clis/twitter/delete.ts +1 -1
  296. package/src/clis/twitter/hide-reply.ts +70 -0
  297. package/src/clis/twitter/like.ts +1 -1
  298. package/src/clis/twitter/post.ts +1 -1
  299. package/src/clis/twitter/reply-dm.ts +1 -1
  300. package/src/clis/twitter/reply.ts +2 -2
  301. package/src/clis/twitter/search.ts +1 -1
  302. package/src/clis/twitter/thread.ts +2 -2
  303. package/src/clis/twitter/trending.ts +113 -0
  304. package/src/clis/twitter/unblock.ts +75 -0
  305. package/src/clis/v2ex/topic.yaml +1 -0
  306. package/src/clis/weibo/hot.ts +0 -1
  307. package/src/clis/weread/book.ts +1 -1
  308. package/src/clis/weread/highlights.ts +1 -1
  309. package/src/clis/weread/notes.ts +1 -1
  310. package/src/clis/weread/search.ts +1 -1
  311. package/src/clis/wikipedia/search.ts +1 -1
  312. package/src/clis/xiaohongshu/creator-note-detail.test.ts +82 -33
  313. package/src/clis/xiaohongshu/creator-note-detail.ts +89 -5
  314. package/src/clis/xiaohongshu/creator-notes.test.ts +39 -6
  315. package/src/clis/xiaohongshu/creator-notes.ts +44 -5
  316. package/src/clis/xiaohongshu/creator-profile.ts +0 -1
  317. package/src/clis/xiaohongshu/creator-stats.ts +0 -1
  318. package/src/clis/xiaohongshu/download.ts +2 -3
  319. package/src/clis/xiaohongshu/feed.yaml +0 -1
  320. package/src/clis/xiaohongshu/notifications.yaml +0 -1
  321. package/src/clis/xiaohongshu/search.ts +2 -2
  322. package/src/clis/xiaohongshu/user.ts +1 -2
  323. package/src/clis/yahoo-finance/quote.ts +0 -1
  324. package/src/clis/youtube/search.ts +1 -1
  325. package/src/clis/youtube/transcript.ts +1 -1
  326. package/src/clis/youtube/video.ts +1 -1
  327. package/src/clis/zhihu/download.ts +1 -2
  328. package/src/clis/zhihu/question.ts +1 -1
  329. package/src/clis/zhihu/search.yaml +4 -3
  330. package/src/commanderAdapter.ts +113 -0
  331. package/src/{engine.ts → discovery.ts} +1 -108
  332. package/src/download/index.ts +21 -54
  333. package/src/engine.test.ts +8 -7
  334. package/src/execution.ts +138 -0
  335. package/src/explore.ts +135 -109
  336. package/src/external-clis.yaml +9 -0
  337. package/src/external.ts +15 -12
  338. package/src/main.ts +1 -1
  339. package/src/pipeline/steps/browser.ts +7 -2
  340. package/src/registry.ts +5 -0
  341. package/src/runtime.ts +9 -0
  342. package/src/serialization.ts +79 -0
  343. package/src/types.ts +1 -1
  344. package/tests/e2e/browser-public.test.ts +25 -0
  345. package/tests/e2e/public-commands.test.ts +55 -1
  346. package/dist/clis/twitter/trending.yaml +0 -46
  347. package/docs/public/CNAME +0 -1
  348. package/src/clis/twitter/trending.yaml +0 -46
  349. /package/dist/{bilibili.js → clis/bilibili/utils.js} +0 -0
  350. /package/dist/{chaoxing.test.d.ts → clis/bloomberg/businessweek.d.ts} +0 -0
  351. /package/dist/{coupang.test.d.ts → clis/bloomberg/economics.d.ts} +0 -0
  352. /package/dist/{coupang.d.ts → clis/coupang/utils.d.ts} +0 -0
  353. /package/dist/{coupang.js → clis/coupang/utils.js} +0 -0
  354. /package/src/{coupang.ts → clis/coupang/utils.ts} +0 -0
package/src/explore.ts CHANGED
@@ -223,6 +223,132 @@ export interface DiscoveredStore {
223
223
 
224
224
  const INTERACT_FUZZ_JS = interactFuzz.toString();
225
225
 
226
+ // ── Analysis helpers (extracted from exploreUrl) ───────────────────────────
227
+
228
+ /** Filter, deduplicate, and score network endpoints. */
229
+ function analyzeEndpoints(networkEntries: NetworkEntry[]): { analyzed: AnalyzedEndpoint[]; totalCount: number } {
230
+ const seen = new Map<string, AnalyzedEndpoint>();
231
+ for (const entry of networkEntries) {
232
+ if (!entry.url) continue;
233
+ const ct = entry.contentType.toLowerCase();
234
+ if (ct.includes('image/') || ct.includes('font/') || ct.includes('css') || ct.includes('javascript') || ct.includes('wasm')) continue;
235
+ if (entry.status && entry.status >= 400) continue;
236
+
237
+ const pattern = urlToPattern(entry.url);
238
+ const key = `${entry.method}:${pattern}`;
239
+ if (seen.has(key)) continue;
240
+
241
+ const qp: string[] = [];
242
+ try { new URL(entry.url).searchParams.forEach((_v, k) => { if (!VOLATILE_PARAMS.has(k)) qp.push(k); }); } catch {}
243
+
244
+ const ep: AnalyzedEndpoint = {
245
+ pattern, method: entry.method, url: entry.url, status: entry.status, contentType: ct,
246
+ queryParams: qp, hasSearchParam: qp.some(p => SEARCH_PARAMS.has(p)),
247
+ hasPaginationParam: qp.some(p => PAGINATION_PARAMS.has(p)),
248
+ hasLimitParam: qp.some(p => LIMIT_PARAMS.has(p)),
249
+ authIndicators: detectAuthIndicators(entry.requestHeaders),
250
+ responseAnalysis: entry.responseBody ? analyzeResponseBody(entry.responseBody) : null,
251
+ score: 0,
252
+ };
253
+ ep.score = scoreEndpoint(ep);
254
+ seen.set(key, ep);
255
+ }
256
+
257
+ const analyzed = [...seen.values()].filter(ep => ep.score >= 5).sort((a, b) => b.score - a.score);
258
+ return { analyzed, totalCount: seen.size };
259
+ }
260
+
261
+ /** Infer CLI capabilities from analyzed endpoints. */
262
+ function inferCapabilitiesFromEndpoints(
263
+ endpoints: AnalyzedEndpoint[],
264
+ stores: DiscoveredStore[],
265
+ opts: { site?: string; goal?: string; url: string },
266
+ ): { capabilities: InferredCapability[]; topStrategy: string; authIndicators: string[] } {
267
+ const capabilities: InferredCapability[] = [];
268
+ const usedNames = new Set<string>();
269
+
270
+ for (const ep of endpoints.slice(0, 8)) {
271
+ let capName = inferCapabilityName(ep.url, opts.goal);
272
+ if (usedNames.has(capName)) {
273
+ const suffix = ep.pattern.split('/').filter(s => s && !s.startsWith('{') && !s.includes('.')).pop();
274
+ capName = suffix ? `${capName}_${suffix}` : `${capName}_${usedNames.size}`;
275
+ }
276
+ usedNames.add(capName);
277
+
278
+ const cols: string[] = [];
279
+ if (ep.responseAnalysis) {
280
+ for (const role of ['title', 'url', 'author', 'score', 'time']) {
281
+ if (ep.responseAnalysis.detectedFields[role]) cols.push(role);
282
+ }
283
+ }
284
+
285
+ const args: InferredCapability['recommendedArgs'] = [];
286
+ if (ep.hasSearchParam) args.push({ name: 'keyword', type: 'str', required: true });
287
+ args.push({ name: 'limit', type: 'int', required: false, default: 20 });
288
+ if (ep.hasPaginationParam) args.push({ name: 'page', type: 'int', required: false, default: 1 });
289
+
290
+ const epStrategy = inferStrategy(ep.authIndicators);
291
+ let storeHint: { store: string; action: string } | undefined;
292
+ if ((epStrategy === 'intercept' || ep.authIndicators.includes('signature')) && stores.length > 0) {
293
+ for (const s of stores) {
294
+ const matchingAction = s.actions.find(a =>
295
+ capName.split('_').some(part => a.toLowerCase().includes(part)) ||
296
+ a.toLowerCase().includes('fetch') || a.toLowerCase().includes('get')
297
+ );
298
+ if (matchingAction) { storeHint = { store: s.id, action: matchingAction }; break; }
299
+ }
300
+ }
301
+
302
+ capabilities.push({
303
+ name: capName, description: `${opts.site ?? detectSiteName(opts.url)} ${capName}`,
304
+ strategy: storeHint ? 'store-action' : epStrategy,
305
+ confidence: Math.min(ep.score / 20, 1.0), endpoint: ep.pattern,
306
+ itemPath: ep.responseAnalysis?.itemPath ?? null,
307
+ recommendedColumns: cols.length ? cols : ['title', 'url'],
308
+ recommendedArgs: args,
309
+ ...(storeHint ? { storeHint } : {}),
310
+ });
311
+ }
312
+
313
+ const allAuth = new Set(endpoints.flatMap(ep => ep.authIndicators));
314
+ const topStrategy = allAuth.has('signature') ? 'intercept'
315
+ : allAuth.has('bearer') || allAuth.has('csrf') ? 'header'
316
+ : allAuth.size === 0 ? 'public' : 'cookie';
317
+
318
+ return { capabilities, topStrategy, authIndicators: [...allAuth] };
319
+ }
320
+
321
+ /** Write explore artifacts (manifest, endpoints, capabilities, auth, stores) to disk. */
322
+ async function writeExploreArtifacts(
323
+ targetDir: string,
324
+ result: Record<string, any>,
325
+ analyzedEndpoints: AnalyzedEndpoint[],
326
+ stores: DiscoveredStore[],
327
+ ): Promise<void> {
328
+ await fs.promises.mkdir(targetDir, { recursive: true });
329
+ const tasks = [
330
+ fs.promises.writeFile(path.join(targetDir, 'manifest.json'), JSON.stringify({
331
+ site: result.site, target_url: result.target_url, final_url: result.final_url, title: result.title,
332
+ framework: result.framework, stores: stores.map(s => ({ type: s.type, id: s.id, actions: s.actions })),
333
+ top_strategy: result.top_strategy, explored_at: new Date().toISOString(),
334
+ }, null, 2)),
335
+ fs.promises.writeFile(path.join(targetDir, 'endpoints.json'), JSON.stringify(analyzedEndpoints.map(ep => ({
336
+ pattern: ep.pattern, method: ep.method, url: ep.url, status: ep.status,
337
+ contentType: ep.contentType, score: ep.score, queryParams: ep.queryParams,
338
+ itemPath: ep.responseAnalysis?.itemPath ?? null, itemCount: ep.responseAnalysis?.itemCount ?? 0,
339
+ detectedFields: ep.responseAnalysis?.detectedFields ?? {}, authIndicators: ep.authIndicators,
340
+ })), null, 2)),
341
+ fs.promises.writeFile(path.join(targetDir, 'capabilities.json'), JSON.stringify(result.capabilities, null, 2)),
342
+ fs.promises.writeFile(path.join(targetDir, 'auth.json'), JSON.stringify({
343
+ top_strategy: result.top_strategy, indicators: result.auth_indicators, framework: result.framework,
344
+ }, null, 2)),
345
+ ];
346
+ if (stores.length > 0) {
347
+ tasks.push(fs.promises.writeFile(path.join(targetDir, 'stores.json'), JSON.stringify(stores, null, 2)));
348
+ }
349
+ await Promise.all(tasks);
350
+ }
351
+
226
352
  // ── Main explore function ──────────────────────────────────────────────────
227
353
 
228
354
  export async function exploreUrl(
@@ -317,125 +443,25 @@ export async function exploreUrl(
317
443
  } catch {}
318
444
  }
319
445
 
320
- // Step 7: Analyze endpoints
321
- const seen = new Map<string, AnalyzedEndpoint>();
322
- for (const entry of networkEntries) {
323
- if (!entry.url) continue;
324
- const ct = entry.contentType.toLowerCase();
325
- if (ct.includes('image/') || ct.includes('font/') || ct.includes('css') || ct.includes('javascript') || ct.includes('wasm')) continue;
326
- if (entry.status && entry.status >= 400) continue;
327
-
328
- const pattern = urlToPattern(entry.url);
329
- const key = `${entry.method}:${pattern}`;
330
- if (seen.has(key)) continue;
331
-
332
- const qp: string[] = [];
333
- try { new URL(entry.url).searchParams.forEach((_v, k) => { if (!VOLATILE_PARAMS.has(k)) qp.push(k); }); } catch {}
334
-
335
- const ep: AnalyzedEndpoint = {
336
- pattern, method: entry.method, url: entry.url, status: entry.status, contentType: ct,
337
- queryParams: qp, hasSearchParam: qp.some(p => SEARCH_PARAMS.has(p)),
338
- hasPaginationParam: qp.some(p => PAGINATION_PARAMS.has(p)),
339
- hasLimitParam: qp.some(p => LIMIT_PARAMS.has(p)),
340
- authIndicators: detectAuthIndicators(entry.requestHeaders),
341
- responseAnalysis: entry.responseBody ? analyzeResponseBody(entry.responseBody) : null,
342
- score: 0,
343
- };
344
- ep.score = scoreEndpoint(ep);
345
- seen.set(key, ep);
346
- }
347
-
348
- const analyzedEndpoints = [...seen.values()].filter(ep => ep.score >= 5).sort((a, b) => b.score - a.score);
349
-
350
- // Step 8: Infer capabilities
351
- const capabilities: InferredCapability[] = [];
352
- const usedNames = new Set<string>();
353
- for (const ep of analyzedEndpoints.slice(0, 8)) {
354
- let capName = inferCapabilityName(ep.url, opts.goal);
355
- if (usedNames.has(capName)) {
356
- const suffix = ep.pattern.split('/').filter(s => s && !s.startsWith('{') && !s.includes('.')).pop();
357
- capName = suffix ? `${capName}_${suffix}` : `${capName}_${usedNames.size}`;
358
- }
359
- usedNames.add(capName);
360
-
361
- const cols: string[] = [];
362
- if (ep.responseAnalysis) {
363
- for (const role of ['title', 'url', 'author', 'score', 'time']) {
364
- if (ep.responseAnalysis.detectedFields[role]) cols.push(role);
365
- }
366
- }
367
-
368
- const args: InferredCapability['recommendedArgs'] = [];
369
- if (ep.hasSearchParam) args.push({ name: 'keyword', type: 'str', required: true });
370
- args.push({ name: 'limit', type: 'int', required: false, default: 20 });
371
- if (ep.hasPaginationParam) args.push({ name: 'page', type: 'int', required: false, default: 1 });
372
-
373
- // Link store actions to capabilities when store-action strategy is recommended
374
- const epStrategy = inferStrategy(ep.authIndicators);
375
- let storeHint: { store: string; action: string } | undefined;
376
- if ((epStrategy === 'intercept' || ep.authIndicators.includes('signature')) && stores.length > 0) {
377
- // Try to find a store/action that matches this endpoint's purpose
378
- for (const s of stores) {
379
- const matchingAction = s.actions.find(a =>
380
- capName.split('_').some(part => a.toLowerCase().includes(part)) ||
381
- a.toLowerCase().includes('fetch') || a.toLowerCase().includes('get')
382
- );
383
- if (matchingAction) {
384
- storeHint = { store: s.id, action: matchingAction };
385
- break;
386
- }
387
- }
388
- }
389
-
390
- capabilities.push({
391
- name: capName, description: `${opts.site ?? detectSiteName(url)} ${capName}`,
392
- strategy: storeHint ? 'store-action' : epStrategy,
393
- confidence: Math.min(ep.score / 20, 1.0), endpoint: ep.pattern,
394
- itemPath: ep.responseAnalysis?.itemPath ?? null,
395
- recommendedColumns: cols.length ? cols : ['title', 'url'],
396
- recommendedArgs: args,
397
- ...(storeHint ? { storeHint } : {}),
398
- });
399
- }
400
-
401
- // Step 9: Determine overall auth strategy
402
- const allAuth = new Set(analyzedEndpoints.flatMap(ep => ep.authIndicators));
403
- const topStrategy = allAuth.has('signature') ? 'intercept' : allAuth.has('bearer') || allAuth.has('csrf') ? 'header' : allAuth.size === 0 ? 'public' : 'cookie';
446
+ // Step 7+8: Analyze endpoints and infer capabilities
447
+ const { analyzed: analyzedEndpoints, totalCount } = analyzeEndpoints(networkEntries);
448
+ const { capabilities, topStrategy, authIndicators } = inferCapabilitiesFromEndpoints(
449
+ analyzedEndpoints, stores, { site: opts.site, goal: opts.goal, url },
450
+ );
404
451
 
452
+ // Step 9: Assemble result and write artifacts
405
453
  const siteName = opts.site ?? detectSiteName(metadata.url || url);
406
454
  const targetDir = opts.outDir ?? path.join('.opencli', 'explore', siteName);
407
- await fs.promises.mkdir(targetDir, { recursive: true });
408
455
 
409
456
  const result = {
410
457
  site: siteName, target_url: url, final_url: metadata.url, title: metadata.title,
411
458
  framework, stores, top_strategy: topStrategy,
412
- endpoint_count: analyzedEndpoints.length + [...seen.values()].filter(ep => ep.score < 5).length,
459
+ endpoint_count: totalCount,
413
460
  api_endpoint_count: analyzedEndpoints.length,
414
- capabilities, auth_indicators: [...allAuth],
461
+ capabilities, auth_indicators: authIndicators,
415
462
  };
416
463
 
417
- // Write artifacts
418
- const writeTasks = [];
419
- writeTasks.push(fs.promises.writeFile(path.join(targetDir, 'manifest.json'), JSON.stringify({
420
- site: siteName, target_url: url, final_url: metadata.url, title: metadata.title,
421
- framework, stores: stores.map(s => ({ type: s.type, id: s.id, actions: s.actions })),
422
- top_strategy: topStrategy, explored_at: new Date().toISOString(),
423
- }, null, 2)));
424
- writeTasks.push(fs.promises.writeFile(path.join(targetDir, 'endpoints.json'), JSON.stringify(analyzedEndpoints.map(ep => ({
425
- pattern: ep.pattern, method: ep.method, url: ep.url, status: ep.status,
426
- contentType: ep.contentType, score: ep.score, queryParams: ep.queryParams,
427
- itemPath: ep.responseAnalysis?.itemPath ?? null, itemCount: ep.responseAnalysis?.itemCount ?? 0,
428
- detectedFields: ep.responseAnalysis?.detectedFields ?? {}, authIndicators: ep.authIndicators,
429
- })), null, 2)));
430
- writeTasks.push(fs.promises.writeFile(path.join(targetDir, 'capabilities.json'), JSON.stringify(capabilities, null, 2)));
431
- writeTasks.push(fs.promises.writeFile(path.join(targetDir, 'auth.json'), JSON.stringify({
432
- top_strategy: topStrategy, indicators: [...allAuth], framework,
433
- }, null, 2)));
434
- if (stores.length > 0) {
435
- writeTasks.push(fs.promises.writeFile(path.join(targetDir, 'stores.json'), JSON.stringify(stores, null, 2)));
436
- }
437
- await Promise.all(writeTasks);
438
-
464
+ await writeExploreArtifacts(targetDir, result, analyzedEndpoints, stores);
439
465
  return { ...result, out_dir: targetDir };
440
466
  })(), { timeout: exploreTimeout, label: `Explore ${url}` });
441
467
  }, { workspace: opts.workspace });
@@ -37,3 +37,12 @@
37
37
  tags: [docker, containers, devops]
38
38
  install:
39
39
  mac: "brew install --cask docker"
40
+
41
+ - name: gws
42
+ binary: gws
43
+ description: "Google Workspace CLI — Docs, Sheets, Drive, Gmail, Calendar"
44
+ homepage: "https://github.com/nicholasgasior/gws"
45
+ tags: [google, docs, sheets, drive, workspace]
46
+ install:
47
+ mac: "brew install gws"
48
+ default: "npm install -g @nicholasgasior/gws"
package/src/external.ts CHANGED
@@ -2,7 +2,7 @@ import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import * as os from 'node:os';
4
4
  import { fileURLToPath } from 'node:url';
5
- import { spawnSync, execSync } from 'node:child_process';
5
+ import { spawnSync, execSync, execFileSync } from 'node:child_process';
6
6
  import yaml from 'js-yaml';
7
7
  import chalk from 'chalk';
8
8
  import { log } from './logger.js';
@@ -65,8 +65,7 @@ export function loadExternalClis(): ExternalCliConfig[] {
65
65
  export function isBinaryInstalled(binary: string): boolean {
66
66
  try {
67
67
  const isWindows = os.platform() === 'win32';
68
- const cmd = isWindows ? 'where' : 'command -v';
69
- execSync(`${cmd} ${binary}`, { stdio: 'ignore' });
68
+ execFileSync(isWindows ? 'where' : 'which', [binary], { stdio: 'ignore' });
70
69
  return true;
71
70
  } catch {
72
71
  return false;
@@ -109,8 +108,8 @@ export function installExternalCli(cli: ExternalCliConfig): boolean {
109
108
  }
110
109
  }
111
110
 
112
- export async function executeExternalCli(name: string, args: string[]): Promise<void> {
113
- const configs = loadExternalClis();
111
+ export function executeExternalCli(name: string, args: string[], preloaded?: ExternalCliConfig[]): void {
112
+ const configs = preloaded ?? loadExternalClis();
114
113
  const cli = configs.find((c) => c.name === name);
115
114
  if (!cli) {
116
115
  throw new Error(`External CLI '${name}' not found in registry.`);
@@ -126,8 +125,7 @@ export async function executeExternalCli(name: string, args: string[]): Promise<
126
125
  }
127
126
  }
128
127
 
129
- // 3. Passthrough execution
130
- // We use spawnSync to properly inherit stdio and block until completion
128
+ // 3. Passthrough execution with stdio inherited
131
129
  const result = spawnSync(cli.binary, args, { stdio: 'inherit' });
132
130
  if (result.error) {
133
131
  console.error(chalk.red(`Failed to execute '${cli.binary}': ${result.error.message}`));
@@ -140,7 +138,13 @@ export async function executeExternalCli(name: string, args: string[]): Promise<
140
138
  }
141
139
  }
142
140
 
143
- export function registerExternalCli(name: string, binary?: string, install?: string, description?: string): void {
141
+ export interface RegisterOptions {
142
+ binary?: string;
143
+ install?: string;
144
+ description?: string;
145
+ }
146
+
147
+ export function registerExternalCli(name: string, opts?: RegisterOptions): void {
144
148
  const userPath = getUserRegistryPath();
145
149
  const configDir = path.dirname(userPath);
146
150
 
@@ -162,13 +166,12 @@ export function registerExternalCli(name: string, binary?: string, install?: str
162
166
 
163
167
  const newItem: ExternalCliConfig = {
164
168
  name,
165
- binary: binary || name,
169
+ binary: opts?.binary || name,
166
170
  };
167
- if (description) newItem.description = description;
168
- if (install) newItem.install = { default: install };
171
+ if (opts?.description) newItem.description = opts.description;
172
+ if (opts?.install) newItem.install = { default: opts.install };
169
173
 
170
174
  if (existingIndex >= 0) {
171
- // Merge
172
175
  items[existingIndex] = { ...items[existingIndex], ...newItem };
173
176
  console.log(chalk.green(`Updated '${name}' in user registry.`));
174
177
  } else {
package/src/main.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  import * as os from 'node:os';
7
7
  import * as path from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
- import { discoverClis } from './engine.js';
9
+ import { discoverClis } from './discovery.js';
10
10
  import { getCompletions } from './completion.js';
11
11
  import { runCli } from './cli.js';
12
12
 
@@ -7,8 +7,13 @@ import type { IPage } from '../../types.js';
7
7
  import { render } from '../template.js';
8
8
 
9
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));
10
+ if (typeof params === 'object' && params && 'url' in params) {
11
+ const url = String(render(params.url, { args, data }));
12
+ await page!.goto(url, { waitUntil: params.waitUntil, settleMs: params.settleMs });
13
+ } else {
14
+ const url = render(params, { args, data });
15
+ await page!.goto(String(url));
16
+ }
12
17
  return data;
13
18
  }
14
19
 
package/src/registry.ts CHANGED
@@ -89,3 +89,8 @@ export function strategyLabel(cmd: CliCommand): string {
89
89
  export function registerCommand(cmd: CliCommand): void {
90
90
  _registry.set(fullName(cmd), cmd);
91
91
  }
92
+
93
+ // Re-export serialization helpers from their dedicated module
94
+ export { serializeArg, serializeCommand, formatArgSummary, formatRegistryHelpText } from './serialization.js';
95
+ export type { SerializedArg } from './serialization.js';
96
+
package/src/runtime.ts CHANGED
@@ -1,5 +1,14 @@
1
+ import { BrowserBridge, CDPBridge } from './browser/index.js';
1
2
  import type { IPage } from './types.js';
2
3
 
4
+ /**
5
+ * Returns the appropriate browser factory based on environment config.
6
+ * Uses CDPBridge when OPENCLI_CDP_ENDPOINT is set, otherwise BrowserBridge.
7
+ */
8
+ export function getBrowserFactory(): new () => IBrowserFactory {
9
+ return (process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge) as any;
10
+ }
11
+
3
12
  export const DEFAULT_BROWSER_CONNECT_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_CONNECT_TIMEOUT ?? '30', 10);
4
13
  export const DEFAULT_BROWSER_COMMAND_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_COMMAND_TIMEOUT ?? '60', 10);
5
14
  export const DEFAULT_BROWSER_EXPLORE_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_EXPLORE_TIMEOUT ?? '120', 10);
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Serialization and formatting helpers for CLI commands and args.
3
+ *
4
+ * Used by the `list` command, Commander --help, and build-manifest.
5
+ * Separated from registry.ts to keep the registry focused on types + registration.
6
+ */
7
+
8
+ import type { Arg, CliCommand } from './registry.js';
9
+ import { fullName, strategyLabel } from './registry.js';
10
+
11
+ // ── Serialization ───────────────────────────────────────────────────────────
12
+
13
+ export type SerializedArg = {
14
+ name: string;
15
+ type: string;
16
+ required: boolean;
17
+ positional: boolean;
18
+ choices: string[];
19
+ default: unknown;
20
+ help: string;
21
+ };
22
+
23
+ /** Stable arg schema — every field is always present (no sparse objects). */
24
+ export function serializeArg(a: Arg): SerializedArg {
25
+ return {
26
+ name: a.name,
27
+ type: a.type ?? 'string',
28
+ required: !!a.required,
29
+ positional: !!a.positional,
30
+ choices: a.choices ?? [],
31
+ default: a.default ?? null,
32
+ help: a.help ?? '',
33
+ };
34
+ }
35
+
36
+ /** Full command metadata for structured output (json/yaml). */
37
+ export function serializeCommand(cmd: CliCommand) {
38
+ return {
39
+ command: fullName(cmd),
40
+ site: cmd.site,
41
+ name: cmd.name,
42
+ description: cmd.description,
43
+ strategy: strategyLabel(cmd),
44
+ browser: !!cmd.browser,
45
+ args: cmd.args.map(serializeArg),
46
+ columns: cmd.columns ?? [],
47
+ domain: cmd.domain ?? null,
48
+ };
49
+ }
50
+
51
+ // ── Formatting ──────────────────────────────────────────────────────────────
52
+
53
+ /** Human-readable arg summary: `<required> [optional]` style. */
54
+ export function formatArgSummary(args: Arg[]): string {
55
+ return args
56
+ .map(a => {
57
+ if (a.positional) return a.required ? `<${a.name}>` : `[${a.name}]`;
58
+ return a.required ? `--${a.name}` : `[--${a.name}]`;
59
+ })
60
+ .join(' ');
61
+ }
62
+
63
+ /** Generate the --help appendix showing registry metadata not exposed by Commander. */
64
+ export function formatRegistryHelpText(cmd: CliCommand): string {
65
+ const lines: string[] = [];
66
+ const choicesArgs = cmd.args.filter(a => a.choices?.length);
67
+ for (const a of choicesArgs) {
68
+ const prefix = a.positional ? `<${a.name}>` : `--${a.name}`;
69
+ const def = a.default != null ? ` (default: ${a.default})` : '';
70
+ lines.push(` ${prefix}: ${a.choices!.join(', ')}${def}`);
71
+ }
72
+ const meta: string[] = [];
73
+ meta.push(`Strategy: ${strategyLabel(cmd)}`);
74
+ meta.push(`Browser: ${cmd.browser ? 'yes' : 'no'}`);
75
+ if (cmd.domain) meta.push(`Domain: ${cmd.domain}`);
76
+ lines.push(meta.join(' | '));
77
+ if (cmd.columns?.length) lines.push(`Output columns: ${cmd.columns.join(', ')}`);
78
+ return '\n' + lines.join('\n') + '\n';
79
+ }
package/src/types.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  export interface IPage {
9
- goto(url: string): Promise<void>;
9
+ goto(url: string, options?: { waitUntil?: 'load' | 'none'; settleMs?: number }): Promise<void>;
10
10
  evaluate(js: string): Promise<any>;
11
11
  getCookies(opts?: { domain?: string; url?: string }): Promise<Array<{
12
12
  name: string;
@@ -46,6 +46,31 @@ describe('browser public-data commands E2E', () => {
46
46
  }
47
47
  }, 60_000);
48
48
 
49
+ it('bloomberg news returns article detail when the article page is accessible', async () => {
50
+ const feedResult = await runCli(['bloomberg', 'tech', '--limit', '1', '-f', 'json']);
51
+ if (feedResult.code !== 0) {
52
+ console.warn('bloomberg news: skipped — could not load Bloomberg tech feed');
53
+ return;
54
+ }
55
+
56
+ const feedItems = parseJsonOutput(feedResult.stdout);
57
+ const link = Array.isArray(feedItems) ? feedItems[0]?.link : null;
58
+ if (!link) {
59
+ console.warn('bloomberg news: skipped — tech feed returned no link');
60
+ return;
61
+ }
62
+
63
+ const data = await tryBrowserCommand(['bloomberg', 'news', link, '-f', 'json']);
64
+ expectDataOrSkip(data, 'bloomberg news');
65
+ if (data) {
66
+ expect(data[0]).toHaveProperty('title');
67
+ expect(data[0]).toHaveProperty('summary');
68
+ expect(data[0]).toHaveProperty('link');
69
+ expect(data[0]).toHaveProperty('mediaLinks');
70
+ expect(data[0]).toHaveProperty('content');
71
+ }
72
+ }, 60_000);
73
+
49
74
  // ── v2ex daily (browser: true) ──
50
75
  it('v2ex daily returns topics', async () => {
51
76
  const data = await tryBrowserCommand(['v2ex', 'daily', '--limit', '3', '-f', 'json']);
@@ -11,10 +11,60 @@ function isExpectedChineseSiteRestriction(code: number, stderr: string): boolean
11
11
  return /Error \[FETCH_ERROR\]: HTTP (403|429|451|503)\b/.test(stderr);
12
12
  }
13
13
 
14
+ function isExpectedApplePodcastsRestriction(code: number, stderr: string): boolean {
15
+ if (code === 0) return false;
16
+ return /Error \[FETCH_ERROR\]: (Charts API HTTP \d+|Unable to reach Apple Podcasts charts)/.test(stderr);
17
+ }
18
+
14
19
  // Keep old name as alias for existing tests
15
20
  const isExpectedXiaoyuzhouRestriction = isExpectedChineseSiteRestriction;
16
21
 
17
22
  describe('public commands E2E', () => {
23
+ // ── bloomberg (RSS-backed, browser: false) ──
24
+ it('bloomberg main returns structured headline data', async () => {
25
+ const { stdout, code } = await runCli(['bloomberg', 'main', '--limit', '1', '-f', 'json']);
26
+ expect(code).toBe(0);
27
+ const data = parseJsonOutput(stdout);
28
+ expect(Array.isArray(data)).toBe(true);
29
+ expect(data.length).toBe(1);
30
+ expect(data[0]).toHaveProperty('title');
31
+ expect(data[0]).toHaveProperty('summary');
32
+ expect(data[0]).toHaveProperty('link');
33
+ expect(data[0]).toHaveProperty('mediaLinks');
34
+ expect(Array.isArray(data[0].mediaLinks)).toBe(true);
35
+ }, 30_000);
36
+
37
+ it.each(['markets', 'economics', 'industries', 'tech', 'politics', 'businessweek', 'opinions'])(
38
+ 'bloomberg %s returns structured RSS items',
39
+ async (section) => {
40
+ const { stdout, code } = await runCli(['bloomberg', section, '--limit', '1', '-f', 'json']);
41
+ expect(code).toBe(0);
42
+ const data = parseJsonOutput(stdout);
43
+ expect(Array.isArray(data)).toBe(true);
44
+ expect(data.length).toBe(1);
45
+ expect(data[0]).toHaveProperty('title');
46
+ expect(data[0]).toHaveProperty('summary');
47
+ expect(data[0]).toHaveProperty('link');
48
+ expect(data[0]).toHaveProperty('mediaLinks');
49
+ },
50
+ 30_000,
51
+ );
52
+
53
+ it('bloomberg feeds lists the supported RSS aliases', async () => {
54
+ const { stdout, code } = await runCli(['bloomberg', 'feeds', '-f', 'json']);
55
+ expect(code).toBe(0);
56
+ const data = parseJsonOutput(stdout);
57
+ expect(Array.isArray(data)).toBe(true);
58
+ expect(data).toEqual(
59
+ expect.arrayContaining([
60
+ expect.objectContaining({ name: 'main' }),
61
+ expect.objectContaining({ name: 'markets' }),
62
+ expect.objectContaining({ name: 'tech' }),
63
+ expect.objectContaining({ name: 'opinions' }),
64
+ ]),
65
+ );
66
+ }, 30_000);
67
+
18
68
  // ── apple-podcasts ──
19
69
  it('apple-podcasts search returns structured podcast results', async () => {
20
70
  const { stdout, code } = await runCli(['apple-podcasts', 'search', 'technology', '--limit', '3', '-f', 'json']);
@@ -39,7 +89,11 @@ describe('public commands E2E', () => {
39
89
  }, 30_000);
40
90
 
41
91
  it('apple-podcasts top returns ranked podcasts', async () => {
42
- const { stdout, code } = await runCli(['apple-podcasts', 'top', '--limit', '3', '--country', 'us', '-f', 'json']);
92
+ const { stdout, stderr, code } = await runCli(['apple-podcasts', 'top', '--limit', '3', '--country', 'us', '-f', 'json']);
93
+ if (isExpectedApplePodcastsRestriction(code, stderr)) {
94
+ console.warn(`apple-podcasts top skipped: ${stderr.trim()}`);
95
+ return;
96
+ }
43
97
  expect(code).toBe(0);
44
98
  const data = parseJsonOutput(stdout);
45
99
  expect(Array.isArray(data)).toBe(true);
@@ -1,46 +0,0 @@
1
- site: twitter
2
- name: trending
3
- description: Twitter/X trending topics
4
- domain: x.com
5
-
6
- args:
7
- limit:
8
- type: int
9
- default: 20
10
- description: Number of trends to show
11
-
12
- pipeline:
13
- - navigate: https://x.com/explore/tabs/trending
14
-
15
- - evaluate: |
16
- (async () => {
17
- const cookies = document.cookie.split(';').reduce((acc, c) => {
18
- const [k, v] = c.trim().split('=');
19
- acc[k] = v;
20
- return acc;
21
- }, {});
22
- const csrfToken = cookies['ct0'] || '';
23
- const bearerToken = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
24
- const res = await fetch('/i/api/2/guide.json?include_page_configuration=true', {
25
- credentials: 'include',
26
- headers: { 'x-twitter-active-user': 'yes', 'x-csrf-token': csrfToken, 'authorization': 'Bearer ' + bearerToken }
27
- });
28
- if (!res.ok) throw new Error('HTTP ' + res.status + '. Hint: trending endpoint may require login or API shape changed.');
29
- const data = await res.json();
30
- const instructions = data?.timeline?.instructions || [];
31
- const entries = instructions.flatMap(inst => inst?.addEntries?.entries || inst?.entries || []);
32
- return entries
33
- .filter(e => e.content?.timelineModule)
34
- .flatMap(e => e.content.timelineModule.items || [])
35
- .map(t => t?.item?.content?.trend)
36
- .filter(Boolean);
37
- })()
38
-
39
- - map:
40
- rank: ${{ index + 1 }}
41
- topic: ${{ item.name }}
42
- tweets: ${{ item.tweetCount || 'N/A' }}
43
-
44
- - limit: ${{ args.limit }}
45
-
46
- columns: [rank, topic, tweets]
package/docs/public/CNAME DELETED
@@ -1 +0,0 @@
1
- opencli.info