@jackwener/opencli 1.7.12 → 1.7.14
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/README.md +8 -7
- package/README.zh-CN.md +9 -8
- package/cli-manifest.json +12128 -6665
- package/clis/1point3acres/digest.js +35 -0
- package/clis/1point3acres/forum.js +51 -0
- package/clis/1point3acres/forums.js +44 -0
- package/clis/1point3acres/hot.js +35 -0
- package/clis/1point3acres/latest.js +35 -0
- package/clis/1point3acres/notifications.js +64 -0
- package/clis/1point3acres/search.js +71 -0
- package/clis/1point3acres/thread.js +117 -0
- package/clis/1point3acres/user.js +77 -0
- package/clis/1point3acres/utils.js +247 -0
- package/clis/_shared/desktop-commands.js +4 -0
- package/clis/aibase/news.js +110 -0
- package/clis/aibase/news.test.js +59 -0
- package/clis/amazon/discussion.test.js +1 -28
- package/clis/antigravity/watch.js +3 -2
- package/clis/arxiv/author.js +44 -0
- package/clis/baidu-scholar/search.js +0 -1
- package/clis/bbc/topic.js +57 -0
- package/clis/bbc/utils.js +79 -0
- package/clis/chaoxing/assignments.js +1 -1
- package/clis/chaoxing/exams.js +1 -1
- package/clis/chatgpt/ask.js +57 -0
- package/clis/chatgpt/commands.test.js +45 -0
- package/clis/chatgpt/detail.js +46 -0
- package/clis/chatgpt/history.js +39 -0
- package/clis/chatgpt/image.js +12 -11
- package/clis/chatgpt/image.test.js +23 -0
- package/clis/chatgpt/new.js +25 -0
- package/clis/chatgpt/read.js +43 -0
- package/clis/chatgpt/send.js +46 -0
- package/clis/chatgpt/status.js +29 -0
- package/clis/chatgpt/utils.js +294 -4
- package/clis/chatgpt/utils.test.js +13 -0
- package/clis/chatgpt-app/ask.js +6 -3
- package/clis/chatwise/ask.js +16 -43
- package/clis/chatwise/composer.test.js +186 -0
- package/clis/chatwise/send.js +2 -24
- package/clis/chatwise/utils.js +143 -0
- package/clis/claude/ask.js +1 -1
- package/clis/claude/detail.js +1 -0
- package/clis/claude/history.js +1 -0
- package/clis/claude/new.js +1 -0
- package/clis/claude/read.js +1 -0
- package/clis/claude/send.js +1 -0
- package/clis/claude/status.js +1 -0
- package/clis/codex/ask.js +15 -9
- package/clis/codex/history.js +16 -33
- package/clis/codex/projects.js +28 -0
- package/clis/codex/read.js +10 -4
- package/clis/codex/send.js +10 -3
- package/clis/codex/sidebar.js +356 -0
- package/clis/codex/sidebar.test.js +329 -0
- package/clis/coingecko/categories.js +75 -0
- package/clis/coingecko/coin.js +107 -0
- package/clis/coingecko/coingecko.test.js +109 -0
- package/clis/coingecko/derivatives.js +84 -0
- package/clis/coingecko/exchanges.js +74 -0
- package/clis/coingecko/global.js +71 -0
- package/clis/coingecko/top.js +64 -0
- package/clis/coingecko/trending.js +55 -0
- package/clis/coupang/add-to-cart.js +21 -13
- package/clis/coupang/coupang.test.js +159 -0
- package/clis/coupang/product.js +257 -0
- package/clis/coupang/search.js +38 -16
- package/clis/coupang/utils.js +55 -1
- package/clis/crates/crate.js +62 -0
- package/clis/crates/search.js +44 -0
- package/clis/crates/utils.js +72 -0
- package/clis/ctrip/ctrip.test.js +234 -0
- package/clis/ctrip/hotel-suggest.js +45 -0
- package/clis/ctrip/search.js +22 -68
- package/clis/ctrip/utils.js +175 -0
- package/clis/cursor/ask.js +6 -3
- package/clis/dblp/author.js +133 -0
- package/clis/dblp/venue.js +64 -0
- package/clis/deepseek/ask.js +12 -7
- package/clis/deepseek/ask.test.js +13 -13
- package/clis/deepseek/detail.js +38 -0
- package/clis/deepseek/detail.test.js +81 -0
- package/clis/deepseek/history.js +1 -0
- package/clis/deepseek/new.js +1 -0
- package/clis/deepseek/read.js +1 -0
- package/clis/deepseek/send.js +140 -0
- package/clis/deepseek/send.test.js +107 -0
- package/clis/deepseek/status.js +1 -0
- package/clis/deepseek/utils.js +66 -0
- package/clis/deepseek/utils.test.js +107 -1
- package/clis/defillama/defillama.test.js +99 -0
- package/clis/defillama/protocol.js +84 -0
- package/clis/defillama/protocols.js +55 -0
- package/clis/defillama/utils.js +99 -0
- package/clis/devto/latest.js +74 -0
- package/clis/dockerhub/image.js +52 -0
- package/clis/dockerhub/search.js +47 -0
- package/clis/dockerhub/utils.js +100 -0
- package/clis/doubao/ask.js +7 -3
- package/clis/doubao/detail.js +1 -0
- package/clis/doubao/history.js +1 -0
- package/clis/doubao/meeting-summary.js +1 -0
- package/clis/doubao/meeting-transcript.js +1 -0
- package/clis/doubao/new.js +1 -0
- package/clis/doubao/read.js +1 -0
- package/clis/doubao/send.js +1 -0
- package/clis/doubao/status.js +1 -0
- package/clis/douyin/draft.test.js +1 -30
- package/clis/endoflife/endoflife.test.js +51 -0
- package/clis/endoflife/product.js +55 -0
- package/clis/endoflife/utils.js +89 -0
- package/clis/facebook/__fixtures__/notifications-page.html +13 -0
- package/clis/facebook/notifications.js +326 -30
- package/clis/facebook/notifications.test.js +458 -0
- package/clis/flathub/app.js +71 -0
- package/clis/flathub/flathub.test.js +90 -0
- package/clis/flathub/search.js +80 -0
- package/clis/flathub/utils.js +114 -0
- package/clis/gemini/ask.js +7 -3
- package/clis/gemini/ask.test.js +2 -2
- package/clis/gemini/deep-research-result.js +6 -2
- package/clis/gemini/deep-research-result.test.js +15 -14
- package/clis/gemini/deep-research.js +8 -4
- package/clis/gemini/deep-research.test.js +15 -18
- package/clis/gemini/image.js +7 -2
- package/clis/gemini/new.js +1 -0
- package/clis/gemini/utils.js +0 -4
- package/clis/google-scholar/cite.js +0 -1
- package/clis/google-scholar/profile.js +0 -1
- package/clis/google-scholar/search.js +0 -1
- package/clis/goproxy/goproxy.test.js +103 -0
- package/clis/goproxy/module.js +47 -0
- package/clis/goproxy/utils.js +165 -0
- package/clis/goproxy/versions.js +59 -0
- package/clis/gov-law/recent.js +0 -1
- package/clis/gov-law/search.js +0 -1
- package/clis/gov-policy/__fixtures__/recent.html +16 -0
- package/clis/gov-policy/__fixtures__/search.html +41 -0
- package/clis/gov-policy/gov-policy.test.js +224 -0
- package/clis/gov-policy/recent.js +66 -24
- package/clis/gov-policy/search.js +65 -23
- package/clis/gov-policy/utils.js +54 -0
- package/clis/grok/ask.js +49 -265
- package/clis/grok/ask.test.js +21 -46
- package/clis/grok/detail.js +60 -0
- package/clis/grok/history.js +48 -0
- package/clis/grok/{image.ts → image.js} +56 -70
- package/clis/grok/image.test.ts +20 -0
- package/clis/grok/new.js +20 -0
- package/clis/grok/read.js +39 -0
- package/clis/grok/send.js +50 -0
- package/clis/grok/status.js +41 -0
- package/clis/grok/utils.js +326 -0
- package/clis/grok/utils.test.js +103 -0
- package/clis/hf/datasets.js +88 -0
- package/clis/hf/hf.test.js +16 -0
- package/clis/hf/models.js +91 -0
- package/clis/hf/paper.js +79 -0
- package/clis/hf/spaces.js +101 -0
- package/clis/hf/top.js +1 -0
- package/clis/homebrew/cask.js +39 -0
- package/clis/homebrew/formula.js +41 -0
- package/clis/homebrew/popular.js +54 -0
- package/clis/homebrew/utils.js +100 -0
- package/clis/hupu/__fixtures__/hot-home.html +64 -0
- package/clis/hupu/detail.js +0 -1
- package/clis/hupu/hot.js +156 -35
- package/clis/hupu/hot.test.js +224 -0
- package/clis/hupu/search.js +0 -1
- package/clis/instagram/note.js +1 -1
- package/clis/instagram/note.test.js +1 -29
- package/clis/instagram/post.js +1 -1
- package/clis/instagram/post.test.js +1 -1
- package/clis/instagram/reel.js +1 -1
- package/clis/instagram/story.js +1 -1
- package/clis/instagram/story.test.js +1 -34
- package/clis/jd/commands.test.js +1 -24
- package/clis/lichess/lichess.test.js +85 -0
- package/clis/lichess/top.js +46 -0
- package/clis/lichess/user.js +91 -0
- package/clis/lichess/utils.js +97 -0
- package/clis/linkedin/search.js +107 -10
- package/clis/linkedin/search.test.js +222 -0
- package/clis/linux-do/feed.js +2 -5
- package/clis/linux-do/feed.test.js +35 -0
- package/clis/lobsters/domain.js +92 -0
- package/clis/maven/artifact.js +49 -0
- package/clis/maven/search.js +51 -0
- package/clis/maven/utils.js +110 -0
- package/clis/mdn/search.js +97 -0
- package/clis/medium/tag.js +135 -0
- package/clis/npm/downloads.js +59 -0
- package/clis/npm/package.js +70 -0
- package/clis/npm/search.js +49 -0
- package/clis/npm/utils.js +76 -0
- package/clis/nuget/nuget.test.js +111 -0
- package/clis/nuget/package.js +101 -0
- package/clis/nuget/search.js +69 -0
- package/clis/nuget/utils.js +87 -0
- package/clis/nvd/cve.js +121 -0
- package/clis/oeis/oeis.test.js +88 -0
- package/clis/oeis/search.js +63 -0
- package/clis/oeis/sequence.js +71 -0
- package/clis/oeis/utils.js +88 -0
- package/clis/openalex/search.js +69 -0
- package/clis/openalex/utils.js +160 -0
- package/clis/openalex/work.js +65 -0
- package/clis/openfda/drug-label.js +74 -0
- package/clis/openfda/food-recall.js +65 -0
- package/clis/openfda/openfda.test.js +114 -0
- package/clis/openfda/utils.js +67 -0
- package/clis/osv/osv.test.js +97 -0
- package/clis/osv/query.js +72 -0
- package/clis/osv/utils.js +169 -0
- package/clis/osv/vulnerability.js +54 -0
- package/clis/packagist/package.js +49 -0
- package/clis/packagist/search.js +43 -0
- package/clis/packagist/utils.js +113 -0
- package/clis/paperreview/feedback.js +1 -1
- package/clis/paperreview/review.js +1 -1
- package/clis/paperreview/submit.js +1 -1
- package/clis/pixiv/download.test.js +1 -1
- package/clis/pixiv/illusts.test.js +1 -1
- package/clis/pixiv/search.test.js +1 -1
- package/clis/pubmed/article.js +50 -0
- package/clis/pubmed/author.js +64 -0
- package/clis/pubmed/citations.js +36 -0
- package/clis/pubmed/pubmed.test.js +276 -0
- package/clis/pubmed/related.js +45 -0
- package/clis/pubmed/search.js +75 -0
- package/clis/pubmed/utils.js +309 -0
- package/clis/pypi/downloads.js +66 -0
- package/clis/pypi/package.js +79 -0
- package/clis/pypi/utils.js +55 -0
- package/clis/quark/mv.js +1 -1
- package/clis/quark/save.js +1 -1
- package/clis/qwen/ask.js +85 -0
- package/clis/qwen/detail.js +62 -0
- package/clis/qwen/history.js +61 -0
- package/clis/qwen/image.js +179 -0
- package/clis/qwen/new.js +23 -0
- package/clis/qwen/read.js +41 -0
- package/clis/qwen/send.js +55 -0
- package/clis/qwen/status.js +37 -0
- package/clis/qwen/utils.js +409 -0
- package/clis/qwen/utils.test.js +45 -0
- package/clis/rest-countries/country.js +65 -0
- package/clis/rest-countries/region.js +64 -0
- package/clis/rest-countries/rest-countries.test.js +83 -0
- package/clis/rest-countries/utils.js +126 -0
- package/clis/reuters/article-detail.js +53 -0
- package/clis/reuters/reuters.test.js +299 -0
- package/clis/reuters/search.js +45 -34
- package/clis/reuters/utils.js +159 -0
- package/clis/rfc/rfc.js +52 -0
- package/clis/rfc/rfc.test.js +74 -0
- package/clis/rfc/utils.js +72 -0
- package/clis/rubygems/gem.js +42 -0
- package/clis/rubygems/search.js +47 -0
- package/clis/rubygems/utils.js +86 -0
- package/clis/stackoverflow/related.js +66 -0
- package/clis/stackoverflow/stackoverflow.test.js +58 -0
- package/clis/stackoverflow/tag.js +60 -0
- package/clis/stackoverflow/user.js +50 -0
- package/clis/stackoverflow/utils.js +118 -0
- package/clis/steam/app.js +67 -0
- package/clis/steam/search.js +58 -0
- package/clis/steam/steam.test.js +46 -0
- package/clis/steam/utils.js +107 -0
- package/clis/taobao/commands.test.js +1 -24
- package/clis/test-utils.js +61 -0
- package/clis/tieba/hot.js +0 -1
- package/clis/tiktok/comment.js +128 -41
- package/clis/tiktok/creator-videos.js +270 -0
- package/clis/tiktok/creator-videos.test.js +113 -0
- package/clis/tiktok/explore.js +137 -29
- package/clis/tiktok/follow.js +115 -33
- package/clis/tiktok/following.js +157 -36
- package/clis/tiktok/friends.js +139 -37
- package/clis/tiktok/live.js +137 -41
- package/clis/tiktok/notifications.js +141 -38
- package/clis/tiktok/refactor.test.js +389 -0
- package/clis/tiktok/unfollow.js +124 -38
- package/clis/tiktok/user.js +203 -29
- package/clis/tiktok/utils.js +505 -0
- package/clis/tiktok/write-refactor.test.js +370 -0
- package/clis/toutiao/articles.js +36 -62
- package/clis/toutiao/hot.js +63 -0
- package/clis/toutiao/toutiao.test.js +378 -0
- package/clis/toutiao/utils.js +161 -0
- package/clis/tvmaze/search.js +61 -0
- package/clis/tvmaze/show.js +60 -0
- package/clis/tvmaze/tvmaze.test.js +93 -0
- package/clis/tvmaze/utils.js +110 -0
- package/clis/twitter/accept.js +1 -1
- package/clis/twitter/followers.js +134 -69
- package/clis/twitter/quote.js +139 -0
- package/clis/twitter/quote.test.js +106 -0
- package/clis/twitter/reply-dm.js +1 -1
- package/clis/twitter/reply.test.js +1 -29
- package/clis/twitter/retweet.js +99 -0
- package/clis/twitter/retweet.test.js +69 -0
- package/clis/twitter/shared.js +38 -0
- package/clis/twitter/shared.test.js +28 -1
- package/clis/twitter/unlike.js +87 -0
- package/clis/twitter/unlike.test.js +72 -0
- package/clis/twitter/unretweet.js +99 -0
- package/clis/twitter/unretweet.test.js +69 -0
- package/clis/uisdc/news.js +105 -0
- package/clis/uisdc/news.test.js +66 -0
- package/clis/wanfang/search.js +0 -1
- package/clis/web/read.js +47 -17
- package/clis/web/read.test.js +101 -1
- package/clis/weixin/create-draft.js +1 -1
- package/clis/weixin/drafts.js +1 -1
- package/clis/weixin/drafts.test.js +5 -1
- package/clis/weixin/search.js +157 -0
- package/clis/weixin/search.test.js +227 -0
- package/clis/wikidata/entity.js +60 -0
- package/clis/wikidata/search.js +50 -0
- package/clis/wikidata/utils.js +117 -0
- package/clis/wikidata/wikidata.test.js +83 -0
- package/clis/wikipedia/page.js +95 -0
- package/clis/wttr/current.js +63 -0
- package/clis/wttr/forecast.js +71 -0
- package/clis/wttr/utils.js +50 -0
- package/clis/wttr/wttr.test.js +84 -0
- package/clis/xianyu/chat.js +16 -4
- package/clis/xianyu/chat.test.js +64 -0
- package/clis/xianyu/publish.js +485 -0
- package/clis/xianyu/publish.test.js +220 -0
- package/clis/xiaoe/catalog.js +105 -40
- package/clis/xiaoe/content.js +164 -29
- package/clis/xiaoe/courses.js +86 -29
- package/clis/xiaoe/xiaoe.test.js +486 -0
- package/clis/xiaohongshu/creator-notes-summary.js +1 -1
- package/clis/xiaohongshu/publish.js +16 -3
- package/clis/xiaohongshu/publish.test.js +46 -1
- package/clis/youtube/transcript.js +13 -19
- package/clis/youtube/transcript.test.js +17 -0
- package/clis/yuanbao/ask.js +17 -66
- package/clis/yuanbao/ask.test.js +5 -5
- package/clis/yuanbao/detail.js +65 -0
- package/clis/yuanbao/history.js +51 -0
- package/clis/yuanbao/new.js +1 -0
- package/clis/yuanbao/read.js +38 -0
- package/clis/yuanbao/send.js +57 -0
- package/clis/yuanbao/shared.js +297 -5
- package/clis/yuanbao/shared.test.js +80 -0
- package/clis/yuanbao/status.js +44 -0
- package/clis/zlibrary/commands.test.js +1 -11
- package/dist/src/browser/base-page.d.ts +9 -0
- package/dist/src/browser/base-page.js +44 -1
- package/dist/src/browser/base-page.test.js +66 -0
- package/dist/src/browser/bridge.js +47 -45
- package/dist/src/browser/cdp.d.ts +1 -0
- package/dist/src/browser/cdp.js +51 -9
- package/dist/src/browser/daemon-client.d.ts +4 -0
- package/dist/src/browser/errors.js +1 -1
- package/dist/src/browser/page.d.ts +1 -1
- package/dist/src/browser/page.js +3 -1
- package/dist/src/browser/page.test.js +29 -0
- package/dist/src/browser/target-errors.d.ts +2 -1
- package/dist/src/browser/target-errors.js +1 -0
- package/dist/src/browser/target-resolver.d.ts +25 -0
- package/dist/src/browser/target-resolver.js +43 -0
- package/dist/src/browser.test.js +18 -0
- package/dist/src/build-manifest.js +9 -4
- package/dist/src/build-manifest.test.js +2 -8
- package/dist/src/capabilityRouting.d.ts +16 -1
- package/dist/src/capabilityRouting.js +24 -1
- package/dist/src/capabilityRouting.test.js +19 -1
- package/dist/src/cli.js +76 -11
- package/dist/src/cli.test.js +241 -1
- package/dist/src/commanderAdapter.js +23 -9
- package/dist/src/commanderAdapter.test.js +0 -1
- package/dist/src/discovery.js +2 -5
- package/dist/src/errors.js +1 -1
- package/dist/src/execution.d.ts +1 -1
- package/dist/src/execution.js +111 -27
- package/dist/src/execution.test.js +326 -17
- package/dist/src/help.d.ts +27 -2
- package/dist/src/help.js +196 -23
- package/dist/src/help.test.d.ts +1 -0
- package/dist/src/help.test.js +54 -0
- package/dist/src/main.js +14 -1
- package/dist/src/manifest-types.d.ts +5 -3
- package/dist/src/pipeline/executor.js +1 -1
- package/dist/src/pipeline/executor.test.js +8 -0
- package/dist/src/pipeline/registry.d.ts +9 -0
- package/dist/src/pipeline/registry.js +13 -1
- package/dist/src/pipeline/steps/browser.d.ts +1 -0
- package/dist/src/pipeline/steps/browser.js +10 -0
- package/dist/src/pipeline/steps/download.test.js +1 -0
- package/dist/src/registry-api.d.ts +1 -1
- package/dist/src/registry.d.ts +12 -11
- package/dist/src/registry.js +16 -6
- package/dist/src/registry.test.js +2 -2
- package/dist/src/runtime.d.ts +2 -1
- package/dist/src/runtime.js +1 -1
- package/dist/src/serialization.d.ts +2 -2
- package/dist/src/serialization.js +4 -6
- package/dist/src/serialization.test.js +17 -0
- package/dist/src/types.d.ts +17 -0
- package/dist/src/validate.js +15 -11
- package/dist/src/validate.test.d.ts +9 -0
- package/dist/src/validate.test.js +90 -0
- package/package.json +1 -1
- package/scripts/fetch-adapters.js +1 -1
- package/scripts/typed-error-lint-baseline.json +5 -77
- package/clis/ctrip/search.test.js +0 -64
- package/clis/gov-policy/commands.test.js +0 -27
- package/clis/linux-do/category.js +0 -37
- package/clis/linux-do/hot.js +0 -26
- package/clis/linux-do/latest.js +0 -19
- package/clis/pixiv/test-utils.js +0 -23
- package/clis/toutiao/articles.test.js +0 -30
- package/dist/src/analysis.d.ts +0 -40
- package/dist/src/analysis.js +0 -172
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// coingecko categories — top crypto categories by aggregated market cap.
|
|
2
|
+
//
|
|
3
|
+
// Hits the public `/api/v3/coins/categories` endpoint. Useful for spotting
|
|
4
|
+
// which sectors (DeFi, L1, gaming, RWAs, …) are leading the market.
|
|
5
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
6
|
+
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
7
|
+
|
|
8
|
+
const ORDER_OPTIONS = ['market_cap_desc', 'market_cap_asc', 'name_desc', 'name_asc', 'market_cap_change_24h_desc', 'market_cap_change_24h_asc'];
|
|
9
|
+
|
|
10
|
+
cli({
|
|
11
|
+
site: 'coingecko',
|
|
12
|
+
name: 'categories',
|
|
13
|
+
access: 'read',
|
|
14
|
+
description: 'Crypto categories ranked by aggregated market cap',
|
|
15
|
+
domain: 'api.coingecko.com',
|
|
16
|
+
strategy: Strategy.PUBLIC,
|
|
17
|
+
browser: false,
|
|
18
|
+
args: [
|
|
19
|
+
{ name: 'sort', default: 'market_cap_desc', help: `Sort order (${ORDER_OPTIONS.join(' / ')})` },
|
|
20
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of categories (1-100; CoinGecko returns ~120 max)' },
|
|
21
|
+
],
|
|
22
|
+
columns: ['rank', 'id', 'name', 'marketCap', 'volume24h', 'marketCapChange24hPct', 'top3Coins'],
|
|
23
|
+
func: async (args) => {
|
|
24
|
+
const sort = String(args.sort ?? 'market_cap_desc').trim().toLowerCase();
|
|
25
|
+
if (!ORDER_OPTIONS.includes(sort)) {
|
|
26
|
+
throw new ArgumentError(
|
|
27
|
+
`coingecko sort "${args.sort}" is not supported`,
|
|
28
|
+
`Supported sorts: ${ORDER_OPTIONS.join(', ')}`,
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const limit = Number(args.limit ?? 20);
|
|
32
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
33
|
+
throw new ArgumentError('coingecko limit must be a positive integer');
|
|
34
|
+
}
|
|
35
|
+
if (limit > 100) {
|
|
36
|
+
throw new ArgumentError('coingecko limit must be <= 100');
|
|
37
|
+
}
|
|
38
|
+
const url = `https://api.coingecko.com/api/v3/coins/categories?order=${encodeURIComponent(sort)}`;
|
|
39
|
+
let resp;
|
|
40
|
+
try {
|
|
41
|
+
resp = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0' } });
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
throw new CommandExecutionError(`coingecko categories request failed: ${err?.message ?? err}`);
|
|
45
|
+
}
|
|
46
|
+
if (resp.status === 429) {
|
|
47
|
+
throw new CommandExecutionError(
|
|
48
|
+
'coingecko returned HTTP 429 (rate limited)',
|
|
49
|
+
'Free tier allows ~30 calls/min. Wait and retry.',
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
if (!resp.ok) {
|
|
53
|
+
throw new CommandExecutionError(`coingecko categories returned HTTP ${resp.status}`);
|
|
54
|
+
}
|
|
55
|
+
let data;
|
|
56
|
+
try {
|
|
57
|
+
data = await resp.json();
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
throw new CommandExecutionError(`coingecko categories returned malformed JSON: ${err?.message ?? err}`);
|
|
61
|
+
}
|
|
62
|
+
if (!Array.isArray(data) || !data.length) {
|
|
63
|
+
throw new EmptyResultError('coingecko categories', 'CoinGecko returned no category data.');
|
|
64
|
+
}
|
|
65
|
+
return data.slice(0, limit).map((cat, i) => ({
|
|
66
|
+
rank: i + 1,
|
|
67
|
+
id: String(cat.id ?? ''),
|
|
68
|
+
name: String(cat.name ?? ''),
|
|
69
|
+
marketCap: cat.market_cap != null ? Number(cat.market_cap) : null,
|
|
70
|
+
volume24h: cat.volume_24h != null ? Number(cat.volume_24h) : null,
|
|
71
|
+
marketCapChange24hPct: cat.market_cap_change_24h != null ? Number(cat.market_cap_change_24h) : null,
|
|
72
|
+
top3Coins: Array.isArray(cat.top_3_coins_id) ? cat.top_3_coins_id.join(', ') : '',
|
|
73
|
+
}));
|
|
74
|
+
},
|
|
75
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// coingecko coin — fetch a single cryptocurrency's market detail by id.
|
|
2
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
3
|
+
import {
|
|
4
|
+
ArgumentError,
|
|
5
|
+
CommandExecutionError,
|
|
6
|
+
EmptyResultError,
|
|
7
|
+
} from '@jackwener/opencli/errors';
|
|
8
|
+
|
|
9
|
+
cli({
|
|
10
|
+
site: 'coingecko',
|
|
11
|
+
name: 'coin',
|
|
12
|
+
access: 'read',
|
|
13
|
+
description: 'Fetch a single cryptocurrency\'s market data by CoinGecko id (e.g. bitcoin, ethereum).',
|
|
14
|
+
domain: 'api.coingecko.com',
|
|
15
|
+
strategy: Strategy.PUBLIC,
|
|
16
|
+
browser: false,
|
|
17
|
+
args: [
|
|
18
|
+
{ name: 'id', positional: true, required: true, type: 'string', help: 'CoinGecko coin id (lowercase, e.g. bitcoin / ethereum / solana).' },
|
|
19
|
+
{ name: 'currency', type: 'string', default: 'usd', help: 'Quote currency (usd, cny, eur, jpy, ...).' },
|
|
20
|
+
],
|
|
21
|
+
columns: [
|
|
22
|
+
'id', 'symbol', 'name', 'rank', 'price', 'marketCap', 'volume24h',
|
|
23
|
+
'change24hPct', 'change7dPct', 'change30dPct', 'ath', 'athDate', 'atl', 'atlDate',
|
|
24
|
+
'circulatingSupply', 'totalSupply', 'maxSupply', 'genesisDate', 'homepage',
|
|
25
|
+
],
|
|
26
|
+
func: async (args) => {
|
|
27
|
+
const id = String(args.id ?? '').trim().toLowerCase();
|
|
28
|
+
if (!id) {
|
|
29
|
+
throw new ArgumentError('coingecko coin id cannot be empty', 'Example: opencli coingecko coin bitcoin');
|
|
30
|
+
}
|
|
31
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(id)) {
|
|
32
|
+
throw new ArgumentError(`coingecko coin id must look like a CoinGecko slug (got "${args.id}")`);
|
|
33
|
+
}
|
|
34
|
+
const currency = String(args.currency ?? 'usd').trim().toLowerCase();
|
|
35
|
+
if (!/^[a-z0-9-]{2,20}$/.test(currency)) {
|
|
36
|
+
throw new ArgumentError(`coingecko currency must look like a currency slug (got "${args.currency}")`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const url = new URL(`https://api.coingecko.com/api/v3/coins/${id}`);
|
|
40
|
+
url.searchParams.set('localization', 'false');
|
|
41
|
+
url.searchParams.set('tickers', 'false');
|
|
42
|
+
url.searchParams.set('market_data', 'true');
|
|
43
|
+
url.searchParams.set('community_data', 'false');
|
|
44
|
+
url.searchParams.set('developer_data', 'false');
|
|
45
|
+
url.searchParams.set('sparkline', 'false');
|
|
46
|
+
|
|
47
|
+
let resp;
|
|
48
|
+
try {
|
|
49
|
+
resp = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0' } });
|
|
50
|
+
} catch (error) {
|
|
51
|
+
throw new CommandExecutionError(`coingecko coin request failed: ${error?.message || error}`);
|
|
52
|
+
}
|
|
53
|
+
if (resp.status === 404) {
|
|
54
|
+
throw new EmptyResultError('coingecko coin', `coingecko has no coin with id "${id}".`);
|
|
55
|
+
}
|
|
56
|
+
if (resp.status === 429) {
|
|
57
|
+
throw new CommandExecutionError('coingecko returned HTTP 429 (rate limited)', 'Free tier allows ~30 calls/min. Wait and retry.');
|
|
58
|
+
}
|
|
59
|
+
if (!resp.ok) {
|
|
60
|
+
throw new CommandExecutionError(`coingecko coin failed: HTTP ${resp.status}`);
|
|
61
|
+
}
|
|
62
|
+
let data;
|
|
63
|
+
try {
|
|
64
|
+
data = await resp.json();
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw new CommandExecutionError(`coingecko returned malformed JSON: ${error?.message || error}`);
|
|
67
|
+
}
|
|
68
|
+
if (data?.error) {
|
|
69
|
+
throw new CommandExecutionError(`coingecko returned error: ${data.error}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const md = data.market_data || {};
|
|
73
|
+
const pick = (obj, key) => (obj && obj[key] != null ? obj[key] : null);
|
|
74
|
+
const isoFromMaybe = (s) => (s ? String(s).slice(0, 10) : '');
|
|
75
|
+
const price = pick(md.current_price, currency);
|
|
76
|
+
const marketCap = pick(md.market_cap, currency);
|
|
77
|
+
const volume24h = pick(md.total_volume, currency);
|
|
78
|
+
if (price == null && marketCap == null && volume24h == null) {
|
|
79
|
+
throw new CommandExecutionError(
|
|
80
|
+
`coingecko returned no market data for currency "${currency}"`,
|
|
81
|
+
'Use a CoinGecko-supported quote currency such as usd, cny, eur, or jpy.',
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return [{
|
|
86
|
+
id: data.id || id,
|
|
87
|
+
symbol: String(data.symbol || '').toUpperCase(),
|
|
88
|
+
name: data.name || '',
|
|
89
|
+
rank: data.market_cap_rank ?? null,
|
|
90
|
+
price,
|
|
91
|
+
marketCap,
|
|
92
|
+
volume24h,
|
|
93
|
+
change24hPct: md.price_change_percentage_24h ?? null,
|
|
94
|
+
change7dPct: md.price_change_percentage_7d ?? null,
|
|
95
|
+
change30dPct: md.price_change_percentage_30d ?? null,
|
|
96
|
+
ath: pick(md.ath, currency),
|
|
97
|
+
athDate: isoFromMaybe(pick(md.ath_date, currency)),
|
|
98
|
+
atl: pick(md.atl, currency),
|
|
99
|
+
atlDate: isoFromMaybe(pick(md.atl_date, currency)),
|
|
100
|
+
circulatingSupply: md.circulating_supply ?? null,
|
|
101
|
+
totalSupply: md.total_supply ?? null,
|
|
102
|
+
maxSupply: md.max_supply ?? null,
|
|
103
|
+
genesisDate: data.genesis_date || '',
|
|
104
|
+
homepage: Array.isArray(data.links?.homepage) ? (data.links.homepage.find(Boolean) || '') : '',
|
|
105
|
+
}];
|
|
106
|
+
},
|
|
107
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getRegistry } from '@jackwener/opencli/registry';
|
|
3
|
+
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
4
|
+
import './coin.js';
|
|
5
|
+
import './trending.js';
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
vi.unstubAllGlobals();
|
|
9
|
+
vi.restoreAllMocks();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe('coingecko coin adapter', () => {
|
|
13
|
+
const cmd = getRegistry().get('coingecko/coin');
|
|
14
|
+
|
|
15
|
+
it('rejects invalid id and currency before fetching', async () => {
|
|
16
|
+
const fetchMock = vi.fn();
|
|
17
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
18
|
+
|
|
19
|
+
await expect(cmd.func({ id: '../btc', currency: 'usd' }))
|
|
20
|
+
.rejects.toThrow(ArgumentError);
|
|
21
|
+
await expect(cmd.func({ id: 'bitcoin', currency: '$$$' }))
|
|
22
|
+
.rejects.toThrow(ArgumentError);
|
|
23
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('fails fast when the requested currency has no market fields', async () => {
|
|
27
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(
|
|
28
|
+
new Response(JSON.stringify({
|
|
29
|
+
id: 'bitcoin',
|
|
30
|
+
symbol: 'btc',
|
|
31
|
+
name: 'Bitcoin',
|
|
32
|
+
market_data: {
|
|
33
|
+
current_price: { usd: 1 },
|
|
34
|
+
market_cap: { usd: 2 },
|
|
35
|
+
total_volume: { usd: 3 },
|
|
36
|
+
},
|
|
37
|
+
}), { status: 200 }),
|
|
38
|
+
));
|
|
39
|
+
|
|
40
|
+
await expect(cmd.func({ id: 'bitcoin', currency: 'zzz' }))
|
|
41
|
+
.rejects.toThrow(CommandExecutionError);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('returns selected currency market data', async () => {
|
|
45
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(
|
|
46
|
+
new Response(JSON.stringify({
|
|
47
|
+
id: 'bitcoin',
|
|
48
|
+
symbol: 'btc',
|
|
49
|
+
name: 'Bitcoin',
|
|
50
|
+
market_cap_rank: 1,
|
|
51
|
+
genesis_date: '2009-01-03',
|
|
52
|
+
links: { homepage: ['https://bitcoin.org', ''] },
|
|
53
|
+
market_data: {
|
|
54
|
+
current_price: { cny: 7 },
|
|
55
|
+
market_cap: { cny: 8 },
|
|
56
|
+
total_volume: { cny: 9 },
|
|
57
|
+
price_change_percentage_24h: 1.23,
|
|
58
|
+
ath: { cny: 10 },
|
|
59
|
+
ath_date: { cny: '2024-01-02T00:00:00Z' },
|
|
60
|
+
atl: { cny: 1 },
|
|
61
|
+
atl_date: { cny: '2015-01-14T00:00:00Z' },
|
|
62
|
+
circulating_supply: 19,
|
|
63
|
+
},
|
|
64
|
+
}), { status: 200 }),
|
|
65
|
+
));
|
|
66
|
+
|
|
67
|
+
const rows = await cmd.func({ id: 'bitcoin', currency: 'cny' });
|
|
68
|
+
|
|
69
|
+
expect(rows).toEqual([expect.objectContaining({
|
|
70
|
+
id: 'bitcoin',
|
|
71
|
+
symbol: 'BTC',
|
|
72
|
+
rank: 1,
|
|
73
|
+
price: 7,
|
|
74
|
+
marketCap: 8,
|
|
75
|
+
volume24h: 9,
|
|
76
|
+
athDate: '2024-01-02',
|
|
77
|
+
homepage: 'https://bitcoin.org',
|
|
78
|
+
})]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('maps 404 to EmptyResultError', async () => {
|
|
82
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(
|
|
83
|
+
new Response(JSON.stringify({ error: 'not found' }), { status: 404 }),
|
|
84
|
+
));
|
|
85
|
+
|
|
86
|
+
await expect(cmd.func({ id: 'missing', currency: 'usd' }))
|
|
87
|
+
.rejects.toThrow(EmptyResultError);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('coingecko trending adapter', () => {
|
|
92
|
+
const cmd = getRegistry().get('coingecko/trending');
|
|
93
|
+
|
|
94
|
+
it('returns ids that round-trip into coingecko coin <id>', async () => {
|
|
95
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(
|
|
96
|
+
new Response(JSON.stringify({
|
|
97
|
+
coins: [{ item: { id: 'bitcoin', symbol: 'btc', name: 'Bitcoin', market_cap_rank: 1, price_btc: 1, thumb: 'thumb.png' } }],
|
|
98
|
+
}), { status: 200 }),
|
|
99
|
+
));
|
|
100
|
+
|
|
101
|
+
const rows = await cmd.func({});
|
|
102
|
+
|
|
103
|
+
expect(rows).toEqual([expect.objectContaining({
|
|
104
|
+
id: 'bitcoin',
|
|
105
|
+
symbol: 'BTC',
|
|
106
|
+
marketCapRank: 1,
|
|
107
|
+
})]);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// coingecko derivatives — perpetual / futures tickers across crypto exchanges.
|
|
2
|
+
//
|
|
3
|
+
// Hits the public `/api/v3/derivatives` endpoint (no auth, free tier). Each
|
|
4
|
+
// row is one exchange-symbol combo: market, contract type, mark price, 24h
|
|
5
|
+
// %, basis vs index, funding rate, open interest (USD), 24h volume.
|
|
6
|
+
//
|
|
7
|
+
// CoinGecko sorts the response by 24h volume desc, so `rank` mirrors the
|
|
8
|
+
// listing order with no client-side reshuffling.
|
|
9
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
10
|
+
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
11
|
+
|
|
12
|
+
const ENDPOINT = 'https://api.coingecko.com/api/v3/derivatives';
|
|
13
|
+
|
|
14
|
+
cli({
|
|
15
|
+
site: 'coingecko',
|
|
16
|
+
name: 'derivatives',
|
|
17
|
+
access: 'read',
|
|
18
|
+
description: 'Top crypto derivative (perpetual / futures) markets by 24h volume',
|
|
19
|
+
domain: 'api.coingecko.com',
|
|
20
|
+
strategy: Strategy.PUBLIC,
|
|
21
|
+
browser: false,
|
|
22
|
+
args: [
|
|
23
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Max rows to return (1-500; CoinGecko returns one large page).' },
|
|
24
|
+
{ name: 'symbol', type: 'string', required: false, help: 'Optional symbol substring filter (e.g. "BTC", "ETHUSDT").' },
|
|
25
|
+
],
|
|
26
|
+
columns: ['rank', 'market', 'symbol', 'indexId', 'contractType', 'price', 'change24hPct', 'fundingRate', 'openInterestUsd', 'volume24hUsd', 'expired'],
|
|
27
|
+
func: async (args) => {
|
|
28
|
+
const limit = Number(args.limit ?? 20);
|
|
29
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
30
|
+
throw new ArgumentError('coingecko derivatives limit must be a positive integer');
|
|
31
|
+
}
|
|
32
|
+
if (limit > 500) {
|
|
33
|
+
throw new ArgumentError('coingecko derivatives limit must be <= 500');
|
|
34
|
+
}
|
|
35
|
+
const filter = args.symbol == null ? '' : String(args.symbol).trim().toUpperCase();
|
|
36
|
+
let resp;
|
|
37
|
+
try {
|
|
38
|
+
resp = await fetch(ENDPOINT, { headers: { 'User-Agent': 'Mozilla/5.0' } });
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
throw new CommandExecutionError(`coingecko derivatives request failed: ${err?.message ?? err}`);
|
|
42
|
+
}
|
|
43
|
+
if (resp.status === 429) {
|
|
44
|
+
throw new CommandExecutionError(
|
|
45
|
+
'coingecko derivatives returned HTTP 429 (rate limited)',
|
|
46
|
+
'Free tier allows ~30 calls/min. Wait and retry.',
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (!resp.ok) {
|
|
50
|
+
throw new CommandExecutionError(`coingecko derivatives returned HTTP ${resp.status}`);
|
|
51
|
+
}
|
|
52
|
+
let data;
|
|
53
|
+
try {
|
|
54
|
+
data = await resp.json();
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
throw new CommandExecutionError(`coingecko derivatives returned malformed JSON: ${err?.message ?? err}`);
|
|
58
|
+
}
|
|
59
|
+
if (!Array.isArray(data) || !data.length) {
|
|
60
|
+
throw new EmptyResultError('coingecko derivatives', 'CoinGecko returned no derivative tickers.');
|
|
61
|
+
}
|
|
62
|
+
let rows = data;
|
|
63
|
+
if (filter) {
|
|
64
|
+
rows = data.filter((d) => String(d.symbol ?? '').toUpperCase().includes(filter)
|
|
65
|
+
|| String(d.index_id ?? '').toUpperCase().includes(filter));
|
|
66
|
+
if (!rows.length) {
|
|
67
|
+
throw new EmptyResultError('coingecko derivatives', `No derivative tickers matched symbol="${filter}".`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return rows.slice(0, limit).map((d, i) => ({
|
|
71
|
+
rank: i + 1,
|
|
72
|
+
market: String(d.market ?? ''),
|
|
73
|
+
symbol: String(d.symbol ?? ''),
|
|
74
|
+
indexId: String(d.index_id ?? ''),
|
|
75
|
+
contractType: String(d.contract_type ?? ''),
|
|
76
|
+
price: d.price != null ? Number(d.price) : null,
|
|
77
|
+
change24hPct: d.price_percentage_change_24h != null ? Number(d.price_percentage_change_24h) : null,
|
|
78
|
+
fundingRate: d.funding_rate != null ? Number(d.funding_rate) : null,
|
|
79
|
+
openInterestUsd: d.open_interest != null ? Number(d.open_interest) : null,
|
|
80
|
+
volume24hUsd: d.volume_24h != null ? Number(d.volume_24h) : null,
|
|
81
|
+
expired: d.expired_at ? String(d.expired_at) : '',
|
|
82
|
+
}));
|
|
83
|
+
},
|
|
84
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// coingecko exchanges — top crypto exchanges by 24h BTC trading volume.
|
|
2
|
+
//
|
|
3
|
+
// Hits the public `/api/v3/exchanges` endpoint. Returns the columns most
|
|
4
|
+
// useful for an agent: trust score, 24h BTC volume, country, year founded,
|
|
5
|
+
// canonical URL.
|
|
6
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
7
|
+
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
8
|
+
|
|
9
|
+
cli({
|
|
10
|
+
site: 'coingecko',
|
|
11
|
+
name: 'exchanges',
|
|
12
|
+
access: 'read',
|
|
13
|
+
description: 'Top crypto exchanges by 24h BTC trading volume',
|
|
14
|
+
domain: 'api.coingecko.com',
|
|
15
|
+
strategy: Strategy.PUBLIC,
|
|
16
|
+
browser: false,
|
|
17
|
+
args: [
|
|
18
|
+
{ name: 'limit', type: 'int', default: 20, help: 'Number of exchanges (1-250, CoinGecko per_page upper bound)' },
|
|
19
|
+
{ name: 'page', type: 'int', default: 1, help: 'Page number (1-based)' },
|
|
20
|
+
],
|
|
21
|
+
columns: ['rank', 'id', 'name', 'trustScore', 'volume24hBtc', 'country', 'yearEstablished', 'url'],
|
|
22
|
+
func: async (args) => {
|
|
23
|
+
const limit = Number(args.limit ?? 20);
|
|
24
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
25
|
+
throw new ArgumentError('coingecko limit must be a positive integer');
|
|
26
|
+
}
|
|
27
|
+
if (limit > 250) {
|
|
28
|
+
throw new ArgumentError('coingecko limit must be <= 250 (per_page upper bound)');
|
|
29
|
+
}
|
|
30
|
+
const page = Number(args.page ?? 1);
|
|
31
|
+
if (!Number.isInteger(page) || page <= 0) {
|
|
32
|
+
throw new ArgumentError('coingecko page must be a positive integer');
|
|
33
|
+
}
|
|
34
|
+
const url = new URL('https://api.coingecko.com/api/v3/exchanges');
|
|
35
|
+
url.searchParams.set('per_page', String(limit));
|
|
36
|
+
url.searchParams.set('page', String(page));
|
|
37
|
+
let resp;
|
|
38
|
+
try {
|
|
39
|
+
resp = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0' } });
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
throw new CommandExecutionError(`coingecko exchanges request failed: ${err?.message ?? err}`);
|
|
43
|
+
}
|
|
44
|
+
if (resp.status === 429) {
|
|
45
|
+
throw new CommandExecutionError(
|
|
46
|
+
'coingecko returned HTTP 429 (rate limited)',
|
|
47
|
+
'Free tier allows ~30 calls/min. Wait and retry.',
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
if (!resp.ok) {
|
|
51
|
+
throw new CommandExecutionError(`coingecko exchanges returned HTTP ${resp.status}`);
|
|
52
|
+
}
|
|
53
|
+
let data;
|
|
54
|
+
try {
|
|
55
|
+
data = await resp.json();
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
throw new CommandExecutionError(`coingecko exchanges returned malformed JSON: ${err?.message ?? err}`);
|
|
59
|
+
}
|
|
60
|
+
if (!Array.isArray(data) || !data.length) {
|
|
61
|
+
throw new EmptyResultError('coingecko exchanges', 'CoinGecko returned no exchange data.');
|
|
62
|
+
}
|
|
63
|
+
return data.map((ex, i) => ({
|
|
64
|
+
rank: (page - 1) * limit + i + 1,
|
|
65
|
+
id: String(ex.id ?? ''),
|
|
66
|
+
name: String(ex.name ?? ''),
|
|
67
|
+
trustScore: ex.trust_score != null ? Number(ex.trust_score) : null,
|
|
68
|
+
volume24hBtc: ex.trade_volume_24h_btc != null ? Number(ex.trade_volume_24h_btc) : null,
|
|
69
|
+
country: String(ex.country ?? ''),
|
|
70
|
+
yearEstablished: ex.year_established != null ? Number(ex.year_established) : null,
|
|
71
|
+
url: String(ex.url ?? ''),
|
|
72
|
+
}));
|
|
73
|
+
},
|
|
74
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// coingecko global — total crypto market cap, volume, BTC/ETH dominance,
|
|
2
|
+
// active currencies, ICO counts, in a single row.
|
|
3
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
4
|
+
import { ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
5
|
+
|
|
6
|
+
cli({
|
|
7
|
+
site: 'coingecko',
|
|
8
|
+
name: 'global',
|
|
9
|
+
access: 'read',
|
|
10
|
+
description: 'Aggregate crypto market stats: total market cap, volume, dominance',
|
|
11
|
+
domain: 'api.coingecko.com',
|
|
12
|
+
strategy: Strategy.PUBLIC,
|
|
13
|
+
browser: false,
|
|
14
|
+
args: [
|
|
15
|
+
{ name: 'currency', type: 'string', default: 'usd', help: 'Quote currency for total market cap / volume (usd, cny, eur, jpy, ...)' },
|
|
16
|
+
],
|
|
17
|
+
columns: ['currency', 'totalMarketCap', 'totalVolume24h', 'marketCapChange24hPct', 'btcDominancePct', 'ethDominancePct', 'activeCryptocurrencies', 'markets', 'ongoingIcos', 'updatedAt'],
|
|
18
|
+
func: async (args) => {
|
|
19
|
+
const currency = String(args.currency ?? 'usd').trim().toLowerCase();
|
|
20
|
+
if (!/^[a-z0-9-]{2,20}$/.test(currency)) {
|
|
21
|
+
throw new ArgumentError(`coingecko currency must look like a currency slug (got "${args.currency}")`);
|
|
22
|
+
}
|
|
23
|
+
let resp;
|
|
24
|
+
try {
|
|
25
|
+
resp = await fetch('https://api.coingecko.com/api/v3/global', { headers: { 'User-Agent': 'Mozilla/5.0' } });
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
throw new CommandExecutionError(`coingecko global request failed: ${err?.message ?? err}`);
|
|
29
|
+
}
|
|
30
|
+
if (resp.status === 429) {
|
|
31
|
+
throw new CommandExecutionError(
|
|
32
|
+
'coingecko returned HTTP 429 (rate limited)',
|
|
33
|
+
'Free tier allows ~30 calls/min. Wait and retry.',
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
if (!resp.ok) {
|
|
37
|
+
throw new CommandExecutionError(`coingecko global returned HTTP ${resp.status}`);
|
|
38
|
+
}
|
|
39
|
+
let body;
|
|
40
|
+
try {
|
|
41
|
+
body = await resp.json();
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
throw new CommandExecutionError(`coingecko global returned malformed JSON: ${err?.message ?? err}`);
|
|
45
|
+
}
|
|
46
|
+
const data = body?.data;
|
|
47
|
+
if (!data) {
|
|
48
|
+
throw new CommandExecutionError('coingecko global returned no data envelope');
|
|
49
|
+
}
|
|
50
|
+
const totalMarketCap = data?.total_market_cap?.[currency];
|
|
51
|
+
const totalVolume = data?.total_volume?.[currency];
|
|
52
|
+
if (totalMarketCap == null && totalVolume == null) {
|
|
53
|
+
throw new ArgumentError(
|
|
54
|
+
`coingecko has no market totals for currency "${currency}"`,
|
|
55
|
+
'Use a CoinGecko-supported quote currency such as usd, cny, eur, or jpy.',
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return [{
|
|
59
|
+
currency: currency.toUpperCase(),
|
|
60
|
+
totalMarketCap: totalMarketCap != null ? Number(totalMarketCap) : null,
|
|
61
|
+
totalVolume24h: totalVolume != null ? Number(totalVolume) : null,
|
|
62
|
+
marketCapChange24hPct: data.market_cap_change_percentage_24h_usd != null ? Number(data.market_cap_change_percentage_24h_usd) : null,
|
|
63
|
+
btcDominancePct: data?.market_cap_percentage?.btc != null ? Number(data.market_cap_percentage.btc) : null,
|
|
64
|
+
ethDominancePct: data?.market_cap_percentage?.eth != null ? Number(data.market_cap_percentage.eth) : null,
|
|
65
|
+
activeCryptocurrencies: data.active_cryptocurrencies != null ? Number(data.active_cryptocurrencies) : null,
|
|
66
|
+
markets: data.markets != null ? Number(data.markets) : null,
|
|
67
|
+
ongoingIcos: data.ongoing_icos != null ? Number(data.ongoing_icos) : null,
|
|
68
|
+
updatedAt: data.updated_at ? new Date(data.updated_at * 1000).toISOString() : '',
|
|
69
|
+
}];
|
|
70
|
+
},
|
|
71
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// coingecko top — top coins by market cap.
|
|
2
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
3
|
+
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
4
|
+
|
|
5
|
+
cli({
|
|
6
|
+
site: 'coingecko',
|
|
7
|
+
name: 'top',
|
|
8
|
+
access: 'read',
|
|
9
|
+
description: '按市值排序的加密货币行情(默认 USD)',
|
|
10
|
+
domain: 'api.coingecko.com',
|
|
11
|
+
strategy: Strategy.PUBLIC,
|
|
12
|
+
browser: false,
|
|
13
|
+
args: [
|
|
14
|
+
{ name: 'currency', type: 'string', default: 'usd', help: '计价币种 (usd / cny / eur / jpy ...)' },
|
|
15
|
+
{ name: 'limit', type: 'int', default: 10, help: '返回数量(默认 10,最多 250)' },
|
|
16
|
+
],
|
|
17
|
+
columns: ['rank', 'symbol', 'name', 'price', 'change24hPct', 'marketCap', 'volume24h', 'high24h', 'low24h'],
|
|
18
|
+
func: async (args) => {
|
|
19
|
+
const currency = String(args.currency ?? 'usd').toLowerCase();
|
|
20
|
+
const limit = Number(args.limit ?? 10);
|
|
21
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
22
|
+
throw new ArgumentError('limit must be a positive integer');
|
|
23
|
+
}
|
|
24
|
+
if (limit > 250) {
|
|
25
|
+
throw new ArgumentError('limit must be <= 250 (CoinGecko per_page upper bound)');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const url = new URL('https://api.coingecko.com/api/v3/coins/markets');
|
|
29
|
+
url.searchParams.set('vs_currency', currency);
|
|
30
|
+
url.searchParams.set('order', 'market_cap_desc');
|
|
31
|
+
url.searchParams.set('per_page', String(limit));
|
|
32
|
+
url.searchParams.set('page', '1');
|
|
33
|
+
url.searchParams.set('sparkline', 'false');
|
|
34
|
+
|
|
35
|
+
let resp;
|
|
36
|
+
try {
|
|
37
|
+
resp = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0' } });
|
|
38
|
+
} catch (error) {
|
|
39
|
+
throw new CommandExecutionError(`coingecko top request failed: ${error?.message || error}`);
|
|
40
|
+
}
|
|
41
|
+
if (!resp.ok) throw new CommandExecutionError(`coingecko top failed: HTTP ${resp.status}`);
|
|
42
|
+
let data;
|
|
43
|
+
try {
|
|
44
|
+
data = await resp.json();
|
|
45
|
+
} catch (error) {
|
|
46
|
+
throw new CommandExecutionError(`coingecko returned malformed JSON: ${error?.message || error}`);
|
|
47
|
+
}
|
|
48
|
+
if (data?.error) throw new CommandExecutionError(`coingecko returned error: ${data.error}`);
|
|
49
|
+
if (!Array.isArray(data)) throw new CommandExecutionError('coingecko returned an unexpected response');
|
|
50
|
+
if (data.length === 0) throw new EmptyResultError('coingecko top', 'coingecko returned no market data');
|
|
51
|
+
|
|
52
|
+
return data.map((c) => ({
|
|
53
|
+
rank: c.market_cap_rank,
|
|
54
|
+
symbol: String(c.symbol ?? '').toUpperCase(),
|
|
55
|
+
name: c.name,
|
|
56
|
+
price: c.current_price,
|
|
57
|
+
change24hPct: c.price_change_percentage_24h,
|
|
58
|
+
marketCap: c.market_cap,
|
|
59
|
+
volume24h: c.total_volume,
|
|
60
|
+
high24h: c.high_24h,
|
|
61
|
+
low24h: c.low_24h,
|
|
62
|
+
}));
|
|
63
|
+
},
|
|
64
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// coingecko trending — top trending coins on CoinGecko (by user search activity).
|
|
2
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
3
|
+
import {
|
|
4
|
+
CommandExecutionError,
|
|
5
|
+
EmptyResultError,
|
|
6
|
+
} from '@jackwener/opencli/errors';
|
|
7
|
+
|
|
8
|
+
cli({
|
|
9
|
+
site: 'coingecko',
|
|
10
|
+
name: 'trending',
|
|
11
|
+
access: 'read',
|
|
12
|
+
description: 'Top trending cryptocurrencies on CoinGecko in the last 24h (search-volume based).',
|
|
13
|
+
domain: 'api.coingecko.com',
|
|
14
|
+
strategy: Strategy.PUBLIC,
|
|
15
|
+
browser: false,
|
|
16
|
+
args: [],
|
|
17
|
+
columns: ['rank', 'id', 'symbol', 'name', 'marketCapRank', 'priceBtc', 'thumb'],
|
|
18
|
+
func: async () => {
|
|
19
|
+
const url = 'https://api.coingecko.com/api/v3/search/trending';
|
|
20
|
+
let resp;
|
|
21
|
+
try {
|
|
22
|
+
resp = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0' } });
|
|
23
|
+
} catch (error) {
|
|
24
|
+
throw new CommandExecutionError(`coingecko trending request failed: ${error?.message || error}`);
|
|
25
|
+
}
|
|
26
|
+
if (resp.status === 429) {
|
|
27
|
+
throw new CommandExecutionError('coingecko returned HTTP 429 (rate limited)', 'Wait and retry.');
|
|
28
|
+
}
|
|
29
|
+
if (!resp.ok) {
|
|
30
|
+
throw new CommandExecutionError(`coingecko trending failed: HTTP ${resp.status}`);
|
|
31
|
+
}
|
|
32
|
+
let data;
|
|
33
|
+
try {
|
|
34
|
+
data = await resp.json();
|
|
35
|
+
} catch (error) {
|
|
36
|
+
throw new CommandExecutionError(`coingecko returned malformed JSON: ${error?.message || error}`);
|
|
37
|
+
}
|
|
38
|
+
const coins = Array.isArray(data?.coins) ? data.coins : [];
|
|
39
|
+
if (coins.length === 0) {
|
|
40
|
+
throw new EmptyResultError('coingecko trending', 'coingecko returned no trending coins.');
|
|
41
|
+
}
|
|
42
|
+
return coins.map((entry, i) => {
|
|
43
|
+
const c = entry?.item || {};
|
|
44
|
+
return {
|
|
45
|
+
rank: i + 1,
|
|
46
|
+
id: c.id || '',
|
|
47
|
+
symbol: String(c.symbol || '').toUpperCase(),
|
|
48
|
+
name: c.name || '',
|
|
49
|
+
marketCapRank: c.market_cap_rank ?? null,
|
|
50
|
+
priceBtc: c.price_btc ?? null,
|
|
51
|
+
thumb: c.thumb || c.small || c.large || '',
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
},
|
|
55
|
+
});
|