@jackwener/opencli 1.0.6 → 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.
- package/.agents/skills/cross-project-adapter-migration/SKILL.md +2 -2
- package/.github/pull_request_template.md +7 -0
- package/.github/workflows/doc-check.yml +36 -0
- package/.github/workflows/docs.yml +7 -42
- package/CHANGELOG.md +23 -0
- package/CLI-EXPLORER.md +9 -8
- package/README.md +51 -10
- package/README.zh-CN.md +29 -11
- package/SKILL.md +102 -33
- package/dist/browser/cdp.js +6 -1
- package/dist/browser/page.d.ts +4 -1
- package/dist/browser/page.js +7 -1
- package/dist/build-manifest.js +23 -16
- package/dist/cli-manifest.json +951 -296
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +225 -148
- package/dist/clis/antigravity/serve.js +296 -47
- package/dist/clis/apple-podcasts/commands.test.d.ts +2 -0
- package/dist/clis/apple-podcasts/commands.test.js +76 -0
- package/dist/clis/apple-podcasts/search.js +2 -2
- package/dist/clis/apple-podcasts/top.js +9 -2
- package/dist/clis/arxiv/paper.js +21 -0
- package/dist/clis/arxiv/search.js +24 -0
- package/dist/clis/arxiv/utils.d.ts +18 -0
- package/dist/clis/arxiv/utils.js +49 -0
- package/dist/clis/bilibili/dynamic.js +1 -1
- package/dist/clis/bilibili/favorite.js +1 -1
- package/dist/clis/bilibili/feed.js +1 -1
- package/dist/clis/bilibili/following.js +1 -1
- package/dist/clis/bilibili/history.js +1 -1
- package/dist/clis/bilibili/me.js +1 -1
- package/dist/clis/bilibili/ranking.js +1 -1
- package/dist/clis/bilibili/search.js +3 -3
- package/dist/clis/bilibili/subtitle.js +1 -1
- package/dist/clis/bilibili/user-videos.js +1 -1
- package/dist/{bilibili.d.ts → clis/bilibili/utils.d.ts} +1 -1
- package/dist/clis/bloomberg/businessweek.d.ts +1 -0
- package/dist/clis/bloomberg/businessweek.js +17 -0
- package/dist/clis/bloomberg/economics.d.ts +1 -0
- package/dist/clis/bloomberg/economics.js +17 -0
- package/dist/clis/bloomberg/feeds.d.ts +1 -0
- package/dist/clis/bloomberg/feeds.js +15 -0
- package/dist/clis/bloomberg/industries.d.ts +1 -0
- package/dist/clis/bloomberg/industries.js +17 -0
- package/dist/clis/bloomberg/main.d.ts +1 -0
- package/dist/clis/bloomberg/main.js +17 -0
- package/dist/clis/bloomberg/markets.d.ts +1 -0
- package/dist/clis/bloomberg/markets.js +17 -0
- package/dist/clis/bloomberg/news.d.ts +1 -0
- package/dist/clis/bloomberg/news.js +105 -0
- package/dist/clis/bloomberg/opinions.d.ts +1 -0
- package/dist/clis/bloomberg/opinions.js +17 -0
- package/dist/clis/bloomberg/politics.d.ts +1 -0
- package/dist/clis/bloomberg/politics.js +17 -0
- package/dist/clis/bloomberg/tech.d.ts +1 -0
- package/dist/clis/bloomberg/tech.js +17 -0
- package/dist/clis/bloomberg/utils.d.ts +34 -0
- package/dist/clis/bloomberg/utils.js +364 -0
- package/dist/clis/bloomberg/utils.test.d.ts +1 -0
- package/dist/clis/bloomberg/utils.test.js +129 -0
- package/dist/clis/boss/batchgreet.d.ts +1 -0
- package/dist/clis/boss/batchgreet.js +147 -0
- package/dist/clis/boss/chatlist.js +2 -2
- package/dist/clis/boss/detail.js +2 -2
- package/dist/clis/boss/exchange.d.ts +1 -0
- package/dist/clis/boss/exchange.js +111 -0
- package/dist/clis/boss/greet.d.ts +1 -0
- package/dist/clis/boss/greet.js +175 -0
- package/dist/clis/boss/invite.d.ts +1 -0
- package/dist/clis/boss/invite.js +158 -0
- package/dist/clis/boss/joblist.d.ts +1 -0
- package/dist/clis/boss/joblist.js +55 -0
- package/dist/clis/boss/mark.d.ts +1 -0
- package/dist/clis/boss/mark.js +141 -0
- package/dist/clis/boss/recommend.d.ts +1 -0
- package/dist/clis/boss/recommend.js +83 -0
- package/dist/clis/boss/search.js +1 -1
- package/dist/clis/boss/send.js +1 -1
- package/dist/clis/boss/stats.d.ts +1 -0
- package/dist/clis/boss/stats.js +116 -0
- package/dist/clis/chaoxing/assignments.js +1 -1
- package/dist/clis/chaoxing/exams.js +1 -1
- package/dist/{chaoxing.d.ts → clis/chaoxing/utils.d.ts} +1 -1
- package/dist/{chaoxing.js → clis/chaoxing/utils.js} +0 -2
- package/dist/clis/chaoxing/utils.test.d.ts +1 -0
- package/dist/{chaoxing.test.js → clis/chaoxing/utils.test.js} +1 -1
- package/dist/clis/chatgpt/read.js +1 -1
- package/dist/clis/chatwise/export.js +1 -1
- package/dist/clis/chatwise/model.js +2 -2
- package/dist/clis/chatwise/screenshot.js +1 -1
- package/dist/clis/codex/export.js +1 -1
- package/dist/clis/codex/model.js +2 -2
- package/dist/clis/codex/screenshot.js +1 -1
- package/dist/clis/coupang/add-to-cart.js +3 -4
- package/dist/clis/coupang/search.js +2 -4
- package/dist/clis/coupang/utils.test.d.ts +1 -0
- package/dist/{coupang.test.js → clis/coupang/utils.test.js} +1 -1
- package/dist/clis/ctrip/search.js +1 -1
- package/dist/clis/cursor/export.js +1 -1
- package/dist/clis/cursor/model.js +2 -2
- package/dist/clis/cursor/screenshot.js +1 -1
- package/dist/clis/jike/comment.js +2 -3
- package/dist/clis/jike/create.js +1 -2
- package/dist/clis/jike/feed.js +0 -1
- package/dist/clis/jike/like.js +1 -2
- package/dist/clis/jike/notifications.js +0 -1
- package/dist/clis/jike/post.yaml +1 -0
- package/dist/clis/jike/repost.js +1 -2
- package/dist/clis/jike/search.js +2 -3
- package/dist/clis/jike/topic.yaml +1 -0
- package/dist/clis/jike/user.yaml +1 -0
- package/dist/clis/jimeng/history.yaml +0 -1
- package/dist/clis/linkedin/search.js +7 -7
- package/dist/clis/linux-do/category.yaml +1 -0
- package/dist/clis/linux-do/search.yaml +4 -3
- package/dist/clis/linux-do/topic.yaml +1 -0
- package/dist/clis/notion/export.js +1 -1
- package/dist/clis/reddit/comment.js +3 -4
- package/dist/clis/reddit/read.js +4 -5
- package/dist/clis/reddit/save.js +2 -3
- package/dist/clis/reddit/saved.js +0 -1
- package/dist/clis/reddit/search.yaml +1 -0
- package/dist/clis/reddit/subscribe.js +0 -1
- package/dist/clis/reddit/upvote.js +2 -3
- package/dist/clis/reddit/upvoted.js +0 -1
- package/dist/clis/reddit/user-comments.yaml +1 -0
- package/dist/clis/reddit/user-posts.yaml +1 -0
- package/dist/clis/reddit/user.yaml +1 -0
- package/dist/clis/reuters/search.js +1 -1
- package/dist/clis/sinafinance/news.d.ts +7 -0
- package/dist/clis/sinafinance/news.js +61 -0
- package/dist/clis/smzdm/search.js +2 -3
- package/dist/clis/stackoverflow/search.yaml +1 -0
- package/dist/clis/steam/top-sellers.yaml +29 -0
- package/dist/clis/twitter/accept.js +2 -2
- package/dist/clis/twitter/article.js +2 -2
- package/dist/clis/twitter/block.d.ts +1 -0
- package/dist/clis/twitter/block.js +88 -0
- package/dist/clis/twitter/delete.js +1 -1
- package/dist/clis/twitter/hide-reply.d.ts +1 -0
- package/dist/clis/twitter/hide-reply.js +66 -0
- package/dist/clis/twitter/like.js +1 -1
- package/dist/clis/twitter/post.js +1 -1
- package/dist/clis/twitter/reply-dm.js +1 -1
- package/dist/clis/twitter/reply.js +2 -2
- package/dist/clis/twitter/search.js +1 -1
- package/dist/clis/twitter/thread.js +2 -2
- package/dist/clis/twitter/trending.d.ts +1 -0
- package/dist/clis/twitter/trending.js +91 -0
- package/dist/clis/twitter/unblock.d.ts +1 -0
- package/dist/clis/twitter/unblock.js +71 -0
- package/dist/clis/v2ex/topic.yaml +1 -0
- package/dist/clis/weibo/hot.js +0 -1
- package/dist/clis/weread/book.js +1 -1
- package/dist/clis/weread/highlights.js +1 -1
- package/dist/clis/weread/notes.js +1 -1
- package/dist/clis/weread/search.js +1 -1
- package/dist/clis/wikipedia/search.d.ts +1 -0
- package/dist/clis/wikipedia/search.js +30 -0
- package/dist/clis/wikipedia/summary.d.ts +1 -0
- package/dist/clis/wikipedia/summary.js +28 -0
- package/dist/clis/wikipedia/utils.d.ts +8 -0
- package/dist/clis/wikipedia/utils.js +18 -0
- package/dist/clis/xiaohongshu/creator-note-detail.d.ts +79 -5
- package/dist/clis/xiaohongshu/creator-note-detail.js +323 -70
- package/dist/clis/xiaohongshu/creator-note-detail.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/creator-note-detail.test.js +258 -0
- package/dist/clis/xiaohongshu/creator-notes-summary.d.ts +28 -0
- package/dist/clis/xiaohongshu/creator-notes-summary.js +92 -0
- package/dist/clis/xiaohongshu/creator-notes-summary.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/creator-notes-summary.test.js +49 -0
- package/dist/clis/xiaohongshu/creator-notes.d.ts +18 -5
- package/dist/clis/xiaohongshu/creator-notes.js +189 -71
- package/dist/clis/xiaohongshu/creator-notes.test.d.ts +1 -0
- package/dist/clis/xiaohongshu/creator-notes.test.js +191 -0
- package/dist/clis/xiaohongshu/creator-profile.js +0 -1
- package/dist/clis/xiaohongshu/creator-stats.js +0 -1
- package/dist/clis/xiaohongshu/download.js +2 -3
- package/dist/clis/xiaohongshu/feed.yaml +0 -1
- package/dist/clis/xiaohongshu/notifications.yaml +0 -1
- package/dist/clis/xiaohongshu/search.js +2 -2
- package/dist/clis/xiaohongshu/user.js +1 -2
- package/dist/clis/yahoo-finance/quote.js +0 -1
- package/dist/clis/youtube/search.js +1 -1
- package/dist/clis/youtube/transcript.js +1 -1
- package/dist/clis/youtube/video.js +1 -1
- package/dist/clis/zhihu/download.js +1 -2
- package/dist/clis/zhihu/question.js +1 -1
- package/dist/clis/zhihu/search.yaml +4 -3
- package/dist/commanderAdapter.d.ts +21 -0
- package/dist/commanderAdapter.js +111 -0
- package/dist/{engine.d.ts → discovery.d.ts} +0 -6
- package/dist/{engine.js → discovery.js} +1 -98
- package/dist/download/index.d.ts +2 -6
- package/dist/download/index.js +19 -46
- package/dist/engine.test.d.ts +1 -1
- package/dist/engine.test.js +8 -7
- package/dist/execution.d.ts +22 -0
- package/dist/execution.js +129 -0
- package/dist/explore.js +121 -107
- package/dist/external-clis.yaml +48 -0
- package/dist/external.d.ts +25 -0
- package/dist/external.js +156 -0
- package/dist/main.js +1 -1
- package/dist/pipeline/steps/browser.js +8 -2
- package/dist/registry.d.ts +2 -0
- package/dist/registry.js +2 -0
- package/dist/runtime.d.ts +5 -0
- package/dist/runtime.js +8 -0
- package/dist/serialization.d.ts +34 -0
- package/dist/serialization.js +63 -0
- package/dist/types.d.ts +4 -1
- package/docs/.vitepress/config.mts +14 -3
- package/docs/adapters/browser/arxiv.md +27 -0
- package/docs/adapters/browser/barchart.md +32 -0
- package/docs/adapters/browser/bloomberg.md +70 -0
- package/docs/adapters/browser/chaoxing.md +39 -0
- package/docs/adapters/browser/grok.md +35 -0
- package/docs/adapters/browser/hf.md +42 -0
- package/docs/adapters/browser/jike.md +45 -0
- package/docs/adapters/browser/jimeng.md +39 -0
- package/docs/adapters/browser/linux-do.md +45 -0
- package/docs/adapters/browser/sinafinance.md +35 -0
- package/docs/adapters/browser/stackoverflow.md +35 -0
- package/docs/adapters/browser/steam.md +26 -0
- package/docs/adapters/browser/twitter.md +3 -0
- package/docs/adapters/browser/weread.md +48 -0
- package/docs/adapters/browser/wikipedia.md +30 -0
- package/docs/adapters/browser/xiaohongshu.md +5 -1
- package/docs/adapters/desktop/chatgpt.md +3 -3
- package/docs/adapters/index.md +13 -0
- package/docs/advanced/download.md +4 -4
- package/docs/developer/architecture.md +17 -4
- package/package.json +1 -1
- package/scripts/check-doc-coverage.sh +69 -0
- package/scripts/copy-yaml.cjs +7 -0
- package/src/browser/cdp.ts +9 -4
- package/src/browser/page.ts +7 -1
- package/src/build-manifest.ts +25 -19
- package/src/cli.ts +253 -119
- package/src/clis/antigravity/serve.ts +323 -50
- package/src/clis/apple-podcasts/commands.test.ts +95 -0
- package/src/clis/apple-podcasts/search.ts +2 -2
- package/src/clis/apple-podcasts/top.ts +12 -2
- package/src/clis/arxiv/paper.ts +21 -0
- package/src/clis/arxiv/search.ts +24 -0
- package/src/clis/arxiv/utils.ts +63 -0
- package/src/clis/bilibili/dynamic.ts +1 -1
- package/src/clis/bilibili/favorite.ts +1 -1
- package/src/clis/bilibili/feed.ts +1 -1
- package/src/clis/bilibili/following.ts +1 -1
- package/src/clis/bilibili/history.ts +1 -1
- package/src/clis/bilibili/me.ts +1 -1
- package/src/clis/bilibili/ranking.ts +1 -1
- package/src/clis/bilibili/search.ts +3 -3
- package/src/clis/bilibili/subtitle.ts +1 -1
- package/src/clis/bilibili/user-videos.ts +1 -1
- package/src/{bilibili.ts → clis/bilibili/utils.ts} +1 -1
- package/src/clis/bloomberg/businessweek.ts +18 -0
- package/src/clis/bloomberg/economics.ts +18 -0
- package/src/clis/bloomberg/feeds.ts +16 -0
- package/src/clis/bloomberg/industries.ts +18 -0
- package/src/clis/bloomberg/main.ts +18 -0
- package/src/clis/bloomberg/markets.ts +18 -0
- package/src/clis/bloomberg/news.ts +136 -0
- package/src/clis/bloomberg/opinions.ts +18 -0
- package/src/clis/bloomberg/politics.ts +18 -0
- package/src/clis/bloomberg/tech.ts +18 -0
- package/src/clis/bloomberg/utils.test.ts +135 -0
- package/src/clis/bloomberg/utils.ts +429 -0
- package/src/clis/boss/batchgreet.ts +167 -0
- package/src/clis/boss/chatlist.ts +2 -2
- package/src/clis/boss/detail.ts +2 -2
- package/src/clis/boss/exchange.ts +126 -0
- package/src/clis/boss/greet.ts +198 -0
- package/src/clis/boss/invite.ts +177 -0
- package/src/clis/boss/joblist.ts +63 -0
- package/src/clis/boss/mark.ts +155 -0
- package/src/clis/boss/recommend.ts +94 -0
- package/src/clis/boss/search.ts +1 -1
- package/src/clis/boss/send.ts +1 -1
- package/src/clis/boss/stats.ts +130 -0
- package/src/clis/chaoxing/assignments.ts +1 -1
- package/src/clis/chaoxing/exams.ts +1 -1
- package/src/{chaoxing.test.ts → clis/chaoxing/utils.test.ts} +1 -1
- package/src/{chaoxing.ts → clis/chaoxing/utils.ts} +1 -3
- package/src/clis/chatgpt/README.zh-CN.md +3 -3
- package/src/clis/chatgpt/read.ts +1 -1
- package/src/clis/chatwise/export.ts +1 -1
- package/src/clis/chatwise/model.ts +2 -2
- package/src/clis/chatwise/screenshot.ts +1 -1
- package/src/clis/codex/export.ts +1 -1
- package/src/clis/codex/model.ts +2 -2
- package/src/clis/codex/screenshot.ts +1 -1
- package/src/clis/coupang/add-to-cart.ts +3 -4
- package/src/clis/coupang/search.ts +2 -4
- package/src/{coupang.test.ts → clis/coupang/utils.test.ts} +1 -1
- package/src/clis/ctrip/search.ts +1 -1
- package/src/clis/cursor/export.ts +1 -1
- package/src/clis/cursor/model.ts +2 -2
- package/src/clis/cursor/screenshot.ts +1 -1
- package/src/clis/jike/comment.ts +2 -3
- package/src/clis/jike/create.ts +1 -2
- package/src/clis/jike/feed.ts +0 -1
- package/src/clis/jike/like.ts +1 -2
- package/src/clis/jike/notifications.ts +0 -1
- package/src/clis/jike/post.yaml +1 -0
- package/src/clis/jike/repost.ts +1 -2
- package/src/clis/jike/search.ts +2 -3
- package/src/clis/jike/topic.yaml +1 -0
- package/src/clis/jike/user.yaml +1 -0
- package/src/clis/jimeng/history.yaml +0 -1
- package/src/clis/linkedin/search.ts +7 -7
- package/src/clis/linux-do/category.yaml +1 -0
- package/src/clis/linux-do/search.yaml +4 -3
- package/src/clis/linux-do/topic.yaml +1 -0
- package/src/clis/notion/export.ts +1 -1
- package/src/clis/reddit/comment.ts +3 -4
- package/src/clis/reddit/read.ts +4 -5
- package/src/clis/reddit/save.ts +2 -3
- package/src/clis/reddit/saved.ts +0 -1
- package/src/clis/reddit/search.yaml +1 -0
- package/src/clis/reddit/subscribe.ts +0 -1
- package/src/clis/reddit/upvote.ts +2 -3
- package/src/clis/reddit/upvoted.ts +0 -1
- package/src/clis/reddit/user-comments.yaml +1 -0
- package/src/clis/reddit/user-posts.yaml +1 -0
- package/src/clis/reddit/user.yaml +1 -0
- package/src/clis/reuters/search.ts +1 -1
- package/src/clis/sinafinance/news.ts +76 -0
- package/src/clis/smzdm/search.ts +2 -3
- package/src/clis/stackoverflow/search.yaml +1 -0
- package/src/clis/steam/top-sellers.yaml +29 -0
- package/src/clis/twitter/accept.ts +2 -2
- package/src/clis/twitter/article.ts +2 -2
- package/src/clis/twitter/block.ts +92 -0
- package/src/clis/twitter/delete.ts +1 -1
- package/src/clis/twitter/hide-reply.ts +70 -0
- package/src/clis/twitter/like.ts +1 -1
- package/src/clis/twitter/post.ts +1 -1
- package/src/clis/twitter/reply-dm.ts +1 -1
- package/src/clis/twitter/reply.ts +2 -2
- package/src/clis/twitter/search.ts +1 -1
- package/src/clis/twitter/thread.ts +2 -2
- package/src/clis/twitter/trending.ts +113 -0
- package/src/clis/twitter/unblock.ts +75 -0
- package/src/clis/v2ex/topic.yaml +1 -0
- package/src/clis/weibo/hot.ts +0 -1
- package/src/clis/weread/book.ts +1 -1
- package/src/clis/weread/highlights.ts +1 -1
- package/src/clis/weread/notes.ts +1 -1
- package/src/clis/weread/search.ts +1 -1
- package/src/clis/wikipedia/search.ts +32 -0
- package/src/clis/wikipedia/summary.ts +28 -0
- package/src/clis/wikipedia/utils.ts +20 -0
- package/src/clis/xiaohongshu/creator-note-detail.test.ts +272 -0
- package/src/clis/xiaohongshu/creator-note-detail.ts +425 -73
- package/src/clis/xiaohongshu/creator-notes-summary.test.ts +54 -0
- package/src/clis/xiaohongshu/creator-notes-summary.ts +120 -0
- package/src/clis/xiaohongshu/creator-notes.test.ts +211 -0
- package/src/clis/xiaohongshu/creator-notes.ts +254 -75
- package/src/clis/xiaohongshu/creator-profile.ts +0 -1
- package/src/clis/xiaohongshu/creator-stats.ts +0 -1
- package/src/clis/xiaohongshu/download.ts +2 -3
- package/src/clis/xiaohongshu/feed.yaml +0 -1
- package/src/clis/xiaohongshu/notifications.yaml +0 -1
- package/src/clis/xiaohongshu/search.ts +2 -2
- package/src/clis/xiaohongshu/user.ts +1 -2
- package/src/clis/yahoo-finance/quote.ts +0 -1
- package/src/clis/youtube/search.ts +1 -1
- package/src/clis/youtube/transcript.ts +1 -1
- package/src/clis/youtube/video.ts +1 -1
- package/src/clis/zhihu/download.ts +1 -2
- package/src/clis/zhihu/question.ts +1 -1
- package/src/clis/zhihu/search.yaml +4 -3
- package/src/commanderAdapter.ts +113 -0
- package/src/daemon.ts +3 -3
- package/src/{engine.ts → discovery.ts} +1 -108
- package/src/download/index.ts +21 -54
- package/src/engine.test.ts +8 -7
- package/src/execution.ts +138 -0
- package/src/explore.ts +135 -109
- package/src/external-clis.yaml +48 -0
- package/src/external.ts +185 -0
- package/src/main.ts +1 -1
- package/src/pipeline/steps/browser.ts +7 -2
- package/src/registry.ts +5 -0
- package/src/runtime.ts +9 -0
- package/src/serialization.ts +79 -0
- package/src/types.ts +1 -1
- package/tests/e2e/browser-public.test.ts +25 -0
- package/tests/e2e/public-commands.test.ts +55 -1
- package/dist/clis/twitter/trending.yaml +0 -46
- package/src/clis/twitter/trending.yaml +0 -46
- /package/dist/{chaoxing.test.d.ts → clis/arxiv/paper.d.ts} +0 -0
- /package/dist/{coupang.test.d.ts → clis/arxiv/search.d.ts} +0 -0
- /package/dist/{bilibili.js → clis/bilibili/utils.js} +0 -0
- /package/dist/{coupang.d.ts → clis/coupang/utils.d.ts} +0 -0
- /package/dist/{coupang.js → clis/coupang/utils.js} +0 -0
- /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
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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:
|
|
459
|
+
endpoint_count: totalCount,
|
|
413
460
|
api_endpoint_count: analyzedEndpoints.length,
|
|
414
|
-
capabilities, auth_indicators:
|
|
461
|
+
capabilities, auth_indicators: authIndicators,
|
|
415
462
|
};
|
|
416
463
|
|
|
417
|
-
|
|
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 });
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
- name: gh
|
|
2
|
+
binary: gh
|
|
3
|
+
description: "GitHub CLI — repos, PRs, issues, releases, gists"
|
|
4
|
+
homepage: "https://cli.github.com"
|
|
5
|
+
tags: [github, git, dev]
|
|
6
|
+
install:
|
|
7
|
+
mac: "brew install gh"
|
|
8
|
+
|
|
9
|
+
- name: obsidian
|
|
10
|
+
binary: obsidian
|
|
11
|
+
description: "Obsidian vault management — notes, search, tags, tasks, sync"
|
|
12
|
+
homepage: "https://obsidian.md/help/cli"
|
|
13
|
+
tags: [notes, knowledge, markdown]
|
|
14
|
+
install:
|
|
15
|
+
mac: "brew install --cask obsidian"
|
|
16
|
+
|
|
17
|
+
- name: readwise
|
|
18
|
+
binary: readwise
|
|
19
|
+
description: "Readwise & Reader CLI — highlights, annotations, reading list"
|
|
20
|
+
homepage: "https://github.com/readwiseio/readwise-cli"
|
|
21
|
+
tags: [reading, highlights]
|
|
22
|
+
install:
|
|
23
|
+
default: "npm install -g @readwiseio/readwise-cli"
|
|
24
|
+
|
|
25
|
+
- name: kubectl
|
|
26
|
+
binary: kubectl
|
|
27
|
+
description: "Kubernetes command-line tool"
|
|
28
|
+
homepage: "https://kubernetes.io/docs/reference/kubectl/"
|
|
29
|
+
tags: [kubernetes, k8s, devops]
|
|
30
|
+
install:
|
|
31
|
+
mac: "brew install kubectl"
|
|
32
|
+
|
|
33
|
+
- name: docker
|
|
34
|
+
binary: docker
|
|
35
|
+
description: "Docker command-line interface"
|
|
36
|
+
homepage: "https://docs.docker.com/engine/reference/commandline/cli/"
|
|
37
|
+
tags: [docker, containers, devops]
|
|
38
|
+
install:
|
|
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
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import * as os from 'node:os';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { spawnSync, execSync, execFileSync } from 'node:child_process';
|
|
6
|
+
import yaml from 'js-yaml';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { log } from './logger.js';
|
|
9
|
+
|
|
10
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
|
|
12
|
+
export interface ExternalCliInstall {
|
|
13
|
+
mac?: string;
|
|
14
|
+
linux?: string;
|
|
15
|
+
windows?: string;
|
|
16
|
+
default?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ExternalCliConfig {
|
|
20
|
+
name: string;
|
|
21
|
+
binary: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
homepage?: string;
|
|
24
|
+
tags?: string[];
|
|
25
|
+
install?: ExternalCliInstall;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getUserRegistryPath(): string {
|
|
29
|
+
const home = os.homedir();
|
|
30
|
+
return path.join(home, '.opencli', 'external-clis.yaml');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function loadExternalClis(): ExternalCliConfig[] {
|
|
34
|
+
const configs = new Map<string, ExternalCliConfig>();
|
|
35
|
+
|
|
36
|
+
// 1. Load built-in
|
|
37
|
+
const builtinPath = path.resolve(__dirname, 'external-clis.yaml');
|
|
38
|
+
try {
|
|
39
|
+
if (fs.existsSync(builtinPath)) {
|
|
40
|
+
const raw = fs.readFileSync(builtinPath, 'utf8');
|
|
41
|
+
const parsed = (yaml.load(raw) || []) as ExternalCliConfig[];
|
|
42
|
+
for (const item of parsed) configs.set(item.name, item);
|
|
43
|
+
}
|
|
44
|
+
} catch (err: any) {
|
|
45
|
+
log.warn(`Failed to parse built-in external-clis.yaml: ${err.message}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 2. Load user custom
|
|
49
|
+
const userPath = getUserRegistryPath();
|
|
50
|
+
try {
|
|
51
|
+
if (fs.existsSync(userPath)) {
|
|
52
|
+
const raw = fs.readFileSync(userPath, 'utf8');
|
|
53
|
+
const parsed = (yaml.load(raw) || []) as ExternalCliConfig[];
|
|
54
|
+
for (const item of parsed) {
|
|
55
|
+
configs.set(item.name, item); // Overwrite built-in if duplicated
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
} catch (err: any) {
|
|
59
|
+
log.warn(`Failed to parse user external-clis.yaml: ${err.message}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return Array.from(configs.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function isBinaryInstalled(binary: string): boolean {
|
|
66
|
+
try {
|
|
67
|
+
const isWindows = os.platform() === 'win32';
|
|
68
|
+
execFileSync(isWindows ? 'where' : 'which', [binary], { stdio: 'ignore' });
|
|
69
|
+
return true;
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getInstallCmd(installConfig?: ExternalCliInstall): string | null {
|
|
76
|
+
if (!installConfig) return null;
|
|
77
|
+
const platform = os.platform();
|
|
78
|
+
if (platform === 'darwin' && installConfig.mac) return installConfig.mac;
|
|
79
|
+
if (platform === 'linux' && installConfig.linux) return installConfig.linux;
|
|
80
|
+
if (platform === 'win32' && installConfig.windows) return installConfig.windows;
|
|
81
|
+
if (installConfig.default) return installConfig.default;
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function installExternalCli(cli: ExternalCliConfig): boolean {
|
|
86
|
+
if (!cli.install) {
|
|
87
|
+
console.error(chalk.red(`No auto-install command configured for '${cli.name}'.`));
|
|
88
|
+
console.error(`Please install '${cli.binary}' manually.`);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const cmd = getInstallCmd(cli.install);
|
|
93
|
+
if (!cmd) {
|
|
94
|
+
console.error(chalk.red(`No install command for your platform (${os.platform()}) for '${cli.name}'.`));
|
|
95
|
+
if (cli.homepage) console.error(`See: ${cli.homepage}`);
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log(chalk.cyan(`🔹 '${cli.name}' is not installed. Auto-installing...`));
|
|
100
|
+
console.log(chalk.dim(`$ ${cmd}`));
|
|
101
|
+
try {
|
|
102
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
103
|
+
console.log(chalk.green(`✅ Installed '${cli.name}' successfully.\n`));
|
|
104
|
+
return true;
|
|
105
|
+
} catch (err: any) {
|
|
106
|
+
console.error(chalk.red(`❌ Failed to install '${cli.name}': ${err.message}`));
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function executeExternalCli(name: string, args: string[], preloaded?: ExternalCliConfig[]): void {
|
|
112
|
+
const configs = preloaded ?? loadExternalClis();
|
|
113
|
+
const cli = configs.find((c) => c.name === name);
|
|
114
|
+
if (!cli) {
|
|
115
|
+
throw new Error(`External CLI '${name}' not found in registry.`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 1. Check if installed
|
|
119
|
+
if (!isBinaryInstalled(cli.binary)) {
|
|
120
|
+
// 2. Try to auto install
|
|
121
|
+
const success = installExternalCli(cli);
|
|
122
|
+
if (!success) {
|
|
123
|
+
process.exitCode = 1;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 3. Passthrough execution with stdio inherited
|
|
129
|
+
const result = spawnSync(cli.binary, args, { stdio: 'inherit' });
|
|
130
|
+
if (result.error) {
|
|
131
|
+
console.error(chalk.red(`Failed to execute '${cli.binary}': ${result.error.message}`));
|
|
132
|
+
process.exitCode = 1;
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (result.status !== null) {
|
|
137
|
+
process.exitCode = result.status;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface RegisterOptions {
|
|
142
|
+
binary?: string;
|
|
143
|
+
install?: string;
|
|
144
|
+
description?: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function registerExternalCli(name: string, opts?: RegisterOptions): void {
|
|
148
|
+
const userPath = getUserRegistryPath();
|
|
149
|
+
const configDir = path.dirname(userPath);
|
|
150
|
+
|
|
151
|
+
if (!fs.existsSync(configDir)) {
|
|
152
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
let items: ExternalCliConfig[] = [];
|
|
156
|
+
if (fs.existsSync(userPath)) {
|
|
157
|
+
try {
|
|
158
|
+
const raw = fs.readFileSync(userPath, 'utf8');
|
|
159
|
+
items = (yaml.load(raw) || []) as ExternalCliConfig[];
|
|
160
|
+
} catch {
|
|
161
|
+
// Ignore
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const existingIndex = items.findIndex((c) => c.name === name);
|
|
166
|
+
|
|
167
|
+
const newItem: ExternalCliConfig = {
|
|
168
|
+
name,
|
|
169
|
+
binary: opts?.binary || name,
|
|
170
|
+
};
|
|
171
|
+
if (opts?.description) newItem.description = opts.description;
|
|
172
|
+
if (opts?.install) newItem.install = { default: opts.install };
|
|
173
|
+
|
|
174
|
+
if (existingIndex >= 0) {
|
|
175
|
+
items[existingIndex] = { ...items[existingIndex], ...newItem };
|
|
176
|
+
console.log(chalk.green(`Updated '${name}' in user registry.`));
|
|
177
|
+
} else {
|
|
178
|
+
items.push(newItem);
|
|
179
|
+
console.log(chalk.green(`Registered '${name}' in user registry.`));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const dump = yaml.dump(items, { indent: 2, sortKeys: true });
|
|
183
|
+
fs.writeFileSync(userPath, dump, 'utf8');
|
|
184
|
+
console.log(chalk.dim(userPath));
|
|
185
|
+
}
|
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 './
|
|
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
|
-
|
|
11
|
-
|
|
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']);
|