@jackwener/opencli 1.7.12 → 1.7.13
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 +12194 -6843
- 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/reply-dm.js +1 -1
- package/clis/twitter/reply.test.js +1 -29
- 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/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/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 +150 -0
- package/dist/src/commanderAdapter.js +0 -5
- 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 +23 -2
- package/dist/src/help.js +41 -19
- 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,309 @@
|
|
|
1
|
+
import { ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
2
|
+
|
|
3
|
+
export const EUTILS_BASE = 'https://eutils.ncbi.nlm.nih.gov/entrez/eutils';
|
|
4
|
+
export const SEARCH_COLUMNS = ['rank', 'pmid', 'title', 'authors', 'journal', 'year', 'article_type', 'doi', 'url'];
|
|
5
|
+
export const LINK_COLUMNS = ['rank', 'pmid', 'title', 'authors', 'journal', 'year', 'article_type', 'doi', 'url'];
|
|
6
|
+
export const RELATED_COLUMNS = ['rank', 'pmid', 'title', 'authors', 'journal', 'year', 'article_type', 'score', 'doi', 'url'];
|
|
7
|
+
|
|
8
|
+
let lastRequestAt = 0;
|
|
9
|
+
|
|
10
|
+
export function requireText(value, label) {
|
|
11
|
+
const text = String(value ?? '').trim();
|
|
12
|
+
if (!text) {
|
|
13
|
+
throw new ArgumentError(`pubmed ${label} cannot be empty`);
|
|
14
|
+
}
|
|
15
|
+
return text;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function requirePmid(value, label = 'pmid') {
|
|
19
|
+
const pmid = requireText(value, label);
|
|
20
|
+
if (!/^\d+$/.test(pmid)) {
|
|
21
|
+
throw new ArgumentError(`pubmed ${label} must be a numeric PMID`, 'Example: 37780221');
|
|
22
|
+
}
|
|
23
|
+
return pmid;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function requireBoundedInt(value, defaultValue, maxValue, label = 'limit') {
|
|
27
|
+
const raw = value ?? defaultValue;
|
|
28
|
+
const text = String(raw).trim();
|
|
29
|
+
if (!/^\d+$/.test(text)) {
|
|
30
|
+
throw new ArgumentError(`pubmed ${label} must be a positive integer`);
|
|
31
|
+
}
|
|
32
|
+
const n = Number(text);
|
|
33
|
+
if (!Number.isSafeInteger(n) || n < 1) {
|
|
34
|
+
throw new ArgumentError(`pubmed ${label} must be a positive integer`);
|
|
35
|
+
}
|
|
36
|
+
if (n > maxValue) {
|
|
37
|
+
throw new ArgumentError(`pubmed ${label} must be <= ${maxValue}`);
|
|
38
|
+
}
|
|
39
|
+
return n;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function requireYear(value, label) {
|
|
43
|
+
if (value === undefined || value === null || value === '') {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const year = requireBoundedInt(value, 1900, 3000, label);
|
|
47
|
+
if (year < 1800) {
|
|
48
|
+
throw new ArgumentError(`pubmed ${label} must be >= 1800`);
|
|
49
|
+
}
|
|
50
|
+
return year;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function requireChoice(value, choices, label, defaultValue) {
|
|
54
|
+
const text = String(value ?? defaultValue).trim();
|
|
55
|
+
if (!choices.includes(text)) {
|
|
56
|
+
throw new ArgumentError(`pubmed ${label} must be one of: ${choices.join(', ')}`);
|
|
57
|
+
}
|
|
58
|
+
return text;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function buildEutilsUrl(tool, params = {}) {
|
|
62
|
+
const searchParams = new URLSearchParams();
|
|
63
|
+
searchParams.set('db', 'pubmed');
|
|
64
|
+
if (!params.retmode) {
|
|
65
|
+
searchParams.set('retmode', 'json');
|
|
66
|
+
}
|
|
67
|
+
if (process.env.NCBI_API_KEY) {
|
|
68
|
+
searchParams.set('api_key', process.env.NCBI_API_KEY);
|
|
69
|
+
}
|
|
70
|
+
if (process.env.NCBI_EMAIL) {
|
|
71
|
+
searchParams.set('email', process.env.NCBI_EMAIL);
|
|
72
|
+
}
|
|
73
|
+
for (const [key, value] of Object.entries(params)) {
|
|
74
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
75
|
+
searchParams.set(key, String(value));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return `${EUTILS_BASE}/${tool}.fcgi?${searchParams.toString()}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function waitForRateLimit() {
|
|
82
|
+
if (process.env.NODE_ENV === 'test') {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const delayMs = process.env.NCBI_API_KEY ? 110 : 360;
|
|
86
|
+
const now = Date.now();
|
|
87
|
+
const waitMs = Math.max(0, lastRequestAt + delayMs - now);
|
|
88
|
+
if (waitMs > 0) {
|
|
89
|
+
await new Promise(resolve => setTimeout(resolve, waitMs));
|
|
90
|
+
}
|
|
91
|
+
lastRequestAt = Date.now();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function eutilsFetch(tool, params = {}, { retmode = 'json', label = 'PubMed E-utilities' } = {}) {
|
|
95
|
+
const url = buildEutilsUrl(tool, { ...params, retmode });
|
|
96
|
+
await waitForRateLimit();
|
|
97
|
+
let response;
|
|
98
|
+
try {
|
|
99
|
+
response = await fetch(url);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
103
|
+
throw new CommandExecutionError(`${label} request failed`, detail);
|
|
104
|
+
}
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw new CommandExecutionError(`${label} HTTP ${response.status}`, 'Check NCBI availability, request parameters, and optional NCBI_API_KEY.');
|
|
107
|
+
}
|
|
108
|
+
if (retmode === 'xml') {
|
|
109
|
+
return response.text();
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
const json = await response.json();
|
|
113
|
+
assertNoEutilsError(json, label);
|
|
114
|
+
return json;
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (error instanceof CommandExecutionError) {
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
121
|
+
throw new CommandExecutionError(`${label} returned invalid JSON`, detail);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function assertNoEutilsError(json, label = 'PubMed E-utilities') {
|
|
126
|
+
const error = json?.error
|
|
127
|
+
|| json?.esearchresult?.errorlist?.phrasesnotfound?.join(', ')
|
|
128
|
+
|| json?.esearchresult?.errorlist?.fieldsnotfound?.join(', ');
|
|
129
|
+
if (error) {
|
|
130
|
+
throw new CommandExecutionError(`${label} returned an error`, String(error));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function buildPubMedUrl(pmid) {
|
|
135
|
+
return `https://pubmed.ncbi.nlm.nih.gov/${pmid}/`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function decodeXmlEntities(value) {
|
|
139
|
+
return String(value ?? '')
|
|
140
|
+
.replace(/&/g, '&')
|
|
141
|
+
.replace(/</g, '<')
|
|
142
|
+
.replace(/>/g, '>')
|
|
143
|
+
.replace(/"/g, '"')
|
|
144
|
+
.replace(/'/g, "'")
|
|
145
|
+
.replace(/'/g, "'")
|
|
146
|
+
.replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCodePoint(Number.parseInt(hex, 16)))
|
|
147
|
+
.replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(Number.parseInt(dec, 10)));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function cleanText(value) {
|
|
151
|
+
return decodeXmlEntities(value).replace(/\s+/g, ' ').trim();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function truncateText(value, maxLength) {
|
|
155
|
+
const text = cleanText(value);
|
|
156
|
+
if (!text || text.length <= maxLength) {
|
|
157
|
+
return text;
|
|
158
|
+
}
|
|
159
|
+
return `${text.slice(0, maxLength - 3)}...`;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function extractFirst(xml, tag) {
|
|
163
|
+
const match = String(xml ?? '').match(new RegExp(`<${tag}\\b[^>]*>([\\s\\S]*?)<\\/${tag}>`, 'i'));
|
|
164
|
+
return match ? cleanText(match[1].replace(/<[^>]+>/g, ' ')) : '';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function extractAll(xml, tag) {
|
|
168
|
+
const re = new RegExp(`<${tag}\\b[^>]*>([\\s\\S]*?)<\\/${tag}>`, 'gi');
|
|
169
|
+
const out = [];
|
|
170
|
+
let match;
|
|
171
|
+
while ((match = re.exec(String(xml ?? ''))) !== null) {
|
|
172
|
+
out.push(cleanText(match[1].replace(/<[^>]+>/g, ' ')));
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function extractAttribute(xml, tag, attr) {
|
|
178
|
+
const match = String(xml ?? '').match(new RegExp(`<${tag}\\b[^>]*\\b${attr}="([^"]*)"`, 'i'));
|
|
179
|
+
return match ? decodeXmlEntities(match[1]) : '';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function extractAuthors(authorList, maxAuthors = 3) {
|
|
183
|
+
if (!Array.isArray(authorList) || authorList.length === 0) {
|
|
184
|
+
return '';
|
|
185
|
+
}
|
|
186
|
+
const names = authorList.map(author => author?.name || author?.collectivename || [author?.lastname, author?.initials].filter(Boolean).join(' ')).filter(Boolean);
|
|
187
|
+
const shown = names.slice(0, maxAuthors);
|
|
188
|
+
if (names.length > maxAuthors) {
|
|
189
|
+
shown.push('et al.');
|
|
190
|
+
}
|
|
191
|
+
return shown.join(', ');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function extractDoi(articleIds) {
|
|
195
|
+
if (!Array.isArray(articleIds)) {
|
|
196
|
+
return '';
|
|
197
|
+
}
|
|
198
|
+
const doi = articleIds.find(id => String(id?.idtype ?? '').toLowerCase() === 'doi');
|
|
199
|
+
return String(doi?.value ?? '').trim();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function articleTypeFromList(types) {
|
|
203
|
+
const values = Array.isArray(types)
|
|
204
|
+
? types.map(type => typeof type === 'string' ? type : type?.value).filter(Boolean)
|
|
205
|
+
: [];
|
|
206
|
+
const priority = ['Systematic Review', 'Meta-Analysis', 'Review', 'Randomized Controlled Trial', 'Clinical Trial', 'Case Reports', 'Journal Article'];
|
|
207
|
+
for (const wanted of priority) {
|
|
208
|
+
const found = values.find(type => type.toLowerCase() === wanted.toLowerCase());
|
|
209
|
+
if (found) {
|
|
210
|
+
return found;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return values[0] || 'Journal Article';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function summaryToRow(article, rank, pmid = article?.uid) {
|
|
217
|
+
const id = String(pmid ?? article?.uid ?? '').trim();
|
|
218
|
+
return {
|
|
219
|
+
rank,
|
|
220
|
+
pmid: id,
|
|
221
|
+
title: truncateText(String(article?.title ?? '').replace(/\.$/, ''), 120),
|
|
222
|
+
authors: extractAuthors(article?.authors, 3),
|
|
223
|
+
journal: truncateText(article?.fulljournalname || article?.source || '', 60),
|
|
224
|
+
year: String(article?.pubdate ?? '').split(' ')[0] || '',
|
|
225
|
+
article_type: articleTypeFromList(article?.pubtype),
|
|
226
|
+
doi: extractDoi(article?.articleids),
|
|
227
|
+
url: buildPubMedUrl(id),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function ensureCompleteSummaryRows(pmids, result, commandLabel) {
|
|
232
|
+
if (!result || typeof result !== 'object' || !result.result || typeof result.result !== 'object') {
|
|
233
|
+
throw new CommandExecutionError(`${commandLabel} returned an unreadable summary payload`);
|
|
234
|
+
}
|
|
235
|
+
const rows = pmids.map((pmid, index) => {
|
|
236
|
+
const article = result.result[pmid];
|
|
237
|
+
if (!article) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
return summaryToRow(article, index + 1, pmid);
|
|
241
|
+
});
|
|
242
|
+
if (rows.some(row => row === null)) {
|
|
243
|
+
throw new CommandExecutionError(`${commandLabel} omitted summaries for one or more PMIDs`, 'Refusing to return a partial result set.');
|
|
244
|
+
}
|
|
245
|
+
return rows;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function buildSearchQuery(query, filters = {}) {
|
|
249
|
+
const terms = [requireText(query, 'query')];
|
|
250
|
+
if (filters.author) terms.push(`${requireText(filters.author, 'author')}[Author]`);
|
|
251
|
+
if (filters.journal) terms.push(`${requireText(filters.journal, 'journal')}[Journal]`);
|
|
252
|
+
if (filters.yearFrom || filters.yearTo) {
|
|
253
|
+
const from = filters.yearFrom || 1800;
|
|
254
|
+
const to = filters.yearTo || new Date().getFullYear();
|
|
255
|
+
if (from > to) {
|
|
256
|
+
throw new ArgumentError('pubmed year-from must be <= year-to');
|
|
257
|
+
}
|
|
258
|
+
terms.push(`${from}:${to}[PDAT]`);
|
|
259
|
+
}
|
|
260
|
+
if (filters.articleType) terms.push(`${requireText(filters.articleType, 'article-type')}[PT]`);
|
|
261
|
+
if (filters.hasAbstract) terms.push('hasabstract[text]');
|
|
262
|
+
if (filters.hasFullText) terms.push('free full text[sb]');
|
|
263
|
+
if (filters.humanOnly) terms.push('humans[mesh]');
|
|
264
|
+
if (filters.englishOnly) terms.push('english[lang]');
|
|
265
|
+
return terms.join(' AND ');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function parseArticleXml(xml, pmid) {
|
|
269
|
+
const text = String(xml ?? '');
|
|
270
|
+
if (!text || /<ERROR\b/i.test(text) || !/<PubmedArticle\b/i.test(text)) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
const articleBlock = text.match(/<Article\b[^>]*>([\s\S]*?)<\/Article>/i)?.[1] || text;
|
|
274
|
+
const journalBlock = articleBlock.match(/<Journal\b[^>]*>([\s\S]*?)<\/Journal>/i)?.[1] || '';
|
|
275
|
+
const journalIssue = journalBlock.match(/<JournalIssue\b[^>]*>([\s\S]*?)<\/JournalIssue>/i)?.[1] || '';
|
|
276
|
+
const pubDate = journalIssue.match(/<PubDate\b[^>]*>([\s\S]*?)<\/PubDate>/i)?.[1] || '';
|
|
277
|
+
const authorBlocks = [...text.matchAll(/<Author\b[^>]*>([\s\S]*?)<\/Author>/gi)].map(match => match[1]);
|
|
278
|
+
const authors = authorBlocks.map(block => {
|
|
279
|
+
const name = extractFirst(block, 'CollectiveName') || [extractFirst(block, 'LastName'), extractFirst(block, 'ForeName') || extractFirst(block, 'Initials')].filter(Boolean).join(' ');
|
|
280
|
+
return name;
|
|
281
|
+
}).filter(Boolean);
|
|
282
|
+
const abstract = extractAll(articleBlock, 'AbstractText').join(' ');
|
|
283
|
+
const pubTypes = extractAll(articleBlock, 'PublicationType');
|
|
284
|
+
const meshTerms = extractAll(text, 'DescriptorName');
|
|
285
|
+
const keywords = extractAll(text, 'Keyword');
|
|
286
|
+
const doi = text.match(/<ArticleId\b[^>]*IdType="doi"[^>]*>([\s\S]*?)<\/ArticleId>/i)?.[1] || '';
|
|
287
|
+
const pmc = text.match(/<ArticleId\b[^>]*IdType="pmc"[^>]*>([\s\S]*?)<\/ArticleId>/i)?.[1] || '';
|
|
288
|
+
return {
|
|
289
|
+
pmid,
|
|
290
|
+
title: extractFirst(articleBlock, 'ArticleTitle'),
|
|
291
|
+
abstract,
|
|
292
|
+
authors,
|
|
293
|
+
journal: extractFirst(journalBlock, 'Title') || extractFirst(journalBlock, 'ISOAbbreviation'),
|
|
294
|
+
year: extractFirst(pubDate, 'Year') || extractFirst(text, 'MedlineDate').slice(0, 4),
|
|
295
|
+
date: [extractFirst(pubDate, 'Year'), extractFirst(pubDate, 'Month'), extractFirst(pubDate, 'Day')].filter(Boolean).join(' '),
|
|
296
|
+
doi: cleanText(doi),
|
|
297
|
+
pmc: cleanText(pmc),
|
|
298
|
+
article_type: articleTypeFromList(pubTypes),
|
|
299
|
+
language: extractFirst(articleBlock, 'Language'),
|
|
300
|
+
mesh_terms: meshTerms.slice(0, 10).join(', '),
|
|
301
|
+
keywords: keywords.slice(0, 10).join(', '),
|
|
302
|
+
url: buildPubMedUrl(pmid),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export async function fetchSummaryRows(pmids, commandLabel) {
|
|
307
|
+
const result = await eutilsFetch('esummary', { id: pmids.join(',') }, { label: commandLabel });
|
|
308
|
+
return ensureCompleteSummaryRows(pmids, result, commandLabel);
|
|
309
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// pypi downloads — fetch download counts for a single PyPI package via
|
|
2
|
+
// pypistats.org's public JSON API.
|
|
3
|
+
//
|
|
4
|
+
// Default endpoint is `/api/packages/<pkg>/recent` which returns last-day /
|
|
5
|
+
// last-week / last-month totals as a single row. Pass `--period overall` to
|
|
6
|
+
// hit `/api/packages/<pkg>/overall` for the full daily history (one row per
|
|
7
|
+
// day).
|
|
8
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
9
|
+
import { ArgumentError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
10
|
+
import { PYPISTATS_BASE, pypiFetch, requirePackageName } from './utils.js';
|
|
11
|
+
|
|
12
|
+
const PERIODS = new Set(['recent', 'overall']);
|
|
13
|
+
|
|
14
|
+
function requirePeriod(value) {
|
|
15
|
+
const s = String(value ?? 'recent').trim().toLowerCase();
|
|
16
|
+
if (!PERIODS.has(s)) {
|
|
17
|
+
throw new ArgumentError(
|
|
18
|
+
`pypi downloads period "${value}" is invalid`,
|
|
19
|
+
'Allowed values: recent (default — last day/week/month totals) or overall (full daily history).',
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
return s;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
cli({
|
|
26
|
+
site: 'pypi',
|
|
27
|
+
name: 'downloads',
|
|
28
|
+
access: 'read',
|
|
29
|
+
description: 'PyPI download stats for a package (recent totals or full daily history)',
|
|
30
|
+
domain: 'pypistats.org',
|
|
31
|
+
strategy: Strategy.PUBLIC,
|
|
32
|
+
browser: false,
|
|
33
|
+
args: [
|
|
34
|
+
{ name: 'name', positional: true, required: true, help: 'PyPI package name (e.g. "requests", "pandas")' },
|
|
35
|
+
{ name: 'period', default: 'recent', help: 'recent (default — 1 row, last day/week/month) or overall (1 row per day)' },
|
|
36
|
+
],
|
|
37
|
+
columns: ['rank', 'package', 'period', 'date', 'downloads'],
|
|
38
|
+
func: async (args) => {
|
|
39
|
+
const name = requirePackageName(args.name);
|
|
40
|
+
const period = requirePeriod(args.period);
|
|
41
|
+
if (period === 'recent') {
|
|
42
|
+
const body = await pypiFetch(`${PYPISTATS_BASE}/api/packages/${encodeURIComponent(name)}/recent`, `pypi downloads ${name}`);
|
|
43
|
+
const data = body?.data;
|
|
44
|
+
if (!data || (data.last_day == null && data.last_week == null && data.last_month == null)) {
|
|
45
|
+
throw new EmptyResultError('pypi downloads', `pypistats has no recent download data for "${name}".`);
|
|
46
|
+
}
|
|
47
|
+
return [
|
|
48
|
+
{ rank: 1, package: String(body.package ?? name), period: 'last_day', date: '', downloads: data.last_day != null ? Number(data.last_day) : null },
|
|
49
|
+
{ rank: 2, package: String(body.package ?? name), period: 'last_week', date: '', downloads: data.last_week != null ? Number(data.last_week) : null },
|
|
50
|
+
{ rank: 3, package: String(body.package ?? name), period: 'last_month', date: '', downloads: data.last_month != null ? Number(data.last_month) : null },
|
|
51
|
+
];
|
|
52
|
+
}
|
|
53
|
+
const body = await pypiFetch(`${PYPISTATS_BASE}/api/packages/${encodeURIComponent(name)}/overall?mirrors=false`, `pypi downloads ${name}`);
|
|
54
|
+
const days = Array.isArray(body?.data) ? body.data : [];
|
|
55
|
+
if (!days.length) {
|
|
56
|
+
throw new EmptyResultError('pypi downloads', `pypistats has no overall download history for "${name}".`);
|
|
57
|
+
}
|
|
58
|
+
return days.map((row, i) => ({
|
|
59
|
+
rank: i + 1,
|
|
60
|
+
package: String(body.package ?? name),
|
|
61
|
+
period: 'daily',
|
|
62
|
+
date: String(row.date ?? ''),
|
|
63
|
+
downloads: row.downloads != null ? Number(row.downloads) : null,
|
|
64
|
+
}));
|
|
65
|
+
},
|
|
66
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// pypi package — fetch a single PyPI package's metadata.
|
|
2
|
+
//
|
|
3
|
+
// Hits `https://pypi.org/pypi/<pkg>/json`. Returns the most agent-useful
|
|
4
|
+
// projection: name, latest version, summary, author, license, homepage,
|
|
5
|
+
// project URLs, requires-python, last-modified time. Download stats are
|
|
6
|
+
// intentionally separate (see `pypi downloads`).
|
|
7
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
8
|
+
import { EmptyResultError } from '@jackwener/opencli/errors';
|
|
9
|
+
import { PYPI_BASE, pypiFetch, requirePackageName } from './utils.js';
|
|
10
|
+
|
|
11
|
+
function pickHomepage(info) {
|
|
12
|
+
if (info.home_page) return String(info.home_page);
|
|
13
|
+
const proj = info.project_urls;
|
|
14
|
+
if (proj && typeof proj === 'object') {
|
|
15
|
+
return String(proj.Homepage || proj.homepage || proj.Documentation || proj.Source || proj['Source Code'] || '');
|
|
16
|
+
}
|
|
17
|
+
return '';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function pickRepository(info) {
|
|
21
|
+
const proj = info.project_urls;
|
|
22
|
+
if (proj && typeof proj === 'object') {
|
|
23
|
+
return String(proj.Source || proj['Source Code'] || proj.Repository || proj.repository || '');
|
|
24
|
+
}
|
|
25
|
+
return '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
cli({
|
|
29
|
+
site: 'pypi',
|
|
30
|
+
name: 'package',
|
|
31
|
+
access: 'read',
|
|
32
|
+
description: 'Single PyPI package metadata (latest version, license, homepage, classifiers)',
|
|
33
|
+
domain: 'pypi.org',
|
|
34
|
+
strategy: Strategy.PUBLIC,
|
|
35
|
+
browser: false,
|
|
36
|
+
args: [
|
|
37
|
+
{ name: 'name', positional: true, required: true, help: 'PyPI package name (e.g. "requests", "pandas")' },
|
|
38
|
+
],
|
|
39
|
+
columns: [
|
|
40
|
+
'name', 'latestVersion', 'summary', 'author', 'license', 'homepage', 'repository',
|
|
41
|
+
'requiresPython', 'keywords', 'releases', 'firstReleased', 'lastReleased', 'url',
|
|
42
|
+
],
|
|
43
|
+
func: async (args) => {
|
|
44
|
+
const name = requirePackageName(args.name);
|
|
45
|
+
const body = await pypiFetch(`${PYPI_BASE}/pypi/${encodeURIComponent(name)}/json`, `pypi package ${name}`);
|
|
46
|
+
const info = body?.info;
|
|
47
|
+
if (!info || !info.name) {
|
|
48
|
+
throw new EmptyResultError('pypi package', `PyPI returned no metadata for "${name}".`);
|
|
49
|
+
}
|
|
50
|
+
const releases = body?.releases ?? {};
|
|
51
|
+
const releaseVersions = Object.keys(releases).filter((v) => Array.isArray(releases[v]) && releases[v].length > 0);
|
|
52
|
+
// earliest / latest release timestamps from the upload_time fields
|
|
53
|
+
let firstReleased = '';
|
|
54
|
+
let lastReleased = '';
|
|
55
|
+
for (const v of releaseVersions) {
|
|
56
|
+
for (const file of releases[v]) {
|
|
57
|
+
const t = String(file?.upload_time ?? '').slice(0, 10);
|
|
58
|
+
if (!t) continue;
|
|
59
|
+
if (!firstReleased || t < firstReleased) firstReleased = t;
|
|
60
|
+
if (!lastReleased || t > lastReleased) lastReleased = t;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return [{
|
|
64
|
+
name: String(info.name),
|
|
65
|
+
latestVersion: String(info.version ?? ''),
|
|
66
|
+
summary: String(info.summary ?? ''),
|
|
67
|
+
author: String(info.author ?? info.author_email ?? ''),
|
|
68
|
+
license: String(info.license_expression ?? info.license ?? ''),
|
|
69
|
+
homepage: pickHomepage(info),
|
|
70
|
+
repository: pickRepository(info),
|
|
71
|
+
requiresPython: String(info.requires_python ?? ''),
|
|
72
|
+
keywords: String(info.keywords ?? ''),
|
|
73
|
+
releases: releaseVersions.length,
|
|
74
|
+
firstReleased,
|
|
75
|
+
lastReleased,
|
|
76
|
+
url: String(info.package_url ?? `${PYPI_BASE}/project/${info.name}/`),
|
|
77
|
+
}];
|
|
78
|
+
},
|
|
79
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Shared helpers for the pypi adapters that hit the PyPI public JSON API
|
|
2
|
+
// (pypi.org/pypi/<pkg>/json) and pypistats.org for download stats.
|
|
3
|
+
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
4
|
+
|
|
5
|
+
export const PYPI_BASE = 'https://pypi.org';
|
|
6
|
+
export const PYPISTATS_BASE = 'https://pypistats.org';
|
|
7
|
+
const UA = 'opencli-pypi-adapter (+https://github.com/jackwener/opencli)';
|
|
8
|
+
|
|
9
|
+
// PEP 508 / PEP 426 normalized name: letters, digits, "._-", with leading-letter rule relaxed by PyPI.
|
|
10
|
+
const PKG_NAME = /^[A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
11
|
+
|
|
12
|
+
export function requirePackageName(value) {
|
|
13
|
+
const s = String(value ?? '').trim();
|
|
14
|
+
if (!s) throw new ArgumentError('pypi package name is required (e.g. "requests", "pandas")');
|
|
15
|
+
if (!PKG_NAME.test(s)) {
|
|
16
|
+
throw new ArgumentError(
|
|
17
|
+
`pypi package name "${value}" is not a valid distribution name`,
|
|
18
|
+
'PyPI accepts ASCII letters / digits / "._-" with no leading or trailing separator.',
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
return s;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function pypiFetch(url, label) {
|
|
25
|
+
let resp;
|
|
26
|
+
try {
|
|
27
|
+
resp = await fetch(url, { headers: { 'user-agent': UA, accept: 'application/json' } });
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
throw new CommandExecutionError(
|
|
31
|
+
`${label} request failed: ${err?.message ?? err}`,
|
|
32
|
+
'Check that pypi.org / pypistats.org are reachable from this network.',
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
if (resp.status === 404) {
|
|
36
|
+
throw new EmptyResultError(label, `PyPI returned 404 for ${url}.`);
|
|
37
|
+
}
|
|
38
|
+
if (resp.status === 429) {
|
|
39
|
+
throw new CommandExecutionError(
|
|
40
|
+
`${label} returned HTTP 429 (rate limited)`,
|
|
41
|
+
'PyPI throttles unauthenticated bursts; wait a few seconds and retry.',
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (!resp.ok) {
|
|
45
|
+
throw new CommandExecutionError(`${label} returned HTTP ${resp.status}`);
|
|
46
|
+
}
|
|
47
|
+
let body;
|
|
48
|
+
try {
|
|
49
|
+
body = await resp.json();
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
throw new CommandExecutionError(`${label} returned malformed JSON: ${err?.message ?? err}`);
|
|
53
|
+
}
|
|
54
|
+
return body;
|
|
55
|
+
}
|
package/clis/quark/mv.js
CHANGED
|
@@ -9,11 +9,11 @@ cli({
|
|
|
9
9
|
domain: 'pan.quark.cn',
|
|
10
10
|
strategy: Strategy.COOKIE,
|
|
11
11
|
defaultFormat: 'json',
|
|
12
|
-
timeoutSeconds: 120,
|
|
13
12
|
args: [
|
|
14
13
|
{ name: 'fids', required: true, positional: true, help: 'File IDs to move (comma-separated)' },
|
|
15
14
|
{ name: 'to', default: '', help: 'Destination folder path (required unless --to-fid is set)' },
|
|
16
15
|
{ name: 'to-fid', default: '', help: 'Destination folder ID (overrides --to)' },
|
|
16
|
+
{ name: 'timeout', type: 'int', required: false, default: 120, help: 'Max seconds for the overall command (default: 120)' },
|
|
17
17
|
],
|
|
18
18
|
func: async (page, kwargs) => {
|
|
19
19
|
const to = kwargs.to;
|
package/clis/quark/save.js
CHANGED
|
@@ -21,7 +21,6 @@ cli({
|
|
|
21
21
|
domain: 'pan.quark.cn',
|
|
22
22
|
strategy: Strategy.COOKIE,
|
|
23
23
|
defaultFormat: 'json',
|
|
24
|
-
timeoutSeconds: 120,
|
|
25
24
|
args: [
|
|
26
25
|
{ name: 'url', required: true, positional: true, help: 'Quark share URL or pwd_id' },
|
|
27
26
|
{ name: 'to', default: '', help: 'Destination folder path' },
|
|
@@ -29,6 +28,7 @@ cli({
|
|
|
29
28
|
{ name: 'fids', default: '', help: 'File IDs to save (comma-separated, from share-tree). Omit to save all.' },
|
|
30
29
|
{ name: 'stoken', default: '', help: 'Share token (from share-tree output, required with --fids)' },
|
|
31
30
|
{ name: 'passcode', default: '', help: 'Share passcode (if required)' },
|
|
31
|
+
{ name: 'timeout', type: 'int', required: false, default: 120, help: 'Max seconds for the overall command (default: 120)' },
|
|
32
32
|
],
|
|
33
33
|
func: async (page, kwargs) => {
|
|
34
34
|
const url = kwargs.url;
|
package/clis/qwen/ask.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { ArgumentError, CommandExecutionError, TimeoutError } from '@jackwener/opencli/errors';
|
|
3
|
+
import {
|
|
4
|
+
QIANWEN_DOMAIN,
|
|
5
|
+
QIANWEN_URL,
|
|
6
|
+
authRequired,
|
|
7
|
+
bubbleHtmlToMarkdown,
|
|
8
|
+
dismissLoginModal,
|
|
9
|
+
ensureOnQianwen,
|
|
10
|
+
getMessageBubbles,
|
|
11
|
+
hasLoginGate,
|
|
12
|
+
normalizeBooleanFlag,
|
|
13
|
+
sendMessage,
|
|
14
|
+
setFeatureToggle,
|
|
15
|
+
startNewChat,
|
|
16
|
+
waitForAnswer,
|
|
17
|
+
} from './utils.js';
|
|
18
|
+
|
|
19
|
+
cli({
|
|
20
|
+
site: 'qwen',
|
|
21
|
+
name: 'ask',
|
|
22
|
+
access: 'write',
|
|
23
|
+
description: 'Send a prompt to Qianwen and return the assistant reply',
|
|
24
|
+
domain: QIANWEN_DOMAIN,
|
|
25
|
+
strategy: Strategy.COOKIE,
|
|
26
|
+
browser: true,
|
|
27
|
+
browserSession: { reuse: 'site' },
|
|
28
|
+
navigateBefore: false,
|
|
29
|
+
defaultFormat: 'plain',
|
|
30
|
+
args: [
|
|
31
|
+
{ name: 'prompt', required: true, positional: true, help: 'Prompt to send to Qianwen' },
|
|
32
|
+
{ name: 'timeout', type: 'int', default: 120, help: 'Max seconds to wait for the response' },
|
|
33
|
+
{ name: 'new', type: 'boolean', default: false, help: 'Start a new chat before sending' },
|
|
34
|
+
{ name: 'think', type: 'boolean', default: false, help: 'Enable 深度思考 (DeepThink)' },
|
|
35
|
+
{ name: 'research', type: 'boolean', default: false, help: 'Enable 深度研究 (DeepResearch)' },
|
|
36
|
+
{ name: 'markdown', type: 'boolean', default: false, help: 'Emit assistant reply as markdown' },
|
|
37
|
+
],
|
|
38
|
+
columns: ['Role', 'Text'],
|
|
39
|
+
func: async (page, kwargs) => {
|
|
40
|
+
const prompt = String(kwargs.prompt || '').trim();
|
|
41
|
+
if (!prompt) throw new ArgumentError('prompt is required');
|
|
42
|
+
const timeout = Number(kwargs.timeout ?? 120);
|
|
43
|
+
if (!Number.isInteger(timeout) || timeout <= 0) {
|
|
44
|
+
throw new ArgumentError('timeout must be a positive integer');
|
|
45
|
+
}
|
|
46
|
+
const startFresh = normalizeBooleanFlag(kwargs.new, false);
|
|
47
|
+
const useThink = normalizeBooleanFlag(kwargs.think, false);
|
|
48
|
+
const useResearch = normalizeBooleanFlag(kwargs.research, false);
|
|
49
|
+
const wantMarkdown = normalizeBooleanFlag(kwargs.markdown, false);
|
|
50
|
+
|
|
51
|
+
await ensureOnQianwen(page);
|
|
52
|
+
await dismissLoginModal(page);
|
|
53
|
+
|
|
54
|
+
if (startFresh) {
|
|
55
|
+
await startNewChat(page);
|
|
56
|
+
await dismissLoginModal(page);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (useThink) await setFeatureToggle(page, 'think', true);
|
|
60
|
+
if (useResearch) await setFeatureToggle(page, 'research', true);
|
|
61
|
+
|
|
62
|
+
const send = await sendMessage(page, prompt);
|
|
63
|
+
if (!send?.ok) {
|
|
64
|
+
if (await hasLoginGate(page)) throw authRequired();
|
|
65
|
+
throw new CommandExecutionError(send?.reason || 'Failed to send Qianwen prompt');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const result = await waitForAnswer(page, prompt, timeout);
|
|
69
|
+
if (result.status === 'auth_required') throw authRequired();
|
|
70
|
+
if (result.status === 'timeout') {
|
|
71
|
+
throw new TimeoutError('qianwen ask', timeout, 'No Qianwen reply observed before timeout. Retry with --timeout increased.');
|
|
72
|
+
}
|
|
73
|
+
const assistant = result.assistant;
|
|
74
|
+
if (!assistant) {
|
|
75
|
+
throw new CommandExecutionError('No assistant reply found in Qianwen chat.');
|
|
76
|
+
}
|
|
77
|
+
const answer = wantMarkdown && assistant.html
|
|
78
|
+
? (bubbleHtmlToMarkdown(assistant.html) || assistant.text)
|
|
79
|
+
: assistant.text;
|
|
80
|
+
return [
|
|
81
|
+
{ Role: 'User', Text: prompt },
|
|
82
|
+
{ Role: 'Assistant', Text: answer },
|
|
83
|
+
];
|
|
84
|
+
},
|
|
85
|
+
});
|