@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
|
@@ -2,44 +2,33 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import * as crypto from 'node:crypto';
|
|
4
4
|
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
5
|
-
import
|
|
5
|
+
import { ArgumentError, CommandExecutionError, TimeoutError } from '@jackwener/opencli/errors';
|
|
6
6
|
|
|
7
7
|
const GROK_URL = 'https://grok.com/';
|
|
8
|
-
const NO_IMAGE_PREFIX = '[NO IMAGE]';
|
|
9
|
-
const BLOCKED_PREFIX = '[BLOCKED]';
|
|
10
8
|
const SESSION_HINT = 'Likely login/auth/challenge/session issue in the existing grok.com browser session.';
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
ok?: boolean;
|
|
14
|
-
msg?: string;
|
|
15
|
-
reason?: string;
|
|
16
|
-
detail?: string;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
type BubbleImage = {
|
|
20
|
-
src: string;
|
|
21
|
-
w: number;
|
|
22
|
-
h: number;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
type BubbleImageSet = BubbleImage[];
|
|
26
|
-
|
|
27
|
-
type FetchResult = {
|
|
28
|
-
ok: boolean;
|
|
29
|
-
base64?: string;
|
|
30
|
-
contentType?: string;
|
|
31
|
-
error?: string;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
function normalizeBooleanFlag(value: unknown): boolean {
|
|
10
|
+
function normalizeBooleanFlag(value) {
|
|
35
11
|
if (typeof value === 'boolean') return value;
|
|
36
12
|
const normalized = String(value ?? '').trim().toLowerCase();
|
|
37
13
|
return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';
|
|
38
14
|
}
|
|
39
15
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Validate a positive-integer arg without silently flooring/clamping.
|
|
18
|
+
* Throws ArgumentError on `0`, negatives, non-integers, or non-numeric input.
|
|
19
|
+
*/
|
|
20
|
+
function normalizePositiveInteger(value, defaultValue, label) {
|
|
21
|
+
const raw = value ?? defaultValue;
|
|
22
|
+
const n = Number(raw);
|
|
23
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
24
|
+
throw new ArgumentError(`${label} must be a positive integer`);
|
|
25
|
+
}
|
|
26
|
+
return n;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function dedupeBySrc(images) {
|
|
30
|
+
const seen = new Set();
|
|
31
|
+
const out = [];
|
|
43
32
|
for (const img of images) {
|
|
44
33
|
if (!img.src || seen.has(img.src)) continue;
|
|
45
34
|
seen.add(img.src);
|
|
@@ -48,11 +37,11 @@ function dedupeBySrc(images: BubbleImage[]): BubbleImage[] {
|
|
|
48
37
|
return out;
|
|
49
38
|
}
|
|
50
39
|
|
|
51
|
-
function imagesSignature(images
|
|
40
|
+
function imagesSignature(images) {
|
|
52
41
|
return images.map(i => i.src).sort().join('|');
|
|
53
42
|
}
|
|
54
43
|
|
|
55
|
-
function extFromContentType(ct
|
|
44
|
+
function extFromContentType(ct) {
|
|
56
45
|
if (!ct) return 'jpg';
|
|
57
46
|
if (ct.includes('png')) return 'png';
|
|
58
47
|
if (ct.includes('webp')) return 'webp';
|
|
@@ -60,14 +49,14 @@ function extFromContentType(ct?: string): string {
|
|
|
60
49
|
return 'jpg';
|
|
61
50
|
}
|
|
62
51
|
|
|
63
|
-
function buildFilename(src
|
|
52
|
+
function buildFilename(src, ct) {
|
|
64
53
|
const ext = extFromContentType(ct);
|
|
65
54
|
const hash = crypto.createHash('sha1').update(src).digest('hex').slice(0, 12);
|
|
66
55
|
return `grok-${Date.now()}-${hash}.${ext}`;
|
|
67
56
|
}
|
|
68
57
|
|
|
69
58
|
/** Check whether the tab is already on grok.com (any path). */
|
|
70
|
-
async function isOnGrok(page
|
|
59
|
+
async function isOnGrok(page) {
|
|
71
60
|
const url = await page.evaluate('window.location.href').catch(() => '');
|
|
72
61
|
if (typeof url !== 'string' || !url) return false;
|
|
73
62
|
try {
|
|
@@ -78,7 +67,7 @@ async function isOnGrok(page: IPage): Promise<boolean> {
|
|
|
78
67
|
}
|
|
79
68
|
}
|
|
80
69
|
|
|
81
|
-
async function tryStartFreshChat(page
|
|
70
|
+
async function tryStartFreshChat(page) {
|
|
82
71
|
await page.evaluate(`(() => {
|
|
83
72
|
const isVisible = (node) => {
|
|
84
73
|
if (!(node instanceof HTMLElement)) return false;
|
|
@@ -102,7 +91,7 @@ async function tryStartFreshChat(page: IPage): Promise<void> {
|
|
|
102
91
|
})()`);
|
|
103
92
|
}
|
|
104
93
|
|
|
105
|
-
async function sendPrompt(page
|
|
94
|
+
async function sendPrompt(page, prompt) {
|
|
106
95
|
const promptJson = JSON.stringify(prompt);
|
|
107
96
|
return page.evaluate(`(async () => {
|
|
108
97
|
try {
|
|
@@ -180,11 +169,11 @@ async function sendPrompt(page: IPage, prompt: string): Promise<SendResult> {
|
|
|
180
169
|
box.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
|
|
181
170
|
return { ok: true, msg: 'enter' };
|
|
182
171
|
} catch (e) { return { ok: false, msg: e && e.toString ? e.toString() : String(e) }; }
|
|
183
|
-
})()`)
|
|
172
|
+
})()`);
|
|
184
173
|
}
|
|
185
174
|
|
|
186
175
|
/** Read <img> elements from all message bubbles so callers can filter by baseline. */
|
|
187
|
-
async function getBubbleImageSets(page
|
|
176
|
+
async function getBubbleImageSets(page) {
|
|
188
177
|
const result = await page.evaluate(`(() => {
|
|
189
178
|
const bubbles = document.querySelectorAll('div.message-bubble, [data-testid="message-bubble"]');
|
|
190
179
|
return Array.from(bubbles).map(bubble => Array.from(bubble.querySelectorAll('img'))
|
|
@@ -196,16 +185,13 @@ async function getBubbleImageSets(page: IPage): Promise<BubbleImageSet[]> {
|
|
|
196
185
|
.filter(i => i.src && /^https?:/.test(i.src))
|
|
197
186
|
// Ignore tiny UI/avatar images that may live in the bubble chrome.
|
|
198
187
|
.filter(i => (i.w === 0 || i.w >= 128) && (i.h === 0 || i.h >= 128)));
|
|
199
|
-
})()`)
|
|
188
|
+
})()`);
|
|
200
189
|
|
|
201
190
|
const raw = Array.isArray(result) ? result : [];
|
|
202
191
|
return raw.map(dedupeBySrc);
|
|
203
192
|
}
|
|
204
193
|
|
|
205
|
-
function pickLatestImageCandidate(
|
|
206
|
-
bubbleImageSets: BubbleImageSet[],
|
|
207
|
-
baselineCount: number,
|
|
208
|
-
): BubbleImage[] {
|
|
194
|
+
function pickLatestImageCandidate(bubbleImageSets, baselineCount) {
|
|
209
195
|
const freshSets = bubbleImageSets.slice(Math.max(0, baselineCount));
|
|
210
196
|
for (let i = freshSets.length - 1; i >= 0; i -= 1) {
|
|
211
197
|
if (freshSets[i].length) return freshSets[i];
|
|
@@ -216,7 +202,7 @@ function pickLatestImageCandidate(
|
|
|
216
202
|
// Download through the browser's fetch so grok.com cookies and referer are
|
|
217
203
|
// attached automatically — assets.grok.com is gated by Cloudflare and will
|
|
218
204
|
// refuse direct curl/node downloads.
|
|
219
|
-
async function fetchImageAsBase64(page
|
|
205
|
+
async function fetchImageAsBase64(page, url) {
|
|
220
206
|
const urlJson = JSON.stringify(url);
|
|
221
207
|
return page.evaluate(`(async () => {
|
|
222
208
|
try {
|
|
@@ -229,21 +215,24 @@ async function fetchImageAsBase64(page: IPage, url: string): Promise<FetchResult
|
|
|
229
215
|
for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
|
|
230
216
|
return { ok: true, base64: btoa(binary), contentType: blob.type || 'image/jpeg' };
|
|
231
217
|
} catch (e) { return { ok: false, error: e && e.message || String(e) }; }
|
|
232
|
-
})()`)
|
|
218
|
+
})()`);
|
|
233
219
|
}
|
|
234
220
|
|
|
235
|
-
async function saveImages(
|
|
236
|
-
page: IPage,
|
|
237
|
-
images: BubbleImage[],
|
|
238
|
-
outDir: string,
|
|
239
|
-
): Promise<Array<BubbleImage & { path: string }>> {
|
|
221
|
+
async function saveImages(page, images, outDir) {
|
|
240
222
|
fs.mkdirSync(outDir, { recursive: true });
|
|
241
|
-
const results
|
|
223
|
+
const results = [];
|
|
242
224
|
for (const img of images) {
|
|
243
225
|
const fetched = await fetchImageAsBase64(page, img.src);
|
|
244
226
|
if (!fetched || !fetched.ok) {
|
|
245
|
-
|
|
246
|
-
|
|
227
|
+
// Fail loudly on per-image download failure rather than emit a sentinel
|
|
228
|
+
// row with path = '[DOWNLOAD FAILED] ...' that downstream tools cannot
|
|
229
|
+
// distinguish from a real path. assets.grok.com is Cloudflare-gated, so
|
|
230
|
+
// a single 401/403 typically means the whole batch is unrecoverable.
|
|
231
|
+
const reason = fetched?.error ? `: ${fetched.error}` : '';
|
|
232
|
+
throw new CommandExecutionError(
|
|
233
|
+
`Failed to download grok image ${img.src}${reason}`,
|
|
234
|
+
'assets.grok.com download requires the live grok.com browser session — verify the tab is logged in and try again.',
|
|
235
|
+
);
|
|
247
236
|
}
|
|
248
237
|
const filepath = path.join(outDir, buildFilename(img.src, fetched.contentType));
|
|
249
238
|
fs.writeFileSync(filepath, Buffer.from(fetched.base64 || '', 'base64'));
|
|
@@ -252,7 +241,7 @@ async function saveImages(
|
|
|
252
241
|
return results;
|
|
253
242
|
}
|
|
254
243
|
|
|
255
|
-
function toRow(img
|
|
244
|
+
function toRow(img, savedPath = '') {
|
|
256
245
|
return { url: img.src, width: img.w, height: img.h, path: savedPath };
|
|
257
246
|
}
|
|
258
247
|
|
|
@@ -264,6 +253,7 @@ export const imageCommand = cli({
|
|
|
264
253
|
domain: 'grok.com',
|
|
265
254
|
strategy: Strategy.COOKIE,
|
|
266
255
|
browser: true,
|
|
256
|
+
browserSession: { reuse: 'site' },
|
|
267
257
|
args: [
|
|
268
258
|
{ name: 'prompt', positional: true, type: 'string', required: true, help: 'Image generation prompt' },
|
|
269
259
|
{ name: 'timeout', type: 'int', default: 240, help: 'Max seconds to wait for the image (default: 240)' },
|
|
@@ -272,11 +262,11 @@ export const imageCommand = cli({
|
|
|
272
262
|
{ name: 'out', type: 'string', default: '', help: 'Directory to save downloaded images (uses browser session to bypass auth)' },
|
|
273
263
|
],
|
|
274
264
|
columns: ['url', 'width', 'height', 'path'],
|
|
275
|
-
func: async (page
|
|
276
|
-
const prompt = kwargs.prompt
|
|
277
|
-
const timeoutMs = (
|
|
265
|
+
func: async (page, kwargs) => {
|
|
266
|
+
const prompt = kwargs.prompt;
|
|
267
|
+
const timeoutMs = (kwargs.timeout || 240) * 1000;
|
|
278
268
|
const newChat = normalizeBooleanFlag(kwargs.new);
|
|
279
|
-
const minCount =
|
|
269
|
+
const minCount = normalizePositiveInteger(kwargs.count, 1, 'count');
|
|
280
270
|
const outDir = (kwargs.out || '').toString().trim();
|
|
281
271
|
|
|
282
272
|
if (newChat) {
|
|
@@ -292,18 +282,16 @@ export const imageCommand = cli({
|
|
|
292
282
|
const baselineBubbleCount = (await getBubbleImageSets(page)).length;
|
|
293
283
|
const sendResult = await sendPrompt(page, prompt);
|
|
294
284
|
if (!sendResult || !sendResult.ok) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
path: '',
|
|
300
|
-
}];
|
|
285
|
+
throw new CommandExecutionError(
|
|
286
|
+
`Grok composer rejected the prompt: ${JSON.stringify(sendResult)}`,
|
|
287
|
+
SESSION_HINT,
|
|
288
|
+
);
|
|
301
289
|
}
|
|
302
290
|
|
|
303
291
|
const startTime = Date.now();
|
|
304
292
|
let lastSignature = '';
|
|
305
293
|
let stableCount = 0;
|
|
306
|
-
let lastImages
|
|
294
|
+
let lastImages = [];
|
|
307
295
|
|
|
308
296
|
while (Date.now() - startTime < timeoutMs) {
|
|
309
297
|
await page.wait(3);
|
|
@@ -330,6 +318,8 @@ export const imageCommand = cli({
|
|
|
330
318
|
}
|
|
331
319
|
}
|
|
332
320
|
|
|
321
|
+
// Timeout — keep best-effort partial results if any image bubble showed
|
|
322
|
+
// up, otherwise surface the timeout instead of a sentinel row.
|
|
333
323
|
if (lastImages.length) {
|
|
334
324
|
if (outDir) {
|
|
335
325
|
const saved = await saveImages(page, lastImages, outDir);
|
|
@@ -337,17 +327,13 @@ export const imageCommand = cli({
|
|
|
337
327
|
}
|
|
338
328
|
return lastImages.map(i => toRow(i));
|
|
339
329
|
}
|
|
340
|
-
|
|
341
|
-
url: `${NO_IMAGE_PREFIX} No image appeared within ${Math.round(timeoutMs / 1000)}s.`,
|
|
342
|
-
width: 0,
|
|
343
|
-
height: 0,
|
|
344
|
-
path: '',
|
|
345
|
-
}];
|
|
330
|
+
throw new TimeoutError('grok image generation', Math.round(timeoutMs / 1000));
|
|
346
331
|
},
|
|
347
332
|
});
|
|
348
333
|
|
|
349
334
|
export const __test__ = {
|
|
350
335
|
normalizeBooleanFlag,
|
|
336
|
+
normalizePositiveInteger,
|
|
351
337
|
isOnGrok,
|
|
352
338
|
dedupeBySrc,
|
|
353
339
|
imagesSignature,
|
package/clis/grok/image.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import type { IPage } from '@jackwener/opencli/types';
|
|
3
|
+
import { ArgumentError } from '@jackwener/opencli/errors';
|
|
3
4
|
import { __test__ } from './image.js';
|
|
4
5
|
|
|
5
6
|
describe('grok image helpers', () => {
|
|
@@ -95,6 +96,25 @@ describe('grok image helpers', () => {
|
|
|
95
96
|
]);
|
|
96
97
|
});
|
|
97
98
|
|
|
99
|
+
describe('normalizePositiveInteger', () => {
|
|
100
|
+
it('returns a parsed positive integer', () => {
|
|
101
|
+
expect(__test__.normalizePositiveInteger(3, 1, 'count')).toBe(3);
|
|
102
|
+
expect(__test__.normalizePositiveInteger('5', 1, 'count')).toBe(5);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('falls back to the default when the user did not pass a value', () => {
|
|
106
|
+
expect(__test__.normalizePositiveInteger(undefined, 1, 'count')).toBe(1);
|
|
107
|
+
expect(__test__.normalizePositiveInteger(null, 4, 'count')).toBe(4);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('throws ArgumentError instead of silently clamping out-of-range input', () => {
|
|
111
|
+
expect(() => __test__.normalizePositiveInteger(0, 1, 'count')).toThrow(ArgumentError);
|
|
112
|
+
expect(() => __test__.normalizePositiveInteger(-1, 1, 'count')).toThrow(ArgumentError);
|
|
113
|
+
expect(() => __test__.normalizePositiveInteger(1.5, 1, 'count')).toThrow(ArgumentError);
|
|
114
|
+
expect(() => __test__.normalizePositiveInteger('not-a-number', 1, 'count')).toThrow(ArgumentError);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
98
118
|
it('does not reuse stale images when no new image bubble appears after baseline', () => {
|
|
99
119
|
const candidate = __test__.pickLatestImageCandidate([
|
|
100
120
|
[{ src: 'https://a.example/stale.jpg', w: 512, h: 512 }],
|
package/clis/grok/new.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { GROK_DOMAIN, startNewChat } from './utils.js';
|
|
3
|
+
|
|
4
|
+
cli({
|
|
5
|
+
site: 'grok',
|
|
6
|
+
name: 'new',
|
|
7
|
+
access: 'write',
|
|
8
|
+
description: 'Start a new conversation in Grok',
|
|
9
|
+
domain: GROK_DOMAIN,
|
|
10
|
+
strategy: Strategy.COOKIE,
|
|
11
|
+
browser: true,
|
|
12
|
+
browserSession: { reuse: 'site' },
|
|
13
|
+
navigateBefore: false,
|
|
14
|
+
args: [],
|
|
15
|
+
columns: ['Status'],
|
|
16
|
+
func: async (page) => {
|
|
17
|
+
await startNewChat(page);
|
|
18
|
+
return [{ Status: 'New chat started' }];
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import {
|
|
3
|
+
GROK_DOMAIN,
|
|
4
|
+
bubbleHtmlToMarkdown,
|
|
5
|
+
ensureOnGrok,
|
|
6
|
+
getMessageBubbles,
|
|
7
|
+
normalizeBooleanFlag,
|
|
8
|
+
} from './utils.js';
|
|
9
|
+
|
|
10
|
+
cli({
|
|
11
|
+
site: 'grok',
|
|
12
|
+
name: 'read',
|
|
13
|
+
access: 'read',
|
|
14
|
+
description: 'Read messages in the current Grok conversation',
|
|
15
|
+
domain: GROK_DOMAIN,
|
|
16
|
+
strategy: Strategy.COOKIE,
|
|
17
|
+
browser: true,
|
|
18
|
+
browserSession: { reuse: 'site' },
|
|
19
|
+
navigateBefore: false,
|
|
20
|
+
args: [
|
|
21
|
+
{ name: 'markdown', type: 'boolean', default: false, help: 'Emit assistant replies as markdown' },
|
|
22
|
+
],
|
|
23
|
+
columns: ['Role', 'Text'],
|
|
24
|
+
func: async (page, kwargs) => {
|
|
25
|
+
const wantMarkdown = normalizeBooleanFlag(kwargs.markdown, false);
|
|
26
|
+
await ensureOnGrok(page);
|
|
27
|
+
await page.wait(2);
|
|
28
|
+
const bubbles = await getMessageBubbles(page);
|
|
29
|
+
if (!bubbles.length) {
|
|
30
|
+
return [{ Role: 'system', Text: 'No visible messages in the current conversation.' }];
|
|
31
|
+
}
|
|
32
|
+
return bubbles.map((b) => ({
|
|
33
|
+
Role: b.role,
|
|
34
|
+
Text: wantMarkdown && b.role === 'Assistant' && b.html
|
|
35
|
+
? (bubbleHtmlToMarkdown(b.html) || b.text)
|
|
36
|
+
: b.text,
|
|
37
|
+
}));
|
|
38
|
+
},
|
|
39
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import { ArgumentError, CommandExecutionError } from '@jackwener/opencli/errors';
|
|
3
|
+
import {
|
|
4
|
+
GROK_DOMAIN,
|
|
5
|
+
authRequired,
|
|
6
|
+
ensureOnGrok,
|
|
7
|
+
isLoggedIn,
|
|
8
|
+
normalizeBooleanFlag,
|
|
9
|
+
sendMessage,
|
|
10
|
+
startNewChat,
|
|
11
|
+
} from './utils.js';
|
|
12
|
+
|
|
13
|
+
cli({
|
|
14
|
+
site: 'grok',
|
|
15
|
+
name: 'send',
|
|
16
|
+
access: 'write',
|
|
17
|
+
description: 'Fire-and-forget: send a prompt to Grok without waiting for the reply',
|
|
18
|
+
domain: GROK_DOMAIN,
|
|
19
|
+
strategy: Strategy.COOKIE,
|
|
20
|
+
browser: true,
|
|
21
|
+
browserSession: { reuse: 'site' },
|
|
22
|
+
navigateBefore: false,
|
|
23
|
+
args: [
|
|
24
|
+
{ name: 'prompt', required: true, positional: true, help: 'Prompt to send to Grok' },
|
|
25
|
+
{ name: 'new', type: 'boolean', default: false, help: 'Start a new chat before sending' },
|
|
26
|
+
],
|
|
27
|
+
columns: ['Status', 'Prompt'],
|
|
28
|
+
func: async (page, kwargs) => {
|
|
29
|
+
const prompt = String(kwargs.prompt || '').trim();
|
|
30
|
+
if (!prompt) throw new ArgumentError('prompt', 'is required');
|
|
31
|
+
const startFresh = normalizeBooleanFlag(kwargs.new, false);
|
|
32
|
+
|
|
33
|
+
await ensureOnGrok(page);
|
|
34
|
+
if (startFresh) {
|
|
35
|
+
await startNewChat(page);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const send = await sendMessage(page, prompt);
|
|
39
|
+
if (!send?.ok) {
|
|
40
|
+
// If the composer is missing, the most likely cause is that the
|
|
41
|
+
// signed-in session expired (Grok then renders a sign-in CTA in
|
|
42
|
+
// place of the composer). Surface that as AuthRequiredError so
|
|
43
|
+
// agents can prompt for re-auth instead of treating it as a
|
|
44
|
+
// generic execution failure.
|
|
45
|
+
if (!(await isLoggedIn(page))) throw authRequired();
|
|
46
|
+
throw new CommandExecutionError(send?.reason || 'Failed to send Grok prompt');
|
|
47
|
+
}
|
|
48
|
+
return [{ Status: 'sent', Prompt: prompt }];
|
|
49
|
+
},
|
|
50
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { cli, Strategy } from '@jackwener/opencli/registry';
|
|
2
|
+
import {
|
|
3
|
+
GROK_DOMAIN,
|
|
4
|
+
ensureOnGrok,
|
|
5
|
+
getCurrentSessionId,
|
|
6
|
+
getModelLabel,
|
|
7
|
+
isLoggedIn,
|
|
8
|
+
} from './utils.js';
|
|
9
|
+
|
|
10
|
+
cli({
|
|
11
|
+
site: 'grok',
|
|
12
|
+
name: 'status',
|
|
13
|
+
access: 'read',
|
|
14
|
+
description: 'Check Grok page availability, login state, current session and model',
|
|
15
|
+
domain: GROK_DOMAIN,
|
|
16
|
+
strategy: Strategy.COOKIE,
|
|
17
|
+
browser: true,
|
|
18
|
+
browserSession: { reuse: 'site' },
|
|
19
|
+
navigateBefore: false,
|
|
20
|
+
args: [],
|
|
21
|
+
columns: ['Status', 'Login', 'Model', 'SessionId', 'Url'],
|
|
22
|
+
func: async (page) => {
|
|
23
|
+
await ensureOnGrok(page);
|
|
24
|
+
await page.wait(2);
|
|
25
|
+
const [loggedIn, sessionId, model, url] = await Promise.all([
|
|
26
|
+
isLoggedIn(page),
|
|
27
|
+
getCurrentSessionId(page),
|
|
28
|
+
getModelLabel(page),
|
|
29
|
+
page.evaluate('window.location.href').catch(() => ''),
|
|
30
|
+
]);
|
|
31
|
+
// Surface unknowns as `null` rather than a sentinel string so agents
|
|
32
|
+
// can branch cleanly.
|
|
33
|
+
return [{
|
|
34
|
+
Status: 'Connected',
|
|
35
|
+
Login: loggedIn ? 'Yes' : 'No',
|
|
36
|
+
Model: model ? model : null,
|
|
37
|
+
SessionId: sessionId ? sessionId : null,
|
|
38
|
+
Url: typeof url === 'string' ? url : '',
|
|
39
|
+
}];
|
|
40
|
+
},
|
|
41
|
+
});
|