@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,80 @@
|
|
|
1
|
+
// flathub search — keyword search the Flathub app registry.
|
|
2
|
+
//
|
|
3
|
+
// POSTs to `/api/v2/search` with `{query}`. The `appId` column round-trips into
|
|
4
|
+
// `flathub app` for full appstream detail. Note: the search hit's `id` is
|
|
5
|
+
// underscored (e.g. `org_mozilla_firefox`); the actual reverse-DNS appId lives
|
|
6
|
+
// at `app_id`. We surface the dotted form so it round-trips without translation.
|
|
7
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
8
|
+
import { EmptyResultError } from '@jackwener/opencli/errors';
|
|
9
|
+
import {
|
|
10
|
+
FLATHUB_API_BASE,
|
|
11
|
+
FLATHUB_APP_BASE,
|
|
12
|
+
flathubFetch,
|
|
13
|
+
joinList,
|
|
14
|
+
requireBoundedInt,
|
|
15
|
+
requireString,
|
|
16
|
+
} from './utils.js';
|
|
17
|
+
|
|
18
|
+
cli({
|
|
19
|
+
site: 'flathub',
|
|
20
|
+
name: 'search',
|
|
21
|
+
access: 'read',
|
|
22
|
+
description: 'Search Flathub apps by keyword',
|
|
23
|
+
domain: 'flathub.org',
|
|
24
|
+
strategy: Strategy.PUBLIC,
|
|
25
|
+
browser: false,
|
|
26
|
+
args: [
|
|
27
|
+
{ name: 'query', positional: true, required: true, help: 'Search keyword' },
|
|
28
|
+
{ name: 'limit', type: 'int', default: 25, help: 'Max apps (1-100)' },
|
|
29
|
+
],
|
|
30
|
+
columns: [
|
|
31
|
+
'rank',
|
|
32
|
+
'appId',
|
|
33
|
+
'name',
|
|
34
|
+
'summary',
|
|
35
|
+
'developer',
|
|
36
|
+
'license',
|
|
37
|
+
'isFreeLicense',
|
|
38
|
+
'mainCategories',
|
|
39
|
+
'installsLastMonth',
|
|
40
|
+
'updatedAt',
|
|
41
|
+
'url',
|
|
42
|
+
],
|
|
43
|
+
func: async (args) => {
|
|
44
|
+
const query = requireString(args.query, 'query');
|
|
45
|
+
const limit = requireBoundedInt(args.limit, 25, 100);
|
|
46
|
+
const url = `${FLATHUB_API_BASE}/search`;
|
|
47
|
+
const body = await flathubFetch(url, 'flathub search', {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: { 'content-type': 'application/json' },
|
|
50
|
+
body: JSON.stringify({ query, hitsPerPage: limit, page: 1 }),
|
|
51
|
+
});
|
|
52
|
+
const list = Array.isArray(body?.hits) ? body.hits : [];
|
|
53
|
+
if (!list.length) {
|
|
54
|
+
throw new EmptyResultError('flathub search', `No Flathub apps matched "${query}".`);
|
|
55
|
+
}
|
|
56
|
+
return list.slice(0, limit).map((hit, i) => {
|
|
57
|
+
const appId = typeof hit?.app_id === 'string' ? hit.app_id : null;
|
|
58
|
+
return {
|
|
59
|
+
rank: i + 1,
|
|
60
|
+
appId,
|
|
61
|
+
name: typeof hit?.name === 'string' ? hit.name : null,
|
|
62
|
+
summary: typeof hit?.summary === 'string' ? hit.summary : null,
|
|
63
|
+
developer: typeof hit?.developer_name === 'string' ? hit.developer_name : null,
|
|
64
|
+
license: typeof hit?.project_license === 'string' ? hit.project_license : null,
|
|
65
|
+
isFreeLicense: hit?.is_free_license === true,
|
|
66
|
+
// `main_categories` comes back as a string (single value) on /search, not an array.
|
|
67
|
+
mainCategories: typeof hit?.main_categories === 'string'
|
|
68
|
+
? hit.main_categories
|
|
69
|
+
: joinList(hit?.main_categories),
|
|
70
|
+
installsLastMonth: typeof hit?.installs_last_month === 'number' ? hit.installs_last_month : null,
|
|
71
|
+
// /search emits `updated_at` as unix-seconds int; /appstream emits ISO strings.
|
|
72
|
+
// Normalise to ISO date here so both surfaces look consistent.
|
|
73
|
+
updatedAt: typeof hit?.updated_at === 'number' && hit.updated_at > 0
|
|
74
|
+
? new Date(hit.updated_at * 1000).toISOString().slice(0, 10)
|
|
75
|
+
: (typeof hit?.updated_at === 'string' ? hit.updated_at : null),
|
|
76
|
+
url: appId ? `${FLATHUB_APP_BASE}/${appId}` : '',
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Shared helpers for the Flathub adapters (https://flathub.org).
|
|
2
|
+
//
|
|
3
|
+
// Flathub is the canonical Linux flatpak app registry. Public REST API at
|
|
4
|
+
// `flathub.org/api/v2`, no auth, no key. Two endpoints we surface:
|
|
5
|
+
// • POST /search → keyword search, returns app metadata
|
|
6
|
+
// • GET /appstream/<appId> → full appstream metadata for one app
|
|
7
|
+
import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
|
|
8
|
+
|
|
9
|
+
export const FLATHUB_API_BASE = 'https://flathub.org/api/v2';
|
|
10
|
+
export const FLATHUB_APP_BASE = 'https://flathub.org/apps';
|
|
11
|
+
const UA = 'opencli-flathub-adapter/1.0 (+https://github.com/jackwener/opencli; mailto:opencli@example.com)';
|
|
12
|
+
|
|
13
|
+
// AppStream IDs are reverse-DNS (e.g. "org.gnome.Calculator"); the spec allows
|
|
14
|
+
// letters, digits, `.`, `_`, `-`. Min two segments separated by `.`.
|
|
15
|
+
const APP_ID_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z0-9_][A-Za-z0-9_-]*){1,}$/;
|
|
16
|
+
|
|
17
|
+
export function requireString(value, label) {
|
|
18
|
+
const s = String(value ?? '').trim();
|
|
19
|
+
if (!s) throw new ArgumentError(`flathub ${label} cannot be empty`);
|
|
20
|
+
return s;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function requireBoundedInt(value, defaultValue, maxValue, label = 'limit') {
|
|
24
|
+
const raw = value ?? defaultValue;
|
|
25
|
+
const n = typeof raw === 'number' ? raw : Number(raw);
|
|
26
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
27
|
+
throw new ArgumentError(`flathub ${label} must be a positive integer`);
|
|
28
|
+
}
|
|
29
|
+
if (n > maxValue) {
|
|
30
|
+
throw new ArgumentError(`flathub ${label} must be <= ${maxValue}`);
|
|
31
|
+
}
|
|
32
|
+
return n;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function requireAppId(value) {
|
|
36
|
+
const raw = String(value ?? '').trim();
|
|
37
|
+
if (!raw) throw new ArgumentError('flathub appId is required (e.g. "org.mozilla.firefox")');
|
|
38
|
+
if (!APP_ID_PATTERN.test(raw)) {
|
|
39
|
+
throw new ArgumentError(
|
|
40
|
+
`flathub appId "${value}" is not a valid AppStream identifier`,
|
|
41
|
+
'AppStream IDs use reverse-DNS like "org.mozilla.firefox" — letters/digits/`._-` with at least one dot.',
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return raw;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function flathubFetch(url, label, init) {
|
|
48
|
+
let resp;
|
|
49
|
+
try {
|
|
50
|
+
resp = await fetch(url, {
|
|
51
|
+
method: init?.method ?? 'GET',
|
|
52
|
+
headers: { 'user-agent': UA, accept: 'application/json', ...(init?.headers ?? {}) },
|
|
53
|
+
body: init?.body,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
throw new CommandExecutionError(
|
|
58
|
+
`${label} request failed: ${err?.message ?? err}`,
|
|
59
|
+
'Check that flathub.org is reachable from this network.',
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
if (resp.status === 404) {
|
|
63
|
+
throw new EmptyResultError(label, `Flathub returned 404 for ${url}.`);
|
|
64
|
+
}
|
|
65
|
+
if (resp.status === 429) {
|
|
66
|
+
throw new CommandExecutionError(`${label} returned HTTP 429 (rate limited)`);
|
|
67
|
+
}
|
|
68
|
+
if (!resp.ok) {
|
|
69
|
+
throw new CommandExecutionError(`${label} returned HTTP ${resp.status}`);
|
|
70
|
+
}
|
|
71
|
+
let body;
|
|
72
|
+
try {
|
|
73
|
+
body = await resp.json();
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
throw new CommandExecutionError(`${label} returned malformed JSON: ${err?.message ?? err}`);
|
|
77
|
+
}
|
|
78
|
+
return body;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function joinList(value, max = 10) {
|
|
82
|
+
if (!Array.isArray(value)) return '';
|
|
83
|
+
const items = value.filter((v) => typeof v === 'string' && v.trim());
|
|
84
|
+
if (items.length === 0) return '';
|
|
85
|
+
if (items.length > max) return [...items.slice(0, max), `(+${items.length - max})`].join(', ');
|
|
86
|
+
return items.join(', ');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Coerce flathub's `timestamp` field (sometimes int, sometimes numeric string).
|
|
90
|
+
function timestampNumber(value) {
|
|
91
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
92
|
+
if (typeof value === 'string' && /^\d+$/.test(value.trim())) return Number(value.trim());
|
|
93
|
+
return 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Pick the most recent appstream `releases[].version` if present. */
|
|
97
|
+
export function pickLatestRelease(releases) {
|
|
98
|
+
if (!Array.isArray(releases) || releases.length === 0) return { version: null, date: null };
|
|
99
|
+
// releases[].timestamp is unix-seconds; flathub returns either int or numeric string.
|
|
100
|
+
const sorted = [...releases].filter((r) => r && typeof r === 'object').sort((a, b) => {
|
|
101
|
+
return timestampNumber(b?.timestamp) - timestampNumber(a?.timestamp);
|
|
102
|
+
});
|
|
103
|
+
const top = sorted[0];
|
|
104
|
+
if (!top) return { version: null, date: null };
|
|
105
|
+
let date = null;
|
|
106
|
+
const ts = timestampNumber(top.timestamp);
|
|
107
|
+
if (ts > 0) {
|
|
108
|
+
const d = new Date(ts * 1000);
|
|
109
|
+
if (!Number.isNaN(d.getTime())) date = d.toISOString().slice(0, 10);
|
|
110
|
+
}
|
|
111
|
+
// Fallback: appstream emits `date` as ISO string ("2024-12-09") for some apps.
|
|
112
|
+
if (!date && typeof top.date === 'string' && top.date.trim()) date = top.date.trim();
|
|
113
|
+
return { version: typeof top.version === 'string' ? top.version : null, date };
|
|
114
|
+
}
|
package/clis/gemini/ask.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { ArgumentError } from '@jackwener/opencli/errors';
|
|
2
3
|
import { GEMINI_DOMAIN, readGeminiSnapshot, sendGeminiMessage, startNewGeminiChat, waitForGeminiResponse, waitForGeminiSubmission } from './utils.js';
|
|
3
4
|
function normalizeBooleanFlag(value) {
|
|
4
5
|
if (typeof value === 'boolean')
|
|
@@ -15,18 +16,21 @@ export const askCommand = cli({
|
|
|
15
16
|
domain: GEMINI_DOMAIN,
|
|
16
17
|
strategy: Strategy.COOKIE,
|
|
17
18
|
browser: true,
|
|
19
|
+
browserSession: { reuse: 'site' },
|
|
18
20
|
navigateBefore: false,
|
|
19
21
|
defaultFormat: 'plain',
|
|
20
|
-
timeoutSeconds: 180,
|
|
21
22
|
args: [
|
|
22
23
|
{ name: 'prompt', required: true, positional: true, help: 'Prompt to send' },
|
|
23
|
-
{ name: 'timeout', required: false, help: 'Max seconds to wait (default: 60)', default:
|
|
24
|
+
{ name: 'timeout', type: 'int', required: false, help: 'Max seconds to wait (default: 60)', default: 60 },
|
|
24
25
|
{ name: 'new', required: false, help: 'Start a new chat first (true/false, default: false)', default: 'false' },
|
|
25
26
|
],
|
|
26
27
|
columns: ['response'],
|
|
27
28
|
func: async (page, kwargs) => {
|
|
28
29
|
const prompt = kwargs.prompt;
|
|
29
|
-
const timeout =
|
|
30
|
+
const timeout = kwargs.timeout;
|
|
31
|
+
if (!Number.isInteger(timeout) || timeout < 1) {
|
|
32
|
+
throw new ArgumentError('--timeout must be a positive integer (seconds)');
|
|
33
|
+
}
|
|
30
34
|
const startFresh = normalizeBooleanFlag(kwargs.new);
|
|
31
35
|
if (startFresh)
|
|
32
36
|
await startNewGeminiChat(page);
|
package/clis/gemini/ask.test.js
CHANGED
|
@@ -79,7 +79,7 @@ describe('gemini ask orchestration', () => {
|
|
|
79
79
|
mocks.sendGeminiMessage.mockResolvedValueOnce('button');
|
|
80
80
|
mocks.waitForGeminiSubmission.mockResolvedValueOnce(submission);
|
|
81
81
|
mocks.waitForGeminiResponse.mockResolvedValueOnce('OK');
|
|
82
|
-
const result = await askCommand.func(page, { prompt: '请只回复:OK', timeout:
|
|
82
|
+
const result = await askCommand.func(page, { prompt: '请只回复:OK', timeout: 20, new: 'false' });
|
|
83
83
|
expect(mocks.readGeminiSnapshot).toHaveBeenCalledWith(page);
|
|
84
84
|
expect(mocks.waitForGeminiSubmission).toHaveBeenCalledWith(page, baseline, 20);
|
|
85
85
|
expect(mocks.waitForGeminiResponse).toHaveBeenCalledWith(page, submission, '请只回复:OK', 18);
|
|
@@ -94,7 +94,7 @@ describe('gemini ask orchestration', () => {
|
|
|
94
94
|
mocks.sendGeminiMessage.mockResolvedValueOnce('button');
|
|
95
95
|
mocks.waitForGeminiSubmission.mockResolvedValueOnce(submission);
|
|
96
96
|
mocks.waitForGeminiResponse.mockResolvedValueOnce('');
|
|
97
|
-
await askCommand.func(page, { prompt: '请只回复:OK', timeout:
|
|
97
|
+
await askCommand.func(page, { prompt: '请只回复:OK', timeout: 20, new: 'false' });
|
|
98
98
|
expect(mocks.waitForGeminiResponse).toHaveBeenCalledWith(page, submission, '请只回复:OK', 0);
|
|
99
99
|
});
|
|
100
100
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { ArgumentError } from '@jackwener/opencli/errors';
|
|
2
3
|
import { GEMINI_DOMAIN, clickGeminiConversationByTitle, exportGeminiDeepResearchReport, getLatestGeminiAssistantResponse, getGeminiPageState, parseGeminiConversationUrl, parseGeminiTitleMatchMode, readGeminiSnapshot, resolveGeminiConversationForQuery, waitForGeminiTranscript, getGeminiConversationList, } from './utils.js';
|
|
3
4
|
const DEEP_RESEARCH_WAITING_MESSAGE = 'Deep Research is still running. Please wait and retry later.';
|
|
4
5
|
const DEEP_RESEARCH_NO_DOCS_MESSAGE = 'No Docs URL found. Please check Share & Export -> Export to Docs in Gemini UI.';
|
|
@@ -43,6 +44,7 @@ export const deepResearchResultCommand = cli({
|
|
|
43
44
|
domain: GEMINI_DOMAIN,
|
|
44
45
|
strategy: Strategy.COOKIE,
|
|
45
46
|
browser: true,
|
|
47
|
+
browserSession: { reuse: 'site' },
|
|
46
48
|
navigateBefore: false,
|
|
47
49
|
defaultFormat: 'plain',
|
|
48
50
|
args: [
|
|
@@ -54,8 +56,10 @@ export const deepResearchResultCommand = cli({
|
|
|
54
56
|
func: async (page, kwargs) => {
|
|
55
57
|
const query = String(kwargs.query ?? '').trim();
|
|
56
58
|
const matchMode = parseGeminiTitleMatchMode(kwargs.match);
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
+
const timeoutSeconds = kwargs.timeout;
|
|
60
|
+
if (!Number.isInteger(timeoutSeconds) || timeoutSeconds < 1) {
|
|
61
|
+
throw new ArgumentError('--timeout must be a positive integer (seconds)');
|
|
62
|
+
}
|
|
59
63
|
if (!matchMode) {
|
|
60
64
|
return [{ response: 'Invalid match mode. Use contains or exact.' }];
|
|
61
65
|
}
|
|
@@ -53,52 +53,53 @@ describe('gemini/deep-research-result', () => {
|
|
|
53
53
|
structuredTurnsTrusted: true,
|
|
54
54
|
});
|
|
55
55
|
});
|
|
56
|
+
const runCommand = (kwargs) => deepResearchResultCommand.func(page, { timeout: 120, ...kwargs });
|
|
56
57
|
it('uses latest conversation when query is empty', async () => {
|
|
57
|
-
const result = await
|
|
58
|
+
const result = await runCommand({ query: ' ' });
|
|
58
59
|
expect(page.goto).toHaveBeenCalledWith('https://gemini.google.com/app/abc', { waitUntil: 'load', settleMs: 2500 });
|
|
59
60
|
expect(result).toEqual([{ response: 'https://files.example.com/report.md' }]);
|
|
60
61
|
});
|
|
61
62
|
it('falls back to current page response when query is empty and sidebar has no conversations', async () => {
|
|
62
63
|
mockGetGeminiConversationList.mockResolvedValue([]);
|
|
63
64
|
mockResolveGeminiConversationForQuery.mockReturnValue(null);
|
|
64
|
-
const result = await
|
|
65
|
+
const result = await runCommand({ query: '' });
|
|
65
66
|
expect(page.goto).not.toHaveBeenCalled();
|
|
66
67
|
expect(result).toEqual([{ response: 'https://files.example.com/report.md' }]);
|
|
67
68
|
});
|
|
68
69
|
it('returns a validation message when match mode is invalid', async () => {
|
|
69
|
-
const result = await
|
|
70
|
+
const result = await runCommand({ query: 'A', match: 'prefix' });
|
|
70
71
|
expect(result).toEqual([{ response: 'Invalid match mode. Use contains or exact.' }]);
|
|
71
72
|
});
|
|
72
73
|
it('returns a signed-out message when Gemini page state indicates logged out', async () => {
|
|
73
74
|
mockGetGeminiPageState.mockResolvedValue({ isSignedIn: false });
|
|
74
|
-
const result = await
|
|
75
|
+
const result = await runCommand({ query: 'A' });
|
|
75
76
|
expect(result).toEqual([{ response: 'Not signed in to Gemini.' }]);
|
|
76
77
|
});
|
|
77
78
|
it('opens matched conversation by URL and returns exported report url', async () => {
|
|
78
|
-
const result = await
|
|
79
|
+
const result = await runCommand({ query: 'A title', match: 'exact' });
|
|
79
80
|
expect(page.goto).toHaveBeenCalledWith('https://gemini.google.com/app/abc', { waitUntil: 'load', settleMs: 2500 });
|
|
80
81
|
expect(result).toEqual([{ response: 'https://files.example.com/report.md' }]);
|
|
81
82
|
});
|
|
82
83
|
it('accepts a direct conversation URL and reads response from that page', async () => {
|
|
83
84
|
const url = 'https://gemini.google.com/app/direct-id';
|
|
84
|
-
const result = await
|
|
85
|
+
const result = await runCommand({ query: url, match: 'contains' });
|
|
85
86
|
expect(page.goto).toHaveBeenCalledWith(url, { waitUntil: 'load', settleMs: 2500 });
|
|
86
87
|
expect(result).toEqual([{ response: 'https://files.example.com/report.md' }]);
|
|
87
88
|
});
|
|
88
89
|
it('passes query and mode into resolveGeminiConversationForQuery', async () => {
|
|
89
|
-
const result = await
|
|
90
|
+
const result = await runCommand({ query: 'title', match: 'contains' });
|
|
90
91
|
expect(mockResolveGeminiConversationForQuery).toHaveBeenCalledWith([{ Title: 'A title', Url: 'https://gemini.google.com/app/abc' }], 'title', 'contains');
|
|
91
92
|
expect(result).toEqual([{ response: 'https://files.example.com/report.md' }]);
|
|
92
93
|
});
|
|
93
94
|
it('falls back to click-by-title and returns not-found when click fails', async () => {
|
|
94
95
|
mockResolveGeminiConversationForQuery.mockReturnValue(null);
|
|
95
96
|
mockClickGeminiConversationByTitle.mockResolvedValue(false);
|
|
96
|
-
const result = await
|
|
97
|
+
const result = await runCommand({ query: 'missing', match: 'contains' });
|
|
97
98
|
expect(result).toEqual([{ response: 'No conversation matched: missing' }]);
|
|
98
99
|
});
|
|
99
100
|
it('returns pending message when export url is unavailable and completion is not confirmed', async () => {
|
|
100
101
|
mockExportGeminiDeepResearchReport.mockResolvedValue({ url: '', source: 'none' });
|
|
101
|
-
const result = await
|
|
102
|
+
const result = await runCommand({ query: 'A title' });
|
|
102
103
|
expect(result).toEqual([{ response: 'Deep Research may still be running or preparing export. Please wait and retry later.' }]);
|
|
103
104
|
});
|
|
104
105
|
it('returns waiting message when deep research is still generating', async () => {
|
|
@@ -110,13 +111,13 @@ describe('gemini/deep-research-result', () => {
|
|
|
110
111
|
isGenerating: true,
|
|
111
112
|
structuredTurnsTrusted: true,
|
|
112
113
|
});
|
|
113
|
-
const result = await
|
|
114
|
+
const result = await runCommand({ query: 'A title' });
|
|
114
115
|
expect(result).toEqual([{ response: 'Deep Research is still running. Please wait and retry later.' }]);
|
|
115
116
|
});
|
|
116
117
|
it('returns waiting message when assistant response indicates research in progress', async () => {
|
|
117
118
|
mockExportGeminiDeepResearchReport.mockResolvedValue({ url: '', source: 'none' });
|
|
118
119
|
mockGetLatestGeminiAssistantResponse.mockResolvedValue('正在研究中,请稍候。');
|
|
119
|
-
const result = await
|
|
120
|
+
const result = await runCommand({ query: 'A title' });
|
|
120
121
|
expect(result).toEqual([{ response: 'Deep Research is still running. Please wait and retry later.' }]);
|
|
121
122
|
});
|
|
122
123
|
it('returns waiting message when transcript indicates in-progress status', async () => {
|
|
@@ -129,7 +130,7 @@ describe('gemini/deep-research-result', () => {
|
|
|
129
130
|
isGenerating: false,
|
|
130
131
|
structuredTurnsTrusted: true,
|
|
131
132
|
});
|
|
132
|
-
const result = await
|
|
133
|
+
const result = await runCommand({ query: 'A title' });
|
|
133
134
|
expect(result).toEqual([{ response: 'Deep Research is still running. Please wait and retry later.' }]);
|
|
134
135
|
});
|
|
135
136
|
it('returns no-docs message when text indicates completed state', async () => {
|
|
@@ -142,13 +143,13 @@ describe('gemini/deep-research-result', () => {
|
|
|
142
143
|
isGenerating: false,
|
|
143
144
|
structuredTurnsTrusted: true,
|
|
144
145
|
});
|
|
145
|
-
const result = await
|
|
146
|
+
const result = await runCommand({ query: 'A title' });
|
|
146
147
|
expect(result).toEqual([{ response: 'No Docs URL found. Please check Share & Export -> Export to Docs in Gemini UI.' }]);
|
|
147
148
|
});
|
|
148
149
|
it('returns pending message when assistant response is empty', async () => {
|
|
149
150
|
mockExportGeminiDeepResearchReport.mockResolvedValue({ url: '', source: 'none' });
|
|
150
151
|
mockGetLatestGeminiAssistantResponse.mockResolvedValue('');
|
|
151
|
-
const result = await
|
|
152
|
+
const result = await runCommand({ query: 'A title' });
|
|
152
153
|
expect(result).toEqual([{ response: 'Deep Research may still be running or preparing export. Please wait and retry later.' }]);
|
|
153
154
|
});
|
|
154
155
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
-
import {
|
|
2
|
+
import { ArgumentError } from '@jackwener/opencli/errors';
|
|
3
|
+
import { GEMINI_DEEP_RESEARCH_DEFAULT_CONFIRM_LABELS, GEMINI_DEEP_RESEARCH_DEFAULT_TOOL_LABELS, GEMINI_APP_URL, GEMINI_DOMAIN, getCurrentGeminiUrl, getLatestGeminiAssistantResponse, readGeminiSnapshot, resolveGeminiLabels, selectGeminiTool, sendGeminiMessage, startNewGeminiChat, waitForGeminiSubmission, waitForGeminiConfirmButton, } from './utils.js';
|
|
3
4
|
function isGeminiRootAppUrl(url) {
|
|
4
5
|
try {
|
|
5
6
|
const parsed = new URL(url);
|
|
@@ -22,19 +23,22 @@ export const deepResearchCommand = cli({
|
|
|
22
23
|
domain: GEMINI_DOMAIN,
|
|
23
24
|
strategy: Strategy.COOKIE,
|
|
24
25
|
browser: true,
|
|
26
|
+
browserSession: { reuse: 'site' },
|
|
25
27
|
navigateBefore: false,
|
|
26
28
|
defaultFormat: 'plain',
|
|
27
|
-
timeoutSeconds: 180,
|
|
28
29
|
args: [
|
|
29
30
|
{ name: 'prompt', positional: true, required: true, help: 'Prompt to send' },
|
|
30
|
-
{ name: 'timeout', type: 'int', required: false, help: 'Max seconds
|
|
31
|
+
{ name: 'timeout', type: 'int', required: false, help: 'Max seconds for the overall command (default: 180; confirm-wait clamps internally to 6-20s)', default: 180 },
|
|
31
32
|
{ name: 'tool', required: false, help: 'Override tool label (default: Deep Research)' },
|
|
32
33
|
{ name: 'confirm', required: false, help: 'Override confirm button label (default: Start research)' },
|
|
33
34
|
],
|
|
34
35
|
columns: ['status', 'url'],
|
|
35
36
|
func: async (page, kwargs) => {
|
|
36
37
|
const prompt = kwargs.prompt;
|
|
37
|
-
const timeout =
|
|
38
|
+
const timeout = kwargs.timeout;
|
|
39
|
+
if (!Number.isInteger(timeout) || timeout < 1) {
|
|
40
|
+
throw new ArgumentError('--timeout must be a positive integer (seconds)');
|
|
41
|
+
}
|
|
38
42
|
const submitTimeout = Math.min(Math.max(timeout, 6), 20);
|
|
39
43
|
await startNewGeminiChat(page);
|
|
40
44
|
const toolLabels = resolveGeminiLabels(kwargs.tool, GEMINI_DEEP_RESEARCH_DEFAULT_TOOL_LABELS);
|
|
@@ -28,10 +28,6 @@ vi.mock('./utils.js', () => ({
|
|
|
28
28
|
'\u751f\u6210\u7814\u7a76\u8ba1\u5212',
|
|
29
29
|
'\u751f\u6210\u8c03\u7814\u8ba1\u5212',
|
|
30
30
|
],
|
|
31
|
-
parseGeminiPositiveInt: (value, fallback) => {
|
|
32
|
-
const parsed = Number.parseInt(String(value ?? ''), 10);
|
|
33
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
34
|
-
},
|
|
35
31
|
resolveGeminiLabels: (value, fallback) => {
|
|
36
32
|
const label = String(value ?? '').trim();
|
|
37
33
|
return label ? [label] : fallback;
|
|
@@ -48,6 +44,7 @@ vi.mock('./utils.js', () => ({
|
|
|
48
44
|
import { deepResearchCommand } from './deep-research.js';
|
|
49
45
|
describe('gemini/deep-research', () => {
|
|
50
46
|
const page = {};
|
|
47
|
+
const runCommand = (kwargs) => deepResearchCommand.func(page, { timeout: 180, ...kwargs });
|
|
51
48
|
beforeEach(() => {
|
|
52
49
|
vi.clearAllMocks();
|
|
53
50
|
mockGetCurrentGeminiUrl.mockResolvedValue('https://gemini.google.com/app/chat');
|
|
@@ -71,17 +68,17 @@ describe('gemini/deep-research', () => {
|
|
|
71
68
|
mockGetLatestGeminiAssistantResponse.mockResolvedValue('');
|
|
72
69
|
});
|
|
73
70
|
it('starts a new chat by default, then sends prompt and confirms deep research', async () => {
|
|
74
|
-
const result = await
|
|
71
|
+
const result = await runCommand({ prompt: 'research this topic' });
|
|
75
72
|
expect(mockStartNewGeminiChat).toHaveBeenCalledTimes(1);
|
|
76
73
|
expect(mockSelectGeminiTool).toHaveBeenCalledTimes(1);
|
|
77
74
|
expect(mockSendGeminiMessage).toHaveBeenCalledWith(page, 'research this topic');
|
|
78
75
|
expect(mockWaitForGeminiSubmission).toHaveBeenCalledTimes(1);
|
|
79
|
-
expect(mockWaitForGeminiConfirmButton).toHaveBeenCalledWith(page, expect.arrayContaining(['Start research', 'Start deep research', 'Generate research plan', '\u751f\u6210\u7814\u7a76\u8ba1\u5212']),
|
|
76
|
+
expect(mockWaitForGeminiConfirmButton).toHaveBeenCalledWith(page, expect.arrayContaining(['Start research', 'Start deep research', 'Generate research plan', '\u751f\u6210\u7814\u7a76\u8ba1\u5212']), 180);
|
|
80
77
|
expect(result).toEqual([{ status: 'started', url: 'https://gemini.google.com/app/chat' }]);
|
|
81
78
|
});
|
|
82
79
|
it('returns tool-not-found when the tool cannot be selected', async () => {
|
|
83
80
|
mockSelectGeminiTool.mockResolvedValue('');
|
|
84
|
-
const result = await
|
|
81
|
+
const result = await runCommand({ prompt: 'research this topic' });
|
|
85
82
|
expect(result).toEqual([{ status: 'tool-not-found', url: 'https://gemini.google.com/app/chat' }]);
|
|
86
83
|
expect(mockSendGeminiMessage).not.toHaveBeenCalled();
|
|
87
84
|
expect(mockWaitForGeminiSubmission).not.toHaveBeenCalled();
|
|
@@ -94,7 +91,7 @@ describe('gemini/deep-research', () => {
|
|
|
94
91
|
userAnchorTurn: null,
|
|
95
92
|
reason: 'user_turn',
|
|
96
93
|
});
|
|
97
|
-
const result = await
|
|
94
|
+
const result = await runCommand({ prompt: 'research this topic' });
|
|
98
95
|
expect(mockSelectGeminiTool).toHaveBeenCalledTimes(2);
|
|
99
96
|
expect(mockReadGeminiSnapshot).toHaveBeenCalledTimes(2);
|
|
100
97
|
expect(mockSendGeminiMessage).toHaveBeenCalledTimes(2);
|
|
@@ -103,7 +100,7 @@ describe('gemini/deep-research', () => {
|
|
|
103
100
|
});
|
|
104
101
|
it('returns submit-not-found when submission cannot be confirmed after retry', async () => {
|
|
105
102
|
mockWaitForGeminiSubmission.mockResolvedValue(null);
|
|
106
|
-
const result = await
|
|
103
|
+
const result = await runCommand({ prompt: 'research this topic' });
|
|
107
104
|
expect(mockSelectGeminiTool).toHaveBeenCalledTimes(2);
|
|
108
105
|
expect(mockSendGeminiMessage).toHaveBeenCalledTimes(2);
|
|
109
106
|
expect(mockWaitForGeminiConfirmButton).not.toHaveBeenCalled();
|
|
@@ -111,27 +108,27 @@ describe('gemini/deep-research', () => {
|
|
|
111
108
|
});
|
|
112
109
|
it('returns confirm-not-found when no confirm button is found', async () => {
|
|
113
110
|
mockWaitForGeminiConfirmButton.mockResolvedValue('');
|
|
114
|
-
const result = await
|
|
111
|
+
const result = await runCommand({ prompt: 'research this topic' });
|
|
115
112
|
expect(result).toEqual([{ status: 'confirm-not-found', url: 'https://gemini.google.com/app/chat' }]);
|
|
116
113
|
});
|
|
117
114
|
it('returns started when confirm is missing but research appears to be running', async () => {
|
|
118
115
|
mockWaitForGeminiConfirmButton.mockResolvedValue('');
|
|
119
116
|
mockGetCurrentGeminiUrl.mockResolvedValue('https://gemini.google.com/app/abc123');
|
|
120
117
|
mockGetLatestGeminiAssistantResponse.mockResolvedValue('Researching websites now');
|
|
121
|
-
const result = await
|
|
118
|
+
const result = await runCommand({ prompt: 'research this topic' });
|
|
122
119
|
expect(result).toEqual([{ status: 'started', url: 'https://gemini.google.com/app/abc123' }]);
|
|
123
120
|
});
|
|
124
121
|
it('does not treat conversation url alone as started when confirm is missing', async () => {
|
|
125
122
|
mockWaitForGeminiConfirmButton.mockResolvedValue('');
|
|
126
123
|
mockGetCurrentGeminiUrl.mockResolvedValue('https://gemini.google.com/app/abc999');
|
|
127
124
|
mockGetLatestGeminiAssistantResponse.mockResolvedValue('I drafted a plan. Start research');
|
|
128
|
-
const result = await
|
|
125
|
+
const result = await runCommand({ prompt: 'research this topic' });
|
|
129
126
|
expect(result).toEqual([{ status: 'confirm-not-found', url: 'https://gemini.google.com/app/abc999' }]);
|
|
130
127
|
});
|
|
131
128
|
it('retries once when stuck on root app URL and starts successfully on second confirm', async () => {
|
|
132
129
|
mockWaitForGeminiConfirmButton.mockResolvedValueOnce('').mockResolvedValueOnce('Start research');
|
|
133
130
|
mockGetCurrentGeminiUrl.mockResolvedValueOnce('https://gemini.google.com/app').mockResolvedValueOnce('https://gemini.google.com/app/retry123');
|
|
134
|
-
const result = await
|
|
131
|
+
const result = await runCommand({ prompt: 'research this topic', timeout: 20 });
|
|
135
132
|
expect(mockSelectGeminiTool).toHaveBeenCalledTimes(2);
|
|
136
133
|
expect(mockSendGeminiMessage).toHaveBeenCalledTimes(1);
|
|
137
134
|
expect(mockWaitForGeminiConfirmButton).toHaveBeenCalledTimes(2);
|
|
@@ -140,7 +137,7 @@ describe('gemini/deep-research', () => {
|
|
|
140
137
|
it('treats root-url confirm as false-positive and retries', async () => {
|
|
141
138
|
mockWaitForGeminiConfirmButton.mockResolvedValueOnce('Start research').mockResolvedValueOnce('Start research');
|
|
142
139
|
mockGetCurrentGeminiUrl.mockResolvedValueOnce('https://gemini.google.com/app').mockResolvedValueOnce('https://gemini.google.com/app/retry456');
|
|
143
|
-
const result = await
|
|
140
|
+
const result = await runCommand({ prompt: 'research this topic', timeout: 20 });
|
|
144
141
|
expect(mockSelectGeminiTool).toHaveBeenCalledTimes(2);
|
|
145
142
|
expect(mockSendGeminiMessage).toHaveBeenCalledTimes(1);
|
|
146
143
|
expect(result).toEqual([{ status: 'started', url: 'https://gemini.google.com/app/retry456' }]);
|
|
@@ -149,7 +146,7 @@ describe('gemini/deep-research', () => {
|
|
|
149
146
|
mockWaitForGeminiConfirmButton.mockResolvedValueOnce('Start research').mockResolvedValueOnce('');
|
|
150
147
|
mockGetCurrentGeminiUrl.mockResolvedValueOnce('https://gemini.google.com/app').mockResolvedValueOnce('https://gemini.google.com/app');
|
|
151
148
|
mockGetLatestGeminiAssistantResponse.mockResolvedValue('');
|
|
152
|
-
const result = await
|
|
149
|
+
const result = await runCommand({ prompt: 'research this topic', timeout: 20 });
|
|
153
150
|
expect(mockSelectGeminiTool).toHaveBeenCalledTimes(2);
|
|
154
151
|
expect(mockSendGeminiMessage).toHaveBeenCalledTimes(1);
|
|
155
152
|
expect(mockWaitForGeminiConfirmButton).toHaveBeenCalledTimes(2);
|
|
@@ -165,18 +162,18 @@ describe('gemini/deep-research', () => {
|
|
|
165
162
|
mockGetLatestGeminiAssistantResponse
|
|
166
163
|
.mockResolvedValueOnce('I drafted a plan. Start research')
|
|
167
164
|
.mockResolvedValueOnce('Researching websites now');
|
|
168
|
-
const result = await
|
|
165
|
+
const result = await runCommand({ prompt: 'research this topic', timeout: 20 });
|
|
169
166
|
expect(mockWaitForGeminiConfirmButton).toHaveBeenCalledTimes(2);
|
|
170
167
|
expect(mockWaitForGeminiConfirmButton).toHaveBeenNthCalledWith(2, page, expect.arrayContaining(['Start research', 'Start deep research', '开始研究', '开始深度研究']), 8);
|
|
171
168
|
expect(mockSendGeminiMessage).toHaveBeenCalledTimes(1);
|
|
172
169
|
expect(result).toEqual([{ status: 'started', url: 'https://gemini.google.com/app/xyz123' }]);
|
|
173
170
|
});
|
|
174
171
|
it('uses custom tool/confirm labels when provided', async () => {
|
|
175
|
-
await
|
|
172
|
+
await runCommand({
|
|
176
173
|
prompt: 'research this topic',
|
|
177
174
|
tool: 'Custom Tool',
|
|
178
175
|
confirm: 'Custom Confirm',
|
|
179
|
-
timeout:
|
|
176
|
+
timeout: 42,
|
|
180
177
|
});
|
|
181
178
|
expect(mockSelectGeminiTool).toHaveBeenCalledWith(page, ['Custom Tool']);
|
|
182
179
|
expect(mockWaitForGeminiConfirmButton).toHaveBeenCalledWith(page, ['Custom Confirm'], 42);
|
package/clis/gemini/image.js
CHANGED
|
@@ -2,6 +2,7 @@ import * as os from 'node:os';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
4
4
|
import { saveBase64ToFile } from '@jackwener/opencli/utils';
|
|
5
|
+
import { ArgumentError } from '@jackwener/opencli/errors';
|
|
5
6
|
import { GEMINI_DOMAIN, exportGeminiImages, getGeminiVisibleImageUrls, sendGeminiMessage, startNewGeminiChat, waitForGeminiImages } from './utils.js';
|
|
6
7
|
function extFromMime(mime) {
|
|
7
8
|
if (mime.includes('png'))
|
|
@@ -51,15 +52,16 @@ export const imageCommand = cli({
|
|
|
51
52
|
domain: GEMINI_DOMAIN,
|
|
52
53
|
strategy: Strategy.COOKIE,
|
|
53
54
|
browser: true,
|
|
55
|
+
browserSession: { reuse: 'site' },
|
|
54
56
|
navigateBefore: false,
|
|
55
57
|
defaultFormat: 'plain',
|
|
56
|
-
timeoutSeconds: 240,
|
|
57
58
|
args: [
|
|
58
59
|
{ name: 'prompt', positional: true, required: true, help: 'Image prompt to send to Gemini' },
|
|
59
60
|
{ name: 'rt', default: '1:1', help: 'Ratio shorthand for aspect ratio (1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3)' },
|
|
60
61
|
{ name: 'st', default: '', help: 'Style shorthand, e.g. anime, icon, watercolor' },
|
|
61
62
|
{ name: 'op', default: '~/tmp/gemini-images', help: 'Output directory shorthand' },
|
|
62
63
|
{ name: 'sd', type: 'boolean', default: false, help: 'Skip download shorthand; only show Gemini page link' },
|
|
64
|
+
{ name: 'timeout', type: 'int', required: false, default: 240, help: 'Max seconds for the overall command (default: 240)' },
|
|
63
65
|
],
|
|
64
66
|
columns: ['status', 'file', 'link'],
|
|
65
67
|
func: async (page, kwargs) => {
|
|
@@ -67,7 +69,10 @@ export const imageCommand = cli({
|
|
|
67
69
|
const ratio = normalizeRatio(String(kwargs.rt ?? '1:1'));
|
|
68
70
|
const style = String(kwargs.st ?? '').trim();
|
|
69
71
|
const outputDir = kwargs.op || path.join(os.homedir(), 'tmp', 'gemini-images');
|
|
70
|
-
const timeout =
|
|
72
|
+
const timeout = kwargs.timeout;
|
|
73
|
+
if (!Number.isInteger(timeout) || timeout < 1) {
|
|
74
|
+
throw new ArgumentError('--timeout must be a positive integer (seconds)');
|
|
75
|
+
}
|
|
71
76
|
const startFresh = true;
|
|
72
77
|
const skipDownloadRaw = kwargs.sd;
|
|
73
78
|
const skipDownload = skipDownloadRaw === '' || skipDownloadRaw === true || normalizeBooleanFlag(skipDownloadRaw);
|
package/clis/gemini/new.js
CHANGED
package/clis/gemini/utils.js
CHANGED
|
@@ -78,10 +78,6 @@ export function resolveGeminiLabels(value, fallback) {
|
|
|
78
78
|
const label = String(value ?? '').trim();
|
|
79
79
|
return label ? [label] : fallback;
|
|
80
80
|
}
|
|
81
|
-
export function parseGeminiPositiveInt(value, fallback) {
|
|
82
|
-
const parsed = Number.parseInt(String(value ?? ''), 10);
|
|
83
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
84
|
-
}
|
|
85
81
|
export function parseGeminiTitleMatchMode(value, fallback = 'contains') {
|
|
86
82
|
const raw = String(value ?? fallback).trim().toLowerCase();
|
|
87
83
|
if (raw === 'contains' || raw === 'exact')
|
|
@@ -16,7 +16,6 @@ cli({
|
|
|
16
16
|
{ name: 'index', type: 'int', default: 1, help: 'Which search result to cite (1-based)' },
|
|
17
17
|
],
|
|
18
18
|
columns: ['title', 'format', 'citation'],
|
|
19
|
-
navigateBefore: false,
|
|
20
19
|
func: async (page, kwargs) => {
|
|
21
20
|
const query = requireNonEmptyQuery(kwargs.query);
|
|
22
21
|
const format = kwargs.style || 'bibtex';
|
|
@@ -15,7 +15,6 @@ cli({
|
|
|
15
15
|
{ name: 'limit', type: 'int', default: 10, help: 'Max papers to show (max 20)' },
|
|
16
16
|
],
|
|
17
17
|
columns: ['rank', 'title', 'cited', 'year'],
|
|
18
|
-
navigateBefore: false,
|
|
19
18
|
func: async (page, kwargs) => {
|
|
20
19
|
const author = requireNonEmptyQuery(kwargs.author, 'author');
|
|
21
20
|
const limit = clampInt(kwargs.limit, 10, 1, 20);
|
|
@@ -14,7 +14,6 @@ cli({
|
|
|
14
14
|
{ name: 'limit', type: 'int', default: 10, help: '返回结果数量 (max 20)' },
|
|
15
15
|
],
|
|
16
16
|
columns: ['rank', 'title', 'authors', 'source', 'year', 'cited', 'url'],
|
|
17
|
-
navigateBefore: false,
|
|
18
17
|
func: async (page, kwargs) => {
|
|
19
18
|
const limit = clampInt(kwargs.limit, 10, 1, 20);
|
|
20
19
|
const query = requireNonEmptyQuery(kwargs.query);
|